@ontrails/testing 1.0.0-beta.11 → 1.0.0-beta.13

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 (45) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +72 -22
  3. package/README.md +9 -9
  4. package/dist/context.d.ts +41 -11
  5. package/dist/context.d.ts.map +1 -1
  6. package/dist/context.js +36 -21
  7. package/dist/context.js.map +1 -1
  8. package/dist/contracts.js +8 -8
  9. package/dist/contracts.js.map +1 -1
  10. package/dist/crosses.d.ts +38 -0
  11. package/dist/crosses.d.ts.map +1 -0
  12. package/dist/crosses.js +213 -0
  13. package/dist/crosses.js.map +1 -0
  14. package/dist/examples.d.ts +4 -4
  15. package/dist/examples.d.ts.map +1 -1
  16. package/dist/examples.js +52 -32
  17. package/dist/examples.js.map +1 -1
  18. package/dist/harness-mcp.d.ts +1 -1
  19. package/dist/harness-mcp.js +2 -2
  20. package/dist/harness-mcp.js.map +1 -1
  21. package/dist/index.d.ts +6 -6
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3 -3
  24. package/dist/index.js.map +1 -1
  25. package/dist/trail.js +1 -1
  26. package/dist/trail.js.map +1 -1
  27. package/dist/types.d.ts +8 -8
  28. package/dist/types.d.ts.map +1 -1
  29. package/package.json +5 -5
  30. package/src/__tests__/all.test.ts +20 -18
  31. package/src/__tests__/context.test.ts +27 -27
  32. package/src/__tests__/contracts.test.ts +29 -29
  33. package/src/__tests__/{follows.test.ts → crosses.test.ts} +193 -195
  34. package/src/__tests__/detours.test.ts +3 -3
  35. package/src/__tests__/examples.test.ts +221 -126
  36. package/src/__tests__/trail.test.ts +4 -4
  37. package/src/context.ts +80 -32
  38. package/src/contracts.ts +12 -12
  39. package/src/{follows.ts → crosses.ts} +97 -92
  40. package/src/examples.ts +76 -46
  41. package/src/harness-mcp.ts +2 -2
  42. package/src/index.ts +15 -7
  43. package/src/trail.ts +1 -1
  44. package/src/types.ts +9 -9
  45. package/tsconfig.tsbuildinfo +1 -1
@@ -1,16 +1,17 @@
1
1
  import { describe } from 'bun:test';
2
2
 
3
3
  import type { AnyTrail, TrailContext } from '@ontrails/core';
4
- import { InternalError, Result, service, trail } from '@ontrails/core';
4
+ import { InternalError, Result, provision, trail } from '@ontrails/core';
5
5
  import { z } from 'zod';
6
6
 
7
- import { testFollows } from '../follows.js';
7
+ import { testCrosses } from '../crosses.js';
8
8
 
9
9
  // ---------------------------------------------------------------------------
10
- // Test trails (followed by composition trail)
10
+ // Test trails (trails with crossings)
11
11
  // ---------------------------------------------------------------------------
12
12
 
13
13
  const addTrail = trail('entity.add', {
14
+ blaze: (input: { name: string }) => Result.ok({ id: '1', name: input.name }),
14
15
  description: 'Add an entity',
15
16
  examples: [
16
17
  { input: { name: 'Alpha' }, name: 'success' },
@@ -23,15 +24,14 @@ const addTrail = trail('entity.add', {
23
24
  ],
24
25
  input: z.object({ name: z.string() }),
25
26
  output: z.object({ id: z.string(), name: z.string() }),
26
- run: (input: { name: string }) => Result.ok({ id: '1', name: input.name }),
27
27
  });
28
28
 
29
29
  const relateTrail = trail('entity.relate', {
30
+ blaze: (input: { from: string; to: string }) =>
31
+ Result.ok({ from: input.from, to: input.to }),
30
32
  description: 'Relate two entities',
31
33
  input: z.object({ from: z.string(), to: z.string() }),
32
34
  output: z.object({ from: z.string(), to: z.string() }),
33
- run: (input: { from: string; to: string }) =>
34
- Result.ok({ from: input.from, to: input.to }),
35
35
  });
36
36
 
37
37
  // ---------------------------------------------------------------------------
@@ -39,17 +39,14 @@ const relateTrail = trail('entity.relate', {
39
39
  // ---------------------------------------------------------------------------
40
40
 
41
41
  const onboardTrail = trail('entity.onboard', {
42
- follow: ['entity.add', 'entity.relate'],
43
- input: z.object({ name: z.string(), relatedTo: z.string() }),
44
- output: z.object({ name: z.string(), relatedTo: z.string() }),
45
- run: async (
42
+ blaze: async (
46
43
  input: { name: string; relatedTo: string },
47
44
  ctx: TrailContext
48
45
  ) => {
49
- if (!ctx.follow) {
50
- return Result.err(new Error('follow not available'));
46
+ if (!ctx.cross) {
47
+ return Result.err(new Error('cross not available'));
51
48
  }
52
- const addResult = await ctx.follow<{ id: string; name: string }>(
49
+ const addResult = await ctx.cross<{ id: string; name: string }>(
53
50
  'entity.add',
54
51
  { name: input.name }
55
52
  );
@@ -57,7 +54,7 @@ const onboardTrail = trail('entity.onboard', {
57
54
  return Result.err(addResult.error);
58
55
  }
59
56
 
60
- const relateResult = await ctx.follow<{ from: string; to: string }>(
57
+ const relateResult = await ctx.cross<{ from: string; to: string }>(
61
58
  'entity.relate',
62
59
  { from: addResult.value.name, to: input.relatedTo }
63
60
  );
@@ -70,6 +67,9 @@ const onboardTrail = trail('entity.onboard', {
70
67
  relatedTo: relateResult.value.to,
71
68
  });
72
69
  },
70
+ crosses: ['entity.add', 'entity.relate'],
71
+ input: z.object({ name: z.string(), relatedTo: z.string() }),
72
+ output: z.object({ name: z.string(), relatedTo: z.string() }),
73
73
  });
74
74
 
75
75
  const trailsMap = new Map<string, AnyTrail>([
@@ -83,9 +83,9 @@ const trailsMap = new Map<string, AnyTrail>([
83
83
 
84
84
  const opts = { trails: trailsMap };
85
85
 
86
- describe('testFollows: expectOk', () => {
86
+ describe('testCrosses: expectOk', () => {
87
87
  // eslint-disable-next-line jest/require-hook
88
- testFollows(
88
+ testCrosses(
89
89
  onboardTrail,
90
90
  [
91
91
  {
@@ -98,14 +98,14 @@ describe('testFollows: expectOk', () => {
98
98
  );
99
99
  });
100
100
 
101
- describe('testFollows: expectFollowed', () => {
101
+ describe('testCrosses: expectCrossed', () => {
102
102
  // eslint-disable-next-line jest/require-hook
103
- testFollows(
103
+ testCrosses(
104
104
  onboardTrail,
105
105
  [
106
106
  {
107
- description: 'follows add then relate in order',
108
- expectFollowed: ['entity.add', 'entity.relate'],
107
+ description: 'crosses add then relate in order',
108
+ expectCrossed: ['entity.add', 'entity.relate'],
109
109
  expectOk: true,
110
110
  input: { name: 'Alpha', relatedTo: 'Beta' },
111
111
  },
@@ -114,14 +114,14 @@ describe('testFollows: expectFollowed', () => {
114
114
  );
115
115
  });
116
116
 
117
- describe('testFollows: expectFollowedCount', () => {
117
+ describe('testCrosses: expectCrossedCount', () => {
118
118
  // eslint-disable-next-line jest/require-hook
119
- testFollows(
119
+ testCrosses(
120
120
  onboardTrail,
121
121
  [
122
122
  {
123
- description: 'each trail followed exactly once',
124
- expectFollowedCount: { 'entity.add': 1, 'entity.relate': 1 },
123
+ description: 'each trail crossed exactly once',
124
+ expectCrossedCount: { 'entity.add': 1, 'entity.relate': 1 },
125
125
  expectOk: true,
126
126
  input: { name: 'Alpha', relatedTo: 'Beta' },
127
127
  },
@@ -130,9 +130,9 @@ describe('testFollows: expectFollowedCount', () => {
130
130
  );
131
131
  });
132
132
 
133
- describe('testFollows: injectFromExample', () => {
133
+ describe('testCrosses: injectFromExample', () => {
134
134
  // eslint-disable-next-line jest/require-hook
135
- testFollows(
135
+ testCrosses(
136
136
  onboardTrail,
137
137
  [
138
138
  {
@@ -147,9 +147,9 @@ describe('testFollows: injectFromExample', () => {
147
147
  );
148
148
  });
149
149
 
150
- describe('testFollows: expectValue', () => {
150
+ describe('testCrosses: expectValue', () => {
151
151
  // eslint-disable-next-line jest/require-hook
152
- testFollows(
152
+ testCrosses(
153
153
  onboardTrail,
154
154
  [
155
155
  {
@@ -163,51 +163,48 @@ describe('testFollows: expectValue', () => {
163
163
  });
164
164
 
165
165
  // ---------------------------------------------------------------------------
166
- // Nested follow chain: A → B → C
166
+ // Nested cross chain: A → B → C
167
167
  // ---------------------------------------------------------------------------
168
168
 
169
169
  const leafTrail = trail('step.leaf', {
170
+ blaze: (input: { value: string }) => Result.ok({ leaf: input.value }),
170
171
  description: 'Leaf trail in a nested chain',
171
172
  input: z.object({ value: z.string() }),
172
173
  output: z.object({ leaf: z.string() }),
173
- run: (input: { value: string }) => Result.ok({ leaf: input.value }),
174
174
  });
175
175
 
176
176
  const middleTrail = trail('step.middle', {
177
- description: 'Middle trail that follows the leaf',
178
- follow: ['step.leaf'],
179
- input: z.object({ value: z.string() }),
180
- output: z.object({ middle: z.string() }),
181
- run: async (input: { value: string }, ctx: TrailContext) => {
182
- if (!ctx.follow) {
183
- return Result.err(new Error('follow not available'));
177
+ blaze: async (input: { value: string }, ctx: TrailContext) => {
178
+ if (!ctx.cross) {
179
+ return Result.err(new Error('cross not available'));
184
180
  }
185
- const leafResult = await ctx.follow<{ leaf: string }>('step.leaf', input);
181
+ const leafResult = await ctx.cross<{ leaf: string }>('step.leaf', input);
186
182
  if (leafResult.isErr()) {
187
183
  return leafResult;
188
184
  }
189
185
  return Result.ok({ middle: leafResult.value.leaf });
190
186
  },
187
+ crosses: ['step.leaf'],
188
+ description: 'Middle trail that crosses the leaf',
189
+ input: z.object({ value: z.string() }),
190
+ output: z.object({ middle: z.string() }),
191
191
  });
192
192
 
193
193
  const nestedRootTrail = trail('step.root', {
194
- description: 'Root trail that follows the middle trail',
195
- follow: ['step.middle'],
196
- input: z.object({ value: z.string() }),
197
- output: z.object({ root: z.string() }),
198
- run: async (input: { value: string }, ctx: TrailContext) => {
199
- if (!ctx.follow) {
200
- return Result.err(new Error('follow not available'));
194
+ blaze: async (input: { value: string }, ctx: TrailContext) => {
195
+ if (!ctx.cross) {
196
+ return Result.err(new Error('cross not available'));
201
197
  }
202
- const midResult = await ctx.follow<{ middle: string }>(
203
- 'step.middle',
204
- input
205
- );
198
+ const midResult = await ctx.cross<{ middle: string }>('step.middle', input);
206
199
  if (midResult.isErr()) {
207
200
  return midResult;
208
201
  }
209
202
  return Result.ok({ root: midResult.value.middle });
210
203
  },
204
+ crosses: ['step.middle'],
205
+ description: 'Root trail that crosses the middle trail',
206
+ input: z.object({ value: z.string() }),
207
+ output: z.object({ root: z.string() }),
211
208
  });
212
209
 
213
210
  const nestedTrailsMap = new Map<string, AnyTrail>([
@@ -215,13 +212,13 @@ const nestedTrailsMap = new Map<string, AnyTrail>([
215
212
  ['step.middle', middleTrail],
216
213
  ]);
217
214
 
218
- describe('testFollows: nested follow chain (A → B → C)', () => {
215
+ describe('testCrosses: nested cross chain (A → B → C)', () => {
219
216
  // eslint-disable-next-line jest/require-hook
220
- testFollows(
217
+ testCrosses(
221
218
  nestedRootTrail,
222
219
  [
223
220
  {
224
- description: 'nested ctx.follow works through A → B → C',
221
+ description: 'nested ctx.cross works through A → B → C',
225
222
  expectOk: true,
226
223
  expectValue: { root: 'hello' },
227
224
  input: { value: 'hello' },
@@ -231,31 +228,27 @@ describe('testFollows: nested follow chain (A → B → C)', () => {
231
228
  );
232
229
  });
233
230
 
234
- const mockDbService = service('db.mock.follows', {
231
+ const mockDbProvision = provision('db.mock.crosses', {
235
232
  create: () => Result.ok({ source: 'factory' }),
236
233
  mock: () => ({ source: 'mock' }),
237
234
  });
238
235
 
239
- const serviceLeafTrail = trail('service.leaf', {
240
- description: 'Leaf trail that reads from a service',
236
+ const provisionLeafTrail = trail('provision.leaf', {
237
+ blaze: (_input, ctx) =>
238
+ Result.ok({ childSource: mockDbProvision.from(ctx).source }),
239
+ description: 'Leaf trail that reads from a provision',
241
240
  input: z.object({}),
242
241
  output: z.object({ childSource: z.string() }),
243
- run: (_input, ctx) =>
244
- Result.ok({ childSource: mockDbService.from(ctx).source }),
245
- services: [mockDbService],
242
+ provisions: [mockDbProvision],
246
243
  });
247
244
 
248
- const serviceRootTrail = trail('service.root', {
249
- description: 'Root trail that reads from a service and follows a child trail',
250
- follow: ['service.leaf'],
251
- input: z.object({}),
252
- output: z.object({ childSource: z.string(), rootSource: z.string() }),
253
- run: async (_input, ctx: TrailContext) => {
254
- if (!ctx.follow) {
255
- return Result.err(new Error('follow not available'));
245
+ const provisionRootTrail = trail('provision.root', {
246
+ blaze: async (_input, ctx: TrailContext) => {
247
+ if (!ctx.cross) {
248
+ return Result.err(new Error('cross not available'));
256
249
  }
257
- const childResult = await ctx.follow<{ childSource: string }>(
258
- 'service.leaf',
250
+ const childResult = await ctx.cross<{ childSource: string }>(
251
+ 'provision.leaf',
259
252
  {}
260
253
  );
261
254
  if (childResult.isErr()) {
@@ -264,46 +257,46 @@ const serviceRootTrail = trail('service.root', {
264
257
 
265
258
  return Result.ok({
266
259
  childSource: childResult.value.childSource,
267
- rootSource: mockDbService.from(ctx).source,
260
+ rootSource: mockDbProvision.from(ctx).source,
268
261
  });
269
262
  },
270
- services: [mockDbService],
263
+ crosses: ['provision.leaf'],
264
+ description:
265
+ 'Root trail that reads from a provision and crosses a child trail',
266
+ input: z.object({}),
267
+ output: z.object({ childSource: z.string(), rootSource: z.string() }),
268
+ provisions: [mockDbProvision],
271
269
  });
272
270
 
273
- const serviceTrailsMap = new Map<string, AnyTrail>([
274
- ['service.leaf', serviceLeafTrail],
271
+ const provisionTrailsMap = new Map<string, AnyTrail>([
272
+ ['provision.leaf', provisionLeafTrail],
275
273
  ]);
276
274
 
277
- const statefulMockDbService = service('db.mock.follows.stateful', {
275
+ const statefulMockDbProvision = provision('db.mock.crosses.stateful', {
278
276
  create: () => Result.ok({ count: 0 }),
279
277
  mock: () => ({ count: 0 }),
280
278
  });
281
279
 
282
- const statefulServiceLeafTrail = trail('service.stateful.leaf', {
283
- description: 'Leaf trail that observes the current mock service state',
280
+ const statefulProvisionLeafTrail = trail('provision.stateful.leaf', {
281
+ blaze: (_input, ctx) =>
282
+ Result.ok({ childCount: statefulMockDbProvision.from(ctx).count }),
283
+ description: 'Leaf trail that observes the current mock provision state',
284
284
  input: z.object({}),
285
285
  output: z.object({ childCount: z.number() }),
286
- run: (_input, ctx) =>
287
- Result.ok({ childCount: statefulMockDbService.from(ctx).count }),
288
- services: [statefulMockDbService],
286
+ provisions: [statefulMockDbProvision],
289
287
  });
290
288
 
291
- const statefulServiceRootTrail = trail('service.stateful.root', {
292
- description:
293
- 'Root trail that mutates a mocked service and follows a child trail',
294
- follow: ['service.stateful.leaf'],
295
- input: z.object({}),
296
- output: z.object({ childCount: z.number(), rootCount: z.number() }),
297
- run: async (_input, ctx: TrailContext) => {
298
- if (!ctx.follow) {
299
- return Result.err(new Error('follow not available'));
289
+ const statefulProvisionRootTrail = trail('provision.stateful.root', {
290
+ blaze: async (_input, ctx: TrailContext) => {
291
+ if (!ctx.cross) {
292
+ return Result.err(new Error('cross not available'));
300
293
  }
301
294
 
302
- const statefulService = statefulMockDbService.from(ctx);
303
- statefulService.count += 1;
295
+ const statefulProvision = statefulMockDbProvision.from(ctx);
296
+ statefulProvision.count += 1;
304
297
 
305
- const childResult = await ctx.follow<{ childCount: number }>(
306
- 'service.stateful.leaf',
298
+ const childResult = await ctx.cross<{ childCount: number }>(
299
+ 'provision.stateful.leaf',
307
300
  {}
308
301
  );
309
302
  if (childResult.isErr()) {
@@ -312,45 +305,46 @@ const statefulServiceRootTrail = trail('service.stateful.root', {
312
305
 
313
306
  return Result.ok({
314
307
  childCount: childResult.value.childCount,
315
- rootCount: statefulService.count,
308
+ rootCount: statefulProvision.count,
316
309
  });
317
310
  },
318
- services: [statefulMockDbService],
311
+ crosses: ['provision.stateful.leaf'],
312
+ description:
313
+ 'Root trail that mutates a mocked provision and crosses a child trail',
314
+ input: z.object({}),
315
+ output: z.object({ childCount: z.number(), rootCount: z.number() }),
316
+ provisions: [statefulMockDbProvision],
319
317
  });
320
318
 
321
- const statefulServiceTrailsMap = new Map<string, AnyTrail>([
322
- ['service.stateful.leaf', statefulServiceLeafTrail],
319
+ const statefulProvisionTrailsMap = new Map<string, AnyTrail>([
320
+ ['provision.stateful.leaf', statefulProvisionLeafTrail],
323
321
  ]);
324
322
 
325
- const scenarioStateService = service('db.mock.scenarios', {
323
+ const scenarioStateProvision = provision('db.mock.scenarios', {
326
324
  create: () => Result.ok({ count: 0 }),
327
325
  mock: () => ({ count: 0 }),
328
326
  });
329
327
 
330
- const scenarioLeafTrail = trail('service.scenario.leaf', {
328
+ const scenarioLeafTrail = trail('provision.scenario.leaf', {
329
+ blaze: (_input, ctx) =>
330
+ Result.ok({ count: scenarioStateProvision.from(ctx).count }),
331
331
  description: 'Leaf trail that reads mutable scenario state',
332
332
  input: z.object({}),
333
333
  output: z.object({ count: z.number() }),
334
- run: (_input, ctx) =>
335
- Result.ok({ count: scenarioStateService.from(ctx).count }),
336
- services: [scenarioStateService],
334
+ provisions: [scenarioStateProvision],
337
335
  });
338
336
 
339
- const scenarioRootTrail = trail('service.scenario.root', {
340
- description: 'Root trail that mutates scenario state and follows a leaf',
341
- follow: ['service.scenario.leaf'],
342
- input: z.object({}),
343
- output: z.object({ count: z.number() }),
344
- run: async (_input, ctx: TrailContext) => {
345
- if (!ctx.follow) {
346
- return Result.err(new Error('follow not available'));
337
+ const scenarioRootTrail = trail('provision.scenario.root', {
338
+ blaze: async (_input, ctx: TrailContext) => {
339
+ if (!ctx.cross) {
340
+ return Result.err(new Error('cross not available'));
347
341
  }
348
342
 
349
- const state = scenarioStateService.from(ctx);
343
+ const state = scenarioStateProvision.from(ctx);
350
344
  state.count += 1;
351
345
 
352
- const leafResult = await ctx.follow<{ count: number }>(
353
- 'service.scenario.leaf',
346
+ const leafResult = await ctx.cross<{ count: number }>(
347
+ 'provision.scenario.leaf',
354
348
  {}
355
349
  );
356
350
  if (leafResult.isErr()) {
@@ -359,34 +353,32 @@ const scenarioRootTrail = trail('service.scenario.root', {
359
353
 
360
354
  return Result.ok({ count: leafResult.value.count });
361
355
  },
362
- services: [scenarioStateService],
356
+ crosses: ['provision.scenario.leaf'],
357
+ description: 'Root trail that mutates scenario state and crosses a leaf',
358
+ input: z.object({}),
359
+ output: z.object({ count: z.number() }),
360
+ provisions: [scenarioStateProvision],
363
361
  });
364
362
 
365
- const scenarioTrailsMap = new Map<string, AnyTrail>([
366
- ['service.scenario.leaf', scenarioLeafTrail],
363
+ const scenarioProvisionTrailsMap = new Map<string, AnyTrail>([
364
+ ['provision.scenario.leaf', scenarioLeafTrail],
367
365
  ]);
368
366
 
369
- const transformedFollowLeafTrail = trail('follow.transformed.leaf', {
370
- description: 'Leaf trail in a transformed follow chain',
367
+ const transformedCrossLeafTrail = trail('cross.transformed.leaf', {
368
+ blaze: (input: { value: number }) => Result.ok({ value: input.value }),
369
+ description: 'Leaf trail in a transformed cross chain',
371
370
  input: z.object({ value: z.number() }),
372
371
  output: z.object({ value: z.number() }),
373
- run: (input: { value: number }) => Result.ok({ value: input.value }),
374
372
  });
375
373
 
376
- const transformedFollowRootTrail = trail('follow.transformed.root', {
377
- description: 'Root trail that transforms input once before following',
378
- follow: ['follow.transformed.leaf'],
379
- input: z
380
- .object({ value: z.string() })
381
- .transform(({ value }) => ({ value: Number(value) + 1 })),
382
- output: z.object({ root: z.number() }),
383
- run: async (input: { value: number }, ctx: TrailContext) => {
384
- if (!ctx.follow) {
385
- return Result.err(new Error('follow not available'));
374
+ const transformedCrossRootTrail = trail('cross.transformed.root', {
375
+ blaze: async (input: { value: number }, ctx: TrailContext) => {
376
+ if (!ctx.cross) {
377
+ return Result.err(new Error('cross not available'));
386
378
  }
387
379
 
388
- const leafResult = await ctx.follow<{ value: number }>(
389
- 'follow.transformed.leaf',
380
+ const leafResult = await ctx.cross<{ value: number }>(
381
+ 'cross.transformed.leaf',
390
382
  { value: input.value }
391
383
  );
392
384
  if (leafResult.isErr()) {
@@ -395,38 +387,40 @@ const transformedFollowRootTrail = trail('follow.transformed.root', {
395
387
 
396
388
  return Result.ok({ root: leafResult.value.value });
397
389
  },
390
+ crosses: ['cross.transformed.leaf'],
391
+ description: 'Root trail that transforms input once before crossing',
392
+ input: z
393
+ .object({ value: z.string() })
394
+ .transform(({ value }) => ({ value: Number(value) + 1 })),
395
+ output: z.object({ root: z.number() }),
398
396
  });
399
397
 
400
- const transformedFollowTrailsMap = new Map<string, AnyTrail>([
401
- ['follow.transformed.leaf', transformedFollowLeafTrail],
398
+ const transformedCrossTrailsMap = new Map<string, AnyTrail>([
399
+ ['cross.transformed.leaf', transformedCrossLeafTrail],
402
400
  ]);
403
401
 
404
- const undeclaredFollowService = service('db.undeclared.follows', {
402
+ const undeclaredCrossProvision = provision('db.undeclared.crosses', {
405
403
  create: () => Result.ok({ source: 'factory' }),
406
404
  mock: () => ({ source: 'mock' }),
407
405
  });
408
406
 
409
- const undeclaredServiceLeafTrail = trail('service.undeclared.leaf', {
410
- description: 'Leaf trail that declares the shared service',
407
+ const undeclaredProvisionLeafTrail = trail('provision.undeclared.leaf', {
408
+ blaze: (_input, ctx) =>
409
+ Result.ok({ childSource: undeclaredCrossProvision.from(ctx).source }),
410
+ description: 'Leaf trail that declares the shared provision',
411
411
  input: z.object({}),
412
412
  output: z.object({ childSource: z.string() }),
413
- run: (_input, ctx) =>
414
- Result.ok({ childSource: undeclaredFollowService.from(ctx).source }),
415
- services: [undeclaredFollowService],
413
+ provisions: [undeclaredCrossProvision],
416
414
  });
417
415
 
418
- const undeclaredServiceRootTrail = trail('service.undeclared.root', {
419
- description: 'Root trail that uses a service without declaring it',
420
- follow: ['service.undeclared.leaf'],
421
- input: z.object({}),
422
- output: z.object({ childSource: z.string(), rootSource: z.string() }),
423
- run: async (_input, ctx: TrailContext) => {
424
- if (!ctx.follow) {
425
- return Result.err(new Error('follow not available'));
416
+ const undeclaredProvisionRootTrail = trail('provision.undeclared.root', {
417
+ blaze: async (_input, ctx: TrailContext) => {
418
+ if (!ctx.cross) {
419
+ return Result.err(new Error('cross not available'));
426
420
  }
427
421
 
428
- const childResult = await ctx.follow<{ childSource: string }>(
429
- 'service.undeclared.leaf',
422
+ const childResult = await ctx.cross<{ childSource: string }>(
423
+ 'provision.undeclared.leaf',
430
424
  {}
431
425
  );
432
426
  if (childResult.isErr()) {
@@ -435,92 +429,96 @@ const undeclaredServiceRootTrail = trail('service.undeclared.root', {
435
429
 
436
430
  return Result.ok({
437
431
  childSource: childResult.value.childSource,
438
- rootSource: undeclaredFollowService.from(ctx).source,
432
+ rootSource: undeclaredCrossProvision.from(ctx).source,
439
433
  });
440
434
  },
435
+ crosses: ['provision.undeclared.leaf'],
436
+ description: 'Root trail that uses a provision without declaring it',
437
+ input: z.object({}),
438
+ output: z.object({ childSource: z.string(), rootSource: z.string() }),
441
439
  });
442
440
 
443
- const undeclaredServiceTrailsMap = new Map<string, AnyTrail>([
444
- ['service.undeclared.leaf', undeclaredServiceLeafTrail],
441
+ const undeclaredProvisionTrailsMap = new Map<string, AnyTrail>([
442
+ ['provision.undeclared.leaf', undeclaredProvisionLeafTrail],
445
443
  ]);
446
444
 
447
- const unrelatedFollowService = service('db.unrelated.follows', {
445
+ const unrelatedCrossProvision = provision('db.unrelated.crosses', {
448
446
  create: () => Result.ok({ source: 'factory' }),
449
447
  mock: () => {
450
448
  throw new Error('unrelated mock should not be resolved');
451
449
  },
452
450
  });
453
451
 
454
- const unrelatedServiceTrail = trail('service.unrelated', {
452
+ const unrelatedProvisionTrail = trail('provision.unrelated', {
453
+ blaze: (_input, ctx) =>
454
+ Result.ok({ source: unrelatedCrossProvision.from(ctx).source }),
455
455
  description: 'Trail that should not be traversed or mocked',
456
456
  input: z.object({}),
457
457
  output: z.object({ source: z.string() }),
458
- run: (_input, ctx) =>
459
- Result.ok({ source: unrelatedFollowService.from(ctx).source }),
460
- services: [unrelatedFollowService],
458
+ provisions: [unrelatedCrossProvision],
461
459
  });
462
460
 
463
- const unrelatedServiceTrailsMap = new Map<string, AnyTrail>([
464
- ['service.unrelated', unrelatedServiceTrail],
461
+ const unrelatedProvisionTrailsMap = new Map<string, AnyTrail>([
462
+ ['provision.unrelated', unrelatedProvisionTrail],
465
463
  ]);
466
464
 
467
- describe('testFollows service mocks', () => {
465
+ describe('testCrosses provision mocks', () => {
468
466
  // eslint-disable-next-line jest/require-hook
469
- testFollows(
470
- serviceRootTrail,
467
+ testCrosses(
468
+ provisionRootTrail,
471
469
  [
472
470
  {
473
- description: 'propagates auto-resolved service mocks through follow',
471
+ description: 'propagates auto-resolved provision mocks through cross',
474
472
  expectValue: { childSource: 'mock', rootSource: 'mock' },
475
473
  input: {},
476
474
  },
477
475
  ],
478
- { trails: serviceTrailsMap }
476
+ { trails: provisionTrailsMap }
479
477
  );
480
478
  });
481
479
 
482
- describe('testFollows service mocks are fresh per scenario', () => {
480
+ describe('testCrosses provision mocks are fresh per scenario', () => {
483
481
  // eslint-disable-next-line jest/require-hook
484
- testFollows(
482
+ testCrosses(
485
483
  scenarioRootTrail,
486
484
  [
487
485
  {
488
- description: 'first scenario sees a fresh mutable service',
486
+ description: 'first scenario sees a fresh mutable provision',
489
487
  expectValue: { count: 1 },
490
488
  input: {},
491
489
  },
492
490
  {
493
- description: 'second scenario also sees a fresh mutable service',
491
+ description: 'second scenario also sees a fresh mutable provision',
494
492
  expectValue: { count: 1 },
495
493
  input: {},
496
494
  },
497
495
  ],
498
- { trails: scenarioTrailsMap }
496
+ { trails: scenarioProvisionTrailsMap }
499
497
  );
500
498
  });
501
499
 
502
- describe('testFollows explicit service overrides', () => {
500
+ describe('testCrosses explicit provision overrides', () => {
503
501
  // eslint-disable-next-line jest/require-hook
504
- testFollows(
505
- serviceRootTrail,
502
+ testCrosses(
503
+ provisionRootTrail,
506
504
  [
507
505
  {
508
- description: 'propagates explicit service overrides through follow',
506
+ description: 'propagates explicit provision overrides through cross',
509
507
  expectValue: { childSource: 'override', rootSource: 'override' },
510
508
  input: {},
511
509
  },
512
510
  ],
513
511
  {
514
- services: { 'db.mock.follows': { source: 'override' } },
515
- trails: serviceTrailsMap,
512
+ provisions: { 'db.mock.crosses': { source: 'override' } },
513
+ trails: provisionTrailsMap,
516
514
  }
517
515
  );
518
516
  });
519
517
 
520
- describe('testFollows raw transformed input', () => {
518
+ describe('testCrosses raw transformed input', () => {
521
519
  // eslint-disable-next-line jest/require-hook
522
- testFollows(
523
- transformedFollowRootTrail,
520
+ testCrosses(
521
+ transformedCrossRootTrail,
524
522
  [
525
523
  {
526
524
  description: 'raw scenario input is only transformed once',
@@ -528,62 +526,62 @@ describe('testFollows raw transformed input', () => {
528
526
  input: { value: '1' },
529
527
  },
530
528
  ],
531
- { trails: transformedFollowTrailsMap }
529
+ { trails: transformedCrossTrailsMap }
532
530
  );
533
531
  });
534
532
 
535
- describe('testFollows service declarations', () => {
533
+ describe('testCrosses provision declarations', () => {
536
534
  // eslint-disable-next-line jest/require-hook
537
- testFollows(
538
- undeclaredServiceRootTrail,
535
+ testCrosses(
536
+ undeclaredProvisionRootTrail,
539
537
  [
540
538
  {
541
- description: 'fails when the root trail omits a required service',
539
+ description: 'fails when the root trail omits a required provision',
542
540
  expectErr: InternalError,
543
- expectErrMessage: undeclaredFollowService.id,
541
+ expectErrMessage: undeclaredCrossProvision.id,
544
542
  input: {},
545
543
  },
546
544
  ],
547
- { trails: undeclaredServiceTrailsMap }
545
+ { trails: undeclaredProvisionTrailsMap }
548
546
  );
549
547
  });
550
548
 
551
- describe('testFollows only resolves mocks for trails under test', () => {
549
+ describe('testCrosses only resolves mocks for trails under test', () => {
552
550
  // eslint-disable-next-line jest/require-hook
553
- testFollows(
554
- serviceRootTrail,
551
+ testCrosses(
552
+ provisionRootTrail,
555
553
  [
556
554
  {
557
- description: 'unrelated service mocks are not resolved',
555
+ description: 'unrelated provision mocks are not resolved',
558
556
  expectValue: { childSource: 'mock', rootSource: 'mock' },
559
557
  input: {},
560
558
  },
561
559
  ],
562
560
  {
563
561
  trails: new Map<string, AnyTrail>([
564
- ...serviceTrailsMap.entries(),
565
- ...unrelatedServiceTrailsMap.entries(),
562
+ ...provisionTrailsMap.entries(),
563
+ ...unrelatedProvisionTrailsMap.entries(),
566
564
  ]),
567
565
  }
568
566
  );
569
567
  });
570
568
 
571
- describe('testFollows fresh service mocks per scenario', () => {
569
+ describe('testCrosses fresh provision mocks per scenario', () => {
572
570
  // eslint-disable-next-line jest/require-hook
573
- testFollows(
574
- statefulServiceRootTrail,
571
+ testCrosses(
572
+ statefulProvisionRootTrail,
575
573
  [
576
574
  {
577
- description: 'first scenario gets a fresh service mock',
575
+ description: 'first scenario gets a fresh provision mock',
578
576
  expectValue: { childCount: 1, rootCount: 1 },
579
577
  input: {},
580
578
  },
581
579
  {
582
- description: 'second scenario also gets a fresh service mock',
580
+ description: 'second scenario also gets a fresh provision mock',
583
581
  expectValue: { childCount: 1, rootCount: 1 },
584
582
  input: {},
585
583
  },
586
584
  ],
587
- { trails: statefulServiceTrailsMap }
585
+ { trails: statefulProvisionTrailsMap }
588
586
  );
589
587
  });