@systemfsoftware/trace-spec 0.0.0

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.
package/dist/index.mjs ADDED
@@ -0,0 +1,824 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { Cause, Context, Effect, Equal, FileSystem, Layer, Match, Option, Predicate, Result, Schema } from "effect";
3
+ import { dual } from "effect/Function";
4
+ import { Prototype } from "effect/Pipeable";
5
+ import "effect/PlatformError";
6
+ import "@systemfsoftware/trace-taxonomy";
7
+ import * as OtelTracer from "@effect/opentelemetry/OtelTracer";
8
+ import * as OtelResource from "@effect/opentelemetry/Resource";
9
+ import { SpanStatusCode } from "@opentelemetry/api";
10
+ import { AlwaysOnSampler, BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
11
+ import { Suite, TaskRef } from "@systemfsoftware/effect-spec-runtime";
12
+ //#region src/ContractDecodeError.schema.ts
13
+ var ContractDecodeError = class extends Schema.TaggedError()("ContractDecodeError", {
14
+ declarationId: Schema.String,
15
+ spanName: Schema.String,
16
+ spanId: Schema.String,
17
+ attribute: Schema.String,
18
+ detail: Schema.String
19
+ }) {};
20
+ //#endregion
21
+ //#region src/EmptyObservationError.schema.ts
22
+ var EmptyObservationError = class extends Schema.TaggedError()("EmptyObservationError", {
23
+ traceId: Schema.String,
24
+ detail: Schema.String
25
+ }) {};
26
+ //#endregion
27
+ //#region src/Verdict.schema.ts
28
+ const VerdictTypeId = Symbol.for("@systemfsoftware/trace-spec/Verdict");
29
+ var Hold = class extends Schema.TaggedClass()("Hold", {
30
+ conjunct: Schema.String,
31
+ inspected: Schema.Array(Schema.String)
32
+ }) {
33
+ [VerdictTypeId] = VerdictTypeId;
34
+ };
35
+ var Break = class extends Schema.TaggedClass()("Break", {
36
+ conjunct: Schema.String,
37
+ inspected: Schema.Array(Schema.String),
38
+ detail: Schema.String
39
+ }) {
40
+ [VerdictTypeId] = VerdictTypeId;
41
+ };
42
+ const Verdict = Schema.Union([Hold, Break]);
43
+ //#endregion
44
+ //#region src/FailureDump.ts
45
+ const DEFAULT_DIRECTORY = "artifacts/traces";
46
+ const isBreak$2 = Schema.is(Break);
47
+ /**
48
+ * The rendered evidence a break carries; a held verdict leaves nothing to write.
49
+ */
50
+ const report = (verdict) => Option.map(Option.liftPredicate(verdict, isBreak$2), renderBreach);
51
+ const renderValue = (value) => JSON.stringify(value);
52
+ const renderEntries = (entries) => [...entries].map(([key, value]) => `${key}=${renderValue(value)}`).join(" ");
53
+ const indentOf = (depth) => " ".repeat(depth);
54
+ const renderBreach = (breach) => `break ${breach.conjunct} inspected=[${breach.inspected.join(", ")}]\n${indentOf(1)}${breach.detail}`;
55
+ const childrenOf = (spans, spanId) => spans.filter((span) => span.parentSpanId === spanId);
56
+ const renderNode = (spans, node, depth) => {
57
+ return [`${indentOf(depth)}${node.name} (${node.spanId}) status=${node.status} error.type=${String(node.errorType)} duration=${node.durationMillis}ms`, ...[
58
+ `${indentOf(depth)} attrs: ${renderEntries(Object.entries(node.attributes))}`,
59
+ ...node.events.map((event) => `${indentOf(depth)} event ${event.name} ${renderEntries(Object.entries(event.attributes))}`),
60
+ ...node.links.map((link) => `${indentOf(depth)} link ${link.traceId}/${link.spanId}`),
61
+ ...childrenOf(spans, node.spanId).map((child) => renderNode(spans, child, depth + 1))
62
+ ]].join("\n");
63
+ };
64
+ /**
65
+ * The dump document: the trace it belongs to, the break that refused it, and the recorded
66
+ * spans as the observation window saw them.
67
+ */
68
+ const document = (observed, report) => [
69
+ `trace ${observed.traceId}`,
70
+ report,
71
+ ...observed.spans.map((node) => renderNode(observed.spans, node, 0))
72
+ ].join("\n");
73
+ const sanitize = (value) => value.replace(/[^a-zA-Z0-9._-]+/g, "-");
74
+ const fileNameOf = (request) => `${sanitize(request.name ?? request.traceId)}.${sanitize(request.conjunct)}.txt`;
75
+ /**
76
+ * Writes the dump under the artifacts directory and answers with its path. The file is
77
+ * named after the request's name when one is given — a generated case names its dump after
78
+ * itself, so every failing draw overwrites one file and the last write is the reported
79
+ * counterexample.
80
+ */
81
+ const write = (request) => Effect.gen(function* () {
82
+ const fs = yield* FileSystem.FileSystem;
83
+ yield* fs.makeDirectory(DEFAULT_DIRECTORY, { recursive: true });
84
+ const path = `${DEFAULT_DIRECTORY}/${fileNameOf(request)}`;
85
+ yield* fs.writeFileString(path, document(request, request.report));
86
+ return path;
87
+ });
88
+ //#endregion
89
+ //#region src/TraceGraph.schema.ts
90
+ const Status = Schema.Literals([
91
+ "ok",
92
+ "unset",
93
+ "error"
94
+ ]);
95
+ const AttributeValue = Schema.Union([
96
+ Schema.String,
97
+ Schema.Finite,
98
+ Schema.Boolean,
99
+ Schema.Array(Schema.Union([
100
+ Schema.String,
101
+ Schema.Finite,
102
+ Schema.Boolean
103
+ ]))
104
+ ]);
105
+ const Attributes = Schema.Record(Schema.String, AttributeValue);
106
+ const SpanEvent = Schema.Struct({
107
+ name: Schema.String,
108
+ attributes: Attributes
109
+ });
110
+ const SpanLink = Schema.Struct({
111
+ traceId: Schema.String,
112
+ spanId: Schema.String
113
+ });
114
+ const SpanRecord = Schema.Struct({
115
+ traceId: Schema.String,
116
+ spanId: Schema.String,
117
+ parentSpanId: Schema.NullOr(Schema.String),
118
+ name: Schema.String,
119
+ status: Status,
120
+ errorType: Schema.NullOr(Schema.String),
121
+ startMillis: Schema.Finite,
122
+ durationMillis: Schema.Finite,
123
+ attributes: Attributes,
124
+ events: Schema.Array(SpanEvent),
125
+ links: Schema.Array(SpanLink)
126
+ });
127
+ const GraphNode = Schema.Struct({
128
+ spanId: Schema.String,
129
+ parentSpanId: Schema.NullOr(Schema.String),
130
+ name: Schema.String,
131
+ status: Status,
132
+ errorType: Schema.NullOr(Schema.String),
133
+ startMillis: Schema.Finite,
134
+ durationMillis: Schema.Finite,
135
+ attrs: Attributes,
136
+ events: Schema.Array(SpanEvent),
137
+ links: Schema.Array(SpanLink)
138
+ });
139
+ const TraceGraph = Schema.Struct({
140
+ traceId: Schema.String,
141
+ nodes: Schema.Array(GraphNode)
142
+ });
143
+ //#endregion
144
+ //#region src/Graph.ts
145
+ var Graph_exports = /* @__PURE__ */ __exportAll({
146
+ AttributeValue: () => AttributeValue,
147
+ Attributes: () => Attributes,
148
+ GraphNode: () => GraphNode,
149
+ SpanEvent: () => SpanEvent,
150
+ SpanLink: () => SpanLink,
151
+ SpanRecord: () => SpanRecord,
152
+ Status: () => Status,
153
+ TraceGraph: () => TraceGraph,
154
+ byId: () => byId,
155
+ children: () => children,
156
+ decode: () => decode,
157
+ descendants: () => descendants
158
+ });
159
+ const byId = (graph, spec) => graph.nodes.filter((node) => node.name === spec.name);
160
+ const children = (graph, node) => graph.nodes.filter((candidate) => candidate.parentSpanId === node.spanId);
161
+ const descendants = (graph, node) => {
162
+ const direct = children(graph, node);
163
+ return [...direct, ...direct.flatMap((child) => descendants(graph, child))];
164
+ };
165
+ const indexDeclarations = (taxonomy) => new Map(taxonomy.spans.map((span) => [span.name, span]));
166
+ const keyOfPath = (path) => {
167
+ const last = path.at(-1);
168
+ return last === void 0 ? null : String(last);
169
+ };
170
+ const pointerAttributeName = (pointer) => keyOfPath(pointer.path) ?? attributeNameOf(pointer.issue);
171
+ const filterAttributeName = (filter) => attributeNameOf(filter.issue);
172
+ const encodingAttributeName = (encoding) => attributeNameOf(encoding.issue);
173
+ const compositeAttributeName = (composite) => firstAttributeName(composite.issues);
174
+ const anyOfAttributeName = (anyOf) => firstAttributeName(anyOf.issues);
175
+ const noAttributeName = () => null;
176
+ const attributeNameOf = (issue) => Match.value(issue).pipe(Match.tag("Pointer", pointerAttributeName), Match.tag("Filter", filterAttributeName), Match.tag("Encoding", encodingAttributeName), Match.tag("Composite", compositeAttributeName), Match.tag("AnyOf", anyOfAttributeName), Match.orElse(noAttributeName));
177
+ const firstAttributeName = (issues) => issues.map(attributeNameOf).find((name) => name !== null) ?? null;
178
+ const decodeFailure = (spec, record, detail, attribute) => ContractDecodeError.make({
179
+ declarationId: spec.id,
180
+ spanName: record.name,
181
+ spanId: record.spanId,
182
+ attribute: attribute ?? "*",
183
+ detail
184
+ });
185
+ const decodeAttrs = (spec, record) => Result.mapError(Schema.decodeResult(Schema.toType(spec.attrs))(record.attributes), (error) => decodeFailure(spec, record, error.message, attributeNameOf(error.issue)));
186
+ const nodeOf = (record, attrs) => ({
187
+ spanId: record.spanId,
188
+ parentSpanId: record.parentSpanId,
189
+ name: record.name,
190
+ status: record.status,
191
+ errorType: record.errorType,
192
+ startMillis: record.startMillis,
193
+ durationMillis: record.durationMillis,
194
+ attrs,
195
+ events: record.events,
196
+ links: record.links
197
+ });
198
+ const decodedNodeOf = (record, spec) => Result.map(decodeAttrs(spec, record), (attrs) => nodeOf(record, attrs));
199
+ const declaredNodeOf = (record, declarations) => {
200
+ const spec = declarations.get(record.name);
201
+ return spec === void 0 ? Result.succeed(nodeOf(record, record.attributes)) : decodedNodeOf(record, spec);
202
+ };
203
+ const graphOf = (traceId, nodes) => ({
204
+ traceId,
205
+ nodes
206
+ });
207
+ /**
208
+ * Decodes the recorded spans of one trace against a taxonomy, purely: a declared span
209
+ * whose required attributes do not decode refuses with {@link ContractDecodeError}
210
+ * naming the declaration and the attribute, and a span no declaration accounts for enters
211
+ * the graph as recorded. Failure is a `Result`, so the phase that reads the trace cannot
212
+ * fail the pipeline by accident.
213
+ */
214
+ const decode = (traceId, spans, taxonomy) => {
215
+ const declarations = indexDeclarations(taxonomy);
216
+ return Result.map(Result.all(spans.map((record) => declaredNodeOf(record, declarations))), (nodes) => graphOf(traceId, nodes));
217
+ };
218
+ //#endregion
219
+ //#region src/Observation.service.ts
220
+ var Observation_service_exports = /* @__PURE__ */ __exportAll({
221
+ EmptyObservationError: () => EmptyObservationError,
222
+ Observation: () => Observation
223
+ });
224
+ var Observation = class extends Context.Service()("@systemfsoftware/trace-spec/Observation") {};
225
+ //#endregion
226
+ //#region src/TraceDisparityError.schema.ts
227
+ var TraceDisparityError = class extends Schema.TaggedError()("TraceDisparityError", {
228
+ relationId: Schema.String,
229
+ traceId: Schema.String,
230
+ breaks: Schema.Array(Break),
231
+ dumpPath: Schema.NullOr(Schema.String)
232
+ }) {};
233
+ //#endregion
234
+ //#region src/Contract.ts
235
+ var Contract_exports = /* @__PURE__ */ __exportAll({
236
+ ContractDecodeError: () => ContractDecodeError,
237
+ EmptyObservationError: () => EmptyObservationError,
238
+ TraceDisparityError: () => TraceDisparityError,
239
+ TypeId: () => TypeId$3,
240
+ check: () => check,
241
+ holds: () => holds,
242
+ judge: () => judge,
243
+ of: () => of,
244
+ stimulate: () => stimulate
245
+ });
246
+ const TypeId$3 = Symbol.for("@systemfsoftware/trace-spec/Contract");
247
+ const stimulatedOf = (taxonomy, stimulus) => ({
248
+ [TypeId$3]: TypeId$3,
249
+ taxonomy,
250
+ stimulus,
251
+ holds: (relation) => contractOf(taxonomy, stimulus, relation),
252
+ ...Prototype
253
+ });
254
+ const contractOf = (taxonomy, stimulus, relation) => ({
255
+ [TypeId$3]: TypeId$3,
256
+ taxonomy,
257
+ stimulus,
258
+ relation,
259
+ ...Prototype
260
+ });
261
+ const of = (taxonomy) => ({
262
+ [TypeId$3]: TypeId$3,
263
+ taxonomy,
264
+ stimulate: (stimulus) => stimulatedOf(taxonomy, stimulus),
265
+ ...Prototype
266
+ });
267
+ const stimulateDual = (self, stimulus) => stimulatedOf(self.taxonomy, stimulus);
268
+ const stimulate = dual(2, stimulateDual);
269
+ const holdsDual = (self, relation) => contractOf(self.taxonomy, self.stimulus, relation);
270
+ const holds = dual(2, holdsDual);
271
+ const isContract = (value) => Predicate.hasProperty(value, TypeId$3);
272
+ const dumpOf = (observed, verdict, options) => Option.match(report(verdict), {
273
+ onNone: () => Effect.succeed(null),
274
+ onSome: (report) => Effect.map(Effect.option(write({
275
+ ...observed,
276
+ conjunct: verdict.conjunct,
277
+ report,
278
+ name: options?.dumpName
279
+ })), Option.getOrNull)
280
+ });
281
+ const judgeDual = (self, input, options) => Effect.gen(function* () {
282
+ const observation = yield* Observation;
283
+ const run = yield* self.stimulus(input);
284
+ const spans = yield* observation.collect(run.traceId);
285
+ const graph = yield* Effect.fromResult(decode(run.traceId, spans, self.taxonomy));
286
+ const verdict = self.relation(graph);
287
+ return {
288
+ run,
289
+ verdict,
290
+ dumpPath: yield* dumpOf({
291
+ traceId: run.traceId,
292
+ spans
293
+ }, verdict, options)
294
+ };
295
+ });
296
+ const judge = dual((args) => isContract(args[0]), judgeDual);
297
+ const refuseBreak = (relationId, judgment) => Match.value(judgment.verdict).pipe(Match.tag("Break", (breach) => Effect.fail(TraceDisparityError.make({
298
+ relationId,
299
+ traceId: judgment.run.traceId,
300
+ breaks: [breach],
301
+ dumpPath: judgment.dumpPath
302
+ }))), Match.tag("Hold", () => Effect.succeed(judgment)), Match.exhaustive);
303
+ const checkDual = (self, input, options) => Effect.flatMap(judgeDual(self, input, options), (judgment) => refuseBreak(self.relation.id, judgment));
304
+ const check = dual((args) => isContract(args[0]), checkDual);
305
+ //#endregion
306
+ //#region src/observation-window.handle.ts
307
+ const TypeId$2 = Symbol.for("~systemfsoftware/trace-spec/ObservationWindow");
308
+ const ExporterId = Symbol.for("~systemfsoftware/trace-spec/ObservationWindow/exporter");
309
+ const ProviderId = Symbol.for("~systemfsoftware/trace-spec/ObservationWindow/provider");
310
+ const isObservationWindow = (u) => Predicate.hasProperty(u, TypeId$2);
311
+ const make$3 = (spec) => {
312
+ const exporter = new InMemorySpanExporter();
313
+ const provider = new BasicTracerProvider({
314
+ sampler: new AlwaysOnSampler(),
315
+ spanProcessors: [new SimpleSpanProcessor(exporter)]
316
+ });
317
+ return {
318
+ [TypeId$2]: TypeId$2,
319
+ [ExporterId]: exporter,
320
+ [ProviderId]: provider,
321
+ serviceName: spec.serviceName,
322
+ ...Prototype
323
+ };
324
+ };
325
+ const shutdown = (self) => Effect.promise(() => self[ProviderId].shutdown());
326
+ const SCALAR_TYPES = {
327
+ string: true,
328
+ number: true,
329
+ boolean: true
330
+ };
331
+ const isScalar = (value) => SCALAR_TYPES[typeof value] === true;
332
+ const scalarArrayOrNull = (value) => Array.isArray(value) ? value.filter(isScalar) : null;
333
+ const definedOrEmpty = (value) => value ?? [];
334
+ const attributeValue = (value) => isScalar(value) ? value : scalarArrayOrNull(definedOrEmpty(value));
335
+ const isPresent = (entry) => entry[1] !== null;
336
+ const attributeRecordOf = (attributes) => Object.fromEntries(Object.entries(attributes).map((entry) => [entry[0], attributeValue(entry[1])]).filter(isPresent));
337
+ const STATUS_BY_CODE = {
338
+ [SpanStatusCode.OK]: "ok",
339
+ [SpanStatusCode.ERROR]: "error",
340
+ [SpanStatusCode.UNSET]: "unset"
341
+ };
342
+ const statusOf = (code) => STATUS_BY_CODE[code] ?? "unset";
343
+ const errorTypeOf = (attributes) => {
344
+ const value = attributes["error.type"];
345
+ return typeof value === "string" ? value : null;
346
+ };
347
+ const parentOf = (span) => {
348
+ const parent = span.parentSpanContext;
349
+ return parent === void 0 ? null : parent.spanId;
350
+ };
351
+ const eventsOf = (span) => span.events.map((event) => ({
352
+ name: event.name,
353
+ attributes: attributeRecordOf(event.attributes ?? {})
354
+ }));
355
+ const linksOf = (span) => span.links.map((link) => ({
356
+ traceId: link.context.traceId,
357
+ spanId: link.context.spanId
358
+ }));
359
+ const millisOf = (duration) => duration[0] * 1e3 + duration[1] / 1e6;
360
+ const spanRecordOf = (span) => {
361
+ const attributes = attributeRecordOf(span.attributes);
362
+ return {
363
+ traceId: span.spanContext().traceId,
364
+ spanId: span.spanContext().spanId,
365
+ parentSpanId: parentOf(span),
366
+ name: span.name,
367
+ status: statusOf(span.status.code),
368
+ errorType: errorTypeOf(attributes),
369
+ startMillis: millisOf(span.startTime),
370
+ durationMillis: millisOf(span.duration),
371
+ attributes,
372
+ events: eventsOf(span),
373
+ links: linksOf(span)
374
+ };
375
+ };
376
+ const emptyObservation = (traceId) => new EmptyObservationError({
377
+ traceId,
378
+ detail: "the observation window closed with no span carrying the stimulated trace id"
379
+ });
380
+ const recordsOf = (self, traceId) => self[ExporterId].getFinishedSpans().filter((span) => span.spanContext().traceId === traceId).map(spanRecordOf);
381
+ const collect = dual(2, (self, traceId) => Effect.suspend(() => {
382
+ const records = recordsOf(self, traceId);
383
+ return records.length > 0 ? Effect.succeed(records) : Effect.fail(emptyObservation(traceId));
384
+ }));
385
+ const context = (self) => Context.make(Observation, { collect: (traceId) => collect(self, traceId) }).pipe(Context.add(OtelTracer.OtelTracerProvider, self[ProviderId]));
386
+ //#endregion
387
+ //#region src/ObservationWindowSpec.schema.ts
388
+ var ObservationWindowSpec = class extends Schema.Class("ObservationWindowSpec")({ serviceName: Schema.NonEmptyString }) {};
389
+ //#endregion
390
+ //#region src/observation-window.resource.ts
391
+ var observation_window_resource_exports = /* @__PURE__ */ __exportAll({
392
+ ObservationWindowSpec: () => ObservationWindowSpec,
393
+ ObservationWindowTypeId: () => TypeId$2,
394
+ collect: () => collect,
395
+ isObservationWindow: () => isObservationWindow,
396
+ make: () => make$2
397
+ });
398
+ const TypeId$1 = Symbol.for("~systemfsoftware/trace-spec/ObservationWindowResource");
399
+ const scoped = (spec) => Effect.acquireRelease(Effect.sync(() => make$3(spec)), shutdown);
400
+ const layer = (spec) => OtelTracer.layer.pipe(Layer.provideMerge(OtelResource.layer({ serviceName: spec.serviceName })), Layer.provideMerge(Layer.effectContext(Effect.map(scoped(spec), context))));
401
+ const makeProto = (spec) => ({
402
+ [TypeId$1]: TypeId$1,
403
+ spec,
404
+ get scoped() {
405
+ return scoped(spec);
406
+ },
407
+ get layer() {
408
+ return layer(spec);
409
+ },
410
+ ...Prototype
411
+ });
412
+ const make$2 = (serviceName) => makeProto(new ObservationWindowSpec({ serviceName }));
413
+ //#endregion
414
+ //#region src/Rel.ts
415
+ var Rel_exports = /* @__PURE__ */ __exportAll({
416
+ Break: () => Break,
417
+ Hold: () => Hold,
418
+ Verdict: () => Verdict,
419
+ absent: () => absent,
420
+ all: () => all,
421
+ any: () => any,
422
+ attrs: () => attrs,
423
+ child: () => child,
424
+ descendant: () => descendant,
425
+ durationLessThan: () => durationLessThan,
426
+ errorType: () => errorType,
427
+ event: () => event,
428
+ exists: () => exists,
429
+ forall: () => forall,
430
+ fromTaxonomy: () => fromTaxonomy,
431
+ not: () => not,
432
+ order: () => order,
433
+ soft: () => soft,
434
+ status: () => status,
435
+ unique: () => unique
436
+ });
437
+ const relation = (parts) => Object.assign((graph) => parts.evaluate(graph), {
438
+ id: parts.id,
439
+ soft: parts.soft,
440
+ softenable: parts.softenable
441
+ });
442
+ const matchedNodes = (graph, spec) => byId(graph, spec);
443
+ const inspectedOf = (nodes) => nodes.map((node) => node.spanId);
444
+ const matchedAll = (items, predicate) => items.length > 0 && items.every(predicate);
445
+ const isBreak$1 = (verdict) => Schema.is(Break)(verdict);
446
+ const isHold$1 = (verdict) => Schema.is(Hold)(verdict);
447
+ const verdictOf = (options) => options.holds ? Hold.make({
448
+ conjunct: options.id,
449
+ inspected: options.inspected
450
+ }) : Break.make({
451
+ conjunct: options.id,
452
+ inspected: options.inspected,
453
+ detail: options.detail
454
+ });
455
+ const declared = (declaration) => relation({
456
+ id: declaration.id,
457
+ soft: false,
458
+ softenable: declaration.softenable,
459
+ evaluate: (graph) => {
460
+ const inspected = declaration.spans.flatMap((span) => inspectedOf(byId(graph, span)));
461
+ return verdictOf({
462
+ id: declaration.id,
463
+ inspected,
464
+ holds: declaration.holds(graph),
465
+ detail: declaration.detail
466
+ });
467
+ }
468
+ });
469
+ const holdsCountOf = (spec, expected) => (graph) => expected(matchedNodes(graph, spec).length);
470
+ const holdsEveryOf = (spec, predicate) => (graph) => matchedAll(matchedNodes(graph, spec), predicate);
471
+ const exists = (spec) => declared({
472
+ id: `exists(${spec.id})`,
473
+ spans: [spec],
474
+ softenable: false,
475
+ detail: `no ${spec.id} span was emitted`,
476
+ holds: holdsCountOf(spec, (count) => count > 0)
477
+ });
478
+ const absent = (spec) => declared({
479
+ id: `absent(${spec.id})`,
480
+ spans: [spec],
481
+ softenable: false,
482
+ detail: `a ${spec.id} span was emitted`,
483
+ holds: holdsCountOf(spec, (count) => count === 0)
484
+ });
485
+ const unique = (spec) => declared({
486
+ id: `unique(${spec.id})`,
487
+ spans: [spec],
488
+ softenable: true,
489
+ detail: `expected exactly one ${spec.id} span`,
490
+ holds: holdsCountOf(spec, (count) => count === 1)
491
+ });
492
+ const childIdsOf = (graph, parents) => new Set(parents.flatMap((parent) => children(graph, parent)).map((node) => node.spanId));
493
+ const descendantIdsOf = (graph, parents) => new Set(parents.flatMap((parent) => descendants(graph, parent)).map((node) => node.spanId));
494
+ const startsAtOrAfter = (earlier, later) => later.startMillis >= earlier.startMillis;
495
+ const startsAfterSome = (earlier, later) => earlier.some((node) => startsAtOrAfter(node, later));
496
+ const reachedUnder = (graph, parent, childSpan, reach) => {
497
+ const reached = reach(graph, matchedNodes(graph, parent));
498
+ return matchedNodes(graph, childSpan).some((node) => reached.has(node.spanId));
499
+ };
500
+ const child = (parent, childSpan) => declared({
501
+ id: `child(${parent.id},${childSpan.id})`,
502
+ spans: [parent, childSpan],
503
+ softenable: true,
504
+ detail: `no ${childSpan.id} span is a direct child of a ${parent.id} span`,
505
+ holds: (graph) => reachedUnder(graph, parent, childSpan, childIdsOf)
506
+ });
507
+ const descendant = (parent, childSpan) => declared({
508
+ id: `descendant(${parent.id},${childSpan.id})`,
509
+ spans: [parent, childSpan],
510
+ softenable: true,
511
+ detail: `no ${childSpan.id} span descends from a ${parent.id} span`,
512
+ holds: (graph) => reachedUnder(graph, parent, childSpan, descendantIdsOf)
513
+ });
514
+ const order = (before, after) => declared({
515
+ id: `order(${before.id},${after.id})`,
516
+ spans: [before, after],
517
+ softenable: true,
518
+ detail: `a ${after.id} span starts before every ${before.id} span`,
519
+ holds: (graph) => matchedAll(matchedNodes(graph, after), (node) => startsAfterSome(matchedNodes(graph, before), node))
520
+ });
521
+ const status = (spec, expected) => declared({
522
+ id: `status(${spec.id},${expected})`,
523
+ spans: [spec],
524
+ softenable: true,
525
+ detail: `a ${spec.id} span is not ${expected}`,
526
+ holds: holdsEveryOf(spec, (node) => node.status === expected)
527
+ });
528
+ const errorType = (spec, expected) => declared({
529
+ id: `errorType(${spec.id},${expected})`,
530
+ spans: [spec],
531
+ softenable: true,
532
+ detail: `a ${spec.id} span carries a different error.type`,
533
+ holds: holdsEveryOf(spec, (node) => node.errorType === expected)
534
+ });
535
+ const partialMatches = (attributes, partial) => Object.entries(partial).filter(([, value]) => value !== void 0).every(([key, value]) => Equal.equals(attributes[key], value));
536
+ const attrs = (spec, partial) => declared({
537
+ id: `attrs(${spec.id})`,
538
+ spans: [spec],
539
+ softenable: true,
540
+ detail: `a ${spec.id} span carries a different declared attribute`,
541
+ holds: holdsEveryOf(spec, (node) => partialMatches(node.attrs, partial))
542
+ });
543
+ const durationLessThan = (spec, millis) => declared({
544
+ id: `durationLessThan(${spec.id},${millis})`,
545
+ spans: [spec],
546
+ softenable: true,
547
+ detail: `a ${spec.id} span exceeded ${millis}ms`,
548
+ holds: holdsEveryOf(spec, (node) => node.durationMillis < millis)
549
+ });
550
+ const carriesEvent = (name) => (node) => node.events.some((event) => event.name === name);
551
+ const event = (spec, name) => declared({
552
+ id: `event(${spec.id},${name})`,
553
+ spans: [spec],
554
+ softenable: true,
555
+ detail: `a ${spec.id} span carries no ${name} event`,
556
+ holds: holdsEveryOf(spec, carriesEvent(name))
557
+ });
558
+ const forall = (spec, predicate, detail) => {
559
+ const id = `forall(${spec.id})`;
560
+ return relation({
561
+ id,
562
+ soft: false,
563
+ softenable: true,
564
+ evaluate: (graph) => {
565
+ const nodes = matchedNodes(graph, spec);
566
+ return verdictOf({
567
+ id,
568
+ inspected: inspectedOf(nodes),
569
+ holds: matchedAll(nodes, predicate),
570
+ detail: nodes.length === 0 ? `no ${spec.id} span was emitted: forall is non-vacuous` : detail
571
+ });
572
+ }
573
+ });
574
+ };
575
+ const soft = (self) => self.softenable ? relation({
576
+ id: self.id,
577
+ soft: true,
578
+ softenable: true,
579
+ evaluate: (graph) => self(graph)
580
+ }) : self;
581
+ const negatedVerdict = (id, inner) => Match.value(inner).pipe(Match.tag("Hold", (held) => Break.make({
582
+ conjunct: id,
583
+ inspected: held.inspected,
584
+ detail: `${held.conjunct} held`
585
+ })), Match.tag("Break", (breach) => Hold.make({
586
+ conjunct: id,
587
+ inspected: breach.inspected
588
+ })), Match.exhaustive);
589
+ const not = (self) => {
590
+ const id = `not(${self.id})`;
591
+ return relation({
592
+ id,
593
+ soft: false,
594
+ softenable: false,
595
+ evaluate: (graph) => negatedVerdict(id, self(graph))
596
+ });
597
+ };
598
+ const collectSoft = (id, breaks) => {
599
+ const inspected = breaks.flatMap((breach) => breach.inspected);
600
+ return breaks.length === 0 ? Hold.make({
601
+ conjunct: id,
602
+ inspected
603
+ }) : Break.make({
604
+ conjunct: id,
605
+ inspected,
606
+ detail: breaks.map((breach) => breach.conjunct).join(", ")
607
+ });
608
+ };
609
+ const firstHardBreak = (relations, graph) => relations.filter((relation) => !relation.soft).map((relation) => relation(graph)).find(isBreak$1) ?? null;
610
+ const softBreaks = (relations, graph) => relations.filter((relation) => relation.soft).map((relation) => relation(graph)).filter(isBreak$1);
611
+ const combined = (options) => relation({
612
+ id: options.id,
613
+ soft: options.soft,
614
+ softenable: options.softenable,
615
+ evaluate: (graph) => firstHardBreak(options.relations, graph) ?? collectSoft(options.id, softBreaks(options.relations, graph))
616
+ });
617
+ const all = (...relations) => combined({
618
+ id: `all(${relations.map((relation) => relation.id).join(", ")})`,
619
+ relations,
620
+ soft: relations.every((relation) => relation.soft),
621
+ softenable: true
622
+ });
623
+ const any = (...relations) => {
624
+ const id = `any(${relations.map((relation) => relation.id).join(", ")})`;
625
+ return relation({
626
+ id,
627
+ soft: false,
628
+ softenable: false,
629
+ evaluate: (graph) => {
630
+ const verdicts = relations.map((relation) => relation(graph));
631
+ const inspected = verdicts.flatMap((verdict) => verdict.inspected);
632
+ return verdicts.some(isHold$1) ? Hold.make({
633
+ conjunct: id,
634
+ inspected
635
+ }) : Break.make({
636
+ conjunct: id,
637
+ inspected,
638
+ detail: verdicts.map((verdict) => verdict.conjunct).join(", ")
639
+ });
640
+ }
641
+ });
642
+ };
643
+ const REACH_BY_RELATION = {
644
+ child: childIdsOf,
645
+ descendant: descendantIdsOf
646
+ };
647
+ const placementOf = (edge) => {
648
+ const id = `placement(${edge.relation}:${edge.parent.id},${edge.child.id})`;
649
+ return relation({
650
+ id,
651
+ soft: false,
652
+ softenable: false,
653
+ evaluate: (graph) => {
654
+ const placed = matchedNodes(graph, edge.child);
655
+ const reached = REACH_BY_RELATION[edge.relation](graph, matchedNodes(graph, edge.parent));
656
+ return verdictOf({
657
+ id,
658
+ inspected: inspectedOf(placed),
659
+ holds: placed.every((node) => reached.has(node.spanId)),
660
+ detail: `a ${edge.child.id} span is not a ${edge.relation} of any ${edge.parent.id} span`
661
+ });
662
+ }
663
+ });
664
+ };
665
+ const forbidsAt = (entry, path) => Option.match(entry.unless, {
666
+ onNone: () => true,
667
+ onSome: (tag) => tag !== path
668
+ });
669
+ const forbiddenOn = (path) => (entry) => forbidsAt(entry, path) ? [absent(entry.span)] : [];
670
+ const fromTaxonomy = (taxonomy, options) => combined({
671
+ id: `fromTaxonomy(${taxonomy.id},${options.path})`,
672
+ relations: [...taxonomy.edges.map(placementOf), ...taxonomy.forbidden.flatMap(forbiddenOn(options.path))],
673
+ soft: false,
674
+ softenable: false
675
+ });
676
+ //#endregion
677
+ //#region src/Stimulus.ts
678
+ var Stimulus_exports = /* @__PURE__ */ __exportAll({
679
+ make: () => make$1,
680
+ traceContext: () => traceContext
681
+ });
682
+ const byteHex = (byte) => byte.toString(16).padStart(2, "0");
683
+ const randomHex = (byteLength) => Array.from(crypto.getRandomValues(new Uint8Array(byteLength)), byteHex).join("");
684
+ const traceContext = Effect.sync(() => {
685
+ const traceId = randomHex(16);
686
+ const spanId = randomHex(8);
687
+ return {
688
+ traceId,
689
+ spanId,
690
+ traceparent: `00-${traceId}-${spanId}-01`
691
+ };
692
+ });
693
+ const make$1 = (options) => {
694
+ const stimulus = (input) => Effect.gen(function* () {
695
+ const context = yield* traceContext;
696
+ const output = yield* options.run({
697
+ input,
698
+ traceId: context.traceId,
699
+ traceparent: context.traceparent
700
+ }).pipe(Effect.withParentSpan(OtelTracer.makeExternalSpan({
701
+ traceId: context.traceId,
702
+ spanId: context.spanId,
703
+ traceFlags: 1
704
+ })));
705
+ return {
706
+ input,
707
+ traceId: context.traceId,
708
+ output
709
+ };
710
+ });
711
+ Object.defineProperty(stimulus, "name", { value: options.name });
712
+ return stimulus;
713
+ };
714
+ //#endregion
715
+ //#region src/Prop.ts
716
+ const isHold = Schema.is(Hold);
717
+ const isBreak = Schema.is(Break);
718
+ const annotate = (context, message) => Option.match(Option.flatMap(Option.fromNullishOr(context), (task) => Option.fromNullishOr(task.annotate)), {
719
+ onNone: () => Effect.void,
720
+ onSome: (record) => Effect.promise(() => Promise.resolve(record(message, "info")))
721
+ });
722
+ const dumpMessageOf = (verdict, dumpPath) => Option.map(Option.flatMap(Option.liftPredicate(verdict, isBreak), () => Option.fromNullishOr(dumpPath)), (path) => `trace contract failed; observed graph dumped to ${path}`);
723
+ const announce = (context, verdict, dumpPath) => Option.match(dumpMessageOf(verdict, dumpPath), {
724
+ onNone: () => Effect.void,
725
+ onSome: (message) => annotate(context, message)
726
+ });
727
+ /**
728
+ * The generated-case predicate for one case: a factory that runs the contract's cell with
729
+ * the dump named after the case — so every failing draw overwrites one file and the last
730
+ * failing draw is the shrunk counterexample the runner reports — and answers the verdict as
731
+ * the boolean: `true` while the relation held, `false` on a break, falsifying the property.
732
+ * Only infrastructure refusals — a failing behaviour, an undecodable span, an empty
733
+ * observation — stay on the error channel. The scenario layer is built fresh per draw, so
734
+ * each draw owns its observation window.
735
+ */
736
+ function predicate(title, contract, scenario) {
737
+ const checked = (input, context) => judge(contract, input, { dumpName: title }).pipe(Effect.tap((judgment) => announce(context, judgment.verdict, judgment.dumpPath)), Effect.map((judgment) => isHold(judgment.verdict)), Effect.provide(Layer.fresh(scenario)));
738
+ return (input, context) => checked(input, context);
739
+ }
740
+ //#endregion
741
+ //#region src/StimulusFailure.schema.ts
742
+ var StimulusFailure = class extends Schema.TaggedError()("StimulusFailure", {
743
+ stimulus: Schema.String,
744
+ detail: Schema.String
745
+ }) {};
746
+ //#endregion
747
+ //#region src/TaskAnnounce.ts
748
+ const dispatchAnnotate = (annotate, message, type) => Option.match(Option.fromNullishOr(annotate), {
749
+ onNone: () => Effect.void,
750
+ onSome: (record) => Effect.promise(() => Promise.resolve(record(message, type)))
751
+ });
752
+ const announcement = (taskContext, message, type) => Option.match(Option.fromNullishOr(taskContext), {
753
+ onNone: () => Effect.void,
754
+ onSome: (context) => dispatchAnnotate(context.annotate, message, type)
755
+ });
756
+ const record = (message, type) => Effect.gen(function* () {
757
+ const taskContext = yield* TaskRef.RawVitestTaskRef;
758
+ return yield* announcement(taskContext, message, type);
759
+ });
760
+ const announceDump = (error) => Option.match(Option.fromNullishOr(error.dumpPath), {
761
+ onNone: () => Effect.void,
762
+ onSome: (dumpPath) => record(`trace contract failed; observed graph dumped to ${dumpPath}`, "info")
763
+ });
764
+ //#endregion
765
+ //#region src/Suite.ts
766
+ var Suite_exports = /* @__PURE__ */ __exportAll({
767
+ StimulusFailure: () => StimulusFailure,
768
+ TypeId: () => TypeId,
769
+ make: () => make,
770
+ withLayer: () => withLayer,
771
+ withScenarioLayer: () => withScenarioLayer
772
+ });
773
+ const TypeId = Symbol.for("@systemfsoftware/trace-spec/Suite");
774
+ const isCheckFailure = Schema.is(Schema.Union([
775
+ ContractDecodeError,
776
+ EmptyObservationError,
777
+ TraceDisparityError
778
+ ]));
779
+ const caseFailureOf = (stimulus) => (failure) => isCheckFailure(failure) ? failure : new StimulusFailure({
780
+ stimulus,
781
+ detail: Cause.pretty(Cause.fail(failure))
782
+ });
783
+ const rethrowAfter = (annotation, error) => Effect.andThen(annotation, () => Effect.fail(error));
784
+ const annotateDisparity = (error) => rethrowAfter(announceDump(error), error);
785
+ const caseBody = (contract, input) => check(contract, input).pipe(Effect.catchIf(Schema.is(TraceDisparityError), annotateDisparity), Effect.asVoid, Effect.mapError(caseFailureOf(contract.stimulus.name)));
786
+ const caseTools = (register, propIt, scenario) => {
787
+ const Case = (name, contract, input) => register(name, caseBody(contract, input), "run");
788
+ Case.prop = (name, contract, arbitrary) => {
789
+ propIt.effect.prop(name, [arbitrary], (values, context) => predicate(name, contract, scenario)(values[0], context));
790
+ };
791
+ return { Case };
792
+ };
793
+ const config = (name) => ({
794
+ name,
795
+ describe: "describe",
796
+ options: void 0,
797
+ liveClock: true
798
+ });
799
+ const openScenario = (bindings, name, scenario) => ({ body: (use) => Suite.openCase(bindings, config(name), scenario, (register, propIt) => use(caseTools(register, propIt, scenario))) });
800
+ const openSharedScenario = (bindings, name, shared, scenario) => ({ body: (use) => Suite.openSharedCase(bindings, config(name), {
801
+ layer: shared,
802
+ excludeTestServices: false
803
+ }, scenario, (register, propIt) => use(caseTools(register, propIt, scenario.pipe(Layer.provideMerge(shared))))) });
804
+ const openShared = (bindings, name, shared) => ({
805
+ [TypeId]: TypeId,
806
+ body: (use) => Suite.openShared(bindings, config(name), {
807
+ layer: shared,
808
+ excludeTestServices: false
809
+ }, (register, propIt) => use(caseTools(register, propIt, shared))),
810
+ withScenarioLayer: (scenario) => openSharedScenario(bindings, name, shared, scenario),
811
+ ...Prototype
812
+ });
813
+ const withLayerDual = (self, shared) => self.withLayer(shared);
814
+ const withLayer = dual(2, withLayerDual);
815
+ const withScenarioLayerDual = (self, scenario) => self.withScenarioLayer(scenario);
816
+ const withScenarioLayer = dual(2, withScenarioLayerDual);
817
+ const make = (bindings) => (name) => ({
818
+ [TypeId]: TypeId,
819
+ withLayer: (shared) => openShared(bindings, name, shared),
820
+ withScenarioLayer: (scenario) => openScenario(bindings, name, scenario),
821
+ ...Prototype
822
+ });
823
+ //#endregion
824
+ export { Contract_exports as Contract, Graph_exports as Graph, Observation_service_exports as Observation, observation_window_resource_exports as ObservationWindow, Rel_exports as Rel, Stimulus_exports as Stimulus, Suite_exports as Suite };