@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/CHANGELOG.md +9 -0
- package/LICENSE +203 -0
- package/README.md +93 -0
- package/dist/index.d.ts +344 -0
- package/dist/index.mjs +824 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +78 -0
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 };
|