@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
@@ -0,0 +1,633 @@
1
+ /**
2
+ * Runtime diagnostics for typed signal emission.
3
+ *
4
+ * Diagnostics are side-channel records: they make signal runtime problems
5
+ * observable without changing the best-effort producer API.
6
+ */
7
+
8
+ import { createHash } from 'node:crypto';
9
+
10
+ import type { z } from 'zod';
11
+
12
+ import type { ActivationProvenance } from './activation-provenance.js';
13
+ import type { Logger } from './types.js';
14
+
15
+ export const SIGNAL_DIAGNOSTICS_SINK_KEY =
16
+ '__trails_signal_diagnostics_sink' as const;
17
+
18
+ export const SIGNAL_DIAGNOSTICS_STRICT_MODE_KEY =
19
+ '__trails_signal_diagnostics_strict_mode' as const;
20
+
21
+ export const signalDiagnosticDefinitions = {
22
+ 'signal.fire.suppressed': {
23
+ category: 'activation',
24
+ description:
25
+ 'A signal fire was intentionally suppressed by a runtime guard.',
26
+ level: 'warning',
27
+ },
28
+ 'signal.handler.failed': {
29
+ category: 'handler',
30
+ description: 'A signal consumer returned a failed Result.',
31
+ level: 'error',
32
+ },
33
+ 'signal.handler.predicate_failed': {
34
+ category: 'activation',
35
+ description: 'A signal consumer activation predicate threw or rejected.',
36
+ level: 'error',
37
+ },
38
+ 'signal.handler.rejected': {
39
+ category: 'handler',
40
+ description: 'A signal consumer rejected outside Result normalization.',
41
+ level: 'error',
42
+ },
43
+ 'signal.invalid': {
44
+ category: 'validation',
45
+ description:
46
+ 'A signal payload failed schema validation at the fire boundary.',
47
+ level: 'error',
48
+ },
49
+ 'signal.unknown': {
50
+ category: 'topology',
51
+ description: 'A producer fired a signal that is not present in the topo.',
52
+ level: 'error',
53
+ },
54
+ } as const;
55
+
56
+ export type SignalDiagnosticCode = keyof typeof signalDiagnosticDefinitions;
57
+
58
+ export type SignalDiagnosticLevel =
59
+ (typeof signalDiagnosticDefinitions)[SignalDiagnosticCode]['level'];
60
+
61
+ export type SignalDiagnosticCategory =
62
+ (typeof signalDiagnosticDefinitions)[SignalDiagnosticCode]['category'];
63
+
64
+ export type SignalDiagnosticOrigin =
65
+ | 'fan-out-guard'
66
+ | 'fire-boundary'
67
+ | 'handler'
68
+ | 'predicate';
69
+
70
+ export type SignalPayloadShape =
71
+ | 'array'
72
+ | 'bigint'
73
+ | 'boolean'
74
+ | 'function'
75
+ | 'null'
76
+ | 'number'
77
+ | 'object'
78
+ | 'string'
79
+ | 'symbol'
80
+ | 'undefined';
81
+
82
+ export interface SignalPayloadSummary {
83
+ readonly byteLength: number;
84
+ readonly digest: string;
85
+ readonly redacted: true;
86
+ readonly shape: SignalPayloadShape;
87
+ readonly topLevelEntryCount?: number | undefined;
88
+ }
89
+
90
+ export type SignalDiagnosticPathSegment = number | string;
91
+
92
+ export interface SignalDiagnosticSchemaIssue {
93
+ readonly code?: string | undefined;
94
+ readonly message: string;
95
+ readonly path: readonly SignalDiagnosticPathSegment[];
96
+ }
97
+
98
+ export interface SignalDiagnosticCause {
99
+ readonly message: string;
100
+ readonly name?: string | undefined;
101
+ }
102
+
103
+ export interface SignalDiagnosticSourceLocation {
104
+ readonly column?: number | undefined;
105
+ readonly filePath?: string | undefined;
106
+ readonly line?: number | undefined;
107
+ }
108
+
109
+ interface SignalDiagnosticBase<
110
+ TCode extends SignalDiagnosticCode,
111
+ TOrigin extends SignalDiagnosticOrigin,
112
+ > {
113
+ readonly activation?: ActivationProvenance | undefined;
114
+ readonly category: (typeof signalDiagnosticDefinitions)[TCode]['category'];
115
+ readonly code: TCode;
116
+ readonly level: (typeof signalDiagnosticDefinitions)[TCode]['level'];
117
+ readonly message: string;
118
+ readonly origin: TOrigin;
119
+ readonly producerTrailId?: string | undefined;
120
+ readonly runId?: string | undefined;
121
+ readonly signalId: string;
122
+ readonly sourceLocation?: SignalDiagnosticSourceLocation | undefined;
123
+ readonly traceId?: string | undefined;
124
+ }
125
+
126
+ export interface SignalInvalidDiagnostic extends SignalDiagnosticBase<
127
+ 'signal.invalid',
128
+ 'fire-boundary'
129
+ > {
130
+ readonly payload: SignalPayloadSummary;
131
+ readonly schemaIssues: readonly SignalDiagnosticSchemaIssue[];
132
+ }
133
+
134
+ export type SignalUnknownDiagnostic = SignalDiagnosticBase<
135
+ 'signal.unknown',
136
+ 'fire-boundary'
137
+ >;
138
+
139
+ export interface SignalHandlerFailedDiagnostic extends SignalDiagnosticBase<
140
+ 'signal.handler.failed',
141
+ 'handler'
142
+ > {
143
+ readonly cause: SignalDiagnosticCause;
144
+ readonly handlerTrailId: string;
145
+ readonly payload?: SignalPayloadSummary | undefined;
146
+ }
147
+
148
+ export interface SignalHandlerRejectedDiagnostic extends SignalDiagnosticBase<
149
+ 'signal.handler.rejected',
150
+ 'handler'
151
+ > {
152
+ readonly cause: SignalDiagnosticCause;
153
+ readonly handlerTrailId: string;
154
+ readonly payload?: SignalPayloadSummary | undefined;
155
+ }
156
+
157
+ export interface SignalPredicateFailedDiagnostic extends SignalDiagnosticBase<
158
+ 'signal.handler.predicate_failed',
159
+ 'predicate'
160
+ > {
161
+ readonly cause: SignalDiagnosticCause;
162
+ readonly handlerTrailId: string;
163
+ readonly payload?: SignalPayloadSummary | undefined;
164
+ }
165
+
166
+ export interface SignalFireSuppressedDiagnostic extends SignalDiagnosticBase<
167
+ 'signal.fire.suppressed',
168
+ 'fan-out-guard'
169
+ > {
170
+ readonly fireStack?: readonly string[] | undefined;
171
+ readonly limit?: number | undefined;
172
+ readonly reason: 'cycle' | 'depth';
173
+ }
174
+
175
+ export type SignalDiagnostic =
176
+ | SignalFireSuppressedDiagnostic
177
+ | SignalHandlerFailedDiagnostic
178
+ | SignalHandlerRejectedDiagnostic
179
+ | SignalPredicateFailedDiagnostic
180
+ | SignalInvalidDiagnostic
181
+ | SignalUnknownDiagnostic;
182
+
183
+ export type SignalDiagnosticSink = (
184
+ diagnostic: SignalDiagnostic
185
+ ) => Promise<void> | void;
186
+
187
+ export type SignalDiagnosticStrictMode =
188
+ | 'all'
189
+ | 'error'
190
+ | 'off'
191
+ | boolean
192
+ | readonly SignalDiagnosticCode[]
193
+ | ((diagnostic: SignalDiagnostic) => boolean);
194
+
195
+ export interface SignalDiagnosticContext {
196
+ readonly extensions?: Readonly<Record<string, unknown>> | undefined;
197
+ readonly logger?: Pick<Logger, 'warn'> | undefined;
198
+ }
199
+
200
+ export interface SignalDiagnosticRecordResult {
201
+ readonly delivered: boolean;
202
+ readonly diagnostic: SignalDiagnostic;
203
+ readonly promoted: boolean;
204
+ readonly sinkError?: SignalDiagnosticCause | undefined;
205
+ }
206
+
207
+ export interface SignalDiagnosticCommonInput {
208
+ readonly activation?: ActivationProvenance | undefined;
209
+ readonly message?: string | undefined;
210
+ readonly producerTrailId?: string | undefined;
211
+ readonly runId?: string | undefined;
212
+ readonly signalId: string;
213
+ readonly sourceLocation?: SignalDiagnosticSourceLocation | undefined;
214
+ readonly traceId?: string | undefined;
215
+ }
216
+
217
+ export interface CreateSignalInvalidDiagnosticInput extends SignalDiagnosticCommonInput {
218
+ readonly payload: unknown;
219
+ readonly schemaIssues: readonly z.core.$ZodIssue[];
220
+ }
221
+
222
+ export interface CreateSignalHandlerDiagnosticInput extends SignalDiagnosticCommonInput {
223
+ readonly cause: unknown;
224
+ readonly handlerTrailId: string;
225
+ readonly payload?: unknown;
226
+ }
227
+
228
+ export interface CreateSignalFireSuppressedDiagnosticInput extends SignalDiagnosticCommonInput {
229
+ readonly fireStack?: readonly string[] | undefined;
230
+ readonly limit?: number | undefined;
231
+ readonly reason: 'cycle' | 'depth';
232
+ }
233
+
234
+ const payloadShape = (payload: unknown): SignalPayloadShape => {
235
+ if (payload === null) {
236
+ return 'null';
237
+ }
238
+
239
+ const type = typeof payload;
240
+ if (type !== 'object') {
241
+ return type;
242
+ }
243
+
244
+ try {
245
+ if (Array.isArray(payload)) {
246
+ return 'array';
247
+ }
248
+ } catch {
249
+ return 'object';
250
+ }
251
+
252
+ return 'object';
253
+ };
254
+
255
+ const topLevelEntryCount = (payload: unknown): number | undefined => {
256
+ try {
257
+ if (Array.isArray(payload)) {
258
+ return payload.length;
259
+ }
260
+ if (payload !== null && typeof payload === 'object') {
261
+ return Object.keys(payload).length;
262
+ }
263
+ } catch {
264
+ return undefined;
265
+ }
266
+ return undefined;
267
+ };
268
+
269
+ const UNREADABLE_PAYLOAD_VALUE = '[Unreadable]';
270
+
271
+ const canonicalLeaf = (value: unknown): unknown => {
272
+ switch (typeof value) {
273
+ case 'bigint': {
274
+ return `[BigInt:${value.toString()}]`;
275
+ }
276
+ case 'function': {
277
+ return '[Function]';
278
+ }
279
+ case 'symbol': {
280
+ return `[Symbol:${value.description ?? ''}]`;
281
+ }
282
+ case 'undefined': {
283
+ return '[Undefined]';
284
+ }
285
+ default: {
286
+ return value;
287
+ }
288
+ }
289
+ };
290
+
291
+ const canonicalize = (value: unknown, seen: WeakSet<object>): unknown => {
292
+ if (Array.isArray(value)) {
293
+ if (seen.has(value)) {
294
+ return '[Circular]';
295
+ }
296
+ seen.add(value);
297
+ const length = topLevelEntryCount(value) ?? 0;
298
+ const entries: unknown[] = [];
299
+ for (let index = 0; index < length; index += 1) {
300
+ try {
301
+ entries.push(canonicalize(value[index], seen));
302
+ } catch {
303
+ entries.push(UNREADABLE_PAYLOAD_VALUE);
304
+ }
305
+ }
306
+ return entries;
307
+ }
308
+ if (value instanceof Date) {
309
+ return Number.isNaN(value.getTime())
310
+ ? '[Invalid Date]'
311
+ : value.toISOString();
312
+ }
313
+ if (value instanceof RegExp) {
314
+ return value.toString();
315
+ }
316
+ if (value !== null && typeof value === 'object') {
317
+ if (seen.has(value)) {
318
+ return '[Circular]';
319
+ }
320
+ seen.add(value);
321
+ const sorted: Record<string, unknown> = {};
322
+ let keys: string[];
323
+ try {
324
+ keys = Object.keys(value).toSorted();
325
+ } catch {
326
+ return UNREADABLE_PAYLOAD_VALUE;
327
+ }
328
+ for (const key of keys) {
329
+ try {
330
+ const next = (value as Record<string, unknown>)[key];
331
+ sorted[key] = canonicalize(next, seen);
332
+ } catch {
333
+ sorted[key] = UNREADABLE_PAYLOAD_VALUE;
334
+ }
335
+ }
336
+ return sorted;
337
+ }
338
+ return canonicalLeaf(value);
339
+ };
340
+
341
+ const stableJson = (value: unknown): string => {
342
+ try {
343
+ return (
344
+ JSON.stringify(canonicalize(value, new WeakSet<object>())) ??
345
+ '"[Undefined]"'
346
+ );
347
+ } catch {
348
+ return `"${UNREADABLE_PAYLOAD_VALUE}"`;
349
+ }
350
+ };
351
+
352
+ const hashText = (text: string): string =>
353
+ createHash('sha256').update(text).digest('hex');
354
+
355
+ export const summarizeSignalPayload = (
356
+ payload: unknown
357
+ ): SignalPayloadSummary => {
358
+ const canonical = stableJson(payload);
359
+ return {
360
+ byteLength: new TextEncoder().encode(canonical).byteLength,
361
+ digest: hashText(canonical),
362
+ redacted: true,
363
+ shape: payloadShape(payload),
364
+ topLevelEntryCount: topLevelEntryCount(payload),
365
+ };
366
+ };
367
+
368
+ export const schemaIssuesFromZod = (
369
+ issues: readonly z.core.$ZodIssue[]
370
+ ): readonly SignalDiagnosticSchemaIssue[] =>
371
+ issues.map((issue) => ({
372
+ code: typeof issue.code === 'string' ? issue.code : undefined,
373
+ message: issue.message,
374
+ path: issue.path.map((segment) =>
375
+ typeof segment === 'number' ? segment : String(segment)
376
+ ),
377
+ }));
378
+
379
+ export const signalDiagnosticCauseFromUnknown = (
380
+ cause: unknown
381
+ ): SignalDiagnosticCause => {
382
+ if (cause instanceof Error) {
383
+ return {
384
+ message: cause.message,
385
+ name: cause.name,
386
+ };
387
+ }
388
+ return {
389
+ message: String(cause),
390
+ };
391
+ };
392
+
393
+ export const shouldPromoteSignalDiagnostic = (
394
+ diagnostic: SignalDiagnostic,
395
+ strictMode?: SignalDiagnosticStrictMode
396
+ ): boolean => {
397
+ if (
398
+ strictMode === undefined ||
399
+ strictMode === false ||
400
+ strictMode === 'off'
401
+ ) {
402
+ return false;
403
+ }
404
+ if (strictMode === true || strictMode === 'all') {
405
+ return true;
406
+ }
407
+ if (strictMode === 'error') {
408
+ return diagnostic.level === 'error';
409
+ }
410
+ if (Array.isArray(strictMode)) {
411
+ return strictMode.includes(diagnostic.code);
412
+ }
413
+ if (typeof strictMode !== 'function') {
414
+ return false;
415
+ }
416
+ try {
417
+ return strictMode(diagnostic);
418
+ } catch {
419
+ return false;
420
+ }
421
+ };
422
+
423
+ const signalDiagnosticSinkFrom = (
424
+ ctx: SignalDiagnosticContext | undefined
425
+ ): SignalDiagnosticSink | undefined => {
426
+ const sink = ctx?.extensions?.[SIGNAL_DIAGNOSTICS_SINK_KEY];
427
+ return typeof sink === 'function'
428
+ ? (sink as SignalDiagnosticSink)
429
+ : undefined;
430
+ };
431
+
432
+ const signalDiagnosticStrictModeFrom = (
433
+ ctx: SignalDiagnosticContext | undefined
434
+ ): SignalDiagnosticStrictMode | undefined => {
435
+ const strictMode = ctx?.extensions?.[SIGNAL_DIAGNOSTICS_STRICT_MODE_KEY];
436
+ if (
437
+ strictMode === true ||
438
+ strictMode === false ||
439
+ strictMode === 'all' ||
440
+ strictMode === 'error' ||
441
+ strictMode === 'off' ||
442
+ Array.isArray(strictMode) ||
443
+ typeof strictMode === 'function'
444
+ ) {
445
+ return strictMode as SignalDiagnosticStrictMode;
446
+ }
447
+ return undefined;
448
+ };
449
+
450
+ export const recordSignalDiagnostic = async (
451
+ ctx: SignalDiagnosticContext | undefined,
452
+ diagnostic: SignalDiagnostic
453
+ ): Promise<SignalDiagnosticRecordResult> => {
454
+ const promoted = shouldPromoteSignalDiagnostic(
455
+ diagnostic,
456
+ signalDiagnosticStrictModeFrom(ctx)
457
+ );
458
+ const sink = signalDiagnosticSinkFrom(ctx);
459
+ if (sink === undefined) {
460
+ return {
461
+ delivered: false,
462
+ diagnostic,
463
+ promoted,
464
+ };
465
+ }
466
+ try {
467
+ await sink(diagnostic);
468
+ return {
469
+ delivered: true,
470
+ diagnostic,
471
+ promoted,
472
+ };
473
+ } catch (error) {
474
+ const sinkError = signalDiagnosticCauseFromUnknown(error);
475
+ ctx?.logger?.warn('Signal diagnostic sink failed', {
476
+ code: diagnostic.code,
477
+ error: sinkError.message,
478
+ signalId: diagnostic.signalId,
479
+ });
480
+ return {
481
+ delivered: false,
482
+ diagnostic,
483
+ promoted,
484
+ sinkError,
485
+ };
486
+ }
487
+ };
488
+
489
+ export const createSignalInvalidDiagnostic = (
490
+ input: CreateSignalInvalidDiagnosticInput
491
+ ): SignalInvalidDiagnostic => {
492
+ const definition = signalDiagnosticDefinitions['signal.invalid'];
493
+ return {
494
+ activation: input.activation,
495
+ category: definition.category,
496
+ code: 'signal.invalid',
497
+ level: definition.level,
498
+ message: input.message ?? `Invalid payload for signal "${input.signalId}"`,
499
+ origin: 'fire-boundary',
500
+ payload: summarizeSignalPayload(input.payload),
501
+ producerTrailId: input.producerTrailId,
502
+ runId: input.runId,
503
+ schemaIssues: schemaIssuesFromZod(input.schemaIssues),
504
+ signalId: input.signalId,
505
+ sourceLocation: input.sourceLocation,
506
+ traceId: input.traceId,
507
+ };
508
+ };
509
+
510
+ export const createSignalUnknownDiagnostic = (
511
+ input: SignalDiagnosticCommonInput
512
+ ): SignalUnknownDiagnostic => {
513
+ const definition = signalDiagnosticDefinitions['signal.unknown'];
514
+ return {
515
+ activation: input.activation,
516
+ category: definition.category,
517
+ code: 'signal.unknown',
518
+ level: definition.level,
519
+ message: input.message ?? `Unknown signal "${input.signalId}"`,
520
+ origin: 'fire-boundary',
521
+ producerTrailId: input.producerTrailId,
522
+ runId: input.runId,
523
+ signalId: input.signalId,
524
+ sourceLocation: input.sourceLocation,
525
+ traceId: input.traceId,
526
+ };
527
+ };
528
+
529
+ export const createSignalHandlerFailedDiagnostic = (
530
+ input: CreateSignalHandlerDiagnosticInput
531
+ ): SignalHandlerFailedDiagnostic => {
532
+ const definition = signalDiagnosticDefinitions['signal.handler.failed'];
533
+ return {
534
+ activation: input.activation,
535
+ category: definition.category,
536
+ cause: signalDiagnosticCauseFromUnknown(input.cause),
537
+ code: 'signal.handler.failed',
538
+ handlerTrailId: input.handlerTrailId,
539
+ level: definition.level,
540
+ message:
541
+ input.message ??
542
+ `Signal handler "${input.handlerTrailId}" failed for "${input.signalId}"`,
543
+ origin: 'handler',
544
+ payload:
545
+ input.payload === undefined
546
+ ? undefined
547
+ : summarizeSignalPayload(input.payload),
548
+ producerTrailId: input.producerTrailId,
549
+ runId: input.runId,
550
+ signalId: input.signalId,
551
+ sourceLocation: input.sourceLocation,
552
+ traceId: input.traceId,
553
+ };
554
+ };
555
+
556
+ export const createSignalHandlerRejectedDiagnostic = (
557
+ input: CreateSignalHandlerDiagnosticInput
558
+ ): SignalHandlerRejectedDiagnostic => {
559
+ const definition = signalDiagnosticDefinitions['signal.handler.rejected'];
560
+ return {
561
+ activation: input.activation,
562
+ category: definition.category,
563
+ cause: signalDiagnosticCauseFromUnknown(input.cause),
564
+ code: 'signal.handler.rejected',
565
+ handlerTrailId: input.handlerTrailId,
566
+ level: definition.level,
567
+ message:
568
+ input.message ??
569
+ `Signal handler "${input.handlerTrailId}" rejected for "${input.signalId}"`,
570
+ origin: 'handler',
571
+ payload:
572
+ input.payload === undefined
573
+ ? undefined
574
+ : summarizeSignalPayload(input.payload),
575
+ producerTrailId: input.producerTrailId,
576
+ runId: input.runId,
577
+ signalId: input.signalId,
578
+ sourceLocation: input.sourceLocation,
579
+ traceId: input.traceId,
580
+ };
581
+ };
582
+
583
+ export const createSignalPredicateFailedDiagnostic = (
584
+ input: CreateSignalHandlerDiagnosticInput
585
+ ): SignalPredicateFailedDiagnostic => {
586
+ const definition =
587
+ signalDiagnosticDefinitions['signal.handler.predicate_failed'];
588
+ return {
589
+ activation: input.activation,
590
+ category: definition.category,
591
+ cause: signalDiagnosticCauseFromUnknown(input.cause),
592
+ code: 'signal.handler.predicate_failed',
593
+ handlerTrailId: input.handlerTrailId,
594
+ level: definition.level,
595
+ message:
596
+ input.message ??
597
+ `Signal handler predicate for "${input.handlerTrailId}" failed for "${input.signalId}"`,
598
+ origin: 'predicate',
599
+ payload:
600
+ input.payload === undefined
601
+ ? undefined
602
+ : summarizeSignalPayload(input.payload),
603
+ producerTrailId: input.producerTrailId,
604
+ runId: input.runId,
605
+ signalId: input.signalId,
606
+ sourceLocation: input.sourceLocation,
607
+ traceId: input.traceId,
608
+ };
609
+ };
610
+
611
+ export const createSignalFireSuppressedDiagnostic = (
612
+ input: CreateSignalFireSuppressedDiagnosticInput
613
+ ): SignalFireSuppressedDiagnostic => {
614
+ const definition = signalDiagnosticDefinitions['signal.fire.suppressed'];
615
+ return {
616
+ activation: input.activation,
617
+ category: definition.category,
618
+ code: 'signal.fire.suppressed',
619
+ fireStack: input.fireStack,
620
+ level: definition.level,
621
+ limit: input.limit,
622
+ message:
623
+ input.message ??
624
+ `Signal "${input.signalId}" fire was suppressed by ${input.reason} guard`,
625
+ origin: 'fan-out-guard',
626
+ producerTrailId: input.producerTrailId,
627
+ reason: input.reason,
628
+ runId: input.runId,
629
+ signalId: input.signalId,
630
+ sourceLocation: input.sourceLocation,
631
+ traceId: input.traceId,
632
+ };
633
+ };
@@ -1,4 +1,4 @@
1
- import type { AnySignal, Signal } from '../signal.js';
1
+ import type { AnySignal, Signal } from './signal.js';
2
2
 
3
3
  export interface LateBoundSignalRef {
4
4
  readonly kind: 'store-derived';
package/src/signal.ts CHANGED
@@ -4,6 +4,29 @@
4
4
 
5
5
  import type { z } from 'zod';
6
6
 
7
+ const formatExampleIssues = (issues: readonly z.core.$ZodIssue[]): string =>
8
+ issues
9
+ .map((issue) => {
10
+ const path = issue.path.length > 0 ? issue.path.join('.') : '<root>';
11
+ return `${path}: ${issue.message}`;
12
+ })
13
+ .join('; ');
14
+
15
+ const assertSignalExamples = <T>(
16
+ id: string,
17
+ payload: z.ZodType<T>,
18
+ examples: readonly T[]
19
+ ): void => {
20
+ for (const [index, example] of examples.entries()) {
21
+ const parsed = payload.safeParse(example);
22
+ if (!parsed.success) {
23
+ throw new TypeError(
24
+ `signal("${id}") example ${index} is invalid: ${formatExampleIssues(parsed.error.issues)}`
25
+ );
26
+ }
27
+ }
28
+ };
29
+
7
30
  // ---------------------------------------------------------------------------
8
31
  // Spec (input to the factory)
9
32
  // ---------------------------------------------------------------------------
@@ -11,6 +34,8 @@ import type { z } from 'zod';
11
34
  export interface SignalSpec<T> {
12
35
  readonly payload: z.ZodType<T>;
13
36
  readonly description?: string | undefined;
37
+ /** Example payloads validated against the signal payload schema. */
38
+ readonly examples?: readonly T[] | undefined;
14
39
  readonly meta?: Readonly<Record<string, unknown>> | undefined;
15
40
  /** Trail IDs that produce this signal (e.g. the trails it originates from). */
16
41
  readonly from?: readonly string[] | undefined;
@@ -25,6 +50,8 @@ export interface Signal<T> {
25
50
  readonly kind: 'signal';
26
51
  readonly payload: z.ZodType<T>;
27
52
  readonly description?: string | undefined;
53
+ /** Example payloads validated against the signal payload schema. */
54
+ readonly examples?: readonly T[] | undefined;
28
55
  readonly meta?: Readonly<Record<string, unknown>> | undefined;
29
56
  /** Trail IDs that produce this signal (e.g. the trails it originates from). */
30
57
  readonly from?: readonly string[] | undefined;
@@ -51,8 +78,18 @@ export function signal<T>(
51
78
  const resolvedId = typeof idOrSpec === 'string' ? idOrSpec : idOrSpec.id;
52
79
  // oxlint-disable-next-line no-non-null-assertion -- overload guarantees maybeSpec when idOrSpec is string
53
80
  const resolvedSpec = typeof idOrSpec === 'string' ? maybeSpec! : idOrSpec;
81
+ if (resolvedSpec.examples !== undefined) {
82
+ assertSignalExamples(
83
+ resolvedId,
84
+ resolvedSpec.payload,
85
+ resolvedSpec.examples
86
+ );
87
+ }
54
88
  return Object.freeze({
55
89
  description: resolvedSpec.description,
90
+ examples: resolvedSpec.examples
91
+ ? Object.freeze([...resolvedSpec.examples])
92
+ : undefined,
56
93
  from: resolvedSpec.from ? Object.freeze([...resolvedSpec.from]) : undefined,
57
94
  id: resolvedId,
58
95
  kind: 'signal' as const,