@playfast/reform-proof 1.1.1 → 1.2.1

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/src/index.ts CHANGED
@@ -1,25 +1,28 @@
1
- import { Effect, type Schema } from 'effect'
2
- import type { YieldWrap } from 'effect/Utils'
1
+ import { Effect, Layer, TestContext, type TestServices } from 'effect'
3
2
  import { ProofRunner, type ProofRunnerApi, proofRunnerLayer, withProofRunner } from './runner'
4
- import { type ProductClass, type RequirementClass } from './product'
5
- import type { ProofDriver } from './driverTypes'
3
+ import { isRecord } from './assert'
4
+ import type { ContractOf, Facade, SlotsOf } from './facade'
6
5
  import type {
7
- CompositionClass,
8
- CompositionService,
6
+ AnyRequirementClass,
7
+ ProductClass,
8
+ ProductComposition,
9
+ RequirementClass,
10
+ } from './product'
11
+ import type { ProofCapture, ProofCase, ProofGenerator, SuiteResult } from './proofCase'
12
+ import type {
13
+ CapturedScene,
9
14
  Scene,
10
- SlotInstance,
11
- Trigger,
15
+ SlotComposition as ReformSlotComposition,
12
16
  UiContract,
13
17
  } from '@playfast/reform'
14
-
15
- type AnyValue = Schema.Schema.Type<Schema.Schema.Any>
16
- type AnyComposition = CompositionClass<AnyValue, AnyValue>
18
+ import type { AnyScene, CapturedRender } from '@playfast/reform/internal'
17
19
 
18
20
  export { ProofRunner, proofRunnerLayer, withProofRunner }
19
21
  export type { ProofRunnerApi }
20
22
 
21
23
  export {
22
24
  makeFacade,
25
+ makeProofFacade,
23
26
  makeRuntimeHandleFacade,
24
27
  makeRuntimeTreeFacade,
25
28
  makeSink,
@@ -28,72 +31,37 @@ export {
28
31
  export type {
29
32
  MountedFacade,
30
33
  MountedSlotFacade,
31
- RuntimeServices,
34
+ MountedSlotFacadesOf,
32
35
  RuntimeTreeFacade,
33
36
  Sink,
34
37
  } from './engine'
35
38
 
36
- export {
37
- Product,
38
- type ProductClass,
39
- type ProductManifest,
40
- ProductRequirement,
41
- type RequirementClass,
42
- type RequirementManifest,
39
+ export { Product, ProductRequirement } from './product'
40
+ export type {
41
+ AnyRequirementClass,
42
+ ProductClass,
43
+ ProductComposition,
44
+ ProductManifest,
45
+ RequirementCapture,
46
+ RequirementClass,
47
+ RequirementManifest,
43
48
  } from './product'
44
-
45
- export { expect, matchProps } from './assertions'
46
-
47
- export type EventsOf<C extends UiContract> = C extends { events: infer E } ? E : Record<never, never>
48
- type SlotsOf<C extends UiContract> = C extends { slots: infer S } ? S : Record<never, never>
49
- type PayloadOf<T> = T extends Trigger<infer P> ? P : never
50
- export type ContractOf<Comp> = Comp extends CompositionClass<AnyValue, infer C> ? C : never
51
- type ContractOfSlot<S> = S extends SlotInstance<infer Comp> ? ContractOf<Comp> : never
52
-
53
- export type ActionsOf<C extends UiContract> = {
54
- readonly [K in keyof EventsOf<C>]: (
55
- payload: PayloadOf<EventsOf<C>[K]>,
56
- ) => Effect.Effect<C['props'], never, CompositionService>
57
- }
58
- export type SlotFacadesOf<C extends UiContract> = {
59
- readonly [K in keyof SlotsOf<C>]: SlotFacade<ContractOfSlot<SlotsOf<C>[K]>>
60
- }
61
-
62
- export type Action = (payload: unknown) => Effect.Effect<unknown, never, CompositionService>
63
-
64
- export interface Facade<C extends UiContract> {
65
- readonly props: Effect.Effect<C['props'], never, CompositionService>
66
- readonly expectProps: (partial: Partial<C['props']>) => Effect.Effect<void, never, CompositionService>
67
- readonly actions: ActionsOf<C>
68
- readonly slots: SlotFacadesOf<C>
69
- readonly frame: Effect.Effect<void, never, CompositionService>
70
- }
71
-
72
- export interface SlotFacade<C extends UiContract> extends Facade<C> {
73
- readonly first: Effect.Effect<Facade<C>, never, CompositionService>
74
- readonly at: (index: number) => Effect.Effect<Facade<C>, never, CompositionService>
75
- readonly all: Effect.Effect<ReadonlyArray<Facade<C>>, never, CompositionService>
76
- readonly byKey: (key: string) => Effect.Effect<Facade<C>, never, CompositionService>
77
- readonly where: (
78
- predicate: (props: C['props']) => boolean,
79
- ) => Effect.Effect<ReadonlyArray<Facade<C>>, never, CompositionService>
80
- readonly count: Effect.Effect<number, never, CompositionService>
81
- }
82
-
83
- type ProofGenerator = Generator<
84
- YieldWrap<Effect.Effect<unknown, unknown, CompositionService>>,
85
- void,
86
- unknown
87
- >
88
-
89
- type AnyFacade = Facade<UiContract>
90
-
91
- export type ProofBody = (app: AnyFacade) => ProofGenerator
49
+ export { expect, matchProps } from './assert'
50
+ export type {
51
+ Action,
52
+ ActionsOf,
53
+ ContractOf,
54
+ EventsOf,
55
+ Facade,
56
+ SlotFacade,
57
+ SlotFacadesOf,
58
+ } from './facade'
59
+ export type { ProofCapture, ProofCase, ProofResult, SuiteResult } from './proofCase'
92
60
 
93
61
  export interface Proof {
94
- readonly requirement: RequirementClass<AnyComposition, string>
95
- readonly scene: Scene
96
- readonly body: ProofBody
62
+ readonly requirement: AnyRequirementClass
63
+ readonly scene: AnyScene
64
+ readonly capture: <Result>(visit: ProofCapture<Result>) => Result
97
65
  }
98
66
 
99
67
  export interface ProofSuite {
@@ -102,45 +70,78 @@ export interface ProofSuite {
102
70
  readonly proofs: ReadonlyArray<Proof>
103
71
  }
104
72
 
105
- const isRecord = (candidate: unknown): candidate is Record<string, unknown> =>
106
- typeof candidate === 'object' && candidate !== null
107
-
108
- export interface ProofResult {
109
- readonly requirement: string
110
- readonly ok: boolean
111
- // oxlint-disable-next-line reform-rules/no-optional-fields -- serialized result DTO read as `error ?? …` by the vitest adapter (external); optional kept for wire compat
112
- readonly error?: string
113
- }
114
-
115
- export interface SuiteResult {
116
- readonly product: string
117
- readonly results: ReadonlyArray<ProofResult>
118
- readonly ok: boolean
119
- }
120
-
121
- const implement = <Comp extends AnyComposition>(
73
+ const implement = <Comp extends ProductComposition>(
122
74
  requirement: RequirementClass<Comp, string>,
123
- scene: Scene<ContractOf<Comp>>,
124
- body: (app: Facade<ContractOf<Comp>>) => ProofGenerator,
125
- ): Proof => {
126
- // oxlint-disable-next-line reform-rules/no-type-assertion -- contract erasure seam: Facade<C> → AnyFacade is contravariant, sound by construction
127
- const erased = body as ProofBody
128
- return { requirement, scene, body: erased }
75
+ scene: Scene<ContractOf<NoInfer<Comp>>, NoInfer<Comp>['States']>,
76
+ body: (app: Facade<ContractOf<Comp>, never>) => ProofGenerator,
77
+ ): Proof =>
78
+ scene.capture(
79
+ <Services, P, N extends string, Identity>(
80
+ exactScene: CapturedScene<ContractOf<Comp>, Comp['States'], Services, P, N, Identity>,
81
+ ): Proof => {
82
+ const proofCase: ProofCase<
83
+ Comp,
84
+ ContractOf<Comp>,
85
+ Comp['States'],
86
+ Services,
87
+ P,
88
+ N,
89
+ Identity
90
+ > = {
91
+ requirement,
92
+ scene: exactScene,
93
+ placement: 'Root',
94
+ body,
95
+ }
96
+ return {
97
+ requirement,
98
+ scene: exactScene,
99
+ capture: (visit) =>
100
+ visit<Comp, ContractOf<Comp>, Comp['States'], Services, P, N, Identity>(proofCase),
101
+ }
102
+ },
103
+ )
104
+
105
+ type SlotsHolding<C extends UiContract, Target extends ProductComposition> = C extends {
106
+ readonly slots: NonNullable<UiContract['slots']>
129
107
  }
130
-
131
- type SlotsHolding<C extends UiContract, Target> = {
132
- [K in keyof SlotsOf<C>]: ContractOfSlot<SlotsOf<C>[K]> extends Target ? K : never
133
- }[keyof SlotsOf<C>]
134
-
135
- const implementVia = <Comp extends AnyComposition, C extends UiContract>(
108
+ ? {
109
+ [K in keyof SlotsOf<C>]: [ReformSlotComposition<SlotsOf<C>[K]>] extends [Target] ? K : never
110
+ }[keyof SlotsOf<C>]
111
+ : never
112
+
113
+ type SceneHolding<
114
+ Comp extends ProductComposition,
115
+ C extends UiContract,
116
+ S extends ReadonlyArray<unknown>,
117
+ > = [SlotsHolding<C, Comp>] extends [never] ? never : Scene<C, S>
118
+
119
+ const implementVia = <
120
+ Comp extends ProductComposition,
121
+ C extends UiContract,
122
+ S extends ReadonlyArray<unknown>,
123
+ >(
136
124
  requirement: RequirementClass<Comp, string>,
137
- scene: Scene<C> & ([SlotsHolding<C, ContractOf<Comp>>] extends [never] ? never : unknown),
138
- body: (app: Facade<C>) => ProofGenerator,
139
- ): Proof => {
140
- // oxlint-disable-next-line reform-rules/no-type-assertion -- contract erasure seam: Facade<C> → AnyFacade is contravariant, sound by construction
141
- const erased = body as ProofBody
142
- return { requirement, scene, body: erased }
143
- }
125
+ scene: SceneHolding<NoInfer<Comp>, C, S>,
126
+ body: (app: Facade<C, never>) => ProofGenerator,
127
+ ): Proof =>
128
+ scene.capture(
129
+ <Services, P, N extends string, Identity>(
130
+ exactScene: CapturedScene<C, S, Services, P, N, Identity>,
131
+ ): Proof => {
132
+ const proofCase: ProofCase<Comp, C, S, Services, P, N, Identity> = {
133
+ requirement,
134
+ scene: exactScene,
135
+ placement: 'Slot',
136
+ body,
137
+ }
138
+ return {
139
+ requirement,
140
+ scene: exactScene,
141
+ capture: (visit) => visit<Comp, C, S, Services, P, N, Identity>(proofCase),
142
+ }
143
+ },
144
+ )
144
145
 
145
146
  interface MakeSuiteConfig {
146
147
  readonly proofs: ReadonlyArray<Proof>
@@ -156,28 +157,77 @@ export const isProofSuite = (candidate: unknown): candidate is ProofSuite =>
156
157
  isRecord(candidate) && candidate['kind'] === 'ProofSuite' && Array.isArray(candidate['proofs'])
157
158
 
158
159
  const run = (proofSuite: ProofSuite): Promise<SuiteResult> =>
159
- withProofRunner(async (runner: ProofRunnerApi) => {
160
- const reports = await Effect.runPromise(
161
- Effect.forEach(proofSuite.proofs, (proof) => Effect.promise(() => runner.executeProof(proof))),
162
- )
163
- return {
160
+ Effect.runPromise(runEffect(proofSuite))
161
+
162
+ const runEffect = (proofSuite: ProofSuite): Effect.Effect<SuiteResult, never, never> =>
163
+ ProofRunner.pipe(
164
+ Effect.flatMap((runner) =>
165
+ Effect.forEach(proofSuite.proofs, (proof) => runner.executeProofEffect(proof)),
166
+ ),
167
+ Effect.map((reports) => ({
164
168
  product: proofSuite.product.manifest.name,
165
169
  results: reports,
166
170
  ok: reports.every((report) => report.ok),
167
- }
168
- })
171
+ })),
172
+ Effect.provide(proofRunnerLayer),
173
+ )
174
+
175
+ export interface StepFrame {
176
+ readonly index: number
177
+ readonly captures: ReadonlyArray<CapturedRender>
178
+ }
179
+
180
+ export interface DriveResult {
181
+ readonly requirement: string
182
+ readonly frames: ReadonlyArray<StepFrame>
183
+ readonly ok: boolean
184
+ readonly error: string | undefined
185
+ }
186
+
187
+ export interface ProofDriver {
188
+ readonly requirement: string
189
+ readonly run: () => Promise<DriveResult>
190
+ }
169
191
 
170
- export type { DriveResult, ProofDriver, StepFrame } from './driverTypes'
171
192
  const driver = (proofSuite: ProofSuite): ReadonlyArray<ProofDriver> =>
172
193
  proofSuite.proofs.map((proof) => ({
173
194
  requirement: proof.requirement.manifest.statement,
174
195
  run: () => withProofRunner((runner: ProofRunnerApi) => runner.driveProof(proof)),
175
196
  }))
176
197
 
198
+ /**
199
+ * Wrap a scene's layers in a virtual clock, so every `Effect.sleep`, `Schedule` and
200
+ * `Clock` read its procedures make elapses only when the proof says so via
201
+ * `app.advance(...)`:
202
+ *
203
+ * ```ts
204
+ * const MyScene = scene(MyComposition, { provide: [Proof.withTestClock(App)] })
205
+ * ```
206
+ *
207
+ * It has to WRAP the scene's layers, never sit beside them in `provide`. The engine
208
+ * forks its loop while its layer is being built, and a forked fiber inherits the
209
+ * clock in scope at that moment — so a clock merged as a sibling reaches the proof
210
+ * body but not the procedures whose timers it is meant to control. That mis-wiring
211
+ * is silent (`app.advance(...)` finds a clock, adjusts it, and no product timer
212
+ * moves), which is why the clock is exposed as this combinator, not a bare layer.
213
+ *
214
+ * Opt-in per scene rather than on by default: a scene without it keeps the real
215
+ * clock, so proofs that observe product timers by waiting stay correct. Once a scene
216
+ * has it, a product timer NEVER fires on its own — an unadvanced proof sees the
217
+ * pre-timer state, which is the point.
218
+ */
219
+ const withTestClock = <ROut, E, RIn>(
220
+ layer: Layer.Layer<ROut, E, RIn>,
221
+ ): Layer.Layer<ROut | TestServices.TestServices, E, RIn> =>
222
+ layer.pipe(Layer.provideMerge(TestContext.TestContext))
223
+
177
224
  export const Proof: {
178
225
  readonly implement: typeof implement
179
226
  readonly implementVia: typeof implementVia
180
227
  readonly suite: typeof suite
181
228
  readonly run: typeof run
229
+ readonly runEffect: typeof runEffect
182
230
  readonly driver: typeof driver
183
- } = { implement, implementVia, suite, run, driver }
231
+ readonly withTestClock: typeof withTestClock
232
+ } = { implement, implementVia, suite, run, runEffect, driver, withTestClock }
233
+
@@ -0,0 +1,174 @@
1
+ import { expect, it } from '@effect/vitest'
2
+ import { Effect, Layer, Schema as S } from 'effect'
3
+ import {
4
+ Composition,
5
+ Engine,
6
+ Feature,
7
+ type FeatureSlotTarget,
8
+ featureModule,
9
+ mount,
10
+ one,
11
+ provide,
12
+ scene,
13
+ slot,
14
+ State,
15
+ ui,
16
+ } from '@playfast/reform'
17
+ import { FeatureInput, featureModuleFromInput } from '@playfast/reform/internal'
18
+ import { Product, Proof, ProductRequirement } from './index'
19
+
20
+ const NestedFeatureInput = S.Struct({ value: S.Number })
21
+
22
+ class NestedFeatureValue extends State.make('proof.nestedFeature.value', S.Number) {}
23
+
24
+ class NestedUi extends ui('ProofNestedFeature')<{
25
+ props: { mountValue: number; featureValue: number }
26
+ }>() {}
27
+
28
+ class Nested extends Composition.make('ProofNestedFeature', {
29
+ title: 'Nested feature',
30
+ props: NestedFeatureInput,
31
+ states: [NestedFeatureValue],
32
+ ui: NestedUi,
33
+ })<Nested>() {}
34
+
35
+ const NestedLive = Composition.live(Nested, function* () {
36
+ const props = yield* Nested.props
37
+ const featureValue = yield* NestedFeatureValue
38
+ return mount({
39
+ props: { mountValue: props.value, featureValue },
40
+ slots: {},
41
+ })
42
+ })
43
+
44
+ const NestedModule = featureModuleFromInput(
45
+ NestedFeatureInput,
46
+ [],
47
+ NestedLive.pipe(
48
+ Layer.provideMerge(
49
+ Layer.unwrapEffect(
50
+ Effect.map(FeatureInput, (input) =>
51
+ State.live(NestedFeatureValue, S.decodeUnknownSync(NestedFeatureInput)(input).value),
52
+ ),
53
+ ),
54
+ ),
55
+ ),
56
+ )
57
+
58
+ class PlaceholderUi extends ui('ProofNestedFeaturePlaceholder')<{
59
+ props: { phase: string }
60
+ }>() {}
61
+
62
+ class Placeholder extends Composition.make('ProofNestedFeaturePlaceholder', {
63
+ title: 'Nested feature placeholder',
64
+ ui: PlaceholderUi,
65
+ })<Placeholder>() {}
66
+
67
+ const PlaceholderLive = Composition.live(Placeholder, function* () {
68
+ return mount({ props: { phase: 'loading' }, slots: {} })
69
+ })
70
+
71
+ class NestedFeature extends Feature.make('proof.nestedFeature', {
72
+ loadingStrategy: 'lazy',
73
+ input: NestedFeatureInput,
74
+ composition: Nested,
75
+ load: Effect.succeed(NestedModule),
76
+ placeholder: { loading: Placeholder, failed: Placeholder },
77
+ }) {}
78
+
79
+ class NestedSlot extends slot('ProofNestedFeatureSlot')<
80
+ NestedSlot,
81
+ FeatureSlotTarget<typeof NestedFeature>
82
+ >() {}
83
+
84
+ class RootUi extends ui('ProofNestedFeatureRoot')<{
85
+ props: { ready: boolean }
86
+ slots: { Nested: NestedSlot }
87
+ }>() {}
88
+
89
+ class Root extends Composition.make('ProofNestedFeatureRoot', {
90
+ title: 'Nested feature root',
91
+ slots: { Nested: NestedSlot },
92
+ ui: RootUi,
93
+ })<Root>() {}
94
+
95
+ const RootLive = Composition.live(Root, function* () {
96
+ return mount({
97
+ props: { ready: true },
98
+ slots: { Nested: one({ value: 42 }) },
99
+ })
100
+ })
101
+
102
+ const RootModule = featureModule(
103
+ [],
104
+ Layer.mergeAll(RootLive, PlaceholderLive, provide(NestedSlot, NestedFeature)),
105
+ )
106
+
107
+ class RootFeature extends Feature.make('proof.rootFeature', {
108
+ composition: Root,
109
+ module: RootModule,
110
+ }) {}
111
+
112
+ class HarnessRootSlot extends slot('ProofHarnessRootSlot')<
113
+ HarnessRootSlot,
114
+ FeatureSlotTarget<typeof RootFeature>
115
+ >() {}
116
+
117
+ class HarnessUi extends ui('ProofNestedFeatureHarness')<{
118
+ props: { ready: boolean }
119
+ slots: { Root: HarnessRootSlot }
120
+ }>() {}
121
+
122
+ class Harness extends Composition.make('ProofNestedFeatureHarness', {
123
+ title: 'Nested feature harness',
124
+ slots: { Root: HarnessRootSlot },
125
+ ui: HarnessUi,
126
+ })<Harness>() {}
127
+
128
+ const HarnessLive = Composition.live(Harness, function* () {
129
+ return mount({
130
+ props: { ready: true },
131
+ slots: { Root: one({}) },
132
+ })
133
+ })
134
+
135
+ const RootScene = scene(Harness, {
136
+ provide: [
137
+ Layer.mergeAll(HarnessLive, provide(HarnessRootSlot, RootFeature)).pipe(
138
+ Layer.provideMerge(Engine),
139
+ ),
140
+ ],
141
+ })
142
+
143
+ class MountsNestedFeature extends ProductRequirement.make(
144
+ Root,
145
+ 'mounts a nested native Feature with its slot input',
146
+ ) {}
147
+
148
+ class RootProduct extends Product.make(Root, {
149
+ requirements: [MountsNestedFeature],
150
+ }) {}
151
+
152
+ const mountsNestedFeature = Proof.implementVia(MountsNestedFeature, RootScene, function* (app) {
153
+ const root = yield* app.slots.Root.first
154
+ const nested = yield* root.slots.Nested.first
155
+ yield* nested.expectProps({ mountValue: 42, featureValue: 42 })
156
+ })
157
+
158
+ const NestedFeatureProofs = Proof.suite(RootProduct, {
159
+ proofs: [mountsNestedFeature],
160
+ })
161
+
162
+ it.live('proves a Feature that mounts a nested native Feature', () =>
163
+ Effect.gen(function* () {
164
+ const result = yield* Proof.runEffect(NestedFeatureProofs)
165
+ expect(result.results).toEqual([
166
+ {
167
+ requirement: 'mounts a nested native Feature with its slot input',
168
+ ok: true,
169
+ error: undefined,
170
+ },
171
+ ])
172
+ expect(result.ok).toBe(true)
173
+ }),
174
+ )
package/src/product.ts CHANGED
@@ -1,40 +1,65 @@
1
- import { Option, type Schema } from 'effect'
2
- import type { CompositionClass } from '@playfast/reform'
3
- import type { ContractOf, EventsOf } from './index'
1
+ import { Option } from 'effect'
2
+ import type { AnyComposition } from '@playfast/reform/internal'
3
+ import type { ContractOf, EventNamesOf } from './facade'
4
4
 
5
- type AnyValue = Schema.Schema.Type<Schema.Schema.Any>
6
- type AnyComposition = CompositionClass<AnyValue, AnyValue>
5
+ export type ProductComposition = AnyComposition & { readonly identity: symbol }
7
6
 
8
- export interface RequirementManifest<Comp extends AnyComposition, Statement extends string> {
7
+ const RequirementCompositionTypeId: unique symbol = Symbol.for(
8
+ 'reform-proof/RequirementComposition',
9
+ )
10
+
11
+ export interface AnyRequirementClass {
12
+ new (): {}
13
+ readonly manifest: {
14
+ readonly kind: 'ProductRequirement'
15
+ readonly name: string
16
+ readonly statement: string
17
+ readonly composition: ProductComposition
18
+ readonly events: Option.Option<ReadonlyArray<PropertyKey>>
19
+ }
20
+ readonly capture: <Result>(visit: RequirementCapture<Result>) => Result
21
+ }
22
+
23
+ export type RequirementCapture<Result> = <
24
+ Comp extends ProductComposition,
25
+ Statement extends string,
26
+ >(
27
+ requirement: RequirementClass<Comp, Statement>,
28
+ ) => Result
29
+
30
+ export interface RequirementManifest<Comp extends ProductComposition, Statement extends string> {
9
31
  readonly kind: 'ProductRequirement'
10
32
  readonly name: Statement
11
33
  readonly statement: Statement
12
34
  readonly composition: Comp
13
- readonly events: Option.Option<ReadonlyArray<keyof EventsOf<ContractOf<Comp>>>>
35
+ readonly events: Option.Option<ReadonlyArray<EventNamesOf<ContractOf<Comp>>>>
14
36
  }
15
37
 
16
- export interface RequirementClass<Comp extends AnyComposition, Statement extends string> {
17
- new (): {}
38
+ export interface RequirementClass<
39
+ Comp extends ProductComposition,
40
+ Statement extends string,
41
+ > extends AnyRequirementClass {
18
42
  readonly manifest: RequirementManifest<Comp, Statement>
43
+ readonly [RequirementCompositionTypeId]: (composition: Comp) => Comp
19
44
  }
20
45
 
21
- export interface ProductManifest<Comp extends AnyComposition> {
46
+ export interface ProductManifest<Comp extends ProductComposition> {
22
47
  readonly kind: 'Product'
23
48
  readonly name: string
24
49
  readonly composition: Comp
25
- readonly requirements: ReadonlyArray<RequirementClass<Comp, string>>
50
+ readonly requirements: ReadonlyArray<AnyRequirementClass>
26
51
  }
27
52
 
28
- export interface ProductClass<Comp extends AnyComposition = AnyComposition> {
53
+ export interface ProductClass<Comp extends ProductComposition = ProductComposition> {
29
54
  new (): {}
30
55
  readonly manifest: ProductManifest<Comp>
31
56
  }
32
57
 
33
- interface MakeRequirementOptionsExternalApi<Comp extends AnyComposition> {
34
- readonly events?: ReadonlyArray<keyof EventsOf<ContractOf<Comp>>>
58
+ interface MakeRequirementOptionsExternalApi<Comp extends ProductComposition> {
59
+ readonly events?: ReadonlyArray<EventNamesOf<ContractOf<Comp>>>
35
60
  }
36
61
 
37
- const makeRequirement = <Comp extends AnyComposition, const Statement extends string>(
62
+ const makeRequirement = <Comp extends ProductComposition, const Statement extends string>(
38
63
  composition: Comp,
39
64
  statement: Statement,
40
65
  options?: MakeRequirementOptionsExternalApi<Comp>,
@@ -46,20 +71,24 @@ const makeRequirement = <Comp extends AnyComposition, const Statement extends st
46
71
  composition,
47
72
  events: Option.fromNullable(options?.events),
48
73
  }
49
- return class {
74
+ class Requirement {
50
75
  static readonly manifest = manifest
76
+ static readonly [RequirementCompositionTypeId] = (exact: Comp): Comp => exact
77
+ static readonly capture = <Result>(visit: RequirementCapture<Result>): Result =>
78
+ visit(Requirement)
51
79
  }
80
+ return Requirement
52
81
  }
53
82
 
54
83
  export const ProductRequirement: { readonly make: typeof makeRequirement } = {
55
84
  make: makeRequirement,
56
85
  }
57
86
 
58
- interface MakeProductConfig<Comp extends AnyComposition> {
87
+ interface MakeProductConfig<Comp extends ProductComposition> {
59
88
  readonly requirements: ReadonlyArray<RequirementClass<Comp, string>>
60
89
  }
61
90
 
62
- const makeProduct = <Comp extends AnyComposition>(
91
+ const makeProduct = <Comp extends ProductComposition>(
63
92
  composition: Comp,
64
93
  config: MakeProductConfig<Comp>,
65
94
  ): ProductClass<Comp> => {
@@ -74,4 +103,6 @@ const makeProduct = <Comp extends AnyComposition>(
74
103
  }
75
104
  }
76
105
 
77
- export const Product: { readonly make: typeof makeProduct } = { make: makeProduct }
106
+ export const Product: { readonly make: typeof makeProduct } = {
107
+ make: makeProduct,
108
+ }
@@ -0,0 +1,46 @@
1
+ import type { Effect } from 'effect'
2
+ import type { YieldWrap } from 'effect/Utils'
3
+ import type { CapturedScene, UiContract } from '@playfast/reform'
4
+ import type { Facade } from './facade'
5
+ import type { ProductComposition, RequirementClass } from './product'
6
+
7
+ export type ProofGenerator = Generator<YieldWrap<Effect.Effect<unknown, unknown, never>>, void, unknown>
8
+
9
+ export interface ProofCase<
10
+ Comp extends ProductComposition,
11
+ C extends UiContract,
12
+ S extends ReadonlyArray<unknown>,
13
+ Services,
14
+ P,
15
+ N extends string,
16
+ Identity,
17
+ > {
18
+ readonly requirement: RequirementClass<Comp, string>
19
+ readonly scene: CapturedScene<C, S, Services, P, N, Identity>
20
+ readonly placement: 'Root' | 'Slot'
21
+ readonly body: (app: Facade<C, never>) => ProofGenerator
22
+ }
23
+
24
+ export type ProofCapture<Result> = <
25
+ Comp extends ProductComposition,
26
+ C extends UiContract,
27
+ S extends ReadonlyArray<unknown>,
28
+ Services,
29
+ P,
30
+ N extends string,
31
+ Identity,
32
+ >(
33
+ proof: ProofCase<Comp, C, S, Services, P, N, Identity>,
34
+ ) => Result
35
+
36
+ export interface ProofResult {
37
+ readonly requirement: string
38
+ readonly ok: boolean
39
+ readonly error: string | undefined
40
+ }
41
+
42
+ export interface SuiteResult {
43
+ readonly product: string
44
+ readonly results: ReadonlyArray<ProofResult>
45
+ readonly ok: boolean
46
+ }