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

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 (280) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +17 -13
  4. package/dist/branded.d.ts +1 -1
  5. package/dist/branded.d.ts.map +1 -1
  6. package/dist/branded.js +1 -1
  7. package/dist/branded.js.map +1 -1
  8. package/dist/context.d.ts +13 -1
  9. package/dist/context.d.ts.map +1 -1
  10. package/dist/context.js +19 -3
  11. package/dist/context.js.map +1 -1
  12. package/dist/contour.d.ts +81 -0
  13. package/dist/contour.d.ts.map +1 -0
  14. package/dist/contour.js +176 -0
  15. package/dist/contour.js.map +1 -0
  16. package/dist/cross-schema.d.ts +18 -0
  17. package/dist/cross-schema.d.ts.map +1 -0
  18. package/dist/cross-schema.js +29 -0
  19. package/dist/cross-schema.js.map +1 -0
  20. package/dist/draft.d.ts +5 -5
  21. package/dist/draft.d.ts.map +1 -1
  22. package/dist/draft.js +28 -27
  23. package/dist/draft.js.map +1 -1
  24. package/dist/errors.d.ts +62 -4
  25. package/dist/errors.d.ts.map +1 -1
  26. package/dist/errors.js +44 -1
  27. package/dist/errors.js.map +1 -1
  28. package/dist/execute.d.ts +26 -9
  29. package/dist/execute.d.ts.map +1 -1
  30. package/dist/execute.js +441 -20
  31. package/dist/execute.js.map +1 -1
  32. package/dist/fire.d.ts +43 -0
  33. package/dist/fire.d.ts.map +1 -0
  34. package/dist/fire.js +185 -0
  35. package/dist/fire.js.map +1 -0
  36. package/dist/index.d.ts +31 -20
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +23 -13
  39. package/dist/index.js.map +1 -1
  40. package/dist/internal/cross-batch.d.ts +44 -0
  41. package/dist/internal/cross-batch.d.ts.map +1 -0
  42. package/dist/internal/cross-batch.js +58 -0
  43. package/dist/internal/cross-batch.js.map +1 -0
  44. package/dist/internal/fork-ctx.d.ts +41 -0
  45. package/dist/internal/fork-ctx.d.ts.map +1 -0
  46. package/dist/internal/fork-ctx.js +47 -0
  47. package/dist/internal/fork-ctx.js.map +1 -0
  48. package/dist/internal/signal-ref.d.ts +15 -0
  49. package/dist/internal/signal-ref.d.ts.map +1 -0
  50. package/dist/internal/signal-ref.js +41 -0
  51. package/dist/internal/signal-ref.js.map +1 -0
  52. package/dist/internal/topo-saves.d.ts +8 -2
  53. package/dist/internal/topo-saves.d.ts.map +1 -1
  54. package/dist/internal/topo-saves.js +162 -23
  55. package/dist/internal/topo-saves.js.map +1 -1
  56. package/dist/internal/topo-snapshots.d.ts +57 -0
  57. package/dist/internal/topo-snapshots.d.ts.map +1 -0
  58. package/dist/internal/topo-snapshots.js +372 -0
  59. package/dist/internal/topo-snapshots.js.map +1 -0
  60. package/dist/internal/topo-store-read.d.ts +23 -20
  61. package/dist/internal/topo-store-read.d.ts.map +1 -1
  62. package/dist/internal/topo-store-read.js +82 -83
  63. package/dist/internal/topo-store-read.js.map +1 -1
  64. package/dist/internal/topo-store.d.ts +19 -5
  65. package/dist/internal/topo-store.d.ts.map +1 -1
  66. package/dist/internal/topo-store.js +174 -124
  67. package/dist/internal/topo-store.js.map +1 -1
  68. package/dist/internal/tracing.d.ts +98 -0
  69. package/dist/internal/tracing.d.ts.map +1 -0
  70. package/dist/internal/tracing.js +102 -0
  71. package/dist/internal/tracing.js.map +1 -0
  72. package/dist/internal/trails-db.d.ts +2 -2
  73. package/dist/internal/trails-db.d.ts.map +1 -1
  74. package/dist/internal/trails-db.js +8 -8
  75. package/dist/internal/trails-db.js.map +1 -1
  76. package/dist/internal/zod-wrappers.d.ts +40 -0
  77. package/dist/internal/zod-wrappers.d.ts.map +1 -0
  78. package/dist/internal/zod-wrappers.js +59 -0
  79. package/dist/internal/zod-wrappers.js.map +1 -0
  80. package/dist/layer.d.ts +22 -5
  81. package/dist/layer.d.ts.map +1 -1
  82. package/dist/layer.js +20 -3
  83. package/dist/layer.js.map +1 -1
  84. package/dist/path-security.d.ts +1 -1
  85. package/dist/path-security.d.ts.map +1 -1
  86. package/dist/path-security.js +1 -1
  87. package/dist/path-security.js.map +1 -1
  88. package/dist/resilience.d.ts +1 -1
  89. package/dist/resilience.d.ts.map +1 -1
  90. package/dist/resilience.js +3 -3
  91. package/dist/resilience.js.map +1 -1
  92. package/dist/resource-config.d.ts +22 -0
  93. package/dist/resource-config.d.ts.map +1 -0
  94. package/dist/resource-config.js +210 -0
  95. package/dist/resource-config.js.map +1 -0
  96. package/dist/resource.d.ts +74 -0
  97. package/dist/resource.d.ts.map +1 -0
  98. package/dist/resource.js +61 -0
  99. package/dist/resource.js.map +1 -0
  100. package/dist/run.d.ts +4 -3
  101. package/dist/run.d.ts.map +1 -1
  102. package/dist/run.js +5 -4
  103. package/dist/run.js.map +1 -1
  104. package/dist/serialization.d.ts.map +1 -1
  105. package/dist/serialization.js +2 -1
  106. package/dist/serialization.js.map +1 -1
  107. package/dist/store/accessor-protocol.d.ts +51 -0
  108. package/dist/store/accessor-protocol.d.ts.map +1 -0
  109. package/dist/store/accessor-protocol.js +2 -0
  110. package/dist/store/accessor-protocol.js.map +1 -0
  111. package/dist/store/index.d.ts +2 -0
  112. package/dist/store/index.d.ts.map +1 -0
  113. package/dist/store/index.js +2 -0
  114. package/dist/store/index.js.map +1 -0
  115. package/dist/surface-filter.d.ts +13 -0
  116. package/dist/surface-filter.d.ts.map +1 -0
  117. package/dist/surface-filter.js +76 -0
  118. package/dist/surface-filter.js.map +1 -0
  119. package/dist/topo-store.d.ts +36 -22
  120. package/dist/topo-store.d.ts.map +1 -1
  121. package/dist/topo-store.js +215 -71
  122. package/dist/topo-store.js.map +1 -1
  123. package/dist/topo.d.ts +23 -9
  124. package/dist/topo.d.ts.map +1 -1
  125. package/dist/topo.js +175 -46
  126. package/dist/topo.js.map +1 -1
  127. package/dist/trail.d.ts +86 -19
  128. package/dist/trail.d.ts.map +1 -1
  129. package/dist/trail.js +59 -4
  130. package/dist/trail.js.map +1 -1
  131. package/dist/trails/derive-trail.d.ts +66 -0
  132. package/dist/trails/derive-trail.d.ts.map +1 -0
  133. package/dist/trails/derive-trail.js +378 -0
  134. package/dist/trails/derive-trail.js.map +1 -0
  135. package/dist/trails/index.d.ts +5 -0
  136. package/dist/trails/index.d.ts.map +1 -0
  137. package/dist/trails/index.js +3 -0
  138. package/dist/trails/index.js.map +1 -0
  139. package/dist/trails/ingest.d.ts +24 -0
  140. package/dist/trails/ingest.d.ts.map +1 -0
  141. package/dist/trails/ingest.js +63 -0
  142. package/dist/trails/ingest.js.map +1 -0
  143. package/dist/transport-error-map.d.ts +106 -0
  144. package/dist/transport-error-map.d.ts.map +1 -0
  145. package/dist/transport-error-map.js +24 -0
  146. package/dist/transport-error-map.js.map +1 -0
  147. package/dist/type-checks.test-d.d.ts +71 -0
  148. package/dist/type-checks.test-d.d.ts.map +1 -0
  149. package/dist/type-checks.test-d.js +12 -0
  150. package/dist/type-checks.test-d.js.map +1 -0
  151. package/dist/type-utils.d.ts +11 -2
  152. package/dist/type-utils.d.ts.map +1 -1
  153. package/dist/type-utils.js.map +1 -1
  154. package/dist/types.d.ts +113 -7
  155. package/dist/types.d.ts.map +1 -1
  156. package/dist/types.js.map +1 -1
  157. package/dist/validate-established-topo.js +2 -2
  158. package/dist/validate-established-topo.js.map +1 -1
  159. package/dist/validate-topo.d.ts.map +1 -1
  160. package/dist/validate-topo.js +24 -7
  161. package/dist/validate-topo.js.map +1 -1
  162. package/dist/workspace.d.ts +1 -1
  163. package/dist/workspace.d.ts.map +1 -1
  164. package/dist/workspace.js +1 -1
  165. package/dist/workspace.js.map +1 -1
  166. package/package.json +8 -2
  167. package/src/__tests__/branded.test.ts +5 -5
  168. package/src/__tests__/context.test.ts +3 -3
  169. package/src/__tests__/contour.test.ts +263 -0
  170. package/src/__tests__/derive-trail.test.ts +668 -0
  171. package/src/__tests__/errors.test.ts +118 -0
  172. package/src/__tests__/execute.test.ts +1179 -141
  173. package/src/__tests__/fire.test.ts +1056 -0
  174. package/src/__tests__/fork-ctx.test.ts +83 -0
  175. package/src/__tests__/ingest.test.ts +191 -0
  176. package/src/__tests__/{gate.test.ts → layer.test.ts} +21 -21
  177. package/src/__tests__/path-security.test.ts +7 -7
  178. package/src/__tests__/resilience.test.ts +8 -8
  179. package/src/__tests__/resource.test.ts +204 -0
  180. package/src/__tests__/run.test.ts +13 -13
  181. package/src/__tests__/service-config.test.ts +28 -32
  182. package/src/__tests__/surface-filter.test.ts +196 -0
  183. package/src/__tests__/topo-store-read.test.ts +41 -36
  184. package/src/__tests__/topo-store.test.ts +493 -93
  185. package/src/__tests__/topo.test.ts +350 -54
  186. package/src/__tests__/trail.test.ts +189 -24
  187. package/src/__tests__/trails-db.test.ts +32 -25
  188. package/src/__tests__/transport-error-map.test.ts +82 -0
  189. package/src/__tests__/type-utils.test.ts +29 -1
  190. package/src/__tests__/validate-topo.test.ts +147 -25
  191. package/src/__tests__/workspace.test.ts +6 -6
  192. package/src/__tests__/zod-wrappers.test.ts +62 -0
  193. package/src/branded.ts +1 -1
  194. package/src/context.ts +24 -4
  195. package/src/contour.ts +344 -0
  196. package/src/cross-schema.ts +36 -0
  197. package/src/draft.ts +56 -52
  198. package/src/errors.ts +62 -18
  199. package/src/execute.ts +840 -29
  200. package/src/fire.ts +295 -0
  201. package/src/index.ts +116 -29
  202. package/src/internal/cross-batch.ts +77 -0
  203. package/src/internal/fork-ctx.ts +69 -0
  204. package/src/internal/signal-ref.ts +87 -0
  205. package/src/internal/topo-snapshots.ts +517 -0
  206. package/src/internal/topo-store-read.ts +140 -132
  207. package/src/internal/topo-store.ts +291 -186
  208. package/src/internal/tracing.ts +187 -0
  209. package/src/internal/trails-db.ts +9 -11
  210. package/src/internal/zod-wrappers.ts +78 -0
  211. package/src/layer.ts +50 -0
  212. package/src/path-security.ts +1 -1
  213. package/src/resilience.ts +3 -3
  214. package/src/{provision-config.ts → resource-config.ts} +109 -123
  215. package/src/resource.ts +155 -0
  216. package/src/run.ts +5 -4
  217. package/src/serialization.ts +2 -0
  218. package/src/store/accessor-protocol.ts +56 -0
  219. package/src/store/index.ts +4 -0
  220. package/src/surface-filter.ts +182 -0
  221. package/src/topo-store.ts +313 -104
  222. package/src/topo.ts +328 -57
  223. package/src/trail.ts +199 -31
  224. package/src/trails/derive-trail.ts +842 -0
  225. package/src/trails/index.ts +9 -0
  226. package/src/trails/ingest.ts +146 -0
  227. package/src/transport-error-map.ts +56 -0
  228. package/src/type-checks.test-d.ts +106 -0
  229. package/src/type-utils.ts +17 -2
  230. package/src/types.ts +148 -11
  231. package/src/validate-established-topo.ts +2 -2
  232. package/src/validate-topo.ts +32 -8
  233. package/src/workspace.ts +1 -1
  234. package/tsconfig.tests.json +10 -0
  235. package/tsconfig.tsbuildinfo +1 -1
  236. package/dist/adapters.d.ts +0 -39
  237. package/dist/adapters.d.ts.map +0 -1
  238. package/dist/adapters.js +0 -2
  239. package/dist/adapters.js.map +0 -1
  240. package/dist/dispatch.d.ts +0 -27
  241. package/dist/dispatch.d.ts.map +0 -1
  242. package/dist/dispatch.js +0 -34
  243. package/dist/dispatch.js.map +0 -1
  244. package/dist/event.d.ts +0 -8
  245. package/dist/event.d.ts.map +0 -1
  246. package/dist/event.js +0 -7
  247. package/dist/event.js.map +0 -1
  248. package/dist/gate.d.ts +0 -17
  249. package/dist/gate.d.ts.map +0 -1
  250. package/dist/gate.js +0 -21
  251. package/dist/gate.js.map +0 -1
  252. package/dist/health.d.ts +0 -18
  253. package/dist/health.d.ts.map +0 -1
  254. package/dist/health.js +0 -5
  255. package/dist/health.js.map +0 -1
  256. package/dist/job.d.ts +0 -24
  257. package/dist/job.d.ts.map +0 -1
  258. package/dist/job.js +0 -17
  259. package/dist/job.js.map +0 -1
  260. package/dist/provision-config.d.ts +0 -22
  261. package/dist/provision-config.d.ts.map +0 -1
  262. package/dist/provision-config.js +0 -210
  263. package/dist/provision-config.js.map +0 -1
  264. package/dist/provision.d.ts +0 -71
  265. package/dist/provision.d.ts.map +0 -1
  266. package/dist/provision.js +0 -56
  267. package/dist/provision.js.map +0 -1
  268. package/dist/service-config.d.ts +0 -22
  269. package/dist/service-config.d.ts.map +0 -1
  270. package/dist/service-config.js +0 -210
  271. package/dist/service-config.js.map +0 -1
  272. package/dist/service.d.ts +0 -71
  273. package/dist/service.d.ts.map +0 -1
  274. package/dist/service.js +0 -56
  275. package/dist/service.js.map +0 -1
  276. package/src/__tests__/service.test.ts +0 -197
  277. package/src/event.ts +0 -15
  278. package/src/gate.ts +0 -44
  279. package/src/internal/topo-saves.ts +0 -429
  280. package/src/provision.ts +0 -148
@@ -2,9 +2,12 @@ import { describe, test, expect } from 'bun:test';
2
2
 
3
3
  import { z } from 'zod';
4
4
 
5
+ import { contour } from '../contour';
5
6
  import { createTrailContext } from '../context';
7
+ import { ConflictError } from '../errors';
6
8
  import { Result } from '../result';
7
- import { provision } from '../provision';
9
+ import { resource } from '../resource';
10
+ import { signal } from '../signal';
8
11
  import { trail } from '../trail';
9
12
  import type { TrailContext } from '../types';
10
13
 
@@ -13,16 +16,25 @@ const stubCtx: TrailContext = createTrailContext({
13
16
  requestId: 'test-123',
14
17
  });
15
18
 
16
- const dbProvision = provision('db.main', {
19
+ const dbResource = resource('db.main', {
17
20
  create: () =>
18
21
  Result.ok({
19
22
  query(sql: string) {
20
23
  return sql.length;
21
24
  },
22
25
  }),
23
- description: 'Primary database provision',
26
+ description: 'Primary database resource',
24
27
  });
25
28
 
29
+ const userContour = contour(
30
+ 'user',
31
+ {
32
+ id: z.string().uuid(),
33
+ name: z.string(),
34
+ },
35
+ { identity: 'id' }
36
+ );
37
+
26
38
  describe('trail()', () => {
27
39
  const inputSchema = z.object({ name: z.string() });
28
40
  const outputSchema = z.object({ greeting: z.string() });
@@ -92,19 +104,34 @@ describe('trail()', () => {
92
104
  expect(withMeta.meta).toEqual({ domain: 'billing', tier: 1 });
93
105
  });
94
106
 
107
+ test('pattern is stored when declared', () => {
108
+ const withPattern = trail('feature.enable', {
109
+ blaze: () => Result.ok({ enabled: true }),
110
+ input: z.object({ id: z.string() }),
111
+ pattern: 'toggle',
112
+ });
113
+
114
+ expect(withPattern.pattern).toBe('toggle');
115
+ });
116
+
95
117
  test('detours are stored', () => {
96
118
  const withDetours = trail('orchestrator', {
97
119
  blaze: () => Result.ok(),
98
- detours: {
99
- onFailure: ['alert'],
100
- onSuccess: ['notify', 'audit'],
101
- },
120
+ /* oxlint-disable-next-line require-await -- test stub */
121
+ detours: [{ on: ConflictError, recover: async () => Result.ok() }],
102
122
  input: z.object({}),
103
123
  });
104
- expect(withDetours.detours).toEqual({
105
- onFailure: ['alert'],
106
- onSuccess: ['notify', 'audit'],
124
+ expect(withDetours.detours).toHaveLength(1);
125
+ expect(withDetours.detours[0]?.on).toBe(ConflictError);
126
+ });
127
+
128
+ test('detours default to empty frozen array when omitted', () => {
129
+ const noDetours = trail('bare', {
130
+ blaze: () => Result.ok(),
131
+ input: z.object({}),
107
132
  });
133
+ expect(noDetours.detours).toEqual([]);
134
+ expect(Object.isFrozen(noDetours.detours)).toBe(true);
108
135
  });
109
136
  });
110
137
 
@@ -135,35 +162,109 @@ describe('trail()', () => {
135
162
  });
136
163
  expect(Object.isFrozen(withCrosses.crosses)).toBe(true);
137
164
  });
165
+
166
+ test('trail object in crosses is normalized to its id', () => {
167
+ const target = trail('target.trail', {
168
+ blaze: () => Result.ok(),
169
+ input: z.object({}),
170
+ });
171
+ const composed = trail('composed', {
172
+ blaze: () => Result.ok(),
173
+ crosses: [target],
174
+ input: z.object({}),
175
+ });
176
+ expect(composed.crosses).toEqual(['target.trail']);
177
+ });
178
+
179
+ test('mixed string and trail object in crosses normalizes correctly', () => {
180
+ const target = trail('target.trail', {
181
+ blaze: () => Result.ok(),
182
+ input: z.object({}),
183
+ });
184
+ const composed = trail('composed', {
185
+ blaze: () => Result.ok(),
186
+ crosses: ['string-id', target],
187
+ input: z.object({}),
188
+ });
189
+ expect(composed.crosses).toEqual(['string-id', 'target.trail']);
190
+ });
191
+
192
+ test('crossInput is stored on the trail', () => {
193
+ const crossInputSchema = z.object({ forkedFrom: z.string().optional() });
194
+ const t = trail('gist.create', {
195
+ blaze: () => Result.ok(),
196
+ crossInput: crossInputSchema,
197
+ input: z.object({ content: z.string() }),
198
+ });
199
+ expect(t.crossInput).toBe(crossInputSchema);
200
+ });
201
+
202
+ test('crossInput is undefined when omitted', () => {
203
+ const t = trail('bare', {
204
+ blaze: () => Result.ok(),
205
+ input: z.object({}),
206
+ });
207
+ expect(t.crossInput).toBeUndefined();
208
+ });
209
+ });
210
+
211
+ describe('contours', () => {
212
+ test('defaults to empty frozen array when omitted', () => {
213
+ const minimal = trail('bare', {
214
+ blaze: () => Result.ok(),
215
+ input: z.object({}),
216
+ });
217
+ expect(minimal.contours).toEqual([]);
218
+ expect(Object.isFrozen(minimal.contours)).toBe(true);
219
+ });
220
+
221
+ test('preserves declared contour objects', () => {
222
+ const withContours = trail('user.create', {
223
+ blaze: () => Result.ok(),
224
+ contours: [userContour],
225
+ input: z.object({}),
226
+ });
227
+ expect(withContours.contours).toEqual([userContour]);
228
+ expect(withContours.contours[0]).toBe(userContour);
229
+ });
230
+
231
+ test('contours array is frozen', () => {
232
+ const withContours = trail('user.create', {
233
+ blaze: () => Result.ok(),
234
+ contours: [userContour],
235
+ input: z.object({}),
236
+ });
237
+ expect(Object.isFrozen(withContours.contours)).toBe(true);
238
+ });
138
239
  });
139
240
 
140
- describe('provisions', () => {
241
+ describe('resources', () => {
141
242
  test('defaults to empty frozen array when omitted', () => {
142
243
  const minimal = trail('bare', {
143
244
  blaze: () => Result.ok(),
144
245
  input: z.object({}),
145
246
  });
146
- expect(minimal.provisions).toEqual([]);
147
- expect(Object.isFrozen(minimal.provisions)).toBe(true);
247
+ expect(minimal.resources).toEqual([]);
248
+ expect(Object.isFrozen(minimal.resources)).toBe(true);
148
249
  });
149
250
 
150
- test('preserves declared provision objects', () => {
151
- const withProvisions = trail('search', {
251
+ test('preserves declared resource objects', () => {
252
+ const withResources = trail('search', {
152
253
  blaze: () => Result.ok(),
153
254
  input: z.object({}),
154
- provisions: [dbProvision],
255
+ resources: [dbResource],
155
256
  });
156
- expect(withProvisions.provisions).toEqual([dbProvision]);
157
- expect(withProvisions.provisions[0]).toBe(dbProvision);
257
+ expect(withResources.resources).toEqual([dbResource]);
258
+ expect(withResources.resources[0]).toBe(dbResource);
158
259
  });
159
260
 
160
- test('provisions array is frozen', () => {
161
- const withProvisions = trail('search', {
261
+ test('resources array is frozen', () => {
262
+ const withResources = trail('search', {
162
263
  blaze: () => Result.ok(),
163
264
  input: z.object({}),
164
- provisions: [dbProvision],
265
+ resources: [dbResource],
165
266
  });
166
- expect(Object.isFrozen(withProvisions.provisions)).toBe(true);
267
+ expect(Object.isFrozen(withResources.resources)).toBe(true);
167
268
  });
168
269
  });
169
270
 
@@ -201,6 +302,23 @@ describe('trail()', () => {
201
302
  });
202
303
  expect(t.idempotent).toBe(true);
203
304
  });
305
+
306
+ test('visibility defaults to public', () => {
307
+ const minimal = trail('visible', {
308
+ blaze: () => Result.ok(),
309
+ input: z.object({}),
310
+ });
311
+ expect(minimal.visibility).toBe('public');
312
+ });
313
+
314
+ test('visibility is preserved when set', () => {
315
+ const t = trail('internal.helper', {
316
+ blaze: () => Result.ok(),
317
+ input: z.object({}),
318
+ visibility: 'internal',
319
+ });
320
+ expect(t.visibility).toBe('internal');
321
+ });
204
322
  });
205
323
 
206
324
  describe('single-object overload', () => {
@@ -225,12 +343,12 @@ describe('trail()', () => {
225
343
  input: inputSchema,
226
344
  intent: 'read',
227
345
  output: outputSchema,
228
- provisions: [dbProvision],
346
+ resources: [dbResource],
229
347
  });
230
348
  expect(t.description).toBe('A full trail');
231
349
  expect(t.intent).toBe('read');
232
350
  expect(t.examples).toHaveLength(1);
233
- expect(t.provisions).toEqual([dbProvision]);
351
+ expect(t.resources).toEqual([dbResource]);
234
352
  });
235
353
 
236
354
  test('implementation is callable', async () => {
@@ -259,3 +377,50 @@ describe('trail()', () => {
259
377
  });
260
378
  });
261
379
  });
380
+
381
+ describe('trail() fires/on normalization', () => {
382
+ const orderPlaced = signal('order.placed', {
383
+ payload: z.object({ id: z.string() }),
384
+ });
385
+ const auditLogged = signal('audit.logged', {
386
+ payload: z.object({ actor: z.string() }),
387
+ });
388
+
389
+ test('Signal value in fires: is normalized to its id', () => {
390
+ const t = trail('checkout', {
391
+ blaze: () => Result.ok({}),
392
+ fires: [orderPlaced],
393
+ input: z.object({}),
394
+ });
395
+ expect(t.fires).toEqual(['order.placed']);
396
+ });
397
+
398
+ test('Signal value in on: is normalized to its id', () => {
399
+ const t = trail('notify', {
400
+ blaze: () => Result.ok({}),
401
+ input: z.object({}),
402
+ on: [orderPlaced],
403
+ });
404
+ expect(t.on).toEqual(['order.placed']);
405
+ });
406
+
407
+ test('mixed string + Signal value in fires: is normalized', () => {
408
+ const t = trail('checkout', {
409
+ blaze: () => Result.ok({}),
410
+ fires: ['metric.emitted', orderPlaced, auditLogged],
411
+ input: z.object({}),
412
+ });
413
+ expect(t.fires).toEqual(['metric.emitted', 'order.placed', 'audit.logged']);
414
+ });
415
+
416
+ test('defaults to empty frozen arrays when omitted', () => {
417
+ const minimal = trail('bare', {
418
+ blaze: () => Result.ok(),
419
+ input: z.object({}),
420
+ });
421
+ expect(minimal.fires).toEqual([]);
422
+ expect(Object.isFrozen(minimal.fires)).toBe(true);
423
+ expect(minimal.on).toEqual([]);
424
+ expect(Object.isFrozen(minimal.on)).toBe(true);
425
+ });
426
+ });
@@ -7,16 +7,15 @@ import {
7
7
  ensureSubsystemSchema,
8
8
  openReadTrailsDb,
9
9
  openWriteTrailsDb,
10
- resolveTrailsDbPath,
10
+ deriveTrailsDbPath,
11
11
  } from '../internal/trails-db.js';
12
12
  import {
13
- createTopoSave,
14
- ensureTopoHistorySchema,
15
- listTopoPins,
16
- listTopoSaves,
17
- pinTopoSave,
18
- pruneUnpinnedTopoSaves,
19
- } from '../internal/topo-saves.js';
13
+ createTopoSnapshot,
14
+ ensureTopoSnapshotSchema,
15
+ listTopoSnapshots,
16
+ pinTopoSnapshot,
17
+ pruneUnpinnedSnapshots,
18
+ } from '../internal/topo-snapshots.js';
20
19
 
21
20
  describe('trails db foundation', () => {
22
21
  let tmpRoot: string | undefined;
@@ -40,9 +39,9 @@ describe('trails db foundation', () => {
40
39
  expect(existsSync(join(rootDir, '.trails', 'generated'))).toBe(true);
41
40
  };
42
41
 
43
- test('resolveTrailsDbPath places the database in .trails/trails.db', () => {
42
+ test('deriveTrailsDbPath places the database in .trails/trails.db', () => {
44
43
  const rootDir = '/tmp/example-app';
45
- expect(resolveTrailsDbPath({ rootDir })).toBe(
44
+ expect(deriveTrailsDbPath({ rootDir })).toBe(
46
45
  '/tmp/example-app/.trails/trails.db'
47
46
  );
48
47
  });
@@ -52,7 +51,7 @@ describe('trails db foundation', () => {
52
51
  const db = openWriteTrailsDb({ rootDir });
53
52
 
54
53
  try {
55
- expect(existsSync(resolveTrailsDbPath({ rootDir }))).toBe(true);
54
+ expect(existsSync(deriveTrailsDbPath({ rootDir }))).toBe(true);
56
55
  expectWorkspaceLayout(rootDir);
57
56
 
58
57
  const journal = db
@@ -127,7 +126,7 @@ describe('trails db foundation', () => {
127
126
  });
128
127
  });
129
128
 
130
- describe('topo save primitives', () => {
129
+ describe('topo snapshot primitives', () => {
131
130
  let tmpRoot: string | undefined;
132
131
 
133
132
  afterEach(() => {
@@ -138,52 +137,60 @@ describe('topo save primitives', () => {
138
137
  });
139
138
 
140
139
  const makeRoot = (): string => {
141
- tmpRoot = mkdtempSync(join(tmpdir(), 'topo-saves-'));
140
+ tmpRoot = mkdtempSync(join(tmpdir(), 'topo-snapshots-'));
142
141
  return tmpRoot;
143
142
  };
144
143
 
145
144
  const seedHistory = (db: ReturnType<typeof openWriteTrailsDb>) => {
146
- ensureTopoHistorySchema(db);
145
+ ensureTopoSnapshotSchema(db);
147
146
 
148
- const pinned = createTopoSave(db, {
147
+ const pinned = createTopoSnapshot(db, {
149
148
  createdAt: '2026-04-01T00:00:00.000Z',
150
149
  gitDirty: false,
151
150
  gitSha: 'abc123',
152
- provisionCount: 2,
151
+ resourceCount: 2,
153
152
  signalCount: 1,
154
153
  trailCount: 3,
155
154
  });
156
- const disposable = createTopoSave(db, {
155
+ const disposable = createTopoSnapshot(db, {
157
156
  createdAt: '2026-04-02T00:00:00.000Z',
158
157
  gitDirty: true,
159
158
  gitSha: 'def456',
160
- provisionCount: 3,
159
+ resourceCount: 3,
161
160
  signalCount: 2,
162
161
  trailCount: 4,
163
162
  });
164
163
 
165
164
  return {
166
165
  disposable,
167
- pin: pinTopoSave(db, { name: 'before-auth', saveId: pinned.id }),
168
166
  pinned,
167
+ pinnedSnapshot: pinTopoSnapshot(db, {
168
+ id: pinned.id,
169
+ name: 'before-auth',
170
+ }),
169
171
  };
170
172
  };
171
173
 
172
- test('creates saves, pins them, and prunes only unpinned history', () => {
174
+ test('creates snapshots, pins them, and prunes only unpinned history', () => {
173
175
  const rootDir = makeRoot();
174
176
  const db = openWriteTrailsDb({ rootDir });
175
177
 
176
178
  try {
177
- const { disposable, pin, pinned } = seedHistory(db);
179
+ const { disposable, pinned, pinnedSnapshot } = seedHistory(db);
178
180
 
179
- expect(listTopoPins(db)).toEqual([pin]);
180
- expect(listTopoSaves(db).map((save) => save.id)).toEqual([
181
+ expect(pinnedSnapshot?.pinnedAs).toBe('before-auth');
182
+ expect(
183
+ listTopoSnapshots(db, { pinned: true }).map((snapshot) => snapshot.id)
184
+ ).toEqual([pinned.id]);
185
+ expect(listTopoSnapshots(db).map((snapshot) => snapshot.id)).toEqual([
181
186
  disposable.id,
182
187
  pinned.id,
183
188
  ]);
184
189
 
185
- expect(pruneUnpinnedTopoSaves(db, { keep: 0 })).toBe(1);
186
- expect(listTopoSaves(db).map((save) => save.id)).toEqual([pinned.id]);
190
+ expect(pruneUnpinnedSnapshots(db, { keep: 0 })).toBe(1);
191
+ expect(listTopoSnapshots(db).map((snapshot) => snapshot.id)).toEqual([
192
+ pinned.id,
193
+ ]);
187
194
  } finally {
188
195
  db.close();
189
196
  }
@@ -0,0 +1,82 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import {
4
+ CancelledError,
5
+ NetworkError,
6
+ NotFoundError,
7
+ ValidationError,
8
+ errorCategories,
9
+ exitCodeMap,
10
+ jsonRpcCodeMap,
11
+ statusCodeMap,
12
+ } from '../errors.js';
13
+ import {
14
+ createTransportErrorMapper,
15
+ mapTransportError,
16
+ transportErrorMap,
17
+ transportErrorRegistry,
18
+ transportNames,
19
+ } from '../transport-error-map.js';
20
+
21
+ describe('transportErrorMap', () => {
22
+ test('covers every error category for every transport', () => {
23
+ for (const transport of transportNames) {
24
+ const mappings = transportErrorMap[transport];
25
+ for (const category of errorCategories) {
26
+ expect(category in mappings).toBe(true);
27
+ }
28
+ }
29
+ });
30
+
31
+ test('reuses the existing public transport maps', () => {
32
+ expect(transportErrorMap.cli).toBe(exitCodeMap);
33
+ expect(transportErrorMap.http).toBe(statusCodeMap);
34
+ expect(transportErrorMap.mcp).toBe(jsonRpcCodeMap);
35
+ });
36
+ });
37
+
38
+ describe('transportErrorRegistry', () => {
39
+ test('exposes a callable mapper for each transport', () => {
40
+ const notFound = new NotFoundError('missing');
41
+
42
+ expect(transportErrorRegistry.cli.map(notFound)).toBe(2);
43
+ expect(transportErrorRegistry.http.map(notFound)).toBe(404);
44
+ expect(transportErrorRegistry.mcp.map(notFound)).toBe(-32_601);
45
+ });
46
+ });
47
+
48
+ describe('mapTransportError', () => {
49
+ test('maps known error instances through each registered transport', () => {
50
+ const validation = new ValidationError('bad input');
51
+ const notFound = new NotFoundError('missing');
52
+ const network = new NetworkError('offline');
53
+ const cancelled = new CancelledError('cancelled');
54
+
55
+ expect(mapTransportError('cli', validation)).toBe(1);
56
+ expect(mapTransportError('http', notFound)).toBe(404);
57
+ expect(mapTransportError('mcp', notFound)).toBe(-32_601);
58
+ expect(mapTransportError('http', network)).toBe(502);
59
+ expect(mapTransportError('cli', cancelled)).toBe(130);
60
+ });
61
+ });
62
+
63
+ describe('createTransportErrorMapper', () => {
64
+ test('uses the error category to project into transport-specific values', () => {
65
+ const mapper = createTransportErrorMapper({
66
+ auth: 'auth',
67
+ cancelled: 'cancelled',
68
+ conflict: 'conflict',
69
+ internal: 'internal',
70
+ network: 'network',
71
+ not_found: 'not_found',
72
+ permission: 'permission',
73
+ rate_limit: 'rate_limit',
74
+ timeout: 'timeout',
75
+ validation: 'validation',
76
+ });
77
+
78
+ expect(mapper(new ValidationError('bad input'))).toBe('validation');
79
+ expect(mapper(new NetworkError('offline'))).toBe('network');
80
+ expect(mapper(new CancelledError('cancelled'))).toBe('cancelled');
81
+ });
82
+ });
@@ -4,7 +4,12 @@ import { z } from 'zod';
4
4
 
5
5
  import { Result } from '../result';
6
6
  import { trail } from '../trail';
7
- import type { TrailInput, TrailOutput, TrailResult } from '../type-utils';
7
+ import type {
8
+ CrossInput,
9
+ TrailInput,
10
+ TrailOutput,
11
+ TrailResult,
12
+ } from '../type-utils';
8
13
  import { inputOf, outputOf } from '../type-utils';
9
14
 
10
15
  const greetTrail = trail('greet', {
@@ -68,6 +73,29 @@ describe('type-utils', () => {
68
73
  });
69
74
  });
70
75
 
76
+ describe('CrossInput', () => {
77
+ test('returns base input when no crossInput defined', () => {
78
+ type Input = CrossInput<typeof greetTrail>;
79
+ const _input: Input = { name: 'test' };
80
+ expect(_input.name).toBe('test');
81
+ });
82
+
83
+ test('merges crossInput with base input when crossInput defined', () => {
84
+ const crossTrail = trail('cross.test', {
85
+ blaze: (input) => Result.ok({ name: input.name }),
86
+ crossInput: z.object({ forkedFrom: z.string() }),
87
+ input: z.object({ name: z.string() }),
88
+ output: z.object({ name: z.string() }),
89
+ });
90
+
91
+ type Input = CrossInput<typeof crossTrail>;
92
+ // If this compiles, the type correctly merges both schemas
93
+ const _input: Input = { forkedFrom: 'origin', name: 'test' };
94
+ expect(_input.name).toBe('test');
95
+ expect(_input.forkedFrom).toBe('origin');
96
+ });
97
+ });
98
+
71
99
  describe('TrailResult', () => {
72
100
  test('extracts Result<Output, Error> from a trail', () => {
73
101
  const t = trail('test.result', {