@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.
- package/CHANGELOG.md +849 -0
- package/README.md +190 -0
- package/package.json +36 -0
- package/src/activation-provenance.ts +116 -0
- package/src/activation-source-compatibility.ts +430 -0
- package/src/activation-source-derivation.ts +227 -0
- package/src/activation-source.ts +93 -0
- package/src/blob-ref.ts +90 -0
- package/src/branded.ts +135 -0
- package/src/collections.ts +99 -0
- package/src/compose-batch.ts +69 -0
- package/src/compose-schema.ts +36 -0
- package/src/context.ts +66 -0
- package/src/derive.ts +485 -0
- package/src/detours.ts +8 -0
- package/src/diagnostics.ts +21 -0
- package/src/draft.ts +350 -0
- package/src/entity.ts +346 -0
- package/src/error-rendering.ts +87 -0
- package/src/errors.ts +483 -0
- package/src/execute.ts +1577 -0
- package/src/fetch.ts +138 -0
- package/src/fire.ts +1172 -0
- package/src/glob.ts +81 -0
- package/src/guards.ts +37 -0
- package/src/index.ts +704 -0
- package/src/internal/fork-ctx.ts +69 -0
- package/src/layer-field-rendering.ts +193 -0
- package/src/layer.ts +81 -0
- package/src/observe.ts +361 -0
- package/src/path-scope.ts +66 -0
- package/src/path-security.ts +98 -0
- package/src/patterns/bulk.ts +16 -0
- package/src/patterns/change.ts +12 -0
- package/src/patterns/date-range.ts +12 -0
- package/src/patterns/index.ts +8 -0
- package/src/patterns/pagination.ts +22 -0
- package/src/patterns/progress.ts +13 -0
- package/src/patterns/sorting.ts +14 -0
- package/src/patterns/status.ts +11 -0
- package/src/patterns/timestamps.ts +12 -0
- package/src/permits.ts +12 -0
- package/src/queue.ts +163 -0
- package/src/redaction/index.ts +3 -0
- package/src/redaction/patterns.ts +50 -0
- package/src/redaction/redactor.ts +178 -0
- package/src/resilience.ts +234 -0
- package/src/resource-config.ts +804 -0
- package/src/resource.ts +194 -0
- package/src/result.ts +212 -0
- package/src/run.ts +76 -0
- package/src/runtime-builtins.ts +69 -0
- package/src/schedule-runtime.ts +689 -0
- package/src/schedule.ts +326 -0
- package/src/serialization.ts +265 -0
- package/src/sha256.ts +136 -0
- package/src/signal-diagnostics.ts +633 -0
- package/src/signal-ref.ts +111 -0
- package/src/signal.ts +104 -0
- package/src/store/accessor-protocol.ts +56 -0
- package/src/store/index.ts +4 -0
- package/src/structured-examples.ts +248 -0
- package/src/surface-derivation.ts +91 -0
- package/src/surface-filter.ts +101 -0
- package/src/surface-overlay.ts +694 -0
- package/src/surface-versioning.ts +42 -0
- package/src/topo.ts +835 -0
- package/src/tracing.ts +346 -0
- package/src/trail-id-glob.ts +15 -0
- package/src/trail.ts +1351 -0
- package/src/trails/derive-trail.ts +835 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +152 -0
- package/src/trails-db.ts +212 -0
- package/src/transport-error-map.ts +163 -0
- package/src/type-utils.ts +87 -0
- package/src/types.ts +300 -0
- package/src/validate-established-topo.ts +73 -0
- package/src/validate-topo.ts +725 -0
- package/src/validation.ts +330 -0
- package/src/version-marker.ts +716 -0
- package/src/version-resolution.ts +308 -0
- package/src/version-runtime.ts +120 -0
- package/src/webhook.ts +461 -0
- package/src/workspace.ts +244 -0
- 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
|
+
};
|