@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
@@ -8,14 +8,14 @@ import type { TrailInput, TrailOutput, TrailResult } from '../type-utils';
8
8
  import { inputOf, outputOf } from '../type-utils';
9
9
 
10
10
  const greetTrail = trail('greet', {
11
+ blaze: (input) => Result.ok({ message: `Hello, ${input.name}!` }),
11
12
  input: z.object({ name: z.string() }),
12
13
  output: z.object({ message: z.string() }),
13
- run: (input) => Result.ok({ message: `Hello, ${input.name}!` }),
14
14
  });
15
15
 
16
16
  const noOutputTrail = trail('ping', {
17
+ blaze: () => Result.ok(),
17
18
  input: z.object({}),
18
- run: () => Result.ok(),
19
19
  });
20
20
 
21
21
  describe('type-utils', () => {
@@ -71,9 +71,9 @@ describe('type-utils', () => {
71
71
  describe('TrailResult', () => {
72
72
  test('extracts Result<Output, Error> from a trail', () => {
73
73
  const t = trail('test.result', {
74
+ blaze: (input) => Result.ok({ answer: input.q }),
74
75
  input: z.object({ q: z.string() }),
75
76
  output: z.object({ answer: z.string() }),
76
- run: (input) => Result.ok({ answer: input.q }),
77
77
  });
78
78
 
79
79
  type Expected = Result<{ answer: string }, Error>;
@@ -2,8 +2,8 @@ import { describe, expect, test } from 'bun:test';
2
2
 
3
3
  import { z } from 'zod';
4
4
 
5
+ import { provision } from '../provision.js';
5
6
  import { Result } from '../result.js';
6
- import { service } from '../service.js';
7
7
  import { topo } from '../topo.js';
8
8
  import type { TopoIssue } from '../validate-topo.js';
9
9
  import { validateTopo } from '../validate-topo.js';
@@ -18,7 +18,7 @@ const noop = async () => Result.ok();
18
18
  const mockTrail = (
19
19
  id: string,
20
20
  overrides?: {
21
- follow?: readonly string[];
21
+ crosses?: readonly string[];
22
22
  examples?: readonly {
23
23
  name: string;
24
24
  input: unknown;
@@ -26,27 +26,27 @@ const mockTrail = (
26
26
  error?: string;
27
27
  }[];
28
28
  output?: z.ZodType;
29
- services?: readonly ReturnType<typeof service>[];
29
+ provisions?: readonly ReturnType<typeof provision>[];
30
30
  }
31
31
  ) => ({
32
- follow: Object.freeze([...(overrides?.follow ?? [])]),
32
+ blaze: noop,
33
+ crosses: Object.freeze([...(overrides?.crosses ?? [])]),
33
34
  id,
34
35
  input: z.object({ name: z.string() }),
35
36
  kind: 'trail' as const,
36
- run: noop,
37
- services: Object.freeze([...(overrides?.services ?? [])]),
37
+ provisions: Object.freeze([...(overrides?.provisions ?? [])]),
38
38
  ...overrides,
39
39
  });
40
40
 
41
- const mockService = (id: string) =>
42
- service(id, {
41
+ const mockProvision = (id: string) =>
42
+ provision(id, {
43
43
  create: () => Result.ok({ id }),
44
44
  });
45
45
 
46
46
  const mockEvent = (id: string, from?: readonly string[]) => ({
47
47
  from,
48
48
  id,
49
- kind: 'event' as const,
49
+ kind: 'signal' as const,
50
50
  payload: z.object({ data: z.string() }),
51
51
  });
52
52
 
@@ -68,7 +68,7 @@ describe('validateTopo', () => {
68
68
  const app = topo('app', {
69
69
  add: mockTrail('entity.add'),
70
70
  onboard: mockTrail('entity.onboard', {
71
- follow: ['entity.add'],
71
+ crosses: ['entity.add'],
72
72
  }),
73
73
  updated: mockEvent('entity.updated', ['entity.add']),
74
74
  });
@@ -77,11 +77,11 @@ describe('validateTopo', () => {
77
77
  expect(result.isOk()).toBe(true);
78
78
  });
79
79
 
80
- describe('trail follow', () => {
81
- test('trail following non-existent trail fails', () => {
80
+ describe('trail crossing', () => {
81
+ test('trail crossing a non-existent trail fails', () => {
82
82
  const app = topo('app', {
83
83
  onboard: mockTrail('entity.onboard', {
84
- follow: ['entity.missing'],
84
+ crosses: ['entity.missing'],
85
85
  }),
86
86
  });
87
87
 
@@ -90,57 +90,57 @@ describe('validateTopo', () => {
90
90
 
91
91
  const issues = extractIssues(result);
92
92
  expect(issues).toHaveLength(1);
93
- expect(issues[0]?.rule).toBe('follow-exists');
93
+ expect(issues[0]?.rule).toBe('cross-exists');
94
94
  expect(issues[0]?.message).toContain('entity.missing');
95
95
  });
96
96
 
97
- test('trail following itself fails', () => {
97
+ test('trail crossing itself fails', () => {
98
98
  const app = topo('app', {
99
- loop: mockTrail('entity.loop', { follow: ['entity.loop'] }),
99
+ loop: mockTrail('entity.loop', { crosses: ['entity.loop'] }),
100
100
  });
101
101
 
102
102
  const result = validateTopo(app);
103
103
  expect(result.isErr()).toBe(true);
104
104
 
105
105
  const issues = extractIssues(result);
106
- expect(issues.some((i) => i.rule === 'no-self-follow')).toBe(true);
106
+ expect(issues.some((i) => i.rule === 'no-self-cross')).toBe(true);
107
107
  });
108
108
 
109
109
  test('two-node cycle (a→b→a) is detected', () => {
110
110
  const app = topo('app', {
111
- a: mockTrail('a', { follow: ['b'] }),
112
- b: mockTrail('b', { follow: ['a'] }),
111
+ a: mockTrail('a', { crosses: ['b'] }),
112
+ b: mockTrail('b', { crosses: ['a'] }),
113
113
  });
114
114
 
115
115
  const result = validateTopo(app);
116
116
  expect(result.isErr()).toBe(true);
117
117
 
118
118
  const issues = extractIssues(result);
119
- const cycleIssues = issues.filter((i) => i.rule === 'follow-cycle');
119
+ const cycleIssues = issues.filter((i) => i.rule === 'cross-cycle');
120
120
  expect(cycleIssues.length).toBeGreaterThanOrEqual(1);
121
121
  expect(cycleIssues[0]?.message).toContain('Cycle detected');
122
122
  });
123
123
 
124
124
  test('three-node cycle (a→b→c→a) is detected', () => {
125
125
  const app = topo('app', {
126
- a: mockTrail('a', { follow: ['b'] }),
127
- b: mockTrail('b', { follow: ['c'] }),
128
- c: mockTrail('c', { follow: ['a'] }),
126
+ a: mockTrail('a', { crosses: ['b'] }),
127
+ b: mockTrail('b', { crosses: ['c'] }),
128
+ c: mockTrail('c', { crosses: ['a'] }),
129
129
  });
130
130
 
131
131
  const result = validateTopo(app);
132
132
  expect(result.isErr()).toBe(true);
133
133
 
134
134
  const issues = extractIssues(result);
135
- const cycleIssues = issues.filter((i) => i.rule === 'follow-cycle');
135
+ const cycleIssues = issues.filter((i) => i.rule === 'cross-cycle');
136
136
  expect(cycleIssues.length).toBeGreaterThanOrEqual(1);
137
137
  expect(cycleIssues[0]?.message).toContain('Cycle detected');
138
138
  });
139
139
 
140
140
  test('valid DAG with shared targets is not flagged', () => {
141
141
  const app = topo('app', {
142
- a: mockTrail('a', { follow: ['c'] }),
143
- b: mockTrail('b', { follow: ['c'] }),
142
+ a: mockTrail('a', { crosses: ['c'] }),
143
+ b: mockTrail('b', { crosses: ['c'] }),
144
144
  c: mockTrail('c'),
145
145
  });
146
146
 
@@ -149,13 +149,13 @@ describe('validateTopo', () => {
149
149
  });
150
150
  });
151
151
 
152
- describe('service declarations', () => {
153
- test('trail declaring a registered service passes', () => {
154
- const db = mockService('db.main');
152
+ describe('provision declarations', () => {
153
+ test('trail declaring a registered provision passes', () => {
154
+ const db = mockProvision('db.main');
155
155
  const app = topo('app', {
156
156
  db,
157
157
  show: mockTrail('entity.show', {
158
- services: [db],
158
+ provisions: [db],
159
159
  }),
160
160
  });
161
161
 
@@ -163,11 +163,11 @@ describe('validateTopo', () => {
163
163
  expect(result.isOk()).toBe(true);
164
164
  });
165
165
 
166
- test('trail declaring a missing service fails', () => {
167
- const db = mockService('db.main');
166
+ test('trail declaring a missing provision fails', () => {
167
+ const db = mockProvision('db.main');
168
168
  const app = topo('app', {
169
169
  show: mockTrail('entity.show', {
170
- services: [db],
170
+ provisions: [db],
171
171
  }),
172
172
  });
173
173
 
@@ -176,7 +176,7 @@ describe('validateTopo', () => {
176
176
 
177
177
  const issues = extractIssues(result);
178
178
  expect(issues).toHaveLength(1);
179
- expect(issues[0]?.rule).toBe('service-exists');
179
+ expect(issues[0]?.rule).toBe('provision-exists');
180
180
  expect(issues[0]?.message).toContain('db.main');
181
181
  });
182
182
  });
@@ -286,7 +286,7 @@ describe('validateTopo', () => {
286
286
 
287
287
  const issues = extractIssues(result);
288
288
  expect(issues).toHaveLength(1);
289
- expect(issues[0]?.rule).toBe('event-origin-exists');
289
+ expect(issues[0]?.rule).toBe('signal-origin-exists');
290
290
  expect(issues[0]?.message).toContain('entity.ghost');
291
291
  });
292
292
 
@@ -301,11 +301,11 @@ describe('validateTopo', () => {
301
301
  });
302
302
 
303
303
  test('collects multiple issues', () => {
304
- const db = mockService('db.main');
304
+ const db = mockProvision('db.main');
305
305
  const app = topo('app', {
306
- broken: mockTrail('entity.broken', { follow: ['entity.missing'] }),
306
+ broken: mockTrail('entity.broken', { crosses: ['entity.missing'] }),
307
307
  missingService: mockTrail('entity.missing-service', {
308
- services: [db],
308
+ provisions: [db],
309
309
  }),
310
310
  show: mockTrail('entity.show', {
311
311
  examples: [{ input: { name: 123 }, name: 'Bad' }],
package/src/context.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { createServiceLookup } from './service.js';
1
+ import { createProvisionLookup } from './provision.js';
2
2
  import type { TrailContext, TrailContextInit } from './types.js';
3
3
 
4
4
  type MutableTrailContext = {
@@ -9,19 +9,20 @@ type MutableTrailContext = {
9
9
  * Create a TrailContext with sensible defaults.
10
10
  *
11
11
  * - `requestId` defaults to `Bun.randomUUIDv7()` (sortable v7 UUID)
12
- * - `signal` defaults to a fresh, non-aborted `AbortSignal`
12
+ * - `abortSignal` defaults to a fresh, non-aborted `AbortSignal`
13
13
  * - All other fields come from `overrides`
14
14
  */
15
15
  export const createTrailContext = (
16
16
  overrides?: Partial<TrailContextInit>
17
17
  ): TrailContext => {
18
18
  const ctx = {
19
+ abortSignal: new AbortController().signal,
19
20
  cwd: process.cwd(),
20
21
  env: process.env as Record<string, string | undefined>,
21
22
  requestId: Bun.randomUUIDv7(),
22
- signal: new AbortController().signal,
23
23
  ...overrides,
24
24
  } as MutableTrailContext;
25
- ctx.service = overrides?.service ?? createServiceLookup(() => ctx);
25
+ const lookup = overrides?.provision ?? createProvisionLookup(() => ctx);
26
+ ctx.provision = lookup;
26
27
  return ctx;
27
28
  };
package/src/derive.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Schema-driven field derivation for @ontrails/core
3
3
  *
4
- * Introspects Zod v4 schemas to produce a surface-agnostic Field[] descriptor
5
- * that UI layers (CLI prompts, web forms, etc.) can consume.
4
+ * Introspects Zod v4 schemas to produce a runtime-agnostic Field[] descriptor
5
+ * that UI consumers (CLI prompts, web forms, etc.) can consume.
6
6
  */
7
7
 
8
8
  import type { z } from 'zod';
@@ -11,7 +11,7 @@ import type { z } from 'zod';
11
11
  // Public types
12
12
  // ---------------------------------------------------------------------------
13
13
 
14
- /** A surface-agnostic field descriptor derived from a Zod schema. */
14
+ /** A runtime-agnostic field descriptor derived from a Zod schema. */
15
15
  export interface Field {
16
16
  readonly name: string;
17
17
  readonly type:
@@ -193,11 +193,6 @@ const buildOptions = (
193
193
  // Public API
194
194
  // ---------------------------------------------------------------------------
195
195
 
196
- /**
197
- * Derive a surface-agnostic Field[] from a Zod object schema.
198
- *
199
- * Uses Zod v4's `_zod.def` for introspection. Returns fields sorted by name.
200
- */
201
196
  /** Derive a single field from a shape entry. */
202
197
  const deriveField = (
203
198
  key: string,
@@ -212,6 +207,11 @@ const deriveField = (
212
207
  return { default: defaultValue, label, name: key, options, required, type };
213
208
  };
214
209
 
210
+ /**
211
+ * Derive a runtime-agnostic Field[] from a Zod object schema.
212
+ *
213
+ * Uses Zod v4's `_zod.def` for introspection. Returns fields sorted by name.
214
+ */
215
215
  export const deriveFields = (
216
216
  schema: z.ZodType,
217
217
  overrides?: Record<string, FieldOverride>
package/src/event.ts CHANGED
@@ -1,77 +1,15 @@
1
1
  /**
2
- * Event a named payload schema with optional provenance metadata.
3
- */
4
-
5
- import type { z } from 'zod';
6
-
7
- // ---------------------------------------------------------------------------
8
- // Spec (input to the factory)
9
- // ---------------------------------------------------------------------------
10
-
11
- export interface EventSpec<T> {
12
- readonly payload: z.ZodType<T>;
13
- readonly description?: string | undefined;
14
- readonly metadata?: Readonly<Record<string, unknown>> | undefined;
15
- /** Trail IDs that produce this event (e.g. the trails it originates from). */
16
- readonly from?: readonly string[] | undefined;
17
- }
18
-
19
- // ---------------------------------------------------------------------------
20
- // Shape (output of the factory)
21
- // ---------------------------------------------------------------------------
22
-
23
- export interface Event<T> {
24
- readonly id: string;
25
- readonly kind: 'event';
26
- readonly payload: z.ZodType<T>;
27
- readonly description?: string | undefined;
28
- readonly metadata?: Readonly<Record<string, unknown>> | undefined;
29
- /** Trail IDs that produce this event (e.g. the trails it originates from). */
30
- readonly from?: readonly string[] | undefined;
31
- }
32
-
33
- // ---------------------------------------------------------------------------
34
- // Factory
35
- // ---------------------------------------------------------------------------
36
-
37
- /**
38
- * Create an event definition.
39
- *
40
- * An event is a named payload schema describing something that happened.
41
- * Returns a frozen object with `kind: "event"` and all spec fields.
42
- *
43
- * @example
44
- * ```typescript
45
- * // ID as first argument
46
- * const updated = event("entity.updated", {
47
- * payload: EntityUpdatedSchema,
48
- * from: ["entity.add", "entity.update"],
49
- * });
2
+ * Legacy event aliases for the signal model.
50
3
  *
51
- * // Full spec object (programmatic)
52
- * const updated = event({ id: "entity.updated", payload: ..., from: [...] });
53
- * ```
4
+ * Keep this file as a compatibility seam while the repo-wide cutover lands.
54
5
  */
55
- export function event<T>(id: string, spec: EventSpec<T>): Event<T>;
56
- export function event<T>(
57
- spec: EventSpec<T> & { readonly id: string }
58
- ): Event<T>;
59
- export function event<T>(
60
- idOrSpec: string | (EventSpec<T> & { readonly id: string }),
61
- maybeSpec?: EventSpec<T>
62
- ): Event<T> {
63
- const resolvedId = typeof idOrSpec === 'string' ? idOrSpec : idOrSpec.id;
64
- // oxlint-disable-next-line no-non-null-assertion -- overload guarantees maybeSpec when idOrSpec is string
65
- const resolvedSpec = typeof idOrSpec === 'string' ? maybeSpec! : idOrSpec;
66
- return Object.freeze({
67
- description: resolvedSpec.description,
68
- from: resolvedSpec.from ? Object.freeze([...resolvedSpec.from]) : undefined,
69
- id: resolvedId,
70
- kind: 'event' as const,
71
- metadata: resolvedSpec.metadata,
72
- payload: resolvedSpec.payload,
73
- });
74
- }
75
6
 
76
- /** Existential type for heterogeneous event collections */
77
- export type AnyEvent = Event<unknown>;
7
+ export { signal as event } from './signal.js';
8
+ export type {
9
+ AnySignal,
10
+ AnySignal as AnyEvent,
11
+ Signal,
12
+ Signal as Event,
13
+ SignalSpec,
14
+ SignalSpec as EventSpec,
15
+ } from './signal.js';
package/src/execute.ts CHANGED
@@ -1,22 +1,22 @@
1
1
  /**
2
2
  * Centralized trail execution pipeline.
3
3
  *
4
- * Validates input, builds context, composes layers, and runs the
4
+ * Validates input, builds context, composes gates, and runs the
5
5
  * implementation. Surfaces (CLI, MCP, HTTP) delegate here instead
6
6
  * of reimplementing the pipeline.
7
7
  */
8
8
 
9
9
  import type { AnyTrail } from './trail.js';
10
- import type { Layer } from './layer.js';
11
- import type { ServiceOverrideMap } from './service.js';
10
+ import type { Gate } from './gate.js';
11
+ import type { ProvisionOverrideMap } from './provision.js';
12
12
  import type { TrailContext, TrailContextInit } from './types.js';
13
13
 
14
- import { composeLayers } from './layer.js';
14
+ import { composeGates } from './gate.js';
15
15
  import { createTrailContext } from './context.js';
16
16
  import { InternalError } from './errors.js';
17
17
  import { Result } from './result.js';
18
- import { createServiceLookup } from './service.js';
19
- import { resolveServices } from './service-config.js';
18
+ import { createProvisionLookup } from './provision.js';
19
+ import { resolveProvisions } from './provision-config.js';
20
20
  import { validateInput } from './validation.js';
21
21
 
22
22
  type MutableTrailContext = {
@@ -32,16 +32,16 @@ export interface ExecuteTrailOptions {
32
32
  /** Partial context overrides merged on top of the base context. */
33
33
  readonly ctx?: Partial<TrailContextInit> | undefined;
34
34
  /** AbortSignal override (takes final precedence over ctx and factory). */
35
- readonly signal?: AbortSignal | undefined;
36
- /** Layers to compose around the implementation. */
37
- readonly layers?: readonly Layer[] | undefined;
35
+ readonly abortSignal?: AbortSignal | undefined;
36
+ /** Gates to compose around the implementation. */
37
+ readonly gates?: readonly Gate[] | undefined;
38
38
  /** Factory that produces a base TrailContext (takes precedence over defaults). */
39
39
  readonly createContext?:
40
40
  | (() => TrailContextInit | Promise<TrailContextInit>)
41
41
  | undefined;
42
- /** Explicit service instance overrides keyed by service ID. */
43
- readonly services?: ServiceOverrideMap | undefined;
44
- /** Config values for services that declare a `config` schema, keyed by service ID. */
42
+ /** Explicit provision instance overrides keyed by provision ID. */
43
+ readonly provisions?: ProvisionOverrideMap | undefined;
44
+ /** Config values for provisions that declare a `config` schema, keyed by provision ID. */
45
45
  readonly configValues?:
46
46
  | Readonly<Record<string, Record<string, unknown>>>
47
47
  | undefined;
@@ -51,21 +51,10 @@ export interface ExecuteTrailOptions {
51
51
  // Context resolution
52
52
  // ---------------------------------------------------------------------------
53
53
 
54
- /**
55
- * Build a TrailContext from options.
56
- *
57
- * Resolution order:
58
- * 1. Factory (`createContext`) or `createTrailContext()` defaults.
59
- * 2. Partial `ctx` overrides merged on top.
60
- * 3. `signal` override takes final precedence.
61
- */
62
- const resolveContext = async (
54
+ const applyContextOverrides = (
55
+ base: TrailContextInit,
63
56
  options?: ExecuteTrailOptions
64
- ): Promise<TrailContext> => {
65
- const seed = options?.createContext
66
- ? await options.createContext()
67
- : createTrailContext();
68
- const base = seed.service ? seed : createTrailContext(seed);
57
+ ): TrailContextInit => {
69
58
  const withOverrides = options?.ctx
70
59
  ? {
71
60
  ...base,
@@ -73,19 +62,46 @@ const resolveContext = async (
73
62
  extensions: { ...base.extensions, ...options.ctx.extensions },
74
63
  }
75
64
  : base;
76
- const resolved = options?.signal
77
- ? { ...withOverrides, signal: options.signal }
65
+
66
+ return options?.abortSignal
67
+ ? { ...withOverrides, abortSignal: options.abortSignal }
78
68
  : withOverrides;
69
+ };
70
+
71
+ const bindProvisionLookup = (
72
+ resolved: TrailContextInit,
73
+ options?: ExecuteTrailOptions
74
+ ): TrailContext => {
79
75
  if (
80
- options?.ctx?.extensions !== undefined ||
81
- resolved.service === undefined
76
+ options?.ctx?.extensions === undefined &&
77
+ resolved.provision !== undefined
82
78
  ) {
83
- const bound = { ...resolved } as MutableTrailContext;
84
- bound.service = createServiceLookup(() => bound);
85
- return bound;
79
+ return resolved as TrailContext;
86
80
  }
87
81
 
88
- return resolved as TrailContext;
82
+ const bound = { ...resolved } as MutableTrailContext;
83
+ const lookup = createProvisionLookup(() => bound);
84
+ bound.provision = lookup;
85
+ return bound;
86
+ };
87
+
88
+ /**
89
+ * Build a TrailContext from options.
90
+ *
91
+ * Resolution order:
92
+ * 1. Factory (`createContext`) or `createTrailContext()` defaults.
93
+ * 2. Partial `ctx` overrides merged on top.
94
+ * 3. `abortSignal` override takes final precedence.
95
+ */
96
+ const resolveContext = async (
97
+ options?: ExecuteTrailOptions
98
+ ): Promise<TrailContext> => {
99
+ const seed = options?.createContext
100
+ ? await options.createContext()
101
+ : createTrailContext();
102
+ const base = seed.provision ? seed : createTrailContext(seed);
103
+ const resolved = applyContextOverrides(base, options);
104
+ return bindProvisionLookup(resolved, options);
89
105
  };
90
106
 
91
107
  const prepareContext = async (
@@ -93,10 +109,10 @@ const prepareContext = async (
93
109
  options?: ExecuteTrailOptions
94
110
  ): Promise<Result<TrailContext, Error>> => {
95
111
  const baseCtx = await resolveContext(options);
96
- return await resolveServices(
112
+ return await resolveProvisions(
97
113
  trail,
98
114
  baseCtx,
99
- options?.services,
115
+ options?.provisions,
100
116
  options?.configValues
101
117
  );
102
118
  };
@@ -105,9 +121,9 @@ const runTrail = async (
105
121
  trail: AnyTrail,
106
122
  input: unknown,
107
123
  ctx: TrailContext,
108
- layers: readonly Layer[]
124
+ gates: readonly Gate[]
109
125
  ): Promise<Result<unknown, Error>> => {
110
- const impl = composeLayers([...layers], trail, trail.run);
126
+ const impl = composeGates([...gates], trail, trail.blaze);
111
127
  return await impl(input, ctx);
112
128
  };
113
129
 
@@ -116,7 +132,7 @@ const runTrail = async (
116
132
  // ---------------------------------------------------------------------------
117
133
 
118
134
  /**
119
- * Execute a trail through the standard validate-context-layers-run pipeline.
135
+ * Execute a trail through the standard validate-context-gates-run pipeline.
120
136
  *
121
137
  * The function never throws -- unexpected exceptions are caught and
122
138
  * returned as `Result.err(InternalError)`.
@@ -141,7 +157,7 @@ export const executeTrail = async (
141
157
  trail,
142
158
  validated.value,
143
159
  resolvedCtx.value,
144
- options?.layers ?? []
160
+ options?.gates ?? []
145
161
  );
146
162
  } catch (error: unknown) {
147
163
  const message = error instanceof Error ? error.message : String(error);
@@ -2,15 +2,15 @@ import type { Trail } from './trail.js';
2
2
  import type { Implementation } from './types.js';
3
3
 
4
4
  // ---------------------------------------------------------------------------
5
- // Layer interface
5
+ // Gate interface
6
6
  // ---------------------------------------------------------------------------
7
7
 
8
- /** A composable middleware that wraps trail implementations */
9
- export interface Layer {
8
+ /** A composable gate that wraps trail implementations. */
9
+ export interface Gate {
10
10
  readonly name: string;
11
11
  readonly description?: string | undefined;
12
12
 
13
- /** Wrap a trail's implementation, returning a new implementation */
13
+ /** Wrap a trail's implementation, returning a new implementation. */
14
14
  wrap<I, O>(
15
15
  trail: Trail<I, O>,
16
16
  implementation: Implementation<I, O>
@@ -22,22 +22,22 @@ export interface Layer {
22
22
  // ---------------------------------------------------------------------------
23
23
 
24
24
  /**
25
- * Apply layers outermost-first: layers[0] wraps layers[1] wraps ... wraps
25
+ * Apply gates outermost-first: gates[0] wraps gates[1] wraps ... wraps
26
26
  * the base implementation.
27
27
  *
28
- * An empty layers array returns the implementation unchanged.
28
+ * An empty gates array returns the implementation unchanged.
29
29
  */
30
- export const composeLayers = <I, O>(
31
- layers: readonly Layer[],
30
+ export const composeGates = <I, O>(
31
+ gates: readonly Gate[],
32
32
  trail: Trail<I, O>,
33
33
  implementation: Implementation<I, O>
34
34
  ): Implementation<I, O> => {
35
- // Fold right so layers[0] is the outermost wrapper
35
+ // Fold right so gates[0] is the outermost wrapper.
36
36
  let result = implementation;
37
- for (let i = layers.length - 1; i >= 0; i -= 1) {
38
- const layer = layers[i];
39
- if (layer) {
40
- result = layer.wrap(trail, result);
37
+ for (let i = gates.length - 1; i >= 0; i -= 1) {
38
+ const gate = gates[i];
39
+ if (gate) {
40
+ result = gate.wrap(trail, result);
41
41
  }
42
42
  }
43
43
  return result;