@ontrails/core 1.0.0-beta.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 (216) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/.turbo/turbo-lint.log +3 -0
  3. package/.turbo/turbo-typecheck.log +1 -0
  4. package/CHANGELOG.md +15 -0
  5. package/README.md +179 -0
  6. package/dist/adapters.d.ts +39 -0
  7. package/dist/adapters.d.ts.map +1 -0
  8. package/dist/adapters.js +2 -0
  9. package/dist/adapters.js.map +1 -0
  10. package/dist/blob-ref.d.ts +20 -0
  11. package/dist/blob-ref.d.ts.map +1 -0
  12. package/dist/blob-ref.js +22 -0
  13. package/dist/blob-ref.js.map +1 -0
  14. package/dist/branded.d.ts +36 -0
  15. package/dist/branded.d.ts.map +1 -0
  16. package/dist/branded.js +89 -0
  17. package/dist/branded.js.map +1 -0
  18. package/dist/collections.d.ts +31 -0
  19. package/dist/collections.d.ts.map +1 -0
  20. package/dist/collections.js +60 -0
  21. package/dist/collections.js.map +1 -0
  22. package/dist/context.d.ts +10 -0
  23. package/dist/context.d.ts.map +1 -0
  24. package/dist/context.js +15 -0
  25. package/dist/context.js.map +1 -0
  26. package/dist/derive.d.ts +33 -0
  27. package/dist/derive.d.ts.map +1 -0
  28. package/dist/derive.js +122 -0
  29. package/dist/derive.js.map +1 -0
  30. package/dist/errors.d.ts +83 -0
  31. package/dist/errors.d.ts.map +1 -0
  32. package/dist/errors.js +142 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/event.d.ts +45 -0
  35. package/dist/event.d.ts.map +1 -0
  36. package/dist/event.js +17 -0
  37. package/dist/event.js.map +1 -0
  38. package/dist/fetch.d.ts +15 -0
  39. package/dist/fetch.d.ts.map +1 -0
  40. package/dist/fetch.js +102 -0
  41. package/dist/fetch.js.map +1 -0
  42. package/dist/guards.d.ts +17 -0
  43. package/dist/guards.d.ts.map +1 -0
  44. package/dist/guards.js +25 -0
  45. package/dist/guards.js.map +1 -0
  46. package/dist/health.d.ts +18 -0
  47. package/dist/health.d.ts.map +1 -0
  48. package/dist/health.js +5 -0
  49. package/dist/health.js.map +1 -0
  50. package/dist/hike.d.ts +36 -0
  51. package/dist/hike.d.ts.map +1 -0
  52. package/dist/hike.js +20 -0
  53. package/dist/hike.js.map +1 -0
  54. package/dist/index.d.ts +34 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +38 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/job.d.ts +24 -0
  59. package/dist/job.d.ts.map +1 -0
  60. package/dist/job.js +17 -0
  61. package/dist/job.js.map +1 -0
  62. package/dist/layer.d.ts +17 -0
  63. package/dist/layer.d.ts.map +1 -0
  64. package/dist/layer.js +21 -0
  65. package/dist/layer.js.map +1 -0
  66. package/dist/path-security.d.ts +28 -0
  67. package/dist/path-security.d.ts.map +1 -0
  68. package/dist/path-security.js +63 -0
  69. package/dist/path-security.js.map +1 -0
  70. package/dist/patterns/bulk.d.ts +15 -0
  71. package/dist/patterns/bulk.d.ts.map +1 -0
  72. package/dist/patterns/bulk.js +14 -0
  73. package/dist/patterns/bulk.js.map +1 -0
  74. package/dist/patterns/change.d.ts +10 -0
  75. package/dist/patterns/change.d.ts.map +1 -0
  76. package/dist/patterns/change.js +10 -0
  77. package/dist/patterns/change.js.map +1 -0
  78. package/dist/patterns/date-range.d.ts +10 -0
  79. package/dist/patterns/date-range.d.ts.map +1 -0
  80. package/dist/patterns/date-range.js +10 -0
  81. package/dist/patterns/date-range.js.map +1 -0
  82. package/dist/patterns/index.d.ts +9 -0
  83. package/dist/patterns/index.d.ts.map +1 -0
  84. package/dist/patterns/index.js +9 -0
  85. package/dist/patterns/index.js.map +1 -0
  86. package/dist/patterns/pagination.d.ts +18 -0
  87. package/dist/patterns/pagination.d.ts.map +1 -0
  88. package/dist/patterns/pagination.js +18 -0
  89. package/dist/patterns/pagination.js.map +1 -0
  90. package/dist/patterns/progress.d.ts +11 -0
  91. package/dist/patterns/progress.d.ts.map +1 -0
  92. package/dist/patterns/progress.js +11 -0
  93. package/dist/patterns/progress.js.map +1 -0
  94. package/dist/patterns/sorting.d.ts +13 -0
  95. package/dist/patterns/sorting.d.ts.map +1 -0
  96. package/dist/patterns/sorting.js +10 -0
  97. package/dist/patterns/sorting.js.map +1 -0
  98. package/dist/patterns/status.d.ts +15 -0
  99. package/dist/patterns/status.d.ts.map +1 -0
  100. package/dist/patterns/status.js +9 -0
  101. package/dist/patterns/status.js.map +1 -0
  102. package/dist/patterns/timestamps.d.ts +10 -0
  103. package/dist/patterns/timestamps.d.ts.map +1 -0
  104. package/dist/patterns/timestamps.js +10 -0
  105. package/dist/patterns/timestamps.js.map +1 -0
  106. package/dist/redaction/index.d.ts +4 -0
  107. package/dist/redaction/index.d.ts.map +1 -0
  108. package/dist/redaction/index.js +3 -0
  109. package/dist/redaction/index.js.map +1 -0
  110. package/dist/redaction/patterns.d.ts +9 -0
  111. package/dist/redaction/patterns.d.ts.map +1 -0
  112. package/dist/redaction/patterns.js +39 -0
  113. package/dist/redaction/patterns.js.map +1 -0
  114. package/dist/redaction/redactor.d.ts +27 -0
  115. package/dist/redaction/redactor.d.ts.map +1 -0
  116. package/dist/redaction/redactor.js +89 -0
  117. package/dist/redaction/redactor.js.map +1 -0
  118. package/dist/resilience.d.ts +34 -0
  119. package/dist/resilience.d.ts.map +1 -0
  120. package/dist/resilience.js +164 -0
  121. package/dist/resilience.js.map +1 -0
  122. package/dist/result.d.ts +57 -0
  123. package/dist/result.d.ts.map +1 -0
  124. package/dist/result.js +145 -0
  125. package/dist/result.js.map +1 -0
  126. package/dist/serialization.d.ts +27 -0
  127. package/dist/serialization.d.ts.map +1 -0
  128. package/dist/serialization.js +115 -0
  129. package/dist/serialization.js.map +1 -0
  130. package/dist/topo.d.ts +18 -0
  131. package/dist/topo.d.ts.map +1 -0
  132. package/dist/topo.js +74 -0
  133. package/dist/topo.js.map +1 -0
  134. package/dist/trail.d.ts +83 -0
  135. package/dist/trail.d.ts.map +1 -0
  136. package/dist/trail.js +16 -0
  137. package/dist/trail.js.map +1 -0
  138. package/dist/types.d.ts +46 -0
  139. package/dist/types.d.ts.map +1 -0
  140. package/dist/types.js +2 -0
  141. package/dist/types.js.map +1 -0
  142. package/dist/validate-topo.d.ts +24 -0
  143. package/dist/validate-topo.d.ts.map +1 -0
  144. package/dist/validate-topo.js +108 -0
  145. package/dist/validate-topo.js.map +1 -0
  146. package/dist/validation.d.ts +27 -0
  147. package/dist/validation.d.ts.map +1 -0
  148. package/dist/validation.js +134 -0
  149. package/dist/validation.js.map +1 -0
  150. package/dist/workspace.d.ts +25 -0
  151. package/dist/workspace.d.ts.map +1 -0
  152. package/dist/workspace.js +57 -0
  153. package/dist/workspace.js.map +1 -0
  154. package/package.json +21 -0
  155. package/src/__tests__/blob-ref.test.ts +103 -0
  156. package/src/__tests__/branded.test.ts +148 -0
  157. package/src/__tests__/collections.test.ts +126 -0
  158. package/src/__tests__/context.test.ts +66 -0
  159. package/src/__tests__/derive.test.ts +159 -0
  160. package/src/__tests__/errors.test.ts +309 -0
  161. package/src/__tests__/event.test.ts +82 -0
  162. package/src/__tests__/fetch.test.ts +217 -0
  163. package/src/__tests__/guards.test.ts +102 -0
  164. package/src/__tests__/hike.test.ts +117 -0
  165. package/src/__tests__/job.test.ts +98 -0
  166. package/src/__tests__/layer.test.ts +224 -0
  167. package/src/__tests__/path-security.test.ts +114 -0
  168. package/src/__tests__/patterns.test.ts +273 -0
  169. package/src/__tests__/redaction.test.ts +244 -0
  170. package/src/__tests__/resilience.test.ts +246 -0
  171. package/src/__tests__/result.test.ts +155 -0
  172. package/src/__tests__/serialization.test.ts +236 -0
  173. package/src/__tests__/topo.test.ts +184 -0
  174. package/src/__tests__/trail.test.ts +179 -0
  175. package/src/__tests__/validate-topo.test.ts +201 -0
  176. package/src/__tests__/validation.test.ts +283 -0
  177. package/src/__tests__/workspace.test.ts +183 -0
  178. package/src/adapters.ts +68 -0
  179. package/src/blob-ref.ts +39 -0
  180. package/src/branded.ts +135 -0
  181. package/src/collections.ts +99 -0
  182. package/src/context.ts +18 -0
  183. package/src/derive.ts +223 -0
  184. package/src/errors.ts +196 -0
  185. package/src/event.ts +77 -0
  186. package/src/fetch.ts +138 -0
  187. package/src/guards.ts +37 -0
  188. package/src/health.ts +23 -0
  189. package/src/hike.ts +77 -0
  190. package/src/index.ts +158 -0
  191. package/src/job.ts +20 -0
  192. package/src/layer.ts +44 -0
  193. package/src/path-security.ts +90 -0
  194. package/src/patterns/bulk.ts +16 -0
  195. package/src/patterns/change.ts +12 -0
  196. package/src/patterns/date-range.ts +12 -0
  197. package/src/patterns/index.ts +8 -0
  198. package/src/patterns/pagination.ts +22 -0
  199. package/src/patterns/progress.ts +13 -0
  200. package/src/patterns/sorting.ts +14 -0
  201. package/src/patterns/status.ts +11 -0
  202. package/src/patterns/timestamps.ts +12 -0
  203. package/src/redaction/index.ts +3 -0
  204. package/src/redaction/patterns.ts +47 -0
  205. package/src/redaction/redactor.ts +178 -0
  206. package/src/resilience.ts +234 -0
  207. package/src/result.ts +180 -0
  208. package/src/serialization.ts +183 -0
  209. package/src/topo.ts +123 -0
  210. package/src/trail.ts +130 -0
  211. package/src/types.ts +58 -0
  212. package/src/validate-topo.ts +151 -0
  213. package/src/validation.ts +182 -0
  214. package/src/workspace.ts +77 -0
  215. package/tsconfig.json +9 -0
  216. package/tsconfig.tsbuildinfo +1 -0
package/src/errors.ts ADDED
@@ -0,0 +1,196 @@
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 type ErrorCategory =
14
+ | 'validation'
15
+ | 'not_found'
16
+ | 'conflict'
17
+ | 'permission'
18
+ | 'timeout'
19
+ | 'rate_limit'
20
+ | 'network'
21
+ | 'internal'
22
+ | 'auth'
23
+ | 'cancelled';
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Base class
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export abstract class TrailsError extends Error {
30
+ abstract readonly category: ErrorCategory;
31
+ abstract readonly retryable: boolean;
32
+ readonly context?: Record<string, unknown> | undefined;
33
+
34
+ constructor(
35
+ message: string,
36
+ options?: { cause?: Error; context?: Record<string, unknown> }
37
+ ) {
38
+ super(message, { cause: options?.cause });
39
+ this.name = this.constructor.name;
40
+ this.context = options?.context;
41
+ }
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Concrete error classes
46
+ // ---------------------------------------------------------------------------
47
+
48
+ export class ValidationError extends TrailsError {
49
+ readonly category = 'validation' as const;
50
+ readonly retryable = false as const;
51
+ }
52
+
53
+ export class AmbiguousError extends TrailsError {
54
+ readonly category = 'validation' as const;
55
+ readonly retryable = false as const;
56
+ }
57
+
58
+ export class AssertionError extends TrailsError {
59
+ readonly category = 'internal' as const;
60
+ readonly retryable = false as const;
61
+ }
62
+
63
+ export class NotFoundError extends TrailsError {
64
+ readonly category = 'not_found' as const;
65
+ readonly retryable = false as const;
66
+ }
67
+
68
+ export class AlreadyExistsError extends TrailsError {
69
+ readonly category = 'conflict' as const;
70
+ readonly retryable = false as const;
71
+ }
72
+
73
+ export class ConflictError extends TrailsError {
74
+ readonly category = 'conflict' as const;
75
+ readonly retryable = false as const;
76
+ }
77
+
78
+ export class PermissionError extends TrailsError {
79
+ readonly category = 'permission' as const;
80
+ readonly retryable = false as const;
81
+ }
82
+
83
+ export class TimeoutError extends TrailsError {
84
+ readonly category = 'timeout' as const;
85
+ readonly retryable = true as const;
86
+ }
87
+
88
+ export class RateLimitError extends TrailsError {
89
+ readonly category = 'rate_limit' as const;
90
+ readonly retryable = true as const;
91
+ readonly retryAfter?: number | undefined;
92
+
93
+ constructor(
94
+ message: string,
95
+ options?: {
96
+ cause?: Error;
97
+ context?: Record<string, unknown>;
98
+ retryAfter?: number;
99
+ }
100
+ ) {
101
+ super(message, options);
102
+ this.retryAfter = options?.retryAfter;
103
+ }
104
+ }
105
+
106
+ export class NetworkError extends TrailsError {
107
+ readonly category = 'network' as const;
108
+ readonly retryable = true as const;
109
+ }
110
+
111
+ export class InternalError extends TrailsError {
112
+ readonly category = 'internal' as const;
113
+ readonly retryable = false as const;
114
+ }
115
+
116
+ export class AuthError extends TrailsError {
117
+ readonly category = 'auth' as const;
118
+ readonly retryable = false as const;
119
+ }
120
+
121
+ export class CancelledError extends TrailsError {
122
+ readonly category = 'cancelled' as const;
123
+ readonly retryable = false as const;
124
+ }
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // Taxonomy maps
128
+ // ---------------------------------------------------------------------------
129
+
130
+ export const exitCodeMap: Record<ErrorCategory, number> = {
131
+ auth: 9,
132
+ cancelled: 130,
133
+ conflict: 3,
134
+ internal: 8,
135
+ network: 7,
136
+ not_found: 2,
137
+ permission: 4,
138
+ rate_limit: 6,
139
+ timeout: 5,
140
+ validation: 1,
141
+ } as const;
142
+
143
+ export const statusCodeMap: Record<ErrorCategory, number> = {
144
+ auth: 401,
145
+ cancelled: 499,
146
+ conflict: 409,
147
+ internal: 500,
148
+ network: 502,
149
+ not_found: 404,
150
+ permission: 403,
151
+ rate_limit: 429,
152
+ timeout: 504,
153
+ validation: 400,
154
+ } as const;
155
+
156
+ export const jsonRpcCodeMap: Record<ErrorCategory, number> = {
157
+ auth: -32_600,
158
+ cancelled: -32_603,
159
+ conflict: -32_603,
160
+ internal: -32_603,
161
+ network: -32_603,
162
+ not_found: -32_601,
163
+ permission: -32_600,
164
+ rate_limit: -32_603,
165
+ timeout: -32_603,
166
+ validation: -32_602,
167
+ } as const;
168
+
169
+ export const retryableMap: Record<ErrorCategory, boolean> = {
170
+ auth: false,
171
+ cancelled: false,
172
+ conflict: false,
173
+ internal: false,
174
+ network: true,
175
+ not_found: false,
176
+ permission: false,
177
+ rate_limit: true,
178
+ timeout: true,
179
+ validation: false,
180
+ } as const;
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Helper functions
184
+ // ---------------------------------------------------------------------------
185
+
186
+ /** Type guard: narrows unknown to TrailsError */
187
+ export const isTrailsError = (error?: unknown): error is TrailsError =>
188
+ error instanceof TrailsError;
189
+
190
+ /** Returns true if the error is retryable (TrailsError with retryable category). */
191
+ export const isRetryable = (error: Error): boolean => {
192
+ if (isTrailsError(error)) {
193
+ return retryableMap[error.category];
194
+ }
195
+ return false;
196
+ };
package/src/event.ts ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Event — a named payload schema with optional provenance metadata.
3
+ */
4
+
5
+ import type { z } from 'zod';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Spec (input to the factory)
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export interface EventSpec<T> {
12
+ readonly payload: z.ZodType<T>;
13
+ readonly description?: string | undefined;
14
+ readonly markers?: Readonly<Record<string, unknown>> | undefined;
15
+ /** Trail IDs that produce this event (e.g. the trails it originates from). */
16
+ readonly from?: readonly string[] | undefined;
17
+ }
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Shape (output of the factory)
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export interface Event<T> {
24
+ readonly id: string;
25
+ readonly kind: 'event';
26
+ readonly payload: z.ZodType<T>;
27
+ readonly description?: string | undefined;
28
+ readonly markers?: Readonly<Record<string, unknown>> | undefined;
29
+ /** Trail IDs that produce this event (e.g. the trails it originates from). */
30
+ readonly from?: readonly string[] | undefined;
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Factory
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * Create an event definition.
39
+ *
40
+ * An event is a named payload schema describing something that happened.
41
+ * Returns a frozen object with `kind: "event"` and all spec fields.
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * // ID as first argument
46
+ * const updated = event("entity.updated", {
47
+ * payload: EntityUpdatedSchema,
48
+ * from: ["entity.add", "entity.update"],
49
+ * });
50
+ *
51
+ * // Full spec object (programmatic)
52
+ * const updated = event({ id: "entity.updated", payload: ..., from: [...] });
53
+ * ```
54
+ */
55
+ export function event<T>(id: string, spec: EventSpec<T>): Event<T>;
56
+ export function event<T>(
57
+ spec: EventSpec<T> & { readonly id: string }
58
+ ): Event<T>;
59
+ export function event<T>(
60
+ idOrSpec: string | (EventSpec<T> & { readonly id: string }),
61
+ maybeSpec?: EventSpec<T>
62
+ ): Event<T> {
63
+ const resolvedId = typeof idOrSpec === 'string' ? idOrSpec : idOrSpec.id;
64
+ // oxlint-disable-next-line no-non-null-assertion -- overload guarantees maybeSpec when idOrSpec is string
65
+ const resolvedSpec = typeof idOrSpec === 'string' ? maybeSpec! : idOrSpec;
66
+ return Object.freeze({
67
+ description: resolvedSpec.description,
68
+ from: resolvedSpec.from ? Object.freeze([...resolvedSpec.from]) : undefined,
69
+ id: resolvedId,
70
+ kind: 'event' as const,
71
+ markers: resolvedSpec.markers,
72
+ payload: resolvedSpec.payload,
73
+ });
74
+ }
75
+
76
+ /** Existential type for heterogeneous event collections */
77
+ export type AnyEvent = Event<unknown>;
package/src/fetch.ts ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Fetch utilities for @ontrails/core
3
+ *
4
+ * Wraps the standard fetch API, mapping errors and HTTP status codes
5
+ * to the TrailsError taxonomy.
6
+ */
7
+
8
+ import {
9
+ AuthError,
10
+ CancelledError,
11
+ ConflictError,
12
+ InternalError,
13
+ NetworkError,
14
+ NotFoundError,
15
+ PermissionError,
16
+ RateLimitError,
17
+ TimeoutError,
18
+ ValidationError,
19
+ } from './errors.js';
20
+ import { Result } from './result.js';
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Internal helpers (defined before usage)
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const toError = (err: unknown): Error =>
27
+ err instanceof Error ? err : new Error(String(err));
28
+
29
+ const parseRetryAfter = (header: string | null): number | undefined => {
30
+ if (!header) {
31
+ return undefined;
32
+ }
33
+ const seconds = Number(header);
34
+ if (Number.isFinite(seconds) && seconds > 0) {
35
+ return seconds;
36
+ }
37
+ // Try parsing as HTTP-date
38
+ const date = Date.parse(header);
39
+ if (!Number.isNaN(date)) {
40
+ const delta = Math.ceil((date - Date.now()) / 1000);
41
+ return delta > 0 ? delta : undefined;
42
+ }
43
+ return undefined;
44
+ };
45
+
46
+ const mapFetchError = (err: unknown): Error => {
47
+ if (err instanceof DOMException && err.name === 'AbortError') {
48
+ return new CancelledError('Request was aborted', { cause: toError(err) });
49
+ }
50
+
51
+ // TypeError is thrown for network failures in the fetch spec
52
+ if (err instanceof TypeError) {
53
+ return new NetworkError('Network request failed', { cause: err });
54
+ }
55
+
56
+ return new NetworkError('Network request failed', {
57
+ cause: toError(err),
58
+ });
59
+ };
60
+
61
+ type StatusMapper = (
62
+ context: Record<string, unknown>,
63
+ response: Response
64
+ ) => Error;
65
+
66
+ const statusMappers: Record<number, StatusMapper> = {
67
+ 401: (ctx) => new AuthError('Unauthorized', { context: ctx }),
68
+ 403: (ctx) => new PermissionError('Forbidden', { context: ctx }),
69
+ 404: (ctx) => new NotFoundError('Not found', { context: ctx }),
70
+ 429: (ctx, response) => {
71
+ const retryAfter = parseRetryAfter(response.headers.get('retry-after'));
72
+ const opts: { context: Record<string, unknown>; retryAfter?: number } = {
73
+ context: ctx,
74
+ };
75
+ if (retryAfter !== undefined) {
76
+ opts.retryAfter = retryAfter;
77
+ }
78
+ return new RateLimitError('Rate limited', opts);
79
+ },
80
+ 500: (ctx) => new InternalError('Internal server error', { context: ctx }),
81
+ 502: (ctx) => new NetworkError('Bad gateway', { context: ctx }),
82
+ 504: (ctx) => new TimeoutError('Gateway timeout', { context: ctx }),
83
+ };
84
+
85
+ /** Map 4xx status codes not in the explicit mapper to appropriate error types. */
86
+ const mapClientError = (
87
+ status: number,
88
+ context: Record<string, unknown>
89
+ ): Error => {
90
+ if (status === 400 || status === 422) {
91
+ return new ValidationError(`Validation error (${status})`, { context });
92
+ }
93
+ if (status === 409) {
94
+ return new ConflictError(`Conflict (${status})`, { context });
95
+ }
96
+ return new InternalError(`HTTP error (${status})`, { context });
97
+ };
98
+
99
+ const mapStatusCode = (response: Response): Error => {
100
+ const context = { status: response.status, url: response.url };
101
+ const mapper = statusMappers[response.status];
102
+ if (mapper) {
103
+ return mapper(context, response);
104
+ }
105
+ if (response.status >= 500) {
106
+ return new InternalError(`Server error (${response.status})`, { context });
107
+ }
108
+ return mapClientError(response.status, context);
109
+ };
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // fromFetch
113
+ // ---------------------------------------------------------------------------
114
+
115
+ /**
116
+ * Wrap a fetch call in a Result, mapping failures to TrailsError subclasses.
117
+ *
118
+ * Network errors become NetworkError. Abort signals become CancelledError.
119
+ * HTTP error status codes map to the appropriate error category.
120
+ */
121
+ export const fromFetch = async (
122
+ input: string | URL | Request,
123
+ init?: RequestInit
124
+ ): Promise<Result<Response, Error>> => {
125
+ let response: Response;
126
+
127
+ try {
128
+ response = await fetch(input, init);
129
+ } catch (error) {
130
+ return Result.err(mapFetchError(error));
131
+ }
132
+
133
+ if (response.ok) {
134
+ return Result.ok(response);
135
+ }
136
+
137
+ return Result.err(mapStatusCode(response));
138
+ };
package/src/guards.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Type guards and assertion helpers for @ontrails/core.
3
+ */
4
+
5
+ /** Narrows `T | null | undefined` to `T`. */
6
+ export const isDefined = <T>(value?: T | null | undefined): value is T =>
7
+ value !== null && value !== undefined;
8
+
9
+ /** Returns true when `value` is a string with length > 0. */
10
+ export const isNonEmptyString = (value?: unknown): value is string =>
11
+ typeof value === 'string' && value.length > 0;
12
+
13
+ /** Returns true when `value` is a plain object (not an array, Date, etc.). */
14
+ export const isPlainObject = (
15
+ value: unknown
16
+ ): value is Record<string, unknown> => {
17
+ if (typeof value !== 'object' || value === null) {
18
+ return false;
19
+ }
20
+ const proto = Object.getPrototypeOf(value) as unknown;
21
+ return proto === Object.prototype || proto === null;
22
+ };
23
+
24
+ /** Checks that `obj` is an object with the given key present. */
25
+ export const hasProperty = <K extends string>(
26
+ obj: unknown,
27
+ key: K
28
+ ): obj is Record<K, unknown> =>
29
+ typeof obj === 'object' && obj !== null && key in obj;
30
+
31
+ /**
32
+ * Exhaustive switch helper. Place in the `default` branch to get a compile
33
+ * error when a union case is unhandled.
34
+ */
35
+ export const assertNever = (value: never): never => {
36
+ throw new Error(`Unexpected value: ${String(value)}`);
37
+ };
package/src/health.ts ADDED
@@ -0,0 +1,23 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Health check types
3
+ // ---------------------------------------------------------------------------
4
+
5
+ /** Aggregate health status */
6
+ export type HealthStatus = 'healthy' | 'degraded' | 'unhealthy';
7
+
8
+ /** Individual component check result */
9
+ export interface HealthCheck {
10
+ readonly status: HealthStatus;
11
+ readonly message?: string | undefined;
12
+ /** Latency in milliseconds */
13
+ readonly latency?: number | undefined;
14
+ }
15
+
16
+ /** Full health report returned by a health endpoint */
17
+ export interface HealthResult {
18
+ readonly status: HealthStatus;
19
+ readonly checks: Readonly<Record<string, HealthCheck>>;
20
+ readonly version?: string | undefined;
21
+ /** Uptime in seconds */
22
+ readonly uptime?: number | undefined;
23
+ }
package/src/hike.ts ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Hike — a composition that follows trails.
3
+ */
4
+
5
+ import type { Trail, TrailSpec } from './trail.js';
6
+ import type { TrailContext } from './types.js';
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Spec (input to the factory)
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export interface HikeSpec<I, O> extends TrailSpec<I, O> {
13
+ readonly follows: readonly string[];
14
+ }
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Shape (output of the factory)
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export interface Hike<I, O> extends Omit<Trail<I, O>, 'kind'> {
21
+ readonly kind: 'hike';
22
+ readonly follows: readonly string[];
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Factory
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /**
30
+ * Create a hike definition.
31
+ *
32
+ * A hike is a composition that declares which trails it follows.
33
+ * Returns a frozen object with `kind: "hike"` and all spec fields.
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * // ID as first argument
38
+ * const onboard = hike("entity.onboard", {
39
+ * follows: ["entity.add", "entity.relate"],
40
+ * input: z.object({ name: z.string() }),
41
+ * implementation: (input, ctx) => Result.ok(...),
42
+ * });
43
+ *
44
+ * // Full spec object (programmatic)
45
+ * const onboard = hike({ id: "entity.onboard", follows: [...], ... });
46
+ * ```
47
+ */
48
+ export function hike<I, O>(id: string, spec: HikeSpec<I, O>): Hike<I, O>;
49
+ export function hike<I, O>(
50
+ spec: HikeSpec<I, O> & { readonly id: string }
51
+ ): Hike<I, O>;
52
+ export function hike<I, O>(
53
+ idOrSpec: string | (HikeSpec<I, O> & { readonly id: string }),
54
+ maybeSpec?: HikeSpec<I, O>
55
+ ): Hike<I, O> {
56
+ const resolved =
57
+ typeof idOrSpec === 'string'
58
+ ? { id: idOrSpec, spec: maybeSpec }
59
+ : { id: idOrSpec.id, spec: idOrSpec };
60
+
61
+ if (!resolved.spec) {
62
+ throw new TypeError('hike() requires a spec when an id is provided');
63
+ }
64
+
65
+ const { follows, implementation, ...rest } = resolved.spec;
66
+ return Object.freeze({
67
+ ...rest,
68
+ follows: Object.freeze([...follows]),
69
+ id: resolved.id,
70
+ implementation: async (input: I, ctx: TrailContext) =>
71
+ await implementation(input, ctx),
72
+ kind: 'hike' as const,
73
+ });
74
+ }
75
+
76
+ // oxlint-disable-next-line no-explicit-any -- existential type; see AnyTrail
77
+ export type AnyHike = Hike<any, any>;
package/src/index.ts ADDED
@@ -0,0 +1,158 @@
1
+ // Result
2
+ export { Result } from './result.js';
3
+
4
+ // Errors
5
+ export {
6
+ TrailsError,
7
+ ValidationError,
8
+ AmbiguousError,
9
+ AssertionError,
10
+ NotFoundError,
11
+ AlreadyExistsError,
12
+ ConflictError,
13
+ PermissionError,
14
+ TimeoutError,
15
+ RateLimitError,
16
+ NetworkError,
17
+ InternalError,
18
+ AuthError,
19
+ CancelledError,
20
+ exitCodeMap,
21
+ statusCodeMap,
22
+ jsonRpcCodeMap,
23
+ retryableMap,
24
+ isRetryable,
25
+ isTrailsError,
26
+ } from './errors.js';
27
+ export type { ErrorCategory } from './errors.js';
28
+
29
+ // Types
30
+ export type {
31
+ Implementation,
32
+ TrailContext,
33
+ FollowFn,
34
+ ProgressCallback,
35
+ ProgressEvent,
36
+ Logger,
37
+ Surface,
38
+ } from './types.js';
39
+
40
+ // Context factory
41
+ export { createTrailContext } from './context.js';
42
+
43
+ // Trail
44
+ export { trail } from './trail.js';
45
+ export type { AnyTrail, Trail, TrailSpec, TrailExample } from './trail.js';
46
+
47
+ // Hike
48
+ export { hike } from './hike.js';
49
+ export type { AnyHike, Hike, HikeSpec } from './hike.js';
50
+
51
+ // Event
52
+ export { event } from './event.js';
53
+ export type { AnyEvent, Event, EventSpec } from './event.js';
54
+
55
+ // Topo
56
+ export { topo } from './topo.js';
57
+ export type { Topo } from './topo.js';
58
+
59
+ // Topo validation
60
+ export { validateTopo } from './validate-topo.js';
61
+ export type { TopoIssue } from './validate-topo.js';
62
+
63
+ // Layer
64
+ export { composeLayers } from './layer.js';
65
+ export type { Layer } from './layer.js';
66
+
67
+ // Health
68
+ export type { HealthStatus, HealthResult } from './health.js';
69
+
70
+ // Adapters
71
+ export type {
72
+ IndexAdapter,
73
+ StorageAdapter,
74
+ CacheAdapter,
75
+ SearchOptions,
76
+ SearchResult,
77
+ StorageOptions,
78
+ } from './adapters.js';
79
+
80
+ // Derive
81
+ export { deriveFields } from './derive.js';
82
+ export type { Field, FieldOverride } from './derive.js';
83
+
84
+ // Validation
85
+ export {
86
+ validateInput,
87
+ validateOutput,
88
+ formatZodIssues,
89
+ zodToJsonSchema,
90
+ } from './validation.js';
91
+
92
+ // Serialization
93
+ export { serializeError, deserializeError } from './serialization.js';
94
+ export type { SerializedError } from './serialization.js';
95
+
96
+ // Resilience
97
+ export {
98
+ retry,
99
+ withTimeout,
100
+ shouldRetry,
101
+ getBackoffDelay,
102
+ } from './resilience.js';
103
+ export type { RetryOptions } from './resilience.js';
104
+
105
+ // Fetch — fromFetch is available as Result.fromFetch()
106
+
107
+ // Branded types
108
+ export {
109
+ brand,
110
+ unbrand,
111
+ uuid,
112
+ email,
113
+ nonEmptyString,
114
+ positiveInt,
115
+ shortId,
116
+ hashId,
117
+ } from './branded.js';
118
+ export type {
119
+ Branded,
120
+ UUID,
121
+ Email,
122
+ NonEmptyString,
123
+ PositiveInt,
124
+ } from './branded.js';
125
+
126
+ // Path Security
127
+ export { securePath, isPathSafe, resolveSafePath } from './path-security.js';
128
+
129
+ // Workspace
130
+ export {
131
+ findWorkspaceRoot,
132
+ isInsideWorkspace,
133
+ getRelativePath,
134
+ } from './workspace.js';
135
+
136
+ // Guards
137
+ export {
138
+ isDefined,
139
+ isNonEmptyString,
140
+ isPlainObject,
141
+ hasProperty,
142
+ assertNever,
143
+ } from './guards.js';
144
+
145
+ // Collections
146
+ export {
147
+ chunk,
148
+ dedupe,
149
+ groupBy,
150
+ sortBy,
151
+ isNonEmptyArray,
152
+ } from './collections.js';
153
+ export type {
154
+ NonEmptyArray,
155
+ DeepPartial,
156
+ Prettify,
157
+ AtLeastOne,
158
+ } from './collections.js';