@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.
Files changed (139) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +65 -35
  3. package/README.md +21 -20
  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 +14 -3
  9. package/dist/derive.d.ts.map +1 -1
  10. package/dist/derive.js +36 -12
  11. package/dist/derive.js.map +1 -1
  12. package/dist/draft.d.ts +28 -0
  13. package/dist/draft.d.ts.map +1 -0
  14. package/dist/draft.js +156 -0
  15. package/dist/draft.js.map +1 -0
  16. package/dist/event.d.ts +4 -41
  17. package/dist/event.d.ts.map +1 -1
  18. package/dist/event.js +4 -14
  19. package/dist/event.js.map +1 -1
  20. package/dist/execute.d.ts +10 -10
  21. package/dist/execute.d.ts.map +1 -1
  22. package/dist/execute.js +35 -28
  23. package/dist/execute.js.map +1 -1
  24. package/dist/gate.d.ts +17 -0
  25. package/dist/gate.d.ts.map +1 -0
  26. package/dist/gate.js +21 -0
  27. package/dist/gate.js.map +1 -0
  28. package/dist/index.d.ts +16 -9
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +14 -9
  31. package/dist/index.js.map +1 -1
  32. package/dist/internal/topo-saves.d.ts +47 -0
  33. package/dist/internal/topo-saves.d.ts.map +1 -0
  34. package/dist/internal/topo-saves.js +310 -0
  35. package/dist/internal/topo-saves.js.map +1 -0
  36. package/dist/internal/topo-store-read.d.ts +67 -0
  37. package/dist/internal/topo-store-read.d.ts.map +1 -0
  38. package/dist/internal/topo-store-read.js +222 -0
  39. package/dist/internal/topo-store-read.js.map +1 -0
  40. package/dist/internal/topo-store.d.ts +12 -0
  41. package/dist/internal/topo-store.d.ts.map +1 -0
  42. package/dist/internal/topo-store.js +571 -0
  43. package/dist/internal/topo-store.js.map +1 -0
  44. package/dist/internal/trails-db.d.ts +16 -0
  45. package/dist/internal/trails-db.d.ts.map +1 -0
  46. package/dist/internal/trails-db.js +118 -0
  47. package/dist/internal/trails-db.js.map +1 -0
  48. package/dist/layer.d.ts +5 -16
  49. package/dist/layer.d.ts.map +1 -1
  50. package/dist/layer.js +3 -20
  51. package/dist/layer.js.map +1 -1
  52. package/dist/provision-config.d.ts +22 -0
  53. package/dist/provision-config.d.ts.map +1 -0
  54. package/dist/provision-config.js +210 -0
  55. package/dist/provision-config.js.map +1 -0
  56. package/dist/provision.d.ts +71 -0
  57. package/dist/provision.d.ts.map +1 -0
  58. package/dist/provision.js +56 -0
  59. package/dist/provision.js.map +1 -0
  60. package/dist/run.d.ts +27 -0
  61. package/dist/run.d.ts.map +1 -0
  62. package/dist/run.js +34 -0
  63. package/dist/run.js.map +1 -0
  64. package/dist/service-config.d.ts +7 -7
  65. package/dist/service-config.d.ts.map +1 -1
  66. package/dist/service-config.js +75 -73
  67. package/dist/service-config.js.map +1 -1
  68. package/dist/service.d.ts +32 -36
  69. package/dist/service.d.ts.map +1 -1
  70. package/dist/service.js +21 -21
  71. package/dist/service.js.map +1 -1
  72. package/dist/signal.d.ts +33 -0
  73. package/dist/signal.d.ts.map +1 -0
  74. package/dist/signal.js +17 -0
  75. package/dist/signal.js.map +1 -0
  76. package/dist/topo-store.d.ts +48 -0
  77. package/dist/topo-store.d.ts.map +1 -0
  78. package/dist/topo-store.js +175 -0
  79. package/dist/topo-store.js.map +1 -0
  80. package/dist/topo.d.ts +10 -10
  81. package/dist/topo.d.ts.map +1 -1
  82. package/dist/topo.js +48 -35
  83. package/dist/topo.js.map +1 -1
  84. package/dist/trail.d.ts +16 -16
  85. package/dist/trail.d.ts.map +1 -1
  86. package/dist/trail.js +5 -4
  87. package/dist/trail.js.map +1 -1
  88. package/dist/types.d.ts +10 -10
  89. package/dist/types.d.ts.map +1 -1
  90. package/dist/types.js +2 -2
  91. package/dist/types.js.map +1 -1
  92. package/dist/validate-established-topo.d.ts +76 -0
  93. package/dist/validate-established-topo.d.ts.map +1 -0
  94. package/dist/validate-established-topo.js +43 -0
  95. package/dist/validate-established-topo.js.map +1 -0
  96. package/dist/validate-topo.d.ts +2 -2
  97. package/dist/validate-topo.d.ts.map +1 -1
  98. package/dist/validate-topo.js +34 -32
  99. package/dist/validate-topo.js.map +1 -1
  100. package/package.json +4 -1
  101. package/src/__tests__/context.test.ts +8 -8
  102. package/src/__tests__/derive.test.ts +58 -1
  103. package/src/__tests__/execute.test.ts +124 -122
  104. package/src/__tests__/{layer.test.ts → gate.test.ts} +26 -26
  105. package/src/__tests__/{dispatch.test.ts → run.test.ts} +39 -39
  106. package/src/__tests__/service-config.test.ts +41 -37
  107. package/src/__tests__/service.test.ts +71 -71
  108. package/src/__tests__/{event.test.ts → signal.test.ts} +15 -15
  109. package/src/__tests__/topo-store-read.test.ts +251 -0
  110. package/src/__tests__/topo-store.test.ts +469 -0
  111. package/src/__tests__/topo.test.ts +54 -54
  112. package/src/__tests__/trail-permit.test.ts +4 -4
  113. package/src/__tests__/trail.test.ts +58 -58
  114. package/src/__tests__/trails-db.test.ts +191 -0
  115. package/src/__tests__/type-utils.test.ts +3 -3
  116. package/src/__tests__/validate-topo.test.ts +205 -38
  117. package/src/context.ts +5 -4
  118. package/src/derive.ts +47 -16
  119. package/src/draft.ts +334 -0
  120. package/src/event.ts +11 -73
  121. package/src/execute.ts +56 -40
  122. package/src/{layer.ts → gate.ts} +13 -13
  123. package/src/index.ts +54 -23
  124. package/src/internal/topo-saves.ts +429 -0
  125. package/src/internal/topo-store-read.ts +473 -0
  126. package/src/internal/topo-store.ts +1087 -0
  127. package/src/internal/trails-db.ts +189 -0
  128. package/src/{service-config.ts → provision-config.ts} +124 -105
  129. package/src/provision.ts +148 -0
  130. package/src/{dispatch.ts → run.ts} +8 -8
  131. package/src/signal.ts +65 -0
  132. package/src/topo-store.ts +301 -0
  133. package/src/topo.ts +74 -52
  134. package/src/trail.ts +23 -22
  135. package/src/types.ts +11 -11
  136. package/src/validate-established-topo.ts +63 -0
  137. package/src/validate-topo.ts +38 -34
  138. package/tsconfig.tsbuildinfo +1 -1
  139. 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 surface-agnostic Field[] descriptor
5
- * that UI layers (CLI prompts, web forms, etc.) can consume.
4
+ * Introspects Zod v4 schemas to produce a runtime-agnostic Field[] descriptor
5
+ * that UI consumers (CLI prompts, web forms, etc.) can consume.
6
6
  */
7
7
 
8
8
  import type { z } from 'zod';
9
9
 
10
+ import { ValidationError } from './errors.js';
11
+
10
12
  // ---------------------------------------------------------------------------
11
13
  // Public types
12
14
  // ---------------------------------------------------------------------------
13
15
 
14
- /** A surface-agnostic field descriptor derived from a Zod schema. */
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<string, (s: ZodInternals) => DerivedFieldType> = {
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 elementType = element._zod.def['type'] as string;
151
+ const { inner } = unwrap(element);
152
+ const elementType = inner._zod.def['type'] as string;
147
153
  if (elementType === 'enum') {
148
- const entries = element._zod.def['entries'] as Record<string, string>;
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) : { options: undefined, type: 'string' };
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 { type, options: rawOptions } = deriveFieldType(inner);
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.toSorted((a, b) => a.name.localeCompare(b.name));
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
- * Event — a named payload schema with optional provenance metadata.
3
- */
4
-
5
- import type { z } from 'zod';
6
-
7
- // ---------------------------------------------------------------------------
8
- // Spec (input to the factory)
9
- // ---------------------------------------------------------------------------
10
-
11
- export interface EventSpec<T> {
12
- readonly payload: z.ZodType<T>;
13
- readonly description?: string | undefined;
14
- readonly metadata?: Readonly<Record<string, unknown>> | undefined;
15
- /** Trail IDs that produce this event (e.g. the trails it originates from). */
16
- readonly from?: readonly string[] | undefined;
17
- }
18
-
19
- // ---------------------------------------------------------------------------
20
- // Shape (output of the factory)
21
- // ---------------------------------------------------------------------------
22
-
23
- export interface Event<T> {
24
- readonly id: string;
25
- readonly kind: 'event';
26
- readonly payload: z.ZodType<T>;
27
- readonly description?: string | undefined;
28
- readonly metadata?: Readonly<Record<string, unknown>> | undefined;
29
- /** Trail IDs that produce this event (e.g. the trails it originates from). */
30
- readonly from?: readonly string[] | undefined;
31
- }
32
-
33
- // ---------------------------------------------------------------------------
34
- // Factory
35
- // ---------------------------------------------------------------------------
36
-
37
- /**
38
- * Create an event definition.
39
- *
40
- * An event is a named payload schema describing something that happened.
41
- * Returns a frozen object with `kind: "event"` and all spec fields.
42
- *
43
- * @example
44
- * ```typescript
45
- * // ID as first argument
46
- * const updated = event("entity.updated", {
47
- * payload: EntityUpdatedSchema,
48
- * from: ["entity.add", "entity.update"],
49
- * });
2
+ * Legacy event aliases for the signal model.
50
3
  *
51
- * // Full spec object (programmatic)
52
- * const updated = event({ id: "entity.updated", payload: ..., from: [...] });
53
- * ```
4
+ * Keep this file as a compatibility seam while the repo-wide cutover lands.
54
5
  */
55
- export function event<T>(id: string, spec: EventSpec<T>): Event<T>;
56
- export function event<T>(
57
- spec: EventSpec<T> & { readonly id: string }
58
- ): Event<T>;
59
- export function event<T>(
60
- idOrSpec: string | (EventSpec<T> & { readonly id: string }),
61
- maybeSpec?: EventSpec<T>
62
- ): Event<T> {
63
- const resolvedId = typeof idOrSpec === 'string' ? idOrSpec : idOrSpec.id;
64
- // oxlint-disable-next-line no-non-null-assertion -- overload guarantees maybeSpec when idOrSpec is string
65
- const resolvedSpec = typeof idOrSpec === 'string' ? maybeSpec! : idOrSpec;
66
- return Object.freeze({
67
- description: resolvedSpec.description,
68
- from: resolvedSpec.from ? Object.freeze([...resolvedSpec.from]) : undefined,
69
- id: resolvedId,
70
- kind: 'event' as const,
71
- metadata: resolvedSpec.metadata,
72
- payload: resolvedSpec.payload,
73
- });
74
- }
75
6
 
76
- /** Existential type for heterogeneous event collections */
77
- export type AnyEvent = Event<unknown>;
7
+ export { signal as event } from './signal.js';
8
+ export type {
9
+ AnySignal,
10
+ AnySignal as AnyEvent,
11
+ Signal,
12
+ Signal as Event,
13
+ SignalSpec,
14
+ SignalSpec as EventSpec,
15
+ } from './signal.js';