@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/fire.ts ADDED
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Signal emission and auto-activation.
3
+ *
4
+ * `createFireFn(topo, producerCtx?, executor)` returns a `FireFn` bound to
5
+ * a topo. Calling `fire(signalId, payload)` looks up the signal, validates
6
+ * the payload against its schema, finds every trail with the signal in its
7
+ * `on:` array, and invokes each consumer via the supplied executor.
8
+ *
9
+ * The `executor` parameter is an indirection that lets `execute.ts` pass in
10
+ * `executeTrail` without `fire.ts` importing it directly — keeping the two
11
+ * modules dependency-cycle-free.
12
+ *
13
+ * Consumer contexts inherit the producer's full ctx (logger, extensions,
14
+ * resources, abortSignal, requestId, env, workspaceRoot, permit) with
15
+ * `fire` rebound to the same closure so consumers can fan out further.
16
+ * Each consumer gets its own derived context so sibling fan-out branches do
17
+ * not share mutable top-level state. The consumer logger is derived from the
18
+ * producer logger as a child tagged with `signalId` and `consumerId` when
19
+ * `logger.child` exists.
20
+ *
21
+ * Error semantics match the fire-and-forget framing: producers get
22
+ * `Result.ok(undefined)` unless the signal id is unknown or the payload
23
+ * fails schema validation. Consumer errors are logged via the producer's
24
+ * logger but do NOT propagate back to the producer. Consumers that need
25
+ * transactional coupling should use `crosses:`.
26
+ */
27
+
28
+ import { NotFoundError, ValidationError } from './errors.js';
29
+ import { forkCtx } from './internal/fork-ctx.js';
30
+ import { Result } from './result.js';
31
+ import type { Topo } from './topo.js';
32
+ import type { AnyTrail } from './trail.js';
33
+ import type { FireFn, Logger, TrailContextInit } from './types.js';
34
+
35
+ /** Signature execute.ts passes in to avoid a fire ↔ execute import cycle. */
36
+ export type ConsumerExecutor = (
37
+ consumer: AnyTrail,
38
+ input: unknown,
39
+ ctx: Partial<TrailContextInit>
40
+ ) => Promise<Result<unknown, Error>>;
41
+
42
+ type MutableConsumerContext = {
43
+ -readonly [K in keyof Partial<TrailContextInit>]: Partial<TrailContextInit>[K];
44
+ };
45
+
46
+ const FIRE_STACK_KEY = '__trails_fire_stack';
47
+
48
+ /**
49
+ * Maximum depth for signal fan-out chains.
50
+ *
51
+ * Cycle detection catches re-entrant fires of the same signal ID (A→B→A),
52
+ * but a chain of distinct signals (A→B→C→D→...) bypasses it. This limit
53
+ * prevents runaway fan-out in pathological topologies.
54
+ */
55
+ const MAX_FIRE_DEPTH = 16;
56
+
57
+ const getFireStack = (
58
+ ctx: Pick<TrailContextInit, 'extensions'> | undefined
59
+ ): readonly string[] => {
60
+ const value = ctx?.extensions?.[FIRE_STACK_KEY];
61
+ return Array.isArray(value) ? (value as readonly string[]) : [];
62
+ };
63
+
64
+ /** Binds a per-consumer `fire` onto a mutable consumer context. */
65
+ type ConsumerFireBinder = (
66
+ consumerCtx: MutableConsumerContext
67
+ ) => MutableConsumerContext;
68
+
69
+ const deriveConsumerLogger = (
70
+ producerCtx: TrailContextInit | undefined,
71
+ signalId: string,
72
+ consumerId: string
73
+ ): Logger | undefined =>
74
+ producerCtx?.logger?.child?.({ consumerId, signalId }) ?? producerCtx?.logger;
75
+
76
+ const deriveConsumerEnv = (
77
+ producerCtx: TrailContextInit | undefined
78
+ ): TrailContextInit['env'] =>
79
+ producerCtx?.env ? { ...producerCtx.env } : undefined;
80
+
81
+ const deriveConsumerExtensions = (
82
+ producerCtx: TrailContextInit | undefined,
83
+ signalId: string
84
+ ): TrailContextInit['extensions'] => ({
85
+ ...producerCtx?.extensions,
86
+ [FIRE_STACK_KEY]: [...getFireStack(producerCtx), signalId],
87
+ });
88
+
89
+ const deriveConsumerCtx = (
90
+ producerCtx: TrailContextInit | undefined,
91
+ signalId: string,
92
+ consumerId: string
93
+ ): MutableConsumerContext =>
94
+ producerCtx
95
+ ? forkCtx(producerCtx as MutableConsumerContext, {
96
+ env: deriveConsumerEnv(producerCtx),
97
+ extensions: deriveConsumerExtensions(producerCtx, signalId),
98
+ logger: deriveConsumerLogger(producerCtx, signalId, consumerId),
99
+ })
100
+ : {};
101
+
102
+ /**
103
+ * Fan out a validated signal payload to its consumer trails.
104
+ *
105
+ * @remarks
106
+ * Consumers fan out in parallel by design. Signal delivery is fire-and-forget
107
+ * notification, not ordered orchestration; if one consumer depends on another,
108
+ * the dependency belongs in `crosses:` instead of sibling signal sequencing.
109
+ *
110
+ * `Promise.allSettled` preserves failure isolation and waits for every branch
111
+ * to settle. Each consumer gets its own derived context so sibling branches do
112
+ * not share mutable top-level state while they overlap. Re-entrant suppression
113
+ * elsewhere in this module is still based on signal-id membership in the
114
+ * current fire stack: it prevents infinite loops, but it can over-suppress
115
+ * legitimate diamond re-fires. Per-path provenance is a documented future
116
+ * direction rather than part of the pre-v1 runtime contract.
117
+ */
118
+ const fanOutToConsumers = async (
119
+ consumers: readonly AnyTrail[],
120
+ payload: unknown,
121
+ signalId: string,
122
+ producerCtx: TrailContextInit | undefined,
123
+ bindFire: ConsumerFireBinder,
124
+ executor: ConsumerExecutor,
125
+ logger: Logger | undefined
126
+ ): Promise<void> => {
127
+ const settled = await Promise.allSettled(
128
+ consumers.map(async (consumer) => {
129
+ const consumerCtx = bindFire(
130
+ deriveConsumerCtx(producerCtx, signalId, consumer.id)
131
+ );
132
+ const consumerResult = await executor(consumer, payload, consumerCtx);
133
+ if (consumerResult.isErr()) {
134
+ (consumerCtx.logger ?? logger)?.warn('Signal consumer failed', {
135
+ consumerId: consumer.id,
136
+ error: consumerResult.error.message,
137
+ signalId,
138
+ });
139
+ }
140
+ return consumer.id;
141
+ })
142
+ );
143
+ for (const [index, entry] of settled.entries()) {
144
+ if (entry.status !== 'rejected') {
145
+ continue;
146
+ }
147
+ // `executeTrail` normalizes throws into `Result.err`, so reaching this
148
+ // branch means the executor (or the warn call above) rejected
149
+ // unexpectedly. Log at debug to preserve provenance without propagating
150
+ // the failure to the producer (fire-and-forget semantics).
151
+ logger?.debug('Signal consumer rejected unexpectedly', {
152
+ consumerId: consumers[index]?.id,
153
+ error:
154
+ entry.reason instanceof Error
155
+ ? entry.reason.message
156
+ : String(entry.reason),
157
+ signalId,
158
+ });
159
+ }
160
+ };
161
+
162
+ const resolveFireDispatch = (
163
+ topo: Topo,
164
+ signalId: string,
165
+ payload: unknown
166
+ ): Result<
167
+ { readonly consumers: readonly AnyTrail[]; readonly payload: unknown },
168
+ Error
169
+ > => {
170
+ const signal = topo.signals.get(signalId);
171
+ if (signal === undefined) {
172
+ return Result.err(
173
+ new NotFoundError(`Signal "${signalId}" not found in topo "${topo.name}"`)
174
+ );
175
+ }
176
+ const parsed = signal.payload.safeParse(payload);
177
+ if (!parsed.success) {
178
+ return Result.err(
179
+ new ValidationError(
180
+ `Invalid payload for signal "${signalId}": ${parsed.error.message}`
181
+ )
182
+ );
183
+ }
184
+ return Result.ok({
185
+ consumers: topo.list().filter((trail) => trail.on.includes(signalId)),
186
+ payload: parsed.data,
187
+ });
188
+ };
189
+
190
+ const resolveSignalId = (signalOrId: unknown): Result<string, Error> => {
191
+ if (typeof signalOrId === 'string') {
192
+ return Result.ok(signalOrId);
193
+ }
194
+ if (
195
+ typeof signalOrId === 'object' &&
196
+ signalOrId !== null &&
197
+ 'id' in signalOrId &&
198
+ typeof (signalOrId as { id: unknown }).id === 'string'
199
+ ) {
200
+ return Result.ok((signalOrId as { id: string }).id);
201
+ }
202
+ return Result.err(
203
+ new ValidationError(
204
+ 'ctx.fire() requires a signal id string or a Signal value'
205
+ )
206
+ );
207
+ };
208
+
209
+ /**
210
+ * Build a `FireFn` closure bound to a topo.
211
+ *
212
+ * When `producerCtx` is provided, consumer trails activated via `on:`
213
+ * inherit the producer's logger, extensions, resources, abortSignal,
214
+ * requestId, env, workspaceRoot, and permit. `ctx.fire` on the consumer
215
+ * is rebound to the same closure so consumers can emit downstream
216
+ * signals naturally.
217
+ */
218
+ export const createFireFn = (
219
+ topo: Topo,
220
+ producerCtx: TrailContextInit | undefined,
221
+ executor: ConsumerExecutor
222
+ ): FireFn => {
223
+ const bindConsumerFire: ConsumerFireBinder = (consumerCtx) => ({
224
+ ...consumerCtx,
225
+ // Pre-bind fire on the consumer ctx as a safety net for direct
226
+ // executeTrail calls that skip the topo-aware path. In the normal
227
+ // fan-out flow below, bindFireToCtx in execute.ts rebinds fire to
228
+ // the fully-traced ctx before the blaze runs, so this assignment
229
+ // is superseded — but keeping it makes consumerCtx self-sufficient
230
+ // for any caller that inspects it pre-execution.
231
+ fire: createFireFn(topo, consumerCtx as TrailContextInit, executor),
232
+ });
233
+
234
+ const dispatchFire = async (
235
+ signalId: string,
236
+ payload: unknown
237
+ ): Promise<Result<void, Error>> => {
238
+ const dispatch = resolveFireDispatch(topo, signalId, payload);
239
+ if (dispatch.isErr()) {
240
+ return Result.err(dispatch.error);
241
+ }
242
+ await fanOutToConsumers(
243
+ dispatch.value.consumers,
244
+ dispatch.value.payload,
245
+ signalId,
246
+ producerCtx,
247
+ bindConsumerFire,
248
+ executor,
249
+ producerCtx?.logger
250
+ );
251
+ return Result.ok();
252
+ };
253
+
254
+ /** Return an early Result if the fire should be suppressed, or null to proceed. */
255
+ const guardFire = (
256
+ signalId: string,
257
+ stack: readonly string[]
258
+ ): Result<void, Error> | null => {
259
+ if (stack.length >= MAX_FIRE_DEPTH) {
260
+ producerCtx?.logger?.warn(
261
+ 'Signal fan-out depth limit reached — skipping fire',
262
+ { depth: stack.length, signalId }
263
+ );
264
+ return Result.ok();
265
+ }
266
+ if (stack.includes(signalId)) {
267
+ producerCtx?.logger?.debug('Signal fan-out suppressed due to cycle', {
268
+ fireStack: [...stack],
269
+ signalId,
270
+ });
271
+ producerCtx?.logger?.warn(
272
+ 'Signal cycle detected — skipping re-entrant fire',
273
+ { fireStack: [...stack], signalId }
274
+ );
275
+ return Result.ok();
276
+ }
277
+ return null;
278
+ };
279
+
280
+ const fireImpl: FireFn = async (
281
+ signalOrId: unknown,
282
+ payload: unknown
283
+ ): Promise<Result<void, Error>> => {
284
+ const resolved = resolveSignalId(signalOrId);
285
+ if (resolved.isErr()) {
286
+ return Result.err(resolved.error);
287
+ }
288
+ const suppressed = guardFire(resolved.value, getFireStack(producerCtx));
289
+ if (suppressed) {
290
+ return suppressed;
291
+ }
292
+ return await dispatchFire(resolved.value, payload);
293
+ };
294
+ return fireImpl;
295
+ };
package/src/index.ts CHANGED
@@ -17,92 +17,208 @@ export {
17
17
  InternalError,
18
18
  AuthError,
19
19
  CancelledError,
20
+ DerivationError,
21
+ errorCategories,
20
22
  exitCodeMap,
21
23
  statusCodeMap,
22
24
  jsonRpcCodeMap,
23
25
  retryableMap,
26
+ RetryExhaustedError,
24
27
  isRetryable,
25
28
  isTrailsError,
26
29
  } from './errors.js';
27
30
  export type { ErrorCategory } from './errors.js';
31
+ export {
32
+ createTransportErrorMapper,
33
+ mapTransportError,
34
+ transportErrorMap,
35
+ transportErrorRegistry,
36
+ transportNames,
37
+ } from './transport-error-map.js';
38
+ export type {
39
+ TransportErrorCode,
40
+ TransportErrorMapper,
41
+ TransportErrorMappings,
42
+ TransportName,
43
+ } from './transport-error-map.js';
28
44
 
29
45
  // Types
30
46
  export type {
47
+ Detour,
48
+ DetourAttempt,
31
49
  Implementation,
32
50
  TrailContext,
33
51
  TrailContextInit,
52
+ CrossBatchOptions,
34
53
  CrossFn,
54
+ FireFn,
35
55
  BasePermit,
36
56
  PermitRequirement,
37
57
  ProgressCallback,
38
58
  ProgressEvent,
39
59
  Logger,
40
- ProvisionLookup,
60
+ ResourceLookup,
41
61
  } from './types.js';
42
62
  export { TRAILHEAD_KEY } from './types.js';
43
63
 
44
64
  // Context factory
45
- export { createTrailContext } from './context.js';
65
+ export { createTrailContext, passthroughTrace } from './context.js';
46
66
 
47
- // Provision
67
+ // Resource
48
68
  export {
49
- createProvisionLookup,
50
- findDuplicateProvisionId,
51
- isProvision,
52
- provision,
53
- } from './provision.js';
69
+ createResourceLookup,
70
+ findDuplicateResourceId,
71
+ isResource,
72
+ resource,
73
+ } from './resource.js';
54
74
  export type {
55
- AnyProvision,
56
- Provision,
57
- ProvisionContext,
58
- ProvisionOverrideMap,
59
- ProvisionSpec,
60
- } from './provision.js';
75
+ AnyResource,
76
+ Resource,
77
+ ResourceContext,
78
+ ResourceOverrideMap,
79
+ ResourceSpec,
80
+ } from './resource.js';
61
81
 
62
82
  // Trail
63
83
  export { trail } from './trail.js';
64
84
  export type {
65
85
  AnyTrail,
86
+ BlazeInput,
66
87
  Intent,
67
88
  Trail,
68
89
  TrailSpec,
69
90
  TrailExample,
91
+ TrailVisibility,
70
92
  } from './trail.js';
93
+ export {
94
+ filterSurfaceTrails,
95
+ matchesTrailPattern,
96
+ shouldIncludeTrailForSurface,
97
+ } from './surface-filter.js';
98
+ export type { SurfaceFilterOptions } from './surface-filter.js';
71
99
 
72
100
  // Type utilities
73
- export type { TrailInput, TrailOutput, TrailResult } from './type-utils.js';
101
+ export type {
102
+ CrossInput,
103
+ TrailInput,
104
+ TrailOutput,
105
+ TrailResult,
106
+ } from './type-utils.js';
74
107
  export { inputOf, outputOf } from './type-utils.js';
75
108
 
76
109
  // Signal
77
110
  export { signal } from './signal.js';
78
111
  export type { AnySignal, Signal, SignalSpec } from './signal.js';
79
- export { event } from './event.js';
80
- export type { AnyEvent, Event, EventSpec } from './event.js';
112
+
113
+ // Contour
114
+ export { contour } from './contour.js';
115
+ export {
116
+ CONTOUR_ID_METADATA,
117
+ getContourIdMetadata,
118
+ getContourReferences,
119
+ } from './contour.js';
120
+ export type {
121
+ AnyContour,
122
+ Contour,
123
+ ContourIdBrand,
124
+ ContourIdMetadata,
125
+ ContourIdSchema,
126
+ ContourIdValue,
127
+ ContourOptions,
128
+ ContourReference,
129
+ } from './contour.js';
81
130
 
82
131
  // Topo
83
132
  export { topo } from './topo.js';
84
- export type { Topo } from './topo.js';
133
+ export type { Topo, TopoIdentity } from './topo.js';
134
+ export {
135
+ createTopoSnapshot,
136
+ createMockTopoStore,
137
+ createTopoStore,
138
+ listTopoSnapshots,
139
+ pinTopoSnapshot,
140
+ topoStore,
141
+ unpinTopoSnapshot,
142
+ } from './topo-store.js';
143
+ export type {
144
+ CreateTopoSnapshotInput,
145
+ ListTopoSnapshotsOptions,
146
+ MockTopoStoreSeed,
147
+ ReadOnlyTopoStore,
148
+ TopoSnapshot,
149
+ TopoStoreExportRecord,
150
+ TopoStoreResourceRecord,
151
+ TopoStoreRef,
152
+ TopoStoreTrailDetailRecord,
153
+ TopoStoreTrailRecord,
154
+ } from './topo-store.js';
155
+
156
+ // Draft state
157
+ export {
158
+ DRAFT_ID_PREFIX,
159
+ deriveDraftReport,
160
+ isDraftId,
161
+ validateDraftFreeTopo,
162
+ } from './draft.js';
163
+ export type {
164
+ DraftDependency,
165
+ DraftDependencyKind,
166
+ DraftFinding,
167
+ DraftReport,
168
+ } from './draft.js';
85
169
 
86
170
  // Topo validation
87
171
  export { validateTopo } from './validate-topo.js';
88
172
  export type { TopoIssue } from './validate-topo.js';
173
+ export { validateEstablishedTopo } from './validate-established-topo.js';
89
174
 
90
- // Gate
91
- export { composeGates } from './gate.js';
92
- export type { Gate } from './gate.js';
175
+ // Layer
176
+ export { composeLayers } from './layer.js';
177
+ export type { Layer } from './layer.js';
93
178
 
94
179
  // Derive
95
- export { deriveFields } from './derive.js';
180
+ export { deriveCliPath, deriveFields } from './derive.js';
96
181
  export type { Field, FieldOverride } from './derive.js';
97
182
 
183
+ // Cross schema
184
+ export { buildCrossValidationSchema } from './cross-schema.js';
185
+
98
186
  // Execute
99
187
  export { executeTrail } from './execute.js';
100
188
  export type { ExecuteTrailOptions } from './execute.js';
101
189
 
190
+ // Intrinsic tracing
191
+ export {
192
+ clearTraceSink,
193
+ createTraceRecord,
194
+ getTraceContext,
195
+ getTraceSink,
196
+ NOOP_SINK,
197
+ registerTraceSink,
198
+ TRACE_CONTEXT_KEY,
199
+ } from './internal/tracing.js';
200
+ export type {
201
+ TraceContext,
202
+ TraceRecord,
203
+ TraceSink,
204
+ } from './internal/tracing.js';
205
+ export type { TraceFn } from './types.js';
206
+
102
207
  // Run
103
208
  export { run } from './run.js';
104
209
  export type { RunOptions } from './run.js';
105
210
 
211
+ // Trail factories
212
+ export { deriveTrail, ingest } from './trails/index.js';
213
+ export type {
214
+ DeriveTrailInput,
215
+ DeriveTrailOperation,
216
+ DeriveTrailOutput,
217
+ DeriveTrailSpec,
218
+ IngestOptions,
219
+ IngestTransform,
220
+ } from './trails/index.js';
221
+
106
222
  // Validation
107
223
  export {
108
224
  validateInput,
@@ -120,7 +236,7 @@ export {
120
236
  retry,
121
237
  withTimeout,
122
238
  shouldRetry,
123
- getBackoffDelay,
239
+ deriveBackoffDelay,
124
240
  } from './resilience.js';
125
241
  export type { RetryOptions } from './resilience.js';
126
242
 
@@ -135,7 +251,7 @@ export {
135
251
  nonEmptyString,
136
252
  positiveInt,
137
253
  shortId,
138
- hashId,
254
+ deriveIdHash,
139
255
  } from './branded.js';
140
256
  export type {
141
257
  Branded,
@@ -146,13 +262,13 @@ export type {
146
262
  } from './branded.js';
147
263
 
148
264
  // Path Security
149
- export { securePath, isPathSafe, resolveSafePath } from './path-security.js';
265
+ export { securePath, isPathSafe, deriveSafePath } from './path-security.js';
150
266
 
151
267
  // Workspace
152
268
  export {
153
269
  findWorkspaceRoot,
154
270
  isInsideWorkspace,
155
- getRelativePath,
271
+ deriveRelativePath,
156
272
  } from './workspace.js';
157
273
 
158
274
  // Blob
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Shared helpers for `ctx.cross([...])` batch execution.
3
+ *
4
+ * These helpers normalize batch options, produce validation results, and
5
+ * implement the unlimited/limited worker-pool execution strategies used by
6
+ * both the real executor (`packages/core/src/execute.ts`) and the scenario
7
+ * runner in `@ontrails/testing`. Extracting them here keeps the validation
8
+ * rule, error message, and worker-pool semantics authored in one place so
9
+ * the two call sites cannot drift.
10
+ *
11
+ * @internal
12
+ */
13
+
14
+ import { ValidationError } from '../errors.js';
15
+ import { Result } from '../result.js';
16
+ import type { CrossBatchOptions } from '../types.js';
17
+
18
+ /**
19
+ * Validate the `concurrency` option on a batch `ctx.cross()` call.
20
+ *
21
+ * Returns `Ok(undefined)` when no limit is requested, `Ok(n)` when a
22
+ * positive integer is supplied, and `Err(ValidationError)` for any other
23
+ * value. The error message is load-bearing: callers and tests depend on
24
+ * the exact string.
25
+ *
26
+ * @internal
27
+ */
28
+ export const normalizeCrossBatchConcurrency = (
29
+ options: CrossBatchOptions | undefined
30
+ ): Result<number | undefined, Error> => {
31
+ const concurrency = options?.concurrency;
32
+ if (concurrency === undefined) {
33
+ return Result.ok();
34
+ }
35
+
36
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
37
+ return Result.err(
38
+ new ValidationError(
39
+ 'ctx.cross() batch concurrency must be a positive integer'
40
+ )
41
+ );
42
+ }
43
+
44
+ return Result.ok(concurrency);
45
+ };
46
+
47
+ /**
48
+ * Produce one validation-error result per call, preserving the original
49
+ * call order. Used when `normalizeCrossBatchConcurrency` fails so the caller
50
+ * can surface a uniform batch shape to the trail implementation.
51
+ *
52
+ * @internal
53
+ */
54
+ export const createCrossBatchValidationResults = <TCall>(
55
+ calls: readonly TCall[],
56
+ error: Error
57
+ ): Result<unknown, Error>[] => calls.map(() => Result.err(error));
58
+
59
+ /**
60
+ * Claim the next branch index from a shared counter. Safe to call from
61
+ * multiple worker coroutines because JavaScript is single-threaded between
62
+ * awaits — the read/increment pair runs without interleaving.
63
+ *
64
+ * @internal
65
+ */
66
+ export const claimNextCrossBatchIndex = <TCall>(
67
+ nextIndex: { value: number },
68
+ calls: readonly TCall[]
69
+ ): number | undefined => {
70
+ if (nextIndex.value >= calls.length) {
71
+ return undefined;
72
+ }
73
+
74
+ const branchIndex = nextIndex.value;
75
+ nextIndex.value += 1;
76
+ return branchIndex;
77
+ };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Internal helper for "forking" a trail context.
3
+ *
4
+ * Several execution sites need to derive a child context from a parent
5
+ * context while **resetting** a well-known set of bound closures (`cross`,
6
+ * `fire`, `resource`). Those closures capture the parent scope, so reusing
7
+ * them on the child would re-enter execution with the wrong attribution,
8
+ * the wrong resource scope, or the wrong fan-out identity. The reset list
9
+ * is the same at every fork site.
10
+ *
11
+ * Codifying the reset list in one helper keeps that contract in a single
12
+ * place. Callers supply the overrides they need (typically `env`,
13
+ * `extensions`, and `logger`) and let the helper handle the reset.
14
+ *
15
+ * This module is internal — do not export it from the package entry point.
16
+ *
17
+ * @remarks
18
+ * The helper is intentionally generic over the concrete context shape. Some
19
+ * callers work with the fully-resolved `TrailContext` produced inside the
20
+ * executor; others work with `Partial<TrailContextInit>` during consumer
21
+ * context derivation in `fire.ts`. Both shapes share the same reset keys.
22
+ */
23
+
24
+ import type { TrailContext, TrailContextInit } from '../types.js';
25
+
26
+ /** Keys cleared by default when forking a context. */
27
+ export type ForkCtxResetKey = 'cross' | 'fire' | 'resource';
28
+
29
+ /** Override fields callers are allowed to apply when forking a context. */
30
+ export type ForkCtxOverrides = Readonly<
31
+ Partial<Pick<TrailContext, 'env' | 'extensions' | 'logger'>>
32
+ >;
33
+
34
+ const DEFAULT_RESET_KEYS: readonly ForkCtxResetKey[] = [
35
+ 'cross',
36
+ 'fire',
37
+ 'resource',
38
+ ];
39
+
40
+ /**
41
+ * Fork a parent context into a child context.
42
+ *
43
+ * Spreads the parent, clears each reset key to `undefined`, and applies the
44
+ * supplied overrides last so callers may replace logger, env, and extensions
45
+ * with branch-local values.
46
+ *
47
+ * The generic parameter is constrained to the minimal shape the helper
48
+ * reads and writes. This lets the helper serve both `TrailContext`
49
+ * (executor scope) and `Partial<TrailContextInit>` (fire-consumer scope)
50
+ * without widening the public surface of either type.
51
+ */
52
+ export const forkCtx = <
53
+ TCtx extends Partial<
54
+ Pick<
55
+ TrailContextInit,
56
+ 'cross' | 'env' | 'extensions' | 'fire' | 'logger' | 'resource'
57
+ >
58
+ >,
59
+ >(
60
+ parentCtx: TCtx,
61
+ overrides: ForkCtxOverrides = {},
62
+ reset: readonly ForkCtxResetKey[] = DEFAULT_RESET_KEYS
63
+ ): TCtx => {
64
+ const forked: TCtx = { ...parentCtx };
65
+ for (const key of reset) {
66
+ forked[key as keyof TCtx] = undefined as TCtx[keyof TCtx];
67
+ }
68
+ return { ...forked, ...overrides };
69
+ };