Skip to content

Commit 2abf0a3

Browse files
committed
fix(spec): close the ctx.api nameability chain completely — four names, 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>
1 parent db6a3df commit 2abf0a3

3 files changed

Lines changed: 30 additions & 14 deletions

File tree

‎.changeset/18163-export-hook-api-types.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
'@objectstack/spec': minor
33
---
44

5-
`@objectstack/spec/data` now exports the typed hook `ctx.api` face — `HookApi`, `HookObjectApi`, `HookQuery`, `HookCountQuery`, `HookUpdateDoc`, `HookUpdateOptions`, `HookDeleteOptions`, `HookDoc` and `HookDriverPassthroughOptions` — so a metadata app's `*.hook.ts` imports the platform's type instead of hand-declaring one (#18163). The same entry additionally re-exports `EngineTransactionInfo`, `EngineTransactionOptions` and `IScopedContext`, which its public declarations reference structurally: without them a consumer that imports only `@objectstack/spec/data` and emits declarations answers `TS2883: The inferred type ... cannot be named without a reference to ...`. Type-only re-exports of the declarations `@objectstack/spec/contracts` already publishes, not second declarations.
5+
`@objectstack/spec/data` now exports the typed hook `ctx.api` face — `HookApi`, `HookObjectApi`, `HookQuery`, `HookCountQuery`, `HookUpdateDoc`, `HookUpdateOptions`, `HookDeleteOptions`, `HookDoc` and `HookDriverPassthroughOptions` — so a metadata app's `*.hook.ts` imports the platform's type instead of hand-declaring one (#18163). The same entry additionally re-exports `EngineTransactionInfo`, `EngineTransactionOptions`, `IScopedContext` and `IScopedObjectRepository`, which its public declarations reference structurally: without them a consumer that imports only `@objectstack/spec/data` and emits declarations answers `TS2883: The inferred type ... cannot be named without a reference to ...`. Type-only re-exports of the declarations `@objectstack/spec/contracts` already publishes, not second declarations.
66

77
```ts
88
import type { HookApi } from '@objectstack/spec/data';
@@ -19,6 +19,6 @@ The platform already implemented this surface; it just never published a type an
1919
- **Every option shape is DERIVED, not transcribed.** Each is an `Omit`/`Pick` over the `Engine*Options` schemas that the engine's own per-method legal-key sets are pinned against, so a key added to a schema reaches the published type in the same run it reaches the engine's accepted set. `count` is 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, and exactly what a hand-written copy gets wrong.
2020
- **What is deliberately absent, each for a stated reason**: `context` (the repository injects it and discards a caller's), the `cursor` / `distinct` / `upsert` tombstones, `sudo()` (the #5945 exclusion stands — `Hook.runAs: 'system'` is the declared way to run elevated), and `aggregate` / `execute` / `create` / `deleteById`.
2121

22-
Additive only: twelve new exported names from `./data` (nine new declarations plus three type-only re-exports), no removal and no signature change, so nothing an existing consumer imports moves.
22+
Additive only: thirteen new exported names from `./data` (nine new declarations plus four type-only re-exports), no removal and no signature change, so nothing an existing consumer imports moves.
2323

2424
Clause-②: yes (widening)

‎packages/spec/src/data/hook-api.test.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import type {
6060
HookQuery,
6161
HookUpdateOptions,
6262
IScopedContext as ReExportedScopedContext,
63+
IScopedObjectRepository as ReExportedScopedRepository,
6364
} from './hook-api';
6465
import type { IScopedContext, IScopedObjectRepository } from '../contracts/scoped-context';
6566

@@ -190,15 +191,19 @@ describe('HookApi — the published hook ctx.api face', () => {
190191
// `@objectstack/spec/data` and emitting declarations answers TS2883 without
191192
// them, and `check:entry-nameability` cannot see it (it probes the call
192193
// surface of VALUE exports; `HookApi` is a type).
193-
it('the transaction signature\'s two types, and the type ctx.api already carries', () => {
194+
it('the transaction signature\'s two types, and the ctx.api pair', () => {
194195
const infoIsReachable: Assignable<EngineTransactionInfo, EngineTransactionInfo> = true;
195196
const optsIsReachable: Assignable<EngineTransactionOptions, EngineTransactionOptions> = true;
196197
// Same declaration, reached through both entries — one declaration, two
197198
// paths, which is what `check:dual-source-exports` asks about.
198199
const oneDeclaration: Assignable<ReExportedScopedContext, IScopedContext> = true;
199200
const andBack: Assignable<IScopedContext, ReExportedScopedContext> = true;
200-
expect([infoIsReachable, optsIsReachable, oneDeclaration, andBack]).toEqual([
201-
true, true, true, true,
201+
// The repository comes with it: `IScopedContext.object(name)` returns it,
202+
// so exporting the context without it is a HALF closure — measured as a
203+
// surviving TS2883 on `ctx.api.object(...)`.
204+
const repoReachable: Assignable<ReExportedScopedRepository, IScopedObjectRepository> = true;
205+
expect([infoIsReachable, optsIsReachable, oneDeclaration, andBack, repoReachable]).toEqual([
206+
true, true, true, true, true,
202207
]);
203208
});
204209
});

‎packages/spec/src/data/hook-api.ts‎

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -328,13 +328,24 @@ export interface HookApi {
328328
* ```
329329
*
330330
* The second position — `transaction`'s `opts` — answers the same way for
331-
* `EngineTransactionOptions`, and `HookContext.api` has answered it for
332-
* `IScopedContext` since #5945, which is why that third name is here too: it is
333-
* the SAME defect in the SAME entry, its remedy is this same one-line form, it
334-
* lands in this file rather than in any file another card holds, and it adds no
335-
* gate beyond the three this diff already regenerates. Leaving it would publish
336-
* a hook seam that still cannot be written from one entry — the exact gap this
337-
* card was opened to close.
331+
* `EngineTransactionOptions`. Those two are the blocking finding.
332+
*
333+
* The other two names close the SAME defect one frame out, and they come as a
334+
* PAIR because that is what the measurement said. `HookContext.api` has leaked
335+
* `IScopedContext` since #5945, and exporting that name alone is a HALF
336+
* closure: `IScopedContext.object(name)` returns `IScopedObjectRepository`, so
337+
* with three names `(ctx) => ctx.api!.object('deal')` still answered TS2883 on
338+
* that fourth name. With all four, a probe carrying every position — both
339+
* `transaction` arguments, `ctx.api`, `ctx.api.object(…)`, `api.object(…)` and
340+
* a repository read — emits at exit 0, and the emitted declarations resolve
341+
* every inferred type through `@objectstack/spec/data`. The chain terminates
342+
* there: the repository's own members answer in structural types
343+
* (`Promise` of `Record` of `string` to `any`, or `null`) that name nothing
344+
* further. Taken in one stroke because leaving it publishes a hook seam whose
345+
* own `ctx.api` cannot be named from the entry that publishes the seam — the
346+
* exact gap this card was opened to close — and because all four are one-line
347+
* re-exports in this file, of declarations `@objectstack/spec/contracts`
348+
* already publishes, adding no gate beyond the three this diff regenerates.
338349
*
339350
* ⛔ `check:entry-nameability` is NOT the instrument that answers this. By its
340351
* own docblock it probes the CALL surface of VALUE exports that have a call
@@ -344,9 +355,9 @@ export interface HookApi {
344355
* are a known target outside it. The instrument that answers is a consumer
345356
* program with `declaration` emit, which is what the excerpt above is.
346357
*
347-
* Type-only re-exports: they add three names to this entry and no runtime byte,
358+
* Type-only re-exports: they add four names to this entry and no runtime byte,
348359
* and each is ONE declaration reachable from two entries rather than two
349360
* declarations sharing a name, which is what `check:dual-source-exports` asks.
350361
*/
351362
export type { EngineTransactionInfo, EngineTransactionOptions } from '../contracts/objectql-engine';
352-
export type { IScopedContext } from '../contracts/scoped-context';
363+
export type { IScopedContext, IScopedObjectRepository } from '../contracts/scoped-context';

0 commit comments

Comments
 (0)