@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,87 @@
1
+ import type { ErrorCategory } from './errors.js';
2
+ import { isTrailsError } from './errors.js';
3
+ import { createRedactor } from './redaction/index.js';
4
+
5
+ const errorRedactor = createRedactor();
6
+
7
+ export const INTERNAL_ERROR_PUBLIC_MESSAGE = 'Internal server error';
8
+
9
+ export interface ErrorDiagnosticsRendering {
10
+ readonly category?: ErrorCategory | undefined;
11
+ readonly context?: Record<string, unknown> | undefined;
12
+ readonly message: string;
13
+ readonly name: string;
14
+ readonly retryable?: boolean | undefined;
15
+ readonly stack?: string | undefined;
16
+ }
17
+
18
+ export interface PublicErrorRendering {
19
+ readonly category: ErrorCategory;
20
+ readonly message: string;
21
+ readonly name: string;
22
+ readonly retryable: boolean;
23
+ }
24
+
25
+ export const redactErrorString = (value: string): string =>
26
+ errorRedactor.redact(value);
27
+
28
+ export const redactErrorContext = (
29
+ context: Record<string, unknown> | undefined
30
+ ): Record<string, unknown> | undefined =>
31
+ context === undefined ? undefined : errorRedactor.redactObject(context);
32
+
33
+ export const redactErrorStack = (
34
+ stack: string | undefined
35
+ ): string | undefined =>
36
+ stack === undefined ? undefined : redactErrorString(stack);
37
+
38
+ export const renderErrorDiagnostics = (
39
+ error: Error
40
+ ): ErrorDiagnosticsRendering => {
41
+ const context = isTrailsError(error)
42
+ ? redactErrorContext(error.context)
43
+ : undefined;
44
+ const stack = redactErrorStack(error.stack);
45
+
46
+ return {
47
+ ...(isTrailsError(error)
48
+ ? {
49
+ category: error.category,
50
+ retryable: error.retryable,
51
+ }
52
+ : {}),
53
+ ...(context === undefined ? {} : { context }),
54
+ message: redactErrorString(error.message),
55
+ name: error.name || error.constructor.name || 'Error',
56
+ ...(stack === undefined ? {} : { stack }),
57
+ };
58
+ };
59
+
60
+ /**
61
+ * Render an error through the shared public redaction policy.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const rendering = renderPublicError(new NotFoundError('missing'));
66
+ * ```
67
+ */
68
+ export const renderPublicError = (error: Error): PublicErrorRendering => {
69
+ if (isTrailsError(error)) {
70
+ return {
71
+ category: error.category,
72
+ message:
73
+ error.category === 'internal'
74
+ ? INTERNAL_ERROR_PUBLIC_MESSAGE
75
+ : redactErrorString(error.message),
76
+ name: error.name,
77
+ retryable: error.retryable,
78
+ };
79
+ }
80
+
81
+ return {
82
+ category: 'internal',
83
+ message: INTERNAL_ERROR_PUBLIC_MESSAGE,
84
+ name: 'InternalError',
85
+ retryable: false,
86
+ };
87
+ };
package/src/errors.ts ADDED
@@ -0,0 +1,483 @@
1
+ /* oxlint-disable max-classes-per-file -- error taxonomy requires co-located class definitions */
2
+ /**
3
+ * Error taxonomy for @ontrails/core
4
+ *
5
+ * Provides a structured error hierarchy with category-based mapping
6
+ * to exit codes, HTTP status codes, and JSON-RPC error codes.
7
+ */
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Category
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export const errorCategories = [
14
+ 'validation',
15
+ 'not_found',
16
+ 'conflict',
17
+ 'permission',
18
+ 'timeout',
19
+ 'rate_limit',
20
+ 'network',
21
+ 'shift',
22
+ 'internal',
23
+ 'auth',
24
+ 'cancelled',
25
+ ] as const;
26
+
27
+ export type ErrorCategory = (typeof errorCategories)[number];
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Base class
31
+ // ---------------------------------------------------------------------------
32
+
33
+ export abstract class TrailsError extends Error {
34
+ abstract readonly category: ErrorCategory;
35
+ abstract readonly retryable: boolean;
36
+ readonly context?: Record<string, unknown> | undefined;
37
+
38
+ constructor(
39
+ message: string,
40
+ options?: { cause?: Error; context?: Record<string, unknown> }
41
+ ) {
42
+ super(message, { cause: options?.cause });
43
+ this.name = this.constructor.name;
44
+ this.context = options?.context;
45
+ }
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Concrete error classes
50
+ // ---------------------------------------------------------------------------
51
+
52
+ export class ValidationError extends TrailsError {
53
+ readonly category = 'validation' as const;
54
+ readonly retryable = false as const;
55
+ }
56
+
57
+ export class AmbiguousError extends TrailsError {
58
+ readonly category = 'validation' as const;
59
+ readonly retryable = false as const;
60
+ }
61
+
62
+ export class AssertionError extends TrailsError {
63
+ readonly category = 'internal' as const;
64
+ readonly retryable = false as const;
65
+ }
66
+
67
+ export class NotFoundError extends TrailsError {
68
+ readonly category = 'not_found' as const;
69
+ readonly retryable = false as const;
70
+ }
71
+
72
+ export class VersionNotSupportedError extends NotFoundError {
73
+ readonly reason?: string | undefined;
74
+ readonly requested?: number | string | undefined;
75
+ readonly supported?: readonly number[] | undefined;
76
+ readonly trailId?: string | undefined;
77
+
78
+ constructor(
79
+ message: string,
80
+ options?: { cause?: Error; context?: Record<string, unknown> }
81
+ );
82
+ constructor(
83
+ trailId: string,
84
+ requested: number | string,
85
+ supported: readonly number[],
86
+ reason?: string | undefined
87
+ );
88
+ constructor(
89
+ messageOrTrailId: string,
90
+ optionsOrRequested?:
91
+ | { cause?: Error; context?: Record<string, unknown> }
92
+ | number
93
+ | string,
94
+ supported?: readonly number[],
95
+ reason?: string | undefined
96
+ ) {
97
+ if (supported === undefined) {
98
+ super(
99
+ messageOrTrailId,
100
+ optionsOrRequested as
101
+ | { cause?: Error; context?: Record<string, unknown> }
102
+ | undefined
103
+ );
104
+ this.name = 'VersionNotSupportedError';
105
+ return;
106
+ }
107
+
108
+ const requested = optionsOrRequested as number | string;
109
+ const supportedLabel =
110
+ supported.length === 0 ? 'none' : supported.join(', ');
111
+ super(
112
+ `Trail "${messageOrTrailId}" version ${String(requested)} is not supported (supported: ${supportedLabel})`,
113
+ {
114
+ context: {
115
+ ...(reason === undefined ? {} : { reason }),
116
+ requested,
117
+ supported,
118
+ trailId: messageOrTrailId,
119
+ },
120
+ }
121
+ );
122
+ this.name = 'VersionNotSupportedError';
123
+ this.reason = reason;
124
+ this.requested = requested;
125
+ this.supported = supported;
126
+ this.trailId = messageOrTrailId;
127
+ }
128
+ }
129
+
130
+ export class AlreadyExistsError extends TrailsError {
131
+ readonly category = 'conflict' as const;
132
+ readonly retryable = false as const;
133
+ }
134
+
135
+ export class ConflictError extends TrailsError {
136
+ readonly category = 'conflict' as const;
137
+ readonly retryable = false as const;
138
+ }
139
+
140
+ export class PermissionError extends TrailsError {
141
+ readonly category = 'permission' as const;
142
+ readonly retryable = false as const;
143
+ }
144
+
145
+ export class PermitError extends PermissionError {
146
+ constructor(
147
+ message: string,
148
+ options?: { cause?: Error; context?: Record<string, unknown> }
149
+ ) {
150
+ super(message, options);
151
+ this.name = 'PermitError';
152
+ }
153
+ }
154
+
155
+ export class TimeoutError extends TrailsError {
156
+ readonly category = 'timeout' as const;
157
+ readonly retryable = true as const;
158
+ }
159
+
160
+ export class RateLimitError extends TrailsError {
161
+ readonly category = 'rate_limit' as const;
162
+ readonly retryable = true as const;
163
+ readonly retryAfter?: number | undefined;
164
+
165
+ constructor(
166
+ message: string,
167
+ options?: {
168
+ cause?: Error;
169
+ context?: Record<string, unknown>;
170
+ retryAfter?: number;
171
+ }
172
+ ) {
173
+ super(message, options);
174
+ this.retryAfter = options?.retryAfter;
175
+ }
176
+ }
177
+
178
+ export class NetworkError extends TrailsError {
179
+ readonly category = 'network' as const;
180
+ readonly retryable = true as const;
181
+ }
182
+
183
+ /**
184
+ * @example
185
+ * ```ts
186
+ * return Result.err(new WorkspaceShiftError('workspace changed during check'));
187
+ * ```
188
+ *
189
+ * Raised when the observed workspace substrate moves during one run.
190
+ *
191
+ * A shift voids the run's verdict, including passes. Callers can retry on
192
+ * stable ground without changing their request.
193
+ */
194
+ export class WorkspaceShiftError extends TrailsError {
195
+ readonly category = 'shift' as const;
196
+ readonly retryable = true as const;
197
+ }
198
+
199
+ export class InternalError extends TrailsError {
200
+ readonly category: ErrorCategory = 'internal';
201
+ readonly retryable = false as const;
202
+ }
203
+
204
+ export class DerivationError extends TrailsError {
205
+ readonly category = 'internal' as const;
206
+ readonly retryable = false as const;
207
+ }
208
+
209
+ export class RecoverableCompletionError extends InternalError {}
210
+
211
+ export class AuthError extends TrailsError {
212
+ readonly category = 'auth' as const;
213
+ readonly retryable = false as const;
214
+ }
215
+
216
+ export class CancelledError extends TrailsError {
217
+ readonly category = 'cancelled' as const;
218
+ readonly retryable = false as const;
219
+ }
220
+
221
+ /**
222
+ * Returned when a detour exhausts all recovery attempts.
223
+ *
224
+ * Inherits the wrapped error's category for surface mapping (e.g. a
225
+ * `RetryExhaustedError<ConflictError>` maps to HTTP 409), but always
226
+ * sets `retryable = false` to prevent amplification across `ctx.compose()`
227
+ * boundaries or stacked layers.
228
+ */
229
+ export class RetryExhaustedError<
230
+ TErr extends TrailsError = TrailsError,
231
+ > extends InternalError {
232
+ readonly category: ErrorCategory;
233
+ readonly cause: TErr;
234
+
235
+ /** Number of recovery attempts made before exhaustion. */
236
+ readonly attempts: number;
237
+
238
+ /** Name of the detour whose recovery was exhausted. */
239
+ readonly detour: string;
240
+
241
+ constructor(
242
+ wrapped: TErr,
243
+ metadata: { readonly attempts: number; readonly detour: string }
244
+ ) {
245
+ super(
246
+ `Recovery exhausted after ${metadata.attempts} attempts: ${wrapped.message}`,
247
+ { cause: wrapped }
248
+ );
249
+ this.cause = wrapped;
250
+ this.attempts = metadata.attempts;
251
+ this.detour = metadata.detour;
252
+ // Dynamic — inherited from wrapped error at construction time.
253
+ this.category = wrapped.category;
254
+ }
255
+ }
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // Class registry
259
+ // ---------------------------------------------------------------------------
260
+
261
+ export type ErrorClassConstructor = new (...args: never[]) => TrailsError;
262
+
263
+ export interface FixedErrorClassRegistryEntry {
264
+ readonly category: ErrorCategory;
265
+ readonly ctor: ErrorClassConstructor;
266
+ readonly name: string;
267
+ readonly retryable: boolean;
268
+ }
269
+
270
+ export interface DynamicErrorClassRegistryEntry {
271
+ readonly category: 'dynamic';
272
+ readonly ctor: ErrorClassConstructor;
273
+ readonly inheritsCategoryFrom: 'wrapped-error';
274
+ readonly name: string;
275
+ readonly retryable: false;
276
+ }
277
+
278
+ export type ErrorClassRegistryEntry =
279
+ | DynamicErrorClassRegistryEntry
280
+ | FixedErrorClassRegistryEntry;
281
+
282
+ /**
283
+ * Authored registry of concrete TrailsError classes.
284
+ *
285
+ * JavaScript cannot enumerate subclasses at runtime, so rule and derivation
286
+ * tooling should walk this owner-held list instead of hardcoding parallel
287
+ * class-name tables. `RetryExhaustedError` is marked dynamic because it
288
+ * inherits its runtime category from the wrapped error rather than always
289
+ * mapping as `internal`.
290
+ */
291
+ export const errorClasses = [
292
+ {
293
+ category: 'validation',
294
+ ctor: ValidationError,
295
+ name: 'ValidationError',
296
+ retryable: false,
297
+ },
298
+ {
299
+ category: 'validation',
300
+ ctor: AmbiguousError,
301
+ name: 'AmbiguousError',
302
+ retryable: false,
303
+ },
304
+ {
305
+ category: 'internal',
306
+ ctor: AssertionError,
307
+ name: 'AssertionError',
308
+ retryable: false,
309
+ },
310
+ {
311
+ category: 'not_found',
312
+ ctor: NotFoundError,
313
+ name: 'NotFoundError',
314
+ retryable: false,
315
+ },
316
+ {
317
+ category: 'not_found',
318
+ ctor: VersionNotSupportedError,
319
+ name: 'VersionNotSupportedError',
320
+ retryable: false,
321
+ },
322
+ {
323
+ category: 'conflict',
324
+ ctor: AlreadyExistsError,
325
+ name: 'AlreadyExistsError',
326
+ retryable: false,
327
+ },
328
+ {
329
+ category: 'conflict',
330
+ ctor: ConflictError,
331
+ name: 'ConflictError',
332
+ retryable: false,
333
+ },
334
+ {
335
+ category: 'permission',
336
+ ctor: PermissionError,
337
+ name: 'PermissionError',
338
+ retryable: false,
339
+ },
340
+ {
341
+ category: 'permission',
342
+ ctor: PermitError,
343
+ name: 'PermitError',
344
+ retryable: false,
345
+ },
346
+ {
347
+ category: 'timeout',
348
+ ctor: TimeoutError,
349
+ name: 'TimeoutError',
350
+ retryable: true,
351
+ },
352
+ {
353
+ category: 'rate_limit',
354
+ ctor: RateLimitError,
355
+ name: 'RateLimitError',
356
+ retryable: true,
357
+ },
358
+ {
359
+ category: 'network',
360
+ ctor: NetworkError,
361
+ name: 'NetworkError',
362
+ retryable: true,
363
+ },
364
+ {
365
+ category: 'shift',
366
+ ctor: WorkspaceShiftError,
367
+ name: 'WorkspaceShiftError',
368
+ retryable: true,
369
+ },
370
+ {
371
+ category: 'internal',
372
+ ctor: InternalError,
373
+ name: 'InternalError',
374
+ retryable: false,
375
+ },
376
+ {
377
+ category: 'internal',
378
+ ctor: DerivationError,
379
+ name: 'DerivationError',
380
+ retryable: false,
381
+ },
382
+ {
383
+ category: 'internal',
384
+ ctor: RecoverableCompletionError,
385
+ name: 'RecoverableCompletionError',
386
+ retryable: false,
387
+ },
388
+ { category: 'auth', ctor: AuthError, name: 'AuthError', retryable: false },
389
+ {
390
+ category: 'cancelled',
391
+ ctor: CancelledError,
392
+ name: 'CancelledError',
393
+ retryable: false,
394
+ },
395
+ {
396
+ category: 'dynamic',
397
+ ctor: RetryExhaustedError,
398
+ inheritsCategoryFrom: 'wrapped-error',
399
+ name: 'RetryExhaustedError',
400
+ retryable: false,
401
+ },
402
+ ] as const satisfies readonly ErrorClassRegistryEntry[];
403
+
404
+ // ---------------------------------------------------------------------------
405
+ // Taxonomy maps
406
+ // ---------------------------------------------------------------------------
407
+
408
+ export interface ErrorCategoryCodes {
409
+ readonly exit: number;
410
+ readonly http: number;
411
+ readonly jsonRpc: number;
412
+ }
413
+
414
+ export const codesByCategory = {
415
+ auth: { exit: 9, http: 401, jsonRpc: -32_600 },
416
+ cancelled: { exit: 130, http: 499, jsonRpc: -32_603 },
417
+ conflict: { exit: 3, http: 409, jsonRpc: -32_603 },
418
+ internal: { exit: 8, http: 500, jsonRpc: -32_603 },
419
+ network: { exit: 7, http: 502, jsonRpc: -32_603 },
420
+ not_found: { exit: 2, http: 404, jsonRpc: -32_601 },
421
+ permission: { exit: 4, http: 403, jsonRpc: -32_600 },
422
+ rate_limit: { exit: 6, http: 429, jsonRpc: -32_603 },
423
+ shift: { exit: 10, http: 503, jsonRpc: -32_603 },
424
+ timeout: { exit: 5, http: 504, jsonRpc: -32_603 },
425
+ validation: { exit: 1, http: 400, jsonRpc: -32_602 },
426
+ } as const satisfies Record<ErrorCategory, ErrorCategoryCodes>;
427
+
428
+ const deriveCodeMap = <TCode extends keyof ErrorCategoryCodes>(
429
+ code: TCode
430
+ ): {
431
+ readonly [TCategory in ErrorCategory]: (typeof codesByCategory)[TCategory][TCode];
432
+ } => ({
433
+ auth: codesByCategory.auth[code],
434
+ cancelled: codesByCategory.cancelled[code],
435
+ conflict: codesByCategory.conflict[code],
436
+ internal: codesByCategory.internal[code],
437
+ network: codesByCategory.network[code],
438
+ not_found: codesByCategory.not_found[code],
439
+ permission: codesByCategory.permission[code],
440
+ rate_limit: codesByCategory.rate_limit[code],
441
+ shift: codesByCategory.shift[code],
442
+ timeout: codesByCategory.timeout[code],
443
+ validation: codesByCategory.validation[code],
444
+ });
445
+
446
+ /** @deprecated Prefer `codesByCategory[category].exit`. */
447
+ export const exitCodeMap = deriveCodeMap('exit');
448
+
449
+ /** @deprecated Prefer `codesByCategory[category].http`. */
450
+ export const statusCodeMap = deriveCodeMap('http');
451
+
452
+ /** @deprecated Prefer `codesByCategory[category].jsonRpc`. */
453
+ export const jsonRpcCodeMap = deriveCodeMap('jsonRpc');
454
+
455
+ export const retryableMap: Record<ErrorCategory, boolean> = {
456
+ auth: false,
457
+ cancelled: false,
458
+ conflict: false,
459
+ internal: false,
460
+ network: true,
461
+ not_found: false,
462
+ permission: false,
463
+ rate_limit: true,
464
+ shift: true,
465
+ timeout: true,
466
+ validation: false,
467
+ } as const;
468
+
469
+ // ---------------------------------------------------------------------------
470
+ // Helper functions
471
+ // ---------------------------------------------------------------------------
472
+
473
+ /** Type guard: narrows unknown to TrailsError */
474
+ export const isTrailsError = (error?: unknown): error is TrailsError =>
475
+ error instanceof TrailsError;
476
+
477
+ /** Returns true if the error is retryable (TrailsError with retryable category). */
478
+ export const isRetryable = (error: Error): boolean => {
479
+ if (isTrailsError(error)) {
480
+ return error.retryable;
481
+ }
482
+ return false;
483
+ };