fix(hooks): give count its own query type, measured against the engine - #1675
Merged
Conversation
`src/objects/_hook-api.ts` declared all three `ctx.api` read methods as one
`HookQuery` (`where` / `fields` / `top`). The engine does not accept that key
set on all three: `count`'s legal set is the predicate and nothing else, so
`count({ where, fields })` and `count({ where, top })` compiled and threw on
every invocation.
Measured against the pinned `@objectstack` packages (17.3.0), on the object the
kernel injects as `ctx.api`, by handing each key to the engine and reading its
unknown-option guard, which prints the legal set verbatim:
count('crm_account') does not recognise option 'top'. The engine executes
none of it, so the call would succeed with the option silently ignored
(#4371). Legal keys for count: context, where.
`fields` produces the same message word for word; the two were measured
separately rather than one inferred from the other. `find` / `findOne` accept
both — their legal set is `bypassTenantAudit, context, expand, fields, limit,
offset, orderBy, preserveAudit, search, searchFields, tenantId, tenantIds,
timezone, transaction, where` — and `top` reaches them as a declared alias of
`limit`, the same fold that makes `filter` an alias of `where`.
So `count` takes a new `HookCountQuery` carrying only `where`, and `find` /
`findOne` keep `HookQuery` unchanged. `HookQuery` was deliberately NOT narrowed
to the three-method intersection: that would break the four live
`find({ fields, top })` call sites in `campaign.hook.ts` and
`campaign_member.hook.ts`, trading one latent defect for four real ones.
No call site moves — all 23 `count()` call sites under `src/` pass only `where`,
so the defect was latent. The new type is enforced by `tsc --noEmit`, already in
`pnpm verify`; no gate and no test file is added. It reads as the continuation
of the reasoning `HookUpdateOptions` / `HookDeleteOptions` already carry, and
records the verbatim engine reading it was derived from.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018xtjdpZFjgWh4Ad9Wcx68J
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
os-steve
marked this pull request as ready for review
September 6, 2026 09:40
This was referenced Sep 6, 2026
os-steve
pushed a commit
that referenced
this pull request
Sep 6, 2026
Two reasons, neither of which changes this PR's own scope. 1. The head commit carries a permanently-failed `Check Changeset` run. That run started 7s after the PR was opened; the `skip-changeset` label landed 1s later, but the job had already read a payload without it, so the workflow's `if: !contains(labels, 'skip-changeset')` guard let the job run and it failed 8s after starting. The `labeled` re-run skipped correctly, but the failed run stays attached to that head for ever and no event re-evaluates it. A new head is the only way to clear it without a rebase or a force-push. 2. The base had moved 18 commits since the PR was opened, so the last green `Build and Test` measured a base that no longer exists. One of those 18 touches the surface this brief documents: #1689 re-scoped the pin-claim comment in `src/objects/_hook-api.ts` from "measured on 17.2.0" to "first taken on 17.2.0, re-taken on the current pin 17.3.0". The brief already states 17.3.0, so the merge agrees with it rather than contradicting it. #1675, which gave `count` its own `HookCountQuery`, was already in this branch's history when it was cut and the brief was written against it. The diff against main is unchanged: `.github/instructions/logic.md` plus `.github/instructions/metadata.md`. Co-authored-by: Claude
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1528
src/objects/_hook-api.tsdeclared all threectx.apiread methods as oneHookQuery(where/fields/top). The engine does not accept that key seton all three, so the file blessed two calls at compile time that throw on every
invocation.
The measurement — taken here, not copied from the card
The card's table was taken on 17.2.0; this repo pins 17.3.0. Zone 1 rule 4
required re-measuring, so every cell below was read off the engine's own
unknown-option guard on the pinned packages, on the object the kernel injects as
ctx.api(a realScopedContextover a real ObjectQL engine on the in-memorydriver, the same apparatus
test/hook-query-predicate.test.tsuses).Verbatim, from the engine:
countcontext, wherefindbypassTenantAudit, context, expand, fields, limit, offset, orderBy, preserveAudit, search, searchFields, tenantId, tenantIds, timezone, transaction, wherefindOnefindMeasured, each separately rather than one inferred from the other:
count({ where, fields: ['name'] })→ throws, same message word for word;count({ where, top: 5000 })→ throws;count({ where })andcount({ context, where })→ OK (socontextreally is legal);count({ where, nonsense_key_xyz })→ throws — negative control, the guard is alive, so the two greens above are evidence of something.topis still a live alias oflimiton 17.3.0 (Zone 2 item 3): over threerows,
find({ top: 2 })andfind({ limit: 2 })both return 2 whilefind({})returns 3, and
find({ top: 2, limit: 3 })throwsConflicting options … 'limit', 'top' are spellings of the same parameter (canonical 'limit')— the same foldmechanism that makes
filteran alias ofwhere. Sofind/findOneneed nochange.
No drift from the card's 17.2.0 reading. Both key sets came back identical.
The change
counttakes a newHookCountQuerycarrying onlywhere;find/findOnekeep
HookQueryunchanged.HookQuerywas deliberately not narrowed to thethree-method intersection (Zone 1 rule 2) — that would break the four live
find({ fields, top })call sites incampaign.hook.ts(×3) andcampaign_member.hook.ts(×1), trading one latent defect for four real ones.The new type is written as a continuation of
HookUpdateOptions/HookDeleteOptions, which already carry the "deliberately narrow, excess-propertychecking rejects the rest at the call site" reasoning, and it records the verbatim
engine reading it was derived from so the next author does not have to rediscover
how to take that measurement.
No call site moves — the card stays latent
A structural scan of every
count()argument bag undersrc/(brace-balanced, somulti-line calls are read whole): 23 call sites, 0 passing
fields,top, orany other key outside the legal set, and none spreading. The reclassify trigger
in the R30 grading (any
count()passingfields/top⇒ regrade toBug) didnot fire.
tsc --noEmitagrees — it is green with zero call-site edits.Reverse verification — the gate can actually fail
A green typecheck proves nothing on its own, so both legs were run from the
committed state, with the mutation proven on disk by grepping the injected text
and the restore proven by blob-hash equality against HEAD plus an empty
git diff HEAD:tsc --noEmitHookCountQuery+ two illegal keys injected into realcount()call sitesHookQueryLeg 1, verbatim:
Leg 2 is the defect itself, reproduced: the old declaration compiled both calls
clean. That is the pair the fix is for.
Gates
pnpm verifyfully green one6b9eef9— all eight stages:validate(passed, 1265ms) ·
typecheck(exit 0) ·lint·lint:i18n-gate(0i18n/missing-*) ·hygiene(clean) ·hygiene:tokens(clean) ·build(complete, 1389ms) ·
test(161 files, 3402 passed, 1 skipped), whichincludes
test/hook-query-predicate.test.tsunchanged and passing.Token ratchet moved +14 tokens (business semantics ~85,018 → ~85,032, ceiling
~100,000) — the three lines of interface; the docblock is comment-stripped by
design.
Per Zone 1 rule 5, no gate and no test file is added, and nothing under
test/**is touched (epic #1579's territory). The card's suggestion to pin thisin
test/hook-query-predicate.test.tswas explicitly not adopted: the split isself-proving at compile time, and
tsc --noEmitinsidepnpm verifyis thischange's real gate — demonstrated above rather than asserted.
One changeset,
patch.🤖 Generated with Claude Code
https://claude.ai/code/session_018xtjdpZFjgWh4Ad9Wcx68J
Generated by Claude Code