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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +61 -16
  3. package/package.json +9 -8
  4. package/src/activation-provenance.ts +111 -0
  5. package/src/activation-source-compatibility.ts +430 -0
  6. package/src/activation-source-projection.ts +221 -0
  7. package/src/activation-source.ts +91 -0
  8. package/src/blob-ref.ts +51 -0
  9. package/src/context.ts +4 -0
  10. package/src/{internal/cross-batch.ts → cross-batch.ts} +3 -11
  11. package/src/detours.ts +8 -0
  12. package/src/draft.ts +4 -0
  13. package/src/error-projection.ts +51 -0
  14. package/src/errors.ts +192 -39
  15. package/src/execute.ts +443 -44
  16. package/src/fire.ts +965 -91
  17. package/src/index.ts +285 -38
  18. package/src/layer-projection.ts +192 -0
  19. package/src/layer.ts +32 -1
  20. package/src/observe.ts +361 -0
  21. package/src/permits.ts +12 -0
  22. package/src/redaction/patterns.ts +6 -3
  23. package/src/resilience.ts +8 -8
  24. package/src/resource-config.ts +477 -44
  25. package/src/resource.ts +21 -1
  26. package/src/result.ts +18 -0
  27. package/src/run.ts +2 -2
  28. package/src/schedule-runtime.ts +689 -0
  29. package/src/schedule.ts +324 -0
  30. package/src/serialization.ts +90 -57
  31. package/src/signal-diagnostics.ts +633 -0
  32. package/src/{internal/signal-ref.ts → signal-ref.ts} +1 -1
  33. package/src/signal.ts +37 -0
  34. package/src/store/accessor-protocol.ts +5 -5
  35. package/src/structured-examples.ts +245 -0
  36. package/src/surface-derivation.ts +91 -0
  37. package/src/surface-filter.ts +7 -13
  38. package/src/topo.ts +386 -6
  39. package/src/tracing.ts +345 -0
  40. package/src/trail.ts +168 -22
  41. package/src/trails/derive-trail.ts +1 -1
  42. package/src/trails/ingest.ts +7 -15
  43. package/src/{internal/trails-db.ts → trails-db.ts} +42 -21
  44. package/src/transport-error-map.ts +146 -28
  45. package/src/types.ts +82 -28
  46. package/src/validate-established-topo.ts +6 -0
  47. package/src/validate-topo.ts +212 -5
  48. package/src/validation.ts +58 -1
  49. package/src/webhook.ts +276 -0
  50. package/src/{internal/zod-wrappers.ts → zod-wrappers.ts} +0 -6
  51. package/.turbo/turbo-build.log +0 -1
  52. package/.turbo/turbo-lint.log +0 -3
  53. package/.turbo/turbo-typecheck.log +0 -1
  54. package/dist/blob-ref.d.ts +0 -20
  55. package/dist/blob-ref.d.ts.map +0 -1
  56. package/dist/blob-ref.js +0 -22
  57. package/dist/blob-ref.js.map +0 -1
  58. package/dist/branded.d.ts +0 -36
  59. package/dist/branded.d.ts.map +0 -1
  60. package/dist/branded.js +0 -89
  61. package/dist/branded.js.map +0 -1
  62. package/dist/collections.d.ts +0 -31
  63. package/dist/collections.d.ts.map +0 -1
  64. package/dist/collections.js +0 -60
  65. package/dist/collections.js.map +0 -1
  66. package/dist/context.d.ts +0 -22
  67. package/dist/context.d.ts.map +0 -1
  68. package/dist/context.js +0 -37
  69. package/dist/context.js.map +0 -1
  70. package/dist/contour.d.ts +0 -81
  71. package/dist/contour.d.ts.map +0 -1
  72. package/dist/contour.js +0 -176
  73. package/dist/contour.js.map +0 -1
  74. package/dist/cross-schema.d.ts +0 -18
  75. package/dist/cross-schema.d.ts.map +0 -1
  76. package/dist/cross-schema.js +0 -29
  77. package/dist/cross-schema.js.map +0 -1
  78. package/dist/derive.d.ts +0 -44
  79. package/dist/derive.d.ts.map +0 -1
  80. package/dist/derive.js +0 -149
  81. package/dist/derive.js.map +0 -1
  82. package/dist/draft.d.ts +0 -28
  83. package/dist/draft.d.ts.map +0 -1
  84. package/dist/draft.js +0 -157
  85. package/dist/draft.js.map +0 -1
  86. package/dist/errors.d.ts +0 -141
  87. package/dist/errors.d.ts.map +0 -1
  88. package/dist/errors.js +0 -185
  89. package/dist/errors.js.map +0 -1
  90. package/dist/execute.d.ts +0 -52
  91. package/dist/execute.d.ts.map +0 -1
  92. package/dist/execute.js +0 -511
  93. package/dist/execute.js.map +0 -1
  94. package/dist/fetch.d.ts +0 -15
  95. package/dist/fetch.d.ts.map +0 -1
  96. package/dist/fetch.js +0 -102
  97. package/dist/fetch.js.map +0 -1
  98. package/dist/fire.d.ts +0 -43
  99. package/dist/fire.d.ts.map +0 -1
  100. package/dist/fire.js +0 -185
  101. package/dist/fire.js.map +0 -1
  102. package/dist/guards.d.ts +0 -17
  103. package/dist/guards.d.ts.map +0 -1
  104. package/dist/guards.js +0 -25
  105. package/dist/guards.js.map +0 -1
  106. package/dist/index.d.ts +0 -59
  107. package/dist/index.d.ts.map +0 -1
  108. package/dist/index.js +0 -61
  109. package/dist/index.js.map +0 -1
  110. package/dist/internal/cross-batch.d.ts +0 -44
  111. package/dist/internal/cross-batch.d.ts.map +0 -1
  112. package/dist/internal/cross-batch.js +0 -58
  113. package/dist/internal/cross-batch.js.map +0 -1
  114. package/dist/internal/fork-ctx.d.ts +0 -41
  115. package/dist/internal/fork-ctx.d.ts.map +0 -1
  116. package/dist/internal/fork-ctx.js +0 -47
  117. package/dist/internal/fork-ctx.js.map +0 -1
  118. package/dist/internal/signal-ref.d.ts +0 -15
  119. package/dist/internal/signal-ref.d.ts.map +0 -1
  120. package/dist/internal/signal-ref.js +0 -41
  121. package/dist/internal/signal-ref.js.map +0 -1
  122. package/dist/internal/topo-saves.d.ts +0 -53
  123. package/dist/internal/topo-saves.d.ts.map +0 -1
  124. package/dist/internal/topo-saves.js +0 -449
  125. package/dist/internal/topo-saves.js.map +0 -1
  126. package/dist/internal/topo-snapshots.d.ts +0 -57
  127. package/dist/internal/topo-snapshots.d.ts.map +0 -1
  128. package/dist/internal/topo-snapshots.js +0 -372
  129. package/dist/internal/topo-snapshots.js.map +0 -1
  130. package/dist/internal/topo-store-read.d.ts +0 -70
  131. package/dist/internal/topo-store-read.d.ts.map +0 -1
  132. package/dist/internal/topo-store-read.js +0 -221
  133. package/dist/internal/topo-store-read.js.map +0 -1
  134. package/dist/internal/topo-store.d.ts +0 -26
  135. package/dist/internal/topo-store.d.ts.map +0 -1
  136. package/dist/internal/topo-store.js +0 -621
  137. package/dist/internal/topo-store.js.map +0 -1
  138. package/dist/internal/tracing.d.ts +0 -98
  139. package/dist/internal/tracing.d.ts.map +0 -1
  140. package/dist/internal/tracing.js +0 -102
  141. package/dist/internal/tracing.js.map +0 -1
  142. package/dist/internal/trails-db.d.ts +0 -16
  143. package/dist/internal/trails-db.d.ts.map +0 -1
  144. package/dist/internal/trails-db.js +0 -118
  145. package/dist/internal/trails-db.js.map +0 -1
  146. package/dist/internal/zod-wrappers.d.ts +0 -40
  147. package/dist/internal/zod-wrappers.d.ts.map +0 -1
  148. package/dist/internal/zod-wrappers.js +0 -59
  149. package/dist/internal/zod-wrappers.js.map +0 -1
  150. package/dist/layer.d.ts +0 -23
  151. package/dist/layer.d.ts.map +0 -1
  152. package/dist/layer.js +0 -21
  153. package/dist/layer.js.map +0 -1
  154. package/dist/path-security.d.ts +0 -28
  155. package/dist/path-security.d.ts.map +0 -1
  156. package/dist/path-security.js +0 -63
  157. package/dist/path-security.js.map +0 -1
  158. package/dist/patterns/bulk.d.ts +0 -15
  159. package/dist/patterns/bulk.d.ts.map +0 -1
  160. package/dist/patterns/bulk.js +0 -14
  161. package/dist/patterns/bulk.js.map +0 -1
  162. package/dist/patterns/change.d.ts +0 -10
  163. package/dist/patterns/change.d.ts.map +0 -1
  164. package/dist/patterns/change.js +0 -10
  165. package/dist/patterns/change.js.map +0 -1
  166. package/dist/patterns/date-range.d.ts +0 -10
  167. package/dist/patterns/date-range.d.ts.map +0 -1
  168. package/dist/patterns/date-range.js +0 -10
  169. package/dist/patterns/date-range.js.map +0 -1
  170. package/dist/patterns/index.d.ts +0 -9
  171. package/dist/patterns/index.d.ts.map +0 -1
  172. package/dist/patterns/index.js +0 -9
  173. package/dist/patterns/index.js.map +0 -1
  174. package/dist/patterns/pagination.d.ts +0 -18
  175. package/dist/patterns/pagination.d.ts.map +0 -1
  176. package/dist/patterns/pagination.js +0 -18
  177. package/dist/patterns/pagination.js.map +0 -1
  178. package/dist/patterns/progress.d.ts +0 -11
  179. package/dist/patterns/progress.d.ts.map +0 -1
  180. package/dist/patterns/progress.js +0 -11
  181. package/dist/patterns/progress.js.map +0 -1
  182. package/dist/patterns/sorting.d.ts +0 -13
  183. package/dist/patterns/sorting.d.ts.map +0 -1
  184. package/dist/patterns/sorting.js +0 -10
  185. package/dist/patterns/sorting.js.map +0 -1
  186. package/dist/patterns/status.d.ts +0 -15
  187. package/dist/patterns/status.d.ts.map +0 -1
  188. package/dist/patterns/status.js +0 -9
  189. package/dist/patterns/status.js.map +0 -1
  190. package/dist/patterns/timestamps.d.ts +0 -10
  191. package/dist/patterns/timestamps.d.ts.map +0 -1
  192. package/dist/patterns/timestamps.js +0 -10
  193. package/dist/patterns/timestamps.js.map +0 -1
  194. package/dist/redaction/index.d.ts +0 -4
  195. package/dist/redaction/index.d.ts.map +0 -1
  196. package/dist/redaction/index.js +0 -3
  197. package/dist/redaction/index.js.map +0 -1
  198. package/dist/redaction/patterns.d.ts +0 -9
  199. package/dist/redaction/patterns.d.ts.map +0 -1
  200. package/dist/redaction/patterns.js +0 -39
  201. package/dist/redaction/patterns.js.map +0 -1
  202. package/dist/redaction/redactor.d.ts +0 -27
  203. package/dist/redaction/redactor.d.ts.map +0 -1
  204. package/dist/redaction/redactor.js +0 -89
  205. package/dist/redaction/redactor.js.map +0 -1
  206. package/dist/resilience.d.ts +0 -34
  207. package/dist/resilience.d.ts.map +0 -1
  208. package/dist/resilience.js +0 -164
  209. package/dist/resilience.js.map +0 -1
  210. package/dist/resource-config.d.ts +0 -22
  211. package/dist/resource-config.d.ts.map +0 -1
  212. package/dist/resource-config.js +0 -210
  213. package/dist/resource-config.js.map +0 -1
  214. package/dist/resource.d.ts +0 -74
  215. package/dist/resource.d.ts.map +0 -1
  216. package/dist/resource.js +0 -61
  217. package/dist/resource.js.map +0 -1
  218. package/dist/result.d.ts +0 -57
  219. package/dist/result.d.ts.map +0 -1
  220. package/dist/result.js +0 -156
  221. package/dist/result.js.map +0 -1
  222. package/dist/run.d.ts +0 -28
  223. package/dist/run.d.ts.map +0 -1
  224. package/dist/run.js +0 -35
  225. package/dist/run.js.map +0 -1
  226. package/dist/serialization.d.ts +0 -27
  227. package/dist/serialization.d.ts.map +0 -1
  228. package/dist/serialization.js +0 -154
  229. package/dist/serialization.js.map +0 -1
  230. package/dist/signal.d.ts +0 -33
  231. package/dist/signal.d.ts.map +0 -1
  232. package/dist/signal.js +0 -17
  233. package/dist/signal.js.map +0 -1
  234. package/dist/store/accessor-protocol.d.ts +0 -51
  235. package/dist/store/accessor-protocol.d.ts.map +0 -1
  236. package/dist/store/accessor-protocol.js +0 -2
  237. package/dist/store/accessor-protocol.js.map +0 -1
  238. package/dist/store/index.d.ts +0 -2
  239. package/dist/store/index.d.ts.map +0 -1
  240. package/dist/store/index.js +0 -2
  241. package/dist/store/index.js.map +0 -1
  242. package/dist/surface-filter.d.ts +0 -13
  243. package/dist/surface-filter.d.ts.map +0 -1
  244. package/dist/surface-filter.js +0 -76
  245. package/dist/surface-filter.js.map +0 -1
  246. package/dist/topo-store.d.ts +0 -62
  247. package/dist/topo-store.d.ts.map +0 -1
  248. package/dist/topo-store.js +0 -319
  249. package/dist/topo-store.js.map +0 -1
  250. package/dist/topo.d.ts +0 -39
  251. package/dist/topo.d.ts.map +0 -1
  252. package/dist/topo.js +0 -241
  253. package/dist/topo.js.map +0 -1
  254. package/dist/trail.d.ts +0 -163
  255. package/dist/trail.d.ts.map +0 -1
  256. package/dist/trail.js +0 -75
  257. package/dist/trail.js.map +0 -1
  258. package/dist/trails/derive-trail.d.ts +0 -66
  259. package/dist/trails/derive-trail.d.ts.map +0 -1
  260. package/dist/trails/derive-trail.js +0 -378
  261. package/dist/trails/derive-trail.js.map +0 -1
  262. package/dist/trails/index.d.ts +0 -5
  263. package/dist/trails/index.d.ts.map +0 -1
  264. package/dist/trails/index.js +0 -3
  265. package/dist/trails/index.js.map +0 -1
  266. package/dist/trails/ingest.d.ts +0 -24
  267. package/dist/trails/ingest.d.ts.map +0 -1
  268. package/dist/trails/ingest.js +0 -63
  269. package/dist/trails/ingest.js.map +0 -1
  270. package/dist/transport-error-map.d.ts +0 -106
  271. package/dist/transport-error-map.d.ts.map +0 -1
  272. package/dist/transport-error-map.js +0 -24
  273. package/dist/transport-error-map.js.map +0 -1
  274. package/dist/type-checks.test-d.d.ts +0 -71
  275. package/dist/type-checks.test-d.d.ts.map +0 -1
  276. package/dist/type-checks.test-d.js +0 -12
  277. package/dist/type-checks.test-d.js.map +0 -1
  278. package/dist/type-utils.d.ts +0 -33
  279. package/dist/type-utils.d.ts.map +0 -1
  280. package/dist/type-utils.js +0 -12
  281. package/dist/type-utils.js.map +0 -1
  282. package/dist/types.d.ts +0 -177
  283. package/dist/types.d.ts.map +0 -1
  284. package/dist/types.js +0 -3
  285. package/dist/types.js.map +0 -1
  286. package/dist/validate-established-topo.d.ts +0 -76
  287. package/dist/validate-established-topo.d.ts.map +0 -1
  288. package/dist/validate-established-topo.js +0 -43
  289. package/dist/validate-established-topo.js.map +0 -1
  290. package/dist/validate-topo.d.ts +0 -24
  291. package/dist/validate-topo.d.ts.map +0 -1
  292. package/dist/validate-topo.js +0 -193
  293. package/dist/validate-topo.js.map +0 -1
  294. package/dist/validation.d.ts +0 -27
  295. package/dist/validation.d.ts.map +0 -1
  296. package/dist/validation.js +0 -165
  297. package/dist/validation.js.map +0 -1
  298. package/dist/workspace.d.ts +0 -25
  299. package/dist/workspace.d.ts.map +0 -1
  300. package/dist/workspace.js +0 -57
  301. package/dist/workspace.js.map +0 -1
  302. package/src/__tests__/blob-ref.test.ts +0 -103
  303. package/src/__tests__/branded.test.ts +0 -148
  304. package/src/__tests__/collections.test.ts +0 -126
  305. package/src/__tests__/context.test.ts +0 -77
  306. package/src/__tests__/contour.test.ts +0 -263
  307. package/src/__tests__/derive-trail.test.ts +0 -668
  308. package/src/__tests__/derive.test.ts +0 -260
  309. package/src/__tests__/errors.test.ts +0 -427
  310. package/src/__tests__/execute.test.ts +0 -1563
  311. package/src/__tests__/fetch.test.ts +0 -217
  312. package/src/__tests__/fire.test.ts +0 -1056
  313. package/src/__tests__/fork-ctx.test.ts +0 -83
  314. package/src/__tests__/guards.test.ts +0 -102
  315. package/src/__tests__/ingest.test.ts +0 -191
  316. package/src/__tests__/layer.test.ts +0 -125
  317. package/src/__tests__/path-security.test.ts +0 -114
  318. package/src/__tests__/patterns.test.ts +0 -273
  319. package/src/__tests__/redaction.test.ts +0 -244
  320. package/src/__tests__/resilience.test.ts +0 -246
  321. package/src/__tests__/resource.test.ts +0 -204
  322. package/src/__tests__/result.test.ts +0 -155
  323. package/src/__tests__/run.test.ts +0 -181
  324. package/src/__tests__/serialization.test.ts +0 -401
  325. package/src/__tests__/service-config.test.ts +0 -224
  326. package/src/__tests__/signal.test.ts +0 -82
  327. package/src/__tests__/surface-filter.test.ts +0 -196
  328. package/src/__tests__/topo-store-read.test.ts +0 -256
  329. package/src/__tests__/topo-store.test.ts +0 -869
  330. package/src/__tests__/topo.test.ts +0 -573
  331. package/src/__tests__/trail-permit.test.ts +0 -60
  332. package/src/__tests__/trail.test.ts +0 -426
  333. package/src/__tests__/trails-db.test.ts +0 -198
  334. package/src/__tests__/transport-error-map.test.ts +0 -82
  335. package/src/__tests__/type-utils.test.ts +0 -118
  336. package/src/__tests__/validate-topo.test.ts +0 -611
  337. package/src/__tests__/validation.test.ts +0 -336
  338. package/src/__tests__/workspace.test.ts +0 -183
  339. package/src/__tests__/zod-wrappers.test.ts +0 -62
  340. package/src/internal/topo-snapshots.ts +0 -517
  341. package/src/internal/topo-store-read.ts +0 -481
  342. package/src/internal/topo-store.ts +0 -1192
  343. package/src/internal/tracing.ts +0 -187
  344. package/src/topo-store.ts +0 -510
  345. package/src/type-checks.test-d.ts +0 -106
  346. package/tsconfig.json +0 -9
  347. package/tsconfig.tests.json +0 -10
  348. package/tsconfig.tsbuildinfo +0 -1
package/src/tracing.ts ADDED
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Intrinsic tracing primitives.
3
+ *
4
+ * This module is the home for the trace record type, the sink interface,
5
+ * the sink registry, and the helpers `executeTrail` uses to create root
6
+ * trace records and child spans. Core keeps this minimal contract public so
7
+ * `@ontrails/observe`, `@ontrails/tracing`, adapters, and tests share the
8
+ * same intrinsic execution record shape.
9
+ *
10
+ * Tracing is intrinsic: every `executeTrail` call automatically produces a
11
+ * root `TraceRecord`, `ctx.trace(label, fn)` creates nested child spans, and
12
+ * the signal runtime records lifecycle points underneath the active producer
13
+ * trace. A default no-op sink is installed at module load so core never
14
+ * crashes when no real sink has been registered.
15
+ */
16
+
17
+ /** Signal lifecycle records emitted by the typed signal runtime. */
18
+ export type SignalTraceRecordName =
19
+ | 'signal.fired'
20
+ | 'signal.handler.completed'
21
+ | 'signal.handler.failed'
22
+ | 'signal.handler.invoked'
23
+ | 'signal.handler.predicate_failed'
24
+ | 'signal.handler.predicate_matched'
25
+ | 'signal.handler.predicate_skipped'
26
+ | 'signal.invalid';
27
+
28
+ /** Activation boundary records emitted by runtime materializers. */
29
+ export type ActivationTraceRecordName =
30
+ | 'activation.cycle_detected'
31
+ | 'activation.scheduled'
32
+ | 'activation.webhook'
33
+ | 'activation.webhook.invalid';
34
+
35
+ /** Evidence of a single trail execution, manual span, activation boundary, or signal lifecycle point. */
36
+ export interface TraceRecord {
37
+ readonly id: string;
38
+ readonly traceId: string;
39
+ readonly rootId: string;
40
+ readonly parentId?: string | undefined;
41
+ readonly kind: 'activation' | 'signal' | 'span' | 'trail';
42
+ readonly name: string;
43
+ readonly trailId?: string | undefined;
44
+ readonly surface?: 'cli' | 'mcp' | 'http' | 'ws' | undefined;
45
+ readonly intent?: 'read' | 'write' | 'destroy' | undefined;
46
+ readonly startedAt: number;
47
+ readonly endedAt?: number | undefined;
48
+ readonly status: 'ok' | 'err' | 'cancelled';
49
+ readonly errorCategory?: string | undefined;
50
+ readonly sampled?: boolean | undefined;
51
+ readonly permit?:
52
+ | { readonly id: string; readonly tenantId?: string }
53
+ | undefined;
54
+ readonly attrs: Readonly<Record<string, unknown>>;
55
+ }
56
+
57
+ /**
58
+ * Minimal shape a tracing sink must satisfy.
59
+ *
60
+ * Kept intentionally tiny so adapters in `@ontrails/tracing`,
61
+ * `@ontrails/observe`, and user code can all satisfy it without
62
+ * additional dependencies.
63
+ */
64
+ export interface TraceSink {
65
+ readonly write: (record: TraceRecord) => void | Promise<void>;
66
+ }
67
+
68
+ /** Trace context carried through trail execution via `ctx.extensions`. */
69
+ export interface TraceContext {
70
+ readonly traceId: string;
71
+ readonly spanId: string;
72
+ readonly rootId: string;
73
+ readonly sampled: boolean;
74
+ }
75
+
76
+ /** Key used to store trace context in `ctx.extensions`. */
77
+ export const TRACE_CONTEXT_KEY = '__trace_context';
78
+
79
+ /** Read trace context from trail context extensions. */
80
+ export const getTraceContext = (ctx: {
81
+ readonly extensions?: Readonly<Record<string, unknown>> | undefined;
82
+ }): TraceContext | undefined =>
83
+ ctx.extensions?.[TRACE_CONTEXT_KEY] as TraceContext | undefined;
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Default no-op sink + sink registry
87
+ // ---------------------------------------------------------------------------
88
+
89
+ /** No-op sink installed by default so core never crashes without configuration. */
90
+ export const NOOP_SINK: TraceSink = {
91
+ // oxlint-disable-next-line no-empty-function -- intentional no-op
92
+ write: () => {},
93
+ };
94
+
95
+ // oxlint-disable-next-line eslint-plugin-jest/require-hook -- module-level sink registry, not test setup
96
+ let currentSink: TraceSink = NOOP_SINK;
97
+
98
+ /**
99
+ * Register a trace sink globally.
100
+ *
101
+ * All trails executed via `executeTrail` will write their completed trace
102
+ * records to this sink, as will every `ctx.trace(label, fn)` child span.
103
+ * Registering `undefined` or calling {@link clearTraceSink} resets back to
104
+ * the default no-op sink.
105
+ */
106
+ export const registerTraceSink = (sink: TraceSink | undefined): void => {
107
+ currentSink = sink ?? NOOP_SINK;
108
+ };
109
+
110
+ /** Retrieve the currently registered sink (never undefined). */
111
+ export const getTraceSink = (): TraceSink => currentSink;
112
+
113
+ /** True when tracing is effectively disabled and executeTrail should skip allocation. */
114
+ export const isTracingDisabled = (sink: TraceSink = currentSink): boolean =>
115
+ sink === NOOP_SINK;
116
+
117
+ /** Reset the sink registry back to the default no-op sink. */
118
+ export const clearTraceSink = (): void => {
119
+ currentSink = NOOP_SINK;
120
+ };
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Record + span helpers
124
+ // ---------------------------------------------------------------------------
125
+
126
+ /** Options for creating a trail-scoped {@link TraceRecord}. */
127
+ interface CreateTraceRecordOptions {
128
+ readonly trailId: string;
129
+ readonly traceId?: string | undefined;
130
+ readonly parentId?: string | undefined;
131
+ readonly rootId?: string | undefined;
132
+ readonly surface?: TraceRecord['surface'];
133
+ readonly intent?: TraceRecord['intent'];
134
+ readonly sampled?: boolean | undefined;
135
+ readonly permit?:
136
+ | { readonly id: string; readonly tenantId?: string }
137
+ | undefined;
138
+ }
139
+
140
+ interface CreateActivationTraceRecordOptions {
141
+ readonly attrs?: Readonly<Record<string, unknown>> | undefined;
142
+ readonly parentId?: string | undefined;
143
+ readonly rootId?: string | undefined;
144
+ readonly traceId?: string | undefined;
145
+ readonly sampled?: boolean | undefined;
146
+ }
147
+
148
+ /** Create a fresh trail-kind {@link TraceRecord}. */
149
+ export const createTraceRecord = (
150
+ options: CreateTraceRecordOptions
151
+ ): TraceRecord => {
152
+ const id = Bun.randomUUIDv7();
153
+ const traceId = options.traceId ?? Bun.randomUUIDv7();
154
+
155
+ return {
156
+ attrs: {},
157
+ endedAt: undefined,
158
+ id,
159
+ intent: options.intent,
160
+ kind: 'trail',
161
+ name: options.trailId,
162
+ parentId: options.parentId,
163
+ permit: options.permit,
164
+ rootId: options.rootId ?? id,
165
+ sampled: options.sampled,
166
+ startedAt: Date.now(),
167
+ status: 'ok',
168
+ surface: options.surface,
169
+ traceId,
170
+ trailId: options.trailId,
171
+ };
172
+ };
173
+
174
+ /** Create an activation-kind {@link TraceRecord}. */
175
+ export const createActivationTraceRecord = (
176
+ name: ActivationTraceRecordName,
177
+ options: CreateActivationTraceRecordOptions = {}
178
+ ): TraceRecord => {
179
+ const id = Bun.randomUUIDv7();
180
+ const traceId = options.traceId ?? Bun.randomUUIDv7();
181
+
182
+ return {
183
+ attrs: options.attrs ?? {},
184
+ endedAt: undefined,
185
+ errorCategory: undefined,
186
+ id,
187
+ intent: undefined,
188
+ kind: 'activation',
189
+ name,
190
+ parentId: options.parentId,
191
+ permit: undefined,
192
+ rootId: options.rootId ?? id,
193
+ sampled: options.sampled,
194
+ startedAt: Date.now(),
195
+ status: 'ok',
196
+ surface: undefined,
197
+ traceId,
198
+ trailId: undefined,
199
+ };
200
+ };
201
+
202
+ /** Build a span record from a parent trace context. */
203
+ export const createSpanRecord = (
204
+ parent: TraceContext,
205
+ label: string
206
+ ): TraceRecord => ({
207
+ attrs: {},
208
+ endedAt: undefined,
209
+ errorCategory: undefined,
210
+ id: Bun.randomUUIDv7(),
211
+ intent: undefined,
212
+ kind: 'span',
213
+ name: label,
214
+ parentId: parent.spanId,
215
+ rootId: parent.rootId,
216
+ sampled: parent.sampled,
217
+ startedAt: Date.now(),
218
+ status: 'ok',
219
+ surface: undefined,
220
+ traceId: parent.traceId,
221
+ trailId: undefined,
222
+ });
223
+
224
+ /** Build a signal lifecycle record from a parent trace context. */
225
+ export const createSignalTraceRecord = (
226
+ parent: TraceContext,
227
+ name: SignalTraceRecordName,
228
+ attrs: Readonly<Record<string, unknown>> = {}
229
+ ): TraceRecord => ({
230
+ attrs,
231
+ endedAt: undefined,
232
+ errorCategory: undefined,
233
+ id: Bun.randomUUIDv7(),
234
+ intent: undefined,
235
+ kind: 'signal',
236
+ name,
237
+ parentId: parent.spanId,
238
+ rootId: parent.rootId,
239
+ sampled: parent.sampled,
240
+ startedAt: Date.now(),
241
+ status: 'ok',
242
+ surface: undefined,
243
+ traceId: parent.traceId,
244
+ trailId: undefined,
245
+ });
246
+
247
+ /** Use a completed record as the current trace parent for subsequent trail execution. */
248
+ export const traceContextFromRecord = (record: TraceRecord): TraceContext => ({
249
+ rootId: record.rootId,
250
+ sampled: record.sampled ?? true,
251
+ spanId: record.id,
252
+ traceId: record.traceId,
253
+ });
254
+
255
+ /** Mark a record as completed with timing and status. */
256
+ export const completeRecord = (
257
+ record: TraceRecord,
258
+ status: TraceRecord['status'],
259
+ errorCategory?: string | undefined
260
+ ): TraceRecord => ({
261
+ ...record,
262
+ endedAt: Date.now(),
263
+ errorCategory,
264
+ status,
265
+ });
266
+
267
+ /**
268
+ * Best-effort sink write that never throws.
269
+ *
270
+ * Returns `true` when the sink accepted the record, `false` when the write
271
+ * threw. Most callers can ignore the return value -- it exists so that
272
+ * callers that hand the written record back as a parent trace context
273
+ * (e.g. {@link writeActivationTraceRecord}) can refuse to do so when the
274
+ * record never actually made it to storage.
275
+ */
276
+ export const writeToSink = async (
277
+ sink: TraceSink,
278
+ record: TraceRecord
279
+ ): Promise<boolean> => {
280
+ try {
281
+ await Promise.resolve(sink.write(record));
282
+ return true;
283
+ } catch {
284
+ // Sink failures must never affect trail result delivery.
285
+ return false;
286
+ }
287
+ };
288
+
289
+ /** Best-effort write for signal lifecycle records, no-op when tracing is disabled. */
290
+ export const writeSignalTraceRecord = async (
291
+ ctx: { readonly extensions?: Readonly<Record<string, unknown>> | undefined },
292
+ name: SignalTraceRecordName,
293
+ attrs: Readonly<Record<string, unknown>>,
294
+ status: TraceRecord['status'] = 'ok',
295
+ errorCategory?: string | undefined,
296
+ sink: TraceSink = getTraceSink()
297
+ ): Promise<void> => {
298
+ const parent = getTraceContext(ctx);
299
+ if (parent === undefined || isTracingDisabled(sink)) {
300
+ return;
301
+ }
302
+ await writeToSink(
303
+ sink,
304
+ completeRecord(
305
+ createSignalTraceRecord(parent, name, attrs),
306
+ status,
307
+ errorCategory
308
+ )
309
+ );
310
+ };
311
+
312
+ /** Best-effort write for activation boundary records, no-op when tracing is disabled. */
313
+ export const writeActivationTraceRecord = async (
314
+ name: ActivationTraceRecordName,
315
+ attrs: Readonly<Record<string, unknown>>,
316
+ status: TraceRecord['status'] = 'ok',
317
+ errorCategory?: string | undefined,
318
+ parent?: TraceContext | undefined,
319
+ sink: TraceSink = getTraceSink()
320
+ ): Promise<TraceRecord | undefined> => {
321
+ if (isTracingDisabled(sink)) {
322
+ return undefined;
323
+ }
324
+ const record = completeRecord(
325
+ createActivationTraceRecord(name, {
326
+ attrs,
327
+ parentId: parent?.spanId,
328
+ rootId: parent?.rootId,
329
+ // Parentless activations are the root span of their trace tree. Default
330
+ // sampled to true so the activation boundary stays consistent with
331
+ // child trail records, which default sampled=true via
332
+ // traceContextFromRecord. Inconsistent sampled flags within a trace
333
+ // break filters/exporters that gate on the activation boundary.
334
+ sampled: parent?.sampled ?? true,
335
+ traceId: parent?.traceId,
336
+ }),
337
+ status,
338
+ errorCategory
339
+ );
340
+ const written = await writeToSink(sink, record);
341
+ // When the sink dropped the record we must not hand it back to callers as
342
+ // a parent trace context -- subsequent child writes would reference an
343
+ // activation span that never reached storage, producing broken lineage.
344
+ return written ? record : undefined;
345
+ };
package/src/trail.ts CHANGED
@@ -1,14 +1,25 @@
1
1
  import type { z } from 'zod';
2
2
 
3
+ import type {
4
+ ActivationEntry,
5
+ ActivationEntrySpec,
6
+ ActivationSource,
7
+ ActivationSourceRef,
8
+ } from './activation-source.js';
9
+ import {
10
+ isActivationEntrySpec,
11
+ isActivationSource,
12
+ } from './activation-source.js';
3
13
  import type { AnyContour } from './contour.js';
4
14
  import type { FieldOverride } from './derive.js';
15
+ import type { Layer } from './layer.js';
5
16
  import type { Result } from './result.js';
6
17
  import type { AnyResource } from './resource.js';
7
18
  import type { AnySignal } from './signal.js';
8
19
  import {
9
20
  createLateBoundSignalMarker,
10
21
  getLateBoundSignalRef,
11
- } from './internal/signal-ref.js';
22
+ } from './signal-ref.js';
12
23
  import type { TrailsError } from './errors.js';
13
24
  import type {
14
25
  Detour,
@@ -21,6 +32,17 @@ import type {
21
32
  // Trail example
22
33
  // ---------------------------------------------------------------------------
23
34
 
35
+ export interface TrailExampleSignalAssertion {
36
+ /** Signal contract object or stable signal ID expected during the example. */
37
+ readonly signal: AnySignal | string;
38
+ /** Exact payload assertion for the fired signal. */
39
+ readonly payload?: unknown | undefined;
40
+ /** Partial payload assertion; declared fields must match, extras ignored. */
41
+ readonly payloadMatch?: unknown | undefined;
42
+ /** Number of matching fired signals expected. Defaults to one. */
43
+ readonly times?: number | undefined;
44
+ }
45
+
24
46
  /**
25
47
  * A named example for documentation and testing.
26
48
  *
@@ -41,6 +63,8 @@ export interface TrailExample<I, O> {
41
63
  readonly expectedMatch?: Partial<O> | undefined;
42
64
  /** Error class name for error-path examples */
43
65
  readonly error?: string | undefined;
66
+ /** Signal fires expected while executing this example. */
67
+ readonly signals?: readonly TrailExampleSignalAssertion[] | undefined;
44
68
  }
45
69
 
46
70
  // ---------------------------------------------------------------------------
@@ -79,12 +103,33 @@ export interface TrailSpec<I, O, CI = never> {
79
103
  readonly intent?: 'read' | 'write' | 'destroy' | undefined;
80
104
  /** Trail is idempotent (safe to retry) */
81
105
  readonly idempotent?: boolean | undefined;
82
- /** Whether trailheads expose this trail by default. */
106
+ /**
107
+ * Trail explicitly supports dry-run execution semantics.
108
+ *
109
+ * This is a declaration for governance, derivation, and surface tooling. It
110
+ * does not change runtime behavior by itself; the active invocation signal is
111
+ * `TrailContext.dryRun`.
112
+ */
113
+ readonly dryRun?: boolean | undefined;
114
+ /** Whether surfaces expose this trail by default. */
83
115
  readonly visibility?: TrailVisibility | undefined;
84
116
  /** Arbitrary meta for tooling and filtering */
85
117
  readonly meta?: Readonly<Record<string, unknown>> | undefined;
86
118
  /** Recovery paths activated when blaze fails with a matching error class. */
87
119
  readonly detours?: readonly Detour<I, O, TrailsError>[] | undefined;
120
+ /**
121
+ * Typed layers attached at trail scope.
122
+ *
123
+ * Layers declared here wrap this trail's implementation on every execution,
124
+ * regardless of which surface invokes it. The execution pipeline composes
125
+ * trail-scope layers innermost — closer to the blaze than surface-scope
126
+ * or topo-scope layers — so the final order is
127
+ * `topo → surface → trail → blaze` (outermost-first).
128
+ *
129
+ * Layers are typed and inspectable. Omit `input` for surface-invisible
130
+ * wrappers that do not project any fields.
131
+ */
132
+ readonly layers?: readonly Layer[] | undefined;
88
133
  /** Per-field overrides for deriveFields() (labels, hints, options) */
89
134
  readonly fields?: Readonly<Record<string, FieldOverride>> | undefined;
90
135
  /** Contours this trail operates on. */
@@ -93,7 +138,7 @@ export interface TrailSpec<I, O, CI = never> {
93
138
  readonly crosses?: readonly (string | AnyTrail)[] | undefined;
94
139
  /**
95
140
  * Composition-only input schema — merged with `input` for `ctx.cross()` calls,
96
- * invisible to public trailheads (CLI, MCP, HTTP).
141
+ * invisible to public surfaces (CLI, MCP, HTTP).
97
142
  *
98
143
  * Fields here are available in the blaze but are not derived into CLI flags,
99
144
  * MCP tool parameters, or HTTP request bodies. Use for data that only makes
@@ -103,7 +148,7 @@ export interface TrailSpec<I, O, CI = never> {
103
148
  /** Resources this trail may access via resource.from(ctx) */
104
149
  readonly resources?: readonly AnyResource[] | undefined;
105
150
  /**
106
- * Signals this trail emits via `ctx.fire()`.
151
+ * Signals this trail fires via `ctx.fire()`.
107
152
  *
108
153
  * Accepts either a string id or a `Signal` value. Both forms are
109
154
  * normalized to the signal's id at trail definition time, so
@@ -114,12 +159,14 @@ export interface TrailSpec<I, O, CI = never> {
114
159
  */
115
160
  readonly fires?: readonly (string | AnySignal)[] | undefined;
116
161
  /**
117
- * Signals that activate this trail (framework auto-subscribes).
162
+ * Activation sources that can invoke this trail.
118
163
  *
119
- * Accepts either a string id or a `Signal` value. Both forms are
120
- * normalized to the signal's id at trail definition time.
164
+ * Bare strings and `Signal` values are signal-source shorthand. Object form
165
+ * preserves the source kind and per-source metadata for the activation graph.
121
166
  */
122
- readonly on?: readonly (string | AnySignal)[] | undefined;
167
+ readonly on?:
168
+ | readonly (ActivationEntrySpec | ActivationSourceRef)[]
169
+ | undefined;
123
170
  /** Auth requirement: scopes object, 'public', or omitted (undeclared) */
124
171
  readonly permit?: PermitRequirement | undefined;
125
172
  /** Primary input fields and their order. CLI projects as positional args. */
@@ -130,10 +177,16 @@ export interface TrailSpec<I, O, CI = never> {
130
177
  // Trail (the frozen runtime object)
131
178
  // ---------------------------------------------------------------------------
132
179
 
133
- /** Intent describes what a trail does to the world */
134
- export type Intent = 'read' | 'write' | 'destroy';
180
+ /** Intent describes what a trail does to the world. */
181
+ export const intentValues = Object.freeze([
182
+ 'read',
183
+ 'write',
184
+ 'destroy',
185
+ ] as const);
135
186
 
136
- /** Whether trailheads expose a trail by default. */
187
+ export type Intent = (typeof intentValues)[number];
188
+
189
+ /** Whether surfaces expose a trail by default. */
137
190
  export type TrailVisibility = 'public' | 'internal';
138
191
 
139
192
  /** A fully-defined trail — the unit of work in the Trails system */
@@ -147,6 +200,7 @@ export interface Trail<I, O, CI = never> extends Omit<
147
200
  | 'detours'
148
201
  | 'fires'
149
202
  | 'intent'
203
+ | 'layers'
150
204
  | 'on'
151
205
  | 'resources'
152
206
  > {
@@ -161,15 +215,27 @@ export interface Trail<I, O, CI = never> extends Omit<
161
215
  readonly crossInput?: z.ZodType<CI> | undefined;
162
216
  /** Recovery paths activated when blaze fails with a matching error (always present, default []). */
163
217
  readonly detours: readonly Detour<I, O, TrailsError>[];
218
+ /**
219
+ * Typed layers attached at trail scope (always present, default []).
220
+ *
221
+ * Composed innermost in the layer chain — closest to the blaze. The final
222
+ * composition order is `topo → surface → trail → blaze` (outermost-first).
223
+ */
224
+ readonly layers: readonly Layer[];
164
225
  /** Resources this trail may access via resource.from(ctx) (always present, default []) */
165
226
  readonly resources: readonly AnyResource[];
166
- /** IDs of signals this trail emits via ctx.fire() (always present, default []) */
227
+ /** IDs of signals this trail fires via ctx.fire() (always present, default []) */
167
228
  readonly fires: readonly string[];
168
- /** IDs of signals that activate this trail (always present, default []) */
229
+ /**
230
+ * IDs of signal sources that activate this trail (always present, default []).
231
+ * Non-signal activation sources live in `activationSources`.
232
+ */
169
233
  readonly on: readonly string[];
234
+ /** Normalized activation source entries declared through `on` (always present, default []). */
235
+ readonly activationSources: readonly ActivationEntry[];
170
236
  /** What this trail does to the world (always present, default 'write') */
171
237
  readonly intent: Intent;
172
- /** Whether trailheads expose this trail by default (always present, default 'public'). */
238
+ /** Whether surfaces expose this trail by default (always present, default 'public'). */
173
239
  readonly visibility: TrailVisibility;
174
240
  /** Primary input fields and their order (always present, default undefined) */
175
241
  readonly args?: readonly string[] | false | undefined;
@@ -220,6 +286,78 @@ const normalizeSignalRef = (entry: string | AnySignal): string => {
220
286
  return createLateBoundSignalMarker(ref, entry.id);
221
287
  };
222
288
 
289
+ const freezeActivationSource = (source: ActivationSource): ActivationSource =>
290
+ Object.freeze({
291
+ ...source,
292
+ ...(source.meta === undefined
293
+ ? {}
294
+ : { meta: Object.freeze({ ...source.meta }) }),
295
+ });
296
+
297
+ const shouldPreserveSignalSource = (source: ActivationSource): boolean =>
298
+ source.kind === 'signal' &&
299
+ (!('payload' in source) ||
300
+ 'input' in source ||
301
+ 'parse' in source ||
302
+ 'cron' in source ||
303
+ 'timezone' in source);
304
+
305
+ const normalizeActivationSource = (
306
+ source: ActivationSourceRef
307
+ ): ActivationSource => {
308
+ if (typeof source === 'string') {
309
+ return freezeActivationSource({ id: source, kind: 'signal' });
310
+ }
311
+
312
+ if (isActivationSource(source) && shouldPreserveSignalSource(source)) {
313
+ return freezeActivationSource({
314
+ ...source,
315
+ id: normalizeSignalRef(source.id),
316
+ kind: 'signal',
317
+ });
318
+ }
319
+
320
+ if (isActivationSource(source) && source.kind !== 'signal') {
321
+ return freezeActivationSource(source);
322
+ }
323
+
324
+ return freezeActivationSource({
325
+ id: normalizeSignalRef(source as string | AnySignal),
326
+ kind: 'signal',
327
+ });
328
+ };
329
+
330
+ const normalizeActivationEntry = (
331
+ entry: ActivationEntrySpec | ActivationSourceRef
332
+ ): ActivationEntry => {
333
+ const source = isActivationEntrySpec(entry) ? entry.source : entry;
334
+ const normalized: ActivationEntry = {
335
+ source: normalizeActivationSource(source),
336
+ ...(isActivationEntrySpec(entry) && entry.meta !== undefined
337
+ ? { meta: Object.freeze({ ...entry.meta }) }
338
+ : {}),
339
+ ...(isActivationEntrySpec(entry) && entry.where !== undefined
340
+ ? { where: entry.where }
341
+ : {}),
342
+ };
343
+
344
+ return Object.freeze(normalized);
345
+ };
346
+
347
+ const normalizeActivationSources = (
348
+ entries: readonly (ActivationEntrySpec | ActivationSourceRef)[]
349
+ ): readonly ActivationEntry[] =>
350
+ Object.freeze(entries.map((entry) => normalizeActivationEntry(entry)));
351
+
352
+ const extractSignalActivationIds = (
353
+ activations: readonly ActivationEntry[]
354
+ ): readonly string[] =>
355
+ Object.freeze(
356
+ activations
357
+ .filter((entry) => entry.source.kind === 'signal')
358
+ .map((entry) => entry.source.id)
359
+ );
360
+
223
361
  /** Normalize a crosses entry — trail objects are reduced to their id. */
224
362
  const normalizeCrossRef = (entry: string | AnyTrail): string =>
225
363
  typeof entry === 'string' ? entry : entry.id;
@@ -229,19 +367,26 @@ const normalizeCollections = <I, O, CI>(
229
367
  spec: TrailSpec<I, O, CI>
230
368
  ): {
231
369
  readonly args: readonly string[] | false | undefined;
370
+ readonly activationSources: readonly ActivationEntry[];
232
371
  readonly contours: readonly AnyContour[];
233
372
  readonly detours: readonly Detour<I, O, TrailsError>[];
234
373
  readonly fires: readonly string[];
374
+ readonly layers: readonly Layer[];
235
375
  readonly on: readonly string[];
236
376
  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
- });
377
+ } => {
378
+ const activationSources = normalizeActivationSources(spec.on ?? []);
379
+ return {
380
+ activationSources,
381
+ args: Array.isArray(spec.args) ? Object.freeze([...spec.args]) : spec.args,
382
+ contours: Object.freeze([...(spec.contours ?? [])]),
383
+ detours: Object.freeze([...(spec.detours ?? [])]),
384
+ fires: Object.freeze((spec.fires ?? []).map(normalizeSignalRef)),
385
+ layers: Object.freeze([...(spec.layers ?? [])]),
386
+ on: extractSignalActivationIds(activationSources),
387
+ resources: Object.freeze([...(spec.resources ?? [])]),
388
+ };
389
+ };
245
390
 
246
391
  /**
247
392
  * Create a trail definition.
@@ -296,6 +441,7 @@ export function trail<I, O, CI = never>(
296
441
  contours: _c,
297
442
  detours: _d,
298
443
  fires: _f,
444
+ layers: _l,
299
445
  on: _o,
300
446
  resources: _r,
301
447
  ...spec
@@ -7,7 +7,7 @@ import {
7
7
  isTrailsError,
8
8
  NotFoundError,
9
9
  } from '../errors.js';
10
- import { stripDefaultsFromShape } from '../internal/zod-wrappers.js';
10
+ import { stripDefaultsFromShape } from '../zod-wrappers.js';
11
11
  import type { AnyResource } from '../resource.js';
12
12
  import { Result } from '../result.js';
13
13
  import type { StoreAccessorProtocol } from '../store/accessor-protocol.js';