@ontrails/core 1.0.0-beta.13 → 1.0.0-beta.15

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 (287) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +19 -14
  4. package/dist/branded.d.ts +1 -1
  5. package/dist/branded.d.ts.map +1 -1
  6. package/dist/branded.js +1 -1
  7. package/dist/branded.js.map +1 -1
  8. package/dist/context.d.ts +13 -1
  9. package/dist/context.d.ts.map +1 -1
  10. package/dist/context.js +19 -3
  11. package/dist/context.js.map +1 -1
  12. package/dist/contour.d.ts +81 -0
  13. package/dist/contour.d.ts.map +1 -0
  14. package/dist/contour.js +176 -0
  15. package/dist/contour.js.map +1 -0
  16. package/dist/cross-schema.d.ts +18 -0
  17. package/dist/cross-schema.d.ts.map +1 -0
  18. package/dist/cross-schema.js +29 -0
  19. package/dist/cross-schema.js.map +1 -0
  20. package/dist/derive.d.ts +6 -0
  21. package/dist/derive.d.ts.map +1 -1
  22. package/dist/derive.js +29 -5
  23. package/dist/derive.js.map +1 -1
  24. package/dist/draft.d.ts +28 -0
  25. package/dist/draft.d.ts.map +1 -0
  26. package/dist/draft.js +157 -0
  27. package/dist/draft.js.map +1 -0
  28. package/dist/errors.d.ts +62 -4
  29. package/dist/errors.d.ts.map +1 -1
  30. package/dist/errors.js +44 -1
  31. package/dist/errors.js.map +1 -1
  32. package/dist/execute.d.ts +26 -9
  33. package/dist/execute.d.ts.map +1 -1
  34. package/dist/execute.js +441 -20
  35. package/dist/execute.js.map +1 -1
  36. package/dist/fire.d.ts +43 -0
  37. package/dist/fire.d.ts.map +1 -0
  38. package/dist/fire.js +185 -0
  39. package/dist/fire.js.map +1 -0
  40. package/dist/index.d.ts +33 -17
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +26 -12
  43. package/dist/index.js.map +1 -1
  44. package/dist/internal/cross-batch.d.ts +44 -0
  45. package/dist/internal/cross-batch.d.ts.map +1 -0
  46. package/dist/internal/cross-batch.js +58 -0
  47. package/dist/internal/cross-batch.js.map +1 -0
  48. package/dist/internal/fork-ctx.d.ts +41 -0
  49. package/dist/internal/fork-ctx.d.ts.map +1 -0
  50. package/dist/internal/fork-ctx.js +47 -0
  51. package/dist/internal/fork-ctx.js.map +1 -0
  52. package/dist/internal/signal-ref.d.ts +15 -0
  53. package/dist/internal/signal-ref.d.ts.map +1 -0
  54. package/dist/internal/signal-ref.js +41 -0
  55. package/dist/internal/signal-ref.js.map +1 -0
  56. package/dist/internal/topo-saves.d.ts +53 -0
  57. package/dist/internal/topo-saves.d.ts.map +1 -0
  58. package/dist/internal/topo-saves.js +449 -0
  59. package/dist/internal/topo-saves.js.map +1 -0
  60. package/dist/internal/topo-snapshots.d.ts +57 -0
  61. package/dist/internal/topo-snapshots.d.ts.map +1 -0
  62. package/dist/internal/topo-snapshots.js +372 -0
  63. package/dist/internal/topo-snapshots.js.map +1 -0
  64. package/dist/internal/topo-store-read.d.ts +70 -0
  65. package/dist/internal/topo-store-read.d.ts.map +1 -0
  66. package/dist/internal/topo-store-read.js +221 -0
  67. package/dist/internal/topo-store-read.js.map +1 -0
  68. package/dist/internal/topo-store.d.ts +26 -0
  69. package/dist/internal/topo-store.d.ts.map +1 -0
  70. package/dist/internal/topo-store.js +621 -0
  71. package/dist/internal/topo-store.js.map +1 -0
  72. package/dist/internal/tracing.d.ts +98 -0
  73. package/dist/internal/tracing.d.ts.map +1 -0
  74. package/dist/internal/tracing.js +102 -0
  75. package/dist/internal/tracing.js.map +1 -0
  76. package/dist/internal/trails-db.d.ts +16 -0
  77. package/dist/internal/trails-db.d.ts.map +1 -0
  78. package/dist/internal/trails-db.js +118 -0
  79. package/dist/internal/trails-db.js.map +1 -0
  80. package/dist/internal/zod-wrappers.d.ts +40 -0
  81. package/dist/internal/zod-wrappers.d.ts.map +1 -0
  82. package/dist/internal/zod-wrappers.js +59 -0
  83. package/dist/internal/zod-wrappers.js.map +1 -0
  84. package/dist/layer.d.ts +22 -5
  85. package/dist/layer.d.ts.map +1 -1
  86. package/dist/layer.js +20 -3
  87. package/dist/layer.js.map +1 -1
  88. package/dist/path-security.d.ts +1 -1
  89. package/dist/path-security.d.ts.map +1 -1
  90. package/dist/path-security.js +1 -1
  91. package/dist/path-security.js.map +1 -1
  92. package/dist/resilience.d.ts +1 -1
  93. package/dist/resilience.d.ts.map +1 -1
  94. package/dist/resilience.js +3 -3
  95. package/dist/resilience.js.map +1 -1
  96. package/dist/resource-config.d.ts +22 -0
  97. package/dist/resource-config.d.ts.map +1 -0
  98. package/dist/resource-config.js +210 -0
  99. package/dist/resource-config.js.map +1 -0
  100. package/dist/resource.d.ts +74 -0
  101. package/dist/resource.d.ts.map +1 -0
  102. package/dist/resource.js +61 -0
  103. package/dist/resource.js.map +1 -0
  104. package/dist/run.d.ts +4 -3
  105. package/dist/run.d.ts.map +1 -1
  106. package/dist/run.js +5 -4
  107. package/dist/run.js.map +1 -1
  108. package/dist/serialization.d.ts.map +1 -1
  109. package/dist/serialization.js +2 -1
  110. package/dist/serialization.js.map +1 -1
  111. package/dist/store/accessor-protocol.d.ts +51 -0
  112. package/dist/store/accessor-protocol.d.ts.map +1 -0
  113. package/dist/store/accessor-protocol.js +2 -0
  114. package/dist/store/accessor-protocol.js.map +1 -0
  115. package/dist/store/index.d.ts +2 -0
  116. package/dist/store/index.d.ts.map +1 -0
  117. package/dist/store/index.js +2 -0
  118. package/dist/store/index.js.map +1 -0
  119. package/dist/surface-filter.d.ts +13 -0
  120. package/dist/surface-filter.d.ts.map +1 -0
  121. package/dist/surface-filter.js +76 -0
  122. package/dist/surface-filter.js.map +1 -0
  123. package/dist/topo-store.d.ts +62 -0
  124. package/dist/topo-store.d.ts.map +1 -0
  125. package/dist/topo-store.js +319 -0
  126. package/dist/topo-store.js.map +1 -0
  127. package/dist/topo.d.ts +23 -9
  128. package/dist/topo.d.ts.map +1 -1
  129. package/dist/topo.js +175 -46
  130. package/dist/topo.js.map +1 -1
  131. package/dist/trail.d.ts +86 -19
  132. package/dist/trail.d.ts.map +1 -1
  133. package/dist/trail.js +59 -4
  134. package/dist/trail.js.map +1 -1
  135. package/dist/trails/derive-trail.d.ts +66 -0
  136. package/dist/trails/derive-trail.d.ts.map +1 -0
  137. package/dist/trails/derive-trail.js +378 -0
  138. package/dist/trails/derive-trail.js.map +1 -0
  139. package/dist/trails/index.d.ts +5 -0
  140. package/dist/trails/index.d.ts.map +1 -0
  141. package/dist/trails/index.js +3 -0
  142. package/dist/trails/index.js.map +1 -0
  143. package/dist/trails/ingest.d.ts +24 -0
  144. package/dist/trails/ingest.d.ts.map +1 -0
  145. package/dist/trails/ingest.js +63 -0
  146. package/dist/trails/ingest.js.map +1 -0
  147. package/dist/transport-error-map.d.ts +106 -0
  148. package/dist/transport-error-map.d.ts.map +1 -0
  149. package/dist/transport-error-map.js +24 -0
  150. package/dist/transport-error-map.js.map +1 -0
  151. package/dist/type-checks.test-d.d.ts +71 -0
  152. package/dist/type-checks.test-d.d.ts.map +1 -0
  153. package/dist/type-checks.test-d.js +12 -0
  154. package/dist/type-checks.test-d.js.map +1 -0
  155. package/dist/type-utils.d.ts +11 -2
  156. package/dist/type-utils.d.ts.map +1 -1
  157. package/dist/type-utils.js.map +1 -1
  158. package/dist/types.d.ts +113 -7
  159. package/dist/types.d.ts.map +1 -1
  160. package/dist/types.js.map +1 -1
  161. package/dist/validate-established-topo.d.ts +76 -0
  162. package/dist/validate-established-topo.d.ts.map +1 -0
  163. package/dist/validate-established-topo.js +43 -0
  164. package/dist/validate-established-topo.js.map +1 -0
  165. package/dist/validate-topo.d.ts.map +1 -1
  166. package/dist/validate-topo.js +27 -8
  167. package/dist/validate-topo.js.map +1 -1
  168. package/dist/workspace.d.ts +1 -1
  169. package/dist/workspace.d.ts.map +1 -1
  170. package/dist/workspace.js +1 -1
  171. package/dist/workspace.js.map +1 -1
  172. package/package.json +10 -1
  173. package/src/__tests__/branded.test.ts +5 -5
  174. package/src/__tests__/context.test.ts +3 -3
  175. package/src/__tests__/contour.test.ts +263 -0
  176. package/src/__tests__/derive-trail.test.ts +668 -0
  177. package/src/__tests__/derive.test.ts +58 -1
  178. package/src/__tests__/errors.test.ts +118 -0
  179. package/src/__tests__/execute.test.ts +1179 -141
  180. package/src/__tests__/fire.test.ts +1056 -0
  181. package/src/__tests__/fork-ctx.test.ts +83 -0
  182. package/src/__tests__/ingest.test.ts +191 -0
  183. package/src/__tests__/{gate.test.ts → layer.test.ts} +21 -21
  184. package/src/__tests__/path-security.test.ts +7 -7
  185. package/src/__tests__/resilience.test.ts +8 -8
  186. package/src/__tests__/resource.test.ts +204 -0
  187. package/src/__tests__/run.test.ts +13 -13
  188. package/src/__tests__/service-config.test.ts +28 -32
  189. package/src/__tests__/surface-filter.test.ts +196 -0
  190. package/src/__tests__/topo-store-read.test.ts +256 -0
  191. package/src/__tests__/topo-store.test.ts +869 -0
  192. package/src/__tests__/topo.test.ts +350 -54
  193. package/src/__tests__/trail.test.ts +189 -24
  194. package/src/__tests__/trails-db.test.ts +198 -0
  195. package/src/__tests__/transport-error-map.test.ts +82 -0
  196. package/src/__tests__/type-utils.test.ts +29 -1
  197. package/src/__tests__/validate-topo.test.ts +305 -16
  198. package/src/__tests__/workspace.test.ts +6 -6
  199. package/src/__tests__/zod-wrappers.test.ts +62 -0
  200. package/src/branded.ts +1 -1
  201. package/src/context.ts +24 -4
  202. package/src/contour.ts +344 -0
  203. package/src/cross-schema.ts +36 -0
  204. package/src/derive.ts +39 -8
  205. package/src/draft.ts +338 -0
  206. package/src/errors.ts +62 -18
  207. package/src/execute.ts +840 -29
  208. package/src/fire.ts +295 -0
  209. package/src/index.ts +142 -26
  210. package/src/internal/cross-batch.ts +77 -0
  211. package/src/internal/fork-ctx.ts +69 -0
  212. package/src/internal/signal-ref.ts +87 -0
  213. package/src/internal/topo-snapshots.ts +517 -0
  214. package/src/internal/topo-store-read.ts +481 -0
  215. package/src/internal/topo-store.ts +1192 -0
  216. package/src/internal/tracing.ts +187 -0
  217. package/src/internal/trails-db.ts +187 -0
  218. package/src/internal/zod-wrappers.ts +78 -0
  219. package/src/layer.ts +50 -0
  220. package/src/path-security.ts +1 -1
  221. package/src/resilience.ts +3 -3
  222. package/src/{provision-config.ts → resource-config.ts} +109 -123
  223. package/src/resource.ts +155 -0
  224. package/src/run.ts +5 -4
  225. package/src/serialization.ts +2 -0
  226. package/src/store/accessor-protocol.ts +56 -0
  227. package/src/store/index.ts +4 -0
  228. package/src/surface-filter.ts +182 -0
  229. package/src/topo-store.ts +510 -0
  230. package/src/topo.ts +328 -57
  231. package/src/trail.ts +199 -31
  232. package/src/trails/derive-trail.ts +842 -0
  233. package/src/trails/index.ts +9 -0
  234. package/src/trails/ingest.ts +146 -0
  235. package/src/transport-error-map.ts +56 -0
  236. package/src/type-checks.test-d.ts +106 -0
  237. package/src/type-utils.ts +17 -2
  238. package/src/types.ts +148 -11
  239. package/src/validate-established-topo.ts +63 -0
  240. package/src/validate-topo.ts +37 -9
  241. package/src/workspace.ts +1 -1
  242. package/tsconfig.tests.json +10 -0
  243. package/tsconfig.tsbuildinfo +1 -1
  244. package/dist/adapters.d.ts +0 -39
  245. package/dist/adapters.d.ts.map +0 -1
  246. package/dist/adapters.js +0 -2
  247. package/dist/adapters.js.map +0 -1
  248. package/dist/dispatch.d.ts +0 -27
  249. package/dist/dispatch.d.ts.map +0 -1
  250. package/dist/dispatch.js +0 -34
  251. package/dist/dispatch.js.map +0 -1
  252. package/dist/event.d.ts +0 -8
  253. package/dist/event.d.ts.map +0 -1
  254. package/dist/event.js +0 -7
  255. package/dist/event.js.map +0 -1
  256. package/dist/gate.d.ts +0 -17
  257. package/dist/gate.d.ts.map +0 -1
  258. package/dist/gate.js +0 -21
  259. package/dist/gate.js.map +0 -1
  260. package/dist/health.d.ts +0 -18
  261. package/dist/health.d.ts.map +0 -1
  262. package/dist/health.js +0 -5
  263. package/dist/health.js.map +0 -1
  264. package/dist/job.d.ts +0 -24
  265. package/dist/job.d.ts.map +0 -1
  266. package/dist/job.js +0 -17
  267. package/dist/job.js.map +0 -1
  268. package/dist/provision-config.d.ts +0 -22
  269. package/dist/provision-config.d.ts.map +0 -1
  270. package/dist/provision-config.js +0 -210
  271. package/dist/provision-config.js.map +0 -1
  272. package/dist/provision.d.ts +0 -71
  273. package/dist/provision.d.ts.map +0 -1
  274. package/dist/provision.js +0 -56
  275. package/dist/provision.js.map +0 -1
  276. package/dist/service-config.d.ts +0 -22
  277. package/dist/service-config.d.ts.map +0 -1
  278. package/dist/service-config.js +0 -210
  279. package/dist/service-config.js.map +0 -1
  280. package/dist/service.d.ts +0 -71
  281. package/dist/service.d.ts.map +0 -1
  282. package/dist/service.js +0 -56
  283. package/dist/service.js.map +0 -1
  284. package/src/__tests__/service.test.ts +0 -197
  285. package/src/event.ts +0 -15
  286. package/src/gate.ts +0 -44
  287. package/src/provision.ts +0 -148
package/src/trail.ts CHANGED
@@ -1,9 +1,17 @@
1
1
  import type { z } from 'zod';
2
2
 
3
+ import type { AnyContour } from './contour.js';
3
4
  import type { FieldOverride } from './derive.js';
4
5
  import type { Result } from './result.js';
5
- import type { AnyProvision } from './provision.js';
6
+ import type { AnyResource } from './resource.js';
7
+ import type { AnySignal } from './signal.js';
8
+ import {
9
+ createLateBoundSignalMarker,
10
+ getLateBoundSignalRef,
11
+ } from './internal/signal-ref.js';
12
+ import type { TrailsError } from './errors.js';
6
13
  import type {
14
+ Detour,
7
15
  Implementation,
8
16
  PermitRequirement,
9
17
  TrailContext,
@@ -27,44 +35,95 @@ export interface TrailExample<I, O> {
27
35
  readonly description?: string | undefined;
28
36
  /** The input value — fields with schema defaults may be omitted */
29
37
  readonly input: Partial<I>;
30
- /** Expected output for success-path examples */
38
+ /** Expected output for success-path examples (deep equality) */
31
39
  readonly expected?: O | undefined;
40
+ /** Partial output assertion — declared fields must match, others ignored */
41
+ readonly expectedMatch?: Partial<O> | undefined;
32
42
  /** Error class name for error-path examples */
33
43
  readonly error?: string | undefined;
34
44
  }
35
45
 
46
+ // ---------------------------------------------------------------------------
47
+ // Blaze input — merges crossInput when declared
48
+ // ---------------------------------------------------------------------------
49
+
50
+ /**
51
+ * The input type received by a trail's blaze function.
52
+ *
53
+ * When a trail declares `crossInput`, the runtime merges those fields into
54
+ * the input object before calling blaze. This type makes the compiler aware
55
+ * of the merged shape so developers can access crossInput fields without a
56
+ * cast. Falls back to plain `I` when `CI` is `never` (the default).
57
+ */
58
+ export type BlazeInput<I, CI> = [CI] extends [never] ? I : I & CI;
59
+
36
60
  // ---------------------------------------------------------------------------
37
61
  // Trail spec
38
62
  // ---------------------------------------------------------------------------
39
63
 
40
64
  /** Everything needed to define a trail (minus the id) */
41
- export interface TrailSpec<I, O> {
65
+ export interface TrailSpec<I, O, CI = never> {
42
66
  /** Zod schema for validating input */
43
67
  readonly input: z.ZodType<I>;
44
68
  /** Zod schema for validating output (optional — some trails are fire-and-forget) */
45
69
  readonly output?: z.ZodType<O> | undefined;
46
70
  /** The pure function that does the work (sync or async authoring) */
47
- readonly blaze: Implementation<I, O>;
71
+ readonly blaze: Implementation<BlazeInput<I, CI>, O>;
48
72
  /** Human-readable description */
49
73
  readonly description?: string | undefined;
74
+ /** Declared operational shape for governance, derivation, and agent guidance. */
75
+ readonly pattern?: string | undefined;
50
76
  /** Named examples for docs and testing */
51
77
  readonly examples?: readonly TrailExample<I, O>[] | undefined;
52
78
  /** What this trail does to the world: read, write (default), or destroy */
53
79
  readonly intent?: 'read' | 'write' | 'destroy' | undefined;
54
80
  /** Trail is idempotent (safe to retry) */
55
81
  readonly idempotent?: boolean | undefined;
82
+ /** Whether trailheads expose this trail by default. */
83
+ readonly visibility?: TrailVisibility | undefined;
56
84
  /** Arbitrary meta for tooling and filtering */
57
85
  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;
86
+ /** Recovery paths activated when blaze fails with a matching error class. */
87
+ readonly detours?: readonly Detour<I, O, TrailsError>[] | undefined;
60
88
  /** Per-field overrides for deriveFields() (labels, hints, options) */
61
89
  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;
90
+ /** Contours this trail operates on. */
91
+ readonly contours?: readonly AnyContour[] | undefined;
92
+ /** IDs or trail objects of downstream trails this trail may invoke via ctx.cross() */
93
+ readonly crosses?: readonly (string | AnyTrail)[] | undefined;
94
+ /**
95
+ * Composition-only input schema — merged with `input` for `ctx.cross()` calls,
96
+ * invisible to public trailheads (CLI, MCP, HTTP).
97
+ *
98
+ * Fields here are available in the blaze but are not derived into CLI flags,
99
+ * MCP tool parameters, or HTTP request bodies. Use for data that only makes
100
+ * sense when one trail crosses another (e.g. `forkedFrom`).
101
+ */
102
+ readonly crossInput?: z.ZodType<CI> | undefined;
103
+ /** Resources this trail may access via resource.from(ctx) */
104
+ readonly resources?: readonly AnyResource[] | undefined;
105
+ /**
106
+ * Signals this trail emits via `ctx.fire()`.
107
+ *
108
+ * Accepts either a string id or a `Signal` value. Both forms are
109
+ * normalized to the signal's id at trail definition time, so
110
+ * `trail.fires` is always `readonly string[]`.
111
+ *
112
+ * Note: `crosses` also accepts trail objects (normalized to IDs),
113
+ * following the same pattern as signal references here.
114
+ */
115
+ readonly fires?: readonly (string | AnySignal)[] | undefined;
116
+ /**
117
+ * Signals that activate this trail (framework auto-subscribes).
118
+ *
119
+ * Accepts either a string id or a `Signal` value. Both forms are
120
+ * normalized to the signal's id at trail definition time.
121
+ */
122
+ readonly on?: readonly (string | AnySignal)[] | undefined;
66
123
  /** Auth requirement: scopes object, 'public', or omitted (undeclared) */
67
124
  readonly permit?: PermitRequirement | undefined;
125
+ /** Primary input fields and their order. CLI projects as positional args. */
126
+ readonly args?: readonly string[] | false | undefined;
68
127
  }
69
128
 
70
129
  // ---------------------------------------------------------------------------
@@ -74,26 +133,116 @@ export interface TrailSpec<I, O> {
74
133
  /** Intent describes what a trail does to the world */
75
134
  export type Intent = 'read' | 'write' | 'destroy';
76
135
 
136
+ /** Whether trailheads expose a trail by default. */
137
+ export type TrailVisibility = 'public' | 'internal';
138
+
77
139
  /** 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'
140
+ export interface Trail<I, O, CI = never> extends Omit<
141
+ TrailSpec<I, O, CI>,
142
+ | 'args'
143
+ | 'blaze'
144
+ | 'contours'
145
+ | 'crosses'
146
+ | 'crossInput'
147
+ | 'detours'
148
+ | 'fires'
149
+ | 'intent'
150
+ | 'on'
151
+ | 'resources'
81
152
  > {
82
153
  readonly kind: 'trail';
83
154
  readonly id: string;
84
- readonly blaze: Implementation<I, O>;
155
+ readonly blaze: Implementation<BlazeInput<I, CI>, O>;
156
+ /** Contours this trail operates on (always present, default []). */
157
+ readonly contours: readonly AnyContour[];
85
158
  /** IDs of downstream trails this trail may invoke via ctx.cross() (always present, default []) */
86
159
  readonly crosses: readonly string[];
87
- /** Provisions this trail may access via provision.from(ctx) (always present, default []) */
88
- readonly provisions: readonly AnyProvision[];
160
+ /** Composition-only input schema, merged with `input` for ctx.cross() calls (optional) */
161
+ readonly crossInput?: z.ZodType<CI> | undefined;
162
+ /** Recovery paths activated when blaze fails with a matching error (always present, default []). */
163
+ readonly detours: readonly Detour<I, O, TrailsError>[];
164
+ /** Resources this trail may access via resource.from(ctx) (always present, default []) */
165
+ readonly resources: readonly AnyResource[];
166
+ /** IDs of signals this trail emits via ctx.fire() (always present, default []) */
167
+ readonly fires: readonly string[];
168
+ /** IDs of signals that activate this trail (always present, default []) */
169
+ readonly on: readonly string[];
89
170
  /** What this trail does to the world (always present, default 'write') */
90
171
  readonly intent: Intent;
172
+ /** Whether trailheads expose this trail by default (always present, default 'public'). */
173
+ readonly visibility: TrailVisibility;
174
+ /** Primary input fields and their order (always present, default undefined) */
175
+ readonly args?: readonly string[] | false | undefined;
91
176
  }
92
177
 
93
178
  // ---------------------------------------------------------------------------
94
179
  // Factory
95
180
  // ---------------------------------------------------------------------------
96
181
 
182
+ /**
183
+ * Canonical scoped-signal id shape: `<scope>:<table>.<event>`.
184
+ *
185
+ * Matches exactly one `:` separating a non-empty scope from a non-empty
186
+ * dotted tail of at least two segments (e.g. `identity:users.created`).
187
+ * The scope forbids only `:` and whitespace so resource ids may contain
188
+ * dots for namespacing (e.g. `demo.store:gists.created`). Tail segments
189
+ * forbid both `:` and `.` so strings like `foo:bar` (no dot) or
190
+ * `a:b.c.d` stay unambiguous.
191
+ */
192
+ const SCOPED_SIGNAL_ID = /^[^:\s]+:[^:.\s]+(?:\.[^:.\s]+)+$/;
193
+
194
+ const normalizeSignalRef = (entry: string | AnySignal): string => {
195
+ if (typeof entry === 'string') {
196
+ return entry;
197
+ }
198
+
199
+ const ref = getLateBoundSignalRef(entry);
200
+ if (!ref) {
201
+ return entry.id;
202
+ }
203
+
204
+ // Already-scoped canonical ids (e.g. "identity:users.created") must pass
205
+ // through unchanged. Rewriting them to a bare marker token collapses
206
+ // multi-binding cases where the same store definition is bound under two
207
+ // separate resources: both bindings share the late-bound token, so the
208
+ // caller's explicit choice of scope would be lost and topo resolution
209
+ // would throw an ambiguity error.
210
+ //
211
+ // Use a strict predicate that matches the canonical scoped shape
212
+ // `<scope>:<table>.<event>` (exactly one `:` separating a non-empty scope
213
+ // from a non-empty dotted tail of at least two segments). A looser
214
+ // `includes(':')` check would let unscoped ids that happen to contain `:`
215
+ // slip past markerization and then fail to resolve at topo finalization.
216
+ if (SCOPED_SIGNAL_ID.test(entry.id)) {
217
+ return entry.id;
218
+ }
219
+
220
+ return createLateBoundSignalMarker(ref, entry.id);
221
+ };
222
+
223
+ /** Normalize a crosses entry — trail objects are reduced to their id. */
224
+ const normalizeCrossRef = (entry: string | AnyTrail): string =>
225
+ typeof entry === 'string' ? entry : entry.id;
226
+
227
+ /** Freeze and normalize all collection fields from a trail spec. */
228
+ const normalizeCollections = <I, O, CI>(
229
+ spec: TrailSpec<I, O, CI>
230
+ ): {
231
+ readonly args: readonly string[] | false | undefined;
232
+ readonly contours: readonly AnyContour[];
233
+ readonly detours: readonly Detour<I, O, TrailsError>[];
234
+ readonly fires: readonly string[];
235
+ readonly on: readonly string[];
236
+ readonly resources: readonly AnyResource[];
237
+ } => ({
238
+ args: Array.isArray(spec.args) ? Object.freeze([...spec.args]) : spec.args,
239
+ contours: Object.freeze([...(spec.contours ?? [])]),
240
+ detours: Object.freeze([...(spec.detours ?? [])]),
241
+ fires: Object.freeze((spec.fires ?? []).map(normalizeSignalRef)),
242
+ on: Object.freeze((spec.on ?? []).map(normalizeSignalRef)),
243
+ resources: Object.freeze([...(spec.resources ?? [])]),
244
+ });
245
+
97
246
  /**
98
247
  * Create a trail definition.
99
248
  *
@@ -116,14 +265,17 @@ export interface Trail<I, O> extends Omit<
116
265
  * });
117
266
  * ```
118
267
  */
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> {
268
+ export function trail<I, O, CI = never>(
269
+ id: string,
270
+ spec: TrailSpec<I, O, CI>
271
+ ): Trail<I, O, CI>;
272
+ export function trail<I, O, CI = never>(
273
+ spec: TrailSpec<I, O, CI> & { readonly id: string }
274
+ ): Trail<I, O, CI>;
275
+ export function trail<I, O, CI = never>(
276
+ idOrSpec: string | (TrailSpec<I, O, CI> & { readonly id: string }),
277
+ maybeSpec?: TrailSpec<I, O, CI>
278
+ ): Trail<I, O, CI> {
127
279
  const resolved =
128
280
  typeof idOrSpec === 'string'
129
281
  ? { id: idOrSpec, spec: maybeSpec }
@@ -135,26 +287,42 @@ export function trail<I, O>(
135
287
 
136
288
  const {
137
289
  blaze,
290
+ crossInput,
138
291
  crosses: rawCrosses,
139
292
  intent: rawIntent,
140
- provisions: rawProvisions,
293
+ visibility: rawVisibility,
294
+ // Destructure away fields handled by normalizeCollections
295
+ args: _a,
296
+ contours: _c,
297
+ detours: _d,
298
+ fires: _f,
299
+ on: _o,
300
+ resources: _r,
141
301
  ...spec
142
302
  } = resolved.spec;
143
- const provisions = Object.freeze([...(rawProvisions ?? [])]);
303
+ const collections = normalizeCollections(resolved.spec);
144
304
 
145
305
  return Object.freeze({
146
306
  ...spec,
147
- blaze: async (input: I, ctx: TrailContext) => await blaze(input, ctx),
148
- crosses: Object.freeze([...(rawCrosses ?? [])]),
307
+ ...collections,
308
+ blaze: async (input: BlazeInput<I, CI>, ctx: TrailContext) =>
309
+ await blaze(input, ctx),
310
+ crossInput,
311
+ crosses: Object.freeze((rawCrosses ?? []).map(normalizeCrossRef)),
149
312
  id: resolved.id,
150
313
  intent: rawIntent ?? 'write',
151
314
  kind: 'trail' as const,
152
- provisions,
315
+ visibility: rawVisibility ?? 'public',
153
316
  });
154
317
  }
155
318
 
156
319
  // 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>;
320
+ // The Omit+override avoids a TypeScript limitation where BlazeInput's conditional type
321
+ // makes Trail<any, any, any> structurally incompatible with Trail<I, O, never>.
322
+ /* oxlint-disable no-explicit-any -- existential type for heterogeneous collections */
323
+ export type AnyTrail = Omit<Trail<any, any, any>, 'blaze'> & {
324
+ readonly blaze: Implementation<any, any>;
325
+ };
326
+ /* oxlint-enable no-explicit-any */
159
327
 
160
328
  export type { Implementation, TrailContext, Result };