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.
- package/CHANGELOG.md +354 -0
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/bin/zopia.js +20 -0
- package/docs/01-overview.md +94 -0
- package/docs/02-targets.md +55 -0
- package/docs/03-roadmap.md +205 -0
- package/docs/04-architecture.md +345 -0
- package/docs/05-concepts.md +239 -0
- package/docs/06-conversions.md +493 -0
- package/docs/07-api-docs.md +337 -0
- package/docs/08-components.md +223 -0
- package/docs/09-configuration.md +167 -0
- package/docs/10-usage.md +208 -0
- package/docs/11-testing.md +267 -0
- package/docs/12-standards.md +242 -0
- package/docs/README.md +42 -0
- package/docs/publish-workflow.yml.example +48 -0
- package/package.json +77 -0
- package/src/api-docs-navigation.ts +353 -0
- package/src/cli-command.ts +537 -0
- package/src/cli.ts +4 -0
- package/src/config.ts +190 -0
- package/src/conversions/api-docs-facade.ts +42 -0
- package/src/conversions/api-docs-generate.ts +567 -0
- package/src/conversions/api-docs-layout.ts +39 -0
- package/src/conversions/api-docs-plan.ts +130 -0
- package/src/conversions/api-docs-presets.ts +246 -0
- package/src/conversions/json-schema-to-zod.ts +931 -0
- package/src/conversions/manifest-staleness.ts +211 -0
- package/src/conversions/manifest-to-openapi.ts +1861 -0
- package/src/conversions/manifest-writer.ts +778 -0
- package/src/conversions/openapi-contracts.ts +333 -0
- package/src/conversions/openapi-external-ref.ts +233 -0
- package/src/conversions/openapi-ir.ts +74 -0
- package/src/conversions/openapi-ref.ts +38 -0
- package/src/conversions/openapi-to-api-docs-public.ts +466 -0
- package/src/conversions/openapi-to-api-docs.ts +203 -0
- package/src/conversions/openapi.ts +80 -0
- package/src/conversions/reverse-security.ts +68 -0
- package/src/conversions/yaml.ts +876 -0
- package/src/conversions/zod-to-json-schema.ts +536 -0
- package/src/diff.ts +353 -0
- package/src/errors.ts +114 -0
- package/src/index.ts +80 -0
- package/src/validation.ts +299 -0
- package/src/warnings.ts +164 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# π§ͺ Testing
|
|
2
|
+
|
|
3
|
+
> π― **T-13** β *tests in all scenarios, with Vitest, run with Bun.*
|
|
4
|
+
> Every rule in this project (R-β¦), every target (T-β¦), and every mapping row
|
|
5
|
+
> in [Conversions](06-conversions.md) is **pinned by at least one test**.
|
|
6
|
+
|
|
7
|
+
## π οΈ Toolchain
|
|
8
|
+
|
|
9
|
+
| π§© Piece | π Choice | π Why |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Runner | **Vitest 4.1.11** (exact pin) | requested standard (D-02); snapshots, V8 coverage, type-aware assertions |
|
|
12
|
+
| Runtime | **Bun** | runs the package, the tests, *and* the generated code (D-08) |
|
|
13
|
+
| Types | `tsc --noEmit` (`strict`) | the type-level test gate |
|
|
14
|
+
| Fixtures | plain JSON files under `tests/fixtures/` | specs are the unit of integration |
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bun run typecheck # β
strict TS
|
|
18
|
+
bun run test # π§ͺ vitest run (CI mode)
|
|
19
|
+
bun run test:watch # π vitest watch
|
|
20
|
+
bun run coverage # π vitest --coverage
|
|
21
|
+
bun run golden:update # πΈ regenerate golden trees deliberately (R-112)
|
|
22
|
+
bun run package:check # π¦ exact npm archive + isolated consumer smoke
|
|
23
|
+
bun run release:check # π’ frozen install + every release check
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The Bun gate is the release-level wrapper around these individual commands. It
|
|
27
|
+
also validates the pinned Bun version and `bun.lock`, executes the package binary,
|
|
28
|
+
proves reverse conversion can import freshly generated TypeScript under Bun,
|
|
29
|
+
and packs the exact npm artifact for an isolated offline install, package-root
|
|
30
|
+
import, and generate/reverse CLI smoke test. `prepublishOnly` delegates to this
|
|
31
|
+
same gate so local and publish-time validation cannot drift.
|
|
32
|
+
|
|
33
|
+
## π Test pyramid
|
|
34
|
+
|
|
35
|
+
| ποΈ Layer | π Where | π― What it proves |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| **Focused** | `tests/*.test.ts` | schema keywords, OpenAPI helpers, layouts, planning, manifests, warnings, and errors on in-memory values |
|
|
38
|
+
| **Integration** | top-level `tests/*generate*.test.ts`, `tests/*public*.test.ts`, and `tests/*to-openapi*.test.ts` | full engine runs: spec in β tree out (both modes/options); trusted generated tree in β spec out |
|
|
39
|
+
| **Round-trip** | `tests/roundtrip/**/*.test.ts` | property: `openapi(docs(spec)) β spec` and `zodSchema(zod(jsonSchema(zodSchema))) β schema` (see below) |
|
|
40
|
+
| **Golden files** | `tests/fixtures/expected/**` | byte-exact generated trees (determinism, P-1), regenerated deliberately and reviewed with their fixture inputs |
|
|
41
|
+
| **Contract** | `tests/contract/**/*.test.ts` | public API/JSDoc, scenario identifiers, quality/forbidden behavior, changelog history, golden output, npm artifact, release metadata, and documentation status |
|
|
42
|
+
|
|
43
|
+
> π **Rule R-111** β *no test touches the network*; *no test writes outside
|
|
44
|
+
> a per-test temp directory*. Every test-created directory goes through the
|
|
45
|
+
> shared `useTemporaryDirectories()` helper (`fs.mkdtemp` under `os.tmpdir()`),
|
|
46
|
+
> which removes all owned trees in `afterEach`; a hygiene contract rejects direct
|
|
47
|
+
> temp-directory factories in test files. The same contract rejects network,
|
|
48
|
+
> wall-clock, random, and host-locale behavior in tests.
|
|
49
|
+
|
|
50
|
+
## π§Ύ The scenario matrix
|
|
51
|
+
|
|
52
|
+
The suite **must** cover every cell. A cell is a *spec axis Γ an output axis*.
|
|
53
|
+
Each `S-β¦` identifier below appears in the executable test that covers it, and a
|
|
54
|
+
contract test compares the complete documented identifier set with the complete
|
|
55
|
+
Vitest source tree so a newly documented scenario cannot remain unimplemented:
|
|
56
|
+
|
|
57
|
+
### π Spec-input scenarios
|
|
58
|
+
|
|
59
|
+
| # | Scenario | π Rules pinned |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| S-01 | Swagger 2.0 β basic (definitions, body params, consumes/produces, securityDefinitions, host/basePath) | β’ v2 table |
|
|
62
|
+
| S-02 | Swagger 2.0 β `formData` (urlencoded *and* multipart) | R-503 |
|
|
63
|
+
| S-03 | Swagger 2.0 β primitive params (`type`/`format`/`enum` inline) | v2 param normalization |
|
|
64
|
+
| S-04 | OpenAPI 3.0 β cookie params, requestBody, `nullable: true`, single `example` | β’ v3 table |
|
|
65
|
+
| S-05 | OpenAPI 3.1 β `type: [t, "null"]`, `const`, numeric `exclusiveMinimum`, `prefixItems`, `examples` array | R-503 |
|
|
66
|
+
| S-06 | Both dialects β `deprecated`, tags with descriptions, multiple servers | R-641 |
|
|
67
|
+
| S-07 | Error inputs β invalid JSON, unknown version, missing `paths`, unknown/external/malformed/circular `$ref` | error model (R-404) |
|
|
68
|
+
| S-08 | typed-error boundary matrix β engines β ββ£, low-level helpers, warning validation, generated-module imports, filesystem writers, and CLI arguments all fail with a catalogued `ZopiaError`, actionable `hint`, discoverable `at`, and preserved `cause` | R-141β¦R-143/R-404 |
|
|
69
|
+
|
|
70
|
+
### π Ref-graph scenarios
|
|
71
|
+
|
|
72
|
+
| # | Scenario | π Rules pinned |
|
|
73
|
+
| --- | --- | --- |
|
|
74
|
+
| S-11 | simple ref (operation β component) | R-402 |
|
|
75
|
+
| S-12 | nested refs (component β component β component) | R-811 |
|
|
76
|
+
| S-13 | cycle (component β itself) β `z.lazy()` | R-402/R-812 |
|
|
77
|
+
| S-14 | same component used by many operations (self-contained inlining in default mode, identity-preserving imports in ref mode) | R-403/R-822 |
|
|
78
|
+
| S-15 | missing ref / external ref β typed error | R-404 |
|
|
79
|
+
|
|
80
|
+
### π Layout scenarios
|
|
81
|
+
|
|
82
|
+
| # | Scenario | π Rules pinned |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| S-21 | `directory` β the canonical Admin API tree (golden file) | R-711β¦R-714 |
|
|
85
|
+
| S-22 | `flat` β same spec, flat names (golden file) | R-721β¦R-723 |
|
|
86
|
+
| S-23 | flat name collision β `-2` suffix | R-722 |
|
|
87
|
+
| S-24 | path with a literal segment equal to a method name (`/users/get`) | R-714 |
|
|
88
|
+
| S-25 | deep paths (5+ segments) & params at every level | R-711 |
|
|
89
|
+
| S-26 | `TRACE` operation β emitted as a `trace/` method dir with `method: 'TRACE'` (km-api β₯ 0.4.1) | R-712/R-642 |
|
|
90
|
+
|
|
91
|
+
### βοΈ Option scenarios
|
|
92
|
+
|
|
93
|
+
| # | Scenario | π Rules pinned |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| S-31 | defaults (`directory`, no components) β self-contained files (imports only `zod`/`km-api`) | R-502, defaults |
|
|
96
|
+
| S-32 | `insertComponents: true` β `components/**` + barrel + inlined endpoints | R-801 |
|
|
97
|
+
| S-33 | `insertComponents + useComponentAsReference` β endpoint schemas import emitted components recursively; cycles stay lazy | R-802, R-821 |
|
|
98
|
+
| S-34 | `useComponentAsReference` alone β `ZOPIA_CONFIG_INVALID` | R-911 |
|
|
99
|
+
| S-35 | manifest written & valid in all of the above (schema test) | D-06 |
|
|
100
|
+
| S-36 | dedicated manifest writer β canonical hash/bytes, complete OpenAPI and Swagger frame metadata, portable paths, literal-aware `$ref` collection, invalid-shape rejection, atomic replacement/cleanup, temporary-symlink refusal, and reader round-trip compatibility | D-06, P-1, R-751β¦R-754 |
|
|
101
|
+
| S-37 | manifest staleness β source/config drift, invalid manifests, missing owned files, canonical key-reordering equivalence, manifest disablement, obsolete-owned-file pruning, custom-file retention, and symlink-ancestor rejection | R-741β¦R-743/R-406 |
|
|
102
|
+
|
|
103
|
+
### βοΈ Zod / JSON Schema coverage (engines β & β‘)
|
|
104
|
+
|
|
105
|
+
| # | Scenario |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| S-41 | every format row of R-627 (email, uuid, url/uri alias, hostname, ipv4/6, date-time, date, time, duration, `byte` β `z.base64()`) + unmapped formats (password, binary, int32/64, float, double, β¦) β base type + warning + overlay |
|
|
108
|
+
| S-42 | every numeric/string/array constraint of R-628 (min/max, int, regex, multipleOf, exclusive bounds both forms) |
|
|
109
|
+
| S-43 | enum (string/non-string), const, nullable (both spellings), tuples (both spellings) |
|
|
110
|
+
| S-44 | objects: required/optional, `additionalProperties` (false/schema/true), defaults, catchall |
|
|
111
|
+
| S-45 | oneOf/anyOf/allOf, discriminator β `discriminatedUnion` (+ fallback case) |
|
|
112
|
+
| S-46 | D-12 unsupported keywords β warning + approximation + `// @zopia:warn` comment (uniqueItems, not, if/then/else, patternProperties, propertyNames, min/maxProperties, contains) β and the manifest overlay restores the original keywords verbatim (R-635, asserted in the round-trip) |
|
|
113
|
+
| S-47 | β targets β output diffs between `openapi-3.1` / `openapi-3.0` / `draft-2020-12` / `draft-07` for the same input |
|
|
114
|
+
| S-48 | β unrepresentable (transforms, functions, NaN, `z.set`) β `{}` + warning (R-614) |
|
|
115
|
+
| S-49 | β‘ cross-check: generated code's runtime schema behaves like `z.fromJSONSchema()`'s (experimental) one on the fixture set |
|
|
116
|
+
| S-50 | β‘/β determinism β same input β identical output, twice in a row |
|
|
117
|
+
| S-51 | β /β£ value normalizations β sentinel integer bounds stripped (R-618), const-literal unions β `enum` (R-654) β asserted before the round-trip comparison |
|
|
118
|
+
| S-52 | `io: 'input'` request conversion (R-615) β defaulted request fields stay out of `required`, transformed request fields convert to their *input* type; response schemas use `io: 'output'` |
|
|
119
|
+
| S-53 | shared warning normalization β stable-code validation, one-line sanitization, deduplication, deterministic order, JSON Pointer rebasing, and canonical comment/log formatting; nested β‘ siblings retain distinct exact source locations |
|
|
120
|
+
|
|
121
|
+
### π Reverse-conversion scenarios (engine β£)
|
|
122
|
+
|
|
123
|
+
| # | Scenario | π Rules pinned |
|
|
124
|
+
| --- | --- | --- |
|
|
125
|
+
| S-61 | reverse of S-21 (directory) β equals original spec after canonicalization | R-651β¦R-658 |
|
|
126
|
+
| S-62 | reverse of S-22 (flat) β same result as S-61 (mode-independence) | D-06 |
|
|
127
|
+
| S-63 | reverse with `insertComponents + refs` on β `components.schemas` + `$ref`s restored | R-821β¦R-823 |
|
|
128
|
+
| S-64 | `version: '3.0'` vs `'3.1'` output diff | D-09 |
|
|
129
|
+
| S-84 | `version: '2.0'` dialect downgrade β host/basePath/schemes decomposition, `x-nullable`, body/formData parameters, global `parameters`/`responses` tables, security-scheme mapping, deterministic downgrade warnings, native-Swagger identity | D-20 |
|
|
130
|
+
| S-86 | native `z.record` conversion for exact `propertyNames`+`additionalProperties` objects β warning/overlay-free code, runtime key enforcement, verbatim engine β’ββ£ round-trip; non-native propertyNames forms stay refined + frozen | D-22 |
|
|
131
|
+
| S-87 | 3.1 webhook endpoint generation β deterministic `webhooks/` files, manifest `webhooks[]`/`webhookOrder`/`webhooksOverlay` records, exact-order engine β£ round-trip with runtime refresh, derived operationIds, local path-item `$ref` items restored verbatim unchanged (`webhookItemRef`) and expanded on edit, empty `x-` webhook items preserved through the order list, operation-less map preservation, malformed webhook items raise typed `ZOPIA_SPEC_INVALID`, 3.0/2.0 omission warnings with in-place expansion of path-item `$ref`s into omitted containers, cross-scope operationId rejection at β’ plus the defense of record at β£ | D-23 |
|
|
132
|
+
| S-91 | spec diff β key-order-insensitive identity across JSON/YAML/object inputs; endpoint add/remove operations labeled by method+path+operationId in path-primary order; shared-operation headers with scalar/parameter/request-body/response/`x-` details (add/remove/change per field); dialect, info, webhooks, components (dialect-aligned `at` pointers), document fields, and root extensions compared; typed failures for unreadable inputs; CLI glyph/stdout-silence/exit-0 contract plus grammar rejection and `--help` coverage; round 6 coverage β component registries (`parameters`/`responses`/`securitySchemes`/`securityDefinitions`/`requestBodies`/`headers`/`links`/`callbacks`/`examples`/`pathItems`) with dialect-aligned pointers, path-item/webhook-item metadata via `$ref` resolution, `x-` entries inside `paths`/`webhooks`, typed `$ref`-chain failures | Phase 3 diff tool |
|
|
133
|
+
| S-92 | split-generation presets β `planPresetBuckets` pure planner (primary-tag routing with `ZOPIA_WARN_PRESET_PRIMARY_TAG` on multi-tagged operations, effective-first-server routing, untagged/default buckets, collision-safe slugs, `x-` entries copied per bucket, `$ref` whole-item items verbatim vs partial-route inline expansion, fallthrough to the normal single tree); `openApiToApiDocs` preset split (one reversible tree + manifest per bucket, `trees[]` result, warning locations prefixed per bucket); invalid presets fail `ZOPIA_CONFIG_INVALID`; CLI `--preset` value/alias-rejection, summary line, `--help` coverage; round 7 fixes β op-less path/webhook items retained in every bucket (planner + public reverse round-trip), an explicit empty `servers: []` is decisive and routes to the default-server bucket, Swagger 2.0 multi-server/multi-tag defensive fallthrough | Phase 3 presets |
|
|
134
|
+
| S-93 | spec β tree navigation β manifest-driven index answers both directions exactly (endpoints/webhooks/components/companion `custom.ts` files/component barrels/the manifest itself; flat+directory layouts; preset bucket roots each carry an index); unknown pointer/file/component-(emission-disabled) + missing/invalid manifests fail with typed `ZOPIA_CONFIG_INVALID`/`ZOPIA_DOCS_MISSING_MANIFEST`/`ZOPIA_MANIFEST_INVALID`; single-pass JSON pointerβline scanning (exact lines incl. escaped segments + minified documents, absent pointers stay absent, malformed JSON β `ZOPIA_SPEC_INVALID_JSON`); `specPointerAtLine` cursor rule deterministic; CLI `zopia navigate` stable lines both directions, flag grammar (mutual exclusion, value requirements, repeat rejection), `--help`/unknown-command hint coverage | Phase 3 VS Code navigation |
|
|
135
|
+
| S-90 | incremental regeneration (D-24) β byte-identical regen leaves endpoint/manifest mtimes untouched; changed sources refresh them; one scaffolded `custom.ts` per endpoint+webhook in both layouts with the `export * as custom` line; hand edits and symlinks at the path survive regeneration; default off, toggle off drops the export line but keeps the file; staleness message covers the `custom` toggle; non-boolean options rejected at both layers; CLI `--custom` + `--help` coverage; a directory shadowing the companion path is a typed `ZOPIA_FS_OUTSIDE_OUTDIR`, and `custom.ts`-shaped path segments (planner-renamed) still receive a real scaffold file in both layouts |
|
|
136
|
+
| S-89 | `zopia validate` β clean spec/docs trees report `ok`, broken `$ref`s error at the offending pointer, cross-namespace duplicate operationIds error, unreachable 3.1 components (including orphan chains) and Swagger 2.0 definitions warn, webhook-only references stay reachable, same-folder external refs bundle before linting, generated trees run a reverse dry-run, missing/tampered manifests report typed errors, km-api outside the peer range or unresolvable warns without failing, CLI grammar/help/stdout-stderr/exit-status contracts | Phase 3 validate |
|
|
137
|
+
| S-88 | `zopia generate --watch` β immediate initial run, coalesced spec-change regeneration with in-place stale-tree refresh (one `ZOPIA_WARN_STALE_TREE`), error-recovery across broken edits (previous tree untouched, watching continues), atomic-save survival (write-temp + rename, parent-directory watch), forward warnings surfaced each run, duplicate-flag rejection, `--help` coverage | watch mode |
|
|
138
|
+
| S-65 | missing manifest / renamed file / broken export β typed errors | R-651/R-652 |
|
|
139
|
+
| S-66 | metadata restoration β titles, examples, servers, tag descriptions, security schemes, multi-content types come back verbatim | R-656/R-657 + honest-limits table |
|
|
140
|
+
| S-67 | idempotence β `reverse(generate(spec))` then `generate(β¦)` β identical tree, including source-order-sensitive method/path collisions and empty Path Items (T-11) | R-409 |
|
|
141
|
+
| S-68 | non-standard status (`419`) + `default` response β emitted as numeric/`default` response keys, round-trips exactly (km-api β₯ 0.4.1) | R-642 |
|
|
142
|
+
| S-69 | exotic media type (`application/vnd.custom+json`) β emitted verbatim as the content type, used as the `content` key on reverse (km-api β₯ 0.4.1) | R-642 |
|
|
143
|
+
| S-70 | parameter extras (`allowEmptyValue`, `style`, `explode`) + response `headers` β overlay/`responseOverlay`, restored verbatim on reverse | R-635/R-754 |
|
|
144
|
+
| S-71 | multiple security schemes + per-operation requirements with scopes (oauth2) + an explicit `security: []` operation β `defaultSecurity` / `apis[].security` manifest fields, round-trips exactly (km-api stores only the `auth` boolean) | R-653/R-656 |
|
|
145
|
+
| S-72 | reverse warnings β runtime Zod losses, fallback info/security, and 3.1β3.0 omissions return/callback with exact output pointers; security fallback coverage includes multiple operations, definition-name collisions, manifest-authoritative explicit/global requirements, and OpenAPI/Swagger representations | R-408/R-654/R-656β¦R-658 |
|
|
146
|
+
| S-73 | CLI warning channels β generate/reverse diagnostics go to stderr while reverse stdout remains parseable JSON | R-408/R-933 |
|
|
147
|
+
| S-74 | CLI contract β every flag maps to its API option; options may surround positionals; missing/extra arguments, unknown/cross-command/duplicate/valueless flags fail before engine work; help includes the trusted-tree warning; exit statuses distinguish typed and unexpected failures | R-931β¦R-934 |
|
|
148
|
+
| S-75 | JSDoc AST audit β every directly or named-only exported declaration and exposed public/nested-shape member has a useful summary; all callable forms require specific parameters/returns, optional configuration defaults are stated, TypeScript examples semantically typecheck against the source API, and relative `@see` links resolve | T-12/R-131β¦R-135/R-1003 |
|
|
149
|
+
| S-76 | Package/release contract β version and public metadata stay synchronized; Vitest/coverage versions and Bun scripts stay pinned; the npm archive is allowlisted and executable; `prepublishOnly` runs the complete release gate; the exact tarball installs offline and passes package-root import plus generate/reverse CLI smoke tests | R-191β¦R-193 |
|
|
150
|
+
| S-77 | YAML input β `.yaml`/`.yml` paths, inline YAML text, extension-less YAML fallback, and JSON-inside-YAML flow text enter engine β’; byte-identical trees vs JSON twins, reverse round-trips, CLI parity, stable YAML-side error codes | D-16/R-404 |
|
|
151
|
+
| S-78 | YAML parser (D-16) β core-schema scalars, nested block/flow collections, quoted escapes, literal/folded block scalars with chomping/indent indicators, anchors/aliases/`<<` merge keys, directives/markers, deterministic failure matrix, recursion guard | D-16/R-1006 |
|
|
152
|
+
| S-79 | external `$ref` bundling (D-17) β same-folder YAML/JSON chains resolve inline with clone-on-splice, sibling-key merges, self-file refs, literal/example shielding, and read-once caching; API/CLI generated trees are byte-identical to inline twins, reverse emits the bundled single file, and manifest staleness reacts to sibling-file edits | D-17/P-1 |
|
|
153
|
+
| S-80 | external `$ref` failure matrix (D-17) β URL/`../`/absolute/subdirectory/drive/unknown-extension targets keep `ZOPIA_REF_EXTERNAL`; unreadable, unparsable (JSON/YAML), missing-pointer, bad-fragment, circular, >512-deep, and sibling-on-scalar targets fail with typed codes located at the referencing pointer | D-17/R-404 |
|
|
154
|
+
| S-81 | reusable parameters & responses generation (D-18) β declarations become `components/parameters/<Name>/index.ts` / `components/responses/<Name>/index.ts` modules with `<Name>Parameter` / `<Name>Response` exports and kind barrels; bare-`$ref` use sites import through the barrel while sibling-merged `$ref`s inline; cross-schema/ref-chain derivations, body/formData slots, manifest `kind` entries, schema-less response exclusion, and invalid-container/collision errors | D-18/P-1 |
|
|
155
|
+
| S-82 | reusable parameters & responses round-trip (D-18) β both fixtures (`reusables-3.1.json`, `reusables-2.0.json`) reproduce byte-exactly after canonicalization; edited modules refresh their declarations on reverse (Swagger 2.0 constraints and 3.x content forms) while every use-site `$ref` restores verbatim from manifest placements and declaration chains stay chains | D-18/R-716 |
|
|
156
|
+
| S-83 | `zopia.config.ts` project defaults (D-19) β working-directory discovery + explicit `--config` paths, default/named `config` exports, structural validation with key-located `ZOPIA_CONFIG_INVALID`, precedence CLI > config > defaults (`--no-manifest` always wins), optional `<output-dir>` from `generate.outDir`, and reverse `version`/`out` defaults | D-19/R-940 |
|
|
157
|
+
|
|
158
|
+
## π Round-trip property tests
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// π§ͺ tests/roundtrip/property.test.ts (implemented)
|
|
162
|
+
for (const fixture of fixtures) {
|
|
163
|
+
it(`round-trips ${fixture}`, async () => {
|
|
164
|
+
const outDir = await temporaryDirectory(); // shared R-111 cleanup helper
|
|
165
|
+
await openApiToApiDocs(await loadFixture(fixture), { outDir });
|
|
166
|
+
// Omitting `version` preserves the source dialect, including Swagger 2.0
|
|
167
|
+
// and an exact OpenAPI patch version such as 3.0.3.
|
|
168
|
+
const back = await manifestFileToOpenApi(path.join(outDir, '.zopia-manifest.json'));
|
|
169
|
+
expect(canonicalize(back)).toEqual(canonicalize(await loadFixture(fixture)));
|
|
170
|
+
const regenerated = await temporaryDirectory();
|
|
171
|
+
await openApiToApiDocs(back, { outDir: regenerated });
|
|
172
|
+
expect(await treeSnapshot(regenerated)).toEqual(await treeSnapshot(outDir));
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`canonicalize()` = R-401 key ordering + deep-equal on JSON (whitespace
|
|
178
|
+
independent). Value normalizations are **not** part of canonicalization β
|
|
179
|
+
engine β£'s serializer applies them (R-654) *before* comparison, so a mismatch
|
|
180
|
+
is a real engine bug. Every dialect fixture (S-01β¦S-06) round-trips against
|
|
181
|
+
**its own original**; the canonical Admin API additionally asserts an
|
|
182
|
+
**empty `overlay` on every API** β a spec-clean spec must round-trip without a
|
|
183
|
+
single frozen subtree or keyword restoration.
|
|
184
|
+
|
|
185
|
+
The implemented matrix also runs flat mode, emitted-but-inlined components,
|
|
186
|
+
emitted component references, nested and cyclic refs, and frozen overlays. For
|
|
187
|
+
every fixture/layout/component case it asserts that reverse output reproduces
|
|
188
|
+
the source and regenerates a byte-identical tree; separate properties cover
|
|
189
|
+
same-input regeneration and collision-sensitive path plans. It checks source-preserving
|
|
190
|
+
Swagger β OpenAPI selection separately, covers explicit empty schema containers,
|
|
191
|
+
empty Path Items, boolean/tuple/local-definition schemas, schema-less media and
|
|
192
|
+
absent optional flags, and verifies that supported Zod β JSON Schema β Zod
|
|
193
|
+
pipelines (including `z.never()`) converge on the same canonical schema. Every temporary tree is removed after its test (R-111).
|
|
194
|
+
|
|
195
|
+
## π§° Fixtures
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
tests/fixtures/
|
|
199
|
+
βββ specs/
|
|
200
|
+
β βββ admin-api-3.0.json # β the canonical Admin API (docs/07)
|
|
201
|
+
β βββ admin-api-3.0.yaml # π YAML twin β parses/generates byte-identically (S-77)
|
|
202
|
+
β βββ admin-api-2.0.json # same API as Swagger 2.0
|
|
203
|
+
β βββ admin-api-2.0.yaml # π YAML twin (S-77)
|
|
204
|
+
β βββ petstore-mini-3.1.json # 3.1 keywords (const, prefixItems, β¦)
|
|
205
|
+
β βββ cycle-comment.json # π self-referential component
|
|
206
|
+
β βββ nested-refs.json # π§© component β component β component
|
|
207
|
+
β βββ formdata-2.0.json # π§Ύ formData multipart + urlencoded
|
|
208
|
+
β βββ cookies-3.0.json # πͺ cookie parameters
|
|
209
|
+
β βββ unsupported-keywords.json # π« D-12 matrix in one spec
|
|
210
|
+
β βββ path-item-ref-3.1.json # π local path-item reference identity
|
|
211
|
+
β βββ km-api-contract-3.1.json # π trace/custom/default/extension type surface
|
|
212
|
+
β βββ external-refs/ # π spec folder with sibling YAML/JSON shards (D-17/S-79)
|
|
213
|
+
β β βββ admin-3.0.yaml # root β cross-file refs, sibling merges, self-file refs
|
|
214
|
+
β β βββ shared-schemas.yaml # schemas with local + cross-file refs of their own
|
|
215
|
+
β β βββ shared-responses.yaml # reusable response target
|
|
216
|
+
β β βββ common.json # JSON leaf with its own local refs
|
|
217
|
+
β βββ external-refs-inline/ # π fully-inline twin β byte-identical generated tree (S-79)
|
|
218
|
+
β βββ admin-3.0-inline.json
|
|
219
|
+
βββ expected/
|
|
220
|
+
βββ admin-api-3.0.directory/ # πΈ golden tree (defaults)
|
|
221
|
+
βββ admin-api-3.0.flat/ # πΈ golden tree (flat)
|
|
222
|
+
βββ admin-api-3.0.components/ # πΈ golden tree (components + refs)
|
|
223
|
+
βββ km-api-0.4.1.contract/ # π generated open-value type contract
|
|
224
|
+
βββ tsconfig.json # π dedicated strict no-emit gate
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
> π **Rule R-112** β golden trees are checked in and reviewed like code.
|
|
228
|
+
> Changing one requires a deliberate `bun run golden:update` run and a PR
|
|
229
|
+
> showing the diff β determinism regressions are visible in review.
|
|
230
|
+
>
|
|
231
|
+
> The implemented updater removes and recreates only the governed output trees
|
|
232
|
+
> from their checked-in JSON fixtures. The contract suite generates each variant
|
|
233
|
+
> into a cleaned `os.tmpdir()` directory, compares the complete relative-path β
|
|
234
|
+
> UTF-8-byte map (so extra files fail too), and verifies that `expected/` itself
|
|
235
|
+
> contains no ungoverned tree.
|
|
236
|
+
|
|
237
|
+
## π Coverage gates
|
|
238
|
+
|
|
239
|
+
| π Gate | π― Threshold |
|
|
240
|
+
| --- | --- |
|
|
241
|
+
| Lines / functions | β₯ **90%** overall |
|
|
242
|
+
| Branches | β₯ **85%** overall |
|
|
243
|
+
| `src/conversions/**` | β₯ **95%** lines β these files implement engines β ββ£ and their mapping tables |
|
|
244
|
+
| Any single `src/**/*.ts` file | never below **80%** lines |
|
|
245
|
+
|
|
246
|
+
`bun run coverage` runs Vitest's V8 provider over **all** `src/**/*.ts` files,
|
|
247
|
+
including files that no test imported. Vitest enforces the overall thresholds;
|
|
248
|
+
`scripts/check-coverage.ts` then inventories source files against the JSON
|
|
249
|
+
summary, enforces the aggregate conversion-engine and per-file line gates, and
|
|
250
|
+
exits non-zero on any omission or shortfall. Warnings paths (D-12) are tested β
|
|
251
|
+
a warning that never fires in tests is a red flag, not a shrug.
|
|
252
|
+
|
|
253
|
+
## π Writing tests (standard)
|
|
254
|
+
|
|
255
|
+
| # | Rule |
|
|
256
|
+
| --- | --- |
|
|
257
|
+
| R-121 | **AAA** β Arrange / Act / Assert sections, one behaviour per `it()` |
|
|
258
|
+
| R-122 | **Name = spec** β test names cite the rule they pin: `it('R-627: format email β z.email()', β¦)` |
|
|
259
|
+
| R-123 | **Errors assert on `code`** (R-404), never on message text |
|
|
260
|
+
| R-124 | **New rule β new test** β adding an R-β¦ row to any doc requires the matching test in the same PR |
|
|
261
|
+
| R-125 | **No skipped tests in main** β `it.skip` is allowed only with a linked issue and a removal date |
|
|
262
|
+
| R-126 | **Golden trees typecheck** β the contract suite runs dedicated strict, no-emit `tsc` over every golden `api_docs` tree against installed published `km-api@0.4.1` (D-15), without skipping declaration checks. The contract fixture pins `trace`, custom and `default` statuses, and an arbitrary extension media type. `makeApiConfig` remains the actual call-site gate rather than a source-text substitute (D-14/R-642) |
|
|
263
|
+
|
|
264
|
+
## π Next
|
|
265
|
+
|
|
266
|
+
- π The rules being tested β [Standards](12-standards.md)
|
|
267
|
+
- π The engines' exact contracts β [Conversions](06-conversions.md)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# π Engineering Standards
|
|
2
|
+
|
|
3
|
+
How zopia is written, committed, and released. These standards apply to
|
|
4
|
+
**source, tests, and documentation equally** β "pure, safe, clean, with
|
|
5
|
+
descriptions" (T-16) is the headline; everything below makes that checkable.
|
|
6
|
+
|
|
7
|
+
## π£ Toolchain
|
|
8
|
+
|
|
9
|
+
| π§© Piece | π Standard | π |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Runtime & toolchain | **Bun β₯ 1.1** β install, run, test, CLI (D-01) | T-14 |
|
|
12
|
+
| Language | **TypeScript 5.9+**, `strict: true`, ESM-only (`"type": "module"`), target ES2022 | β |
|
|
13
|
+
| Test runner | **Vitest 4.1.11** (exactly pinned; D-02) β `bun run test` | T-13 |
|
|
14
|
+
| Package manager lockfiles | authoritative `bun.lock`; npm/Node compatibility `package-lock.json` | β |
|
|
15
|
+
| Node compatibility | generated code must also run on Node β₯ 18 (no Bun-only APIs in generated output) | β |
|
|
16
|
+
|
|
17
|
+
> π Generated `index.ts` files use **no** Bun-only or Node-only APIs β only
|
|
18
|
+
> `zod`, `km-api`, and relative imports (R-502), so the tree runs anywhere.
|
|
19
|
+
|
|
20
|
+
### π£ Bun gate
|
|
21
|
+
|
|
22
|
+
`packageManager` pins the repository's Bun version and the checked-in `bun.lock`
|
|
23
|
+
pins every dependency. Run the complete gate with one PowerShell- and shell-valid
|
|
24
|
+
command:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bun run release:check
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The gate rejects a different runtime/version, performs a frozen Bun install,
|
|
31
|
+
runs strict TypeScript, the full Vitest suite, and coverage gates, exercises both
|
|
32
|
+
the direct CLI and package binary, then generates and reverses a component-based
|
|
33
|
+
tree so Bun itself must import the generated `.ts` modules. It also installs the
|
|
34
|
+
exact npm archive in an isolated offline consumer and exercises its package-root
|
|
35
|
+
import and CLI. Temporary output is created under `os.tmpdir()` and always
|
|
36
|
+
removed. `bun:gate`, `release:check`, and `prepublishOnly` share this one gate
|
|
37
|
+
implementation so local, CI, and publish-time validation cannot drift.
|
|
38
|
+
|
|
39
|
+
`package-lock.json` remains checked in as the npm/Node compatibility resolution;
|
|
40
|
+
`bun.lock` is authoritative for the Bun gate and release workflow.
|
|
41
|
+
|
|
42
|
+
**CI publishing** β `docs/publish-workflow.yml.example` is the ready-made
|
|
43
|
+
GitHub Actions workflow: drop it at `.github/workflows/publish.yml` (the
|
|
44
|
+
sandbox's GitHub App token cannot push workflow files β adding it once via
|
|
45
|
+
the GitHub UI or an owner-shell works), then every GitHub Release publishes
|
|
46
|
+
to npm with tagβversion verification and `npm publish --provenance --access
|
|
47
|
+
public` (the `prepublishOnly` hook re-runs the same Bun gate inside CI, so a
|
|
48
|
+
publish cannot bypass it). The pipeline reads an `NPM_TOKEN` repository
|
|
49
|
+
secret or npm trusted-publishing; credentials never appear in the repository
|
|
50
|
+
or in chat.
|
|
51
|
+
|
|
52
|
+
## π Code standard
|
|
53
|
+
|
|
54
|
+
| # | Rule |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| R-1001 | π§Ό **Pure transforms, explicit adapters** β in-memory schema/operation transforms are pure; documented file reads, generated-tree writes/imports, and process output live at public adapter/CLI boundaries (R-405) |
|
|
57
|
+
| R-1002 | π§ **Narrow at decisions** β user-facing option/result contracts are concrete; deliberately open JSON/OpenAPI records are narrowed before branching, rendering, or filesystem use |
|
|
58
|
+
| R-1003 | π¦ **Named exports only** β no default exports anywhere in `src/` (generated files may have defaults: that's their contract, R-732) |
|
|
59
|
+
| R-1004 | π§© **One concern per module** β modules are divided by conversion/boundary responsibility; `src/index.ts` remains re-exports only |
|
|
60
|
+
| R-1005 | π§΅ **Cycle-aware traversal** β reference chains carry seen sets and schema definition/dependency traversals carry cycle state; cycles terminate as errors or `z.lazy()` according to reference kind (R-402) |
|
|
61
|
+
| R-1006 | π― **Determinism (P-1)** β canonical code-unit order everywhere (R-401), never host-locale collation; no `Date.now()`, `Math.random()`, or environment reads in the pure core |
|
|
62
|
+
| R-1007 | π‘οΈ **Safe I/O** β every generated write passes the outDir guard (R-406); no shell-out, `eval`, or `new Function` in conversion paths |
|
|
63
|
+
|
|
64
|
+
## π JSDoc standard (T-12)
|
|
65
|
+
|
|
66
|
+
**Every exported symbol** β function, class, interface, type alias, enum, and
|
|
67
|
+
constant β carries JSDoc, including declarations exposed through a named export
|
|
68
|
+
list rather than an `export` modifier. The audited public boundary also includes
|
|
69
|
+
every exposed member of an exported interface, class, or nested type-literal
|
|
70
|
+
shape; public methods, constructors, call/construct signatures, and
|
|
71
|
+
function-valued properties are callables. Private and protected implementation
|
|
72
|
+
members are excluded. The AST contract suite scans every production module
|
|
73
|
+
under `src/`, not only the package-root re-export list. The template:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
/**
|
|
77
|
+
* π One-line summary β what it does, in the active voice.
|
|
78
|
+
*
|
|
79
|
+
* π Optional longer description: contracts, invariants, edge cases.
|
|
80
|
+
* Links to the governing rule, e.g. "See R-641 for the media-type policy."
|
|
81
|
+
*
|
|
82
|
+
* @param input π What it accepts (units/range when relevant).
|
|
83
|
+
* @param options π Behaviour knobs; each field documented inline.
|
|
84
|
+
* @returns π What it produces.
|
|
85
|
+
* @throws {ZopiaError} π `ZOPIA_β¦` β when.
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* const r = await openApiToApiDocs('swagger.json', { mode: 'flat' });
|
|
89
|
+
* ```
|
|
90
|
+
* @see [docs/06-conversions.md β Engine β’](./06-conversions.md)
|
|
91
|
+
*/
|
|
92
|
+
export function openApiToApiDocs(
|
|
93
|
+
input: string | Record<string, unknown>,
|
|
94
|
+
options?: ZopiaGenerateOptions,
|
|
95
|
+
): Promise<ZopiaGenerateResult> { /* β¦ */ }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
| # | Rule |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| R-131 | every `@param`, `@returns`, `@throws` is **specific** β no "the options", always *which* option and *what it does* |
|
|
101
|
+
| R-132 | `@default` on every optional config field |
|
|
102
|
+
| R-133 | `@example` is **runnable** code; the contract suite extracts each TypeScript block and typechecks it semantically against the real public source API |
|
|
103
|
+
| R-134 | internal (non-exported) helpers get a one-line comment when the *why* is non-obvious |
|
|
104
|
+
| R-135 | the docs link in `@see` must resolve (checked in CI) |
|
|
105
|
+
|
|
106
|
+
## π Errors
|
|
107
|
+
|
|
108
|
+
| # | Rule |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| R-141 | one base class `ZopiaError` (`code`, `at?`, actionable `hint`, preserved `cause?`) at every public/CLI boundary; `ZOPIA_ERROR_CODES` is the immutable runtime catalogue and TypeScript-union source β full table in [Architecture β Error model](04-architecture.md#-error-model) |
|
|
111
|
+
| R-142 | codes are stable strings, UPPER_SNAKE, prefixed `ZOPIA_`; adding a code is a **MINOR** change |
|
|
112
|
+
| R-143 | `hint` is always an *action* ("enable `insertComponents` first"), never a lecture; translated parser/import/filesystem failures retain the original value in `cause` |
|
|
113
|
+
| R-144 | warnings (not errors) for lossy-but-recoverable conversions (D-12) β shape: `{ code: ZopiaWarningCode, at?: string, message: string }`; codes come from the stable `ZOPIA_WARNING_CODES` catalogue, `at` is an escaped JSON Pointer when discoverable, and public emission is sanitized, deduplicated, deterministic, and callback/result consistent |
|
|
114
|
+
|
|
115
|
+
## π‘οΈ Safety
|
|
116
|
+
|
|
117
|
+
| # | Rule |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| R-151 | **outDir guard** (R-406) β canonicalize + prefix-check every path, reject symlinked generated ancestors, exclusively create manifest temporary files, and prune only paths owned by a validated prior manifest; unit-tested with traversal/symlink attempts |
|
|
120
|
+
| R-152 | **Trusted-input contract (D-08)** β engine β£ imports generated `.ts`; the manifest is the trust marker. Documented loudly in [Usage](10-usage.md) and in the CLI help |
|
|
121
|
+
| R-153 | **No secret handling** β zopia reads specs and writes docs; it never touches credentials, never sends data anywhere (no network at all) |
|
|
122
|
+
| R-154 | **Predictable failure** β configuration, source, layout, and component-render validation happen before governed writes; every surfaced write/import failure is typed. Individual manifest replacement is atomic, while the generated tree is updated as ordered guarded file writes rather than as one directory transaction |
|
|
123
|
+
|
|
124
|
+
## π·οΈ Naming
|
|
125
|
+
|
|
126
|
+
| π§© Thing | π Convention |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| Files | `kebab-case.ts` (`zod-to-json-schema.ts`) |
|
|
129
|
+
| Modules/dirs | `kebab-case`, plural for collections (`conversions/`, `fixtures/`) |
|
|
130
|
+
| Functions | `camelCase`, verb-first (`zodToJsonSchema`, `normalizeOpenApiDocument`) |
|
|
131
|
+
| Types/interfaces | `PascalCase` (`GenerateApiDocsOptions`, `OpenApiOperationIR`) |
|
|
132
|
+
| Constants | `UPPER_SNAKE_CASE` (`ZOPIA_MANIFEST_SCHEMA`) |
|
|
133
|
+
| Tests | centralized under `tests/` as `*.test.ts`; contract and round-trip suites use dedicated subdirectories; `it()` cites rules (R-122) |
|
|
134
|
+
| Generated identifiers | fixed by [API docs β Naming](07-api-docs.md#-naming-conventions-fixed) |
|
|
135
|
+
|
|
136
|
+
## π Commit convention
|
|
137
|
+
|
|
138
|
+
[Conventional Commits](https://www.conventionalcommits.org/), no Co-Authored noise:
|
|
139
|
+
|
|
140
|
+
```text
|
|
141
|
+
<type>(<scope>): <imperative summary β€ 72 chars>
|
|
142
|
+
|
|
143
|
+
[optional body β WHY, not WHAT]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
| π·οΈ Type | π Use for |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| `feat` | new engine behaviour, option, CLI flag |
|
|
149
|
+
| `fix` | bug fixes (mapping corrections count as fixes β they are *spec* corrections) |
|
|
150
|
+
| `docs` | documentation-only changes |
|
|
151
|
+
| `test` | tests without behaviour change (e.g. new golden fixtures) |
|
|
152
|
+
| `refactor` | internal restructuring, zero behaviour change |
|
|
153
|
+
| `chore` / `ci` / `build` | tooling, workflows, packaging |
|
|
154
|
+
|
|
155
|
+
> π **Rule R-161** β *a commit that changes behaviour is not merged without
|
|
156
|
+
> its `CHANGELOG.md` entry* (below) and its doc update (if it changes a
|
|
157
|
+
> documented contract) β one commit, one story.
|
|
158
|
+
|
|
159
|
+
## π Changelog convention
|
|
160
|
+
|
|
161
|
+
> π― **T-17** β *changelog after commits.*
|
|
162
|
+
|
|
163
|
+
| # | Rule |
|
|
164
|
+
| --- | --- |
|
|
165
|
+
| R-171 | every user-visible commit appends a bullet under `## [Unreleased]` in `CHANGELOG.md` β same commit (R-161) |
|
|
166
|
+
| R-172 | entries are **user-phrased** ("reverse conversion now restores `servers`"), not internal ("fixed serializer.ts:42") |
|
|
167
|
+
| R-173 | on release: `[Unreleased]` β `[x.y.z] - YYYY-MM-DD`; a fresh empty `[Unreleased]` is created |
|
|
168
|
+
| R-174 | sections per [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): Added / Changed / Deprecated / Removed / Fixed / Security β with the project's emoji markers (β¨ π β οΈ ποΈ π π‘οΈ π π π§) |
|
|
169
|
+
|
|
170
|
+
The contract suite audits every post-release commit that changes `src/`, public
|
|
171
|
+
documentation, the package entry points, or package metadata and fails unless
|
|
172
|
+
that same commit also changes `CHANGELOG.md`. The static Unreleased check remains
|
|
173
|
+
active in shallow/source-only environments where the release boundary is absent.
|
|
174
|
+
|
|
175
|
+
## π Docs convention
|
|
176
|
+
|
|
177
|
+
| # | Rule |
|
|
178
|
+
| --- | --- |
|
|
179
|
+
| R-181 | **docs ship with code** β a behaviour change and its doc change land in the same commit (R-161) |
|
|
180
|
+
| R-182 | **Style** β emoji section headers, tables for anything list-like, code blocks with language tags, one idea per paragraph |
|
|
181
|
+
| R-183 | **Numbering** β targets `T-β¦`, rules `R-β¦`, decisions `D-β¦`, warnings/errors `ZOPIA_β¦` β referenced from code & tests |
|
|
182
|
+
| R-184 | **Cross-links** β every doc links forward & back; the map in [`docs/README.md`](README.md) stays current |
|
|
183
|
+
| R-185 | **Diagrams** β Mermaid for flow, ASCII trees for file layouts (both render on GitHub) |
|
|
184
|
+
| R-186 | **Status honesty** β "planned/contract" is labeled π§ until implemented; never documented as done |
|
|
185
|
+
|
|
186
|
+
## π’ Release flow
|
|
187
|
+
|
|
188
|
+
```text
|
|
189
|
+
1. π bump version (SemVer β [Roadmap β Versioning](03-roadmap.md#-versioning))
|
|
190
|
+
2. π CHANGELOG: [Unreleased] β [x.y.z] - YYYY-MM-DD
|
|
191
|
+
3. π README status banner updated to the new phase
|
|
192
|
+
4. π’ bun run release:check
|
|
193
|
+
5. π·οΈ git tag v0.1.0
|
|
194
|
+
6. π¦ npm publish (package.json: name "zopia", peerDeps zod ^4 + km-api ^0.4 β
|
|
195
|
+
the published `km-api@0.4.1` dependency is installed, D-15)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
| # | Release-readiness rule |
|
|
199
|
+
| --- | --- |
|
|
200
|
+
| R-191 | **One release identity** β `package.json`, the manifest writer, lockfile, versioned changelog heading, and README status agree on the SemVer version |
|
|
201
|
+
| R-192 | **Minimal verified artifact** β npm receives only `bin/`, `src/`, `docs/`, and the package/legal markdown; the exact archive is installed in isolation and must pass library-import plus generate/reverse CLI smoke tests |
|
|
202
|
+
| R-193 | **Publish guard** β `prepublishOnly` runs the pinned-Bun release gate, including typecheck, all tests, coverage, direct runtime checks, and R-192's packed-consumer check |
|
|
203
|
+
|
|
204
|
+
Preparing these artifacts does not publish or tag a release. Those external steps
|
|
205
|
+
remain explicit maintainer actions after the committed release gate is green.
|
|
206
|
+
|
|
207
|
+
## π¦ Dependencies
|
|
208
|
+
|
|
209
|
+
| π¦ Dep | π·οΈ Kind | π Rule |
|
|
210
|
+
| --- | --- | --- |
|
|
211
|
+
| `zod` `^4` | peer + dev | engines β /β‘ and generated schemas use it at runtime; tests exercise both paths |
|
|
212
|
+
| `km-api` `^0.4.1` (0.4.x) | peer + dev | generated code imports it and engine β£ loads those results β its type surface (method/status codes/content types/operationId) is part of zopia's output contract (D-14). **Resolution:** published npm package `km-api@^0.4.1` (D-15) |
|
|
213
|
+
| *(no `dependencies` entries)* | β | **zero direct/bundled runtime dependencies** in v0.1.0 (D-11); runtime capabilities are declared as peers and every new dependency needs a D-β¦ decision |
|
|
214
|
+
|
|
215
|
+
## π Key decisions
|
|
216
|
+
|
|
217
|
+
> Every non-obvious choice is recorded here β ID, decision, rationale.
|
|
218
|
+
> Changing one is a **MINOR** change that also updates the affected docs.
|
|
219
|
+
|
|
220
|
+
| π | Decision | Rationale |
|
|
221
|
+
| --- | --- | --- |
|
|
222
|
+
| **D-01** | π£ Bun is the primary runtime/toolchain (Node β₯ 18 stays compatible for *generated* code) | project standard; speed; native TS execution β required by the "bun must run the project" target (T-14) |
|
|
223
|
+
| **D-02** | π§ͺ Vitest is the test runner (not `bun:test`) | requested standard; mature snapshot/coverage ecosystem |
|
|
224
|
+
| **D-03** | β builds on Zod v4's built-in `z.toJSONSchema()` | the third-party `zod-to-json-schema` is deprecated (Nov 2025); Zod v4 is self-sufficient; fewer deps (D-11) |
|
|
225
|
+
| **D-04** | β‘ is a custom emitter (Zod's experimental `z.fromJSONSchema()` is test-only) | we control the *style* of emitted code (the product surface); experimental APIs don't sit on an output path; `z.fromJSONSchema` still cross-checks us in S-49 |
|
|
226
|
+
| **D-05** | π£οΈ generated `pathShape` uses OpenAPI `{param}` syntax | km-api's dual syntax makes it lossless both ways; matches the spec |
|
|
227
|
+
| **D-06** | π¦ every generated tree carries `.zopia-manifest.json` (written by default β the `manifest` option, on unless explicitly disabled; no timestamps) | flat names can collide; component schemas, `$ref` placement (`refs`) and non-representable keywords (`overlay`) have no home in Zod code; the manifest is what makes reverse conversion lossless and deterministic |
|
|
228
|
+
| **D-07** | π default layout is `directory` | mirrors the spec's path structure β the most intuitive mapping of "route = directory path" |
|
|
229
|
+
| **D-08** | β£ imports generated `.ts` at runtime (Bun) | the files *are* the source of truth (developers may extend them); importing is the only way to read edited schemas; trust is bounded by the manifest (R-152) |
|
|
230
|
+
| **D-09** | π€ reverse output defaults to OpenAPI **3.1** | 3.1 schemas = full JSON Schema 2020-12 (the "same standard" the project is built on); 3.0 remains one flag away |
|
|
231
|
+
| **D-10** | π MIT license | consistency with the whole `km-*` ecosystem |
|
|
232
|
+
| **D-11** | π¦ zero direct/bundled runtime dependencies in v0.1.0 | `zod` + `km-api` are explicit peers used by conversion/generated-code paths; a small owned dependency surface reduces install and security risk (P-6) |
|
|
233
|
+
| **D-12** | β οΈ unsupported facts never fail silently β every engine emits the shared structured warning; schema emission adds a canonical `// @zopia:warn` marker; restorable source facts also enter the manifest; CLI diagnostics use stderr only | "pure, safe, clean" means *visible* loss without corrupting generated output; reverse conversion restores manifest-recorded facts verbatim where the target dialect permits |
|
|
234
|
+
| **D-13** | π v0.1.0 input is JSON only (external `$ref` resolution β Phase 2, same-folder in v0.2.x via D-17; **YAML input lifted in v0.2.x via D-16**); server variables are manifest-preserved but warn because endpoint modules cannot represent them | keeps the v0.1.0 parsing/resolution contract tight while round-tripping document-frame data |
|
|
235
|
+
| **D-14** | π zopia **targets km-api β₯ 0.4.1** β the output contract is "the generated tree **typechecks** against installed published km-api 0.4.1" (enforced by the golden-tree test, R-126). Eight methods including `trace`, custom/`default` statuses, arbitrary MIME strings, and `operationId` are emitted as code. Published 0.4.1 enumerates known MIME values, so exact OpenAPI extension strings cross one narrow type-only assertion; non-representable parameter metadata and response `headers` remain in manifest overlays (R-635/R-754) | `makeApiConfig` is a type-level factory with no runtime validation. The dedicated strict golden `tsc` gate verifies its real published declarations; exact runtime MIME values remain reversible. Re-verify the boundary on every km-api bump |
|
|
236
|
+
| **D-15** | π¦ **km-api is consumed from npm** β zopia depends on the published `km-api@^0.4.1`; no Git submodule or unpublished commit is required. | reproducible fresh clones and published dependency resolution |
|
|
237
|
+
| **D-16** | π **v0.2.x YAML input is parsed by an owned, deterministic YAML 1.2 core-schema parser** (`src/conversions/yaml.ts`) β block/flow collections, plain/single/double-quoted scalars, literal/folded block scalars with chomping/indent indicators, comments, anchors/aliases/`<<` merge keys (explicit keys win), `%YAML 1.x` directives, single `---`/`...` document; keys are stringified like a JSON round-trip; tab indentation, duplicate keys, undefined aliases, custom tags, multi-document streams, complex `?` keys, and non-JSON numbers (`.inf`/`.nan`) fail with `ZOPIA_SPEC_INVALID_YAML` | a dependency (D-11) would import parser state we cannot pin for determinism (P-1); the subset covers real-world `spec.yaml` files fully, and every rejection is a typed, line-located diagnostic instead of silent approximation (P-4) |
|
|
238
|
+
| **D-17** | π **v0.2.x file input resolves same-folder external `$ref`s by bundling them inline before normalization** (`src/conversions/openapi-external-ref.ts`) β `other.(json|yaml|yml)` with `./β¦` spellings, an optional `#` JSON Pointer ('' = whole file); sibling files are read once (P-1), bundled content is deep-cloned, nested cross-file refs resolve against their owning file, and sibling keys win over bundled content; URLs, `../`, absolute paths, subdirectories, drives, and non-spec extensions keep `ZOPIA_REF_EXTERNAL`; unreadable targets, missing pointers, circular chains, sibling-on-scalar targets, and >512-level expansion fail typed; reverse conversion emits the bundled single file and never re-splits | one predictable grammar keeps resolution deterministic (P-1/P-4) with zero network access or directory walking, while object/text inputs keep their original semantics byte-for-byte |
|
|
239
|
+
|
|
240
|
+
## π Back to
|
|
241
|
+
|
|
242
|
+
- π [README](../README.md) Β· π [Docs home](README.md)
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# π zopia β Documentation
|
|
2
|
+
|
|
3
|
+
Welcome to the **zopia** documentation home. This directory is the single
|
|
4
|
+
source of truth for *how zopia works, what it must do, and how it is built*.
|
|
5
|
+
|
|
6
|
+
> π **Rule** β behaviour and documentation change together. If a commit
|
|
7
|
+
> changes what zopia does, the matching document in this directory changes in
|
|
8
|
+
> the **same** commit. See [Standards β Docs convention](12-standards.md#-docs-convention).
|
|
9
|
+
|
|
10
|
+
## πΊοΈ Map
|
|
11
|
+
|
|
12
|
+
| # | π Document | Read it whenβ¦ |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| 1 | π§ [Overview](01-overview.md) | You want to know **what** zopia is and **why** it exists |
|
|
15
|
+
| 2 | π― [Targets](02-targets.md) | You want the **explicit, testable goals** of the project |
|
|
16
|
+
| 3 | πΊοΈ [Roadmap](03-roadmap.md) | You want to know **what ships in which phase** |
|
|
17
|
+
| 4 | ποΈ [Architecture](04-architecture.md) | You are **maintaining or extending** modules, pipelines, representations, or errors |
|
|
18
|
+
| 5 | π§© [Concepts](05-concepts.md) | You need the **glossary** β Swagger 2.0, OpenAPI 3.x, JSON Schema, `$ref`, Zod v4, km-api |
|
|
19
|
+
| 6 | π [Conversions](06-conversions.md) | You need the **exact mapping rules** of the four engines |
|
|
20
|
+
| 7 | π [API docs format](07-api-docs.md) | You need the **output contract** β layouts, `index.ts`, manifest |
|
|
21
|
+
| 8 | π§± [Components](08-components.md) | You work with **`$ref`s** and the component options |
|
|
22
|
+
| 9 | βοΈ [Configuration](09-configuration.md) | You want the **full option reference** with defaults |
|
|
23
|
+
| 10 | π [Usage](10-usage.md) | You want to **use** zopia β programmatic API & CLI |
|
|
24
|
+
| 11 | π§ͺ [Testing](11-testing.md) | You are **writing tests** β scenario matrix, fixtures, coverage gates |
|
|
25
|
+
| 12 | π [Standards](12-standards.md) | You are **consuming the project** β code, JSDoc, commits, changelog, decisions |
|
|
26
|
+
|
|
27
|
+
## π§ Suggested paths
|
|
28
|
+
|
|
29
|
+
- π **New here** β [Overview](01-overview.md) β [Targets](02-targets.md) β
|
|
30
|
+
[Concepts](05-concepts.md) β [Usage](10-usage.md)
|
|
31
|
+
- π οΈ **Maintaining or extending zopia** β [Architecture](04-architecture.md) β
|
|
32
|
+
[Conversions](06-conversions.md) β [API docs format](07-api-docs.md) β
|
|
33
|
+
[Testing](11-testing.md)
|
|
34
|
+
- π€ **Question about a decision?** β [Standards β Key decisions](12-standards.md#-key-decisions)
|
|
35
|
+
|
|
36
|
+
## β
How this documentation is written
|
|
37
|
+
|
|
38
|
+
- π¨ Every document uses consistent emoji section headers and tables
|
|
39
|
+
- π Documents cross-link each other β follow a link, don't re-read
|
|
40
|
+
- π’ Rules are numbered (**R-β¦**), decisions are numbered (**D-β¦**), targets are
|
|
41
|
+
numbered (**T-β¦**) so they can be referenced from anywhere (code, tests, PRs)
|
|
42
|
+
- π§Ύ Every non-trivial design choice is recorded as a key decision with its rationale
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
# Publishes zopia to npm whenever a GitHub Release is published, or manually
|
|
4
|
+
# via workflow_dispatch. Credentials live in the repository secret NPM_TOKEN
|
|
5
|
+
# (automation/classic token with publish rights) or npm trusted publishing;
|
|
6
|
+
# the agent/maintainer running the pipeline never sees them. --provenance
|
|
7
|
+
# attestations require the id-token permission declared below.
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
release:
|
|
11
|
+
types: [published]
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
id-token: write
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
publish:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout
|
|
23
|
+
uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- name: Install Bun (pinned packageManager version)
|
|
26
|
+
uses: oven-sh/setup-bun@v2
|
|
27
|
+
with:
|
|
28
|
+
bun-version: 1.2.21
|
|
29
|
+
|
|
30
|
+
- name: Install Node (for the npm client)
|
|
31
|
+
uses: actions/setup-node@v4
|
|
32
|
+
with:
|
|
33
|
+
node-version: 22
|
|
34
|
+
registry-url: https://registry.npmjs.org
|
|
35
|
+
|
|
36
|
+
- name: Verify release tag matches package.json version
|
|
37
|
+
if: github.event_name == 'release'
|
|
38
|
+
run: |
|
|
39
|
+
version="$(node -p "require('./package.json').version")"
|
|
40
|
+
test "v${version}" == "${{ github.event.release.tag_name }}" || {
|
|
41
|
+
echo "Tag ${{ github.event.release.tag_name }} does not match package.json v${version}"
|
|
42
|
+
exit 1
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
- name: Publish to npm
|
|
46
|
+
run: npm publish --provenance --access public
|
|
47
|
+
env:
|
|
48
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|