@ontrails/core 1.0.0-beta.3 → 1.0.0-beta.32

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 (249) hide show
  1. package/CHANGELOG.md +323 -8
  2. package/README.md +114 -33
  3. package/package.json +11 -1
  4. package/src/activation-provenance.ts +111 -0
  5. package/src/activation-source-compatibility.ts +430 -0
  6. package/src/activation-source-projection.ts +221 -0
  7. package/src/activation-source.ts +91 -0
  8. package/src/blob-ref.ts +51 -0
  9. package/src/branded.ts +1 -1
  10. package/src/compose-batch.ts +69 -0
  11. package/src/compose-schema.ts +36 -0
  12. package/src/context.ts +44 -10
  13. package/src/contour.ts +346 -0
  14. package/src/derive.ts +250 -19
  15. package/src/detours.ts +8 -0
  16. package/src/diagnostics.ts +21 -0
  17. package/src/draft.ts +350 -0
  18. package/src/error-projection.ts +51 -0
  19. package/src/errors.ts +335 -48
  20. package/src/execute.ts +1577 -0
  21. package/src/fire.ts +1169 -0
  22. package/src/glob.ts +81 -0
  23. package/src/index.ts +526 -35
  24. package/src/internal/fork-ctx.ts +69 -0
  25. package/src/layer-projection.ts +193 -0
  26. package/src/layer.ts +43 -6
  27. package/src/observe.ts +361 -0
  28. package/src/path-scope.ts +66 -0
  29. package/src/path-security.ts +1 -1
  30. package/src/permits.ts +12 -0
  31. package/src/redaction/patterns.ts +6 -3
  32. package/src/resilience.ts +11 -11
  33. package/src/resource-config.ts +792 -0
  34. package/src/resource.ts +194 -0
  35. package/src/result.ts +18 -0
  36. package/src/run.ts +76 -0
  37. package/src/schedule-runtime.ts +689 -0
  38. package/src/schedule.ts +326 -0
  39. package/src/serialization.ts +92 -55
  40. package/src/signal-diagnostics.ts +633 -0
  41. package/src/signal-ref.ts +87 -0
  42. package/src/signal.ts +104 -0
  43. package/src/store/accessor-protocol.ts +56 -0
  44. package/src/store/index.ts +4 -0
  45. package/src/structured-examples.ts +248 -0
  46. package/src/surface-derivation.ts +91 -0
  47. package/src/surface-filter.ts +101 -0
  48. package/src/surface-versioning.ts +42 -0
  49. package/src/topo.ts +769 -67
  50. package/src/tracing.ts +345 -0
  51. package/src/trail-id-glob.ts +15 -0
  52. package/src/trail.ts +1168 -30
  53. package/src/trails/derive-trail.ts +842 -0
  54. package/src/trails/index.ts +9 -0
  55. package/src/trails/ingest.ts +139 -0
  56. package/src/trails-db.ts +206 -0
  57. package/src/transport-error-map.ts +174 -0
  58. package/src/type-utils.ts +87 -0
  59. package/src/types.ts +254 -12
  60. package/src/validate-established-topo.ts +69 -0
  61. package/src/validate-topo.ts +414 -56
  62. package/src/validation.ts +152 -4
  63. package/src/version-marker.ts +716 -0
  64. package/src/version-resolution.ts +308 -0
  65. package/src/version-runtime.ts +120 -0
  66. package/src/webhook.ts +278 -0
  67. package/src/workspace.ts +162 -1
  68. package/src/zod-wrappers.ts +72 -0
  69. package/.turbo/turbo-build.log +0 -1
  70. package/.turbo/turbo-lint.log +0 -3
  71. package/.turbo/turbo-typecheck.log +0 -1
  72. package/dist/adapters.d.ts +0 -39
  73. package/dist/adapters.d.ts.map +0 -1
  74. package/dist/adapters.js +0 -2
  75. package/dist/adapters.js.map +0 -1
  76. package/dist/blob-ref.d.ts +0 -20
  77. package/dist/blob-ref.d.ts.map +0 -1
  78. package/dist/blob-ref.js +0 -22
  79. package/dist/blob-ref.js.map +0 -1
  80. package/dist/branded.d.ts +0 -36
  81. package/dist/branded.d.ts.map +0 -1
  82. package/dist/branded.js +0 -89
  83. package/dist/branded.js.map +0 -1
  84. package/dist/collections.d.ts +0 -31
  85. package/dist/collections.d.ts.map +0 -1
  86. package/dist/collections.js +0 -60
  87. package/dist/collections.js.map +0 -1
  88. package/dist/context.d.ts +0 -10
  89. package/dist/context.d.ts.map +0 -1
  90. package/dist/context.js +0 -15
  91. package/dist/context.js.map +0 -1
  92. package/dist/derive.d.ts +0 -33
  93. package/dist/derive.d.ts.map +0 -1
  94. package/dist/derive.js +0 -125
  95. package/dist/derive.js.map +0 -1
  96. package/dist/errors.d.ts +0 -83
  97. package/dist/errors.d.ts.map +0 -1
  98. package/dist/errors.js +0 -142
  99. package/dist/errors.js.map +0 -1
  100. package/dist/event.d.ts +0 -45
  101. package/dist/event.d.ts.map +0 -1
  102. package/dist/event.js +0 -17
  103. package/dist/event.js.map +0 -1
  104. package/dist/fetch.d.ts +0 -15
  105. package/dist/fetch.d.ts.map +0 -1
  106. package/dist/fetch.js +0 -102
  107. package/dist/fetch.js.map +0 -1
  108. package/dist/guards.d.ts +0 -17
  109. package/dist/guards.d.ts.map +0 -1
  110. package/dist/guards.js +0 -25
  111. package/dist/guards.js.map +0 -1
  112. package/dist/health.d.ts +0 -18
  113. package/dist/health.d.ts.map +0 -1
  114. package/dist/health.js +0 -5
  115. package/dist/health.js.map +0 -1
  116. package/dist/hike.d.ts +0 -36
  117. package/dist/hike.d.ts.map +0 -1
  118. package/dist/hike.js +0 -20
  119. package/dist/hike.js.map +0 -1
  120. package/dist/index.d.ts +0 -36
  121. package/dist/index.d.ts.map +0 -1
  122. package/dist/index.js +0 -40
  123. package/dist/index.js.map +0 -1
  124. package/dist/job.d.ts +0 -24
  125. package/dist/job.d.ts.map +0 -1
  126. package/dist/job.js +0 -17
  127. package/dist/job.js.map +0 -1
  128. package/dist/layer.d.ts +0 -17
  129. package/dist/layer.d.ts.map +0 -1
  130. package/dist/layer.js +0 -21
  131. package/dist/layer.js.map +0 -1
  132. package/dist/path-security.d.ts +0 -28
  133. package/dist/path-security.d.ts.map +0 -1
  134. package/dist/path-security.js +0 -63
  135. package/dist/path-security.js.map +0 -1
  136. package/dist/patterns/bulk.d.ts +0 -15
  137. package/dist/patterns/bulk.d.ts.map +0 -1
  138. package/dist/patterns/bulk.js +0 -14
  139. package/dist/patterns/bulk.js.map +0 -1
  140. package/dist/patterns/change.d.ts +0 -10
  141. package/dist/patterns/change.d.ts.map +0 -1
  142. package/dist/patterns/change.js +0 -10
  143. package/dist/patterns/change.js.map +0 -1
  144. package/dist/patterns/date-range.d.ts +0 -10
  145. package/dist/patterns/date-range.d.ts.map +0 -1
  146. package/dist/patterns/date-range.js +0 -10
  147. package/dist/patterns/date-range.js.map +0 -1
  148. package/dist/patterns/index.d.ts +0 -9
  149. package/dist/patterns/index.d.ts.map +0 -1
  150. package/dist/patterns/index.js +0 -9
  151. package/dist/patterns/index.js.map +0 -1
  152. package/dist/patterns/pagination.d.ts +0 -18
  153. package/dist/patterns/pagination.d.ts.map +0 -1
  154. package/dist/patterns/pagination.js +0 -18
  155. package/dist/patterns/pagination.js.map +0 -1
  156. package/dist/patterns/progress.d.ts +0 -11
  157. package/dist/patterns/progress.d.ts.map +0 -1
  158. package/dist/patterns/progress.js +0 -11
  159. package/dist/patterns/progress.js.map +0 -1
  160. package/dist/patterns/sorting.d.ts +0 -13
  161. package/dist/patterns/sorting.d.ts.map +0 -1
  162. package/dist/patterns/sorting.js +0 -10
  163. package/dist/patterns/sorting.js.map +0 -1
  164. package/dist/patterns/status.d.ts +0 -15
  165. package/dist/patterns/status.d.ts.map +0 -1
  166. package/dist/patterns/status.js +0 -9
  167. package/dist/patterns/status.js.map +0 -1
  168. package/dist/patterns/timestamps.d.ts +0 -10
  169. package/dist/patterns/timestamps.d.ts.map +0 -1
  170. package/dist/patterns/timestamps.js +0 -10
  171. package/dist/patterns/timestamps.js.map +0 -1
  172. package/dist/redaction/index.d.ts +0 -4
  173. package/dist/redaction/index.d.ts.map +0 -1
  174. package/dist/redaction/index.js +0 -3
  175. package/dist/redaction/index.js.map +0 -1
  176. package/dist/redaction/patterns.d.ts +0 -9
  177. package/dist/redaction/patterns.d.ts.map +0 -1
  178. package/dist/redaction/patterns.js +0 -39
  179. package/dist/redaction/patterns.js.map +0 -1
  180. package/dist/redaction/redactor.d.ts +0 -27
  181. package/dist/redaction/redactor.d.ts.map +0 -1
  182. package/dist/redaction/redactor.js +0 -89
  183. package/dist/redaction/redactor.js.map +0 -1
  184. package/dist/resilience.d.ts +0 -34
  185. package/dist/resilience.d.ts.map +0 -1
  186. package/dist/resilience.js +0 -164
  187. package/dist/resilience.js.map +0 -1
  188. package/dist/result.d.ts +0 -57
  189. package/dist/result.d.ts.map +0 -1
  190. package/dist/result.js +0 -156
  191. package/dist/result.js.map +0 -1
  192. package/dist/serialization.d.ts +0 -27
  193. package/dist/serialization.d.ts.map +0 -1
  194. package/dist/serialization.js +0 -153
  195. package/dist/serialization.js.map +0 -1
  196. package/dist/topo.d.ts +0 -18
  197. package/dist/topo.d.ts.map +0 -1
  198. package/dist/topo.js +0 -80
  199. package/dist/topo.js.map +0 -1
  200. package/dist/trail.d.ts +0 -83
  201. package/dist/trail.d.ts.map +0 -1
  202. package/dist/trail.js +0 -16
  203. package/dist/trail.js.map +0 -1
  204. package/dist/types.d.ts +0 -46
  205. package/dist/types.d.ts.map +0 -1
  206. package/dist/types.js +0 -2
  207. package/dist/types.js.map +0 -1
  208. package/dist/validate-topo.d.ts +0 -24
  209. package/dist/validate-topo.d.ts.map +0 -1
  210. package/dist/validate-topo.js +0 -156
  211. package/dist/validate-topo.js.map +0 -1
  212. package/dist/validation.d.ts +0 -27
  213. package/dist/validation.d.ts.map +0 -1
  214. package/dist/validation.js +0 -134
  215. package/dist/validation.js.map +0 -1
  216. package/dist/workspace.d.ts +0 -25
  217. package/dist/workspace.d.ts.map +0 -1
  218. package/dist/workspace.js +0 -57
  219. package/dist/workspace.js.map +0 -1
  220. package/src/__tests__/blob-ref.test.ts +0 -103
  221. package/src/__tests__/branded.test.ts +0 -148
  222. package/src/__tests__/collections.test.ts +0 -126
  223. package/src/__tests__/context.test.ts +0 -66
  224. package/src/__tests__/derive.test.ts +0 -203
  225. package/src/__tests__/errors.test.ts +0 -309
  226. package/src/__tests__/event.test.ts +0 -82
  227. package/src/__tests__/fetch.test.ts +0 -217
  228. package/src/__tests__/guards.test.ts +0 -102
  229. package/src/__tests__/hike.test.ts +0 -117
  230. package/src/__tests__/job.test.ts +0 -98
  231. package/src/__tests__/layer.test.ts +0 -224
  232. package/src/__tests__/path-security.test.ts +0 -114
  233. package/src/__tests__/patterns.test.ts +0 -273
  234. package/src/__tests__/redaction.test.ts +0 -244
  235. package/src/__tests__/resilience.test.ts +0 -246
  236. package/src/__tests__/result.test.ts +0 -155
  237. package/src/__tests__/serialization.test.ts +0 -401
  238. package/src/__tests__/topo.test.ts +0 -220
  239. package/src/__tests__/trail.test.ts +0 -179
  240. package/src/__tests__/validate-topo.test.ts +0 -280
  241. package/src/__tests__/validation.test.ts +0 -283
  242. package/src/__tests__/workspace.test.ts +0 -183
  243. package/src/adapters.ts +0 -68
  244. package/src/event.ts +0 -77
  245. package/src/health.ts +0 -23
  246. package/src/hike.ts +0 -77
  247. package/src/job.ts +0 -20
  248. package/tsconfig.json +0 -9
  249. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,326 @@
1
+ import { ValidationError } from './errors.js';
2
+ import type {
3
+ ActivationSource,
4
+ ActivationSourceMeta,
5
+ } from './activation-source.js';
6
+
7
+ type ScheduleInputDefault = Record<string, never>;
8
+
9
+ export interface ScheduleSpec<TInput = unknown> {
10
+ readonly cron: string;
11
+ readonly input?: TInput | undefined;
12
+ readonly meta?: ActivationSourceMeta | undefined;
13
+ readonly timezone?: string | undefined;
14
+ /** Reserved for future schedule-specific design; trail versioning is trail-only. */
15
+ readonly version?: never;
16
+ }
17
+
18
+ export interface ScheduleSource<TInput = unknown> extends ActivationSource {
19
+ readonly cron: string;
20
+ readonly input: TInput;
21
+ readonly kind: 'schedule';
22
+ readonly meta?: ActivationSourceMeta | undefined;
23
+ readonly timezone?: string | undefined;
24
+ }
25
+
26
+ export interface ScheduleValidationIssue {
27
+ readonly field: 'cron' | 'input' | 'timezone';
28
+ readonly message: string;
29
+ }
30
+
31
+ const CRON_FIELD_BOUNDS = Object.freeze([
32
+ { max: 59, min: 0, name: 'minute' },
33
+ { max: 23, min: 0, name: 'hour' },
34
+ { max: 31, min: 1, name: 'day of month' },
35
+ { max: 12, min: 1, name: 'month' },
36
+ { max: 7, min: 0, name: 'day of week' },
37
+ ] as const);
38
+
39
+ const EMPTY_INPUT = Object.freeze({}) as ScheduleInputDefault;
40
+
41
+ const normalizeCron = (cron: string): string =>
42
+ cron.trim().replaceAll(/\s+/g, ' ');
43
+
44
+ const isIntegerToken = (value: string): boolean => /^\d+$/.test(value);
45
+
46
+ const parseIntegerToken = (value: string): number | undefined =>
47
+ isIntegerToken(value) ? Number.parseInt(value, 10) : undefined;
48
+
49
+ const isNumberInRange = (
50
+ value: string,
51
+ bounds: (typeof CRON_FIELD_BOUNDS)[number]
52
+ ): boolean => {
53
+ const parsed = parseIntegerToken(value);
54
+ return parsed !== undefined && parsed >= bounds.min && parsed <= bounds.max;
55
+ };
56
+
57
+ const isCronAtomValid = (
58
+ atom: string,
59
+ bounds: (typeof CRON_FIELD_BOUNDS)[number]
60
+ ): boolean => {
61
+ if (atom === '*') {
62
+ return true;
63
+ }
64
+ if (isNumberInRange(atom, bounds)) {
65
+ return true;
66
+ }
67
+
68
+ const range = atom.split('-');
69
+ if (range.length !== 2) {
70
+ return false;
71
+ }
72
+ const [rangeStart, rangeEnd] = range;
73
+ if (rangeStart === undefined || rangeEnd === undefined) {
74
+ return false;
75
+ }
76
+ if (
77
+ !isNumberInRange(rangeStart, bounds) ||
78
+ !isNumberInRange(rangeEnd, bounds)
79
+ ) {
80
+ return false;
81
+ }
82
+ const start = Number.parseInt(rangeStart, 10);
83
+ const end = Number.parseInt(rangeEnd, 10);
84
+ return start <= end;
85
+ };
86
+
87
+ const isCronPartValid = (
88
+ part: string,
89
+ bounds: (typeof CRON_FIELD_BOUNDS)[number]
90
+ ): boolean => {
91
+ const stepped = part.split('/');
92
+ if (stepped.length > 2) {
93
+ return false;
94
+ }
95
+ const [atom, step] = stepped;
96
+ if (!atom || !isCronAtomValid(atom, bounds)) {
97
+ return false;
98
+ }
99
+ if (step === undefined) {
100
+ return true;
101
+ }
102
+ const parsedStep = parseIntegerToken(step);
103
+ return parsedStep !== undefined && parsedStep > 0;
104
+ };
105
+
106
+ const validateCronField = (
107
+ field: string,
108
+ bounds: (typeof CRON_FIELD_BOUNDS)[number]
109
+ ): string | undefined => {
110
+ if (field.length === 0) {
111
+ return `${bounds.name} field is empty`;
112
+ }
113
+ const parts = field.split(',');
114
+ return parts.every((part) => isCronPartValid(part, bounds))
115
+ ? undefined
116
+ : `${bounds.name} field is not a supported cron expression`;
117
+ };
118
+
119
+ const validateCron = (cron: unknown): ScheduleValidationIssue[] => {
120
+ if (typeof cron !== 'string' || cron.trim().length === 0) {
121
+ return [
122
+ { field: 'cron', message: 'Cron expression must be a non-empty string' },
123
+ ];
124
+ }
125
+
126
+ const normalized = normalizeCron(cron);
127
+ const fields = normalized.split(' ');
128
+ if (fields.length !== CRON_FIELD_BOUNDS.length) {
129
+ return [
130
+ {
131
+ field: 'cron',
132
+ message: 'Cron expression must contain exactly five fields',
133
+ },
134
+ ];
135
+ }
136
+
137
+ return fields.flatMap((field, index) => {
138
+ const bounds = CRON_FIELD_BOUNDS[index];
139
+ if (!bounds) {
140
+ return [];
141
+ }
142
+ const issue = validateCronField(field, bounds);
143
+ return issue === undefined
144
+ ? []
145
+ : [{ field: 'cron' as const, message: issue }];
146
+ });
147
+ };
148
+
149
+ const isTimezoneValid = (timezone: string): boolean => {
150
+ try {
151
+ new Intl.DateTimeFormat('en-US', { timeZone: timezone }).format();
152
+ return true;
153
+ } catch {
154
+ return false;
155
+ }
156
+ };
157
+
158
+ const validateTimezone = (
159
+ timezone: unknown
160
+ ): readonly ScheduleValidationIssue[] => {
161
+ if (timezone === undefined) {
162
+ return [];
163
+ }
164
+ if (typeof timezone !== 'string' || timezone.trim().length === 0) {
165
+ return [
166
+ {
167
+ field: 'timezone',
168
+ message: 'Timezone must be a non-empty IANA timezone string',
169
+ },
170
+ ];
171
+ }
172
+ return isTimezoneValid(timezone.trim())
173
+ ? []
174
+ : [
175
+ {
176
+ field: 'timezone',
177
+ message: `Timezone "${timezone}" is not supported by Intl.DateTimeFormat`,
178
+ },
179
+ ];
180
+ };
181
+
182
+ const isNonJsonLeaf = (value: unknown): boolean => {
183
+ if (value === undefined) {
184
+ return true;
185
+ }
186
+ const kind = typeof value;
187
+ if (kind === 'function' || kind === 'symbol' || kind === 'bigint') {
188
+ return true;
189
+ }
190
+ return (
191
+ value instanceof Date ||
192
+ value instanceof RegExp ||
193
+ value instanceof Map ||
194
+ value instanceof Set
195
+ );
196
+ };
197
+
198
+ const containsNonSerializableLeaf = (
199
+ value: unknown,
200
+ seen: WeakSet<object>
201
+ ): boolean => {
202
+ if (isNonJsonLeaf(value)) {
203
+ return true;
204
+ }
205
+ if (value === null || typeof value !== 'object') {
206
+ return false;
207
+ }
208
+ if (seen.has(value)) {
209
+ return true;
210
+ }
211
+ seen.add(value);
212
+ if (Array.isArray(value)) {
213
+ return value.some((entry) => containsNonSerializableLeaf(entry, seen));
214
+ }
215
+ return Object.values(value).some((entry) =>
216
+ containsNonSerializableLeaf(entry, seen)
217
+ );
218
+ };
219
+
220
+ const validateInput = (input: unknown): readonly ScheduleValidationIssue[] => {
221
+ if (containsNonSerializableLeaf(input, new WeakSet<object>())) {
222
+ return [
223
+ {
224
+ field: 'input',
225
+ message: 'Schedule input must be JSON-serializable data',
226
+ },
227
+ ];
228
+ }
229
+ try {
230
+ JSON.stringify(input);
231
+ return [];
232
+ } catch {
233
+ return [
234
+ {
235
+ field: 'input',
236
+ message: 'Schedule input must be JSON-serializable data',
237
+ },
238
+ ];
239
+ }
240
+ };
241
+
242
+ const scheduleIssuesMessage = (
243
+ id: string,
244
+ issues: readonly ScheduleValidationIssue[]
245
+ ): string =>
246
+ `schedule("${id}") is invalid: ${issues.map((issue) => `${issue.field}: ${issue.message}`).join('; ')}`;
247
+
248
+ const assertScheduleSpec = (
249
+ id: string,
250
+ spec: ScheduleSpec
251
+ ): {
252
+ readonly cron: string;
253
+ readonly input: unknown;
254
+ readonly timezone?: string | undefined;
255
+ } => {
256
+ const input = spec.input === undefined ? EMPTY_INPUT : spec.input;
257
+ const timezone = spec.timezone?.trim();
258
+ const issues = [
259
+ ...validateCron(spec.cron),
260
+ ...validateTimezone(timezone),
261
+ ...validateInput(input),
262
+ ];
263
+
264
+ if (issues.length > 0) {
265
+ throw new ValidationError(scheduleIssuesMessage(id, issues), {
266
+ context: { issues },
267
+ });
268
+ }
269
+
270
+ return {
271
+ cron: normalizeCron(spec.cron),
272
+ input,
273
+ ...(timezone === undefined ? {} : { timezone }),
274
+ };
275
+ };
276
+
277
+ export const validateScheduleSource = (
278
+ source: ActivationSource
279
+ ): readonly ScheduleValidationIssue[] => {
280
+ if (source.kind !== 'schedule') {
281
+ return [];
282
+ }
283
+ const input = Object.hasOwn(source, 'input') ? source.input : EMPTY_INPUT;
284
+ return [
285
+ ...validateCron(source.cron),
286
+ ...validateTimezone(source.timezone),
287
+ ...validateInput(input),
288
+ ];
289
+ };
290
+
291
+ export function schedule<TInput>(
292
+ id: string,
293
+ spec: ScheduleSpec<TInput> & { readonly input: TInput }
294
+ ): ScheduleSource<TInput>;
295
+ export function schedule(
296
+ id: string,
297
+ spec: ScheduleSpec
298
+ ): ScheduleSource<ScheduleInputDefault>;
299
+ export function schedule<TInput>(
300
+ spec: ScheduleSpec<TInput> & { readonly id: string; readonly input: TInput }
301
+ ): ScheduleSource<TInput>;
302
+ export function schedule(
303
+ spec: ScheduleSpec & { readonly id: string }
304
+ ): ScheduleSource<ScheduleInputDefault>;
305
+ export function schedule<TInput>(
306
+ idOrSpec: string | (ScheduleSpec<TInput> & { readonly id: string }),
307
+ maybeSpec?: ScheduleSpec<TInput>
308
+ ): ScheduleSource<TInput | ScheduleInputDefault> {
309
+ const id = typeof idOrSpec === 'string' ? idOrSpec : idOrSpec.id;
310
+ // oxlint-disable-next-line no-non-null-assertion -- overload guarantees maybeSpec when idOrSpec is string
311
+ const spec = typeof idOrSpec === 'string' ? maybeSpec! : idOrSpec;
312
+ const normalized = assertScheduleSpec(id, spec);
313
+
314
+ return Object.freeze({
315
+ cron: normalized.cron,
316
+ id,
317
+ input: normalized.input as TInput | ScheduleInputDefault,
318
+ kind: 'schedule' as const,
319
+ ...(spec.meta === undefined
320
+ ? {}
321
+ : { meta: Object.freeze({ ...spec.meta }) }),
322
+ ...(normalized.timezone === undefined
323
+ ? {}
324
+ : { timezone: normalized.timezone }),
325
+ });
326
+ }
@@ -7,21 +7,26 @@
7
7
 
8
8
  import type { ErrorCategory, TrailsError } from './errors.js';
9
9
  import {
10
+ AuthError,
11
+ CancelledError,
12
+ ConflictError,
10
13
  ValidationError,
11
- AmbiguousError,
12
- AssertionError,
14
+ InternalError,
15
+ NetworkError,
13
16
  NotFoundError,
14
- AlreadyExistsError,
15
- ConflictError,
16
17
  PermissionError,
17
- TimeoutError,
18
18
  RateLimitError,
19
- NetworkError,
20
- InternalError,
21
- AuthError,
22
- CancelledError,
19
+ RetryExhaustedError,
20
+ TimeoutError,
21
+ WorkspaceShiftError,
22
+ errorClasses,
23
23
  isTrailsError,
24
24
  } from './errors.js';
25
+ import {
26
+ redactErrorContext,
27
+ redactErrorStack,
28
+ redactErrorString,
29
+ } from './error-projection.js';
25
30
  import { Result } from './result.js';
26
31
 
27
32
  // ---------------------------------------------------------------------------
@@ -31,7 +36,10 @@ import { Result } from './result.js';
31
36
  export interface SerializedError {
32
37
  readonly name: string;
33
38
  readonly message: string;
39
+ readonly attempts?: number | undefined;
34
40
  readonly category?: ErrorCategory | undefined;
41
+ readonly cause?: SerializedError | undefined;
42
+ readonly detour?: string | undefined;
35
43
  readonly retryable?: boolean | undefined;
36
44
  readonly retryAfter?: number | undefined;
37
45
  readonly context?: Record<string, unknown> | undefined;
@@ -56,10 +64,15 @@ const buildOpts = (
56
64
 
57
65
  type ErrorFactory = (
58
66
  message: string,
59
- opts: { context?: Record<string, unknown> },
67
+ opts: { cause?: Error; context?: Record<string, unknown> },
60
68
  retryAfter: number | undefined
61
69
  ) => TrailsError;
62
70
 
71
+ type FixedErrorConstructor = new (
72
+ message: string,
73
+ options?: { cause?: Error; context?: Record<string, unknown> }
74
+ ) => TrailsError;
75
+
63
76
  const errorFactories: Record<ErrorCategory, ErrorFactory> = {
64
77
  auth: (msg, opts) => new AuthError(msg, opts),
65
78
  cancelled: (msg, opts) => new CancelledError(msg, opts),
@@ -77,6 +90,7 @@ const errorFactories: Record<ErrorCategory, ErrorFactory> = {
77
90
  }
78
91
  return new RateLimitError(msg, rlOpts);
79
92
  },
93
+ shift: (msg, opts) => new WorkspaceShiftError(msg, opts),
80
94
  timeout: (msg, opts) => new TimeoutError(msg, opts),
81
95
  validation: (msg, opts) => new ValidationError(msg, opts),
82
96
  };
@@ -92,30 +106,42 @@ const createErrorByCategory = (
92
106
  return factory(message, opts, retryAfter);
93
107
  };
94
108
 
95
- /** Map error class names to their constructors for precise round-tripping. */
96
- const errorConstructorsByName: Record<string, ErrorFactory> = {
97
- AlreadyExistsError: (msg, opts) => new AlreadyExistsError(msg, opts),
98
- AmbiguousError: (msg, opts) => new AmbiguousError(msg, opts),
99
- AssertionError: (msg, opts) => new AssertionError(msg, opts),
100
- AuthError: (msg, opts) => new AuthError(msg, opts),
101
- CancelledError: (msg, opts) => new CancelledError(msg, opts),
102
- ConflictError: (msg, opts) => new ConflictError(msg, opts),
103
- InternalError: (msg, opts) => new InternalError(msg, opts),
104
- NetworkError: (msg, opts) => new NetworkError(msg, opts),
105
- NotFoundError: (msg, opts) => new NotFoundError(msg, opts),
106
- PermissionError: (msg, opts) => new PermissionError(msg, opts),
107
- RateLimitError: (msg, opts, retryAfter) => {
108
- const rlOpts: { context?: Record<string, unknown>; retryAfter?: number } = {
109
- ...opts,
110
- };
111
- if (retryAfter !== undefined) {
112
- rlOpts.retryAfter = retryAfter;
113
- }
114
- return new RateLimitError(msg, rlOpts);
115
- },
116
- TimeoutError: (msg, opts) => new TimeoutError(msg, opts),
117
- ValidationError: (msg, opts) => new ValidationError(msg, opts),
118
- };
109
+ /** Map fixed error class names to constructors for precise round-tripping. */
110
+ const errorConstructorsByName: Readonly<Record<string, ErrorFactory>> =
111
+ Object.fromEntries(
112
+ errorClasses.flatMap((entry): [string, ErrorFactory][] => {
113
+ if (entry.category === 'dynamic') {
114
+ return [];
115
+ }
116
+ const ctor = entry.ctor as FixedErrorConstructor;
117
+ return [
118
+ [
119
+ entry.name,
120
+ (message, opts, retryAfter) => {
121
+ if (ctor === RateLimitError) {
122
+ const rateLimitOptions:
123
+ | {
124
+ cause?: Error;
125
+ context?: Record<string, unknown>;
126
+ retryAfter?: number;
127
+ }
128
+ | undefined =
129
+ opts.context === undefined &&
130
+ opts.cause === undefined &&
131
+ retryAfter === undefined
132
+ ? undefined
133
+ : {
134
+ ...opts,
135
+ ...(retryAfter === undefined ? {} : { retryAfter }),
136
+ };
137
+ return new RateLimitError(message, rateLimitOptions);
138
+ }
139
+ return new ctor(message, opts);
140
+ },
141
+ ],
142
+ ];
143
+ })
144
+ );
119
145
 
120
146
  // ---------------------------------------------------------------------------
121
147
  // Error serialization
@@ -124,16 +150,23 @@ const errorConstructorsByName: Record<string, ErrorFactory> = {
124
150
  /** Extract structured data from an Error for transport. */
125
151
  export const serializeError = (error: Error): SerializedError => {
126
152
  const result: SerializedError = {
127
- message: error.message,
153
+ message: redactErrorString(error.message),
128
154
  name: error.name,
129
- stack: error.stack,
155
+ stack: redactErrorStack(error.stack),
130
156
  };
131
157
 
132
158
  if (isTrailsError(error)) {
133
159
  return {
134
160
  ...result,
135
161
  category: error.category,
136
- context: error.context,
162
+ context: redactErrorContext(error.context),
163
+ ...(error instanceof RetryExhaustedError
164
+ ? {
165
+ attempts: error.attempts,
166
+ cause: serializeError(error.cause),
167
+ detour: error.detour,
168
+ }
169
+ : {}),
137
170
  retryAfter:
138
171
  error instanceof RateLimitError ? error.retryAfter : undefined,
139
172
  retryable: error.retryable,
@@ -146,6 +179,28 @@ export const serializeError = (error: Error): SerializedError => {
146
179
  /** Reconstruct a TrailsError from serialized data. */
147
180
  export const deserializeError = (data: SerializedError): TrailsError => {
148
181
  const opts = buildOpts(data.context);
182
+ if (data.name === 'RetryExhaustedError') {
183
+ const wrapped =
184
+ data.cause === undefined
185
+ ? createErrorByCategory(
186
+ data.category ?? 'internal',
187
+ data.message,
188
+ data.context,
189
+ data.retryAfter
190
+ )
191
+ : deserializeError(data.cause);
192
+ const error = new RetryExhaustedError(wrapped, {
193
+ attempts: data.attempts ?? 0,
194
+ detour: data.detour ?? 'unknown',
195
+ });
196
+ if (data.message !== error.message) {
197
+ error.message = data.message;
198
+ }
199
+ if (data.stack) {
200
+ error.stack = data.stack;
201
+ }
202
+ return error;
203
+ }
149
204
  const nameFactory = errorConstructorsByName[data.name];
150
205
 
151
206
  const error = nameFactory
@@ -164,24 +219,6 @@ export const deserializeError = (data: SerializedError): TrailsError => {
164
219
  return error;
165
220
  };
166
221
 
167
- // ---------------------------------------------------------------------------
168
- // Safe JSON
169
- // ---------------------------------------------------------------------------
170
-
171
- /** Parse a JSON string, returning a Result instead of throwing. */
172
- export const safeParse = (json: string): Result<unknown, ValidationError> => {
173
- try {
174
- return Result.ok(JSON.parse(json) as unknown);
175
- } catch (error) {
176
- return Result.err(
177
- new ValidationError('Invalid JSON', {
178
- cause: error instanceof Error ? error : new Error(String(error)),
179
- context: { input: json.slice(0, 200) },
180
- })
181
- );
182
- }
183
- };
184
-
185
222
  /** Stringify a value, returning a Result. Handles circular references. */
186
223
  export const safeStringify = (
187
224
  value: unknown