@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.
- package/CHANGELOG.md +59 -35
- package/README.md +19 -19
- package/dist/context.d.ts +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +5 -4
- package/dist/context.js.map +1 -1
- package/dist/derive.d.ts +8 -3
- package/dist/derive.d.ts.map +1 -1
- package/dist/derive.js +7 -7
- package/dist/derive.js.map +1 -1
- package/dist/event.d.ts +4 -41
- package/dist/event.d.ts.map +1 -1
- package/dist/event.js +4 -14
- package/dist/event.js.map +1 -1
- package/dist/execute.d.ts +10 -10
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +35 -28
- package/dist/execute.js.map +1 -1
- package/dist/gate.d.ts +17 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +21 -0
- package/dist/gate.js.map +1 -0
- package/dist/index.d.ts +10 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -8
- package/dist/index.js.map +1 -1
- package/dist/layer.d.ts +5 -16
- package/dist/layer.d.ts.map +1 -1
- package/dist/layer.js +3 -20
- package/dist/layer.js.map +1 -1
- package/dist/provision-config.d.ts +22 -0
- package/dist/provision-config.d.ts.map +1 -0
- package/dist/provision-config.js +210 -0
- package/dist/provision-config.js.map +1 -0
- package/dist/provision.d.ts +71 -0
- package/dist/provision.d.ts.map +1 -0
- package/dist/provision.js +56 -0
- package/dist/provision.js.map +1 -0
- package/dist/run.d.ts +27 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +34 -0
- package/dist/run.js.map +1 -0
- package/dist/service-config.d.ts +7 -7
- package/dist/service-config.d.ts.map +1 -1
- package/dist/service-config.js +75 -73
- package/dist/service-config.js.map +1 -1
- package/dist/service.d.ts +32 -36
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +21 -21
- package/dist/service.js.map +1 -1
- package/dist/signal.d.ts +33 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +17 -0
- package/dist/signal.js.map +1 -0
- package/dist/topo.d.ts +10 -10
- package/dist/topo.d.ts.map +1 -1
- package/dist/topo.js +48 -35
- package/dist/topo.js.map +1 -1
- package/dist/trail.d.ts +16 -16
- package/dist/trail.d.ts.map +1 -1
- package/dist/trail.js +5 -4
- package/dist/trail.js.map +1 -1
- package/dist/types.d.ts +10 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/dist/types.js.map +1 -1
- package/dist/validate-topo.d.ts +2 -2
- package/dist/validate-topo.js +31 -31
- package/dist/validate-topo.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/context.test.ts +8 -8
- package/src/__tests__/execute.test.ts +124 -122
- package/src/__tests__/{layer.test.ts → gate.test.ts} +26 -26
- package/src/__tests__/{dispatch.test.ts → run.test.ts} +39 -39
- package/src/__tests__/service-config.test.ts +41 -37
- package/src/__tests__/service.test.ts +71 -71
- package/src/__tests__/{event.test.ts → signal.test.ts} +15 -15
- package/src/__tests__/topo.test.ts +54 -54
- package/src/__tests__/trail-permit.test.ts +4 -4
- package/src/__tests__/trail.test.ts +58 -58
- package/src/__tests__/type-utils.test.ts +3 -3
- package/src/__tests__/validate-topo.test.ts +38 -38
- package/src/context.ts +5 -4
- package/src/derive.ts +8 -8
- package/src/event.ts +11 -73
- package/src/execute.ts +56 -40
- package/src/{layer.ts → gate.ts} +13 -13
- package/src/index.ts +24 -22
- package/src/{service-config.ts → provision-config.ts} +124 -105
- package/src/provision.ts +148 -0
- package/src/{dispatch.ts → run.ts} +8 -8
- package/src/signal.ts +65 -0
- package/src/topo.ts +74 -52
- package/src/trail.ts +23 -22
- package/src/types.ts +11 -11
- package/src/validate-topo.ts +33 -33
- package/tsconfig.tsbuildinfo +1 -1
- 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
|
-
|
|
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
|
-
|
|
29
|
+
provisions?: readonly ReturnType<typeof provision>[];
|
|
30
30
|
}
|
|
31
31
|
) => ({
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
services: Object.freeze([...(overrides?.services ?? [])]),
|
|
37
|
+
provisions: Object.freeze([...(overrides?.provisions ?? [])]),
|
|
38
38
|
...overrides,
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
const
|
|
42
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
|
81
|
-
test('trail
|
|
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
|
-
|
|
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('
|
|
93
|
+
expect(issues[0]?.rule).toBe('cross-exists');
|
|
94
94
|
expect(issues[0]?.message).toContain('entity.missing');
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
test('trail
|
|
97
|
+
test('trail crossing itself fails', () => {
|
|
98
98
|
const app = topo('app', {
|
|
99
|
-
loop: mockTrail('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-
|
|
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', {
|
|
112
|
-
b: mockTrail('b', {
|
|
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 === '
|
|
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', {
|
|
127
|
-
b: mockTrail('b', {
|
|
128
|
-
c: mockTrail('c', {
|
|
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 === '
|
|
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', {
|
|
143
|
-
b: mockTrail('b', {
|
|
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('
|
|
153
|
-
test('trail declaring a registered
|
|
154
|
-
const db =
|
|
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
|
-
|
|
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
|
|
167
|
-
const db =
|
|
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
|
-
|
|
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('
|
|
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('
|
|
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 =
|
|
304
|
+
const db = mockProvision('db.main');
|
|
305
305
|
const app = topo('app', {
|
|
306
|
-
broken: mockTrail('entity.broken', {
|
|
306
|
+
broken: mockTrail('entity.broken', { crosses: ['entity.missing'] }),
|
|
307
307
|
missingService: mockTrail('entity.missing-service', {
|
|
308
|
-
|
|
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 {
|
|
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
|
-
* - `
|
|
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
|
-
|
|
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
|
|
5
|
-
* that UI
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
77
|
-
export type
|
|
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
|
|
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 {
|
|
11
|
-
import type {
|
|
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 {
|
|
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 {
|
|
19
|
-
import {
|
|
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
|
|
36
|
-
/**
|
|
37
|
-
readonly
|
|
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
|
|
43
|
-
readonly
|
|
44
|
-
/** Config values for
|
|
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
|
-
|
|
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
|
-
):
|
|
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
|
-
|
|
77
|
-
|
|
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
|
|
81
|
-
resolved.
|
|
76
|
+
options?.ctx?.extensions === undefined &&
|
|
77
|
+
resolved.provision !== undefined
|
|
82
78
|
) {
|
|
83
|
-
|
|
84
|
-
bound.service = createServiceLookup(() => bound);
|
|
85
|
-
return bound;
|
|
79
|
+
return resolved as TrailContext;
|
|
86
80
|
}
|
|
87
81
|
|
|
88
|
-
|
|
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
|
|
112
|
+
return await resolveProvisions(
|
|
97
113
|
trail,
|
|
98
114
|
baseCtx,
|
|
99
|
-
options?.
|
|
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
|
-
|
|
124
|
+
gates: readonly Gate[]
|
|
109
125
|
): Promise<Result<unknown, Error>> => {
|
|
110
|
-
const impl =
|
|
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-
|
|
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?.
|
|
160
|
+
options?.gates ?? []
|
|
145
161
|
);
|
|
146
162
|
} catch (error: unknown) {
|
|
147
163
|
const message = error instanceof Error ? error.message : String(error);
|
package/src/{layer.ts → gate.ts}
RENAMED
|
@@ -2,15 +2,15 @@ import type { Trail } from './trail.js';
|
|
|
2
2
|
import type { Implementation } from './types.js';
|
|
3
3
|
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
|
-
//
|
|
5
|
+
// Gate interface
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
|
|
8
|
-
/** A composable
|
|
9
|
-
export interface
|
|
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
|
|
25
|
+
* Apply gates outermost-first: gates[0] wraps gates[1] wraps ... wraps
|
|
26
26
|
* the base implementation.
|
|
27
27
|
*
|
|
28
|
-
* An empty
|
|
28
|
+
* An empty gates array returns the implementation unchanged.
|
|
29
29
|
*/
|
|
30
|
-
export const
|
|
31
|
-
|
|
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
|
|
35
|
+
// Fold right so gates[0] is the outermost wrapper.
|
|
36
36
|
let result = implementation;
|
|
37
|
-
for (let i =
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
40
|
-
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;
|