Skip to content

Commit 3d2de76

Browse files
committed
fix(spec): take the two names the finding owes, and decline the third 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>
1 parent 2abf0a3 commit 3d2de76

6 files changed

Lines changed: 37 additions & 73 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`, `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.
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` and `EngineTransactionOptions`, 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: 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.
22+
Additive only: eleven new exported names from `./data` (nine new declarations plus two type-only re-exports), no removal and no signature change, so nothing an existing consumer imports moves.
2323

2424
Clause-②: yes (widening)

‎packages/spec/api-surface-declarations/data.txt‎

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
# excluded: documentation drift is `check:docs`'s axis, not this one.
1313
#
1414
# entry: ./data
15-
# exported names: 845
16-
# declarations: 858
15+
# exported names: 844
16+
# declarations: 857
1717
#
1818
# GENERATED — ⛔ never hand-edited. Regenerate after a real build:
1919
# pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec gen:api-surface-declarations
@@ -14631,39 +14631,6 @@ declare const INLINE_CREDENTIAL_REFUSED: (key: string) => string;
1463114631
// ── INSTANT_TYPES (const) ──
1463214632
declare const INSTANT_TYPES: ReadonlySet<string>;
1463314633

14634-
// ── IScopedContext (interface) ──
14635-
interface IScopedContext {
14636-
/** The repository for `name`, bound to this context. */
14637-
object(name: string): IScopedObjectRepository;
14638-
/**
14639-
* Run `callback` inside one driver transaction: committed when it returns,
14640-
* rolled back when it throws.
14641-
*
14642-
* The callback receives a NEW `IScopedContext` whose operations share the
14643-
* transaction handle — reach objects through THAT context (`tx.object(…)`),
14644-
* not the outer one, or the writes land outside the transaction.
14645-
*
14646-
* ## Why the callback's second parameter is declared even though no corpus
14647-
* site reads it
14648-
*
14649-
* The evidence bar above governs WHICH MEMBERS exist, not what a declared
14650-
* member's signature is allowed to say. A parameter list describes what the
14651-
* PRODUCER hands the callback, and the producer hands it two arguments
14652-
* unconditionally (`ScopedContext.transaction`, #5696): declaring one would
14653-
* be a false statement that additionally makes `async (tx, info) => …`
14654-
* — legal, working code — a compile error. Contravariance means the
14655-
* zero-argument and one-argument callbacks the corpus actually writes still
14656-
* satisfy this, so the truthful signature is also the more permissive one.
14657-
* {@link EngineTransactionInfo} is reused rather than re-spelled, for the
14658-
* same reason `opts` is: ADR-0119 D1 and #5696 rule that this surface is a
14659-
* second IMPLEMENTATION of `IObjectQLEngine.transaction`, never a second
14660-
* DIALECT of it — `opts.require`'s fail-closed refusal is honoured here
14661-
* identically, and a contract that omitted it would re-open in declaration
14662-
* the dialect the implementation was made to close.
14663-
*/
14664-
transaction<T>(callback: (trxCtx: IScopedContext, info: EngineTransactionInfo) => Promise<T>, opts?: EngineTransactionOptions): Promise<T>;
14665-
}
14666-
1466714634
// ── ImportFieldMapping (type) ──
1466814635
type ImportFieldMapping = z.input<typeof ImportFieldMappingSchema>;
1466914636

‎packages/spec/api-surface/data.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,6 @@
365365
"IMPORT_REFERENCE_TYPES (const)",
366366
"INLINE_CREDENTIAL_REFUSED (const)",
367367
"INSTANT_TYPES (const)",
368-
"IScopedContext (interface)",
369368
"ImportFieldMapping (type)",
370369
"ImportFieldMappingParsed (type)",
371370
"ImportFieldMappingSchema (const)",

‎packages/spec/export-origins/data.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,6 @@
355355
"IMPORT_REFERENCE_TYPES": "src/data/import-coercion.ts#IMPORT_REFERENCE_TYPES (const)",
356356
"INLINE_CREDENTIAL_REFUSED": "src/data/driver/common.zod.ts#INLINE_CREDENTIAL_REFUSED (const)",
357357
"INSTANT_TYPES": "src/data/field-value.zod.ts#INSTANT_TYPES (const)",
358-
"IScopedContext": "src/contracts/scoped-context.ts#IScopedContext (interface)",
359358
"ImportFieldMapping": "src/data/mapping.zod.ts#ImportFieldMapping (type)",
360359
"ImportFieldMappingParsed": "src/data/mapping.zod.ts#ImportFieldMappingParsed (type)",
361360
"ImportFieldMappingSchema": "src/data/mapping.zod.ts#ImportFieldMappingSchema (const)",

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

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,6 @@ import type {
5959
HookObjectApi,
6060
HookQuery,
6161
HookUpdateOptions,
62-
IScopedContext as ReExportedScopedContext,
63-
IScopedObjectRepository as ReExportedScopedRepository,
6462
} from './hook-api';
6563
import type { IScopedContext, IScopedObjectRepository } from '../contracts/scoped-context';
6664

@@ -191,20 +189,10 @@ describe('HookApi — the published hook ctx.api face', () => {
191189
// `@objectstack/spec/data` and emitting declarations answers TS2883 without
192190
// them, and `check:entry-nameability` cannot see it (it probes the call
193191
// surface of VALUE exports; `HookApi` is a type).
194-
it('the transaction signature\'s two types, and the ctx.api pair', () => {
192+
it('both types the transaction signature references', () => {
195193
const infoIsReachable: Assignable<EngineTransactionInfo, EngineTransactionInfo> = true;
196194
const optsIsReachable: Assignable<EngineTransactionOptions, EngineTransactionOptions> = true;
197-
// Same declaration, reached through both entries — one declaration, two
198-
// paths, which is what `check:dual-source-exports` asks about.
199-
const oneDeclaration: Assignable<ReExportedScopedContext, IScopedContext> = true;
200-
const andBack: Assignable<IScopedContext, ReExportedScopedContext> = 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,
207-
]);
195+
expect([infoIsReachable, optsIsReachable]).toEqual([true, true]);
208196
});
209197
});
210198

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

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -328,24 +328,36 @@ export interface HookApi {
328328
* ```
329329
*
330330
* The second position — `transaction`'s `opts` — answers the same way for
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.
331+
* `EngineTransactionOptions`. Those two are what this card owes, and they are
332+
* what this file exports: they are names THIS card's own new declarations
333+
* introduced to the entry.
334+
*
335+
* ⛔ A THIRD instance of the same defect is deliberately NOT closed here, and
336+
* the reason is a measurement rather than a scope reflex. `HookContext.api` has
337+
* leaked `IScopedContext` off this entry since #5945 — pre-existing, present on
338+
* this card's base and unchanged by it. Closing it is not the one-line job it
339+
* looks like:
340+
*
341+
* - It needs TWO names, not one. `IScopedContext.object(name)` returns
342+
* `IScopedObjectRepository`, so with `IScopedContext` exported alone a
343+
* consumer writing `ctx.api.object('deal')` still answers TS2883 on the
344+
* repository — measured, at head, with the three-name variant applied. A
345+
* one-name patch publishes a HALF closure that READS closed, which is the
346+
* declared-not-enforced shape this repo refuses.
347+
* - The second of those two names is not free. Adding
348+
* `IScopedObjectRepository` to this entry moves the dts bundler's module
349+
* order enough to reorder members inside object type literals in the
350+
* UNRELATED `ui` shard: 330 lines, which `check:api-surface-declarations`
351+
* reports as "33 reshaped" and asks a reviewer to rule on. Measured as
352+
* order-only — identical token multiset, identical line count, nothing
353+
* added, removed or renamed — and the generator is stable against a fixed
354+
* dist, so it is noise rather than drift. But it is a verdict somebody has
355+
* to read, in a shard this card does not touch.
356+
*
357+
* Two names with a clean surface delta, or four names plus an adjudication in
358+
* someone else's shard: that is a trade for its own card and its own review,
359+
* not a rider on a FAIL remediation. The pre-existing leak is reported with
360+
* both measurements so that card can be written without re-deriving them.
349361
*
350362
* ⛔ `check:entry-nameability` is NOT the instrument that answers this. By its
351363
* own docblock it probes the CALL surface of VALUE exports that have a call
@@ -355,9 +367,8 @@ export interface HookApi {
355367
* are a known target outside it. The instrument that answers is a consumer
356368
* program with `declaration` emit, which is what the excerpt above is.
357369
*
358-
* Type-only re-exports: they add four names to this entry and no runtime byte,
370+
* Type-only re-exports: they add two names to this entry and no runtime byte,
359371
* and each is ONE declaration reachable from two entries rather than two
360372
* declarations sharing a name, which is what `check:dual-source-exports` asks.
361373
*/
362374
export type { EngineTransactionInfo, EngineTransactionOptions } from '../contracts/objectql-engine';
363-
export type { IScopedContext, IScopedObjectRepository } from '../contracts/scoped-context';

0 commit comments

Comments
 (0)