@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 +890 -73
- package/README.md +371 -83
- package/index.js +96 -31
- package/index.mjs +12140 -4448
- package/lib/commands/generate-registry.d.ts +1 -1
- package/lib/commands/generate.d.ts +42 -1
- package/lib/commands/list-types.d.ts +11 -0
- package/lib/commands/run-registry.d.ts +1 -1
- package/lib/commands/run.d.ts +46 -1
- package/lib/commands/serve.d.ts +158 -0
- package/lib/import-source.d.ts +15 -4
- package/lib/logging.d.ts +1 -1
- package/lib/orchestrator.d.ts +76 -11
- package/lib/plugin-registry.d.ts +33 -0
- package/package.json +23 -9
- package/default-tsconfig.json +0 -10
- package/lib/pipeline.d.ts +0 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,89 +1,906 @@
|
|
|
1
|
-
# Changelog —
|
|
1
|
+
# Changelog — @adhd/apigen-cli
|
|
2
2
|
|
|
3
|
-
All notable changes to
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
per-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
###
|
|
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
|
-
##
|
|
857
|
+
## 0.1.0 — 2026-07-02
|
|
73
858
|
|
|
74
859
|
### Added
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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.
|