@ontrails/core 1.0.0-beta.12 → 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 (98) hide show
  1. package/CHANGELOG.md +59 -35
  2. package/README.md +19 -19
  3. package/dist/context.d.ts +1 -1
  4. package/dist/context.d.ts.map +1 -1
  5. package/dist/context.js +5 -4
  6. package/dist/context.js.map +1 -1
  7. package/dist/derive.d.ts +8 -3
  8. package/dist/derive.d.ts.map +1 -1
  9. package/dist/derive.js +7 -7
  10. package/dist/derive.js.map +1 -1
  11. package/dist/event.d.ts +4 -41
  12. package/dist/event.d.ts.map +1 -1
  13. package/dist/event.js +4 -14
  14. package/dist/event.js.map +1 -1
  15. package/dist/execute.d.ts +10 -10
  16. package/dist/execute.d.ts.map +1 -1
  17. package/dist/execute.js +35 -28
  18. package/dist/execute.js.map +1 -1
  19. package/dist/gate.d.ts +17 -0
  20. package/dist/gate.d.ts.map +1 -0
  21. package/dist/gate.js +21 -0
  22. package/dist/gate.js.map +1 -0
  23. package/dist/index.d.ts +10 -8
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +9 -8
  26. package/dist/index.js.map +1 -1
  27. package/dist/layer.d.ts +5 -16
  28. package/dist/layer.d.ts.map +1 -1
  29. package/dist/layer.js +3 -20
  30. package/dist/layer.js.map +1 -1
  31. package/dist/provision-config.d.ts +22 -0
  32. package/dist/provision-config.d.ts.map +1 -0
  33. package/dist/provision-config.js +210 -0
  34. package/dist/provision-config.js.map +1 -0
  35. package/dist/provision.d.ts +71 -0
  36. package/dist/provision.d.ts.map +1 -0
  37. package/dist/provision.js +56 -0
  38. package/dist/provision.js.map +1 -0
  39. package/dist/run.d.ts +27 -0
  40. package/dist/run.d.ts.map +1 -0
  41. package/dist/run.js +34 -0
  42. package/dist/run.js.map +1 -0
  43. package/dist/service-config.d.ts +7 -7
  44. package/dist/service-config.d.ts.map +1 -1
  45. package/dist/service-config.js +75 -73
  46. package/dist/service-config.js.map +1 -1
  47. package/dist/service.d.ts +32 -36
  48. package/dist/service.d.ts.map +1 -1
  49. package/dist/service.js +21 -21
  50. package/dist/service.js.map +1 -1
  51. package/dist/signal.d.ts +33 -0
  52. package/dist/signal.d.ts.map +1 -0
  53. package/dist/signal.js +17 -0
  54. package/dist/signal.js.map +1 -0
  55. package/dist/topo.d.ts +10 -10
  56. package/dist/topo.d.ts.map +1 -1
  57. package/dist/topo.js +48 -35
  58. package/dist/topo.js.map +1 -1
  59. package/dist/trail.d.ts +16 -16
  60. package/dist/trail.d.ts.map +1 -1
  61. package/dist/trail.js +5 -4
  62. package/dist/trail.js.map +1 -1
  63. package/dist/types.d.ts +10 -10
  64. package/dist/types.d.ts.map +1 -1
  65. package/dist/types.js +2 -2
  66. package/dist/types.js.map +1 -1
  67. package/dist/validate-topo.d.ts +2 -2
  68. package/dist/validate-topo.js +31 -31
  69. package/dist/validate-topo.js.map +1 -1
  70. package/package.json +1 -1
  71. package/src/__tests__/context.test.ts +8 -8
  72. package/src/__tests__/execute.test.ts +124 -122
  73. package/src/__tests__/{layer.test.ts → gate.test.ts} +26 -26
  74. package/src/__tests__/{dispatch.test.ts → run.test.ts} +39 -39
  75. package/src/__tests__/service-config.test.ts +41 -37
  76. package/src/__tests__/service.test.ts +71 -71
  77. package/src/__tests__/{event.test.ts → signal.test.ts} +15 -15
  78. package/src/__tests__/topo.test.ts +54 -54
  79. package/src/__tests__/trail-permit.test.ts +4 -4
  80. package/src/__tests__/trail.test.ts +58 -58
  81. package/src/__tests__/type-utils.test.ts +3 -3
  82. package/src/__tests__/validate-topo.test.ts +38 -38
  83. package/src/context.ts +5 -4
  84. package/src/derive.ts +8 -8
  85. package/src/event.ts +11 -73
  86. package/src/execute.ts +56 -40
  87. package/src/{layer.ts → gate.ts} +13 -13
  88. package/src/index.ts +24 -22
  89. package/src/{service-config.ts → provision-config.ts} +124 -105
  90. package/src/provision.ts +148 -0
  91. package/src/{dispatch.ts → run.ts} +8 -8
  92. package/src/signal.ts +65 -0
  93. package/src/topo.ts +74 -52
  94. package/src/trail.ts +23 -22
  95. package/src/types.ts +11 -11
  96. package/src/validate-topo.ts +33 -33
  97. package/tsconfig.tsbuildinfo +1 -1
  98. package/src/service.ts +0 -145
@@ -1,24 +1,24 @@
1
- import { describe, test, expect } from 'bun:test';
1
+ import { describe, expect, test } from 'bun:test';
2
2
 
3
3
  import { z } from 'zod';
4
4
 
5
- import { event } from '../event';
5
+ import { signal } from '../signal.js';
6
6
 
7
7
  const payloadSchema = z.object({
8
8
  action: z.string(),
9
9
  userId: z.string(),
10
10
  });
11
11
 
12
- const userAction = event('user.action', {
12
+ const userAction = signal('user.action', {
13
13
  description: 'A user performed an action',
14
14
  from: ['auth.login', 'auth.signup'],
15
- metadata: { domain: 'auth', priority: 1 },
15
+ meta: { domain: 'auth', priority: 1 },
16
16
  payload: payloadSchema,
17
17
  });
18
18
 
19
- describe('event() basics', () => {
20
- test("returns kind 'event'", () => {
21
- expect(userAction.kind).toBe('event');
19
+ describe('signal() basics', () => {
20
+ test("returns kind 'signal'", () => {
21
+ expect(userAction.kind).toBe('signal');
22
22
  });
23
23
 
24
24
  test('returns correct id', () => {
@@ -45,7 +45,7 @@ describe('event() basics', () => {
45
45
  });
46
46
  });
47
47
 
48
- describe('event() from and metadata', () => {
48
+ describe('signal() from and meta', () => {
49
49
  test('preserves from', () => {
50
50
  expect(userAction.from).toEqual(['auth.login', 'auth.signup']);
51
51
  });
@@ -54,29 +54,29 @@ describe('event() from and metadata', () => {
54
54
  expect(Object.isFrozen(userAction.from)).toBe(true);
55
55
  });
56
56
 
57
- test('preserves metadata', () => {
58
- expect(userAction.metadata).toEqual({ domain: 'auth', priority: 1 });
57
+ test('preserves meta', () => {
58
+ expect(userAction.meta).toEqual({ domain: 'auth', priority: 1 });
59
59
  });
60
60
 
61
61
  test('optional fields default to undefined', () => {
62
- const minimal = event('minimal', {
62
+ const minimal = signal('minimal', {
63
63
  payload: z.string(),
64
64
  });
65
65
  expect(minimal.description).toBeUndefined();
66
66
  expect(minimal.from).toBeUndefined();
67
- expect(minimal.metadata).toBeUndefined();
67
+ expect(minimal.meta).toBeUndefined();
68
68
  });
69
69
  });
70
70
 
71
- describe('event() single-object overload', () => {
71
+ describe('signal() single-object overload', () => {
72
72
  test('accepts spec with id property', () => {
73
- const e = event({
73
+ const e = signal({
74
74
  from: ['entity.add', 'entity.delete'],
75
75
  id: 'entity.updated',
76
76
  payload: z.object({ entityId: z.string() }),
77
77
  });
78
78
  expect(e.id).toBe('entity.updated');
79
- expect(e.kind).toBe('event');
79
+ expect(e.kind).toBe('signal');
80
80
  expect(e.from).toEqual(['entity.add', 'entity.delete']);
81
81
  });
82
82
  });
@@ -3,8 +3,8 @@ import { describe, test, expect } from 'bun:test';
3
3
  import { z } from 'zod';
4
4
 
5
5
  import { ValidationError } from '../errors.js';
6
+ import { provision } from '../provision.js';
6
7
  import { Result } from '../result.js';
7
- import { service } from '../service.js';
8
8
  import { topo } from '../topo.js';
9
9
 
10
10
  // ---------------------------------------------------------------------------
@@ -14,25 +14,25 @@ import { topo } from '../topo.js';
14
14
  // oxlint-disable-next-line require-await -- satisfies async interface without needing await
15
15
  const noop = async () => Result.ok();
16
16
 
17
- const mockTrail = (id: string, follow?: readonly string[]) => ({
18
- follow: Object.freeze([...(follow ?? [])]),
17
+ const mockTrail = (id: string, crosses?: readonly string[]) => ({
18
+ blaze: noop,
19
+ crosses: Object.freeze([...(crosses ?? [])]),
19
20
  id,
20
21
  input: z.object({ x: z.number() }),
21
22
  kind: 'trail' as const,
22
23
  output: z.object({ y: z.number() }),
23
- run: noop,
24
24
  });
25
25
 
26
26
  const mockEvent = (id: string) => ({
27
27
  id,
28
- kind: 'event' as const,
28
+ kind: 'signal' as const,
29
29
  payload: z.object({ payload: z.string() }),
30
30
  });
31
31
 
32
- const mockService = (id: string) =>
33
- service(id, {
32
+ const mockProvision = (id: string) =>
33
+ provision(id, {
34
34
  create: () => Result.ok({ id }),
35
- description: `${id} service`,
35
+ description: `${id} provision`,
36
36
  });
37
37
 
38
38
  // ---------------------------------------------------------------------------
@@ -57,15 +57,15 @@ describe('topo', () => {
57
57
  test('auto-scans exports by kind discriminant', () => {
58
58
  const mod = {
59
59
  event1: mockEvent('e1'),
60
- service1: mockService('s1'),
60
+ provision1: mockProvision('s1'),
61
61
  trail1: mockTrail('t1'),
62
62
  trail2: mockTrail('t2', ['t1']),
63
63
  };
64
64
  const t = topo('app', mod);
65
65
 
66
66
  expect(t.trails.size).toBe(2);
67
- expect(t.events.size).toBe(1);
68
- expect(t.services.size).toBe(1);
67
+ expect(t.signals.size).toBe(1);
68
+ expect(t.provisions.size).toBe(1);
69
69
  });
70
70
 
71
71
  test('collects from multiple modules', () => {
@@ -89,25 +89,25 @@ describe('topo', () => {
89
89
  const t = topo('app', mod);
90
90
 
91
91
  expect(t.trails.size).toBe(1);
92
- expect(t.events.size).toBe(0);
93
- expect(t.services.size).toBe(0);
92
+ expect(t.signals.size).toBe(0);
93
+ expect(t.provisions.size).toBe(0);
94
94
  });
95
95
 
96
- test('trail with follow registers correctly', () => {
96
+ test('trail with crossings registers correctly', () => {
97
97
  const mod = { t: mockTrail('trail-1', ['trail-2']) };
98
98
  const t = topo('app', mod);
99
99
 
100
100
  expect(t.trails.size).toBe(1);
101
101
  const registered = t.trails.get('trail-1');
102
- expect(registered?.follow).toEqual(['trail-2']);
102
+ expect(registered?.crosses).toEqual(['trail-2']);
103
103
  });
104
104
 
105
- test('collects services from modules', () => {
106
- const mod = { db: mockService('db.main') };
105
+ test('collects provisions from modules', () => {
106
+ const mod = { db: mockProvision('db.main') };
107
107
  const t = topo('app', mod);
108
108
 
109
- expect(t.services.size).toBe(1);
110
- expect(t.services.get('db.main')).toBe(mod.db);
109
+ expect(t.provisions.size).toBe(1);
110
+ expect(t.provisions.get('db.main')).toBe(mod.db);
111
111
  });
112
112
  });
113
113
 
@@ -128,17 +128,17 @@ describe('topo', () => {
128
128
 
129
129
  expect(() => topo('app', mod1, mod2)).toThrow(ValidationError);
130
130
  expect(() => topo('app', mod1, mod2)).toThrow(
131
- 'Duplicate event ID: "dup"'
131
+ 'Duplicate signal ID: "dup"'
132
132
  );
133
133
  });
134
134
 
135
- test('rejects duplicate service IDs', () => {
136
- const mod1 = { a: mockService('dup') };
137
- const mod2 = { b: mockService('dup') };
135
+ test('rejects duplicate provision IDs', () => {
136
+ const mod1 = { a: mockProvision('dup') };
137
+ const mod2 = { b: mockProvision('dup') };
138
138
 
139
139
  expect(() => topo('app', mod1, mod2)).toThrow(ValidationError);
140
140
  expect(() => topo('app', mod1, mod2)).toThrow(
141
- 'Duplicate service ID: "dup"'
141
+ 'Duplicate provision ID: "dup"'
142
142
  );
143
143
  });
144
144
  });
@@ -162,26 +162,26 @@ describe('topo accessors', () => {
162
162
  expect(app.count).toBe(1);
163
163
  });
164
164
 
165
- test('serviceCount returns number of services', () => {
166
- const db = mockService('db.main');
167
- const cache = mockService('cache.main');
165
+ test('provisionCount returns number of provisions', () => {
166
+ const db = mockProvision('db.main');
167
+ const cache = mockProvision('cache.main');
168
168
  const app = topo('test', { cache, db });
169
- expect(app.serviceCount).toBe(2);
169
+ expect(app.provisionCount).toBe(2);
170
170
  });
171
171
 
172
172
  test('empty topo has zero count and empty ids', () => {
173
173
  const app = topo('empty');
174
174
  expect(app.count).toBe(0);
175
175
  expect(app.ids()).toEqual([]);
176
- expect(app.serviceCount).toBe(0);
177
- expect(app.serviceIds()).toEqual([]);
176
+ expect(app.provisionCount).toBe(0);
177
+ expect(app.provisionIds()).toEqual([]);
178
178
  });
179
179
 
180
- test('serviceIds() returns all service IDs', () => {
181
- const db = mockService('db.main');
182
- const cache = mockService('cache.main');
180
+ test('provisionIds() returns all provision IDs', () => {
181
+ const db = mockProvision('db.main');
182
+ const cache = mockProvision('cache.main');
183
183
  const app = topo('test', { cache, db });
184
- expect(app.serviceIds().toSorted()).toEqual(['cache.main', 'db.main']);
184
+ expect(app.provisionIds().toSorted()).toEqual(['cache.main', 'db.main']);
185
185
  });
186
186
  });
187
187
 
@@ -192,7 +192,7 @@ describe('topo accessors', () => {
192
192
  describe('Topo', () => {
193
193
  const mod = {
194
194
  e1: mockEvent('event-1'),
195
- s1: mockService('service-1'),
195
+ p1: mockProvision('provision-1'),
196
196
  t1: mockTrail('trail-1'),
197
197
  t2: mockTrail('trail-2'),
198
198
  t3: mockTrail('trail-3', ['trail-1']),
@@ -206,7 +206,7 @@ describe('Topo', () => {
206
206
  expect(app.get('trail-1')).toBe(mod.t1);
207
207
  });
208
208
 
209
- test('retrieves trail with follow by ID', () => {
209
+ test('retrieves trail with crossings by ID', () => {
210
210
  expect(app.get('trail-3')).toBe(mod.t3);
211
211
  });
212
212
 
@@ -220,7 +220,7 @@ describe('Topo', () => {
220
220
  expect(app.has('trail-1')).toBe(true);
221
221
  });
222
222
 
223
- test('returns true for trail with follow', () => {
223
+ test('returns true for trail with crossings', () => {
224
224
  expect(app.has('trail-3')).toBe(true);
225
225
  });
226
226
 
@@ -228,33 +228,33 @@ describe('Topo', () => {
228
228
  expect(app.has('nope')).toBe(false);
229
229
  });
230
230
 
231
- test('returns false for event ID (events are not trails)', () => {
231
+ test('returns false for event ID (signals are not trails)', () => {
232
232
  expect(app.has('event-1')).toBe(false);
233
233
  });
234
234
  });
235
235
 
236
- describe('getService()', () => {
237
- test('retrieves service by ID', () => {
238
- expect(app.getService('service-1')).toBe(mod.s1);
236
+ describe('getProvision()', () => {
237
+ test('retrieves provision by ID', () => {
238
+ expect(app.getProvision('provision-1')).toBe(mod.p1);
239
239
  });
240
240
 
241
- test('returns undefined for unknown service ID', () => {
242
- expect(app.getService('missing-service')).toBeUndefined();
241
+ test('returns undefined for unknown provision ID', () => {
242
+ expect(app.getProvision('missing-provision')).toBeUndefined();
243
243
  });
244
244
  });
245
245
 
246
- describe('hasService()', () => {
247
- test('returns true for known service', () => {
248
- expect(app.hasService('service-1')).toBe(true);
246
+ describe('hasProvision()', () => {
247
+ test('returns true for known provision', () => {
248
+ expect(app.hasProvision('provision-1')).toBe(true);
249
249
  });
250
250
 
251
- test('returns false for unknown service', () => {
252
- expect(app.hasService('missing-service')).toBe(false);
251
+ test('returns false for unknown provision', () => {
252
+ expect(app.hasProvision('missing-provision')).toBe(false);
253
253
  });
254
254
  });
255
255
 
256
256
  describe('listing', () => {
257
- test('list() returns all trails (with and without follow)', () => {
257
+ test('list() returns all trails (with and without crossings)', () => {
258
258
  const items = app.list();
259
259
  expect(items).toHaveLength(3);
260
260
  expect(items).toContain(mod.t1);
@@ -262,16 +262,16 @@ describe('Topo', () => {
262
262
  expect(items).toContain(mod.t3);
263
263
  });
264
264
 
265
- test('listEvents() returns all events', () => {
266
- const items = app.listEvents();
265
+ test('listEvents() returns all signals', () => {
266
+ const items = app.listSignals();
267
267
  expect(items).toHaveLength(1);
268
268
  expect(items).toContain(mod.e1);
269
269
  });
270
270
 
271
- test('listServices() returns all services', () => {
272
- const items = app.listServices();
271
+ test('listProvisions() returns all provisions', () => {
272
+ const items = app.listProvisions();
273
273
  expect(items).toHaveLength(1);
274
- expect(items).toContain(mod.s1);
274
+ expect(items).toContain(mod.p1);
275
275
  });
276
276
  });
277
277
  });
@@ -21,38 +21,38 @@ describe('PermitRequirement type', () => {
21
21
  describe('trail() with permit field', () => {
22
22
  test('accepts permit with scopes', () => {
23
23
  const t = trail('user.delete', {
24
+ blaze: (input) => Result.ok({ deleted: input.id }),
24
25
  input: z.object({ id: z.string() }),
25
26
  intent: 'destroy',
26
27
  permit: { scopes: ['user:write'] },
27
- run: (input) => Result.ok({ deleted: input.id }),
28
28
  });
29
29
  expect(t.permit).toEqual({ scopes: ['user:write'] });
30
30
  });
31
31
 
32
32
  test('accepts permit: public', () => {
33
33
  const t = trail('health.check', {
34
+ blaze: () => Result.ok({ status: 'ok' }),
34
35
  input: z.object({}),
35
36
  intent: 'read',
36
37
  permit: 'public',
37
- run: () => Result.ok({ status: 'ok' }),
38
38
  });
39
39
  expect(t.permit).toBe('public');
40
40
  });
41
41
 
42
42
  test('permit is undefined when omitted (backward compatible)', () => {
43
43
  const t = trail('legacy.trail', {
44
+ blaze: () => Result.ok(),
44
45
  input: z.object({}),
45
- run: () => Result.ok(),
46
46
  });
47
47
  expect(t.permit).toBeUndefined();
48
48
  });
49
49
 
50
50
  test('preserves permit on the frozen Trail object', () => {
51
51
  const t = trail('user.list', {
52
+ blaze: () => Result.ok([]),
52
53
  input: z.object({}),
53
54
  intent: 'read',
54
55
  permit: { scopes: ['user:read'] },
55
- run: () => Result.ok([]),
56
56
  });
57
57
  expect(Object.isFrozen(t)).toBe(true);
58
58
  expect(t.permit).toEqual({ scopes: ['user:read'] });
@@ -4,23 +4,23 @@ import { z } from 'zod';
4
4
 
5
5
  import { createTrailContext } from '../context';
6
6
  import { Result } from '../result';
7
- import { service } from '../service';
7
+ import { provision } from '../provision';
8
8
  import { trail } from '../trail';
9
9
  import type { TrailContext } from '../types';
10
10
 
11
11
  const stubCtx: TrailContext = createTrailContext({
12
+ abortSignal: AbortSignal.timeout(5000),
12
13
  requestId: 'test-123',
13
- signal: AbortSignal.timeout(5000),
14
14
  });
15
15
 
16
- const dbService = service('db.main', {
16
+ const dbProvision = provision('db.main', {
17
17
  create: () =>
18
18
  Result.ok({
19
19
  query(sql: string) {
20
20
  return sql.length;
21
21
  },
22
22
  }),
23
- description: 'Primary database service',
23
+ description: 'Primary database provision',
24
24
  });
25
25
 
26
26
  describe('trail()', () => {
@@ -28,10 +28,10 @@ describe('trail()', () => {
28
28
  const outputSchema = z.object({ greeting: z.string() });
29
29
 
30
30
  const greet = trail('greet', {
31
+ blaze: (input) => Result.ok({ greeting: `Hello, ${input.name}!` }),
31
32
  description: 'Greet someone',
32
33
  input: inputSchema,
33
34
  output: outputSchema,
34
- run: (input) => Result.ok({ greeting: `Hello, ${input.name}!` }),
35
35
  });
36
36
 
37
37
  describe('basics', () => {
@@ -53,28 +53,28 @@ describe('trail()', () => {
53
53
 
54
54
  test('output schema is optional', () => {
55
55
  const minimal = trail('noop', {
56
+ blaze: () => Result.ok(),
56
57
  input: z.object({}),
57
- run: () => Result.ok(),
58
58
  });
59
59
  expect(minimal.output).toBeUndefined();
60
60
  });
61
61
 
62
62
  test('implementation is callable', async () => {
63
- const result = await greet.run({ name: 'World' }, stubCtx);
63
+ const result = await greet.blaze({ name: 'World' }, stubCtx);
64
64
  expect(result.isOk()).toBe(true);
65
65
  expect(result.unwrap()).toEqual({ greeting: 'Hello, World!' });
66
66
  });
67
67
  });
68
68
 
69
- describe('metadata', () => {
69
+ describe('meta', () => {
70
70
  test('examples are stored', () => {
71
71
  const withExamples = trail('echo', {
72
+ blaze: (input) => Result.ok({ text: input.text }),
72
73
  examples: [
73
74
  { error: 'ValidationError', input: { text: '' }, name: 'error-case' },
74
75
  { expected: { text: 'hi' }, input: { text: 'hi' }, name: 'basic' },
75
76
  ],
76
77
  input: z.object({ text: z.string() }),
77
- run: (input) => Result.ok({ text: input.text }),
78
78
  });
79
79
  expect(withExamples.examples).toHaveLength(2);
80
80
  const first = withExamples.examples?.[0];
@@ -83,23 +83,23 @@ describe('trail()', () => {
83
83
  expect(second?.name).toBe('basic');
84
84
  });
85
85
 
86
- test('metadata is stored', () => {
87
- const withMetadata = trail('tagged', {
86
+ test('meta is stored', () => {
87
+ const withMeta = trail('tagged', {
88
+ blaze: () => Result.ok(),
88
89
  input: z.object({}),
89
- metadata: { domain: 'billing', tier: 1 },
90
- run: () => Result.ok(),
90
+ meta: { domain: 'billing', tier: 1 },
91
91
  });
92
- expect(withMetadata.metadata).toEqual({ domain: 'billing', tier: 1 });
92
+ expect(withMeta.meta).toEqual({ domain: 'billing', tier: 1 });
93
93
  });
94
94
 
95
95
  test('detours are stored', () => {
96
96
  const withDetours = trail('orchestrator', {
97
+ blaze: () => Result.ok(),
97
98
  detours: {
98
99
  onFailure: ['alert'],
99
100
  onSuccess: ['notify', 'audit'],
100
101
  },
101
102
  input: z.object({}),
102
- run: () => Result.ok(),
103
103
  });
104
104
  expect(withDetours.detours).toEqual({
105
105
  onFailure: ['alert'],
@@ -108,70 +108,70 @@ describe('trail()', () => {
108
108
  });
109
109
  });
110
110
 
111
- describe('follow', () => {
111
+ describe('crosses', () => {
112
112
  test('defaults to empty frozen array when omitted', () => {
113
113
  const minimal = trail('bare', {
114
+ blaze: () => Result.ok(),
114
115
  input: z.object({}),
115
- run: () => Result.ok(),
116
116
  });
117
- expect(minimal.follow).toEqual([]);
118
- expect(Object.isFrozen(minimal.follow)).toBe(true);
117
+ expect(minimal.crosses).toEqual([]);
118
+ expect(Object.isFrozen(minimal.crosses)).toBe(true);
119
119
  });
120
120
 
121
- test('preserves follow array', () => {
122
- const withFollow = trail('composed', {
123
- follow: ['authenticate', 'validate-session'],
121
+ test('preserves crosses array', () => {
122
+ const withCrosses = trail('composed', {
123
+ blaze: () => Result.ok(),
124
+ crosses: ['authenticate', 'validate-session'],
124
125
  input: z.object({}),
125
- run: () => Result.ok(),
126
126
  });
127
- expect(withFollow.follow).toEqual(['authenticate', 'validate-session']);
127
+ expect(withCrosses.crosses).toEqual(['authenticate', 'validate-session']);
128
128
  });
129
129
 
130
- test('follow array is frozen', () => {
131
- const withFollow = trail('composed', {
132
- follow: ['authenticate'],
130
+ test('crosses array is frozen', () => {
131
+ const withCrosses = trail('composed', {
132
+ blaze: () => Result.ok(),
133
+ crosses: ['authenticate'],
133
134
  input: z.object({}),
134
- run: () => Result.ok(),
135
135
  });
136
- expect(Object.isFrozen(withFollow.follow)).toBe(true);
136
+ expect(Object.isFrozen(withCrosses.crosses)).toBe(true);
137
137
  });
138
138
  });
139
139
 
140
- describe('services', () => {
140
+ describe('provisions', () => {
141
141
  test('defaults to empty frozen array when omitted', () => {
142
142
  const minimal = trail('bare', {
143
+ blaze: () => Result.ok(),
143
144
  input: z.object({}),
144
- run: () => Result.ok(),
145
145
  });
146
- expect(minimal.services).toEqual([]);
147
- expect(Object.isFrozen(minimal.services)).toBe(true);
146
+ expect(minimal.provisions).toEqual([]);
147
+ expect(Object.isFrozen(minimal.provisions)).toBe(true);
148
148
  });
149
149
 
150
- test('preserves declared service objects', () => {
151
- const withServices = trail('search', {
150
+ test('preserves declared provision objects', () => {
151
+ const withProvisions = trail('search', {
152
+ blaze: () => Result.ok(),
152
153
  input: z.object({}),
153
- run: () => Result.ok(),
154
- services: [dbService],
154
+ provisions: [dbProvision],
155
155
  });
156
- expect(withServices.services).toEqual([dbService]);
157
- expect(withServices.services[0]).toBe(dbService);
156
+ expect(withProvisions.provisions).toEqual([dbProvision]);
157
+ expect(withProvisions.provisions[0]).toBe(dbProvision);
158
158
  });
159
159
 
160
- test('services array is frozen', () => {
161
- const withServices = trail('search', {
160
+ test('provisions array is frozen', () => {
161
+ const withProvisions = trail('search', {
162
+ blaze: () => Result.ok(),
162
163
  input: z.object({}),
163
- run: () => Result.ok(),
164
- services: [dbService],
164
+ provisions: [dbProvision],
165
165
  });
166
- expect(Object.isFrozen(withServices.services)).toBe(true);
166
+ expect(Object.isFrozen(withProvisions.provisions)).toBe(true);
167
167
  });
168
168
  });
169
169
 
170
170
  describe('intent and idempotent', () => {
171
171
  test('intent defaults to write', () => {
172
172
  const minimal = trail('bare', {
173
+ blaze: () => Result.ok(),
173
174
  input: z.object({}),
174
- run: () => Result.ok(),
175
175
  });
176
176
  expect(minimal.intent).toBe('write');
177
177
  expect(minimal.idempotent).toBeUndefined();
@@ -179,25 +179,25 @@ describe('trail()', () => {
179
179
 
180
180
  test('intent is preserved when set', () => {
181
181
  const readTrail = trail('reader', {
182
+ blaze: () => Result.ok(),
182
183
  input: z.object({}),
183
184
  intent: 'read',
184
- run: () => Result.ok(),
185
185
  });
186
186
  expect(readTrail.intent).toBe('read');
187
187
 
188
188
  const destroyTrail = trail('destroyer', {
189
+ blaze: () => Result.ok(),
189
190
  input: z.object({}),
190
191
  intent: 'destroy',
191
- run: () => Result.ok(),
192
192
  });
193
193
  expect(destroyTrail.intent).toBe('destroy');
194
194
  });
195
195
 
196
196
  test('idempotent is preserved when set', () => {
197
197
  const t = trail('idempotent', {
198
+ blaze: () => Result.ok(),
198
199
  idempotent: true,
199
200
  input: z.object({}),
200
- run: () => Result.ok(),
201
201
  });
202
202
  expect(t.idempotent).toBe(true);
203
203
  });
@@ -206,10 +206,10 @@ describe('trail()', () => {
206
206
  describe('single-object overload', () => {
207
207
  test('accepts spec with id property', () => {
208
208
  const t = trail({
209
+ blaze: (input: { name: string }, _ctx: TrailContext) =>
210
+ Result.ok({ greeting: `Hi, ${input.name}` }),
209
211
  id: 'entity.show',
210
212
  input: inputSchema,
211
- run: (input: { name: string }, _ctx: TrailContext) =>
212
- Result.ok({ greeting: `Hi, ${input.name}` }),
213
213
  });
214
214
  expect(t.id).toBe('entity.show');
215
215
  expect(t.kind).toBe('trail');
@@ -217,40 +217,40 @@ describe('trail()', () => {
217
217
 
218
218
  test('preserves all spec fields', () => {
219
219
  const t = trail({
220
+ blaze: (input: { name: string }, _ctx: TrailContext) =>
221
+ Result.ok({ greeting: `Hi, ${input.name}` }),
220
222
  description: 'A full trail',
221
223
  examples: [{ input: { name: 'World' }, name: 'test' }],
222
224
  id: 'full',
223
225
  input: inputSchema,
224
226
  intent: 'read',
225
227
  output: outputSchema,
226
- run: (input: { name: string }, _ctx: TrailContext) =>
227
- Result.ok({ greeting: `Hi, ${input.name}` }),
228
- services: [dbService],
228
+ provisions: [dbProvision],
229
229
  });
230
230
  expect(t.description).toBe('A full trail');
231
231
  expect(t.intent).toBe('read');
232
232
  expect(t.examples).toHaveLength(1);
233
- expect(t.services).toEqual([dbService]);
233
+ expect(t.provisions).toEqual([dbProvision]);
234
234
  });
235
235
 
236
236
  test('implementation is callable', async () => {
237
237
  const t = trail({
238
+ blaze: (input: { x: number }) => Result.ok(input.x * 2),
238
239
  id: 'callable',
239
240
  input: z.object({ x: z.number() }),
240
- run: (input: { x: number }) => Result.ok(input.x * 2),
241
241
  });
242
- const result = await t.run({ x: 5 }, stubCtx);
242
+ const result = await t.blaze({ x: 5 }, stubCtx);
243
243
  expect(result.isOk()).toBe(true);
244
244
  expect(result.unwrap()).toBe(10);
245
245
  });
246
246
 
247
247
  test('sync implementations are normalized to an awaitable runtime function', async () => {
248
248
  const t = trail('normalized', {
249
+ blaze: (input: { value: number }) => Result.ok(input.value + 1),
249
250
  input: z.object({ value: z.number() }),
250
- run: (input: { value: number }) => Result.ok(input.value + 1),
251
251
  });
252
252
 
253
- const promise = t.run({ value: 2 }, stubCtx);
253
+ const promise = t.blaze({ value: 2 }, stubCtx);
254
254
  expect(promise).toBeInstanceOf(Promise);
255
255
 
256
256
  const result = await promise;