@systemfsoftware/trace-spec 0.2.1 → 0.4.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 +24 -0
- package/README.md +19 -8
- package/dist/index.d.ts +86 -41
- package/dist/index.mjs +195 -127
- package/package.json +8 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @systemfsoftware/trace-spec
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- `Contract.check(expect, input, options?)` takes the test's `expect` and ends in one check: a break fails it with the report as the message. `Contract.judge` returns the `{ verdict: 'Hold' | 'Break', report }` value, and `Contract.verdictCheck(contract, expect, judgment)` turns one into a check. `TraceDisparityError` is removed. `Suite` cases register as generator tests and judge each case once.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies:
|
|
12
|
+
- @systemfsoftware/vitest@0.1.0
|
|
13
|
+
|
|
14
|
+
## 0.3.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- Requires `@systemfsoftware/vitest` installed under the `@effect/vitest` name (`"@effect/vitest": "npm:@systemfsoftware/vitest"`) in place of the upstream package. `Case.prop` checks the stimulus it is handed on every run and pins the run's input, so a stimulus that ignores its input fails; the trace dump path is annotated on the failing test.
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- The observation window is built on the `Blueprint` and `Handle` kinds, with unchanged behaviour. The blueprint's `TypeId` is now exported as a value.
|
|
23
|
+
|
|
24
|
+
- Updated dependencies:
|
|
25
|
+
- @systemfsoftware/effect-spec-runtime@0.2.0
|
|
26
|
+
|
|
3
27
|
## 0.2.1
|
|
4
28
|
|
|
5
29
|
### Patch 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 nine namespaces — `Contract`, `Graph`, `Observation`, `ObservationWindow`, `RemoteObservation`, `Rel`, `Stimulus`, `Suite`, `TempoTraceStore`. Errors
|
|
13
|
+
The package exports nine namespaces — `Contract`, `Graph`, `Observation`, `ObservationWindow`, `RemoteObservation`, `Rel`, `Stimulus`, `Suite`, `TempoTraceStore`. Errors belong to the capability that raises them: `Contract.ContractDecodeError`, `Observation.EmptyObservationError`, `Observation.IncompleteObservationError`, `Observation.TransportObservationError`, `Suite.StimulusFailure`; the verdicts are `Rel.Hold`/`Rel.Break`/`Rel.Verdict`. A break is not an error: it is the check the contract states in the test, and the report is that check's message.
|
|
14
14
|
|
|
15
15
|
## Write a contract
|
|
16
16
|
|
|
@@ -40,11 +40,22 @@ const paymentUnderCheckout = Contract.of(checkout)
|
|
|
40
40
|
|
|
41
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
|
-
`Contract.check(contract, input)` is the test edge over `judge
|
|
43
|
+
`Contract.check(contract, expect, input)` is the test edge over `judge`, and takes the `expect` the test body received. It ends in the judgement's one check: a hold is the check passing, and a break fails it with the report — the relation that broke, the conjunct it was evaluating, the spans it inspected, the trace, and where the decoded graph was written. `Contract.verdictCheck(contract, expect, judgment)` states an already judged contract the same way, and `Contract.Report` — `{ verdict, report }`, the report empty on a hold — is what the check asserts.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { Contract } from '@systemfsoftware/trace-spec'
|
|
47
|
+
import { it } from '@systemfsoftware/vitest'
|
|
48
|
+
|
|
49
|
+
it('a charged settlement satisfies the contract', function*({ expect }) {
|
|
50
|
+
yield* Contract.check(chargeBeneathSettlement, expect, { orderId: 'order-7', charge: true })
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Navigation over the decoded graph is standalone: `Graph.byId`, `Graph.children`, and `Graph.descendants`.
|
|
44
55
|
|
|
45
56
|
| Outcome | Failure | Meaning |
|
|
46
57
|
| --------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
47
|
-
| The relation broke |
|
|
58
|
+
| The relation broke | the judgement's check | its message is the report: the broken conjunct, the spans inspected, the trace and the dump path |
|
|
48
59
|
| A contracted span lacked a required attribute | `Contract.ContractDecodeError` | names the declaration and the attribute — never reported as a broken relation |
|
|
49
60
|
| Nothing ran under the owned trace | `Observation.EmptyObservationError` | the observation was empty, which is its own outcome, not a break |
|
|
50
61
|
| The trace never finished arriving | `Observation.IncompleteObservationError` | spans were read but kept changing, or the store marked the trace partial; carries the span count |
|
|
@@ -54,11 +65,11 @@ const paymentUnderCheckout = Contract.of(checkout)
|
|
|
54
65
|
|
|
55
66
|
```ts
|
|
56
67
|
import { NodeFileSystem } from '@effect/platform-node'
|
|
57
|
-
import { it, layer } from '@effect/vitest'
|
|
58
68
|
import { Observation, ObservationWindow, Suite } from '@systemfsoftware/trace-spec'
|
|
69
|
+
import { it } from '@systemfsoftware/vitest'
|
|
59
70
|
import { Layer } from 'effect'
|
|
60
71
|
|
|
61
|
-
const TraceSuite = Suite.make({ it
|
|
72
|
+
const TraceSuite = Suite.make({ it })
|
|
62
73
|
const harness = Layer.mergeAll(ObservationWindow.make('checkout').layer, NodeFileSystem.layer, CheckoutDoubles)
|
|
63
74
|
|
|
64
75
|
TraceSuite('checkout.place_order')
|
|
@@ -68,11 +79,11 @@ TraceSuite('checkout.place_order')
|
|
|
68
79
|
})
|
|
69
80
|
```
|
|
70
81
|
|
|
71
|
-
`Suite.make({ it
|
|
82
|
+
`Suite.make({ it })(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 kernel under the zero-preemption schedule plus the profile's seeded PCT schedules; every case gets its own crypto-random salt, so two cases never share a trace id. A case that must stay on the live clock declares why with `.live(reason)` at any stage before `body` (including the `Suite.live` dual), and the reason appears in the run report.
|
|
72
83
|
|
|
73
|
-
`.withLayer(shared)` registers the suite over a layer
|
|
84
|
+
`.withLayer(shared)` registers the suite over a layer rebuilt fresh for each case for replay; the shared layer then carries the observation harness, and `.withScenarioLayer(scenario)` can follow it for what each case needs fresh.
|
|
74
85
|
|
|
75
|
-
`Case.prop(title, contract, schema)` runs the contract over inputs drawn from the Schema's derived arbitrary through `it.effect.prop` on the suite's runner, so shrinking is the runner's own: a failing draw fails the property and the runner reports the smallest failing input. The dump is named after the case title, so every failing draw overwrites one file and the surviving dump is the reported counterexample's evidence. When a case fails with
|
|
86
|
+
`Case.prop(title, contract, schema)` runs the contract over inputs drawn from the Schema's derived arbitrary through `it.effect.prop` on the suite's runner, so shrinking is the runner's own: a failing draw fails the property and the runner reports the smallest failing input. The dump is named after the case title, so every failing draw overwrites one file and the surviving dump is the reported counterexample's evidence. When a case's relation breaks, its check fails with the report and the test carries a Vitest annotation naming that dump, so the decoded graph is found from the failing test.
|
|
76
87
|
|
|
77
88
|
## Observe in memory
|
|
78
89
|
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,10 @@ import { Pipeable } from "effect/Pipeable";
|
|
|
3
3
|
import { Span, Taxonomy } from "@systemfsoftware/trace-taxonomy";
|
|
4
4
|
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
5
5
|
import * as OtelTracer from "@effect/opentelemetry/OtelTracer";
|
|
6
|
-
import {
|
|
6
|
+
import { Blueprint, Handle } from "@systemfsoftware/effect-cell-types";
|
|
7
|
+
import { BasicTracerProvider, ReadableSpan } from "@opentelemetry/sdk-trace-base";
|
|
7
8
|
import { Suite } from "@systemfsoftware/effect-spec-runtime";
|
|
9
|
+
import { Asserted, Check, Expect } from "@systemfsoftware/vitest";
|
|
8
10
|
import * as Scope from "effect/Scope";
|
|
9
11
|
import * as Arbitrary from "effect/unstable/arbitrary/Arbitrary";
|
|
10
12
|
//#region src/ContractDecodeError.schema.d.ts
|
|
@@ -260,17 +262,8 @@ declare const make$3: <Input, Output, E, R>(options: {
|
|
|
260
262
|
readonly traceparent: string;
|
|
261
263
|
}) => Effect.Effect<Output, E, R>;
|
|
262
264
|
}) => Stimulus<Input, Output, E, R>;
|
|
263
|
-
//#endregion
|
|
264
|
-
//#region src/TraceDisparityError.schema.d.ts
|
|
265
|
-
declare const TraceDisparityError_base: Schema.Class<TraceDisparityError, Schema.TaggedStruct<"TraceDisparityError", {
|
|
266
|
-
readonly relationId: Schema.String;
|
|
267
|
-
readonly traceId: Schema.String;
|
|
268
|
-
readonly breaks: Schema.$Array<typeof Break>;
|
|
269
|
-
readonly dumpPath: Schema.NullOr<Schema.String>;
|
|
270
|
-
}>, import("effect/Cause").YieldableError>;
|
|
271
|
-
declare class TraceDisparityError extends TraceDisparityError_base {}
|
|
272
265
|
declare namespace Contract_d_exports {
|
|
273
|
-
export {
|
|
266
|
+
export { CheckOptions, Contract, ContractDecodeError, Declared$1 as Declared, EmptyObservationError, IncompleteObservationError, JudgeFailure, Judgment, Report, Services, Stimulated, TransportObservationError, TypeId$3 as TypeId, check, holds, judge, of, stimulate, verdictCheck };
|
|
274
267
|
}
|
|
275
268
|
declare const TypeId$3: unique symbol;
|
|
276
269
|
type TypeId$3 = typeof TypeId$3;
|
|
@@ -309,18 +302,41 @@ interface CheckOptions {
|
|
|
309
302
|
readonly dumpName?: string | undefined;
|
|
310
303
|
}
|
|
311
304
|
type JudgeFailure<E> = E | ContractDecodeError | ObservationFailure;
|
|
312
|
-
type CheckFailure<E> = JudgeFailure<E> | TraceDisparityError;
|
|
313
305
|
type Services<R> = R | Observation | FileSystem.FileSystem;
|
|
314
306
|
declare const judge: {
|
|
315
307
|
<Input>(input: Input, options?: CheckOptions): <Output, E, R>(self: Contract<Input, Output, E, R>) => Effect.Effect<Judgment<Input, Output>, JudgeFailure<E>, Services<R>>;
|
|
316
308
|
<Input, Output, E, R>(self: Contract<Input, Output, E, R>, input: Input, options?: CheckOptions): Effect.Effect<Judgment<Input, Output>, JudgeFailure<E>, Services<R>>;
|
|
317
309
|
};
|
|
310
|
+
/**
|
|
311
|
+
* What a judged contract answers: the verdict, and — when the relation broke — the report the judgement
|
|
312
|
+
* leaves behind. The report is empty only for a hold, so a check over it asserts the whole judgement.
|
|
313
|
+
*/
|
|
314
|
+
interface Report {
|
|
315
|
+
readonly verdict: 'Hold' | 'Break';
|
|
316
|
+
/**
|
|
317
|
+
* The relation that broke, the conjunct it was evaluating, the spans it inspected, the trace, and where the
|
|
318
|
+
* decoded graph was written.
|
|
319
|
+
*/
|
|
320
|
+
readonly report: string;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* The one check a judgement makes: it passes only when the relation held, and its message is the report, so a
|
|
324
|
+
* break names the conjunct that broke, the spans it inspected and where the evidence was written.
|
|
325
|
+
*/
|
|
326
|
+
declare const verdictCheck: {
|
|
327
|
+
<Input, Output, E, R>(expect: Expect, judgment: Judgment<Input, Output>): (self: Contract<Input, Output, E, R>) => Check;
|
|
328
|
+
<Input, Output, E, R>(self: Contract<Input, Output, E, R>, expect: Expect, judgment: Judgment<Input, Output>): Check;
|
|
329
|
+
};
|
|
330
|
+
/**
|
|
331
|
+
* The test edge over `judge`: the judgement and its one check. A break fails the check with the report;
|
|
332
|
+
* behaviour failures and infrastructure refusals stay on the error channel.
|
|
333
|
+
*/
|
|
318
334
|
declare const check: {
|
|
319
|
-
<Input>(input: Input, options?: CheckOptions): <Output, E, R>(self: Contract<Input, Output, E, R>) => Effect.Effect<
|
|
320
|
-
<Input, Output, E, R>(self: Contract<Input, Output, E, R>, input: Input, options?: CheckOptions): Effect.Effect<
|
|
335
|
+
<Input>(expect: Expect, input: Input, options?: CheckOptions): <Output, E, R>(self: Contract<Input, Output, E, R>) => Effect.Effect<void, JudgeFailure<E>, Asserted | Services<R>>;
|
|
336
|
+
<Input, Output, E, R>(self: Contract<Input, Output, E, R>, expect: Expect, input: Input, options?: CheckOptions): Effect.Effect<void, JudgeFailure<E>, Asserted | Services<R>>;
|
|
321
337
|
};
|
|
322
338
|
declare namespace RemoteObservation_d_exports {
|
|
323
|
-
export { Options, TraceSource, layer };
|
|
339
|
+
export { Options, TraceSource, layer$1 as layer };
|
|
324
340
|
}
|
|
325
341
|
/**
|
|
326
342
|
* The cadence of the poll, the quiet window the observed span set must hold
|
|
@@ -348,7 +364,7 @@ type TraceSource<R> = (traceId: string) => Effect.Effect<ReadonlyArray<SpanRecor
|
|
|
348
364
|
* the source requires (`Scope.Scope` is supplied by the layer machinery, as
|
|
349
365
|
* every Effect layer constructor scopes it away).
|
|
350
366
|
*/
|
|
351
|
-
declare const layer: {
|
|
367
|
+
declare const layer$1: {
|
|
352
368
|
(options: Options): <R>(source: TraceSource<R>) => Layer.Layer<Observation, never, Exclude<R, Scope.Scope>>;
|
|
353
369
|
<R>(source: TraceSource<R>, options: Options): Layer.Layer<Observation, never, Exclude<R, Scope.Scope>>;
|
|
354
370
|
};
|
|
@@ -370,15 +386,25 @@ declare namespace observation_window_handle_d_exports {
|
|
|
370
386
|
}
|
|
371
387
|
declare const TypeId$2: unique symbol;
|
|
372
388
|
type TypeId$2 = typeof TypeId$2;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
readonly
|
|
378
|
-
readonly [ProviderId]: BasicTracerProvider;
|
|
379
|
-
readonly serviceName: string;
|
|
389
|
+
/**
|
|
390
|
+
* The finished-span storage a window reads back through.
|
|
391
|
+
*/
|
|
392
|
+
interface SpanRecordStore {
|
|
393
|
+
readonly getFinishedSpans: () => ReadonlyArray<ReadableSpan>;
|
|
380
394
|
}
|
|
381
|
-
declare const
|
|
395
|
+
declare const ObservationWindow: Handle.Definition<typeof TypeId$2, {
|
|
396
|
+
readonly serviceName: string;
|
|
397
|
+
}, {
|
|
398
|
+
readonly store: SpanRecordStore;
|
|
399
|
+
readonly provider: BasicTracerProvider;
|
|
400
|
+
}, never>;
|
|
401
|
+
type ObservationWindow = Handle.Of<typeof ObservationWindow>;
|
|
402
|
+
declare const isObservationWindow: (u: unknown) => u is Handle.Handle<typeof TypeId$2, {
|
|
403
|
+
readonly serviceName: string;
|
|
404
|
+
}, {
|
|
405
|
+
readonly store: SpanRecordStore;
|
|
406
|
+
readonly provider: BasicTracerProvider;
|
|
407
|
+
}, never>;
|
|
382
408
|
declare const make$2: (spec: ObservationWindowSpec) => ObservationWindow;
|
|
383
409
|
declare const shutdown: (self: ObservationWindow) => Effect.Effect<void>;
|
|
384
410
|
declare const collect: {
|
|
@@ -386,21 +412,22 @@ declare const collect: {
|
|
|
386
412
|
(self: ObservationWindow, traceId: string): Effect.Effect<ReadonlyArray<SpanRecord>, EmptyObservationError>;
|
|
387
413
|
};
|
|
388
414
|
declare const context: (self: ObservationWindow) => Context.Context<Observation | OtelTracer.OtelTracerProvider>;
|
|
389
|
-
declare namespace
|
|
390
|
-
export { ObservationWindow,
|
|
415
|
+
declare namespace observation_window_blueprint_d_exports {
|
|
416
|
+
export { ObservationWindow, ObservationWindowBlueprint, ObservationWindowSpec, TypeId$2 as ObservationWindowTypeId, TypeId$1 as TypeId, collect, isObservationWindow, make$1 as make };
|
|
391
417
|
}
|
|
392
418
|
declare const TypeId$1: unique symbol;
|
|
393
419
|
type TypeId$1 = typeof TypeId$1;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
readonly scoped:
|
|
398
|
-
readonly layer:
|
|
399
|
-
}
|
|
400
|
-
|
|
420
|
+
declare const scoped: (spec: ObservationWindowSpec) => Effect.Effect<ObservationWindow, never, Scope.Scope>;
|
|
421
|
+
declare const layer: (spec: ObservationWindowSpec) => Layer.Layer<Observation | OtelTracer.OtelTracer>;
|
|
422
|
+
declare const ObservationWindows: Blueprint.Definition<typeof TypeId$1, ObservationWindowSpec, Blueprint.Derived<ObservationWindowSpec, {}, {
|
|
423
|
+
readonly scoped: typeof scoped;
|
|
424
|
+
readonly layer: typeof layer;
|
|
425
|
+
}>, ObservationWindowSpec>;
|
|
426
|
+
type ObservationWindowBlueprint = Blueprint.Of<typeof ObservationWindows>;
|
|
427
|
+
declare const make$1: (serviceName: string) => ObservationWindowBlueprint;
|
|
401
428
|
//#endregion
|
|
402
429
|
//#region src/HarnessFailure.schema.d.ts
|
|
403
|
-
declare const HarnessFailure: Schema.Union<readonly [typeof ContractDecodeError, typeof EmptyObservationError, typeof IncompleteObservationError, typeof TransportObservationError
|
|
430
|
+
declare const HarnessFailure: Schema.Union<readonly [typeof ContractDecodeError, typeof EmptyObservationError, typeof IncompleteObservationError, typeof TransportObservationError]>;
|
|
404
431
|
type HarnessFailure = typeof HarnessFailure.Type;
|
|
405
432
|
//#endregion
|
|
406
433
|
//#region src/StimulusFailure.schema.d.ts
|
|
@@ -410,11 +437,17 @@ declare const StimulusFailure_base: Schema.Class<StimulusFailure, Schema.TaggedS
|
|
|
410
437
|
}>, import("effect/Cause").YieldableError>;
|
|
411
438
|
declare class StimulusFailure extends StimulusFailure_base {}
|
|
412
439
|
declare namespace Suite_d_exports {
|
|
413
|
-
export { ArbitraryInput, CaseFailure, CaseRegistrar, CaseTools, Declared, Harness, Opened, Shared, StimulusFailure, TypeId, make, withLayer, withScenarioLayer };
|
|
440
|
+
export { ArbitraryInput, CaseFailure, CaseRegistrar, CaseTools, Declared, Harness, LiveStage, Opened, Shared, StimulusFailure, TypeId, live, make, withLayer, withScenarioLayer };
|
|
414
441
|
}
|
|
415
442
|
declare const TypeId: unique symbol;
|
|
416
443
|
type TypeId = typeof TypeId;
|
|
417
444
|
type CaseFailure = HarnessFailure | StimulusFailure;
|
|
445
|
+
/**
|
|
446
|
+
* What every case's layers carry around the body: the observation harness and
|
|
447
|
+
* the file system failure dumps are written through. Trace ids draw through
|
|
448
|
+
* the `Random` reference, which the runner salts per case with
|
|
449
|
+
* `Random.withSeed`, so no layer has to provide it.
|
|
450
|
+
*/
|
|
418
451
|
type Harness = Observation | FileSystem.FileSystem;
|
|
419
452
|
type ArbitraryInput<Input> = Schema.Schema<Input> | Arbitrary.Arbitrary<Input>;
|
|
420
453
|
interface CaseRegistrar<Provided> {
|
|
@@ -424,18 +457,26 @@ interface CaseRegistrar<Provided> {
|
|
|
424
457
|
interface CaseTools<Provided> {
|
|
425
458
|
readonly Case: CaseRegistrar<Provided>;
|
|
426
459
|
}
|
|
427
|
-
|
|
460
|
+
/**
|
|
461
|
+
* Every stage that can still reach `body` also declares whether its cases stay
|
|
462
|
+
* on the live clock, and a live declaration carries the reason it stayed there
|
|
463
|
+
* (KTD11). Without one, the stage's cases run on the kernel.
|
|
464
|
+
*/
|
|
465
|
+
interface LiveStage<Self> {
|
|
466
|
+
readonly live: (reason: string) => Self;
|
|
467
|
+
}
|
|
468
|
+
interface Opened<Provided> extends LiveStage<Opened<Provided>> {
|
|
428
469
|
readonly body: (use: (tools: CaseTools<Provided>) => void) => void;
|
|
429
470
|
}
|
|
430
|
-
interface Declared extends Pipeable {
|
|
471
|
+
interface Declared extends Pipeable, LiveStage<Declared> {
|
|
431
472
|
readonly [TypeId]: typeof TypeId;
|
|
432
473
|
readonly withLayer: <SharedProvided>(shared: Layer.Layer<SharedProvided | Harness>) => Shared<SharedProvided>;
|
|
433
474
|
readonly withScenarioLayer: <Provided>(scenario: Layer.Layer<Provided | Harness>) => Opened<Provided | Harness>;
|
|
434
475
|
}
|
|
435
|
-
interface Shared<SharedProvided> extends Pipeable {
|
|
476
|
+
interface Shared<SharedProvided> extends Pipeable, LiveStage<Shared<SharedProvided>> {
|
|
436
477
|
readonly [TypeId]: typeof TypeId;
|
|
437
|
-
readonly withScenarioLayer: <Provided>(scenario: Layer.Layer<Provided | Harness, never, SharedProvided>) => Opened<Provided | SharedProvided>;
|
|
438
|
-
readonly body: (use: (tools: CaseTools<SharedProvided>) => void) => void;
|
|
478
|
+
readonly withScenarioLayer: <Provided>(scenario: Layer.Layer<Provided | Harness, never, SharedProvided>) => Opened<Provided | SharedProvided | Harness>;
|
|
479
|
+
readonly body: (use: (tools: CaseTools<SharedProvided | Harness>) => void) => void;
|
|
439
480
|
}
|
|
440
481
|
declare const withLayer: {
|
|
441
482
|
<SharedProvided>(shared: Layer.Layer<SharedProvided | Harness>): (self: Declared) => Shared<SharedProvided>;
|
|
@@ -445,6 +486,10 @@ declare const withScenarioLayer: {
|
|
|
445
486
|
<Provided>(scenario: Layer.Layer<Provided | Harness>): (self: Declared) => Opened<Provided | Harness>;
|
|
446
487
|
<Provided>(self: Declared, scenario: Layer.Layer<Provided | Harness>): Opened<Provided | Harness>;
|
|
447
488
|
};
|
|
489
|
+
declare const live: {
|
|
490
|
+
(reason: string): <Self>(self: LiveStage<Self>) => Self;
|
|
491
|
+
<Self>(self: LiveStage<Self>, reason: string): Self;
|
|
492
|
+
};
|
|
448
493
|
declare const make: (bindings: Suite.Bindings) => (name: string) => Declared;
|
|
449
494
|
//#endregion
|
|
450
|
-
export { Contract_d_exports as Contract, Graph_d_exports as Graph, Observation_service_d_exports as Observation,
|
|
495
|
+
export { Contract_d_exports as Contract, Graph_d_exports as Graph, Observation_service_d_exports as Observation, observation_window_blueprint_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,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
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";
|
|
2
|
+
import { Array as Array$1, Cause, Clock, Context, Duration, Effect, Encoding, Equal, FileSystem, Layer, Match, Option, Predicate, Random, 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";
|
|
@@ -9,8 +9,9 @@ import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
|
|
|
9
9
|
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
|
|
10
10
|
import * as OtelTracer from "@effect/opentelemetry/OtelTracer";
|
|
11
11
|
import * as OtelResource from "@effect/opentelemetry/Resource";
|
|
12
|
+
import { Blueprint, Handle } from "@systemfsoftware/effect-cell-types";
|
|
12
13
|
import { SpanStatusCode } from "@opentelemetry/api";
|
|
13
|
-
import { AlwaysOnSampler, BasicTracerProvider
|
|
14
|
+
import { AlwaysOnSampler, BasicTracerProvider } from "@opentelemetry/sdk-trace-base";
|
|
14
15
|
import { Suite, TaskRef } from "@systemfsoftware/effect-spec-runtime";
|
|
15
16
|
//#region src/ContractDecodeError.schema.ts
|
|
16
17
|
var ContractDecodeError = class extends Schema.TaggedError()("ContractDecodeError", {
|
|
@@ -46,11 +47,11 @@ const Verdict = Schema.Union([Hold, Break]);
|
|
|
46
47
|
//#endregion
|
|
47
48
|
//#region src/FailureDump.ts
|
|
48
49
|
const DEFAULT_DIRECTORY = "artifacts/traces";
|
|
49
|
-
const isBreak$
|
|
50
|
+
const isBreak$1 = Schema.is(Break);
|
|
50
51
|
/**
|
|
51
52
|
* The rendered evidence a break carries; a held verdict leaves nothing to write.
|
|
52
53
|
*/
|
|
53
|
-
const report = (verdict) => Option.map(Option.liftPredicate(verdict, isBreak$
|
|
54
|
+
const report = (verdict) => Option.map(Option.liftPredicate(verdict, isBreak$1), renderBreach);
|
|
54
55
|
const renderValue = (value) => JSON.stringify(value);
|
|
55
56
|
const renderEntries = (entries) => [...entries].map(([key, value]) => `${key}=${renderValue(value)}`).join(" ");
|
|
56
57
|
const indentOf = (depth) => " ".repeat(depth);
|
|
@@ -247,27 +248,19 @@ var Observation_service_exports = /* @__PURE__ */ __exportAll({
|
|
|
247
248
|
});
|
|
248
249
|
var Observation = class extends Context.Service()("@systemfsoftware/trace-spec/Observation") {};
|
|
249
250
|
//#endregion
|
|
250
|
-
//#region src/TraceDisparityError.schema.ts
|
|
251
|
-
var TraceDisparityError = class extends Schema.TaggedError()("TraceDisparityError", {
|
|
252
|
-
relationId: Schema.String,
|
|
253
|
-
traceId: Schema.String,
|
|
254
|
-
breaks: Schema.Array(Break),
|
|
255
|
-
dumpPath: Schema.NullOr(Schema.String)
|
|
256
|
-
}) {};
|
|
257
|
-
//#endregion
|
|
258
251
|
//#region src/Contract.ts
|
|
259
252
|
var Contract_exports = /* @__PURE__ */ __exportAll({
|
|
260
253
|
ContractDecodeError: () => ContractDecodeError,
|
|
261
254
|
EmptyObservationError: () => EmptyObservationError,
|
|
262
255
|
IncompleteObservationError: () => IncompleteObservationError,
|
|
263
|
-
TraceDisparityError: () => TraceDisparityError,
|
|
264
256
|
TransportObservationError: () => TransportObservationError,
|
|
265
257
|
TypeId: () => TypeId$3,
|
|
266
258
|
check: () => check,
|
|
267
259
|
holds: () => holds,
|
|
268
260
|
judge: () => judge,
|
|
269
261
|
of: () => of,
|
|
270
|
-
stimulate: () => stimulate
|
|
262
|
+
stimulate: () => stimulate,
|
|
263
|
+
verdictCheck: () => verdictCheck
|
|
271
264
|
});
|
|
272
265
|
const TypeId$3 = Symbol.for("@systemfsoftware/trace-spec/Contract");
|
|
273
266
|
const stimulatedOf = (taxonomy, stimulus) => ({
|
|
@@ -320,13 +313,34 @@ const judgeDual = (self, input, options) => Effect.gen(function* () {
|
|
|
320
313
|
};
|
|
321
314
|
});
|
|
322
315
|
const judge = dual((args) => isContract(args[0]), judgeDual);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
316
|
+
/** What a hold answers: nothing to report. */
|
|
317
|
+
const held = {
|
|
318
|
+
verdict: "Hold",
|
|
319
|
+
report: ""
|
|
320
|
+
};
|
|
321
|
+
const reportOf = (relationId, judgment) => Match.value(judgment.verdict).pipe(Match.tag("Hold", () => held), Match.tag("Break", (breach) => ({
|
|
322
|
+
verdict: "Break",
|
|
323
|
+
report: [
|
|
324
|
+
`trace contract broke: ${relationId}`,
|
|
325
|
+
Option.getOrElse(report(breach), () => ""),
|
|
326
|
+
`trace ${judgment.run.traceId}`,
|
|
327
|
+
`dump ${judgment.dumpPath ?? "(not written)"}`
|
|
328
|
+
].join("\n")
|
|
329
|
+
})), Match.exhaustive);
|
|
330
|
+
const verdictCheckImpl = (self, expect, judgment) => {
|
|
331
|
+
const report = reportOf(self.relation.id, judgment);
|
|
332
|
+
return expect(report, report.report).toEqual(held);
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* The one check a judgement makes: it passes only when the relation held, and its message is the report, so a
|
|
336
|
+
* break names the conjunct that broke, the spans it inspected and where the evidence was written.
|
|
337
|
+
*/
|
|
338
|
+
const verdictCheck = dual(3, verdictCheckImpl);
|
|
339
|
+
const checkDual = (self, expect, input, options) => Effect.flatMap(judgeDual(self, input, options), (judgment) => verdictCheckImpl(self, expect, judgment));
|
|
340
|
+
/**
|
|
341
|
+
* The test edge over `judge`: the judgement and its one check. A break fails the check with the report;
|
|
342
|
+
* behaviour failures and infrastructure refusals stay on the error channel.
|
|
343
|
+
*/
|
|
330
344
|
const check = dual((args) => isContract(args[0]), checkDual);
|
|
331
345
|
//#endregion
|
|
332
346
|
//#region src/drivers/tempo-trace.schema.ts
|
|
@@ -544,24 +558,45 @@ Schema.encodeEffect(HexId);
|
|
|
544
558
|
//#endregion
|
|
545
559
|
//#region src/observation-window.handle.ts
|
|
546
560
|
const TypeId$2 = Symbol.for("~systemfsoftware/trace-spec/ObservationWindow");
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
561
|
+
const ObservationWindow = Handle.make()(TypeId$2);
|
|
562
|
+
/**
|
|
563
|
+
* Records every finished span the moment it ends, and answers `shutdown`
|
|
564
|
+
* inline. The OpenTelemetry `InMemorySpanExporter` defers its result callback
|
|
565
|
+
* to a `setTimeout`, a real timer the kernel must fail as an escaped schedule;
|
|
566
|
+
* a window records nothing that waits, so it records synchronously.
|
|
567
|
+
*/
|
|
568
|
+
const spanRecorder = () => {
|
|
569
|
+
const finishedSpans = [];
|
|
570
|
+
let stopped = false;
|
|
571
|
+
return {
|
|
572
|
+
processor: {
|
|
573
|
+
onStart: () => void 0,
|
|
574
|
+
onEnd: (span) => {
|
|
575
|
+
if (!stopped) finishedSpans.push(span);
|
|
576
|
+
},
|
|
577
|
+
shutdown: () => {
|
|
578
|
+
stopped = true;
|
|
579
|
+
finishedSpans.length = 0;
|
|
580
|
+
return Promise.resolve();
|
|
581
|
+
},
|
|
582
|
+
forceFlush: () => Promise.resolve()
|
|
583
|
+
},
|
|
584
|
+
store: { getFinishedSpans: () => finishedSpans }
|
|
585
|
+
};
|
|
586
|
+
};
|
|
587
|
+
const isObservationWindow = ObservationWindow.is;
|
|
550
588
|
const make$3 = (spec) => {
|
|
551
|
-
const
|
|
589
|
+
const { processor, store } = spanRecorder();
|
|
552
590
|
const provider = new BasicTracerProvider({
|
|
553
591
|
sampler: new AlwaysOnSampler(),
|
|
554
|
-
spanProcessors: [
|
|
592
|
+
spanProcessors: [processor]
|
|
593
|
+
});
|
|
594
|
+
return ObservationWindow.make({ serviceName: spec.serviceName }, {
|
|
595
|
+
store,
|
|
596
|
+
provider
|
|
555
597
|
});
|
|
556
|
-
return {
|
|
557
|
-
[TypeId$2]: TypeId$2,
|
|
558
|
-
[ExporterId]: exporter,
|
|
559
|
-
[ProviderId]: provider,
|
|
560
|
-
serviceName: spec.serviceName,
|
|
561
|
-
...Prototype
|
|
562
|
-
};
|
|
563
598
|
};
|
|
564
|
-
const shutdown = (self) => Effect.promise(() => self
|
|
599
|
+
const shutdown = (self) => Effect.promise(() => ObservationWindow.slot(self).provider.shutdown());
|
|
565
600
|
const SCALAR_TYPES = {
|
|
566
601
|
string: true,
|
|
567
602
|
number: true,
|
|
@@ -616,39 +651,36 @@ const emptyObservation = (traceId) => new EmptyObservationError({
|
|
|
616
651
|
traceId,
|
|
617
652
|
detail: "the observation window closed with no span carrying the stimulated trace id"
|
|
618
653
|
});
|
|
619
|
-
const recordsOf = (self, traceId) => self
|
|
654
|
+
const recordsOf = (self, traceId) => ObservationWindow.slot(self).store.getFinishedSpans().filter((span) => span.spanContext().traceId === traceId).map(spanRecordOf);
|
|
620
655
|
const collect = dual(2, (self, traceId) => Effect.suspend(() => {
|
|
621
656
|
const records = recordsOf(self, traceId);
|
|
622
657
|
return records.length > 0 ? Effect.succeed(records) : Effect.fail(emptyObservation(traceId));
|
|
623
658
|
}));
|
|
624
|
-
const context = (self) => Context.make(Observation, { collect: (traceId) => collect(self, traceId) }).pipe(Context.add(OtelTracer.OtelTracerProvider, self
|
|
659
|
+
const context = (self) => Context.make(Observation, { collect: (traceId) => collect(self, traceId) }).pipe(Context.add(OtelTracer.OtelTracerProvider, ObservationWindow.slot(self).provider));
|
|
625
660
|
//#endregion
|
|
626
661
|
//#region src/ObservationWindowSpec.schema.ts
|
|
627
662
|
var ObservationWindowSpec = class extends Schema.Class("ObservationWindowSpec")({ serviceName: Schema.NonEmptyString }) {};
|
|
628
663
|
//#endregion
|
|
629
|
-
//#region src/observation-window.
|
|
630
|
-
var
|
|
664
|
+
//#region src/observation-window.blueprint.ts
|
|
665
|
+
var observation_window_blueprint_exports = /* @__PURE__ */ __exportAll({
|
|
631
666
|
ObservationWindowSpec: () => ObservationWindowSpec,
|
|
632
667
|
ObservationWindowTypeId: () => TypeId$2,
|
|
668
|
+
TypeId: () => TypeId$1,
|
|
633
669
|
collect: () => collect,
|
|
634
670
|
isObservationWindow: () => isObservationWindow,
|
|
635
671
|
make: () => make$2
|
|
636
672
|
});
|
|
637
|
-
const TypeId$1 = Symbol.for("~systemfsoftware/trace-spec/
|
|
673
|
+
const TypeId$1 = Symbol.for("~systemfsoftware/trace-spec/ObservationWindowBlueprint");
|
|
638
674
|
const scoped = (spec) => Effect.acquireRelease(Effect.sync(() => make$3(spec)), shutdown);
|
|
639
675
|
const layer$1 = (spec) => OtelTracer.layer.pipe(Layer.provideMerge(OtelResource.layer({ serviceName: spec.serviceName })), Layer.provideMerge(Layer.effectContext(Effect.map(scoped(spec), context))));
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
}
|
|
646
|
-
get layer() {
|
|
647
|
-
return layer$1(spec);
|
|
648
|
-
},
|
|
649
|
-
...Prototype
|
|
676
|
+
const ObservationWindows = Blueprint.make()(TypeId$1).steps({
|
|
677
|
+
steps: {},
|
|
678
|
+
targets: {
|
|
679
|
+
scoped,
|
|
680
|
+
layer: layer$1
|
|
681
|
+
}
|
|
650
682
|
});
|
|
651
|
-
const make$2 = (serviceName) =>
|
|
683
|
+
const make$2 = (serviceName) => ObservationWindows.of(new ObservationWindowSpec({ serviceName }));
|
|
652
684
|
//#endregion
|
|
653
685
|
//#region src/Rel.ts
|
|
654
686
|
var Rel_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -681,7 +713,7 @@ const relation = (parts) => Object.assign((graph) => parts.evaluate(graph), {
|
|
|
681
713
|
const matchedNodes = (graph, spec) => byId(graph, spec);
|
|
682
714
|
const inspectedOf = (nodes) => nodes.map((node) => node.spanId);
|
|
683
715
|
const matchedAll = (items, predicate) => items.length > 0 && items.every(predicate);
|
|
684
|
-
const isBreak
|
|
716
|
+
const isBreak = (verdict) => Schema.is(Break)(verdict);
|
|
685
717
|
const isHold$1 = (verdict) => Schema.is(Hold)(verdict);
|
|
686
718
|
const verdictOf$1 = (options) => options.holds ? Hold.make({
|
|
687
719
|
conjunct: options.id,
|
|
@@ -691,7 +723,7 @@ const verdictOf$1 = (options) => options.holds ? Hold.make({
|
|
|
691
723
|
inspected: options.inspected,
|
|
692
724
|
detail: options.detail
|
|
693
725
|
});
|
|
694
|
-
const declared = (declaration) => relation({
|
|
726
|
+
const declared$1 = (declaration) => relation({
|
|
695
727
|
id: declaration.id,
|
|
696
728
|
soft: false,
|
|
697
729
|
softenable: declaration.softenable,
|
|
@@ -707,21 +739,21 @@ const declared = (declaration) => relation({
|
|
|
707
739
|
});
|
|
708
740
|
const holdsCountOf = (spec, expected) => (graph) => expected(matchedNodes(graph, spec).length);
|
|
709
741
|
const holdsEveryOf = (spec, predicate) => (graph) => matchedAll(matchedNodes(graph, spec), predicate);
|
|
710
|
-
const exists = (spec) => declared({
|
|
742
|
+
const exists = (spec) => declared$1({
|
|
711
743
|
id: `exists(${spec.id})`,
|
|
712
744
|
spans: [spec],
|
|
713
745
|
softenable: false,
|
|
714
746
|
detail: `no ${spec.id} span was emitted`,
|
|
715
747
|
holds: holdsCountOf(spec, (count) => count > 0)
|
|
716
748
|
});
|
|
717
|
-
const absent = (spec) => declared({
|
|
749
|
+
const absent = (spec) => declared$1({
|
|
718
750
|
id: `absent(${spec.id})`,
|
|
719
751
|
spans: [spec],
|
|
720
752
|
softenable: false,
|
|
721
753
|
detail: `a ${spec.id} span was emitted`,
|
|
722
754
|
holds: holdsCountOf(spec, (count) => count === 0)
|
|
723
755
|
});
|
|
724
|
-
const unique = (spec) => declared({
|
|
756
|
+
const unique = (spec) => declared$1({
|
|
725
757
|
id: `unique(${spec.id})`,
|
|
726
758
|
spans: [spec],
|
|
727
759
|
softenable: true,
|
|
@@ -736,7 +768,7 @@ const reachedUnder = (graph, parent, childSpan, reach) => {
|
|
|
736
768
|
const reached = reach(graph, matchedNodes(graph, parent));
|
|
737
769
|
return matchedNodes(graph, childSpan).some((node) => reached.has(node.spanId));
|
|
738
770
|
};
|
|
739
|
-
const childImpl = (parent, childSpan) => declared({
|
|
771
|
+
const childImpl = (parent, childSpan) => declared$1({
|
|
740
772
|
id: `child(${parent.id},${childSpan.id})`,
|
|
741
773
|
spans: [parent, childSpan],
|
|
742
774
|
softenable: true,
|
|
@@ -744,7 +776,7 @@ const childImpl = (parent, childSpan) => declared({
|
|
|
744
776
|
holds: (graph) => reachedUnder(graph, parent, childSpan, childIdsOf)
|
|
745
777
|
});
|
|
746
778
|
const child = dual(2, childImpl);
|
|
747
|
-
const descendantImpl = (parent, childSpan) => declared({
|
|
779
|
+
const descendantImpl = (parent, childSpan) => declared$1({
|
|
748
780
|
id: `descendant(${parent.id},${childSpan.id})`,
|
|
749
781
|
spans: [parent, childSpan],
|
|
750
782
|
softenable: true,
|
|
@@ -752,7 +784,7 @@ const descendantImpl = (parent, childSpan) => declared({
|
|
|
752
784
|
holds: (graph) => reachedUnder(graph, parent, childSpan, descendantIdsOf)
|
|
753
785
|
});
|
|
754
786
|
const descendant = dual(2, descendantImpl);
|
|
755
|
-
const orderImpl = (before, after) => declared({
|
|
787
|
+
const orderImpl = (before, after) => declared$1({
|
|
756
788
|
id: `order(${before.id},${after.id})`,
|
|
757
789
|
spans: [before, after],
|
|
758
790
|
softenable: true,
|
|
@@ -760,7 +792,7 @@ const orderImpl = (before, after) => declared({
|
|
|
760
792
|
holds: (graph) => matchedAll(matchedNodes(graph, after), (node) => startsAfterSome(matchedNodes(graph, before), node))
|
|
761
793
|
});
|
|
762
794
|
const order = dual(2, orderImpl);
|
|
763
|
-
const statusImpl = (spec, expected) => declared({
|
|
795
|
+
const statusImpl = (spec, expected) => declared$1({
|
|
764
796
|
id: `status(${spec.id},${expected})`,
|
|
765
797
|
spans: [spec],
|
|
766
798
|
softenable: true,
|
|
@@ -768,7 +800,7 @@ const statusImpl = (spec, expected) => declared({
|
|
|
768
800
|
holds: holdsEveryOf(spec, (node) => node.status === expected)
|
|
769
801
|
});
|
|
770
802
|
const status = dual(2, statusImpl);
|
|
771
|
-
const errorTypeImpl = (spec, expected) => declared({
|
|
803
|
+
const errorTypeImpl = (spec, expected) => declared$1({
|
|
772
804
|
id: `errorType(${spec.id},${expected})`,
|
|
773
805
|
spans: [spec],
|
|
774
806
|
softenable: true,
|
|
@@ -777,7 +809,7 @@ const errorTypeImpl = (spec, expected) => declared({
|
|
|
777
809
|
});
|
|
778
810
|
const errorType = dual(2, errorTypeImpl);
|
|
779
811
|
const partialMatches = (attributes, partial) => Object.entries(partial).filter(([, value]) => value !== void 0).every(([key, value]) => Equal.equals(attributes[key], value));
|
|
780
|
-
const attrsImpl = (spec, partial) => declared({
|
|
812
|
+
const attrsImpl = (spec, partial) => declared$1({
|
|
781
813
|
id: `attrs(${spec.id})`,
|
|
782
814
|
spans: [spec],
|
|
783
815
|
softenable: true,
|
|
@@ -785,7 +817,7 @@ const attrsImpl = (spec, partial) => declared({
|
|
|
785
817
|
holds: holdsEveryOf(spec, (node) => partialMatches(node.attrs, partial))
|
|
786
818
|
});
|
|
787
819
|
const attrs = dual(2, attrsImpl);
|
|
788
|
-
const durationLessThanImpl = (spec, millis) => declared({
|
|
820
|
+
const durationLessThanImpl = (spec, millis) => declared$1({
|
|
789
821
|
id: `durationLessThan(${spec.id},${millis})`,
|
|
790
822
|
spans: [spec],
|
|
791
823
|
softenable: true,
|
|
@@ -794,7 +826,7 @@ const durationLessThanImpl = (spec, millis) => declared({
|
|
|
794
826
|
});
|
|
795
827
|
const durationLessThan = dual(2, durationLessThanImpl);
|
|
796
828
|
const carriesEvent = (name) => (node) => node.events.some((event) => event.name === name);
|
|
797
|
-
const eventImpl = (spec, name) => declared({
|
|
829
|
+
const eventImpl = (spec, name) => declared$1({
|
|
798
830
|
id: `event(${spec.id},${name})`,
|
|
799
831
|
spans: [spec],
|
|
800
832
|
softenable: true,
|
|
@@ -854,8 +886,8 @@ const collectSoft = (id, breaks) => {
|
|
|
854
886
|
detail: breaks.map((breach) => breach.conjunct).join(", ")
|
|
855
887
|
});
|
|
856
888
|
};
|
|
857
|
-
const firstHardBreak = (relations, graph) => relations.filter((relation) => !relation.soft).map((relation) => relation(graph)).find(isBreak
|
|
858
|
-
const softBreaks = (relations, graph) => relations.filter((relation) => relation.soft).map((relation) => relation(graph)).filter(isBreak
|
|
889
|
+
const firstHardBreak = (relations, graph) => relations.filter((relation) => !relation.soft).map((relation) => relation(graph)).find(isBreak) ?? null;
|
|
890
|
+
const softBreaks = (relations, graph) => relations.filter((relation) => relation.soft).map((relation) => relation(graph)).filter(isBreak);
|
|
859
891
|
const combined = (options) => relation({
|
|
860
892
|
id: options.id,
|
|
861
893
|
soft: options.soft,
|
|
@@ -1043,10 +1075,11 @@ var Stimulus_exports = /* @__PURE__ */ __exportAll({
|
|
|
1043
1075
|
traceContext: () => traceContext
|
|
1044
1076
|
});
|
|
1045
1077
|
const byteHex = (byte) => byte.toString(16).padStart(2, "0");
|
|
1046
|
-
const
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
const
|
|
1078
|
+
const randomByte = Random.nextIntBetween(0, 255);
|
|
1079
|
+
const randomHex = (byteLength) => Effect.map(Effect.all(Array.from({ length: byteLength }, () => randomByte)), (bytes) => bytes.map(byteHex).join(""));
|
|
1080
|
+
const traceContext = Effect.gen(function* () {
|
|
1081
|
+
const traceId = yield* randomHex(16);
|
|
1082
|
+
const spanId = yield* randomHex(8);
|
|
1050
1083
|
return {
|
|
1051
1084
|
traceId,
|
|
1052
1085
|
spanId,
|
|
@@ -1080,38 +1113,16 @@ const HarnessFailure = Schema.Union([
|
|
|
1080
1113
|
ContractDecodeError,
|
|
1081
1114
|
EmptyObservationError,
|
|
1082
1115
|
IncompleteObservationError,
|
|
1083
|
-
TransportObservationError
|
|
1084
|
-
TraceDisparityError
|
|
1116
|
+
TransportObservationError
|
|
1085
1117
|
]);
|
|
1086
1118
|
//#endregion
|
|
1087
|
-
//#region src/Prop.ts
|
|
1088
|
-
const isHold = Schema.is(Hold);
|
|
1089
|
-
const isBreak = Schema.is(Break);
|
|
1090
|
-
const annotate = (context, message) => Option.match(Option.flatMap(Option.fromNullishOr(context), (task) => Option.fromNullishOr(task.annotate)), {
|
|
1091
|
-
onNone: () => Effect.void,
|
|
1092
|
-
onSome: (record) => Effect.promise(() => Promise.resolve(record(message, "info")))
|
|
1093
|
-
});
|
|
1094
|
-
const dumpMessageOf = (verdict, dumpPath) => Option.map(Option.flatMap(Option.liftPredicate(verdict, isBreak), () => Option.fromNullishOr(dumpPath)), (path) => `trace contract failed; observed graph dumped to ${path}`);
|
|
1095
|
-
const announce = (context, verdict, dumpPath) => Option.match(dumpMessageOf(verdict, dumpPath), {
|
|
1096
|
-
onNone: () => Effect.void,
|
|
1097
|
-
onSome: (message) => annotate(context, message)
|
|
1098
|
-
});
|
|
1099
|
-
const predicateImpl = (title, contract, scenario) => {
|
|
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)));
|
|
1101
|
-
return (input, context) => checked(input, context);
|
|
1102
|
-
};
|
|
1103
|
-
const predicate = dual(3, predicateImpl);
|
|
1104
|
-
//#endregion
|
|
1105
|
-
//#region src/StimulusFailure.schema.ts
|
|
1106
|
-
var StimulusFailure = class extends Schema.TaggedError()("StimulusFailure", {
|
|
1107
|
-
stimulus: Schema.String,
|
|
1108
|
-
detail: Schema.String
|
|
1109
|
-
}) {};
|
|
1110
|
-
//#endregion
|
|
1111
1119
|
//#region src/TaskAnnounce.ts
|
|
1120
|
+
const forget = (annotation) => {
|
|
1121
|
+
Promise.resolve(annotation);
|
|
1122
|
+
};
|
|
1112
1123
|
const dispatchAnnotate = (annotate, message, type) => Option.match(Option.fromNullishOr(annotate), {
|
|
1113
1124
|
onNone: () => Effect.void,
|
|
1114
|
-
onSome: (record) => Effect.
|
|
1125
|
+
onSome: (record) => Effect.sync(() => forget(record(message, type)))
|
|
1115
1126
|
});
|
|
1116
1127
|
const announcement = (taskContext, message, type) => Option.match(Option.fromNullishOr(taskContext), {
|
|
1117
1128
|
onNone: () => Effect.void,
|
|
@@ -1121,64 +1132,121 @@ const record = (message, type) => Effect.gen(function* () {
|
|
|
1121
1132
|
const taskContext = yield* TaskRef.RawVitestTaskRef;
|
|
1122
1133
|
return yield* announcement(taskContext, message, type);
|
|
1123
1134
|
});
|
|
1124
|
-
const
|
|
1135
|
+
const dumpMessageOf = (judged) => Option.map(Option.flatMap(Option.liftPredicate(judged.verdict, Schema.is(Break)), () => Option.fromNullishOr(judged.dumpPath)), (dumpPath) => `trace contract failed; observed graph dumped to ${dumpPath}`);
|
|
1136
|
+
const announceDump = (judged) => Option.match(dumpMessageOf(judged), {
|
|
1125
1137
|
onNone: () => Effect.void,
|
|
1126
|
-
onSome: (
|
|
1138
|
+
onSome: (message) => record(message, "info")
|
|
1139
|
+
});
|
|
1140
|
+
//#endregion
|
|
1141
|
+
//#region src/Prop.ts
|
|
1142
|
+
const isHold = Schema.is(Hold);
|
|
1143
|
+
/**
|
|
1144
|
+
* The contract with its stimulus replaced by the function the property was handed: the impostor gate
|
|
1145
|
+
* swaps in a constant fake, so the judgement must reach the stimulus through the argument, never the
|
|
1146
|
+
* closure.
|
|
1147
|
+
*/
|
|
1148
|
+
const judgedOver = (contract, subject) => ({
|
|
1149
|
+
...contract,
|
|
1150
|
+
stimulus: subject
|
|
1127
1151
|
});
|
|
1152
|
+
/**
|
|
1153
|
+
* The run must be the run of the generated input: a stimulus that ignores its input and answers with one
|
|
1154
|
+
* frozen run satisfies every relation about that run, so the input pin is what refutes it.
|
|
1155
|
+
*/
|
|
1156
|
+
const consumedInput = (judgment, input) => judgment.run.input === input;
|
|
1157
|
+
const predicateImpl = (title, contract, scenario, shared) => {
|
|
1158
|
+
const provided = scenario.pipe(Layer.provideMerge(shared));
|
|
1159
|
+
const checked = (subject, input) => judge(judgedOver(contract, subject), input, { dumpName: title }).pipe(Effect.tap(announceDump), Effect.map((judgment) => isHold(judgment.verdict) && consumedInput(judgment, input)), Effect.provide(Layer.fresh(provided)));
|
|
1160
|
+
return (subject, input) => checked(subject, input);
|
|
1161
|
+
};
|
|
1162
|
+
const predicate = dual(4, predicateImpl);
|
|
1163
|
+
//#endregion
|
|
1164
|
+
//#region src/StimulusFailure.schema.ts
|
|
1165
|
+
var StimulusFailure = class extends Schema.TaggedError()("StimulusFailure", {
|
|
1166
|
+
stimulus: Schema.String,
|
|
1167
|
+
detail: Schema.String
|
|
1168
|
+
}) {};
|
|
1128
1169
|
//#endregion
|
|
1129
1170
|
//#region src/Suite.ts
|
|
1130
1171
|
var Suite_exports = /* @__PURE__ */ __exportAll({
|
|
1131
1172
|
StimulusFailure: () => StimulusFailure,
|
|
1132
1173
|
TypeId: () => TypeId,
|
|
1174
|
+
live: () => live,
|
|
1133
1175
|
make: () => make,
|
|
1134
1176
|
withLayer: () => withLayer,
|
|
1135
1177
|
withScenarioLayer: () => withScenarioLayer
|
|
1136
1178
|
});
|
|
1137
1179
|
const TypeId = Symbol.for("@systemfsoftware/trace-spec/Suite");
|
|
1138
1180
|
const isHarnessFailure = Schema.is(HarnessFailure);
|
|
1139
|
-
const
|
|
1181
|
+
const SALT_BYTES = 8;
|
|
1182
|
+
/**
|
|
1183
|
+
* Per-case salt for globally unique W3C trace ids: drawn here, at case
|
|
1184
|
+
* registration and outside any kernel run, so a seeded Random inside the run
|
|
1185
|
+
* never replays another case's ids and the raw crypto source never enters an
|
|
1186
|
+
* explored body.
|
|
1187
|
+
*/
|
|
1188
|
+
const saltOf = () => {
|
|
1189
|
+
const bytes = crypto.getRandomValues(new Uint8Array(SALT_BYTES));
|
|
1190
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1191
|
+
};
|
|
1192
|
+
const caseFailureOf = (stimulus, salt) => (failure) => isHarnessFailure(failure) ? failure : new StimulusFailure({
|
|
1140
1193
|
stimulus,
|
|
1141
|
-
detail: Cause.pretty(Cause.fail(failure))
|
|
1194
|
+
detail: `${Cause.pretty(Cause.fail(failure))}\nrandom salt: ${salt}`
|
|
1142
1195
|
});
|
|
1143
|
-
const
|
|
1144
|
-
const
|
|
1145
|
-
const
|
|
1146
|
-
const
|
|
1147
|
-
|
|
1196
|
+
const caseBody = (expect, contract, input, salt) => judge(contract, input).pipe(Effect.tap(announceDump), Effect.flatMap((judgment) => verdictCheck(contract, expect, judgment)), Effect.mapError(caseFailureOf(contract.stimulus.name, salt)));
|
|
1197
|
+
const caseTools = (register, propIt, scenario, shared) => {
|
|
1198
|
+
const Case = (name, contract, input) => {
|
|
1199
|
+
const salt = saltOf();
|
|
1200
|
+
register(name, (expect) => caseBody(expect, contract, input, salt).pipe(Random.withSeed(salt)), "run");
|
|
1201
|
+
};
|
|
1148
1202
|
Case.prop = (name, contract, arbitrary) => {
|
|
1149
|
-
|
|
1203
|
+
const salt = saltOf();
|
|
1204
|
+
propIt.effect.prop(name, {
|
|
1205
|
+
of: [arbitrary],
|
|
1206
|
+
subject: contract.stimulus
|
|
1207
|
+
}, (subject, values) => predicate(name, contract, scenario, shared)(subject, values[0]).pipe(Random.withSeed(salt)));
|
|
1150
1208
|
};
|
|
1151
1209
|
return { Case };
|
|
1152
1210
|
};
|
|
1153
|
-
const config = (name) =>
|
|
1211
|
+
const config = (name, live) => live === void 0 ? {
|
|
1212
|
+
name,
|
|
1213
|
+
describe: "describe",
|
|
1214
|
+
options: void 0
|
|
1215
|
+
} : {
|
|
1154
1216
|
name,
|
|
1155
1217
|
describe: "describe",
|
|
1156
1218
|
options: void 0,
|
|
1157
|
-
|
|
1158
|
-
}
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1219
|
+
live
|
|
1220
|
+
};
|
|
1221
|
+
const openedScenario = (bindings, name, live, scenario) => ({
|
|
1222
|
+
live: (reason) => openedScenario(bindings, name, { reason }, scenario),
|
|
1223
|
+
body: (use) => Suite.openCase(config(name, live), scenario, (register, propIt) => use(caseTools(register, propIt, scenario, Layer.empty)))(bindings)
|
|
1224
|
+
});
|
|
1225
|
+
const openedSharedScenario = (bindings, name, live, shared, scenario) => ({
|
|
1226
|
+
live: (reason) => openedSharedScenario(bindings, name, { reason }, shared, scenario),
|
|
1227
|
+
body: (use) => Suite.openSharedCase(config(name, live), { layer: shared }, scenario, (register, propIt) => use(caseTools(register, propIt, scenario.pipe(Layer.provideMerge(shared)), shared)))(bindings)
|
|
1228
|
+
});
|
|
1229
|
+
const registerSharedCases = (bindings, name, live, shared, use) => Suite.openShared(config(name, live), { layer: shared }, (register, propIt) => use(caseTools(register, propIt, shared, shared)))(bindings);
|
|
1230
|
+
const sharedOver = (bindings, name, live, shared) => ({
|
|
1231
|
+
[TypeId]: TypeId,
|
|
1232
|
+
live: (reason) => sharedOver(bindings, name, { reason }, shared),
|
|
1233
|
+
withScenarioLayer: (scenario) => openedSharedScenario(bindings, name, live, shared, scenario),
|
|
1234
|
+
body: (use) => registerSharedCases(bindings, name, live, shared, use),
|
|
1235
|
+
...Prototype
|
|
1236
|
+
});
|
|
1237
|
+
const declared = (bindings, name, live) => ({
|
|
1165
1238
|
[TypeId]: TypeId,
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
}, (register, propIt) => use(caseTools(register, propIt, shared))),
|
|
1170
|
-
withScenarioLayer: (scenario) => openSharedScenario(bindings, name, shared, scenario),
|
|
1239
|
+
live: (reason) => declared(bindings, name, { reason }),
|
|
1240
|
+
withLayer: (shared) => sharedOver(bindings, name, live, shared),
|
|
1241
|
+
withScenarioLayer: (scenario) => openedScenario(bindings, name, live, scenario),
|
|
1171
1242
|
...Prototype
|
|
1172
1243
|
});
|
|
1173
1244
|
const withLayerDual = (self, shared) => self.withLayer(shared);
|
|
1174
1245
|
const withLayer = dual(2, withLayerDual);
|
|
1175
1246
|
const withScenarioLayerDual = (self, scenario) => self.withScenarioLayer(scenario);
|
|
1176
1247
|
const withScenarioLayer = dual(2, withScenarioLayerDual);
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
withScenarioLayer: (scenario) => openScenario(bindings, name, scenario),
|
|
1181
|
-
...Prototype
|
|
1182
|
-
});
|
|
1248
|
+
const liveDual = (self, reason) => self.live(reason);
|
|
1249
|
+
const live = dual(2, liveDual);
|
|
1250
|
+
const make = (bindings) => (name) => declared(bindings, name, void 0);
|
|
1183
1251
|
//#endregion
|
|
1184
|
-
export { Contract_exports as Contract, Graph_exports as Graph, Observation_service_exports as Observation,
|
|
1252
|
+
export { Contract_exports as Contract, Graph_exports as Graph, Observation_service_exports as Observation, observation_window_blueprint_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.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"author": "Ryan Lee <drdgvhbh@gmail.com>",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -38,17 +38,19 @@
|
|
|
38
38
|
"@opentelemetry/resources": "^2",
|
|
39
39
|
"@opentelemetry/sdk-trace-base": "^2",
|
|
40
40
|
"@opentelemetry/semantic-conventions": "^1.33",
|
|
41
|
-
"@systemfsoftware/effect-
|
|
41
|
+
"@systemfsoftware/effect-cell-types": "^10.2.0",
|
|
42
|
+
"@systemfsoftware/effect-spec-runtime": "^0.3.0",
|
|
42
43
|
"@systemfsoftware/trace-taxonomy": "^0.0.1"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"@effect/platform-node": "4.0.0-rc.117",
|
|
46
|
-
"@effect/vitest": "4.0.0-rc.117",
|
|
47
47
|
"@systemfsoftware/arethetypeswrong-cli": "^4.2.0",
|
|
48
|
-
"@systemfsoftware/
|
|
49
|
-
"@systemfsoftware/
|
|
48
|
+
"@systemfsoftware/conformance-spec": "^0.1.0",
|
|
49
|
+
"@systemfsoftware/effect-gherkin-spec": "^5.0.0",
|
|
50
|
+
"@systemfsoftware/oxlint-config-recommended": "^3.1.0",
|
|
50
51
|
"@systemfsoftware/tsconfig": "^2.0.1",
|
|
51
52
|
"@systemfsoftware/tsdown-config": "^0.1.0",
|
|
53
|
+
"@systemfsoftware/vitest": "^0.1.0",
|
|
52
54
|
"@systemfsoftware/vitest-config": "^0.1.0",
|
|
53
55
|
"@types/node": "^26",
|
|
54
56
|
"effect": "4.0.0-rc.117",
|
|
@@ -59,7 +61,7 @@
|
|
|
59
61
|
"vitest": "^5"
|
|
60
62
|
},
|
|
61
63
|
"peerDependencies": {
|
|
62
|
-
"@
|
|
64
|
+
"@systemfsoftware/vitest": "^0.1.0",
|
|
63
65
|
"effect": "4.0.0-rc.117",
|
|
64
66
|
"vitest": "^5"
|
|
65
67
|
},
|