@playfast/reform-proof 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { formatFingerprint } from './fingerprint'
3
+
4
+ describe('proof render fingerprints', () => {
5
+ it('distinguishes bigint props from strings without throwing', () => {
6
+ expect(formatFingerprint({ sequence: 1n })).not.toBe(formatFingerprint({ sequence: '1' }))
7
+ })
8
+
9
+ it('stabilizes equivalent cyclic render props', () => {
10
+ const left: Array<unknown> = []
11
+ left.push(left)
12
+ const right: Array<unknown> = []
13
+ right.push(right)
14
+
15
+ expect(formatFingerprint(left)).toBe(formatFingerprint(right))
16
+ })
17
+
18
+ it('tracks Map and Set contents', () => {
19
+ expect(formatFingerprint(new Map([['feature', 'home']]))).not.toBe(
20
+ formatFingerprint(new Map([['feature', 'tasks']])),
21
+ )
22
+ expect(formatFingerprint(new Set(['home']))).not.toBe(formatFingerprint(new Set(['tasks'])))
23
+ })
24
+ })
@@ -0,0 +1,73 @@
1
+ import * as Option from 'effect/Option'
2
+
3
+ const readPropertyOption = Option.liftThrowable((target: object, key: PropertyKey): unknown =>
4
+ Reflect.get(target, key),
5
+ )
6
+
7
+ const readProperty = (target: object, key: PropertyKey): unknown =>
8
+ Option.getOrElse(readPropertyOption(target, key), () => '[Property read threw]')
9
+
10
+ const propertyKeyFingerprint = (key: PropertyKey): string =>
11
+ typeof key === 'symbol'
12
+ ? `symbol:${String(key.description)}`
13
+ : typeof key === 'number'
14
+ ? `number-key:${String(key)}`
15
+ : `key:${key.length}:${key}`
16
+
17
+ const valueFingerprint = (candidate: unknown, seen: WeakSet<object>): string => {
18
+ if (typeof candidate === 'bigint') {
19
+ return `bigint:${String(candidate)}`
20
+ }
21
+ if (typeof candidate === 'boolean') {
22
+ return candidate ? 'boolean:true' : 'boolean:false'
23
+ }
24
+ if (typeof candidate === 'function') {
25
+ return `function:${String(candidate)}`
26
+ }
27
+ if (typeof candidate === 'number') {
28
+ return `number:${String(candidate)}`
29
+ }
30
+ if (typeof candidate === 'string') {
31
+ return `string:${candidate.length}:${candidate}`
32
+ }
33
+ if (typeof candidate === 'symbol') {
34
+ return `symbol:${String(candidate.description)}`
35
+ }
36
+ if (typeof candidate === 'undefined') {
37
+ return 'undefined'
38
+ }
39
+ if (candidate === null) {
40
+ return 'null'
41
+ }
42
+ if (seen.has(candidate)) {
43
+ return '[Circular]'
44
+ }
45
+ seen.add(candidate)
46
+ if (Array.isArray(candidate)) {
47
+ return `[${candidate.map((arrayEntry) => valueFingerprint(arrayEntry, seen)).join(',')}]`
48
+ }
49
+ if (candidate instanceof Date) {
50
+ return `date:${String(candidate.getTime())}`
51
+ }
52
+ if (candidate instanceof Map) {
53
+ return `map:[${Array.from(
54
+ candidate,
55
+ ([mapKey, mapEntry]) =>
56
+ `${valueFingerprint(mapKey, seen)}=>${valueFingerprint(mapEntry, seen)}`,
57
+ ).join(',')}]`
58
+ }
59
+ if (candidate instanceof Set) {
60
+ return `set:[${Array.from(candidate, (setEntry) => valueFingerprint(setEntry, seen)).join(
61
+ ',',
62
+ )}]`
63
+ }
64
+ return `{${Reflect.ownKeys(candidate)
65
+ .map(
66
+ (key) =>
67
+ `${propertyKeyFingerprint(key)}:${valueFingerprint(readProperty(candidate, key), seen)}`,
68
+ )
69
+ .join(',')}}`
70
+ }
71
+
72
+ export const formatFingerprint = (fingerprintInput: unknown): string =>
73
+ valueFingerprint(fingerprintInput, new WeakSet())
package/src/index.ts CHANGED
@@ -1,25 +1,24 @@
1
- import { Effect, type Schema } from 'effect'
1
+ import { Array as Arr, Effect, Option, Record as Rec } from 'effect'
2
2
  import type { YieldWrap } from 'effect/Utils'
3
+ import { AssertionFailed } from './errors'
4
+ import { formatFingerprint } from './fingerprint'
3
5
  import { ProofRunner, type ProofRunnerApi, proofRunnerLayer, withProofRunner } from './runner'
4
- import { type ProductClass, type RequirementClass } from './product'
5
- import type { ProofDriver } from './driverTypes'
6
6
  import type {
7
- CompositionClass,
8
- CompositionService,
7
+ CapturedScene,
9
8
  Scene,
10
- SlotInstance,
9
+ SlotComposition as ReformSlotComposition,
10
+ SlotContract as ReformSlotContract,
11
11
  Trigger,
12
12
  UiContract,
13
13
  } from '@playfast/reform'
14
-
15
- type AnyValue = Schema.Schema.Type<Schema.Schema.Any>
16
- type AnyComposition = CompositionClass<AnyValue, AnyValue>
14
+ import type { AnyComposition, AnyScene, CapturedRender } from '@playfast/reform/internal'
17
15
 
18
16
  export { ProofRunner, proofRunnerLayer, withProofRunner }
19
17
  export type { ProofRunnerApi }
20
18
 
21
19
  export {
22
20
  makeFacade,
21
+ makeProofFacade,
23
22
  makeRuntimeHandleFacade,
24
23
  makeRuntimeTreeFacade,
25
24
  makeSink,
@@ -28,72 +27,272 @@ export {
28
27
  export type {
29
28
  MountedFacade,
30
29
  MountedSlotFacade,
31
- RuntimeServices,
30
+ MountedSlotFacadesOf,
32
31
  RuntimeTreeFacade,
33
32
  Sink,
34
33
  } from './engine'
35
34
 
36
- export {
37
- Product,
38
- type ProductClass,
39
- type ProductManifest,
40
- ProductRequirement,
41
- type RequirementClass,
42
- type RequirementManifest,
43
- } 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>
35
+ export type ProductComposition = AnyComposition & { readonly identity: symbol }
36
+
37
+ const RequirementCompositionTypeId: unique symbol = Symbol.for(
38
+ 'reform-proof/RequirementComposition',
39
+ )
40
+
41
+ export interface AnyRequirementClass {
42
+ new (): {}
43
+ readonly manifest: {
44
+ readonly kind: 'ProductRequirement'
45
+ readonly name: string
46
+ readonly statement: string
47
+ readonly composition: ProductComposition
48
+ readonly events: Option.Option<ReadonlyArray<PropertyKey>>
49
+ }
50
+ readonly capture: <Result>(visit: RequirementCapture<Result>) => Result
51
+ }
52
+
53
+ export type RequirementCapture<Result> = <
54
+ Comp extends ProductComposition,
55
+ Statement extends string,
56
+ >(
57
+ requirement: RequirementClass<Comp, Statement>,
58
+ ) => Result
59
+
60
+ export interface RequirementManifest<Comp extends ProductComposition, Statement extends string> {
61
+ readonly kind: 'ProductRequirement'
62
+ readonly name: Statement
63
+ readonly statement: Statement
64
+ readonly composition: Comp
65
+ readonly events: Option.Option<ReadonlyArray<EventNamesOf<ContractOf<Comp>>>>
66
+ }
67
+
68
+ export interface RequirementClass<
69
+ Comp extends ProductComposition,
70
+ Statement extends string,
71
+ > extends AnyRequirementClass {
72
+ readonly manifest: RequirementManifest<Comp, Statement>
73
+ readonly [RequirementCompositionTypeId]: (composition: Comp) => Comp
74
+ }
75
+
76
+ export interface ProductManifest<Comp extends ProductComposition> {
77
+ readonly kind: 'Product'
78
+ readonly name: string
79
+ readonly composition: Comp
80
+ readonly requirements: ReadonlyArray<AnyRequirementClass>
81
+ }
82
+
83
+ export interface ProductClass<Comp extends ProductComposition = ProductComposition> {
84
+ new (): {}
85
+ readonly manifest: ProductManifest<Comp>
86
+ }
87
+
88
+ interface MakeRequirementOptionsExternalApi<Comp extends ProductComposition> {
89
+ readonly events?: ReadonlyArray<EventNamesOf<ContractOf<Comp>>>
90
+ }
91
+
92
+ const makeRequirement = <Comp extends ProductComposition, const Statement extends string>(
93
+ composition: Comp,
94
+ statement: Statement,
95
+ options?: MakeRequirementOptionsExternalApi<Comp>,
96
+ ): RequirementClass<Comp, Statement> => {
97
+ const manifest: RequirementManifest<Comp, Statement> = {
98
+ kind: 'ProductRequirement',
99
+ name: statement,
100
+ statement,
101
+ composition,
102
+ events: Option.fromNullable(options?.events),
103
+ }
104
+ class Requirement {
105
+ static readonly manifest = manifest
106
+ static readonly [RequirementCompositionTypeId] = (exact: Comp): Comp => exact
107
+ static readonly capture = <Result>(visit: RequirementCapture<Result>): Result =>
108
+ visit(Requirement)
109
+ }
110
+ return Requirement
111
+ }
112
+
113
+ export const ProductRequirement: { readonly make: typeof makeRequirement } = {
114
+ make: makeRequirement,
115
+ }
116
+
117
+ interface MakeProductConfig<Comp extends ProductComposition> {
118
+ readonly requirements: ReadonlyArray<RequirementClass<Comp, string>>
119
+ }
120
+
121
+ const makeProduct = <Comp extends ProductComposition>(
122
+ composition: Comp,
123
+ config: MakeProductConfig<Comp>,
124
+ ): ProductClass<Comp> => {
125
+ const manifest: ProductManifest<Comp> = {
126
+ kind: 'Product',
127
+ name: composition.manifest.name,
128
+ composition,
129
+ requirements: config.requirements,
130
+ }
131
+ return class {
132
+ static readonly manifest = manifest
133
+ }
134
+ }
135
+
136
+ export const Product: { readonly make: typeof makeProduct } = {
137
+ make: makeProduct,
138
+ }
139
+
140
+ interface ComparePair {
141
+ readonly left: unknown
142
+ readonly right: unknown
143
+ }
144
+
145
+ const show = (subject: unknown): string => formatFingerprint(subject)
146
+
147
+ const deepEqual = ({ left, right }: ComparePair): boolean =>
148
+ Object.is(left, right) || show(left) === show(right)
149
+
150
+ const fail = (message: string): Effect.Effect<never> =>
151
+ Effect.dieMessage(new AssertionFailed({ detail: message }).message)
152
+
153
+ export const expect = <A>(actual: A) => ({
154
+ toBe: (expected: A): Effect.Effect<void> =>
155
+ Object.is(actual, expected)
156
+ ? Effect.void
157
+ : fail(`expected ${show(actual)} to be ${show(expected)}`),
158
+ toEqual: (expected: A): Effect.Effect<void> =>
159
+ deepEqual({ left: actual, right: expected })
160
+ ? Effect.void
161
+ : fail(`expected ${show(actual)} to equal ${show(expected)}`),
162
+ toContain: (expected: A extends ReadonlyArray<infer E> ? E : unknown): Effect.Effect<void> =>
163
+ Array.isArray(actual) && actual.some((element) => deepEqual({ left: element, right: expected }))
164
+ ? Effect.void
165
+ : fail(`expected ${show(actual)} to contain ${show(expected)}`),
166
+ toMatchObject: (expected: Partial<A>): Effect.Effect<void> => {
167
+ const mismatch = matchPartial({ actual, expected })
168
+ return mismatch === undefined ? Effect.void : fail(mismatch)
169
+ },
170
+ })
171
+
172
+ const isRecord = (candidate: unknown): candidate is Record<string, unknown> =>
173
+ typeof candidate === 'object' && candidate !== null
174
+
175
+ interface MatchInput {
176
+ readonly actual: unknown
177
+ readonly expected: unknown
178
+ }
179
+
180
+ const matchPartial = ({ actual, expected }: MatchInput): string | undefined => {
181
+ if (!isRecord(expected)) {
182
+ return deepEqual({ left: actual, right: expected })
183
+ ? undefined
184
+ : `expected ${show(actual)} to match ${show(expected)}`
185
+ }
186
+ if (!isRecord(actual)) {
187
+ return `expected ${show(actual)} to be an object matching ${show(expected)}`
188
+ }
189
+ const mismatch = Arr.findFirst(
190
+ Rec.keys(expected),
191
+ (key) => !deepEqual({ left: actual[key], right: expected[key] }),
192
+ )
193
+ return Option.match(mismatch, {
194
+ onNone: () => undefined,
195
+ onSome: (key) =>
196
+ `expected key ${show(key)} to match ${show(expected[key])}, got ${show(actual[key])}`,
197
+ })
198
+ }
199
+
200
+ export const matchProps: (input: MatchInput) => string | undefined = matchPartial
201
+
202
+ export type EventsOf<C extends UiContract> = C extends {
203
+ readonly events: infer E extends NonNullable<UiContract['events']>
204
+ }
205
+ ? E
206
+ : Readonly<Record<PropertyKey, never>>
207
+ type EventNamesOf<C extends UiContract> = C extends {
208
+ readonly events: NonNullable<UiContract['events']>
209
+ }
210
+ ? keyof EventsOf<C>
211
+ : never
212
+ type SlotsOf<C extends UiContract> = C extends {
213
+ readonly slots: infer S extends NonNullable<UiContract['slots']>
214
+ }
215
+ ? S
216
+ : Readonly<Record<PropertyKey, never>>
49
217
  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
218
+ export type ContractOf<Comp> = Comp extends {
219
+ readonly Contract: infer C extends UiContract
220
+ }
221
+ ? C
222
+ : never
223
+ type ContractOfSlot<S> = ReformSlotContract<S>
52
224
 
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>
225
+ export type ActionsOf<C extends UiContract, R = never> = C extends {
226
+ readonly events: NonNullable<UiContract['events']>
57
227
  }
58
- export type SlotFacadesOf<C extends UiContract> = {
59
- readonly [K in keyof SlotsOf<C>]: SlotFacade<ContractOfSlot<SlotsOf<C>[K]>>
228
+ ? {
229
+ readonly [K in keyof EventsOf<C>]: (
230
+ payload: PayloadOf<EventsOf<C>[K]>,
231
+ ) => Effect.Effect<C['props'], never, R>
232
+ }
233
+ : Readonly<Record<PropertyKey, never>>
234
+ export type SlotFacadesOf<C extends UiContract, R = never> = C extends {
235
+ readonly slots: NonNullable<UiContract['slots']>
60
236
  }
237
+ ? {
238
+ readonly [K in keyof SlotsOf<C>]: SlotFacade<ContractOfSlot<SlotsOf<C>[K]>, R>
239
+ }
240
+ : Readonly<Record<PropertyKey, never>>
61
241
 
62
- export type Action = (payload: unknown) => Effect.Effect<unknown, never, CompositionService>
242
+ export type Action<R = never> = (payload: unknown) => Effect.Effect<unknown, never, R>
63
243
 
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>
244
+ export interface Facade<C extends UiContract, R = never> {
245
+ readonly props: Effect.Effect<C['props'], never, R>
246
+ readonly expectProps: (partial: Partial<C['props']>) => Effect.Effect<void, never, R>
247
+ readonly actions: ActionsOf<C, R>
248
+ readonly slots: SlotFacadesOf<C, R>
249
+ readonly frame: Effect.Effect<void, never, R>
70
250
  }
71
251
 
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>
252
+ export interface SlotFacade<C extends UiContract, R = never> extends Facade<C, R> {
253
+ readonly first: Effect.Effect<Facade<C, R>, never, R>
254
+ readonly at: (index: number) => Effect.Effect<Facade<C, R>, never, R>
255
+ readonly all: Effect.Effect<ReadonlyArray<Facade<C, R>>, never, R>
256
+ readonly byKey: (key: string) => Effect.Effect<Facade<C, R>, never, R>
77
257
  readonly where: (
78
258
  predicate: (props: C['props']) => boolean,
79
- ) => Effect.Effect<ReadonlyArray<Facade<C>>, never, CompositionService>
80
- readonly count: Effect.Effect<number, never, CompositionService>
259
+ ) => Effect.Effect<ReadonlyArray<Facade<C, R>>, never, R>
260
+ readonly count: Effect.Effect<number, never, R>
81
261
  }
82
262
 
83
- type ProofGenerator = Generator<
84
- YieldWrap<Effect.Effect<unknown, unknown, CompositionService>>,
85
- void,
86
- unknown
87
- >
263
+ type ProofGenerator = Generator<YieldWrap<Effect.Effect<unknown, unknown, never>>, void, unknown>
88
264
 
89
- type AnyFacade = Facade<UiContract>
265
+ export interface ProofCase<
266
+ Comp extends ProductComposition,
267
+ C extends UiContract,
268
+ S extends ReadonlyArray<unknown>,
269
+ Services,
270
+ P,
271
+ N extends string,
272
+ Identity,
273
+ > {
274
+ readonly requirement: RequirementClass<Comp, string>
275
+ readonly scene: CapturedScene<C, S, Services, P, N, Identity>
276
+ readonly placement: 'Root' | 'Slot'
277
+ readonly body: (app: Facade<C, never>) => ProofGenerator
278
+ }
90
279
 
91
- export type ProofBody = (app: AnyFacade) => ProofGenerator
280
+ export type ProofCapture<Result> = <
281
+ Comp extends ProductComposition,
282
+ C extends UiContract,
283
+ S extends ReadonlyArray<unknown>,
284
+ Services,
285
+ P,
286
+ N extends string,
287
+ Identity,
288
+ >(
289
+ proof: ProofCase<Comp, C, S, Services, P, N, Identity>,
290
+ ) => Result
92
291
 
93
292
  export interface Proof {
94
- readonly requirement: RequirementClass<AnyComposition, string>
95
- readonly scene: Scene
96
- readonly body: ProofBody
293
+ readonly requirement: AnyRequirementClass
294
+ readonly scene: AnyScene
295
+ readonly capture: <Result>(visit: ProofCapture<Result>) => Result
97
296
  }
98
297
 
99
298
  export interface ProofSuite {
@@ -102,14 +301,10 @@ export interface ProofSuite {
102
301
  readonly proofs: ReadonlyArray<Proof>
103
302
  }
104
303
 
105
- const isRecord = (candidate: unknown): candidate is Record<string, unknown> =>
106
- typeof candidate === 'object' && candidate !== null
107
-
108
304
  export interface ProofResult {
109
305
  readonly requirement: string
110
306
  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
307
+ readonly error: string | undefined
113
308
  }
114
309
 
115
310
  export interface SuiteResult {
@@ -118,29 +313,78 @@ export interface SuiteResult {
118
313
  readonly ok: boolean
119
314
  }
120
315
 
121
- const implement = <Comp extends AnyComposition>(
316
+ const implement = <Comp extends ProductComposition>(
122
317
  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 }
318
+ scene: Scene<ContractOf<NoInfer<Comp>>, NoInfer<Comp>['States']>,
319
+ body: (app: Facade<ContractOf<Comp>, never>) => ProofGenerator,
320
+ ): Proof =>
321
+ scene.capture(
322
+ <Services, P, N extends string, Identity>(
323
+ exactScene: CapturedScene<ContractOf<Comp>, Comp['States'], Services, P, N, Identity>,
324
+ ): Proof => {
325
+ const proofCase: ProofCase<
326
+ Comp,
327
+ ContractOf<Comp>,
328
+ Comp['States'],
329
+ Services,
330
+ P,
331
+ N,
332
+ Identity
333
+ > = {
334
+ requirement,
335
+ scene: exactScene,
336
+ placement: 'Root',
337
+ body,
338
+ }
339
+ return {
340
+ requirement,
341
+ scene: exactScene,
342
+ capture: (visit) =>
343
+ visit<Comp, ContractOf<Comp>, Comp['States'], Services, P, N, Identity>(proofCase),
344
+ }
345
+ },
346
+ )
347
+
348
+ type SlotsHolding<C extends UiContract, Target extends ProductComposition> = C extends {
349
+ readonly slots: NonNullable<UiContract['slots']>
129
350
  }
351
+ ? {
352
+ [K in keyof SlotsOf<C>]: [ReformSlotComposition<SlotsOf<C>[K]>] extends [Target] ? K : never
353
+ }[keyof SlotsOf<C>]
354
+ : never
130
355
 
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>]
356
+ type SceneHolding<
357
+ Comp extends ProductComposition,
358
+ C extends UiContract,
359
+ S extends ReadonlyArray<unknown>,
360
+ > = [SlotsHolding<C, Comp>] extends [never] ? never : Scene<C, S>
134
361
 
135
- const implementVia = <Comp extends AnyComposition, C extends UiContract>(
362
+ const implementVia = <
363
+ Comp extends ProductComposition,
364
+ C extends UiContract,
365
+ S extends ReadonlyArray<unknown>,
366
+ >(
136
367
  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
- }
368
+ scene: SceneHolding<NoInfer<Comp>, C, S>,
369
+ body: (app: Facade<C, never>) => ProofGenerator,
370
+ ): Proof =>
371
+ scene.capture(
372
+ <Services, P, N extends string, Identity>(
373
+ exactScene: CapturedScene<C, S, Services, P, N, Identity>,
374
+ ): Proof => {
375
+ const proofCase: ProofCase<Comp, C, S, Services, P, N, Identity> = {
376
+ requirement,
377
+ scene: exactScene,
378
+ placement: 'Slot',
379
+ body,
380
+ }
381
+ return {
382
+ requirement,
383
+ scene: exactScene,
384
+ capture: (visit) => visit<Comp, C, S, Services, P, N, Identity>(proofCase),
385
+ }
386
+ },
387
+ )
144
388
 
145
389
  interface MakeSuiteConfig {
146
390
  readonly proofs: ReadonlyArray<Proof>
@@ -156,18 +400,38 @@ export const isProofSuite = (candidate: unknown): candidate is ProofSuite =>
156
400
  isRecord(candidate) && candidate['kind'] === 'ProofSuite' && Array.isArray(candidate['proofs'])
157
401
 
158
402
  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 {
403
+ Effect.runPromise(runEffect(proofSuite))
404
+
405
+ const runEffect = (proofSuite: ProofSuite): Effect.Effect<SuiteResult, never, never> =>
406
+ ProofRunner.pipe(
407
+ Effect.flatMap((runner) =>
408
+ Effect.forEach(proofSuite.proofs, (proof) => runner.executeProofEffect(proof)),
409
+ ),
410
+ Effect.map((reports) => ({
164
411
  product: proofSuite.product.manifest.name,
165
412
  results: reports,
166
413
  ok: reports.every((report) => report.ok),
167
- }
168
- })
414
+ })),
415
+ Effect.provide(proofRunnerLayer),
416
+ )
417
+
418
+ export interface StepFrame {
419
+ readonly index: number
420
+ readonly captures: ReadonlyArray<CapturedRender>
421
+ }
422
+
423
+ export interface DriveResult {
424
+ readonly requirement: string
425
+ readonly frames: ReadonlyArray<StepFrame>
426
+ readonly ok: boolean
427
+ readonly error: string | undefined
428
+ }
429
+
430
+ export interface ProofDriver {
431
+ readonly requirement: string
432
+ readonly run: () => Promise<DriveResult>
433
+ }
169
434
 
170
- export type { DriveResult, ProofDriver, StepFrame } from './driverTypes'
171
435
  const driver = (proofSuite: ProofSuite): ReadonlyArray<ProofDriver> =>
172
436
  proofSuite.proofs.map((proof) => ({
173
437
  requirement: proof.requirement.manifest.statement,
@@ -179,5 +443,6 @@ export const Proof: {
179
443
  readonly implementVia: typeof implementVia
180
444
  readonly suite: typeof suite
181
445
  readonly run: typeof run
446
+ readonly runEffect: typeof runEffect
182
447
  readonly driver: typeof driver
183
- } = { implement, implementVia, suite, run, driver }
448
+ } = { implement, implementVia, suite, run, runEffect, driver }