@adhd/apigen-core-client 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/batch.d.ts ADDED
@@ -0,0 +1,89 @@
1
+ import { InlineDiscriminator } from './schema-builders/morph-walk';
2
+ import { Operation, OperationKind, JSONSchema } from './descriptor';
3
+ import { Descriptor, MountedOperation } from './plugin';
4
+
5
+ /**
6
+ * Options accepted when deriving `_batch/<kind>` mounts (§2.1).
7
+ *
8
+ * `exclude` is the plugin's own opt-out switch — `--opt batch.exclude=...`
9
+ * (CLI `--use` path) or a literal argument at a hand-wired direct-invocation
10
+ * call site (§2.1). There is deliberately no other configuration surface
11
+ * (no runtime flag, no `apigen.config` file — Tenet 1).
12
+ */
13
+ export interface BatchMountOptions {
14
+ /** Operation ids to exclude from every `_batch/<kind>` branch set. */
15
+ exclude?: string[];
16
+ }
17
+ /** One discriminator branch derived from a single batchable `Operation` (§1.2). */
18
+ export interface BatchOperationBranch {
19
+ /** The literal `operation` value selecting this branch. */
20
+ operationConst: string;
21
+ /** The per-item input schema — `op.input` verbatim. */
22
+ itemsSchema: JSONSchema;
23
+ /** `BatchItemResult<op.output>` as a JSON Schema fragment (§3/§F3). */
24
+ resultSchema: JSONSchema;
25
+ }
26
+ /**
27
+ * Pure `Operation → schema fragment` transform (§1.2). Host-agnostic: takes
28
+ * only the already-host-neutral `Operation` IR, produces plain JSON Schema.
29
+ */
30
+ export declare function deriveBatchOperationBranch(op: Operation): BatchOperationBranch;
31
+ /**
32
+ * Group batchable operations (excluding `opts.exclude`) by `Operation.kind`
33
+ * (F1). Each distinct kind present gets its own `_batch/<kind>` mount —
34
+ * `_batch/query`, `_batch/action`, etc. — because `Operation.kind`/`.safe`
35
+ * are static per-op classifications transports read for wire decisions
36
+ * (HTTP verb/cacheability, gRPC idempotency), and a single `_batch` mount
37
+ * whose *actual* kind varies per selected `operation` branch cannot carry
38
+ * that truthfully (architect review F1).
39
+ */
40
+ export declare function groupBatchableOperationsByKind(operations: readonly Operation[], opts?: BatchMountOptions): Map<OperationKind, Operation[]>;
41
+ /** The input/output schema pair (+ discriminator, when applicable) for one `_batch/<kind>` mount. */
42
+ export interface BatchKindSchema {
43
+ input: JSONSchema;
44
+ output: JSONSchema;
45
+ /** Present only when ≥2 operations share this kind (a 1-branch `oneOf` is not a union). */
46
+ discriminator?: InlineDiscriminator;
47
+ }
48
+ /**
49
+ * Build the input/output schema pair for one `_batch/<kind>` mount (F1).
50
+ *
51
+ * - **≥2 ops of that kind:** a real `oneOf` + `InlineDiscriminator` union,
52
+ * using the same-document JSON-Pointer mechanism (`morph-walk.ts`), never
53
+ * `union.ts`'s $ref/nominal mechanism (§1.1).
54
+ * - **Exactly 1 op of that kind:** the single branch's shape directly, no
55
+ * `oneOf` wrapper — `detectDiscriminator` itself refuses below 2 variants
56
+ * (§1.1's "edge case the 0.0.1 draft missed"), and a one-variant `oneOf`
57
+ * is not a union.
58
+ *
59
+ * Throws if `ops` is empty — callers (mount-building) must never invoke this
60
+ * for a kind with zero operations; `groupBatchableOperationsByKind` never
61
+ * produces an empty group.
62
+ */
63
+ export declare function buildBatchKindSchema(ops: readonly Operation[]): BatchKindSchema;
64
+ /**
65
+ * One synthetic `_batch/<kind>` operation shape, minus `handler` — mounting
66
+ * a real, request-servable handler requires wiring `invokeBatch`
67
+ * (`@adhd/apigen-engine-runtime`), which is TS-runtime plumbing this
68
+ * (host-agnostic) module deliberately does not depend on. A `MountCapability`
69
+ * implementation composes `{ ...op, handler: ... }` using this fragment plus
70
+ * its own `invokeBatch` wiring (see BATCH_0.0.1.md §3/§5).
71
+ */
72
+ export type BatchKindOperation = Omit<MountedOperation, 'handler'> & {
73
+ kind: OperationKind;
74
+ /** Ids of the operations this `_batch/<kind>` mount can fan out over. */
75
+ operationIds: string[];
76
+ };
77
+ /**
78
+ * Derive one `_batch/<kind>` synthetic mount per distinct kind present in the
79
+ * batchable operation set (F1). Refuses to mount anything for a descriptor
80
+ * with zero batchable operations (empty array — §1.1's "edge case the 0.0.1
81
+ * draft missed" restated per-kind rather than for a single global mount).
82
+ *
83
+ * `kind`/`safe` are truthful per mount (query kinds are `safe: true`;
84
+ * everything else defaults `safe: false` per the existing `Operation.safe`
85
+ * default-from-kind rule) — never the single hardcoded
86
+ * `kind:'action', safe:false` the 0.0.1 draft proposed for one omnibus
87
+ * `_batch` mount (architect review F1).
88
+ */
89
+ export declare function buildBatchMountedOperations(descriptor: Descriptor, opts?: BatchMountOptions): BatchKindOperation[];
package/lib/plugin.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { PluginLanguage } from './types';
2
- import { Operation, JSONSchema } from './descriptor';
2
+ import { Operation, OperationKind, JSONSchema, TypeText } from './descriptor';
3
3
 
4
4
  /**
5
5
  * The four canonical transport carriers apigen knows about (SPEC §5/§7/§9.1).
@@ -248,6 +248,49 @@ export interface LayerCapability {
248
248
  */
249
249
  layer(call: Call, next: Next): Promise<Result> | AsyncIterable<Chunk>;
250
250
  }
251
+ /**
252
+ * Runtime dispatch options a {@link MountHostBridge} hands back to a mount
253
+ * plugin's handler — structurally equivalent to `apigen-engine-runtime`'s
254
+ * real `InvokeOptions` (duck-typed here, never imported: `apigen-core-client`
255
+ * is a lower tier than `apigen-engine-runtime` and must never depend upward
256
+ * on it — batch-rollout architect review, Finding 3).
257
+ */
258
+ export interface MountHostBridgeInvokeOptions {
259
+ /** The live function table (op-id → implementation), spanning EVERY package the host serves. */
260
+ fns: Record<string, (...args: unknown[]) => unknown>;
261
+ /** Composed schemas (op-id → schema), spanning every package the host serves. */
262
+ schemas: Record<string, unknown>;
263
+ /** Optional client factory (session-ctx middleware), when the host has one. */
264
+ createClient?: (envelope: Record<string, unknown>) => Promise<unknown>;
265
+ }
266
+ /**
267
+ * A host-supplied bridge letting a `MountCapability`'s handler invoke OTHER
268
+ * operations already registered on that host — the missing piece a fan-out
269
+ * mount (e.g. `_batch/<kind>`) needs that a pure mount like `/meta/health` or
270
+ * `/meta/openapi` never did (batch-rollout design note + architect review).
271
+ *
272
+ * `invoke`/`invokeOptions` are structurally equivalent to
273
+ * `apigen-engine-runtime`'s real `InvokeFn`/`InvokeOptions` pair so a plugin
274
+ * can hand them straight to `invokeBatch(hostBridge.invoke, fnName, calls,
275
+ * hostBridge.invokeOptions, batchOptions)` with zero adapter shim (Finding 1
276
+ * of the review) — the tier boundary is preserved by keeping this an inline
277
+ * structural type, never an import from `apigen-engine-runtime` (Finding 3).
278
+ */
279
+ export interface MountHostBridge {
280
+ /**
281
+ * Invoke `fnName` (any operation id already registered on this host, from
282
+ * any package) with the given domain call, through the SAME composed
283
+ * `--use` Layer stack (auth/logging/validate) every other request goes
284
+ * through — never a bypass of it.
285
+ */
286
+ invoke(fnName: string, call: {
287
+ domainArgs: Record<string, unknown>;
288
+ envelope: Record<string, unknown>;
289
+ signal?: AbortSignal;
290
+ }, opts: MountHostBridgeInvokeOptions): Promise<unknown>;
291
+ /** The host's merged, package-spanning `InvokeOptions`-equivalent — pass straight to `invokeBatch`. */
292
+ invokeOptions: MountHostBridgeInvokeOptions;
293
+ }
251
294
  /**
252
295
  * **`mount`** capability — add synthetic operations to the descriptor
253
296
  * (SPEC §7.1 / §7.2b / §7.2c).
@@ -255,7 +298,7 @@ export interface LayerCapability {
255
298
  * A `MountCapability` is loaded via `--use <plugin>` and contributes extra
256
299
  * `Operation`-like entries (with an in-process `handler`) that flow through the
257
300
  * harness and Layer stack exactly like extracted operations. Typical uses:
258
- * `/meta/openapi`, `/meta/health`, version endpoints.
301
+ * `/meta/openapi`, `/meta/health`, version endpoints, `_batch/<kind>` fan-out.
259
302
  */
260
303
  export interface MountCapability {
261
304
  /**
@@ -264,11 +307,18 @@ export interface MountCapability {
264
307
  * `MountedOperation` extends `Operation` with an in-process `handler` and
265
308
  * an optional `transports` filter (default: all transports).
266
309
  *
267
- * @param descriptor - The current merged descriptor (read-only).
268
- * @param opts - Plugin-specific options.
310
+ * @param descriptor - The current merged descriptor (read-only).
311
+ * @param opts - Plugin-specific options.
312
+ * @param hostBridge - (BATCH_0.0.1.md §2/§F1 rollout) Optional, additive —
313
+ * a host-supplied bridge letting the returned operations' handlers invoke
314
+ * OTHER operations on this host (e.g. a `_batch/<kind>` fan-out target).
315
+ * Omitted by plugins (like `health`/`openapi`) that never need it; a
316
+ * plugin whose handler DOES need cross-op invocation (batch) must throw a
317
+ * clear error if it's undefined rather than silently no-op, since that
318
+ * means the host hasn't wired hostBridge support yet.
269
319
  * @returns Array of `MountedOperation`s; may be empty.
270
320
  */
271
- operations(descriptor: Descriptor, opts?: Record<string, unknown>): MountedOperation[];
321
+ operations(descriptor: Descriptor, opts?: Record<string, unknown>, hostBridge?: MountHostBridge): MountedOperation[];
272
322
  }
273
323
  /**
274
324
  * A synthetic operation contributed by a {@link MountCapability}.
@@ -296,6 +346,50 @@ export type MountedOperation = Operation & {
296
346
  */
297
347
  handler(call: Call): unknown | Promise<unknown> | AsyncIterable<Chunk>;
298
348
  };
349
+ /**
350
+ * Overridable fields accepted by {@link syntheticOp}. Every field has a safe
351
+ * default so a minimal mount (like health's `_meta/health`) needs to specify
352
+ * only what actually differs from a zero-input, zero-output `action`.
353
+ */
354
+ export interface SyntheticOpFields {
355
+ /** @default 'action' */
356
+ kind?: OperationKind;
357
+ /** @default `kind === 'query'` (mirrors {@link Operation.safe}'s own default-from-kind rule) */
358
+ safe?: boolean;
359
+ /** @default false */
360
+ async?: boolean;
361
+ /** @default false */
362
+ streaming?: boolean;
363
+ /** @default {} */
364
+ input?: JSONSchema;
365
+ /** @default {} */
366
+ output?: JSONSchema;
367
+ /** @default {} */
368
+ envelope?: JSONSchema;
369
+ /** @default null */
370
+ typeText?: TypeText | null;
371
+ /** @default undefined (all transports) */
372
+ transports?: Transport[];
373
+ hasCtx?: boolean;
374
+ }
375
+ /**
376
+ * Build the common ~10-field {@link Operation} boilerplate for a synthetic
377
+ * mount-contributed operation (F2 — `apigen-core-client/src/lib/plugin.ts`
378
+ * JSDoc previously referenced this as though it already existed; it did not).
379
+ *
380
+ * `id` is `"<namespace>/<path...>"` (e.g. `'_meta/health'`, `'_batch/query'`).
381
+ * Per the existing `_meta/*` mount convention (health/openapi, pre-retrofit),
382
+ * a leading run of underscores on the first segment marks a synthetic
383
+ * namespace in the *id* but is stripped when tokenizing the namespace
384
+ * {@link Segment} itself — `'_meta/health'` → `namespace: {raw:'meta',…}`,
385
+ * `path: [{raw:'health',…}]`, matching what both plugins already produced
386
+ * by hand.
387
+ *
388
+ * Returns everything `MountedOperation` needs except `handler` — the caller
389
+ * supplies that (it's the one part that's genuinely plugin-specific) via
390
+ * `{ ...syntheticOp(id, descriptor, fields), handler: ... }`.
391
+ */
392
+ export declare function syntheticOp(id: string, descriptor: Descriptor, fields?: SyntheticOpFields): Omit<MountedOperation, 'handler'>;
299
393
  /**
300
394
  * **`envelope`** capability — declare request/response side-channel fields
301
395
  * (SPEC §7.1 / §9 / §9.1).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhd/apigen-core-client",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "dependencies": {
5
5
  "ts-morph": "^23.0.0",
6
6
  "ts-json-schema-generator": "^2.3.0",