@ontrails/core 1.0.0-beta.3 → 1.0.0-beta.32

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 (249) hide show
  1. package/CHANGELOG.md +323 -8
  2. package/README.md +114 -33
  3. package/package.json +11 -1
  4. package/src/activation-provenance.ts +111 -0
  5. package/src/activation-source-compatibility.ts +430 -0
  6. package/src/activation-source-projection.ts +221 -0
  7. package/src/activation-source.ts +91 -0
  8. package/src/blob-ref.ts +51 -0
  9. package/src/branded.ts +1 -1
  10. package/src/compose-batch.ts +69 -0
  11. package/src/compose-schema.ts +36 -0
  12. package/src/context.ts +44 -10
  13. package/src/contour.ts +346 -0
  14. package/src/derive.ts +250 -19
  15. package/src/detours.ts +8 -0
  16. package/src/diagnostics.ts +21 -0
  17. package/src/draft.ts +350 -0
  18. package/src/error-projection.ts +51 -0
  19. package/src/errors.ts +335 -48
  20. package/src/execute.ts +1577 -0
  21. package/src/fire.ts +1169 -0
  22. package/src/glob.ts +81 -0
  23. package/src/index.ts +526 -35
  24. package/src/internal/fork-ctx.ts +69 -0
  25. package/src/layer-projection.ts +193 -0
  26. package/src/layer.ts +43 -6
  27. package/src/observe.ts +361 -0
  28. package/src/path-scope.ts +66 -0
  29. package/src/path-security.ts +1 -1
  30. package/src/permits.ts +12 -0
  31. package/src/redaction/patterns.ts +6 -3
  32. package/src/resilience.ts +11 -11
  33. package/src/resource-config.ts +792 -0
  34. package/src/resource.ts +194 -0
  35. package/src/result.ts +18 -0
  36. package/src/run.ts +76 -0
  37. package/src/schedule-runtime.ts +689 -0
  38. package/src/schedule.ts +326 -0
  39. package/src/serialization.ts +92 -55
  40. package/src/signal-diagnostics.ts +633 -0
  41. package/src/signal-ref.ts +87 -0
  42. package/src/signal.ts +104 -0
  43. package/src/store/accessor-protocol.ts +56 -0
  44. package/src/store/index.ts +4 -0
  45. package/src/structured-examples.ts +248 -0
  46. package/src/surface-derivation.ts +91 -0
  47. package/src/surface-filter.ts +101 -0
  48. package/src/surface-versioning.ts +42 -0
  49. package/src/topo.ts +769 -67
  50. package/src/tracing.ts +345 -0
  51. package/src/trail-id-glob.ts +15 -0
  52. package/src/trail.ts +1168 -30
  53. package/src/trails/derive-trail.ts +842 -0
  54. package/src/trails/index.ts +9 -0
  55. package/src/trails/ingest.ts +139 -0
  56. package/src/trails-db.ts +206 -0
  57. package/src/transport-error-map.ts +174 -0
  58. package/src/type-utils.ts +87 -0
  59. package/src/types.ts +254 -12
  60. package/src/validate-established-topo.ts +69 -0
  61. package/src/validate-topo.ts +414 -56
  62. package/src/validation.ts +152 -4
  63. package/src/version-marker.ts +716 -0
  64. package/src/version-resolution.ts +308 -0
  65. package/src/version-runtime.ts +120 -0
  66. package/src/webhook.ts +278 -0
  67. package/src/workspace.ts +162 -1
  68. package/src/zod-wrappers.ts +72 -0
  69. package/.turbo/turbo-build.log +0 -1
  70. package/.turbo/turbo-lint.log +0 -3
  71. package/.turbo/turbo-typecheck.log +0 -1
  72. package/dist/adapters.d.ts +0 -39
  73. package/dist/adapters.d.ts.map +0 -1
  74. package/dist/adapters.js +0 -2
  75. package/dist/adapters.js.map +0 -1
  76. package/dist/blob-ref.d.ts +0 -20
  77. package/dist/blob-ref.d.ts.map +0 -1
  78. package/dist/blob-ref.js +0 -22
  79. package/dist/blob-ref.js.map +0 -1
  80. package/dist/branded.d.ts +0 -36
  81. package/dist/branded.d.ts.map +0 -1
  82. package/dist/branded.js +0 -89
  83. package/dist/branded.js.map +0 -1
  84. package/dist/collections.d.ts +0 -31
  85. package/dist/collections.d.ts.map +0 -1
  86. package/dist/collections.js +0 -60
  87. package/dist/collections.js.map +0 -1
  88. package/dist/context.d.ts +0 -10
  89. package/dist/context.d.ts.map +0 -1
  90. package/dist/context.js +0 -15
  91. package/dist/context.js.map +0 -1
  92. package/dist/derive.d.ts +0 -33
  93. package/dist/derive.d.ts.map +0 -1
  94. package/dist/derive.js +0 -125
  95. package/dist/derive.js.map +0 -1
  96. package/dist/errors.d.ts +0 -83
  97. package/dist/errors.d.ts.map +0 -1
  98. package/dist/errors.js +0 -142
  99. package/dist/errors.js.map +0 -1
  100. package/dist/event.d.ts +0 -45
  101. package/dist/event.d.ts.map +0 -1
  102. package/dist/event.js +0 -17
  103. package/dist/event.js.map +0 -1
  104. package/dist/fetch.d.ts +0 -15
  105. package/dist/fetch.d.ts.map +0 -1
  106. package/dist/fetch.js +0 -102
  107. package/dist/fetch.js.map +0 -1
  108. package/dist/guards.d.ts +0 -17
  109. package/dist/guards.d.ts.map +0 -1
  110. package/dist/guards.js +0 -25
  111. package/dist/guards.js.map +0 -1
  112. package/dist/health.d.ts +0 -18
  113. package/dist/health.d.ts.map +0 -1
  114. package/dist/health.js +0 -5
  115. package/dist/health.js.map +0 -1
  116. package/dist/hike.d.ts +0 -36
  117. package/dist/hike.d.ts.map +0 -1
  118. package/dist/hike.js +0 -20
  119. package/dist/hike.js.map +0 -1
  120. package/dist/index.d.ts +0 -36
  121. package/dist/index.d.ts.map +0 -1
  122. package/dist/index.js +0 -40
  123. package/dist/index.js.map +0 -1
  124. package/dist/job.d.ts +0 -24
  125. package/dist/job.d.ts.map +0 -1
  126. package/dist/job.js +0 -17
  127. package/dist/job.js.map +0 -1
  128. package/dist/layer.d.ts +0 -17
  129. package/dist/layer.d.ts.map +0 -1
  130. package/dist/layer.js +0 -21
  131. package/dist/layer.js.map +0 -1
  132. package/dist/path-security.d.ts +0 -28
  133. package/dist/path-security.d.ts.map +0 -1
  134. package/dist/path-security.js +0 -63
  135. package/dist/path-security.js.map +0 -1
  136. package/dist/patterns/bulk.d.ts +0 -15
  137. package/dist/patterns/bulk.d.ts.map +0 -1
  138. package/dist/patterns/bulk.js +0 -14
  139. package/dist/patterns/bulk.js.map +0 -1
  140. package/dist/patterns/change.d.ts +0 -10
  141. package/dist/patterns/change.d.ts.map +0 -1
  142. package/dist/patterns/change.js +0 -10
  143. package/dist/patterns/change.js.map +0 -1
  144. package/dist/patterns/date-range.d.ts +0 -10
  145. package/dist/patterns/date-range.d.ts.map +0 -1
  146. package/dist/patterns/date-range.js +0 -10
  147. package/dist/patterns/date-range.js.map +0 -1
  148. package/dist/patterns/index.d.ts +0 -9
  149. package/dist/patterns/index.d.ts.map +0 -1
  150. package/dist/patterns/index.js +0 -9
  151. package/dist/patterns/index.js.map +0 -1
  152. package/dist/patterns/pagination.d.ts +0 -18
  153. package/dist/patterns/pagination.d.ts.map +0 -1
  154. package/dist/patterns/pagination.js +0 -18
  155. package/dist/patterns/pagination.js.map +0 -1
  156. package/dist/patterns/progress.d.ts +0 -11
  157. package/dist/patterns/progress.d.ts.map +0 -1
  158. package/dist/patterns/progress.js +0 -11
  159. package/dist/patterns/progress.js.map +0 -1
  160. package/dist/patterns/sorting.d.ts +0 -13
  161. package/dist/patterns/sorting.d.ts.map +0 -1
  162. package/dist/patterns/sorting.js +0 -10
  163. package/dist/patterns/sorting.js.map +0 -1
  164. package/dist/patterns/status.d.ts +0 -15
  165. package/dist/patterns/status.d.ts.map +0 -1
  166. package/dist/patterns/status.js +0 -9
  167. package/dist/patterns/status.js.map +0 -1
  168. package/dist/patterns/timestamps.d.ts +0 -10
  169. package/dist/patterns/timestamps.d.ts.map +0 -1
  170. package/dist/patterns/timestamps.js +0 -10
  171. package/dist/patterns/timestamps.js.map +0 -1
  172. package/dist/redaction/index.d.ts +0 -4
  173. package/dist/redaction/index.d.ts.map +0 -1
  174. package/dist/redaction/index.js +0 -3
  175. package/dist/redaction/index.js.map +0 -1
  176. package/dist/redaction/patterns.d.ts +0 -9
  177. package/dist/redaction/patterns.d.ts.map +0 -1
  178. package/dist/redaction/patterns.js +0 -39
  179. package/dist/redaction/patterns.js.map +0 -1
  180. package/dist/redaction/redactor.d.ts +0 -27
  181. package/dist/redaction/redactor.d.ts.map +0 -1
  182. package/dist/redaction/redactor.js +0 -89
  183. package/dist/redaction/redactor.js.map +0 -1
  184. package/dist/resilience.d.ts +0 -34
  185. package/dist/resilience.d.ts.map +0 -1
  186. package/dist/resilience.js +0 -164
  187. package/dist/resilience.js.map +0 -1
  188. package/dist/result.d.ts +0 -57
  189. package/dist/result.d.ts.map +0 -1
  190. package/dist/result.js +0 -156
  191. package/dist/result.js.map +0 -1
  192. package/dist/serialization.d.ts +0 -27
  193. package/dist/serialization.d.ts.map +0 -1
  194. package/dist/serialization.js +0 -153
  195. package/dist/serialization.js.map +0 -1
  196. package/dist/topo.d.ts +0 -18
  197. package/dist/topo.d.ts.map +0 -1
  198. package/dist/topo.js +0 -80
  199. package/dist/topo.js.map +0 -1
  200. package/dist/trail.d.ts +0 -83
  201. package/dist/trail.d.ts.map +0 -1
  202. package/dist/trail.js +0 -16
  203. package/dist/trail.js.map +0 -1
  204. package/dist/types.d.ts +0 -46
  205. package/dist/types.d.ts.map +0 -1
  206. package/dist/types.js +0 -2
  207. package/dist/types.js.map +0 -1
  208. package/dist/validate-topo.d.ts +0 -24
  209. package/dist/validate-topo.d.ts.map +0 -1
  210. package/dist/validate-topo.js +0 -156
  211. package/dist/validate-topo.js.map +0 -1
  212. package/dist/validation.d.ts +0 -27
  213. package/dist/validation.d.ts.map +0 -1
  214. package/dist/validation.js +0 -134
  215. package/dist/validation.js.map +0 -1
  216. package/dist/workspace.d.ts +0 -25
  217. package/dist/workspace.d.ts.map +0 -1
  218. package/dist/workspace.js +0 -57
  219. package/dist/workspace.js.map +0 -1
  220. package/src/__tests__/blob-ref.test.ts +0 -103
  221. package/src/__tests__/branded.test.ts +0 -148
  222. package/src/__tests__/collections.test.ts +0 -126
  223. package/src/__tests__/context.test.ts +0 -66
  224. package/src/__tests__/derive.test.ts +0 -203
  225. package/src/__tests__/errors.test.ts +0 -309
  226. package/src/__tests__/event.test.ts +0 -82
  227. package/src/__tests__/fetch.test.ts +0 -217
  228. package/src/__tests__/guards.test.ts +0 -102
  229. package/src/__tests__/hike.test.ts +0 -117
  230. package/src/__tests__/job.test.ts +0 -98
  231. package/src/__tests__/layer.test.ts +0 -224
  232. package/src/__tests__/path-security.test.ts +0 -114
  233. package/src/__tests__/patterns.test.ts +0 -273
  234. package/src/__tests__/redaction.test.ts +0 -244
  235. package/src/__tests__/resilience.test.ts +0 -246
  236. package/src/__tests__/result.test.ts +0 -155
  237. package/src/__tests__/serialization.test.ts +0 -401
  238. package/src/__tests__/topo.test.ts +0 -220
  239. package/src/__tests__/trail.test.ts +0 -179
  240. package/src/__tests__/validate-topo.test.ts +0 -280
  241. package/src/__tests__/validation.test.ts +0 -283
  242. package/src/__tests__/workspace.test.ts +0 -183
  243. package/src/adapters.ts +0 -68
  244. package/src/event.ts +0 -77
  245. package/src/health.ts +0 -23
  246. package/src/hike.ts +0 -77
  247. package/src/job.ts +0 -20
  248. package/tsconfig.json +0 -9
  249. package/tsconfig.tsbuildinfo +0 -1
@@ -1,29 +1,59 @@
1
1
  /**
2
2
  * Structural validation for a Topo graph.
3
3
  *
4
- * Checks hike follows references, example input validity, event origin
5
- * references, and output schema completeness. Returns a Result with all
6
- * issues collected into a single ValidationError.
4
+ * Checks trail composing references, example input validity, signal origin
5
+ * references, activation source kinds, and output schema completeness. Returns
6
+ * a Result with all issues collected into a single ValidationError.
7
7
  */
8
8
 
9
+ import type { ActivationSchemaIssue } from './activation-source-compatibility.js';
10
+ import { getActivationSourceInputCompatibilityIssues } from './activation-source-compatibility.js';
11
+ import {
12
+ activationSourceDeclarationSignature,
13
+ activationSourceKey,
14
+ } from './activation-source-projection.js';
15
+ import type { AnyContour } from './contour.js';
16
+ import { getContourReferences } from './contour.js';
9
17
  import { ValidationError } from './errors.js';
10
- import type { AnyEvent } from './event.js';
11
- import type { AnyHike } from './hike.js';
18
+ import type { ActivationEntry } from './activation-source.js';
19
+ import { isKnownActivationSourceKind } from './activation-source.js';
20
+ import { isDraftId } from './draft.js';
21
+ import type { AnySignal } from './signal.js';
22
+ import { validateScheduleSource } from './schedule.js';
12
23
  import { Result } from './result.js';
13
24
  import type { Topo } from './topo.js';
14
- import type { AnyTrail } from './trail.js';
25
+ import type { AnyTrail, TrailVersionForkEntry } from './trail.js';
26
+ import {
27
+ getTrailVersionEntryKind,
28
+ isArchivedTrailVersionEntry,
29
+ } from './trail.js';
15
30
  import { validateInput } from './validation.js';
31
+ import { validateWebhookSource } from './webhook.js';
16
32
 
17
33
  // ---------------------------------------------------------------------------
18
34
  // Issue shape
19
35
  // ---------------------------------------------------------------------------
20
36
 
21
- export interface TopoIssue {
37
+ export interface TopoDiagnostic {
22
38
  readonly trailId: string;
23
39
  readonly rule: string;
24
40
  readonly message: string;
41
+ readonly inputPath?: readonly (string | number)[];
42
+ readonly schemaIssues?: readonly TopoSchemaIssue[];
43
+ readonly sourceId?: string;
44
+ readonly sourceKind?: string;
25
45
  }
26
46
 
47
+ /**
48
+ * @deprecated Use {@link TopoDiagnostic}. Kept as a source-compatible alias
49
+ * during the v1 vocabulary cutover.
50
+ */
51
+ export interface TopoIssue extends TopoDiagnostic {
52
+ readonly trailId: TopoDiagnostic['trailId'];
53
+ }
54
+
55
+ export type TopoSchemaIssue = ActivationSchemaIssue;
56
+
27
57
  // ---------------------------------------------------------------------------
28
58
  // Validators
29
59
  // ---------------------------------------------------------------------------
@@ -32,16 +62,33 @@ const WHITE = 0;
32
62
  const GRAY = 1;
33
63
  const BLACK = 2;
34
64
 
35
- /** Build an adjacency list and initial color map from hikes. */
36
- const buildFollowGraph = (
37
- hikes: ReadonlyMap<string, AnyHike>
65
+ /** Build an adjacency list and initial color map from trails with compositions. */
66
+ const buildComposeGraph = (
67
+ trails: ReadonlyMap<string, AnyTrail>
38
68
  ): {
39
69
  graph: Map<string, readonly string[]>;
40
70
  color: Map<string, number>;
41
71
  } => {
42
72
  const graph = new Map<string, readonly string[]>();
43
- for (const [id, h] of hikes) {
44
- graph.set(id, h.follows);
73
+ for (const [id, trail] of trails) {
74
+ const composedIds = new Set<string>(trail.composes);
75
+ for (const entry of Object.values(trail.versions ?? {})) {
76
+ if (
77
+ isArchivedTrailVersionEntry(entry) ||
78
+ getTrailVersionEntryKind(entry) !== 'fork'
79
+ ) {
80
+ continue;
81
+ }
82
+
83
+ const fork = entry as TrailVersionForkEntry;
84
+ for (const composed of fork.composes ?? []) {
85
+ composedIds.add(typeof composed === 'string' ? composed : composed.id);
86
+ }
87
+ }
88
+
89
+ if (composedIds.size > 0) {
90
+ graph.set(id, [...composedIds]);
91
+ }
45
92
  }
46
93
  const color = new Map<string, number>();
47
94
  for (const id of graph.keys()) {
@@ -50,12 +97,12 @@ const buildFollowGraph = (
50
97
  return { color, graph };
51
98
  };
52
99
 
53
- /** Detect multi-node cycles in the hike follow graph via DFS. */
54
- const detectFollowCycles = (
55
- hikes: ReadonlyMap<string, AnyHike>
56
- ): TopoIssue[] => {
57
- const issues: TopoIssue[] = [];
58
- const { color, graph } = buildFollowGraph(hikes);
100
+ /** Detect multi-node cycles in the trail composing graph via DFS. */
101
+ const detectComposeCycles = (
102
+ trails: ReadonlyMap<string, AnyTrail>
103
+ ): TopoDiagnostic[] => {
104
+ const issues: TopoDiagnostic[] = [];
105
+ const { color, graph } = buildComposeGraph(trails);
59
106
 
60
107
  const dfs = (node: string, path: string[]): void => {
61
108
  color.set(node, GRAY);
@@ -68,7 +115,7 @@ const detectFollowCycles = (
68
115
  const cycle = [...path.slice(path.indexOf(next)), next];
69
116
  issues.push({
70
117
  message: `Cycle detected: ${cycle.join(' → ')}`,
71
- rule: 'follow-cycle',
118
+ rule: 'compose-cycle',
72
119
  trailId: next,
73
120
  });
74
121
  } else if (c === WHITE) {
@@ -86,29 +133,104 @@ const detectFollowCycles = (
86
133
  return issues;
87
134
  };
88
135
 
89
- const checkFollows = (
90
- hikes: ReadonlyMap<string, AnyHike>,
136
+ const checkComposes = (
137
+ trails: ReadonlyMap<string, AnyTrail>,
91
138
  topo: Topo
92
- ): TopoIssue[] => {
93
- const issues: TopoIssue[] = [];
94
- for (const [id, hike] of hikes) {
95
- for (const followId of hike.follows) {
96
- if (followId === id) {
139
+ ): TopoDiagnostic[] => {
140
+ const issues: TopoDiagnostic[] = [];
141
+ for (const [id, trail] of trails) {
142
+ for (const composedId of trail.composes) {
143
+ if (composedId === id) {
97
144
  issues.push({
98
- message: `Hike follows itself`,
99
- rule: 'no-self-follow',
145
+ message: `Trail composes itself`,
146
+ rule: 'no-self-compose',
100
147
  trailId: id,
101
148
  });
102
- } else if (!topo.has(followId)) {
149
+ } else if (!topo.has(composedId) && !isDraftId(composedId)) {
103
150
  issues.push({
104
- message: `Follows "${followId}" which is not in the topo`,
105
- rule: 'follows-exist',
151
+ message: `Composes "${composedId}" which is not in the topo`,
152
+ rule: 'compose-exists',
106
153
  trailId: id,
107
154
  });
108
155
  }
109
156
  }
157
+ for (const [rawVersion, entry] of Object.entries(trail.versions ?? {})) {
158
+ if (
159
+ isArchivedTrailVersionEntry(entry) ||
160
+ getTrailVersionEntryKind(entry) !== 'fork'
161
+ ) {
162
+ continue;
163
+ }
164
+
165
+ const version = Number(rawVersion);
166
+ const fork = entry as TrailVersionForkEntry;
167
+ for (const composed of fork.composes ?? []) {
168
+ const composedId =
169
+ typeof composed === 'string' ? composed : composed.id;
170
+ if (composedId === id) {
171
+ issues.push({
172
+ message: `Trail version ${version} composes itself`,
173
+ rule: 'no-self-compose',
174
+ trailId: id,
175
+ });
176
+ } else if (!topo.has(composedId) && !isDraftId(composedId)) {
177
+ issues.push({
178
+ message: `Version ${version} composes "${composedId}" which is not in the topo`,
179
+ rule: 'compose-exists',
180
+ trailId: id,
181
+ });
182
+ }
183
+ }
184
+ }
110
185
  }
111
- issues.push(...detectFollowCycles(hikes));
186
+ issues.push(...detectComposeCycles(trails));
187
+ return issues;
188
+ };
189
+
190
+ const checkResources = (
191
+ trails: ReadonlyMap<string, AnyTrail>,
192
+ topo: Topo
193
+ ): TopoDiagnostic[] => {
194
+ const issues: TopoDiagnostic[] = [];
195
+
196
+ for (const [id, trail] of trails) {
197
+ for (const declaredResource of trail.resources) {
198
+ if (
199
+ !topo.hasResource(declaredResource.id) &&
200
+ !isDraftId(declaredResource.id)
201
+ ) {
202
+ issues.push({
203
+ message: `Resource "${declaredResource.id}" is not in the topo`,
204
+ rule: 'resource-exists',
205
+ trailId: id,
206
+ });
207
+ }
208
+ }
209
+ for (const [rawVersion, entry] of Object.entries(trail.versions ?? {})) {
210
+ if (
211
+ isArchivedTrailVersionEntry(entry) ||
212
+ getTrailVersionEntryKind(entry) !== 'fork'
213
+ ) {
214
+ continue;
215
+ }
216
+
217
+ const version = Number(rawVersion);
218
+ const fork = entry as TrailVersionForkEntry;
219
+ for (const declaredResource of fork.resources ?? []) {
220
+ if (
221
+ !topo.hasResource(declaredResource.id) &&
222
+ !isDraftId(declaredResource.id)
223
+ ) {
224
+ issues.push({
225
+ message: `Version ${version} resource "${declaredResource.id}" is not in the topo`,
226
+ rule: 'resource-exists',
227
+ trailId: id,
228
+ });
229
+ }
230
+ }
231
+ }
232
+ }
233
+
112
234
  return issues;
113
235
  };
114
236
 
@@ -121,20 +243,21 @@ const checkOneExample = (
121
243
  error?: string | undefined;
122
244
  },
123
245
  inputSchema: { safeParse: (data: unknown) => { success: boolean } },
124
- hasOutput: boolean
125
- ): TopoIssue[] => {
126
- const issues: TopoIssue[] = [];
246
+ hasOutput: boolean,
247
+ label = `Example "${example.name}"`
248
+ ): TopoDiagnostic[] => {
249
+ const issues: TopoDiagnostic[] = [];
127
250
  const result = validateInput(inputSchema as AnyTrail['input'], example.input);
128
251
  if (result.isErr() && example.error !== 'ValidationError') {
129
252
  issues.push({
130
- message: `Example "${example.name}" input does not parse against schema`,
253
+ message: `${label} input does not parse against schema`,
131
254
  rule: 'example-input-valid',
132
255
  trailId: id,
133
256
  });
134
257
  }
135
258
  if (example.expected !== undefined && !hasOutput) {
136
259
  issues.push({
137
- message: `Example "${example.name}" has expected output but trail has no output schema`,
260
+ message: `${label} has expected output but trail has no output schema`,
138
261
  rule: 'output-schema-present',
139
262
  trailId: id,
140
263
  });
@@ -142,38 +265,267 @@ const checkOneExample = (
142
265
  return issues;
143
266
  };
144
267
 
145
- const checkExamples = (trails: ReadonlyMap<string, AnyTrail>): TopoIssue[] => {
146
- const issues: TopoIssue[] = [];
147
- for (const [id, trail] of trails) {
148
- if (!trail.examples) {
149
- continue;
268
+ const checkVersionExamples = (id: string, trail: AnyTrail): TopoDiagnostic[] =>
269
+ Object.entries(trail.versions ?? {}).flatMap(([version, entry]) => {
270
+ if (isArchivedTrailVersionEntry(entry)) {
271
+ return [];
150
272
  }
151
- for (const example of trail.examples) {
152
- issues.push(...checkOneExample(id, example, trail.input, !!trail.output));
273
+
274
+ return (entry.examples ?? []).flatMap((example) =>
275
+ checkOneExample(
276
+ id,
277
+ example,
278
+ entry.input,
279
+ true,
280
+ `Example "${example.name}" on version ${version}`
281
+ )
282
+ );
283
+ });
284
+
285
+ const checkExamples = (
286
+ trails: ReadonlyMap<string, AnyTrail>
287
+ ): TopoDiagnostic[] => {
288
+ const issues: TopoDiagnostic[] = [];
289
+ for (const [id, trail] of trails) {
290
+ if (trail.examples) {
291
+ for (const example of trail.examples) {
292
+ issues.push(
293
+ ...checkOneExample(id, example, trail.input, !!trail.output)
294
+ );
295
+ }
153
296
  }
297
+ issues.push(...checkVersionExamples(id, trail));
154
298
  }
155
299
  return issues;
156
300
  };
157
301
 
158
- const checkEventOrigins = (
159
- events: ReadonlyMap<string, AnyEvent>,
302
+ const checkSignalOrigins = (
303
+ signals: ReadonlyMap<string, AnySignal>,
160
304
  topo: Topo
161
- ): TopoIssue[] => {
162
- const issues: TopoIssue[] = [];
163
- for (const [id, evt] of events) {
305
+ ): TopoDiagnostic[] => {
306
+ const issues: TopoDiagnostic[] = [];
307
+ for (const [id, evt] of signals) {
164
308
  if (!evt.from) {
165
309
  continue;
166
310
  }
167
311
  for (const originId of evt.from) {
168
- if (!topo.has(originId)) {
312
+ if (!topo.has(originId) && !isDraftId(originId)) {
313
+ issues.push({
314
+ message: `Signal origin "${originId}" is not in the topo`,
315
+ rule: 'signal-origin-exists',
316
+ trailId: id,
317
+ });
318
+ }
319
+ }
320
+ }
321
+ return issues;
322
+ };
323
+
324
+ const checkSignalReferences = (
325
+ trails: ReadonlyMap<string, AnyTrail>,
326
+ signals: ReadonlyMap<string, AnySignal>
327
+ ): TopoDiagnostic[] => {
328
+ const issues: TopoDiagnostic[] = [];
329
+
330
+ for (const [id, trail] of trails) {
331
+ for (const signalId of trail.fires ?? []) {
332
+ if (!signals.has(signalId) && !isDraftId(signalId)) {
333
+ issues.push({
334
+ message: `Trail fires signal "${signalId}" which is not in the topo`,
335
+ rule: 'signal-fire-exists',
336
+ trailId: id,
337
+ });
338
+ }
339
+ }
340
+
341
+ for (const signalId of trail.on ?? []) {
342
+ if (!signals.has(signalId) && !isDraftId(signalId)) {
343
+ issues.push({
344
+ message: `Trail declares on signal "${signalId}" which is not in the topo`,
345
+ rule: 'signal-on-exists',
346
+ trailId: id,
347
+ });
348
+ }
349
+ }
350
+ }
351
+
352
+ return issues;
353
+ };
354
+
355
+ const checkActivationSources = (
356
+ trails: ReadonlyMap<string, AnyTrail>
357
+ ): TopoDiagnostic[] => {
358
+ const issues: TopoDiagnostic[] = [];
359
+ const sourceDeclarations = new Map<
360
+ string,
361
+ {
362
+ readonly signature: string;
363
+ readonly trailId: string;
364
+ }
365
+ >();
366
+ const trailSourceEdges = new Set<string>();
367
+
368
+ for (const [id, trail] of trails) {
369
+ for (const activation of trail.activationSources ?? []) {
370
+ if (!isKnownActivationSourceKind(activation.source.kind)) {
169
371
  issues.push({
170
- message: `Event origin "${originId}" is not in the topo`,
171
- rule: 'event-origin-exists',
372
+ message: `Trail declares on source "${activation.source.id}" with unsupported source kind "${activation.source.kind}"`,
373
+ rule: 'activation-source-kind-known',
374
+ trailId: id,
375
+ });
376
+ continue;
377
+ }
378
+
379
+ const sourceKey = activationSourceKey(activation.source);
380
+ const edgeKey = `${id}\0${sourceKey}`;
381
+ if (trailSourceEdges.has(edgeKey)) {
382
+ issues.push({
383
+ message: `Trail declares activation source "${activation.source.id}" (${activation.source.kind}) more than once. Keep one source-to-trail activation edge, or split distinct activation behavior into distinct source ids.`,
384
+ rule: 'activation-source-edge-unique',
385
+ sourceId: activation.source.id,
386
+ sourceKind: activation.source.kind,
387
+ trailId: id,
388
+ });
389
+ } else {
390
+ trailSourceEdges.add(edgeKey);
391
+ }
392
+
393
+ if (!isDraftId(activation.source.id)) {
394
+ const signature = activationSourceDeclarationSignature(
395
+ activation.source
396
+ );
397
+ const previous = sourceDeclarations.get(sourceKey);
398
+ if (previous === undefined) {
399
+ sourceDeclarations.set(sourceKey, { signature, trailId: id });
400
+ } else if (previous.signature !== signature) {
401
+ issues.push({
402
+ message: `Activation source "${activation.source.id}" (${activation.source.kind}) is declared with conflicting source options by trails "${previous.trailId}" and "${id}". Use one canonical source declaration per source id, or give distinct source configurations distinct ids.`,
403
+ rule: 'activation-source-definition-unique',
404
+ sourceId: activation.source.id,
405
+ sourceKind: activation.source.kind,
406
+ trailId: id,
407
+ });
408
+ }
409
+ }
410
+
411
+ const scheduleIssues = validateScheduleSource(activation.source);
412
+ for (const issue of scheduleIssues) {
413
+ issues.push({
414
+ inputPath: [issue.field],
415
+ message: `Trail declares schedule source "${activation.source.id}" with invalid ${issue.field}: ${issue.message}`,
416
+ rule: 'activation-schedule-valid',
417
+ schemaIssues: [
418
+ { code: issue.field, message: issue.message, path: [issue.field] },
419
+ ],
420
+ sourceId: activation.source.id,
421
+ sourceKind: activation.source.kind,
422
+ trailId: id,
423
+ });
424
+ }
425
+
426
+ const webhookIssues = validateWebhookSource(activation.source);
427
+ for (const issue of webhookIssues) {
428
+ issues.push({
429
+ inputPath: [issue.field],
430
+ message: `Trail declares webhook source "${activation.source.id}" with invalid ${issue.field}: ${issue.message}`,
431
+ rule: 'activation-webhook-valid',
432
+ schemaIssues: [
433
+ { code: issue.field, message: issue.message, path: [issue.field] },
434
+ ],
435
+ sourceId: activation.source.id,
436
+ sourceKind: activation.source.kind,
172
437
  trailId: id,
173
438
  });
174
439
  }
175
440
  }
176
441
  }
442
+
443
+ return issues;
444
+ };
445
+
446
+ const issuePathText = (path: readonly (string | number)[]): string =>
447
+ path.length > 0 ? path.join('.') : '<root>';
448
+
449
+ const createSourceCompatibilityIssue = (
450
+ trailId: string,
451
+ activation: ActivationEntry,
452
+ schemaIssues: readonly TopoSchemaIssue[]
453
+ ): TopoDiagnostic => {
454
+ const [firstIssue] = schemaIssues;
455
+ const inputPath = firstIssue?.path ?? Object.freeze([]);
456
+ return {
457
+ inputPath,
458
+ message: `Activation source "${activation.source.id}" (${activation.source.kind}) does not satisfy trail input at ${issuePathText(inputPath)}: ${firstIssue?.message ?? 'source payload is incompatible with trail input'}`,
459
+ rule: 'activation-source-input-compatible',
460
+ schemaIssues,
461
+ sourceId: activation.source.id,
462
+ sourceKind: activation.source.kind,
463
+ trailId,
464
+ };
465
+ };
466
+
467
+ const checkSourcePayloadCompatibility = (
468
+ trail: AnyTrail,
469
+ activation: ActivationEntry,
470
+ signals: ReadonlyMap<string, AnySignal>
471
+ ): TopoDiagnostic | undefined => {
472
+ if (
473
+ !isKnownActivationSourceKind(activation.source.kind) ||
474
+ isDraftId(activation.source.id)
475
+ ) {
476
+ return undefined;
477
+ }
478
+
479
+ const schemaIssues = getActivationSourceInputCompatibilityIssues(
480
+ trail.input,
481
+ activation.source,
482
+ signals
483
+ );
484
+ if (!schemaIssues) {
485
+ return undefined;
486
+ }
487
+
488
+ return schemaIssues.length > 0
489
+ ? createSourceCompatibilityIssue(trail.id, activation, schemaIssues)
490
+ : undefined;
491
+ };
492
+
493
+ const checkActivationSourceInputCompatibility = (
494
+ trails: ReadonlyMap<string, AnyTrail>,
495
+ signals: ReadonlyMap<string, AnySignal>
496
+ ): TopoDiagnostic[] => {
497
+ const issues: TopoDiagnostic[] = [];
498
+
499
+ for (const trail of trails.values()) {
500
+ for (const activation of trail.activationSources ?? []) {
501
+ const issue = checkSourcePayloadCompatibility(trail, activation, signals);
502
+ if (issue) {
503
+ issues.push(issue);
504
+ }
505
+ }
506
+ }
507
+
508
+ return issues;
509
+ };
510
+
511
+ const checkContourReferences = (
512
+ contours: ReadonlyMap<string, AnyContour>,
513
+ topo: Topo
514
+ ): TopoDiagnostic[] => {
515
+ const issues: TopoDiagnostic[] = [];
516
+
517
+ for (const [name, contourDef] of contours) {
518
+ for (const ref of getContourReferences(contourDef)) {
519
+ if (!topo.hasContour(ref.contour) && !isDraftId(ref.contour)) {
520
+ issues.push({
521
+ message: `Contour "${name}" references "${ref.contour}" which is not in the topo`,
522
+ rule: 'contour-reference-exists',
523
+ trailId: name,
524
+ });
525
+ }
526
+ }
527
+ }
528
+
177
529
  return issues;
178
530
  };
179
531
 
@@ -184,15 +536,21 @@ const checkEventOrigins = (
184
536
  /**
185
537
  * Validate the structural integrity of a Topo graph.
186
538
  *
187
- * Checks follows references, example inputs, event origins, and output
188
- * schema presence. Returns `Result.ok()` when no issues are found, or
539
+ * Checks composing references, example inputs, signal origins, activation
540
+ * source kinds, and output schema presence. Returns `Result.ok()` when no
541
+ * issues are found, or
189
542
  * `Result.err(ValidationError)` with all issues in the error context.
190
543
  */
191
544
  export const validateTopo = (topo: Topo): Result<void, ValidationError> => {
192
545
  const issues = [
193
- ...checkFollows(topo.hikes, topo),
546
+ ...checkComposes(topo.trails, topo),
547
+ ...checkResources(topo.trails, topo),
548
+ ...checkContourReferences(topo.contours, topo),
194
549
  ...checkExamples(topo.trails),
195
- ...checkEventOrigins(topo.events, topo),
550
+ ...checkSignalOrigins(topo.signals, topo),
551
+ ...checkSignalReferences(topo.trails, topo.signals),
552
+ ...checkActivationSources(topo.trails),
553
+ ...checkActivationSourceInputCompatibility(topo.trails, topo.signals),
196
554
  ];
197
555
 
198
556
  if (issues.length === 0) {