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

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