@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.
- package/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +67 -30
- 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 +11 -9
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +34 -151
- 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 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -7
- 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 +22 -0
- package/dist/service-config.d.ts.map +1 -0
- package/dist/service-config.js +210 -0
- package/dist/service-config.js.map +1 -0
- package/dist/service.d.ts +39 -37
- 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 +19 -17
- 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 +26 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -1
- 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 +228 -0
- 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 +60 -0
- 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 +59 -238
- package/src/{layer.ts → gate.ts} +13 -13
- package/src/index.ts +26 -21
- package/src/provision-config.ts +373 -0
- 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 +30 -23
- package/src/types.ts +30 -10
- package/src/validate-topo.ts +33 -33
- package/tsconfig.tsbuildinfo +1 -1
- package/src/service.ts +0 -139
package/src/topo.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { ValidationError } from './errors.js';
|
|
6
|
-
import type {
|
|
7
|
-
import type {
|
|
8
|
-
import {
|
|
6
|
+
import type { AnySignal } from './event.js';
|
|
7
|
+
import type { AnyProvision } from './provision.js';
|
|
8
|
+
import { isProvision } from './provision.js';
|
|
9
9
|
import type { AnyTrail } from './trail.js';
|
|
10
10
|
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
@@ -15,33 +15,33 @@ import type { AnyTrail } from './trail.js';
|
|
|
15
15
|
export interface Topo {
|
|
16
16
|
readonly name: string;
|
|
17
17
|
readonly trails: ReadonlyMap<string, AnyTrail>;
|
|
18
|
-
readonly
|
|
19
|
-
readonly
|
|
18
|
+
readonly signals: ReadonlyMap<string, AnySignal>;
|
|
19
|
+
readonly provisions: ReadonlyMap<string, AnyProvision>;
|
|
20
20
|
readonly count: number;
|
|
21
|
-
readonly
|
|
21
|
+
readonly provisionCount: number;
|
|
22
22
|
get(id: string): AnyTrail | undefined;
|
|
23
|
-
|
|
23
|
+
getProvision(id: string): AnyProvision | undefined;
|
|
24
24
|
has(id: string): boolean;
|
|
25
|
-
|
|
25
|
+
hasProvision(id: string): boolean;
|
|
26
26
|
ids(): string[];
|
|
27
|
-
|
|
27
|
+
provisionIds(): string[];
|
|
28
28
|
list(): AnyTrail[];
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
listSignals(): AnySignal[];
|
|
30
|
+
listProvisions(): AnyProvision[];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
34
34
|
// Kind discriminant check
|
|
35
35
|
// ---------------------------------------------------------------------------
|
|
36
36
|
|
|
37
|
-
type Registrable = AnyTrail |
|
|
37
|
+
type Registrable = AnyTrail | AnySignal | AnyProvision;
|
|
38
38
|
|
|
39
39
|
const isRegistrable = (value: unknown): value is Registrable => {
|
|
40
40
|
if (typeof value !== 'object' || value === null) {
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
43
|
const { kind } = value as Record<string, unknown>;
|
|
44
|
-
return kind === 'trail' || kind === '
|
|
44
|
+
return kind === 'trail' || kind === 'signal';
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
@@ -51,24 +51,22 @@ const isRegistrable = (value: unknown): value is Registrable => {
|
|
|
51
51
|
const createTopo = (
|
|
52
52
|
name: string,
|
|
53
53
|
trails: ReadonlyMap<string, AnyTrail>,
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
signals: ReadonlyMap<string, AnySignal>,
|
|
55
|
+
provisions: ReadonlyMap<string, AnyProvision>
|
|
56
56
|
): Topo => ({
|
|
57
57
|
count: trails.size,
|
|
58
|
-
events,
|
|
59
58
|
get(id: string): AnyTrail | undefined {
|
|
60
59
|
return trails.get(id);
|
|
61
60
|
},
|
|
62
|
-
|
|
63
|
-
return
|
|
61
|
+
getProvision(id: string): AnyProvision | undefined {
|
|
62
|
+
return provisions.get(id);
|
|
64
63
|
},
|
|
65
64
|
has(id: string): boolean {
|
|
66
65
|
return trails.has(id);
|
|
67
66
|
},
|
|
68
|
-
|
|
69
|
-
return
|
|
67
|
+
hasProvision(id: string): boolean {
|
|
68
|
+
return provisions.has(id);
|
|
70
69
|
},
|
|
71
|
-
|
|
72
70
|
ids(): string[] {
|
|
73
71
|
return [...trails.keys()];
|
|
74
72
|
},
|
|
@@ -76,21 +74,22 @@ const createTopo = (
|
|
|
76
74
|
list(): AnyTrail[] {
|
|
77
75
|
return [...trails.values()];
|
|
78
76
|
},
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return [...events.values()];
|
|
77
|
+
listProvisions(): AnyProvision[] {
|
|
78
|
+
return [...provisions.values()];
|
|
82
79
|
},
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
|
|
81
|
+
listSignals(): AnySignal[] {
|
|
82
|
+
return [...signals.values()];
|
|
85
83
|
},
|
|
86
84
|
|
|
87
85
|
name,
|
|
88
|
-
|
|
89
|
-
serviceIds(): string[] {
|
|
90
|
-
return [...services.keys()];
|
|
91
|
-
},
|
|
86
|
+
provisionCount: provisions.size,
|
|
92
87
|
|
|
93
|
-
|
|
88
|
+
provisionIds(): string[] {
|
|
89
|
+
return [...provisions.keys()];
|
|
90
|
+
},
|
|
91
|
+
provisions,
|
|
92
|
+
signals,
|
|
94
93
|
trails,
|
|
95
94
|
});
|
|
96
95
|
|
|
@@ -102,22 +101,22 @@ const createTopo = (
|
|
|
102
101
|
const register = (
|
|
103
102
|
value: Registrable,
|
|
104
103
|
trails: Map<string, AnyTrail>,
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
signals: Map<string, AnySignal>,
|
|
105
|
+
provisions: Map<string, AnyProvision>
|
|
107
106
|
): void => {
|
|
108
107
|
const { id } = value as { id: string };
|
|
109
108
|
const registrars: Record<string, () => void> = {
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
throw new ValidationError(`Duplicate
|
|
109
|
+
provision: () => {
|
|
110
|
+
if (provisions.has(id)) {
|
|
111
|
+
throw new ValidationError(`Duplicate provision ID: "${id}"`);
|
|
113
112
|
}
|
|
114
|
-
|
|
113
|
+
provisions.set(id, value as AnyProvision);
|
|
115
114
|
},
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
throw new ValidationError(`Duplicate
|
|
115
|
+
signal: () => {
|
|
116
|
+
if (signals.has(id)) {
|
|
117
|
+
throw new ValidationError(`Duplicate signal ID: "${id}"`);
|
|
119
118
|
}
|
|
120
|
-
|
|
119
|
+
signals.set(id, value as AnySignal);
|
|
121
120
|
},
|
|
122
121
|
trail: () => {
|
|
123
122
|
if (trails.has(id)) {
|
|
@@ -129,20 +128,43 @@ const register = (
|
|
|
129
128
|
registrars[value.kind]?.();
|
|
130
129
|
};
|
|
131
130
|
|
|
131
|
+
const markUniqueObject = (
|
|
132
|
+
value: unknown,
|
|
133
|
+
seenValues: WeakSet<object>
|
|
134
|
+
): boolean => {
|
|
135
|
+
if (typeof value !== 'object' || value === null) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if (seenValues.has(value)) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
seenValues.add(value);
|
|
142
|
+
return true;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const registerModuleValue = (
|
|
146
|
+
value: unknown,
|
|
147
|
+
trails: Map<string, AnyTrail>,
|
|
148
|
+
signals: Map<string, AnySignal>,
|
|
149
|
+
provisions: Map<string, AnyProvision>
|
|
150
|
+
): void => {
|
|
151
|
+
if (isProvision(value) || isRegistrable(value)) {
|
|
152
|
+
register(value, trails, signals, provisions);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
132
156
|
const registerModuleValues = (
|
|
133
157
|
mod: Record<string, unknown>,
|
|
134
158
|
trails: Map<string, AnyTrail>,
|
|
135
|
-
|
|
136
|
-
|
|
159
|
+
signals: Map<string, AnySignal>,
|
|
160
|
+
provisions: Map<string, AnyProvision>
|
|
137
161
|
): void => {
|
|
162
|
+
const seenValues = new WeakSet<object>();
|
|
138
163
|
for (const value of Object.values(mod)) {
|
|
139
|
-
if (
|
|
140
|
-
register(value, trails, events, services);
|
|
164
|
+
if (!markUniqueObject(value, seenValues)) {
|
|
141
165
|
continue;
|
|
142
166
|
}
|
|
143
|
-
|
|
144
|
-
register(value, trails, events, services);
|
|
145
|
-
}
|
|
167
|
+
registerModuleValue(value, trails, signals, provisions);
|
|
146
168
|
}
|
|
147
169
|
};
|
|
148
170
|
|
|
@@ -151,12 +173,12 @@ export const topo = (
|
|
|
151
173
|
...modules: Record<string, unknown>[]
|
|
152
174
|
): Topo => {
|
|
153
175
|
const trails = new Map<string, AnyTrail>();
|
|
154
|
-
const
|
|
155
|
-
const
|
|
176
|
+
const signals = new Map<string, AnySignal>();
|
|
177
|
+
const provisions = new Map<string, AnyProvision>();
|
|
156
178
|
|
|
157
179
|
for (const mod of modules) {
|
|
158
|
-
registerModuleValues(mod, trails,
|
|
180
|
+
registerModuleValues(mod, trails, signals, provisions);
|
|
159
181
|
}
|
|
160
182
|
|
|
161
|
-
return createTopo(name, trails,
|
|
183
|
+
return createTopo(name, trails, signals, provisions);
|
|
162
184
|
};
|
package/src/trail.ts
CHANGED
|
@@ -2,8 +2,12 @@ import type { z } from 'zod';
|
|
|
2
2
|
|
|
3
3
|
import type { FieldOverride } from './derive.js';
|
|
4
4
|
import type { Result } from './result.js';
|
|
5
|
-
import type {
|
|
6
|
-
import type {
|
|
5
|
+
import type { AnyProvision } from './provision.js';
|
|
6
|
+
import type {
|
|
7
|
+
Implementation,
|
|
8
|
+
PermitRequirement,
|
|
9
|
+
TrailContext,
|
|
10
|
+
} from './types.js';
|
|
7
11
|
|
|
8
12
|
// ---------------------------------------------------------------------------
|
|
9
13
|
// Trail example
|
|
@@ -40,7 +44,7 @@ export interface TrailSpec<I, O> {
|
|
|
40
44
|
/** Zod schema for validating output (optional — some trails are fire-and-forget) */
|
|
41
45
|
readonly output?: z.ZodType<O> | undefined;
|
|
42
46
|
/** The pure function that does the work (sync or async authoring) */
|
|
43
|
-
readonly
|
|
47
|
+
readonly blaze: Implementation<I, O>;
|
|
44
48
|
/** Human-readable description */
|
|
45
49
|
readonly description?: string | undefined;
|
|
46
50
|
/** Named examples for docs and testing */
|
|
@@ -49,16 +53,18 @@ export interface TrailSpec<I, O> {
|
|
|
49
53
|
readonly intent?: 'read' | 'write' | 'destroy' | undefined;
|
|
50
54
|
/** Trail is idempotent (safe to retry) */
|
|
51
55
|
readonly idempotent?: boolean | undefined;
|
|
52
|
-
/** Arbitrary
|
|
53
|
-
readonly
|
|
56
|
+
/** Arbitrary meta for tooling and filtering */
|
|
57
|
+
readonly meta?: Readonly<Record<string, unknown>> | undefined;
|
|
54
58
|
/** Named sets of downstream trail IDs that may be invoked */
|
|
55
59
|
readonly detours?: Readonly<Record<string, readonly string[]>> | undefined;
|
|
56
60
|
/** Per-field overrides for deriveFields() (labels, hints, options) */
|
|
57
61
|
readonly fields?: Readonly<Record<string, FieldOverride>> | undefined;
|
|
58
|
-
/** IDs of downstream trails this trail may invoke via ctx.
|
|
59
|
-
readonly
|
|
60
|
-
/**
|
|
61
|
-
readonly
|
|
62
|
+
/** IDs of downstream trails this trail may invoke via ctx.cross() */
|
|
63
|
+
readonly crosses?: readonly string[] | undefined;
|
|
64
|
+
/** Provisions this trail may access via provision.from(ctx) */
|
|
65
|
+
readonly provisions?: readonly AnyProvision[] | undefined;
|
|
66
|
+
/** Auth requirement: scopes object, 'public', or omitted (undeclared) */
|
|
67
|
+
readonly permit?: PermitRequirement | undefined;
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
// ---------------------------------------------------------------------------
|
|
@@ -71,15 +77,15 @@ export type Intent = 'read' | 'write' | 'destroy';
|
|
|
71
77
|
/** A fully-defined trail — the unit of work in the Trails system */
|
|
72
78
|
export interface Trail<I, O> extends Omit<
|
|
73
79
|
TrailSpec<I, O>,
|
|
74
|
-
'
|
|
80
|
+
'blaze' | 'crosses' | 'intent' | 'provisions'
|
|
75
81
|
> {
|
|
76
82
|
readonly kind: 'trail';
|
|
77
83
|
readonly id: string;
|
|
78
|
-
readonly
|
|
79
|
-
/** IDs of downstream trails this trail may invoke via ctx.
|
|
80
|
-
readonly
|
|
81
|
-
/**
|
|
82
|
-
readonly
|
|
84
|
+
readonly blaze: Implementation<I, O>;
|
|
85
|
+
/** IDs of downstream trails this trail may invoke via ctx.cross() (always present, default []) */
|
|
86
|
+
readonly crosses: readonly string[];
|
|
87
|
+
/** Provisions this trail may access via provision.from(ctx) (always present, default []) */
|
|
88
|
+
readonly provisions: readonly AnyProvision[];
|
|
83
89
|
/** What this trail does to the world (always present, default 'write') */
|
|
84
90
|
readonly intent: Intent;
|
|
85
91
|
}
|
|
@@ -99,14 +105,14 @@ export interface Trail<I, O> extends Omit<
|
|
|
99
105
|
* // ID as first argument (recommended for human authoring)
|
|
100
106
|
* const show = trail("entity.show", {
|
|
101
107
|
* input: z.object({ name: z.string() }),
|
|
102
|
-
*
|
|
108
|
+
* blaze: (input) => Result.ok(entity),
|
|
103
109
|
* });
|
|
104
110
|
*
|
|
105
111
|
* // Full spec object (for programmatic generation)
|
|
106
112
|
* const show = trail({
|
|
107
113
|
* id: "entity.show",
|
|
108
114
|
* input: z.object({ name: z.string() }),
|
|
109
|
-
*
|
|
115
|
+
* blaze: (input) => Result.ok(entity),
|
|
110
116
|
* });
|
|
111
117
|
* ```
|
|
112
118
|
*/
|
|
@@ -128,21 +134,22 @@ export function trail<I, O>(
|
|
|
128
134
|
}
|
|
129
135
|
|
|
130
136
|
const {
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
blaze,
|
|
138
|
+
crosses: rawCrosses,
|
|
133
139
|
intent: rawIntent,
|
|
134
|
-
|
|
140
|
+
provisions: rawProvisions,
|
|
135
141
|
...spec
|
|
136
142
|
} = resolved.spec;
|
|
143
|
+
const provisions = Object.freeze([...(rawProvisions ?? [])]);
|
|
137
144
|
|
|
138
145
|
return Object.freeze({
|
|
139
146
|
...spec,
|
|
140
|
-
|
|
147
|
+
blaze: async (input: I, ctx: TrailContext) => await blaze(input, ctx),
|
|
148
|
+
crosses: Object.freeze([...(rawCrosses ?? [])]),
|
|
141
149
|
id: resolved.id,
|
|
142
150
|
intent: rawIntent ?? 'write',
|
|
143
151
|
kind: 'trail' as const,
|
|
144
|
-
|
|
145
|
-
services: Object.freeze([...(rawServices ?? [])]),
|
|
152
|
+
provisions,
|
|
146
153
|
});
|
|
147
154
|
}
|
|
148
155
|
|
package/src/types.ts
CHANGED
|
@@ -12,14 +12,14 @@ export type Implementation<I, O> = (
|
|
|
12
12
|
) => Result<O, Error> | Promise<Result<O, Error>>;
|
|
13
13
|
|
|
14
14
|
/** Invoke another trail by id — used for trail composition */
|
|
15
|
-
export type
|
|
15
|
+
export type CrossFn = <O>(
|
|
16
16
|
id: string,
|
|
17
17
|
input: unknown
|
|
18
18
|
) => Promise<Result<O, Error>>;
|
|
19
19
|
|
|
20
|
-
/** Resolve a
|
|
21
|
-
export type
|
|
22
|
-
|
|
20
|
+
/** Resolve a provision instance from the current trail context. */
|
|
21
|
+
export type ProvisionLookup = <T = unknown>(
|
|
22
|
+
provisionOrId: { readonly id: string } | string
|
|
23
23
|
) => T;
|
|
24
24
|
|
|
25
25
|
/** Callback for reporting progress from long-running trails */
|
|
@@ -46,22 +46,42 @@ export interface Logger {
|
|
|
46
46
|
child(context: Record<string, unknown>): Logger;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** Context extension key for the invoking trailhead name. */
|
|
50
|
+
export const TRAILHEAD_KEY = '__trails_trailhead' as const;
|
|
51
|
+
|
|
52
|
+
/** Minimal permit shape available on TrailContext. Permits extends this. */
|
|
53
|
+
export interface BasePermit {
|
|
54
|
+
readonly id: string;
|
|
55
|
+
readonly scopes: readonly string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
/** Runtime context threaded through every trail execution */
|
|
50
59
|
export interface TrailContext {
|
|
51
60
|
readonly requestId: string;
|
|
52
|
-
readonly
|
|
53
|
-
readonly
|
|
54
|
-
readonly permit?:
|
|
61
|
+
readonly abortSignal: AbortSignal;
|
|
62
|
+
readonly cross?: CrossFn | undefined;
|
|
63
|
+
readonly permit?: BasePermit;
|
|
55
64
|
readonly workspaceRoot?: string | undefined;
|
|
56
65
|
readonly logger?: Logger | undefined;
|
|
57
66
|
readonly progress?: ProgressCallback | undefined;
|
|
58
67
|
readonly cwd?: string | undefined;
|
|
59
68
|
readonly env?: Record<string, string | undefined> | undefined;
|
|
60
69
|
readonly extensions?: Readonly<Record<string, unknown>> | undefined;
|
|
61
|
-
readonly
|
|
70
|
+
readonly provision?: ProvisionLookup | undefined;
|
|
62
71
|
}
|
|
63
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Permit requirement declared on a trail spec.
|
|
75
|
+
*
|
|
76
|
+
* A scopes object means the trail requires a permit with those scopes.
|
|
77
|
+
* `'public'` means the trail has explicitly opted out of auth.
|
|
78
|
+
* Omitting the field entirely means the trail hasn't declared an auth posture.
|
|
79
|
+
*/
|
|
80
|
+
export type PermitRequirement =
|
|
81
|
+
| { readonly scopes: readonly string[] }
|
|
82
|
+
| 'public';
|
|
83
|
+
|
|
64
84
|
/** Input shape used to seed a runtime TrailContext before resolution. */
|
|
65
|
-
export type TrailContextInit = Omit<TrailContext, '
|
|
66
|
-
readonly
|
|
85
|
+
export type TrailContextInit = Omit<TrailContext, 'provision'> & {
|
|
86
|
+
readonly provision?: ProvisionLookup | undefined;
|
|
67
87
|
};
|
package/src/validate-topo.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Structural validation for a Topo graph.
|
|
3
3
|
*
|
|
4
|
-
* Checks trail
|
|
4
|
+
* Checks trail crossing references, example input validity, event origin
|
|
5
5
|
* references, and output schema completeness. Returns a Result with all
|
|
6
6
|
* issues collected into a single ValidationError.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { ValidationError } from './errors.js';
|
|
10
|
-
import type {
|
|
10
|
+
import type { AnySignal } from './event.js';
|
|
11
11
|
import { Result } from './result.js';
|
|
12
12
|
import type { Topo } from './topo.js';
|
|
13
13
|
import type { AnyTrail } from './trail.js';
|
|
@@ -31,8 +31,8 @@ const WHITE = 0;
|
|
|
31
31
|
const GRAY = 1;
|
|
32
32
|
const BLACK = 2;
|
|
33
33
|
|
|
34
|
-
/** Build an adjacency list and initial color map from trails with
|
|
35
|
-
const
|
|
34
|
+
/** Build an adjacency list and initial color map from trails with crossings. */
|
|
35
|
+
const buildCrossGraph = (
|
|
36
36
|
trails: ReadonlyMap<string, AnyTrail>
|
|
37
37
|
): {
|
|
38
38
|
graph: Map<string, readonly string[]>;
|
|
@@ -40,8 +40,8 @@ const buildFollowGraph = (
|
|
|
40
40
|
} => {
|
|
41
41
|
const graph = new Map<string, readonly string[]>();
|
|
42
42
|
for (const [id, t] of trails) {
|
|
43
|
-
if (t.
|
|
44
|
-
graph.set(id, t.
|
|
43
|
+
if (t.crosses.length > 0) {
|
|
44
|
+
graph.set(id, t.crosses);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
const color = new Map<string, number>();
|
|
@@ -51,12 +51,12 @@ const buildFollowGraph = (
|
|
|
51
51
|
return { color, graph };
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
/** Detect multi-node cycles in the trail
|
|
55
|
-
const
|
|
54
|
+
/** Detect multi-node cycles in the trail crossing graph via DFS. */
|
|
55
|
+
const detectCrossCycles = (
|
|
56
56
|
trails: ReadonlyMap<string, AnyTrail>
|
|
57
57
|
): TopoIssue[] => {
|
|
58
58
|
const issues: TopoIssue[] = [];
|
|
59
|
-
const { color, graph } =
|
|
59
|
+
const { color, graph } = buildCrossGraph(trails);
|
|
60
60
|
|
|
61
61
|
const dfs = (node: string, path: string[]): void => {
|
|
62
62
|
color.set(node, GRAY);
|
|
@@ -69,7 +69,7 @@ const detectFollowCycles = (
|
|
|
69
69
|
const cycle = [...path.slice(path.indexOf(next)), next];
|
|
70
70
|
issues.push({
|
|
71
71
|
message: `Cycle detected: ${cycle.join(' → ')}`,
|
|
72
|
-
rule: '
|
|
72
|
+
rule: 'cross-cycle',
|
|
73
73
|
trailId: next,
|
|
74
74
|
});
|
|
75
75
|
} else if (c === WHITE) {
|
|
@@ -87,44 +87,44 @@ const detectFollowCycles = (
|
|
|
87
87
|
return issues;
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
-
const
|
|
90
|
+
const checkCrosses = (
|
|
91
91
|
trails: ReadonlyMap<string, AnyTrail>,
|
|
92
92
|
topo: Topo
|
|
93
93
|
): TopoIssue[] => {
|
|
94
94
|
const issues: TopoIssue[] = [];
|
|
95
95
|
for (const [id, trail] of trails) {
|
|
96
|
-
for (const
|
|
97
|
-
if (
|
|
96
|
+
for (const crossedId of trail.crosses) {
|
|
97
|
+
if (crossedId === id) {
|
|
98
98
|
issues.push({
|
|
99
|
-
message: `Trail
|
|
100
|
-
rule: 'no-self-
|
|
99
|
+
message: `Trail crosses itself`,
|
|
100
|
+
rule: 'no-self-cross',
|
|
101
101
|
trailId: id,
|
|
102
102
|
});
|
|
103
|
-
} else if (!topo.has(
|
|
103
|
+
} else if (!topo.has(crossedId)) {
|
|
104
104
|
issues.push({
|
|
105
|
-
message: `
|
|
106
|
-
rule: '
|
|
105
|
+
message: `Crosses "${crossedId}" which is not in the topo`,
|
|
106
|
+
rule: 'cross-exists',
|
|
107
107
|
trailId: id,
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
-
issues.push(...
|
|
112
|
+
issues.push(...detectCrossCycles(trails));
|
|
113
113
|
return issues;
|
|
114
114
|
};
|
|
115
115
|
|
|
116
|
-
const
|
|
116
|
+
const checkProvisions = (
|
|
117
117
|
trails: ReadonlyMap<string, AnyTrail>,
|
|
118
118
|
topo: Topo
|
|
119
119
|
): TopoIssue[] => {
|
|
120
120
|
const issues: TopoIssue[] = [];
|
|
121
121
|
|
|
122
122
|
for (const [id, trail] of trails) {
|
|
123
|
-
for (const
|
|
124
|
-
if (!topo.
|
|
123
|
+
for (const declaredProvision of trail.provisions) {
|
|
124
|
+
if (!topo.hasProvision(declaredProvision.id)) {
|
|
125
125
|
issues.push({
|
|
126
|
-
message: `
|
|
127
|
-
rule: '
|
|
126
|
+
message: `Provision "${declaredProvision.id}" is not in the topo`,
|
|
127
|
+
rule: 'provision-exists',
|
|
128
128
|
trailId: id,
|
|
129
129
|
});
|
|
130
130
|
}
|
|
@@ -177,20 +177,20 @@ const checkExamples = (trails: ReadonlyMap<string, AnyTrail>): TopoIssue[] => {
|
|
|
177
177
|
return issues;
|
|
178
178
|
};
|
|
179
179
|
|
|
180
|
-
const
|
|
181
|
-
|
|
180
|
+
const checkSignalOrigins = (
|
|
181
|
+
signals: ReadonlyMap<string, AnySignal>,
|
|
182
182
|
topo: Topo
|
|
183
183
|
): TopoIssue[] => {
|
|
184
184
|
const issues: TopoIssue[] = [];
|
|
185
|
-
for (const [id, evt] of
|
|
185
|
+
for (const [id, evt] of signals) {
|
|
186
186
|
if (!evt.from) {
|
|
187
187
|
continue;
|
|
188
188
|
}
|
|
189
189
|
for (const originId of evt.from) {
|
|
190
190
|
if (!topo.has(originId)) {
|
|
191
191
|
issues.push({
|
|
192
|
-
message: `
|
|
193
|
-
rule: '
|
|
192
|
+
message: `Signal origin "${originId}" is not in the topo`,
|
|
193
|
+
rule: 'signal-origin-exists',
|
|
194
194
|
trailId: id,
|
|
195
195
|
});
|
|
196
196
|
}
|
|
@@ -206,16 +206,16 @@ const checkEventOrigins = (
|
|
|
206
206
|
/**
|
|
207
207
|
* Validate the structural integrity of a Topo graph.
|
|
208
208
|
*
|
|
209
|
-
* Checks
|
|
209
|
+
* Checks crossing references, example inputs, event origins, and output
|
|
210
210
|
* schema presence. Returns `Result.ok()` when no issues are found, or
|
|
211
211
|
* `Result.err(ValidationError)` with all issues in the error context.
|
|
212
212
|
*/
|
|
213
213
|
export const validateTopo = (topo: Topo): Result<void, ValidationError> => {
|
|
214
214
|
const issues = [
|
|
215
|
-
...
|
|
216
|
-
...
|
|
215
|
+
...checkCrosses(topo.trails, topo),
|
|
216
|
+
...checkProvisions(topo.trails, topo),
|
|
217
217
|
...checkExamples(topo.trails),
|
|
218
|
-
...
|
|
218
|
+
...checkSignalOrigins(topo.signals, topo),
|
|
219
219
|
];
|
|
220
220
|
|
|
221
221
|
if (issues.length === 0) {
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/blob-ref.ts","./src/branded.ts","./src/collections.ts","./src/context.ts","./src/derive.ts","./src/
|
|
1
|
+
{"root":["./src/blob-ref.ts","./src/branded.ts","./src/collections.ts","./src/context.ts","./src/derive.ts","./src/errors.ts","./src/event.ts","./src/execute.ts","./src/fetch.ts","./src/gate.ts","./src/guards.ts","./src/index.ts","./src/path-security.ts","./src/provision-config.ts","./src/provision.ts","./src/resilience.ts","./src/result.ts","./src/run.ts","./src/serialization.ts","./src/signal.ts","./src/topo.ts","./src/trail.ts","./src/type-utils.ts","./src/types.ts","./src/validate-topo.ts","./src/validation.ts","./src/workspace.ts","./src/patterns/bulk.ts","./src/patterns/change.ts","./src/patterns/date-range.ts","./src/patterns/index.ts","./src/patterns/pagination.ts","./src/patterns/progress.ts","./src/patterns/sorting.ts","./src/patterns/status.ts","./src/patterns/timestamps.ts","./src/redaction/index.ts","./src/redaction/patterns.ts","./src/redaction/redactor.ts"],"version":"5.9.3"}
|