feat(spec): publish the typed hook ctx.api face — HookApi / HookObjectApi / HookQuery - #19067
Conversation
…pec/data Export `HookApi`, `HookObjectApi`, `HookQuery`, `HookCountQuery`, `HookUpdateOptions`, `HookDeleteOptions` and `HookUpdateDoc` so an app's `*.hook.ts` imports the platform's type instead of re-deriving engine semantics in its own copy. Every option shape is derived from the `Engine*Options` schemas the engine's own per-method legal-key sets are pinned against, and carries no `filter` key: the engine folds `filter` into `where` and refuses the slot when the two spellings disagree, so omitting the alias turns that hazard into a compile error from the platform's own type. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
…ins artifacts Nine additive exported names from `@objectstack/spec/data`; no removals (exported names 832 -> 841, declarations 845 -> 854). Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
Clause-②: yes (widening) Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
…port-hook-api-types
…tree Discharges the merge deferral from 5f0fef7. Both sides survive: this branch's nine `Hook*` entries on `./data` and main's `driverSupportsTransactions`. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 24 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 136 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 cdc64bee1a9d022b8dff5c2994712533bb427dd4 && git checkout cdc64bee1a9d022b8dff5c2994712533bb427dd4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 176b03582e600ee5628d21bff9422073c5a5530c 6d8fc4f12fcdaa8fa25e9abb698aba308ce64b84 && git checkout -B drift-repro 176b03582e600ee5628d21bff9422073c5a5530c && git merge --no-ff 6d8fc4f12fcdaa8fa25e9abb698aba308ce64b84
node scripts/docs-audit/affected-docs.mjs --json 176b03582e600ee5628d21bff9422073c5a5530c
|
Contract reviewServed-tier: Trees and instruments. Head worktree at (1) Derived judgments
(2) Semver level
(3) Boundary flags
Implemented-by: VERDICT: FAIL — one BLOCKING finding, (1) item 7: two names the published face structurally references are not nameable from the entry that publishes them, measured as TS2883 in the consumer shape this card serves. The fix is two type-only re-exports on Generated by Claude Code Generated by Claude Code |
…e from ./data Contract review's one blocking finding. `HookApi.transaction` references `EngineTransactionInfo` and `EngineTransactionOptions` structurally, and `HookContext.api` has referenced `IScopedContext` the same way since #5945; none was reachable from `./data`, so a consumer importing only that entry and emitting declarations answered TS2883. Three type-only re-exports, zero runtime. `check:entry-nameability` cannot see this: it probes the call surface of value exports, and `HookApi` is a type. The sibling test now imports the three names THROUGH `./hook-api`, so deleting a re-export reds `check:test-typecheck`. Also records two decisions the review settled, in the docblock rather than as open questions: `top` stays omitted (it is absent from `ENGINE_FIND_OPTION_KEYS` itself, so the face carries the engine's accepted set verbatim), and `HookDoc` stays a string-keyed record of unknown (loud refusal, one-word call-site remedy, and widening later is the additive direction). Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
…ports Exported names 842 -> 845, declarations 855 -> 858. Added exactly EngineTransactionInfo, EngineTransactionOptions and IScopedContext on ./data; no removals. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
…s, not three
Measured, not reasoned: exporting `IScopedContext` alone is a HALF closure,
because `IScopedContext.object(name)` returns `IScopedObjectRepository` and a
consumer writing `ctx.api.object('deal')` still answered TS2883 on that fourth
name. With all four, a probe covering both `transaction` arguments, `ctx.api`,
`ctx.api.object(...)`, `api.object(...)` and a repository read emits at exit 0
and every inferred type in the emitted declarations resolves through
`@objectstack/spec/data`. The chain terminates there — the repository's members
answer in structural types that name nothing further.
Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Co-authored-by: Claude <noreply@anthropic.com>
… on measurement Reverts the exploratory fourth name. The blocking finding is `EngineTransactionInfo` and `EngineTransactionOptions`, both introduced to this entry by this card's own declarations; those two ship. The optional leg — `IScopedContext`, leaking off `HookContext.api` since #5945 — is declined here, measured rather than waved off. It needs TWO names, not one: `IScopedContext.object(name)` returns `IScopedObjectRepository`, so with the context exported alone `ctx.api.object('deal')` still answered TS2883 on the repository. And the second name is not free: adding it reorders members inside object type literals in the unrelated `ui` shard, 330 lines that `check:api-surface-declarations` reports as "33 reshaped" and asks a reviewer to rule on. Measured order-only — identical token multiset, identical line count, generator stable against a fixed dist — but an adjudication in a shard this card does not touch is its own card, not a rider on a FAIL remediation. Surface delta vs the merge base: exported names 833 -> 844, no removals, and `ui.txt` untouched. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
…port-hook-api-types
Discharges the merge deferral from d95e7cf. `data.txt` carries this branch's eleven additions on top of what main brought; `ui.txt` and `automation.txt` moved order-only (identical token multiset, identical line count, nothing added, removed or renamed). Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
The previous regeneration read a dist that turbo served from the shared cache, which every worktree on this container replays, so the committed bytes were not what this tree builds. Two independent `build --force` runs of this exact tree agree with each other and disagree with what was committed; these are the forced-build bytes. Both shards move order-only against the merge base — identical token multiset, identical line count, nothing added, removed or renamed. `data.txt` is unchanged by this commit and still carries this branch's +11 exported names. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
…he-tainted The previous commit message and this docblock justified declining the pre-existing `IScopedContext` leak partly on "330 lines of order-only churn in the ui shard". That measurement was taken against a dist turbo served from the shared cache, so it is VOID, not disproven. From a forced build this tree emits `ui.txt` and `automation.txt` byte-identical to main, and no such churn exists. The decision is unchanged and now rests only on what survives measurement: the leak is pre-existing (since #5945, on base as on head), it is outside this card's ruling, and closing it takes TWO names rather than one — so it is a change to a face this card does not own, with its own review. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
Contract review — round 2Served-tier: Re-verification of the round-1 record (comment 5732448431, head (1) Derived judgments
(2) Semver level
(3) Boundary flags
Implemented-by: VERDICT: PASS — the BLOCKING finding is closed by measurement at Generated by Claude Code Generated by Claude Code |
Fixes #18163
Clause-②: yes (widening)
Executes batch #147 item 1, letter A — maintainer 「其他同意」 2026-09-17T14:01Z.
@objectstack/spec/datanow exports the typed hookctx.apiface, so a metadata app's*.hook.tsimports the platform's type instead of hand-declaring one. Nine additive exported names, zero runtime, no removal and no signature change.HookApi·HookObjectApi·HookQuery·HookCountQuery·HookUpdateOptions·HookDeleteOptions·HookUpdateDoc·HookDoc·HookDriverPassthroughOptionsMeasured at the engine seam, not from any app's copy
The ruling's third prescription. Located by declaration site, never by a mention:
ctx.apiis built per dispatch byObjectQL.buildHookApiand is aScopedContextpackages/objectql/src/engine.ts:4716ScopedContext.object(name)returns anObjectRepositorypackages/objectql/src/engine.ts:15625contextafter the caller's spreadpackages/objectql/src/engine.ts:15420-15535packages/objectql/src/engine.ts:519-535filtertowhereandtoptolimitpackages/spec/src/data/data-engine.zod.ts:517,551packages/objectql/src/engine.ts:792-799ctx.apialready carries —IScopedContext, from@objectstack/spec/contractspackages/spec/src/contracts/scoped-context.ts:208What the fold actually does, which is finer than "refuses both": redundant spellings that are deep-equal collapse into
wherein silence; spellings that carry different values are irreconcilable and the engine throws, naming both. So{ where, filter }is a coin toss decided by whether the two happen to agree. Omitting the alias key makes it neither —TS2353at the authoring site.The same measurement carries
top, the OData alias oflimit: same slot table, same throw on a value disagreement, no expressive power of its own. Extending the ruling'sfilterinstruction totopis this PR's reading of the same rule and is flagged in the file's own docblock as the one place contract review should decide whether the type should be wider than the ruling's letter.Not a second dialect of
IScopedContextcontracts/scoped-context.tsstays the CHECKED IMPLEMENTATION contract —ScopedContextandObjectRepositorycarryimplementsclauses against it, and its query bags are deliberately loose, for the reason that file argues at length. This is the authoring half of the same seam. They cannot drift because every option shape here is anOmitorPickover the veryEngine*Optionsschemas the engine's own legal-key sets are pinned against (engine-unknown-option.test.ts), andhook-api.test.tspinsHookApias assignable toIScopedContextin both the context and the repository position — soctx.api as HookApistays a direct cast, neveras unknown as.Deliberately absent, each with its reason in the docblock:
context(injected and discarded), thecursor/distinct/upserttombstones,sudo()(the #5945 exclusion stands —Hook.runAs: 'system'is the declared way to run elevated), andaggregate/execute/create/deleteById.countis the one shape without the driver pass-through keys, because the engine forwards no bag on that method and rejects them there — engine behaviour no document states.Verification, all at
d3895054b2Reverse verification from a consumer's vantage — a throwaway probe in a package that resolves
@objectstack/spec/datathrough the exports map into the BUILTdist, run in two legs and then removed (worktree confirmed clean):On-disk proof was taken per leg (1 occurrence of the mutated text, then 0), so neither leg is a no-op.
pnpm --filter @objectstack/spec typecheck— exit 0. Three programs: the build config,tsconfig.scripts.json, andcheck:test-typecheck, which is what compiles the test layer. The six@ts-expect-errorpins inhook-api.test.tsare therefore real: an unused directive is itself an error, so a directive that stopped catching anything turns this red.pnpm --filter @objectstack/spec test— 492 files, 14311 tests, all pass.pnpm --filter @objectstack/spec check:generated— all 16 artifacts up to date.pnpm lintrepo-wide (eslint . --no-inline-config) — exit 0, no narrowing claimed.scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackand reconciled with--ran: 83 derived, 79 run green, 4 NOT MEASURED. The four arecheck:doc-formula-expressions,check:dual-build-cjs-loads,check:lean-entry-closureandcheck:type-check-debt, each exiting 3 — PREREQUISITE NOT MET, which is the code those gates use for "nothing was measured". All four need the whole workspace built (turbo run buildover every package); this diff's own dependency closure is empty, sincepackages/spechas no workspace dependencies. CI builds fresh and runs all four.origin/mainmoved onto the same three generated artifacts and ontodata/driver.zod.ts, which this file reads for the pass-through keys, so the merge went throughscripts/pm/os-regen-merge.shand the regeneration is its own commit. Both sides survive in the regenerated artifacts: this branch's nineHook*entries and main'sdriverSupportsTransactions.Acceptance notes
grep -rhoE "export (type|interface) (HookApi|HookContext|HookObjectApi)" ... dist/data/*.d.tsreturns zero forHookContexttoo — a symbol that has always been exported from that entry point. That glob reaches exactly one file,dist/data/index.d.ts, which is a renamed re-export barrel (export { k as HookContext } from '../datasource.zod-...js'); every declaration lives in a hashed chunk one directory up, outside the glob. Re-derived with a corrected instrument that counts the barrel's export bindings:HookContext1,EngineQueryOptions1 (positive controls),HookApi/HookObjectApi/HookQuery0 across the whole publisheddistand 0 across every package source. So the gap was real; the instrument that found it could not have told.ScopedContextsatisfiesHookApi.packages/specmust not depend onpackages/objectql, so that leg belongs besidehook-input-shape-contract.test.tsin objectql, which this card's file surface excludes. Carrier: whoever next editsScopedContextorObjectRepositoryis inpackages/objectql/src/engine.ts, where the pin would live, and the reference app's follow-up card the ruling names is the other side of the same check.IScopedContextis reachable only from@objectstack/spec/contracts, so an author who wants to name the declared type ofctx.apialongsideHookApiimports from two entry points. Carrier: the same follow-up card, which is the first consumer to feel it.EngineTransactionInfoandEngineTransactionOptionsare referenced byHookApi.transactionbut are not nameable from./data. Structural use needs no name andcheck:entry-nameabilitypasses, so this is an observation, not a gap. Carrier: none — no consumer needs to spell them.NOT MEASURED — say it plainly
The ruling names hotcrm's
src/objects/_hook-api.tsas the acceptance fixture: it must type-check against this export with its own copy deleted. That leg was not run. hotcrm is not a repository this session can reach, and the ruling is explicit that its file is the fixture and not the source of truth, so nothing here was written from the card's quoted excerpts of it. The reverse verification above is the closest reachable stand-in: a real consumer, resolving through the published exports map into the built.d.ts, refusingfilterand accepting the canonical shape. It is not the fixture, and it is not claimed to be.Generated by Claude Code