@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
@@ -0,0 +1,9 @@
1
+ export { deriveTrail } from './derive-trail.js';
2
+ export type {
3
+ DeriveTrailInput,
4
+ DeriveTrailOperation,
5
+ DeriveTrailOutput,
6
+ DeriveTrailSpec,
7
+ } from './derive-trail.js';
8
+ export { ingest } from './ingest.js';
9
+ export type { IngestOptions, IngestTransform } from './ingest.js';
@@ -0,0 +1,146 @@
1
+ import { z } from 'zod';
2
+
3
+ import { InternalError } from '../errors.js';
4
+ import { composeLayers } from '../layer.js';
5
+ import type { Layer } from '../layer.js';
6
+ import { Result } from '../result.js';
7
+ import type { Signal } from '../signal.js';
8
+ import { trail } from '../trail.js';
9
+ import type { Trail, TrailExample, TrailSpec } from '../trail.js';
10
+ import type { TrailContext } from '../types.js';
11
+
12
+ type SchemaValue<TSchema extends z.ZodType> = z.output<TSchema>;
13
+ type SignalRef<TSignal> = string | Signal<TSignal>;
14
+
15
+ type ExampleBearingSchema<TSchema extends z.ZodType> = TSchema & {
16
+ readonly examples?: readonly Partial<SchemaValue<TSchema>>[] | undefined;
17
+ };
18
+
19
+ interface IngestBaseOptions<TSchema extends z.ZodType, TSignal> extends Omit<
20
+ TrailSpec<SchemaValue<TSchema>, void>,
21
+ 'blaze' | 'examples' | 'fires' | 'input' | 'intent' | 'output' | 'pattern'
22
+ > {
23
+ /** Override the derived trail id. Defaults to `${signal}.ingest`. */
24
+ readonly id?: string | undefined;
25
+ /** Validated external payload shape. */
26
+ readonly schema: TSchema;
27
+ /** Signal to emit after verification and optional transformation. */
28
+ readonly signal: SignalRef<TSignal>;
29
+ /** Optional per-trail verification layer, e.g. HMAC signature checks. */
30
+ readonly verify?: Layer | undefined;
31
+ }
32
+
33
+ export type IngestTransform<TInput, TSignal> = (
34
+ payload: TInput,
35
+ ctx: TrailContext
36
+ ) => TSignal | Promise<TSignal>;
37
+
38
+ export interface IngestOptions<
39
+ TSchema extends z.ZodType,
40
+ TSignal,
41
+ > extends IngestBaseOptions<TSchema, TSignal> {
42
+ readonly transform?:
43
+ | IngestTransform<SchemaValue<TSchema>, TSignal>
44
+ | undefined;
45
+ }
46
+
47
+ const resolveSignalId = <TSignal>(signalRef: SignalRef<TSignal>): string =>
48
+ typeof signalRef === 'string' ? signalRef : signalRef.id;
49
+
50
+ const deriveExampleName = (signalId: string, index: number): string =>
51
+ `Ingest ${signalId} ${index + 1}`;
52
+
53
+ const deriveExamples = <TSchema extends z.ZodType>(
54
+ schema: ExampleBearingSchema<TSchema>,
55
+ signalId: string
56
+ ): readonly TrailExample<SchemaValue<TSchema>, void>[] | undefined => {
57
+ const { examples } = schema;
58
+ if (examples === undefined || examples.length === 0) {
59
+ return undefined;
60
+ }
61
+
62
+ return Object.freeze(
63
+ examples.map((example, index) => ({
64
+ input: example,
65
+ name: deriveExampleName(signalId, index),
66
+ }))
67
+ );
68
+ };
69
+
70
+ const createIngestBlaze =
71
+ <TSchema extends z.ZodType, TSignal>(
72
+ signalRef: SignalRef<TSignal>,
73
+ signalId: string,
74
+ trailId: string,
75
+ transform: IngestTransform<SchemaValue<TSchema>, TSignal> | undefined
76
+ ) =>
77
+ async (
78
+ input: SchemaValue<TSchema>,
79
+ ctx: TrailContext
80
+ ): Promise<Result<void, Error>> => {
81
+ if (ctx.fire === undefined) {
82
+ return Result.err(
83
+ new InternalError(
84
+ `ingest("${trailId}") requires topo-backed execution to emit "${signalId}"`
85
+ )
86
+ );
87
+ }
88
+
89
+ try {
90
+ const payload =
91
+ transform === undefined
92
+ ? (input as TSignal)
93
+ : await transform(input, ctx);
94
+ const fired =
95
+ typeof signalRef === 'string'
96
+ ? await ctx.fire(signalRef, payload as unknown)
97
+ : await ctx.fire(signalRef as Signal<unknown>, payload as unknown);
98
+
99
+ return fired.isErr() ? Result.err(fired.error) : Result.ok();
100
+ } catch (error) {
101
+ const message = `ingest("${trailId}"): ${error instanceof Error ? error.message : String(error)}`;
102
+ return Result.err(
103
+ error instanceof Error
104
+ ? new InternalError(message, { cause: error })
105
+ : new InternalError(message)
106
+ );
107
+ }
108
+ };
109
+
110
+ export const ingest = <
111
+ TSchema extends z.ZodType,
112
+ TSignal = SchemaValue<TSchema>,
113
+ >(
114
+ options: IngestOptions<TSchema, TSignal>
115
+ ): Trail<SchemaValue<TSchema>, void> => {
116
+ const signalId = resolveSignalId(options.signal);
117
+ const id = options.id ?? `${signalId}.ingest`;
118
+ const { id: _id, schema, signal, transform, verify, ...trailSpec } = options;
119
+ const baseBlaze = createIngestBlaze<TSchema, TSignal>(
120
+ signal,
121
+ signalId,
122
+ id,
123
+ transform
124
+ );
125
+ const baseTrail = trail(id, {
126
+ ...trailSpec,
127
+ blaze: baseBlaze,
128
+ examples: deriveExamples(schema as ExampleBearingSchema<TSchema>, signalId),
129
+ fires: [signal],
130
+ input: schema as z.ZodType<SchemaValue<TSchema>>,
131
+ intent: 'write',
132
+ output: z.void(),
133
+ pattern: 'ingest',
134
+ }) as Trail<SchemaValue<TSchema>, void>;
135
+
136
+ if (verify === undefined) {
137
+ return baseTrail;
138
+ }
139
+
140
+ // Verification is a per-factory concern, so compose it locally instead of
141
+ // mutating runner-wide layer configuration.
142
+ return Object.freeze({
143
+ ...baseTrail,
144
+ blaze: composeLayers([verify], baseTrail, baseTrail.blaze),
145
+ }) as Trail<SchemaValue<TSchema>, void>;
146
+ };
@@ -0,0 +1,56 @@
1
+ import type { ErrorCategory, TrailsError } from './errors.js';
2
+ import { exitCodeMap, jsonRpcCodeMap, statusCodeMap } from './errors.js';
3
+
4
+ export const transportNames = ['cli', 'http', 'mcp'] as const;
5
+
6
+ export type TransportName = (typeof transportNames)[number];
7
+
8
+ export type TransportErrorMapper<T> = (error: TrailsError) => T;
9
+
10
+ export type TransportErrorMappings<T> = Record<ErrorCategory, T>;
11
+
12
+ /**
13
+ * Union of every transport-specific error code emitted by {@link transportErrorMap}.
14
+ *
15
+ * @remarks
16
+ * Previously parameterised by transport (`TransportErrorCode<'cli'>` etc.), but
17
+ * the generic collapsed to `number` because the underlying maps were typed as
18
+ * `Record<ErrorCategory, number>`. With `as const satisfies` on the maps the
19
+ * per-transport literals are now observable, but TypeScript cannot narrow
20
+ * `transportErrorMap[transport][error.category]` through a generic
21
+ * `TTransport` without an unsound cast. The non-generic union honestly reflects
22
+ * what `mapTransportError` returns at the call site.
23
+ */
24
+ export type TransportErrorCode =
25
+ (typeof transportErrorMap)[TransportName][ErrorCategory];
26
+
27
+ export const createTransportErrorMapper =
28
+ <T>(mappings: TransportErrorMappings<T>): TransportErrorMapper<T> =>
29
+ (error) =>
30
+ mappings[error.category];
31
+
32
+ export const transportErrorMap = {
33
+ cli: exitCodeMap,
34
+ http: statusCodeMap,
35
+ mcp: jsonRpcCodeMap,
36
+ } as const satisfies Record<TransportName, TransportErrorMappings<number>>;
37
+
38
+ export const transportErrorRegistry = {
39
+ cli: {
40
+ map: createTransportErrorMapper(transportErrorMap.cli),
41
+ values: transportErrorMap.cli,
42
+ },
43
+ http: {
44
+ map: createTransportErrorMapper(transportErrorMap.http),
45
+ values: transportErrorMap.http,
46
+ },
47
+ mcp: {
48
+ map: createTransportErrorMapper(transportErrorMap.mcp),
49
+ values: transportErrorMap.mcp,
50
+ },
51
+ } as const;
52
+
53
+ export const mapTransportError = (
54
+ transport: TransportName,
55
+ error: TrailsError
56
+ ): TransportErrorCode => transportErrorMap[transport][error.category];
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Compile-time type assertions for type-utils.
3
+ *
4
+ * This file lives in src/ (not __tests__/) so it is included in the
5
+ * typecheck pass. It contains no runtime code — only type-level
6
+ * assertions that fail the build when type inference regresses.
7
+ *
8
+ * Assertion types are exported to satisfy `noUnusedLocals` but are not
9
+ * re-exported from the package index.
10
+ */
11
+
12
+ import type { Trail } from './trail.js';
13
+ import type { CrossInput, TrailInput } from './type-utils.js';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Helpers
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /** A trail with crossInput declared. */
20
+ type CrossTrail = Trail<
21
+ { name: string },
22
+ { id: string },
23
+ { forkedFrom: string }
24
+ >;
25
+
26
+ /** A trail without crossInput. */
27
+ type PlainTrail = Trail<{ name: string }, { id: string }>;
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // CrossInput<T> must include crossInput fields
31
+ // ---------------------------------------------------------------------------
32
+
33
+ type WithCrossInput = CrossInput<CrossTrail>;
34
+
35
+ // Must require both `name` AND `forkedFrom`.
36
+ // Before the fix, `forkedFrom` was erased.
37
+ type AssertMerged = WithCrossInput extends { name: string; forkedFrom: string }
38
+ ? true
39
+ : false;
40
+ export type Merged = [AssertMerged] extends [true] ? 'pass' : never;
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // CrossInput<T> falls back to TrailInput<T> when no crossInput
44
+ // ---------------------------------------------------------------------------
45
+
46
+ type WithoutCrossInput = CrossInput<PlainTrail>;
47
+ type BaseInput = TrailInput<PlainTrail>;
48
+
49
+ // These should be mutually assignable (identical).
50
+ type AssertFallback1 = WithoutCrossInput extends BaseInput ? true : false;
51
+ type AssertFallback2 = BaseInput extends WithoutCrossInput ? true : false;
52
+ export type Fallback = [AssertFallback1, AssertFallback2] extends [true, true]
53
+ ? 'pass'
54
+ : never;
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // BlazeInput: blaze receives crossInput fields when declared
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /**
61
+ * When a trail has crossInput, the blaze's first parameter must include
62
+ * both the public input fields AND the crossInput fields. Before the fix,
63
+ * blaze was typed as Implementation<I, O>, losing the CI fields.
64
+ */
65
+ type CrossBlazeParam = Parameters<CrossTrail['blaze']>[0];
66
+
67
+ type AssertBlazeHasCrossFields = CrossBlazeParam extends {
68
+ name: string;
69
+ forkedFrom: string;
70
+ }
71
+ ? true
72
+ : false;
73
+ export type BlazeWithCross = [AssertBlazeHasCrossFields] extends [true]
74
+ ? 'pass'
75
+ : never;
76
+
77
+ // Plain trail blaze should only receive the public input type.
78
+ type PlainBlazeParam = Parameters<PlainTrail['blaze']>[0];
79
+
80
+ type AssertPlainBlazeIsInput = PlainBlazeParam extends { name: string }
81
+ ? true
82
+ : false;
83
+ type AssertPlainBlazeNoExtra = { name: string } extends PlainBlazeParam
84
+ ? true
85
+ : false;
86
+ export type BlazeWithoutCross = [
87
+ AssertPlainBlazeIsInput,
88
+ AssertPlainBlazeNoExtra,
89
+ ] extends [true, true]
90
+ ? 'pass'
91
+ : never;
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // CI = never default preserves backward compat
95
+ // ---------------------------------------------------------------------------
96
+
97
+ // Trail<I, O> (two generics) should be assignable to Trail<I, O, never>
98
+ type AssertDefault =
99
+ Trail<{ x: number }, { y: number }> extends Trail<
100
+ { x: number },
101
+ { y: number },
102
+ never
103
+ >
104
+ ? true
105
+ : false;
106
+ export type Default = [AssertDefault] extends [true] ? 'pass' : never;
package/src/type-utils.ts CHANGED
@@ -13,11 +13,26 @@ import type { AnyTrail, Trail } from './trail.js';
13
13
 
14
14
  /** Extract the input type from a Trail. */
15
15
  export type TrailInput<T extends AnyTrail> =
16
- T extends Trail<infer I, any> ? I : never;
16
+ T extends Trail<infer I, any, any> ? I : never;
17
17
 
18
18
  /** Extract the output type from a Trail. */
19
19
  export type TrailOutput<T extends AnyTrail> =
20
- T extends Trail<any, infer O> ? O : never;
20
+ T extends Trail<any, infer O, any> ? O : never;
21
+
22
+ /**
23
+ * Extract the cross-callable input type from a trail.
24
+ *
25
+ * When a trail declares `crossInput`, callers via `ctx.cross()` must pass
26
+ * both the public input fields and the composition-only fields. This type
27
+ * merges both schemas so the compiler enforces the full shape at the call
28
+ * site. Falls back to plain `TrailInput<T>` when no `crossInput` exists.
29
+ */
30
+ export type CrossInput<T extends AnyTrail> =
31
+ T extends Trail<infer I, any, infer CI>
32
+ ? [CI] extends [never]
33
+ ? I
34
+ : I & CI
35
+ : never;
21
36
 
22
37
  /**
23
38
  * Extracts the full `Result<Output, Error>` type from a trail definition.
package/src/types.ts CHANGED
@@ -1,4 +1,64 @@
1
+ import type { TrailsError } from './errors.js';
1
2
  import type { Result } from './result.js';
3
+ import type { Signal } from './signal.js';
4
+ import type { AnyTrail } from './trail.js';
5
+ import type { CrossInput, TrailOutput } from './type-utils.js';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Detour
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /** A recovery path that activates when a trail's blaze fails with a matching error. */
12
+ export interface Detour<Input, Output, TErr extends TrailsError = TrailsError> {
13
+ /* oxlint-disable-next-line no-explicit-any -- standard pattern for matching abstract+concrete class constructors */
14
+ readonly on: abstract new (...args: any[]) => TErr;
15
+ readonly maxAttempts?: number | undefined;
16
+ readonly recover: (
17
+ attempt: DetourAttempt<Input, TErr>,
18
+ ctx: TrailContext
19
+ ) => Promise<Result<Output, TrailsError>>;
20
+ }
21
+
22
+ /** Context passed to a detour's recover function on each attempt. */
23
+ export interface DetourAttempt<Input, TErr extends TrailsError = TrailsError> {
24
+ /** 1-indexed attempt number */
25
+ readonly attempt: number;
26
+ /** The matched error */
27
+ readonly error: TErr;
28
+ /** Original trail input */
29
+ readonly input: Input;
30
+ }
31
+
32
+ type CrossBatchCall<TTarget extends AnyTrail | string = AnyTrail | string> =
33
+ TTarget extends AnyTrail
34
+ ? readonly [trail: TTarget, input: CrossInput<TTarget>]
35
+ : readonly [id: string, input: unknown];
36
+
37
+ type CrossBatchResult<TTarget extends AnyTrail | string> =
38
+ TTarget extends AnyTrail
39
+ ? Result<TrailOutput<TTarget>, Error>
40
+ : Result<unknown, Error>;
41
+
42
+ type CrossBatchResults<TCalls extends readonly CrossBatchCall[]> = {
43
+ readonly [K in keyof TCalls]: TCalls[K] extends readonly [
44
+ infer TTarget,
45
+ unknown,
46
+ ]
47
+ ? TTarget extends AnyTrail | string
48
+ ? CrossBatchResult<TTarget>
49
+ : never
50
+ : never;
51
+ };
52
+
53
+ /** Runtime options for batch `ctx.cross([...])` calls. */
54
+ export interface CrossBatchOptions {
55
+ /**
56
+ * Maximum number of branches to execute concurrently.
57
+ *
58
+ * Omit for unbounded concurrency. `1` is equivalent to sequential execution.
59
+ */
60
+ readonly concurrency?: number | undefined;
61
+ }
2
62
 
3
63
  /**
4
64
  * Trail implementation — sync or async.
@@ -11,17 +71,78 @@ export type Implementation<I, O> = (
11
71
  ctx: TrailContext
12
72
  ) => Result<O, Error> | Promise<Result<O, Error>>;
13
73
 
14
- /** Invoke another trail by id — used for trail composition */
15
- export type CrossFn = <O>(
16
- id: string,
17
- input: unknown
18
- ) => Promise<Result<O, Error>>;
74
+ /**
75
+ * Invoke another trail — used for trail composition.
76
+ *
77
+ * Two call shapes:
78
+ *
79
+ * - **By trail object** (typed): `ctx.cross(showGist, { id })` — the compiler
80
+ * infers `I` and `O` from the trail's schemas, so the result is fully typed.
81
+ * - **By string id** (untyped escape hatch): `ctx.cross('gist.show', { id })`
82
+ * — returns `Result<O, Error>` where `O` defaults to `unknown`.
83
+ * - **By batch**: `ctx.cross([[showGist, { id }], ['audit.log', payload]])`
84
+ * — executes every crossing concurrently and resolves once all results are
85
+ * available. Result ordering always matches the input tuple ordering. Pass
86
+ * `{ concurrency: N }` as the second argument to limit how many branches
87
+ * run at once.
88
+ */
89
+ export interface CrossFn {
90
+ <const TCalls extends readonly CrossBatchCall[]>(
91
+ calls: TCalls,
92
+ options?: CrossBatchOptions
93
+ ): Promise<CrossBatchResults<TCalls>>;
94
+ <T extends AnyTrail>(
95
+ trail: T,
96
+ input: CrossInput<T>
97
+ ): Promise<Result<TrailOutput<T>, Error>>;
98
+ <O = unknown>(id: string, input: unknown): Promise<Result<O, Error>>;
99
+ }
19
100
 
20
- /** Resolve a provision instance from the current trail context. */
21
- export type ProvisionLookup = <T = unknown>(
22
- provisionOrId: { readonly id: string } | string
101
+ /**
102
+ * Emit a signal used for signal-driven activation.
103
+ *
104
+ * Fan-out to consumer trails (those with the signal in their `on:` array) is
105
+ * the framework's responsibility. Producers get `Result.ok(undefined)` unless
106
+ * the signal id is unknown or the payload fails schema validation. Consumer
107
+ * errors are logged but do not propagate back to the producer. Consumers fan
108
+ * out in parallel, each with its own derived context. Runtime cycle
109
+ * suppression is still signal-id-based against the current fire stack: it
110
+ * prevents re-entrant loops but can over-suppress legitimate diamond
111
+ * re-fires, with a debug breadcrumb and a warn emitted when suppression happens.
112
+ *
113
+ * Two call shapes are supported:
114
+ *
115
+ * - **By id** (base shape): `ctx.fire('order.placed', { ... })`. Matches
116
+ * the shape of `ctx.cross`; payload is typed as `unknown` and validated
117
+ * against the signal's schema at the fire boundary.
118
+ * - **By signal value** (progressive disclosure): `ctx.fire(orderPlaced, payload)`
119
+ * where `orderPlaced` is a `Signal<T>`. The compiler enforces that
120
+ * `payload` matches the signal's declared schema type at the call site,
121
+ * on top of the runtime validation.
122
+ */
123
+ export interface FireFn {
124
+ <T>(signal: Signal<T>, payload: T): Promise<Result<void, Error>>;
125
+ (signalId: string, payload: unknown): Promise<Result<void, Error>>;
126
+ }
127
+
128
+ /** Resolve a resource instance from the current trail context. */
129
+ export type ResourceLookup = <T = unknown>(
130
+ resourceOrId: { readonly id: string } | string
23
131
  ) => T;
24
132
 
133
+ /**
134
+ * Wrap the execution of `fn` in a child trace span.
135
+ *
136
+ * Creates a nested span under the current trail's root trace record, times
137
+ * the callback, records success or failure (including error category), and
138
+ * writes the completed span to the registered sink. Errors thrown by `fn`
139
+ * are recorded on the span and then rethrown — tracing never swallows them.
140
+ */
141
+ export type TraceFn = <T>(
142
+ label: string,
143
+ fn: () => T | Promise<T>
144
+ ) => Promise<T>;
145
+
25
146
  /** Callback for reporting progress from long-running trails */
26
147
  export type ProgressCallback = (event: ProgressEvent) => void;
27
148
 
@@ -60,6 +181,12 @@ export interface TrailContext {
60
181
  readonly requestId: string;
61
182
  readonly abortSignal: AbortSignal;
62
183
  readonly cross?: CrossFn | undefined;
184
+ /**
185
+ * Emit a signal by id. Fans out to every trail with the signal in its
186
+ * `on:` declaration. Bound by the runner that holds the topo (typically
187
+ * `run()`); undefined when a context is constructed without topo access.
188
+ */
189
+ readonly fire?: FireFn | undefined;
63
190
  readonly permit?: BasePermit;
64
191
  readonly workspaceRoot?: string | undefined;
65
192
  readonly logger?: Logger | undefined;
@@ -67,7 +194,16 @@ export interface TrailContext {
67
194
  readonly cwd?: string | undefined;
68
195
  readonly env?: Record<string, string | undefined> | undefined;
69
196
  readonly extensions?: Readonly<Record<string, unknown>> | undefined;
70
- readonly provision?: ProvisionLookup | undefined;
197
+ readonly resource?: ResourceLookup | undefined;
198
+ /**
199
+ * Wrap a callback in a child trace span.
200
+ *
201
+ * Always present on contexts produced by `executeTrail` or
202
+ * `createTrailContext`. Optional on the interface so manually constructed
203
+ * contexts (tests, ad-hoc compositions) don't have to supply one — call
204
+ * sites tolerate `undefined` by falling back to a no-op passthrough.
205
+ */
206
+ readonly trace?: TraceFn | undefined;
71
207
  }
72
208
 
73
209
  /**
@@ -82,6 +218,7 @@ export type PermitRequirement =
82
218
  | 'public';
83
219
 
84
220
  /** Input shape used to seed a runtime TrailContext before resolution. */
85
- export type TrailContextInit = Omit<TrailContext, 'provision'> & {
86
- readonly provision?: ProvisionLookup | undefined;
221
+ export type TrailContextInit = Omit<TrailContext, 'resource' | 'trace'> & {
222
+ readonly resource?: ResourceLookup | undefined;
223
+ readonly trace?: TraceFn | undefined;
87
224
  };
@@ -0,0 +1,63 @@
1
+ import { ValidationError } from './errors.js';
2
+ import { Result } from './result.js';
3
+ import type { Topo } from './topo.js';
4
+ import { validateDraftFreeTopo } from './draft.js';
5
+ import type { TopoIssue } from './validate-topo.js';
6
+ import { validateTopo } from './validate-topo.js';
7
+
8
+ const PROJECTION_BLOCKING_RULES = new Set([
9
+ 'cross-cycle',
10
+ 'cross-exists',
11
+ 'no-self-cross',
12
+ 'resource-exists',
13
+ 'signal-origin-exists',
14
+ ]);
15
+
16
+ const keepProjectionBlockingIssues = (
17
+ result: ReturnType<typeof validateTopo>
18
+ ) => {
19
+ if (result.isOk()) {
20
+ return result;
21
+ }
22
+
23
+ const issues = (
24
+ result.error.context as { issues?: readonly TopoIssue[] } | undefined
25
+ )?.issues;
26
+ const remainingIssues = issues?.filter((issue) =>
27
+ PROJECTION_BLOCKING_RULES.has(issue.rule)
28
+ );
29
+
30
+ if (remainingIssues === undefined || remainingIssues.length === 0) {
31
+ return Result.ok();
32
+ }
33
+
34
+ return Result.err(
35
+ new ValidationError(
36
+ `Topo validation failed with ${remainingIssues.length} issue(s)`,
37
+ {
38
+ cause: result.error,
39
+ context: { issues: remainingIssues },
40
+ }
41
+ )
42
+ );
43
+ };
44
+
45
+ /**
46
+ * Validate that a topo is ready for established outputs.
47
+ *
48
+ * Established surfaces still require the authored graph to be structurally
49
+ * valid, and they must also reject any remaining draft state.
50
+ */
51
+ export const validateEstablishedTopo = (topo: Topo) => {
52
+ const structural = keepProjectionBlockingIssues(validateTopo(topo));
53
+ if (structural.isErr()) {
54
+ return structural;
55
+ }
56
+
57
+ const established = validateDraftFreeTopo(topo);
58
+ if (established.isErr()) {
59
+ return established;
60
+ }
61
+
62
+ return Result.ok();
63
+ };