@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/trail.ts CHANGED
@@ -1,9 +1,28 @@
1
1
  import type { z } from 'zod';
2
2
 
3
+ import type {
4
+ ActivationEntry,
5
+ ActivationEntrySpec,
6
+ ActivationSource,
7
+ ActivationSourceRef,
8
+ } from './activation-source.js';
9
+ import {
10
+ isActivationEntrySpec,
11
+ isActivationSource,
12
+ } from './activation-source.js';
13
+ import type { AnyContour } from './contour.js';
3
14
  import type { FieldOverride } from './derive.js';
15
+ import type { Layer } from './layer.js';
4
16
  import type { Result } from './result.js';
5
- import type { AnyProvision } from './provision.js';
17
+ import type { AnyResource } from './resource.js';
18
+ import type { AnySignal } from './signal.js';
19
+ import {
20
+ createLateBoundSignalMarker,
21
+ getLateBoundSignalRef,
22
+ } from './signal-ref.js';
23
+ import type { TrailsError } from './errors.js';
6
24
  import type {
25
+ Detour,
7
26
  Implementation,
8
27
  PermitRequirement,
9
28
  TrailContext,
@@ -13,6 +32,17 @@ import type {
13
32
  // Trail example
14
33
  // ---------------------------------------------------------------------------
15
34
 
35
+ export interface TrailExampleSignalAssertion {
36
+ /** Signal contract object or stable signal ID expected during the example. */
37
+ readonly signal: AnySignal | string;
38
+ /** Exact payload assertion for the fired signal. */
39
+ readonly payload?: unknown | undefined;
40
+ /** Partial payload assertion; declared fields must match, extras ignored. */
41
+ readonly payloadMatch?: unknown | undefined;
42
+ /** Number of matching fired signals expected. Defaults to one. */
43
+ readonly times?: number | undefined;
44
+ }
45
+
16
46
  /**
17
47
  * A named example for documentation and testing.
18
48
  *
@@ -27,73 +57,337 @@ export interface TrailExample<I, O> {
27
57
  readonly description?: string | undefined;
28
58
  /** The input value — fields with schema defaults may be omitted */
29
59
  readonly input: Partial<I>;
30
- /** Expected output for success-path examples */
60
+ /** Expected output for success-path examples (deep equality) */
31
61
  readonly expected?: O | undefined;
62
+ /** Partial output assertion — declared fields must match, others ignored */
63
+ readonly expectedMatch?: Partial<O> | undefined;
32
64
  /** Error class name for error-path examples */
33
65
  readonly error?: string | undefined;
66
+ /** Signal fires expected while executing this example. */
67
+ readonly signals?: readonly TrailExampleSignalAssertion[] | undefined;
34
68
  }
35
69
 
70
+ // ---------------------------------------------------------------------------
71
+ // Blaze input — merges crossInput when declared
72
+ // ---------------------------------------------------------------------------
73
+
74
+ /**
75
+ * The input type received by a trail's blaze function.
76
+ *
77
+ * When a trail declares `crossInput`, the runtime merges those fields into
78
+ * the input object before calling blaze. This type makes the compiler aware
79
+ * of the merged shape so developers can access crossInput fields without a
80
+ * cast. Falls back to plain `I` when `CI` is `never` (the default).
81
+ */
82
+ export type BlazeInput<I, CI> = [CI] extends [never] ? I : I & CI;
83
+
36
84
  // ---------------------------------------------------------------------------
37
85
  // Trail spec
38
86
  // ---------------------------------------------------------------------------
39
87
 
40
88
  /** Everything needed to define a trail (minus the id) */
41
- export interface TrailSpec<I, O> {
89
+ export interface TrailSpec<I, O, CI = never> {
42
90
  /** Zod schema for validating input */
43
91
  readonly input: z.ZodType<I>;
44
92
  /** Zod schema for validating output (optional — some trails are fire-and-forget) */
45
93
  readonly output?: z.ZodType<O> | undefined;
46
94
  /** The pure function that does the work (sync or async authoring) */
47
- readonly blaze: Implementation<I, O>;
95
+ readonly blaze: Implementation<BlazeInput<I, CI>, O>;
48
96
  /** Human-readable description */
49
97
  readonly description?: string | undefined;
98
+ /** Declared operational shape for governance, derivation, and agent guidance. */
99
+ readonly pattern?: string | undefined;
50
100
  /** Named examples for docs and testing */
51
101
  readonly examples?: readonly TrailExample<I, O>[] | undefined;
52
102
  /** What this trail does to the world: read, write (default), or destroy */
53
103
  readonly intent?: 'read' | 'write' | 'destroy' | undefined;
54
104
  /** Trail is idempotent (safe to retry) */
55
105
  readonly idempotent?: boolean | undefined;
106
+ /**
107
+ * Trail explicitly supports dry-run execution semantics.
108
+ *
109
+ * This is a declaration for governance, derivation, and surface tooling. It
110
+ * does not change runtime behavior by itself; the active invocation signal is
111
+ * `TrailContext.dryRun`.
112
+ */
113
+ readonly dryRun?: boolean | undefined;
114
+ /** Whether surfaces expose this trail by default. */
115
+ readonly visibility?: TrailVisibility | undefined;
56
116
  /** Arbitrary meta for tooling and filtering */
57
117
  readonly meta?: Readonly<Record<string, unknown>> | undefined;
58
- /** Named sets of downstream trail IDs that may be invoked */
59
- readonly detours?: Readonly<Record<string, readonly string[]>> | undefined;
118
+ /** Recovery paths activated when blaze fails with a matching error class. */
119
+ readonly detours?: readonly Detour<I, O, TrailsError>[] | undefined;
120
+ /**
121
+ * Typed layers attached at trail scope.
122
+ *
123
+ * Layers declared here wrap this trail's implementation on every execution,
124
+ * regardless of which surface invokes it. The execution pipeline composes
125
+ * trail-scope layers innermost — closer to the blaze than surface-scope
126
+ * or topo-scope layers — so the final order is
127
+ * `topo → surface → trail → blaze` (outermost-first).
128
+ *
129
+ * Layers are typed and inspectable. Omit `input` for surface-invisible
130
+ * wrappers that do not project any fields.
131
+ */
132
+ readonly layers?: readonly Layer[] | undefined;
60
133
  /** Per-field overrides for deriveFields() (labels, hints, options) */
61
134
  readonly fields?: Readonly<Record<string, FieldOverride>> | undefined;
62
- /** IDs of downstream trails this trail may invoke via ctx.cross() */
63
- readonly crosses?: readonly string[] | undefined;
64
- /** Provisions this trail may access via provision.from(ctx) */
65
- readonly provisions?: readonly AnyProvision[] | undefined;
135
+ /** Contours this trail operates on. */
136
+ readonly contours?: readonly AnyContour[] | undefined;
137
+ /** IDs or trail objects of downstream trails this trail may invoke via ctx.cross() */
138
+ readonly crosses?: readonly (string | AnyTrail)[] | undefined;
139
+ /**
140
+ * Composition-only input schema — merged with `input` for `ctx.cross()` calls,
141
+ * invisible to public surfaces (CLI, MCP, HTTP).
142
+ *
143
+ * Fields here are available in the blaze but are not derived into CLI flags,
144
+ * MCP tool parameters, or HTTP request bodies. Use for data that only makes
145
+ * sense when one trail crosses another (e.g. `forkedFrom`).
146
+ */
147
+ readonly crossInput?: z.ZodType<CI> | undefined;
148
+ /** Resources this trail may access via resource.from(ctx) */
149
+ readonly resources?: readonly AnyResource[] | undefined;
150
+ /**
151
+ * Signals this trail fires via `ctx.fire()`.
152
+ *
153
+ * Accepts either a string id or a `Signal` value. Both forms are
154
+ * normalized to the signal's id at trail definition time, so
155
+ * `trail.fires` is always `readonly string[]`.
156
+ *
157
+ * Note: `crosses` also accepts trail objects (normalized to IDs),
158
+ * following the same pattern as signal references here.
159
+ */
160
+ readonly fires?: readonly (string | AnySignal)[] | undefined;
161
+ /**
162
+ * Activation sources that can invoke this trail.
163
+ *
164
+ * Bare strings and `Signal` values are signal-source shorthand. Object form
165
+ * preserves the source kind and per-source metadata for the activation graph.
166
+ */
167
+ readonly on?:
168
+ | readonly (ActivationEntrySpec | ActivationSourceRef)[]
169
+ | undefined;
66
170
  /** Auth requirement: scopes object, 'public', or omitted (undeclared) */
67
171
  readonly permit?: PermitRequirement | undefined;
172
+ /** Primary input fields and their order. CLI projects as positional args. */
173
+ readonly args?: readonly string[] | false | undefined;
68
174
  }
69
175
 
70
176
  // ---------------------------------------------------------------------------
71
177
  // Trail (the frozen runtime object)
72
178
  // ---------------------------------------------------------------------------
73
179
 
74
- /** Intent describes what a trail does to the world */
75
- export type Intent = 'read' | 'write' | 'destroy';
180
+ /** Intent describes what a trail does to the world. */
181
+ export const intentValues = Object.freeze([
182
+ 'read',
183
+ 'write',
184
+ 'destroy',
185
+ ] as const);
186
+
187
+ export type Intent = (typeof intentValues)[number];
188
+
189
+ /** Whether surfaces expose a trail by default. */
190
+ export type TrailVisibility = 'public' | 'internal';
76
191
 
77
192
  /** A fully-defined trail — the unit of work in the Trails system */
78
- export interface Trail<I, O> extends Omit<
79
- TrailSpec<I, O>,
80
- 'blaze' | 'crosses' | 'intent' | 'provisions'
193
+ export interface Trail<I, O, CI = never> extends Omit<
194
+ TrailSpec<I, O, CI>,
195
+ | 'args'
196
+ | 'blaze'
197
+ | 'contours'
198
+ | 'crosses'
199
+ | 'crossInput'
200
+ | 'detours'
201
+ | 'fires'
202
+ | 'intent'
203
+ | 'layers'
204
+ | 'on'
205
+ | 'resources'
81
206
  > {
82
207
  readonly kind: 'trail';
83
208
  readonly id: string;
84
- readonly blaze: Implementation<I, O>;
209
+ readonly blaze: Implementation<BlazeInput<I, CI>, O>;
210
+ /** Contours this trail operates on (always present, default []). */
211
+ readonly contours: readonly AnyContour[];
85
212
  /** IDs of downstream trails this trail may invoke via ctx.cross() (always present, default []) */
86
213
  readonly crosses: readonly string[];
87
- /** Provisions this trail may access via provision.from(ctx) (always present, default []) */
88
- readonly provisions: readonly AnyProvision[];
214
+ /** Composition-only input schema, merged with `input` for ctx.cross() calls (optional) */
215
+ readonly crossInput?: z.ZodType<CI> | undefined;
216
+ /** Recovery paths activated when blaze fails with a matching error (always present, default []). */
217
+ readonly detours: readonly Detour<I, O, TrailsError>[];
218
+ /**
219
+ * Typed layers attached at trail scope (always present, default []).
220
+ *
221
+ * Composed innermost in the layer chain — closest to the blaze. The final
222
+ * composition order is `topo → surface → trail → blaze` (outermost-first).
223
+ */
224
+ readonly layers: readonly Layer[];
225
+ /** Resources this trail may access via resource.from(ctx) (always present, default []) */
226
+ readonly resources: readonly AnyResource[];
227
+ /** IDs of signals this trail fires via ctx.fire() (always present, default []) */
228
+ readonly fires: readonly string[];
229
+ /**
230
+ * IDs of signal sources that activate this trail (always present, default []).
231
+ * Non-signal activation sources live in `activationSources`.
232
+ */
233
+ readonly on: readonly string[];
234
+ /** Normalized activation source entries declared through `on` (always present, default []). */
235
+ readonly activationSources: readonly ActivationEntry[];
89
236
  /** What this trail does to the world (always present, default 'write') */
90
237
  readonly intent: Intent;
238
+ /** Whether surfaces expose this trail by default (always present, default 'public'). */
239
+ readonly visibility: TrailVisibility;
240
+ /** Primary input fields and their order (always present, default undefined) */
241
+ readonly args?: readonly string[] | false | undefined;
91
242
  }
92
243
 
93
244
  // ---------------------------------------------------------------------------
94
245
  // Factory
95
246
  // ---------------------------------------------------------------------------
96
247
 
248
+ /**
249
+ * Canonical scoped-signal id shape: `<scope>:<table>.<event>`.
250
+ *
251
+ * Matches exactly one `:` separating a non-empty scope from a non-empty
252
+ * dotted tail of at least two segments (e.g. `identity:users.created`).
253
+ * The scope forbids only `:` and whitespace so resource ids may contain
254
+ * dots for namespacing (e.g. `demo.store:gists.created`). Tail segments
255
+ * forbid both `:` and `.` so strings like `foo:bar` (no dot) or
256
+ * `a:b.c.d` stay unambiguous.
257
+ */
258
+ const SCOPED_SIGNAL_ID = /^[^:\s]+:[^:.\s]+(?:\.[^:.\s]+)+$/;
259
+
260
+ const normalizeSignalRef = (entry: string | AnySignal): string => {
261
+ if (typeof entry === 'string') {
262
+ return entry;
263
+ }
264
+
265
+ const ref = getLateBoundSignalRef(entry);
266
+ if (!ref) {
267
+ return entry.id;
268
+ }
269
+
270
+ // Already-scoped canonical ids (e.g. "identity:users.created") must pass
271
+ // through unchanged. Rewriting them to a bare marker token collapses
272
+ // multi-binding cases where the same store definition is bound under two
273
+ // separate resources: both bindings share the late-bound token, so the
274
+ // caller's explicit choice of scope would be lost and topo resolution
275
+ // would throw an ambiguity error.
276
+ //
277
+ // Use a strict predicate that matches the canonical scoped shape
278
+ // `<scope>:<table>.<event>` (exactly one `:` separating a non-empty scope
279
+ // from a non-empty dotted tail of at least two segments). A looser
280
+ // `includes(':')` check would let unscoped ids that happen to contain `:`
281
+ // slip past markerization and then fail to resolve at topo finalization.
282
+ if (SCOPED_SIGNAL_ID.test(entry.id)) {
283
+ return entry.id;
284
+ }
285
+
286
+ return createLateBoundSignalMarker(ref, entry.id);
287
+ };
288
+
289
+ const freezeActivationSource = (source: ActivationSource): ActivationSource =>
290
+ Object.freeze({
291
+ ...source,
292
+ ...(source.meta === undefined
293
+ ? {}
294
+ : { meta: Object.freeze({ ...source.meta }) }),
295
+ });
296
+
297
+ const shouldPreserveSignalSource = (source: ActivationSource): boolean =>
298
+ source.kind === 'signal' &&
299
+ (!('payload' in source) ||
300
+ 'input' in source ||
301
+ 'parse' in source ||
302
+ 'cron' in source ||
303
+ 'timezone' in source);
304
+
305
+ const normalizeActivationSource = (
306
+ source: ActivationSourceRef
307
+ ): ActivationSource => {
308
+ if (typeof source === 'string') {
309
+ return freezeActivationSource({ id: source, kind: 'signal' });
310
+ }
311
+
312
+ if (isActivationSource(source) && shouldPreserveSignalSource(source)) {
313
+ return freezeActivationSource({
314
+ ...source,
315
+ id: normalizeSignalRef(source.id),
316
+ kind: 'signal',
317
+ });
318
+ }
319
+
320
+ if (isActivationSource(source) && source.kind !== 'signal') {
321
+ return freezeActivationSource(source);
322
+ }
323
+
324
+ return freezeActivationSource({
325
+ id: normalizeSignalRef(source as string | AnySignal),
326
+ kind: 'signal',
327
+ });
328
+ };
329
+
330
+ const normalizeActivationEntry = (
331
+ entry: ActivationEntrySpec | ActivationSourceRef
332
+ ): ActivationEntry => {
333
+ const source = isActivationEntrySpec(entry) ? entry.source : entry;
334
+ const normalized: ActivationEntry = {
335
+ source: normalizeActivationSource(source),
336
+ ...(isActivationEntrySpec(entry) && entry.meta !== undefined
337
+ ? { meta: Object.freeze({ ...entry.meta }) }
338
+ : {}),
339
+ ...(isActivationEntrySpec(entry) && entry.where !== undefined
340
+ ? { where: entry.where }
341
+ : {}),
342
+ };
343
+
344
+ return Object.freeze(normalized);
345
+ };
346
+
347
+ const normalizeActivationSources = (
348
+ entries: readonly (ActivationEntrySpec | ActivationSourceRef)[]
349
+ ): readonly ActivationEntry[] =>
350
+ Object.freeze(entries.map((entry) => normalizeActivationEntry(entry)));
351
+
352
+ const extractSignalActivationIds = (
353
+ activations: readonly ActivationEntry[]
354
+ ): readonly string[] =>
355
+ Object.freeze(
356
+ activations
357
+ .filter((entry) => entry.source.kind === 'signal')
358
+ .map((entry) => entry.source.id)
359
+ );
360
+
361
+ /** Normalize a crosses entry — trail objects are reduced to their id. */
362
+ const normalizeCrossRef = (entry: string | AnyTrail): string =>
363
+ typeof entry === 'string' ? entry : entry.id;
364
+
365
+ /** Freeze and normalize all collection fields from a trail spec. */
366
+ const normalizeCollections = <I, O, CI>(
367
+ spec: TrailSpec<I, O, CI>
368
+ ): {
369
+ readonly args: readonly string[] | false | undefined;
370
+ readonly activationSources: readonly ActivationEntry[];
371
+ readonly contours: readonly AnyContour[];
372
+ readonly detours: readonly Detour<I, O, TrailsError>[];
373
+ readonly fires: readonly string[];
374
+ readonly layers: readonly Layer[];
375
+ readonly on: readonly string[];
376
+ readonly resources: readonly AnyResource[];
377
+ } => {
378
+ const activationSources = normalizeActivationSources(spec.on ?? []);
379
+ return {
380
+ activationSources,
381
+ args: Array.isArray(spec.args) ? Object.freeze([...spec.args]) : spec.args,
382
+ contours: Object.freeze([...(spec.contours ?? [])]),
383
+ detours: Object.freeze([...(spec.detours ?? [])]),
384
+ fires: Object.freeze((spec.fires ?? []).map(normalizeSignalRef)),
385
+ layers: Object.freeze([...(spec.layers ?? [])]),
386
+ on: extractSignalActivationIds(activationSources),
387
+ resources: Object.freeze([...(spec.resources ?? [])]),
388
+ };
389
+ };
390
+
97
391
  /**
98
392
  * Create a trail definition.
99
393
  *
@@ -116,14 +410,17 @@ export interface Trail<I, O> extends Omit<
116
410
  * });
117
411
  * ```
118
412
  */
119
- export function trail<I, O>(id: string, spec: TrailSpec<I, O>): Trail<I, O>;
120
- export function trail<I, O>(
121
- spec: TrailSpec<I, O> & { readonly id: string }
122
- ): Trail<I, O>;
123
- export function trail<I, O>(
124
- idOrSpec: string | (TrailSpec<I, O> & { readonly id: string }),
125
- maybeSpec?: TrailSpec<I, O>
126
- ): Trail<I, O> {
413
+ export function trail<I, O, CI = never>(
414
+ id: string,
415
+ spec: TrailSpec<I, O, CI>
416
+ ): Trail<I, O, CI>;
417
+ export function trail<I, O, CI = never>(
418
+ spec: TrailSpec<I, O, CI> & { readonly id: string }
419
+ ): Trail<I, O, CI>;
420
+ export function trail<I, O, CI = never>(
421
+ idOrSpec: string | (TrailSpec<I, O, CI> & { readonly id: string }),
422
+ maybeSpec?: TrailSpec<I, O, CI>
423
+ ): Trail<I, O, CI> {
127
424
  const resolved =
128
425
  typeof idOrSpec === 'string'
129
426
  ? { id: idOrSpec, spec: maybeSpec }
@@ -135,26 +432,43 @@ export function trail<I, O>(
135
432
 
136
433
  const {
137
434
  blaze,
435
+ crossInput,
138
436
  crosses: rawCrosses,
139
437
  intent: rawIntent,
140
- provisions: rawProvisions,
438
+ visibility: rawVisibility,
439
+ // Destructure away fields handled by normalizeCollections
440
+ args: _a,
441
+ contours: _c,
442
+ detours: _d,
443
+ fires: _f,
444
+ layers: _l,
445
+ on: _o,
446
+ resources: _r,
141
447
  ...spec
142
448
  } = resolved.spec;
143
- const provisions = Object.freeze([...(rawProvisions ?? [])]);
449
+ const collections = normalizeCollections(resolved.spec);
144
450
 
145
451
  return Object.freeze({
146
452
  ...spec,
147
- blaze: async (input: I, ctx: TrailContext) => await blaze(input, ctx),
148
- crosses: Object.freeze([...(rawCrosses ?? [])]),
453
+ ...collections,
454
+ blaze: async (input: BlazeInput<I, CI>, ctx: TrailContext) =>
455
+ await blaze(input, ctx),
456
+ crossInput,
457
+ crosses: Object.freeze((rawCrosses ?? []).map(normalizeCrossRef)),
149
458
  id: resolved.id,
150
459
  intent: rawIntent ?? 'write',
151
460
  kind: 'trail' as const,
152
- provisions,
461
+ visibility: rawVisibility ?? 'public',
153
462
  });
154
463
  }
155
464
 
156
465
  // Re-export types that callers of trail() will need
157
- // oxlint-disable-next-line no-explicit-any -- existential type for heterogeneous collections; `any` is correct here because Implementation is contravariant in I
158
- export type AnyTrail = Trail<any, any>;
466
+ // The Omit+override avoids a TypeScript limitation where BlazeInput's conditional type
467
+ // makes Trail<any, any, any> structurally incompatible with Trail<I, O, never>.
468
+ /* oxlint-disable no-explicit-any -- existential type for heterogeneous collections */
469
+ export type AnyTrail = Omit<Trail<any, any, any>, 'blaze'> & {
470
+ readonly blaze: Implementation<any, any>;
471
+ };
472
+ /* oxlint-enable no-explicit-any */
159
473
 
160
474
  export type { Implementation, TrailContext, Result };