@ontrails/core 1.0.0-beta.14 → 1.0.0-beta.16

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 (314) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +73 -24
  3. package/package.json +11 -4
  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/context.ts +28 -4
  11. package/src/contour.ts +344 -0
  12. package/src/cross-batch.ts +69 -0
  13. package/src/cross-schema.ts +36 -0
  14. package/src/detours.ts +8 -0
  15. package/src/draft.ts +60 -52
  16. package/src/error-projection.ts +51 -0
  17. package/src/errors.ts +245 -48
  18. package/src/execute.ts +1250 -40
  19. package/src/fire.ts +1169 -0
  20. package/src/index.ts +383 -49
  21. package/src/internal/fork-ctx.ts +69 -0
  22. package/src/layer-projection.ts +192 -0
  23. package/src/layer.ts +81 -0
  24. package/src/observe.ts +361 -0
  25. package/src/path-security.ts +1 -1
  26. package/src/permits.ts +12 -0
  27. package/src/redaction/patterns.ts +6 -3
  28. package/src/resilience.ts +11 -11
  29. package/src/resource-config.ts +792 -0
  30. package/src/resource.ts +175 -0
  31. package/src/result.ts +18 -0
  32. package/src/run.ts +7 -6
  33. package/src/schedule-runtime.ts +689 -0
  34. package/src/schedule.ts +324 -0
  35. package/src/serialization.ts +90 -55
  36. package/src/signal-diagnostics.ts +633 -0
  37. package/src/signal-ref.ts +87 -0
  38. package/src/signal.ts +37 -0
  39. package/src/store/accessor-protocol.ts +56 -0
  40. package/src/store/index.ts +4 -0
  41. package/src/structured-examples.ts +245 -0
  42. package/src/surface-derivation.ts +91 -0
  43. package/src/surface-filter.ts +176 -0
  44. package/src/topo.ts +710 -59
  45. package/src/tracing.ts +345 -0
  46. package/src/trail.ts +347 -33
  47. package/src/trails/derive-trail.ts +842 -0
  48. package/src/trails/index.ts +9 -0
  49. package/src/trails/ingest.ts +138 -0
  50. package/src/{internal/trails-db.ts → trails-db.ts} +50 -31
  51. package/src/transport-error-map.ts +174 -0
  52. package/src/type-utils.ts +17 -2
  53. package/src/types.ts +208 -17
  54. package/src/validate-established-topo.ts +8 -2
  55. package/src/validate-topo.ts +244 -13
  56. package/src/validation.ts +58 -1
  57. package/src/webhook.ts +276 -0
  58. package/src/workspace.ts +1 -1
  59. package/src/zod-wrappers.ts +72 -0
  60. package/.turbo/turbo-build.log +0 -1
  61. package/.turbo/turbo-lint.log +0 -3
  62. package/.turbo/turbo-typecheck.log +0 -1
  63. package/dist/adapters.d.ts +0 -39
  64. package/dist/adapters.d.ts.map +0 -1
  65. package/dist/adapters.js +0 -2
  66. package/dist/adapters.js.map +0 -1
  67. package/dist/blob-ref.d.ts +0 -20
  68. package/dist/blob-ref.d.ts.map +0 -1
  69. package/dist/blob-ref.js +0 -22
  70. package/dist/blob-ref.js.map +0 -1
  71. package/dist/branded.d.ts +0 -36
  72. package/dist/branded.d.ts.map +0 -1
  73. package/dist/branded.js +0 -89
  74. package/dist/branded.js.map +0 -1
  75. package/dist/collections.d.ts +0 -31
  76. package/dist/collections.d.ts.map +0 -1
  77. package/dist/collections.js +0 -60
  78. package/dist/collections.js.map +0 -1
  79. package/dist/context.d.ts +0 -10
  80. package/dist/context.d.ts.map +0 -1
  81. package/dist/context.js +0 -21
  82. package/dist/context.js.map +0 -1
  83. package/dist/derive.d.ts +0 -44
  84. package/dist/derive.d.ts.map +0 -1
  85. package/dist/derive.js +0 -149
  86. package/dist/derive.js.map +0 -1
  87. package/dist/dispatch.d.ts +0 -27
  88. package/dist/dispatch.d.ts.map +0 -1
  89. package/dist/dispatch.js +0 -34
  90. package/dist/dispatch.js.map +0 -1
  91. package/dist/draft.d.ts +0 -28
  92. package/dist/draft.d.ts.map +0 -1
  93. package/dist/draft.js +0 -156
  94. package/dist/draft.js.map +0 -1
  95. package/dist/errors.d.ts +0 -83
  96. package/dist/errors.d.ts.map +0 -1
  97. package/dist/errors.js +0 -142
  98. package/dist/errors.js.map +0 -1
  99. package/dist/event.d.ts +0 -8
  100. package/dist/event.d.ts.map +0 -1
  101. package/dist/event.js +0 -7
  102. package/dist/event.js.map +0 -1
  103. package/dist/execute.d.ts +0 -35
  104. package/dist/execute.d.ts.map +0 -1
  105. package/dist/execute.js +0 -90
  106. package/dist/execute.js.map +0 -1
  107. package/dist/fetch.d.ts +0 -15
  108. package/dist/fetch.d.ts.map +0 -1
  109. package/dist/fetch.js +0 -102
  110. package/dist/fetch.js.map +0 -1
  111. package/dist/gate.d.ts +0 -17
  112. package/dist/gate.d.ts.map +0 -1
  113. package/dist/gate.js +0 -21
  114. package/dist/gate.js.map +0 -1
  115. package/dist/guards.d.ts +0 -17
  116. package/dist/guards.d.ts.map +0 -1
  117. package/dist/guards.js +0 -25
  118. package/dist/guards.js.map +0 -1
  119. package/dist/health.d.ts +0 -18
  120. package/dist/health.d.ts.map +0 -1
  121. package/dist/health.js +0 -5
  122. package/dist/health.js.map +0 -1
  123. package/dist/index.d.ts +0 -48
  124. package/dist/index.d.ts.map +0 -1
  125. package/dist/index.js +0 -51
  126. package/dist/index.js.map +0 -1
  127. package/dist/internal/topo-saves.d.ts +0 -47
  128. package/dist/internal/topo-saves.d.ts.map +0 -1
  129. package/dist/internal/topo-saves.js +0 -310
  130. package/dist/internal/topo-saves.js.map +0 -1
  131. package/dist/internal/topo-store-read.d.ts +0 -67
  132. package/dist/internal/topo-store-read.d.ts.map +0 -1
  133. package/dist/internal/topo-store-read.js +0 -222
  134. package/dist/internal/topo-store-read.js.map +0 -1
  135. package/dist/internal/topo-store.d.ts +0 -12
  136. package/dist/internal/topo-store.d.ts.map +0 -1
  137. package/dist/internal/topo-store.js +0 -571
  138. package/dist/internal/topo-store.js.map +0 -1
  139. package/dist/internal/trails-db.d.ts +0 -16
  140. package/dist/internal/trails-db.d.ts.map +0 -1
  141. package/dist/internal/trails-db.js +0 -118
  142. package/dist/internal/trails-db.js.map +0 -1
  143. package/dist/job.d.ts +0 -24
  144. package/dist/job.d.ts.map +0 -1
  145. package/dist/job.js +0 -17
  146. package/dist/job.js.map +0 -1
  147. package/dist/layer.d.ts +0 -6
  148. package/dist/layer.d.ts.map +0 -1
  149. package/dist/layer.js +0 -4
  150. package/dist/layer.js.map +0 -1
  151. package/dist/path-security.d.ts +0 -28
  152. package/dist/path-security.d.ts.map +0 -1
  153. package/dist/path-security.js +0 -63
  154. package/dist/path-security.js.map +0 -1
  155. package/dist/patterns/bulk.d.ts +0 -15
  156. package/dist/patterns/bulk.d.ts.map +0 -1
  157. package/dist/patterns/bulk.js +0 -14
  158. package/dist/patterns/bulk.js.map +0 -1
  159. package/dist/patterns/change.d.ts +0 -10
  160. package/dist/patterns/change.d.ts.map +0 -1
  161. package/dist/patterns/change.js +0 -10
  162. package/dist/patterns/change.js.map +0 -1
  163. package/dist/patterns/date-range.d.ts +0 -10
  164. package/dist/patterns/date-range.d.ts.map +0 -1
  165. package/dist/patterns/date-range.js +0 -10
  166. package/dist/patterns/date-range.js.map +0 -1
  167. package/dist/patterns/index.d.ts +0 -9
  168. package/dist/patterns/index.d.ts.map +0 -1
  169. package/dist/patterns/index.js +0 -9
  170. package/dist/patterns/index.js.map +0 -1
  171. package/dist/patterns/pagination.d.ts +0 -18
  172. package/dist/patterns/pagination.d.ts.map +0 -1
  173. package/dist/patterns/pagination.js +0 -18
  174. package/dist/patterns/pagination.js.map +0 -1
  175. package/dist/patterns/progress.d.ts +0 -11
  176. package/dist/patterns/progress.d.ts.map +0 -1
  177. package/dist/patterns/progress.js +0 -11
  178. package/dist/patterns/progress.js.map +0 -1
  179. package/dist/patterns/sorting.d.ts +0 -13
  180. package/dist/patterns/sorting.d.ts.map +0 -1
  181. package/dist/patterns/sorting.js +0 -10
  182. package/dist/patterns/sorting.js.map +0 -1
  183. package/dist/patterns/status.d.ts +0 -15
  184. package/dist/patterns/status.d.ts.map +0 -1
  185. package/dist/patterns/status.js +0 -9
  186. package/dist/patterns/status.js.map +0 -1
  187. package/dist/patterns/timestamps.d.ts +0 -10
  188. package/dist/patterns/timestamps.d.ts.map +0 -1
  189. package/dist/patterns/timestamps.js +0 -10
  190. package/dist/patterns/timestamps.js.map +0 -1
  191. package/dist/provision-config.d.ts +0 -22
  192. package/dist/provision-config.d.ts.map +0 -1
  193. package/dist/provision-config.js +0 -210
  194. package/dist/provision-config.js.map +0 -1
  195. package/dist/provision.d.ts +0 -71
  196. package/dist/provision.d.ts.map +0 -1
  197. package/dist/provision.js +0 -56
  198. package/dist/provision.js.map +0 -1
  199. package/dist/redaction/index.d.ts +0 -4
  200. package/dist/redaction/index.d.ts.map +0 -1
  201. package/dist/redaction/index.js +0 -3
  202. package/dist/redaction/index.js.map +0 -1
  203. package/dist/redaction/patterns.d.ts +0 -9
  204. package/dist/redaction/patterns.d.ts.map +0 -1
  205. package/dist/redaction/patterns.js +0 -39
  206. package/dist/redaction/patterns.js.map +0 -1
  207. package/dist/redaction/redactor.d.ts +0 -27
  208. package/dist/redaction/redactor.d.ts.map +0 -1
  209. package/dist/redaction/redactor.js +0 -89
  210. package/dist/redaction/redactor.js.map +0 -1
  211. package/dist/resilience.d.ts +0 -34
  212. package/dist/resilience.d.ts.map +0 -1
  213. package/dist/resilience.js +0 -164
  214. package/dist/resilience.js.map +0 -1
  215. package/dist/result.d.ts +0 -57
  216. package/dist/result.d.ts.map +0 -1
  217. package/dist/result.js +0 -156
  218. package/dist/result.js.map +0 -1
  219. package/dist/run.d.ts +0 -27
  220. package/dist/run.d.ts.map +0 -1
  221. package/dist/run.js +0 -34
  222. package/dist/run.js.map +0 -1
  223. package/dist/serialization.d.ts +0 -27
  224. package/dist/serialization.d.ts.map +0 -1
  225. package/dist/serialization.js +0 -153
  226. package/dist/serialization.js.map +0 -1
  227. package/dist/service-config.d.ts +0 -22
  228. package/dist/service-config.d.ts.map +0 -1
  229. package/dist/service-config.js +0 -210
  230. package/dist/service-config.js.map +0 -1
  231. package/dist/service.d.ts +0 -71
  232. package/dist/service.d.ts.map +0 -1
  233. package/dist/service.js +0 -56
  234. package/dist/service.js.map +0 -1
  235. package/dist/signal.d.ts +0 -33
  236. package/dist/signal.d.ts.map +0 -1
  237. package/dist/signal.js +0 -17
  238. package/dist/signal.js.map +0 -1
  239. package/dist/topo-store.d.ts +0 -48
  240. package/dist/topo-store.d.ts.map +0 -1
  241. package/dist/topo-store.js +0 -175
  242. package/dist/topo-store.js.map +0 -1
  243. package/dist/topo.d.ts +0 -25
  244. package/dist/topo.d.ts.map +0 -1
  245. package/dist/topo.js +0 -112
  246. package/dist/topo.js.map +0 -1
  247. package/dist/trail.d.ts +0 -96
  248. package/dist/trail.d.ts.map +0 -1
  249. package/dist/trail.js +0 -20
  250. package/dist/trail.js.map +0 -1
  251. package/dist/type-utils.d.ts +0 -24
  252. package/dist/type-utils.d.ts.map +0 -1
  253. package/dist/type-utils.js +0 -12
  254. package/dist/type-utils.js.map +0 -1
  255. package/dist/types.d.ts +0 -71
  256. package/dist/types.d.ts.map +0 -1
  257. package/dist/types.js +0 -3
  258. package/dist/types.js.map +0 -1
  259. package/dist/validate-established-topo.d.ts +0 -76
  260. package/dist/validate-established-topo.d.ts.map +0 -1
  261. package/dist/validate-established-topo.js +0 -43
  262. package/dist/validate-established-topo.js.map +0 -1
  263. package/dist/validate-topo.d.ts +0 -24
  264. package/dist/validate-topo.d.ts.map +0 -1
  265. package/dist/validate-topo.js +0 -176
  266. package/dist/validate-topo.js.map +0 -1
  267. package/dist/validation.d.ts +0 -27
  268. package/dist/validation.d.ts.map +0 -1
  269. package/dist/validation.js +0 -165
  270. package/dist/validation.js.map +0 -1
  271. package/dist/workspace.d.ts +0 -25
  272. package/dist/workspace.d.ts.map +0 -1
  273. package/dist/workspace.js +0 -57
  274. package/dist/workspace.js.map +0 -1
  275. package/src/__tests__/blob-ref.test.ts +0 -103
  276. package/src/__tests__/branded.test.ts +0 -148
  277. package/src/__tests__/collections.test.ts +0 -126
  278. package/src/__tests__/context.test.ts +0 -77
  279. package/src/__tests__/derive.test.ts +0 -260
  280. package/src/__tests__/errors.test.ts +0 -309
  281. package/src/__tests__/execute.test.ts +0 -525
  282. package/src/__tests__/fetch.test.ts +0 -217
  283. package/src/__tests__/gate.test.ts +0 -125
  284. package/src/__tests__/guards.test.ts +0 -102
  285. package/src/__tests__/path-security.test.ts +0 -114
  286. package/src/__tests__/patterns.test.ts +0 -273
  287. package/src/__tests__/redaction.test.ts +0 -244
  288. package/src/__tests__/resilience.test.ts +0 -246
  289. package/src/__tests__/result.test.ts +0 -155
  290. package/src/__tests__/run.test.ts +0 -181
  291. package/src/__tests__/serialization.test.ts +0 -401
  292. package/src/__tests__/service-config.test.ts +0 -228
  293. package/src/__tests__/service.test.ts +0 -197
  294. package/src/__tests__/signal.test.ts +0 -82
  295. package/src/__tests__/topo-store-read.test.ts +0 -251
  296. package/src/__tests__/topo-store.test.ts +0 -469
  297. package/src/__tests__/topo.test.ts +0 -277
  298. package/src/__tests__/trail-permit.test.ts +0 -60
  299. package/src/__tests__/trail.test.ts +0 -261
  300. package/src/__tests__/trails-db.test.ts +0 -191
  301. package/src/__tests__/type-utils.test.ts +0 -90
  302. package/src/__tests__/validate-topo.test.ts +0 -489
  303. package/src/__tests__/validation.test.ts +0 -336
  304. package/src/__tests__/workspace.test.ts +0 -183
  305. package/src/event.ts +0 -15
  306. package/src/gate.ts +0 -44
  307. package/src/internal/topo-saves.ts +0 -429
  308. package/src/internal/topo-store-read.ts +0 -473
  309. package/src/internal/topo-store.ts +0 -1087
  310. package/src/provision-config.ts +0 -373
  311. package/src/provision.ts +0 -148
  312. package/src/topo-store.ts +0 -301
  313. package/tsconfig.json +0 -9
  314. package/tsconfig.tsbuildinfo +0 -1
package/src/types.ts CHANGED
@@ -1,4 +1,66 @@
1
+ import type { TrailsError } from './errors.js';
2
+ import type { BasePermit } from './permits.js';
1
3
  import type { Result } from './result.js';
4
+ import type { Signal } from './signal.js';
5
+ import type { AnyTrail } from './trail.js';
6
+ import type { CrossInput, TrailOutput } from './type-utils.js';
7
+ import type { ActivationProvenance } from './activation-provenance.js';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Detour
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /** A recovery path that activates when a trail's blaze fails with a matching error. */
14
+ export interface Detour<Input, Output, TErr extends TrailsError = TrailsError> {
15
+ /* oxlint-disable-next-line no-explicit-any -- standard pattern for matching abstract+concrete class constructors */
16
+ readonly on: abstract new (...args: any[]) => TErr;
17
+ readonly maxAttempts?: number | undefined;
18
+ readonly recover: (
19
+ attempt: DetourAttempt<Input, TErr>,
20
+ ctx: TrailContext
21
+ ) => Promise<Result<Output, TrailsError>>;
22
+ }
23
+
24
+ /** Context passed to a detour's recover function on each attempt. */
25
+ export interface DetourAttempt<Input, TErr extends TrailsError = TrailsError> {
26
+ /** 1-indexed attempt number */
27
+ readonly attempt: number;
28
+ /** The matched error */
29
+ readonly error: TErr;
30
+ /** Original trail input */
31
+ readonly input: Input;
32
+ }
33
+
34
+ type CrossBatchCall<TTarget extends AnyTrail | string = AnyTrail | string> =
35
+ TTarget extends AnyTrail
36
+ ? readonly [trail: TTarget, input: CrossInput<TTarget>]
37
+ : readonly [id: string, input: unknown];
38
+
39
+ type CrossBatchResult<TTarget extends AnyTrail | string> =
40
+ TTarget extends AnyTrail
41
+ ? Result<TrailOutput<TTarget>, Error>
42
+ : Result<unknown, Error>;
43
+
44
+ type CrossBatchResults<TCalls extends readonly CrossBatchCall[]> = {
45
+ readonly [K in keyof TCalls]: TCalls[K] extends readonly [
46
+ infer TTarget,
47
+ unknown,
48
+ ]
49
+ ? TTarget extends AnyTrail | string
50
+ ? CrossBatchResult<TTarget>
51
+ : never
52
+ : never;
53
+ };
54
+
55
+ /** Runtime options for batch `ctx.cross([...])` calls. */
56
+ export interface CrossBatchOptions {
57
+ /**
58
+ * Maximum number of branches to execute concurrently.
59
+ *
60
+ * Omit for unbounded concurrency. `1` is equivalent to sequential execution.
61
+ */
62
+ readonly concurrency?: number | undefined;
63
+ }
2
64
 
3
65
  /**
4
66
  * Trail implementation — sync or async.
@@ -11,17 +73,67 @@ export type Implementation<I, O> = (
11
73
  ctx: TrailContext
12
74
  ) => Result<O, Error> | Promise<Result<O, Error>>;
13
75
 
14
- /** Invoke another trail by id — used for trail composition */
15
- export type CrossFn = <O>(
16
- id: string,
17
- input: unknown
18
- ) => Promise<Result<O, Error>>;
76
+ /**
77
+ * Invoke another trail — used for trail composition.
78
+ *
79
+ * Two call shapes:
80
+ *
81
+ * - **By trail object** (typed): `ctx.cross(showGist, { id })` — the compiler
82
+ * infers `I` and `O` from the trail's schemas, so the result is fully typed.
83
+ * - **By string id** (untyped escape hatch): `ctx.cross('gist.show', { id })`
84
+ * — returns `Result<O, Error>` where `O` defaults to `unknown`.
85
+ * - **By batch**: `ctx.cross([[showGist, { id }], ['audit.log', payload]])`
86
+ * — executes every crossing concurrently and resolves once all results are
87
+ * available. Result ordering always matches the input tuple ordering. Pass
88
+ * `{ concurrency: N }` as the second argument to limit how many branches
89
+ * run at once.
90
+ */
91
+ export interface CrossFn {
92
+ <const TCalls extends readonly CrossBatchCall[]>(
93
+ calls: TCalls,
94
+ options?: CrossBatchOptions
95
+ ): Promise<CrossBatchResults<TCalls>>;
96
+ <T extends AnyTrail>(
97
+ trail: T,
98
+ input: CrossInput<T>
99
+ ): Promise<Result<TrailOutput<T>, Error>>;
100
+ <O = unknown>(id: string, input: unknown): Promise<Result<O, Error>>;
101
+ }
102
+
103
+ /**
104
+ * Emit a signal — used for signal-driven activation.
105
+ *
106
+ * Fan-out to consumer trails (those with the signal in their `on:` array) is
107
+ * the framework's responsibility. Producers call with a `Signal<T>` value and
108
+ * get best-effort `Promise<void>` semantics: payload validation, missing topo
109
+ * entries, guard suppression, and consumer failures are observable through
110
+ * diagnostics/logging but do not become producer-facing `Result` plumbing.
111
+ * Consumers fan out in parallel, each with its own derived context. Runtime
112
+ * cycle suppression is still signal-id-based against the current fire stack:
113
+ * it prevents re-entrant loops but can over-suppress legitimate diamond
114
+ * re-fires, with a debug breadcrumb and a warn emitted when suppression
115
+ * happens.
116
+ */
117
+ export type FireFn = <T>(signal: Signal<T>, payload: T) => Promise<void>;
19
118
 
20
- /** Resolve a provision instance from the current trail context. */
21
- export type ProvisionLookup = <T = unknown>(
22
- provisionOrId: { readonly id: string } | string
119
+ /** Resolve a resource instance from the current trail context. */
120
+ export type ResourceLookup = <T = unknown>(
121
+ resourceOrId: { readonly id: string } | string
23
122
  ) => T;
24
123
 
124
+ /**
125
+ * Wrap the execution of `fn` in a child trace span.
126
+ *
127
+ * Creates a nested span under the current trail's root trace record, times
128
+ * the callback, records success or failure (including error category), and
129
+ * writes the completed span to the registered sink. Errors thrown by `fn`
130
+ * are recorded on the span and then rethrown — tracing never swallows them.
131
+ */
132
+ export type TraceFn = <T>(
133
+ label: string,
134
+ fn: () => T | Promise<T>
135
+ ) => Promise<T>;
136
+
25
137
  /** Callback for reporting progress from long-running trails */
26
138
  export type ProgressCallback = (event: ProgressEvent) => void;
27
139
 
@@ -46,20 +158,68 @@ export interface Logger {
46
158
  child(context: Record<string, unknown>): Logger;
47
159
  }
48
160
 
49
- /** Context extension key for the invoking trailhead name. */
50
- export const TRAILHEAD_KEY = '__trails_trailhead' as const;
161
+ export type LogLevel =
162
+ | 'debug'
163
+ | 'error'
164
+ | 'fatal'
165
+ | 'info'
166
+ | 'silent'
167
+ | 'trace'
168
+ | 'warn';
169
+
170
+ export interface LogRecord {
171
+ readonly category: string;
172
+ readonly level: LogLevel;
173
+ readonly message: string;
174
+ readonly metadata: Record<string, unknown>;
175
+ readonly timestamp: Date;
176
+ }
177
+
178
+ export interface LogSink {
179
+ readonly name: string;
180
+ readonly write: (record: LogRecord) => void;
181
+ readonly flush?: (() => Promise<void>) | undefined;
182
+ }
51
183
 
52
- /** Minimal permit shape available on TrailContext. Permits extends this. */
53
- export interface BasePermit {
54
- readonly id: string;
55
- readonly scopes: readonly string[];
184
+ export interface LogFormatter {
185
+ format(record: LogRecord): string;
56
186
  }
57
187
 
188
+ /**
189
+ * Context extension key for the invoking surface name.
190
+ */
191
+ export const SURFACE_KEY = '__trails_surface' as const;
192
+
193
+ /**
194
+ * Context extension key for the layer names attached by the invoking surface.
195
+ */
196
+ export const SURFACE_LAYER_NAMES_KEY = '__trails_surface_layer_names' as const;
197
+
198
+ /**
199
+ * Context extension key carrying per-layer runtime input.
200
+ *
201
+ * Surfaces (CLI, MCP, HTTP) project each typed layer's `input` schema onto
202
+ * their native idioms (flags, tool params, query strings). At execute time
203
+ * the parsed values are partitioned per layer and stored under this key as
204
+ * `Record<layerName, unknown>`. Layers that need runtime input read their
205
+ * own slot via `ctx.extensions?.[LAYER_INPUTS_KEY]?.[layer.name]`.
206
+ *
207
+ * @see TRL-473 for the CLI projection contract.
208
+ */
209
+ export const LAYER_INPUTS_KEY = '__trails_layer_inputs' as const;
210
+
58
211
  /** Runtime context threaded through every trail execution */
59
212
  export interface TrailContext {
213
+ readonly activation?: ActivationProvenance | undefined;
60
214
  readonly requestId: string;
61
215
  readonly abortSignal: AbortSignal;
62
216
  readonly cross?: CrossFn | undefined;
217
+ /**
218
+ * Emit a typed signal. Fans out to every trail with the signal in its
219
+ * `on:` declaration. Bound by the runner that holds the topo (typically
220
+ * `run()`); undefined when a context is constructed without topo access.
221
+ */
222
+ readonly fire?: FireFn | undefined;
63
223
  readonly permit?: BasePermit;
64
224
  readonly workspaceRoot?: string | undefined;
65
225
  readonly logger?: Logger | undefined;
@@ -67,7 +227,37 @@ export interface TrailContext {
67
227
  readonly cwd?: string | undefined;
68
228
  readonly env?: Record<string, string | undefined> | undefined;
69
229
  readonly extensions?: Readonly<Record<string, unknown>> | undefined;
70
- readonly provision?: ProvisionLookup | undefined;
230
+ readonly resource?: ResourceLookup | undefined;
231
+ /**
232
+ * Whether the current invocation is a dry run.
233
+ *
234
+ * Defaults to `false`. Trails that don't read this field are unaffected.
235
+ * Trails that do read it decide what dry-run means for their domain — for
236
+ * example: preview the change without committing, validate inputs without
237
+ * performing side effects, or return what would happen without actually
238
+ * doing it.
239
+ *
240
+ * The framework only carries the flag from the surface (e.g. CLI
241
+ * `--dry-run`) into the context. It never short-circuits trail execution
242
+ * on its own based on this field.
243
+ *
244
+ * Pair this runtime signal with `TrailSpec.dryRun`, which declares whether a
245
+ * trail supports dry-run semantics for governance, derivation, and surface
246
+ * tooling.
247
+ *
248
+ * @remarks Always defined on contexts produced by `executeTrail` or
249
+ * `createTrailContext` (normalized to `false` when not provided).
250
+ */
251
+ readonly dryRun?: boolean | undefined;
252
+ /**
253
+ * Wrap a callback in a child trace span.
254
+ *
255
+ * Always present on contexts produced by `executeTrail` or
256
+ * `createTrailContext`. Optional on the interface so manually constructed
257
+ * contexts (tests, ad-hoc compositions) don't have to supply one — call
258
+ * sites tolerate `undefined` by falling back to a no-op passthrough.
259
+ */
260
+ readonly trace?: TraceFn | undefined;
71
261
  }
72
262
 
73
263
  /**
@@ -82,6 +272,7 @@ export type PermitRequirement =
82
272
  | 'public';
83
273
 
84
274
  /** Input shape used to seed a runtime TrailContext before resolution. */
85
- export type TrailContextInit = Omit<TrailContext, 'provision'> & {
86
- readonly provision?: ProvisionLookup | undefined;
275
+ export type TrailContextInit = Omit<TrailContext, 'resource' | 'trace'> & {
276
+ readonly resource?: ResourceLookup | undefined;
277
+ readonly trace?: TraceFn | undefined;
87
278
  };
@@ -1,7 +1,7 @@
1
1
  import { ValidationError } from './errors.js';
2
2
  import { Result } from './result.js';
3
3
  import type { Topo } from './topo.js';
4
- import { validateEstablishedTopo as validateDraftFreeTopo } from './draft.js';
4
+ import { validateDraftFreeTopo } from './draft.js';
5
5
  import type { TopoIssue } from './validate-topo.js';
6
6
  import { validateTopo } from './validate-topo.js';
7
7
 
@@ -9,7 +9,13 @@ const PROJECTION_BLOCKING_RULES = new Set([
9
9
  'cross-cycle',
10
10
  'cross-exists',
11
11
  'no-self-cross',
12
- 'provision-exists',
12
+ 'activation-source-definition-unique',
13
+ 'activation-source-edge-unique',
14
+ 'activation-source-kind-known',
15
+ 'activation-schedule-valid',
16
+ 'resource-exists',
17
+ 'signal-fire-exists',
18
+ 'signal-on-exists',
13
19
  'signal-origin-exists',
14
20
  ]);
15
21
 
@@ -1,18 +1,30 @@
1
1
  /**
2
2
  * Structural validation for a Topo graph.
3
3
  *
4
- * Checks trail crossing 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 crossing 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';
18
+ import type { ActivationEntry } from './activation-source.js';
19
+ import { isKnownActivationSourceKind } from './activation-source.js';
10
20
  import { isDraftId } from './draft.js';
11
- import type { AnySignal } from './event.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
25
  import type { AnyTrail } from './trail.js';
15
26
  import { validateInput } from './validation.js';
27
+ import { validateWebhookSource } from './webhook.js';
16
28
 
17
29
  // ---------------------------------------------------------------------------
18
30
  // Issue shape
@@ -22,8 +34,14 @@ export interface TopoIssue {
22
34
  readonly trailId: string;
23
35
  readonly rule: string;
24
36
  readonly message: string;
37
+ readonly inputPath?: readonly (string | number)[];
38
+ readonly schemaIssues?: readonly TopoSchemaIssue[];
39
+ readonly sourceId?: string;
40
+ readonly sourceKind?: string;
25
41
  }
26
42
 
43
+ export type TopoSchemaIssue = ActivationSchemaIssue;
44
+
27
45
  // ---------------------------------------------------------------------------
28
46
  // Validators
29
47
  // ---------------------------------------------------------------------------
@@ -114,21 +132,21 @@ const checkCrosses = (
114
132
  return issues;
115
133
  };
116
134
 
117
- const checkProvisions = (
135
+ const checkResources = (
118
136
  trails: ReadonlyMap<string, AnyTrail>,
119
137
  topo: Topo
120
138
  ): TopoIssue[] => {
121
139
  const issues: TopoIssue[] = [];
122
140
 
123
141
  for (const [id, trail] of trails) {
124
- for (const declaredProvision of trail.provisions) {
142
+ for (const declaredResource of trail.resources) {
125
143
  if (
126
- !topo.hasProvision(declaredProvision.id) &&
127
- !isDraftId(declaredProvision.id)
144
+ !topo.hasResource(declaredResource.id) &&
145
+ !isDraftId(declaredResource.id)
128
146
  ) {
129
147
  issues.push({
130
- message: `Provision "${declaredProvision.id}" is not in the topo`,
131
- rule: 'provision-exists',
148
+ message: `Resource "${declaredResource.id}" is not in the topo`,
149
+ rule: 'resource-exists',
132
150
  trailId: id,
133
151
  });
134
152
  }
@@ -203,6 +221,214 @@ const checkSignalOrigins = (
203
221
  return issues;
204
222
  };
205
223
 
224
+ const checkSignalReferences = (
225
+ trails: ReadonlyMap<string, AnyTrail>,
226
+ signals: ReadonlyMap<string, AnySignal>
227
+ ): TopoIssue[] => {
228
+ const issues: TopoIssue[] = [];
229
+
230
+ for (const [id, trail] of trails) {
231
+ for (const signalId of trail.fires ?? []) {
232
+ if (!signals.has(signalId) && !isDraftId(signalId)) {
233
+ issues.push({
234
+ message: `Trail fires signal "${signalId}" which is not in the topo`,
235
+ rule: 'signal-fire-exists',
236
+ trailId: id,
237
+ });
238
+ }
239
+ }
240
+
241
+ for (const signalId of trail.on ?? []) {
242
+ if (!signals.has(signalId) && !isDraftId(signalId)) {
243
+ issues.push({
244
+ message: `Trail declares on signal "${signalId}" which is not in the topo`,
245
+ rule: 'signal-on-exists',
246
+ trailId: id,
247
+ });
248
+ }
249
+ }
250
+ }
251
+
252
+ return issues;
253
+ };
254
+
255
+ const checkActivationSources = (
256
+ trails: ReadonlyMap<string, AnyTrail>
257
+ ): TopoIssue[] => {
258
+ const issues: TopoIssue[] = [];
259
+ const sourceDeclarations = new Map<
260
+ string,
261
+ {
262
+ readonly signature: string;
263
+ readonly trailId: string;
264
+ }
265
+ >();
266
+ const trailSourceEdges = new Set<string>();
267
+
268
+ for (const [id, trail] of trails) {
269
+ for (const activation of trail.activationSources ?? []) {
270
+ if (!isKnownActivationSourceKind(activation.source.kind)) {
271
+ issues.push({
272
+ message: `Trail declares on source "${activation.source.id}" with unsupported source kind "${activation.source.kind}"`,
273
+ rule: 'activation-source-kind-known',
274
+ trailId: id,
275
+ });
276
+ continue;
277
+ }
278
+
279
+ const sourceKey = activationSourceKey(activation.source);
280
+ const edgeKey = `${id}\0${sourceKey}`;
281
+ if (trailSourceEdges.has(edgeKey)) {
282
+ issues.push({
283
+ 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.`,
284
+ rule: 'activation-source-edge-unique',
285
+ sourceId: activation.source.id,
286
+ sourceKind: activation.source.kind,
287
+ trailId: id,
288
+ });
289
+ } else {
290
+ trailSourceEdges.add(edgeKey);
291
+ }
292
+
293
+ if (!isDraftId(activation.source.id)) {
294
+ const signature = activationSourceDeclarationSignature(
295
+ activation.source
296
+ );
297
+ const previous = sourceDeclarations.get(sourceKey);
298
+ if (previous === undefined) {
299
+ sourceDeclarations.set(sourceKey, { signature, trailId: id });
300
+ } else if (previous.signature !== signature) {
301
+ issues.push({
302
+ 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.`,
303
+ rule: 'activation-source-definition-unique',
304
+ sourceId: activation.source.id,
305
+ sourceKind: activation.source.kind,
306
+ trailId: id,
307
+ });
308
+ }
309
+ }
310
+
311
+ const scheduleIssues = validateScheduleSource(activation.source);
312
+ for (const issue of scheduleIssues) {
313
+ issues.push({
314
+ inputPath: [issue.field],
315
+ message: `Trail declares schedule source "${activation.source.id}" with invalid ${issue.field}: ${issue.message}`,
316
+ rule: 'activation-schedule-valid',
317
+ schemaIssues: [
318
+ { code: issue.field, message: issue.message, path: [issue.field] },
319
+ ],
320
+ sourceId: activation.source.id,
321
+ sourceKind: activation.source.kind,
322
+ trailId: id,
323
+ });
324
+ }
325
+
326
+ const webhookIssues = validateWebhookSource(activation.source);
327
+ for (const issue of webhookIssues) {
328
+ issues.push({
329
+ inputPath: [issue.field],
330
+ message: `Trail declares webhook source "${activation.source.id}" with invalid ${issue.field}: ${issue.message}`,
331
+ rule: 'activation-webhook-valid',
332
+ schemaIssues: [
333
+ { code: issue.field, message: issue.message, path: [issue.field] },
334
+ ],
335
+ sourceId: activation.source.id,
336
+ sourceKind: activation.source.kind,
337
+ trailId: id,
338
+ });
339
+ }
340
+ }
341
+ }
342
+
343
+ return issues;
344
+ };
345
+
346
+ const issuePathText = (path: readonly (string | number)[]): string =>
347
+ path.length > 0 ? path.join('.') : '<root>';
348
+
349
+ const createSourceCompatibilityIssue = (
350
+ trailId: string,
351
+ activation: ActivationEntry,
352
+ schemaIssues: readonly TopoSchemaIssue[]
353
+ ): TopoIssue => {
354
+ const [firstIssue] = schemaIssues;
355
+ const inputPath = firstIssue?.path ?? Object.freeze([]);
356
+ return {
357
+ inputPath,
358
+ 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'}`,
359
+ rule: 'activation-source-input-compatible',
360
+ schemaIssues,
361
+ sourceId: activation.source.id,
362
+ sourceKind: activation.source.kind,
363
+ trailId,
364
+ };
365
+ };
366
+
367
+ const checkSourcePayloadCompatibility = (
368
+ trail: AnyTrail,
369
+ activation: ActivationEntry,
370
+ signals: ReadonlyMap<string, AnySignal>
371
+ ): TopoIssue | undefined => {
372
+ if (
373
+ !isKnownActivationSourceKind(activation.source.kind) ||
374
+ isDraftId(activation.source.id)
375
+ ) {
376
+ return undefined;
377
+ }
378
+
379
+ const schemaIssues = getActivationSourceInputCompatibilityIssues(
380
+ trail.input,
381
+ activation.source,
382
+ signals
383
+ );
384
+ if (!schemaIssues) {
385
+ return undefined;
386
+ }
387
+
388
+ return schemaIssues.length > 0
389
+ ? createSourceCompatibilityIssue(trail.id, activation, schemaIssues)
390
+ : undefined;
391
+ };
392
+
393
+ const checkActivationSourceInputCompatibility = (
394
+ trails: ReadonlyMap<string, AnyTrail>,
395
+ signals: ReadonlyMap<string, AnySignal>
396
+ ): TopoIssue[] => {
397
+ const issues: TopoIssue[] = [];
398
+
399
+ for (const trail of trails.values()) {
400
+ for (const activation of trail.activationSources ?? []) {
401
+ const issue = checkSourcePayloadCompatibility(trail, activation, signals);
402
+ if (issue) {
403
+ issues.push(issue);
404
+ }
405
+ }
406
+ }
407
+
408
+ return issues;
409
+ };
410
+
411
+ const checkContourReferences = (
412
+ contours: ReadonlyMap<string, AnyContour>,
413
+ topo: Topo
414
+ ): TopoIssue[] => {
415
+ const issues: TopoIssue[] = [];
416
+
417
+ for (const [name, contourDef] of contours) {
418
+ for (const ref of getContourReferences(contourDef)) {
419
+ if (!topo.hasContour(ref.contour) && !isDraftId(ref.contour)) {
420
+ issues.push({
421
+ message: `Contour "${name}" references "${ref.contour}" which is not in the topo`,
422
+ rule: 'contour-reference-exists',
423
+ trailId: name,
424
+ });
425
+ }
426
+ }
427
+ }
428
+
429
+ return issues;
430
+ };
431
+
206
432
  // ---------------------------------------------------------------------------
207
433
  // Public API
208
434
  // ---------------------------------------------------------------------------
@@ -210,16 +436,21 @@ const checkSignalOrigins = (
210
436
  /**
211
437
  * Validate the structural integrity of a Topo graph.
212
438
  *
213
- * Checks crossing references, example inputs, event origins, and output
214
- * schema presence. Returns `Result.ok()` when no issues are found, or
439
+ * Checks crossing references, example inputs, signal origins, activation
440
+ * source kinds, and output schema presence. Returns `Result.ok()` when no
441
+ * issues are found, or
215
442
  * `Result.err(ValidationError)` with all issues in the error context.
216
443
  */
217
444
  export const validateTopo = (topo: Topo): Result<void, ValidationError> => {
218
445
  const issues = [
219
446
  ...checkCrosses(topo.trails, topo),
220
- ...checkProvisions(topo.trails, topo),
447
+ ...checkResources(topo.trails, topo),
448
+ ...checkContourReferences(topo.contours, topo),
221
449
  ...checkExamples(topo.trails),
222
450
  ...checkSignalOrigins(topo.signals, topo),
451
+ ...checkSignalReferences(topo.trails, topo.signals),
452
+ ...checkActivationSources(topo.trails),
453
+ ...checkActivationSourceInputCompatibility(topo.trails, topo.signals),
223
454
  ];
224
455
 
225
456
  if (issues.length === 0) {
package/src/validation.ts CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { z } from 'zod';
9
9
 
10
+ import { BLOB_REF_SCHEMA_META_KEY, blobRefJsonSchema } from './blob-ref.js';
10
11
  import { ValidationError } from './errors.js';
11
12
  import { Result } from './result.js';
12
13
 
@@ -31,10 +32,57 @@ type JsonSchemaConverter = (schema: z.ZodType) => JsonSchema;
31
32
  // ---------------------------------------------------------------------------
32
33
 
33
34
  const isOptionalLike = (s: ZodInternals): boolean => {
34
- const defType = s._zod.def['type'] as string;
35
+ let current = s;
36
+ const seen = new Set<ZodInternals>();
37
+ while (
38
+ current._zod.def['type'] === 'readonly' &&
39
+ !seen.has(current) &&
40
+ current._zod.def['innerType'] !== undefined
41
+ ) {
42
+ seen.add(current);
43
+ current = current._zod.def['innerType'] as ZodInternals;
44
+ }
45
+ const defType = current._zod.def['type'] as string;
35
46
  return defType === 'optional' || defType === 'default';
36
47
  };
37
48
 
49
+ const getSchemaMeta = (
50
+ schema: z.ZodType
51
+ ): Readonly<Record<string, unknown>> | undefined => {
52
+ const maybeMeta = (schema as unknown as { meta?: () => unknown }).meta;
53
+ if (typeof maybeMeta !== 'function') {
54
+ return undefined;
55
+ }
56
+ const meta = maybeMeta.call(schema);
57
+ return typeof meta === 'object' && meta !== null
58
+ ? (meta as Readonly<Record<string, unknown>>)
59
+ : undefined;
60
+ };
61
+
62
+ const getSchemaJsonSchemaOverride = (
63
+ schema: z.ZodType
64
+ ): JsonSchema | undefined => {
65
+ const meta = getSchemaMeta(schema);
66
+ if (meta?.[BLOB_REF_SCHEMA_META_KEY] === true) {
67
+ const override: JsonSchema = {
68
+ properties: Object.fromEntries(
69
+ Object.entries(blobRefJsonSchema.properties).map(([key, value]) => [
70
+ key,
71
+ { ...value },
72
+ ])
73
+ ),
74
+ required: [...blobRefJsonSchema.required],
75
+ type: blobRefJsonSchema.type,
76
+ };
77
+ const { description } = schema as unknown as ZodInternals;
78
+ if (description) {
79
+ override['description'] = description;
80
+ }
81
+ return override;
82
+ }
83
+ return undefined;
84
+ };
85
+
38
86
  // ---------------------------------------------------------------------------
39
87
  // Issue formatting
40
88
  // ---------------------------------------------------------------------------
@@ -132,6 +180,11 @@ const resolveDefault = (def: Record<string, unknown>): unknown => {
132
180
  export const zodToJsonSchema: JsonSchemaConverter = (
133
181
  schema: z.ZodType
134
182
  ): JsonSchema => {
183
+ const jsonSchemaOverride = getSchemaJsonSchemaOverride(schema);
184
+ if (jsonSchemaOverride !== undefined) {
185
+ return jsonSchemaOverride;
186
+ }
187
+
135
188
  const s = schema as unknown as ZodInternals;
136
189
 
137
190
  const collectObjectFields = (shape: Record<string, ZodInternals>) => {
@@ -197,6 +250,10 @@ export const zodToJsonSchema: JsonSchemaConverter = (
197
250
  const inner = value._zod.def['innerType'] as unknown as z.ZodType;
198
251
  return zodToJsonSchema(inner);
199
252
  },
253
+ readonly: (value) => {
254
+ const inner = value._zod.def['innerType'] as unknown as z.ZodType;
255
+ return zodToJsonSchema(inner);
256
+ },
200
257
  string: () => ({ type: 'string' }),
201
258
  union: (value) => {
202
259
  const options = value._zod.def['options'] as unknown as z.ZodType[];