@ontrails/core 1.0.0-beta.2 → 1.0.0-beta.21

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 (245) hide show
  1. package/CHANGELOG.md +283 -5
  2. package/README.md +113 -33
  3. package/package.json +11 -1
  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/branded.ts +1 -1
  10. package/src/compose-batch.ts +69 -0
  11. package/src/compose-schema.ts +36 -0
  12. package/src/context.ts +44 -10
  13. package/src/contour.ts +346 -0
  14. package/src/derive.ts +59 -18
  15. package/src/detours.ts +8 -0
  16. package/src/draft.ts +342 -0
  17. package/src/error-projection.ts +51 -0
  18. package/src/errors.ts +309 -48
  19. package/src/execute.ts +1577 -0
  20. package/src/fire.ts +1169 -0
  21. package/src/index.ts +477 -32
  22. package/src/internal/fork-ctx.ts +69 -0
  23. package/src/layer-projection.ts +193 -0
  24. package/src/layer.ts +43 -6
  25. package/src/observe.ts +361 -0
  26. package/src/path-security.ts +1 -1
  27. package/src/permits.ts +12 -0
  28. package/src/redaction/patterns.ts +6 -3
  29. package/src/resilience.ts +11 -11
  30. package/src/resource-config.ts +792 -0
  31. package/src/resource.ts +194 -0
  32. package/src/result.ts +36 -4
  33. package/src/run.ts +76 -0
  34. package/src/schedule-runtime.ts +689 -0
  35. package/src/schedule.ts +326 -0
  36. package/src/serialization.ts +119 -39
  37. package/src/signal-diagnostics.ts +633 -0
  38. package/src/signal-ref.ts +87 -0
  39. package/src/signal.ts +104 -0
  40. package/src/store/accessor-protocol.ts +56 -0
  41. package/src/store/index.ts +4 -0
  42. package/src/structured-examples.ts +248 -0
  43. package/src/surface-derivation.ts +91 -0
  44. package/src/surface-filter.ts +176 -0
  45. package/src/surface-versioning.ts +42 -0
  46. package/src/topo.ts +769 -57
  47. package/src/tracing.ts +345 -0
  48. package/src/trail.ts +1161 -29
  49. package/src/trails/derive-trail.ts +842 -0
  50. package/src/trails/index.ts +9 -0
  51. package/src/trails/ingest.ts +139 -0
  52. package/src/trails-db.ts +208 -0
  53. package/src/transport-error-map.ts +174 -0
  54. package/src/type-utils.ts +87 -0
  55. package/src/types.ts +254 -12
  56. package/src/validate-established-topo.ts +69 -0
  57. package/src/validate-topo.ts +442 -35
  58. package/src/validation.ts +152 -4
  59. package/src/version-marker.ts +716 -0
  60. package/src/version-resolution.ts +308 -0
  61. package/src/version-runtime.ts +120 -0
  62. package/src/webhook.ts +278 -0
  63. package/src/workspace.ts +1 -1
  64. package/src/zod-wrappers.ts +72 -0
  65. package/.turbo/turbo-build.log +0 -1
  66. package/.turbo/turbo-lint.log +0 -3
  67. package/.turbo/turbo-typecheck.log +0 -1
  68. package/dist/adapters.d.ts +0 -39
  69. package/dist/adapters.d.ts.map +0 -1
  70. package/dist/adapters.js +0 -2
  71. package/dist/adapters.js.map +0 -1
  72. package/dist/blob-ref.d.ts +0 -20
  73. package/dist/blob-ref.d.ts.map +0 -1
  74. package/dist/blob-ref.js +0 -22
  75. package/dist/blob-ref.js.map +0 -1
  76. package/dist/branded.d.ts +0 -36
  77. package/dist/branded.d.ts.map +0 -1
  78. package/dist/branded.js +0 -89
  79. package/dist/branded.js.map +0 -1
  80. package/dist/collections.d.ts +0 -31
  81. package/dist/collections.d.ts.map +0 -1
  82. package/dist/collections.js +0 -60
  83. package/dist/collections.js.map +0 -1
  84. package/dist/context.d.ts +0 -10
  85. package/dist/context.d.ts.map +0 -1
  86. package/dist/context.js +0 -15
  87. package/dist/context.js.map +0 -1
  88. package/dist/derive.d.ts +0 -33
  89. package/dist/derive.d.ts.map +0 -1
  90. package/dist/derive.js +0 -122
  91. package/dist/derive.js.map +0 -1
  92. package/dist/errors.d.ts +0 -83
  93. package/dist/errors.d.ts.map +0 -1
  94. package/dist/errors.js +0 -142
  95. package/dist/errors.js.map +0 -1
  96. package/dist/event.d.ts +0 -45
  97. package/dist/event.d.ts.map +0 -1
  98. package/dist/event.js +0 -17
  99. package/dist/event.js.map +0 -1
  100. package/dist/fetch.d.ts +0 -15
  101. package/dist/fetch.d.ts.map +0 -1
  102. package/dist/fetch.js +0 -102
  103. package/dist/fetch.js.map +0 -1
  104. package/dist/guards.d.ts +0 -17
  105. package/dist/guards.d.ts.map +0 -1
  106. package/dist/guards.js +0 -25
  107. package/dist/guards.js.map +0 -1
  108. package/dist/health.d.ts +0 -18
  109. package/dist/health.d.ts.map +0 -1
  110. package/dist/health.js +0 -5
  111. package/dist/health.js.map +0 -1
  112. package/dist/hike.d.ts +0 -36
  113. package/dist/hike.d.ts.map +0 -1
  114. package/dist/hike.js +0 -20
  115. package/dist/hike.js.map +0 -1
  116. package/dist/index.d.ts +0 -34
  117. package/dist/index.d.ts.map +0 -1
  118. package/dist/index.js +0 -38
  119. package/dist/index.js.map +0 -1
  120. package/dist/job.d.ts +0 -24
  121. package/dist/job.d.ts.map +0 -1
  122. package/dist/job.js +0 -17
  123. package/dist/job.js.map +0 -1
  124. package/dist/layer.d.ts +0 -17
  125. package/dist/layer.d.ts.map +0 -1
  126. package/dist/layer.js +0 -21
  127. package/dist/layer.js.map +0 -1
  128. package/dist/path-security.d.ts +0 -28
  129. package/dist/path-security.d.ts.map +0 -1
  130. package/dist/path-security.js +0 -63
  131. package/dist/path-security.js.map +0 -1
  132. package/dist/patterns/bulk.d.ts +0 -15
  133. package/dist/patterns/bulk.d.ts.map +0 -1
  134. package/dist/patterns/bulk.js +0 -14
  135. package/dist/patterns/bulk.js.map +0 -1
  136. package/dist/patterns/change.d.ts +0 -10
  137. package/dist/patterns/change.d.ts.map +0 -1
  138. package/dist/patterns/change.js +0 -10
  139. package/dist/patterns/change.js.map +0 -1
  140. package/dist/patterns/date-range.d.ts +0 -10
  141. package/dist/patterns/date-range.d.ts.map +0 -1
  142. package/dist/patterns/date-range.js +0 -10
  143. package/dist/patterns/date-range.js.map +0 -1
  144. package/dist/patterns/index.d.ts +0 -9
  145. package/dist/patterns/index.d.ts.map +0 -1
  146. package/dist/patterns/index.js +0 -9
  147. package/dist/patterns/index.js.map +0 -1
  148. package/dist/patterns/pagination.d.ts +0 -18
  149. package/dist/patterns/pagination.d.ts.map +0 -1
  150. package/dist/patterns/pagination.js +0 -18
  151. package/dist/patterns/pagination.js.map +0 -1
  152. package/dist/patterns/progress.d.ts +0 -11
  153. package/dist/patterns/progress.d.ts.map +0 -1
  154. package/dist/patterns/progress.js +0 -11
  155. package/dist/patterns/progress.js.map +0 -1
  156. package/dist/patterns/sorting.d.ts +0 -13
  157. package/dist/patterns/sorting.d.ts.map +0 -1
  158. package/dist/patterns/sorting.js +0 -10
  159. package/dist/patterns/sorting.js.map +0 -1
  160. package/dist/patterns/status.d.ts +0 -15
  161. package/dist/patterns/status.d.ts.map +0 -1
  162. package/dist/patterns/status.js +0 -9
  163. package/dist/patterns/status.js.map +0 -1
  164. package/dist/patterns/timestamps.d.ts +0 -10
  165. package/dist/patterns/timestamps.d.ts.map +0 -1
  166. package/dist/patterns/timestamps.js +0 -10
  167. package/dist/patterns/timestamps.js.map +0 -1
  168. package/dist/redaction/index.d.ts +0 -4
  169. package/dist/redaction/index.d.ts.map +0 -1
  170. package/dist/redaction/index.js +0 -3
  171. package/dist/redaction/index.js.map +0 -1
  172. package/dist/redaction/patterns.d.ts +0 -9
  173. package/dist/redaction/patterns.d.ts.map +0 -1
  174. package/dist/redaction/patterns.js +0 -39
  175. package/dist/redaction/patterns.js.map +0 -1
  176. package/dist/redaction/redactor.d.ts +0 -27
  177. package/dist/redaction/redactor.d.ts.map +0 -1
  178. package/dist/redaction/redactor.js +0 -89
  179. package/dist/redaction/redactor.js.map +0 -1
  180. package/dist/resilience.d.ts +0 -34
  181. package/dist/resilience.d.ts.map +0 -1
  182. package/dist/resilience.js +0 -164
  183. package/dist/resilience.js.map +0 -1
  184. package/dist/result.d.ts +0 -57
  185. package/dist/result.d.ts.map +0 -1
  186. package/dist/result.js +0 -145
  187. package/dist/result.js.map +0 -1
  188. package/dist/serialization.d.ts +0 -27
  189. package/dist/serialization.d.ts.map +0 -1
  190. package/dist/serialization.js +0 -115
  191. package/dist/serialization.js.map +0 -1
  192. package/dist/topo.d.ts +0 -18
  193. package/dist/topo.d.ts.map +0 -1
  194. package/dist/topo.js +0 -74
  195. package/dist/topo.js.map +0 -1
  196. package/dist/trail.d.ts +0 -83
  197. package/dist/trail.d.ts.map +0 -1
  198. package/dist/trail.js +0 -16
  199. package/dist/trail.js.map +0 -1
  200. package/dist/types.d.ts +0 -46
  201. package/dist/types.d.ts.map +0 -1
  202. package/dist/types.js +0 -2
  203. package/dist/types.js.map +0 -1
  204. package/dist/validate-topo.d.ts +0 -24
  205. package/dist/validate-topo.d.ts.map +0 -1
  206. package/dist/validate-topo.js +0 -108
  207. package/dist/validate-topo.js.map +0 -1
  208. package/dist/validation.d.ts +0 -27
  209. package/dist/validation.d.ts.map +0 -1
  210. package/dist/validation.js +0 -134
  211. package/dist/validation.js.map +0 -1
  212. package/dist/workspace.d.ts +0 -25
  213. package/dist/workspace.d.ts.map +0 -1
  214. package/dist/workspace.js +0 -57
  215. package/dist/workspace.js.map +0 -1
  216. package/src/__tests__/blob-ref.test.ts +0 -103
  217. package/src/__tests__/branded.test.ts +0 -148
  218. package/src/__tests__/collections.test.ts +0 -126
  219. package/src/__tests__/context.test.ts +0 -66
  220. package/src/__tests__/derive.test.ts +0 -159
  221. package/src/__tests__/errors.test.ts +0 -309
  222. package/src/__tests__/event.test.ts +0 -82
  223. package/src/__tests__/fetch.test.ts +0 -217
  224. package/src/__tests__/guards.test.ts +0 -102
  225. package/src/__tests__/hike.test.ts +0 -117
  226. package/src/__tests__/job.test.ts +0 -98
  227. package/src/__tests__/layer.test.ts +0 -224
  228. package/src/__tests__/path-security.test.ts +0 -114
  229. package/src/__tests__/patterns.test.ts +0 -273
  230. package/src/__tests__/redaction.test.ts +0 -244
  231. package/src/__tests__/resilience.test.ts +0 -246
  232. package/src/__tests__/result.test.ts +0 -155
  233. package/src/__tests__/serialization.test.ts +0 -236
  234. package/src/__tests__/topo.test.ts +0 -184
  235. package/src/__tests__/trail.test.ts +0 -179
  236. package/src/__tests__/validate-topo.test.ts +0 -201
  237. package/src/__tests__/validation.test.ts +0 -283
  238. package/src/__tests__/workspace.test.ts +0 -183
  239. package/src/adapters.ts +0 -68
  240. package/src/event.ts +0 -77
  241. package/src/health.ts +0 -23
  242. package/src/hike.ts +0 -77
  243. package/src/job.ts +0 -20
  244. package/tsconfig.json +0 -9
  245. 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
+ };
@@ -0,0 +1,87 @@
1
+ import type { AnySignal, Signal } from './signal.js';
2
+
3
+ export interface LateBoundSignalRef {
4
+ readonly kind: 'store-derived';
5
+ readonly token: string;
6
+ }
7
+
8
+ export interface LateBoundSignalMarker {
9
+ readonly displayId: string;
10
+ readonly token: string;
11
+ }
12
+
13
+ const LATE_BOUND_SIGNAL_REF = Symbol('trails.late-bound-signal-ref');
14
+ const LATE_BOUND_SIGNAL_MARKER_PREFIX = '@@trails:late-bound-signal-ref:';
15
+
16
+ const defineLateBoundSignalRef = <T extends object>(
17
+ value: T,
18
+ ref: LateBoundSignalRef
19
+ ): T => {
20
+ Object.defineProperty(value, LATE_BOUND_SIGNAL_REF, {
21
+ configurable: false,
22
+ enumerable: false,
23
+ value: ref,
24
+ writable: false,
25
+ });
26
+ return value;
27
+ };
28
+
29
+ export const getLateBoundSignalRef = (
30
+ signal: Pick<AnySignal, 'id'> | undefined
31
+ ): LateBoundSignalRef | undefined =>
32
+ signal === undefined
33
+ ? undefined
34
+ : ((signal as Record<PropertyKey, unknown>)[LATE_BOUND_SIGNAL_REF] as
35
+ | LateBoundSignalRef
36
+ | undefined);
37
+
38
+ export const attachLateBoundSignalRef = <T>(
39
+ signal: Signal<T>,
40
+ ref: LateBoundSignalRef
41
+ ): Signal<T> =>
42
+ Object.freeze(
43
+ defineLateBoundSignalRef(
44
+ {
45
+ ...signal,
46
+ },
47
+ ref
48
+ )
49
+ ) as Signal<T>;
50
+
51
+ export const cloneSignalWithId = <T>(
52
+ signal: Signal<T>,
53
+ id: string
54
+ ): Signal<T> => {
55
+ const clone = {
56
+ ...signal,
57
+ id,
58
+ };
59
+ const ref = getLateBoundSignalRef(signal);
60
+ return Object.freeze(
61
+ ref ? defineLateBoundSignalRef(clone, ref) : clone
62
+ ) as Signal<T>;
63
+ };
64
+
65
+ export const createLateBoundSignalMarker = (
66
+ ref: LateBoundSignalRef,
67
+ displayId: string
68
+ ): string => `${LATE_BOUND_SIGNAL_MARKER_PREFIX}${ref.token}:${displayId}`;
69
+
70
+ export const parseLateBoundSignalMarker = (
71
+ value: string
72
+ ): LateBoundSignalMarker | null => {
73
+ if (!value.startsWith(LATE_BOUND_SIGNAL_MARKER_PREFIX)) {
74
+ return null;
75
+ }
76
+
77
+ const remainder = value.slice(LATE_BOUND_SIGNAL_MARKER_PREFIX.length);
78
+ const separator = remainder.indexOf(':');
79
+ if (separator <= 0 || separator === remainder.length - 1) {
80
+ return null;
81
+ }
82
+
83
+ return {
84
+ displayId: remainder.slice(separator + 1),
85
+ token: remainder.slice(0, separator),
86
+ };
87
+ };