@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
@@ -3,11 +3,11 @@ import { describe, test, expect } from 'bun:test';
3
3
 
4
4
  import { z } from 'zod';
5
5
 
6
- import { dispatch } from '../dispatch';
6
+ import { run } from '../run';
7
7
  import { InternalError, NotFoundError, ValidationError } from '../errors';
8
- import type { Layer } from '../layer';
8
+ import type { Gate } from '../gate';
9
9
  import { Result } from '../result';
10
- import { service } from '../service';
10
+ import { provision } from '../provision';
11
11
  import { topo } from '../topo';
12
12
  import { trail } from '../trail';
13
13
  import type { TrailContext, TrailContextInit } from '../types';
@@ -17,16 +17,16 @@ import type { TrailContext, TrailContextInit } from '../types';
17
17
  // ---------------------------------------------------------------------------
18
18
 
19
19
  const echoTrail = trail('echo', {
20
+ blaze: (input) => Result.ok({ value: input.value }),
20
21
  input: z.object({ value: z.string() }),
21
22
  output: z.object({ value: z.string() }),
22
- run: (input) => Result.ok({ value: input.value }),
23
23
  });
24
24
 
25
25
  const throwingTrail = trail('throws', {
26
- input: z.object({}),
27
- run: () => {
26
+ blaze: () => {
28
27
  throw new Error('kaboom');
29
28
  },
29
+ input: z.object({}),
30
30
  });
31
31
 
32
32
  const testTopo = topo('test', { echoTrail, throwingTrail });
@@ -35,34 +35,34 @@ const testTopo = topo('test', { echoTrail, throwingTrail });
35
35
  // Tests
36
36
  // ---------------------------------------------------------------------------
37
37
 
38
- describe('dispatch', () => {
38
+ describe('run', () => {
39
39
  describe('happy path', () => {
40
- test('dispatches by ID and returns Result.ok with expected value', async () => {
41
- const result = await dispatch(testTopo, 'echo', { value: 'hello' });
40
+ test('executes by ID and returns Result.ok with expected value', async () => {
41
+ const result = await run(testTopo, 'echo', { value: 'hello' });
42
42
 
43
43
  expect(result.isOk()).toBe(true);
44
44
  expect(result.unwrap()).toEqual({ value: 'hello' });
45
45
  });
46
46
 
47
- test('passes service overrides through to executeTrail', async () => {
48
- const id = `dispatch.service.${Bun.randomUUIDv7()}`;
49
- const db = service(id, {
47
+ test('passes provision overrides through to executeTrail', async () => {
48
+ const id = `run.provision.${Bun.randomUUIDv7()}`;
49
+ const db = provision(id, {
50
50
  create: () => Result.ok({ source: 'factory' }),
51
51
  });
52
52
  const searchTrail = trail('search', {
53
+ blaze: (_input, ctx) => Result.ok({ source: db.from(ctx).source }),
53
54
  input: z.object({}),
54
55
  output: z.object({ source: z.string() }),
55
- run: (_input, ctx) => Result.ok({ source: db.from(ctx).source }),
56
- services: [db],
56
+ provisions: [db],
57
57
  });
58
- const searchTopo = topo('service-test', { searchTrail });
58
+ const searchTopo = topo('provision-test', { searchTrail });
59
59
 
60
- const result = await dispatch(
60
+ const result = await run(
61
61
  searchTopo,
62
62
  'search',
63
63
  {},
64
64
  {
65
- services: { [id]: { source: 'override' } },
65
+ provisions: { [id]: { source: 'override' } },
66
66
  }
67
67
  );
68
68
 
@@ -73,7 +73,7 @@ describe('dispatch', () => {
73
73
 
74
74
  describe('not found', () => {
75
75
  test('returns NotFoundError for unknown trail ID', async () => {
76
- const result = await dispatch(testTopo, 'nonexistent', {});
76
+ const result = await run(testTopo, 'nonexistent', {});
77
77
 
78
78
  expect(result.isErr()).toBe(true);
79
79
  expect(result.error).toBeInstanceOf(NotFoundError);
@@ -84,18 +84,18 @@ describe('dispatch', () => {
84
84
 
85
85
  describe('validation', () => {
86
86
  test('returns ValidationError for invalid input', async () => {
87
- const result = await dispatch(testTopo, 'echo', { value: 42 });
87
+ const result = await run(testTopo, 'echo', { value: 42 });
88
88
 
89
89
  expect(result.isErr()).toBe(true);
90
90
  expect(result.error).toBeInstanceOf(ValidationError);
91
91
  });
92
92
  });
93
93
 
94
- describe('layers', () => {
95
- test('layer composition works through dispatch', async () => {
94
+ describe('gates', () => {
95
+ test('gate composition works through run', async () => {
96
96
  const log: string[] = [];
97
- const layer: Layer = {
98
- name: 'test-layer',
97
+ const gate: Gate = {
98
+ name: 'test-gate',
99
99
  wrap(_trail, impl) {
100
100
  return async (input, ctx) => {
101
101
  log.push('before');
@@ -106,11 +106,11 @@ describe('dispatch', () => {
106
106
  },
107
107
  };
108
108
 
109
- const result = await dispatch(
109
+ const result = await run(
110
110
  testTopo,
111
111
  'echo',
112
112
  { value: 'x' },
113
- { layers: [layer] }
113
+ { gates: [gate] }
114
114
  );
115
115
 
116
116
  expect(result.isOk()).toBe(true);
@@ -119,20 +119,20 @@ describe('dispatch', () => {
119
119
  });
120
120
 
121
121
  describe('context', () => {
122
- test('context overrides work through dispatch', async () => {
122
+ test('context overrides work through run', async () => {
123
123
  let capturedCtx: TrailContext | undefined;
124
- const ctxTrail = trail('ctx-dispatch-test', {
125
- input: z.object({}),
126
- run: (_input, ctx) => {
124
+ const ctxTrail = trail('ctx-run-test', {
125
+ blaze: (_input, ctx) => {
127
126
  capturedCtx = ctx;
128
127
  return Result.ok(null);
129
128
  },
129
+ input: z.object({}),
130
130
  });
131
131
 
132
132
  const ctxTopo = topo('ctx-test', { ctxTrail });
133
- await dispatch(
133
+ await run(
134
134
  ctxTopo,
135
- 'ctx-dispatch-test',
135
+ 'ctx-run-test',
136
136
  {},
137
137
  { ctx: { requestId: 'override-id' } }
138
138
  );
@@ -140,26 +140,26 @@ describe('dispatch', () => {
140
140
  expect(capturedCtx?.requestId).toBe('override-id');
141
141
  });
142
142
 
143
- test('createContext factory works through dispatch', async () => {
143
+ test('createContext factory works through run', async () => {
144
144
  let capturedCtx: TrailContext | undefined;
145
- const ctxTrail = trail('factory-dispatch-test', {
146
- input: z.object({}),
147
- run: (_input, ctx) => {
145
+ const ctxTrail = trail('factory-run-test', {
146
+ blaze: (_input, ctx) => {
148
147
  capturedCtx = ctx;
149
148
  return Result.ok(null);
150
149
  },
150
+ input: z.object({}),
151
151
  });
152
152
 
153
153
  const ctxTopo = topo('factory-test', { ctxTrail });
154
154
  const customCtx: TrailContextInit = {
155
+ abortSignal: new AbortController().signal,
155
156
  cwd: '/custom',
156
157
  requestId: 'factory-id',
157
- signal: new AbortController().signal,
158
158
  };
159
159
 
160
- await dispatch(
160
+ await run(
161
161
  ctxTopo,
162
- 'factory-dispatch-test',
162
+ 'factory-run-test',
163
163
  {},
164
164
  { createContext: () => customCtx }
165
165
  );
@@ -171,7 +171,7 @@ describe('dispatch', () => {
171
171
 
172
172
  describe('error handling', () => {
173
173
  test('never throws — exceptions become InternalError', async () => {
174
- const result = await dispatch(testTopo, 'throws', {});
174
+ const result = await run(testTopo, 'throws', {});
175
175
 
176
176
  expect(result.isErr()).toBe(true);
177
177
  expect(result.error).toBeInstanceOf(InternalError);
@@ -5,9 +5,9 @@ import { z } from 'zod';
5
5
 
6
6
  import { executeTrail } from '../execute.js';
7
7
  import { Result } from '../result.js';
8
- import { service } from '../service.js';
8
+ import { provision } from '../provision.js';
9
9
  import { trail } from '../trail.js';
10
- import type { ServiceContext } from '../service.js';
10
+ import type { ProvisionContext } from '../provision.js';
11
11
 
12
12
  // ---------------------------------------------------------------------------
13
13
  // Helpers
@@ -18,9 +18,9 @@ const nextId = (name: string): string =>
18
18
 
19
19
  const createSingletonConfigTrail = (id: string) => {
20
20
  const captures = { createCalls: 0 };
21
- const svc = service(id, {
21
+ const svc = provision(id, {
22
22
  config: z.object({ key: z.string() }),
23
- create: (ctx: ServiceContext<{ key: string }>) => {
23
+ create: (ctx: ProvisionContext<{ key: string }>) => {
24
24
  captures.createCalls += 1;
25
25
  return Result.ok({
26
26
  createCall: captures.createCalls,
@@ -32,14 +32,14 @@ const createSingletonConfigTrail = (id: string) => {
32
32
  return {
33
33
  captures,
34
34
  trail: trail('svc-config.singleton', {
35
- input: z.object({}),
36
- output: z.object({ createCall: z.number(), key: z.string() }),
37
- run: (_input, ctx) =>
35
+ blaze: (_input, ctx) =>
38
36
  Result.ok({
39
37
  createCall: svc.from(ctx).createCall,
40
38
  key: svc.from(ctx).key,
41
39
  }),
42
- services: [svc],
40
+ input: z.object({}),
41
+ output: z.object({ createCall: z.number(), key: z.string() }),
42
+ provisions: [svc],
43
43
  }),
44
44
  };
45
45
  };
@@ -48,24 +48,24 @@ const createSingletonConfigTrail = (id: string) => {
48
48
  // Tests
49
49
  // ---------------------------------------------------------------------------
50
50
 
51
- describe('ServiceContext.config', () => {
52
- test('service with config schema receives validated config in svc.config', async () => {
51
+ describe('ProvisionContext.config', () => {
52
+ test('provision with config schema receives validated config in svc.config', async () => {
53
53
  const id = nextId('typed-config');
54
54
  let capturedConfig: unknown;
55
55
 
56
- const db = service(id, {
56
+ const db = provision(id, {
57
57
  config: z.object({ poolSize: z.number(), url: z.string().url() }),
58
- create: (svc: ServiceContext<{ url: string; poolSize: number }>) => {
58
+ create: (svc: ProvisionContext<{ url: string; poolSize: number }>) => {
59
59
  capturedConfig = svc.config;
60
60
  return Result.ok({ connected: true });
61
61
  },
62
62
  });
63
63
 
64
64
  const dbTrail = trail('svc-config.typed', {
65
+ blaze: (_input, ctx) => Result.ok({ connected: db.from(ctx).connected }),
65
66
  input: z.object({}),
66
67
  output: z.object({ connected: z.boolean() }),
67
- run: (_input, ctx) => Result.ok({ connected: db.from(ctx).connected }),
68
- services: [db],
68
+ provisions: [db],
69
69
  });
70
70
 
71
71
  const result = await executeTrail(
@@ -82,11 +82,11 @@ describe('ServiceContext.config', () => {
82
82
  expect(capturedConfig).toEqual({ poolSize: 5, url: 'https://example.com' });
83
83
  });
84
84
 
85
- test('service without config still works — svc.config is undefined', async () => {
85
+ test('provision without config still works — svc.config is undefined', async () => {
86
86
  const id = nextId('no-config');
87
87
  let capturedConfig: unknown = 'sentinel';
88
88
 
89
- const counter = service(id, {
89
+ const counter = provision(id, {
90
90
  create: (svc) => {
91
91
  capturedConfig = svc.config;
92
92
  return Result.ok(42);
@@ -94,9 +94,9 @@ describe('ServiceContext.config', () => {
94
94
  });
95
95
 
96
96
  const counterTrail = trail('svc-config.no-config', {
97
+ blaze: (_input, ctx) => Result.ok({ value: counter.from(ctx) }),
97
98
  input: z.object({}),
98
- run: (_input, ctx) => Result.ok({ value: counter.from(ctx) }),
99
- services: [counter],
99
+ provisions: [counter],
100
100
  });
101
101
 
102
102
  const result = await executeTrail(counterTrail, {});
@@ -105,18 +105,18 @@ describe('ServiceContext.config', () => {
105
105
  expect(capturedConfig).toBeUndefined();
106
106
  });
107
107
 
108
- test('config validation failure returns Result.err at service creation time', async () => {
108
+ test('config validation failure returns Result.err at provision creation time', async () => {
109
109
  const id = nextId('invalid-config');
110
110
 
111
- const db = service(id, {
111
+ const db = provision(id, {
112
112
  config: z.object({ url: z.string().url() }),
113
113
  create: () => Result.ok({ connected: true }),
114
114
  });
115
115
 
116
116
  const dbTrail = trail('svc-config.invalid', {
117
+ blaze: () => Result.ok(null),
117
118
  input: z.object({}),
118
- run: () => Result.ok(null),
119
- services: [db],
119
+ provisions: [db],
120
120
  });
121
121
 
122
122
  const result = await executeTrail(
@@ -133,18 +133,18 @@ describe('ServiceContext.config', () => {
133
133
  expect(result.error.message).toContain(id);
134
134
  });
135
135
 
136
- test('missing configValues for a service with config schema returns Result.err', async () => {
136
+ test('missing configValues for a provision with config schema returns Result.err', async () => {
137
137
  const id = nextId('missing-config');
138
138
 
139
- const db = service(id, {
139
+ const db = provision(id, {
140
140
  config: z.object({ url: z.string().url() }),
141
141
  create: () => Result.ok({ connected: true }),
142
142
  });
143
143
 
144
144
  const dbTrail = trail('svc-config.missing', {
145
+ blaze: () => Result.ok(null),
145
146
  input: z.object({}),
146
- run: () => Result.ok(null),
147
- services: [db],
147
+ provisions: [db],
148
148
  });
149
149
 
150
150
  const result = await executeTrail(dbTrail, {});
@@ -153,23 +153,27 @@ describe('ServiceContext.config', () => {
153
153
  expect(result.error.message).toContain(id);
154
154
  });
155
155
 
156
- test('service override bypasses config validation', async () => {
156
+ test('provision override bypasses config validation', async () => {
157
157
  const id = nextId('override-bypass');
158
158
 
159
- const db = service(id, {
159
+ const db = provision(id, {
160
160
  config: z.object({ url: z.string().url() }),
161
161
  create: () => Result.ok({ connected: true }),
162
162
  });
163
163
 
164
164
  const dbTrail = trail('svc-config.override', {
165
+ blaze: (_input, ctx) => Result.ok({ value: db.from(ctx) as number }),
165
166
  input: z.object({}),
166
167
  output: z.object({ value: z.number() }),
167
- run: (_input, ctx) => Result.ok({ value: db.from(ctx) as number }),
168
- services: [db],
168
+ provisions: [db],
169
169
  });
170
170
 
171
- // Provide the service via overrides without any configValues — should NOT fail
172
- const result = await executeTrail(dbTrail, {}, { services: { [id]: 42 } });
171
+ // Provide the provision via overrides without any configValues — should NOT fail
172
+ const result = await executeTrail(
173
+ dbTrail,
174
+ {},
175
+ { provisions: { [id]: 42 } }
176
+ );
173
177
 
174
178
  expect(result.isOk()).toBe(true);
175
179
  expect(result.unwrap()).toEqual({ value: 42 });
@@ -178,19 +182,19 @@ describe('ServiceContext.config', () => {
178
182
  const id = nextId('options-config');
179
183
  const captures: unknown[] = [];
180
184
 
181
- const svc = service(id, {
185
+ const svc = provision(id, {
182
186
  config: z.object({ key: z.string() }),
183
- create: (ctx: ServiceContext<{ key: string }>) => {
187
+ create: (ctx: ProvisionContext<{ key: string }>) => {
184
188
  captures.push(ctx.config);
185
189
  return Result.ok({ key: ctx.config.key });
186
190
  },
187
191
  });
188
192
 
189
193
  const svcTrail = trail('svc-config.options', {
194
+ blaze: (_input, ctx) => Result.ok({ key: svc.from(ctx).key }),
190
195
  input: z.object({}),
191
196
  output: z.object({ key: z.string() }),
192
- run: (_input, ctx) => Result.ok({ key: svc.from(ctx).key }),
193
- services: [svc],
197
+ provisions: [svc],
194
198
  });
195
199
 
196
200
  const result = await executeTrail(
@@ -206,7 +210,7 @@ describe('ServiceContext.config', () => {
206
210
  expect(captures).toEqual([{ key: 'hello' }]);
207
211
  });
208
212
 
209
- test('config-aware singleton services reuse the cached instance', async () => {
213
+ test('config-aware singleton provisions reuse the cached instance', async () => {
210
214
  const id = nextId('singleton-config');
211
215
  const { captures, trail: singletonTrail } = createSingletonConfigTrail(id);
212
216
  const options = {
@@ -4,18 +4,18 @@ import { z } from 'zod';
4
4
  import {
5
5
  Result,
6
6
  createTrailContext,
7
- findDuplicateServiceId,
8
- isService,
9
- service as defineService,
7
+ findDuplicateProvisionId,
8
+ isProvision,
9
+ provision as defineProvision,
10
10
  } from '../index.js';
11
11
  import type {
12
- Service,
13
- ServiceContext,
14
- ServiceSpec,
12
+ Provision,
13
+ ProvisionContext,
14
+ ProvisionSpec,
15
15
  TrailContext,
16
16
  } from '../index.js';
17
17
 
18
- const serviceCtx: ServiceContext = {
18
+ const provisionCtx: ProvisionContext = {
19
19
  cwd: '/tmp/trails',
20
20
  env: { DATABASE_URL: 'file::memory:' },
21
21
  workspaceRoot: '/tmp',
@@ -23,39 +23,39 @@ const serviceCtx: ServiceContext = {
23
23
 
24
24
  let disposedValue: number | undefined;
25
25
 
26
- const counterServiceSpec: ServiceSpec<number> = {
26
+ const counterProvisionSpec: ProvisionSpec<number> = {
27
27
  create: () => Result.ok(3),
28
- description: 'Counter service',
29
- dispose: (service) => {
30
- disposedValue = service;
28
+ description: 'Counter provision',
29
+ dispose: (provision) => {
30
+ disposedValue = provision;
31
31
  },
32
- health: (service) => Result.ok({ healthy: service > 0 }),
33
- metadata: { domain: 'data' },
32
+ health: (provision) => Result.ok({ healthy: provision > 0 }),
33
+ meta: { domain: 'data' },
34
34
  mock: () => 1,
35
35
  };
36
36
 
37
37
  const resolvedServiceCtx = (id: string, instance: unknown): TrailContext =>
38
38
  createTrailContext({
39
+ abortSignal: new AbortController().signal,
39
40
  extensions: { [id]: instance },
40
41
  requestId: `${id}-request`,
41
- signal: new AbortController().signal,
42
42
  });
43
43
 
44
- describe('service types', () => {
45
- test('ServiceContext exposes the stable process-scoped fields', () => {
46
- expect(serviceCtx.cwd).toBe('/tmp/trails');
47
- expect(serviceCtx.env?.DATABASE_URL).toBe('file::memory:');
48
- expect(serviceCtx.workspaceRoot).toBe('/tmp');
44
+ describe('provision types', () => {
45
+ test('ProvisionContext exposes the stable process-scoped fields', () => {
46
+ expect(provisionCtx.cwd).toBe('/tmp/trails');
47
+ expect(provisionCtx.env?.DATABASE_URL).toBe('file::memory:');
48
+ expect(provisionCtx.workspaceRoot).toBe('/tmp');
49
49
  });
50
50
 
51
- test('ServiceSpec stores description and metadata', () => {
52
- expect(counterServiceSpec.description).toBe('Counter service');
53
- expect(counterServiceSpec.metadata).toEqual({ domain: 'data' });
51
+ test('ProvisionSpec stores description and meta', () => {
52
+ expect(counterProvisionSpec.description).toBe('Counter provision');
53
+ expect(counterProvisionSpec.meta).toEqual({ domain: 'data' });
54
54
  });
55
55
 
56
- test('ServiceSpec can reserve a config schema for future composition', () => {
56
+ test('ProvisionSpec can reserve a config schema for future composition', () => {
57
57
  const config = z.object({ url: z.string().url() });
58
- const spec: ServiceSpec<number> = {
58
+ const spec: ProvisionSpec<number> = {
59
59
  config,
60
60
  create: () => Result.ok(1),
61
61
  };
@@ -63,72 +63,72 @@ describe('service types', () => {
63
63
  expect(spec.config).toBe(config);
64
64
  });
65
65
 
66
- test('ServiceSpec create and health callbacks are callable', async () => {
67
- const result = await counterServiceSpec.create(serviceCtx);
66
+ test('ProvisionSpec create and health callbacks are callable', async () => {
67
+ const result = await counterProvisionSpec.create(provisionCtx);
68
68
  expect(result.isOk()).toBe(true);
69
69
  expect(result.unwrap()).toBe(3);
70
70
 
71
- const health = await counterServiceSpec.health?.(result.unwrap());
71
+ const health = await counterProvisionSpec.health?.(result.unwrap());
72
72
  expect(health?.isOk()).toBe(true);
73
73
  expect(health?.unwrap()).toEqual({ healthy: true });
74
74
  });
75
75
 
76
- test('ServiceSpec mock and dispose callbacks are callable', async () => {
76
+ test('ProvisionSpec mock and dispose callbacks are callable', async () => {
77
77
  disposedValue = undefined;
78
- const result = await counterServiceSpec.create(serviceCtx);
78
+ const result = await counterProvisionSpec.create(provisionCtx);
79
79
  expect(result.isOk()).toBe(true);
80
80
 
81
- const service = result.unwrap();
82
- expect(await counterServiceSpec.mock?.()).toBe(1);
81
+ const provision = result.unwrap();
82
+ expect(await counterProvisionSpec.mock?.()).toBe(1);
83
83
 
84
- await counterServiceSpec.dispose?.(service);
84
+ await counterProvisionSpec.dispose?.(provision);
85
85
  expect(disposedValue).toBe(3);
86
86
  });
87
87
 
88
- test('ServiceSpec create can be async', async () => {
89
- const spec: ServiceSpec<number> = {
88
+ test('ProvisionSpec create can be async', async () => {
89
+ const spec: ProvisionSpec<number> = {
90
90
  create: async () => {
91
91
  await Bun.sleep(0);
92
92
  return Result.ok(7);
93
93
  },
94
94
  };
95
95
 
96
- const result = await spec.create(serviceCtx);
96
+ const result = await spec.create(provisionCtx);
97
97
  expect(result.isOk()).toBe(true);
98
98
  expect(result.unwrap()).toBe(7);
99
99
  });
100
100
 
101
- test('Service carries identity alongside the shared spec fields', async () => {
102
- const service: Service<number> = {
101
+ test('Provision carries identity alongside the shared spec fields', async () => {
102
+ const counterProvision: Provision<number> = {
103
103
  from(ctx) {
104
- return ctx.service(this);
104
+ return ctx.provision(this);
105
105
  },
106
106
  id: 'counter.main',
107
- kind: 'service',
108
- ...counterServiceSpec,
107
+ kind: 'provision',
108
+ ...counterProvisionSpec,
109
109
  };
110
110
 
111
- expect(service.kind).toBe('service');
112
- expect(service.id).toBe('counter.main');
111
+ expect(counterProvision.kind).toBe('provision');
112
+ expect(counterProvision.id).toBe('counter.main');
113
113
 
114
- const created = await service.create(serviceCtx);
114
+ const created = await counterProvision.create(provisionCtx);
115
115
  expect(created.isOk()).toBe(true);
116
116
  expect(created.unwrap()).toBe(3);
117
- expect(await service.mock?.()).toBe(1);
117
+ expect(await counterProvision.mock?.()).toBe(1);
118
118
  });
119
119
  });
120
120
 
121
- describe('service()', () => {
122
- test('returns a frozen service object with kind and id', () => {
123
- const counter = defineService('counter.main', counterServiceSpec);
121
+ describe('provision()', () => {
122
+ test('returns a frozen provision object with kind and id', () => {
123
+ const counter = defineProvision('counter.main', counterProvisionSpec);
124
124
 
125
- expect(counter.kind).toBe('service');
125
+ expect(counter.kind).toBe('provision');
126
126
  expect(counter.id).toBe('counter.main');
127
127
  expect(Object.isFrozen(counter)).toBe(true);
128
128
  });
129
129
 
130
- test('infers the service type through from(ctx)', () => {
131
- const db = defineService('db.main', {
130
+ test('infers the provision type through from(ctx)', () => {
131
+ const db = defineProvision('db.main', {
132
132
  create: () =>
133
133
  Result.ok({
134
134
  query(sql: string) {
@@ -149,48 +149,48 @@ describe('service()', () => {
149
149
  expect(resolved.query('select 1')).toBe(8);
150
150
  });
151
151
 
152
- test('from(ctx) throws when the service is missing', () => {
153
- const counter = defineService('counter.main', counterServiceSpec);
152
+ test('from(ctx) throws when the provision is missing', () => {
153
+ const counter = defineProvision('counter.main', counterProvisionSpec);
154
154
  const ctx = createTrailContext({
155
- requestId: 'missing-service',
156
- signal: new AbortController().signal,
155
+ abortSignal: new AbortController().signal,
156
+ requestId: 'missing-provision',
157
157
  });
158
158
 
159
159
  expect(() => counter.from(ctx)).toThrow(
160
- 'Service "counter.main" not found in trail context'
160
+ 'Provision "counter.main" not found in trail context'
161
161
  );
162
162
  });
163
163
 
164
- test('from(ctx) returns undefined when the service key exists with an undefined value', () => {
165
- const optional = defineService<undefined>('optional.main', {
164
+ test('from(ctx) returns undefined when the provision key exists with an undefined value', () => {
165
+ const optional = defineProvision<undefined>('optional.main', {
166
166
  create: () => Result.ok<undefined>(),
167
167
  });
168
168
  const ctx = createTrailContext({
169
+ abortSignal: new AbortController().signal,
169
170
  extensions: { [optional.id]: undefined },
170
- requestId: 'undefined-service',
171
- signal: new AbortController().signal,
171
+ requestId: 'undefined-provision',
172
172
  });
173
173
 
174
174
  expect(optional.from(ctx)).toBeUndefined();
175
175
  });
176
176
  });
177
177
 
178
- describe('service helpers', () => {
179
- test('isService identifies service definitions', () => {
180
- const counter = defineService('counter.main', counterServiceSpec);
178
+ describe('provision helpers', () => {
179
+ test('isProvision identifies provision definitions', () => {
180
+ const counter = defineProvision('counter.main', counterProvisionSpec);
181
181
 
182
- expect(isService(counter)).toBe(true);
183
- expect(isService({ id: 'counter.main', kind: 'trail' })).toBe(false);
184
- expect(isService(null)).toBe(false);
182
+ expect(isProvision(counter)).toBe(true);
183
+ expect(isProvision({ id: 'counter.main', kind: 'trail' })).toBe(false);
184
+ expect(isProvision(null)).toBe(false);
185
185
  });
186
186
 
187
- test('findDuplicateServiceId returns the first repeated ID', () => {
188
- const first = defineService('counter.main', counterServiceSpec);
189
- const duplicate = defineService('counter.main', counterServiceSpec);
190
- const other = defineService('counter.secondary', counterServiceSpec);
187
+ test('findDuplicateProvisionId returns the first repeated ID', () => {
188
+ const first = defineProvision('counter.main', counterProvisionSpec);
189
+ const duplicate = defineProvision('counter.main', counterProvisionSpec);
190
+ const other = defineProvision('counter.secondary', counterProvisionSpec);
191
191
 
192
- expect(findDuplicateServiceId([first, other])).toBeUndefined();
193
- expect(findDuplicateServiceId([first, other, duplicate])).toBe(
192
+ expect(findDuplicateProvisionId([first, other])).toBeUndefined();
193
+ expect(findDuplicateProvisionId([first, other, duplicate])).toBe(
194
194
  'counter.main'
195
195
  );
196
196
  });