@adhd/apigen-cli 0.1.0 → 0.1.1

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 CHANGED
@@ -1,89 +1,906 @@
1
- # Changelog — `@adhd/apigen-*`
1
+ # Changelog — @adhd/apigen-cli
2
2
 
3
- All notable changes to the apigen toolchain (`@adhd/apigen-cli`, `-core`, `-runtime`,
4
- `-logical`, plugins, and generators). Format based on
5
- [Keep a Changelog](https://keepachangelog.com/); this project uses
6
- [Semantic Versioning](https://semver.org/).
3
+ All notable changes to this project are documented here.
7
4
 
8
- Open/actionable work lives in [`/BACKLOG.md`](../../BACKLOG.md); plan-level design in
9
- [`docs/plan/apigen-logical-types/DESIGN.md`](../../docs/plan/apigen-logical-types/DESIGN.md).
5
+ ## Unreleased
10
6
 
11
- ---
7
+ ### Fixed
12
8
 
13
- ## [Unreleased]apigen-logical-types (PENDING)
9
+ - **BUG-APIGEN-029**`$ref` resolution / Ajv strict-mode failures on complex
10
+ external or self-referential types (e.g. `better-sqlite3.Database`,
11
+ `WriteParams`) at DISPATCH time, not extraction time. Real production
12
+ repro (`~/dev/ai/sox-ecosystem/libs/memory-core`, read-only reference):
13
+ several routes 500'd — `/memory/write` →
14
+ `{"code":"internal","message":"can't resolve reference
15
+ #/definitions/WriteParams from id #"}`; the `BetterSqlite3.Database`-taking
16
+ routes → the identical error class for `#/definitions/BetterSqlite3.Database`.
17
+ Confirmed identical under both v1 and v2 (pre-existing, not a
18
+ v1-retirement regression).
14
19
 
15
- > Cross-language serialization of non-JSON-native types and custom classes. Tracked by the
16
- > `apigen-logical-types` plan-state-machine plan (DESIGN.md §1–§18). **Not yet released** —
17
- > gated on the plan reaching its Definition of Done + publishing the apigen v2 packages.
20
+ **Root cause, confirmed against current source (not by inference traced
21
+ empirically with temporary instrumentation, reverted before landing):**
22
+ BUG-APIGEN-026 fixed the common case (a named type used as a top-level
23
+ param) via `topRef: false`, but its own doc comment flagged an unresolved
24
+ follow-up: `topRef` only controls whether the ENTRY type is `$ref`-wrapped,
25
+ not a type it recursively/self-referentially contains, which MUST stay a
26
+ `$ref` + `definitions` entry (JSON Schema has no other way to express a
27
+ cycle). `buildSchema()`'s ancestors/deadlock guard (BUG-APIGEN-CORE-003)
28
+ keys on the literal type-text STRING, but the same TS type is frequently
29
+ reported by ts-morph as a DIFFERENT string depending on calling context (a
30
+ fully-qualified `import("path").Foo` for the top-level param vs. bare `Foo`
31
+ for the same type reached again via one of its own properties) — that
32
+ mismatch defeats the guard, lets `ts-json-schema-generator` resolve the
33
+ same type a second time independently at the nested property position, and
34
+ produces a self-contained `$ref` + `definitions` pair several levels deep
35
+ inside the outer param fragment. `extract.ts`'s `buildActionOpAtPath`
36
+ spliced that fragment straight into `properties[p.name]` at whatever depth
37
+ its `definitions` actually landed; `composeSchemas()` never carried any
38
+ nested `definitions`/`$defs` forward onto the final composed document
39
+ either. Since JSON-Schema `$ref` resolution is always root-relative to the
40
+ document Ajv compiles, the `$ref` permanently dangled once
41
+ `validate-layer.ts`'s `ajv.compile(schema.input)` ran — a genuinely
42
+ distinct mechanism from BUG-APIGEN-030 (that one was Ajv strict-mode
43
+ rejecting unregistered `x-apigen-*`/`discriminator` keywords; this one is a
44
+ structural `$ref`/`definitions` placement gap with no keyword involved).
18
45
 
19
- ### Added
20
- - **Rich types now round-trip through `run`/`generate` with no new flags.** Functions whose
21
- signatures use **`Date`, `int64`/`BigInt`, `Decimal`, `bytes`, `UUID`, `Map`, `Set`,
22
- tuples**, or **user-defined classes** are exposed and (de)serialized correctly across
23
- MCP / HTTP / CLI. Previously these silently broke (e.g. `Date` extracted as `{}`, inputs
24
- never rehydrated). *(dod.1/dod.2)*
25
- - **Custom-class round-trips, including cross-host.** A `User` class survives
26
- `TS wire Python wire → TS` and is reconstructed as a **real instance on both hosts**
27
- (not a prototype-stripped object). Discriminated unions (`Dog | Cat`) dispatch by wire
28
- discriminator. *(dod.3/dod.4)*
29
- - **Meaningful MCP/OpenAPI schemas.** `tools/list` and the OpenAPI emitter now carry proper
30
- `format` (`date-time`, `int64`, `uuid`, `byte`, …) and named `$ref` for classes instead of
31
- empty `{}` so MCP clients (and the driving model) and generated OpenAPI clients in any
32
- language get real, validatable types.
33
- - **Idiomatic generated clients.** Generated TS clients emit `Date`, Python clients emit
34
- `datetime` (via `x-apigen-*` codegen hints) consumers get typed values, not raw ISO
35
- strings to hand-parse.
36
- - **Cross-host interop for rich types.** Expose a TS function, call it from a Python client
37
- (or vice versa), with Date/Decimal/classes preserved — guaranteed **byte-equal** by a
38
- conformance-vector matrix (a TCK every host must pass). *(dod.5)*
39
- - **New developer tooling:** an `apigen-nx` **`host` generator** to scaffold a new host
40
- language (a template column + conformance harness), and a workspace
41
- **`nx run …:conformance`** target that verifies the cross-host matrix.
42
- - **New package `@adhd/apigen-logical`** the single source of truth: canonical wire table +
43
- per-language native-hook template columns + generate-time emitter + run-mode closure builder.
44
- - **Optional knob `--opt logical.decimal=branded|lib`** zero-dependency branded-string mode
45
- vs a real `Decimal` runtime type.
46
+ **Fix (`packages/apigen/apigen-core-client/src/lib/extract.ts:524-603`,
47
+ `hoistNestedDefs`):** recursively walks the ENTIRE per-param fragment tree
48
+ (not just its own top level the same generic object/array walk style
49
+ `findDanglingRefs` already uses in `ts-json-schema.ts`), hoisting every
50
+ `definitions`/`$defs` dict found at ANY depth up to the function-level
51
+ `input` schema's own root (`extract.ts:667,694-696`). Throws a clear,
52
+ generate-time error on a genuine same-name/different-content definition
53
+ collision across params, rather than silently corrupting one.
54
+ `composeSchemas()` (`apigen-core-client/src/lib/compose-schemas.ts:193-197,
55
+ 216-217`) now carries that root-level `definitions`/`$defs` forward onto
56
+ the final composed `input` document, where `ajv.compile()` actually
57
+ resolves `$ref`s against. `validateComposedRefs`'s pre-existing
58
+ BUG-APIGEN-CORE-001 generate-time safety net (`compose-schemas.ts:21-49`)
59
+ was ALSO silently dead code for every real schema before this fix — it
60
+ collected only a `$defs` key (never `definitions`, the key
61
+ `ts-json-schema-generator` actually emits) and keyed its lookup by bare
62
+ definition name where `validateSchemaRefs` (apigen-base-logical) requires
63
+ the full ref-URI string fixed to collect both key spellings and to
64
+ detect already-qualified `#/...`-prefixed keys (an existing hand-built test
65
+ convention) vs. bare names needing the `#/<defKey>/` prefix, so it no
66
+ longer double-prefixes or silently no-ops.
67
+
68
+ **Critical follow-on bug caught by the regression tests themselves (not
69
+ discovered by inspection):** an earlier version of `hoistNestedDefs`
70
+ mutated the per-param fragment tree in place (`delete node.definitions`).
71
+ `buildSchema()`'s results are memoized by reference in a
72
+ session/process-persistent cache (BUG-APIGEN-CORE-003); a NESTED object
73
+ inside a per-param fragment (reached via `properties`, not the top-level
74
+ object the caller already shallow-clones) is the exact cached object, not a
75
+ copy. Deleting `definitions` from it in place permanently stripped the
76
+ cached entry, so the FIRST extraction in a process correctly hoisted and
77
+ passed, but every SUBSEQUENT `extract()` call for the same type in the same
78
+ process (a later test in the same file, or a real watch/serve rebuild) got
79
+ back the now-`definitions`-less cached fragment and reintroduced the exact
80
+ dangling-`$ref` bug — verified live: two BUG-APIGEN-029 regression tests
81
+ run back-to-back reproduced `"can't resolve reference
82
+ #/definitions/SelfRefParams from id #"` on the SECOND test only, while
83
+ either one alone always passed. Fixed by making `hoistNestedDefs` a pure
84
+ clone-and-hoist function that never mutates its input — it returns a
85
+ freshly-built fragment tree with `definitions`/`$defs` removed from
86
+ wherever found, touching only its own new output and the caller-owned
87
+ accumulator, never the cache's objects.
88
+
89
+ **Regression tests**
90
+ (`packages/apigen/apigen-engine-runtime/src/test/bug-apigen-029.spec.ts`,
91
+ fixture `src/test/fixtures/bug-apigen-029.ts`), real `extract()` →
92
+ `composeSchemas()` → real Ajv `validateLayer` pipeline, matching the
93
+ established `bug-apigen-030.spec.ts`/`named-type-param.spec.ts` pattern:
94
+ a self-referential local interface (`SelfRefParams`, deterministically
95
+ reproduces the nested-`definitions` mechanism in any environment) hoists
96
+ its `definitions` to the schema root and dispatches correctly; a real
97
+ complex external type (`better-sqlite3`'s `BetterSqlite3.Database`)
98
+ dispatches correctly end-to-end (the actual reported BACKLOG symptom — in
99
+ this no-tsconfig test environment `Database` happens to resolve to a flat,
100
+ cycle-free schema since `ts-json-schema-generator`'s `functions:"comment"`
101
+ default strips its chainable methods, so this case doesn't exercise the
102
+ identical internal mechanism `SelfRefParams` does, but does prove the real
103
+ reported route dispatches); and a negative control confirming
104
+ `validateComposedRefs` now actually throws on a genuinely dangling `$ref`
105
+ instead of silently no-op-ing. All 6 cases verified to fail pre-fix and
106
+ pass post-fix; also verified stable across 10 consecutive full-suite runs
107
+ (the in-place-mutation bug above was order-dependent and required a
108
+ multi-run/full-suite check to catch, not just the new spec file in
109
+ isolation).
110
+
111
+ Also updated `descriptor.ts`'s `JSONSchema` type
112
+ (`apigen-core-client/src/lib/descriptor.ts`) to document the `definitions`
113
+ field alongside the already-declared `$defs` — the actual key
114
+ `ts-json-schema-generator` emits, now load-bearing via this fix, previously
115
+ undocumented on the type.
116
+
117
+ **Verified:** `apigen-core-client` 267/267 (11 files); `apigen-engine-runtime`
118
+ 157/157 (17 files, including the new spec) — stable across 10 consecutive
119
+ full-suite runs; downstream `ComposedSchemas` consumers
120
+ `apigen-plugin-api-express` 36/36, `apigen-plugin-api-fastify` 48/48,
121
+ `apigen-plugin-mcp` 47/47, `apigen-plugin-jsonschema` 8/8,
122
+ `apigen-plugin-cli-output` 34/34; `apigen-base-logical` 186/186;
123
+ `entrypoint/apigen-cli` 150/150 (18 files) — zero regressions anywhere.
124
+
125
+ - **BUG-APIGEN-034** — `--export <mode>` silently stopped scoping the served/generated
126
+ route surface post-v1-retirement, an undocumented behavior change on an existing
127
+ public CLI flag. Pre-diff, Step 5 of `buildDescriptor()` called v1's
128
+ `generateSchemas({ exportMode })`, so the actual schema/route surface served was
129
+ correctly scoped to the requested export shape. Post-diff, Step 5 derived directly
130
+ from the unscoped `operations` array, so the FULL export matrix was published as
131
+ routes/schemas regardless of `--export` — e.g. a source file with private
132
+ named-export helpers alongside an intentionally `--export default`-scoped public
133
+ object would leak the private helpers as served routes.
134
+
135
+ **Decision: restore the scoping** (`orchestrator.ts`) rather than deprecate the flag.
136
+ Since v2's `extract()` walks the full export-shape matrix in one unconditional pass
137
+ (unlike v1's three mutually-exclusive extractors), there's no way to re-run "only
138
+ this shape" extraction — instead, `opMatchesExportMode()` (new, `orchestrator.ts`)
139
+ reclassifies each already-extracted `Operation` by its `path` shape (`path.length`
140
+ and whether `path[1].raw === 'default'`/a named-object name) and `buildDescriptor()`'s
141
+ Step 5 filters on it per source, before composing `packageSchemas`
142
+ (`orchestrator.ts:484-492`). Scoping applies ONLY to what's served —
143
+ `descriptor.operations` (collision-check, `--use` mount plugins via
144
+ `RunInput.operations`) stays fully unscoped, matching v1's own behavior where the
145
+ earlier collision-check step never respected `exportMode` either.
146
+
147
+ Known residual limitation (documented in `opMatchesExportMode()`'s doc comment,
148
+ not silently papered over): v2 shape 4's bare `export default function foo(){}`
149
+ form and v2 shape 6 (CJS `module.exports` properties) produce the identical
150
+ `[file, name]` path shape as a plain named export, and are therefore
151
+ indistinguishable from one under `'named'` mode without an explicit export-shape
152
+ discriminator on `Operation` — a schema change to the shared, host-agnostic
153
+ descriptor deliberately left out of scope for this fix (extract.ts was being
154
+ concurrently edited by another fix in the same branch). This is a strict superset
155
+ of v1's true `extractNamed` coverage, never a subset: v1 never supported CJS or a
156
+ bare default-fn-decl under ANY `--export` mode either (see extract.ts's shape 4/6
157
+ header comments), so nothing v1 used to serve goes missing under the restored
158
+ scoping.
159
+
160
+ 6 new regression tests (`orchestrator.spec.ts`, `BUG-APIGEN-034` describe block):
161
+ `opMatchesExportMode()` unit tests for all three modes, plus real-pipeline
162
+ `buildDescriptor()` integration tests against a new fixture
163
+ (`export-mode-scoping.ts`, a private named helper alongside a
164
+ `--export default`-scoped object) proving `--export default` includes only the
165
+ default object's properties, omitted `--export` (named mode) includes only the
166
+ named helper, and an absent `exportMode` (the `generate-registry`/`run-registry`
167
+ path) applies no scoping at all — unaffected by this fix.
168
+
169
+ - **BUG-APIGEN-035** — `buildDescriptor()`'s namespace-keyed `groups` Map (Step 5)
170
+ used plain `Map.set()`, silently last-source-wins on a namespace collision across
171
+ sources — no error, no warning, and undocumented as a caller invariant on
172
+ `SourceEntry`/`OrchestratorOptions`. Not reachable today (registry namespaces come
173
+ from a single `fs.readdirSync`, inherently unique; `generate`/`run` only ever pass
174
+ one source), but a latent trap for any future multi-source caller (e.g. the SPEC
175
+ §13 polyglot-host `serve` extension). Fixed by throwing a clear
176
+ `duplicate namespace "<ns>" — ...` error in `buildDescriptor()`
177
+ (`orchestrator.ts:465-482`) the moment a second source resolves to an
178
+ already-seeded namespace, mirroring the pre-existing duplicate-namespace guard in
179
+ `serve.ts`'s `resolveHosts()` (`serve.ts:164-171`) for consistency across the CLI.
180
+
181
+ 2 new regression tests (`orchestrator.spec.ts`, `BUG-APIGEN-035` describe block):
182
+ a synthetic two-source scenario with a deliberate namespace collision now throws
183
+ `/duplicate namespace "dup"/` instead of silently dropping one source's operations,
184
+ plus a negative control proving distinct namespaces still merge cleanly.
185
+
186
+ Related, separate ordering nitpick fixed in the same pass:
187
+ `run-registry.ts` registered its `SIGINT`/`SIGTERM` handlers BEFORE the
188
+ `sources.length === 0` early-return, so on the "nothing to run" path they were
189
+ registered for a run that never started (harmless, but inconsistent with
190
+ `generate-registry.ts`'s equivalent check, which has no such handlers to begin
191
+ with). Reordered so the early-return happens first.
192
+
193
+ `npx vitest run --root entrypoint/apigen-cli`: 17 files, 147/147 passed — zero
194
+ regressions.
195
+
196
+ - **BUG-APIGEN-033** — anonymous default-export functions (`export default (n) => ...` /
197
+ `export default function(n){...}`) were advertised as routes/tools but crashed at
198
+ dispatch time ("function not found"), even though they were correctly listed in the
199
+ served schema. Root cause: `extract.ts` synthesized a filename-derived operation name
200
+ for both anonymous default-export shapes (Shape 5, arrow/`FunctionExpression`; and
201
+ Shape 4's anonymous `FunctionDeclaration` sub-case) — e.g. `foo_default` — and that
202
+ name became the route/tool/schema key. But `apigen-engine-runtime/src/lib/fn-table.ts`'s
203
+ `buildFnTable()`, which builds the `name → fn` table dispatch actually indexes into,
204
+ keys every function by its live JS `.name` property, never by any apigen-synthesized
205
+ name. ECMAScript's `export default AssignmentExpression`/`HoistableDeclaration`
206
+ NamedEvaluation rule gives BOTH anonymous shapes a real runtime `.name` of `"default"`
207
+ (the language's own behavior, not an apigen quirk) — so `buildFnTable()` always produced
208
+ `fns['default']`, never `fns['foo_default']`. At dispatch time `fns[operationName]` was
209
+ `undefined` → crash.
210
+
211
+ **Fix:** name both anonymous shapes' operations using the literal `'default'` — the
212
+ exact runtime `.name` `buildFnTable()` will always resolve — instead of a filename-derived
213
+ synthetic id (`extract.ts`'s Shape 5 arrow/`FunctionExpression` branch and Shape 4's
214
+ anonymous `FunctionDeclaration` branch, both now call `buildActionOp(..., fileSegment,
215
+ 'default', ...)`, mirroring exactly how Shape 4's *named* default function is already
216
+ handled). `fileSegment` still carries the per-file id/route disambiguation
217
+ (`shapes/anonymous-default/default`) — only the dispatch key (the leaf path segment)
218
+ changes. No changes needed to `fn-table.ts`, `compose-schemas.ts`, `dispatch.ts`, or
219
+ `orchestrator.ts`'s Step 5 key derivation (`op.path[op.path.length - 1].raw`) — this was
220
+ a pure naming fix in `extract.ts`, matching an invariant `orchestrator.ts`'s own Step 5
221
+ comment already assumed held for this shape.
222
+
223
+ Regression coverage: `extract.spec.ts` (Shape 5, existing) and
224
+ `export-shape-matrix.spec.ts` (Shape 5, updated to assert `'default'` instead of the old
225
+ synthesized name) prove the static op naming. A new end-to-end test,
226
+ `entrypoint/apigen-cli/src/test/e2e/bug-apigen-033-anonymous-default-dispatch.spec.ts`,
227
+ spawns the BUILT CLI bin as a real `node` child process and drives it with a real
228
+ `@modelcontextprotocol/sdk` client (`tools/list` + `tools/call`) against a new fixture
229
+ covering the previously-uncovered anonymous-`FunctionDeclaration` sub-case
230
+ (`anonymous-default-fn-decl.ts`) plus the existing arrow-form fixture
231
+ (`anonymous-default.ts`), proving both are listed under the name `'default'` AND
232
+ actually dispatch to the correct result — not just present in the schema — with a
233
+ negative control proving a named default export (Shape 4, `default-fn.ts`) is
234
+ unaffected. (This must run against the built bin, not an in-process Vitest `import()` —
235
+ Vitest's own SSR transform resolves a dynamically-imported fixture differently than the
236
+ `tsx`-based loader `run.ts` actually uses at runtime, which is exactly the mechanism this
237
+ bug lived in; see `import-source-cjs-format.spec.ts`'s header comment for the same
238
+ established precedent.) All 3 cases pass against the freshly-built bin (`npx vite build
239
+ --config entrypoint/apigen-cli/vite.config.ts`, `npx vitest run --root
240
+ entrypoint/apigen-cli entrypoint/apigen-cli/src/test/e2e/bug-apigen-033-anonymous-default-dispatch.spec.ts`
241
+ → 3/3), confirmed by the bin's own log lines showing `tools: ["default"]` and a
242
+ successful `→ default` dispatch for both anonymous fixtures pre-fix would instead show
243
+ `fns['foo_default']`/equivalent undefined and throw at the `tools/call`. Additionally
244
+ independently verified via a direct `tsx`-based probe against both real fixtures
245
+ (mirroring `import-source.ts`'s exact loader) that `buildFnTable()` resolves
246
+ `fns['default']` to the correct callable, returning the correct result.
247
+
248
+ `npx vitest run --root packages/apigen/apigen-core-client`: 266/267 passed (1 failure
249
+ in `compose-schemas.spec.ts`, confirmed via `git diff` to be caused by concurrent,
250
+ not-yet-committed BUG-APIGEN-029 work touching `compose-schemas.ts`/`descriptor.ts` —
251
+ neither file touched by this fix). `npx vitest run --root
252
+ packages/apigen/apigen-engine-runtime`: 151/151 passed at the time this fix's changes
253
+ were verified in isolation — `dispatch.spec.ts` (14/14) unaffected; a concurrent,
254
+ untracked, not-yet-committed BUG-APIGEN-029 test file
255
+ (`src/test/bug-apigen-029.spec.ts`, not part of this commit) appeared mid-session and
256
+ is excluded from this count since it belongs to that other in-flight fix.
257
+
258
+ Known follow-up (filed, not fixed here — see BUG-APIGEN-039): this fix moves both
259
+ anonymous shapes into the same `[file, name]` 2-segment `op.path` bucket
260
+ `opMatchesExportMode()` (BUG-APIGEN-034) already can't fully disambiguate from a plain
261
+ named export — the identical class of gap BUG-APIGEN-034 already documented and
262
+ deferred for Shape 4's *named* default case, now with a wider blast radius.
263
+
264
+ - **BUG-APIGEN-024** — `--use openapi` mount produced an empty OpenAPI doc (`paths: {}`)
265
+ on live `run`, even though the underlying operations were correctly extracted and
266
+ served. Root cause: `collectMountRoutes()` — identical in both HTTP transports
267
+ (`apigen-plugin-api-express/src/lib/run.ts`, `apigen-plugin-api-fastify/src/lib/run.ts`)
268
+ — built the synthetic `Descriptor` handed to `--use` mount plugins with `operations`
269
+ hardcoded to `[]`, so `apigen-plugin-openapi`'s `toOpenApi(descriptor.operations, ...)`
270
+ always received an empty array regardless of what was actually extracted (confirmed
271
+ independently: `apigen-plugin-openapi`'s own test suite proves `toOpenApi` produces
272
+ correct `paths` given a real descriptor). Fixed by threading the real merged
273
+ `Operation[]` (already computed by `buildDescriptor()`) through `RunInput.operations`
274
+ (new optional field, `apigen-core-client/src/lib/types.ts`), populated by
275
+ `orchestrator.ts`'s `orchestrateRun()`, and consumed by `collectMountRoutes()` in both
276
+ transports instead of the hardcoded `[]` stub (`input.operations ?? []` — the `?? []`
277
+ fallback keeps non-TS-extraction run paths, e.g. py-flask, safe since they have nothing
278
+ extracted to describe). Scoped to the `run` (live server) path per the bug title; the
279
+ BACKLOG entry's separately-noted gap that `generate.ts` never calls
280
+ `collectMountRoutes` at all (mount plugins are a live-server-only concept today) is
281
+ unchanged, pre-existing behavior, not part of this fix.
282
+
283
+ 9 new regression tests added per HTTP transport (`apigen-plugin-api-express` and
284
+ `apigen-plugin-api-fastify` `plugin.spec.ts`, `[BUG-APIGEN-024]` describe blocks): a
285
+ real `run()` server mounted with the REAL `@adhd/apigen-plugin-openapi` plugin (not a
286
+ stub) against two real multi-route operations, asserting `paths` contains both real
287
+ routes with the correct HTTP method/schema per path (safe→GET, unsafe→POST with
288
+ requestBody), plus a regression control proving `RunInput.operations` omitted still
289
+ safely falls back to empty `paths` rather than crashing. `nx test
290
+ apigen-plugin-api-express` 25/25, `nx test apigen-plugin-api-fastify` 37/37 (2 files),
291
+ `nx run-many -t test -p apigen-core-client apigen-plugin-openapi apigen-cli
292
+ apigen-engine-runtime` 114/114 — zero regressions.
293
+
294
+ - **FEAT-APIGEN-022 / BUG-APIGEN-025** — apigen now auto-hoists a function to `GET`
295
+ when its domain params are ALL "properly typed primitives" (`string`/`number`/
296
+ `boolean`/`integer`, or zero params) — no `--opt http.verb.<id>=GET` override
297
+ needed — and `x-apigen-safe` (which `httpVerb()` already read but nothing ever
298
+ wrote) is now actually stamped by `composeSchemas()`, so the mechanism it
299
+ partially relied on is live end-to-end.
300
+
301
+ **Design:** a new shared helper, `isPrimitiveOnlyInputSchema()`
302
+ (`apigen-core-client/src/lib/get-safety.ts`), inspects a bare domain-input JSON
303
+ Schema (`{type:'object', properties, required}` — the exact shape of both
304
+ `Operation.input` and `GeneratedSchemas.schemas[fn].input`) and returns `true`
305
+ iff every property is `string`/`number`/`boolean`/`integer` (optionally unioned
306
+ with `null`), a primitive-literal `enum`, or there are zero properties at all
307
+ (vacuously true — covers zero-arg functions). `$ref`, `oneOf`/`anyOf`/`allOf`,
308
+ `array`, and `object` properties are excluded. `composeSchemas()`
309
+ (`apigen-core-client/src/lib/compose-schemas.ts`) computes `op.safe ===
310
+ true || isPrimitiveOnlyInputSchema(fnSchema.input)` per function and stamps it
311
+ as `'x-apigen-safe'` on the `ComposedSchemas` entry (BUG-APIGEN-025's fix:
312
+ `op.safe` is threaded from `Operation.safe` through a new `safe?: boolean`
313
+ field on `GeneratedSchemas.schemas[fn]`, populated by
314
+ `orchestrator.ts`'s `buildDescriptor()` Step 5 — previously computed but never
315
+ carried this far, so the field `httpVerb()` read was always `undefined`).
316
+ `apigen-engine-naming`'s `project()` applies the identical OR-condition
317
+ directly against `op.input` for non-`ComposedSchemas` consumers (gRPC/openapi/
318
+ MCP paths). The four previously-duplicated `httpVerb()` definitions
319
+ (`apigen-plugin-api-express`'s `run.ts`/`generate.ts`, `apigen-plugin-api-
320
+ fastify`'s `run.ts`/`generate.ts`) are now ONE definition, exported from
321
+ `apigen-engine-naming`, imported by all four call sites — the manual
322
+ `--opt http.verb.<id>=GET`/`POST` override is checked first there and always
323
+ wins over the auto-detected value, in both directions.
324
+
325
+ **Also fixed as a direct consequence:** GET requests were previously broken
326
+ for any non-`string` primitive param (backlog point 3 — Ajv's validate-Layer
327
+ has no `coerceTypes`, so a query-string `"42"` failed a `number` schema check).
328
+ A new `coerceQueryParams()` helper (`apigen-engine-runtime/src/lib/coerce-
329
+ query.ts`) coerces `number`/`integer`/`boolean` query values to their real JS
330
+ types at the TRANSPORT layer (both `run.ts`s and both `generate.ts`s' emitted
331
+ code), strictly scoped to the GET/query-string path — the shared Ajv instance
332
+ used for POST/body validation is untouched, so this cannot silently coerce a
333
+ malformed POST body. An unparseable value (e.g. `?count=abc`) is left as-is so
334
+ validation still rejects it with a clear `invalid_argument` error.
335
+
336
+ Several existing tests hardcoded `POST`/JSON-body requests for functions whose
337
+ params are now correctly auto-hoisted to `GET` (a zero-param op in
338
+ `orchestrator.spec.ts`'s override test, `price(v: Decimal)` — literally
339
+ `type Decimal = string` — in `cross-host-response-envelope.spec.ts`,
340
+ `upperFirst`/etc. in `real-consumer.spec.ts`, `addNumbers(a: number, b:
341
+ number)` in `serve.spec.ts`) — updated to GET/query-string calls (or, for the
342
+ one test whose actual point was proving the override mechanism itself, to a
343
+ non-primitive-shaped fixture) since the old assertions encoded pre-feature
344
+ default behavior, not the tests' real intent.
345
+
346
+ New regression tests: `apigen-core-client` (`get-safety.spec.ts`, 10 tests;
347
+ `compose-schemas.spec.ts` +5 `x-apigen-safe` tests), `apigen-engine-naming`
348
+ (`naming.spec.ts` +11 auto-hoist/`httpVerb` tests), `apigen-engine-runtime`
349
+ (`coerce-query.spec.ts`, 11 tests incl. a validate-Layer integration proof),
350
+ `entrypoint/apigen-cli` (`orchestrator.spec.ts` +5 real-pipeline
351
+ extract→compose→generate tests covering auto-hoist, non-hoist, override
352
+ precedence, and `x-apigen-safe` presence in the real generated schema). Full
353
+ suites green: `apigen-core-client` 267/267, `apigen-engine-naming` 51/51,
354
+ `apigen-engine-runtime` 151/151, `apigen-plugin-api-express` 36/36,
355
+ `apigen-plugin-api-fastify` 48/48, `apigen-cli` 139/139 — zero regressions.
356
+ `nx run-many -t build` clean for all six + their 32 dependents; `nx run-many -t
357
+ lint` clean.
358
+
359
+ - **BUG-APIGEN-017/018/019/020** — MCP tool-schema hardening bundle (all four filed
360
+ 2026-07-06 from the `scratch-agent-search`/agent-browser consumer). Root-caused each
361
+ independently before fixing (`entrypoint/apigen-cli/BACKLOG.md`'s filed entries per ID):
362
+
363
+ - **BUG-APIGEN-017** (unknown properties silently accepted) and **BUG-APIGEN-020**
364
+ (the `data` envelope convention undocumented) were **already fixed** in an earlier
365
+ session batch (`1498dd45`, 2026-07-15) — `apigen-core-client/src/lib/compose-schemas.ts`
366
+ already stamps `additionalProperties: false` on both the top-level and nested `data`
367
+ schema objects, and a human-readable envelope-calling-convention `description` (via
368
+ `apigen-engine-runtime/src/lib/tool-description.ts`'s `buildToolDescription`, consumed by
369
+ `apigen-plugin-mcp`'s `generate.ts` and `run.ts`) — both with existing regression coverage
370
+ in `apigen-core-client/src/test/compose-schemas.spec.ts`. The BACKLOG entries were simply
371
+ never moved to this file when the fix landed; confirmed via direct read of the committed
372
+ (not just working-tree) source before treating this as bookkeeping-only for those two.
373
+ (The BACKLOG's `BUG-APIGEN-020` entry also briefly collided with an unrelated
374
+ `BUG-APIGEN-037` — py-flask/py-grpc eager imports — under a shared duplicate ID; both
375
+ were independently already using their now-current IDs in code comments, and the
376
+ duplicate was resolved in the BACKLOG text separately from this fix.)
377
+ - **BUG-APIGEN-018** (mcp) (parameter defaults not surfaced) was likewise already fixed —
378
+ `apigen-core-client/src/lib/extract.ts` + `param-defaults.ts`'s `applyParamDefault`
379
+ stamp both the native JSON-Schema `default` keyword and a `"(default: <value>)"` note
380
+ onto each parameter's own property schema, which flows unmodified into the MCP
381
+ `inputSchema` (`apigen-plugin-mcp`'s `run.ts`/templates never touch `input`). Existing
382
+ coverage: `apigen-core-client/src/test/extract.spec.ts`'s `[BUG-APIGEN-018]` describe
383
+ block.
384
+ - **BUG-APIGEN-019** (union return types produce weak schemas) had its *schema-building*
385
+ half already fixed the same way — `ts-json-schema.ts`'s `normalizeTopLevelUnion` rewrites
386
+ a TS union's `anyOf` to `oneOf` + an advisory `discriminator` for both inline and named
387
+ union return types, reaching `extract()`'s `output` fragment for real (confirmed via
388
+ `union.spec.ts` + the wiring trace in BUG-APIGEN-038's BACKLOG entry, which explicitly
389
+ scopes that gap to *parameters*, not return types). **But the MCP transport never
390
+ surfaced any return-type schema to clients at all** — `apigen-plugin-mcp`'s `run.ts` and
391
+ both generated-server templates (`server-stdio.tpl.ts`, `server-http.tpl.ts`) only ever
392
+ emitted `inputSchema` in `tools/list`, never `outputSchema`, for any function, union or
393
+ not. This is the one genuinely live gap in the bundle, and it isn't a simple pass-through
394
+ fix: the MCP SDK's `Tool.outputSchema` is constrained by its own Zod schema to a
395
+ top-level `{ type: "object", ... }` shape (`ToolSchema.outputSchema` in
396
+ `@modelcontextprotocol/sdk`), so naively forwarding a `oneOf`-shaped union output would
397
+ have failed the SDK's own runtime validation and crashed the server for exactly the
398
+ return-type shape this bug describes. Fixed by adding
399
+ `apigen-engine-runtime/src/lib/mcp-output-schema.ts` (`buildMcpOutputSchema` /
400
+ `wrapMcpStructuredContent`, exported from the package index): an already-`type:"object"`
401
+ output schema passes through unwrapped; anything else (the union case, arrays, bare
402
+ scalars) is wrapped as `{ type: "object", properties: { result: <output> }, required:
403
+ ["result"] }` for `outputSchema`, with the paired runtime value wrapped the same way
404
+ (`{ result: <value> }`) as MCP's `structuredContent` (also object-constrained by the
405
+ SDK) alongside the pre-existing `content` text field for backward compatibility. Wired
406
+ into all three MCP server code paths that independently duplicate the `tools/list`/
407
+ `tools/call` handlers: `apigen-plugin-mcp/src/lib/run.ts` (in-process server) and both
408
+ `src/lib/templates/server-stdio.tpl.ts` / `server-http.tpl.ts` (generated standalone
409
+ servers).
410
+
411
+ **Tests:** new `apigen-engine-runtime/src/test/mcp-output-schema.spec.ts` (9 cases:
412
+ object/union/array/scalar output shaping, empty/undefined output, structuredContent
413
+ wrap/passthrough, and the defensive non-object-value case). New
414
+ `apigen-plugin-mcp/src/test/generate.spec.ts` `[plugin-mcp.7]` describe block (4 cases:
415
+ all three transports' generated `server.ts` import and wire `buildMcpOutputSchema`/
416
+ `wrapMcpStructuredContent` into both `outputSchema` and `structuredContent`;
417
+ `additionalProperties:false` survives `generate()` unmodified; a per-param default note
418
+ survives into the generated schema; an envelope-convention description survives into the
419
+ generated schema). New `apigen-plugin-mcp/src/test/run.spec.ts` `[plugin-mcp.7]` describe
420
+ block (5 real end-to-end HTTP `tools/list`/`tools/call` cases: object-shaped output passes
421
+ through `outputSchema` unwrapped; a `oneOf`+`discriminator` union output is wrapped under
422
+ `result` with the discriminator intact; an array-return output is wrapped under `result`;
423
+ `tools/call` `structuredContent` mirrors `content` unwrapped for the object case; and
424
+ wrapped as `{ result: <value> }` for the union case). Verified clean: `nx test
425
+ apigen-engine-runtime` 140/140 (15 files), `nx test apigen-plugin-mcp` 47/47 (4 files) —
426
+ zero regressions to the pre-existing 38 apigen-plugin-mcp tests or 131 apigen-engine-runtime
427
+ tests. Both projects' `nx run <project>:build` (which typechecks via `vite-plugin-dts`) and
428
+ `nx run <project>:lint` pass clean.
429
+
430
+ - **FEAT-APIGEN-023** — a zero-parameter, zero-required-envelope operation's published
431
+ `inputSchema` no longer forces callers to send an empty `data: {}` (or any envelope field).
432
+ `apigen-core-client/src/lib/compose-schemas.ts`'s composed outer schema unconditionally
433
+ listed `'data'` in its top-level `required` array even when a function took no domain
434
+ parameters at all (`listProviders`, `tripwireStatus`, etc.) — a deliberate, named invariant
435
+ (`[inv:data-wrapper-always-present]`) that made every MCP host, LLM tool-caller, or other
436
+ strict schema-driven client believe an empty envelope was mandatory, even though every
437
+ transport's decode path (`apigen-plugin-api-express`, `apigen-plugin-mcp`,
438
+ `apigen-engine-runtime`'s `validate-layer.ts`) already defaulted an omitted body/args/data to
439
+ `{}` at runtime — confirmed a schema/documentation-only issue, not a decode-side one, by
440
+ reading each transport's decode site rather than assuming. Fixed by making the `'data'`
441
+ entry in the outer `required` array conditional on `domainRequired.length > 0` — the exact
442
+ condition already used for the nested `data` schema's own `required` — so `{}` (no body
443
+ needed beyond the operation name) is now a valid call for a truly zero-arg operation, while
444
+ the `data` property itself is still always declared (so `{"data": {}}` remains valid for
445
+ callers who send it anyway) and parameterized operations are completely unaffected.
446
+ Regression tests in `apigen-core-client/src/test/compose-schemas.spec.ts`: two pre-existing
447
+ cases that had baked in the old unconditional-required behavior for a zero-param fixture
448
+ function were corrected, plus a new 7-case `FEAT-APIGEN-023` describe block covering an
449
+ empty top-level `required` for a zero-param/zero-middleware function, that `data` remains a
450
+ declared (not required) property, that a parameterized function's `required` still contains
451
+ `'data'` (regression control), that middleware envelope fields stay required independent of
452
+ `data`, and that overriding a zero-param function's only middleware to `false` yields a
453
+ fully empty `required` array. Verified clean: `nx test apigen-core-client` 252/252, `nx test
454
+ apigen-engine-runtime` 131/131, and `nx run-many -t test -p apigen-plugin-api-express
455
+ apigen-plugin-api-fastify apigen-plugin-mcp apigen-plugin-jsonschema apigen-plugin-cli-output
456
+ apigen-cli` 134/134 across all six downstream `ComposedSchemas` consumers — zero regressions.
457
+
458
+ - **BUG-APIGEN-031** — `generate --type cli` output silently mishandled array/object-typed
459
+ domain params: Commander's raw argv string for a flag like `--arr '[2,4,6]'` was passed
460
+ straight through to `dispatch()` with no JSON-parsing, since only the CLI transport's wire
461
+ values are plain strings (HTTP transports already deliver a real parsed value from the
462
+ request body, and the shared `apigen-base-logical` decode path's array/object branches
463
+ correctly pass a non-array/non-object wire through unchanged for that case). Result: an
464
+ array-typed param either crashed downstream (`arr.reduce is not a function`) or, worse,
465
+ silently returned a wrong-but-plausible-looking answer via string-indexing coincidence
466
+ (`percentile --sorted '[10,20,30,40,50]' --p 0.5` returned `"3"` — the character at index 7
467
+ of the 16-char argv string — instead of the real answer, `30`). Ported from `main`'s
468
+ `entrypoint/apigen-cli/BACKLOG.md` (filed there 2026-07-19) and independently reproduced on
469
+ this worktree's current source before fixing — `generate.ts`, `runmode.ts`, and `dispatch.ts`
470
+ were byte-identical to `main`, confirmed via `git diff main`. Fixed in
471
+ `apigen-plugin-cli-output/src/lib/generate.ts` (transport-specific, not the shared decode
472
+ path, to avoid touching HTTP transports' already-correct behavior): a new `isJsonTypedProp()`
473
+ helper flags any domain param whose schema (or `anyOf` with all non-null members) is
474
+ `array`/`object`-typed; `generate()` now pre-scans every command for such a param and, only
475
+ when at least one exists, embeds a small `__apigenParseJsonArg(flag, raw)` runtime helper in
476
+ the generated `cli.ts` (undefined passthrough for omitted optional params, `JSON.parse` for
477
+ strings, a clear `Invalid JSON for --<flag>: <message>` thrown error on malformed input
478
+ instead of a silent wrong value) and routes each json-typed param's `domainArgs` entry
479
+ through it. Regression tests added to `apigen-plugin-cli-output/src/test/plugin.spec.ts`
480
+ (`BUG-APIGEN-031` describe block, 9 cases): static codegen assertions (array param wrapped,
481
+ object param wrapped, scalar params NOT wrapped, helper omitted entirely when unused,
482
+ nullable `anyOf:[array,null]` param wrapped) plus behavioral assertions that extract the
483
+ actual generated helper source from `content` and evaluate it directly — proving the real
484
+ shipped runtime code parses `'[2,4,6]'` → `[2,4,6]` and `'{"verbose":true}'` →
485
+ `{verbose:true}`, passes `undefined` through for an omitted optional param, and throws
486
+ `Invalid JSON for --arr: ...` (not a silent passthrough) on malformed input. Live end-to-end
487
+ verification against `/Users/nix/dev/ai/sox-ecosystem/libs/memory-core/src/latency-stats.ts`
488
+ (`generate --type cli --link-workspace` then real `tsx cli.ts` invocations): `mean --arr
489
+ '[2,4,6]'` now returns `4` (was a crash), `percentile --sorted '[10,20,30,40,50]' --p 0.5` now
490
+ returns `30` (was the silently-wrong `"3"`), and `mean --arr 'not-json'` now throws
491
+ `Invalid JSON for --arr: Unexpected token 'o', "not-json" is not valid JSON` instead of
492
+ reaching dispatch at all. `apigen-plugin-cli-output` suite: 34/34 (was 25/25 pre-fix,
493
+ 9 new). `apigen-base-logical` suite (untouched, verified no regression): 186/186.
494
+ `eslint` on both touched files: clean. `nx test`/`tsc --noEmit` for these projects not used
495
+ directly for final verification — `nx test` transitively gates on `apigen-core-client:lint`,
496
+ which had an unrelated, concurrent in-flight failure from a different teammate's uncommitted
497
+ WIP at verification time (`apigen-core-client/src/test/extract.spec.ts`, BUG-APIGEN-CORE-004
498
+ in progress, confirmed via `git status`/`git diff` to be outside this fix's diff entirely);
499
+ raw `tsc --noEmit -p tsconfig.spec.json` hits a pre-existing `vite.config.ts`/`defineConfig`
500
+ raw-`tsc`-invocation artifact (untouched file, not a real type error, an artifact of invoking
501
+ `tsc` outside the Vite-aware toolchain — the project's actual CI-relevant check, `vitest run`,
502
+ passed cleanly).
503
+
504
+ - **BUG-APIGEN-032** — `generate --type api-express`/`api-fastify` (including their
505
+ `-registry` variants) emitted genuinely invalid TypeScript/JavaScript for any
506
+ hyphenated discovered package id — the repo-convention, overwhelmingly common case
507
+ (`pkg-a`, any real `@scope/some-package-name`) — because both generators spliced
508
+ `pkg.id` straight into JS identifier positions with no sanitization:
509
+ `` `import * as ${pkg.id}_ns from '${pkg.importPath}'` ``,
510
+ `` `const ${pkg.id}_fns = buildFnTable(...)` ``, and the `dispatch(${pkg.id}_fns …)`
511
+ call sites reusing that same unsanitized name — producing `import * as pkg-a_ns from
512
+ …`, a hard parse error (`Expected "from" but found "-"`; a bare `-` is the
513
+ subtraction operator in an identifier position, not a valid character). Ported from
514
+ `main`'s `entrypoint/apigen-cli/BACKLOG.md` (filed there 2026-07-19 by
515
+ `verify-registry-commands`) and independently reproduced on this worktree's current
516
+ source before fixing — confirmed the plugin files were untouched by the
517
+ v1-retirement diff, so the bug carried over unchanged.
518
+ `apigen-plugin-cli-output/src/lib/generate.ts` already had the correct fix for the
519
+ same bug class (`BUG-APIGEN-CLI-001`, `packages/apigen/BACKLOG.md`, fixed
520
+ 2026-07-06) as a private, unexported `sanitizeIdentifier()` helper — never shared,
521
+ so `api-express`/`api-fastify` never had access to it. Fixed by extracting
522
+ `sanitizeIdentifier()` into `@adhd/apigen-naming`
523
+ (`packages/apigen/apigen-engine-naming/src/lib/naming.ts:383-407`, exported via
524
+ `src/index.ts`) — the one shared naming/projection-helper package all three plugins
525
+ already depended on (zero new dependency edges needed) and whose own doc comment
526
+ states "No transport may inline its own casing logic; it must call one of the
527
+ helpers exported here." All three plugins now import the single shared
528
+ implementation: `apigen-plugin-api-express/src/lib/generate.ts:2,55,58,83,108,129`,
529
+ `apigen-plugin-api-fastify/src/lib/generate.ts:2,66,69,94,123,146`, and
530
+ `apigen-plugin-cli-output/src/lib/generate.ts:3,139,169` (its former private
531
+ duplicate deleted). Regression tests added:
532
+ `apigen-plugin-api-express/src/test/hyphenated-identifier.spec.ts` and
533
+ `apigen-plugin-api-fastify/src/test/hyphenated-identifier.spec.ts` (6 cases each) —
534
+ unit-verify the sanitized identifier is emitted at every import/fn-table/dispatch
535
+ splice site (GET and POST) while the raw hyphenated id stays verbatim in the
536
+ schema-key string, plus a real `esbuild` syntax check proving the pre-fix splice
537
+ pattern is a genuine parse error and the current `generate()` output parses cleanly.
538
+ `apigen-engine-naming`'s existing `naming.spec.ts` suite is untouched (40/40 still
539
+ pass); `apigen-plugin-cli-output`'s existing `hyphenated-namespace.spec.ts` (proving
540
+ `BUG-APIGEN-CLI-001` stays fixed) also still passes unchanged, now exercising the
541
+ shared implementation instead of the deleted local copy. Verified via direct
542
+ `vitest run` in each of the four affected packages (`apigen-engine-naming`:
543
+ 40/40, `apigen-plugin-api-express`: 31/31, `apigen-plugin-api-fastify`: 43/43,
544
+ `apigen-plugin-cli-output`: 34/34) and `tsc --noEmit` on each package's
545
+ `tsconfig.spec.json` (clean, modulo a pre-existing `vite.config.ts`/`defineConfig`
546
+ raw-`tsc`-invocation artifact confirmed to reproduce identically on the untouched
547
+ `apigen-plugin-health` package — not a real type error, an artifact of invoking
548
+ `tsc` outside the Vite-aware toolchain). `nx test` for these projects was not used
549
+ directly for final verification because it transitively gates on `apigen-core-client:
550
+ lint`, which had an unrelated, concurrent in-flight failure from a different
551
+ teammate's uncommitted WIP at verification time (`apigen-core-client/src/test/
552
+ extract.spec.ts`, BUG-APIGEN-CORE-004 in progress) — confirmed via `git status`/`git
553
+ diff` to be outside this fix's diff entirely.
554
+
555
+ - **FEAT-APIGEN-019** — CLI's `--type` plugin discovery was undiscoverable: `generate`'s
556
+ `--type` help text was a hardcoded, already-stale string (missing `py-flask`/`py-grpc`,
557
+ two of the 7 real targets); `run`'s and `run-registry`'s `--type` help text said nothing
558
+ but `'Output target'`; there was no `list-types`/`--list` command at all; and `run`'s
559
+ `!plugin?.run` check conflated two distinct failures into one message — a genuinely
560
+ unrecognized `--type` (e.g. `express`, a plausible near-miss of the real key `api-express`)
561
+ was reported identically to a real, registered generate-only plugin (`jsonschema`, `cli`)
562
+ that legitimately has no `run()` — "Plugin express does not support run mode" instead of
563
+ "Unknown --type: express". Reported live: the user hit exactly this typo case against the
564
+ built CLI. Root cause: every one of these surfaces (`generate.ts`, `generate-registry.ts`,
565
+ `run.ts`, `run-registry.ts`) read/derived its `--type` text independently instead of from
566
+ one source of truth, so they drifted from the real `plugins` map (`index.ts:17-30`) and
567
+ from each other. Fixed by adding `src/lib/plugin-registry.ts` as the single source of truth
568
+ — `describeTypeOption()`/`describeRunTypeOption()` for Commander `--type` help text (the
569
+ latter scoped to only run-capable ids), `unknownTypeError()`/`unsupportedRunError()` for the
570
+ two now-distinct error branches, and `formatTypesList()` for the new `apigen list-types`
571
+ command (`src/lib/commands/list-types.ts`, registered in `index.ts`) — every one reading
572
+ `Object.keys(plugins)` / `plugin.run` live, so they can never drift from the registry or
573
+ from each other again. `run.ts`/`run-registry.ts`'s single `if (!plugin?.run) throw …` is
574
+ now two checks: `if (!plugin) throw unknownTypeError(...)` then
575
+ `if (!plugin.run) throw unsupportedRunError(...)` — the latter lists the generate-only and
576
+ run-capable subsets separately. Verified end-to-end against the real registry (all 8 keys):
577
+ `apigen list-types` prints every id with its `plugin.description` and generate/run
578
+ capability; `apigen run --type express ...` now throws `Unknown --type: express. Available:
579
+ mcp, jsonschema, api-fastify, api-express, cli, cli-output, py-flask, py-grpc` (previously
580
+ the misleading "does not support run mode"); `apigen run --type jsonschema ...` throws
581
+ `Plugin jsonschema does not support run mode. Generate-only plugins: jsonschema, cli,
582
+ cli-output. Run-capable plugins: mcp, api-fastify, api-express, py-flask, py-grpc.`; `apigen
583
+ generate --help`/`apigen run --help` show live, correctly-scoped `--type` option text.
584
+ Covered by `src/test/plugin-registry.spec.ts` (registry-derivation unit tests — proves
585
+ every helper's output changes when a fixture registry gains/loses a plugin, not just that
586
+ it matches a fixed string), `src/test/list-types.spec.ts` (the new command, same
587
+ derivation proof), and new cases in `src/test/run.spec.ts`/`src/test/generate.spec.ts`
588
+ (the unknown-vs-unsupported-run-mode split, and `--help` text scoping for `run` vs
589
+ `generate`). Deferred out of scope: `py-flask`/`py-grpc`'s unconditional eager imports
590
+ (unpublished on npm — filed as BUG-APIGEN-037, since it's a plugin-*loading* concern, not
591
+ a `--type` text/list-derivation concern).
592
+
593
+ - **BUG-APIGEN-021** — `apigen run --source <file> --type <plugin>` crashed with
594
+ `Error: Cannot find module './x.js'` (`MODULE_NOT_FOUND`) against any real
595
+ `--source` whose package has no `"type": "module"` (the common default for
596
+ internal workspace libs, e.g. `@adhd/sox-memory-core`) and that internally
597
+ imports a sibling module via a NodeNext-style `./x.js` specifier resolving to
598
+ `./x.ts` on disk. `importSource()` (`src/lib/import-source.ts`) registered
599
+ only `tsx/esm/api`'s ESM loader hook before the dynamic `import()`; when the
600
+ target's format resolves to CommonJS, Node's ESM loader routes it through the
601
+ CJS translator, which performs a real `require()` that only `tsx/cjs/api`'s
602
+ hook patches — the ESM-only hook never saw it, so the `.js` → `.ts` extension
603
+ mapping never applied. Fixed by also registering `tsx/cjs/api` for the
604
+ duration of the import (mirrors what the full `tsx` CLI does — it patches
605
+ both loaders). Covered by
606
+ `src/test/e2e/import-source-cjs-format.spec.ts`, which spawns the BUILT bin
607
+ as a real `node` child process (the only way to reproduce this — a
608
+ vitest-in-process unit test never hits Node's loader) against a fixture
609
+ reproducing the exact shape (`src/test/fixtures/cjs-format-js-import/`);
610
+ verified red against the pre-fix code (same `MODULE_NOT_FOUND` as the
611
+ original report) and green after.
612
+
613
+ - **BUG-APIGEN-026** — `apigen run --type api-express/api-fastify` crashed EVERY call to
614
+ any function with a plain named-type parameter (interface, type alias — not a scalar
615
+ apigen special-cases, not inline/anonymous) with
616
+ `{"code":"internal","message":"can't resolve reference #/definitions/<Name> from id #"}`,
617
+ a compile-time AJV failure that fired regardless of the actual input sent. Root cause:
618
+ `ts-json-schema-generator`'s own default (`topRef: true`, confirmed in the installed
619
+ package's `Config.js`) wraps every named-type schema as `{ $ref: "#/definitions/X",
620
+ definitions: { X: {...} } }`; `apigen-core-client`'s `runScalarAwareGenerator()`
621
+ (`schema-builders/ts-json-schema.ts`) never overrode it, and nothing dereferenced the
622
+ result before `extract.ts` spliced it into a nested property position inside the
623
+ composed function schema — where `$ref`'s root-relative resolution permanently dangled
624
+ once AJV compiled the full schema (one schema per function, no shared registry).
625
+ Reported live against a real consumer (`agent-browser`'s `search(provider:
626
+ ProviderName, ...)`); reproduced exactly via `apigen generate --type jsonschema`
627
+ against the unmodified source. Fixed by passing `topRef: false` in Path 1's
628
+ `Config`, which inlines the entry type directly instead of wrapping it — verified via a
629
+ standalone `createGenerator()` call before applying, then end-to-end against the real
630
+ file (`POST /agent-browser/search` now returns `200` instead of crashing). Does **not**
631
+ fully solve genuinely self-referential/cyclic named types, which still need an internal
632
+ `$ref` and would carry the identical class of bug if ever embedded nested — tracked as
633
+ a known residual gap, not yet filed as a separate item (no reproducing case found).
634
+ Covered by `apigen-engine-runtime/src/test/named-type-param.spec.ts` (a real
635
+ `generateSchemas` → `composeSchemas` → `Ajv.compile` pipeline test, not hand-built
636
+ schema fixtures — the existing `ts-json-schema.spec.ts` dangling-ref checks only walk
637
+ a schema fragment in isolation and would not have caught this); verified red against
638
+ the pre-fix code (identical `"can't resolve reference"` error) and green after.
639
+
640
+ - **BUG-APIGEN-027** — even after BUG-APIGEN-026's fix, any call that legitimately
641
+ omitted an optional `number`-typed parameter (e.g. `search()`'s `maxContentSize`,
642
+ `timeoutMs`, `maxAttempts`, `challengeWaitMs` — all valid to omit per the AJV schema,
643
+ none in `required`) crashed dispatch with `{"code":"internal","message":"[number-
644
+ special] unrecognized wire value at \"\": undefined. Expected a number or one of NaN,
645
+ Infinity, -Infinity."}`, before the target function was ever called. Root cause:
646
+ `apigen-engine-runtime/src/lib/dispatch.ts`'s `decodeArg()` only guarded on whether the
647
+ parameter's *schema node* was defined, not whether the caller actually *sent* a value
648
+ for it — so every declared optional param the caller omitted was still passed through
649
+ `_transcoder.decode(undefined, node)`, which for a bare `{type:'number'}` node resolves
650
+ to `numberSpecialCodec` and correctly rejects `undefined` in strict mode. The object-
651
+ property walk in `runmode.ts`'s `encodeNode`/`decodeNode` already guards `v !==
652
+ undefined` internally; `dispatch.ts`'s per-parameter call site was the one place that
653
+ lacked the equivalent guard, because it calls the transcoder once per declared param
654
+ name rather than walking a nested object schema. Fixed by adding `wire === undefined`
655
+ to `decodeArg()`'s existing early-return guard — an omitted optional value now passes
656
+ through as `undefined` (so TS default parameters apply normally at the function
657
+ boundary), matching how the AJV validation layer already treated it as valid. Covered
658
+ by three new cases in `apigen-engine-runtime/src/test/dispatch.spec.ts` (omitted
659
+ optional param doesn't throw; omitted param arrives as `undefined` at the fn boundary;
660
+ an explicitly-provided optional param still decodes normally); verified red against the
661
+ pre-fix code (identical `number-special` error — including on the "explicitly
662
+ provided" case, since a *different* still-omitted optional param in the same call
663
+ tripped it too) and green after. Verified end-to-end against the real `search()`
664
+ consumer: `POST /agent-browser/search` with only `{provider, query}` now completes
665
+ (`200`, real dispatch to a live network search) instead of crashing before dispatch.
666
+
667
+ - **BUG-APIGEN-028** — `apigen run`/`generate` (single-source and
668
+ `-registry` variants) still ran the old v1 extraction pipeline by default;
669
+ v1 silently dropped every re-exported operation from a source file
670
+ (`export { x } from './other.js'`), producing e.g. 2 routes instead of
671
+ 140+ for a realistic re-export-barrel file (confirmed live against
672
+ `~/dev/ai/sox-ecosystem/libs/memory-core/src/index.ts`, read-only
673
+ reference). Passing `--v2` fixed the log line ("extracted 140
674
+ operations") but did NOT fix the actually-served routes:
675
+ `orchestrator.ts`'s `buildDescriptor()` had a "Step 5" that independently
676
+ re-ran the buggy v1 extractor a second time to build the `ComposedSchemas`
677
+ every plugin's `generate()`/`run()` actually dispatches against, silently
678
+ discarding the correct extraction from Steps 1-4 — so `--v2` alone,
679
+ without also fixing Step 5, remained broken. Root cause: commit
680
+ `556d02ee` ("apigen v2 — 18-package TS→API toolchain") introduced the v2
681
+ orchestrator behind a cautious `--v2` opt-in flag for staged rollout; the
682
+ follow-up step of flipping the default (or retiring v1) was never done
683
+ and fell through the cracks. Fixed by retiring v1 entirely rather than
684
+ patching it (avoids two parallel implementations drifting again):
685
+ `generate.ts`/`run.ts` now run `orchestrateGenerate`/`orchestrateRun`
686
+ unconditionally (see Removed, below, for the `--v2` flag itself);
687
+ `generate-registry.ts`/`run-registry.ts` rewired from a per-package
688
+ `runPipeline()` loop to build one `SourceEntry[]` and pass it through the
689
+ same orchestrator in a single call, which also surfaced and fixed a real
690
+ latent bug where `orchestrateRun` matched `packageSchemas` entries back to
691
+ their `SourceEntry` by `s.file === p.importPath` (only ever worked by
692
+ coincidence for single-source `run`; silently failed to resolve for the
693
+ registry commands' distinct `file`/`importPath` pair) — fixed by matching
694
+ on namespace instead. Deleted (apigen-core-client side, see
695
+ `BUG-APIGEN-CORE-005` in that package's BACKLOG.md for the extractor-side
696
+ half of this fix): `generateSchemas()`, the three v1 extractors,
697
+ `runPipeline()` (`entrypoint/apigen-cli/src/lib/pipeline.ts`). Covered by
698
+ the existing `generate.spec.ts`/`run.spec.ts`/`orchestrator.spec.ts`/
699
+ `integration/schema.spec.ts` suites, rebuilt against the real
700
+ `extract()`/`composeSchemas()` path in place of the deleted
701
+ `generateSchemas()` (112/112 green, down from 113 — one test removed
702
+ whose entire premise, "behavior is the same whether or not `--v2` is
703
+ passed," no longer applies with only one path left; nothing it protected
704
+ against is now uncovered). Real-world verification: 140 operations
705
+ extracted → 130 real routes served (up from 2), including two
706
+ previously-invisible routes curled and confirmed responding correctly.
707
+ Surfaced, but does not fix (filed separately as **BUG-APIGEN-029**, open):
708
+ a pre-existing `$ref`/ajv-strict-mode dispatch failure on functions taking
709
+ complex external types (e.g. `better-sqlite3.Database`) as params —
710
+ confirmed identical under the old v1 2-route path, so not a regression
711
+ from this fix; it was simply unreachable before because v1 never exposed
712
+ those re-exported routes at all.
713
+
714
+ - **BUG-APIGEN-036** — post-v1-retirement code review caught
715
+ `apigen-engine-runtime/src/test/named-type-param.spec.ts` (the real-pipeline
716
+ regression test authored for BUG-APIGEN-026) still calling the deleted v1
717
+ `generateSchemas()`, which `7c66413d` (v1 retirement) had removed along with
718
+ `lib/generate-schemas.ts` — every one of its 3 test cases failed with
719
+ `TypeError: generateSchemas is not a function`, meaning BUG-APIGEN-026's
720
+ dangling-`$ref` regression test had zero coverage on this branch. Fixed by
721
+ rewriting all 3 call sites to the v2 equivalent: `extract({ sourceFile })`
722
+ → `Operation[]`, adapted into `composeSchemas()`'s expected `GeneratedSchemas`
723
+ shape via a `toGeneratedSchemas()` helper added to the test file (`kind:
724
+ 'action'` operations only, keyed by the terminal path segment's raw
725
+ spelling) — the exact same adaptation `buildDescriptor`'s Step 5 performs in
726
+ `entrypoint/apigen-cli/src/lib/orchestrator.ts`, reproduced locally since
727
+ it isn't exported as a standalone helper. The test's real-pipeline intent
728
+ (extract → compose → real `Ajv.compile`, not hand-built fixtures) and all
729
+ three original assertions (compiles without a dangling `$ref`, a valid
730
+ `pick('a')` call dispatches, an invalid enum value is rejected) are
731
+ unchanged. Confirmed the fix is semantically equivalent, not just
732
+ compiling: the `topRef: false` fix from BUG-APIGEN-026 is still present
733
+ and unchanged in `schema-builders/ts-json-schema.ts`, so this test still
734
+ exercises the real code path the original regression test was written
735
+ against. Verified green:
736
+ `npx nx test apigen-engine-runtime --testFile=src/test/named-type-param.spec.ts`
737
+ (3/3 passing).
738
+
739
+ - **BUG-APIGEN-CORE-001 re-wired (v1 retirement)** — post-v1-retirement code
740
+ review found that v1's deleted `generate-schemas.ts` called
741
+ `validateSchemaRefs()` (`@adhd/apigen-base-logical`) on every function's
742
+ built `input`/`output` schema, pooling `$defs` across all functions in a
743
+ source file, and threw a clear `Schema validation failed for function "X"`
744
+ error at generate time if any `$ref` was unresolvable — an early,
745
+ well-scoped catch for the exact dangling-`$ref` bug class BUG-APIGEN-026
746
+ hit at runtime instead. This safety net was silently dropped when v1 was
747
+ deleted: `validateSchemaRefs` was never called anywhere in the v2 path
748
+ (`extract.ts`, `extraction-session.ts`, `orchestrator.ts`,
749
+ `compose-schemas.ts`) — the only remaining reference was a local
750
+ re-implementation inside `apigen-core-client/src/test/ts-json-schema.spec.ts`,
751
+ which gave false confidence since it never touched the real pipeline.
752
+ Fixed by importing the real `validateSchemaRefs` from
753
+ `@adhd/apigen-base-logical` and wiring it into `composeSchemas()`
754
+ (`apigen-core-client/src/lib/compose-schemas.ts`) — the v2 equivalent
755
+ insertion point to v1's `generate-schemas.ts`, since that's where all of a
756
+ namespace's functions are present together in one pass. Pools `$defs`
757
+ across every function in the `GeneratedSchemas` passed in and validates
758
+ each function's `input`/`output` against the pooled dictionary before
759
+ composing, throwing the same function-scoped error v1 did. Covered by
760
+ three new cases in `apigen-core-client/src/test/compose-schemas.spec.ts`:
761
+ a dangling `$ref` (referencing a `$def` never defined by any function)
762
+ throws `Schema validation failed for function "pick"`; a `$ref` that
763
+ resolves against a `$def` pooled from a *different* function does not
764
+ throw (proves cross-function pooling, not per-function-only validation);
765
+ a schema with no `$defs` at all is left unvalidated (matches v1's
766
+ behavior — a bare `$ref` with no `$defs` anywhere is a structural problem
767
+ left to the downstream AJV compile, not this check). Verified red (test
768
+ fails with the `validateComposedRefs()` call temporarily removed) and
769
+ green (restored) by hand before landing.
770
+
771
+ - **BUG-APIGEN-030** — any `x-apigen-logical`-tagged param (union OR
772
+ nominal/branded) crashed EVERY call on `api-express`/`api-fastify` with
773
+ `strict mode: unknown keyword: "x-apigen-logical"` — a 500 regardless of
774
+ input, not a validation rejection. Root cause: apigen's own schema builders
775
+ (`apigen-core-client/src/lib/schema-builders/union.ts`,
776
+ `.../schema-builders/nominal.ts`, and the inline-union path in
777
+ `.../schema-builders/morph-walk.ts:181`) tag union/nominal schema fragments
778
+ with advisory `x-apigen-logical`/`x-apigen-codec`/`x-apigen-ctor`/
779
+ `x-apigen-tojson` keys (`X_APIGEN_LOGICAL` etc. from
780
+ `@adhd/apigen-base-logical/src/lib/descriptor-ext.ts`) plus an
781
+ OpenAPI-style `discriminator` object on union fragments
782
+ (`morph-walk.ts:172-182`) — read back by `union-codec.ts`/`nominal-codec.ts`
783
+ at decode time, never declared to Ajv. `apigen-engine-runtime/src/lib/
784
+ validate-layer.ts`'s single module-level `Ajv({ allErrors: true })`
785
+ singleton (line 51; both `validateLayer` and `makeValidateLayer` share it —
786
+ there is only one construction site, not two as originally suspected) never
787
+ registered any of these five keywords, and Ajv 8's default `strict: true`
788
+ throws `strict mode: unknown keyword` the first time `ajv.compile()` runs
789
+ on any schema carrying one. Confirmed the impact is wider than
790
+ `x-apigen-logical` alone: `discriminator` itself is also rejected by
791
+ default Ajv (`strict mode: unknown keyword: "discriminator"`), and Ajv's
792
+ own built-in `discriminator: true` option is NOT a fix — it enforces its
793
+ own OpenAPI discriminator semantics and explicitly throws
794
+ `discriminator: mapping is not supported` against the `mapping` object
795
+ apigen's `discriminator` fragment carries (verified by direct reproduction
796
+ with `ajv@8.20.0`, the installed version). Fixed by registering all five
797
+ keys (`X_APIGEN_LOGICAL`, `X_APIGEN_CODEC`, `X_APIGEN_CTOR`,
798
+ `X_APIGEN_TOJSON`, `'discriminator'`) as no-op `ajv.addKeyword({ keyword,
799
+ valid: true })` annotations at the singleton's construction site
800
+ (`validate-layer.ts:73-82`), preserving `strict: true`'s other protections
801
+ rather than disabling strict mode wholesale. Covered by a new
802
+ `apigen-engine-runtime/src/test/bug-apigen-030.spec.ts` (5 cases):
803
+ `[apigen-030.1]`-`[apigen-030.3]` run the REAL `extract()` →
804
+ `composeSchemas()` pipeline against a new fixture
805
+ (`test/fixtures/bug-apigen-030.ts`, an inline `Dog | Cat` discriminated
806
+ union param) proving the real `oneOf`+`discriminator`+
807
+ `x-apigen-logical:"union"` shape `morph-walk.ts` actually emits today
808
+ compiles and a real call reaches dispatch instead of crashing;
809
+ `[apigen-030.4]`/`[apigen-030.5]` hand-build schemas using the real
810
+ `X_APIGEN_LOGICAL`/`X_APIGEN_CODEC`/`X_APIGEN_CTOR`/`X_APIGEN_TOJSON`
811
+ constants (not string literals) to cover the two keys the current
812
+ extraction pipeline doesn't yet reach for class-typed nominal params (see
813
+ BUG-APIGEN-038 below) and the `discriminator.mapping` shape Ajv's built-in
814
+ option rejects. Verified red pre-fix (all three previously-failing cases
815
+ reproduced the exact BACKLOG error strings —
816
+ `strict mode: unknown keyword: "x-apigen-logical"` and
817
+ `strict mode: unknown keyword: "discriminator"` — by temporarily reverting
818
+ the `addKeyword` registration) and green post-fix by hand before landing.
819
+ Full suite verified clean: `apigen-engine-runtime` 131/131,
820
+ `apigen-plugin-api-express` 31/31, `apigen-plugin-api-fastify` 43/43 (run
821
+ directly via `vitest run --config <project>/vite.config.ts`, bypassing
822
+ `nx test`'s cross-project lint-dependency chain, which was independently
823
+ broken by an unrelated, concurrently in-flight edit to
824
+ `apigen-core-client/src/test/extract.spec.ts` from another session sharing
825
+ this worktree — not touched by this fix).
826
+
827
+ **Also discovered, filed separately (not fixed by this change):**
828
+ `buildNominalSchema`/`buildUnionSchema` (the class-based nominal/union
829
+ schema builders in `schema-builders/nominal.ts`/`union.ts`) are not wired
830
+ into the real `extract()`/`composeSchemas()` pipeline for any function
831
+ parameter — confirmed no call site outside their own spec files, and
832
+ `orchestrator.ts`'s `extractClasses()` usage is for constructor/
833
+ instance-method operations, not embedding nominal `$def`s into other
834
+ functions' input schemas. Filed as BUG-APIGEN-038 in the Open section
835
+ below.
46
836
 
47
837
  ### Changed
48
- - `generate` emits a **standalone-runnable** `package.json`: rich-type deps (e.g. `decimal.js`)
49
- are added per-surface (only the types actually used), so generated output runs after a plain
50
- `npm install` — `--link-workspace` no longer required for the publish path.
51
- *(dod.10 — resolves BUG-APIGEN-002 dep-manifest slice)*
52
- - Validation actually enforces formats — the validate-Layer wires `ajv-formats`, so a malformed
53
- `date-time` (`2099-02-30`) is rejected instead of silently passing. *(dod.6)*
54
838
 
55
- ### Fixed
56
- - **BUG-APIGEN-005** language-specific serializable types (`Date → {}`, lost precision,
57
- un-rehydrated inputs) and custom-class identity loss. *(this feature)*
58
- - **BUG-APIGEN-004** `run`/`generate` now **fail fast** with an actionable message on 0
59
- extracted functions and on a used rich type whose optional dependency is missing
60
- (e.g. *"function `quote` takes a Decimal; install `decimal.js`"*), instead of a cryptic
61
- `ERR_MODULE_NOT_FOUND`. *(dod.9)*
839
+ - **v1 extraction pipeline retired — v2 orchestrator is now the ONLY
840
+ extraction path** (BUG-APIGEN-028 / BUG-APIGEN-CORE-005). `generate`,
841
+ `run`, `generate-registry`, and `run-registry` all now unconditionally run
842
+ the `detect extract merge collision-check gen/run` v2 pipeline.
843
+ See BUG-APIGEN-028 above for the full root-cause writeup and verification
844
+ numbers.
62
845
 
63
- ### Engineering notes
64
- - Codegen-first design: encode uses each language's **native hook**
65
- (`toJSON` / `JSONEncoder.default` / serde / Jackson / `MarshalJSON`); decode is schema-driven
66
- glue emitted once by the generator — **no per-host runtime interpreter**.
67
- - Languages with native-hook mappings resolved: **TypeScript/JS, Python, Rust, Go, Java**.
68
- 3rd-party deps introduced: Python 0, Java 1, TS 1, Go 2, Rust ~6 (all pinned, minimal-per-surface).
846
+ ### Removed
69
847
 
70
- ---
848
+ - **`--v2` flag removed from `generate` and `run`.** It selected between the
849
+ (now-deleted) v1 pipeline and the v2 orchestrator; with only one pipeline
850
+ left, the flag has no meaning. Removed cleanly rather than kept as a
851
+ deprecated no-op — there's no prior flag-removal precedent in this
852
+ changelog to follow a softer convention, and a silently-ignored flag would
853
+ be more confusing than an explicit "unknown option" error for the rare
854
+ script still passing it. **Migration:** delete `--v2` from any existing
855
+ invocation; behavior is unchanged (it's what `--v2` already did).
71
856
 
72
- ## [1.0.0]apigen v2 (released to disk, publish pending)
857
+ ## 0.1.0 — 2026-07-02
73
858
 
74
859
  ### Added
75
- - apigen v2 — an 18-package TypeScript→API toolchain. Takes any unmodified `.ts` file and
76
- exposes its exports as an **MCP server, HTTP API, or CLI** without source changes
77
- (`apigen-cli run/generate`). Canonical Operation descriptor (JSON-Schema-2020-12 IR),
78
- symbol/class extractor (ts-morph), v2 plugin interface, Layer harness + streaming + single
79
- canonical dispatch path, `mcp`/`api-fastify`/`api-express`/`cli` plugins, `apigen-nx`
80
- generator, and a real Python host passing the conformance vectors.
81
- Plan: `docs/plan/apigen-client-generation` (46/46 states, final audit 117/117, DoD confirmed).
860
+
861
+ - **`apigen run`** Live server from TypeScript source. Supports MCP (stdio/SSE/streaming-http),
862
+ Fastify, Express, CLI output. Dual v1/v2 pipeline paths.
863
+ - **`apigen generate`** Generate server artifacts to disk. Emits resolution scaffolding
864
+ (package.json, tsconfig.json) so generated code runs standalone.
865
+ - **`apigen serve`** Multi-source, multi-language front server. Spawns child `apigen run`
866
+ processes, multiplexes HTTP/1.1 + HTTP/2 (gRPC) on one TCP port via protocol-peeking
867
+ net.Server. Partial availability: a failed host degrades only its own namespace.
868
+ - **`apigen run-registry`** / **`apigen generate-registry`** — Discover packages by nx tag
869
+ and wire them into a single server surface or generate artifacts for all.
870
+ - **Output plugin system** — Pluggable output targets (mcp, jsonschema, api-fastify,
871
+ api-express, cli, py-flask, py-grpc).
872
+ - **`--use` plugin loading** — Layer/mount plugins (health, logger) loaded via built-in slugs,
873
+ package specifiers, or local paths.
874
+ - **v2 unified orchestrator** (`--v2` flag) — Detect → extract → merge → collision-check →
875
+ generate/run pipeline with shared ExtractionSession.
876
+ - **Projection-override config** (`--config`) — Out-of-source config file for HTTP verb and
877
+ naming overrides (Tenet 1).
878
+ - **Per-surface dependency manifest** — Generated package.json patched with only the
879
+ dependencies your code actually uses (e.g. `decimal.js` when `Decimal` format detected).
880
+ - **Fail-fast guards** — Precondition checks for 0-function sources and missing `decimal.js`
881
+ optional peer dep.
882
+ - **Pino-based logging** — `--log-level`, `--log-format`, `--log-file` with env var
883
+ fallbacks, stderr-only output.
82
884
 
83
885
  ### Fixed
84
- - **BUG-APIGEN-001** — ctx-param functions returned wrong results via generated servers
85
- (`hasCtx` now threaded through extraction→dispatch).
86
- - **BUG-APIGEN-002** generated output portability via the Option-A publish model + default-off
87
- `--link-workspace` (dep-manifest extension for rich types tracked in Unreleased above).
88
- - **BUG-APIGEN-003** MCP SSE transport reachable (stdio + streaming-http + sse); probe
89
- hardened (`waitForPort` 15s→60s for cold compiles).
886
+
887
+ - **BUG-APIGEN-004** — Empty function tables now fail early with actionable message instead
888
+ of cryptic `ERR_MODULE_NOT_FOUND` crash.
889
+ - **BUG-APIGEN-009/010** `--use` plugin loading system threads plugin objects to the run
890
+ plugin via `options.usePlugins` for layered composition.
891
+ - **BUG-APIGEN-016** — `serve` pre-provisions managed Python interpreter via
892
+ `@adhd/apigen-python-env` to avoid cold-start timeout.
893
+ - **PERF-APIGEN-001** — Single `ExtractionSession` per v2 run eliminates duplicate ts-morph
894
+ Project creation.
895
+ - **DEBT-LT-005** — Inline `TS_LOGICAL_TYPE_DEP_MAP` replaced with authoritative
896
+ `tsDepMap()` from `@adhd/apigen-base-logical`.
897
+ - **Leak fixes:** `builtinTsconfigPath()` memoized; gRPC h2c sessions have 60s idle eviction
898
+ + `unref()`.
899
+ - **Stale monorepo paths** corrected from `packages/apigen/cli/` → `entrypoint/apigen-cli/`.
900
+
901
+ ### Known limitations
902
+
903
+ - `export { x as y }` aliases, anonymous default exports, and CJS-source files mis-name
904
+ routes in v1 (fixed in v2 by exporting symbol name instead of declaration identifier).
905
+ - Python and Rust/Go/Java host languages are designed in SPEC.md but only TypeScript and
906
+ Python are implemented.