@systemfsoftware/trace-spec 0.3.0 → 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 +11 -0
- package/README.md +19 -8
- package/dist/index.d.ts +62 -23
- package/dist/index.mjs +161 -108
- package/package.json +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
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
|
+
|
|
3
14
|
## 0.3.0
|
|
4
15
|
|
|
5
16
|
### 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 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
|
@@ -4,8 +4,9 @@ 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
6
|
import { Blueprint, Handle } from "@systemfsoftware/effect-cell-types";
|
|
7
|
-
import { BasicTracerProvider,
|
|
7
|
+
import { BasicTracerProvider, ReadableSpan } from "@opentelemetry/sdk-trace-base";
|
|
8
8
|
import { Suite } from "@systemfsoftware/effect-spec-runtime";
|
|
9
|
+
import { Asserted, Check, Expect } from "@systemfsoftware/vitest";
|
|
9
10
|
import * as Scope from "effect/Scope";
|
|
10
11
|
import * as Arbitrary from "effect/unstable/arbitrary/Arbitrary";
|
|
11
12
|
//#region src/ContractDecodeError.schema.d.ts
|
|
@@ -261,17 +262,8 @@ declare const make$3: <Input, Output, E, R>(options: {
|
|
|
261
262
|
readonly traceparent: string;
|
|
262
263
|
}) => Effect.Effect<Output, E, R>;
|
|
263
264
|
}) => Stimulus<Input, Output, E, R>;
|
|
264
|
-
//#endregion
|
|
265
|
-
//#region src/TraceDisparityError.schema.d.ts
|
|
266
|
-
declare const TraceDisparityError_base: Schema.Class<TraceDisparityError, Schema.TaggedStruct<"TraceDisparityError", {
|
|
267
|
-
readonly relationId: Schema.String;
|
|
268
|
-
readonly traceId: Schema.String;
|
|
269
|
-
readonly breaks: Schema.$Array<typeof Break>;
|
|
270
|
-
readonly dumpPath: Schema.NullOr<Schema.String>;
|
|
271
|
-
}>, import("effect/Cause").YieldableError>;
|
|
272
|
-
declare class TraceDisparityError extends TraceDisparityError_base {}
|
|
273
265
|
declare namespace Contract_d_exports {
|
|
274
|
-
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 };
|
|
275
267
|
}
|
|
276
268
|
declare const TypeId$3: unique symbol;
|
|
277
269
|
type TypeId$3 = typeof TypeId$3;
|
|
@@ -310,15 +302,38 @@ interface CheckOptions {
|
|
|
310
302
|
readonly dumpName?: string | undefined;
|
|
311
303
|
}
|
|
312
304
|
type JudgeFailure<E> = E | ContractDecodeError | ObservationFailure;
|
|
313
|
-
type CheckFailure<E> = JudgeFailure<E> | TraceDisparityError;
|
|
314
305
|
type Services<R> = R | Observation | FileSystem.FileSystem;
|
|
315
306
|
declare const judge: {
|
|
316
307
|
<Input>(input: Input, options?: CheckOptions): <Output, E, R>(self: Contract<Input, Output, E, R>) => Effect.Effect<Judgment<Input, Output>, JudgeFailure<E>, Services<R>>;
|
|
317
308
|
<Input, Output, E, R>(self: Contract<Input, Output, E, R>, input: Input, options?: CheckOptions): Effect.Effect<Judgment<Input, Output>, JudgeFailure<E>, Services<R>>;
|
|
318
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
|
+
*/
|
|
319
334
|
declare const check: {
|
|
320
|
-
<Input>(input: Input, options?: CheckOptions): <Output, E, R>(self: Contract<Input, Output, E, R>) => Effect.Effect<
|
|
321
|
-
<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>>;
|
|
322
337
|
};
|
|
323
338
|
declare namespace RemoteObservation_d_exports {
|
|
324
339
|
export { Options, TraceSource, layer$1 as layer };
|
|
@@ -371,17 +386,23 @@ declare namespace observation_window_handle_d_exports {
|
|
|
371
386
|
}
|
|
372
387
|
declare const TypeId$2: unique symbol;
|
|
373
388
|
type TypeId$2 = typeof TypeId$2;
|
|
389
|
+
/**
|
|
390
|
+
* The finished-span storage a window reads back through.
|
|
391
|
+
*/
|
|
392
|
+
interface SpanRecordStore {
|
|
393
|
+
readonly getFinishedSpans: () => ReadonlyArray<ReadableSpan>;
|
|
394
|
+
}
|
|
374
395
|
declare const ObservationWindow: Handle.Definition<typeof TypeId$2, {
|
|
375
396
|
readonly serviceName: string;
|
|
376
397
|
}, {
|
|
377
|
-
readonly
|
|
398
|
+
readonly store: SpanRecordStore;
|
|
378
399
|
readonly provider: BasicTracerProvider;
|
|
379
400
|
}, never>;
|
|
380
401
|
type ObservationWindow = Handle.Of<typeof ObservationWindow>;
|
|
381
402
|
declare const isObservationWindow: (u: unknown) => u is Handle.Handle<typeof TypeId$2, {
|
|
382
403
|
readonly serviceName: string;
|
|
383
404
|
}, {
|
|
384
|
-
readonly
|
|
405
|
+
readonly store: SpanRecordStore;
|
|
385
406
|
readonly provider: BasicTracerProvider;
|
|
386
407
|
}, never>;
|
|
387
408
|
declare const make$2: (spec: ObservationWindowSpec) => ObservationWindow;
|
|
@@ -406,7 +427,7 @@ type ObservationWindowBlueprint = Blueprint.Of<typeof ObservationWindows>;
|
|
|
406
427
|
declare const make$1: (serviceName: string) => ObservationWindowBlueprint;
|
|
407
428
|
//#endregion
|
|
408
429
|
//#region src/HarnessFailure.schema.d.ts
|
|
409
|
-
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]>;
|
|
410
431
|
type HarnessFailure = typeof HarnessFailure.Type;
|
|
411
432
|
//#endregion
|
|
412
433
|
//#region src/StimulusFailure.schema.d.ts
|
|
@@ -416,11 +437,17 @@ declare const StimulusFailure_base: Schema.Class<StimulusFailure, Schema.TaggedS
|
|
|
416
437
|
}>, import("effect/Cause").YieldableError>;
|
|
417
438
|
declare class StimulusFailure extends StimulusFailure_base {}
|
|
418
439
|
declare namespace Suite_d_exports {
|
|
419
|
-
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 };
|
|
420
441
|
}
|
|
421
442
|
declare const TypeId: unique symbol;
|
|
422
443
|
type TypeId = typeof TypeId;
|
|
423
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
|
+
*/
|
|
424
451
|
type Harness = Observation | FileSystem.FileSystem;
|
|
425
452
|
type ArbitraryInput<Input> = Schema.Schema<Input> | Arbitrary.Arbitrary<Input>;
|
|
426
453
|
interface CaseRegistrar<Provided> {
|
|
@@ -430,18 +457,26 @@ interface CaseRegistrar<Provided> {
|
|
|
430
457
|
interface CaseTools<Provided> {
|
|
431
458
|
readonly Case: CaseRegistrar<Provided>;
|
|
432
459
|
}
|
|
433
|
-
|
|
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>> {
|
|
434
469
|
readonly body: (use: (tools: CaseTools<Provided>) => void) => void;
|
|
435
470
|
}
|
|
436
|
-
interface Declared extends Pipeable {
|
|
471
|
+
interface Declared extends Pipeable, LiveStage<Declared> {
|
|
437
472
|
readonly [TypeId]: typeof TypeId;
|
|
438
473
|
readonly withLayer: <SharedProvided>(shared: Layer.Layer<SharedProvided | Harness>) => Shared<SharedProvided>;
|
|
439
474
|
readonly withScenarioLayer: <Provided>(scenario: Layer.Layer<Provided | Harness>) => Opened<Provided | Harness>;
|
|
440
475
|
}
|
|
441
|
-
interface Shared<SharedProvided> extends Pipeable {
|
|
476
|
+
interface Shared<SharedProvided> extends Pipeable, LiveStage<Shared<SharedProvided>> {
|
|
442
477
|
readonly [TypeId]: typeof TypeId;
|
|
443
|
-
readonly withScenarioLayer: <Provided>(scenario: Layer.Layer<Provided | Harness, never, SharedProvided>) => Opened<Provided | SharedProvided>;
|
|
444
|
-
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;
|
|
445
480
|
}
|
|
446
481
|
declare const withLayer: {
|
|
447
482
|
<SharedProvided>(shared: Layer.Layer<SharedProvided | Harness>): (self: Declared) => Shared<SharedProvided>;
|
|
@@ -451,6 +486,10 @@ declare const withScenarioLayer: {
|
|
|
451
486
|
<Provided>(scenario: Layer.Layer<Provided | Harness>): (self: Declared) => Opened<Provided | Harness>;
|
|
452
487
|
<Provided>(self: Declared, scenario: Layer.Layer<Provided | Harness>): Opened<Provided | Harness>;
|
|
453
488
|
};
|
|
489
|
+
declare const live: {
|
|
490
|
+
(reason: string): <Self>(self: LiveStage<Self>) => Self;
|
|
491
|
+
<Self>(self: LiveStage<Self>, reason: string): Self;
|
|
492
|
+
};
|
|
454
493
|
declare const make: (bindings: Suite.Bindings) => (name: string) => Declared;
|
|
455
494
|
//#endregion
|
|
456
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,6 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
-
import {
|
|
3
|
-
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";
|
|
4
3
|
import { dual } from "effect/Function";
|
|
5
4
|
import { Prototype } from "effect/Pipeable";
|
|
6
5
|
import "effect/PlatformError";
|
|
@@ -12,7 +11,7 @@ import * as OtelTracer from "@effect/opentelemetry/OtelTracer";
|
|
|
12
11
|
import * as OtelResource from "@effect/opentelemetry/Resource";
|
|
13
12
|
import { Blueprint, Handle } from "@systemfsoftware/effect-cell-types";
|
|
14
13
|
import { SpanStatusCode } from "@opentelemetry/api";
|
|
15
|
-
import { AlwaysOnSampler, BasicTracerProvider
|
|
14
|
+
import { AlwaysOnSampler, BasicTracerProvider } from "@opentelemetry/sdk-trace-base";
|
|
16
15
|
import { Suite, TaskRef } from "@systemfsoftware/effect-spec-runtime";
|
|
17
16
|
//#region src/ContractDecodeError.schema.ts
|
|
18
17
|
var ContractDecodeError = class extends Schema.TaggedError()("ContractDecodeError", {
|
|
@@ -48,11 +47,11 @@ const Verdict = Schema.Union([Hold, Break]);
|
|
|
48
47
|
//#endregion
|
|
49
48
|
//#region src/FailureDump.ts
|
|
50
49
|
const DEFAULT_DIRECTORY = "artifacts/traces";
|
|
51
|
-
const isBreak$
|
|
50
|
+
const isBreak$1 = Schema.is(Break);
|
|
52
51
|
/**
|
|
53
52
|
* The rendered evidence a break carries; a held verdict leaves nothing to write.
|
|
54
53
|
*/
|
|
55
|
-
const report = (verdict) => Option.map(Option.liftPredicate(verdict, isBreak$
|
|
54
|
+
const report = (verdict) => Option.map(Option.liftPredicate(verdict, isBreak$1), renderBreach);
|
|
56
55
|
const renderValue = (value) => JSON.stringify(value);
|
|
57
56
|
const renderEntries = (entries) => [...entries].map(([key, value]) => `${key}=${renderValue(value)}`).join(" ");
|
|
58
57
|
const indentOf = (depth) => " ".repeat(depth);
|
|
@@ -249,27 +248,19 @@ var Observation_service_exports = /* @__PURE__ */ __exportAll({
|
|
|
249
248
|
});
|
|
250
249
|
var Observation = class extends Context.Service()("@systemfsoftware/trace-spec/Observation") {};
|
|
251
250
|
//#endregion
|
|
252
|
-
//#region src/TraceDisparityError.schema.ts
|
|
253
|
-
var TraceDisparityError = class extends Schema.TaggedError()("TraceDisparityError", {
|
|
254
|
-
relationId: Schema.String,
|
|
255
|
-
traceId: Schema.String,
|
|
256
|
-
breaks: Schema.Array(Break),
|
|
257
|
-
dumpPath: Schema.NullOr(Schema.String)
|
|
258
|
-
}) {};
|
|
259
|
-
//#endregion
|
|
260
251
|
//#region src/Contract.ts
|
|
261
252
|
var Contract_exports = /* @__PURE__ */ __exportAll({
|
|
262
253
|
ContractDecodeError: () => ContractDecodeError,
|
|
263
254
|
EmptyObservationError: () => EmptyObservationError,
|
|
264
255
|
IncompleteObservationError: () => IncompleteObservationError,
|
|
265
|
-
TraceDisparityError: () => TraceDisparityError,
|
|
266
256
|
TransportObservationError: () => TransportObservationError,
|
|
267
257
|
TypeId: () => TypeId$3,
|
|
268
258
|
check: () => check,
|
|
269
259
|
holds: () => holds,
|
|
270
260
|
judge: () => judge,
|
|
271
261
|
of: () => of,
|
|
272
|
-
stimulate: () => stimulate
|
|
262
|
+
stimulate: () => stimulate,
|
|
263
|
+
verdictCheck: () => verdictCheck
|
|
273
264
|
});
|
|
274
265
|
const TypeId$3 = Symbol.for("@systemfsoftware/trace-spec/Contract");
|
|
275
266
|
const stimulatedOf = (taxonomy, stimulus) => ({
|
|
@@ -322,13 +313,34 @@ const judgeDual = (self, input, options) => Effect.gen(function* () {
|
|
|
322
313
|
};
|
|
323
314
|
});
|
|
324
315
|
const judge = dual((args) => isContract(args[0]), judgeDual);
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
+
*/
|
|
332
344
|
const check = dual((args) => isContract(args[0]), checkDual);
|
|
333
345
|
//#endregion
|
|
334
346
|
//#region src/drivers/tempo-trace.schema.ts
|
|
@@ -547,15 +559,40 @@ Schema.encodeEffect(HexId);
|
|
|
547
559
|
//#region src/observation-window.handle.ts
|
|
548
560
|
const TypeId$2 = Symbol.for("~systemfsoftware/trace-spec/ObservationWindow");
|
|
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
|
+
};
|
|
550
587
|
const isObservationWindow = ObservationWindow.is;
|
|
551
588
|
const make$3 = (spec) => {
|
|
552
|
-
const
|
|
589
|
+
const { processor, store } = spanRecorder();
|
|
553
590
|
const provider = new BasicTracerProvider({
|
|
554
591
|
sampler: new AlwaysOnSampler(),
|
|
555
|
-
spanProcessors: [
|
|
592
|
+
spanProcessors: [processor]
|
|
556
593
|
});
|
|
557
594
|
return ObservationWindow.make({ serviceName: spec.serviceName }, {
|
|
558
|
-
|
|
595
|
+
store,
|
|
559
596
|
provider
|
|
560
597
|
});
|
|
561
598
|
};
|
|
@@ -614,7 +651,7 @@ const emptyObservation = (traceId) => new EmptyObservationError({
|
|
|
614
651
|
traceId,
|
|
615
652
|
detail: "the observation window closed with no span carrying the stimulated trace id"
|
|
616
653
|
});
|
|
617
|
-
const recordsOf = (self, traceId) => ObservationWindow.slot(self).
|
|
654
|
+
const recordsOf = (self, traceId) => ObservationWindow.slot(self).store.getFinishedSpans().filter((span) => span.spanContext().traceId === traceId).map(spanRecordOf);
|
|
618
655
|
const collect = dual(2, (self, traceId) => Effect.suspend(() => {
|
|
619
656
|
const records = recordsOf(self, traceId);
|
|
620
657
|
return records.length > 0 ? Effect.succeed(records) : Effect.fail(emptyObservation(traceId));
|
|
@@ -676,7 +713,7 @@ const relation = (parts) => Object.assign((graph) => parts.evaluate(graph), {
|
|
|
676
713
|
const matchedNodes = (graph, spec) => byId(graph, spec);
|
|
677
714
|
const inspectedOf = (nodes) => nodes.map((node) => node.spanId);
|
|
678
715
|
const matchedAll = (items, predicate) => items.length > 0 && items.every(predicate);
|
|
679
|
-
const isBreak
|
|
716
|
+
const isBreak = (verdict) => Schema.is(Break)(verdict);
|
|
680
717
|
const isHold$1 = (verdict) => Schema.is(Hold)(verdict);
|
|
681
718
|
const verdictOf$1 = (options) => options.holds ? Hold.make({
|
|
682
719
|
conjunct: options.id,
|
|
@@ -686,7 +723,7 @@ const verdictOf$1 = (options) => options.holds ? Hold.make({
|
|
|
686
723
|
inspected: options.inspected,
|
|
687
724
|
detail: options.detail
|
|
688
725
|
});
|
|
689
|
-
const declared = (declaration) => relation({
|
|
726
|
+
const declared$1 = (declaration) => relation({
|
|
690
727
|
id: declaration.id,
|
|
691
728
|
soft: false,
|
|
692
729
|
softenable: declaration.softenable,
|
|
@@ -702,21 +739,21 @@ const declared = (declaration) => relation({
|
|
|
702
739
|
});
|
|
703
740
|
const holdsCountOf = (spec, expected) => (graph) => expected(matchedNodes(graph, spec).length);
|
|
704
741
|
const holdsEveryOf = (spec, predicate) => (graph) => matchedAll(matchedNodes(graph, spec), predicate);
|
|
705
|
-
const exists = (spec) => declared({
|
|
742
|
+
const exists = (spec) => declared$1({
|
|
706
743
|
id: `exists(${spec.id})`,
|
|
707
744
|
spans: [spec],
|
|
708
745
|
softenable: false,
|
|
709
746
|
detail: `no ${spec.id} span was emitted`,
|
|
710
747
|
holds: holdsCountOf(spec, (count) => count > 0)
|
|
711
748
|
});
|
|
712
|
-
const absent = (spec) => declared({
|
|
749
|
+
const absent = (spec) => declared$1({
|
|
713
750
|
id: `absent(${spec.id})`,
|
|
714
751
|
spans: [spec],
|
|
715
752
|
softenable: false,
|
|
716
753
|
detail: `a ${spec.id} span was emitted`,
|
|
717
754
|
holds: holdsCountOf(spec, (count) => count === 0)
|
|
718
755
|
});
|
|
719
|
-
const unique = (spec) => declared({
|
|
756
|
+
const unique = (spec) => declared$1({
|
|
720
757
|
id: `unique(${spec.id})`,
|
|
721
758
|
spans: [spec],
|
|
722
759
|
softenable: true,
|
|
@@ -731,7 +768,7 @@ const reachedUnder = (graph, parent, childSpan, reach) => {
|
|
|
731
768
|
const reached = reach(graph, matchedNodes(graph, parent));
|
|
732
769
|
return matchedNodes(graph, childSpan).some((node) => reached.has(node.spanId));
|
|
733
770
|
};
|
|
734
|
-
const childImpl = (parent, childSpan) => declared({
|
|
771
|
+
const childImpl = (parent, childSpan) => declared$1({
|
|
735
772
|
id: `child(${parent.id},${childSpan.id})`,
|
|
736
773
|
spans: [parent, childSpan],
|
|
737
774
|
softenable: true,
|
|
@@ -739,7 +776,7 @@ const childImpl = (parent, childSpan) => declared({
|
|
|
739
776
|
holds: (graph) => reachedUnder(graph, parent, childSpan, childIdsOf)
|
|
740
777
|
});
|
|
741
778
|
const child = dual(2, childImpl);
|
|
742
|
-
const descendantImpl = (parent, childSpan) => declared({
|
|
779
|
+
const descendantImpl = (parent, childSpan) => declared$1({
|
|
743
780
|
id: `descendant(${parent.id},${childSpan.id})`,
|
|
744
781
|
spans: [parent, childSpan],
|
|
745
782
|
softenable: true,
|
|
@@ -747,7 +784,7 @@ const descendantImpl = (parent, childSpan) => declared({
|
|
|
747
784
|
holds: (graph) => reachedUnder(graph, parent, childSpan, descendantIdsOf)
|
|
748
785
|
});
|
|
749
786
|
const descendant = dual(2, descendantImpl);
|
|
750
|
-
const orderImpl = (before, after) => declared({
|
|
787
|
+
const orderImpl = (before, after) => declared$1({
|
|
751
788
|
id: `order(${before.id},${after.id})`,
|
|
752
789
|
spans: [before, after],
|
|
753
790
|
softenable: true,
|
|
@@ -755,7 +792,7 @@ const orderImpl = (before, after) => declared({
|
|
|
755
792
|
holds: (graph) => matchedAll(matchedNodes(graph, after), (node) => startsAfterSome(matchedNodes(graph, before), node))
|
|
756
793
|
});
|
|
757
794
|
const order = dual(2, orderImpl);
|
|
758
|
-
const statusImpl = (spec, expected) => declared({
|
|
795
|
+
const statusImpl = (spec, expected) => declared$1({
|
|
759
796
|
id: `status(${spec.id},${expected})`,
|
|
760
797
|
spans: [spec],
|
|
761
798
|
softenable: true,
|
|
@@ -763,7 +800,7 @@ const statusImpl = (spec, expected) => declared({
|
|
|
763
800
|
holds: holdsEveryOf(spec, (node) => node.status === expected)
|
|
764
801
|
});
|
|
765
802
|
const status = dual(2, statusImpl);
|
|
766
|
-
const errorTypeImpl = (spec, expected) => declared({
|
|
803
|
+
const errorTypeImpl = (spec, expected) => declared$1({
|
|
767
804
|
id: `errorType(${spec.id},${expected})`,
|
|
768
805
|
spans: [spec],
|
|
769
806
|
softenable: true,
|
|
@@ -772,7 +809,7 @@ const errorTypeImpl = (spec, expected) => declared({
|
|
|
772
809
|
});
|
|
773
810
|
const errorType = dual(2, errorTypeImpl);
|
|
774
811
|
const partialMatches = (attributes, partial) => Object.entries(partial).filter(([, value]) => value !== void 0).every(([key, value]) => Equal.equals(attributes[key], value));
|
|
775
|
-
const attrsImpl = (spec, partial) => declared({
|
|
812
|
+
const attrsImpl = (spec, partial) => declared$1({
|
|
776
813
|
id: `attrs(${spec.id})`,
|
|
777
814
|
spans: [spec],
|
|
778
815
|
softenable: true,
|
|
@@ -780,7 +817,7 @@ const attrsImpl = (spec, partial) => declared({
|
|
|
780
817
|
holds: holdsEveryOf(spec, (node) => partialMatches(node.attrs, partial))
|
|
781
818
|
});
|
|
782
819
|
const attrs = dual(2, attrsImpl);
|
|
783
|
-
const durationLessThanImpl = (spec, millis) => declared({
|
|
820
|
+
const durationLessThanImpl = (spec, millis) => declared$1({
|
|
784
821
|
id: `durationLessThan(${spec.id},${millis})`,
|
|
785
822
|
spans: [spec],
|
|
786
823
|
softenable: true,
|
|
@@ -789,7 +826,7 @@ const durationLessThanImpl = (spec, millis) => declared({
|
|
|
789
826
|
});
|
|
790
827
|
const durationLessThan = dual(2, durationLessThanImpl);
|
|
791
828
|
const carriesEvent = (name) => (node) => node.events.some((event) => event.name === name);
|
|
792
|
-
const eventImpl = (spec, name) => declared({
|
|
829
|
+
const eventImpl = (spec, name) => declared$1({
|
|
793
830
|
id: `event(${spec.id},${name})`,
|
|
794
831
|
spans: [spec],
|
|
795
832
|
softenable: true,
|
|
@@ -849,8 +886,8 @@ const collectSoft = (id, breaks) => {
|
|
|
849
886
|
detail: breaks.map((breach) => breach.conjunct).join(", ")
|
|
850
887
|
});
|
|
851
888
|
};
|
|
852
|
-
const firstHardBreak = (relations, graph) => relations.filter((relation) => !relation.soft).map((relation) => relation(graph)).find(isBreak
|
|
853
|
-
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);
|
|
854
891
|
const combined = (options) => relation({
|
|
855
892
|
id: options.id,
|
|
856
893
|
soft: options.soft,
|
|
@@ -1038,10 +1075,11 @@ var Stimulus_exports = /* @__PURE__ */ __exportAll({
|
|
|
1038
1075
|
traceContext: () => traceContext
|
|
1039
1076
|
});
|
|
1040
1077
|
const byteHex = (byte) => byte.toString(16).padStart(2, "0");
|
|
1041
|
-
const
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1044
|
-
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);
|
|
1045
1083
|
return {
|
|
1046
1084
|
traceId,
|
|
1047
1085
|
spanId,
|
|
@@ -1075,25 +1113,33 @@ const HarnessFailure = Schema.Union([
|
|
|
1075
1113
|
ContractDecodeError,
|
|
1076
1114
|
EmptyObservationError,
|
|
1077
1115
|
IncompleteObservationError,
|
|
1078
|
-
TransportObservationError
|
|
1079
|
-
TraceDisparityError
|
|
1116
|
+
TransportObservationError
|
|
1080
1117
|
]);
|
|
1081
1118
|
//#endregion
|
|
1082
|
-
//#region src/
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1119
|
+
//#region src/TaskAnnounce.ts
|
|
1120
|
+
const forget = (annotation) => {
|
|
1121
|
+
Promise.resolve(annotation);
|
|
1122
|
+
};
|
|
1123
|
+
const dispatchAnnotate = (annotate, message, type) => Option.match(Option.fromNullishOr(annotate), {
|
|
1124
|
+
onNone: () => Effect.void,
|
|
1125
|
+
onSome: (record) => Effect.sync(() => forget(record(message, type)))
|
|
1126
|
+
});
|
|
1127
|
+
const announcement = (taskContext, message, type) => Option.match(Option.fromNullishOr(taskContext), {
|
|
1128
|
+
onNone: () => Effect.void,
|
|
1129
|
+
onSome: (context) => dispatchAnnotate(context.annotate, message, type)
|
|
1130
|
+
});
|
|
1131
|
+
const record = (message, type) => Effect.gen(function* () {
|
|
1132
|
+
const taskContext = yield* TaskRef.RawVitestTaskRef;
|
|
1133
|
+
return yield* announcement(taskContext, message, type);
|
|
1091
1134
|
});
|
|
1092
|
-
const dumpMessageOf = (
|
|
1093
|
-
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), {
|
|
1094
1137
|
onNone: () => Effect.void,
|
|
1095
|
-
onSome: (message) =>
|
|
1138
|
+
onSome: (message) => record(message, "info")
|
|
1096
1139
|
});
|
|
1140
|
+
//#endregion
|
|
1141
|
+
//#region src/Prop.ts
|
|
1142
|
+
const isHold = Schema.is(Hold);
|
|
1097
1143
|
/**
|
|
1098
1144
|
* The contract with its stimulus replaced by the function the property was handed: the impostor gate
|
|
1099
1145
|
* swaps in a constant fake, so the judgement must reach the stimulus through the argument, never the
|
|
@@ -1108,11 +1154,12 @@ const judgedOver = (contract, subject) => ({
|
|
|
1108
1154
|
* frozen run satisfies every relation about that run, so the input pin is what refutes it.
|
|
1109
1155
|
*/
|
|
1110
1156
|
const consumedInput = (judgment, input) => judgment.run.input === input;
|
|
1111
|
-
const predicateImpl = (title, contract, scenario) => {
|
|
1112
|
-
const
|
|
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)));
|
|
1113
1160
|
return (subject, input) => checked(subject, input);
|
|
1114
1161
|
};
|
|
1115
|
-
const predicate = dual(
|
|
1162
|
+
const predicate = dual(4, predicateImpl);
|
|
1116
1163
|
//#endregion
|
|
1117
1164
|
//#region src/StimulusFailure.schema.ts
|
|
1118
1165
|
var StimulusFailure = class extends Schema.TaggedError()("StimulusFailure", {
|
|
@@ -1120,80 +1167,86 @@ var StimulusFailure = class extends Schema.TaggedError()("StimulusFailure", {
|
|
|
1120
1167
|
detail: Schema.String
|
|
1121
1168
|
}) {};
|
|
1122
1169
|
//#endregion
|
|
1123
|
-
//#region src/TaskAnnounce.ts
|
|
1124
|
-
const dispatchAnnotate = (annotate, message, type) => Option.match(Option.fromNullishOr(annotate), {
|
|
1125
|
-
onNone: () => Effect.void,
|
|
1126
|
-
onSome: (record) => Effect.promise(() => Promise.resolve(record(message, type)))
|
|
1127
|
-
});
|
|
1128
|
-
const announcement = (taskContext, message, type) => Option.match(Option.fromNullishOr(taskContext), {
|
|
1129
|
-
onNone: () => Effect.void,
|
|
1130
|
-
onSome: (context) => dispatchAnnotate(context.annotate, message, type)
|
|
1131
|
-
});
|
|
1132
|
-
const record = (message, type) => Effect.gen(function* () {
|
|
1133
|
-
const taskContext = yield* TaskRef.RawVitestTaskRef;
|
|
1134
|
-
return yield* announcement(taskContext, message, type);
|
|
1135
|
-
});
|
|
1136
|
-
const announceDump = (error) => Option.match(Option.fromNullishOr(error.dumpPath), {
|
|
1137
|
-
onNone: () => Effect.void,
|
|
1138
|
-
onSome: (dumpPath) => record(`trace contract failed; observed graph dumped to ${dumpPath}`, "info")
|
|
1139
|
-
});
|
|
1140
|
-
//#endregion
|
|
1141
1170
|
//#region src/Suite.ts
|
|
1142
1171
|
var Suite_exports = /* @__PURE__ */ __exportAll({
|
|
1143
1172
|
StimulusFailure: () => StimulusFailure,
|
|
1144
1173
|
TypeId: () => TypeId,
|
|
1174
|
+
live: () => live,
|
|
1145
1175
|
make: () => make,
|
|
1146
1176
|
withLayer: () => withLayer,
|
|
1147
1177
|
withScenarioLayer: () => withScenarioLayer
|
|
1148
1178
|
});
|
|
1149
1179
|
const TypeId = Symbol.for("@systemfsoftware/trace-spec/Suite");
|
|
1150
1180
|
const isHarnessFailure = Schema.is(HarnessFailure);
|
|
1151
|
-
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({
|
|
1152
1193
|
stimulus,
|
|
1153
|
-
detail: Cause.pretty(Cause.fail(failure))
|
|
1194
|
+
detail: `${Cause.pretty(Cause.fail(failure))}\nrandom salt: ${salt}`
|
|
1154
1195
|
});
|
|
1155
|
-
const
|
|
1156
|
-
const
|
|
1157
|
-
const
|
|
1158
|
-
const
|
|
1159
|
-
|
|
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
|
+
};
|
|
1160
1202
|
Case.prop = (name, contract, arbitrary) => {
|
|
1203
|
+
const salt = saltOf();
|
|
1161
1204
|
propIt.effect.prop(name, {
|
|
1162
1205
|
of: [arbitrary],
|
|
1163
1206
|
subject: contract.stimulus
|
|
1164
|
-
}, (subject, values) => predicate(name, contract, scenario)(subject, values[0]));
|
|
1207
|
+
}, (subject, values) => predicate(name, contract, scenario, shared)(subject, values[0]).pipe(Random.withSeed(salt)));
|
|
1165
1208
|
};
|
|
1166
1209
|
return { Case };
|
|
1167
1210
|
};
|
|
1168
|
-
const config = (name) =>
|
|
1211
|
+
const config = (name, live) => live === void 0 ? {
|
|
1212
|
+
name,
|
|
1213
|
+
describe: "describe",
|
|
1214
|
+
options: void 0
|
|
1215
|
+
} : {
|
|
1169
1216
|
name,
|
|
1170
1217
|
describe: "describe",
|
|
1171
1218
|
options: void 0,
|
|
1172
|
-
|
|
1173
|
-
}
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
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) => ({
|
|
1180
1238
|
[TypeId]: TypeId,
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
}, (register, propIt) => use(caseTools(register, propIt, shared))),
|
|
1185
|
-
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),
|
|
1186
1242
|
...Prototype
|
|
1187
1243
|
});
|
|
1188
1244
|
const withLayerDual = (self, shared) => self.withLayer(shared);
|
|
1189
1245
|
const withLayer = dual(2, withLayerDual);
|
|
1190
1246
|
const withScenarioLayerDual = (self, scenario) => self.withScenarioLayer(scenario);
|
|
1191
1247
|
const withScenarioLayer = dual(2, withScenarioLayerDual);
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
withScenarioLayer: (scenario) => openScenario(bindings, name, scenario),
|
|
1196
|
-
...Prototype
|
|
1197
|
-
});
|
|
1248
|
+
const liveDual = (self, reason) => self.live(reason);
|
|
1249
|
+
const live = dual(2, liveDual);
|
|
1250
|
+
const make = (bindings) => (name) => declared(bindings, name, void 0);
|
|
1198
1251
|
//#endregion
|
|
1199
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",
|
|
@@ -39,17 +39,18 @@
|
|
|
39
39
|
"@opentelemetry/sdk-trace-base": "^2",
|
|
40
40
|
"@opentelemetry/semantic-conventions": "^1.33",
|
|
41
41
|
"@systemfsoftware/effect-cell-types": "^10.2.0",
|
|
42
|
-
"@systemfsoftware/effect-spec-runtime": "^0.
|
|
42
|
+
"@systemfsoftware/effect-spec-runtime": "^0.3.0",
|
|
43
43
|
"@systemfsoftware/trace-taxonomy": "^0.0.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@effect/platform-node": "4.0.0-rc.117",
|
|
47
|
-
"@effect/vitest": "npm:@systemfsoftware/vitest@0.0.0",
|
|
48
47
|
"@systemfsoftware/arethetypeswrong-cli": "^4.2.0",
|
|
49
|
-
"@systemfsoftware/
|
|
50
|
-
"@systemfsoftware/
|
|
48
|
+
"@systemfsoftware/conformance-spec": "^0.1.0",
|
|
49
|
+
"@systemfsoftware/effect-gherkin-spec": "^5.0.0",
|
|
50
|
+
"@systemfsoftware/oxlint-config-recommended": "^3.1.0",
|
|
51
51
|
"@systemfsoftware/tsconfig": "^2.0.1",
|
|
52
52
|
"@systemfsoftware/tsdown-config": "^0.1.0",
|
|
53
|
+
"@systemfsoftware/vitest": "^0.1.0",
|
|
53
54
|
"@systemfsoftware/vitest-config": "^0.1.0",
|
|
54
55
|
"@types/node": "^26",
|
|
55
56
|
"effect": "4.0.0-rc.117",
|
|
@@ -60,7 +61,7 @@
|
|
|
60
61
|
"vitest": "^5"
|
|
61
62
|
},
|
|
62
63
|
"peerDependencies": {
|
|
63
|
-
"@
|
|
64
|
+
"@systemfsoftware/vitest": "^0.1.0",
|
|
64
65
|
"effect": "4.0.0-rc.117",
|
|
65
66
|
"vitest": "^5"
|
|
66
67
|
},
|