@adhd/apigen-engine-runtime 0.1.2 → 0.1.4
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/CHANGELOG.md +21 -0
- package/index.d.ts +6 -0
- package/index.js +1 -1
- package/index.mjs +857 -650
- package/lib/dispatch-for-plan.d.ts +9 -0
- package/lib/op-plan.d.ts +100 -0
- package/lib/package-invoker.d.ts +50 -0
- package/lib/transport-adapter.d.ts +24 -0
- package/package.json +5 -4
- package/test-support/parity-harness.d.ts +108 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { OpPlan } from './op-plan';
|
|
2
|
+
import { Call as RuntimeCall, InvokeFn, InvokeOptions, LayerResult } from './invoke';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve one dispatch for `plan`, given the package's composed `invoke`
|
|
6
|
+
* (`InvokeFn`, e.g. from `createPackageInvoker`) and a partial runtime `Call`
|
|
7
|
+
* (minus `operation`/`ctx`, per the `TransportAdapter` contract).
|
|
8
|
+
*/
|
|
9
|
+
export declare function dispatchForPlan(plan: OpPlan, invoke: InvokeFn, call: Omit<RuntimeCall, 'operation' | 'ctx'>, opts: InvokeOptions): Promise<LayerResult>;
|
package/lib/op-plan.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { LayerResult } from './invoke';
|
|
2
|
+
import { ParamInfo } from './describe-params';
|
|
3
|
+
import { HttpVerb, ProjectionConfig } from '@adhd/apigen-engine-naming';
|
|
4
|
+
import { Call as CoreClientCall, ComposedSchemas, MountedOperation, Operation, Transport } from '@adhd/apigen-core-client';
|
|
5
|
+
|
|
6
|
+
/** One §9.1 envelope side-channel field, projected to every transport binding. */
|
|
7
|
+
export interface OpPlanEnvelopeField {
|
|
8
|
+
/** The bare field name as it appears in the composed schema. */
|
|
9
|
+
field: string;
|
|
10
|
+
/** pluginId resolved from `x-apigen-envelope` metadata (defaults to `'adhd'`). */
|
|
11
|
+
pluginId: string;
|
|
12
|
+
/** `x-<pluginId>-<field>` — used verbatim for HTTP headers and MCP `_meta` keys. */
|
|
13
|
+
httpHeader: string;
|
|
14
|
+
/** Same string as `httpHeader` — an explicit alias so call sites self-document. */
|
|
15
|
+
mcpMetaKey: string;
|
|
16
|
+
/** `--<pluginId>-<field>` CLI flag. */
|
|
17
|
+
cliFlag: string;
|
|
18
|
+
/** `APIGEN_<PLUGINID>_<FIELD>` env-var fallback. */
|
|
19
|
+
envVar: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* One resolved `--flag` for a command, keyed by its argv name (without the
|
|
23
|
+
* leading `--`) — the same shape `apigen-plugin-cli-output`'s `FlagSpec`
|
|
24
|
+
* carries, computed once here instead of per-plugin (F2).
|
|
25
|
+
*/
|
|
26
|
+
export interface OpPlanCliFlag {
|
|
27
|
+
/** The domain param name or envelope field name in camelCase. */
|
|
28
|
+
camelKey: string;
|
|
29
|
+
kind: 'domain' | 'envelope';
|
|
30
|
+
valueKind: 'boolean' | 'json' | 'string';
|
|
31
|
+
/** (envelope only) `APIGEN_<PLUGINID>_<FIELD>` fallback when the flag is absent. */
|
|
32
|
+
envVar?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A fully-resolved, transport-complete plan for one `Operation` (proposal §3a).
|
|
36
|
+
*
|
|
37
|
+
* Computed ONCE per op — never re-derived per request — from the `Operation`
|
|
38
|
+
* and its composed schema. `transport` is stamped by the caller (F3); it is
|
|
39
|
+
* never inferred or hardcoded inside this module.
|
|
40
|
+
*/
|
|
41
|
+
export interface OpPlan {
|
|
42
|
+
/** The canonical (or mounted) operation descriptor this plan was built from. */
|
|
43
|
+
op: Operation;
|
|
44
|
+
/** The transport this plan was resolved FOR — stamped by the caller (F3). */
|
|
45
|
+
transport: Transport;
|
|
46
|
+
http: {
|
|
47
|
+
verb: HttpVerb;
|
|
48
|
+
route: string;
|
|
49
|
+
};
|
|
50
|
+
mcp: {
|
|
51
|
+
name: string;
|
|
52
|
+
};
|
|
53
|
+
cli: {
|
|
54
|
+
path: string[];
|
|
55
|
+
};
|
|
56
|
+
grpc: {
|
|
57
|
+
package: string;
|
|
58
|
+
service: string;
|
|
59
|
+
method: string;
|
|
60
|
+
};
|
|
61
|
+
/** Domain-param summary; `undefined` for a mount op (no composed schema exists). */
|
|
62
|
+
params?: ParamInfo[];
|
|
63
|
+
/** §9.1 envelope fields, one entry per non-`data` top-level input property. */
|
|
64
|
+
envelope: OpPlanEnvelopeField[];
|
|
65
|
+
/** The precomputed `--flag` table, keyed by argv flag name (no leading `--`). */
|
|
66
|
+
cliFlags: Map<string, OpPlanCliFlag>;
|
|
67
|
+
/** True when the underlying export is async-iterable/streaming (`Operation.streaming`). */
|
|
68
|
+
streaming: boolean;
|
|
69
|
+
/** True when `op` is a `MountedOperation` (a `--use` mount capability, not an extracted source op). */
|
|
70
|
+
isMount: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Present only when `isMount` is true — the mount's `handler`, normalised
|
|
73
|
+
* to always return a `Promise<LayerResult>` (its raw return type is
|
|
74
|
+
* `unknown | Promise<unknown> | AsyncIterable<Chunk>` — see
|
|
75
|
+
* `MountedOperation.handler`). Takes the core-client `Call` shape;
|
|
76
|
+
* `dispatchForPlan` is responsible for adapting the runtime `Call` into
|
|
77
|
+
* this shape and stamping `transport` from `plan.transport` (F3).
|
|
78
|
+
*/
|
|
79
|
+
mountHandler?: (call: CoreClientCall) => Promise<LayerResult>;
|
|
80
|
+
}
|
|
81
|
+
export interface BuildOpPlanInput {
|
|
82
|
+
/** The canonical operation descriptor, or a `MountedOperation` contributed by a `--use` mount plugin. */
|
|
83
|
+
op: Operation | MountedOperation;
|
|
84
|
+
/**
|
|
85
|
+
* The composed schema entry for `op`. Omitted for a mount op — mount ops
|
|
86
|
+
* are synthetic and never flow through `composeSchemas()`, so no entry
|
|
87
|
+
* exists for them in a package's `ComposedSchemas` map.
|
|
88
|
+
*/
|
|
89
|
+
schema?: ComposedSchemas[string];
|
|
90
|
+
/** The transport this plan is being resolved for (F3 — stamped by the caller, never inferred). */
|
|
91
|
+
transport: Transport;
|
|
92
|
+
/** Optional per-op projection overrides (`--opt http.verb.<id>=GET`, SPEC §5). */
|
|
93
|
+
projection?: ProjectionConfig;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Resolve `input.op` (+ its composed schema, when present) into a
|
|
97
|
+
* transport-complete `OpPlan`. Call this ONCE per op, at package/transport
|
|
98
|
+
* wiring time — never per request.
|
|
99
|
+
*/
|
|
100
|
+
export declare function buildOpPlan(input: BuildOpPlanInput): OpPlan;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ComposedSchemas } from './types';
|
|
2
|
+
import { Layer, InvokeFn } from './invoke';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A loaded `--use` plugin object (SPEC §7.1). We only depend on the shape we
|
|
6
|
+
* actually consume here (layer / mount capabilities) so the runtime stays
|
|
7
|
+
* decoupled from the full `Plugin` type surface.
|
|
8
|
+
*/
|
|
9
|
+
export interface UsePlugin {
|
|
10
|
+
id: string;
|
|
11
|
+
capabilities?: {
|
|
12
|
+
layer?: {
|
|
13
|
+
layer: (call: unknown, next: () => Promise<unknown>) => Promise<unknown> | AsyncIterable<unknown>;
|
|
14
|
+
};
|
|
15
|
+
mount?: {
|
|
16
|
+
operations: (descriptor: {
|
|
17
|
+
host: string;
|
|
18
|
+
operations: unknown[];
|
|
19
|
+
}, opts?: Record<string, unknown>) => Array<{
|
|
20
|
+
id: string;
|
|
21
|
+
transports?: string[];
|
|
22
|
+
handler: (call: unknown) => unknown;
|
|
23
|
+
}>;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Per-`--use` plugin option bag, keyed by plugin id (carried on `options.useOptions`). */
|
|
28
|
+
export type UseOptions = Record<string, Record<string, unknown>>;
|
|
29
|
+
/**
|
|
30
|
+
* Read the loaded `--use` plugins off `input.options`. The CLI loads the
|
|
31
|
+
* specifiers into real plugin objects and threads them here (`RunInput`
|
|
32
|
+
* carries no dedicated field, so they ride on `options.usePlugins`).
|
|
33
|
+
*/
|
|
34
|
+
export declare function readUsePlugins(options: Record<string, unknown>): UsePlugin[];
|
|
35
|
+
export declare function readUseOptions(options: Record<string, unknown>): UseOptions;
|
|
36
|
+
/**
|
|
37
|
+
* Adapt a core `LayerCapability.layer` (which receives the SPEC §7.1 `Call`
|
|
38
|
+
* shape with `.data`) into a runtime {@link Layer} (which threads the §8.1
|
|
39
|
+
* `Call` with `.domainArgs`). The runtime Call already carries `operation.id`,
|
|
40
|
+
* `envelope`, `ctx`, and `signal`; we additionally surface `.data` (an alias of
|
|
41
|
+
* `domainArgs`) so layers written against either shape see their fields.
|
|
42
|
+
*/
|
|
43
|
+
export declare function adaptCoreLayer(cap: NonNullable<NonNullable<UsePlugin['capabilities']>['layer']>): Layer;
|
|
44
|
+
/**
|
|
45
|
+
* Compose the request-path invoker for one package: the `--use` layer
|
|
46
|
+
* plugins (outermost-first, in declaration order) wrapping the central
|
|
47
|
+
* validate-Layer (innermost, immediately before dispatch — BUG-APIGEN-009).
|
|
48
|
+
* Built ONCE per package, not per request.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createPackageInvoker(schemas: ComposedSchemas, usePlugins: UsePlugin[]): InvokeFn;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { OpPlan } from './op-plan';
|
|
2
|
+
import { Call, LayerResult } from './invoke';
|
|
3
|
+
|
|
4
|
+
export interface TransportAdapter<Raw = unknown> {
|
|
5
|
+
/**
|
|
6
|
+
* Register `plan`'s projected route/tool/command/method with this
|
|
7
|
+
* transport's underlying server/CLI/router, wiring inbound requests to
|
|
8
|
+
* `dispatch`. Called ONCE per `OpPlan`, at server/CLI wiring time.
|
|
9
|
+
*/
|
|
10
|
+
registerRoute(plan: OpPlan, dispatch: (call: Omit<Call, 'operation' | 'ctx'>) => Promise<LayerResult>): void;
|
|
11
|
+
/**
|
|
12
|
+
* Marshal the transport-native inbound request `raw` into the runtime
|
|
13
|
+
* `Call` shape (minus `operation`/`ctx`, filled in by the dispatch path).
|
|
14
|
+
*/
|
|
15
|
+
readCall(raw: Raw, plan: OpPlan): Omit<Call, 'operation' | 'ctx'> | Promise<Omit<Call, 'operation' | 'ctx'>>;
|
|
16
|
+
/**
|
|
17
|
+
* Marshal a resolved `LayerResult` (the value `dispatch`'s returned
|
|
18
|
+
* Promise resolved to — F1) back onto the transport-native outbound
|
|
19
|
+
* carrier `raw`.
|
|
20
|
+
*/
|
|
21
|
+
writeResult(raw: Raw, result: LayerResult, plan: OpPlan): void | Promise<void>;
|
|
22
|
+
/** Marshal a thrown error back onto the transport-native outbound carrier `raw`. */
|
|
23
|
+
writeError(raw: Raw, err: unknown, plan: OpPlan): void | Promise<void>;
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhd/apigen-engine-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"ajv": "^8.20.0",
|
|
6
6
|
"ajv-formats": "2.1.1",
|
|
7
7
|
"pino": "10.3.1",
|
|
8
|
-
"@adhd/apigen-core-client": "^0.1.
|
|
9
|
-
"@adhd/apigen-base-logical": "^0.0.
|
|
10
|
-
"@adhd/apigen-base-errors": "^0.1.
|
|
8
|
+
"@adhd/apigen-core-client": "^0.1.4",
|
|
9
|
+
"@adhd/apigen-base-logical": "^0.0.5",
|
|
10
|
+
"@adhd/apigen-base-errors": "^0.1.5",
|
|
11
|
+
"@adhd/apigen-engine-naming": "^0.1.4",
|
|
11
12
|
"better-sqlite3": "12.10.0"
|
|
12
13
|
},
|
|
13
14
|
"main": "./index.js",
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ChildProcess } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One golden-fixture case. `name` is the stable key under which its result
|
|
5
|
+
* lands in the snapshot — it must be unique within a fixture list so a
|
|
6
|
+
* snapshot round-trips through `JSON.stringify`/parse without collisions.
|
|
7
|
+
*
|
|
8
|
+
* The proposal's fixture classes (safe/scalar GET-hoist, unsafe/mutating
|
|
9
|
+
* scalar, session/envelope, `streaming:true`, `--use` mount, plus one
|
|
10
|
+
* validation-failure and one domain `ApiError` per class) are just
|
|
11
|
+
* `GoldenFixture` values a transport migration's own spec constructs; this
|
|
12
|
+
* harness has no opinion on fixture shape beyond "has a stable name".
|
|
13
|
+
*/
|
|
14
|
+
export interface GoldenFixture<TInput = unknown> {
|
|
15
|
+
/** Stable snapshot key. Must be unique within one fixture list. */
|
|
16
|
+
readonly name: string;
|
|
17
|
+
/** Whatever the driver needs to make the call (request shape is driver-defined). */
|
|
18
|
+
readonly input: TInput;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Drives one fixture through a REAL consumer protocol
|
|
22
|
+
* ([def:real-consumer-protocol]) and returns whatever that protocol yielded
|
|
23
|
+
* (parsed JSON body, tool-call result, stdout+exit-code, …). Implementations
|
|
24
|
+
* live in each transport's own spec — this harness only orchestrates calls
|
|
25
|
+
* through the interface, never reaches into transport/plugin internals.
|
|
26
|
+
*/
|
|
27
|
+
export interface ParityDriver<TInput = unknown, TOutput = unknown> {
|
|
28
|
+
invoke(fixture: GoldenFixture<TInput>): Promise<TOutput>;
|
|
29
|
+
}
|
|
30
|
+
/** A captured (or committed) golden snapshot: fixture name -> captured result. */
|
|
31
|
+
export type GoldenSnapshot<TOutput = unknown> = Record<string, TOutput>;
|
|
32
|
+
/**
|
|
33
|
+
* Drives `driver` over every fixture, in order, and records each result
|
|
34
|
+
* keyed by `fixture.name`. Used BOTH to capture the pre-migration golden
|
|
35
|
+
* snapshot (commit the result) and to re-capture post-migration (feed the
|
|
36
|
+
* result to {@link assertParity}).
|
|
37
|
+
*
|
|
38
|
+
* Fails loudly — never silently — on the two shapes that would make a
|
|
39
|
+
* "parity" claim meaningless: an empty fixture list (nothing was proven) and
|
|
40
|
+
* a duplicate fixture name (a later result would silently clobber an
|
|
41
|
+
* earlier one in the snapshot object).
|
|
42
|
+
*/
|
|
43
|
+
export declare function captureGolden<TInput = unknown, TOutput = unknown>(driver: ParityDriver<TInput, TOutput>, fixtures: readonly GoldenFixture<TInput>[]): Promise<GoldenSnapshot<TOutput>>;
|
|
44
|
+
/** Thrown by {@link assertParity} when a recapture diverges from the committed snapshot. */
|
|
45
|
+
export declare class ParityMismatchError extends Error {
|
|
46
|
+
/** Fixture names that diverged (missing, unexpectedly added, or value-mismatched). */
|
|
47
|
+
readonly fixtureNames: readonly string[];
|
|
48
|
+
constructor(message: string, fixtureNames: readonly string[]);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Asserts `recapture` deep-equals `committed`, fixture-by-fixture
|
|
52
|
+
* ([inv:byte-identical]). Uses `node:util`'s `isDeepStrictEqual` — strict
|
|
53
|
+
* structural equality (type-sensitive, own-enumerable-key-sensitive: e.g.
|
|
54
|
+
* `{a: undefined}` is NOT equal to `{}`) so a silent `undefined`⇄absent or
|
|
55
|
+
* `"1"`⇄`1` drift is caught, not coerced away.
|
|
56
|
+
*
|
|
57
|
+
* Reports every divergence in one error (missing fixtures, unexpectedly
|
|
58
|
+
* added fixtures, and per-fixture value mismatches) rather than failing on
|
|
59
|
+
* the first one, so a broken migration's full blast radius is visible from
|
|
60
|
+
* a single test failure.
|
|
61
|
+
*/
|
|
62
|
+
export declare function assertParity<TOutput = unknown>(committed: GoldenSnapshot<TOutput>, recapture: GoldenSnapshot<TOutput>): void;
|
|
63
|
+
/** Options for {@link proveNegativeControl}. */
|
|
64
|
+
export interface NegativeControlOptions {
|
|
65
|
+
/**
|
|
66
|
+
* Working directory `git apply` runs in — the repository root the
|
|
67
|
+
* committed `patchPath` is expressed relative to. Defaults to
|
|
68
|
+
* `process.cwd()`.
|
|
69
|
+
*/
|
|
70
|
+
cwd?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Proves a parity suite is an actual gate, not dead weight
|
|
74
|
+
* ([inv:negative-control], AGENTS.md §7 pt 2): applies `patchPath` (a
|
|
75
|
+
* one-line regression, `git apply`), asserts `runner()` now REJECTS (goes
|
|
76
|
+
* RED), reverts the patch (`git apply -R`) even if the RED assertion itself
|
|
77
|
+
* fails, then asserts `runner()` resolves again (goes GREEN).
|
|
78
|
+
*
|
|
79
|
+
* `runner` is whatever re-proves parity for the migration under test —
|
|
80
|
+
* typically "recapture via the driver, then `assertParity` against the
|
|
81
|
+
* committed snapshot". It must REJECT (throw / return a rejected promise) to
|
|
82
|
+
* signal RED and resolve to signal GREEN.
|
|
83
|
+
*
|
|
84
|
+
* Throws if the patch fails to turn the suite RED (the gate would not have
|
|
85
|
+
* caught the regression) — this is the one case `proveNegativeControl`
|
|
86
|
+
* itself must fail loudly on, independent of whatever `runner` does. Repo
|
|
87
|
+
* rule: never `git stash` / `git reset --hard` — patch apply/revert is the
|
|
88
|
+
* only mutation performed, and it is always undone in a `finally`.
|
|
89
|
+
*/
|
|
90
|
+
export declare function proveNegativeControl(runner: () => void | Promise<void>, patchPath: string, opts?: NegativeControlOptions): Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* Bulletproof teardown for a spawned test server (`flask_server.py`,
|
|
93
|
+
* `grpc_server.py`, …): sends `SIGTERM`, waits for the process's real `exit`
|
|
94
|
+
* event, and escalates to `SIGKILL` if it hasn't exited within
|
|
95
|
+
* `gracefulTimeoutMs` (BUG-APIGEN-TEST-SUBPROCESS-TEARDOWN-LEAK-001).
|
|
96
|
+
*
|
|
97
|
+
* The bug this fixes: a bare `proc.kill('SIGTERM')` racing a `setTimeout`
|
|
98
|
+
* that resolves regardless of whether the process actually died leaves an
|
|
99
|
+
* unresponsive Python process running — it binds a fixed test port and
|
|
100
|
+
* survives to answer (with stale/pre-migration behavior) a later, unrelated
|
|
101
|
+
* test suite that happens to reuse the same port. Resolving here ONLY on the
|
|
102
|
+
* real `exit` event (never on a bare timer) plus the `SIGKILL` escalation
|
|
103
|
+
* guarantees the process is gone, and gone within a bounded deadline, before
|
|
104
|
+
* the caller proceeds.
|
|
105
|
+
*
|
|
106
|
+
* No-op if the process has already exited.
|
|
107
|
+
*/
|
|
108
|
+
export declare function killChildProcess(proc: ChildProcess, gracefulTimeoutMs?: number): Promise<void>;
|