@relayflows/sdk 2.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 (231) hide show
  1. package/dist/authored-flow-error.d.ts +9 -0
  2. package/dist/authored-flow-error.d.ts.map +1 -0
  3. package/dist/authored-flow-error.js +13 -0
  4. package/dist/authored-flow-error.js.map +1 -0
  5. package/dist/authored-flow-executor.d.ts +28 -0
  6. package/dist/authored-flow-executor.d.ts.map +1 -0
  7. package/dist/authored-flow-executor.js +203 -0
  8. package/dist/authored-flow-executor.js.map +1 -0
  9. package/dist/authored-flow-lifecycle.d.ts +61 -0
  10. package/dist/authored-flow-lifecycle.d.ts.map +1 -0
  11. package/dist/authored-flow-lifecycle.js +284 -0
  12. package/dist/authored-flow-lifecycle.js.map +1 -0
  13. package/dist/authored-flow-loader.d.ts +7 -0
  14. package/dist/authored-flow-loader.d.ts.map +1 -0
  15. package/dist/authored-flow-loader.js +39 -0
  16. package/dist/authored-flow-loader.js.map +1 -0
  17. package/dist/authored-flow-operation.d.ts +43 -0
  18. package/dist/authored-flow-operation.d.ts.map +1 -0
  19. package/dist/authored-flow-operation.js +176 -0
  20. package/dist/authored-flow-operation.js.map +1 -0
  21. package/dist/authored-flow.d.ts +12 -0
  22. package/dist/authored-flow.d.ts.map +1 -0
  23. package/dist/authored-flow.js +13 -0
  24. package/dist/authored-flow.js.map +1 -0
  25. package/dist/authored-promise-graph.d.ts +67 -0
  26. package/dist/authored-promise-graph.d.ts.map +1 -0
  27. package/dist/authored-promise-graph.js +206 -0
  28. package/dist/authored-promise-graph.js.map +1 -0
  29. package/dist/backlog-picker.d.ts +55 -0
  30. package/dist/backlog-picker.d.ts.map +1 -0
  31. package/dist/backlog-picker.js +117 -0
  32. package/dist/backlog-picker.js.map +1 -0
  33. package/dist/canonical.d.ts +18 -0
  34. package/dist/canonical.d.ts.map +1 -0
  35. package/dist/canonical.js +55 -0
  36. package/dist/canonical.js.map +1 -0
  37. package/dist/cli/check.d.ts +30 -0
  38. package/dist/cli/check.d.ts.map +1 -0
  39. package/dist/cli/check.js +324 -0
  40. package/dist/cli/check.js.map +1 -0
  41. package/dist/cli/direct-run.d.ts +3 -0
  42. package/dist/cli/direct-run.d.ts.map +1 -0
  43. package/dist/cli/direct-run.js +69 -0
  44. package/dist/cli/direct-run.js.map +1 -0
  45. package/dist/cli/hn-monitor.d.ts +107 -0
  46. package/dist/cli/hn-monitor.d.ts.map +1 -0
  47. package/dist/cli/hn-monitor.js +220 -0
  48. package/dist/cli/hn-monitor.js.map +1 -0
  49. package/dist/cli/interruptible-sleep.d.ts +13 -0
  50. package/dist/cli/interruptible-sleep.d.ts.map +1 -0
  51. package/dist/cli/interruptible-sleep.js +31 -0
  52. package/dist/cli/interruptible-sleep.js.map +1 -0
  53. package/dist/cli/run.d.ts +54 -0
  54. package/dist/cli/run.d.ts.map +1 -0
  55. package/dist/cli/run.js +316 -0
  56. package/dist/cli/run.js.map +1 -0
  57. package/dist/cli/tick-runner.d.ts +151 -0
  58. package/dist/cli/tick-runner.d.ts.map +1 -0
  59. package/dist/cli/tick-runner.js +258 -0
  60. package/dist/cli/tick-runner.js.map +1 -0
  61. package/dist/cli-adapter.d.ts +29 -0
  62. package/dist/cli-adapter.d.ts.map +1 -0
  63. package/dist/cli-adapter.js +92 -0
  64. package/dist/cli-adapter.js.map +1 -0
  65. package/dist/cli-executable.d.ts +3 -0
  66. package/dist/cli-executable.d.ts.map +1 -0
  67. package/dist/cli-executable.js +10 -0
  68. package/dist/cli-executable.js.map +1 -0
  69. package/dist/cli.d.ts +9 -0
  70. package/dist/cli.d.ts.map +1 -0
  71. package/dist/cli.js +348 -0
  72. package/dist/cli.js.map +1 -0
  73. package/dist/compile.d.ts +42 -0
  74. package/dist/compile.d.ts.map +1 -0
  75. package/dist/compile.js +515 -0
  76. package/dist/compile.js.map +1 -0
  77. package/dist/demo-hn-monitor.d.ts +2 -0
  78. package/dist/demo-hn-monitor.d.ts.map +1 -0
  79. package/dist/demo-hn-monitor.js +118 -0
  80. package/dist/demo-hn-monitor.js.map +1 -0
  81. package/dist/dir-watcher-poller.d.ts +73 -0
  82. package/dist/dir-watcher-poller.d.ts.map +1 -0
  83. package/dist/dir-watcher-poller.js +80 -0
  84. package/dist/dir-watcher-poller.js.map +1 -0
  85. package/dist/direct-input.d.ts +10 -0
  86. package/dist/direct-input.d.ts.map +1 -0
  87. package/dist/direct-input.js +61 -0
  88. package/dist/direct-input.js.map +1 -0
  89. package/dist/failure-kinds.d.ts +25 -0
  90. package/dist/failure-kinds.d.ts.map +1 -0
  91. package/dist/failure-kinds.js +64 -0
  92. package/dist/failure-kinds.js.map +1 -0
  93. package/dist/gate-contract.d.ts +24 -0
  94. package/dist/gate-contract.d.ts.map +1 -0
  95. package/dist/gate-contract.js +50 -0
  96. package/dist/gate-contract.js.map +1 -0
  97. package/dist/hn-poller.d.ts +50 -0
  98. package/dist/hn-poller.d.ts.map +1 -0
  99. package/dist/hn-poller.js +79 -0
  100. package/dist/hn-poller.js.map +1 -0
  101. package/dist/index.d.ts +21 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +30 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/journal-client.d.ts +122 -0
  106. package/dist/journal-client.d.ts.map +1 -0
  107. package/dist/journal-client.js +277 -0
  108. package/dist/journal-client.js.map +1 -0
  109. package/dist/json-schema-bound.d.ts +7 -0
  110. package/dist/json-schema-bound.d.ts.map +1 -0
  111. package/dist/json-schema-bound.js +353 -0
  112. package/dist/json-schema-bound.js.map +1 -0
  113. package/dist/json-schema.d.ts +5 -0
  114. package/dist/json-schema.d.ts.map +1 -0
  115. package/dist/json-schema.js +72 -0
  116. package/dist/json-schema.js.map +1 -0
  117. package/dist/json-value.d.ts +6 -0
  118. package/dist/json-value.d.ts.map +1 -0
  119. package/dist/json-value.js +94 -0
  120. package/dist/json-value.js.map +1 -0
  121. package/dist/model-name.d.ts +7 -0
  122. package/dist/model-name.d.ts.map +1 -0
  123. package/dist/model-name.js +21 -0
  124. package/dist/model-name.js.map +1 -0
  125. package/dist/output-schema.d.ts +8 -0
  126. package/dist/output-schema.d.ts.map +1 -0
  127. package/dist/output-schema.js +28 -0
  128. package/dist/output-schema.js.map +1 -0
  129. package/dist/preflight.d.ts +80 -0
  130. package/dist/preflight.d.ts.map +1 -0
  131. package/dist/preflight.js +361 -0
  132. package/dist/preflight.js.map +1 -0
  133. package/dist/protocol.d.ts +359 -0
  134. package/dist/protocol.d.ts.map +1 -0
  135. package/dist/protocol.js +15 -0
  136. package/dist/protocol.js.map +1 -0
  137. package/dist/spec.d.ts +294 -0
  138. package/dist/spec.d.ts.map +1 -0
  139. package/dist/spec.js +13 -0
  140. package/dist/spec.js.map +1 -0
  141. package/dist/step-dependencies.d.ts +2 -0
  142. package/dist/step-dependencies.d.ts.map +1 -0
  143. package/dist/step-dependencies.js +88 -0
  144. package/dist/step-dependencies.js.map +1 -0
  145. package/dist/step-fields.d.ts +17 -0
  146. package/dist/step-fields.d.ts.map +1 -0
  147. package/dist/step-fields.js +32 -0
  148. package/dist/step-fields.js.map +1 -0
  149. package/dist/tick-source.d.ts +205 -0
  150. package/dist/tick-source.d.ts.map +1 -0
  151. package/dist/tick-source.js +234 -0
  152. package/dist/tick-source.js.map +1 -0
  153. package/dist/unknown-keys.d.ts +7 -0
  154. package/dist/unknown-keys.d.ts.map +1 -0
  155. package/dist/unknown-keys.js +47 -0
  156. package/dist/unknown-keys.js.map +1 -0
  157. package/dist/validate.d.ts +7 -0
  158. package/dist/validate.d.ts.map +1 -0
  159. package/dist/validate.js +458 -0
  160. package/dist/validate.js.map +1 -0
  161. package/dist/work-package-consumer.d.ts +33 -0
  162. package/dist/work-package-consumer.d.ts.map +1 -0
  163. package/dist/work-package-consumer.js +44 -0
  164. package/dist/work-package-consumer.js.map +1 -0
  165. package/dist/work-package-validator.d.ts +15 -0
  166. package/dist/work-package-validator.d.ts.map +1 -0
  167. package/dist/work-package-validator.js +72 -0
  168. package/dist/work-package-validator.js.map +1 -0
  169. package/dist/worker-cli.d.ts +16 -0
  170. package/dist/worker-cli.d.ts.map +1 -0
  171. package/dist/worker-cli.js +77 -0
  172. package/dist/worker-cli.js.map +1 -0
  173. package/dist/worker.d.ts +57 -0
  174. package/dist/worker.d.ts.map +1 -0
  175. package/dist/worker.js +129 -0
  176. package/dist/worker.js.map +1 -0
  177. package/dist/wrapper-runtime.d.ts +10 -0
  178. package/dist/wrapper-runtime.d.ts.map +1 -0
  179. package/dist/wrapper-runtime.js +83 -0
  180. package/dist/wrapper-runtime.js.map +1 -0
  181. package/dist/wrapper-session.d.ts +16 -0
  182. package/dist/wrapper-session.d.ts.map +1 -0
  183. package/dist/wrapper-session.js +244 -0
  184. package/dist/wrapper-session.js.map +1 -0
  185. package/package.json +50 -0
  186. package/src/authored-flow-error.ts +31 -0
  187. package/src/authored-flow-executor.ts +365 -0
  188. package/src/authored-flow-lifecycle.ts +344 -0
  189. package/src/authored-flow-loader.ts +44 -0
  190. package/src/authored-flow-operation.ts +252 -0
  191. package/src/authored-flow.ts +21 -0
  192. package/src/authored-promise-graph.ts +196 -0
  193. package/src/backlog-picker.ts +153 -0
  194. package/src/canonical.ts +57 -0
  195. package/src/cli/check.ts +391 -0
  196. package/src/cli/direct-run.ts +86 -0
  197. package/src/cli/hn-monitor.ts +286 -0
  198. package/src/cli/interruptible-sleep.ts +27 -0
  199. package/src/cli/run.ts +439 -0
  200. package/src/cli/tick-runner.ts +378 -0
  201. package/src/cli-adapter.ts +115 -0
  202. package/src/cli-executable.ts +12 -0
  203. package/src/cli.ts +364 -0
  204. package/src/compile.ts +573 -0
  205. package/src/demo-hn-monitor.ts +132 -0
  206. package/src/dir-watcher-poller.ts +122 -0
  207. package/src/direct-input.ts +69 -0
  208. package/src/failure-kinds.ts +77 -0
  209. package/src/gate-contract.ts +72 -0
  210. package/src/hn-poller.ts +104 -0
  211. package/src/index.ts +195 -0
  212. package/src/journal-client.ts +379 -0
  213. package/src/json-schema-bound.ts +357 -0
  214. package/src/json-schema.ts +65 -0
  215. package/src/json-value.ts +110 -0
  216. package/src/model-name.ts +20 -0
  217. package/src/output-schema.ts +33 -0
  218. package/src/preflight.ts +482 -0
  219. package/src/protocol.ts +364 -0
  220. package/src/spec.ts +343 -0
  221. package/src/step-dependencies.ts +100 -0
  222. package/src/step-fields.ts +36 -0
  223. package/src/tick-source.ts +334 -0
  224. package/src/unknown-keys.ts +52 -0
  225. package/src/validate.ts +503 -0
  226. package/src/work-package-consumer.ts +73 -0
  227. package/src/work-package-validator.ts +90 -0
  228. package/src/worker-cli.ts +110 -0
  229. package/src/worker.ts +150 -0
  230. package/src/wrapper-runtime.ts +93 -0
  231. package/src/wrapper-session.ts +287 -0
@@ -0,0 +1,357 @@
1
+ // Termination bound for JSON Schema declarations — the SDK half.
2
+ //
3
+ // A schema that COMPILES is not a schema whose VALIDATION terminates. The
4
+ // kernel learned this the expensive way: `jsonschema` compiles
5
+ // `$defs.a -> $defs.b -> $defs.a` happily and then recurses until the process
6
+ // aborts with SIGABRT, after the step's command has already run.
7
+ //
8
+ // This is a line-for-line mirror of `kernel/relayflowd-core/src/schema.rs`, and
9
+ // `testdata/json-schema-bound-cases.json` is the corpus both sides are pinned
10
+ // to. Kernel and SDK therefore agree on which schemas are legal by
11
+ // construction, rather than by coincidence of Ajv's catchable RangeError and
12
+ // Rust's uncatchable abort.
13
+ //
14
+ // The rule: follow only the IN-PLACE applicators, which re-apply a subschema to
15
+ // the SAME instance. A cycle among those makes no progress and cannot
16
+ // terminate. Cycles that pass through a CHILD applicator (`properties`,
17
+ // `items`, ...) consume one level of the instance per step, so they terminate,
18
+ // and they stay legal.
19
+
20
+ export const UNBOUNDED_REF_CYCLE = 'unbounded $ref cycle';
21
+
22
+ const REFERENCE_KEYWORDS = ['$ref', '$dynamicRef', '$recursiveRef'] as const;
23
+ const IN_PLACE_SINGLE = ['not', 'if', 'then', 'else'] as const;
24
+ const IN_PLACE_ARRAY = ['allOf', 'anyOf', 'oneOf'] as const;
25
+ const IN_PLACE_MAP = ['dependentSchemas', 'dependencies'] as const;
26
+ const CHILD_SINGLE = [
27
+ 'additionalItems',
28
+ 'additionalProperties',
29
+ 'contains',
30
+ 'items',
31
+ 'propertyNames',
32
+ 'unevaluatedItems',
33
+ 'unevaluatedProperties',
34
+ ] as const;
35
+ const CHILD_MAP = ['properties', 'patternProperties'] as const;
36
+ const CHILD_ARRAY = ['prefixItems'] as const;
37
+
38
+ type Node = Record<string, unknown>;
39
+
40
+ const escape = (segment: string): string => segment.replace(/~/g, '~0').replace(/\//g, '~1');
41
+ const unescape = (segment: string): string => segment.replace(/~1/g, '/').replace(/~0/g, '~');
42
+ const childPointer = (pointer: string, key: string): string => `${pointer}/${escape(key)}`;
43
+ const displayPointer = (pointer: string): string => (pointer === '' ? '#' : `#${pointer}`);
44
+
45
+ const isNode = (value: unknown): value is Node =>
46
+ typeof value === 'object' && value !== null && !Array.isArray(value);
47
+
48
+ /** Resolve a JSON pointer against the document root. */
49
+ function at(root: unknown, pointer: string): unknown {
50
+ if (pointer === '') return root;
51
+ let current: unknown = root;
52
+ for (const raw of pointer.split('/').slice(1)) {
53
+ const segment = unescape(raw);
54
+ if (Array.isArray(current)) {
55
+ const index = Number(segment);
56
+ if (!Number.isInteger(index) || index < 0 || index >= current.length) return undefined;
57
+ current = current[index];
58
+ } else if (isNode(current)) {
59
+ if (!Object.prototype.hasOwnProperty.call(current, segment)) return undefined;
60
+ current = current[segment];
61
+ } else {
62
+ return undefined;
63
+ }
64
+ }
65
+ return current;
66
+ }
67
+
68
+ function percentDecode(input: string): string {
69
+ try {
70
+ return decodeURIComponent(input);
71
+ } catch {
72
+ return input;
73
+ }
74
+ }
75
+
76
+ /** Split `<uri>#<fragment>`. `undefined` means no `#` at all, which is
77
+ * distinct from an empty fragment. */
78
+ function splitFragment(reference: string): [string, string | undefined] {
79
+ const index = reference.indexOf('#');
80
+ return index === -1
81
+ ? [reference, undefined]
82
+ : [reference.slice(0, index), reference.slice(index + 1)];
83
+ }
84
+
85
+ function stripFragment(uri: string): string {
86
+ return splitFragment(uri)[0];
87
+ }
88
+
89
+ /** RFC 3986 section 3.1 scheme detection: absolute URI vs relative reference. */
90
+ function hasScheme(reference: string): boolean {
91
+ return /^[A-Za-z][A-Za-z0-9+\-.]*:/.test(reference);
92
+ }
93
+
94
+ /** RFC 3986 section 5.2.4. */
95
+ function removeDotSegments(path: string): string {
96
+ const absolute = path.startsWith('/');
97
+ const trailing = path.endsWith('/') || path.endsWith('/.') || path.endsWith('/..');
98
+ const out: string[] = [];
99
+ for (const segment of path.split('/')) {
100
+ if (segment === '' || segment === '.') continue;
101
+ if (segment === '..') out.pop();
102
+ else out.push(segment);
103
+ }
104
+ let resolved = absolute ? '/' : '';
105
+ resolved += out.join('/');
106
+ if (trailing && !resolved.endsWith('/')) resolved += '/';
107
+ return resolved;
108
+ }
109
+
110
+ /** Split a URI into the part a rooted path replaces and the path itself. */
111
+ function splitAuthority(uri: string): [string, string] {
112
+ const index = uri.indexOf('://');
113
+ if (index !== -1) {
114
+ const after = uri.slice(index + 3);
115
+ const slash = after.indexOf('/');
116
+ if (slash === -1) return [uri, ''];
117
+ return [uri.slice(0, index + 3 + slash), uri.slice(index + 3 + slash)];
118
+ }
119
+ const last = uri.lastIndexOf('/');
120
+ return last === -1 ? [uri, ''] : [uri.slice(0, last + 1), uri.slice(last + 1)];
121
+ }
122
+
123
+ /**
124
+ * RFC 3986 section 5.3 reference resolution, enough of it for schema
125
+ * identifiers. Exactness matters less than *consistency*: every `$id` is
126
+ * registered through this function and every `$ref` looked up through it, and
127
+ * `collectScopes` also registers each resource under its raw `$id`, so a
128
+ * bundled document matches regardless of normalization. Mirrors
129
+ * `kernel/relayflowd-core/src/schema.rs::resolve_uri`.
130
+ */
131
+ function resolveUri(base: string, reference: string): string {
132
+ if (reference === '') return base;
133
+ if (hasScheme(reference)) return reference;
134
+ if (base === '') return reference;
135
+ if (reference.startsWith('//')) {
136
+ const scheme = base.split(':')[0] ?? '';
137
+ return `${scheme}://${reference.slice(2)}`;
138
+ }
139
+ const [root, path] = splitAuthority(base);
140
+ if (reference.startsWith('/')) return `${root}${removeDotSegments(reference)}`;
141
+ const slash = path.lastIndexOf('/');
142
+ const merged = slash === -1 ? `/${reference}` : `${path.slice(0, slash + 1)}${reference}`;
143
+ return `${root}${removeDotSegments(merged)}`;
144
+ }
145
+
146
+ interface Scopes {
147
+ /** Base URI of every resource declared in this document -> its pointer,
148
+ * registered under both the resolved and the raw `$id`. */
149
+ resources: Map<string, string>;
150
+ /** `<base URI> <anchor name>` -> pointer. Anchors are scoped to the resource
151
+ * that declares them, so the same name under two different `$id`s does not
152
+ * shadow. */
153
+ anchors: Map<string, string>;
154
+ /** Base URI in effect at each node pointer. */
155
+ baseAt: Map<string, string>;
156
+ hasIds: boolean;
157
+ }
158
+
159
+ const anchorKey = (base: string, name: string): string => `${base} ${name}`;
160
+
161
+ function collectScopes(root: unknown): Scopes {
162
+ const resources = new Map<string, string>([['', '']]);
163
+ const anchors = new Map<string, string>();
164
+ const baseAt = new Map<string, string>();
165
+ let hasIds = false;
166
+ const stack: Array<[string, string, unknown]> = [['', '', root]];
167
+ while (stack.length > 0) {
168
+ const [pointer, inherited, node] = stack.pop() as [string, string, unknown];
169
+ if (Array.isArray(node)) {
170
+ baseAt.set(pointer, inherited);
171
+ node.forEach((value, index) => stack.push([`${pointer}/${index}`, inherited, value]));
172
+ } else if (isNode(node)) {
173
+ let base = inherited;
174
+ const declared = node['$id'] ?? node['id'];
175
+ if (typeof declared === 'string') {
176
+ hasIds = true;
177
+ const raw = stripFragment(declared);
178
+ const resolved = resolveUri(base, raw);
179
+ if (!resources.has(resolved)) resources.set(resolved, pointer);
180
+ if (!resources.has(raw)) resources.set(raw, pointer);
181
+ base = resolved;
182
+ }
183
+ baseAt.set(pointer, base);
184
+ for (const keyword of ['$anchor', '$dynamicAnchor', '$recursiveAnchor'] as const) {
185
+ const name = node[keyword];
186
+ if (typeof name === 'string' && !anchors.has(anchorKey(base, name))) {
187
+ anchors.set(anchorKey(base, name), pointer);
188
+ }
189
+ }
190
+ for (const [key, value] of Object.entries(node)) {
191
+ stack.push([childPointer(pointer, key), base, value]);
192
+ }
193
+ } else {
194
+ baseAt.set(pointer, inherited);
195
+ }
196
+ }
197
+ return { resources, anchors, baseAt, hasIds };
198
+ }
199
+
200
+ /**
201
+ * Resolve a reference to the pointer of the node it names, or `undefined` when
202
+ * it names nothing inside this document.
203
+ *
204
+ * A reference naming no in-document resource is left opaque, on a claim
205
+ * narrower than this function used to make. It is either remote -- refused by
206
+ * the engine, which has no retriever -- or a bundled meta-schema, which cannot
207
+ * reference back into this document and so cannot close a cycle rooted here.
208
+ * The older, wider claim was false for an in-document `$id`, which is the gap
209
+ * this resolver closes.
210
+ */
211
+ function resolve(
212
+ base: string,
213
+ reference: string,
214
+ resources: Map<string, string>,
215
+ anchors: Map<string, string>,
216
+ ): string | undefined {
217
+ const [uri, fragment] = splitFragment(reference);
218
+ let targetBase: string;
219
+ let targetPointer: string | undefined;
220
+ if (uri === '') {
221
+ targetBase = base;
222
+ targetPointer = resources.get(base);
223
+ } else {
224
+ const resolved = resolveUri(base, uri);
225
+ targetPointer = resources.get(resolved);
226
+ if (targetPointer !== undefined) {
227
+ targetBase = resolved;
228
+ } else {
229
+ // Literal fallback, in case this resolver and the `$id` that registered
230
+ // the resource normalized differently.
231
+ targetBase = uri;
232
+ targetPointer = resources.get(uri);
233
+ }
234
+ }
235
+ if (targetPointer === undefined) return undefined;
236
+ if (fragment === undefined || fragment === '') return targetPointer;
237
+ if (fragment.startsWith('/')) return `${targetPointer}${percentDecode(fragment)}`;
238
+ return anchors.get(anchorKey(targetBase, percentDecode(fragment)));
239
+ }
240
+
241
+ function collectArray(node: Node, pointer: string, keyword: string, out: string[]): void {
242
+ const value = node[keyword];
243
+ if (Array.isArray(value)) {
244
+ for (let index = 0; index < value.length; index += 1) {
245
+ out.push(`${childPointer(pointer, keyword)}/${index}`);
246
+ }
247
+ }
248
+ }
249
+
250
+ function collectMap(node: Node, pointer: string, keyword: string, out: string[]): void {
251
+ const value = node[keyword];
252
+ if (!isNode(value)) return;
253
+ for (const [name, entry] of Object.entries(value)) {
254
+ // draft-07 `dependencies` values may be a property-name array.
255
+ if (isNode(entry) || typeof entry === 'boolean') {
256
+ out.push(`${childPointer(pointer, keyword)}/${escape(name)}`);
257
+ }
258
+ }
259
+ }
260
+
261
+ /** Iterative DFS — the checker must not recurse, or it inherits the very
262
+ * unbounded recursion it exists to refuse. */
263
+ function findCycle(edges: Map<string, string[]>): string[] | undefined {
264
+ const color = new Map<string, 'gray' | 'black'>();
265
+ for (const start of edges.keys()) {
266
+ if (color.has(start)) continue;
267
+ const stack: Array<{ node: string; index: number }> = [{ node: start, index: 0 }];
268
+ const path: string[] = [start];
269
+ color.set(start, 'gray');
270
+ while (stack.length > 0) {
271
+ const top = stack[stack.length - 1] as { node: string; index: number };
272
+ const successors = edges.get(top.node) ?? [];
273
+ if (top.index < successors.length) {
274
+ const next = successors[top.index] as string;
275
+ top.index += 1;
276
+ const seen = color.get(next);
277
+ if (seen === 'gray') {
278
+ const from = path.indexOf(next);
279
+ return [...path.slice(from === -1 ? 0 : from), next];
280
+ }
281
+ if (seen === undefined) {
282
+ color.set(next, 'gray');
283
+ path.push(next);
284
+ stack.push({ node: next, index: 0 });
285
+ }
286
+ } else {
287
+ color.set(top.node, 'black');
288
+ path.pop();
289
+ stack.pop();
290
+ }
291
+ }
292
+ }
293
+ return undefined;
294
+ }
295
+
296
+ /**
297
+ * Refuse a declaration whose validation is not guaranteed to terminate.
298
+ * Returns the named refusal, or `undefined` when the schema is bounded.
299
+ */
300
+ export function jsonSchemaBoundError(schema: unknown): string | undefined {
301
+ if (!isNode(schema)) return undefined; // boolean schemas carry no references
302
+ const { resources, anchors, baseAt, hasIds } = collectScopes(schema);
303
+ const inPlace = new Map<string, string[]>();
304
+ const seen = new Set<string>(['']);
305
+ const queue: string[] = [''];
306
+
307
+ while (queue.length > 0) {
308
+ const pointer = queue.pop() as string;
309
+ const node = at(schema, pointer);
310
+ if (!isNode(node)) continue;
311
+ const here: string[] = [];
312
+ const children: string[] = [];
313
+
314
+ for (const keyword of REFERENCE_KEYWORDS) {
315
+ const reference = node[keyword];
316
+ if (typeof reference !== 'string') continue;
317
+ const base = hasIds ? (baseAt.get(pointer) ?? '') : '';
318
+ const target = resolve(base, reference, resources, anchors);
319
+ if (target !== undefined && at(schema, target) !== undefined) here.push(target);
320
+ }
321
+ for (const keyword of IN_PLACE_SINGLE) {
322
+ if (keyword in node) here.push(childPointer(pointer, keyword));
323
+ }
324
+ for (const keyword of IN_PLACE_ARRAY) collectArray(node, pointer, keyword, here);
325
+ for (const keyword of IN_PLACE_MAP) collectMap(node, pointer, keyword, here);
326
+ for (const keyword of CHILD_SINGLE) {
327
+ const value = node[keyword];
328
+ if (Array.isArray(value)) {
329
+ // draft-04/07 tuple form: `items` may be an array of schemas.
330
+ for (let index = 0; index < value.length; index += 1) {
331
+ children.push(`${childPointer(pointer, keyword)}/${index}`);
332
+ }
333
+ } else if (value !== undefined) {
334
+ children.push(childPointer(pointer, keyword));
335
+ }
336
+ }
337
+ for (const keyword of CHILD_MAP) collectMap(node, pointer, keyword, children);
338
+ for (const keyword of CHILD_ARRAY) collectArray(node, pointer, keyword, children);
339
+
340
+ for (const next of [...here, ...children]) {
341
+ if (!seen.has(next)) {
342
+ seen.add(next);
343
+ queue.push(next);
344
+ }
345
+ }
346
+ // `$defs`/`definitions` are containers, not applicators: their members are
347
+ // reachable only through a `$ref`, so an unused degenerate definition is
348
+ // never validated and stays legal.
349
+ if (here.length > 0) inPlace.set(pointer, here);
350
+ }
351
+
352
+ const cycle = findCycle(inPlace);
353
+ if (cycle === undefined) return undefined;
354
+ return `${UNBOUNDED_REF_CYCLE}: ${cycle
355
+ .map(displayPointer)
356
+ .join(' -> ')} — this cycle re-applies to the same instance, so validation would not terminate`;
357
+ }
@@ -0,0 +1,65 @@
1
+ import Ajv from 'ajv';
2
+ import AjvDraft4 from 'ajv-draft-04';
3
+ import Ajv2019 from 'ajv/dist/2019.js';
4
+ import Ajv2020 from 'ajv/dist/2020.js';
5
+ import draft6MetaSchema from 'ajv/dist/refs/json-schema-draft-06.json' with { type: 'json' };
6
+ import { jsonSchemaBoundError } from './json-schema-bound.js';
7
+ import { snapshotJsonValue, type JsonValue } from './json-value.js';
8
+
9
+ const DRAFT_4 = 'http://json-schema.org/draft-04/schema';
10
+ const DRAFT_6 = 'http://json-schema.org/draft-06/schema';
11
+ const DRAFT_7 = 'http://json-schema.org/draft-07/schema';
12
+ const DRAFT_2019_09 = 'https://json-schema.org/draft/2019-09/schema';
13
+ const DRAFT_2020_12 = 'https://json-schema.org/draft/2020-12/schema';
14
+
15
+ /** Compile a declaration with the same drafts accepted by the kernel. */
16
+ export function jsonSchemaError(schema: boolean | Record<string, unknown>): string | undefined {
17
+ // Termination first: a schema whose $ref graph cycles without consuming
18
+ // input compiles fine here and aborts the kernel at verification time. Ajv's
19
+ // own overflow is a catchable RangeError, so the authoring path happened to
20
+ // hold for one shape of this bug and not for others; the explicit bound is
21
+ // what makes SDK and kernel agree. See sdk/src/json-schema-bound.ts.
22
+ const unbounded = jsonSchemaBoundError(schema);
23
+ if (unbounded !== undefined) return unbounded;
24
+ try {
25
+ const dialect = typeof schema === 'object' && typeof schema['$schema'] === 'string'
26
+ ? schema['$schema'].replace(/#$/, '')
27
+ : DRAFT_2020_12;
28
+ const options = { strict: false, allErrors: true } as const;
29
+ if (dialect === DRAFT_4) {
30
+ new AjvDraft4(options).compile(schema);
31
+ } else if (dialect === DRAFT_6) {
32
+ const validator = new Ajv(options);
33
+ validator.addMetaSchema(draft6MetaSchema);
34
+ validator.compile(schema);
35
+ } else if (dialect === DRAFT_7) {
36
+ new Ajv(options).compile(schema);
37
+ } else if (dialect === DRAFT_2019_09) {
38
+ new Ajv2019(options).compile(schema);
39
+ } else {
40
+ // Ajv reports unknown dialect identifiers instead of guessing.
41
+ new Ajv2020(options).compile(schema);
42
+ }
43
+ return undefined;
44
+ } catch (error) {
45
+ // A RangeError here is Ajv exhausting its own JS stack while COMPILING,
46
+ // not a verdict about the schema. The bound above has already proved this
47
+ // declaration terminates, and the kernel -- which is the engine that
48
+ // actually validates outputs -- accepts and runs it. Reporting Ajv's stack
49
+ // as `invalid JSON Schema: Maximum call stack size exceeded` would refuse a
50
+ // legal schema, leak an engine-internal message as if it were a named
51
+ // refusal kind, and put legality back in the hands of an engine's
52
+ // accidental overflow behaviour -- which is the precise defect the shared
53
+ // rule and corpus exist to remove. The rule decides legality; Ajv decides
54
+ // only well-formedness, and a stack overflow is neither verdict.
55
+ if (error instanceof RangeError) return undefined;
56
+ return error instanceof Error ? error.message : String(error);
57
+ }
58
+ }
59
+
60
+ export function snapshotJsonSchema(schema: unknown, at: string): boolean | Record<string, JsonValue> {
61
+ const snapshot = snapshotJsonValue(schema, at);
62
+ if (typeof snapshot === 'boolean') return snapshot;
63
+ if (snapshot !== null && !Array.isArray(snapshot) && typeof snapshot === 'object') return snapshot;
64
+ throw new Error(`${at}: expected a JSON Schema object or boolean`);
65
+ }
@@ -0,0 +1,110 @@
1
+ import { isProxy } from 'node:util/types';
2
+
3
+ export type JsonValue =
4
+ | null
5
+ | boolean
6
+ | number
7
+ | string
8
+ | JsonValue[]
9
+ | { [key: string]: JsonValue };
10
+
11
+ /** Copy runtime input into frozen, behavior-free JSON data. */
12
+ export function snapshotJsonValue(value: unknown, at: string): JsonValue {
13
+ return snapshot(value, at, new WeakSet<object>());
14
+ }
15
+
16
+ function snapshot(value: unknown, at: string, ancestors: WeakSet<object>): JsonValue {
17
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
18
+ if (typeof value === 'number') {
19
+ if (Number.isFinite(value)) return value;
20
+ throw nonJson(at, 'numbers must be finite');
21
+ }
22
+ if (typeof value !== 'object') {
23
+ throw nonJson(at, `${typeof value} values are not allowed`);
24
+ }
25
+ // Every ordinary reflective operation on a Proxy can execute author code.
26
+ // Node and Bun expose this trap-free brand check, so reject before touching
27
+ // its prototype, keys, descriptors, or identity collection.
28
+ if (isProxy(value)) throw nonJson(at, 'Proxy objects are not allowed');
29
+ if (ancestors.has(value)) throw nonJson(at, 'cycles are not allowed');
30
+ ancestors.add(value);
31
+ try {
32
+ return Array.isArray(value)
33
+ ? snapshotArray(value, at, ancestors)
34
+ : snapshotObject(value, at, ancestors);
35
+ } finally {
36
+ ancestors.delete(value);
37
+ }
38
+ }
39
+
40
+ function snapshotArray(
41
+ value: unknown[],
42
+ at: string,
43
+ ancestors: WeakSet<object>,
44
+ ): JsonValue[] {
45
+ const keys = Reflect.ownKeys(value);
46
+ for (const key of keys) {
47
+ if (key === 'length') continue;
48
+ if (typeof key !== 'string' || !isArrayIndex(key, value.length)) {
49
+ throw nonJson(at, 'arrays may contain only indexed data');
50
+ }
51
+ }
52
+ const out: JsonValue[] = [];
53
+ for (let index = 0; index < value.length; index += 1) {
54
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
55
+ if (descriptor === undefined) throw nonJson(`${at}[${index}]`, 'array holes are not allowed');
56
+ out.push(snapshotDescriptor(descriptor, `${at}[${index}]`, ancestors));
57
+ }
58
+ return Object.freeze(out) as unknown as JsonValue[];
59
+ }
60
+
61
+ function snapshotObject(
62
+ value: object,
63
+ at: string,
64
+ ancestors: WeakSet<object>,
65
+ ): { [key: string]: JsonValue } {
66
+ const prototype = Object.getPrototypeOf(value);
67
+ if (prototype !== Object.prototype && prototype !== null) {
68
+ throw nonJson(at, 'only plain objects are allowed');
69
+ }
70
+ const out = Object.create(null) as { [key: string]: JsonValue };
71
+ for (const key of Reflect.ownKeys(value)) {
72
+ if (typeof key !== 'string') throw nonJson(at, 'symbol keys are not allowed');
73
+ const childAt = propertyPath(at, key);
74
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
75
+ if (descriptor === undefined) throw nonJson(childAt, 'missing property descriptor');
76
+ const child = descriptorValue(descriptor, childAt);
77
+ // JSON.stringify and the pre-existing compiler omit undefined object
78
+ // optionals. Arrays remain strict because undefined there becomes null.
79
+ if (child === undefined) continue;
80
+ out[key] = snapshot(child, childAt, ancestors);
81
+ }
82
+ return Object.freeze(out);
83
+ }
84
+
85
+ function snapshotDescriptor(
86
+ descriptor: PropertyDescriptor,
87
+ at: string,
88
+ ancestors: WeakSet<object>,
89
+ ): JsonValue {
90
+ return snapshot(descriptorValue(descriptor, at), at, ancestors);
91
+ }
92
+
93
+ function descriptorValue(descriptor: PropertyDescriptor, at: string): unknown {
94
+ if (!descriptor.enumerable) throw nonJson(at, 'non-enumerable properties are not allowed');
95
+ if (!('value' in descriptor)) throw nonJson(at, 'accessors are not allowed');
96
+ return descriptor.value;
97
+ }
98
+
99
+ function isArrayIndex(key: string, length: number): boolean {
100
+ const index = Number(key);
101
+ return Number.isInteger(index) && index >= 0 && index < length && String(index) === key;
102
+ }
103
+
104
+ function propertyPath(at: string, key: string): string {
105
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `${at}.${key}` : `${at}[${JSON.stringify(key)}]`;
106
+ }
107
+
108
+ function nonJson(at: string, detail: string): Error {
109
+ return new Error(`${at}: expected JSON-compatible data; ${detail}`);
110
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Validate model declaration syntax only. Existence is proven separately by
3
+ * the exact project allowlist and the model-scoped CLI preflight; this helper
4
+ * deliberately does not infer providers or accept names by pattern.
5
+ */
6
+ export function modelNameError(value: unknown): string | undefined {
7
+ if (typeof value !== 'string' || value.length === 0) {
8
+ return 'expected a non-empty string';
9
+ }
10
+ if (value !== value.trim()) {
11
+ return 'expected a trimmed string';
12
+ }
13
+ for (const character of value) {
14
+ const code = character.charCodeAt(0);
15
+ if (code < 0x20 || code === 0x7f) {
16
+ return 'must not contain control characters';
17
+ }
18
+ }
19
+ return undefined;
20
+ }
@@ -0,0 +1,33 @@
1
+ import { jsonSchemaError } from './json-schema.js';
2
+
3
+ /** JSON Schema accepted by the `output` authoring declaration. */
4
+ export type JsonOutputSchema = Record<string, unknown>;
5
+
6
+ /** Validate authoring sugar before it can be compiled or submitted. */
7
+ export function validateOutputDeclaration(
8
+ step: { output?: unknown; verification?: unknown },
9
+ at: string,
10
+ ): string[] {
11
+ if (step.output === undefined) return [];
12
+ const errors: string[] = [];
13
+ if (!isObject(step.output)) {
14
+ errors.push(`${at}.output: expected a JSON Schema object`);
15
+ } else {
16
+ // `output` lowers to a `json_schema` gate at compile time, so it must clear
17
+ // exactly the gate a hand-written `verification: {type: json_schema}` does.
18
+ // Without this the kernel refuses at `run.start` a declaration `flows check`
19
+ // had just reported as a gate — the divergence this PR exists to close.
20
+ const invalid = jsonSchemaError(step.output);
21
+ if (invalid !== undefined) {
22
+ errors.push(`${at}.output: invalid JSON Schema: ${invalid}`);
23
+ }
24
+ }
25
+ if (step.verification !== undefined) {
26
+ errors.push(`${at}: output already declares json_schema verification; remove verification`);
27
+ }
28
+ return errors;
29
+ }
30
+
31
+ function isObject(value: unknown): value is Record<string, unknown> {
32
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
33
+ }