@adhd/apigen-engine-runtime 0.1.0
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/README.md +40 -0
- package/index.d.ts +28 -0
- package/index.js +1 -0
- package/index.mjs +1398 -0
- package/lib/api-package.d.ts +4 -0
- package/lib/apigen-runtime.d.ts +1 -0
- package/lib/build-context.d.ts +4 -0
- package/lib/coerce-query.d.ts +38 -0
- package/lib/define-middleware.d.ts +5 -0
- package/lib/describe-params.d.ts +18 -0
- package/lib/dispatch.d.ts +23 -0
- package/lib/event-bus.d.ts +19 -0
- package/lib/fn-table.d.ts +16 -0
- package/lib/instance-registry.d.ts +104 -0
- package/lib/invoke.d.ts +112 -0
- package/lib/logger.d.ts +29 -0
- package/lib/logical/host-ts.d.ts +41 -0
- package/lib/logical/nominal-codec.d.ts +73 -0
- package/lib/logical/union-codec.d.ts +63 -0
- package/lib/mcp-output-schema.d.ts +21 -0
- package/lib/stream.d.ts +88 -0
- package/lib/tool-description.d.ts +18 -0
- package/lib/types.d.ts +35 -0
- package/lib/validate-layer.d.ts +54 -0
- package/package.json +19 -0
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { MiddlewareDef, ApiPackageOptions, ApiPackageResult } from './types';
|
|
2
|
+
|
|
3
|
+
export declare function assertNoSelfSubscription(middlewares: readonly MiddlewareDef[]): void;
|
|
4
|
+
export declare function createApiPackage<M extends readonly MiddlewareDef[]>(options: ApiPackageOptions<M>): ApiPackageResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function apigenRuntime(): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FEAT-APIGEN-022 / BACKLOG point 3 — query-string coercion for GET domain args.
|
|
3
|
+
*
|
|
4
|
+
* HTTP query strings carry every value as a string (or string[] for repeated
|
|
5
|
+
* keys). The composed input schema's validate-Layer (`makeValidateLayer`,
|
|
6
|
+
* `@adhd/apigen-engine-runtime/validate-layer`) uses a single Ajv instance
|
|
7
|
+
* with NO `coerceTypes`, shared by both GET (query) and POST (JSON body)
|
|
8
|
+
* validation — enabling Ajv-level `coerceTypes` globally would silently
|
|
9
|
+
* coerce POST body values too (e.g. a JSON string erroneously accepted where
|
|
10
|
+
* a number was required), which SPEC §6 forbids. Coercion therefore happens
|
|
11
|
+
* here, at the TRANSPORT layer, scoped to exactly the GET/query-string path,
|
|
12
|
+
* before the already-strict (non-coercing) validate-Layer ever sees the
|
|
13
|
+
* value — POST/body validation is completely untouched.
|
|
14
|
+
*
|
|
15
|
+
* Only `number`/`integer`/`boolean` are coerced (the non-`string` members of
|
|
16
|
+
* FEAT-APIGEN-022's "properly typed primitives" set — `string` values are
|
|
17
|
+
* already correctly typed as they arrive). A value that fails to parse (e.g.
|
|
18
|
+
* `?count=abc` against a `number` param) is left AS-IS so the validate-Layer
|
|
19
|
+
* still rejects it with a clear `invalid_argument` error, rather than being
|
|
20
|
+
* silently dropped or coerced to `NaN`/`0`.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Coerces a request's query-string params to the JS types their domain
|
|
24
|
+
* schema declares (`number`/`integer`/`boolean`), based on the function's own
|
|
25
|
+
* composed schema. `string`-typed and unrecognised params pass through
|
|
26
|
+
* unchanged; array-valued query entries (repeated keys) pass through
|
|
27
|
+
* unchanged (arrays are outside FEAT-APIGEN-022's primitive-only hoist
|
|
28
|
+
* boundary, so no domain param reaching here should be array-typed — but this
|
|
29
|
+
* stays defensive rather than assuming that invariant).
|
|
30
|
+
*
|
|
31
|
+
* @param query The raw `req.query` / `request.query` object.
|
|
32
|
+
* @param schema The function's composed schema entry (`pkg.schemas[fnName]`) —
|
|
33
|
+
* loosely typed like `describeParams`'s `schema` param, since
|
|
34
|
+
* callers pass the untyped `ComposedSchemas` entry.
|
|
35
|
+
*/
|
|
36
|
+
export declare function coerceQueryParams(query: Record<string, unknown>, schema: {
|
|
37
|
+
input?: unknown;
|
|
38
|
+
} | undefined): Record<string, unknown>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ParamInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
type: string;
|
|
4
|
+
required: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Extract a composed schema entry's domain parameters for logging.
|
|
8
|
+
*
|
|
9
|
+
* Params live under `input.properties.data.properties` — the `data` envelope
|
|
10
|
+
* wrapper is always present (see [def:ComposedSchemas]). Returns both a
|
|
11
|
+
* structured list (for JSON logs) and a `name?: type` summary string.
|
|
12
|
+
*/
|
|
13
|
+
export declare function describeParams(schema: {
|
|
14
|
+
input?: unknown;
|
|
15
|
+
} | undefined): {
|
|
16
|
+
params: ParamInfo[];
|
|
17
|
+
text: string;
|
|
18
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ComposedSchemas } from './types';
|
|
2
|
+
|
|
3
|
+
/** Returns true when the composed schema has `field` in input.properties (i.e. an envelope field). */
|
|
4
|
+
export declare function needsEnvelopeField(fnSchema: ComposedSchemas[string], field: string): boolean;
|
|
5
|
+
/** Returns ordered domain parameter names (keys of the data: {} sub-object). */
|
|
6
|
+
export declare function dataParamNames(fnSchema: ComposedSchemas[string]): string[];
|
|
7
|
+
/**
|
|
8
|
+
* Single canonical dispatch path used by ALL plugins in both generate and run modes.
|
|
9
|
+
* No plugin may inline this logic. [inv:dispatch-single-path]
|
|
10
|
+
*
|
|
11
|
+
* Logical-type decode/encode seam (DESIGN.md §4.4 / §6):
|
|
12
|
+
* - BEFORE calling the fn: each domain arg is decoded from wire → host using
|
|
13
|
+
* the schema node at `input.properties.data.properties[k]`. A param whose
|
|
14
|
+
* node carries `{type:string, format:date-time}` arrives as a real `Date`;
|
|
15
|
+
* a plain `string` node passes through unchanged.
|
|
16
|
+
* - AFTER the fn returns: the result is encoded from host → wire using the
|
|
17
|
+
* output schema. A `Date` return becomes an RFC 3339 string; plain JSON
|
|
18
|
+
* values pass through unchanged.
|
|
19
|
+
*
|
|
20
|
+
* All existing behaviors (session ctx injection, BUG-APIGEN-001 ctx-param path,
|
|
21
|
+
* envelope handling) are preserved unchanged.
|
|
22
|
+
*/
|
|
23
|
+
export declare function dispatch(fns: Record<string, (...args: unknown[]) => unknown>, createClient: ((e: Record<string, unknown>) => Promise<unknown>) | undefined, schema: ComposedSchemas[string], fnName: string, envelope: Record<string, unknown>, domainArgs: Record<string, unknown>): Promise<unknown>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MiddlewareDef, MiddlewareEvent } from './types';
|
|
2
|
+
|
|
3
|
+
type EventHandler = (event: MiddlewareEvent) => void | Promise<void>;
|
|
4
|
+
export declare class EventBus {
|
|
5
|
+
private handlers;
|
|
6
|
+
/**
|
|
7
|
+
* Register a handler. Returns an unsubscribe function — handlers registered
|
|
8
|
+
* per-request/per-package MUST be removed (via the returned fn, {@link off},
|
|
9
|
+
* or {@link clear}) or they accumulate for the bus's lifetime.
|
|
10
|
+
*/
|
|
11
|
+
on(selector: string, handler: EventHandler): () => void;
|
|
12
|
+
/** Remove a previously registered handler (no-op if absent). */
|
|
13
|
+
off(selector: string, handler: EventHandler): void;
|
|
14
|
+
/** Remove every handler — call on package/server teardown. */
|
|
15
|
+
clear(): void;
|
|
16
|
+
emit(event: MiddlewareEvent): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export declare function wireObservers(middlewares: readonly MiddlewareDef[], bus: EventBus): void;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type AnyFn = (...args: unknown[]) => unknown;
|
|
2
|
+
/**
|
|
3
|
+
* Build a `name → function` table from an imported module namespace, matching how
|
|
4
|
+
* schema extraction names functions so `dispatch` can always resolve them.
|
|
5
|
+
*
|
|
6
|
+
* Robust to every export + interop shape:
|
|
7
|
+
* - **named exports** (`export function f` / `export const f = …`) → keyed by name.
|
|
8
|
+
* - **single default-exported function** (`export default f`) → keyed by the
|
|
9
|
+
* function's declaration name (ESM keys it `default`; CJS-compiled deps double-
|
|
10
|
+
* wrap it as `default.default` / `module.exports.default`). We unwrap those
|
|
11
|
+
* layers and key every function found by its `.name`.
|
|
12
|
+
* - **default object** (`export default { a, b }`) → each function keyed by name.
|
|
13
|
+
*
|
|
14
|
+
* Earlier keys win; explicit named exports are never overwritten by unwrapped ones.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildFnTable(mod: Record<string, unknown>): Record<string, AnyFn>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instance registry for class-export instance dispatch (SPEC §10).
|
|
3
|
+
*
|
|
4
|
+
* When a `kind:'constructor'` op fires, the runtime constructs the target class
|
|
5
|
+
* and stores the live instance here under a unique `instanceId`. Subsequent
|
|
6
|
+
* `kind:'instance-method'` ops look up the instance by `instanceId` and
|
|
7
|
+
* dispatch the method against it.
|
|
8
|
+
*
|
|
9
|
+
* Lifecycle (TTL + dispose):
|
|
10
|
+
* - Every entry carries an optional TTL (milliseconds). A sweeper clears
|
|
11
|
+
* expired entries automatically; the sweep interval is configurable.
|
|
12
|
+
* - Calling `dispose(instanceId)` removes the entry immediately and runs the
|
|
13
|
+
* instance's `dispose()` method if it exists.
|
|
14
|
+
* - `disposeAll()` tears down every live instance and stops the sweeper.
|
|
15
|
+
*
|
|
16
|
+
* Stateful caveat: the registry lives in-process; horizontal scaling without
|
|
17
|
+
* sticky routing or an external store will route requests to the wrong instance.
|
|
18
|
+
* Document this to API consumers (SPEC §10 note).
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* ```ts
|
|
22
|
+
* const registry = new InstanceRegistry({ defaultTtlMs: 30_000 })
|
|
23
|
+
*
|
|
24
|
+
* // constructor op handler
|
|
25
|
+
* const { instanceId } = registry.create(Counter, [0])
|
|
26
|
+
*
|
|
27
|
+
* // instance-method op handler
|
|
28
|
+
* const counter = registry.get<Counter>(instanceId)
|
|
29
|
+
* counter.increment()
|
|
30
|
+
*
|
|
31
|
+
* // explicit teardown
|
|
32
|
+
* await registry.dispose(instanceId)
|
|
33
|
+
* await registry.disposeAll()
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
/** Any class constructor (unknown args → unknown instance). */
|
|
37
|
+
export type AnyConstructor = new (...args: unknown[]) => unknown;
|
|
38
|
+
/** Options for the registry. */
|
|
39
|
+
export interface InstanceRegistryOptions {
|
|
40
|
+
/**
|
|
41
|
+
* Default TTL in milliseconds for each instance. When omitted, instances
|
|
42
|
+
* live until explicitly `dispose()`d or `disposeAll()` is called.
|
|
43
|
+
*/
|
|
44
|
+
defaultTtlMs?: number;
|
|
45
|
+
/**
|
|
46
|
+
* How often (ms) the background sweeper checks for expired entries.
|
|
47
|
+
* Defaults to `Math.max(1_000, defaultTtlMs / 4)` when a TTL is set,
|
|
48
|
+
* or `5_000` otherwise. Pass `0` to disable the automatic sweeper.
|
|
49
|
+
*/
|
|
50
|
+
sweepIntervalMs?: number;
|
|
51
|
+
}
|
|
52
|
+
/** The result returned by `registry.create()` — the `instanceId` is what the
|
|
53
|
+
* client passes back on every `kind:'instance-method'` call. */
|
|
54
|
+
export interface CreateResult {
|
|
55
|
+
instanceId: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* In-process instance store for SPEC §10 class-instance dispatch.
|
|
59
|
+
*
|
|
60
|
+
* Thread-safe within a single Node.js event-loop (single-threaded JS).
|
|
61
|
+
* Stateful: does NOT survive process restarts; scale with sticky routing.
|
|
62
|
+
*/
|
|
63
|
+
export declare class InstanceRegistry {
|
|
64
|
+
private readonly _store;
|
|
65
|
+
private readonly _defaultTtlMs;
|
|
66
|
+
private _sweeper;
|
|
67
|
+
constructor(opts?: InstanceRegistryOptions);
|
|
68
|
+
/**
|
|
69
|
+
* Construct an instance of `Ctor` with `args` and store it in the registry.
|
|
70
|
+
*
|
|
71
|
+
* @param Ctor - The class to instantiate.
|
|
72
|
+
* @param args - Constructor arguments (positional).
|
|
73
|
+
* @param ttlMs - Per-entry TTL override. Falls back to `defaultTtlMs`.
|
|
74
|
+
* @returns `{ instanceId }` — pass this to `dispatch()` / `get()`.
|
|
75
|
+
*/
|
|
76
|
+
create(Ctor: AnyConstructor, args?: unknown[], ttlMs?: number): CreateResult;
|
|
77
|
+
/**
|
|
78
|
+
* Retrieve the instance stored under `instanceId`.
|
|
79
|
+
*
|
|
80
|
+
* @throws When `instanceId` is unknown or the entry has expired.
|
|
81
|
+
*/
|
|
82
|
+
get<T = unknown>(instanceId: string): T;
|
|
83
|
+
/**
|
|
84
|
+
* Look up `instanceId` and call `method` on it with `args`.
|
|
85
|
+
*
|
|
86
|
+
* @throws When `instanceId` is unknown/expired, or `method` is not a function
|
|
87
|
+
* on the instance.
|
|
88
|
+
*/
|
|
89
|
+
dispatch(instanceId: string, method: string, args?: unknown[]): Promise<unknown>;
|
|
90
|
+
/**
|
|
91
|
+
* Remove `instanceId` from the registry and call `instance.dispose()` if the
|
|
92
|
+
* instance exposes one (opt-in lifecycle hook).
|
|
93
|
+
*/
|
|
94
|
+
dispose(instanceId: string): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Dispose every live instance and stop the background sweeper. Call this
|
|
97
|
+
* when the server is shutting down.
|
|
98
|
+
*/
|
|
99
|
+
disposeAll(): Promise<void>;
|
|
100
|
+
/** Number of live (non-expired) entries currently in the registry. */
|
|
101
|
+
get size(): number;
|
|
102
|
+
private _sweep;
|
|
103
|
+
private _evict;
|
|
104
|
+
}
|
package/lib/invoke.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ComposedSchemas } from './types';
|
|
2
|
+
import { Operation } from '@adhd/apigen-core-client';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A symbol-keyed or constructor-keyed typed extension map. Each value is
|
|
6
|
+
* stored under a unique token so Layers can share typed data without a mutable
|
|
7
|
+
* property bag. Modeled after Tower/`http::Extensions` — the only `ctx` shape
|
|
8
|
+
* expressible under Rust's borrow checker, so TS matches it for dual-host
|
|
9
|
+
* alignment.
|
|
10
|
+
*/
|
|
11
|
+
export declare class LayerContext {
|
|
12
|
+
private readonly _map;
|
|
13
|
+
/** Store `value` under `token` (a constructor or a unique symbol). */
|
|
14
|
+
set<T>(token: abstract new (...args: never[]) => T, value: T): void;
|
|
15
|
+
set<T>(token: symbol, value: T): void;
|
|
16
|
+
/**
|
|
17
|
+
* Retrieve the value stored under `token`. Returns `undefined` when the
|
|
18
|
+
* token has not been inserted — callers are responsible for presence checks.
|
|
19
|
+
*/
|
|
20
|
+
get<T>(token: abstract new (...args: never[]) => T): T | undefined;
|
|
21
|
+
get<T>(token: symbol): T | undefined;
|
|
22
|
+
/** Returns true when the token has a stored value. */
|
|
23
|
+
has(token: unknown): boolean;
|
|
24
|
+
}
|
|
25
|
+
/** The resolved domain call, threaded through the Layer stack. */
|
|
26
|
+
export interface Call {
|
|
27
|
+
/**
|
|
28
|
+
* The canonical operation descriptor. For v1 callers that do not have a
|
|
29
|
+
* full Operation, only `id` is guaranteed; the remaining fields are optional.
|
|
30
|
+
*/
|
|
31
|
+
operation: Pick<Operation, 'id'> & Partial<Operation>;
|
|
32
|
+
/** Typed extension map — insert/read per §8.1 rule 3. */
|
|
33
|
+
ctx: LayerContext;
|
|
34
|
+
/** Middleware envelope (side-channel from transport metadata). */
|
|
35
|
+
envelope: Record<string, unknown>;
|
|
36
|
+
/** Domain arguments (the `data` sub-object from the composed input). */
|
|
37
|
+
domainArgs: Record<string, unknown>;
|
|
38
|
+
/** Cancellation signal (AbortSignal) — §11. */
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The primitive result a Layer or dispatch can return.
|
|
43
|
+
*
|
|
44
|
+
* §11: a streaming operation returns `AsyncIterable<unknown>`; a normal
|
|
45
|
+
* operation returns `unknown`. Both are covered by this union.
|
|
46
|
+
*/
|
|
47
|
+
export type LayerResult = unknown | AsyncIterable<unknown>;
|
|
48
|
+
/**
|
|
49
|
+
* `next` continuation — the inner Layer or dispatch Service.
|
|
50
|
+
*
|
|
51
|
+
* Returns a promise of either a scalar result or an AsyncIterable stream
|
|
52
|
+
* (§11). A Layer that short-circuits (§8.1 rule 1) never calls this.
|
|
53
|
+
*/
|
|
54
|
+
export type Next = () => Promise<LayerResult>;
|
|
55
|
+
/**
|
|
56
|
+
* A Layer in the TS harness (§8.1 normative closure signature).
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* const logger: Layer = async (call, next) => {
|
|
60
|
+
* const t = Date.now()
|
|
61
|
+
* try { const r = await next(); return r }
|
|
62
|
+
* catch (e) { throw e } // error unwinds outward — §8.1 rule 2
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* Rule 1 (short-circuit): return a value WITHOUT calling `next`.
|
|
67
|
+
* Rule 2 (error propagation): `throw` propagates to the enclosing Layer.
|
|
68
|
+
* Rule 4 (poll_ready): not present — TS host omits it entirely (host-optional).
|
|
69
|
+
* Rule 5 (streaming): `next()` return type includes `AsyncIterable<unknown>`.
|
|
70
|
+
* Rule 6 (codegen-weave): composition is a pure function; same semantics.
|
|
71
|
+
*/
|
|
72
|
+
export type Layer = (call: Call, next: Next) => Promise<LayerResult>;
|
|
73
|
+
/**
|
|
74
|
+
* Runtime context needed by `invoke` to reach the core dispatch Service.
|
|
75
|
+
*
|
|
76
|
+
* v1 callers supply `fns` + `createClient` + `schemas` directly; future
|
|
77
|
+
* callers may pre-build an invoker from an Operation descriptor.
|
|
78
|
+
*/
|
|
79
|
+
export interface InvokeOptions {
|
|
80
|
+
/** The live function table (fn-name → implementation). */
|
|
81
|
+
fns: Record<string, (...args: unknown[]) => unknown>;
|
|
82
|
+
/** Optional client factory (for session-ctx middleware). */
|
|
83
|
+
createClient?: (envelope: Record<string, unknown>) => Promise<unknown>;
|
|
84
|
+
/** Composed schemas for the target namespace. */
|
|
85
|
+
schemas: ComposedSchemas;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The composed invoke function returned by {@link createInvoker}.
|
|
89
|
+
*
|
|
90
|
+
* Accepts an operation name, a {@link Call}, and runtime dispatch options.
|
|
91
|
+
* Returns the Service result, streaming or scalar.
|
|
92
|
+
*/
|
|
93
|
+
export type InvokeFn = (fnName: string, call: Call, opts: InvokeOptions) => Promise<LayerResult>;
|
|
94
|
+
/**
|
|
95
|
+
* Compose a Layer stack and return a typed `invoke` function.
|
|
96
|
+
*
|
|
97
|
+
* Layers are applied **outermost-first**: `layers[0]` wraps `layers[1]`
|
|
98
|
+
* which wraps … which wraps `dispatch`. Equivalent to Tower's
|
|
99
|
+
* `ServiceBuilder::layer` / `layer_fn` composition order.
|
|
100
|
+
*
|
|
101
|
+
* ```ts
|
|
102
|
+
* const invoke = createInvoker([authLayer, loggerLayer])
|
|
103
|
+
* const result = await invoke('getUser', call, opts)
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* When `layers` is empty, `invoke` calls `dispatch` directly.
|
|
107
|
+
*
|
|
108
|
+
* §8.1 rule 6 (codegen-weave): static hosts receive the composed list at
|
|
109
|
+
* codegen time; `createInvoker` is called once per plugin instantiation, not
|
|
110
|
+
* once per request.
|
|
111
|
+
*/
|
|
112
|
+
export declare function createInvoker(layers?: readonly Layer[]): InvokeFn;
|
package/lib/logger.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Logger } from 'pino';
|
|
2
|
+
|
|
3
|
+
export type { Logger };
|
|
4
|
+
/** Log output format. `pretty` is colorized human-readable; `json` is raw jsonl. */
|
|
5
|
+
export type LogFormat = 'json' | 'pretty';
|
|
6
|
+
export interface CreateLoggerOptions {
|
|
7
|
+
/** pino log level. Default: `info`. */
|
|
8
|
+
level?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Output format. Default: `pretty` when the destination is a TTY, else `json`.
|
|
11
|
+
* `pretty` routes through pino-pretty (colorized, timestamped); `json` emits jsonl.
|
|
12
|
+
*/
|
|
13
|
+
format?: LogFormat;
|
|
14
|
+
/**
|
|
15
|
+
* Where logs are written. Default: stderr (fd 2) — NEVER stdout, which is the
|
|
16
|
+
* MCP stdio JSON-RPC channel. Pass a filesystem path to write logs to a file.
|
|
17
|
+
*/
|
|
18
|
+
destination?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build the shared apigen pino logger.
|
|
22
|
+
*
|
|
23
|
+
* Logging always targets stderr or a file — never stdout — so the MCP stdio
|
|
24
|
+
* transport's JSON-RPC channel on stdout stays free of log noise.
|
|
25
|
+
*
|
|
26
|
+
* @param opts - level, format (`json` | `pretty`), and destination (stderr or a file path).
|
|
27
|
+
* @returns a configured pino {@link Logger}.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createLogger(opts?: CreateLoggerOptions): Logger;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { LogicalTypeCodec, LogicalTypeId } from '@adhd/apigen-base-logical';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @stable A host binding maps a LogicalTypeId to that host's native
|
|
5
|
+
* encode/decode primitives (DESIGN.md §4.6).
|
|
6
|
+
*
|
|
7
|
+
* - `host` — identifies the language/runtime.
|
|
8
|
+
* - `logicalTypeVersion` — pinned to `LOGICAL_TYPE_VERSION` from
|
|
9
|
+
* `@adhd/apigen-base-logical`; bumped on any wire-table
|
|
10
|
+
* or pinned-lib-version change (DESIGN §8.6).
|
|
11
|
+
* - `codecs` — frozen ReadonlyMap keyed by LogicalTypeId. Must
|
|
12
|
+
* cover every well-known id (DESIGN §4.6 "Must cover
|
|
13
|
+
* every well-known id.").
|
|
14
|
+
*/
|
|
15
|
+
export interface HostBinding {
|
|
16
|
+
readonly host: 'ts' | 'python' | 'rust' | 'go' | 'java';
|
|
17
|
+
readonly logicalTypeVersion: string;
|
|
18
|
+
/** The codecs this host provides, keyed by LogicalTypeId. */
|
|
19
|
+
readonly codecs: ReadonlyMap<LogicalTypeId, LogicalTypeCodec>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* @stable Ordered array of all well-known scalar codec instances for the TS
|
|
23
|
+
* host. Derived from the same import list used by `registerWellKnown` in
|
|
24
|
+
* `@adhd/apigen-base-logical`; kept in sync by importing the exact same named
|
|
25
|
+
* exports (a future addition to that barrel automatically lands here).
|
|
26
|
+
*/
|
|
27
|
+
export declare const WELL_KNOWN_TS_CODECS: ReadonlyArray<LogicalTypeCodec>;
|
|
28
|
+
/**
|
|
29
|
+
* @stable The TypeScript HostBinding (DESIGN.md §4.6).
|
|
30
|
+
*
|
|
31
|
+
* A frozen object exposing all TS codec instances keyed by their
|
|
32
|
+
* `LogicalTypeId`. Consumers obtain a per-surface registry by calling
|
|
33
|
+
* `registerWellKnown(registry)` from `@adhd/apigen-base-logical`, then
|
|
34
|
+
* registering the generated nominal/union codecs on top.
|
|
35
|
+
*
|
|
36
|
+
* `logicalTypeVersion` is pinned to `LOGICAL_TYPE_VERSION` exported by
|
|
37
|
+
* `@adhd/apigen-base-logical` (`descriptor-ext.ts`) — `'0.1.0'` at authoring time.
|
|
38
|
+
* Any wire-table or pinned-lib-version change bumps that constant and this
|
|
39
|
+
* binding automatically reflects the new version.
|
|
40
|
+
*/
|
|
41
|
+
export declare const tsHostBinding: HostBinding;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { LogicalTypeCodec, LogicalTypeId, SchemaNode } from '@adhd/apigen-base-logical';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reserved descriptor keyword carrying the extractor's **opt-in-instances**
|
|
5
|
+
* decision (DESIGN.md §4.5 / §13 — risk 8.5, RESOLVED). A class the extractor
|
|
6
|
+
* could not prove reconstructable (sockets, closures, opaque handles) is emitted
|
|
7
|
+
* with `x-apigen-instances:false`; its codec is **encode-only** and `decode`
|
|
8
|
+
* throws a clear `E_NOMINAL_NONRECONSTRUCTABLE` diagnostic rather than minting a
|
|
9
|
+
* half-built instance.
|
|
10
|
+
*/
|
|
11
|
+
export declare const X_APIGEN_INSTANCES: "x-apigen-instances";
|
|
12
|
+
/**
|
|
13
|
+
* Stable, transport-neutral error codes raised by the nominal codec. They are
|
|
14
|
+
* carried on a typed `Error` subclass so callers can branch on `err.code`
|
|
15
|
+
* without string-matching the message.
|
|
16
|
+
*/
|
|
17
|
+
export type NominalCodecErrorCode = 'E_NOMINAL_CYCLE' | 'E_NOMINAL_NONRECONSTRUCTABLE' | 'E_NOMINAL_INVALID_WIRE';
|
|
18
|
+
/** @stable Error carrier for the custom-class (`kind:'nominal'`) codec. */
|
|
19
|
+
export declare class NominalCodecError extends Error {
|
|
20
|
+
readonly code: NominalCodecErrorCode;
|
|
21
|
+
constructor(code: NominalCodecErrorCode, message: string);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A host class that knows how to rebuild itself from its field bag. The codec
|
|
25
|
+
* looks up the static reconstructor named by `x-apigen-ctor` (default
|
|
26
|
+
* `fromJSON`) on the registered host class; absent that, it falls back to a
|
|
27
|
+
* schema-projected plain construction.
|
|
28
|
+
*/
|
|
29
|
+
export type NominalCtor = {
|
|
30
|
+
new (...args: any[]): object;
|
|
31
|
+
fromJSON?(bag: Record<string, unknown>): object;
|
|
32
|
+
[staticCtor: string]: unknown;
|
|
33
|
+
};
|
|
34
|
+
/** Options threaded into {@link createNominalCodec} for one custom class. */
|
|
35
|
+
export interface NominalCodecOptions {
|
|
36
|
+
/** Stable LogicalTypeId (namespace-qualified, e.g. `cli.User`). */
|
|
37
|
+
readonly id: LogicalTypeId;
|
|
38
|
+
/** Resolved `$def` object schema this codec owns (carries `properties`/`required`). */
|
|
39
|
+
readonly schema: SchemaNode;
|
|
40
|
+
/**
|
|
41
|
+
* Host class used to reconstruct instances on decode. OPTIONAL: when absent
|
|
42
|
+
* the codec decodes to a schema-projected plain object (still a faithful
|
|
43
|
+
* round-trip of the field bag — see `[inv:hints-advisory]`).
|
|
44
|
+
*/
|
|
45
|
+
readonly ctor?: NominalCtor;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* @stable Codec factory for a single custom class (`kind:'nominal'`).
|
|
49
|
+
*
|
|
50
|
+
* Encode (host → wire), DESIGN.md §4/§13:
|
|
51
|
+
* 1. **Cycle guard** — track each visited instance in `ctx.seen`; a back-edge
|
|
52
|
+
* in `strict` mode throws `E_NOMINAL_CYCLE` (the approved strict policy,
|
|
53
|
+
* decision 8.3). `lossy` mode breaks the cycle by emitting `null`.
|
|
54
|
+
* 2. **`toJSON` hint** — when the class exposes `toJSON` (named by
|
|
55
|
+
* `x-apigen-tojson`, default `toJSON`), use its field bag.
|
|
56
|
+
* 3. **Field projection** — otherwise project the schema's declared
|
|
57
|
+
* `properties`. Either way every field VALUE is recursed through the
|
|
58
|
+
* registry so nested logical types (e.g. `Date`) encode correctly.
|
|
59
|
+
*
|
|
60
|
+
* Decode (wire → host), validate-then-construct:
|
|
61
|
+
* 1. **Validate** the wire against the node FIRST (object shape + required
|
|
62
|
+
* fields present) → `E_NOMINAL_INVALID_WIRE` on a mismatch.
|
|
63
|
+
* 2. **Reconstructable gate** — `x-apigen-instances:false` ⇒ encode-only;
|
|
64
|
+
* decode throws `E_NOMINAL_NONRECONSTRUCTABLE`.
|
|
65
|
+
* 3. **Construct** — via the static reconstructor named by `x-apigen-ctor`
|
|
66
|
+
* (default `fromJSON`) when the host class is supplied, else a
|
|
67
|
+
* schema-projected plain object. Field VALUES are recursed first.
|
|
68
|
+
*
|
|
69
|
+
* `[inv:hints-advisory]`: the `x-apigen-*` keys only *accelerate* the choice of
|
|
70
|
+
* hook. With every hint stripped the codec still round-trips via schema
|
|
71
|
+
* projection — proven by a negative-control vector.
|
|
72
|
+
*/
|
|
73
|
+
export declare function createNominalCodec(opts: NominalCodecOptions): LogicalTypeCodec<object>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { LogicalTypeCodec, LogicalTypeId, SchemaNode } from '@adhd/apigen-base-logical';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stable, transport-neutral error codes raised by the union codec. Carried on
|
|
5
|
+
* a typed `Error` subclass so callers can branch on `err.code` without
|
|
6
|
+
* string-matching the message.
|
|
7
|
+
*/
|
|
8
|
+
export type UnionCodecErrorCode = 'E_UNION_NO_DISCRIMINATOR' | 'E_UNION_UNKNOWN_TAG' | 'E_UNION_INVALID_WIRE';
|
|
9
|
+
/** @stable Error carrier for the polymorphic-union (`kind:'union'`) codec. */
|
|
10
|
+
export declare class UnionCodecError extends Error {
|
|
11
|
+
readonly code: UnionCodecErrorCode;
|
|
12
|
+
constructor(code: UnionCodecErrorCode, message: string);
|
|
13
|
+
}
|
|
14
|
+
/** Options threaded into {@link createUnionCodec} for one polymorphic union. */
|
|
15
|
+
export interface UnionCodecOptions {
|
|
16
|
+
/** Stable LogicalTypeId (namespace-qualified, e.g. `cli.Pet`). */
|
|
17
|
+
readonly id: LogicalTypeId;
|
|
18
|
+
/**
|
|
19
|
+
* The union schema node: `{ oneOf:[...], discriminator:{propertyName, mapping} }`.
|
|
20
|
+
* This is the authoritative ownership schema stored on the codec; individual
|
|
21
|
+
* `encode`/`decode` calls may pass a node with `x-apigen-logical` stripped
|
|
22
|
+
* (or a structurally equivalent node) — the codec MUST behave identically.
|
|
23
|
+
*/
|
|
24
|
+
readonly schema: SchemaNode;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* @stable Codec factory for a discriminated union (`kind:'union'`).
|
|
28
|
+
*
|
|
29
|
+
* Wire shape (DESIGN §3 + §4.1): the value is a JSON object that carries a
|
|
30
|
+
* const discriminator property (e.g. `kind`). The schema node looks like:
|
|
31
|
+
* ```json
|
|
32
|
+
* { "oneOf": [{"$ref":"#/$defs/Dog"},{"$ref":"#/$defs/Cat"}],
|
|
33
|
+
* "discriminator": {"propertyName":"kind","mapping":{"dog":"#/$defs/Dog","cat":"#/$defs/Cat"}},
|
|
34
|
+
* "x-apigen-logical": "union" }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* Ownership test — `matches(node)`:
|
|
38
|
+
* true when the node has BOTH `oneOf` (array) AND `discriminator` (object with
|
|
39
|
+
* `propertyName`). The `x-apigen-logical:"union"` hint accelerates dispatch but
|
|
40
|
+
* is NOT required — structure is authoritative (`[inv:hints-advisory]`).
|
|
41
|
+
*
|
|
42
|
+
* Encode (host → wire):
|
|
43
|
+
* 1. Read `discriminator.propertyName` off the **value** to get the tag.
|
|
44
|
+
* Throws `E_UNION_NO_DISCRIMINATOR` if the tag prop is absent/undefined.
|
|
45
|
+
* 2. Map tag → branch `$ref` via `discriminator.mapping[tag]`.
|
|
46
|
+
* Throws `E_UNION_UNKNOWN_TAG` if the tag isn't in the mapping.
|
|
47
|
+
* 3. Resolve the branch schema node via `ctx.resolve(ref)`.
|
|
48
|
+
* 4. Delegate encoding of value to the branch codec via `ctx.registry.resolve`,
|
|
49
|
+
* falling back to structural identity pass-through for plain object schemas.
|
|
50
|
+
*
|
|
51
|
+
* Decode (wire → host) — mirror of encode:
|
|
52
|
+
* 1. Validate the wire is a non-null, non-array object.
|
|
53
|
+
* Throws `E_UNION_INVALID_WIRE` on a non-object.
|
|
54
|
+
* 2. Read the discriminator tag off the wire object.
|
|
55
|
+
* Throws `E_UNION_NO_DISCRIMINATOR` if absent.
|
|
56
|
+
* 3. Map tag → branch `$ref` + resolve, then delegate to the branch codec.
|
|
57
|
+
* Throws `E_UNION_UNKNOWN_TAG` if the tag isn't in the mapping.
|
|
58
|
+
*
|
|
59
|
+
* `[inv:hints-advisory]`: stripping `x-apigen-logical:"union"` from the node
|
|
60
|
+
* passed to `encode`/`decode` produces identical output. Proven by the
|
|
61
|
+
* negative-control vector in the spec.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createUnionCodec(opts: UnionCodecOptions): LogicalTypeCodec<object>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Result of adapting a composed-schema `output` fragment for MCP's `outputSchema` field. */
|
|
2
|
+
export interface McpOutputAdapter {
|
|
3
|
+
/** The MCP `Tool.outputSchema` value, or `undefined` when `output` carries no usable schema. */
|
|
4
|
+
outputSchema: Record<string, unknown> | undefined;
|
|
5
|
+
/** True when the output wasn't top-level `type:"object"` and had to be wrapped under `result`. */
|
|
6
|
+
wrapped: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Builds the MCP `outputSchema` for a composed schema's `output` fragment.
|
|
10
|
+
*
|
|
11
|
+
* @param output - `ComposedSchemas[fn].output`, whatever `buildSchema` produced
|
|
12
|
+
* (may be `type:"object"`, `oneOf`+`discriminator` for a union, an array, a
|
|
13
|
+
* bare scalar, or `undefined`/`{}` for an unresolved/void return).
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildMcpOutputSchema(output: unknown): McpOutputAdapter;
|
|
16
|
+
/**
|
|
17
|
+
* Wraps a tool's actual return value to match the `outputSchema` shape built by
|
|
18
|
+
* `buildMcpOutputSchema` for the SAME `output` fragment, for use as MCP's
|
|
19
|
+
* `structuredContent` (also constrained to a plain object by the SDK).
|
|
20
|
+
*/
|
|
21
|
+
export declare function wrapMcpStructuredContent(wrapped: boolean, value: unknown): Record<string, unknown> | undefined;
|