@rayspec/spec 1.5.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 (62) hide show
  1. package/LICENSE +105 -0
  2. package/dist/brace-params.d.ts +32 -0
  3. package/dist/brace-params.d.ts.map +1 -0
  4. package/dist/brace-params.js +89 -0
  5. package/dist/brace-params.js.map +1 -0
  6. package/dist/detect.d.ts +44 -0
  7. package/dist/detect.d.ts.map +1 -0
  8. package/dist/detect.js +72 -0
  9. package/dist/detect.js.map +1 -0
  10. package/dist/errors.d.ts +204 -0
  11. package/dist/errors.d.ts.map +1 -0
  12. package/dist/errors.js +165 -0
  13. package/dist/errors.js.map +1 -0
  14. package/dist/export.d.ts +54 -0
  15. package/dist/export.d.ts.map +1 -0
  16. package/dist/export.js +121 -0
  17. package/dist/export.js.map +1 -0
  18. package/dist/grammar.d.ts +569 -0
  19. package/dist/grammar.d.ts.map +1 -0
  20. package/dist/grammar.js +503 -0
  21. package/dist/grammar.js.map +1 -0
  22. package/dist/index.d.ts +23 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +26 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/lint.d.ts +79 -0
  27. package/dist/lint.d.ts.map +1 -0
  28. package/dist/lint.js +823 -0
  29. package/dist/lint.js.map +1 -0
  30. package/dist/parse.d.ts +8 -0
  31. package/dist/parse.d.ts.map +1 -0
  32. package/dist/parse.js +118 -0
  33. package/dist/parse.js.map +1 -0
  34. package/dist/product-events.d.ts +73 -0
  35. package/dist/product-events.d.ts.map +1 -0
  36. package/dist/product-events.js +42 -0
  37. package/dist/product-events.js.map +1 -0
  38. package/dist/product-grammar.d.ts +948 -0
  39. package/dist/product-grammar.d.ts.map +1 -0
  40. package/dist/product-grammar.js +581 -0
  41. package/dist/product-grammar.js.map +1 -0
  42. package/dist/product-lint.d.ts +74 -0
  43. package/dist/product-lint.d.ts.map +1 -0
  44. package/dist/product-lint.js +821 -0
  45. package/dist/product-lint.js.map +1 -0
  46. package/dist/product-parse.d.ts +8 -0
  47. package/dist/product-parse.d.ts.map +1 -0
  48. package/dist/product-parse.js +119 -0
  49. package/dist/product-parse.js.map +1 -0
  50. package/dist/product-scope.d.ts +53 -0
  51. package/dist/product-scope.d.ts.map +1 -0
  52. package/dist/product-scope.js +55 -0
  53. package/dist/product-scope.js.map +1 -0
  54. package/dist/product-views-lint.d.ts +14 -0
  55. package/dist/product-views-lint.d.ts.map +1 -0
  56. package/dist/product-views-lint.js +669 -0
  57. package/dist/product-views-lint.js.map +1 -0
  58. package/dist/product-views.d.ts +426 -0
  59. package/dist/product-views.d.ts.map +1 -0
  60. package/dist/product-views.js +317 -0
  61. package/dist/product-views.js.map +1 -0
  62. package/package.json +34 -0
package/dist/errors.js ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * SpecError — the closed-code error envelope for the RaySpec config grammar.
3
+ *
4
+ * Mirrors the auth-core `ErrorCode`/`ErrorEnvelope` pattern (packages/auth-core/src/errors.ts):
5
+ * a CLOSED Zod enum of codes + a flat `{ code, message, path? }` envelope. Closed-by-construction
6
+ * so a fresh session reading a parse failure sees a finite, documented vocabulary — never a free
7
+ * string. `parseSpec`/`lintSpec` aggregate the FULL list of violations (not the first), so an
8
+ * author sees every problem in one pass.
9
+ *
10
+ * `path` is a JSON path into the spec (dot/bracket notation, e.g. `agents[0].backend`) so a
11
+ * violation points at the exact offending node. It is absent for whole-document failures
12
+ * (`yaml_parse_error`, `unsupported_version`) where no in-document path applies.
13
+ */
14
+ import { z } from 'zod';
15
+ /**
16
+ * The CLOSED set of spec-error codes. Every parse/lint failure carries exactly one of these.
17
+ *
18
+ * - `yaml_parse_error` — the raw text is not valid YAML (the `yaml` lib threw).
19
+ * - `unsupported_version` — `version` is missing or not the supported literal ('1.0').
20
+ * - `schema_violation` — a Zod shape failure that is not a pure unknown-key rejection
21
+ * (wrong type, missing required field, bad enum value, …).
22
+ * - `unknown_field` — a `.strict()` unknown-key rejection (fail-closed: any extra key).
23
+ * - `dangling_ref` — a cross-reference points at an id/name that is not declared.
24
+ * - `duplicate_name` — two entries in one section share an id/name.
25
+ * - `capability_violation` — an agent demands a capability its chosen backend lacks
26
+ * (via core `validateSpec`; e.g. native structured output on pi).
27
+ * - `invalid_embedded_schema`— an embedded JSON-Schema (tool `parameters`/`outputSchema`,
28
+ * agent `outputSchema.schema`) failed to compile through Ajv2020.
29
+ * - `reserved_column_name` — a store declares a business column whose name collides with a
30
+ * tenancy/GDPR column the table generator injects (`id`,
31
+ * `tenant_id`, `created_at`, `deleted_at`, `retention_days`,
32
+ * `region`) — fail-closed against a shadow/tenancy collision.
33
+ * - `reserved_query_keyword` — a store declares a business column whose name is one of the
34
+ * list-query CONTROL keywords (`order`, `after`, `limit`). Those keys
35
+ * steer the declarative `list` route's sorting + keyset pagination, so a
36
+ * column of that name would be silently un-equality-filterable AND would
37
+ * emit a DUPLICATE OpenAPI query parameter (control param + per-column
38
+ * filter param, same name+location) → an invalid OpenAPI 3.1 document.
39
+ * Rename the business column.
40
+ * - `frontend_route_collision` — a declared static frontend mount's `route` collides with another
41
+ * mount, with a declared `api[].path`, or with a reserved system prefix
42
+ * (`/v1`, `/health`, `/oidc`) — the static mount would either shadow or be
43
+ * shadowed by that route. Root `/` is exempt (it coexists with `/v1/*` via
44
+ * registration order). Rename the frontend route.
45
+ * - `frontend_dir_missing` — a declared frontend `dir` does not resolve to a readable directory of
46
+ * built assets (surfaced by `doctor`, which checks the filesystem).
47
+ * - `fk_cycle` — the declared stores form a CIRCULAR foreign-key reference (store A
48
+ * references B and B references A, directly or transitively). Such a set is
49
+ * UNORDERABLE: each store's FK ADD needs its parent table to already exist,
50
+ * so no CREATE order satisfies every FK. Rejected fail-closed at config time
51
+ * rather than surfacing as a cryptic `42P01 relation does not exist` at
52
+ * apply. Self-references are EXEMPT (a self-FK applies after its own CREATE).
53
+ *
54
+ * PRODUCT-YAML codes — used ONLY by the Product-YAML validation path (`parseProductSpec`,
55
+ * `product-lint.ts`). They share this closed envelope so a fresh session sees ONE error vocabulary
56
+ * across both document families; the RaySpec path never emits them.
57
+ * - `no_code_in_yaml` — a code/handler/SQL/shell key or an inline-code string value appears
58
+ * in a Product-YAML doc: implementation belongs in Tier A/B, not
59
+ * in product meaning. The message names what the offending key should be.
60
+ * - `provider_native_leak` — a provider-native wire blob (raw request/response payload) or a
61
+ * provider/model policy field / provider name leaked into the executable
62
+ * `workflows`/`agents` graph (which must stay provider-neutral so it
63
+ * compiles through the workflow bridge). Provider policy is only allowed
64
+ * in `capabilities[].provider_policy` / `deployment_overrides`.
65
+ * - `invalid_capability_status` — RESERVED (closed-code discipline). The earlier doc-level
66
+ * rejection of `status:'available'` was retired (the Tier B
67
+ * runtime is wired now); capability WIREDNESS is enforced at the deploy
68
+ * composition (fail-closed `unsupported_spec`), not by the parser.
69
+ * - `invalid_contract` — a `contracts[]` schema uses a key/type outside the closed, declarative
70
+ * JSON-Schema-like vocabulary (no functions/transforms/computed
71
+ * expressions/provider-native shapes).
72
+ * - `prompt_execution_claim` — a Product-YAML `workflows`/`agents` graph STRING claims prompt/LLM
73
+ * EXECUTION (`llm call`, `agent call`, `prompt execution`, `execute
74
+ * prompt`). Mirrors the workflow-bridge's `prompt_execution_claim` so a
75
+ * doc that validates here also compiles through the bridge (anti-drift;
76
+ * parity-tested). Prompt/agent execution is a Tier-B runtime concern.
77
+ * - `production_execution_claim` — a graph STRING claims production EXECUTION (`production_ready`,
78
+ * `production execution`, `prod runtime`). Mirrors the bridge's
79
+ * `production_execution_claim` (parity-tested): a product doc declares
80
+ * meaning, not that it EXECUTES in production.
81
+ * - `invalid_dependency_order` — a workflow step's `depends_on` references a step that is NOT declared
82
+ * before it (a forward/self reference). Declaration-order is required, which
83
+ * structurally forbids dependency CYCLES (a cycle needs a forward edge).
84
+ * - `invalid_view` — a view declaration violates the view semantics (product-views-lint):
85
+ * a source/contract conflation (a source ref that names a
86
+ * contract instead of a store/artifact/capability contract), a read/shape
87
+ * context violation (page fields outside `list` mode, group outside `collect`,
88
+ * …), incomplete param coverage, a shape that does not conform to its declared
89
+ * response contract, a reserved (`__proto__`-class) name, or a pagination law
90
+ * violation. Every mis-declared view construct is rejected — never skipped.
91
+ * - `invalid_store` — a declared product store / store-step declaration violates the store
92
+ * semantics: a store name colliding with a
93
+ * derived collection store, a column name on the graph key denylist (it could
94
+ * never be referenced from a workflow step), a key naming an undeclared or
95
+ * nullable column, a store step targeting an undeclared store, a filter/values
96
+ * column outside the store's declared columns, a write omitting the conflict-
97
+ * key column, or store vocabulary on a non-store step type. Fail-closed.
98
+ */
99
+ export const SpecErrorCode = z.enum([
100
+ 'yaml_parse_error',
101
+ 'unsupported_version',
102
+ 'schema_violation',
103
+ 'unknown_field',
104
+ 'dangling_ref',
105
+ 'duplicate_name',
106
+ 'capability_violation',
107
+ 'invalid_embedded_schema',
108
+ 'reserved_column_name',
109
+ 'reserved_query_keyword',
110
+ 'frontend_route_collision',
111
+ 'frontend_dir_missing',
112
+ 'fk_cycle',
113
+ 'no_code_in_yaml',
114
+ 'provider_native_leak',
115
+ 'invalid_capability_status',
116
+ 'invalid_contract',
117
+ 'prompt_execution_claim',
118
+ 'production_execution_claim',
119
+ 'invalid_dependency_order',
120
+ 'invalid_view',
121
+ 'invalid_store',
122
+ ]);
123
+ /** A single fail-closed spec violation (closed code + message + optional JSON path). */
124
+ export const SpecError = z.object({
125
+ code: SpecErrorCode,
126
+ message: z.string(),
127
+ /** JSON path into the spec document (e.g. `agents[0].backend`); absent for whole-doc failures. */
128
+ path: z.string().optional(),
129
+ });
130
+ /** Construct a SpecError (path omitted when undefined so the envelope stays minimal). */
131
+ export function specError(code, message, path) {
132
+ return path !== undefined ? { code, message, path } : { code, message };
133
+ }
134
+ /**
135
+ * The CLOSED set of NON-FATAL spec-warning codes. A warning flags a documented, deliberately-permitted
136
+ * interaction the author should be AWARE of — it does NOT fail `doctor`/`plan` (unlike a `SpecError`).
137
+ * Kept a distinct closed vocabulary from `SpecErrorCode` so a fresh session never confuses "advisory"
138
+ * with "fail-closed".
139
+ *
140
+ * - `softdelete_fk_restrict` — a `softDelete` store is the TARGET of a `restrict` business-key
141
+ * (`referencesColumn`) foreign key. Soft-deleting such a parent is an
142
+ * `UPDATE(deleted_at)` that does NOT fire the database ON DELETE restrict,
143
+ * so children keep pointing at the tombstoned row — the restrict guarantee
144
+ * only binds on a HARD delete. This is a permitted, documented interaction.
145
+ * - `fk_forward_reference` — a store declares a foreign key onto another store declared LATER in the
146
+ * `stores` array. The product-SQL generator topo-sorts stores so the parent
147
+ * table is created before the child's FK is added, so a forward reference
148
+ * still applies cleanly; this advisory notes only that the declared order
149
+ * relies on that reordering (declaring the parent first makes it explicit).
150
+ * Acyclic by construction — a true cycle is the fail-closed `fk_cycle`
151
+ * error, never a warning.
152
+ */
153
+ export const SpecWarningCode = z.enum(['softdelete_fk_restrict', 'fk_forward_reference']);
154
+ /** A single NON-FATAL spec warning (closed code + message + optional JSON path). Never fails a parse. */
155
+ export const SpecWarning = z.object({
156
+ code: SpecWarningCode,
157
+ message: z.string(),
158
+ /** JSON path into the spec document (e.g. `stores[0].softDelete`); absent for whole-doc warnings. */
159
+ path: z.string().optional(),
160
+ });
161
+ /** Construct a SpecWarning (path omitted when undefined so the envelope stays minimal). */
162
+ export function specWarning(code, message, path) {
163
+ return path !== undefined ? { code, message, path } : { code, message };
164
+ }
165
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmFG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,IAAI,CAAC;IAClC,kBAAkB;IAClB,qBAAqB;IACrB,kBAAkB;IAClB,eAAe;IACf,cAAc;IACd,gBAAgB;IAChB,sBAAsB;IACtB,yBAAyB;IACzB,sBAAsB;IACtB,wBAAwB;IACxB,0BAA0B;IAC1B,sBAAsB;IACtB,UAAU;IACV,iBAAiB;IACjB,sBAAsB;IACtB,2BAA2B;IAC3B,kBAAkB;IAClB,wBAAwB;IACxB,4BAA4B;IAC5B,0BAA0B;IAC1B,cAAc;IACd,eAAe;CAChB,CAAC,CAAC;AAGH,wFAAwF;AACxF,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC;IAChC,IAAI,EAAE,aAAa;IACnB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,kGAAkG;IAClG,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC5B,CAAC,CAAC;AAGH,yFAAyF;AACzF,MAAM,UAAU,SAAS,CAAC,IAAmB,EAAE,OAAe,EAAE,IAAa;IAC3E,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC1E,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,wBAAwB,EAAE,sBAAsB,CAAC,CAAC,CAAC;AAG1F,yGAAyG;AACzG,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,IAAI,EAAE,eAAe;IACrB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,qGAAqG;IACrG,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC5B,CAAC,CAAC;AAGH,2FAA2F;AAC3F,MAAM,UAAU,WAAW,CAAC,IAAqB,EAAE,OAAe,EAAE,IAAa;IAC/E,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC1E,CAAC"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Export the `RaySpec` grammar as a JSON-Schema (draft-2020-12) object. Every STRICT grammar
3
+ * object level carries `additionalProperties:false` (the fail-closed shape an Ajv2020 instance
4
+ * enforces byte-for-byte); the three embedded JSON-Schema slots (tool `parameters`/`outputSchema`,
5
+ * agent `outputSchema.schema`) are intentionally OPEN records.
6
+ *
7
+ * `io: 'input'` is LOAD-BEARING: `z.toJSONSchema` defaults to `io: 'output'`, which marks every
8
+ * `.default()`ed field REQUIRED — making the exported artifact REJECT a default-omitting minimal
9
+ * spec (`{version, metadata}`) that `parseSpec` ACCEPTS (the parser fills the defaults). `io:'input'`
10
+ * exports the PRE-default INPUT shape, so the artifact validates exactly what an author may WRITE —
11
+ * matching the parser's accept set (the round-trip contract test drives a default-omitting object
12
+ * through the artifact to lock this).
13
+ *
14
+ * `unrepresentable` defaults to `'throw'`; the grammar uses only representable types
15
+ * (string/literal/enum/array/record/object/boolean/number/discriminatedUnion), all verified to
16
+ * export, so this never throws for the current grammar.
17
+ */
18
+ export declare function exportJsonSchema(): Record<string, unknown>;
19
+ /**
20
+ * Export the `ProductSpec` (Product-YAML) grammar as a JSON-Schema (draft-2020-12) — the SAME
21
+ * `io:'input'` discipline as `exportJsonSchema` (so a default-omitting minimal product doc validates
22
+ * against the artifact exactly as `parseProductSpec` accepts it). The Product-YAML `contracts` slot is
23
+ * an intentionally OPEN record (its closed vocabulary is enforced by product-lint, not this artifact),
24
+ * mirroring how the RaySpec artifact leaves a tool's `parameters` open. This artifact is kept
25
+ * fresh by the `gate:spec-schema` drift gate alongside `spec.schema.json` (`packages/spec/product.schema.json`).
26
+ */
27
+ export declare function exportProductJsonSchema(): Record<string, unknown>;
28
+ /**
29
+ * Export the ONE canonical schema for the `version:'1.0'` document. A
30
+ * `version:'1.0'` doc is one of two PROFILES, discriminated by the presence of the `product:` section:
31
+ * the BACKEND profile (`RaySpec` — the low-level surface AND the internal engine target) or the
32
+ * PRODUCT profile (`ProductSpec` — the high-level product-meaning surface). This is a hand-composed
33
+ * `oneOf` over the two existing profile schemas (a `oneOf` with a `product` required/forbidden
34
+ * discriminant is clearer + diff-reviewable than `z.toJSONSchema` over a top-level union of two strict
35
+ * objects). It is UNAMBIGUOUS by construction: the backend arm keeps `additionalProperties:false`, so a
36
+ * `product`-carrying doc CANNOT match it, and the product arm REQUIRES `product` — so exactly one arm
37
+ * matches. Both arms stay draft-2020-12 + `io:'input'` (defaults-applied-on-parse) and fail-closed
38
+ * (every strict level keeps `additionalProperties:false`). The dialect `$schema` declaration lives ONLY
39
+ * at the unified root; the inner profile `$schema` keys are stripped so the artifact is one well-formed
40
+ * 2020-12 document. Kept fresh by `gate:spec-schema` alongside the two profile views.
41
+ *
42
+ * `$defs` HOISTING (the BLOCKER fix): `z.toJSONSchema` factors a reused sub-schema into a ROOT-scoped
43
+ * `$defs.__schemaN` with document-root-relative `"$ref": "#/$defs/__schemaN"` pointers (the product
44
+ * profile emits one such def today; the backend profile emits none). A JSON-Pointer `$ref` is ALWAYS
45
+ * resolved from the DOCUMENT ROOT (`#`), NOT from the `oneOf[n]` object it sits under — so leaving a
46
+ * profile's `$defs` NESTED under an arm leaves its refs dangling (Ajv2020 raises `can't resolve
47
+ * reference #/$defs/__schema0 from id #`). This exporter therefore HOISTS every arm's `$defs` up to the
48
+ * unified ROOT (deleting it from the arm), so the inner `#/$defs/...` refs resolve against the
49
+ * now-present root `$defs`. A cross-arm `$defs` key collision throws (fail loud, never silently clobber).
50
+ * The `packages/spec/src/export.test.ts` Ajv2020 compile test is the forcing-function that keeps this
51
+ * honest (a regression to nested `$defs` turns it red with a MissingRefError).
52
+ */
53
+ export declare function exportUnifiedJsonSchema(): Record<string, unknown>;
54
+ //# sourceMappingURL=export.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../src/export.ts"],"names":[],"mappings":"AAqBA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAK1D;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAKjE;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAwCjE"}
package/dist/export.js ADDED
@@ -0,0 +1,121 @@
1
+ /**
2
+ * JSON-Schema exporter for the RaySpec config grammar.
3
+ *
4
+ * The design calls for "Zod + JSON-Schema export". The pinned `zod@4.4.3` ships a native
5
+ * `z.toJSONSchema` (verified doc-first) so this needs NO new dependency. We export at the
6
+ * draft-2020-12 dialect — the SAME dialect `packages/platform/src/dispatch.ts` compiles tool
7
+ * schemas with (`ajv/dist/2020`) — so the runtime exporter and the runtime validator speak one
8
+ * JSON-Schema language.
9
+ *
10
+ * Scope: build the RUNTIME exporter + the round-trip contract test.
11
+ *
12
+ * The MINIMAL schema-emit + drift-gate: this exporter is now ALSO the source for a CHECKED-IN
13
+ * `packages/spec/spec.schema.json` artifact, kept fresh by the structural CI gate `gate:spec-schema`
14
+ * (`scripts/check-spec-schema.mjs` — re-derives the schema, serializes it through one shared helper, and
15
+ * byte-compares to the committed file; regenerate with `--write`). The gate only checks FRESHNESS — this
16
+ * function stays the single source of truth. Generated handler TYPES remain deferred.
17
+ */
18
+ import { z } from 'zod';
19
+ import { RaySpec } from './grammar.js';
20
+ import { ProductSpec } from './product-grammar.js';
21
+ /**
22
+ * Export the `RaySpec` grammar as a JSON-Schema (draft-2020-12) object. Every STRICT grammar
23
+ * object level carries `additionalProperties:false` (the fail-closed shape an Ajv2020 instance
24
+ * enforces byte-for-byte); the three embedded JSON-Schema slots (tool `parameters`/`outputSchema`,
25
+ * agent `outputSchema.schema`) are intentionally OPEN records.
26
+ *
27
+ * `io: 'input'` is LOAD-BEARING: `z.toJSONSchema` defaults to `io: 'output'`, which marks every
28
+ * `.default()`ed field REQUIRED — making the exported artifact REJECT a default-omitting minimal
29
+ * spec (`{version, metadata}`) that `parseSpec` ACCEPTS (the parser fills the defaults). `io:'input'`
30
+ * exports the PRE-default INPUT shape, so the artifact validates exactly what an author may WRITE —
31
+ * matching the parser's accept set (the round-trip contract test drives a default-omitting object
32
+ * through the artifact to lock this).
33
+ *
34
+ * `unrepresentable` defaults to `'throw'`; the grammar uses only representable types
35
+ * (string/literal/enum/array/record/object/boolean/number/discriminatedUnion), all verified to
36
+ * export, so this never throws for the current grammar.
37
+ */
38
+ export function exportJsonSchema() {
39
+ return z.toJSONSchema(RaySpec, {
40
+ target: 'draft-2020-12',
41
+ io: 'input',
42
+ });
43
+ }
44
+ /**
45
+ * Export the `ProductSpec` (Product-YAML) grammar as a JSON-Schema (draft-2020-12) — the SAME
46
+ * `io:'input'` discipline as `exportJsonSchema` (so a default-omitting minimal product doc validates
47
+ * against the artifact exactly as `parseProductSpec` accepts it). The Product-YAML `contracts` slot is
48
+ * an intentionally OPEN record (its closed vocabulary is enforced by product-lint, not this artifact),
49
+ * mirroring how the RaySpec artifact leaves a tool's `parameters` open. This artifact is kept
50
+ * fresh by the `gate:spec-schema` drift gate alongside `spec.schema.json` (`packages/spec/product.schema.json`).
51
+ */
52
+ export function exportProductJsonSchema() {
53
+ return z.toJSONSchema(ProductSpec, {
54
+ target: 'draft-2020-12',
55
+ io: 'input',
56
+ });
57
+ }
58
+ /**
59
+ * Export the ONE canonical schema for the `version:'1.0'` document. A
60
+ * `version:'1.0'` doc is one of two PROFILES, discriminated by the presence of the `product:` section:
61
+ * the BACKEND profile (`RaySpec` — the low-level surface AND the internal engine target) or the
62
+ * PRODUCT profile (`ProductSpec` — the high-level product-meaning surface). This is a hand-composed
63
+ * `oneOf` over the two existing profile schemas (a `oneOf` with a `product` required/forbidden
64
+ * discriminant is clearer + diff-reviewable than `z.toJSONSchema` over a top-level union of two strict
65
+ * objects). It is UNAMBIGUOUS by construction: the backend arm keeps `additionalProperties:false`, so a
66
+ * `product`-carrying doc CANNOT match it, and the product arm REQUIRES `product` — so exactly one arm
67
+ * matches. Both arms stay draft-2020-12 + `io:'input'` (defaults-applied-on-parse) and fail-closed
68
+ * (every strict level keeps `additionalProperties:false`). The dialect `$schema` declaration lives ONLY
69
+ * at the unified root; the inner profile `$schema` keys are stripped so the artifact is one well-formed
70
+ * 2020-12 document. Kept fresh by `gate:spec-schema` alongside the two profile views.
71
+ *
72
+ * `$defs` HOISTING (the BLOCKER fix): `z.toJSONSchema` factors a reused sub-schema into a ROOT-scoped
73
+ * `$defs.__schemaN` with document-root-relative `"$ref": "#/$defs/__schemaN"` pointers (the product
74
+ * profile emits one such def today; the backend profile emits none). A JSON-Pointer `$ref` is ALWAYS
75
+ * resolved from the DOCUMENT ROOT (`#`), NOT from the `oneOf[n]` object it sits under — so leaving a
76
+ * profile's `$defs` NESTED under an arm leaves its refs dangling (Ajv2020 raises `can't resolve
77
+ * reference #/$defs/__schema0 from id #`). This exporter therefore HOISTS every arm's `$defs` up to the
78
+ * unified ROOT (deleting it from the arm), so the inner `#/$defs/...` refs resolve against the
79
+ * now-present root `$defs`. A cross-arm `$defs` key collision throws (fail loud, never silently clobber).
80
+ * The `packages/spec/src/export.test.ts` Ajv2020 compile test is the forcing-function that keeps this
81
+ * honest (a regression to nested `$defs` turns it red with a MissingRefError).
82
+ */
83
+ export function exportUnifiedJsonSchema() {
84
+ const strip$schema = (s) => {
85
+ const { $schema: _dialect, ...rest } = s;
86
+ return rest;
87
+ };
88
+ // The two profile arms, each stripped of its inner dialect `$schema` (the unified root carries it).
89
+ const arms = [strip$schema(exportJsonSchema()), strip$schema(exportProductJsonSchema())];
90
+ // Hoist each arm's ROOT-scoped `$defs` up to the unified root, deleting it from the arm (see the
91
+ // `$defs` HOISTING note above). A JSON-Pointer `$ref` resolves from the DOCUMENT ROOT, NOT the
92
+ // `oneOf[n]` object it sits under, so a profile's `#/$defs/...` refs would DANGLE if its `$defs`
93
+ // stayed nested. Merge under one root `$defs`; a cross-arm key collision throws (fail loud).
94
+ const rootDefs = {};
95
+ for (const arm of arms) {
96
+ const armDefs = arm.$defs;
97
+ if (!armDefs)
98
+ continue;
99
+ for (const [key, def] of Object.entries(armDefs)) {
100
+ if (key in rootDefs) {
101
+ throw new Error(`exportUnifiedJsonSchema: $defs key collision '${key}' across the backend/product profiles — ` +
102
+ 'namespace the arm defs before hoisting (both arms cannot share a root-scoped def name).');
103
+ }
104
+ rootDefs[key] = def;
105
+ }
106
+ delete arm.$defs;
107
+ }
108
+ const unified = {
109
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
110
+ title: 'RaySpec spec (version 1.0) — one language, two profiles (backend | product)',
111
+ description: "A version:'1.0' document is one of two profiles, discriminated by the presence of the " +
112
+ '`product:` section: the backend profile (low-level RaySpec, the internal engine target) ' +
113
+ 'or the product profile (high-level product meaning, lowered to the backend profile at deploy).',
114
+ oneOf: arms,
115
+ };
116
+ // Only emit `$defs` if any arm contributed one (keeps the artifact minimal when no arm factors a def).
117
+ if (Object.keys(rootDefs).length > 0)
118
+ unified.$defs = rootDefs;
119
+ return unified;
120
+ }
121
+ //# sourceMappingURL=export.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export.js","sourceRoot":"","sources":["../src/export.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO,CAAC,CAAC,YAAY,CAAC,OAAO,EAAE;QAC7B,MAAM,EAAE,eAAe;QACvB,EAAE,EAAE,OAAO;KACZ,CAA4B,CAAC;AAChC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB;IACrC,OAAO,CAAC,CAAC,YAAY,CAAC,WAAW,EAAE;QACjC,MAAM,EAAE,eAAe;QACvB,EAAE,EAAE,OAAO;KACZ,CAA4B,CAAC;AAChC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,uBAAuB;IACrC,MAAM,YAAY,GAAG,CAAC,CAA0B,EAA2B,EAAE;QAC3E,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC;QACzC,OAAO,IAAI,CAAC;IACd,CAAC,CAAC;IACF,oGAAoG;IACpG,MAAM,IAAI,GAAG,CAAC,YAAY,CAAC,gBAAgB,EAAE,CAAC,EAAE,YAAY,CAAC,uBAAuB,EAAE,CAAC,CAAC,CAAC;IAEzF,iGAAiG;IACjG,+FAA+F;IAC/F,iGAAiG;IACjG,6FAA6F;IAC7F,MAAM,QAAQ,GAA4B,EAAE,CAAC;IAC7C,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,OAAO,GAAI,GAA2C,CAAC,KAAK,CAAC;QACnE,IAAI,CAAC,OAAO;YAAE,SAAS;QACvB,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YACjD,IAAI,GAAG,IAAI,QAAQ,EAAE,CAAC;gBACpB,MAAM,IAAI,KAAK,CACb,iDAAiD,GAAG,0CAA0C;oBAC5F,yFAAyF,CAC5F,CAAC;YACJ,CAAC;YACD,QAAQ,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;QACtB,CAAC;QACD,OAAQ,GAA2B,CAAC,KAAK,CAAC;IAC5C,CAAC;IAED,MAAM,OAAO,GAA4B;QACvC,OAAO,EAAE,8CAA8C;QACvD,KAAK,EAAE,6EAA6E;QACpF,WAAW,EACT,wFAAwF;YACxF,0FAA0F;YAC1F,gGAAgG;QAClG,KAAK,EAAE,IAAI;KACZ,CAAC;IACF,uGAAuG;IACvG,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,CAAC,KAAK,GAAG,QAAQ,CAAC;IAC/D,OAAO,OAAO,CAAC;AACjB,CAAC"}