@ontrails/core 1.0.0-beta.15 → 1.0.0-beta.16

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 (348) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +61 -16
  3. package/package.json +9 -8
  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/context.ts +4 -0
  10. package/src/{internal/cross-batch.ts → cross-batch.ts} +3 -11
  11. package/src/detours.ts +8 -0
  12. package/src/draft.ts +4 -0
  13. package/src/error-projection.ts +51 -0
  14. package/src/errors.ts +192 -39
  15. package/src/execute.ts +443 -44
  16. package/src/fire.ts +965 -91
  17. package/src/index.ts +285 -38
  18. package/src/layer-projection.ts +192 -0
  19. package/src/layer.ts +32 -1
  20. package/src/observe.ts +361 -0
  21. package/src/permits.ts +12 -0
  22. package/src/redaction/patterns.ts +6 -3
  23. package/src/resilience.ts +8 -8
  24. package/src/resource-config.ts +477 -44
  25. package/src/resource.ts +21 -1
  26. package/src/result.ts +18 -0
  27. package/src/run.ts +2 -2
  28. package/src/schedule-runtime.ts +689 -0
  29. package/src/schedule.ts +324 -0
  30. package/src/serialization.ts +90 -57
  31. package/src/signal-diagnostics.ts +633 -0
  32. package/src/{internal/signal-ref.ts → signal-ref.ts} +1 -1
  33. package/src/signal.ts +37 -0
  34. package/src/store/accessor-protocol.ts +5 -5
  35. package/src/structured-examples.ts +245 -0
  36. package/src/surface-derivation.ts +91 -0
  37. package/src/surface-filter.ts +7 -13
  38. package/src/topo.ts +386 -6
  39. package/src/tracing.ts +345 -0
  40. package/src/trail.ts +168 -22
  41. package/src/trails/derive-trail.ts +1 -1
  42. package/src/trails/ingest.ts +7 -15
  43. package/src/{internal/trails-db.ts → trails-db.ts} +42 -21
  44. package/src/transport-error-map.ts +146 -28
  45. package/src/types.ts +82 -28
  46. package/src/validate-established-topo.ts +6 -0
  47. package/src/validate-topo.ts +212 -5
  48. package/src/validation.ts +58 -1
  49. package/src/webhook.ts +276 -0
  50. package/src/{internal/zod-wrappers.ts → zod-wrappers.ts} +0 -6
  51. package/.turbo/turbo-build.log +0 -1
  52. package/.turbo/turbo-lint.log +0 -3
  53. package/.turbo/turbo-typecheck.log +0 -1
  54. package/dist/blob-ref.d.ts +0 -20
  55. package/dist/blob-ref.d.ts.map +0 -1
  56. package/dist/blob-ref.js +0 -22
  57. package/dist/blob-ref.js.map +0 -1
  58. package/dist/branded.d.ts +0 -36
  59. package/dist/branded.d.ts.map +0 -1
  60. package/dist/branded.js +0 -89
  61. package/dist/branded.js.map +0 -1
  62. package/dist/collections.d.ts +0 -31
  63. package/dist/collections.d.ts.map +0 -1
  64. package/dist/collections.js +0 -60
  65. package/dist/collections.js.map +0 -1
  66. package/dist/context.d.ts +0 -22
  67. package/dist/context.d.ts.map +0 -1
  68. package/dist/context.js +0 -37
  69. package/dist/context.js.map +0 -1
  70. package/dist/contour.d.ts +0 -81
  71. package/dist/contour.d.ts.map +0 -1
  72. package/dist/contour.js +0 -176
  73. package/dist/contour.js.map +0 -1
  74. package/dist/cross-schema.d.ts +0 -18
  75. package/dist/cross-schema.d.ts.map +0 -1
  76. package/dist/cross-schema.js +0 -29
  77. package/dist/cross-schema.js.map +0 -1
  78. package/dist/derive.d.ts +0 -44
  79. package/dist/derive.d.ts.map +0 -1
  80. package/dist/derive.js +0 -149
  81. package/dist/derive.js.map +0 -1
  82. package/dist/draft.d.ts +0 -28
  83. package/dist/draft.d.ts.map +0 -1
  84. package/dist/draft.js +0 -157
  85. package/dist/draft.js.map +0 -1
  86. package/dist/errors.d.ts +0 -141
  87. package/dist/errors.d.ts.map +0 -1
  88. package/dist/errors.js +0 -185
  89. package/dist/errors.js.map +0 -1
  90. package/dist/execute.d.ts +0 -52
  91. package/dist/execute.d.ts.map +0 -1
  92. package/dist/execute.js +0 -511
  93. package/dist/execute.js.map +0 -1
  94. package/dist/fetch.d.ts +0 -15
  95. package/dist/fetch.d.ts.map +0 -1
  96. package/dist/fetch.js +0 -102
  97. package/dist/fetch.js.map +0 -1
  98. package/dist/fire.d.ts +0 -43
  99. package/dist/fire.d.ts.map +0 -1
  100. package/dist/fire.js +0 -185
  101. package/dist/fire.js.map +0 -1
  102. package/dist/guards.d.ts +0 -17
  103. package/dist/guards.d.ts.map +0 -1
  104. package/dist/guards.js +0 -25
  105. package/dist/guards.js.map +0 -1
  106. package/dist/index.d.ts +0 -59
  107. package/dist/index.d.ts.map +0 -1
  108. package/dist/index.js +0 -61
  109. package/dist/index.js.map +0 -1
  110. package/dist/internal/cross-batch.d.ts +0 -44
  111. package/dist/internal/cross-batch.d.ts.map +0 -1
  112. package/dist/internal/cross-batch.js +0 -58
  113. package/dist/internal/cross-batch.js.map +0 -1
  114. package/dist/internal/fork-ctx.d.ts +0 -41
  115. package/dist/internal/fork-ctx.d.ts.map +0 -1
  116. package/dist/internal/fork-ctx.js +0 -47
  117. package/dist/internal/fork-ctx.js.map +0 -1
  118. package/dist/internal/signal-ref.d.ts +0 -15
  119. package/dist/internal/signal-ref.d.ts.map +0 -1
  120. package/dist/internal/signal-ref.js +0 -41
  121. package/dist/internal/signal-ref.js.map +0 -1
  122. package/dist/internal/topo-saves.d.ts +0 -53
  123. package/dist/internal/topo-saves.d.ts.map +0 -1
  124. package/dist/internal/topo-saves.js +0 -449
  125. package/dist/internal/topo-saves.js.map +0 -1
  126. package/dist/internal/topo-snapshots.d.ts +0 -57
  127. package/dist/internal/topo-snapshots.d.ts.map +0 -1
  128. package/dist/internal/topo-snapshots.js +0 -372
  129. package/dist/internal/topo-snapshots.js.map +0 -1
  130. package/dist/internal/topo-store-read.d.ts +0 -70
  131. package/dist/internal/topo-store-read.d.ts.map +0 -1
  132. package/dist/internal/topo-store-read.js +0 -221
  133. package/dist/internal/topo-store-read.js.map +0 -1
  134. package/dist/internal/topo-store.d.ts +0 -26
  135. package/dist/internal/topo-store.d.ts.map +0 -1
  136. package/dist/internal/topo-store.js +0 -621
  137. package/dist/internal/topo-store.js.map +0 -1
  138. package/dist/internal/tracing.d.ts +0 -98
  139. package/dist/internal/tracing.d.ts.map +0 -1
  140. package/dist/internal/tracing.js +0 -102
  141. package/dist/internal/tracing.js.map +0 -1
  142. package/dist/internal/trails-db.d.ts +0 -16
  143. package/dist/internal/trails-db.d.ts.map +0 -1
  144. package/dist/internal/trails-db.js +0 -118
  145. package/dist/internal/trails-db.js.map +0 -1
  146. package/dist/internal/zod-wrappers.d.ts +0 -40
  147. package/dist/internal/zod-wrappers.d.ts.map +0 -1
  148. package/dist/internal/zod-wrappers.js +0 -59
  149. package/dist/internal/zod-wrappers.js.map +0 -1
  150. package/dist/layer.d.ts +0 -23
  151. package/dist/layer.d.ts.map +0 -1
  152. package/dist/layer.js +0 -21
  153. package/dist/layer.js.map +0 -1
  154. package/dist/path-security.d.ts +0 -28
  155. package/dist/path-security.d.ts.map +0 -1
  156. package/dist/path-security.js +0 -63
  157. package/dist/path-security.js.map +0 -1
  158. package/dist/patterns/bulk.d.ts +0 -15
  159. package/dist/patterns/bulk.d.ts.map +0 -1
  160. package/dist/patterns/bulk.js +0 -14
  161. package/dist/patterns/bulk.js.map +0 -1
  162. package/dist/patterns/change.d.ts +0 -10
  163. package/dist/patterns/change.d.ts.map +0 -1
  164. package/dist/patterns/change.js +0 -10
  165. package/dist/patterns/change.js.map +0 -1
  166. package/dist/patterns/date-range.d.ts +0 -10
  167. package/dist/patterns/date-range.d.ts.map +0 -1
  168. package/dist/patterns/date-range.js +0 -10
  169. package/dist/patterns/date-range.js.map +0 -1
  170. package/dist/patterns/index.d.ts +0 -9
  171. package/dist/patterns/index.d.ts.map +0 -1
  172. package/dist/patterns/index.js +0 -9
  173. package/dist/patterns/index.js.map +0 -1
  174. package/dist/patterns/pagination.d.ts +0 -18
  175. package/dist/patterns/pagination.d.ts.map +0 -1
  176. package/dist/patterns/pagination.js +0 -18
  177. package/dist/patterns/pagination.js.map +0 -1
  178. package/dist/patterns/progress.d.ts +0 -11
  179. package/dist/patterns/progress.d.ts.map +0 -1
  180. package/dist/patterns/progress.js +0 -11
  181. package/dist/patterns/progress.js.map +0 -1
  182. package/dist/patterns/sorting.d.ts +0 -13
  183. package/dist/patterns/sorting.d.ts.map +0 -1
  184. package/dist/patterns/sorting.js +0 -10
  185. package/dist/patterns/sorting.js.map +0 -1
  186. package/dist/patterns/status.d.ts +0 -15
  187. package/dist/patterns/status.d.ts.map +0 -1
  188. package/dist/patterns/status.js +0 -9
  189. package/dist/patterns/status.js.map +0 -1
  190. package/dist/patterns/timestamps.d.ts +0 -10
  191. package/dist/patterns/timestamps.d.ts.map +0 -1
  192. package/dist/patterns/timestamps.js +0 -10
  193. package/dist/patterns/timestamps.js.map +0 -1
  194. package/dist/redaction/index.d.ts +0 -4
  195. package/dist/redaction/index.d.ts.map +0 -1
  196. package/dist/redaction/index.js +0 -3
  197. package/dist/redaction/index.js.map +0 -1
  198. package/dist/redaction/patterns.d.ts +0 -9
  199. package/dist/redaction/patterns.d.ts.map +0 -1
  200. package/dist/redaction/patterns.js +0 -39
  201. package/dist/redaction/patterns.js.map +0 -1
  202. package/dist/redaction/redactor.d.ts +0 -27
  203. package/dist/redaction/redactor.d.ts.map +0 -1
  204. package/dist/redaction/redactor.js +0 -89
  205. package/dist/redaction/redactor.js.map +0 -1
  206. package/dist/resilience.d.ts +0 -34
  207. package/dist/resilience.d.ts.map +0 -1
  208. package/dist/resilience.js +0 -164
  209. package/dist/resilience.js.map +0 -1
  210. package/dist/resource-config.d.ts +0 -22
  211. package/dist/resource-config.d.ts.map +0 -1
  212. package/dist/resource-config.js +0 -210
  213. package/dist/resource-config.js.map +0 -1
  214. package/dist/resource.d.ts +0 -74
  215. package/dist/resource.d.ts.map +0 -1
  216. package/dist/resource.js +0 -61
  217. package/dist/resource.js.map +0 -1
  218. package/dist/result.d.ts +0 -57
  219. package/dist/result.d.ts.map +0 -1
  220. package/dist/result.js +0 -156
  221. package/dist/result.js.map +0 -1
  222. package/dist/run.d.ts +0 -28
  223. package/dist/run.d.ts.map +0 -1
  224. package/dist/run.js +0 -35
  225. package/dist/run.js.map +0 -1
  226. package/dist/serialization.d.ts +0 -27
  227. package/dist/serialization.d.ts.map +0 -1
  228. package/dist/serialization.js +0 -154
  229. package/dist/serialization.js.map +0 -1
  230. package/dist/signal.d.ts +0 -33
  231. package/dist/signal.d.ts.map +0 -1
  232. package/dist/signal.js +0 -17
  233. package/dist/signal.js.map +0 -1
  234. package/dist/store/accessor-protocol.d.ts +0 -51
  235. package/dist/store/accessor-protocol.d.ts.map +0 -1
  236. package/dist/store/accessor-protocol.js +0 -2
  237. package/dist/store/accessor-protocol.js.map +0 -1
  238. package/dist/store/index.d.ts +0 -2
  239. package/dist/store/index.d.ts.map +0 -1
  240. package/dist/store/index.js +0 -2
  241. package/dist/store/index.js.map +0 -1
  242. package/dist/surface-filter.d.ts +0 -13
  243. package/dist/surface-filter.d.ts.map +0 -1
  244. package/dist/surface-filter.js +0 -76
  245. package/dist/surface-filter.js.map +0 -1
  246. package/dist/topo-store.d.ts +0 -62
  247. package/dist/topo-store.d.ts.map +0 -1
  248. package/dist/topo-store.js +0 -319
  249. package/dist/topo-store.js.map +0 -1
  250. package/dist/topo.d.ts +0 -39
  251. package/dist/topo.d.ts.map +0 -1
  252. package/dist/topo.js +0 -241
  253. package/dist/topo.js.map +0 -1
  254. package/dist/trail.d.ts +0 -163
  255. package/dist/trail.d.ts.map +0 -1
  256. package/dist/trail.js +0 -75
  257. package/dist/trail.js.map +0 -1
  258. package/dist/trails/derive-trail.d.ts +0 -66
  259. package/dist/trails/derive-trail.d.ts.map +0 -1
  260. package/dist/trails/derive-trail.js +0 -378
  261. package/dist/trails/derive-trail.js.map +0 -1
  262. package/dist/trails/index.d.ts +0 -5
  263. package/dist/trails/index.d.ts.map +0 -1
  264. package/dist/trails/index.js +0 -3
  265. package/dist/trails/index.js.map +0 -1
  266. package/dist/trails/ingest.d.ts +0 -24
  267. package/dist/trails/ingest.d.ts.map +0 -1
  268. package/dist/trails/ingest.js +0 -63
  269. package/dist/trails/ingest.js.map +0 -1
  270. package/dist/transport-error-map.d.ts +0 -106
  271. package/dist/transport-error-map.d.ts.map +0 -1
  272. package/dist/transport-error-map.js +0 -24
  273. package/dist/transport-error-map.js.map +0 -1
  274. package/dist/type-checks.test-d.d.ts +0 -71
  275. package/dist/type-checks.test-d.d.ts.map +0 -1
  276. package/dist/type-checks.test-d.js +0 -12
  277. package/dist/type-checks.test-d.js.map +0 -1
  278. package/dist/type-utils.d.ts +0 -33
  279. package/dist/type-utils.d.ts.map +0 -1
  280. package/dist/type-utils.js +0 -12
  281. package/dist/type-utils.js.map +0 -1
  282. package/dist/types.d.ts +0 -177
  283. package/dist/types.d.ts.map +0 -1
  284. package/dist/types.js +0 -3
  285. package/dist/types.js.map +0 -1
  286. package/dist/validate-established-topo.d.ts +0 -76
  287. package/dist/validate-established-topo.d.ts.map +0 -1
  288. package/dist/validate-established-topo.js +0 -43
  289. package/dist/validate-established-topo.js.map +0 -1
  290. package/dist/validate-topo.d.ts +0 -24
  291. package/dist/validate-topo.d.ts.map +0 -1
  292. package/dist/validate-topo.js +0 -193
  293. package/dist/validate-topo.js.map +0 -1
  294. package/dist/validation.d.ts +0 -27
  295. package/dist/validation.d.ts.map +0 -1
  296. package/dist/validation.js +0 -165
  297. package/dist/validation.js.map +0 -1
  298. package/dist/workspace.d.ts +0 -25
  299. package/dist/workspace.d.ts.map +0 -1
  300. package/dist/workspace.js +0 -57
  301. package/dist/workspace.js.map +0 -1
  302. package/src/__tests__/blob-ref.test.ts +0 -103
  303. package/src/__tests__/branded.test.ts +0 -148
  304. package/src/__tests__/collections.test.ts +0 -126
  305. package/src/__tests__/context.test.ts +0 -77
  306. package/src/__tests__/contour.test.ts +0 -263
  307. package/src/__tests__/derive-trail.test.ts +0 -668
  308. package/src/__tests__/derive.test.ts +0 -260
  309. package/src/__tests__/errors.test.ts +0 -427
  310. package/src/__tests__/execute.test.ts +0 -1563
  311. package/src/__tests__/fetch.test.ts +0 -217
  312. package/src/__tests__/fire.test.ts +0 -1056
  313. package/src/__tests__/fork-ctx.test.ts +0 -83
  314. package/src/__tests__/guards.test.ts +0 -102
  315. package/src/__tests__/ingest.test.ts +0 -191
  316. package/src/__tests__/layer.test.ts +0 -125
  317. package/src/__tests__/path-security.test.ts +0 -114
  318. package/src/__tests__/patterns.test.ts +0 -273
  319. package/src/__tests__/redaction.test.ts +0 -244
  320. package/src/__tests__/resilience.test.ts +0 -246
  321. package/src/__tests__/resource.test.ts +0 -204
  322. package/src/__tests__/result.test.ts +0 -155
  323. package/src/__tests__/run.test.ts +0 -181
  324. package/src/__tests__/serialization.test.ts +0 -401
  325. package/src/__tests__/service-config.test.ts +0 -224
  326. package/src/__tests__/signal.test.ts +0 -82
  327. package/src/__tests__/surface-filter.test.ts +0 -196
  328. package/src/__tests__/topo-store-read.test.ts +0 -256
  329. package/src/__tests__/topo-store.test.ts +0 -869
  330. package/src/__tests__/topo.test.ts +0 -573
  331. package/src/__tests__/trail-permit.test.ts +0 -60
  332. package/src/__tests__/trail.test.ts +0 -426
  333. package/src/__tests__/trails-db.test.ts +0 -198
  334. package/src/__tests__/transport-error-map.test.ts +0 -82
  335. package/src/__tests__/type-utils.test.ts +0 -118
  336. package/src/__tests__/validate-topo.test.ts +0 -611
  337. package/src/__tests__/validation.test.ts +0 -336
  338. package/src/__tests__/workspace.test.ts +0 -183
  339. package/src/__tests__/zod-wrappers.test.ts +0 -62
  340. package/src/internal/topo-snapshots.ts +0 -517
  341. package/src/internal/topo-store-read.ts +0 -481
  342. package/src/internal/topo-store.ts +0 -1192
  343. package/src/internal/tracing.ts +0 -187
  344. package/src/topo-store.ts +0 -510
  345. package/src/type-checks.test-d.ts +0 -106
  346. package/tsconfig.json +0 -9
  347. package/tsconfig.tests.json +0 -10
  348. package/tsconfig.tsbuildinfo +0 -1
@@ -1,668 +0,0 @@
1
- import { describe, expect, mock, test } from 'bun:test';
2
-
3
- import { z } from 'zod';
4
-
5
- import { contour } from '../contour.js';
6
- import { createTrailContext } from '../context.js';
7
- import { DerivationError, InternalError, NotFoundError } from '../errors.js';
8
- import { Result } from '../result.js';
9
- import { resource } from '../resource.js';
10
- import type { DeriveTrailOperation } from '../trails/index.js';
11
- import { deriveTrail } from '../trails/index.js';
12
-
13
- const note = contour(
14
- 'note',
15
- {
16
- body: z.string().default('draft'),
17
- createdAt: z.string(),
18
- id: z.string(),
19
- title: z.string(),
20
- },
21
- {
22
- examples: [
23
- {
24
- body: 'Hello, Trails',
25
- createdAt: '2026-04-10T12:00:00.000Z',
26
- id: 'note-1',
27
- title: 'First note',
28
- },
29
- ],
30
- identity: 'id',
31
- }
32
- );
33
-
34
- const noteResource = resource('db.notes', {
35
- create: () =>
36
- Result.ok({
37
- name: 'notes',
38
- }),
39
- });
40
-
41
- const noteExample = note.examples?.[0];
42
-
43
- const createBlaze = () => Result.ok(noteExample);
44
- const deleteBlaze = () => Result.ok();
45
- const listBlaze = () => Result.ok([noteExample]);
46
-
47
- const unsupportedOperation = (operation: never): never => {
48
- throw new TypeError(`Unsupported test operation: ${String(operation)}`);
49
- };
50
-
51
- const deriveNoteTrail = <TOperation extends DeriveTrailOperation>(
52
- operation: TOperation
53
- ) => {
54
- switch (operation) {
55
- case 'create': {
56
- return deriveTrail(note, operation, {
57
- blaze: createBlaze,
58
- generated: ['id', 'createdAt'],
59
- resource: noteResource,
60
- });
61
- }
62
- case 'read': {
63
- return deriveTrail(note, operation, {
64
- blaze: createBlaze,
65
- resource: noteResource,
66
- });
67
- }
68
- case 'update': {
69
- return deriveTrail(note, operation, {
70
- blaze: createBlaze,
71
- generated: ['id', 'createdAt'],
72
- resource: noteResource,
73
- });
74
- }
75
- case 'delete': {
76
- return deriveTrail(note, operation, {
77
- blaze: deleteBlaze,
78
- resource: noteResource,
79
- });
80
- }
81
- case 'list': {
82
- return deriveTrail(note, operation, {
83
- blaze: listBlaze,
84
- resource: noteResource,
85
- });
86
- }
87
- default: {
88
- return unsupportedOperation(operation);
89
- }
90
- }
91
- };
92
-
93
- const deriveCrudTrails = () => ({
94
- created: deriveNoteTrail('create'),
95
- listed: deriveNoteTrail('list'),
96
- read: deriveNoteTrail('read'),
97
- removed: deriveNoteTrail('delete'),
98
- updated: deriveNoteTrail('update'),
99
- });
100
-
101
- const expectTrailMetadata = (
102
- trail: ReturnType<typeof deriveNoteTrail>,
103
- expectedId: string,
104
- expectedIntent: 'destroy' | 'read' | 'write'
105
- ) => {
106
- expect(trail.id).toBe(expectedId);
107
- expect(trail.intent).toBe(expectedIntent);
108
- };
109
-
110
- const expectCreateSchemas = (
111
- created: ReturnType<typeof deriveNoteTrail<'create'>>
112
- ) => {
113
- expect(
114
- created.input.safeParse({ body: 'Body', title: 'Created note' }).success
115
- ).toBe(true);
116
- expect(
117
- created.input.parse({
118
- createdAt: '2026-04-10T12:00:00.000Z',
119
- id: 'note-1',
120
- title: 'Created note',
121
- })
122
- ).toEqual({ body: 'draft', title: 'Created note' });
123
- expect(created.output?.safeParse(note.examples?.[0]).success).toBe(true);
124
- };
125
-
126
- const expectReadSchemas = (
127
- read: ReturnType<typeof deriveNoteTrail<'read'>>
128
- ) => {
129
- expect(read.input.safeParse({ id: 'note-1' }).success).toBe(true);
130
- expect(read.input.safeParse({ title: 'Nope' }).success).toBe(false);
131
- expect(read.output?.safeParse(note.examples?.[0]).success).toBe(true);
132
- };
133
-
134
- const expectUpdateSchemas = (
135
- updated: ReturnType<typeof deriveNoteTrail<'update'>>
136
- ) => {
137
- expect(updated.input.safeParse({ id: 'note-1' }).success).toBe(true);
138
- expect(
139
- updated.input.safeParse({ body: 'Updated', id: 'note-1' }).success
140
- ).toBe(true);
141
- expect(
142
- updated.input.parse({
143
- createdAt: '2026-04-10T12:00:00.000Z',
144
- id: 'note-1',
145
- })
146
- ).toEqual({ id: 'note-1' });
147
- };
148
-
149
- const expectDeleteSchemas = (
150
- removed: ReturnType<typeof deriveNoteTrail<'delete'>>
151
- ) => {
152
- expect(removed.input.safeParse({ id: 'note-1' }).success).toBe(true);
153
- expect(removed.output?.safeParse().success).toBe(true);
154
- expect(removed.output?.safeParse(note.examples?.[0]).success).toBe(false);
155
- };
156
-
157
- const expectListSchemas = (
158
- listed: ReturnType<typeof deriveNoteTrail<'list'>>
159
- ) => {
160
- expect(listed.input.safeParse({}).success).toBe(true);
161
- expect(listed.input.safeParse({ id: 'note-1' }).success).toBe(true);
162
- expect(listed.output?.safeParse([note.examples?.[0]]).success).toBe(true);
163
- };
164
-
165
- describe('deriveTrail()', () => {
166
- test('derives trail ids, intents, contours, and resources for CRUD operations', () => {
167
- const { created, listed, read, removed, updated } = deriveCrudTrails();
168
-
169
- expectTrailMetadata(created, 'note.create', 'write');
170
- expectTrailMetadata(read, 'note.read', 'read');
171
- expectTrailMetadata(updated, 'note.update', 'write');
172
- expectTrailMetadata(removed, 'note.delete', 'destroy');
173
- expectTrailMetadata(listed, 'note.list', 'read');
174
-
175
- expect(created.contours).toEqual([note]);
176
- expect(created.resources).toEqual([noteResource]);
177
- });
178
-
179
- test('derives input and output schemas for each CRUD operation', () => {
180
- const { created, listed, read, removed, updated } = deriveCrudTrails();
181
-
182
- expectCreateSchemas(created);
183
- expectReadSchemas(read);
184
- expectUpdateSchemas(updated);
185
- expectDeleteSchemas(removed);
186
- expectListSchemas(listed);
187
- });
188
-
189
- test('derives per-operation examples from contour examples', () => {
190
- const { created, listed, read, removed, updated } = deriveCrudTrails();
191
-
192
- expect(created.examples).toEqual([
193
- {
194
- expected: note.examples?.[0],
195
- input: { body: 'Hello, Trails', title: 'First note' },
196
- name: 'Create note note-1',
197
- },
198
- ]);
199
- expect(read.examples).toEqual([
200
- {
201
- expected: note.examples?.[0],
202
- input: { id: 'note-1' },
203
- name: 'Read note note-1',
204
- },
205
- ]);
206
- expect(updated.examples).toEqual([
207
- {
208
- expected: note.examples?.[0],
209
- input: { body: 'Hello, Trails', id: 'note-1', title: 'First note' },
210
- name: 'Update note note-1',
211
- },
212
- ]);
213
- expect(removed.examples).toEqual([
214
- {
215
- input: { id: 'note-1' },
216
- name: 'Delete note note-1',
217
- },
218
- ]);
219
- expect(listed.examples).toEqual([
220
- {
221
- expected: [note.examples?.[0]],
222
- input: {},
223
- name: 'note list example',
224
- },
225
- ]);
226
- });
227
-
228
- test('drops schema defaults from update and list filters', () => {
229
- const updated = deriveNoteTrail('update');
230
- const listed = deriveNoteTrail('list');
231
-
232
- expect(updated.input.parse({ id: 'note-1' })).toEqual({ id: 'note-1' });
233
- expect(listed.input.parse({})).toEqual({});
234
- });
235
-
236
- test('does not auto-stamp a pattern when none is declared', () => {
237
- const created = deriveNoteTrail('create');
238
-
239
- expect(created.pattern).toBeUndefined();
240
- });
241
-
242
- test('preserves a caller-declared pattern', () => {
243
- const created = deriveTrail(note, 'create', {
244
- blaze: createBlaze,
245
- generated: ['id', 'createdAt'],
246
- pattern: 'crud',
247
- resource: noteResource,
248
- });
249
-
250
- expect(created.pattern).toBe('crud');
251
- });
252
- });
253
-
254
- // ---------------------------------------------------------------------------
255
- // Default-blaze synthesis
256
- // ---------------------------------------------------------------------------
257
-
258
- interface NoteEntity {
259
- readonly id: string;
260
- readonly title: string;
261
- readonly body: string;
262
- readonly createdAt: string;
263
- }
264
-
265
- interface FakeNoteAccessor {
266
- get: (id: string) => Promise<NoteEntity | null>;
267
- list: (filters?: Partial<NoteEntity>) => Promise<readonly NoteEntity[]>;
268
- upsert: (input: Partial<NoteEntity>) => Promise<NoteEntity>;
269
- remove: (id: string) => Promise<{ readonly deleted: boolean }>;
270
- insert?: (input: Partial<NoteEntity>) => Promise<NoteEntity>;
271
- update?: (
272
- id: string,
273
- patch: Partial<NoteEntity>
274
- ) => Promise<NoteEntity | null>;
275
- }
276
-
277
- const cloneNote = (value: NoteEntity): NoteEntity => ({ ...value });
278
-
279
- const seedNote: NoteEntity = {
280
- body: 'Hello, Trails',
281
- createdAt: '2026-04-10T12:00:00.000Z',
282
- id: 'note-1',
283
- title: 'First note',
284
- };
285
-
286
- const matchesFilters = (
287
- row: NoteEntity,
288
- filters: Partial<NoteEntity> | undefined
289
- ): boolean =>
290
- filters === undefined ||
291
- Object.entries(filters).every(
292
- ([field, value]) => row[field as keyof NoteEntity] === value
293
- );
294
-
295
- const readOneFromMap = <V>(table: Map<string, V>, id: string): V | null => {
296
- const entry = table.get(id);
297
- return entry === undefined ? null : entry;
298
- };
299
-
300
- const seedRecords = (seed: NoteEntity | null): Map<string, NoteEntity> => {
301
- const records = new Map<string, NoteEntity>();
302
- if (seed !== null) {
303
- records.set(seed.id, cloneNote(seed));
304
- }
305
- return records;
306
- };
307
-
308
- const makeBaseAccessor = (
309
- records: Map<string, NoteEntity>
310
- ): FakeNoteAccessor => {
311
- const writePayload = (input: Partial<NoteEntity>) => {
312
- const payload = { ...seedNote, ...(input as NoteEntity) };
313
- records.set(payload.id, payload);
314
- return cloneNote(payload);
315
- };
316
- return {
317
- // eslint-disable-next-line @typescript-eslint/require-await
318
- get: async (id) => readOneFromMap(records, id),
319
- // eslint-disable-next-line @typescript-eslint/require-await
320
- list: async (filters) =>
321
- [...records.values()].filter((row) => matchesFilters(row, filters)),
322
- // eslint-disable-next-line @typescript-eslint/require-await
323
- remove: async (id) => ({ deleted: records.delete(id) }),
324
- // eslint-disable-next-line @typescript-eslint/require-await
325
- upsert: async (input) => writePayload(input),
326
- };
327
- };
328
-
329
- const attachInsert = (
330
- accessor: FakeNoteAccessor,
331
- records: Map<string, NoteEntity>
332
- ): void => {
333
- // eslint-disable-next-line @typescript-eslint/require-await
334
- accessor.insert = async (input) => {
335
- const payload = { ...seedNote, ...(input as NoteEntity) };
336
- records.set(payload.id, payload);
337
- return cloneNote(payload);
338
- };
339
- };
340
-
341
- const attachUpdate = (
342
- accessor: FakeNoteAccessor,
343
- records: Map<string, NoteEntity>
344
- ): void => {
345
- // eslint-disable-next-line @typescript-eslint/require-await
346
- accessor.update = async (id, patch) => {
347
- const existing = records.get(id);
348
- if (!existing) {
349
- return null;
350
- }
351
- const merged = { ...existing, ...patch } as NoteEntity;
352
- records.set(id, merged);
353
- return cloneNote(merged);
354
- };
355
- };
356
-
357
- const createFakeAccessor = (
358
- options: {
359
- insert?: boolean;
360
- update?: boolean;
361
- seed?: NoteEntity | null;
362
- } = {}
363
- ): FakeNoteAccessor => {
364
- const seed = options.seed === undefined ? seedNote : options.seed;
365
- const records = seedRecords(seed);
366
- const accessor = makeBaseAccessor(records);
367
- if (options.insert !== false) {
368
- attachInsert(accessor, records);
369
- }
370
- if (options.update !== false) {
371
- attachUpdate(accessor, records);
372
- }
373
- return accessor;
374
- };
375
-
376
- interface VersionedFake {
377
- readonly accessor: FakeNoteAccessor;
378
- readonly upsertLog: readonly Record<string, unknown>[];
379
- }
380
-
381
- const createVersionedFake = (): VersionedFake => {
382
- const versionedSeed = {
383
- ...seedNote,
384
- version: 7,
385
- } as NoteEntity & { version: number };
386
- const records = new Map<string, NoteEntity & { version: number }>([
387
- [versionedSeed.id, versionedSeed],
388
- ]);
389
- const upsertLog: Record<string, unknown>[] = [];
390
- const accessor: FakeNoteAccessor = {
391
- // eslint-disable-next-line @typescript-eslint/require-await
392
- get: async (id) => readOneFromMap(records, id),
393
- // eslint-disable-next-line @typescript-eslint/require-await
394
- list: async () => [...records.values()],
395
- // eslint-disable-next-line @typescript-eslint/require-await
396
- remove: async (id) => ({ deleted: records.delete(id) }),
397
- // eslint-disable-next-line @typescript-eslint/require-await
398
- upsert: async (input) => {
399
- const payload = input as Record<string, unknown>;
400
- upsertLog.push(payload);
401
- const merged = {
402
- ...versionedSeed,
403
- ...(payload as NoteEntity & { version?: number }),
404
- };
405
- records.set(merged.id, merged);
406
- return cloneNote(merged);
407
- },
408
- };
409
- return { accessor, upsertLog };
410
- };
411
-
412
- const expectOk = <T>(result: Result<T, Error>): T => {
413
- if (result.isErr()) {
414
- throw result.error;
415
- }
416
- return result.value;
417
- };
418
-
419
- const expectErr = <T>(result: Result<T, Error>): Error => {
420
- if (result.isOk()) {
421
- throw new Error(`Expected error, got ok: ${JSON.stringify(result.value)}`);
422
- }
423
- return result.error;
424
- };
425
-
426
- const makeNotesResource = (accessor: FakeNoteAccessor) =>
427
- resource('db.notes', {
428
- create: () => Result.ok({ note: accessor }),
429
- });
430
-
431
- const ctxFor = (accessor: FakeNoteAccessor) =>
432
- createTrailContext({
433
- extensions: {
434
- 'db.notes': { note: accessor },
435
- },
436
- });
437
-
438
- describe('deriveTrail() default blaze synthesis — create', () => {
439
- test('synthesizes create blaze that calls accessor.insert', async () => {
440
- const accessor = createFakeAccessor();
441
- const notesResource = makeNotesResource(accessor);
442
-
443
- const created = deriveTrail(note, 'create', {
444
- generated: ['id', 'createdAt'],
445
- resource: notesResource,
446
- });
447
-
448
- const result = await created.blaze(
449
- { body: 'Bye, Trails', title: 'Second note' },
450
- ctxFor(accessor)
451
- );
452
- expect(result.isOk()).toBe(true);
453
- });
454
-
455
- test('create falls back to upsert when insert is absent', async () => {
456
- const accessor = createFakeAccessor({ insert: false });
457
- const notesResource = makeNotesResource(accessor);
458
-
459
- const created = deriveTrail(note, 'create', {
460
- generated: ['id', 'createdAt'],
461
- resource: notesResource,
462
- });
463
-
464
- const result = await created.blaze(
465
- { body: 'Upsert body', title: 'Upsert note' },
466
- ctxFor(accessor)
467
- );
468
- expect(result.isOk()).toBe(true);
469
- });
470
-
471
- test('custom blaze wins over default synthesis', async () => {
472
- const accessor = createFakeAccessor();
473
- const notesResource = makeNotesResource(accessor);
474
- // eslint-disable-next-line @typescript-eslint/require-await
475
- const customBlaze = mock(async () => Result.ok(seedNote));
476
-
477
- const created = deriveTrail(note, 'create', {
478
- blaze: customBlaze,
479
- generated: ['id', 'createdAt'],
480
- resource: notesResource,
481
- });
482
-
483
- await created.blaze(
484
- { body: 'ignored', title: 'ignored' },
485
- ctxFor(accessor)
486
- );
487
- expect(customBlaze).toHaveBeenCalledTimes(1);
488
- });
489
- });
490
-
491
- describe('deriveTrail() default blaze synthesis — read', () => {
492
- test('synthesizes read blaze that returns entity', async () => {
493
- const accessor = createFakeAccessor();
494
- const notesResource = makeNotesResource(accessor);
495
-
496
- const read = deriveTrail(note, 'read', { resource: notesResource });
497
- const result = await read.blaze({ id: 'note-1' }, ctxFor(accessor));
498
- const value = expectOk(result);
499
- expect(value.id).toBe('note-1');
500
- });
501
-
502
- test('read returns InternalError when accessor has no get', async () => {
503
- const accessor = createFakeAccessor();
504
- const broken = {
505
- ...accessor,
506
- get: undefined,
507
- } as unknown as FakeNoteAccessor;
508
- const notesResource = makeNotesResource(broken);
509
-
510
- const read = deriveTrail(note, 'read', { resource: notesResource });
511
- const result = await read.blaze({ id: 'note-1' }, ctxFor(broken));
512
- expect(expectErr(result)).toBeInstanceOf(InternalError);
513
- });
514
-
515
- test('read returns Result.err when resource lookup throws', async () => {
516
- const accessor = createFakeAccessor();
517
- const notesResource = makeNotesResource(accessor);
518
-
519
- const read = deriveTrail(note, 'read', { resource: notesResource });
520
- const result = await read.blaze(
521
- { id: 'note-1' },
522
- createTrailContext({ extensions: {} })
523
- );
524
-
525
- expect(expectErr(result)).toBeInstanceOf(InternalError);
526
- });
527
- });
528
-
529
- describe('deriveTrail() default blaze synthesis — update', () => {
530
- test('update uses accessor.update when present', async () => {
531
- const accessor = createFakeAccessor();
532
- const notesResource = makeNotesResource(accessor);
533
-
534
- const updated = deriveTrail(note, 'update', {
535
- generated: ['id', 'createdAt'],
536
- resource: notesResource,
537
- });
538
-
539
- const result = await updated.blaze(
540
- { id: 'note-1', title: 'Renamed' },
541
- ctxFor(accessor)
542
- );
543
- const value = expectOk(result);
544
- expect(value.title).toBe('Renamed');
545
- });
546
-
547
- test('update returns NotFoundError when accessor.update returns null', async () => {
548
- const accessor = createFakeAccessor({ seed: null });
549
- const notesResource = makeNotesResource(accessor);
550
-
551
- const updated = deriveTrail(note, 'update', {
552
- generated: ['id', 'createdAt'],
553
- resource: notesResource,
554
- });
555
-
556
- const result = await updated.blaze(
557
- { id: 'missing', title: 'Nope' },
558
- ctxFor(accessor)
559
- );
560
- expect(expectErr(result)).toBeInstanceOf(NotFoundError);
561
- });
562
-
563
- test('update fallback read-null returns NotFoundError', async () => {
564
- const accessor = createFakeAccessor({ seed: null, update: false });
565
- const notesResource = makeNotesResource(accessor);
566
-
567
- const updated = deriveTrail(note, 'update', {
568
- generated: ['id', 'createdAt'],
569
- resource: notesResource,
570
- });
571
-
572
- const result = await updated.blaze(
573
- { id: 'missing', title: 'Nope' },
574
- ctxFor(accessor)
575
- );
576
- expect(expectErr(result)).toBeInstanceOf(NotFoundError);
577
- });
578
-
579
- test('update fallback reads, merges, and upserts when update method is absent', async () => {
580
- const accessor = createFakeAccessor({ update: false });
581
- const upsertSpy = mock(accessor.upsert);
582
- accessor.upsert = upsertSpy;
583
- const notesResource = makeNotesResource(accessor);
584
-
585
- const updated = deriveTrail(note, 'update', {
586
- generated: ['id', 'createdAt'],
587
- resource: notesResource,
588
- });
589
-
590
- const result = await updated.blaze(
591
- { id: 'note-1', title: 'Via fallback' },
592
- ctxFor(accessor)
593
- );
594
- expect(result.isOk()).toBe(true);
595
- expect(upsertSpy).toHaveBeenCalledTimes(1);
596
- });
597
-
598
- test('update fallback strips generated fields from merged payload', async () => {
599
- const { accessor, upsertLog } = createVersionedFake();
600
-
601
- const updated = deriveTrail(note, 'update', {
602
- generated: ['id', 'createdAt'],
603
- resource: makeNotesResource(accessor),
604
- });
605
-
606
- const result = await updated.blaze(
607
- { id: 'note-1', title: 'No version' },
608
- ctxFor(accessor)
609
- );
610
- expectOk(result);
611
- expect(upsertLog.length).toBe(1);
612
- const keys = Object.keys(upsertLog[0] as Record<string, unknown>);
613
- // Identity preserved even though it's in generated; non-identity generated stripped
614
- expect(keys).toContain('id');
615
- expect(keys).not.toContain('createdAt');
616
- expect(keys).toContain('version');
617
- });
618
- });
619
-
620
- describe('deriveTrail() default blaze synthesis — delete and list', () => {
621
- test('synthesizes delete blaze returning Result.ok(undefined)', async () => {
622
- const accessor = createFakeAccessor();
623
- const notesResource = makeNotesResource(accessor);
624
-
625
- const removed = deriveTrail(note, 'delete', { resource: notesResource });
626
- const result = await removed.blaze({ id: 'note-1' }, ctxFor(accessor));
627
-
628
- expect(expectOk(result)).toBeUndefined();
629
- });
630
-
631
- test('synthesizes list blaze returning array', async () => {
632
- const accessor = createFakeAccessor();
633
- const notesResource = makeNotesResource(accessor);
634
-
635
- const listed = deriveTrail(note, 'list', { resource: notesResource });
636
- const result = await listed.blaze({}, ctxFor(accessor));
637
-
638
- const value = expectOk(result);
639
- expect(Array.isArray(value)).toBe(true);
640
- expect(value.length).toBe(1);
641
- });
642
- });
643
-
644
- describe('deriveTrail() multi-resource and invalid input', () => {
645
- test('rejects multi-resource derivation without a blaze', () => {
646
- const accessor = createFakeAccessor();
647
- const a = makeNotesResource(accessor);
648
- const b = makeNotesResource(accessor);
649
-
650
- expect(() =>
651
- deriveTrail(note, 'create', {
652
- generated: ['id', 'createdAt'],
653
- resource: [a, b],
654
- })
655
- ).toThrow(DerivationError);
656
- });
657
-
658
- test('rejects derivation with empty resource array and no blaze', () => {
659
- expect(() =>
660
- deriveTrail(note, 'create', {
661
- generated: ['id', 'createdAt'],
662
- resource: [],
663
- })
664
- ).toThrow(
665
- 'deriveTrail("note.create") requires an explicit `blaze` when no resources are declared'
666
- );
667
- });
668
- });