@ontrails/core 1.0.0-beta.12 → 1.0.0-beta.14
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 +65 -35
- package/README.md +21 -20
- 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 +14 -3
- package/dist/derive.d.ts.map +1 -1
- package/dist/derive.js +36 -12
- package/dist/derive.js.map +1 -1
- package/dist/draft.d.ts +28 -0
- package/dist/draft.d.ts.map +1 -0
- package/dist/draft.js +156 -0
- package/dist/draft.js.map +1 -0
- 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 +16 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -9
- package/dist/index.js.map +1 -1
- package/dist/internal/topo-saves.d.ts +47 -0
- package/dist/internal/topo-saves.d.ts.map +1 -0
- package/dist/internal/topo-saves.js +310 -0
- package/dist/internal/topo-saves.js.map +1 -0
- package/dist/internal/topo-store-read.d.ts +67 -0
- package/dist/internal/topo-store-read.d.ts.map +1 -0
- package/dist/internal/topo-store-read.js +222 -0
- package/dist/internal/topo-store-read.js.map +1 -0
- package/dist/internal/topo-store.d.ts +12 -0
- package/dist/internal/topo-store.d.ts.map +1 -0
- package/dist/internal/topo-store.js +571 -0
- package/dist/internal/topo-store.js.map +1 -0
- package/dist/internal/trails-db.d.ts +16 -0
- package/dist/internal/trails-db.d.ts.map +1 -0
- package/dist/internal/trails-db.js +118 -0
- package/dist/internal/trails-db.js.map +1 -0
- 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-store.d.ts +48 -0
- package/dist/topo-store.d.ts.map +1 -0
- package/dist/topo-store.js +175 -0
- package/dist/topo-store.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-established-topo.d.ts +76 -0
- package/dist/validate-established-topo.d.ts.map +1 -0
- package/dist/validate-established-topo.js +43 -0
- package/dist/validate-established-topo.js.map +1 -0
- package/dist/validate-topo.d.ts +2 -2
- package/dist/validate-topo.d.ts.map +1 -1
- package/dist/validate-topo.js +34 -32
- package/dist/validate-topo.js.map +1 -1
- package/package.json +4 -1
- package/src/__tests__/context.test.ts +8 -8
- package/src/__tests__/derive.test.ts +58 -1
- 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-store-read.test.ts +251 -0
- package/src/__tests__/topo-store.test.ts +469 -0
- 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__/trails-db.test.ts +191 -0
- package/src/__tests__/type-utils.test.ts +3 -3
- package/src/__tests__/validate-topo.test.ts +205 -38
- package/src/context.ts +5 -4
- package/src/derive.ts +47 -16
- package/src/draft.ts +334 -0
- package/src/event.ts +11 -73
- package/src/execute.ts +56 -40
- package/src/{layer.ts → gate.ts} +13 -13
- package/src/index.ts +54 -23
- package/src/internal/topo-saves.ts +429 -0
- package/src/internal/topo-store-read.ts +473 -0
- package/src/internal/topo-store.ts +1087 -0
- package/src/internal/trails-db.ts +189 -0
- 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-store.ts +301 -0
- package/src/topo.ts +74 -52
- package/src/trail.ts +23 -22
- package/src/types.ts +11 -11
- package/src/validate-established-topo.ts +63 -0
- package/src/validate-topo.ts +38 -34
- package/tsconfig.tsbuildinfo +1 -1
- package/src/service.ts +0 -145
package/src/derive.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
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';
|
|
9
9
|
|
|
10
|
+
import { ValidationError } from './errors.js';
|
|
11
|
+
|
|
10
12
|
// ---------------------------------------------------------------------------
|
|
11
13
|
// Public types
|
|
12
14
|
// ---------------------------------------------------------------------------
|
|
13
15
|
|
|
14
|
-
/** A
|
|
16
|
+
/** A runtime-agnostic field descriptor derived from a Zod schema. */
|
|
15
17
|
export interface Field {
|
|
16
18
|
readonly name: string;
|
|
17
19
|
readonly type:
|
|
@@ -140,14 +142,21 @@ interface DerivedFieldType {
|
|
|
140
142
|
type: Field['type'];
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
const fieldTypeByDef: Record<
|
|
145
|
+
const fieldTypeByDef: Record<
|
|
146
|
+
string,
|
|
147
|
+
(s: ZodInternals) => DerivedFieldType | null
|
|
148
|
+
> = {
|
|
144
149
|
array: (s) => {
|
|
145
150
|
const element = s._zod.def['element'] as unknown as ZodInternals;
|
|
146
|
-
const
|
|
151
|
+
const { inner } = unwrap(element);
|
|
152
|
+
const elementType = inner._zod.def['type'] as string;
|
|
147
153
|
if (elementType === 'enum') {
|
|
148
|
-
const entries =
|
|
154
|
+
const entries = inner._zod.def['entries'] as Record<string, string>;
|
|
149
155
|
return { options: Object.values(entries), type: 'multiselect' };
|
|
150
156
|
}
|
|
157
|
+
if (elementType !== 'number' && elementType !== 'string') {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
151
160
|
return {
|
|
152
161
|
options: undefined,
|
|
153
162
|
type: elementType === 'number' ? 'number[]' : 'string[]',
|
|
@@ -163,10 +172,10 @@ const fieldTypeByDef: Record<string, (s: ZodInternals) => DerivedFieldType> = {
|
|
|
163
172
|
};
|
|
164
173
|
|
|
165
174
|
/** Derive field type and raw options from the unwrapped Zod def. */
|
|
166
|
-
const deriveFieldType = (s: ZodInternals): DerivedFieldType => {
|
|
175
|
+
const deriveFieldType = (s: ZodInternals): DerivedFieldType | null => {
|
|
167
176
|
const defType = s._zod.def['type'] as string;
|
|
168
177
|
const derive = fieldTypeByDef[defType];
|
|
169
|
-
return derive ? derive(s) :
|
|
178
|
+
return derive ? derive(s) : null;
|
|
170
179
|
};
|
|
171
180
|
|
|
172
181
|
/** Build options array, merging with overrides when present. */
|
|
@@ -189,29 +198,49 @@ const buildOptions = (
|
|
|
189
198
|
});
|
|
190
199
|
};
|
|
191
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Derive the canonical ordered CLI path from a trail ID.
|
|
203
|
+
*
|
|
204
|
+
* @throws {ValidationError} if the trail ID contains empty segments (e.g. consecutive dots).
|
|
205
|
+
*/
|
|
206
|
+
export const deriveCliPath = (trailId: string): string[] => {
|
|
207
|
+
const segments = trailId.split('.');
|
|
208
|
+
const emptyIndex = segments.findIndex((s) => s.length === 0);
|
|
209
|
+
if (emptyIndex !== -1) {
|
|
210
|
+
throw new ValidationError(
|
|
211
|
+
`Trail ID "${trailId}" contains an empty segment at position ${emptyIndex}`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return segments;
|
|
215
|
+
};
|
|
216
|
+
|
|
192
217
|
// ---------------------------------------------------------------------------
|
|
193
218
|
// Public API
|
|
194
219
|
// ---------------------------------------------------------------------------
|
|
195
220
|
|
|
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
221
|
/** Derive a single field from a shape entry. */
|
|
202
222
|
const deriveField = (
|
|
203
223
|
key: string,
|
|
204
224
|
value: ZodInternals,
|
|
205
225
|
overrides?: Record<string, FieldOverride>
|
|
206
|
-
): Field => {
|
|
226
|
+
): Field | null => {
|
|
207
227
|
const { inner, required, defaultValue, description } = unwrap(value);
|
|
208
|
-
const
|
|
228
|
+
const derived = deriveFieldType(inner);
|
|
229
|
+
if (!derived) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const { type, options: rawOptions } = derived;
|
|
209
233
|
const override = overrides?.[key];
|
|
210
234
|
const label = override?.label ?? description ?? humanize(key);
|
|
211
235
|
const options = buildOptions(rawOptions, override?.options);
|
|
212
236
|
return { default: defaultValue, label, name: key, options, required, type };
|
|
213
237
|
};
|
|
214
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Derive a runtime-agnostic Field[] from a Zod object schema.
|
|
241
|
+
*
|
|
242
|
+
* Uses Zod v4's `_zod.def` for introspection. Returns fields sorted by name.
|
|
243
|
+
*/
|
|
215
244
|
export const deriveFields = (
|
|
216
245
|
schema: z.ZodType,
|
|
217
246
|
overrides?: Record<string, FieldOverride>
|
|
@@ -229,5 +258,7 @@ export const deriveFields = (
|
|
|
229
258
|
const fields = Object.entries(shape).map(([key, value]) =>
|
|
230
259
|
deriveField(key, value, overrides)
|
|
231
260
|
);
|
|
232
|
-
return fields
|
|
261
|
+
return fields
|
|
262
|
+
.filter((field): field is Field => field !== null)
|
|
263
|
+
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
233
264
|
};
|
package/src/draft.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { ValidationError } from './errors.js';
|
|
2
|
+
import type { AnySignal } from './event.js';
|
|
3
|
+
import type { AnyProvision } from './provision.js';
|
|
4
|
+
import { Result } from './result.js';
|
|
5
|
+
import type { Topo } from './topo.js';
|
|
6
|
+
import type { AnyTrail } from './trail.js';
|
|
7
|
+
|
|
8
|
+
export const DRAFT_ID_PREFIX = '_draft.';
|
|
9
|
+
|
|
10
|
+
export type DraftDependencyKind =
|
|
11
|
+
| 'cross'
|
|
12
|
+
| 'detour'
|
|
13
|
+
| 'provision'
|
|
14
|
+
| 'replaced-by'
|
|
15
|
+
| 'signal-from';
|
|
16
|
+
|
|
17
|
+
export interface DraftDependency {
|
|
18
|
+
readonly fromId: string;
|
|
19
|
+
readonly kind: DraftDependencyKind;
|
|
20
|
+
readonly toId: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DraftFinding {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly kind: 'provision' | 'signal' | 'trail' | 'unknown';
|
|
26
|
+
readonly message: string;
|
|
27
|
+
readonly rule: 'draft-contamination' | 'draft-id';
|
|
28
|
+
readonly via?: DraftDependencyKind | undefined;
|
|
29
|
+
readonly dependsOn?: string | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DraftStateAnalysis {
|
|
33
|
+
readonly contaminatedIds: ReadonlySet<string>;
|
|
34
|
+
readonly declaredDraftIds: ReadonlySet<string>;
|
|
35
|
+
readonly dependencies: readonly DraftDependency[];
|
|
36
|
+
readonly findings: readonly DraftFinding[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface DraftReason {
|
|
40
|
+
readonly dependsOn: string;
|
|
41
|
+
readonly via: DraftDependencyKind;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type TopoNode = AnyProvision | AnySignal | AnyTrail;
|
|
45
|
+
|
|
46
|
+
const replacedByTarget = (value: TopoNode): string | undefined => {
|
|
47
|
+
const raw = value as unknown as { replacedBy?: unknown };
|
|
48
|
+
return typeof raw.replacedBy === 'string' ? raw.replacedBy : undefined;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const isDraftId = (id: string): boolean =>
|
|
52
|
+
id.startsWith(DRAFT_ID_PREFIX);
|
|
53
|
+
|
|
54
|
+
const dependenciesFromIds = (
|
|
55
|
+
fromId: string,
|
|
56
|
+
toIds: readonly string[],
|
|
57
|
+
kind: DraftDependencyKind
|
|
58
|
+
): DraftDependency[] =>
|
|
59
|
+
toIds.map((toId) => ({
|
|
60
|
+
fromId,
|
|
61
|
+
kind,
|
|
62
|
+
toId,
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
const dependencyFromTarget = (
|
|
66
|
+
fromId: string,
|
|
67
|
+
toId: string | undefined,
|
|
68
|
+
kind: DraftDependencyKind
|
|
69
|
+
): DraftDependency[] =>
|
|
70
|
+
toId === undefined
|
|
71
|
+
? []
|
|
72
|
+
: [
|
|
73
|
+
{
|
|
74
|
+
fromId,
|
|
75
|
+
kind,
|
|
76
|
+
toId,
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const dependenciesFromDetours = (
|
|
81
|
+
fromId: string,
|
|
82
|
+
detours: Record<string, string[]>
|
|
83
|
+
): DraftDependency[] =>
|
|
84
|
+
Object.values(detours).flatMap((targets) =>
|
|
85
|
+
dependenciesFromIds(fromId, targets, 'detour')
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const trailDependencies = (trail: AnyTrail): DraftDependency[] => {
|
|
89
|
+
const { detours } = trail as unknown as {
|
|
90
|
+
detours?: Record<string, string[]>;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return [
|
|
94
|
+
...dependenciesFromIds(trail.id, trail.crosses, 'cross'),
|
|
95
|
+
...dependenciesFromIds(
|
|
96
|
+
trail.id,
|
|
97
|
+
trail.provisions.map(({ id }) => id),
|
|
98
|
+
'provision'
|
|
99
|
+
),
|
|
100
|
+
...(detours === undefined
|
|
101
|
+
? []
|
|
102
|
+
: dependenciesFromDetours(trail.id, detours)),
|
|
103
|
+
...dependencyFromTarget(trail.id, replacedByTarget(trail), 'replaced-by'),
|
|
104
|
+
];
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const signalDependencies = (signal: AnySignal): DraftDependency[] => [
|
|
108
|
+
...dependenciesFromIds(signal.id, signal.from ?? [], 'signal-from'),
|
|
109
|
+
...dependencyFromTarget(signal.id, replacedByTarget(signal), 'replaced-by'),
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const provisionDependencies = (provision: AnyProvision): DraftDependency[] =>
|
|
113
|
+
dependencyFromTarget(
|
|
114
|
+
provision.id,
|
|
115
|
+
replacedByTarget(provision),
|
|
116
|
+
'replaced-by'
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const nodeKind = (
|
|
120
|
+
id: string,
|
|
121
|
+
trails: ReadonlyMap<string, AnyTrail>,
|
|
122
|
+
signals: ReadonlyMap<string, AnySignal>,
|
|
123
|
+
provisions: ReadonlyMap<string, AnyProvision>
|
|
124
|
+
): DraftFinding['kind'] => {
|
|
125
|
+
if (trails.has(id)) {
|
|
126
|
+
return 'trail';
|
|
127
|
+
}
|
|
128
|
+
if (signals.has(id)) {
|
|
129
|
+
return 'signal';
|
|
130
|
+
}
|
|
131
|
+
if (provisions.has(id)) {
|
|
132
|
+
return 'provision';
|
|
133
|
+
}
|
|
134
|
+
return 'unknown';
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const displayKind = (kind: DraftFinding['kind']): string =>
|
|
138
|
+
kind === 'unknown' ? 'Node' : kind[0]?.toUpperCase() + kind.slice(1);
|
|
139
|
+
|
|
140
|
+
const draftIdsFromKeys = (keys: Iterable<string>): string[] =>
|
|
141
|
+
[...keys].filter(isDraftId);
|
|
142
|
+
|
|
143
|
+
const collectDeclaredDraftIds = (topo: Topo): ReadonlySet<string> =>
|
|
144
|
+
new Set([
|
|
145
|
+
...draftIdsFromKeys(topo.trails.keys()),
|
|
146
|
+
...draftIdsFromKeys(topo.signals.keys()),
|
|
147
|
+
...draftIdsFromKeys(topo.provisions.keys()),
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
const findingForDraftId = (
|
|
151
|
+
id: string,
|
|
152
|
+
kind: DraftFinding['kind']
|
|
153
|
+
): DraftFinding => ({
|
|
154
|
+
id,
|
|
155
|
+
kind,
|
|
156
|
+
message: `${displayKind(id ? kind : 'unknown')} "${id}" is draft and cannot appear in the established graph.`,
|
|
157
|
+
rule: 'draft-id',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const findingForContamination = (
|
|
161
|
+
id: string,
|
|
162
|
+
kind: DraftFinding['kind'],
|
|
163
|
+
reason: DraftReason
|
|
164
|
+
): DraftFinding => {
|
|
165
|
+
const dependencyLabel = isDraftId(reason.dependsOn)
|
|
166
|
+
? `draft "${reason.dependsOn}"`
|
|
167
|
+
: `draft-contaminated "${reason.dependsOn}"`;
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
dependsOn: reason.dependsOn,
|
|
171
|
+
id,
|
|
172
|
+
kind,
|
|
173
|
+
message:
|
|
174
|
+
`Established ${kind} "${id}" depends on ${dependencyLabel} ` +
|
|
175
|
+
`via ${reason.via} and cannot appear in the established graph.`,
|
|
176
|
+
rule: 'draft-contamination',
|
|
177
|
+
via: reason.via,
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const contaminationReason = (
|
|
182
|
+
dependency: DraftDependency,
|
|
183
|
+
contaminatedIds: ReadonlySet<string>
|
|
184
|
+
): DraftReason | undefined => {
|
|
185
|
+
if (contaminatedIds.has(dependency.fromId)) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!isDraftId(dependency.toId) && !contaminatedIds.has(dependency.toId)) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
dependsOn: dependency.toId,
|
|
195
|
+
via: dependency.kind,
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const markContaminatedDependency = (
|
|
200
|
+
dependency: DraftDependency,
|
|
201
|
+
contaminatedIds: Set<string>,
|
|
202
|
+
reasons: Map<string, DraftReason>
|
|
203
|
+
): boolean => {
|
|
204
|
+
const reason = contaminationReason(dependency, contaminatedIds);
|
|
205
|
+
|
|
206
|
+
if (reason === undefined) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
contaminatedIds.add(dependency.fromId);
|
|
211
|
+
reasons.set(dependency.fromId, reason);
|
|
212
|
+
return true;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const propagateContaminatedIds = (
|
|
216
|
+
declaredDraftIds: ReadonlySet<string>,
|
|
217
|
+
dependencies: readonly DraftDependency[]
|
|
218
|
+
): { contaminatedIds: Set<string>; reasons: Map<string, DraftReason> } => {
|
|
219
|
+
const contaminatedIds = new Set<string>(declaredDraftIds);
|
|
220
|
+
const reasons = new Map<string, DraftReason>();
|
|
221
|
+
|
|
222
|
+
const visit = (): void => {
|
|
223
|
+
if (
|
|
224
|
+
dependencies.some((dependency) =>
|
|
225
|
+
markContaminatedDependency(dependency, contaminatedIds, reasons)
|
|
226
|
+
)
|
|
227
|
+
) {
|
|
228
|
+
visit();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
visit();
|
|
233
|
+
return { contaminatedIds, reasons };
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const contaminationFindingForId = (
|
|
237
|
+
id: string,
|
|
238
|
+
declaredDraftIds: ReadonlySet<string>,
|
|
239
|
+
reasons: ReadonlyMap<string, DraftReason>,
|
|
240
|
+
trails: ReadonlyMap<string, AnyTrail>,
|
|
241
|
+
signals: ReadonlyMap<string, AnySignal>,
|
|
242
|
+
provisions: ReadonlyMap<string, AnyProvision>
|
|
243
|
+
): DraftFinding | undefined => {
|
|
244
|
+
if (declaredDraftIds.has(id)) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const reason = reasons.get(id);
|
|
249
|
+
if (reason === undefined) {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return findingForContamination(
|
|
254
|
+
id,
|
|
255
|
+
nodeKind(id, trails, signals, provisions),
|
|
256
|
+
reason
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const collectFindings = (
|
|
261
|
+
declaredDraftIds: ReadonlySet<string>,
|
|
262
|
+
contaminatedIds: ReadonlySet<string>,
|
|
263
|
+
reasons: ReadonlyMap<string, DraftReason>,
|
|
264
|
+
trails: ReadonlyMap<string, AnyTrail>,
|
|
265
|
+
signals: ReadonlyMap<string, AnySignal>,
|
|
266
|
+
provisions: ReadonlyMap<string, AnyProvision>
|
|
267
|
+
): DraftFinding[] => [
|
|
268
|
+
...[...declaredDraftIds]
|
|
269
|
+
.toSorted()
|
|
270
|
+
.map((id) =>
|
|
271
|
+
findingForDraftId(id, nodeKind(id, trails, signals, provisions))
|
|
272
|
+
),
|
|
273
|
+
...[...contaminatedIds].toSorted().flatMap((id) => {
|
|
274
|
+
const finding = contaminationFindingForId(
|
|
275
|
+
id,
|
|
276
|
+
declaredDraftIds,
|
|
277
|
+
reasons,
|
|
278
|
+
trails,
|
|
279
|
+
signals,
|
|
280
|
+
provisions
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
return finding === undefined ? [] : [finding];
|
|
284
|
+
}),
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
const collectDependencies = (topo: Topo): DraftDependency[] => [
|
|
288
|
+
...[...topo.trails.values()].flatMap(trailDependencies),
|
|
289
|
+
...[...topo.signals.values()].flatMap(signalDependencies),
|
|
290
|
+
...[...topo.provisions.values()].flatMap(provisionDependencies),
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
export const analyzeDraftState = (topo: Topo): DraftStateAnalysis => {
|
|
294
|
+
const declaredDraftIds = collectDeclaredDraftIds(topo);
|
|
295
|
+
const dependencies = collectDependencies(topo);
|
|
296
|
+
const { contaminatedIds, reasons } = propagateContaminatedIds(
|
|
297
|
+
declaredDraftIds,
|
|
298
|
+
dependencies
|
|
299
|
+
);
|
|
300
|
+
const findings = collectFindings(
|
|
301
|
+
declaredDraftIds,
|
|
302
|
+
contaminatedIds,
|
|
303
|
+
reasons,
|
|
304
|
+
topo.trails,
|
|
305
|
+
topo.signals,
|
|
306
|
+
topo.provisions
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
contaminatedIds,
|
|
311
|
+
declaredDraftIds,
|
|
312
|
+
dependencies,
|
|
313
|
+
findings,
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
export const validateEstablishedTopo = (
|
|
318
|
+
topo: Topo
|
|
319
|
+
): Result<void, ValidationError> => {
|
|
320
|
+
const analysis = analyzeDraftState(topo);
|
|
321
|
+
|
|
322
|
+
if (analysis.findings.length === 0) {
|
|
323
|
+
return Result.ok();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return Result.err(
|
|
327
|
+
new ValidationError(
|
|
328
|
+
`Established topo validation failed with ${analysis.findings.length} draft issue(s)`,
|
|
329
|
+
{
|
|
330
|
+
context: { issues: analysis.findings },
|
|
331
|
+
}
|
|
332
|
+
)
|
|
333
|
+
);
|
|
334
|
+
};
|
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';
|