@systemfsoftware/trace-spec 0.0.0 → 0.2.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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # @systemfsoftware/trace-spec
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Judge contracts against traces produced in another process. `RemoteObservation.layer(source, { interval, settle, timeout })` provides `Observation` by reading a trace store until no read has added a span for `settle`. `TempoTraceStore.source({ baseUrl })` reads Grafana Tempo's `/api/v2/traces/<traceId>` through the `HttpClient` you provide; put auth and tenant headers on that client.
8
+
9
+ New failures `Observation.IncompleteObservationError` (spans never settled, or the store marked the trace partial) and `Observation.TransportObservationError` (the store could not be read) join `Collector.collect`, `Contract.JudgeFailure`, and `Suite.CaseFailure`. This is a breaking type change: code that matches every case of these unions, or restates them by hand, must handle both.
10
+
11
+ ## 0.1.0
12
+
13
+ ### Minor Changes
14
+
15
+ - `Rel`, `Graph`, `FailureDump` and `Prop` functions that took their subject first can now also be called data-last inside `pipe`. Existing calls keep working.
16
+
17
+ ### Patch Changes
18
+
19
+ - Updated dependencies:
20
+ - @systemfsoftware/effect-spec-runtime@0.1.0
21
+
3
22
  ## 0.0.0
4
23
 
5
24
  ### Minor Changes
package/README.md CHANGED
@@ -10,7 +10,7 @@ In a system with queues, workers, and background consumers, a `200` is not the o
10
10
  pnpm add @systemfsoftware/trace-spec @systemfsoftware/trace-taxonomy effect
11
11
  ```
12
12
 
13
- The package exports seven namespaces — `Contract`, `Graph`, `Observation`, `ObservationWindow`, `Rel`, `Stimulus`, `Suite`. Errors and verdicts belong to the capability that raises them: `Contract.ContractDecodeError`, `Contract.TraceDisparityError`, `Observation.EmptyObservationError`, `Suite.StimulusFailure`, and `Rel.Hold`/`Rel.Break`/`Rel.Verdict`.
13
+ The package exports nine namespaces — `Contract`, `Graph`, `Observation`, `ObservationWindow`, `RemoteObservation`, `Rel`, `Stimulus`, `Suite`, `TempoTraceStore`. Errors and verdicts belong to the capability that raises them: `Contract.ContractDecodeError`, `Contract.TraceDisparityError`, `Observation.EmptyObservationError`, `Observation.IncompleteObservationError`, `Observation.TransportObservationError`, `Suite.StimulusFailure`, and `Rel.Hold`/`Rel.Break`/`Rel.Verdict`.
14
14
 
15
15
  ## Write a contract
16
16
 
@@ -38,15 +38,17 @@ const paymentUnderCheckout = Contract.of(checkout)
38
38
 
39
39
  ## Run the contract
40
40
 
41
- `Contract.judge(contract, input)` stimulates the behaviour under its minted trace, collects that trace's spans, decodes them against the taxonomy (`Graph.decode` answers `Result<TraceGraph, Contract.ContractDecodeError>`), applies the relation, and on a break writes the decoded graph under `artifacts/traces/`. It answers a `Contract.Judgment` — the run, the verdict, and the dump path — so a break is a value, not a failure. Behaviour failures and infrastructure refusals (`Contract.ContractDecodeError`, `Observation.EmptyObservationError`) stay on the error channel.
41
+ `Contract.judge(contract, input)` stimulates the behaviour under its minted trace, collects that trace's spans, decodes them against the taxonomy (`Graph.decode` answers `Result<TraceGraph, Contract.ContractDecodeError>`), applies the relation, and on a break writes the decoded graph under `artifacts/traces/`. It answers a `Contract.Judgment` — the run, the verdict, and the dump path — so a break is a value, not a failure. Behaviour failures and infrastructure refusals (`Contract.ContractDecodeError` and the three `Observation` failures) stay on the error channel.
42
42
 
43
43
  `Contract.check(contract, input)` is the test edge over `judge`: a break fails with `Contract.TraceDisparityError`. Navigation over the decoded graph is standalone: `Graph.byId`, `Graph.children`, and `Graph.descendants`.
44
44
 
45
- | Outcome | Failure | Meaning |
46
- | --------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------- |
47
- | The relation broke | `Contract.TraceDisparityError` | names the broken conjunct, the spans inspected, and where the decoded graph was written |
48
- | A contracted span lacked a required attribute | `Contract.ContractDecodeError` | names the declaration and the attribute — never reported as a broken relation |
49
- | Nothing ran under the owned trace | `Observation.EmptyObservationError` | the observation was empty, which is its own outcome, not a break |
45
+ | Outcome | Failure | Meaning |
46
+ | --------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------- |
47
+ | The relation broke | `Contract.TraceDisparityError` | names the broken conjunct, the spans inspected, and where the decoded graph was written |
48
+ | A contracted span lacked a required attribute | `Contract.ContractDecodeError` | names the declaration and the attribute — never reported as a broken relation |
49
+ | Nothing ran under the owned trace | `Observation.EmptyObservationError` | the observation was empty, which is its own outcome, not a break |
50
+ | The trace never finished arriving | `Observation.IncompleteObservationError` | spans were read but kept changing, or the store marked the trace partial; carries the span count |
51
+ | The trace store could not be read | `Observation.TransportObservationError` | the store refused, was unreachable, or answered a body that does not decode; names the request URL |
50
52
 
51
53
  ## Register cases
52
54
 
@@ -66,7 +68,7 @@ TraceSuite('checkout.place_order')
66
68
  })
67
69
  ```
68
70
 
69
- `Suite.make({ it, layer })(name)` stages a suite as a Pipeable builder, and `withLayer`/`withScenarioLayer` are `dual` combinators whose terminal `body` registers the cases. The scenario layer is built fresh per case and must provide `Observation.Observation` and a `FileSystem` for failure dumps, plus whatever the stimulated behaviour needs — the requirement is enforced at the type level. A failing case fails with the harness's own error channel — the three outcomes above, or `Suite.StimulusFailure` when the behaviour itself failed before its trace could be judged. Cases run on the live clock.
71
+ `Suite.make({ it, layer })(name)` stages a suite as a Pipeable builder, and `withLayer`/`withScenarioLayer` are `dual` combinators whose terminal `body` registers the cases. The scenario layer is built fresh per case and must provide `Observation.Observation` and a `FileSystem` for failure dumps, plus whatever the stimulated behaviour needs — the requirement is enforced at the type level. A failing case fails with the harness's own error channel — the outcomes above, or `Suite.StimulusFailure` when the behaviour itself failed before its trace could be judged. Cases run on the live clock.
70
72
 
71
73
  `.withLayer(shared)` registers the suite over a layer built once for the whole suite; the shared layer then carries the observation harness, and `.withScenarioLayer(scenario)` can follow it for what each case needs fresh.
72
74
 
@@ -76,6 +78,27 @@ TraceSuite('checkout.place_order')
76
78
 
77
79
  `ObservationWindow.make(serviceName)` is the cold resource; the service name is required. `.scoped` acquires an `ObservationWindow` handle for one scope — an OpenTelemetry in-memory exporter behind a simple span processor and an always-on sampler, shut down when the scope closes — and `ObservationWindow.collect(window, traceId)` reads one trace back. `.layer` binds a window as `Observation.Observation` plus the Effect tracer, one window per layer build, so two acquisitions never see each other's spans. Only the handle module imports the OpenTelemetry SDK. The export happens outside any test-clock boundary.
78
80
 
81
+ ## Observe a remote store
82
+
83
+ When the behaviour runs in another process — a CLI binary, a worker, a container — its spans never reach an in-memory window. Export them to a trace store and read them back with `RemoteObservation.layer`, which provides `Observation.Observation` over a trace source:
84
+
85
+ ```ts
86
+ import { RemoteObservation, TempoTraceStore } from '@systemfsoftware/trace-spec'
87
+ import { Layer } from 'effect'
88
+ import { FetchHttpClient } from 'effect/unstable/http'
89
+
90
+ const observation = RemoteObservation.layer(
91
+ TempoTraceStore.source({ baseUrl: 'http://127.0.0.1:3200' }),
92
+ { interval: '50 millis', settle: '500 millis', timeout: '10 seconds' },
93
+ ).pipe(Layer.provide(FetchHttpClient.layer))
94
+ ```
95
+
96
+ `collect(traceId)` reads the source every `interval` and keeps every span it has seen, keyed by span id. It answers once no read has added a span for `settle`, because no trace store says when a trace is complete: a trace read too early looks finished but is missing spans still in flight. A read that returns fewer spans than an earlier one never drops a span. The whole read runs under `timeout`, and a read still in flight at the deadline is cancelled: nothing read by then fails with `Observation.EmptyObservationError`, spans that never settled fail with `Observation.IncompleteObservationError`. A source failure fails at once, without retrying. Every duration must be positive.
97
+
98
+ `TempoTraceStore.source({ baseUrl })` reads Grafana Tempo's `GET /api/v2/traces/<traceId>` once. It needs only an `HttpClient`, so auth and tenant headers go on the client you provide: `HttpClient.mapRequest(client, HttpClientRequest.setHeader('X-Scope-OrgID', tenant))`. A trace Tempo marks `PARTIAL` fails with `Observation.IncompleteObservationError`; a non-2xx answer, including `404`, or a body that does not decode fails with `Observation.TransportObservationError`.
99
+
100
+ Another store needs only another source: a `RemoteObservation.TraceSource<R>` is one read that answers the spans the store holds for a trace id now — empty when it holds none — or fails with `Observation.TransportObservationError` or `Observation.IncompleteObservationError`. It never polls, waits, or retries; the layer owns the cadence.
101
+
79
102
  ## Relations
80
103
 
81
104
  `Rel.exists`, `Rel.absent`, `Rel.unique`, `Rel.child`, `Rel.descendant`, `Rel.status`, `Rel.errorType`, `Rel.attrs`, `Rel.durationLessThan`, `Rel.forall`, `Rel.event`, `Rel.order`, `Rel.any`, `Rel.not`, `Rel.soft`, `Rel.all`, `Rel.fromTaxonomy`.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { Context, Effect, FileSystem, Layer, Result, Schema } from "effect";
1
+ import { Context, Duration, Effect, FileSystem, Layer, Result, Schema } from "effect";
2
2
  import { Pipeable } from "effect/Pipeable";
3
3
  import { Span, Taxonomy } from "@systemfsoftware/trace-taxonomy";
4
+ import * as HttpClient from "effect/unstable/http/HttpClient";
4
5
  import * as OtelTracer from "@effect/opentelemetry/OtelTracer";
5
6
  import { BasicTracerProvider, InMemorySpanExporter } from "@opentelemetry/sdk-trace-base";
6
7
  import { Suite } from "@systemfsoftware/effect-spec-runtime";
@@ -23,6 +24,14 @@ declare const EmptyObservationError_base: Schema.Class<EmptyObservationError, Sc
23
24
  }>, import("effect/Cause").YieldableError>;
24
25
  declare class EmptyObservationError extends EmptyObservationError_base {}
25
26
  //#endregion
27
+ //#region src/IncompleteObservationError.schema.d.ts
28
+ declare const IncompleteObservationError_base: Schema.Class<IncompleteObservationError, Schema.TaggedStruct<"IncompleteObservationError", {
29
+ readonly traceId: Schema.String;
30
+ readonly spanCount: Schema.Finite;
31
+ readonly detail: Schema.String;
32
+ }>, import("effect/Cause").YieldableError>;
33
+ declare class IncompleteObservationError extends IncompleteObservationError_base {}
34
+ //#endregion
26
35
  //#region src/TraceGraph.schema.d.ts
27
36
  declare const Status: Schema.Literals<readonly ["ok", "unset", "error"]>;
28
37
  type Status = typeof Status.Type;
@@ -104,9 +113,18 @@ type TraceGraph = typeof TraceGraph.Type;
104
113
  declare namespace Graph_d_exports {
105
114
  export { AttributeValue, Attributes, GraphNode, SpanEvent, SpanLink, SpanRecord, Status, TraceGraph, byId, children, decode, descendants };
106
115
  }
107
- declare const byId: (graph: TraceGraph, spec: Span.Span) => ReadonlyArray<GraphNode>;
108
- declare const children: (graph: TraceGraph, node: GraphNode) => ReadonlyArray<GraphNode>;
109
- declare const descendants: (graph: TraceGraph, node: GraphNode) => ReadonlyArray<GraphNode>;
116
+ declare const byId: {
117
+ (spec: Span.Span): (graph: TraceGraph) => ReadonlyArray<GraphNode>;
118
+ (graph: TraceGraph, spec: Span.Span): ReadonlyArray<GraphNode>;
119
+ };
120
+ declare const children: {
121
+ (node: GraphNode): (graph: TraceGraph) => ReadonlyArray<GraphNode>;
122
+ (graph: TraceGraph, node: GraphNode): ReadonlyArray<GraphNode>;
123
+ };
124
+ declare const descendants: {
125
+ (node: GraphNode): (graph: TraceGraph) => ReadonlyArray<GraphNode>;
126
+ (graph: TraceGraph, node: GraphNode): ReadonlyArray<GraphNode>;
127
+ };
110
128
  /**
111
129
  * Decodes the recorded spans of one trace against a taxonomy, purely: a declared span
112
130
  * whose required attributes do not decode refuses with {@link ContractDecodeError}
@@ -114,12 +132,24 @@ declare const descendants: (graph: TraceGraph, node: GraphNode) => ReadonlyArray
114
132
  * the graph as recorded. Failure is a `Result`, so the phase that reads the trace cannot
115
133
  * fail the pipeline by accident.
116
134
  */
117
- declare const decode: (traceId: string, spans: ReadonlyArray<SpanRecord>, taxonomy: Taxonomy.Taxonomy) => Result.Result<TraceGraph, ContractDecodeError>;
135
+ declare const decode: {
136
+ (spans: ReadonlyArray<SpanRecord>, taxonomy: Taxonomy.Taxonomy): (traceId: string) => Result.Result<TraceGraph, ContractDecodeError>;
137
+ (traceId: string, spans: ReadonlyArray<SpanRecord>, taxonomy: Taxonomy.Taxonomy): Result.Result<TraceGraph, ContractDecodeError>;
138
+ };
139
+ //#endregion
140
+ //#region src/TransportObservationError.schema.d.ts
141
+ declare const TransportObservationError_base: Schema.Class<TransportObservationError, Schema.TaggedStruct<"TransportObservationError", {
142
+ readonly traceId: Schema.String;
143
+ readonly source: Schema.String;
144
+ readonly detail: Schema.String;
145
+ }>, import("effect/Cause").YieldableError>;
146
+ declare class TransportObservationError extends TransportObservationError_base {}
118
147
  declare namespace Observation_service_d_exports {
119
- export { Collector, EmptyObservationError, Observation };
148
+ export { Collector, EmptyObservationError, IncompleteObservationError, Observation, ObservationFailure, TransportObservationError };
120
149
  }
150
+ type ObservationFailure = EmptyObservationError | IncompleteObservationError | TransportObservationError;
121
151
  interface Collector {
122
- readonly collect: (traceId: string) => Effect.Effect<ReadonlyArray<SpanRecord>, EmptyObservationError>;
152
+ readonly collect: (traceId: string) => Effect.Effect<ReadonlyArray<SpanRecord>, ObservationFailure>;
123
153
  }
124
154
  declare const Observation_base: Context.ServiceClass<Observation, "@systemfsoftware/trace-spec/Observation", Collector>;
125
155
  declare class Observation extends Observation_base {}
@@ -156,22 +186,54 @@ interface Relation {
156
186
  declare const exists: <S extends Span.Span>(spec: S) => Relation;
157
187
  declare const absent: <S extends Span.Span>(spec: S) => Relation;
158
188
  declare const unique: <S extends Span.Span>(spec: S) => Relation;
159
- declare const child: (parent: Span.Span, childSpan: Span.Span) => Relation;
160
- declare const descendant: (parent: Span.Span, childSpan: Span.Span) => Relation;
161
- declare const order: (before: Span.Span, after: Span.Span) => Relation;
162
- declare const status: (spec: Span.Span, expected: Status) => Relation;
163
- declare const errorType: (spec: Span.Span, expected: string) => Relation;
164
- declare const attrs: <S extends Span.Span>(spec: S, partial: Partial<Span.AttrsOf<S>>) => Relation;
165
- declare const durationLessThan: (spec: Span.Span, millis: number) => Relation;
166
- declare const event: (spec: Span.Span, name: string) => Relation;
167
- declare const forall: (spec: Span.Span, predicate: (node: GraphNode) => boolean, detail: string) => Relation;
189
+ declare const child: {
190
+ (childSpan: Span.Span): (parent: Span.Span) => Relation;
191
+ (parent: Span.Span, childSpan: Span.Span): Relation;
192
+ };
193
+ declare const descendant: {
194
+ (childSpan: Span.Span): (parent: Span.Span) => Relation;
195
+ (parent: Span.Span, childSpan: Span.Span): Relation;
196
+ };
197
+ declare const order: {
198
+ (after: Span.Span): (before: Span.Span) => Relation;
199
+ (before: Span.Span, after: Span.Span): Relation;
200
+ };
201
+ declare const status: {
202
+ (expected: Status): (spec: Span.Span) => Relation;
203
+ (spec: Span.Span, expected: Status): Relation;
204
+ };
205
+ declare const errorType: {
206
+ (expected: string): (spec: Span.Span) => Relation;
207
+ (spec: Span.Span, expected: string): Relation;
208
+ };
209
+ declare const attrs: {
210
+ <S extends Span.Span>(partial: Partial<Span.AttrsOf<S>>): (spec: S) => Relation;
211
+ <S extends Span.Span>(spec: S, partial: Partial<Span.AttrsOf<S>>): Relation;
212
+ };
213
+ declare const durationLessThan: {
214
+ (millis: number): (spec: Span.Span) => Relation;
215
+ (spec: Span.Span, millis: number): Relation;
216
+ };
217
+ declare const event: {
218
+ (name: string): (spec: Span.Span) => Relation;
219
+ (spec: Span.Span, name: string): Relation;
220
+ };
221
+ declare const forall: {
222
+ (predicate: (node: GraphNode) => boolean, detail: string): (spec: Span.Span) => Relation;
223
+ (spec: Span.Span, predicate: (node: GraphNode) => boolean, detail: string): Relation;
224
+ };
168
225
  declare const soft: (self: Relation) => Relation;
169
226
  declare const not: (self: Relation) => Relation;
170
227
  declare const all: (...relations: ReadonlyArray<Relation>) => Relation;
171
228
  declare const any: (...relations: ReadonlyArray<Relation>) => Relation;
172
- declare const fromTaxonomy: (taxonomy: Taxonomy.Taxonomy, options: {
173
- readonly path: string;
174
- }) => Relation;
229
+ declare const fromTaxonomy: {
230
+ (options: {
231
+ readonly path: string;
232
+ }): (taxonomy: Taxonomy.Taxonomy) => Relation;
233
+ (taxonomy: Taxonomy.Taxonomy, options: {
234
+ readonly path: string;
235
+ }): Relation;
236
+ };
175
237
  declare namespace Stimulus_d_exports {
176
238
  export { Run, Stimulus, TraceContext, make$3 as make, traceContext };
177
239
  }
@@ -208,7 +270,7 @@ declare const TraceDisparityError_base: Schema.Class<TraceDisparityError, Schema
208
270
  }>, import("effect/Cause").YieldableError>;
209
271
  declare class TraceDisparityError extends TraceDisparityError_base {}
210
272
  declare namespace Contract_d_exports {
211
- export { CheckFailure, CheckOptions, Contract, ContractDecodeError, Declared$1 as Declared, EmptyObservationError, JudgeFailure, Judgment, Services, Stimulated, TraceDisparityError, TypeId$3 as TypeId, check, holds, judge, of, stimulate };
273
+ export { CheckFailure, CheckOptions, Contract, ContractDecodeError, Declared$1 as Declared, EmptyObservationError, IncompleteObservationError, JudgeFailure, Judgment, Services, Stimulated, TraceDisparityError, TransportObservationError, TypeId$3 as TypeId, check, holds, judge, of, stimulate };
212
274
  }
213
275
  declare const TypeId$3: unique symbol;
214
276
  type TypeId$3 = typeof TypeId$3;
@@ -246,7 +308,7 @@ interface Judgment<Input, Output> {
246
308
  interface CheckOptions {
247
309
  readonly dumpName?: string | undefined;
248
310
  }
249
- type JudgeFailure<E> = E | ContractDecodeError | EmptyObservationError;
311
+ type JudgeFailure<E> = E | ContractDecodeError | ObservationFailure;
250
312
  type CheckFailure<E> = JudgeFailure<E> | TraceDisparityError;
251
313
  type Services<R> = R | Observation | FileSystem.FileSystem;
252
314
  declare const judge: {
@@ -257,6 +319,46 @@ declare const check: {
257
319
  <Input>(input: Input, options?: CheckOptions): <Output, E, R>(self: Contract<Input, Output, E, R>) => Effect.Effect<Judgment<Input, Output>, CheckFailure<E>, Services<R>>;
258
320
  <Input, Output, E, R>(self: Contract<Input, Output, E, R>, input: Input, options?: CheckOptions): Effect.Effect<Judgment<Input, Output>, CheckFailure<E>, Services<R>>;
259
321
  };
322
+ declare namespace RemoteObservation_d_exports {
323
+ export { Options, TraceSource, layer };
324
+ }
325
+ /**
326
+ * The cadence of the poll, the quiet window the observed span set must hold
327
+ * before the read is trusted, and the deadline the whole poll runs under.
328
+ * Durations are refused unless they are positive, at layer construction.
329
+ */
330
+ interface Options {
331
+ readonly interval: Duration.Input;
332
+ readonly settle: Duration.Input;
333
+ readonly timeout: Duration.Input;
334
+ }
335
+ /**
336
+ * One read against one store: the spans that store currently holds for a trace
337
+ * id, or the store's own transport or unfinished answer. A source never polls,
338
+ * waits, or retries — the cadence belongs to {@link layer}.
339
+ */
340
+ type TraceSource<R> = (traceId: string) => Effect.Effect<ReadonlyArray<SpanRecord>, IncompleteObservationError | TransportObservationError, R>;
341
+ /**
342
+ * The `Observation` service over one trace source. `collect` reads the source at
343
+ * `options.interval`, keeps every span it has seen keyed by span id, and answers
344
+ * when that union has stayed unchanged for `options.settle`; a trace the store
345
+ * never served is `EmptyObservationError`, one that never settled is
346
+ * `IncompleteObservationError`, and the source's own failure ends the poll
347
+ * immediately, unchanged. Provides `Observation` alone and requires exactly what
348
+ * the source requires (`Scope.Scope` is supplied by the layer machinery, as
349
+ * every Effect layer constructor scopes it away).
350
+ */
351
+ declare const layer: {
352
+ (options: Options): <R>(source: TraceSource<R>) => Layer.Layer<Observation, never, Exclude<R, Scope.Scope>>;
353
+ <R>(source: TraceSource<R>, options: Options): Layer.Layer<Observation, never, Exclude<R, Scope.Scope>>;
354
+ };
355
+ declare namespace tempo_trace_store_d_exports {
356
+ export { source };
357
+ }
358
+ /** One read against one Tempo endpoint. Its only requirement is the caller's HTTP client. */
359
+ declare const source: (options: {
360
+ readonly baseUrl: string;
361
+ }) => TraceSource<HttpClient.HttpClient>;
260
362
  //#endregion
261
363
  //#region src/ObservationWindowSpec.schema.d.ts
262
364
  declare const ObservationWindowSpec_base: Schema.Class<ObservationWindowSpec, Schema.Struct<{
@@ -297,6 +399,10 @@ interface ObservationWindowResource extends Pipeable {
297
399
  }
298
400
  declare const make$1: (serviceName: string) => ObservationWindowResource;
299
401
  //#endregion
402
+ //#region src/HarnessFailure.schema.d.ts
403
+ declare const HarnessFailure: Schema.Union<readonly [typeof ContractDecodeError, typeof EmptyObservationError, typeof IncompleteObservationError, typeof TransportObservationError, typeof TraceDisparityError]>;
404
+ type HarnessFailure = typeof HarnessFailure.Type;
405
+ //#endregion
300
406
  //#region src/StimulusFailure.schema.d.ts
301
407
  declare const StimulusFailure_base: Schema.Class<StimulusFailure, Schema.TaggedStruct<"StimulusFailure", {
302
408
  readonly stimulus: Schema.String;
@@ -308,7 +414,7 @@ declare namespace Suite_d_exports {
308
414
  }
309
415
  declare const TypeId: unique symbol;
310
416
  type TypeId = typeof TypeId;
311
- type CaseFailure = ContractDecodeError | EmptyObservationError | TraceDisparityError | StimulusFailure;
417
+ type CaseFailure = HarnessFailure | StimulusFailure;
312
418
  type Harness = Observation | FileSystem.FileSystem;
313
419
  type ArbitraryInput<Input> = Schema.Schema<Input> | Arbitrary.Arbitrary<Input>;
314
420
  interface CaseRegistrar<Provided> {
@@ -341,4 +447,4 @@ declare const withScenarioLayer: {
341
447
  };
342
448
  declare const make: (bindings: Suite.Bindings) => (name: string) => Declared;
343
449
  //#endregion
344
- export { Contract_d_exports as Contract, Graph_d_exports as Graph, Observation_service_d_exports as Observation, observation_window_resource_d_exports as ObservationWindow, Rel_d_exports as Rel, Stimulus_d_exports as Stimulus, Suite_d_exports as Suite };
450
+ export { Contract_d_exports as Contract, Graph_d_exports as Graph, Observation_service_d_exports as Observation, observation_window_resource_d_exports as ObservationWindow, Rel_d_exports as Rel, RemoteObservation_d_exports as RemoteObservation, Stimulus_d_exports as Stimulus, Suite_d_exports as Suite, tempo_trace_store_d_exports as TempoTraceStore };
package/dist/index.mjs CHANGED
@@ -1,9 +1,12 @@
1
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";
2
+ import { Array as Array$1, Cause, Clock, Context, Duration, Effect, Encoding, Equal, FileSystem, Layer, Match, Option, Predicate, Ref, Result, Schema, SchemaGetter, SchemaTransformation } from "effect";
3
3
  import { dual } from "effect/Function";
4
4
  import { Prototype } from "effect/Pipeable";
5
5
  import "effect/PlatformError";
6
6
  import "@systemfsoftware/trace-taxonomy";
7
+ import * as HttpClient from "effect/unstable/http/HttpClient";
8
+ import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
9
+ import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
7
10
  import * as OtelTracer from "@effect/opentelemetry/OtelTracer";
8
11
  import * as OtelResource from "@effect/opentelemetry/Resource";
9
12
  import { SpanStatusCode } from "@opentelemetry/api";
@@ -61,15 +64,16 @@ const renderNode = (spans, node, depth) => {
61
64
  ...childrenOf(spans, node.spanId).map((child) => renderNode(spans, child, depth + 1))
62
65
  ]].join("\n");
63
66
  };
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) => [
67
+ const documentImpl = (observed, report) => [
69
68
  `trace ${observed.traceId}`,
70
69
  report,
71
70
  ...observed.spans.map((node) => renderNode(observed.spans, node, 0))
72
71
  ].join("\n");
72
+ /**
73
+ * The dump document: the trace it belongs to, the break that refused it, and the recorded
74
+ * spans as the observation window saw them.
75
+ */
76
+ const document = dual(2, documentImpl);
73
77
  const sanitize = (value) => value.replace(/[^a-zA-Z0-9._-]+/g, "-");
74
78
  const fileNameOf = (request) => `${sanitize(request.name ?? request.traceId)}.${sanitize(request.conjunct)}.txt`;
75
79
  /**
@@ -156,12 +160,15 @@ var Graph_exports = /* @__PURE__ */ __exportAll({
156
160
  decode: () => decode,
157
161
  descendants: () => descendants
158
162
  });
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) => {
163
+ const byIdImpl = (graph, spec) => graph.nodes.filter((node) => node.name === spec.name);
164
+ const byId = dual(2, byIdImpl);
165
+ const childrenImpl = (graph, node) => graph.nodes.filter((candidate) => candidate.parentSpanId === node.spanId);
166
+ const children = dual(2, childrenImpl);
167
+ const descendantsImpl = (graph, node) => {
162
168
  const direct = children(graph, node);
163
169
  return [...direct, ...direct.flatMap((child) => descendants(graph, child))];
164
170
  };
171
+ const descendants = dual(2, descendantsImpl);
165
172
  const indexDeclarations = (taxonomy) => new Map(taxonomy.spans.map((span) => [span.name, span]));
166
173
  const keyOfPath = (path) => {
167
174
  const last = path.at(-1);
@@ -204,6 +211,10 @@ const graphOf = (traceId, nodes) => ({
204
211
  traceId,
205
212
  nodes
206
213
  });
214
+ const decodeImpl = (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
+ };
207
218
  /**
208
219
  * Decodes the recorded spans of one trace against a taxonomy, purely: a declared span
209
220
  * whose required attributes do not decode refuses with {@link ContractDecodeError}
@@ -211,15 +222,28 @@ const graphOf = (traceId, nodes) => ({
211
222
  * the graph as recorded. Failure is a `Result`, so the phase that reads the trace cannot
212
223
  * fail the pipeline by accident.
213
224
  */
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
- };
225
+ const decode = dual(3, decodeImpl);
226
+ //#endregion
227
+ //#region src/IncompleteObservationError.schema.ts
228
+ var IncompleteObservationError = class extends Schema.TaggedError()("IncompleteObservationError", {
229
+ traceId: Schema.String,
230
+ spanCount: Schema.Finite,
231
+ detail: Schema.String
232
+ }) {};
233
+ //#endregion
234
+ //#region src/TransportObservationError.schema.ts
235
+ var TransportObservationError = class extends Schema.TaggedError()("TransportObservationError", {
236
+ traceId: Schema.String,
237
+ source: Schema.String,
238
+ detail: Schema.String
239
+ }) {};
218
240
  //#endregion
219
241
  //#region src/Observation.service.ts
220
242
  var Observation_service_exports = /* @__PURE__ */ __exportAll({
221
243
  EmptyObservationError: () => EmptyObservationError,
222
- Observation: () => Observation
244
+ IncompleteObservationError: () => IncompleteObservationError,
245
+ Observation: () => Observation,
246
+ TransportObservationError: () => TransportObservationError
223
247
  });
224
248
  var Observation = class extends Context.Service()("@systemfsoftware/trace-spec/Observation") {};
225
249
  //#endregion
@@ -235,7 +259,9 @@ var TraceDisparityError = class extends Schema.TaggedError()("TraceDisparityErro
235
259
  var Contract_exports = /* @__PURE__ */ __exportAll({
236
260
  ContractDecodeError: () => ContractDecodeError,
237
261
  EmptyObservationError: () => EmptyObservationError,
262
+ IncompleteObservationError: () => IncompleteObservationError,
238
263
  TraceDisparityError: () => TraceDisparityError,
264
+ TransportObservationError: () => TransportObservationError,
239
265
  TypeId: () => TypeId$3,
240
266
  check: () => check,
241
267
  holds: () => holds,
@@ -303,6 +329,219 @@ const refuseBreak = (relationId, judgment) => Match.value(judgment.verdict).pipe
303
329
  const checkDual = (self, input, options) => Effect.flatMap(judgeDual(self, input, options), (judgment) => refuseBreak(self.relation.id, judgment));
304
330
  const check = dual((args) => isContract(args[0]), checkDual);
305
331
  //#endregion
332
+ //#region src/drivers/tempo-trace.schema.ts
333
+ /** Proto bytes (base64 on the wire) as the lowercase hex ids traces are addressed by. */
334
+ const HexId = Schema.Uint8ArrayFromBase64.pipe(Schema.decodeTo(Schema.String, {
335
+ decode: SchemaGetter.transform(Encoding.encodeHex),
336
+ encode: SchemaGetter.decodeHex()
337
+ }));
338
+ const secondsAndNanos = (nanos) => {
339
+ const text = nanos.padStart(10, "0");
340
+ return [Number(text.slice(0, -9)), Number(text.slice(-9))];
341
+ };
342
+ const millisOf$2 = (nanos) => {
343
+ const [seconds, subMillis] = secondsAndNanos(nanos);
344
+ return seconds * 1e3 + subMillis / 1e6;
345
+ };
346
+ const MillisFromNanos = Schema.String.pipe(Schema.decodeTo(Schema.Finite, SchemaTransformation.transform({
347
+ decode: millisOf$2,
348
+ encode: (millis) => String(Math.round(millis * 1e6))
349
+ })));
350
+ const unsetParent = (parent) => parent === "" || /^0+$/.test(parent);
351
+ const heldParent = (parent) => unsetParent(parent) ? null : parent;
352
+ const parentOf$1 = (parent) => heldParent(parent ?? "");
353
+ const EMPTY_STATUS = {};
354
+ const codeOf = (status) => status.code ?? "STATUS_CODE_UNSET";
355
+ const STATUS_BY_CODE$1 = {
356
+ STATUS_CODE_UNSET: "unset",
357
+ STATUS_CODE_OK: "ok",
358
+ STATUS_CODE_ERROR: "error"
359
+ };
360
+ const statusOf$1 = (status) => STATUS_BY_CODE$1[codeOf(status ?? EMPTY_STATUS)];
361
+ const stringValueOf = (value) => value.stringValue ?? null;
362
+ const numberOf = (value) => value.doubleValue ?? intToNumber(value.intValue);
363
+ const intToNumber = (text) => text === void 0 ? null : Number(text);
364
+ const boolOrNumberOf = (value) => value.boolValue ?? numberOf(value);
365
+ const scalarOf = (value) => stringValueOf(value) ?? boolOrNumberOf(value);
366
+ const isScalarMember = (member) => member !== null;
367
+ const arrayValuesOf = (value) => value.arrayValue?.values;
368
+ const scalarMembersOf = (values) => values.map(scalarOf).filter(isScalarMember);
369
+ const arrayValueOf = (values) => values === void 0 ? null : scalarMembersOf(values);
370
+ const presentValueOf = (value) => scalarOf(value) ?? arrayValueOf(arrayValuesOf(value));
371
+ const isPresent$1 = (entry) => entry[1] !== null;
372
+ const entryOf = (attribute) => [attribute.key, attributeValueOf(attribute.value)];
373
+ const attributeValueOf = (value) => value === void 0 ? null : presentValueOf(value);
374
+ const attributesOf = (attributes) => Object.fromEntries((attributes ?? []).map(entryOf).filter(isPresent$1));
375
+ const errorTypeOf$1 = (attributes) => {
376
+ const value = attributes["error.type"];
377
+ return typeof value === "string" ? value : null;
378
+ };
379
+ const eventOf = (event) => ({
380
+ name: event.name,
381
+ attributes: attributesOf(event.attributes)
382
+ });
383
+ const linkIdOf = (id) => id ?? "";
384
+ const linkOf = (link) => ({
385
+ traceId: linkIdOf(link.traceId),
386
+ spanId: linkIdOf(link.spanId)
387
+ });
388
+ const eventsOf$1 = (span) => (span.events ?? []).map(eventOf);
389
+ const linksOf$1 = (span) => (span.links ?? []).map(linkOf);
390
+ const spanRecordOf$1 = (span) => {
391
+ const attributes = attributesOf(span.attributes);
392
+ return {
393
+ traceId: span.traceId,
394
+ spanId: span.spanId,
395
+ parentSpanId: parentOf$1(span.parentSpanId),
396
+ name: span.name,
397
+ status: statusOf$1(span.status),
398
+ errorType: errorTypeOf$1(attributes),
399
+ startMillis: span.startTimeUnixNano,
400
+ durationMillis: span.endTimeUnixNano - span.startTimeUnixNano,
401
+ attributes,
402
+ events: eventsOf$1(span),
403
+ links: linksOf$1(span)
404
+ };
405
+ };
406
+ const wireTextOf = (value) => typeof value === "string" ? { stringValue: value } : void 0;
407
+ const wireBoolOf = (value) => typeof value === "boolean" ? { boolValue: value } : void 0;
408
+ const isValueArray = (value) => Array.isArray(value);
409
+ const wireArrayOf = (value) => ({ arrayValue: { values: isValueArray(value) ? value.map(wireValueOf) : [] } });
410
+ const wireNumberOrArrayOf = (value) => typeof value === "number" ? { doubleValue: value } : wireArrayOf(value);
411
+ const wireBoolOrNumberOf = (value) => wireBoolOf(value) ?? wireNumberOrArrayOf(value);
412
+ const wireValueOf = (value) => wireTextOf(value) ?? wireBoolOrNumberOf(value);
413
+ const wireAttributesOf = (attributes) => Object.entries(attributes).map(([key, value]) => ({
414
+ key,
415
+ value: wireValueOf(value)
416
+ }));
417
+ const STATUS_CODE_BY_STATUS = {
418
+ unset: "STATUS_CODE_UNSET",
419
+ ok: "STATUS_CODE_OK",
420
+ error: "STATUS_CODE_ERROR"
421
+ };
422
+ const wireStatusOf = (status) => ({ code: STATUS_CODE_BY_STATUS[status] });
423
+ const wireSpanOf = (record) => ({
424
+ traceId: record.traceId,
425
+ spanId: record.spanId,
426
+ parentSpanId: record.parentSpanId ?? void 0,
427
+ name: record.name,
428
+ startTimeUnixNano: record.startMillis,
429
+ endTimeUnixNano: record.startMillis + record.durationMillis,
430
+ attributes: wireAttributesOf(record.attributes),
431
+ events: record.events.map((event) => ({
432
+ name: event.name,
433
+ attributes: wireAttributesOf(event.attributes)
434
+ })),
435
+ links: record.links.map((link) => ({
436
+ traceId: link.traceId,
437
+ spanId: link.spanId
438
+ })),
439
+ status: wireStatusOf(record.status)
440
+ });
441
+ const WireAnyValue = Schema.Struct({
442
+ stringValue: Schema.optional(Schema.String),
443
+ boolValue: Schema.optional(Schema.Boolean),
444
+ intValue: Schema.optional(Schema.String),
445
+ doubleValue: Schema.optional(Schema.Finite),
446
+ arrayValue: Schema.optional(Schema.Struct({ values: Schema.optional(Schema.Array(Schema.suspend(() => WireAnyValue))) }))
447
+ });
448
+ const WireAttribute = Schema.Struct({
449
+ key: Schema.String,
450
+ value: Schema.optional(WireAnyValue)
451
+ });
452
+ const WireStatus = Schema.Struct({
453
+ message: Schema.optional(Schema.String),
454
+ code: Schema.optional(Schema.Literals([
455
+ "STATUS_CODE_UNSET",
456
+ "STATUS_CODE_OK",
457
+ "STATUS_CODE_ERROR"
458
+ ]))
459
+ });
460
+ const WireEvent = Schema.Struct({
461
+ name: Schema.String,
462
+ timeUnixNano: Schema.optional(MillisFromNanos),
463
+ attributes: Schema.optional(Schema.Array(WireAttribute))
464
+ });
465
+ const WireLink = Schema.Struct({
466
+ traceId: Schema.optional(HexId),
467
+ spanId: Schema.optional(HexId)
468
+ });
469
+ const WireSpan = Schema.Struct({
470
+ traceId: HexId,
471
+ spanId: HexId,
472
+ parentSpanId: Schema.optional(HexId),
473
+ name: Schema.String,
474
+ kind: Schema.optional(Schema.String),
475
+ startTimeUnixNano: MillisFromNanos,
476
+ endTimeUnixNano: MillisFromNanos,
477
+ attributes: Schema.optional(Schema.Array(WireAttribute)),
478
+ events: Schema.optional(Schema.Array(WireEvent)),
479
+ links: Schema.optional(Schema.Array(WireLink)),
480
+ status: Schema.optional(WireStatus),
481
+ traceState: Schema.optional(Schema.String),
482
+ flags: Schema.optional(Schema.Finite)
483
+ }).pipe(Schema.decodeTo(SpanRecord, SchemaTransformation.transform({
484
+ decode: spanRecordOf$1,
485
+ encode: wireSpanOf
486
+ })));
487
+ const WireScopeSpans = Schema.Struct({ spans: Schema.optional(Schema.Array(WireSpan)) });
488
+ const WireResourceSpans = Schema.Struct({ scopeSpans: Schema.optional(Schema.Array(WireScopeSpans)) });
489
+ /** One trace-by-id answer: the spans, plus the store's completeness status when it is not complete. */
490
+ const TempoV2TraceResponse = Schema.Struct({
491
+ trace: Schema.Struct({ resourceSpans: Schema.optional(Schema.Array(WireResourceSpans)) }),
492
+ status: Schema.optional(Schema.String),
493
+ message: Schema.optional(Schema.String)
494
+ });
495
+ //#endregion
496
+ //#region src/drivers/tempo-trace-store.ts
497
+ var tempo_trace_store_exports = /* @__PURE__ */ __exportAll({ source: () => source });
498
+ /**
499
+ * The Grafana Tempo trace source: one read of one trace by id against one
500
+ * store endpoint. It never polls, waits, or retries — completeness belongs
501
+ * to the remote observation layer. Any refusal (unreachable endpoint,
502
+ * non-2xx answer, undecodable body) fails with the transport failure whose
503
+ * `source` is the request URL; an answer the store marked incomplete fails
504
+ * with the incomplete failure, since waiting cannot complete it.
505
+ */
506
+ const parseBody = Schema.decodeResult(Schema.fromJsonString(TempoV2TraceResponse));
507
+ const orEmpty = (values) => values ?? [];
508
+ const resourceSpansOf = (response) => orEmpty(response.trace.resourceSpans);
509
+ const scopeSpansOf = (resourceSpans) => orEmpty(resourceSpans.scopeSpans);
510
+ const spansOfScope = (scopeSpans) => orEmpty(scopeSpans.spans);
511
+ const spansOf = (response) => resourceSpansOf(response).flatMap((resourceSpans) => scopeSpansOf(resourceSpans).flatMap(spansOfScope));
512
+ const transportFailureOf = (traceId, url, cause) => new TransportObservationError({
513
+ traceId,
514
+ source: url,
515
+ detail: cause
516
+ });
517
+ const refusedOf = (traceId, url, error) => transportFailureOf(traceId, url, error.message);
518
+ const undecodableOf = (traceId, url, error) => transportFailureOf(traceId, url, `undecodable trace body: ${error.message}`);
519
+ const incompleteOf = (traceId, status, spanCount) => new IncompleteObservationError({
520
+ traceId,
521
+ spanCount,
522
+ detail: `the store answered ${status}: waiting cannot complete the trace`
523
+ });
524
+ const isComplete = (status) => status === void 0 || status === "COMPLETE";
525
+ const markedOf = (response) => response.status ?? "INCOMPLETE";
526
+ const recordsOf$1 = (traceId, response) => {
527
+ const spans = spansOf(response);
528
+ return isComplete(response.status) ? Effect.succeed(spans) : Effect.fail(incompleteOf(traceId, markedOf(response), spans.length));
529
+ };
530
+ const readTrace = (traceId, url, body) => {
531
+ const decoded = parseBody(body);
532
+ return Result.isFailure(decoded) ? Effect.fail(undecodableOf(traceId, url, decoded.failure)) : recordsOf$1(traceId, decoded.success);
533
+ };
534
+ /** One read against one Tempo endpoint. Its only requirement is the caller's HTTP client. */
535
+ const source = (options) => {
536
+ const urlOf = (traceId) => `${options.baseUrl}/api/v2/traces/${traceId}`;
537
+ return (traceId) => {
538
+ const url = urlOf(traceId);
539
+ return HttpClient.execute(HttpClientRequest.get(url).pipe(HttpClientRequest.acceptJson)).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk), Effect.flatMap((response) => response.text), Effect.mapError((error) => refusedOf(traceId, url, error)), Effect.flatMap((body) => readTrace(traceId, url, body)));
540
+ };
541
+ };
542
+ Schema.decodeEffect(HexId);
543
+ Schema.encodeEffect(HexId);
544
+ //#endregion
306
545
  //#region src/observation-window.handle.ts
307
546
  const TypeId$2 = Symbol.for("~systemfsoftware/trace-spec/ObservationWindow");
308
547
  const ExporterId = Symbol.for("~systemfsoftware/trace-spec/ObservationWindow/exporter");
@@ -356,7 +595,7 @@ const linksOf = (span) => span.links.map((link) => ({
356
595
  traceId: link.context.traceId,
357
596
  spanId: link.context.spanId
358
597
  }));
359
- const millisOf = (duration) => duration[0] * 1e3 + duration[1] / 1e6;
598
+ const millisOf$1 = (duration) => duration[0] * 1e3 + duration[1] / 1e6;
360
599
  const spanRecordOf = (span) => {
361
600
  const attributes = attributeRecordOf(span.attributes);
362
601
  return {
@@ -366,8 +605,8 @@ const spanRecordOf = (span) => {
366
605
  name: span.name,
367
606
  status: statusOf(span.status.code),
368
607
  errorType: errorTypeOf(attributes),
369
- startMillis: millisOf(span.startTime),
370
- durationMillis: millisOf(span.duration),
608
+ startMillis: millisOf$1(span.startTime),
609
+ durationMillis: millisOf$1(span.duration),
371
610
  attributes,
372
611
  events: eventsOf(span),
373
612
  links: linksOf(span)
@@ -397,7 +636,7 @@ var observation_window_resource_exports = /* @__PURE__ */ __exportAll({
397
636
  });
398
637
  const TypeId$1 = Symbol.for("~systemfsoftware/trace-spec/ObservationWindowResource");
399
638
  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))));
639
+ const layer$1 = (spec) => OtelTracer.layer.pipe(Layer.provideMerge(OtelResource.layer({ serviceName: spec.serviceName })), Layer.provideMerge(Layer.effectContext(Effect.map(scoped(spec), context))));
401
640
  const makeProto = (spec) => ({
402
641
  [TypeId$1]: TypeId$1,
403
642
  spec,
@@ -405,7 +644,7 @@ const makeProto = (spec) => ({
405
644
  return scoped(spec);
406
645
  },
407
646
  get layer() {
408
- return layer(spec);
647
+ return layer$1(spec);
409
648
  },
410
649
  ...Prototype
411
650
  });
@@ -444,7 +683,7 @@ const inspectedOf = (nodes) => nodes.map((node) => node.spanId);
444
683
  const matchedAll = (items, predicate) => items.length > 0 && items.every(predicate);
445
684
  const isBreak$1 = (verdict) => Schema.is(Break)(verdict);
446
685
  const isHold$1 = (verdict) => Schema.is(Hold)(verdict);
447
- const verdictOf = (options) => options.holds ? Hold.make({
686
+ const verdictOf$1 = (options) => options.holds ? Hold.make({
448
687
  conjunct: options.id,
449
688
  inspected: options.inspected
450
689
  }) : Break.make({
@@ -458,7 +697,7 @@ const declared = (declaration) => relation({
458
697
  softenable: declaration.softenable,
459
698
  evaluate: (graph) => {
460
699
  const inspected = declaration.spans.flatMap((span) => inspectedOf(byId(graph, span)));
461
- return verdictOf({
700
+ return verdictOf$1({
462
701
  id: declaration.id,
463
702
  inspected,
464
703
  holds: declaration.holds(graph),
@@ -497,65 +736,73 @@ const reachedUnder = (graph, parent, childSpan, reach) => {
497
736
  const reached = reach(graph, matchedNodes(graph, parent));
498
737
  return matchedNodes(graph, childSpan).some((node) => reached.has(node.spanId));
499
738
  };
500
- const child = (parent, childSpan) => declared({
739
+ const childImpl = (parent, childSpan) => declared({
501
740
  id: `child(${parent.id},${childSpan.id})`,
502
741
  spans: [parent, childSpan],
503
742
  softenable: true,
504
743
  detail: `no ${childSpan.id} span is a direct child of a ${parent.id} span`,
505
744
  holds: (graph) => reachedUnder(graph, parent, childSpan, childIdsOf)
506
745
  });
507
- const descendant = (parent, childSpan) => declared({
746
+ const child = dual(2, childImpl);
747
+ const descendantImpl = (parent, childSpan) => declared({
508
748
  id: `descendant(${parent.id},${childSpan.id})`,
509
749
  spans: [parent, childSpan],
510
750
  softenable: true,
511
751
  detail: `no ${childSpan.id} span descends from a ${parent.id} span`,
512
752
  holds: (graph) => reachedUnder(graph, parent, childSpan, descendantIdsOf)
513
753
  });
514
- const order = (before, after) => declared({
754
+ const descendant = dual(2, descendantImpl);
755
+ const orderImpl = (before, after) => declared({
515
756
  id: `order(${before.id},${after.id})`,
516
757
  spans: [before, after],
517
758
  softenable: true,
518
759
  detail: `a ${after.id} span starts before every ${before.id} span`,
519
760
  holds: (graph) => matchedAll(matchedNodes(graph, after), (node) => startsAfterSome(matchedNodes(graph, before), node))
520
761
  });
521
- const status = (spec, expected) => declared({
762
+ const order = dual(2, orderImpl);
763
+ const statusImpl = (spec, expected) => declared({
522
764
  id: `status(${spec.id},${expected})`,
523
765
  spans: [spec],
524
766
  softenable: true,
525
767
  detail: `a ${spec.id} span is not ${expected}`,
526
768
  holds: holdsEveryOf(spec, (node) => node.status === expected)
527
769
  });
528
- const errorType = (spec, expected) => declared({
770
+ const status = dual(2, statusImpl);
771
+ const errorTypeImpl = (spec, expected) => declared({
529
772
  id: `errorType(${spec.id},${expected})`,
530
773
  spans: [spec],
531
774
  softenable: true,
532
775
  detail: `a ${spec.id} span carries a different error.type`,
533
776
  holds: holdsEveryOf(spec, (node) => node.errorType === expected)
534
777
  });
778
+ const errorType = dual(2, errorTypeImpl);
535
779
  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({
780
+ const attrsImpl = (spec, partial) => declared({
537
781
  id: `attrs(${spec.id})`,
538
782
  spans: [spec],
539
783
  softenable: true,
540
784
  detail: `a ${spec.id} span carries a different declared attribute`,
541
785
  holds: holdsEveryOf(spec, (node) => partialMatches(node.attrs, partial))
542
786
  });
543
- const durationLessThan = (spec, millis) => declared({
787
+ const attrs = dual(2, attrsImpl);
788
+ const durationLessThanImpl = (spec, millis) => declared({
544
789
  id: `durationLessThan(${spec.id},${millis})`,
545
790
  spans: [spec],
546
791
  softenable: true,
547
792
  detail: `a ${spec.id} span exceeded ${millis}ms`,
548
793
  holds: holdsEveryOf(spec, (node) => node.durationMillis < millis)
549
794
  });
795
+ const durationLessThan = dual(2, durationLessThanImpl);
550
796
  const carriesEvent = (name) => (node) => node.events.some((event) => event.name === name);
551
- const event = (spec, name) => declared({
797
+ const eventImpl = (spec, name) => declared({
552
798
  id: `event(${spec.id},${name})`,
553
799
  spans: [spec],
554
800
  softenable: true,
555
801
  detail: `a ${spec.id} span carries no ${name} event`,
556
802
  holds: holdsEveryOf(spec, carriesEvent(name))
557
803
  });
558
- const forall = (spec, predicate, detail) => {
804
+ const event = dual(2, eventImpl);
805
+ const forallImpl = (spec, predicate, detail) => {
559
806
  const id = `forall(${spec.id})`;
560
807
  return relation({
561
808
  id,
@@ -563,7 +810,7 @@ const forall = (spec, predicate, detail) => {
563
810
  softenable: true,
564
811
  evaluate: (graph) => {
565
812
  const nodes = matchedNodes(graph, spec);
566
- return verdictOf({
813
+ return verdictOf$1({
567
814
  id,
568
815
  inspected: inspectedOf(nodes),
569
816
  holds: matchedAll(nodes, predicate),
@@ -572,6 +819,7 @@ const forall = (spec, predicate, detail) => {
572
819
  }
573
820
  });
574
821
  };
822
+ const forall = dual(3, forallImpl);
575
823
  const soft = (self) => self.softenable ? relation({
576
824
  id: self.id,
577
825
  soft: true,
@@ -653,7 +901,7 @@ const placementOf = (edge) => {
653
901
  evaluate: (graph) => {
654
902
  const placed = matchedNodes(graph, edge.child);
655
903
  const reached = REACH_BY_RELATION[edge.relation](graph, matchedNodes(graph, edge.parent));
656
- return verdictOf({
904
+ return verdictOf$1({
657
905
  id,
658
906
  inspected: inspectedOf(placed),
659
907
  holds: placed.every((node) => reached.has(node.spanId)),
@@ -667,12 +915,127 @@ const forbidsAt = (entry, path) => Option.match(entry.unless, {
667
915
  onSome: (tag) => tag !== path
668
916
  });
669
917
  const forbiddenOn = (path) => (entry) => forbidsAt(entry, path) ? [absent(entry.span)] : [];
670
- const fromTaxonomy = (taxonomy, options) => combined({
918
+ const fromTaxonomyImpl = (taxonomy, options) => combined({
671
919
  id: `fromTaxonomy(${taxonomy.id},${options.path})`,
672
920
  relations: [...taxonomy.edges.map(placementOf), ...taxonomy.forbidden.flatMap(forbiddenOn(options.path))],
673
921
  soft: false,
674
922
  softenable: false
675
923
  });
924
+ const fromTaxonomy = dual(2, fromTaxonomyImpl);
925
+ //#endregion
926
+ //#region src/RemoteObservation.ts
927
+ var RemoteObservation_exports = /* @__PURE__ */ __exportAll({ layer: () => layer });
928
+ const NEVER_READ = {
929
+ spans: [],
930
+ addedAtMillis: 0
931
+ };
932
+ const idOf = (span) => span.spanId;
933
+ const sameId = (left, right) => left.spanId === right.spanId;
934
+ const idsOf = (spans) => new Set(spans.map(idOf));
935
+ const firstOfEachId = (read) => Array$1.dedupeWith(read, sameId);
936
+ const freshOf = (settlement, read) => {
937
+ const known = idsOf(settlement.spans);
938
+ return firstOfEachId(read).filter((span) => !known.has(span.spanId));
939
+ };
940
+ const absorbed = (settlement, read, elapsedMillis) => {
941
+ const fresh = freshOf(settlement, read);
942
+ return fresh.length === 0 ? settlement : {
943
+ spans: [...settlement.spans, ...fresh],
944
+ addedAtMillis: elapsedMillis
945
+ };
946
+ };
947
+ const quietForLongEnough = (settlement, elapsedMillis, windows) => settlement.spans.length > 0 && elapsedMillis - settlement.addedAtMillis >= windows.settleMillis;
948
+ const pastDeadline = (elapsedMillis, windows) => elapsedMillis >= windows.timeoutMillis;
949
+ const settledVerdict = (spans) => Option.some(Result.succeed(spans));
950
+ const refusedVerdict = (refusal) => Option.some(Result.fail(refusal));
951
+ const verdictOf = (settlement, elapsedMillis, windows) => Match.value({
952
+ quiet: quietForLongEnough(settlement, elapsedMillis, windows),
953
+ vacant: settlement.spans.length === 0,
954
+ late: pastDeadline(elapsedMillis, windows)
955
+ }).pipe(Match.when({ quiet: true }, () => settledVerdict(settlement.spans)), Match.when({
956
+ vacant: true,
957
+ late: true
958
+ }, () => refusedVerdict("absent")), Match.when({ late: true }, () => refusedVerdict("unfinished")), Match.orElse(() => Option.none()));
959
+ const settleStep = (settlement, read, elapsedMillis, windows) => {
960
+ const advanced = absorbed(settlement, read, elapsedMillis);
961
+ return {
962
+ settlement: advanced,
963
+ verdict: verdictOf(advanced, elapsedMillis, windows)
964
+ };
965
+ };
966
+ const lateRefusal = (settlement) => settlement.spans.length === 0 ? "absent" : "unfinished";
967
+ const deadlineVerdict = (settlement, elapsedMillis, windows) => pastDeadline(elapsedMillis, windows) ? refusedVerdict(lateRefusal(settlement)) : Option.none();
968
+ const millisOf = (input) => Duration.toMillis(input);
969
+ const windowsOf = (options) => ({
970
+ settleMillis: millisOf(options.settle),
971
+ timeoutMillis: millisOf(options.timeout)
972
+ });
973
+ const nonPositive = (options) => [
974
+ options.interval,
975
+ options.settle,
976
+ options.timeout
977
+ ].some((input) => millisOf(input) <= 0);
978
+ const requirePositive = (options) => nonPositive(options) ? Effect.die(/* @__PURE__ */ new Error(`remote observation durations must be positive milliseconds: interval=${millisOf(options.interval)}, settle=${millisOf(options.settle)}, timeout=${millisOf(options.timeout)}`)) : Effect.void;
979
+ const absence = (traceId, windows) => new EmptyObservationError({
980
+ traceId,
981
+ detail: `the remote trace store served no span for trace ${traceId} before the ${windows.timeoutMillis}ms timeout`
982
+ });
983
+ const incompleteness = (traceId, spans, windows) => new IncompleteObservationError({
984
+ traceId,
985
+ spanCount: spans.length,
986
+ detail: `the remote trace store served ${spans.length} span(s) for trace ${traceId}, which never stayed unchanged for ${windows.settleMillis}ms before the ${windows.timeoutMillis}ms timeout`
987
+ });
988
+ const answer = (verdict, settlement, traceId, windows) => Result.match(verdict, {
989
+ onSuccess: (spans) => Effect.succeed(spans),
990
+ onFailure: (refusal) => refusal === "absent" ? Effect.fail(absence(traceId, windows)) : Effect.fail(incompleteness(traceId, settlement.spans, windows))
991
+ });
992
+ const watching = (windows) => Effect.gen(function* () {
993
+ const startedAtMillis = yield* Clock.currentTimeMillis;
994
+ return {
995
+ settlement: yield* Ref.make(NEVER_READ),
996
+ windows,
997
+ startedAtMillis
998
+ };
999
+ });
1000
+ const advanceAt = (watch, read) => Effect.gen(function* () {
1001
+ const elapsedMillis = (yield* Clock.currentTimeMillis) - watch.startedAtMillis;
1002
+ return settleStep(yield* Ref.get(watch.settlement), read, elapsedMillis, watch.windows);
1003
+ });
1004
+ const elapsedFor = (watch) => Effect.map(Clock.currentTimeMillis, (now) => now - watch.startedAtMillis);
1005
+ const poll = (source, traceId, options, watch) => Effect.gen(function* () {
1006
+ const settlement = yield* Ref.get(watch.settlement);
1007
+ const elapsedMillis = yield* elapsedFor(watch);
1008
+ return yield* Option.match(deadlineVerdict(settlement, elapsedMillis, watch.windows), {
1009
+ onNone: () => readOnce(source, traceId, options, watch, watch.windows.timeoutMillis - elapsedMillis),
1010
+ onSome: (reached) => answer(reached, settlement, traceId, watch.windows)
1011
+ });
1012
+ });
1013
+ const overdue = (traceId, watch) => Effect.flatMap(Ref.get(watch.settlement), (settlement) => answer(Result.fail(lateRefusal(settlement)), settlement, traceId, watch.windows));
1014
+ const readOnce = (source, traceId, options, watch, remainingMillis) => Effect.flatMap(Effect.timeoutOption(source(traceId), remainingMillis), (answered) => Option.match(answered, {
1015
+ onNone: () => overdue(traceId, watch),
1016
+ onSome: (read) => judgeRead(source, traceId, options, watch, read)
1017
+ }));
1018
+ const judgeRead = (source, traceId, options, watch, read) => Effect.flatMap(advanceAt(watch, read), (advance) => Option.match(advance.verdict, {
1019
+ onNone: () => keepPolling(source, traceId, options, watch, advance),
1020
+ onSome: (verdict) => answer(verdict, advance.settlement, traceId, watch.windows)
1021
+ }));
1022
+ const keepPolling = (source, traceId, options, watch, advance) => Effect.gen(function* () {
1023
+ yield* Ref.set(watch.settlement, advance.settlement);
1024
+ yield* Effect.sleep(options.interval);
1025
+ return yield* Effect.suspend(() => poll(source, traceId, options, watch));
1026
+ });
1027
+ const collector = (source, options, context) => ({ collect: (traceId) => Effect.flatMap(watching(windowsOf(options)), (watch) => poll(source, traceId, options, watch)).pipe(Effect.provideContext(context)) });
1028
+ /**
1029
+ * The `Observation` service over one trace source. `collect` reads the source at
1030
+ * `options.interval`, keeps every span it has seen keyed by span id, and answers
1031
+ * when that union has stayed unchanged for `options.settle`; a trace the store
1032
+ * never served is `EmptyObservationError`, one that never settled is
1033
+ * `IncompleteObservationError`, and the source's own failure ends the poll
1034
+ * immediately, unchanged. Provides `Observation` alone and requires exactly what
1035
+ * the source requires (`Scope.Scope` is supplied by the layer machinery, as
1036
+ * every Effect layer constructor scopes it away).
1037
+ */
1038
+ const layer = dual(2, (source, options) => Layer.effectContext(Effect.map(Effect.andThen(requirePositive(options), Effect.context()), (context) => Context.make(Observation, collector(source, options, context)))));
676
1039
  //#endregion
677
1040
  //#region src/Stimulus.ts
678
1041
  var Stimulus_exports = /* @__PURE__ */ __exportAll({
@@ -712,6 +1075,15 @@ const make$1 = (options) => {
712
1075
  return stimulus;
713
1076
  };
714
1077
  //#endregion
1078
+ //#region src/HarnessFailure.schema.ts
1079
+ const HarnessFailure = Schema.Union([
1080
+ ContractDecodeError,
1081
+ EmptyObservationError,
1082
+ IncompleteObservationError,
1083
+ TransportObservationError,
1084
+ TraceDisparityError
1085
+ ]);
1086
+ //#endregion
715
1087
  //#region src/Prop.ts
716
1088
  const isHold = Schema.is(Hold);
717
1089
  const isBreak = Schema.is(Break);
@@ -724,19 +1096,11 @@ const announce = (context, verdict, dumpPath) => Option.match(dumpMessageOf(verd
724
1096
  onNone: () => Effect.void,
725
1097
  onSome: (message) => annotate(context, message)
726
1098
  });
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) {
1099
+ const predicateImpl = (title, contract, scenario) => {
737
1100
  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
1101
  return (input, context) => checked(input, context);
739
- }
1102
+ };
1103
+ const predicate = dual(3, predicateImpl);
740
1104
  //#endregion
741
1105
  //#region src/StimulusFailure.schema.ts
742
1106
  var StimulusFailure = class extends Schema.TaggedError()("StimulusFailure", {
@@ -771,12 +1135,8 @@ var Suite_exports = /* @__PURE__ */ __exportAll({
771
1135
  withScenarioLayer: () => withScenarioLayer
772
1136
  });
773
1137
  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({
1138
+ const isHarnessFailure = Schema.is(HarnessFailure);
1139
+ const caseFailureOf = (stimulus) => (failure) => isHarnessFailure(failure) ? failure : new StimulusFailure({
780
1140
  stimulus,
781
1141
  detail: Cause.pretty(Cause.fail(failure))
782
1142
  });
@@ -821,4 +1181,4 @@ const make = (bindings) => (name) => ({
821
1181
  ...Prototype
822
1182
  });
823
1183
  //#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 };
1184
+ export { Contract_exports as Contract, Graph_exports as Graph, Observation_service_exports as Observation, observation_window_resource_exports as ObservationWindow, Rel_exports as Rel, RemoteObservation_exports as RemoteObservation, Stimulus_exports as Stimulus, Suite_exports as Suite, tempo_trace_store_exports as TempoTraceStore };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@systemfsoftware/trace-spec",
3
3
  "license": "Apache-2.0",
4
- "version": "0.0.0",
4
+ "version": "0.2.0",
5
5
  "author": "Ryan Lee <drdgvhbh@gmail.com>",
6
6
  "repository": {
7
7
  "type": "git",
@@ -38,25 +38,25 @@
38
38
  "@opentelemetry/resources": "^2",
39
39
  "@opentelemetry/sdk-trace-base": "^2",
40
40
  "@opentelemetry/semantic-conventions": "^1.33",
41
- "@systemfsoftware/trace-taxonomy": "^0.0.0",
42
- "@systemfsoftware/effect-spec-runtime": "^0.0.0"
41
+ "@systemfsoftware/effect-spec-runtime": "^0.1.0",
42
+ "@systemfsoftware/trace-taxonomy": "^0.0.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@effect/platform-node": "4.0.0-rc.116",
46
46
  "@effect/vitest": "4.0.0-rc.116",
47
47
  "@systemfsoftware/arethetypeswrong-cli": "^4.2.0",
48
+ "@systemfsoftware/effect-gherkin-spec": "^4.3.0",
49
+ "@systemfsoftware/oxlint-config-recommended": "^2.0.0",
50
+ "@systemfsoftware/tsconfig": "^2.0.0",
51
+ "@systemfsoftware/tsdown-config": "^0.1.0",
52
+ "@systemfsoftware/vitest-config": "^0.1.0",
48
53
  "@types/node": "^26",
49
54
  "effect": "4.0.0-rc.116",
50
55
  "oxlint": "~1.82.0",
51
56
  "rimraf": "^6.1.3",
52
57
  "tsdown": "^0.23.0",
53
58
  "tstyche": "^7.2.5",
54
- "vitest": "^5",
55
- "@systemfsoftware/effect-gherkin-spec": "^4.2.3",
56
- "@systemfsoftware/oxlint-config-recommended": "^1.1.1",
57
- "@systemfsoftware/tsconfig": "^1.3.6",
58
- "@systemfsoftware/tsdown-config": "^0.1.0",
59
- "@systemfsoftware/vitest-config": "^0.1.0"
59
+ "vitest": "^5"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "@effect/vitest": "4.0.0-rc.116",
@@ -69,7 +69,7 @@
69
69
  "scripts": {
70
70
  "clean": "rimraf dist",
71
71
  "build": "tsdown -l warn",
72
- "typecheck": "tsc --noEmit --incremental",
72
+ "typecheck": "tsc -b",
73
73
  "test": "vitest run",
74
74
  "test:types": "TSTYCHE_TYPESCRIPT_MODULE=tstyche-typescript tstyche",
75
75
  "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}",