@nekostack/schema 1.0.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 (214) hide show
  1. package/CHANGELOG.md +422 -0
  2. package/LICENSE +202 -0
  3. package/README.md +656 -0
  4. package/dist/src/builders/array.d.ts +12 -0
  5. package/dist/src/builders/array.d.ts.map +1 -0
  6. package/dist/src/builders/array.js +29 -0
  7. package/dist/src/builders/array.js.map +1 -0
  8. package/dist/src/builders/object.d.ts +62 -0
  9. package/dist/src/builders/object.d.ts.map +1 -0
  10. package/dist/src/builders/object.js +263 -0
  11. package/dist/src/builders/object.js.map +1 -0
  12. package/dist/src/builders/primitives.d.ts +37 -0
  13. package/dist/src/builders/primitives.d.ts.map +1 -0
  14. package/dist/src/builders/primitives.js +125 -0
  15. package/dist/src/builders/primitives.js.map +1 -0
  16. package/dist/src/builders/s.d.ts +27 -0
  17. package/dist/src/builders/s.d.ts.map +1 -0
  18. package/dist/src/builders/s.js +39 -0
  19. package/dist/src/builders/s.js.map +1 -0
  20. package/dist/src/builders/schema.d.ts +70 -0
  21. package/dist/src/builders/schema.d.ts.map +1 -0
  22. package/dist/src/builders/schema.js +98 -0
  23. package/dist/src/builders/schema.js.map +1 -0
  24. package/dist/src/cli-integration.d.ts +43 -0
  25. package/dist/src/cli-integration.d.ts.map +1 -0
  26. package/dist/src/cli-integration.js +48 -0
  27. package/dist/src/cli-integration.js.map +1 -0
  28. package/dist/src/errors/issue.d.ts +34 -0
  29. package/dist/src/errors/issue.d.ts.map +1 -0
  30. package/dist/src/errors/issue.js +89 -0
  31. package/dist/src/errors/issue.js.map +1 -0
  32. package/dist/src/generators/errors.d.ts +31 -0
  33. package/dist/src/generators/errors.d.ts.map +1 -0
  34. package/dist/src/generators/errors.js +34 -0
  35. package/dist/src/generators/errors.js.map +1 -0
  36. package/dist/src/generators/header.d.ts +42 -0
  37. package/dist/src/generators/header.d.ts.map +1 -0
  38. package/dist/src/generators/header.js +43 -0
  39. package/dist/src/generators/header.js.map +1 -0
  40. package/dist/src/generators/json-schema-meta.d.ts +36 -0
  41. package/dist/src/generators/json-schema-meta.d.ts.map +1 -0
  42. package/dist/src/generators/json-schema-meta.js +35 -0
  43. package/dist/src/generators/json-schema-meta.js.map +1 -0
  44. package/dist/src/generators/json-schema.d.ts +26 -0
  45. package/dist/src/generators/json-schema.d.ts.map +1 -0
  46. package/dist/src/generators/json-schema.js +88 -0
  47. package/dist/src/generators/json-schema.js.map +1 -0
  48. package/dist/src/generators/openapi.d.ts +33 -0
  49. package/dist/src/generators/openapi.d.ts.map +1 -0
  50. package/dist/src/generators/openapi.js +61 -0
  51. package/dist/src/generators/openapi.js.map +1 -0
  52. package/dist/src/generators/schema-fragment.d.ts +55 -0
  53. package/dist/src/generators/schema-fragment.d.ts.map +1 -0
  54. package/dist/src/generators/schema-fragment.js +253 -0
  55. package/dist/src/generators/schema-fragment.js.map +1 -0
  56. package/dist/src/generators/ts.d.ts +19 -0
  57. package/dist/src/generators/ts.d.ts.map +1 -0
  58. package/dist/src/generators/ts.js +252 -0
  59. package/dist/src/generators/ts.js.map +1 -0
  60. package/dist/src/generators/types.d.ts +96 -0
  61. package/dist/src/generators/types.d.ts.map +1 -0
  62. package/dist/src/generators/types.js +10 -0
  63. package/dist/src/generators/types.js.map +1 -0
  64. package/dist/src/generators/version.d.ts +11 -0
  65. package/dist/src/generators/version.d.ts.map +1 -0
  66. package/dist/src/generators/version.js +11 -0
  67. package/dist/src/generators/version.js.map +1 -0
  68. package/dist/src/generators/zod-mapping.d.ts +90 -0
  69. package/dist/src/generators/zod-mapping.d.ts.map +1 -0
  70. package/dist/src/generators/zod-mapping.js +174 -0
  71. package/dist/src/generators/zod-mapping.js.map +1 -0
  72. package/dist/src/generators/zod.d.ts +17 -0
  73. package/dist/src/generators/zod.d.ts.map +1 -0
  74. package/dist/src/generators/zod.js +118 -0
  75. package/dist/src/generators/zod.js.map +1 -0
  76. package/dist/src/index.d.ts +21 -0
  77. package/dist/src/index.d.ts.map +1 -0
  78. package/dist/src/index.js +43 -0
  79. package/dist/src/index.js.map +1 -0
  80. package/dist/src/ir/hash.d.ts +19 -0
  81. package/dist/src/ir/hash.d.ts.map +1 -0
  82. package/dist/src/ir/hash.js +22 -0
  83. package/dist/src/ir/hash.js.map +1 -0
  84. package/dist/src/ir/nodes.d.ts +121 -0
  85. package/dist/src/ir/nodes.d.ts.map +1 -0
  86. package/dist/src/ir/nodes.js +14 -0
  87. package/dist/src/ir/nodes.js.map +1 -0
  88. package/dist/src/ir/serialize.d.ts +8 -0
  89. package/dist/src/ir/serialize.d.ts.map +1 -0
  90. package/dist/src/ir/serialize.js +23 -0
  91. package/dist/src/ir/serialize.js.map +1 -0
  92. package/dist/src/migrations/build-migration-registry.d.ts +46 -0
  93. package/dist/src/migrations/build-migration-registry.d.ts.map +1 -0
  94. package/dist/src/migrations/build-migration-registry.js +134 -0
  95. package/dist/src/migrations/build-migration-registry.js.map +1 -0
  96. package/dist/src/migrations/handlers/list.d.ts +35 -0
  97. package/dist/src/migrations/handlers/list.d.ts.map +1 -0
  98. package/dist/src/migrations/handlers/list.js +55 -0
  99. package/dist/src/migrations/handlers/list.js.map +1 -0
  100. package/dist/src/migrations/handlers/plan.d.ts +26 -0
  101. package/dist/src/migrations/handlers/plan.d.ts.map +1 -0
  102. package/dist/src/migrations/handlers/plan.js +28 -0
  103. package/dist/src/migrations/handlers/plan.js.map +1 -0
  104. package/dist/src/migrations/handlers/stub.d.ts +22 -0
  105. package/dist/src/migrations/handlers/stub.d.ts.map +1 -0
  106. package/dist/src/migrations/handlers/stub.js +24 -0
  107. package/dist/src/migrations/handlers/stub.js.map +1 -0
  108. package/dist/src/migrations/handlers/verify.d.ts +25 -0
  109. package/dist/src/migrations/handlers/verify.d.ts.map +1 -0
  110. package/dist/src/migrations/handlers/verify.js +27 -0
  111. package/dist/src/migrations/handlers/verify.js.map +1 -0
  112. package/dist/src/migrations/parse-provenance.d.ts +78 -0
  113. package/dist/src/migrations/parse-provenance.d.ts.map +1 -0
  114. package/dist/src/migrations/parse-provenance.js +157 -0
  115. package/dist/src/migrations/parse-provenance.js.map +1 -0
  116. package/dist/src/migrations/plan-migration.d.ts +50 -0
  117. package/dist/src/migrations/plan-migration.d.ts.map +1 -0
  118. package/dist/src/migrations/plan-migration.js +256 -0
  119. package/dist/src/migrations/plan-migration.js.map +1 -0
  120. package/dist/src/migrations/stub.d.ts +55 -0
  121. package/dist/src/migrations/stub.d.ts.map +1 -0
  122. package/dist/src/migrations/stub.js +201 -0
  123. package/dist/src/migrations/stub.js.map +1 -0
  124. package/dist/src/migrations/types.d.ts +297 -0
  125. package/dist/src/migrations/types.d.ts.map +1 -0
  126. package/dist/src/migrations/types.js +28 -0
  127. package/dist/src/migrations/types.js.map +1 -0
  128. package/dist/src/migrations/verify-provenance.d.ts +46 -0
  129. package/dist/src/migrations/verify-provenance.d.ts.map +1 -0
  130. package/dist/src/migrations/verify-provenance.js +158 -0
  131. package/dist/src/migrations/verify-provenance.js.map +1 -0
  132. package/dist/src/registry/build-registry.d.ts +65 -0
  133. package/dist/src/registry/build-registry.d.ts.map +1 -0
  134. package/dist/src/registry/build-registry.js +172 -0
  135. package/dist/src/registry/build-registry.js.map +1 -0
  136. package/dist/src/registry/diff.d.ts +25 -0
  137. package/dist/src/registry/diff.d.ts.map +1 -0
  138. package/dist/src/registry/diff.js +497 -0
  139. package/dist/src/registry/diff.js.map +1 -0
  140. package/dist/src/registry/handlers/check.d.ts +57 -0
  141. package/dist/src/registry/handlers/check.d.ts.map +1 -0
  142. package/dist/src/registry/handlers/check.js +181 -0
  143. package/dist/src/registry/handlers/check.js.map +1 -0
  144. package/dist/src/registry/handlers/diff.d.ts +33 -0
  145. package/dist/src/registry/handlers/diff.d.ts.map +1 -0
  146. package/dist/src/registry/handlers/diff.js +61 -0
  147. package/dist/src/registry/handlers/diff.js.map +1 -0
  148. package/dist/src/registry/handlers/generate.d.ts +87 -0
  149. package/dist/src/registry/handlers/generate.d.ts.map +1 -0
  150. package/dist/src/registry/handlers/generate.js +223 -0
  151. package/dist/src/registry/handlers/generate.js.map +1 -0
  152. package/dist/src/registry/handlers/list.d.ts +36 -0
  153. package/dist/src/registry/handlers/list.d.ts.map +1 -0
  154. package/dist/src/registry/handlers/list.js +84 -0
  155. package/dist/src/registry/handlers/list.js.map +1 -0
  156. package/dist/src/registry/parse-provenance.d.ts +63 -0
  157. package/dist/src/registry/parse-provenance.d.ts.map +1 -0
  158. package/dist/src/registry/parse-provenance.js +182 -0
  159. package/dist/src/registry/parse-provenance.js.map +1 -0
  160. package/dist/src/registry/source-hash.d.ts +28 -0
  161. package/dist/src/registry/source-hash.d.ts.map +1 -0
  162. package/dist/src/registry/source-hash.js +32 -0
  163. package/dist/src/registry/source-hash.js.map +1 -0
  164. package/dist/src/registry/types.d.ts +185 -0
  165. package/dist/src/registry/types.d.ts.map +1 -0
  166. package/dist/src/registry/types.js +22 -0
  167. package/dist/src/registry/types.js.map +1 -0
  168. package/dist/src/runtime/compile.d.ts +38 -0
  169. package/dist/src/runtime/compile.d.ts.map +1 -0
  170. package/dist/src/runtime/compile.js +45 -0
  171. package/dist/src/runtime/compile.js.map +1 -0
  172. package/dist/src/runtime/errors.d.ts +25 -0
  173. package/dist/src/runtime/errors.d.ts.map +1 -0
  174. package/dist/src/runtime/errors.js +43 -0
  175. package/dist/src/runtime/errors.js.map +1 -0
  176. package/dist/src/runtime/normalize-issues.d.ts +65 -0
  177. package/dist/src/runtime/normalize-issues.d.ts.map +1 -0
  178. package/dist/src/runtime/normalize-issues.js +208 -0
  179. package/dist/src/runtime/normalize-issues.js.map +1 -0
  180. package/dist/src/runtime/parse.d.ts +62 -0
  181. package/dist/src/runtime/parse.d.ts.map +1 -0
  182. package/dist/src/runtime/parse.js +107 -0
  183. package/dist/src/runtime/parse.js.map +1 -0
  184. package/dist/src/runtime/strip-defaults.d.ts +51 -0
  185. package/dist/src/runtime/strip-defaults.d.ts.map +1 -0
  186. package/dist/src/runtime/strip-defaults.js +81 -0
  187. package/dist/src/runtime/strip-defaults.js.map +1 -0
  188. package/dist/src/runtime/zod-compile.d.ts +27 -0
  189. package/dist/src/runtime/zod-compile.d.ts.map +1 -0
  190. package/dist/src/runtime/zod-compile.js +92 -0
  191. package/dist/src/runtime/zod-compile.js.map +1 -0
  192. package/dist/src/types.d.ts +116 -0
  193. package/dist/src/types.d.ts.map +1 -0
  194. package/dist/src/types.js +2 -0
  195. package/dist/src/types.js.map +1 -0
  196. package/docs/ABSENCE_SEMANTICS.md +37 -0
  197. package/docs/BENCHMARKS.md +64 -0
  198. package/docs/COMPOSITION.md +206 -0
  199. package/docs/DIFF_CLASSIFICATION.md +137 -0
  200. package/docs/EXAMPLES.md +221 -0
  201. package/docs/HEADER_FORMAT.md +66 -0
  202. package/docs/INVARIANTS.md +58 -0
  203. package/docs/IR_CONTRACT.md +67 -0
  204. package/docs/ISSUE_CODES.md +99 -0
  205. package/docs/JSON_SCHEMA_MAPPING.md +149 -0
  206. package/docs/MIGRATIONS.md +406 -0
  207. package/docs/MIGRATION_GUIDE.md +150 -0
  208. package/docs/OPENAPI_MAPPING.md +66 -0
  209. package/docs/REGISTRY.md +336 -0
  210. package/docs/RUNTIME.md +279 -0
  211. package/docs/SCOPE.md +119 -0
  212. package/docs/USAGE.md +376 -0
  213. package/docs/ZOD_MODIFIER_ORDERING.md +77 -0
  214. package/package.json +45 -0
package/docs/SCOPE.md ADDED
@@ -0,0 +1,119 @@
1
+ # @nekostack/schema — Scope
2
+
3
+ Authoritative for what this package owns. If a capability is not listed under "Owned," it is somebody else's responsibility — see [`../../../BOUNDARIES.md`](../../../BOUNDARIES.md) for the full capability map.
4
+
5
+ ## Owned
6
+
7
+ - Canonical IR (`SchemaNode` AST) — the contract every generator must consume
8
+ - Schema authoring DSL (`s.string()`, `s.object()`, …)
9
+ - Schema metadata: id, version, description, deprecated
10
+ - Schema modifiers: optional, nullable, nullish, default
11
+ - Object unknown-key policy: strict (default), stripUnknown, passthrough
12
+ - Type inference (`s.infer`, `s.input`, `s.output`)
13
+ - **Runtime validation API (v0.6+):** `parse` / `safeParse` / `validate`
14
+ - **Runtime unknown-key enforcement (v0.6+):** strict / passthrough / stripUnknown execute at runtime, not just at generation time
15
+ - **Issue model + normalized `IssueCode` vocabulary** — consumer-facing error contract (Zod errors are normalized through this layer; downstream code sees `Issue`, never `ZodError`)
16
+ - **`ParseError`** (thrown only by `parse`; `safeParse` / `validate` return `Result`)
17
+ - **Result type** consumed by `safeParse` / `validate`
18
+ - Canonical IR serialization (sorted keys, undefined-stripped) — foundation for `irHash`
19
+ - **`sourceHash` provenance (v0.7+):** `sourceHashFromText(text)` and the `ProvenanceOptions.sourceHash` slice on every generator. The slice is **optional** — generators omit the `sourceHash` line/extension when the option is absent, so all pre-v0.7 callers continue to produce byte-identical output.
20
+ - **Registry-lite primitives (v0.7+, integration-subpath only — see boundary note below):** `buildRegistry`, `findSchema`, `parseProvenanceFromText`, `diffNodes`, the four pure handlers (`listHandler`, `diffHandler`, `checkHandler`, `generateHandler`), `suggestedPathFor`, `GENERATOR_KINDS`, and the supporting type surface. Reachable through `@nekostack/schema/cli` only; root `@nekostack/schema` does not export any of these. Documented in [`REGISTRY.md`](./REGISTRY.md).
21
+ - **Diff classification (v0.7+):** the locked breaking / additive / cosmetic table + `worstSeverity` aggregation. Pure data-in / data-out; never touches the filesystem. Documented in [`DIFF_CLASSIFICATION.md`](./DIFF_CLASSIFICATION.md).
22
+ - **Freshness verdict logic (v0.7+):** the two-hash matrix (`clean` / `cosmetic_drift` / `stale` / `integrity_error`) — pure classification given a registry and pre-read artifact bytes. The CLI owns the filesystem reads; this package owns the verdict.
23
+ - **Migration primitives (v0.8+, integration-subpath only — see boundary note below):** `parseMigrationProvenanceFromText`, `buildMigrationRegistry`, `planMigration`, `verifyMigrationProvenance`, `stubMigration`, `suggestedMigrationPathFor`, the four pure handlers (`listMigrationsHandler`, `planMigrationHandler`, `verifyMigrationsHandler`, `stubMigrationHandler`), and the supporting type surface (`Migration`, `AnyMigration`, `MigrationRegistry`, `MigrationPlan`, `MigrationVerdict`, `VerificationResult`, `MigrationStub`, the four `*Opts` / `*Result` pairs). Reachable through `@nekostack/schema/cli` only; root `@nekostack/schema` does not export any of these. **No apply verb, no transform execution** — this package owns *planning* and *verification* of authored migrations, never their application. Documented in [`MIGRATIONS.md`](./MIGRATIONS.md).
24
+
25
+ ## Not owned
26
+
27
+ | Capability | Lives in |
28
+ |---|---|
29
+ | API routing / request-response boundary validation | `@nekostack/api` |
30
+ | Form rendering + state management | `@nekostack/form` |
31
+ | Database schema definition + DDL | `@nekostack/migrate` |
32
+ | OpenAPI route descriptions | `@nekostack/api` |
33
+ | Runtime telemetry / event payloads | `@nekostack/telemetry`, `@nekostack/events` |
34
+ | Auth policy / access decisions | `@nekostack/auth` |
35
+ | Cross-record / continuity validation | `@nekostack/validator` |
36
+ | App-level validation flows | application code |
37
+ | Branded ID primitives (UUID/ULID brands) | `@nekostack/id` |
38
+ | Schema-data migration *execution* (running a migration's `transform` function over real data) | NOT in `@nekostack/schema`. v0.8 ships planning + verification + stub generation only; an "apply" verb is explicitly out of scope. The eventual runner — if any — lives in a downstream package, never in `@nekostack/schema`. |
39
+ | Database / DDL migrations | `@nekostack/migrate` (always — `@nekostack/schema` migrations are schema-data only) |
40
+ | Rollback / reverse migrations | not supported (v0.8 is forward-only; one direction per authored file) |
41
+ | Cross-schema migrations (a single migration touching more than one `schemaId`) | not supported (Decision #4 — one `schemaId` per migration; split into separate migrations) |
42
+ | Filesystem walking of migration directories / dynamic loading of authored migration modules / stdout / exit codes for `neko schema migrate *` | `@nekostack/cli` (Master plan Decision #1 stays in force for v0.8) |
43
+ | Global CLI runtime / plugin discovery | `@nekostack/cli` |
44
+ | Filesystem reads / writes (source discovery, artifact reads, regenerated-artifact writes) | `@nekostack/cli` (Master plan Decision #1 — schema is pure) |
45
+ | Dynamic schema loading via `tsx` | `@nekostack/cli` |
46
+ | stdout / stderr formatting + CLI exit codes | `@nekostack/cli` |
47
+ | `neko schema *` CLI commands (`list` / `diff` / `check` / `generate`) | `@nekostack/cli` (v0.7 — consumes the registry primitives exposed under `@nekostack/schema/cli`) |
48
+ | Runtime validation *engine* (the bytecode-level matcher) | external (Zod is the v0.6 internal engine; consumers don't see it) |
49
+ | Transforms / unions / runtime refinements in v0.6 / v0.7 | deferred (v0.6 / v0.7 support the v0.2 subset; date/union/recursiveRef/transform/runtime-refinement IR throws `UnsupportedNodeKindError` at compile time and at diff time) |
50
+
51
+ ## v0.6-specific scope
52
+
53
+ v0.6 ships (in addition to everything v0.1–v0.5 already shipped):
54
+ - `parse(schema, input): s.output<S>` — throws `ParseError` on failure
55
+ - `safeParse(schema, input): Result<s.output<S>>` — non-throwing Result variant
56
+ - `validate(schema, input): Result<s.input<S>>` — structural check; no default fill, no transforms; portable refinements still run
57
+ - `ParseError extends Error` (frozen defensive issue copy; `code = "parse_failed"`)
58
+ - Issue normalization layer translating Zod issues into the v0.1 `IssueCode` vocabulary (Decision #12)
59
+ - Compile cache keyed by `SchemaNode` identity
60
+ - Validate-only IR variant transform (defaults stripped, default-bearing fields flipped to optional)
61
+ - Four-oracle semantic-parity test matrix (Decision #19)
62
+ - OpenAPI spec-validity carry-forward (Decision #19a) tied to runtime fixture shapes
63
+
64
+ v0.6 explicitly **does not** ship:
65
+ - Date / union / recursiveRef / transform IR support (still throws `UnsupportedNodeKindError` at compile time)
66
+ - Runtime refinements (custom predicates) — IR shape declared, but builders and runtime execution remain deferred; the runtime fails loudly when one appears in IR
67
+ - Method-style API (`schema.parse(input)`) — free functions only in v0.6
68
+ - `ValidateError` companion to `ParseError` — `validate` returns `Result` only
69
+ - Schema registry / freshness / diffing — v0.7
70
+ - Schema-version negotiation — v0.7
71
+ - Locale / i18n of error messages
72
+ - CLI commands — v0.7 (`@nekostack/cli` consumes the runtime)
73
+
74
+ If something on the "does not ship" list appears in code, the scope was crossed and the PR should be rejected.
75
+
76
+ ## v0.7-specific scope (in progress — see [`ROADMAP.md`](./ROADMAP.md))
77
+
78
+ v0.7 ships (schema-side) — *additive* over v0.6, no public-surface breakage at root `@nekostack/schema`:
79
+
80
+ - `sourceHash` provenance slice on every generator (optional; opt-in via `ProvenanceOptions.sourceHash`).
81
+ - `parseProvenanceFromText` — read the JSDoc-header or `x-nekostack` provenance off a committed artifact.
82
+ - Registry primitives — `buildRegistry`, `findSchema` — pure, `Result<Registry>` failure path, never throws.
83
+ - Diff classifier — `diffNodes` + the `worstSeverity` aggregation in `diffHandler`. Locked Decision #12 table; see [`DIFF_CLASSIFICATION.md`](./DIFF_CLASSIFICATION.md).
84
+ - Four pure handlers — `list`, `diff`, `check`, `generate`. Data-in / data-out, gated by [`../tests/registry/handler-purity.test.ts`](../tests/registry/handler-purity.test.ts).
85
+ - Integration subpath — `@nekostack/schema/cli` exposes the v0.7 surface for `@nekostack/cli` only. Root `@nekostack/schema` retains the v0.6 contract unchanged. See [`REGISTRY.md`](./REGISTRY.md).
86
+
87
+ v0.7 explicitly **does not** ship (this package):
88
+
89
+ - Filesystem I/O of any kind — owned by `@nekostack/cli` (Master plan Decision #1).
90
+ - Schema-file discovery, dynamic `import()` of schemas via `tsx` — `@nekostack/cli`.
91
+ - stdout / stderr formatting, exit-code mapping — `@nekostack/cli`.
92
+ - `neko schema *` commands themselves — `@nekostack/cli` (companion plan steps 21–34).
93
+ - Partial generation (subset of artifact kinds) — Master plan Decision #6 locks all-or-nothing per schema.
94
+ - Migration proposal / generation / application — covered by v0.8 (in progress; see below) for the planning + verification + stub surface; **execution** of a migration's `transform` function remains explicitly out of scope.
95
+
96
+ ## v0.8-specific scope (in progress — see [`ROADMAP.md`](./ROADMAP.md) and [`PHASE_PLAN_v0.8.md`](./PHASE_PLAN_v0.8.md))
97
+
98
+ v0.8 ships (schema-side) — *additive* over v0.7, no public-surface breakage at root `@nekostack/schema`. The contract is described in full in [`MIGRATIONS.md`](./MIGRATIONS.md).
99
+
100
+ - **Schema-data migrations only** — authored `Migration<SchemaId, FromVersion, ToVersion, Input, Output>` modules carrying a pure `transform(input)` function plus a 9-field JSDoc provenance header. No database DDL, no rollback, no cross-schema migrations, no transform-correctness proof.
101
+ - **`parseMigrationProvenanceFromText(text)`** — read the 9-field JSDoc provenance carrier (Decision #7) off a committed migration file. Fail-loud `integrity_error` + `metadata.reason` on malformed input; never silently skips (round-2 audit correction).
102
+ - **`buildMigrationRegistry(entries)`** — pure constructor with first-seen-wins duplicate handling; three-level shape supporting exact-triple `(schemaId, fromVersion, toVersion)` lookup AND `(schemaId, fromVersion) → outgoing edges` for chain enumeration.
103
+ - **`planMigration({ schemaRegistry, migrationRegistry, schemaId, fromVersion, toVersion })`** — diff-aware planner. Consumes both registries so it can honor Decision #10's severity → migration-requirement dispatch. The locked rules are: `null` / `cosmetic` diff → empty chain (no notes); `null` / `cosmetic` diff *with* an exact migration also authored → empty chain plus an `over_specified` `PlanNote` (the migration is not included in the chain); `additive` diff *with* an exact migration → chain containing that migration (no notes); `additive` diff *without* a migration → empty chain plus an `additive_no_migration` `PlanNote`; `breaking` diff → chain is **required** (DFS enumeration over the `(schemaId, fromVersion)` adjacency, failure if no chain reaches `toVersion`). Never calls `transform`.
104
+ - **`verifyMigrationProvenance({ schemaRegistry, migrationRegistry })`** — four-way verdict mirroring the v0.7 freshness matrix: `bound` / `cosmetic_drift` / `drift` / `missing_endpoint`. Requires **both** registries because it compares each migration's provenance `from{Ir,Source}Hash` / `to{Ir,Source}Hash` against whatever the current schema registry reports for the two endpoint versions. Pure classifier; the CLI owns the filesystem reads. `bound` and `cosmetic_drift` are success-compatible (`cosmetic_drift` is warning-class on the success branch); `drift` and `missing_endpoint` are failure-class and carry stable metadata reasons.
105
+ - **`stubMigration(opts)` + `suggestedMigrationPathFor(opts)`** — pure file-content generator emitting the JSDoc header + `import type { Migration } from "@nekostack/schema/cli"` + typed declaration + `transform(input) { throw "Not yet implemented"; }` body + default export. The slug rule is byte-compatible with v0.7's `suggestedPathFor`.
106
+ - **Four pure handlers** — `listMigrationsHandler`, `planMigrationHandler`, `verifyMigrationsHandler`, `stubMigrationHandler`. Data-in / data-out; the schema package never touches the filesystem and never calls `.transform(...)`. Cross-handler purity gated by [`../tests/migrations/handler-purity.test.ts`](../tests/migrations/handler-purity.test.ts) (static-import scan + sentinel coverage).
107
+ - **Integration subpath extension** — `@nekostack/schema/cli` exports the v0.8 surface alongside the v0.7 surface. Root `@nekostack/schema` retains the v0.6 contract unchanged; the negative-leakage gate in [`../tests/public-surface.test.ts`](../tests/public-surface.test.ts) extends to every v0.8 runtime + type name.
108
+
109
+ v0.8 explicitly **does not** ship (this package, ever — these are hard-locked non-goals, not "later" items):
110
+
111
+ - **Migration apply / runner / executor** — no verb on the schema side runs `transform(input)` against real data. Master plan Decision #2 (forward-only) and the [`MIGRATIONS.md`](./MIGRATIONS.md) non-goals table are the contract.
112
+ - **Rollback / reverse migrations** — Decision #2 is forward-only.
113
+ - **Cross-schema migrations** — Decision #4 — exactly one `schemaId` per authored migration.
114
+ - **Multi-hop skip** — chain enumeration is required; the planner never silently bypasses intermediate versions.
115
+ - **Database / DDL migrations** — `@nekostack/migrate`'s concern, not `@nekostack/schema`'s.
116
+ - **Transform correctness proof** — the package never executes a migration's `transform`, so it has no opinion on whether the transform is correct. Verification covers provenance integrity only ("provenance says what it says"), not behavioral validity.
117
+ - **Filesystem I/O, dynamic `import()` of migration modules, stdout/stderr, exit codes** — owned by `@nekostack/cli` (Master plan Decision #1 stays in force).
118
+
119
+ **Top-level evaluation nuance** — authored migration files are TypeScript modules. When the CLI loads them via `tsx` (Step 19 of the v0.8 plan), their top-level code evaluates. The schema package itself never imports authored migration files and never triggers their evaluation; it sees them only as parsed provenance + opaque `Migration` records produced by the CLI loader. Authors who put side-effecting code at module top-level will pay for it inside the CLI, not inside the schema package.
package/docs/USAGE.md ADDED
@@ -0,0 +1,376 @@
1
+ # Usage — `@nekostack/schema`
2
+
3
+ > What v0.6 lets you do as an author, end-to-end. For the runtime contract, see [`RUNTIME.md`](./RUNTIME.md). For the why and the scope boundaries, see [`SCOPE.md`](./SCOPE.md). For the full surface, see [`../README.md`](../README.md).
4
+
5
+ ## What v0.6 is good for
6
+
7
+ v0.6 is the phase where `@nekostack/schema` becomes the **runtime-validation workflow**, not just a code generator. From a single schema definition:
8
+
9
+ 1. **Validate input directly** via `parse` / `safeParse` / `validate` — no Zod import in your code.
10
+ 2. A **TypeScript type alias** matching the schema's runtime shape (with the v0.1 input/output split honored).
11
+ 3. A **Zod 3.x validator** as a portable artifact for interoperability (microservices that don't import `@nekostack/schema`, third-party tools, etc.).
12
+ 4. A **JSON Schema draft 2020-12** + **OpenAPI 3.1 component** for cross-language contracts and API documentation.
13
+ 5. A **deterministic header** on every generated file recording schema id, version, IR hash, and generator version.
14
+
15
+ Zod is the internal execution engine for runtime validation. A consumer of `@nekostack/schema` does **not** import Zod for runtime validation; the surface is `parse` / `safeParse` / `validate` from `@nekostack/schema`. See [`RUNTIME.md`](./RUNTIME.md) for the full contract — including default semantics, unknown-key policies, issue normalization, and what the validate-only IR variant does. You **cannot** yet use this package for a CLI workflow (v0.7).
16
+
17
+ ## Defining a schema
18
+
19
+ ```ts
20
+ import { s } from "@nekostack/schema";
21
+
22
+ export const Tenant = s
23
+ .object({
24
+ id: s.string().uuid(),
25
+ slug: s.string().min(2).max(50).regex(/^[a-z0-9-]+$/),
26
+ name: s.string().min(1).max(120),
27
+ plan: s.enum(["free", "pro", "enterprise"] as const).default("free"),
28
+ billingEmail: s.string().email().nullable(),
29
+ })
30
+ .id("com.nekostack.tenant.Tenant")
31
+ .version("1.0.0")
32
+ .describe("A NekoStack workspace tenant.");
33
+ ```
34
+
35
+ Three things every "real" schema should do, derived from v0.1:
36
+
37
+ - Give it an `.id("com.org.area.Name")` — generators put it in the header; v0.7 will look it up here.
38
+ - Give it a `.version("x.y.z")` — generators emit it; future breaking-change detection (v0.7) keys off it.
39
+ - Give it a `.describe("...")` — generators emit it as JSDoc / Zod `.describe()`.
40
+
41
+ Anonymous schemas (no `.id()`) work — generators emit `schemaId: null` with a visible `// anonymous schema` comment — but they're not registry-addressable.
42
+
43
+ ## Validating input at runtime
44
+
45
+ ```ts
46
+ import { s, parse, safeParse, validate, ParseError } from "@nekostack/schema";
47
+ import { Tenant } from "./tenant.schema.js";
48
+
49
+ // Throw on failure — the friction-causing default.
50
+ const tenant = parse(Tenant, input);
51
+ // ^ s.output<typeof Tenant> (defaults filled)
52
+
53
+ // Return a Result instead of throwing.
54
+ const r = safeParse(Tenant, input);
55
+ if (r.success) {
56
+ r.data; // s.output<typeof Tenant>
57
+ } else {
58
+ r.issues; // readonly Issue[]
59
+ }
60
+
61
+ // Structural check only — does NOT fill defaults, does NOT run transforms.
62
+ const v = validate(Tenant, input);
63
+ if (v.success) {
64
+ v.data; // s.input<typeof Tenant> (default-bearing fields stay absent)
65
+ }
66
+
67
+ // ParseError carries the full normalized issue list.
68
+ try {
69
+ parse(Tenant, input);
70
+ } catch (e) {
71
+ if (e instanceof ParseError) {
72
+ for (const i of e.issues) {
73
+ // i.code — stable NekoStack IssueCode
74
+ // i.path — Zod path, copied
75
+ // i.expected/received — verbatim from Zod when available
76
+ // i.schemaId / i.schemaVersion — from schema.metadata when present
77
+ // i.severity — always "error" in v0.6
78
+ }
79
+ } else {
80
+ throw e;
81
+ }
82
+ }
83
+ ```
84
+
85
+ Three things to know:
86
+
87
+ - **`parse` / `safeParse` fill defaults.** `validate` accepts a missing default-bearing field but does **not** fill it. See [`RUNTIME.md` → Default semantics](./RUNTIME.md#default-semantics).
88
+ - **Unknown keys are rejected by default.** The object policy is `strict`. Use `.passthrough()` to preserve them in the output, `.stripUnknown()` to drop them. Same behavior across `parse`, `safeParse`, and `validate`.
89
+ - **Issues use a stable NekoStack vocabulary.** Consumers see `Issue` / `IssueCode` from `@nekostack/schema`, never a `ZodError`. The Zod engine is internal and may be replaced in a future phase without changing the consumer-facing contract. The full mapping table is in [`RUNTIME.md` → Issue normalization](./RUNTIME.md#issue-normalization).
90
+
91
+ ## Generating a TypeScript type
92
+
93
+ ```ts
94
+ import { generateTypeScript } from "@nekostack/schema";
95
+ import { Tenant } from "./tenant.schema.js";
96
+
97
+ const ts = generateTypeScript(Tenant.node);
98
+ // Returns a complete .ts module as a string: header + `export type Tenant = { ... };`
99
+ ```
100
+
101
+ Output mode controls the input/output split:
102
+
103
+ | `options.mode` | Emits | Use when |
104
+ |---|---|---|
105
+ | *(omitted)* / `"output"` | `export type <Name> = ...` | You want the post-default, post-transform type (matches `s.infer<T>` / `s.output<T>`). |
106
+ | `"input"` | `export type <Name>Input = ...` | You're typing API request bodies / form values before defaults apply. |
107
+ | `"both"` | `export type <Name>Input` + `export type <Name>Output` | You want both sides explicit. |
108
+
109
+ The two sides genuinely differ for any schema with a `.default(v)` field — default-bearing fields are object-optional in Input, object-required in Output. See [`ABSENCE_SEMANTICS.md`](./ABSENCE_SEMANTICS.md) for the full table.
110
+
111
+ `mode: "both"` is the safest default for shared API contracts.
112
+
113
+ ## Generating a Zod validator (as an artifact)
114
+
115
+ > Since v0.6, generated Zod is an **interoperability artifact**, not your runtime path. Use `parse` / `safeParse` / `validate` for in-process validation; emit Zod source for downstream services or third-party tools that want to consume a portable Zod schema without importing `@nekostack/schema`.
116
+
117
+ ```ts
118
+ import { generateZod } from "@nekostack/schema";
119
+ import { Tenant } from "./tenant.schema.js";
120
+
121
+ const zod = generateZod(Tenant.node);
122
+ // Returns: header + `import { z } from "zod"` + `export const schema = z.object({...}).strict()...`
123
+ ```
124
+
125
+ The emitted source and the runtime compiler share a single semantic mapping (Decision #6 of the v0.6 plan), so the generated Zod file and `parse(Tenant, input)` agree on accept/reject for every input. The four-oracle parity matrix in [`tests/semantic-parity.test.ts`](../tests/semantic-parity.test.ts) is what asserts that.
126
+
127
+ Modifier ordering is fixed; see [`ZOD_MODIFIER_ORDERING.md`](./ZOD_MODIFIER_ORDERING.md). The headline rule: **default is always last**, so the v0.1 absence-semantics contract survives translation.
128
+
129
+ Consumer of the generated file needs Zod installed; `@nekostack/schema` already depends on Zod for its own runtime, so no extra setup is needed for in-process validation.
130
+
131
+ ## Generating a JSON Schema
132
+
133
+ ```ts
134
+ import { generateJsonSchema } from "@nekostack/schema";
135
+ import { Tenant } from "./tenant.schema.js";
136
+
137
+ const json = generateJsonSchema(Tenant.node);
138
+ // Returns: a complete draft-2020-12 JSON document as a string.
139
+ ```
140
+
141
+ Default `$id` is URN-shaped (`urn:nekostack:schema:<id>:<version>`). For URL-shaped IDs (when you actually host schemas at a real URL), pass `idBase`:
142
+
143
+ ```ts
144
+ generateJsonSchema(Tenant.node, { idBase: "https://schemas.example.com" });
145
+ // $id: "https://schemas.example.com/com.nekostack.tenant.Tenant/1.0.0"
146
+ ```
147
+
148
+ Important properties:
149
+
150
+ - **Models accepted input.** JSON Schema treats `default` as annotation, not behavior — validators don't fill defaults. The output represents what the wire input is allowed to look like; the runtime (or generated Zod) is responsible for applying defaults.
151
+ - **`stripUnknown` is encoded as `additionalProperties: true` + `x-nekostack-strip: true`.** JSON Schema can't express mutation; the runtime strips. The extension key tells NekoStack-aware consumers to do that.
152
+ - **Throws on semantic-loss cases.** Runtime refinements (custom predicates) and regex with non-empty flags throw `UnsupportedNodeKindError` rather than emit JSON Schema that changes validation behavior. See [`JSON_SCHEMA_MAPPING.md`](./JSON_SCHEMA_MAPPING.md) for the full contract.
153
+ - **Provenance under `x-nekostack`.** JSON has no comment syntax, so the v0.2-style header is moved into a single `x-nekostack: { generator, generatorVersion, irHash, schemaId, schemaVersion }` object.
154
+
155
+ Optional consumer dep: `ajv` (any version that supports draft 2020-12 — import via `ajv/dist/2020.js`). Not required to *generate*, only to *validate*.
156
+
157
+ ## Generating an OpenAPI 3.1 component schema
158
+
159
+ ```ts
160
+ import { generateOpenApiSchemaComponent } from "@nekostack/schema";
161
+ import { Tenant } from "./tenant.schema.js";
162
+
163
+ const component = generateOpenApiSchemaComponent(Tenant.node);
164
+ // Returns: canonical JSON for the value at `components.schemas.Tenant` in
165
+ // an OpenAPI 3.1 document. Compose into your own document.
166
+ ```
167
+
168
+ The output is a **component schema fragment**, not a full OpenAPI document. Paths / operations / responses / security schemes / etc. are not in scope for `@nekostack/schema` — they belong to a future `@nekostack/api` package.
169
+
170
+ Differences from `generateJsonSchema`:
171
+ - **No `$schema`** — OpenAPI 3.1 documents declare the schema dialect at the document root via `jsonSchemaDialect`; components inherit it.
172
+ - **No `$id`** — component identity is the position in the document (`#/components/schemas/<Name>`).
173
+ - **`x-nekostack.generator: "openApi"`** — distinguishes the artifact from JSON Schema output.
174
+
175
+ Everything else (absence semantics, object policy, refinement mapping, throw behavior on runtime refinements + regex with flags, `x-nekostack-strip` / `x-nekostack-default-applied-by`) is **identical to JSON Schema** because both generators share the same internal fragment emitter. See [`OPENAPI_MAPPING.md`](./OPENAPI_MAPPING.md) for the delta table and [`JSON_SCHEMA_MAPPING.md`](./JSON_SCHEMA_MAPPING.md) for the full mapping.
176
+
177
+ Optional consumer dep: `@redocly/openapi-core` (or any OpenAPI 3.1 validator) if you want to validate the composed document.
178
+
179
+ ## Composing existing object schemas
180
+
181
+ ```ts
182
+ import { s } from "@nekostack/schema";
183
+
184
+ const User = s.object({
185
+ id: s.string().uuid(),
186
+ email: s.string().email(),
187
+ role: s.string().default("member"),
188
+ });
189
+
190
+ // Common patterns:
191
+ const UserInputForCreate = User.omit({ id: true }); // server fills id
192
+ const UserPatch = User.partial(); // PATCH/update shape
193
+ const UserPublic = User.pick({ id: true, email: true }); // safe-to-expose subset
194
+ const UserAdmin = User.extend({ permissions: s.array(s.string()) });
195
+ const UserWithNumericId = User.override({ id: s.number() });
196
+ ```
197
+
198
+ Seven operators are available on every `ObjectSchema`: `extend`, `pick`, `omit`, `partial`, `required`, `merge`, `override`. All return a new `ObjectSchema` (no mutation), fail loudly on conflicts / unknown keys / missing keys, drop top-level metadata (re-tag with `.id().version().describe()` if needed), and preserve field-level metadata. See [`COMPOSITION.md`](./COMPOSITION.md) for the full contract — especially:
199
+
200
+ - **`partial()` and `required()` both strip `default`.** A partial schema should not silently inject defaults into a PATCH payload; a required + default-bearing field is semantically contradictory.
201
+ - **`merge` throws on field conflict AND on `unknownKeys` mismatch by default.** Resolve explicitly via `{ conflict: "left" | "right" }` and `{ unknownKeys: "left" | "right" }`.
202
+ - **`extend` and `override` are asymmetric on purpose** — `extend` rejects existing keys; `override` rejects missing keys. The pair covers add and replace without overlap.
203
+
204
+ Composition produces a plain `ObjectNode` — no generator changes; the TS / Zod / JSON Schema / OpenAPI generators handle composed schemas identically to hand-written equivalents (asserted by parity tests).
205
+
206
+ ## Generated-file headers
207
+
208
+ Every output file starts with a deterministic JSDoc block — full spec in [`HEADER_FORMAT.md`](./HEADER_FORMAT.md):
209
+
210
+ ```ts
211
+ /**
212
+ * @generated by @nekostack/schema
213
+ * schemaId: com.nekostack.tenant.Tenant
214
+ * schemaVersion: 1.0.0
215
+ * irHash: sha256:7f3e2a9b...
216
+ * generator: typescript
217
+ * generatorVersion: @nekostack/schema@0.7.0
218
+ *
219
+ * DO NOT EDIT MANUALLY.
220
+ */
221
+ ```
222
+
223
+ Consumers may:
224
+ - Read `irHash` to detect stale generated artifacts.
225
+ - Read `schemaId` / `schemaVersion` to identify which schema this file represents.
226
+ - Refuse to load files with an older `generatorVersion`.
227
+
228
+ Consumers must NOT:
229
+ - Edit a generated file by hand (CI may enforce later).
230
+ - Parse data out of free-form comments — use the typed fields.
231
+
232
+ ## Computing `irHash` directly
233
+
234
+ ```ts
235
+ import { irHash } from "@nekostack/schema";
236
+ irHash(Tenant.node); // → "7f3e2a9b..." (64-char hex, sha256 of canonical IR serialization)
237
+ ```
238
+
239
+ Same IR → same hash, every time. Semantic change → different hash. This is the foundation v0.7's CI freshness check uses; you can use it now to gate your own deploys ("if `irHash(latest) !== headerOf(committedFile).irHash`, regenerate").
240
+
241
+ ## Workflow today (no CLI yet)
242
+
243
+ There is no `neko schema generate` command in v0.6, and the v0.7 CLI is in progress but not yet shipped — see [`ROADMAP.md`](./ROADMAP.md). The intended workflow until v0.7 lands:
244
+
245
+ 1. Author the schema in `your-package/schemas/foo.schema.ts`.
246
+ 2. **For runtime validation:** import `parse` / `safeParse` / `validate` from `@nekostack/schema` and call them directly. No generated artifact required.
247
+ 3. **For artifact generation** (cross-language contracts, microservice interop, documentation): write a script (or a vitest snapshot test — see this package's [`tests/examples/regenerate.test.ts`](../tests/examples/regenerate.test.ts) for a worked example) that calls `generateTypeScript` / `generateZod` / `generateJsonSchema` / `generateOpenApiSchemaComponent` and writes the result to disk.
248
+ 4. Commit both the source schema and any generated files.
249
+ 5. Review diffs as ordinary code review.
250
+
251
+ Once v0.7's CLI ships, `neko schema generate` / `check` / `diff` / `list` replace the hand-written generation script and add freshness CI.
252
+
253
+ ## `@nekostack/schema/cli` — internal integration only (v0.7)
254
+
255
+ > Not a public consumer API. This is the subpath `@nekostack/cli` imports from to build the v0.7 `neko schema *` commands.
256
+
257
+ The v0.7 registry / freshness / generation primitives are reachable through a separate subpath:
258
+
259
+ ```text
260
+ @nekostack/schema public v0.6 consumer surface (parse, safeParse, validate, s, generators, …)
261
+ @nekostack/schema/cli package-internal CLI integration surface (buildRegistry, diffNodes, four handlers, …)
262
+ ```
263
+
264
+ If you are writing application code or a library that uses `@nekostack/schema` directly, **do not** import from `@nekostack/schema/cli`. The names exposed there are subject to internal change between minor versions; engine-swap-safety lives at the root, not at this subpath. The negative gate in [`../tests/public-surface.test.ts`](../tests/public-surface.test.ts) enforces that root `@nekostack/schema` does not leak any v0.7 registry name.
265
+
266
+ The subpath exists so the eventual `neko schema *` CLI (companion plan in [`../../cli/docs/PHASE_PLAN_v0.7.md`](../../cli/docs/PHASE_PLAN_v0.7.md)) has a stable, package-internal seam to import from. Full surface and contract in [`REGISTRY.md`](./REGISTRY.md); diff classification in [`DIFF_CLASSIFICATION.md`](./DIFF_CLASSIFICATION.md).
267
+
268
+ ## Schema-data migrations (v0.8 — in progress, preview only)
269
+
270
+ > v0.8 is **in progress** on [`feat/schema-v0.8-candidate`](https://github.com/cmclicker/NekoStack/tree/feat/schema-v0.8-candidate) ([PR #28](https://github.com/cmclicker/NekoStack/pull/28)), not yet shipped. The schema-side primitives below already exist on the branch; the CLI-side `neko schema migrate *` verbs are still being wired up. This section is a preview of the v0.8 workflow once it lands — the contract is locked in [`MIGRATIONS.md`](./MIGRATIONS.md).
271
+
272
+ v0.8 lets you describe how to migrate **data** from one version of a schema to a newer version of the same schema. It ships planning, verification, and stub generation — it does **not** ship an apply verb, and the package never executes a migration's `transform(input)`. If you need to run a migration against real data, that lives in your own code or a downstream package.
273
+
274
+ A migration is a TypeScript module under your package's migration directory. The locked file shape (Decision #5 + Decision #6) is the JSDoc-only nine-field provenance carrier emitted by `stubMigration`, an `import type { Migration } from "@nekostack/schema/cli"`, a typed declaration, and a default export:
275
+
276
+ ```ts
277
+ /**
278
+ * @migration by @nekostack/schema
279
+ * schemaId: com.nekostack.tenant.Tenant
280
+ * fromVersion: 1.0.0
281
+ * toVersion: 2.0.0
282
+ * fromIrHash: sha256:<hex of the v1 IR>
283
+ * toIrHash: sha256:<hex of the v2 IR>
284
+ * fromSourceHash: sha256:<hex of the v1 schema source file's UTF-8 bytes>
285
+ * toSourceHash: sha256:<hex of the v2 schema source file's UTF-8 bytes>
286
+ * generator: neko-schema-migrate-stub
287
+ * generatorVersion: @nekostack/schema@0.8.0
288
+ *
289
+ * DO NOT REMOVE THE HEADER. Authors EDIT THE BODY.
290
+ */
291
+ import type { Migration } from "@nekostack/schema/cli";
292
+ import type { TenantV1 } from "../generated/tenant-1.0.0.types.js";
293
+ import type { TenantV2 } from "../generated/tenant-2.0.0.types.js";
294
+
295
+ const migration: Migration<
296
+ "com.nekostack.tenant.Tenant",
297
+ "1.0.0",
298
+ "2.0.0",
299
+ TenantV1,
300
+ TenantV2
301
+ > = {
302
+ schemaId: "com.nekostack.tenant.Tenant",
303
+ from: "1.0.0",
304
+ to: "2.0.0",
305
+ transform(input) {
306
+ return { ...input, /* v2 shape */ };
307
+ },
308
+ };
309
+
310
+ export default migration;
311
+ ```
312
+
313
+ > **`fromSourceHash` / `toSourceHash` bind the *endpoint schemas'* source bytes**, not the migration file's own source hash. There is no per-migration-file `sourceHash` in the v0.8 header — the verifier compares `from{Ir,Source}Hash` and `to{Ir,Source}Hash` to whatever the schema registry currently reports for the two endpoint versions, so the migration stays anchored to a specific shape of both endpoints. The `Migration` object on disk uses the compact `from` / `to` keys; the `MigrationEntry` produced by `buildMigrationRegistry` exposes the same triple as `(schemaId, fromVersion, toVersion)` for index lookup.
314
+
315
+ Three rules:
316
+
317
+ - **One `schemaId` per migration** (Decision #4). A single migration may never straddle two schemas; split it.
318
+ - **Forward-only** (Decision #2). `fromVersion` is always older than `toVersion`; no reverse module shape, no rollback.
319
+ - **Fail-loud provenance.** All nine header fields are required. Missing or malformed headers fail with `code: "integrity_error"` and a stable `metadata.reason` — they are never silently skipped.
320
+
321
+ Once v0.8's CLI ships, four verbs will mirror the v0.7 `neko schema *` shape:
322
+
323
+ | Verb | What it does | Pure surface it consumes |
324
+ |---|---|---|
325
+ | `neko schema migrate list` | Enumerate authored migrations grouped by `(schemaId, fromVersion)` | `listMigrationsHandler` |
326
+ | `neko schema migrate plan` | Compute the migration chain (or "none needed", or "over-specified") for a `(schemaId, fromVersion, toVersion)` path | `planMigrationHandler` (consumes BOTH schema and migration registries) |
327
+ | `neko schema migrate verify` | Classify every authored migration as `bound` / `cosmetic_drift` / `drift` / `missing_endpoint` against the current schema registry | `verifyMigrationsHandler({ schemaRegistry, migrationRegistry })` (consumes BOTH registries — provenance hashes are compared to current endpoint-schema hashes) |
328
+ | `neko schema migrate stub` | Generate a skeleton migration file (header + typed declaration + `transform(input) { throw "Not yet implemented"; }`) | `stubMigrationHandler` + `suggestedMigrationPathFor` |
329
+
330
+ The schema package never walks the migration directory, never imports your authored migration modules, and never calls `.transform(...)` — those responsibilities all sit in `@nekostack/cli`. Master plan Decision #1 (schema is pure) stays in force; the migration handlers' purity is gated by [`../tests/migrations/handler-purity.test.ts`](../tests/migrations/handler-purity.test.ts).
331
+
332
+ The contract details — provenance header field-by-field, planner severity dispatch table, verifier verdict matrix, schema/CLI ownership, non-goals — live in [`MIGRATIONS.md`](./MIGRATIONS.md).
333
+
334
+ ## Handling unsupported IR
335
+
336
+ A `SchemaNode` whose kind isn't generator-ready throws `UnsupportedNodeKindError` with a stable shape. v0.3 extends the set with two new `kind` values: `"runtimeRefinement"` (from any generator) and `"regexFlags"` (JSON Schema only — regex with non-empty flags).
337
+
338
+ ```ts
339
+ import { UnsupportedNodeKindError } from "@nekostack/schema";
340
+
341
+ try {
342
+ generateZod(someExoticNode);
343
+ } catch (e) {
344
+ if (e instanceof UnsupportedNodeKindError) {
345
+ // e.code === "UNSUPPORTED_NODE_KIND"
346
+ // e.kind === "date" | "union" | "recursiveRef" | "transform" | "runtimeRefinement" | "regexFlags"
347
+ // e.generator === "typescript" | "zod" | "jsonSchema" | "openApi"
348
+ }
349
+ }
350
+ ```
351
+
352
+ Runtime-only refinements (custom predicates added via a future `.refine(fn)` builder) also throw, intentionally. Generators that can't honor the validation must not silently emit code that omits it.
353
+
354
+ For the JSON Schema generator specifically, regex with non-empty flags also throws (`kind: "regexFlags"`) — JSON Schema `pattern` has no flag support, and emitting source-only would silently drop case-insensitivity / unicode / etc.
355
+
356
+ ## What's still deferred (cross-reference to scope)
357
+
358
+ | Want | Wait for | Why |
359
+ |---|---|---|
360
+ | Full OpenAPI document (paths/operations/responses/security/etc.) | `@nekostack/api` package | Schema package generates component schemas only |
361
+ | OpenAPI 3.0 target (`nullable: true` form) | future generator option | v0.4 ships 3.1 only |
362
+ | Deep / recursive composition (nested-field merge) | future | v0.5 ships shallow operators only |
363
+ | Composition history (`metadata.derivedFrom`) | future | Could aid v0.7 diffing; not needed for v0.5 |
364
+ | `neko schema generate / check / diff` CLI | v0.7 | Registry-lite phase |
365
+ | Automatic CLI-populated `sourceHash` in committed artifacts | v0.7 CLI | Direct generator calls can already pass `sourceHash` via `ProvenanceOptions.sourceHash` — see the "Optional `sourceHash` provenance (v0.7+)" section in [`EXAMPLES.md`](./EXAMPLES.md). The CLI will compute it from source files automatically. |
366
+ | `$defs` extraction / cross-package `$ref` | v0.7 (registry-lite) | No IR construct in v0.3 needs it |
367
+ | Output-shape JSON Schema (default-applied, all fields required) | deferred | JSON Schema can't represent the input/output split as a single document |
368
+ | Schema-data migrations: planning + verification + stub generation | v0.8 ([PR #28](https://github.com/cmclicker/NekoStack/pull/28), in progress) | See the v0.8 preview section above and [`MIGRATIONS.md`](./MIGRATIONS.md). |
369
+ | **Executing** a schema-data migration's `transform(input)` | never in `@nekostack/schema` | Hard-locked non-goal of the v0.8 contract; the package owns planning + verification + stub generation only. If you need a runner, it lives in your own code or a downstream package. |
370
+ | Database / DDL migrations | `@nekostack/migrate` | Always — `@nekostack/schema` migrations are schema-data only. |
371
+ | Reverse / rollback migrations | not supported | v0.8 is forward-only (Decision #2). |
372
+ | Date types (`isoDateTime` etc.) | future | IR exists; builders deferred |
373
+
374
+ ## Worked examples
375
+
376
+ [`EXAMPLES.md`](./EXAMPLES.md) walks through the three example schemas in [`../examples/`](../examples/) and links each generated artifact. Read that next.
@@ -0,0 +1,77 @@
1
+ # Zod Modifier Ordering Contract
2
+
3
+ > The fixed order in which `generateZod` applies modifiers to a Zod chain. This file is the contract; the implementation lives in [`../src/generators/zod.ts`](../src/generators/zod.ts).
4
+
5
+ ## Why ordering matters
6
+
7
+ Zod modifier order changes input and output typing AND runtime acceptance behavior. `z.string().optional().default("x")` and `z.string().default("x").optional()` are NOT interchangeable. The v0.1 absence-semantics table is a contract; preserving it through generated Zod requires fixed ordering.
8
+
9
+ ## The order
10
+
11
+ For any IR `SchemaNode`, the emitted chain is constructed in this exact sequence:
12
+
13
+ 1. **Base schema** — `z.string()`, `z.number()`, `z.boolean()`, `z.literal(...)`, `z.enum(...)` / `z.union(...)`, `z.array(...)`, `z.object({...})`.
14
+ 2. **Portable refinements** — each `kind: "portable"` entry of `node.refinements`, applied in IR insertion order. Examples: `.min(3)`, `.max(50)`, `.email()`, `.regex(...)`, `.int()`, `.gt()`, `.multipleOf()`. Runtime refinements are **not** skipped silently — they throw (see ["Runtime refinements"](#runtime-refinements) below).
15
+ 3. **Description** — `.describe(text)` if `node.metadata.description` is set.
16
+ 4. **Nullability** — `.nullable()` if `modifiers.nullable && !modifiers.optional`.
17
+ 5. **Optionality** — `.optional()` if `modifiers.optional && !modifiers.nullable`.
18
+ 6. **Nullish** — `.nullish()` if BOTH `modifiers.optional && modifiers.nullable`.
19
+ 7. **Default LAST** — `.default(value)` if `modifiers.default` is set.
20
+
21
+ Steps 4 / 5 / 6 are mutually exclusive. Step 7 always comes last.
22
+
23
+ ## Runtime refinements
24
+
25
+ Runtime refinements are **unsupported** in v0.2 generator output. They represent custom predicates only the runtime can evaluate; emitting Zod that omits them would silently accept inputs the IR intends to reject — a direct Invariant 7 violation.
26
+
27
+ If a node contains any `kind: "runtime"` refinement, `generateZod` throws `UnsupportedNodeKindError` with:
28
+
29
+ - `code: "UNSUPPORTED_NODE_KIND"`
30
+ - `kind: "runtimeRefinement"`
31
+ - `generator: "zod"`
32
+
33
+ They are not emitted, not approximated, not skipped. Tests assert on the field shape (not message text). The same rule applies to `generateTypeScript`: a node carrying a runtime refinement represents validation intent the TS-only generator cannot guarantee, so it also throws.
34
+
35
+ When runtime refinements become representable in some generator (likely never for JSON Schema/OpenAPI; possibly via an opt-in shape for Zod in a later phase), this section gets updated and the implementations relax in lockstep — never one without the other.
36
+
37
+ ## Object unknown-keys
38
+
39
+ Applied to the base `z.object({...})` expression (effectively part of step 1), always explicit even though Zod's runtime default is `.strip()`:
40
+
41
+ | IR `unknownKeys` | Emitted |
42
+ |---|---|
43
+ | `"strict"` | `.strict()` |
44
+ | `"stripUnknown"` | `.strip()` |
45
+ | `"passthrough"` | `.passthrough()` |
46
+
47
+ ## Worked examples
48
+
49
+ | v0.1 builder call | Emitted Zod chain |
50
+ |---|---|
51
+ | `s.string()` | `z.string()` |
52
+ | `s.string().min(3)` | `z.string().min(3)` |
53
+ | `s.string().optional()` | `z.string().optional()` |
54
+ | `s.string().nullable()` | `z.string().nullable()` |
55
+ | `s.string().nullish()` | `z.string().nullish()` |
56
+ | `s.string().default("x")` | `z.string().optional().default("x")` |
57
+ | `s.string().min(3).email().optional()` | `z.string().min(3).email().optional()` |
58
+ | `s.string().nullable().default("x")` | `z.string().nullish().default("x")` (see "Collapse rule" below) |
59
+ | `s.string().describe("d").optional()` | `z.string().describe("d").optional()` |
60
+ | `s.object({id: s.string()}).passthrough()` | `z.object({ id: z.string() }).passthrough()` |
61
+
62
+ ## Collapse rule
63
+
64
+ v0.1's `.default()` sets `modifiers.optional = true` (because a defaulted field accepts missing input). When the user also calls `.nullable()`, the resulting IR has BOTH `optional` and `nullable` set, which step 6 collapses to `.nullish()`. So:
65
+
66
+ - `s.string().nullable().default("x")` → `z.string().nullish().default("x")`
67
+ - `s.string().nullish().default("x")` → `z.string().nullish().default("x")`
68
+
69
+ Both produce **identical Zod runtime behavior** (accept null, accept undefined → default applies). The collapse is correctness-preserving, not a loss.
70
+
71
+ ## Required test matrix
72
+
73
+ [`../tests/generators/zod-modifier-composition.test.ts`](../tests/generators/zod-modifier-composition.test.ts) asserts on the emitted chain string for each row. [`../tests/generators/zod-execution.test.ts`](../tests/generators/zod-execution.test.ts) loads the generated code into a real Zod runtime and verifies behavior matches the v0.1 absence-semantics table for the same eight rows.
74
+
75
+ ## Why a contract doc and not just code
76
+
77
+ Future generators (JSON Schema v0.3, OpenAPI v0.4) face the same question: in what order do modifiers compose? The constraints differ (no runtime in JSON Schema — `default` is metadata, not behavior), but the contract approach must repeat: pick an order, document it, test it. This doc is the template.
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@nekostack/schema",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/src/index.js",
8
+ "types": "./dist/src/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/src/index.d.ts",
12
+ "default": "./dist/src/index.js"
13
+ },
14
+ "./cli": {
15
+ "types": "./dist/src/cli-integration.d.ts",
16
+ "default": "./dist/src/cli-integration.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist/src",
21
+ "docs/*.md",
22
+ "CHANGELOG.md",
23
+ "LICENSE"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsc -b",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "typecheck": "tsc --noEmit",
30
+ "lint": "eslint ."
31
+ },
32
+ "peerDependencies": {
33
+ "zod": "^3.22.0"
34
+ },
35
+ "devDependencies": {
36
+ "@redocly/openapi-core": "^1.34.0",
37
+ "@types/node": "^22.10.0",
38
+ "ajv": "^8.12.0",
39
+ "ajv-formats": "^3.0.1",
40
+ "fast-check": "^3.19.0",
41
+ "typescript": "^5.7.2",
42
+ "vitest": "^2.1.8",
43
+ "zod": "^3.22.0"
44
+ }
45
+ }