@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,716 @@
|
|
|
1
|
+
import { DETOUR_MAX_ATTEMPTS_CAP } from './detours.js';
|
|
2
|
+
import { AmbiguousError, ValidationError } from './errors.js';
|
|
3
|
+
import { isPlainObject } from './guards.js';
|
|
4
|
+
import {
|
|
5
|
+
getTrailVersionEntryKind,
|
|
6
|
+
isArchivedTrailVersionEntry,
|
|
7
|
+
} from './trail.js';
|
|
8
|
+
import type { AnyTrail, TrailVersionEntry } from './trail.js';
|
|
9
|
+
import { schemaHasJsonSchemaOverride, zodToJsonSchema } from './validation.js';
|
|
10
|
+
|
|
11
|
+
export const TRAIL_VERSION_MARKER_LENGTH = 16;
|
|
12
|
+
export const TRAIL_VERSION_MARKER_MIN_PREFIX_LENGTH = 4;
|
|
13
|
+
|
|
14
|
+
export interface TrailVersionMarkerBinding {
|
|
15
|
+
readonly marker: string;
|
|
16
|
+
readonly version: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TrailVersionMarkerResolution extends TrailVersionMarkerBinding {
|
|
20
|
+
readonly prefix: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TrailVersionMarkerRecord extends TrailVersionMarkerBinding {
|
|
24
|
+
readonly current: boolean;
|
|
25
|
+
readonly kind: 'current' | 'fork' | 'revision';
|
|
26
|
+
readonly supported: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const markerPattern = /^[0-9a-f]{16}$/;
|
|
30
|
+
const markerPrefixPattern = /^[0-9a-f]+$/;
|
|
31
|
+
|
|
32
|
+
const markerValuePath = (path: readonly string[]): string =>
|
|
33
|
+
path.length === 0 ? '<root>' : path.join('.');
|
|
34
|
+
|
|
35
|
+
const markerSchemaPath = (path: readonly string[]): string => {
|
|
36
|
+
if (path.length === 0) {
|
|
37
|
+
return '<schema>';
|
|
38
|
+
}
|
|
39
|
+
let formatted = '';
|
|
40
|
+
for (const segment of path) {
|
|
41
|
+
if (segment === '[]') {
|
|
42
|
+
formatted = `${formatted}[]`;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
formatted = formatted.length === 0 ? segment : `${formatted}.${segment}`;
|
|
46
|
+
}
|
|
47
|
+
return formatted;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const supportedMarkerSchemaTypes = new Set([
|
|
51
|
+
'array',
|
|
52
|
+
'boolean',
|
|
53
|
+
'enum',
|
|
54
|
+
'literal',
|
|
55
|
+
'nullable',
|
|
56
|
+
'number',
|
|
57
|
+
'object',
|
|
58
|
+
'optional',
|
|
59
|
+
'readonly',
|
|
60
|
+
'string',
|
|
61
|
+
'union',
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const zodDef = (
|
|
65
|
+
schema: unknown,
|
|
66
|
+
path: readonly string[]
|
|
67
|
+
): Readonly<Record<string, unknown>> => {
|
|
68
|
+
if (typeof schema !== 'object' || schema === null) {
|
|
69
|
+
throw new ValidationError(
|
|
70
|
+
`Trail version marker schema at ${markerSchemaPath(path)} is not a Zod schema`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const def = (schema as { readonly _zod?: { readonly def?: unknown } })._zod
|
|
75
|
+
?.def;
|
|
76
|
+
if (!isPlainObject(def)) {
|
|
77
|
+
throw new ValidationError(
|
|
78
|
+
`Trail version marker schema at ${markerSchemaPath(path)} is not a supported Zod schema`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return def;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const zodType = (def: Readonly<Record<string, unknown>>): string =>
|
|
85
|
+
typeof def['type'] === 'string' ? def['type'] : '<unknown>';
|
|
86
|
+
|
|
87
|
+
const zodChecks = (
|
|
88
|
+
def: Readonly<Record<string, unknown>>
|
|
89
|
+
): readonly unknown[] => {
|
|
90
|
+
const { checks } = def;
|
|
91
|
+
return Array.isArray(checks) ? checks : [];
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const zodDirectCheckName = (
|
|
95
|
+
def: Readonly<Record<string, unknown>>
|
|
96
|
+
): string | undefined =>
|
|
97
|
+
typeof def['check'] === 'string' ? def['check'] : undefined;
|
|
98
|
+
|
|
99
|
+
const zodCheckName = (check: unknown): string => {
|
|
100
|
+
if (typeof check !== 'object' || check === null) {
|
|
101
|
+
return '<unknown>';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const def = (
|
|
105
|
+
check as {
|
|
106
|
+
readonly _zod?: { readonly def?: Readonly<Record<string, unknown>> };
|
|
107
|
+
}
|
|
108
|
+
)._zod?.def;
|
|
109
|
+
if (!isPlainObject(def)) {
|
|
110
|
+
return '<unknown>';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const name = def['check'] ?? def['type'];
|
|
114
|
+
return typeof name === 'string' ? name : '<unknown>';
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const nestedZodSchema = (value: unknown, path: readonly string[]): unknown => {
|
|
118
|
+
if (value === undefined) {
|
|
119
|
+
throw new ValidationError(
|
|
120
|
+
`Trail version marker schema at ${markerSchemaPath(path)} is missing an expected nested schema`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Wrapper schema types that delegate to a single inner schema.
|
|
127
|
+
const wrappedMarkerSchemaTypes = new Set([
|
|
128
|
+
'default',
|
|
129
|
+
'nullable',
|
|
130
|
+
'optional',
|
|
131
|
+
'readonly',
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
// Schemas with a deterministic JSON-schema override (e.g. blobRefSchema) derive
|
|
135
|
+
// to a canonical descriptor, so the preflight accepts them without inspecting
|
|
136
|
+
// the underlying custom Zod internals once runtime-only checks have been ruled
|
|
137
|
+
// out.
|
|
138
|
+
const hasMarkerSchemaOverride = (schema: unknown): boolean =>
|
|
139
|
+
typeof schema === 'object' &&
|
|
140
|
+
schema !== null &&
|
|
141
|
+
schemaHasJsonSchemaOverride(schema as never);
|
|
142
|
+
|
|
143
|
+
const assertMarkerJsonSafe = (
|
|
144
|
+
value: unknown,
|
|
145
|
+
path: readonly string[]
|
|
146
|
+
): void => {
|
|
147
|
+
if (
|
|
148
|
+
value === null ||
|
|
149
|
+
typeof value === 'boolean' ||
|
|
150
|
+
typeof value === 'string'
|
|
151
|
+
) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (typeof value === 'number') {
|
|
156
|
+
if (!Number.isFinite(value)) {
|
|
157
|
+
throw new ValidationError(
|
|
158
|
+
`Trail version marker schema at ${markerSchemaPath(path)} uses an unsupported non-finite JSON value`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!isPlainObject(value) && !Array.isArray(value)) {
|
|
165
|
+
throw new ValidationError(
|
|
166
|
+
`Trail version marker schema at ${markerSchemaPath(path)} uses an unsupported JSON-lossy value`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
throw new ValidationError(
|
|
171
|
+
`Trail version marker schema at ${markerSchemaPath(path)} uses an unsupported reference-valued literal or enum value`
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const assertMarkerLiteralSupported = (
|
|
176
|
+
def: Readonly<Record<string, unknown>>,
|
|
177
|
+
path: readonly string[]
|
|
178
|
+
): void => {
|
|
179
|
+
// The JSON-schema derivation only emits the first literal value, so a
|
|
180
|
+
// multi-value literal (z.literal(['a', 'b'])) would hash identically to a
|
|
181
|
+
// single-value literal and silently collide.
|
|
182
|
+
const { values } = def;
|
|
183
|
+
if (Array.isArray(values) && values.length > 1) {
|
|
184
|
+
throw new ValidationError(
|
|
185
|
+
`Trail version marker schema at ${markerSchemaPath(path)} uses an unsupported multi-value literal`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const [value] = Array.isArray(values) ? values : [];
|
|
189
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
190
|
+
throw new ValidationError(
|
|
191
|
+
`Trail version marker schema at ${markerSchemaPath(path)} uses an unsupported non-finite literal`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
assertMarkerJsonSafe(value, path);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const assertMarkerEnumSupported = (
|
|
198
|
+
def: Readonly<Record<string, unknown>>,
|
|
199
|
+
path: readonly string[]
|
|
200
|
+
): void => {
|
|
201
|
+
const { entries } = def;
|
|
202
|
+
if (!isPlainObject(entries)) {
|
|
203
|
+
throw new ValidationError(
|
|
204
|
+
`Trail version marker schema at ${markerSchemaPath(path)} has unsupported enum entries`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
208
|
+
assertMarkerJsonSafe(value, [...path, key]);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const nestedMarkerWrappedSchema = (
|
|
213
|
+
type: string,
|
|
214
|
+
def: Readonly<Record<string, unknown>>,
|
|
215
|
+
path: readonly string[],
|
|
216
|
+
options: { readonly optionalWrapperAllowed?: boolean }
|
|
217
|
+
): unknown => {
|
|
218
|
+
if (type === 'optional' && options.optionalWrapperAllowed !== true) {
|
|
219
|
+
throw new ValidationError(
|
|
220
|
+
`Trail version marker schema at ${markerSchemaPath(path)} uses an unsupported hidden optional wrapper`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
return nestedZodSchema(def['innerType'], path);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const assertMarkerSchemaSupported = (
|
|
227
|
+
schema: unknown,
|
|
228
|
+
path: readonly string[],
|
|
229
|
+
options: { readonly optionalWrapperAllowed?: boolean } = {}
|
|
230
|
+
): void => {
|
|
231
|
+
const def = zodDef(schema, path);
|
|
232
|
+
if (def['coerce'] === true) {
|
|
233
|
+
throw new ValidationError(
|
|
234
|
+
`Trail version marker schema at ${markerSchemaPath(path)} uses unsupported Zod coercion`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const [firstCheck] = zodChecks(def);
|
|
239
|
+
if (firstCheck !== undefined) {
|
|
240
|
+
throw new ValidationError(
|
|
241
|
+
`Trail version marker schema at ${markerSchemaPath(path)} uses unsupported Zod validation check "${zodCheckName(firstCheck)}"`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (hasMarkerSchemaOverride(schema)) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const directCheckName = zodDirectCheckName(def);
|
|
250
|
+
if (directCheckName !== undefined) {
|
|
251
|
+
throw new ValidationError(
|
|
252
|
+
`Trail version marker schema at ${markerSchemaPath(path)} uses unsupported Zod validation check "${directCheckName}"`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const type = zodType(def);
|
|
257
|
+
if (!supportedMarkerSchemaTypes.has(type)) {
|
|
258
|
+
throw new ValidationError(
|
|
259
|
+
`Trail version marker schema at ${markerSchemaPath(path)} uses unsupported Zod schema type "${type}"`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (type === 'literal') {
|
|
264
|
+
assertMarkerLiteralSupported(def, path);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (type === 'enum') {
|
|
269
|
+
assertMarkerEnumSupported(def, path);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (type === 'array') {
|
|
274
|
+
assertMarkerSchemaSupported(
|
|
275
|
+
nestedZodSchema(def['element'], [...path, '[]']),
|
|
276
|
+
[...path, '[]']
|
|
277
|
+
);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (type === 'object') {
|
|
282
|
+
if (def['catchall'] !== undefined) {
|
|
283
|
+
throw new ValidationError(
|
|
284
|
+
`Trail version marker schema at ${markerSchemaPath(path)} uses unsupported object catchall or unknown-key policy`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const { shape } = def;
|
|
289
|
+
if (shape === undefined) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (!isPlainObject(shape)) {
|
|
293
|
+
throw new ValidationError(
|
|
294
|
+
`Trail version marker schema at ${markerSchemaPath(path)} has an unsupported object shape`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const [key, value] of Object.entries(shape).toSorted(
|
|
299
|
+
([left], [right]) => left.localeCompare(right)
|
|
300
|
+
)) {
|
|
301
|
+
assertMarkerSchemaSupported(value, [...path, key], {
|
|
302
|
+
optionalWrapperAllowed: true,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (wrappedMarkerSchemaTypes.has(type)) {
|
|
309
|
+
assertMarkerSchemaSupported(
|
|
310
|
+
nestedMarkerWrappedSchema(type, def, path, options),
|
|
311
|
+
path
|
|
312
|
+
);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (type === 'union') {
|
|
317
|
+
const unionOptions = def['options'];
|
|
318
|
+
if (!Array.isArray(unionOptions)) {
|
|
319
|
+
throw new ValidationError(
|
|
320
|
+
`Trail version marker schema at ${markerSchemaPath(path)} has unsupported union options`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
for (const [index, option] of unionOptions.entries()) {
|
|
324
|
+
assertMarkerSchemaSupported(option, [...path, `option${index}`]);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const assertMarkerContentSupported = (
|
|
330
|
+
value: unknown,
|
|
331
|
+
path: readonly string[] = []
|
|
332
|
+
): void => {
|
|
333
|
+
if (Array.isArray(value)) {
|
|
334
|
+
for (const [index, entry] of value.entries()) {
|
|
335
|
+
assertMarkerContentSupported(entry, [...path, String(index)]);
|
|
336
|
+
}
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (value === null || typeof value !== 'object') {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const record = value as Record<string, unknown>;
|
|
345
|
+
const keys = Object.keys(record);
|
|
346
|
+
if (keys.length === 0 && path.at(-1) !== 'properties') {
|
|
347
|
+
throw new ValidationError(
|
|
348
|
+
`Trail version marker content at ${markerValuePath(path)} contains an unsupported empty schema derivation`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
for (const key of keys) {
|
|
353
|
+
assertMarkerContentSupported(record[key], [...path, key]);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const canonicalizeMarkerValue = (
|
|
358
|
+
value: unknown,
|
|
359
|
+
path: readonly string[],
|
|
360
|
+
seen: WeakSet<object>
|
|
361
|
+
): unknown => {
|
|
362
|
+
if (
|
|
363
|
+
value === null ||
|
|
364
|
+
typeof value === 'boolean' ||
|
|
365
|
+
typeof value === 'number' ||
|
|
366
|
+
typeof value === 'string'
|
|
367
|
+
) {
|
|
368
|
+
return value;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (Array.isArray(value)) {
|
|
372
|
+
return value.map((entry, index) =>
|
|
373
|
+
canonicalizeMarkerValue(entry, [...path, String(index)], seen)
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (value === undefined) {
|
|
378
|
+
throw new ValidationError(
|
|
379
|
+
`Trail version marker content cannot contain undefined at ${markerValuePath(path)}`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (typeof value === 'bigint' || typeof value === 'function') {
|
|
384
|
+
throw new ValidationError(
|
|
385
|
+
`Trail version marker content cannot contain ${typeof value} at ${markerValuePath(path)}`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (typeof value === 'symbol') {
|
|
390
|
+
throw new ValidationError(
|
|
391
|
+
`Trail version marker content cannot contain symbol at ${markerValuePath(path)}`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (!isPlainObject(value)) {
|
|
396
|
+
throw new ValidationError(
|
|
397
|
+
`Trail version marker content must be JSON-compatible at ${markerValuePath(path)}`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (seen.has(value)) {
|
|
402
|
+
throw new ValidationError(
|
|
403
|
+
`Trail version marker content cannot contain circular references at ${markerValuePath(path)}`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
seen.add(value);
|
|
407
|
+
|
|
408
|
+
const sorted: Record<string, unknown> = {};
|
|
409
|
+
for (const key of Object.keys(value).toSorted()) {
|
|
410
|
+
const next = value[key];
|
|
411
|
+
if (next !== undefined) {
|
|
412
|
+
sorted[key] = canonicalizeMarkerValue(next, [...path, key], seen);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
seen.delete(value);
|
|
417
|
+
return sorted;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
export const canonicalizeTrailVersionMarkerContent = (
|
|
421
|
+
content: unknown
|
|
422
|
+
): unknown => canonicalizeMarkerValue(content, [], new WeakSet<object>());
|
|
423
|
+
|
|
424
|
+
export const deriveTrailVersionMarker = (content: unknown): string => {
|
|
425
|
+
assertMarkerContentSupported(content);
|
|
426
|
+
const hasher = new Bun.CryptoHasher('sha256');
|
|
427
|
+
hasher.update(JSON.stringify(canonicalizeTrailVersionMarkerContent(content)));
|
|
428
|
+
return hasher.digest('hex').slice(0, TRAIL_VERSION_MARKER_LENGTH);
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const deriveSchema = (schema: unknown, path: readonly string[]): unknown => {
|
|
432
|
+
assertMarkerSchemaSupported(schema, path);
|
|
433
|
+
return canonicalizeTrailVersionMarkerContent(
|
|
434
|
+
zodToJsonSchema(schema as never)
|
|
435
|
+
);
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const deriveVersionDetours = (
|
|
439
|
+
entry: unknown
|
|
440
|
+
): readonly Record<string, unknown>[] | undefined => {
|
|
441
|
+
const raw = entry as unknown as Record<string, unknown>;
|
|
442
|
+
const { detours } = raw;
|
|
443
|
+
if (!Array.isArray(detours) || detours.length === 0) {
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return detours.map((detour) => {
|
|
448
|
+
const candidate = detour as {
|
|
449
|
+
readonly maxAttempts?: number | undefined;
|
|
450
|
+
readonly on?: { readonly name?: string | undefined } | undefined;
|
|
451
|
+
};
|
|
452
|
+
return {
|
|
453
|
+
maxAttempts: Math.max(
|
|
454
|
+
1,
|
|
455
|
+
Math.min(candidate.maxAttempts ?? 1, DETOUR_MAX_ATTEMPTS_CAP)
|
|
456
|
+
),
|
|
457
|
+
on: candidate.on?.name ?? 'Error',
|
|
458
|
+
};
|
|
459
|
+
});
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const deriveVersionRuntimeRefs = (
|
|
463
|
+
entry: unknown,
|
|
464
|
+
field: 'composes' | 'resources'
|
|
465
|
+
): readonly string[] | undefined => {
|
|
466
|
+
const raw = entry as unknown as Record<string, unknown>;
|
|
467
|
+
const values = raw[field];
|
|
468
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
469
|
+
return undefined;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const refs: string[] = [];
|
|
473
|
+
for (const value of values) {
|
|
474
|
+
if (typeof value === 'string') {
|
|
475
|
+
refs.push(value);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (
|
|
479
|
+
typeof value === 'object' &&
|
|
480
|
+
value !== null &&
|
|
481
|
+
typeof (value as { readonly id?: unknown }).id === 'string'
|
|
482
|
+
) {
|
|
483
|
+
refs.push((value as { readonly id: string }).id);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return refs.toSorted();
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
export const deriveCurrentTrailVersionMarkerContent = (
|
|
491
|
+
trail: Pick<
|
|
492
|
+
AnyTrail,
|
|
493
|
+
'composes' | 'detours' | 'input' | 'output' | 'resources'
|
|
494
|
+
>
|
|
495
|
+
): Readonly<Record<string, unknown>> => {
|
|
496
|
+
const content: Record<string, unknown> = {
|
|
497
|
+
input: deriveSchema(trail.input, ['input']),
|
|
498
|
+
kind: 'current',
|
|
499
|
+
...(trail.output === undefined
|
|
500
|
+
? {}
|
|
501
|
+
: { output: deriveSchema(trail.output, ['output']) }),
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const composes = deriveVersionRuntimeRefs(trail, 'composes');
|
|
505
|
+
const resources = deriveVersionRuntimeRefs(trail, 'resources');
|
|
506
|
+
const detours = deriveVersionDetours(trail);
|
|
507
|
+
if (composes !== undefined) {
|
|
508
|
+
content['composes'] = composes;
|
|
509
|
+
}
|
|
510
|
+
if (resources !== undefined) {
|
|
511
|
+
content['resources'] = resources;
|
|
512
|
+
}
|
|
513
|
+
if (detours !== undefined) {
|
|
514
|
+
content['detours'] = detours;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return content;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
export const deriveTrailVersionEntryMarkerContent = (
|
|
521
|
+
entry: TrailVersionEntry
|
|
522
|
+
): Readonly<Record<string, unknown>> => {
|
|
523
|
+
const kind = getTrailVersionEntryKind(entry);
|
|
524
|
+
const content: Record<string, unknown> = {
|
|
525
|
+
input: deriveSchema(entry.input, ['input']),
|
|
526
|
+
kind,
|
|
527
|
+
output: deriveSchema(entry.output, ['output']),
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
if (kind === 'revision' && entry.transpose !== undefined) {
|
|
531
|
+
content['transpose'] = { input: true, output: true };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (kind === 'fork') {
|
|
535
|
+
const composes = deriveVersionRuntimeRefs(entry, 'composes');
|
|
536
|
+
const resources = deriveVersionRuntimeRefs(entry, 'resources');
|
|
537
|
+
const detours = deriveVersionDetours(entry);
|
|
538
|
+
if (composes !== undefined) {
|
|
539
|
+
content['composes'] = composes;
|
|
540
|
+
}
|
|
541
|
+
if (resources !== undefined) {
|
|
542
|
+
content['resources'] = resources;
|
|
543
|
+
}
|
|
544
|
+
if (detours !== undefined) {
|
|
545
|
+
content['detours'] = detours;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return content;
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
export const deriveCurrentTrailVersionMarker = (
|
|
553
|
+
trail: Pick<
|
|
554
|
+
AnyTrail,
|
|
555
|
+
'composes' | 'detours' | 'input' | 'output' | 'resources'
|
|
556
|
+
>
|
|
557
|
+
): string =>
|
|
558
|
+
deriveTrailVersionMarker(deriveCurrentTrailVersionMarkerContent(trail));
|
|
559
|
+
|
|
560
|
+
export const deriveTrailVersionEntryMarker = (
|
|
561
|
+
entry: TrailVersionEntry
|
|
562
|
+
): string =>
|
|
563
|
+
deriveTrailVersionMarker(deriveTrailVersionEntryMarkerContent(entry));
|
|
564
|
+
|
|
565
|
+
export const assertTrailVersionMarker = (marker: string): void => {
|
|
566
|
+
if (!markerPattern.test(marker)) {
|
|
567
|
+
throw new ValidationError(
|
|
568
|
+
`Trail version marker must be a ${TRAIL_VERSION_MARKER_LENGTH}-character lowercase SHA-256 prefix`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
export const normalizeTrailVersionMarkerPrefix = (prefix: string): string => {
|
|
574
|
+
const normalized = prefix.toLowerCase();
|
|
575
|
+
if (
|
|
576
|
+
normalized.length < TRAIL_VERSION_MARKER_MIN_PREFIX_LENGTH ||
|
|
577
|
+
normalized.length > TRAIL_VERSION_MARKER_LENGTH ||
|
|
578
|
+
!markerPrefixPattern.test(normalized)
|
|
579
|
+
) {
|
|
580
|
+
throw new ValidationError(
|
|
581
|
+
`Trail version marker prefix must be ${TRAIL_VERSION_MARKER_MIN_PREFIX_LENGTH}-${TRAIL_VERSION_MARKER_LENGTH} lowercase hexadecimal characters`
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
return normalized;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
export const assertUniqueTrailVersionMarkers = (
|
|
588
|
+
trailId: string,
|
|
589
|
+
markers: readonly TrailVersionMarkerBinding[]
|
|
590
|
+
): void => {
|
|
591
|
+
const byMarker = new Map<string, number[]>();
|
|
592
|
+
for (const { marker, version } of markers) {
|
|
593
|
+
assertTrailVersionMarker(marker);
|
|
594
|
+
const versions = byMarker.get(marker) ?? [];
|
|
595
|
+
versions.push(version);
|
|
596
|
+
byMarker.set(marker, versions);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
for (const [marker, versions] of byMarker) {
|
|
600
|
+
if (versions.length > 1) {
|
|
601
|
+
throw new ValidationError(
|
|
602
|
+
`Trail "${trailId}" versions ${versions.join(', ')} derive the same marker ${marker}`
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
export const deriveTrailVersionMarkers = (
|
|
609
|
+
trail: Pick<
|
|
610
|
+
AnyTrail,
|
|
611
|
+
| 'composes'
|
|
612
|
+
| 'detours'
|
|
613
|
+
| 'id'
|
|
614
|
+
| 'input'
|
|
615
|
+
| 'output'
|
|
616
|
+
| 'resources'
|
|
617
|
+
| 'version'
|
|
618
|
+
| 'versions'
|
|
619
|
+
>
|
|
620
|
+
): readonly TrailVersionMarkerRecord[] => {
|
|
621
|
+
if (trail.version === undefined) {
|
|
622
|
+
return [];
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const records: TrailVersionMarkerRecord[] = [
|
|
626
|
+
{
|
|
627
|
+
current: true,
|
|
628
|
+
kind: 'current',
|
|
629
|
+
marker: deriveCurrentTrailVersionMarker(trail),
|
|
630
|
+
supported: true,
|
|
631
|
+
version: trail.version,
|
|
632
|
+
},
|
|
633
|
+
];
|
|
634
|
+
|
|
635
|
+
for (const [rawVersion, entry] of Object.entries(
|
|
636
|
+
trail.versions ?? {}
|
|
637
|
+
).toSorted(([left], [right]) => Number(left) - Number(right))) {
|
|
638
|
+
const kind = getTrailVersionEntryKind(entry);
|
|
639
|
+
records.push({
|
|
640
|
+
current: false,
|
|
641
|
+
kind,
|
|
642
|
+
marker: deriveTrailVersionEntryMarker(entry),
|
|
643
|
+
supported: !isArchivedTrailVersionEntry(entry),
|
|
644
|
+
version: Number(rawVersion),
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
assertUniqueTrailVersionMarkers(trail.id, records);
|
|
649
|
+
return Object.freeze(records);
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
export const deriveShortestUnambiguousTrailVersionMarkerPrefix = (
|
|
653
|
+
marker: string,
|
|
654
|
+
markers: readonly string[],
|
|
655
|
+
minLength = TRAIL_VERSION_MARKER_MIN_PREFIX_LENGTH
|
|
656
|
+
): string => {
|
|
657
|
+
assertTrailVersionMarker(marker);
|
|
658
|
+
const normalizedMarkers = markers.map((candidate) => {
|
|
659
|
+
assertTrailVersionMarker(candidate);
|
|
660
|
+
return candidate;
|
|
661
|
+
});
|
|
662
|
+
if (!normalizedMarkers.includes(marker)) {
|
|
663
|
+
throw new ValidationError(
|
|
664
|
+
`Trail version marker ${marker} is not in the provided marker set`
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
for (
|
|
669
|
+
let length = Math.max(minLength, TRAIL_VERSION_MARKER_MIN_PREFIX_LENGTH);
|
|
670
|
+
length <= TRAIL_VERSION_MARKER_LENGTH;
|
|
671
|
+
length += 1
|
|
672
|
+
) {
|
|
673
|
+
const prefix = marker.slice(0, length);
|
|
674
|
+
const matches = normalizedMarkers.filter((candidate) =>
|
|
675
|
+
candidate.startsWith(prefix)
|
|
676
|
+
);
|
|
677
|
+
if (matches.length === 1) {
|
|
678
|
+
return prefix;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
throw new AmbiguousError(
|
|
683
|
+
`Trail version marker ${marker} has no unambiguous display prefix`
|
|
684
|
+
);
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
export const resolveTrailVersionMarkerPrefix = (
|
|
688
|
+
markers: readonly TrailVersionMarkerBinding[],
|
|
689
|
+
prefix: string
|
|
690
|
+
): TrailVersionMarkerResolution => {
|
|
691
|
+
const normalized = normalizeTrailVersionMarkerPrefix(prefix);
|
|
692
|
+
const matches = markers.filter((candidate) =>
|
|
693
|
+
candidate.marker.startsWith(normalized)
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
if (matches.length === 0) {
|
|
697
|
+
throw new ValidationError(
|
|
698
|
+
`No trail version marker matches prefix ${normalized}`
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (matches.length > 1) {
|
|
703
|
+
throw new AmbiguousError(
|
|
704
|
+
`Trail version marker prefix ${normalized} is ambiguous across versions ${matches.map((candidate) => candidate.version).join(', ')}`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const [match] = matches;
|
|
709
|
+
if (match === undefined) {
|
|
710
|
+
throw new ValidationError(
|
|
711
|
+
`No trail version marker matches prefix ${normalized}`
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return { ...match, prefix: normalized };
|
|
716
|
+
};
|