@ontrails/core 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 (99) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +67 -30
  3. package/README.md +19 -19
  4. package/dist/context.d.ts +1 -1
  5. package/dist/context.d.ts.map +1 -1
  6. package/dist/context.js +5 -4
  7. package/dist/context.js.map +1 -1
  8. package/dist/derive.d.ts +8 -3
  9. package/dist/derive.d.ts.map +1 -1
  10. package/dist/derive.js +7 -7
  11. package/dist/derive.js.map +1 -1
  12. package/dist/event.d.ts +4 -41
  13. package/dist/event.d.ts.map +1 -1
  14. package/dist/event.js +4 -14
  15. package/dist/event.js.map +1 -1
  16. package/dist/execute.d.ts +11 -9
  17. package/dist/execute.d.ts.map +1 -1
  18. package/dist/execute.js +34 -151
  19. package/dist/execute.js.map +1 -1
  20. package/dist/gate.d.ts +17 -0
  21. package/dist/gate.d.ts.map +1 -0
  22. package/dist/gate.js +21 -0
  23. package/dist/gate.js.map +1 -0
  24. package/dist/index.d.ts +10 -7
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +9 -7
  27. package/dist/index.js.map +1 -1
  28. package/dist/layer.d.ts +5 -16
  29. package/dist/layer.d.ts.map +1 -1
  30. package/dist/layer.js +3 -20
  31. package/dist/layer.js.map +1 -1
  32. package/dist/provision-config.d.ts +22 -0
  33. package/dist/provision-config.d.ts.map +1 -0
  34. package/dist/provision-config.js +210 -0
  35. package/dist/provision-config.js.map +1 -0
  36. package/dist/provision.d.ts +71 -0
  37. package/dist/provision.d.ts.map +1 -0
  38. package/dist/provision.js +56 -0
  39. package/dist/provision.js.map +1 -0
  40. package/dist/run.d.ts +27 -0
  41. package/dist/run.d.ts.map +1 -0
  42. package/dist/run.js +34 -0
  43. package/dist/run.js.map +1 -0
  44. package/dist/service-config.d.ts +22 -0
  45. package/dist/service-config.d.ts.map +1 -0
  46. package/dist/service-config.js +210 -0
  47. package/dist/service-config.js.map +1 -0
  48. package/dist/service.d.ts +39 -37
  49. package/dist/service.d.ts.map +1 -1
  50. package/dist/service.js +21 -21
  51. package/dist/service.js.map +1 -1
  52. package/dist/signal.d.ts +33 -0
  53. package/dist/signal.d.ts.map +1 -0
  54. package/dist/signal.js +17 -0
  55. package/dist/signal.js.map +1 -0
  56. package/dist/topo.d.ts +10 -10
  57. package/dist/topo.d.ts.map +1 -1
  58. package/dist/topo.js +48 -35
  59. package/dist/topo.js.map +1 -1
  60. package/dist/trail.d.ts +19 -17
  61. package/dist/trail.d.ts.map +1 -1
  62. package/dist/trail.js +5 -4
  63. package/dist/trail.js.map +1 -1
  64. package/dist/types.d.ts +26 -9
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/types.js +2 -1
  67. package/dist/types.js.map +1 -1
  68. package/dist/validate-topo.d.ts +2 -2
  69. package/dist/validate-topo.js +31 -31
  70. package/dist/validate-topo.js.map +1 -1
  71. package/package.json +1 -1
  72. package/src/__tests__/context.test.ts +8 -8
  73. package/src/__tests__/execute.test.ts +124 -122
  74. package/src/__tests__/{layer.test.ts → gate.test.ts} +26 -26
  75. package/src/__tests__/{dispatch.test.ts → run.test.ts} +39 -39
  76. package/src/__tests__/service-config.test.ts +228 -0
  77. package/src/__tests__/service.test.ts +71 -71
  78. package/src/__tests__/{event.test.ts → signal.test.ts} +15 -15
  79. package/src/__tests__/topo.test.ts +54 -54
  80. package/src/__tests__/trail-permit.test.ts +60 -0
  81. package/src/__tests__/trail.test.ts +58 -58
  82. package/src/__tests__/type-utils.test.ts +3 -3
  83. package/src/__tests__/validate-topo.test.ts +38 -38
  84. package/src/context.ts +5 -4
  85. package/src/derive.ts +8 -8
  86. package/src/event.ts +11 -73
  87. package/src/execute.ts +59 -238
  88. package/src/{layer.ts → gate.ts} +13 -13
  89. package/src/index.ts +26 -21
  90. package/src/provision-config.ts +373 -0
  91. package/src/provision.ts +148 -0
  92. package/src/{dispatch.ts → run.ts} +8 -8
  93. package/src/signal.ts +65 -0
  94. package/src/topo.ts +74 -52
  95. package/src/trail.ts +30 -23
  96. package/src/types.ts +30 -10
  97. package/src/validate-topo.ts +33 -33
  98. package/tsconfig.tsbuildinfo +1 -1
  99. package/src/service.ts +0 -139
package/src/execute.ts CHANGED
@@ -1,25 +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 {
12
- AnyService,
13
- ServiceContext,
14
- ServiceOverrideMap,
15
- } from './service.js';
10
+ import type { Gate } from './gate.js';
11
+ import type { ProvisionOverrideMap } from './provision.js';
16
12
  import type { TrailContext, TrailContextInit } from './types.js';
17
13
 
18
- import { composeLayers } from './layer.js';
14
+ import { composeGates } from './gate.js';
19
15
  import { createTrailContext } from './context.js';
20
16
  import { InternalError } from './errors.js';
21
17
  import { Result } from './result.js';
22
- import { createServiceLookup } from './service.js';
18
+ import { createProvisionLookup } from './provision.js';
19
+ import { resolveProvisions } from './provision-config.js';
23
20
  import { validateInput } from './validation.js';
24
21
 
25
22
  type MutableTrailContext = {
@@ -35,36 +32,29 @@ export interface ExecuteTrailOptions {
35
32
  /** Partial context overrides merged on top of the base context. */
36
33
  readonly ctx?: Partial<TrailContextInit> | undefined;
37
34
  /** AbortSignal override (takes final precedence over ctx and factory). */
38
- readonly signal?: AbortSignal | undefined;
39
- /** Layers to compose around the implementation. */
40
- readonly layers?: readonly Layer[] | undefined;
35
+ readonly abortSignal?: AbortSignal | undefined;
36
+ /** Gates to compose around the implementation. */
37
+ readonly gates?: readonly Gate[] | undefined;
41
38
  /** Factory that produces a base TrailContext (takes precedence over defaults). */
42
39
  readonly createContext?:
43
40
  | (() => TrailContextInit | Promise<TrailContextInit>)
44
41
  | undefined;
45
- /** Explicit service instance overrides keyed by service ID. */
46
- readonly services?: ServiceOverrideMap | undefined;
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
+ readonly configValues?:
46
+ | Readonly<Record<string, Record<string, unknown>>>
47
+ | undefined;
47
48
  }
48
49
 
49
50
  // ---------------------------------------------------------------------------
50
51
  // Context resolution
51
52
  // ---------------------------------------------------------------------------
52
53
 
53
- /**
54
- * Build a TrailContext from options.
55
- *
56
- * Resolution order:
57
- * 1. Factory (`createContext`) or `createTrailContext()` defaults.
58
- * 2. Partial `ctx` overrides merged on top.
59
- * 3. `signal` override takes final precedence.
60
- */
61
- const resolveContext = async (
54
+ const applyContextOverrides = (
55
+ base: TrailContextInit,
62
56
  options?: ExecuteTrailOptions
63
- ): Promise<TrailContext> => {
64
- const seed = options?.createContext
65
- ? await options.createContext()
66
- : createTrailContext();
67
- const base = seed.service ? seed : createTrailContext(seed);
57
+ ): TrailContextInit => {
68
58
  const withOverrides = options?.ctx
69
59
  ? {
70
60
  ...base,
@@ -72,220 +62,46 @@ const resolveContext = async (
72
62
  extensions: { ...base.extensions, ...options.ctx.extensions },
73
63
  }
74
64
  : base;
75
- const resolved = options?.signal
76
- ? { ...withOverrides, signal: options.signal }
77
- : withOverrides;
78
- if (
79
- options?.ctx?.extensions !== undefined ||
80
- resolved.service === undefined
81
- ) {
82
- const bound = { ...resolved } as MutableTrailContext;
83
- bound.service = createServiceLookup(() => bound);
84
- return bound;
85
- }
86
-
87
- return resolved as TrailContext;
88
- };
89
-
90
- const singletonServices = new WeakMap<AnyService, Map<string, unknown>>();
91
-
92
- /** In-flight service creation promises, keyed by service × context. */
93
- const pendingCreations = new WeakMap<
94
- AnyService,
95
- Map<string, Promise<Result<unknown, Error>>>
96
- >();
97
-
98
- const hasOwnServiceOverride = (
99
- overrides: ServiceOverrideMap | undefined,
100
- id: string
101
- ): overrides is ServiceOverrideMap =>
102
- overrides !== undefined && Object.hasOwn(overrides, id);
103
-
104
- const toServiceContext = (ctx: TrailContext): ServiceContext => ({
105
- cwd: ctx.cwd,
106
- env: ctx.env,
107
- workspaceRoot: ctx.workspaceRoot,
108
- });
109
65
 
110
- const toServiceContextKey = (ctx: ServiceContext): string =>
111
- JSON.stringify({
112
- cwd: ctx.cwd,
113
- env: Object.entries(ctx.env ?? {}).toSorted(([left], [right]) =>
114
- left.localeCompare(right)
115
- ),
116
- workspaceRoot: ctx.workspaceRoot,
117
- });
118
-
119
- const toInternalServiceError = (id: string, error: unknown): InternalError => {
120
- const cause = error instanceof Error ? error : undefined;
121
- const message = cause?.message ?? String(error);
122
- return new InternalError(`Service "${id}" failed to resolve: ${message}`, {
123
- ...(cause ? { cause } : {}),
124
- context: { serviceId: id },
125
- });
126
- };
127
-
128
- const getCachedSingletonService = (
129
- declaredService: AnyService,
130
- serviceContext: ServiceContext
131
- ): { readonly found: boolean; readonly value: unknown } => {
132
- const scopedCache = singletonServices.get(declaredService);
133
- if (scopedCache === undefined) {
134
- return { found: false, value: undefined };
135
- }
136
-
137
- const key = toServiceContextKey(serviceContext);
138
- if (!scopedCache.has(key)) {
139
- return { found: false, value: undefined };
140
- }
141
-
142
- return {
143
- found: true,
144
- value: scopedCache.get(key),
145
- };
146
- };
147
-
148
- const getProvidedService = (
149
- ctx: TrailContext,
150
- overrides: ServiceOverrideMap | undefined,
151
- declaredService: AnyService,
152
- serviceContext: ServiceContext
153
- ): Result<unknown, Error> | undefined => {
154
- const { id } = declaredService;
155
- if (hasOwnServiceOverride(overrides, id)) {
156
- return Result.ok(overrides[id]);
157
- }
158
-
159
- if (Object.hasOwn(ctx.extensions ?? {}, id)) {
160
- return Result.ok(ctx.extensions?.[id]);
161
- }
162
-
163
- const cached = getCachedSingletonService(declaredService, serviceContext);
164
- if (cached.found) {
165
- return Result.ok(cached.value);
166
- }
167
-
168
- return undefined;
169
- };
170
-
171
- const getSingletonServiceCache = (
172
- declaredService: AnyService
173
- ): Map<string, unknown> => {
174
- const existing = singletonServices.get(declaredService);
175
- if (existing !== undefined) {
176
- return existing;
177
- }
178
-
179
- const created = new Map<string, unknown>();
180
- singletonServices.set(declaredService, created);
181
- return created;
66
+ return options?.abortSignal
67
+ ? { ...withOverrides, abortSignal: options.abortSignal }
68
+ : withOverrides;
182
69
  };
183
70
 
184
- const doCreateServiceInstance = async (
185
- declaredService: AnyService,
186
- serviceContext: ServiceContext
187
- ): Promise<Result<unknown, Error>> => {
188
- try {
189
- const created = await declaredService.create(serviceContext);
190
- if (created.isErr()) {
191
- return Result.err(created.error);
192
- }
193
-
194
- const instance = created.unwrap();
195
- getSingletonServiceCache(declaredService).set(
196
- toServiceContextKey(serviceContext),
197
- instance
198
- );
199
- return Result.ok(instance);
200
- } catch (error: unknown) {
201
- return Result.err(toInternalServiceError(declaredService.id, error));
71
+ const bindProvisionLookup = (
72
+ resolved: TrailContextInit,
73
+ options?: ExecuteTrailOptions
74
+ ): TrailContext => {
75
+ if (
76
+ options?.ctx?.extensions === undefined &&
77
+ resolved.provision !== undefined
78
+ ) {
79
+ return resolved as TrailContext;
202
80
  }
203
- };
204
81
 
205
- const trackPendingCreation = (
206
- declaredService: AnyService,
207
- key: string,
208
- promise: Promise<Result<unknown, Error>>
209
- ): void => {
210
- const pending = pendingCreations.get(declaredService);
211
- if (pending) {
212
- pending.set(key, promise);
213
- } else {
214
- pendingCreations.set(declaredService, new Map([[key, promise]]));
215
- }
82
+ const bound = { ...resolved } as MutableTrailContext;
83
+ const lookup = createProvisionLookup(() => bound);
84
+ bound.provision = lookup;
85
+ return bound;
216
86
  };
217
87
 
218
88
  /**
219
- * Deduplicates concurrent creation of the same service singleton.
220
- * If a creation is already in flight for this service × context key,
221
- * returns the existing promise instead of spawning a second factory call.
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.
222
95
  */
223
- const createServiceInstance = async (
224
- declaredService: AnyService,
225
- serviceContext: ServiceContext
226
- ): Promise<Result<unknown, Error>> => {
227
- const key = toServiceContextKey(serviceContext);
228
- const inflight = pendingCreations.get(declaredService)?.get(key);
229
- if (inflight) {
230
- return inflight;
231
- }
232
-
233
- const promise = doCreateServiceInstance(declaredService, serviceContext);
234
- trackPendingCreation(declaredService, key, promise);
235
-
236
- try {
237
- return await promise;
238
- } finally {
239
- pendingCreations.get(declaredService)?.delete(key);
240
- }
241
- };
242
-
243
- const resolveServiceInstance = async (
244
- declaredService: AnyService,
245
- ctx: TrailContext,
246
- serviceContext: ServiceContext,
247
- overrides?: ServiceOverrideMap
248
- ): Promise<Result<unknown, Error>> =>
249
- getProvidedService(ctx, overrides, declaredService, serviceContext) ??
250
- (await createServiceInstance(declaredService, serviceContext));
251
-
252
- const withResolvedServices = (
253
- ctx: TrailContext,
254
- resolvedServices: Record<string, unknown>
255
- ): TrailContext => {
256
- const extensions = { ...ctx.extensions, ...resolvedServices };
257
- const resolvedCtx = { ...ctx, extensions } as MutableTrailContext;
258
- resolvedCtx.service = createServiceLookup(() => resolvedCtx);
259
- return resolvedCtx;
260
- };
261
-
262
- const resolveServices = async (
263
- trail: AnyTrail,
264
- ctx: TrailContext,
265
- overrides?: ServiceOverrideMap
266
- ): Promise<Result<TrailContext, Error>> => {
267
- if (trail.services.length === 0) {
268
- return Result.ok(ctx);
269
- }
270
-
271
- const resolvedServices: Record<string, unknown> = {};
272
- const serviceContext = toServiceContext(ctx);
273
-
274
- for (const declaredService of trail.services) {
275
- const resolved = await resolveServiceInstance(
276
- declaredService,
277
- ctx,
278
- serviceContext,
279
- overrides
280
- );
281
- if (resolved.isErr()) {
282
- return resolved;
283
- }
284
-
285
- resolvedServices[declaredService.id] = resolved.unwrap();
286
- }
287
-
288
- return Result.ok(withResolvedServices(ctx, resolvedServices));
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);
289
105
  };
290
106
 
291
107
  const prepareContext = async (
@@ -293,16 +109,21 @@ const prepareContext = async (
293
109
  options?: ExecuteTrailOptions
294
110
  ): Promise<Result<TrailContext, Error>> => {
295
111
  const baseCtx = await resolveContext(options);
296
- return await resolveServices(trail, baseCtx, options?.services);
112
+ return await resolveProvisions(
113
+ trail,
114
+ baseCtx,
115
+ options?.provisions,
116
+ options?.configValues
117
+ );
297
118
  };
298
119
 
299
120
  const runTrail = async (
300
121
  trail: AnyTrail,
301
122
  input: unknown,
302
123
  ctx: TrailContext,
303
- layers: readonly Layer[]
124
+ gates: readonly Gate[]
304
125
  ): Promise<Result<unknown, Error>> => {
305
- const impl = composeLayers([...layers], trail, trail.run);
126
+ const impl = composeGates([...gates], trail, trail.blaze);
306
127
  return await impl(input, ctx);
307
128
  };
308
129
 
@@ -311,7 +132,7 @@ const runTrail = async (
311
132
  // ---------------------------------------------------------------------------
312
133
 
313
134
  /**
314
- * Execute a trail through the standard validate-context-layers-run pipeline.
135
+ * Execute a trail through the standard validate-context-gates-run pipeline.
315
136
  *
316
137
  * The function never throws -- unexpected exceptions are caught and
317
138
  * returned as `Result.err(InternalError)`.
@@ -336,7 +157,7 @@ export const executeTrail = async (
336
157
  trail,
337
158
  validated.value,
338
159
  resolvedCtx.value,
339
- options?.layers ?? []
160
+ options?.gates ?? []
340
161
  );
341
162
  } catch (error: unknown) {
342
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;
package/src/index.ts CHANGED
@@ -31,30 +31,33 @@ export type {
31
31
  Implementation,
32
32
  TrailContext,
33
33
  TrailContextInit,
34
- FollowFn,
34
+ CrossFn,
35
+ BasePermit,
36
+ PermitRequirement,
35
37
  ProgressCallback,
36
38
  ProgressEvent,
37
39
  Logger,
38
- ServiceLookup,
40
+ ProvisionLookup,
39
41
  } from './types.js';
42
+ export { TRAILHEAD_KEY } from './types.js';
40
43
 
41
44
  // Context factory
42
45
  export { createTrailContext } from './context.js';
43
46
 
44
- // Service
47
+ // Provision
45
48
  export {
46
- createServiceLookup,
47
- findDuplicateServiceId,
48
- isService,
49
- service,
50
- } from './service.js';
49
+ createProvisionLookup,
50
+ findDuplicateProvisionId,
51
+ isProvision,
52
+ provision,
53
+ } from './provision.js';
51
54
  export type {
52
- AnyService,
53
- Service,
54
- ServiceContext,
55
- ServiceOverrideMap,
56
- ServiceSpec,
57
- } from './service.js';
55
+ AnyProvision,
56
+ Provision,
57
+ ProvisionContext,
58
+ ProvisionOverrideMap,
59
+ ProvisionSpec,
60
+ } from './provision.js';
58
61
 
59
62
  // Trail
60
63
  export { trail } from './trail.js';
@@ -70,7 +73,9 @@ export type {
70
73
  export type { TrailInput, TrailOutput, TrailResult } from './type-utils.js';
71
74
  export { inputOf, outputOf } from './type-utils.js';
72
75
 
73
- // Event
76
+ // Signal
77
+ export { signal } from './signal.js';
78
+ export type { AnySignal, Signal, SignalSpec } from './signal.js';
74
79
  export { event } from './event.js';
75
80
  export type { AnyEvent, Event, EventSpec } from './event.js';
76
81
 
@@ -82,9 +87,9 @@ export type { Topo } from './topo.js';
82
87
  export { validateTopo } from './validate-topo.js';
83
88
  export type { TopoIssue } from './validate-topo.js';
84
89
 
85
- // Layer
86
- export { composeLayers } from './layer.js';
87
- export type { Layer } from './layer.js';
90
+ // Gate
91
+ export { composeGates } from './gate.js';
92
+ export type { Gate } from './gate.js';
88
93
 
89
94
  // Derive
90
95
  export { deriveFields } from './derive.js';
@@ -94,9 +99,9 @@ export type { Field, FieldOverride } from './derive.js';
94
99
  export { executeTrail } from './execute.js';
95
100
  export type { ExecuteTrailOptions } from './execute.js';
96
101
 
97
- // Dispatch
98
- export { dispatch } from './dispatch.js';
99
- export type { DispatchOptions } from './dispatch.js';
102
+ // Run
103
+ export { run } from './run.js';
104
+ export type { RunOptions } from './run.js';
100
105
 
101
106
  // Validation
102
107
  export {