@ontrails/core 0.2.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 (86) hide show
  1. package/CHANGELOG.md +849 -0
  2. package/README.md +190 -0
  3. package/package.json +36 -0
  4. package/src/activation-provenance.ts +116 -0
  5. package/src/activation-source-compatibility.ts +430 -0
  6. package/src/activation-source-derivation.ts +227 -0
  7. package/src/activation-source.ts +93 -0
  8. package/src/blob-ref.ts +90 -0
  9. package/src/branded.ts +135 -0
  10. package/src/collections.ts +99 -0
  11. package/src/compose-batch.ts +69 -0
  12. package/src/compose-schema.ts +36 -0
  13. package/src/context.ts +66 -0
  14. package/src/derive.ts +485 -0
  15. package/src/detours.ts +8 -0
  16. package/src/diagnostics.ts +21 -0
  17. package/src/draft.ts +350 -0
  18. package/src/entity.ts +346 -0
  19. package/src/error-rendering.ts +87 -0
  20. package/src/errors.ts +483 -0
  21. package/src/execute.ts +1577 -0
  22. package/src/fetch.ts +138 -0
  23. package/src/fire.ts +1172 -0
  24. package/src/glob.ts +81 -0
  25. package/src/guards.ts +37 -0
  26. package/src/index.ts +704 -0
  27. package/src/internal/fork-ctx.ts +69 -0
  28. package/src/layer-field-rendering.ts +193 -0
  29. package/src/layer.ts +81 -0
  30. package/src/observe.ts +361 -0
  31. package/src/path-scope.ts +66 -0
  32. package/src/path-security.ts +98 -0
  33. package/src/patterns/bulk.ts +16 -0
  34. package/src/patterns/change.ts +12 -0
  35. package/src/patterns/date-range.ts +12 -0
  36. package/src/patterns/index.ts +8 -0
  37. package/src/patterns/pagination.ts +22 -0
  38. package/src/patterns/progress.ts +13 -0
  39. package/src/patterns/sorting.ts +14 -0
  40. package/src/patterns/status.ts +11 -0
  41. package/src/patterns/timestamps.ts +12 -0
  42. package/src/permits.ts +12 -0
  43. package/src/queue.ts +163 -0
  44. package/src/redaction/index.ts +3 -0
  45. package/src/redaction/patterns.ts +50 -0
  46. package/src/redaction/redactor.ts +178 -0
  47. package/src/resilience.ts +234 -0
  48. package/src/resource-config.ts +804 -0
  49. package/src/resource.ts +194 -0
  50. package/src/result.ts +212 -0
  51. package/src/run.ts +76 -0
  52. package/src/runtime-builtins.ts +69 -0
  53. package/src/schedule-runtime.ts +689 -0
  54. package/src/schedule.ts +326 -0
  55. package/src/serialization.ts +265 -0
  56. package/src/sha256.ts +136 -0
  57. package/src/signal-diagnostics.ts +633 -0
  58. package/src/signal-ref.ts +111 -0
  59. package/src/signal.ts +104 -0
  60. package/src/store/accessor-protocol.ts +56 -0
  61. package/src/store/index.ts +4 -0
  62. package/src/structured-examples.ts +248 -0
  63. package/src/surface-derivation.ts +91 -0
  64. package/src/surface-filter.ts +101 -0
  65. package/src/surface-overlay.ts +694 -0
  66. package/src/surface-versioning.ts +42 -0
  67. package/src/topo.ts +835 -0
  68. package/src/tracing.ts +346 -0
  69. package/src/trail-id-glob.ts +15 -0
  70. package/src/trail.ts +1351 -0
  71. package/src/trails/derive-trail.ts +835 -0
  72. package/src/trails/index.ts +9 -0
  73. package/src/trails/ingest.ts +152 -0
  74. package/src/trails-db.ts +212 -0
  75. package/src/transport-error-map.ts +163 -0
  76. package/src/type-utils.ts +87 -0
  77. package/src/types.ts +300 -0
  78. package/src/validate-established-topo.ts +73 -0
  79. package/src/validate-topo.ts +725 -0
  80. package/src/validation.ts +330 -0
  81. package/src/version-marker.ts +716 -0
  82. package/src/version-resolution.ts +308 -0
  83. package/src/version-runtime.ts +120 -0
  84. package/src/webhook.ts +461 -0
  85. package/src/workspace.ts +244 -0
  86. package/src/zod-wrappers.ts +72 -0
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Validation utilities for @ontrails/core
3
+ *
4
+ * Wraps Zod parsing into Result types and provides JSON Schema conversion
5
+ * for trail input schemas.
6
+ */
7
+
8
+ import type { z } from 'zod';
9
+
10
+ import { BLOB_REF_SCHEMA_META_KEY, blobRefJsonSchema } from './blob-ref.js';
11
+ import { ValidationError } from './errors.js';
12
+ import { Result } from './result.js';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Zod → JSON Schema (Zod v4)
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /** Internal accessor for Zod v4's internals. */
19
+ interface ZodInternals {
20
+ readonly _zod: {
21
+ readonly def: Readonly<Record<string, unknown>>;
22
+ readonly traits: ReadonlySet<string>;
23
+ };
24
+ readonly description?: string;
25
+ }
26
+
27
+ type JsonSchema = Record<string, unknown>;
28
+ type JsonSchemaConverter = (schema: z.ZodType) => JsonSchema;
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Internal helpers (defined before usage)
32
+ // ---------------------------------------------------------------------------
33
+
34
+ const isOptionalLike = (s: ZodInternals): boolean => {
35
+ let current = s;
36
+ const seen = new Set<ZodInternals>();
37
+ while (
38
+ current._zod.def['type'] === 'readonly' &&
39
+ !seen.has(current) &&
40
+ current._zod.def['innerType'] !== undefined
41
+ ) {
42
+ seen.add(current);
43
+ current = current._zod.def['innerType'] as ZodInternals;
44
+ }
45
+ const defType = current._zod.def['type'] as string;
46
+ return defType === 'optional' || defType === 'default';
47
+ };
48
+
49
+ const getSchemaMeta = (
50
+ schema: z.ZodType
51
+ ): Readonly<Record<string, unknown>> | undefined => {
52
+ const maybeMeta = (schema as unknown as { meta?: () => unknown }).meta;
53
+ if (typeof maybeMeta !== 'function') {
54
+ return undefined;
55
+ }
56
+ const meta = maybeMeta.call(schema);
57
+ return typeof meta === 'object' && meta !== null
58
+ ? (meta as Readonly<Record<string, unknown>>)
59
+ : undefined;
60
+ };
61
+
62
+ const getSchemaJsonSchemaOverride = (
63
+ schema: z.ZodType
64
+ ): JsonSchema | undefined => {
65
+ const meta = getSchemaMeta(schema);
66
+ if (meta?.[BLOB_REF_SCHEMA_META_KEY] === true) {
67
+ const override: JsonSchema = {
68
+ properties: Object.fromEntries(
69
+ Object.entries(blobRefJsonSchema.properties).map(([key, value]) => [
70
+ key,
71
+ { ...value },
72
+ ])
73
+ ),
74
+ required: [...blobRefJsonSchema.required],
75
+ type: blobRefJsonSchema.type,
76
+ };
77
+ const { description } = schema as unknown as ZodInternals;
78
+ if (description) {
79
+ override['description'] = description;
80
+ }
81
+ return override;
82
+ }
83
+ return undefined;
84
+ };
85
+
86
+ /**
87
+ * Whether a schema has a deterministic JSON-schema override derivation (for
88
+ * example `blobRefSchema`, a `z.custom(...)` carrying the descriptor metadata).
89
+ * Such schemas derive to a canonical descriptor regardless of their underlying
90
+ * Zod internals, so marker derivation can treat them as supported.
91
+ */
92
+ export const schemaHasJsonSchemaOverride = (schema: z.ZodType): boolean =>
93
+ getSchemaJsonSchemaOverride(schema) !== undefined;
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Issue formatting
97
+ // ---------------------------------------------------------------------------
98
+
99
+ /** Format each ZodIssue as "path: message" (or just "message" for root). */
100
+ export const formatZodIssues = (issues: z.ZodIssue[]): string[] =>
101
+ issues.map((issue) => {
102
+ const path = issue.path.join('.');
103
+ return path ? `${path}: ${issue.message}` : issue.message;
104
+ });
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Input validation
108
+ // ---------------------------------------------------------------------------
109
+
110
+ /** Parse unknown data against a Zod schema, returning a Result. */
111
+ export const validateInput = <T>(
112
+ schema: z.ZodType<T>,
113
+ data: unknown
114
+ ): Result<T, ValidationError> => {
115
+ const parsed = schema.safeParse(data);
116
+ if (parsed.success) {
117
+ return Result.ok(parsed.data);
118
+ }
119
+ const messages = formatZodIssues(parsed.error.issues);
120
+ return Result.err(
121
+ new ValidationError(messages.join('; '), {
122
+ cause: parsed.error,
123
+ context: { issues: parsed.error.issues },
124
+ })
125
+ );
126
+ };
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Output validation
130
+ // ---------------------------------------------------------------------------
131
+
132
+ /** Parse unknown data against a Zod schema, returning a Result suitable for output validation. */
133
+ export const validateOutput = <T>(
134
+ schema: z.ZodType<T>,
135
+ data: unknown
136
+ ): Result<T, ValidationError> => {
137
+ const parsed = schema.safeParse(data);
138
+ if (parsed.success) {
139
+ return Result.ok(parsed.data);
140
+ }
141
+ const messages = formatZodIssues(parsed.error.issues);
142
+ return Result.err(
143
+ new ValidationError(`Output validation failed: ${messages.join('; ')}`, {
144
+ cause: parsed.error,
145
+ context: { issues: parsed.error.issues },
146
+ })
147
+ );
148
+ };
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // Zod → JSON Schema (public API)
152
+ // ---------------------------------------------------------------------------
153
+
154
+ /**
155
+ * Sentinel indicating a dynamic default that should be omitted from schema
156
+ * exports. Zod v4 wraps all defaults in getters; dynamic ones (functions)
157
+ * produce new values on each access. We detect this by reading the getter
158
+ * twice and comparing with `Object.is`. If values differ, the default is
159
+ * dynamic and we cache this sentinel to skip it in future calls.
160
+ */
161
+ const DYNAMIC_DEFAULT = Symbol('DYNAMIC_DEFAULT');
162
+ const defaultValueCache = new WeakMap<object, unknown>();
163
+
164
+ const defaultsMatch = (left: unknown, right: unknown): boolean => {
165
+ if (Object.is(left, right)) {
166
+ return true;
167
+ }
168
+ try {
169
+ return JSON.stringify(left) === JSON.stringify(right);
170
+ } catch {
171
+ return false;
172
+ }
173
+ };
174
+
175
+ const waitForClockAdvance = (): void => {
176
+ const wallStart = Date.now();
177
+ const monotonicStart = performance.now();
178
+ while (Date.now() === wallStart && performance.now() - monotonicStart < 4) {
179
+ // Zod hides default factories behind a getter. A bounded sync wait lets
180
+ // Date.now()-style factories reveal themselves without making the API async.
181
+ }
182
+ };
183
+
184
+ const readDefaultWithDateNowOffset = (
185
+ def: Record<string, unknown>
186
+ ): unknown => {
187
+ const originalDateNow = Date.now;
188
+ try {
189
+ Date.now = () => originalDateNow() + 86_400_000;
190
+ return def['defaultValue'];
191
+ } finally {
192
+ Date.now = originalDateNow;
193
+ }
194
+ };
195
+
196
+ /**
197
+ * Read a Zod v4 default getter and decide if it is stable.
198
+ *
199
+ * Uses Object.is for primitives and JSON.stringify for objects/arrays. A delayed
200
+ * third read catches default factories such as `() => Date.now()` that can return
201
+ * equal values for immediate back-to-back reads. A Date.now() probe catches
202
+ * coarser clock factories without requiring marker derivation to wait for the
203
+ * next second/day boundary.
204
+ */
205
+ const resolveDefault = (def: Record<string, unknown>): unknown => {
206
+ try {
207
+ const a = def['defaultValue'];
208
+ const b = def['defaultValue'];
209
+ if (!defaultsMatch(a, b)) {
210
+ return DYNAMIC_DEFAULT;
211
+ }
212
+ waitForClockAdvance();
213
+ const c = def['defaultValue'];
214
+ if (!defaultsMatch(a, c)) {
215
+ return DYNAMIC_DEFAULT;
216
+ }
217
+ const d = readDefaultWithDateNowOffset(def);
218
+ return defaultsMatch(a, d) ? a : DYNAMIC_DEFAULT;
219
+ } catch {
220
+ // BigInt, circular refs, or other non-serializable defaults
221
+ return DYNAMIC_DEFAULT;
222
+ }
223
+ };
224
+
225
+ export const zodDefaultValueIsDynamic = (
226
+ def: Record<string, unknown>
227
+ ): boolean => {
228
+ if (!defaultValueCache.has(def)) {
229
+ defaultValueCache.set(def, resolveDefault(def));
230
+ }
231
+ return defaultValueCache.get(def) === DYNAMIC_DEFAULT;
232
+ };
233
+
234
+ /**
235
+ * Convert common Zod types to a JSON Schema object.
236
+ *
237
+ * Uses Zod v4's `_zod.def` and `_zod.traits` for introspection.
238
+ * Covers: string, number, boolean, object, array, enum, optional,
239
+ * default, union, literal, nullable, and describe.
240
+ */
241
+ export const zodToJsonSchema: JsonSchemaConverter = (
242
+ schema: z.ZodType
243
+ ): JsonSchema => {
244
+ const jsonSchemaOverride = getSchemaJsonSchemaOverride(schema);
245
+ if (jsonSchemaOverride !== undefined) {
246
+ return jsonSchemaOverride;
247
+ }
248
+
249
+ const s = schema as unknown as ZodInternals;
250
+
251
+ const collectObjectFields = (shape: Record<string, ZodInternals>) => {
252
+ const properties: JsonSchema = {};
253
+ const required: string[] = [];
254
+ for (const [key, value] of Object.entries(shape)) {
255
+ properties[key] = zodToJsonSchema(value as unknown as z.ZodType);
256
+ if (!isOptionalLike(value)) {
257
+ required.push(key);
258
+ }
259
+ }
260
+ return { properties, required };
261
+ };
262
+
263
+ const convertObject = (value: ZodInternals): JsonSchema => {
264
+ const shape = value._zod.def['shape'] as
265
+ | Record<string, ZodInternals>
266
+ | undefined;
267
+ if (!shape) {
268
+ return { type: 'object' };
269
+ }
270
+ const { properties, required } = collectObjectFields(shape);
271
+ const result: JsonSchema = { properties, type: 'object' };
272
+ if (required.length > 0) {
273
+ result['required'] = required;
274
+ }
275
+ return result;
276
+ };
277
+
278
+ const zodConverters: Record<string, (value: ZodInternals) => JsonSchema> = {
279
+ array: (value) => {
280
+ const element = value._zod.def['element'] as unknown as z.ZodType;
281
+ return { items: zodToJsonSchema(element), type: 'array' };
282
+ },
283
+ boolean: () => ({ type: 'boolean' }),
284
+ default: (value) => {
285
+ const inner = value._zod.def['innerType'] as unknown as z.ZodType;
286
+ const innerSchema = zodToJsonSchema(inner);
287
+ zodDefaultValueIsDynamic(value._zod.def);
288
+ const cached = defaultValueCache.get(value._zod.def);
289
+ if (cached !== DYNAMIC_DEFAULT) {
290
+ innerSchema['default'] = cached;
291
+ }
292
+ return innerSchema;
293
+ },
294
+ enum: (value) => {
295
+ const entries = value._zod.def['entries'] as Record<string, string>;
296
+ return { enum: Object.values(entries), type: 'string' };
297
+ },
298
+ literal: (value) => {
299
+ const values = value._zod.def['values'] as unknown[];
300
+ return { const: values[0] };
301
+ },
302
+ nullable: (value) => {
303
+ const inner = value._zod.def['innerType'] as unknown as z.ZodType;
304
+ return { anyOf: [zodToJsonSchema(inner), { type: 'null' }] };
305
+ },
306
+ number: () => ({ type: 'number' }),
307
+ object: convertObject,
308
+ optional: (value) => {
309
+ const inner = value._zod.def['innerType'] as unknown as z.ZodType;
310
+ return zodToJsonSchema(inner);
311
+ },
312
+ readonly: (value) => {
313
+ const inner = value._zod.def['innerType'] as unknown as z.ZodType;
314
+ return zodToJsonSchema(inner);
315
+ },
316
+ string: () => ({ type: 'string' }),
317
+ union: (value) => {
318
+ const options = value._zod.def['options'] as unknown as z.ZodType[];
319
+ return { anyOf: options.map((option) => zodToJsonSchema(option)) };
320
+ },
321
+ };
322
+
323
+ const converter = zodConverters[s._zod.def['type'] as string];
324
+ const base = converter ? converter(s) : {};
325
+
326
+ if (s.description) {
327
+ base['description'] = s.description;
328
+ }
329
+ return base;
330
+ };