@ontrails/core 1.0.0-beta.2 → 1.0.0-beta.21

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 +283 -5
  2. package/README.md +113 -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 +59 -18
  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 +309 -48
  19. package/src/execute.ts +1577 -0
  20. package/src/fire.ts +1169 -0
  21. package/src/index.ts +477 -32
  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 +36 -4
  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 +119 -39
  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 -57
  47. package/src/tracing.ts +345 -0
  48. package/src/trail.ts +1161 -29
  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 +208 -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 +442 -35
  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 -122
  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 -34
  117. package/dist/index.d.ts.map +0 -1
  118. package/dist/index.js +0 -38
  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 -145
  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 -115
  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 -74
  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 -108
  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 -159
  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 -236
  234. package/src/__tests__/topo.test.ts +0 -184
  235. package/src/__tests__/trail.test.ts +0 -179
  236. package/src/__tests__/validate-topo.test.ts +0 -201
  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
package/src/types.ts CHANGED
@@ -1,4 +1,78 @@
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 { ComposeInput, TrailOutput } from './type-utils.js';
7
+ import type { ActivationProvenance } from './activation-provenance.js';
8
+ import type { TrailVersionReference } from './version-resolution.js';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Detour
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /** A recovery path that activates when a trail's blaze fails with a matching error. */
15
+ export interface Detour<Input, Output, TErr extends TrailsError = TrailsError> {
16
+ /* oxlint-disable-next-line no-explicit-any -- standard pattern for matching abstract+concrete class constructors */
17
+ readonly on: abstract new (...args: any[]) => TErr;
18
+ readonly maxAttempts?: number | undefined;
19
+ readonly recover: (
20
+ attempt: DetourAttempt<Input, TErr>,
21
+ ctx: TrailContext
22
+ ) => Promise<Result<Output, TrailsError>>;
23
+ }
24
+
25
+ /** Context passed to a detour's recover function on each attempt. */
26
+ export interface DetourAttempt<Input, TErr extends TrailsError = TrailsError> {
27
+ /** 1-indexed attempt number */
28
+ readonly attempt: number;
29
+ /** The matched error */
30
+ readonly error: TErr;
31
+ /** Original trail input */
32
+ readonly input: Input;
33
+ }
34
+
35
+ type ComposeBatchCall<TTarget extends AnyTrail | string = AnyTrail | string> =
36
+ TTarget extends AnyTrail
37
+ ? readonly [trail: TTarget, input: ComposeInput<TTarget>]
38
+ : readonly [id: string, input: unknown];
39
+
40
+ type ComposeBatchResult<TTarget extends AnyTrail | string> =
41
+ TTarget extends AnyTrail
42
+ ? Result<TrailOutput<TTarget>, Error>
43
+ : Result<unknown, Error>;
44
+
45
+ type ComposeBatchResults<TCalls extends readonly ComposeBatchCall[]> = {
46
+ readonly [K in keyof TCalls]: TCalls[K] extends readonly [
47
+ infer TTarget,
48
+ unknown,
49
+ ]
50
+ ? TTarget extends AnyTrail | string
51
+ ? ComposeBatchResult<TTarget>
52
+ : never
53
+ : never;
54
+ };
55
+
56
+ /** Runtime options for batch `ctx.compose([...])` calls. */
57
+ export interface ComposeBatchOptions {
58
+ /**
59
+ * Maximum number of branches to execute concurrently.
60
+ *
61
+ * Omit for unbounded concurrency. `1` is equivalent to sequential execution.
62
+ */
63
+ readonly concurrency?: number | undefined;
64
+ }
65
+
66
+ /** Runtime options for a single `ctx.compose(trail, input, options)` call. */
67
+ export interface ComposeOptions {
68
+ /**
69
+ * Execute a specific live version of the composed trail.
70
+ *
71
+ * Omit to keep composition current by default. Historical revision entries
72
+ * transpose through the current trail; fork entries run their own blaze.
73
+ */
74
+ readonly version?: TrailVersionReference | undefined;
75
+ }
2
76
 
3
77
  /**
4
78
  * Trail implementation — sync or async.
@@ -6,16 +80,76 @@ import type { Result } from './result.js';
6
80
  * Authors can return `Result` directly or wrap it in a `Promise`. The framework
7
81
  * normalizes with `await` at every call site, so both forms work transparently.
8
82
  */
9
- export type Implementation<I, O> = (
83
+ export type Implementation<I, O, Ctx extends TrailContext = TrailContext> = (
10
84
  input: I,
11
- ctx: TrailContext
85
+ ctx: Ctx
12
86
  ) => Result<O, Error> | Promise<Result<O, Error>>;
13
87
 
14
- /** Invoke another trail by id — used for trail composition */
15
- export type FollowFn = <O>(
16
- id: string,
17
- input: unknown
18
- ) => Promise<Result<O, Error>>;
88
+ /**
89
+ * Invoke another trail — used for trail composition.
90
+ *
91
+ * Two call shapes:
92
+ *
93
+ * - **By trail object** (typed): `ctx.compose(showGist, { id })` — the compiler
94
+ * infers `I` and `O` from the trail's schemas, so the result is fully typed.
95
+ * - **By string id** (untyped escape hatch): `ctx.compose('gist.show', { id })`
96
+ * — returns `Result<O, Error>` where `O` defaults to `unknown`.
97
+ * - **By batch**: `ctx.compose([[showGist, { id }], ['audit.log', payload]])`
98
+ * — executes every composing concurrently and resolves once all results are
99
+ * available. Result ordering always matches the input tuple ordering. Pass
100
+ * `{ concurrency: N }` as the second argument to limit how many branches
101
+ * run at once.
102
+ */
103
+ export interface ComposeFn {
104
+ <const TCalls extends readonly ComposeBatchCall[]>(
105
+ calls: TCalls,
106
+ options?: ComposeBatchOptions
107
+ ): Promise<ComposeBatchResults<TCalls>>;
108
+ <T extends AnyTrail>(
109
+ trail: T,
110
+ input: ComposeInput<T>,
111
+ options?: ComposeOptions
112
+ ): Promise<Result<TrailOutput<T>, Error>>;
113
+ <O = unknown>(
114
+ id: string,
115
+ input: unknown,
116
+ options?: ComposeOptions
117
+ ): Promise<Result<O, Error>>;
118
+ }
119
+
120
+ /**
121
+ * Emit a signal — used for signal-driven activation.
122
+ *
123
+ * Fan-out to consumer trails (those with the signal in their `on:` array) is
124
+ * the framework's responsibility. Producers call with a `Signal<T>` value and
125
+ * get best-effort `Promise<void>` semantics: payload validation, missing topo
126
+ * entries, guard suppression, and consumer failures are observable through
127
+ * diagnostics/logging but do not become producer-facing `Result` plumbing.
128
+ * Consumers fan out in parallel, each with its own derived context. Runtime
129
+ * cycle suppression is still signal-id-based against the current fire stack:
130
+ * it prevents re-entrant loops but can over-suppress legitimate diamond
131
+ * re-fires, with a debug breadcrumb and a warn emitted when suppression
132
+ * happens.
133
+ */
134
+ export type FireFn = <T>(signal: Signal<T>, payload: T) => Promise<void>;
135
+
136
+ /** Resolve a resource instance from the current trail context. */
137
+ export type ResourceLookup = <T = unknown>(
138
+ resourceOrId: { readonly id: string } | string
139
+ ) => T;
140
+
141
+ /**
142
+ * Wrap the execution of `fn` in a child trace span.
143
+ *
144
+ * Creates a nested span under the current trail's root trace record, times
145
+ * the callback, records success or failure (including error category), and
146
+ * writes the completed span to the registered sink. Errors thrown by `fn`
147
+ * are recorded on the span and then rethrown — tracing never swallows them.
148
+ */
149
+ export type TraceFn = <T>(
150
+ label: string,
151
+ fn: () => T | Promise<T>
152
+ ) => Promise<T>;
19
153
 
20
154
  /** Callback for reporting progress from long-running trails */
21
155
  export type ProgressCallback = (event: ProgressEvent) => void;
@@ -41,18 +175,126 @@ export interface Logger {
41
175
  child(context: Record<string, unknown>): Logger;
42
176
  }
43
177
 
178
+ export type LogLevel =
179
+ | 'debug'
180
+ | 'error'
181
+ | 'fatal'
182
+ | 'info'
183
+ | 'silent'
184
+ | 'trace'
185
+ | 'warn';
186
+
187
+ export interface LogRecord {
188
+ readonly category: string;
189
+ readonly level: LogLevel;
190
+ readonly message: string;
191
+ readonly metadata: Record<string, unknown>;
192
+ readonly timestamp: Date;
193
+ }
194
+
195
+ export interface LogSink {
196
+ readonly name: string;
197
+ readonly write: (record: LogRecord) => void;
198
+ readonly flush?: (() => Promise<void>) | undefined;
199
+ }
200
+
201
+ export interface LogFormatter {
202
+ format(record: LogRecord): string;
203
+ }
204
+
205
+ /**
206
+ * Context extension key for the invoking surface name.
207
+ */
208
+ export const SURFACE_KEY = '__trails_surface' as const;
209
+
210
+ /**
211
+ * Context extension key for the layer names attached by the invoking surface.
212
+ */
213
+ export const SURFACE_LAYER_NAMES_KEY = '__trails_surface_layer_names' as const;
214
+
215
+ /**
216
+ * Context extension key carrying per-layer runtime input.
217
+ *
218
+ * Surfaces (CLI, MCP, HTTP) project each typed layer's `input` schema onto
219
+ * their native idioms (flags, tool params, query strings). At execute time
220
+ * the parsed values are partitioned per layer and stored under this key as
221
+ * `Record<layerName, unknown>`. Layers that need runtime input read their
222
+ * own slot via `ctx.extensions?.[LAYER_INPUTS_KEY]?.[layer.name]`.
223
+ *
224
+ * @see TRL-473 for the CLI projection contract.
225
+ */
226
+ export const LAYER_INPUTS_KEY = '__trails_layer_inputs' as const;
227
+
44
228
  /** Runtime context threaded through every trail execution */
45
229
  export interface TrailContext {
230
+ readonly activation?: ActivationProvenance | undefined;
46
231
  readonly requestId: string;
47
- readonly signal: AbortSignal;
48
- readonly follow?: FollowFn | undefined;
49
- readonly permit?: unknown | undefined;
232
+ readonly abortSignal: AbortSignal;
233
+ readonly compose?: ComposeFn | undefined;
234
+ /**
235
+ * Emit a typed signal. Fans out to every trail with the signal in its
236
+ * `on:` declaration. Bound by the runner that holds the topo (typically
237
+ * `run()`); undefined when a context is constructed without topo access.
238
+ */
239
+ readonly fire?: FireFn | undefined;
240
+ readonly permit?: BasePermit;
50
241
  readonly workspaceRoot?: string | undefined;
51
242
  readonly logger?: Logger | undefined;
52
243
  readonly progress?: ProgressCallback | undefined;
53
244
  readonly cwd?: string | undefined;
54
245
  readonly env?: Record<string, string | undefined> | undefined;
55
- readonly [key: string]: unknown;
246
+ readonly extensions?: Readonly<Record<string, unknown>> | undefined;
247
+ readonly resource?: ResourceLookup | undefined;
248
+ /**
249
+ * Whether the current invocation is a dry run.
250
+ *
251
+ * Defaults to `false`. Trails that don't read this field are unaffected.
252
+ * Trails that do read it decide what dry-run means for their domain — for
253
+ * example: preview the change without committing, validate inputs without
254
+ * performing side effects, or return what would happen without actually
255
+ * doing it.
256
+ *
257
+ * The framework only carries the flag from the surface (e.g. CLI
258
+ * `--dry-run`) into the context. It never short-circuits trail execution
259
+ * on its own based on this field.
260
+ *
261
+ * Pair this runtime signal with `TrailSpec.dryRun`, which declares whether a
262
+ * trail supports dry-run semantics for governance, derivation, and surface
263
+ * tooling.
264
+ *
265
+ * @remarks Always defined on contexts produced by `executeTrail` or
266
+ * `createTrailContext` (normalized to `false` when not provided).
267
+ */
268
+ readonly dryRun?: boolean | undefined;
269
+ /**
270
+ * Wrap a callback in a child trace span.
271
+ *
272
+ * Always present on contexts produced by `executeTrail` or
273
+ * `createTrailContext`. Optional on the interface so manually constructed
274
+ * contexts (tests, ad-hoc compositions) don't have to supply one — call
275
+ * sites tolerate `undefined` by falling back to a no-op passthrough.
276
+ */
277
+ readonly trace?: TraceFn | undefined;
56
278
  }
57
279
 
58
- export type Surface = 'cli' | 'mcp' | 'http' | 'ws';
280
+ /** Trail context for blazes that declare trail composition. */
281
+ export interface ComposeTrailContext extends TrailContext {
282
+ readonly compose: ComposeFn;
283
+ }
284
+
285
+ /**
286
+ * Permit requirement declared on a trail spec.
287
+ *
288
+ * A scopes object means the trail requires a permit with those scopes.
289
+ * `'public'` means the trail has explicitly opted out of auth.
290
+ * Omitting the field entirely means the trail hasn't declared an auth posture.
291
+ */
292
+ export type PermitRequirement =
293
+ | { readonly scopes: readonly string[] }
294
+ | 'public';
295
+
296
+ /** Input shape used to seed a runtime TrailContext before resolution. */
297
+ export type TrailContextInit = Omit<TrailContext, 'resource' | 'trace'> & {
298
+ readonly resource?: ResourceLookup | undefined;
299
+ readonly trace?: TraceFn | undefined;
300
+ };
@@ -0,0 +1,69 @@
1
+ import { ValidationError } from './errors.js';
2
+ import { Result } from './result.js';
3
+ import type { Topo } from './topo.js';
4
+ import { validateDraftFreeTopo } from './draft.js';
5
+ import type { TopoIssue } from './validate-topo.js';
6
+ import { validateTopo } from './validate-topo.js';
7
+
8
+ const PROJECTION_BLOCKING_RULES = new Set([
9
+ 'compose-cycle',
10
+ 'compose-exists',
11
+ 'no-self-compose',
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',
19
+ 'signal-origin-exists',
20
+ ]);
21
+
22
+ const keepProjectionBlockingIssues = (
23
+ result: ReturnType<typeof validateTopo>
24
+ ) => {
25
+ if (result.isOk()) {
26
+ return result;
27
+ }
28
+
29
+ const issues = (
30
+ result.error.context as { issues?: readonly TopoIssue[] } | undefined
31
+ )?.issues;
32
+ const remainingIssues = issues?.filter((issue) =>
33
+ PROJECTION_BLOCKING_RULES.has(issue.rule)
34
+ );
35
+
36
+ if (remainingIssues === undefined || remainingIssues.length === 0) {
37
+ return Result.ok();
38
+ }
39
+
40
+ return Result.err(
41
+ new ValidationError(
42
+ `Topo validation failed with ${remainingIssues.length} issue(s)`,
43
+ {
44
+ cause: result.error,
45
+ context: { issues: remainingIssues },
46
+ }
47
+ )
48
+ );
49
+ };
50
+
51
+ /**
52
+ * Validate that a topo is ready for established outputs.
53
+ *
54
+ * Established surfaces still require the authored graph to be structurally
55
+ * valid, and they must also reject any remaining draft state.
56
+ */
57
+ export const validateEstablishedTopo = (topo: Topo) => {
58
+ const structural = keepProjectionBlockingIssues(validateTopo(topo));
59
+ if (structural.isErr()) {
60
+ return structural;
61
+ }
62
+
63
+ const established = validateDraftFreeTopo(topo);
64
+ if (established.isErr()) {
65
+ return established;
66
+ }
67
+
68
+ return Result.ok();
69
+ };