zopia 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +354 -0
  2. package/LICENSE +21 -0
  3. package/README.md +167 -0
  4. package/bin/zopia.js +20 -0
  5. package/docs/01-overview.md +94 -0
  6. package/docs/02-targets.md +55 -0
  7. package/docs/03-roadmap.md +205 -0
  8. package/docs/04-architecture.md +345 -0
  9. package/docs/05-concepts.md +239 -0
  10. package/docs/06-conversions.md +493 -0
  11. package/docs/07-api-docs.md +337 -0
  12. package/docs/08-components.md +223 -0
  13. package/docs/09-configuration.md +167 -0
  14. package/docs/10-usage.md +208 -0
  15. package/docs/11-testing.md +267 -0
  16. package/docs/12-standards.md +242 -0
  17. package/docs/README.md +42 -0
  18. package/docs/publish-workflow.yml.example +48 -0
  19. package/package.json +77 -0
  20. package/src/api-docs-navigation.ts +353 -0
  21. package/src/cli-command.ts +537 -0
  22. package/src/cli.ts +4 -0
  23. package/src/config.ts +190 -0
  24. package/src/conversions/api-docs-facade.ts +42 -0
  25. package/src/conversions/api-docs-generate.ts +567 -0
  26. package/src/conversions/api-docs-layout.ts +39 -0
  27. package/src/conversions/api-docs-plan.ts +130 -0
  28. package/src/conversions/api-docs-presets.ts +246 -0
  29. package/src/conversions/json-schema-to-zod.ts +931 -0
  30. package/src/conversions/manifest-staleness.ts +211 -0
  31. package/src/conversions/manifest-to-openapi.ts +1861 -0
  32. package/src/conversions/manifest-writer.ts +778 -0
  33. package/src/conversions/openapi-contracts.ts +333 -0
  34. package/src/conversions/openapi-external-ref.ts +233 -0
  35. package/src/conversions/openapi-ir.ts +74 -0
  36. package/src/conversions/openapi-ref.ts +38 -0
  37. package/src/conversions/openapi-to-api-docs-public.ts +466 -0
  38. package/src/conversions/openapi-to-api-docs.ts +203 -0
  39. package/src/conversions/openapi.ts +80 -0
  40. package/src/conversions/reverse-security.ts +68 -0
  41. package/src/conversions/yaml.ts +876 -0
  42. package/src/conversions/zod-to-json-schema.ts +536 -0
  43. package/src/diff.ts +353 -0
  44. package/src/errors.ts +114 -0
  45. package/src/index.ts +80 -0
  46. package/src/validation.ts +299 -0
  47. package/src/warnings.ts +164 -0
@@ -0,0 +1,345 @@
1
+ # ๐Ÿ—๏ธ Architecture
2
+
3
+ This document describes **how zopia v0.1.0 is built**: module layout, conversion
4
+ pipeline, stage-specific representations, reference handling, and the
5
+ error/safety model. Code and documentation change together; disagreement is a
6
+ release-blocking defect under the
7
+ [docs convention](12-standards.md#-docs-convention).
8
+
9
+ ## ๐Ÿงฉ Module layout
10
+
11
+ ```text
12
+ . # ๐Ÿ“ฆ repository root
13
+ โ”œโ”€โ”€ bin/zopia.js # โŒจ๏ธ npm executable; launches the Bun CLI
14
+ โ”œโ”€โ”€ src/
15
+ โ”‚ โ”œโ”€โ”€ index.ts # ๐Ÿšช public named-export surface
16
+ โ”‚ โ”œโ”€โ”€ cli.ts # โŒจ๏ธ process entry point
17
+ โ”‚ โ”œโ”€โ”€ cli-command.ts # strict parser, help, output/exit contract
18
+ โ”‚ โ”œโ”€โ”€ config.ts # ๐Ÿงพ zopia.config.ts discovery, trusted import, validation (D-19)
19
+ โ”‚ โ”œโ”€โ”€ errors.ts # ๐Ÿ›‘ typed error catalogue
20
+ โ”‚ โ”œโ”€โ”€ warnings.ts # โš ๏ธ structured warning pipeline
21
+ โ”‚ โ”œโ”€โ”€ validation.ts # ๐Ÿงน zopia validate lint batteries (specs + generated trees, S-89)
22
+ โ”‚ โ”œโ”€โ”€ diff.ts # ๐Ÿ” zopia diff semantic spec comparison (S-91)
23
+ โ”‚ โ””โ”€โ”€ conversions/
24
+ โ”‚ โ”œโ”€โ”€ zod-to-json-schema.ts # โ‘  Zod โ†’ JSON Schema
25
+ โ”‚ โ”œโ”€โ”€ json-schema-to-zod.ts # โ‘ก JSON Schema โ†’ Zod
26
+ โ”‚ โ”œโ”€โ”€ yaml.ts # owned YAML 1.2 core-schema parser (D-16)
27
+ โ”‚ โ”œโ”€โ”€ openapi.ts # dialect/envelope normalization
28
+ โ”‚ โ”œโ”€โ”€ openapi-ref.ts # local JSON Pointer resolution
29
+ โ”‚ โ”œโ”€โ”€ openapi-external-ref.ts # same-folder external $ref bundling (D-17)
30
+ โ”‚ โ”œโ”€โ”€ openapi-to-api-docs.ts # operation collection
31
+ โ”‚ โ”œโ”€โ”€ openapi-ir.ts # operation-level generation IR
32
+ โ”‚ โ”œโ”€โ”€ openapi-contracts.ts # request/response extraction
33
+ โ”‚ โ”œโ”€โ”€ api-docs-layout.ts # directory/flat path mapping
34
+ โ”‚ โ”œโ”€โ”€ api-docs-plan.ts # collision-safe file planning
35
+ โ”‚ โ”œโ”€โ”€ api-docs-facade.ts # ergonomic access-path helper
36
+ โ”‚ โ”œโ”€โ”€ api-docs-generate.ts # โ‘ข rendering + guarded writes
37
+ โ”‚ โ”œโ”€โ”€ openapi-to-api-docs-public.ts # public Engine โ‘ข wrapper
38
+ โ”‚ โ”œโ”€โ”€ api-docs-presets.ts # split-generation bucket planner (S-92)
39
+ โ”‚ โ”œโ”€โ”€ api-docs-navigation.ts # spec โ†” tree navigation index (S-93)
40
+ โ”‚ โ”œโ”€โ”€ manifest-writer.ts # canonical manifest contract
41
+ โ”‚ โ”œโ”€โ”€ manifest-staleness.ts # drift/ownership cleanup
42
+ โ”‚ โ”œโ”€โ”€ manifest-to-openapi.ts # โ‘ฃ trusted import + reconstruction
43
+ โ”‚ โ””โ”€โ”€ reverse-security.ts # reverse security fallback
44
+ โ”œโ”€โ”€ tests/ # ๐Ÿงช focused, integration, contract, round-trip
45
+ โ”œโ”€โ”€ scripts/ # ๐ŸŸฃ coverage, golden, package, release gates
46
+ โ””โ”€โ”€ docs/ # ๐Ÿ“– this documentation
47
+ ```
48
+
49
+ `km-api@^0.4.1` is consumed from npm as a peer and development dependency
50
+ (D-15). Generated endpoint files and the golden typecheck use that published
51
+ surface directly; there is no vendored copy or package swap remaining.
52
+
53
+ YAML input (v0.2.x, D-16) is parsed by the owned, deterministic parser in
54
+ `src/conversions/yaml.ts` (YAML 1.2 core-schema scalars, block/flow
55
+ collections, quoted and block scalars, anchors/aliases/`<<` merge keys,
56
+ single-document streams). It stays pure (P-3) and adds no runtime dependency
57
+ (D-11); every rejection is a typed `ZOPIA_SPEC_INVALID_YAML`.
58
+
59
+ File-path inputs additionally resolve **same-folder external `$ref`s** before
60
+ normalization (v0.2.x, D-17): `src/conversions/openapi-external-ref.ts` bundles
61
+ references like `other.yaml#/pointer` (plus `other.json`/`other.yml`/`./โ€ฆ`
62
+ spellings and whole-file targets) inline, with each sibling file read once
63
+ (P-1), bundled content deep-cloned, and nested cross-file references resolved
64
+ against their owning file. Targets outside the spec folder (URLs, absolute
65
+ paths, `../`, subdirectories) still fail with `ZOPIA_REF_EXTERNAL`, exactly
66
+ like external references in non-file inputs.
67
+
68
+ > ๐Ÿ“ Production modules use `kebab-case.ts`; `src/index.ts` is the package-root
69
+ > re-export surface. Focused and integration tests live under `tests/`, with
70
+ > dedicated `tests/contract/` and `tests/roundtrip/` suites. See
71
+ > [Standards โ†’ Naming](12-standards.md#-naming).
72
+
73
+ ## ๐Ÿ”„ The pipeline
74
+
75
+ Conversion work is split between in-memory transforms and explicit adapters.
76
+ Engines โ‘ /โ‘ก are in-memory; the public engine โ‘ข/โ‘ฃ wrappers own documented file
77
+ reads, guarded generated-tree writes, trusted module imports, and warning
78
+ collection. Process arguments/output remain in the CLI.
79
+
80
+ ```mermaid
81
+ flowchart TB
82
+ subgraph IN ["โ‘ข OpenAPI โ†’ api docs"]
83
+ A["object ยท JSON/YAML text ยท .json/.yaml path"] --> B["normalizeOpenApiDocument()"]
84
+ B --> C["collectOpenApiOperations()"]
85
+ C --> D["buildOpenApiOperationIR() + extractOperationContracts()"]
86
+ D --> E["planApiDocsFiles()"]
87
+ E --> F["render endpoints/components"]
88
+ F --> G["createZopiaManifest()"]
89
+ G --> H["guarded writes + stale-owned cleanup"]
90
+ H --> I["๐Ÿ“‚ api_docs/**"]
91
+ end
92
+
93
+ subgraph OUT ["โ‘ฃ api docs โ†’ OpenAPI"]
94
+ J["๐Ÿ“‚ api_docs/**"] --> K["validate manifest + owned paths"]
95
+ K --> L["trusted import of endpoint/component .ts"]
96
+ L --> M["runtime Zod โ†’ schema serialization"]
97
+ M --> N["apply refs + manifest overlays"]
98
+ N --> O["dialect translation + canonical document"]
99
+ end
100
+ ```
101
+
102
+ The manifest boundary in `src/conversions/manifest-writer.ts` separates pure,
103
+ detached snapshot construction and canonical validation/serialization from
104
+ atomic filesystem output. Engine โ‘ข records source facts that generated Zod or
105
+ km-api values cannot carry; engine โ‘ฃ combines those snapshots with imported
106
+ runtime values so developer edits remain authoritative where representable.
107
+
108
+ Round-trip stability does not depend on one repository-wide `ApiModel`. The
109
+ implemented boundaries use a validated document envelope, an operation-level
110
+ IR for generation, normalized operation contracts, and the versioned manifest
111
+ for reverse reconstruction. Each shape is narrower than the stage that consumes
112
+ it, and fixture properties verify their composition (T-11/R-409).
113
+
114
+ ## ๐Ÿงฌ The internal representations
115
+
116
+ The implementation uses stage-specific public shapes rather than one oversized
117
+ model. The generation path starts with the validated source envelope:
118
+
119
+ ```ts
120
+ interface NormalizedOpenApiDocument {
121
+ document: OpenApiDocument;
122
+ version: '2.0' | '3.0' | '3.1';
123
+ title?: string;
124
+ versionString?: string;
125
+ }
126
+ ```
127
+
128
+ Each collected operation is then narrowed to the data endpoint rendering needs:
129
+
130
+ ```ts
131
+ interface OpenApiOperationIR {
132
+ path: string;
133
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'PATCH' | 'TRACE';
134
+ pathShape: string;
135
+ operationId: string;
136
+ summary?: string;
137
+ description?: string;
138
+ tags: string[];
139
+ deprecated: boolean;
140
+ security?: unknown[];
141
+ operation: Record<string, any>;
142
+ parameters: any[];
143
+ document: OpenApiDocument;
144
+ }
145
+
146
+ interface OperationContracts {
147
+ parameters: Array<{
148
+ name: string;
149
+ in: 'path' | 'query' | 'header' | 'cookie';
150
+ required: boolean;
151
+ schema?: unknown;
152
+ }>;
153
+ requestBody?: { contentType: string; schema?: unknown; required: boolean };
154
+ responses: Array<{
155
+ status: string;
156
+ description: string;
157
+ contentType?: string;
158
+ schema?: unknown;
159
+ }>;
160
+ }
161
+ ```
162
+
163
+ Reverse conversion is anchored by `ZopiaManifest`, not by a hidden in-memory
164
+ model. It records source dialect/frame metadata, generated file ownership,
165
+ operation snapshots, component schemas, source path order/empty Path Items,
166
+ explicit schema-container presence, local-reference placements, and restoration
167
+ overlays. Current writer shapes are validated before serialization;
168
+ the reader retains explicit compatibility allowances for older optional fields.
169
+
170
+ ### ๐Ÿ” Canonical order (determinism, P-1)
171
+
172
+ | ๐Ÿ“ฆ Where | ๐Ÿ“ Order |
173
+ | --- | --- |
174
+ | collected operations | source path order; fixed method order `get, post, put, delete, head, options, patch, trace` |
175
+ | public generated-file result | lexical order by portable relative `path` |
176
+ | runtime schema properties | document order (JSON object key order of the source) |
177
+ | generated TypeScript schema maps/literal-object keys | lexical order (reverse restores source `required` order only while membership is unchanged) |
178
+ | OpenAPI output document | `openapi, info, servers, security, tags, paths, components, externalDocs` |
179
+ | path keys inside `paths` | sorted by path string |
180
+
181
+ > ๐Ÿ“Œ **Rule R-401** โ€” anywhere zopia *creates* a list or object, the order above
182
+ > applies. Anywhere zopia *mirrors* source data (runtime properties, params),
183
+ > the source order applies. Generated TypeScript canonicalizes schema-map and
184
+ > literal-object keys so parsing a canonical manifest cannot change the next
185
+ > generated tree. No `Date.now()`, no `Math.random()` anywhere in the package
186
+ > (P-1). Value-level normalizations (Zod sentinel bounds, const-union โ†’
187
+ > `enum`, the `io` input/output split for `required`/`default`) live with the
188
+ > engines that apply them โ€” see
189
+ > [Conversions โ†’ R-615 / R-618 / R-654](06-conversions.md).
190
+
191
+ ## ๐Ÿ”— The reference graph
192
+
193
+ `$ref` is a graph, and it can cycle (e.g. `Comment.replies โ†’ Comment`):
194
+
195
+ ```mermaid
196
+ flowchart LR
197
+ Post["๐Ÿ“„ Post"] -->|"$.ref #/components/schemas/Author"| Author["๐Ÿ‘ค Author"]
198
+ Author -->|"$.ref #/components/schemas/Address"| Address["๐Ÿ“ Address"]
199
+ Comment["๐Ÿ’ฌ Comment"] -->|"$.ref #/components/schemas/Comment"| Comment
200
+ ```
201
+
202
+ **Implemented flow** (`openapi-ref.ts`, generation, and manifest modules):
203
+
204
+ 1. ๐Ÿงท **Bundle external refs** (file inputs only, D-17) โ€” resolve same-folder
205
+ external references inline first so every later step sees one document.
206
+ 2. ๐Ÿ›‘ **Preflight** โ€” walk source values, reject remaining external refs,
207
+ validate local pointer escapes, and report missing targets with exact
208
+ locations.
209
+ 3. ๐Ÿ”— **Resolve operation refs** โ€” path-item and parameter chains use per-chain
210
+ seen sets, so malformed and circular non-schema references fail explicitly.
211
+ 4. ๐Ÿงฉ **Collect component dependencies** โ€” rendering finds schema-component
212
+ targets while excluding literal/example data that merely contains `$ref` text.
213
+ 5. ๐Ÿงต **Render schemas** โ€” engine โ‘ก's local-definition state and component
214
+ dependency reachability detect recursive edges; self and mutual cycles become
215
+ `z.lazy()` references.
216
+ 6. ๐Ÿ“ฆ **Record identity** โ€” the manifest stores original reference placements;
217
+ reverse conversion combines those records with imported runtime schema
218
+ identity to restore local `$ref`s.
219
+
220
+ > ๐Ÿ“Œ **Rule R-402** โ€” circular schema components remain executable through
221
+ > `z.lazy()`, while linear refs become direct references/imports. **Scope:**
222
+ > graph nodes are schema components only. Reusable non-schema objects (Swagger
223
+ > 2.0 global `parameters`/`responses`, OpenAPI 3
224
+ > `components.parameters`/`responses`) *also* get their own component modules in
225
+ > components mode (v0.2.x โ€” D-18): a module holds only the declaration's derived
226
+ > schema, and in-source declarations plus use-site `$ref` placements restore
227
+ > verbatim on reverse conversion while the declaration refreshes from the
228
+ > current module. Bare `$ref` use sites import from the per-kind barrels; merged
229
+ > `$ref`-sibling forms still resolve at use sites for generated runtime configs.
230
+
231
+ ## ๐Ÿงฎ Schema reuse within generated files
232
+
233
+ Default mode keeps every endpoint self-contained: each request/parameter/response
234
+ schema occurrence is rendered in place. Engine โ‘ก may build a local-definition
235
+ closure inside an expression when resolving `$defs` or inlined component refs;
236
+ it does not hoist structurally identical endpoint contracts into shared top-level
237
+ constants.
238
+
239
+ > ๐Ÿ“Œ **Rule R-403** โ€” schema reuse is explicit, not inferred from structural
240
+ > equality. Default mode independently inlines each contract occurrence. With
241
+ > `useComponentAsReference: true`, each referenced component export is imported
242
+ > at most once per endpoint and reused wherever that identity occurs.
243
+
244
+ ## ๐Ÿ›‘ Error model
245
+
246
+ All errors crossing a zopia boundary extend one base class โ€” **no raw `Error`, no thrown strings** (standards โ†’ Errors). `ZOPIA_ERROR_CODES` is the immutable runtime catalogue and the source of the `ZopiaErrorCode` union; `isZopiaError()` narrows unknown failures and `asZopiaError()` preserves an existing typed error or attaches a lower-level failure as `cause`.
247
+
248
+ ```ts
249
+ export class ZopiaError extends Error {
250
+ /** ๐Ÿ†” Stable machine-readable code, e.g. 'ZOPIA_REF_NOT_FOUND'. */
251
+ readonly code: ZopiaErrorCode;
252
+ /** ๐Ÿ“ JSON-pointer, option name, or file location, when discoverable. */
253
+ readonly at?: string;
254
+ /** ๐Ÿ’ก Actionable, human-readable suggestion (always populated). */
255
+ readonly hint: string;
256
+ /** ๐Ÿ”— Original parser, import, or filesystem failure, when translated. */
257
+ readonly cause?: unknown;
258
+ }
259
+ ```
260
+
261
+ | ๐Ÿ†” Code | ๐Ÿ“ Where | ๐Ÿ’ฅ When | ๐Ÿ’ก Hint pattern |
262
+ | --- | --- | --- | --- |
263
+ | `ZOPIA_CONFIG_INVALID` | public options / CLI | an argument, option, or option combination is invalid | "correct the invalid option or argument" |
264
+ | `ZOPIA_DOCS_IMPORT_FAILED` | engine โ‘ฃ | generated modules cannot load, export one expected value, or serialize edited runtime schemas | "fix or regenerate the affected generated module" |
265
+ | `ZOPIA_DOCS_MANIFEST_MISMATCH` | engine โ‘ฃ preflight | a manifest-owned endpoint/component file is missing, renamed, or not a regular file | "regenerate the tree or restore its generated files" |
266
+ | `ZOPIA_DOCS_MISSING_MANIFEST` | engine โ‘ฃ entry | no `.zopia-manifest.json` exists at the selected path | "generate api docs first or pass the manifest path" |
267
+ | `ZOPIA_FS_OUTSIDE_OUTDIR` | generation guard | a generated path escapes `outDir` or traverses an unsafe ancestor | "keep generated paths inside the output directory" |
268
+ | `ZOPIA_FS_WRITE_FAILED` | writers / CLI | output inspection, directory creation, cleanup, or writing fails | "check the output path, permissions, and available disk space" |
269
+ | `ZOPIA_MANIFEST_INVALID` | manifest writer/reader | manifest JSON or metadata violates `zopia:manifest@1` | "regenerate the manifest or fix its invalid metadata" |
270
+ | `ZOPIA_REF_EXTERNAL` | external-ref bundling / reference validation | `$ref` escapes the spec folder, or a non-file input points to another file | "keep external targets next to the spec file" |
271
+ | `ZOPIA_REF_NOT_FOUND` | reference validation | a local `$ref` is malformed, circular where unsupported, or unresolved | "check that the local JSON Pointer target exists" |
272
+ | `ZOPIA_SCHEMA_INVALID` | engines โ‘ /โ‘ก | the Zod or JSON Schema input cannot be converted | "provide a valid Zod or JSON Schema value" |
273
+ | `ZOPIA_SPEC_INVALID` | OpenAPI validation | the parsed document violates the supported Swagger/OpenAPI shape | "fix the invalid Swagger/OpenAPI document" |
274
+ | `ZOPIA_SPEC_INVALID_JSON` | JSON entry points | source text is unreadable or not valid JSON | "provide readable, valid JSON" |
275
+ | `ZOPIA_SPEC_INVALID_YAML` | YAML entry points | source text is unreadable, malformed/unsupported YAML, or holds a non-JSON value | "provide readable, valid YAML" |
276
+ | `ZOPIA_SPEC_MISSING_PATHS` | normalizers | the document has no object-valued `paths` | "add a paths object" |
277
+ | `ZOPIA_SPEC_PATH_REF` | operation collection | a path-item reference is invalid or circular | "use a valid local path-item reference" |
278
+ | `ZOPIA_SPEC_UNSUPPORTED_VERSION` | normalization | neither Swagger 2.0 nor OpenAPI 3.0/3.1 is selected | "use Swagger 2.0, OpenAPI 3.0, or OpenAPI 3.1" |
279
+ | `ZOPIA_WARNING_INVALID` | warnings pipeline | a warning iterable/code/location/message is malformed | "provide a valid warning code, location, and message" |
280
+
281
+ > ๐Ÿ“Œ **Rule R-404** โ€” every error is thrown as a `ZopiaError` with a stable
282
+ > code, a location (`at`) when discoverable, and a `hint`. Tests assert on
283
+ > `code`, never on message text.
284
+
285
+ ## โš ๏ธ Warning model
286
+
287
+ Warnings are non-fatal conversion diagnostics. Every public engine uses the
288
+ same `ZopiaWarning` contract and stable `ZopiaWarningCode` union:
289
+
290
+ ```ts
291
+ interface ZopiaWarning {
292
+ code: ZopiaWarningCode;
293
+ at?: string; // escaped RFC 6901 JSON Pointer, including leading #
294
+ message: string;
295
+ }
296
+ ```
297
+
298
+ | ๐Ÿ†” Stable code | ๐Ÿ“ Meaning |
299
+ | --- | --- |
300
+ | `ZOPIA_WARN_UNREPRESENTABLE` | a Zod node cannot be represented in the selected schema dialect |
301
+ | `ZOPIA_WARN_INVALID_SCHEMA` | a malformed JSON Schema keyword is ignored or approximated |
302
+ | `ZOPIA_WARN_CUSTOM_FORMAT`, `ZOPIA_WARN_CONTENT_ENCODING`, `ZOPIA_WARN_INT64` | a string/numeric format or encoding has no exact runtime equivalent |
303
+ | `ZOPIA_WARN_LEGACY_EXCLUSIVE_BOUND` | a legacy boolean exclusive bound requires normalization |
304
+ | `ZOPIA_WARN_ONE_OF`, `ZOPIA_WARN_NOT`, `ZOPIA_WARN_UNIQUE_ITEMS`, `ZOPIA_WARN_FROZEN_SUBTREE` | an applicator or refinement needs an approximation or frozen manifest restoration |
305
+ | `ZOPIA_WARN_REF` | a recoverable schema-reference conversion cannot be exact |
306
+ | `ZOPIA_WARN_MULTI_CONTENT`, `ZOPIA_WARN_SERVER_VARIABLES`, `ZOPIA_WARN_WEBHOOKS` | an OpenAPI document fact has no direct generated-code representation |
307
+ | `ZOPIA_WARN_STALE_TREE` | regeneration found source/config drift, missing owned files, manifest disablement, or invalid existing metadata |
308
+ | `ZOPIA_WARN_DEFAULT_INFO`, `ZOPIA_WARN_DEFAULT_SECURITY` | reverse conversion synthesized documented fallback metadata or security |
309
+ | `ZOPIA_WARN_DIALECT_DOWNGRADE` | OpenAPI 3.1-only content is omitted from 3.0 output |
310
+
311
+ The shared collector validates codes, collapses line breaks in messages,
312
+ deduplicates identical diagnostics, and sorts by location, code, then message.
313
+ Nested engine warnings are rebased rather than string-concatenated ad hoc, so
314
+ `at` always identifies the affected source or output node. Engine โ‘  and engine
315
+ โ‘ฃ invoke `onWarning` once per normalized warning; engines โ‘กโ€“โ‘ฃ also return
316
+ normalized warning arrays. Engine โ‘ก mirrors losses with the canonical marker
317
+ `// @zopia:warn CODE subject โ€” message (pointer)`. The CLI renders the same
318
+ warning as `Warning: CODE pointer: message` on **stderr**, leaving reverse JSON
319
+ on stdout parseable.
320
+
321
+ ## ๐Ÿ›ก๏ธ Safety & boundaries
322
+
323
+ | # | Rule | Where enforced |
324
+ | --- | --- | --- |
325
+ | R-405 | **Pure core** โ€” schema/operation transforms work on in-memory values; documented file input, generated-tree writes/imports, and process output stay in public adapters and the CLI | conversion modules + `cli-command.ts` |
326
+ | R-406 | **outDir guard** โ€” every generated path is canonicalized and verified to stay inside `outDir`; regeneration refuses symlinked path ancestors, and manifest temporary writes use exclusive creation so stale symlinks cannot redirect output. Obsolete cleanup trusts only a fully validated manifest and never recursively deletes an output root | `api-docs-generate.ts` + manifest boundaries |
327
+ | R-407 | **Trusted-input contract** โ€” engine โ‘ฃ imports generated `.ts` files (executes them). This is by design (D-08) and only for trees that carry a valid zopia manifest | `manifest-to-openapi.ts` |
328
+ | R-408 | **No silent loss** โ€” every lossy/unsupported conversion produces a normalized `ZopiaWarning` (D-12): `{ code, at?, message }` (shape fixed by R-144). Public wrappers return or callback each warning; engine โ‘ก and generated api-doc files mirror schema warnings as canonical `// @zopia:warn โ€ฆ` comments; CLI diagnostics go only to stderr | every engine + CLI |
329
+ | R-409 | **Idempotent regeneration** โ€” re-running engine โ‘ข with identical input + options produces byte-identical output; regenerating source-preserving engine โ‘ฃ output does too. Fixture properties cover Swagger 2.0 and OpenAPI 3.0/3.1 across layout/component modes, while stale source/config/incomplete-tree state is warned and repaired and only obsolete manifest-owned files are pruned; engine โ‘ฃ output is canonical (R-401) | round-trip + staleness tests |
330
+
331
+ ## ๐Ÿ“ Performance
332
+
333
+ - ๐Ÿงฎ Operation and local-reference passes are deterministic traversals with
334
+ explicit seen sets for reference chains.
335
+ - ๐ŸŒณ Component source is rendered and validated before its files are written;
336
+ endpoint files are then rendered and written in planned order.
337
+ - ๐Ÿงต Cycle-aware definition/reachability state terminates recursive schemas and
338
+ emits lazy edges rather than expanding them forever.
339
+ - ๐Ÿ“ฆ Optional component extraction/reference imports avoid repeated component
340
+ definitions; default mode deliberately favors self-contained endpoint files.
341
+
342
+ ## ๐Ÿ”— Next
343
+
344
+ - ๐Ÿงฉ Vocabulary used above โ†’ [Concepts](05-concepts.md)
345
+ - ๐Ÿ”„ Engine-by-engine rules โ†’ [Conversions](06-conversions.md)
@@ -0,0 +1,239 @@
1
+ # ๐Ÿงฉ Concepts & Glossary
2
+
3
+ The vocabulary of this project, pinned to exact versions. If a term is used in
4
+ any other zopia document, it means what it means here.
5
+
6
+ ## ๐Ÿ“š The big picture
7
+
8
+ ```mermaid
9
+ flowchart TB
10
+ S1["๐Ÿ“„ Swagger 2.0<br/>(2016 spec)"] ---|dialect| JS1["๐Ÿ“ JSON Schema draft-04-ish"]
11
+ S2["๐Ÿ“„ OpenAPI 3.0"] ---|dialect| JS2["๐Ÿ“ JSON Schema draft-07 subset<br/>+ nullable"]
12
+ S3["๐Ÿ“„ OpenAPI 3.1"] ---|dialect| JS3["๐Ÿ“ JSON Schema 2020-12 (identical)"]
13
+ JS3 ---|same format| Z1["โš›๏ธ Zod v4 schemas"]
14
+ S2 --> C["๐Ÿ“‚ api docs<br/>(.ts ยท km-api ยท zod v4)"]
15
+ S1 --> C
16
+ S3 --> C
17
+ ```
18
+
19
+ > ๐Ÿ’ก **Key fact** โ€” OpenAPI 3.1's *Schema Object* **is** JSON Schema 2020-12
20
+ > ("the standards of openapi are the same as json schema"). That identity is
21
+ > what lets zopia treat 3.1 schemas and JSON Schema as one format.
22
+
23
+ ## ๐Ÿ“– Terms
24
+
25
+ ### ๐Ÿ“„ Swagger 2.0
26
+
27
+ The 2016 API description format. Recognized by the top-level key
28
+ `"swagger": "2.0"`.
29
+
30
+ | ๐Ÿงฉ Piece | ๐Ÿ“ In Swagger 2.0 |
31
+ | --- | --- |
32
+ | ๐Ÿ“ Schemas | `definitions` (referenced as `#/definitions/X`) |
33
+ | ๐ŸŒ Hosting | `host` + `basePath` + `schemes` |
34
+ | ๐Ÿ“ฆ Request bodies | a special parameter with `"in": "body"` (has `schema`) |
35
+ | ๐Ÿงพ Form data | parameters with `"in": "formData"` (media type from `consumes`) |
36
+ | ๐Ÿช Cookie parameters | โŒ none (introduced in OpenAPI 3.0) |
37
+ | ๐Ÿ” Security | `securityDefinitions` โ†’ mapped to OpenAPI 3 `securitySchemes` |
38
+ | ๐Ÿ“ Schema dialect | draft-04-ish subset (no `nullable`, no `const`, โ€ฆ) |
39
+ | ๐Ÿšฆ Responses | `responses` map โ€” each response may carry a single `schema` |
40
+
41
+ ### ๐Ÿ“„ OpenAPI 3.0
42
+
43
+ Recognized by `"openapi": "3.0.x"`.
44
+
45
+ | ๐Ÿงฉ Piece | ๐Ÿ“ In OpenAPI 3.0 |
46
+ | --- | --- |
47
+ | ๐Ÿ“ Schemas | `components.schemas` (referenced as `#/components/schemas/X`) |
48
+ | ๐ŸŒ Hosting | `servers: [{ url, variables? }]` |
49
+ | ๐Ÿ“ฆ Request bodies | dedicated `requestBody` object with a `content` map (media type โ†’ schema) |
50
+ | ๐Ÿช Cookie parameters | โœ… `"in": "cookie"` |
51
+ | ๐ŸŒ— Nullable | `nullable: true` keyword on schemas |
52
+ | ๐Ÿ“ Schema dialect | draft-07 **subset** + OpenAPI extensions (`nullable`, `discriminator`, `example`, `deprecated`, โ€ฆ) |
53
+
54
+ ### ๐Ÿ“„ OpenAPI 3.1
55
+
56
+ Recognized by `"openapi": "3.1.x"`. Same shape as 3.0, but:
57
+
58
+ - ๐Ÿ“ Schema dialect = **JSON Schema 2020-12** (full equality)
59
+ - ๐ŸŒ— Nullability via `"type": ["string", "null"]` โ€” `nullable` is removed
60
+ - ๐Ÿ“ธ `examples` (array) replaces `example`; `const` is allowed
61
+ - ๐Ÿงฎ `exclusiveMinimum/Maximum` are **numbers** (were booleans in draft-04/07)
62
+ - ๐Ÿช adds `webhooks` (not paths): v0.1.0 warns, preserves them in the manifest,
63
+ and restores them for 3.1 output but defers webhook endpoint files; valid local
64
+ path-item `$ref`s are supported and round-trip
65
+ - ๐Ÿ“Œ zopia treats 3.0 and 3.1 with the same normalizer + a small dialect shim
66
+
67
+ ### ๐Ÿ“ JSON Schema
68
+
69
+ The vocabulary used to describe data shape (`type`, `properties`, `required`,
70
+ `enum`, `oneOf`, โ€ฆ). zopia's engine โ‘ก accepts the union of keywords found in
71
+ the three dialects above and emits warnings for anything it cannot represent
72
+ in Zod (D-12). The canonical dialect of engine โ‘ 's output is **2020-12**.
73
+
74
+ ### ๐Ÿ”— $ref (reference)
75
+
76
+ A pointer like `"#/components/schemas/User"` that makes one schema *use*
77
+ another. Three shapes matter for zopia:
78
+
79
+ ```jsonc
80
+ // 1๏ธโƒฃ operation โ†’ component (responses, requestBody, parameters)
81
+ { "schema": { "$ref": "#/components/schemas/User" } }
82
+
83
+ // 2๏ธโƒฃ component โ†’ component (a part of another component)
84
+ { "properties": { "address": { "$ref": "#/components/schemas/Address" } } }
85
+
86
+ // 3๏ธโƒฃ component โ†’ itself (cycle โ€” always allowed, becomes z.lazy())
87
+ { "properties": { "replies": { "type": "array", "items": { "$ref": "#/components/schemas/Comment" } } } }
88
+ ```
89
+
90
+ > ๐Ÿ“Œ **Rule R-501** โ€” zopia resolves *internal* refs natively (refs stay
91
+ > inside the bundled document). Spec file paths additionally bundle
92
+ > **same-folder** external refs before processing (D-17); everything else โ€”
93
+ > URLs, other folders, external refs in non-file inputs โ€” stays a
94
+ > `ZOPIA_REF_EXTERNAL` error.
95
+
96
+ ### โš›๏ธ Zod v4
97
+
98
+ The schema library (validation at runtime + types at compile time). zopia
99
+ depends on Zod **v4** idioms โ€” *not* v3 โ€” throughout:
100
+
101
+ | ๐Ÿงฉ v4 feature | ๐Ÿ“ Used by zopia |
102
+ | --- | --- |
103
+ | `z.toJSONSchema(schema, { target })` | engine โ‘  โ€” built-in, no third-party converter (D-03); targets: `draft-2020-12` (default), `draft-07`, `draft-04`, `openapi-3.0` |
104
+ | `z.fromJSONSchema(schema)` | โš ๏ธ experimental in Zod โ€” used **only** as a cross-check in tests, never in the output path (D-04) |
105
+ | top-level format schemas | `z.email()`, `z.uuid()`, `z.url()`, `z.hostname()`, `z.ipv4()`, `z.ipv6()` โ€” โš ๏ธ each emits `format` **plus a strict `pattern`** (engine โ‘  strips the redundant pair โ€” R-618); `z.url()` emits `format: "uri"` |
106
+ | ISO builders | `z.iso.datetime()`, `z.iso.date()`, `z.iso.time()`, `z.iso.duration()` โ€” โš ๏ธ `z.iso.time()` emits a pattern but **no `format` key** (round-trip needs the manifest overlay โ€” R-635) |
107
+ | *(no v4 API for arbitrary formats)* | custom `format` values โ†’ `z.string()` + warning + manifest overlay preserving the format verbatim (R-627) |
108
+ | `z.enum([...])`, `z.literal(v)` | `enum` / `const` keywords |
109
+ | `z.union([...])`, `z.discriminatedUnion(key, [...])` | `oneOf` / `anyOf` (with the `discriminator` heuristic) |
110
+ | `z.intersection(a, b)` | `allOf` |
111
+ | `z.tuple([...])` | `prefixItems` / tuple `items` |
112
+ | `z.lazy(() => โ€ฆ)` | circular `$ref`s (R-402) |
113
+ | `.meta({ title, description, examples, โ€ฆ })` / `z.globalRegistry` | **all** metadata fields are copied verbatim into the JSON Schema output (verified); โš ๏ธ never set the `id` key โ€” it triggers `$def` extraction |
114
+ | `z.object({โ€ฆ})` + `.strict()` / `.catchall(s)` / `.optional()` / `.default(v)` | objects, `additionalProperties`, `required`, `default` |
115
+ | `.min()` / `.max()` / `.int()` / `.regex()` / `.multipleOf()` | string/number/array constraints |
116
+
117
+ ### ๐Ÿงฑ km-api
118
+
119
+ The user's endpoint-definition package โ€” **the make function of this project's
120
+ generated code**. zopia targets **km-api `^0.4.1` (0.4.x)** and generates the
121
+ following conceptual shape (formatting abridged):
122
+
123
+ ```ts
124
+ import { makeApiConfig } from 'km-api';
125
+
126
+ const getUser = makeApiConfig({
127
+ method: 'GET', // ๐Ÿงญ IMethod โ€” HTTP method
128
+ pathShape: '/admin/users/{id}', // ๐Ÿ›ฃ๏ธ IPath โ€” OpenAPI {param} syntax (D-05)
129
+ operationId: 'getUser', // ๐Ÿ†” preserved in config + export name
130
+ auth: 'YES', // ๐Ÿ” 'YES' | 'NO'
131
+ responseContentType: 'application/json',
132
+ summary: 'Get user by ID', // ๐Ÿ“
133
+ description: 'Retrieves โ€ฆ', // ๐Ÿ“ (Markdown)
134
+ tags: ['#admin', '#users'], // ๐Ÿท๏ธ km-api convention: '#' prefix
135
+ request: {
136
+ body: z.any(), // ๐Ÿ“ฆ required field โ€” z.any() โ‡” no body
137
+ params: z.object({ id: z.uuid() }), // ๐Ÿ›ฃ๏ธ path params
138
+ query: z.object({}), // โ“
139
+ headers: z.object({}), // ๐ŸŽฉ
140
+ cookies: z.object({}), // ๐Ÿช
141
+ },
142
+ response: { // ๐Ÿšฆ status code โ†’ Zod schema
143
+ 200: z.object({ id: z.uuid(), name: z.string() }),
144
+ 404: z.object({ message: z.string() }),
145
+ },
146
+ });
147
+ ```
148
+
149
+ Helper methods on the result (`makeFullPath`, `makeOpenApiPathShape`,
150
+ `convertResponseType`, โ€ฆ) are available to consumers for free โ€” zopia never
151
+ needs them at generation time, and engine โ‘ฃ uses `makeOpenApiPathShape()` to
152
+ normalize paths back to `{param}` form.
153
+
154
+ > ๐Ÿ“Œ **Rule R-502** โ€” generated files import **only** `zod` and `km-api`.
155
+ > No zopia runtime is imported by generated code. Component files are
156
+ > generated as standalone files, and exact endpoint component references can
157
+ > import through the barrel. Nested component references are recursively rendered, including nested objects, arrays, compositions, nullable schemas, enums, constants, and additional-property schemas. Direct and mutual cyclic component graphs use lazy Zod references.
158
+
159
+ ### ๐Ÿ“ km-api's type surface (verified against published `0.4.1`)
160
+
161
+ `makeApiConfig` is a **type-level factory** โ€” it does no runtime validation, so
162
+ the generated tree's contract is that it **typechecks** against the installed
163
+ published package (D-14; the golden-tree contract test enforces this, R-126):
164
+
165
+ | ๐Ÿงฉ Field | ๐Ÿ“ Published km-api 0.4.1 / zopia contract |
166
+ | --- | --- |
167
+ | `method` (`IMethod`) | all **8** standard methods โ€” `get/post/put/delete/head/options/patch/trace` (each case-insensitive: `GET`, `Get`, โ€ฆ) |
168
+ | `response` keys | standard statuses, **any valid custom `100`โ€“`599` code** (`419`, `499`, `512`, โ€ฆ), OpenAPI `1XX`โ€“`5XX` ranges, and the **`default`** key; all are pinned by generated compilation and validated against the source/output dialect |
169
+ | `responseContentType` / `requestContentType` | km-api's declarations enumerate known MIME values, while OpenAPI media-type maps are open; zopia emits every exact string and applies a narrow type-only assertion at this package boundary, preserving the runtime value and reverse conversion |
170
+ | `operationId` | any string โ€” a real km-api field in 0.4.1; zopia emits it and reuses it as the export identifier (R-732) |
171
+
172
+ Other fixed shapes (source-verified): `ITags` = strings **prefixed with `#`**;
173
+ `IPath` = string starting with `/`; `auth`, `disable`, and the semantically
174
+ separate `deprecated` field use `'YES' | 'NO'`; `request.body` is required (any
175
+ Zod schema), and `params/query/headers/cookies` are required Zod objects. The
176
+ remaining km-api gaps โ€” **per-parameter metadata**
177
+ (`style`, `explode`, `allowEmptyValue`, `deprecated`, `example`) and **response
178
+ `headers`** โ€” have no home in km-api; zopia preserves them in the manifest
179
+ (overlay / `responseOverlay`, R-635/R-754).
180
+
181
+ > ๐Ÿ”— **Where km-api lives.** zopia consumes the published `km-api@^0.4.1`
182
+ > npm package. No Git submodule or unpublished commit is required (D-15).
183
+
184
+ ### ๐Ÿ“‚ api docs
185
+
186
+ The generated artifact: an `api_docs/` directory containing one `index.ts` per
187
+ endpoint (plus optional `components/**` and the manifest). Defined fully in
188
+ [API docs format](07-api-docs.md).
189
+
190
+ ### ๐Ÿ“ฆ .zopia-manifest.json
191
+
192
+ The hidden metadata file written into `api_docs/` (D-06). It is what makes
193
+ engine โ‘ฃ lossless: exact paths & methods per file, spec identity
194
+ (kind/title/version/sha256), servers, tags, security schemes, the **full
195
+ component schemas**, **`$ref` placement** (per-API ref pointers), and
196
+ **non-representable facts** (overlay entries: titles, examples, custom
197
+ formats, unsupported keywords). See
198
+ [API docs format โ†’ The manifest](07-api-docs.md).
199
+
200
+ ### ๐Ÿงฌ Stage-specific representations
201
+
202
+ Engine โ‘ข narrows a validated document into operation-level IR and normalized
203
+ request/response contracts. Engine โ‘ฃ combines imported runtime values with the
204
+ versioned manifest instead of sharing one repository-wide model with engine โ‘ข.
205
+ See [Architecture โ†’ The internal representations](04-architecture.md#-the-internal-representations).
206
+
207
+ ### ๐Ÿ” Round-trip
208
+
209
+ For a spec `S`: `openApiToApiDocs(S) โ†’ apiDocsToOpenApi(...) โ‰ˆ S` โ€” equal after
210
+ **canonicalization** (R-401 order, whitespace-independent JSON equality, and
211
+ documented metadata moves to the manifest). Round-trips are *tested as
212
+ properties*, not by eye (see [Testing](11-testing.md#-round-trip-property-tests)).
213
+
214
+ ## โš–๏ธ Dialect comparison (the table that answers most "why?" questions)
215
+
216
+ | ๐Ÿงฉ Concern | Swagger 2.0 | OpenAPI 3.0 | OpenAPI 3.1 |
217
+ | --- | --- | --- | --- |
218
+ | ๐Ÿท๏ธ Version key | `swagger: "2.0"` | `openapi: "3.0.x"` | `openapi: "3.1.x"` |
219
+ | ๐Ÿ“ Schema home | `definitions` | `components.schemas` | `components.schemas` |
220
+ | ๐Ÿ“ Schema dialect | draft-04-ish | draft-07 subset + extensions | **JSON Schema 2020-12** |
221
+ | ๐ŸŒ Servers | `host`+`basePath`+`schemes` | `servers[]` | `servers[]` |
222
+ | ๐Ÿ“ฆ Body | param `in: body` | `requestBody.content` | `requestBody.content` |
223
+ | ๐Ÿงพ FormData | param `in: formData` | `requestBody.content['multipart/โ€ฆ' \| 'x-www-form-urlencoded']` | same as 3.0 |
224
+ | ๐Ÿช Cookie params | โŒ | โœ… | โœ… |
225
+ | ๐ŸŒ— Nullable | โŒ no standard way (draft-04 allows type arrays, but 2.0 tooling rarely supports them) | `nullable: true` | `type: ["โ€ฆ", "null"]` |
226
+ | ๐Ÿ“ธ Examples | `examples` (media-type map, legacy) | `example` (single) | `examples` (array) |
227
+ | ๐Ÿ” Security | `securityDefinitions` | `components.securitySchemes` | same as 3.0 |
228
+ | ๐Ÿšฆ Response schema | response.schema (single) | per media type | per media type |
229
+ | ๐Ÿงฎ exclusiveMinimum | boolean | boolean | **number** |
230
+
231
+ > ๐Ÿ“Œ **Rule R-503** โ€” the normalizers convert *everything* into the IR using
232
+ > the **OpenAPI-3.1-flavoured** representation (2020-12 schemas, numeric
233
+ > exclusive bounds, `type: [t, "null"]` for nullables). Dialect differences
234
+ > die at the IR boundary; they never leak into generated code.
235
+
236
+ ## ๐Ÿ”— Next
237
+
238
+ - ๐Ÿ”„ How the IR is produced and consumed โ†’ [Conversions](06-conversions.md)
239
+ - ๐Ÿ—๏ธ Where the IR lives in code โ†’ [Architecture](04-architecture.md)