@ontrails/core 1.0.0-beta.2 → 1.0.0-beta.21

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 (245) hide show
  1. package/CHANGELOG.md +283 -5
  2. package/README.md +113 -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 +59 -18
  15. package/src/detours.ts +8 -0
  16. package/src/draft.ts +342 -0
  17. package/src/error-projection.ts +51 -0
  18. package/src/errors.ts +309 -48
  19. package/src/execute.ts +1577 -0
  20. package/src/fire.ts +1169 -0
  21. package/src/index.ts +477 -32
  22. package/src/internal/fork-ctx.ts +69 -0
  23. package/src/layer-projection.ts +193 -0
  24. package/src/layer.ts +43 -6
  25. package/src/observe.ts +361 -0
  26. package/src/path-security.ts +1 -1
  27. package/src/permits.ts +12 -0
  28. package/src/redaction/patterns.ts +6 -3
  29. package/src/resilience.ts +11 -11
  30. package/src/resource-config.ts +792 -0
  31. package/src/resource.ts +194 -0
  32. package/src/result.ts +36 -4
  33. package/src/run.ts +76 -0
  34. package/src/schedule-runtime.ts +689 -0
  35. package/src/schedule.ts +326 -0
  36. package/src/serialization.ts +119 -39
  37. package/src/signal-diagnostics.ts +633 -0
  38. package/src/signal-ref.ts +87 -0
  39. package/src/signal.ts +104 -0
  40. package/src/store/accessor-protocol.ts +56 -0
  41. package/src/store/index.ts +4 -0
  42. package/src/structured-examples.ts +248 -0
  43. package/src/surface-derivation.ts +91 -0
  44. package/src/surface-filter.ts +176 -0
  45. package/src/surface-versioning.ts +42 -0
  46. package/src/topo.ts +769 -57
  47. package/src/tracing.ts +345 -0
  48. package/src/trail.ts +1161 -29
  49. package/src/trails/derive-trail.ts +842 -0
  50. package/src/trails/index.ts +9 -0
  51. package/src/trails/ingest.ts +139 -0
  52. package/src/trails-db.ts +208 -0
  53. package/src/transport-error-map.ts +174 -0
  54. package/src/type-utils.ts +87 -0
  55. package/src/types.ts +254 -12
  56. package/src/validate-established-topo.ts +69 -0
  57. package/src/validate-topo.ts +442 -35
  58. package/src/validation.ts +152 -4
  59. package/src/version-marker.ts +716 -0
  60. package/src/version-resolution.ts +308 -0
  61. package/src/version-runtime.ts +120 -0
  62. package/src/webhook.ts +278 -0
  63. package/src/workspace.ts +1 -1
  64. package/src/zod-wrappers.ts +72 -0
  65. package/.turbo/turbo-build.log +0 -1
  66. package/.turbo/turbo-lint.log +0 -3
  67. package/.turbo/turbo-typecheck.log +0 -1
  68. package/dist/adapters.d.ts +0 -39
  69. package/dist/adapters.d.ts.map +0 -1
  70. package/dist/adapters.js +0 -2
  71. package/dist/adapters.js.map +0 -1
  72. package/dist/blob-ref.d.ts +0 -20
  73. package/dist/blob-ref.d.ts.map +0 -1
  74. package/dist/blob-ref.js +0 -22
  75. package/dist/blob-ref.js.map +0 -1
  76. package/dist/branded.d.ts +0 -36
  77. package/dist/branded.d.ts.map +0 -1
  78. package/dist/branded.js +0 -89
  79. package/dist/branded.js.map +0 -1
  80. package/dist/collections.d.ts +0 -31
  81. package/dist/collections.d.ts.map +0 -1
  82. package/dist/collections.js +0 -60
  83. package/dist/collections.js.map +0 -1
  84. package/dist/context.d.ts +0 -10
  85. package/dist/context.d.ts.map +0 -1
  86. package/dist/context.js +0 -15
  87. package/dist/context.js.map +0 -1
  88. package/dist/derive.d.ts +0 -33
  89. package/dist/derive.d.ts.map +0 -1
  90. package/dist/derive.js +0 -122
  91. package/dist/derive.js.map +0 -1
  92. package/dist/errors.d.ts +0 -83
  93. package/dist/errors.d.ts.map +0 -1
  94. package/dist/errors.js +0 -142
  95. package/dist/errors.js.map +0 -1
  96. package/dist/event.d.ts +0 -45
  97. package/dist/event.d.ts.map +0 -1
  98. package/dist/event.js +0 -17
  99. package/dist/event.js.map +0 -1
  100. package/dist/fetch.d.ts +0 -15
  101. package/dist/fetch.d.ts.map +0 -1
  102. package/dist/fetch.js +0 -102
  103. package/dist/fetch.js.map +0 -1
  104. package/dist/guards.d.ts +0 -17
  105. package/dist/guards.d.ts.map +0 -1
  106. package/dist/guards.js +0 -25
  107. package/dist/guards.js.map +0 -1
  108. package/dist/health.d.ts +0 -18
  109. package/dist/health.d.ts.map +0 -1
  110. package/dist/health.js +0 -5
  111. package/dist/health.js.map +0 -1
  112. package/dist/hike.d.ts +0 -36
  113. package/dist/hike.d.ts.map +0 -1
  114. package/dist/hike.js +0 -20
  115. package/dist/hike.js.map +0 -1
  116. package/dist/index.d.ts +0 -34
  117. package/dist/index.d.ts.map +0 -1
  118. package/dist/index.js +0 -38
  119. package/dist/index.js.map +0 -1
  120. package/dist/job.d.ts +0 -24
  121. package/dist/job.d.ts.map +0 -1
  122. package/dist/job.js +0 -17
  123. package/dist/job.js.map +0 -1
  124. package/dist/layer.d.ts +0 -17
  125. package/dist/layer.d.ts.map +0 -1
  126. package/dist/layer.js +0 -21
  127. package/dist/layer.js.map +0 -1
  128. package/dist/path-security.d.ts +0 -28
  129. package/dist/path-security.d.ts.map +0 -1
  130. package/dist/path-security.js +0 -63
  131. package/dist/path-security.js.map +0 -1
  132. package/dist/patterns/bulk.d.ts +0 -15
  133. package/dist/patterns/bulk.d.ts.map +0 -1
  134. package/dist/patterns/bulk.js +0 -14
  135. package/dist/patterns/bulk.js.map +0 -1
  136. package/dist/patterns/change.d.ts +0 -10
  137. package/dist/patterns/change.d.ts.map +0 -1
  138. package/dist/patterns/change.js +0 -10
  139. package/dist/patterns/change.js.map +0 -1
  140. package/dist/patterns/date-range.d.ts +0 -10
  141. package/dist/patterns/date-range.d.ts.map +0 -1
  142. package/dist/patterns/date-range.js +0 -10
  143. package/dist/patterns/date-range.js.map +0 -1
  144. package/dist/patterns/index.d.ts +0 -9
  145. package/dist/patterns/index.d.ts.map +0 -1
  146. package/dist/patterns/index.js +0 -9
  147. package/dist/patterns/index.js.map +0 -1
  148. package/dist/patterns/pagination.d.ts +0 -18
  149. package/dist/patterns/pagination.d.ts.map +0 -1
  150. package/dist/patterns/pagination.js +0 -18
  151. package/dist/patterns/pagination.js.map +0 -1
  152. package/dist/patterns/progress.d.ts +0 -11
  153. package/dist/patterns/progress.d.ts.map +0 -1
  154. package/dist/patterns/progress.js +0 -11
  155. package/dist/patterns/progress.js.map +0 -1
  156. package/dist/patterns/sorting.d.ts +0 -13
  157. package/dist/patterns/sorting.d.ts.map +0 -1
  158. package/dist/patterns/sorting.js +0 -10
  159. package/dist/patterns/sorting.js.map +0 -1
  160. package/dist/patterns/status.d.ts +0 -15
  161. package/dist/patterns/status.d.ts.map +0 -1
  162. package/dist/patterns/status.js +0 -9
  163. package/dist/patterns/status.js.map +0 -1
  164. package/dist/patterns/timestamps.d.ts +0 -10
  165. package/dist/patterns/timestamps.d.ts.map +0 -1
  166. package/dist/patterns/timestamps.js +0 -10
  167. package/dist/patterns/timestamps.js.map +0 -1
  168. package/dist/redaction/index.d.ts +0 -4
  169. package/dist/redaction/index.d.ts.map +0 -1
  170. package/dist/redaction/index.js +0 -3
  171. package/dist/redaction/index.js.map +0 -1
  172. package/dist/redaction/patterns.d.ts +0 -9
  173. package/dist/redaction/patterns.d.ts.map +0 -1
  174. package/dist/redaction/patterns.js +0 -39
  175. package/dist/redaction/patterns.js.map +0 -1
  176. package/dist/redaction/redactor.d.ts +0 -27
  177. package/dist/redaction/redactor.d.ts.map +0 -1
  178. package/dist/redaction/redactor.js +0 -89
  179. package/dist/redaction/redactor.js.map +0 -1
  180. package/dist/resilience.d.ts +0 -34
  181. package/dist/resilience.d.ts.map +0 -1
  182. package/dist/resilience.js +0 -164
  183. package/dist/resilience.js.map +0 -1
  184. package/dist/result.d.ts +0 -57
  185. package/dist/result.d.ts.map +0 -1
  186. package/dist/result.js +0 -145
  187. package/dist/result.js.map +0 -1
  188. package/dist/serialization.d.ts +0 -27
  189. package/dist/serialization.d.ts.map +0 -1
  190. package/dist/serialization.js +0 -115
  191. package/dist/serialization.js.map +0 -1
  192. package/dist/topo.d.ts +0 -18
  193. package/dist/topo.d.ts.map +0 -1
  194. package/dist/topo.js +0 -74
  195. package/dist/topo.js.map +0 -1
  196. package/dist/trail.d.ts +0 -83
  197. package/dist/trail.d.ts.map +0 -1
  198. package/dist/trail.js +0 -16
  199. package/dist/trail.js.map +0 -1
  200. package/dist/types.d.ts +0 -46
  201. package/dist/types.d.ts.map +0 -1
  202. package/dist/types.js +0 -2
  203. package/dist/types.js.map +0 -1
  204. package/dist/validate-topo.d.ts +0 -24
  205. package/dist/validate-topo.d.ts.map +0 -1
  206. package/dist/validate-topo.js +0 -108
  207. package/dist/validate-topo.js.map +0 -1
  208. package/dist/validation.d.ts +0 -27
  209. package/dist/validation.d.ts.map +0 -1
  210. package/dist/validation.js +0 -134
  211. package/dist/validation.js.map +0 -1
  212. package/dist/workspace.d.ts +0 -25
  213. package/dist/workspace.d.ts.map +0 -1
  214. package/dist/workspace.js +0 -57
  215. package/dist/workspace.js.map +0 -1
  216. package/src/__tests__/blob-ref.test.ts +0 -103
  217. package/src/__tests__/branded.test.ts +0 -148
  218. package/src/__tests__/collections.test.ts +0 -126
  219. package/src/__tests__/context.test.ts +0 -66
  220. package/src/__tests__/derive.test.ts +0 -159
  221. package/src/__tests__/errors.test.ts +0 -309
  222. package/src/__tests__/event.test.ts +0 -82
  223. package/src/__tests__/fetch.test.ts +0 -217
  224. package/src/__tests__/guards.test.ts +0 -102
  225. package/src/__tests__/hike.test.ts +0 -117
  226. package/src/__tests__/job.test.ts +0 -98
  227. package/src/__tests__/layer.test.ts +0 -224
  228. package/src/__tests__/path-security.test.ts +0 -114
  229. package/src/__tests__/patterns.test.ts +0 -273
  230. package/src/__tests__/redaction.test.ts +0 -244
  231. package/src/__tests__/resilience.test.ts +0 -246
  232. package/src/__tests__/result.test.ts +0 -155
  233. package/src/__tests__/serialization.test.ts +0 -236
  234. package/src/__tests__/topo.test.ts +0 -184
  235. package/src/__tests__/trail.test.ts +0 -179
  236. package/src/__tests__/validate-topo.test.ts +0 -201
  237. package/src/__tests__/validation.test.ts +0 -283
  238. package/src/__tests__/workspace.test.ts +0 -183
  239. package/src/adapters.ts +0 -68
  240. package/src/event.ts +0 -77
  241. package/src/health.ts +0 -23
  242. package/src/hike.ts +0 -77
  243. package/src/job.ts +0 -20
  244. package/tsconfig.json +0 -9
  245. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,842 @@
1
+ import { z } from 'zod';
2
+
3
+ import type { AnyContour } from '../contour.js';
4
+ import {
5
+ DerivationError,
6
+ InternalError,
7
+ isTrailsError,
8
+ NotFoundError,
9
+ } from '../errors.js';
10
+ import { stripDefaultsFromShape } from '../zod-wrappers.js';
11
+ import type { AnyResource } from '../resource.js';
12
+ import { Result } from '../result.js';
13
+ import type { StoreAccessorProtocol } from '../store/accessor-protocol.js';
14
+ import { trail } from '../trail.js';
15
+ import type { Trail, TrailExample, TrailSpec } from '../trail.js';
16
+ import type { Implementation, TrailContext } from '../types.js';
17
+
18
+ /**
19
+ * CRUD-shaped operations the base trail derivation helper understands.
20
+ */
21
+ export type DeriveTrailOperation =
22
+ | 'create'
23
+ | 'read'
24
+ | 'update'
25
+ | 'delete'
26
+ | 'list';
27
+
28
+ type ContourInput<TContour extends AnyContour> = z.input<TContour>;
29
+ type ContourOutput<TContour extends AnyContour> = z.output<TContour>;
30
+ type ContourFieldKey<TContour extends AnyContour> = Extract<
31
+ keyof ContourOutput<TContour>,
32
+ string
33
+ >;
34
+ type IdentityKey<TContour extends AnyContour> = Extract<
35
+ TContour['identity'],
36
+ keyof ContourInput<TContour> & string
37
+ >;
38
+
39
+ type GeneratedKey<
40
+ TContour extends AnyContour,
41
+ TGenerated extends readonly ContourFieldKey<TContour>[] | undefined,
42
+ > = TGenerated extends readonly ContourFieldKey<TContour>[]
43
+ ? TGenerated[number]
44
+ : never;
45
+
46
+ type CreateInputOf<
47
+ TContour extends AnyContour,
48
+ TGenerated extends readonly ContourFieldKey<TContour>[] | undefined,
49
+ > = Omit<
50
+ ContourInput<TContour>,
51
+ Extract<GeneratedKey<TContour, TGenerated>, keyof ContourInput<TContour>>
52
+ >;
53
+
54
+ type ReadInputOf<TContour extends AnyContour> = Pick<
55
+ ContourInput<TContour>,
56
+ IdentityKey<TContour>
57
+ >;
58
+
59
+ type UpdateInputOf<
60
+ TContour extends AnyContour,
61
+ TGenerated extends readonly ContourFieldKey<TContour>[] | undefined,
62
+ > = ReadInputOf<TContour> &
63
+ Partial<Omit<CreateInputOf<TContour, TGenerated>, IdentityKey<TContour>>>;
64
+
65
+ type ListInputOf<TContour extends AnyContour> = Partial<ContourInput<TContour>>;
66
+
67
+ /**
68
+ * Input shape derived for one operation against one contour.
69
+ */
70
+ export type DeriveTrailInput<
71
+ TContour extends AnyContour,
72
+ TOperation extends DeriveTrailOperation,
73
+ TGenerated extends readonly ContourFieldKey<TContour>[] | undefined =
74
+ | readonly ContourFieldKey<TContour>[]
75
+ | undefined,
76
+ > = TOperation extends 'create'
77
+ ? CreateInputOf<TContour, TGenerated>
78
+ : TOperation extends 'read' | 'delete'
79
+ ? ReadInputOf<TContour>
80
+ : TOperation extends 'update'
81
+ ? UpdateInputOf<TContour, TGenerated>
82
+ : ListInputOf<TContour>;
83
+
84
+ /**
85
+ * Output shape derived for one operation against one contour.
86
+ */
87
+ export type DeriveTrailOutput<
88
+ TContour extends AnyContour,
89
+ TOperation extends DeriveTrailOperation,
90
+ > = TOperation extends 'delete'
91
+ ? undefined
92
+ : TOperation extends 'list'
93
+ ? ContourOutput<TContour>[]
94
+ : ContourOutput<TContour>;
95
+
96
+ /**
97
+ * Extra authored data accepted by `deriveTrail()` in addition to the
98
+ * operation-derived contract pieces.
99
+ *
100
+ * `blaze` is optional for single-resource calls: when omitted, the helper
101
+ * synthesizes a default blaze that delegates to the resource's accessor via
102
+ * the structural {@link StoreAccessorProtocol}. When multiple resources are
103
+ * declared, an explicit `blaze` is required.
104
+ */
105
+ export interface DeriveTrailSpec<
106
+ TContour extends AnyContour,
107
+ TOperation extends DeriveTrailOperation,
108
+ TGenerated extends readonly ContourFieldKey<TContour>[] | undefined =
109
+ | readonly ContourFieldKey<TContour>[]
110
+ | undefined,
111
+ > extends Omit<
112
+ TrailSpec<
113
+ DeriveTrailInput<TContour, TOperation, TGenerated>,
114
+ DeriveTrailOutput<TContour, TOperation>
115
+ >,
116
+ | 'blaze'
117
+ | 'contours'
118
+ | 'examples'
119
+ | 'input'
120
+ | 'intent'
121
+ | 'output'
122
+ | 'resources'
123
+ > {
124
+ /**
125
+ * Implementation of the trail. Optional for single-resource calls: when
126
+ * omitted, the helper derives a default blaze from the resource accessor
127
+ * for standard CRUD operations.
128
+ */
129
+ readonly blaze?: Implementation<
130
+ DeriveTrailInput<TContour, TOperation, TGenerated>,
131
+ DeriveTrailOutput<TContour, TOperation>
132
+ >;
133
+ /**
134
+ * Server-managed fields that should not be writable through derived create
135
+ * and update inputs.
136
+ */
137
+ readonly generated?: TGenerated;
138
+ /**
139
+ * Resource dependency declared on the derived trail. Pass a single
140
+ * resource for default-blaze synthesis, or an array for multi-resource
141
+ * trails that must provide an explicit `blaze`.
142
+ */
143
+ readonly resource: AnyResource | readonly AnyResource[];
144
+ }
145
+
146
+ const operationIntent = {
147
+ create: 'write',
148
+ delete: 'destroy',
149
+ list: 'read',
150
+ read: 'read',
151
+ update: 'write',
152
+ } as const;
153
+
154
+ const describeDeriveTrailResourceDeclaration = (
155
+ resourceCount: number
156
+ ): string =>
157
+ resourceCount === 0
158
+ ? 'no resources are declared'
159
+ : 'multiple resources are declared';
160
+
161
+ const titleCase = (value: string): string =>
162
+ value.length === 0 ? value : value.slice(0, 1).toUpperCase() + value.slice(1);
163
+
164
+ const uniqueStrings = (
165
+ values: readonly string[] | undefined
166
+ ): readonly string[] =>
167
+ Object.freeze([...(values === undefined ? [] : new Set(values))]);
168
+
169
+ const buildFieldMask = (fields: readonly string[]): Record<string, true> =>
170
+ Object.fromEntries(fields.map((field) => [field, true] as const)) as Record<
171
+ string,
172
+ true
173
+ >;
174
+
175
+ type AnyObjectSchema = z.ZodObject<Record<string, z.ZodType>>;
176
+
177
+ const asObjectSchema = (schema: z.ZodType): AnyObjectSchema =>
178
+ schema as unknown as AnyObjectSchema;
179
+
180
+ const unsupportedOperation = (operation: never): never => {
181
+ throw new DerivationError(
182
+ `Unsupported deriveTrail() operation: ${String(operation)}`
183
+ );
184
+ };
185
+
186
+ const omitFields = (
187
+ schema: z.ZodType,
188
+ fields: readonly string[]
189
+ ): AnyObjectSchema => {
190
+ const objectSchema = asObjectSchema(schema);
191
+
192
+ return fields.length === 0
193
+ ? objectSchema
194
+ : (objectSchema.omit(buildFieldMask(fields)) as unknown as AnyObjectSchema);
195
+ };
196
+
197
+ const pickFields = (
198
+ schema: z.ZodType,
199
+ fields: readonly string[]
200
+ ): AnyObjectSchema =>
201
+ asObjectSchema(schema).pick(
202
+ buildFieldMask(fields)
203
+ ) as unknown as AnyObjectSchema;
204
+
205
+ const toPartialSchema = (schema: z.ZodType): AnyObjectSchema =>
206
+ asObjectSchema(schema)
207
+ .extend(stripDefaultsFromShape(schema))
208
+ .partial() as unknown as AnyObjectSchema;
209
+
210
+ const normalizeResources = (
211
+ resource: AnyResource | readonly AnyResource[]
212
+ ): readonly AnyResource[] =>
213
+ Object.freeze(Array.isArray(resource) ? [...resource] : [resource]);
214
+
215
+ const identityInputSchema = <TContour extends AnyContour>(
216
+ contour: TContour
217
+ ): z.ZodType<ReadInputOf<TContour>> =>
218
+ pickFields(contour, [contour.identity]) as unknown as z.ZodType<
219
+ ReadInputOf<TContour>
220
+ >;
221
+
222
+ const createInputSchema = <
223
+ TContour extends AnyContour,
224
+ TGenerated extends readonly ContourFieldKey<TContour>[] | undefined,
225
+ >(
226
+ contour: TContour,
227
+ generated: readonly string[]
228
+ ): z.ZodType<CreateInputOf<TContour, TGenerated>> =>
229
+ omitFields(contour, generated) as unknown as z.ZodType<
230
+ CreateInputOf<TContour, TGenerated>
231
+ >;
232
+
233
+ const updateInputSchema = <
234
+ TContour extends AnyContour,
235
+ TGenerated extends readonly ContourFieldKey<TContour>[] | undefined,
236
+ >(
237
+ contour: TContour,
238
+ generated: readonly string[]
239
+ ): z.ZodType<UpdateInputOf<TContour, TGenerated>> => {
240
+ const mutableSchema = omitFields(contour, [...generated, contour.identity]);
241
+ const identitySchema = asObjectSchema(identityInputSchema(contour));
242
+
243
+ return identitySchema.extend(
244
+ toPartialSchema(mutableSchema).shape
245
+ ) as unknown as z.ZodType<UpdateInputOf<TContour, TGenerated>>;
246
+ };
247
+
248
+ const listInputSchema = <TContour extends AnyContour>(
249
+ contour: TContour
250
+ ): z.ZodType<ListInputOf<TContour>> =>
251
+ toPartialSchema(contour) as unknown as z.ZodType<ListInputOf<TContour>>;
252
+
253
+ const deriveInputSchema = <
254
+ TContour extends AnyContour,
255
+ TOperation extends DeriveTrailOperation,
256
+ TGenerated extends readonly ContourFieldKey<TContour>[] | undefined,
257
+ >(
258
+ contour: TContour,
259
+ operation: TOperation,
260
+ generated: readonly string[]
261
+ ): z.ZodType<DeriveTrailInput<TContour, TOperation, TGenerated>> => {
262
+ switch (operation) {
263
+ case 'create': {
264
+ return createInputSchema<TContour, TGenerated>(
265
+ contour,
266
+ generated
267
+ ) as z.ZodType<DeriveTrailInput<TContour, TOperation, TGenerated>>;
268
+ }
269
+ case 'read':
270
+ case 'delete': {
271
+ return identityInputSchema(contour) as z.ZodType<
272
+ DeriveTrailInput<TContour, TOperation, TGenerated>
273
+ >;
274
+ }
275
+ case 'update': {
276
+ return updateInputSchema<TContour, TGenerated>(
277
+ contour,
278
+ generated
279
+ ) as z.ZodType<DeriveTrailInput<TContour, TOperation, TGenerated>>;
280
+ }
281
+ case 'list': {
282
+ return listInputSchema(contour) as z.ZodType<
283
+ DeriveTrailInput<TContour, TOperation, TGenerated>
284
+ >;
285
+ }
286
+ default: {
287
+ return unsupportedOperation(operation);
288
+ }
289
+ }
290
+ };
291
+
292
+ const deriveOutputSchema = <
293
+ TContour extends AnyContour,
294
+ TOperation extends DeriveTrailOperation,
295
+ >(
296
+ contour: TContour,
297
+ operation: TOperation
298
+ ): z.ZodType<DeriveTrailOutput<TContour, TOperation>> => {
299
+ switch (operation) {
300
+ case 'delete': {
301
+ return z.void() as unknown as z.ZodType<
302
+ DeriveTrailOutput<TContour, TOperation>
303
+ >;
304
+ }
305
+ case 'list': {
306
+ return contour.array() as unknown as z.ZodType<
307
+ DeriveTrailOutput<TContour, TOperation>
308
+ >;
309
+ }
310
+ case 'create':
311
+ case 'read':
312
+ case 'update': {
313
+ return contour as unknown as z.ZodType<
314
+ DeriveTrailOutput<TContour, TOperation>
315
+ >;
316
+ }
317
+ default: {
318
+ return unsupportedOperation(operation);
319
+ }
320
+ }
321
+ };
322
+
323
+ type ExampleRecord = Readonly<Record<string, unknown>>;
324
+
325
+ const pickValueFields = (
326
+ example: ExampleRecord,
327
+ fields: readonly string[]
328
+ ): Record<string, unknown> =>
329
+ Object.fromEntries(
330
+ fields.flatMap((field) =>
331
+ Object.hasOwn(example, field) ? [[field, example[field]]] : []
332
+ )
333
+ );
334
+
335
+ const omitValueFields = (
336
+ example: ExampleRecord,
337
+ fields: readonly string[]
338
+ ): Record<string, unknown> => {
339
+ const omitted = new Set(fields);
340
+
341
+ return Object.fromEntries(
342
+ Object.entries(example).filter(([field]) => !omitted.has(field))
343
+ );
344
+ };
345
+
346
+ const formatExampleName = (
347
+ contour: AnyContour,
348
+ operation: DeriveTrailOperation,
349
+ example: ExampleRecord,
350
+ index: number
351
+ ): string => {
352
+ const identifier = example[contour.identity];
353
+ const suffix =
354
+ identifier === undefined ? String(index + 1) : String(identifier);
355
+ return `${titleCase(operation)} ${contour.name} ${suffix}`;
356
+ };
357
+
358
+ /**
359
+ * Derive a single trail example from a contour fixture.
360
+ *
361
+ * @remarks
362
+ * For `list` operations, each derived example wraps a single fixture in an
363
+ * array (`expected: [example]`) and uses the fixture's identity as input
364
+ * filters. This means the expected output is always a one-element array,
365
+ * which may not match the real accessor behavior when multiple fixtures
366
+ * share the same filter. A custom `blaze` with hand-authored examples is
367
+ * required for multi-result list assertions.
368
+ */
369
+ const deriveExample = (
370
+ contour: AnyContour,
371
+ operation: DeriveTrailOperation,
372
+ example: ExampleRecord,
373
+ index: number,
374
+ generated: readonly string[]
375
+ ): TrailExample<unknown, unknown> => {
376
+ const name = formatExampleName(contour, operation, example, index);
377
+ const identity = pickValueFields(example, [contour.identity]);
378
+
379
+ switch (operation) {
380
+ case 'create': {
381
+ return {
382
+ expected: example,
383
+ input: omitValueFields(example, generated),
384
+ name,
385
+ };
386
+ }
387
+ case 'read': {
388
+ return {
389
+ expected: example,
390
+ input: identity,
391
+ name,
392
+ };
393
+ }
394
+ case 'update': {
395
+ return {
396
+ expected: example,
397
+ input: {
398
+ ...omitValueFields(example, [...generated, contour.identity]),
399
+ ...identity,
400
+ },
401
+ name,
402
+ };
403
+ }
404
+ case 'delete': {
405
+ return {
406
+ input: identity,
407
+ name,
408
+ };
409
+ }
410
+ case 'list': {
411
+ return {
412
+ expected: [example],
413
+ input: {},
414
+ name,
415
+ };
416
+ }
417
+ default: {
418
+ return unsupportedOperation(operation);
419
+ }
420
+ }
421
+ };
422
+
423
+ const deriveExamples = (
424
+ contour: AnyContour,
425
+ operation: DeriveTrailOperation,
426
+ generated: readonly string[]
427
+ ): readonly TrailExample<unknown, unknown>[] | undefined => {
428
+ if (contour.examples === undefined || contour.examples.length === 0) {
429
+ return undefined;
430
+ }
431
+
432
+ // List examples use a single example with all fixtures in expected,
433
+ // since input: {} returns the full set from a seeded mock. Expected
434
+ // values keep generated fields (createdAt, etc.) because the mock
435
+ // populates them — stripping them would fail output schema validation.
436
+ if (operation === 'list') {
437
+ return Object.freeze([
438
+ {
439
+ expected: contour.examples,
440
+ input: {},
441
+ name: `${contour.name} list example`,
442
+ },
443
+ ]);
444
+ }
445
+
446
+ return Object.freeze(
447
+ contour.examples.map((example, index) =>
448
+ deriveExample(
449
+ contour,
450
+ operation,
451
+ example as ExampleRecord,
452
+ index,
453
+ generated
454
+ )
455
+ )
456
+ );
457
+ };
458
+
459
+ // ---------------------------------------------------------------------------
460
+ // Default-blaze synthesis
461
+ // ---------------------------------------------------------------------------
462
+
463
+ type GenericAccessor = StoreAccessorProtocol<
464
+ unknown,
465
+ unknown,
466
+ unknown,
467
+ unknown
468
+ >;
469
+
470
+ const wrapUnexpected = (
471
+ contourName: string,
472
+ operation: DeriveTrailOperation,
473
+ error: unknown
474
+ ): Error => {
475
+ if (isTrailsError(error)) {
476
+ return error;
477
+ }
478
+ const cause = error instanceof Error ? error : new Error(String(error));
479
+ return new InternalError(
480
+ `deriveTrail("${contourName}.${operation}") synthesized blaze failed: ${cause.message}`,
481
+ { cause }
482
+ );
483
+ };
484
+
485
+ const notFoundError = (contourName: string, id: unknown): NotFoundError =>
486
+ new NotFoundError(
487
+ `deriveTrail("${contourName}"): entity "${String(id)}" not found`
488
+ );
489
+
490
+ const resolveAccessor = (
491
+ contour: AnyContour,
492
+ operation: DeriveTrailOperation,
493
+ resource: AnyResource,
494
+ ctx: TrailContext
495
+ ): GenericAccessor | Error => {
496
+ try {
497
+ const connection = resource.from(ctx) as
498
+ | Readonly<Record<string, GenericAccessor>>
499
+ | undefined;
500
+ if (connection === undefined || connection === null) {
501
+ return new InternalError(
502
+ `deriveTrail("${contour.name}.${operation}"): resource "${resource.id}" produced no connection`
503
+ );
504
+ }
505
+ const accessor = connection[contour.name];
506
+ if (accessor === undefined) {
507
+ return new InternalError(
508
+ `deriveTrail("${contour.name}.${operation}"): resource "${resource.id}" does not expose an accessor for "${contour.name}"`
509
+ );
510
+ }
511
+ return accessor;
512
+ } catch (error) {
513
+ return wrapUnexpected(contour.name, operation, error);
514
+ }
515
+ };
516
+
517
+ const extractIdentity = (contour: AnyContour, input: unknown): unknown => {
518
+ const record = input as Record<string, unknown>;
519
+ return record[contour.identity];
520
+ };
521
+
522
+ const callRead = async (
523
+ contour: AnyContour,
524
+ accessor: GenericAccessor,
525
+ input: unknown
526
+ ): Promise<Result<unknown, Error>> => {
527
+ if (typeof accessor.get !== 'function') {
528
+ return Result.err(
529
+ new InternalError(
530
+ `deriveTrail("${contour.name}.read"): accessor is missing a \`get\` method`
531
+ )
532
+ );
533
+ }
534
+ try {
535
+ const id = extractIdentity(contour, input);
536
+ const entity = await accessor.get(id);
537
+ if (entity === null || entity === undefined) {
538
+ return Result.err(notFoundError(contour.name, id));
539
+ }
540
+ return Result.ok(entity);
541
+ } catch (error) {
542
+ return Result.err(wrapUnexpected(contour.name, 'read', error));
543
+ }
544
+ };
545
+
546
+ const callCreate = async (
547
+ contour: AnyContour,
548
+ accessor: GenericAccessor,
549
+ input: unknown,
550
+ ctx: TrailContext
551
+ ): Promise<Result<unknown, Error>> => {
552
+ try {
553
+ if (typeof accessor.insert === 'function') {
554
+ const created = await accessor.insert(input);
555
+ return Result.ok(created);
556
+ }
557
+
558
+ // Fallback: tabular contract allows `upsert` when `insert` is absent.
559
+ // The warden flags this at build time via a pattern rule (trl-251).
560
+ if (typeof accessor.upsert !== 'function') {
561
+ return Result.err(
562
+ new InternalError(
563
+ `deriveTrail("${contour.name}.create"): accessor is missing both \`insert\` and \`upsert\``
564
+ )
565
+ );
566
+ }
567
+ ctx.logger?.debug(
568
+ `deriveTrail("${contour.name}.create"): accessor has no \`insert\`; falling back to \`upsert\``
569
+ );
570
+ const created = await accessor.upsert(input);
571
+ return Result.ok(created);
572
+ } catch (error) {
573
+ return Result.err(wrapUnexpected(contour.name, 'create', error));
574
+ }
575
+ };
576
+
577
+ /**
578
+ * Strip framework-managed generated fields from a merged payload so that
579
+ * the update-via-upsert fallback doesn't carry stale managed values.
580
+ *
581
+ * Only strips fields that appear in the `generated` array — user-defined
582
+ * fields with the same name (e.g. an API `version` string) are preserved.
583
+ */
584
+ const stripGeneratedFields = (
585
+ payload: Record<string, unknown>,
586
+ generated: readonly string[],
587
+ identity: string
588
+ ): Record<string, unknown> => {
589
+ if (generated.length === 0) {
590
+ return payload;
591
+ }
592
+ const managedKeys = new Set(generated);
593
+ return Object.fromEntries(
594
+ Object.entries(payload).filter(
595
+ ([key]) => key === identity || !managedKeys.has(key)
596
+ )
597
+ );
598
+ };
599
+
600
+ /**
601
+ * Fallback for accessors that lack a native `update`: read the current
602
+ * entity, merge the patch, strip any `version` field so versioned tables
603
+ * keep `update`'s "does not participate in optimistic concurrency" semantic,
604
+ * then `upsert`.
605
+ */
606
+ const updateViaReadAndUpsert = async (
607
+ contour: AnyContour,
608
+ accessor: GenericAccessor,
609
+ id: unknown,
610
+ patch: Record<string, unknown>,
611
+ generated: readonly string[]
612
+ ): Promise<Result<unknown, Error>> => {
613
+ if (typeof accessor.get !== 'function') {
614
+ return Result.err(
615
+ new InternalError(
616
+ `deriveTrail("${contour.name}.update"): accessor is missing both \`update\` and \`get\``
617
+ )
618
+ );
619
+ }
620
+ if (typeof accessor.upsert !== 'function') {
621
+ return Result.err(
622
+ new InternalError(
623
+ `deriveTrail("${contour.name}.update"): accessor is missing both \`update\` and \`upsert\``
624
+ )
625
+ );
626
+ }
627
+ const current = await accessor.get(id);
628
+ if (current === null || current === undefined) {
629
+ return Result.err(notFoundError(contour.name, id));
630
+ }
631
+ const merged = stripGeneratedFields(
632
+ { ...(current as Record<string, unknown>), ...patch },
633
+ generated,
634
+ contour.identity
635
+ );
636
+ const updated = await accessor.upsert(merged);
637
+ return Result.ok(updated);
638
+ };
639
+
640
+ const callUpdate = async (
641
+ contour: AnyContour,
642
+ accessor: GenericAccessor,
643
+ input: unknown,
644
+ generated: readonly string[]
645
+ ): Promise<Result<unknown, Error>> => {
646
+ const id = extractIdentity(contour, input);
647
+ const patch = Object.fromEntries(
648
+ Object.entries(input as Record<string, unknown>).filter(
649
+ ([field]) => field !== contour.identity
650
+ )
651
+ );
652
+
653
+ try {
654
+ if (typeof accessor.update === 'function') {
655
+ const updated = await accessor.update(id, patch);
656
+ if (updated === null || updated === undefined) {
657
+ return Result.err(notFoundError(contour.name, id));
658
+ }
659
+ return Result.ok(updated);
660
+ }
661
+ return await updateViaReadAndUpsert(
662
+ contour,
663
+ accessor,
664
+ id,
665
+ patch,
666
+ generated
667
+ );
668
+ } catch (error) {
669
+ return Result.err(wrapUnexpected(contour.name, 'update', error));
670
+ }
671
+ };
672
+
673
+ const callDelete = async (
674
+ contour: AnyContour,
675
+ accessor: GenericAccessor,
676
+ input: unknown
677
+ ): Promise<Result<undefined, Error>> => {
678
+ if (typeof accessor.remove !== 'function') {
679
+ return Result.err(
680
+ new InternalError(
681
+ `deriveTrail("${contour.name}.delete"): accessor is missing a \`remove\` method`
682
+ )
683
+ );
684
+ }
685
+ try {
686
+ const id = extractIdentity(contour, input);
687
+ await accessor.remove(id);
688
+ // `{ deleted: false }` is a no-op on an absent row, not an error —
689
+ // matches the accessor's documented semantic.
690
+ return Result.ok();
691
+ } catch (error) {
692
+ return Result.err(wrapUnexpected(contour.name, 'delete', error));
693
+ }
694
+ };
695
+
696
+ /**
697
+ * Default `list` synthesis passes the entire input as the filter bag. The
698
+ * derived input type is `Partial<ContourInput>` which matches the accessor's
699
+ * filter shape field-for-field. Pagination controls are not derived — callers
700
+ * that need pagination must provide an explicit blaze.
701
+ */
702
+ const callList = async (
703
+ contour: AnyContour,
704
+ accessor: GenericAccessor,
705
+ input: unknown
706
+ ): Promise<Result<unknown[], Error>> => {
707
+ if (typeof accessor.list !== 'function') {
708
+ return Result.err(
709
+ new InternalError(
710
+ `deriveTrail("${contour.name}.list"): accessor is missing a \`list\` method`
711
+ )
712
+ );
713
+ }
714
+ try {
715
+ const listed = await accessor.list(input);
716
+ return Result.ok([...listed]);
717
+ } catch (error) {
718
+ return Result.err(wrapUnexpected(contour.name, 'list', error));
719
+ }
720
+ };
721
+
722
+ const synthesizeDefaultBlaze = <
723
+ TContour extends AnyContour,
724
+ TOperation extends DeriveTrailOperation,
725
+ TGenerated extends readonly ContourFieldKey<TContour>[] | undefined,
726
+ >(
727
+ contour: TContour,
728
+ operation: TOperation,
729
+ resource: AnyResource,
730
+ generated: readonly string[]
731
+ ): Implementation<
732
+ DeriveTrailInput<TContour, TOperation, TGenerated>,
733
+ DeriveTrailOutput<TContour, TOperation>
734
+ > => {
735
+ const impl: Implementation<unknown, unknown> = (input, ctx) => {
736
+ const accessor = resolveAccessor(contour, operation, resource, ctx);
737
+ if (accessor instanceof Error) {
738
+ return Promise.resolve(Result.err(accessor));
739
+ }
740
+ switch (operation) {
741
+ case 'create': {
742
+ return callCreate(contour, accessor, input, ctx);
743
+ }
744
+ case 'read': {
745
+ return callRead(contour, accessor, input);
746
+ }
747
+ case 'update': {
748
+ return callUpdate(contour, accessor, input, generated);
749
+ }
750
+ case 'delete': {
751
+ return callDelete(contour, accessor, input);
752
+ }
753
+ case 'list': {
754
+ return callList(contour, accessor, input);
755
+ }
756
+ default: {
757
+ return unsupportedOperation(operation);
758
+ }
759
+ }
760
+ };
761
+
762
+ return impl as Implementation<
763
+ DeriveTrailInput<TContour, TOperation, TGenerated>,
764
+ DeriveTrailOutput<TContour, TOperation>
765
+ >;
766
+ };
767
+
768
+ /**
769
+ * Mechanically project one CRUD-shaped trail from a contour declaration.
770
+ *
771
+ * When `spec.blaze` is omitted and the call declares a single resource, the
772
+ * helper derives a default blaze that dispatches to the resource accessor
773
+ * through the structural {@link StoreAccessorProtocol}. Multi-resource calls
774
+ * must supply an explicit blaze and are rejected with {@link DerivationError}
775
+ * at construction time when they do not.
776
+ */
777
+ export const deriveTrail = <
778
+ TContour extends AnyContour,
779
+ TOperation extends DeriveTrailOperation,
780
+ TGenerated extends readonly ContourFieldKey<TContour>[] | undefined =
781
+ | readonly ContourFieldKey<TContour>[]
782
+ | undefined,
783
+ >(
784
+ contour: TContour,
785
+ operation: TOperation,
786
+ spec: DeriveTrailSpec<TContour, TOperation, TGenerated>
787
+ ): Trail<
788
+ DeriveTrailInput<TContour, TOperation, TGenerated>,
789
+ DeriveTrailOutput<TContour, TOperation>
790
+ > => {
791
+ const resources = normalizeResources(spec.resource);
792
+ const generated = uniqueStrings(
793
+ spec.generated as readonly string[] | undefined
794
+ );
795
+
796
+ let blaze: Implementation<
797
+ DeriveTrailInput<TContour, TOperation, TGenerated>,
798
+ DeriveTrailOutput<TContour, TOperation>
799
+ >;
800
+ if (typeof spec.blaze === 'function') {
801
+ ({ blaze } = spec);
802
+ } else if (resources.length === 1) {
803
+ blaze = synthesizeDefaultBlaze<TContour, TOperation, TGenerated>(
804
+ contour,
805
+ operation,
806
+ resources[0] as AnyResource,
807
+ generated
808
+ );
809
+ } else {
810
+ throw new DerivationError(
811
+ `deriveTrail("${contour.name}.${operation}") requires an explicit \`blaze\` when ${describeDeriveTrailResourceDeclaration(resources.length)} — default synthesis is single-resource only`
812
+ );
813
+ }
814
+ const {
815
+ blaze: _blaze,
816
+ resource: _resource,
817
+ generated: _generated,
818
+ ...trailSpec
819
+ } = spec;
820
+ const derivedSpec = {
821
+ ...trailSpec,
822
+ blaze,
823
+ contours: [contour],
824
+ examples: deriveExamples(contour, operation, generated),
825
+ input: deriveInputSchema<TContour, TOperation, TGenerated>(
826
+ contour,
827
+ operation,
828
+ generated
829
+ ),
830
+ intent: operationIntent[operation],
831
+ output: deriveOutputSchema(contour, operation),
832
+ resources,
833
+ } as unknown as TrailSpec<
834
+ DeriveTrailInput<TContour, TOperation, TGenerated>,
835
+ DeriveTrailOutput<TContour, TOperation>
836
+ >;
837
+
838
+ return trail(`${contour.name}.${operation}`, derivedSpec) as unknown as Trail<
839
+ DeriveTrailInput<TContour, TOperation, TGenerated>,
840
+ DeriveTrailOutput<TContour, TOperation>
841
+ >;
842
+ };