@playfast/reform-proof 1.0.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.
package/src/index.ts CHANGED
@@ -1,88 +1,95 @@
1
1
  import { Array as Arr, Effect, Option, Record as Rec } from 'effect'
2
2
  import type { YieldWrap } from 'effect/Utils'
3
3
  import { AssertionFailed } from './errors'
4
+ import { formatFingerprint } from './fingerprint'
4
5
  import { ProofRunner, type ProofRunnerApi, proofRunnerLayer, withProofRunner } from './runner'
5
6
  import type {
6
- CompositionClass,
7
- CompositionService,
7
+ CapturedScene,
8
8
  Scene,
9
- SlotInstance,
9
+ SlotComposition as ReformSlotComposition,
10
+ SlotContract as ReformSlotContract,
10
11
  Trigger,
11
- UiCapture,
12
12
  UiContract,
13
13
  } from '@playfast/reform'
14
+ import type { AnyComposition, AnyScene, CapturedRender } from '@playfast/reform/internal'
14
15
 
15
- // reform-proof — the stability story for AI authorship. Product behaviour is
16
- // declared as human-readable requirements (the definition) and proven by tests
17
- // (the implementation), the same split as the rest of reform. A proof drives the
18
- // real reduce loop through a headless FACADE: no DOM, no text matching. It reads
19
- // the props each composition computed and calls the contract's events as
20
- // callbacks — exactly what the production UI receives. HOW a proof runs is the
21
- // injectable `ProofRunner` layer (./runner); the headless engine (./engine) is its
22
- // default. `Proof.run`/`Proof.driver` resolve that layer, so the vitest adapter
23
- // can drive the very same proofs as native tests.
24
-
25
- // The injectable execution seam is re-exported so adapters can resolve it.
26
16
  export { ProofRunner, proofRunnerLayer, withProofRunner }
27
17
  export type { ProofRunnerApi }
28
18
 
29
- // Engine SPI consumed by @playfast/reform-drive — the no-ceremony scene driver builds
30
- // on the same facade engine. Re-exported from the package index (not the engine
31
- // subpath) so consumers load the package in its normal init order.
32
- export { makeFacade, makeSink, proofLayer } from './engine'
33
- export type { RuntimeServices, Sink } from './engine'
34
-
35
- // ---------------------------------------------------------------------------
36
- // Definitions: ProductRequirement + Product
37
- // ---------------------------------------------------------------------------
19
+ export {
20
+ makeFacade,
21
+ makeProofFacade,
22
+ makeRuntimeHandleFacade,
23
+ makeRuntimeTreeFacade,
24
+ makeSink,
25
+ proofLayer,
26
+ } from './engine'
27
+ export type {
28
+ MountedFacade,
29
+ MountedSlotFacade,
30
+ MountedSlotFacadesOf,
31
+ RuntimeTreeFacade,
32
+ Sink,
33
+ } from './engine'
34
+
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
+ }
38
52
 
39
- /** Any composition, used where the contract type is irrelevant. */
40
- type AnyComposition = CompositionClass<any, any>
53
+ export type RequirementCapture<Result> = <
54
+ Comp extends ProductComposition,
55
+ Statement extends string,
56
+ >(
57
+ requirement: RequirementClass<Comp, Statement>,
58
+ ) => Result
41
59
 
42
- export interface RequirementManifest<Comp extends AnyComposition, Statement extends string> {
60
+ export interface RequirementManifest<Comp extends ProductComposition, Statement extends string> {
43
61
  readonly kind: 'ProductRequirement'
44
62
  readonly name: Statement
45
- /** The human-readable behavioural statement; doubles as the manifest name. */
46
63
  readonly statement: Statement
47
- /** The composition this requirement specifies — its contract types the facade. */
48
64
  readonly composition: Comp
49
- /**
50
- * Optional: the contract events this requirement exercises. Typed to the
51
- * composition's event names, and verified at run — a proof that never
52
- * dispatches a declared event fails, so the requirement and its proof can't
53
- * silently drift (the definition→implementation link past the statement string).
54
- */
55
- readonly events: Option.Option<ReadonlyArray<keyof EventsOf<ContractOf<Comp>>>>
65
+ readonly events: Option.Option<ReadonlyArray<EventNamesOf<ContractOf<Comp>>>>
56
66
  }
57
67
 
58
- export interface RequirementClass<Comp extends AnyComposition, Statement extends string> {
59
- new (): {}
68
+ export interface RequirementClass<
69
+ Comp extends ProductComposition,
70
+ Statement extends string,
71
+ > extends AnyRequirementClass {
60
72
  readonly manifest: RequirementManifest<Comp, Statement>
73
+ readonly [RequirementCompositionTypeId]: (composition: Comp) => Comp
61
74
  }
62
75
 
63
- export interface ProductManifest<Comp extends AnyComposition> {
76
+ export interface ProductManifest<Comp extends ProductComposition> {
64
77
  readonly kind: 'Product'
65
78
  readonly name: string
66
79
  readonly composition: Comp
67
- readonly requirements: ReadonlyArray<RequirementClass<Comp, string>>
80
+ readonly requirements: ReadonlyArray<AnyRequirementClass>
68
81
  }
69
82
 
70
- export interface ProductClass<Comp extends AnyComposition = AnyComposition> {
83
+ export interface ProductClass<Comp extends ProductComposition = ProductComposition> {
71
84
  new (): {}
72
85
  readonly manifest: ProductManifest<Comp>
73
86
  }
74
87
 
75
- /**
76
- * A named, human-readable behavioural statement bound to the composition it
77
- * specifies. The composition types the proof facade; the statement's literal
78
- * type is re-stated (and enforced) at `Proof.implement`.
79
- */
80
- /** Author-facing config for `ProductRequirement.make` — plain JSON the proof author passes. */
81
- interface MakeRequirementOptionsExternalApi<Comp extends AnyComposition> {
82
- readonly events?: ReadonlyArray<keyof EventsOf<ContractOf<Comp>>>
88
+ interface MakeRequirementOptionsExternalApi<Comp extends ProductComposition> {
89
+ readonly events?: ReadonlyArray<EventNamesOf<ContractOf<Comp>>>
83
90
  }
84
91
 
85
- const makeRequirement = <Comp extends AnyComposition, const Statement extends string>(
92
+ const makeRequirement = <Comp extends ProductComposition, const Statement extends string>(
86
93
  composition: Comp,
87
94
  statement: Statement,
88
95
  options?: MakeRequirementOptionsExternalApi<Comp>,
@@ -94,24 +101,24 @@ const makeRequirement = <Comp extends AnyComposition, const Statement extends st
94
101
  composition,
95
102
  events: Option.fromNullable(options?.events),
96
103
  }
97
- return class {
104
+ class Requirement {
98
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)
99
109
  }
110
+ return Requirement
100
111
  }
101
112
 
102
113
  export const ProductRequirement: { readonly make: typeof makeRequirement } = {
103
114
  make: makeRequirement,
104
115
  }
105
116
 
106
- /**
107
- * Group a composition with the full list of requirements that specify it. The
108
- * shared `Comp` type-checks that every requirement targets this composition.
109
- */
110
- interface MakeProductConfig<Comp extends AnyComposition> {
117
+ interface MakeProductConfig<Comp extends ProductComposition> {
111
118
  readonly requirements: ReadonlyArray<RequirementClass<Comp, string>>
112
119
  }
113
120
 
114
- const makeProduct = <Comp extends AnyComposition>(
121
+ const makeProduct = <Comp extends ProductComposition>(
115
122
  composition: Comp,
116
123
  config: MakeProductConfig<Comp>,
117
124
  ): ProductClass<Comp> => {
@@ -126,22 +133,16 @@ const makeProduct = <Comp extends AnyComposition>(
126
133
  }
127
134
  }
128
135
 
129
- export const Product: { readonly make: typeof makeProduct } = { make: makeProduct }
130
-
131
- // ---------------------------------------------------------------------------
132
- // Assertions — Effect-returning matchers; a failed match fails the proof.
133
- // ---------------------------------------------------------------------------
136
+ export const Product: { readonly make: typeof makeProduct } = {
137
+ make: makeProduct,
138
+ }
134
139
 
135
- /** Two values to deep-compare — one options object, not positional primitives. */
136
140
  interface ComparePair {
137
141
  readonly left: unknown
138
142
  readonly right: unknown
139
143
  }
140
144
 
141
- // Display + structural-compare seam: assertion messages and deep-equality operate on
142
- // arbitrary `unknown` values, which Schema cannot encode — JSON is the right tool here.
143
- // oxlint-disable-next-line reform-rules/no-json-parse-stringify -- arbitrary unknown assertion values, not Schema-typed data
144
- const show = (subject: unknown): string => JSON.stringify(subject)
145
+ const show = (subject: unknown): string => formatFingerprint(subject)
145
146
 
146
147
  const deepEqual = ({ left, right }: ComparePair): boolean =>
147
148
  Object.is(left, right) || show(left) === show(right)
@@ -168,21 +169,14 @@ export const expect = <A>(actual: A) => ({
168
169
  },
169
170
  })
170
171
 
171
- /** A non-null object as an indexable record — the narrowing `unknown` doesn't give. */
172
172
  const isRecord = (candidate: unknown): candidate is Record<string, unknown> =>
173
173
  typeof candidate === 'object' && candidate !== null
174
174
 
175
- /** The pair `matchPartial`/`matchProps` deep-matches: `actual` against the `expected` subset. */
176
175
  interface MatchInput {
177
176
  readonly actual: unknown
178
177
  readonly expected: unknown
179
178
  }
180
179
 
181
- /**
182
- * Deep-match every key of `expected` against `actual`; returns an error message
183
- * naming the first failing key, or `undefined` on a full match. Shared by
184
- * `expect(...).toMatchObject` and the facade's `expectProps`.
185
- */
186
180
  const matchPartial = ({ actual, expected }: MatchInput): string | undefined => {
187
181
  if (!isRecord(expected)) {
188
182
  return deepEqual({ left: actual, right: expected })
@@ -203,117 +197,105 @@ const matchPartial = ({ actual, expected }: MatchInput): string | undefined => {
203
197
  })
204
198
  }
205
199
 
206
- /** Internal: the facade's `expectProps` reuses the same partial-match logic. */
207
200
  export const matchProps: (input: MatchInput) => string | undefined = matchPartial
208
201
 
209
- // ---------------------------------------------------------------------------
210
- // Facade a headless view over the live composition tree
211
- // ---------------------------------------------------------------------------
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>>
217
+ type PayloadOf<T> = T extends Trigger<infer P> ? P : 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>
212
224
 
213
- // --- Contract projection: derive the facade's exact shape from a UI contract ---
225
+ export type ActionsOf<C extends UiContract, R = never> = C extends {
226
+ readonly events: NonNullable<UiContract['events']>
227
+ }
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']>
236
+ }
237
+ ? {
238
+ readonly [K in keyof SlotsOf<C>]: SlotFacade<ContractOfSlot<SlotsOf<C>[K]>, R>
239
+ }
240
+ : Readonly<Record<PropertyKey, never>>
214
241
 
215
- /** The event payloads of a contract, keyed by event name. */
216
- type EventsOf<C extends UiContract> = C extends { events: infer E } ? E : Record<never, never>
217
- /** The slot instances of a contract, keyed by slot name. */
218
- type SlotsOf<C extends UiContract> = C extends { slots: infer S } ? S : Record<never, never>
219
- /** The payload a trigger accepts. */
220
- type PayloadOf<T> = T extends Trigger<infer P> ? P : never
221
- /** The UI contract a composition resolves. */
222
- export type ContractOf<Comp> = Comp extends CompositionClass<any, infer C> ? C : never
223
- /** The child contract a slot stands for — the contract of the composition it holds. */
224
- type ContractOfSlot<S> = S extends SlotInstance<infer Comp> ? ContractOf<Comp> : never
225
-
226
- /**
227
- * The contract's events as facade actions: payload in, dispatch-and-settle Effect
228
- * out. The Effect resolves to the props this node computed *after* the dispatch
229
- * settled the proof analog of chat-tests' `emitToolCall → ToolOutput`: in a
230
- * fire-and-forget reduce loop the typed "result" of an event is the next state.
231
- */
232
- export type ActionsOf<C extends UiContract> = {
233
- readonly [K in keyof EventsOf<C>]: (
234
- payload: PayloadOf<EventsOf<C>[K]>,
235
- ) => Effect.Effect<C['props'], never, CompositionService>
236
- }
237
- /** The contract's slots as child facades, each typed by the child's own contract. */
238
- export type SlotFacadesOf<C extends UiContract> = {
239
- readonly [K in keyof SlotsOf<C>]: SlotFacade<ContractOfSlot<SlotsOf<C>[K]>>
240
- }
241
-
242
- /** The Effect a facade action returns: dispatch the event, settle, read props. */
243
- export type Action = (payload: unknown) => Effect.Effect<unknown, never, CompositionService>
244
-
245
- /**
246
- * The handle a proof drives — the same surface the production UI receives, fully
247
- * typed from the composition's contract `C`: read state (`props`), trigger events
248
- * (`actions`), reach children (`slots`), and wait for the next frame (`frame`).
249
- * Each access yields a real Effect/SlotFacade against the running engine.
250
- */
251
- export interface Facade<C extends UiContract> {
252
- /** Re-render the tree and read the props this node last computed. */
253
- readonly props: Effect.Effect<C['props'], never, CompositionService>
254
- /**
255
- * Re-render and assert the computed props match `partial` (a subset, deep).
256
- * Typed from the contract, so a mistyped or unknown prop key is a compile
257
- * error — unlike reading `props` and comparing a free-form object.
258
- */
259
- readonly expectProps: (partial: Partial<C['props']>) => Effect.Effect<void, never, CompositionService>
260
- /** The contract's events as callables; calling one dispatches and settles. */
261
- readonly actions: ActionsOf<C>
262
- /** Child composition facades, keyed by slot name. */
263
- readonly slots: SlotFacadesOf<C>
264
- /** Settle the engine and re-render — wait for the next stable frame. */
265
- readonly frame: Effect.Effect<void, never, CompositionService>
266
- }
267
-
268
- /** A slot may hold many instances (a list); it is also usable as its first one. */
269
- export interface SlotFacade<C extends UiContract> extends Facade<C> {
270
- readonly first: Effect.Effect<Facade<C>, never, CompositionService>
271
- readonly at: (index: number) => Effect.Effect<Facade<C>, never, CompositionService>
272
- readonly all: Effect.Effect<ReadonlyArray<Facade<C>>, never, CompositionService>
273
- /**
274
- * Select the one child mounted under `key` — the `each` item key the structure
275
- * carried (the SINGLE source of the wire key and the per-item family key, so it
276
- * can't drift from what the view places). The returned facade is itself typed by
277
- * the child contract `C`, so `.slots` keeps descending type-safely:
278
- * `app.slots.List.slots.Item.byKey('todo-1').slots…`. Fails the proof
279
- * (`UnknownSlot`) when no fill carries the key.
280
- */
281
- readonly byKey: (key: string) => Effect.Effect<Facade<C>, never, CompositionService>
282
- /** Every child whose computed props satisfy `predicate` — the structure-driven
283
- * analog of a query, resolved against this frame's fills (props typed by `C`). */
242
+ export type Action<R = never> = (payload: unknown) => Effect.Effect<unknown, never, R>
243
+
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>
250
+ }
251
+
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>
284
257
  readonly where: (
285
258
  predicate: (props: C['props']) => boolean,
286
- ) => Effect.Effect<ReadonlyArray<Facade<C>>, never, CompositionService>
287
- /** How many children this slot mounted this frame (the fill length). */
288
- readonly count: Effect.Effect<number, never, CompositionService>
259
+ ) => Effect.Effect<ReadonlyArray<Facade<C, R>>, never, R>
260
+ readonly count: Effect.Effect<number, never, R>
289
261
  }
290
262
 
291
- // ---------------------------------------------------------------------------
292
- // Proofs + suite
293
- // ---------------------------------------------------------------------------
294
-
295
- /** The generator a proof body produces — its yielded effects run on the engine. */
296
- type ProofGenerator = Generator<
297
- YieldWrap<Effect.Effect<unknown, unknown, CompositionService>>,
298
- void,
299
- unknown
300
- >
301
-
302
- /** An erased facade — the runtime shape before the contract type is re-attached. */
303
- type AnyFacade = Facade<UiContract>
263
+ type ProofGenerator = Generator<YieldWrap<Effect.Effect<unknown, unknown, never>>, void, unknown>
264
+
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
+ }
304
279
 
305
- /** The erased body stored on a `Proof` value; `implement` types the contract in. */
306
- 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
307
291
 
308
292
  export interface Proof {
309
- readonly requirement: RequirementClass<AnyComposition, string>
310
- /** The scene this proof runs against: its closed wiring + boot events. */
311
- readonly scene: Scene
312
- readonly body: ProofBody
293
+ readonly requirement: AnyRequirementClass
294
+ readonly scene: AnyScene
295
+ readonly capture: <Result>(visit: ProofCapture<Result>) => Result
313
296
  }
314
297
 
315
298
  export interface ProofSuite {
316
- /** Brand so adapters can duck-type a suite among a module's exports. */
317
299
  readonly kind: 'ProofSuite'
318
300
  readonly product: ProductClass
319
301
  readonly proofs: ReadonlyArray<Proof>
@@ -322,8 +304,7 @@ export interface ProofSuite {
322
304
  export interface ProofResult {
323
305
  readonly requirement: string
324
306
  readonly ok: boolean
325
- // 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
326
- readonly error?: string
307
+ readonly error: string | undefined
327
308
  }
328
309
 
329
310
  export interface SuiteResult {
@@ -332,84 +313,125 @@ export interface SuiteResult {
332
313
  readonly ok: boolean
333
314
  }
334
315
 
335
- /**
336
- * Implement (prove) a requirement by driving the facade against a scene. A proof
337
- * is a VALUE. The scene supplies the runtime (its closed `provide` + `boot`) and
338
- * the composition that types the facade; its contract must equal the
339
- * requirement's composition contract, so a scene for the wrong composition — or
340
- * one whose contract has drifted — is a compile error. The `app` facade is fully
341
- * typed from that contract.
342
- */
343
- const implement = <Comp extends AnyComposition>(
316
+ const implement = <Comp extends ProductComposition>(
344
317
  requirement: RequirementClass<Comp, string>,
345
- scene: Scene<ContractOf<Comp>>,
346
- body: (app: Facade<ContractOf<Comp>>) => ProofGenerator,
347
- ): Proof => {
348
- // oxlint-disable-next-line reform-rules/no-type-assertion -- contract erasure seam: Facade<C> → AnyFacade is contravariant, sound by construction
349
- const erased = body as ProofBody
350
- 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']>
351
350
  }
351
+ ? {
352
+ [K in keyof SlotsOf<C>]: [ReformSlotComposition<SlotsOf<C>[K]>] extends [Target] ? K : never
353
+ }[keyof SlotsOf<C>]
354
+ : never
355
+
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>
361
+
362
+ const implementVia = <
363
+ Comp extends ProductComposition,
364
+ C extends UiContract,
365
+ S extends ReadonlyArray<unknown>,
366
+ >(
367
+ requirement: RequirementClass<Comp, string>,
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
+ )
352
388
 
353
- /** The proofs a `Proof.suite` binds to its product. */
354
389
  interface MakeSuiteConfig {
355
390
  readonly proofs: ReadonlyArray<Proof>
356
391
  }
357
392
 
358
- /** Compose proofs with the product whose requirements they prove. */
359
393
  const suite = (product: ProductClass, config: MakeSuiteConfig): ProofSuite => ({
360
394
  kind: 'ProofSuite',
361
395
  product,
362
396
  proofs: config.proofs,
363
397
  })
364
398
 
365
- /** Duck-type a `ProofSuite` among arbitrary module exports (the adapter's seam). */
366
399
  export const isProofSuite = (candidate: unknown): candidate is ProofSuite =>
367
400
  isRecord(candidate) && candidate['kind'] === 'ProofSuite' && Array.isArray(candidate['proofs'])
368
401
 
369
- /**
370
- * Run every proof against a fresh runtime, returning per-requirement results.
371
- * Each proof gets its own environment, so nothing leaks between proofs. Execution
372
- * goes through the injected `ProofRunner` (the headless engine by default).
373
- */
374
402
  const run = (proofSuite: ProofSuite): Promise<SuiteResult> =>
375
- withProofRunner(async (runner: ProofRunnerApi) => {
376
- const reports = await Effect.runPromise(
377
- Effect.forEach(proofSuite.proofs, (proof) => Effect.promise(() => runner.executeProof(proof))),
378
- )
379
- 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) => ({
380
411
  product: proofSuite.product.manifest.name,
381
412
  results: reports,
382
413
  ok: reports.every((report) => report.ok),
383
- }
384
- })
385
-
386
- // ---------------------------------------------------------------------------
387
- // Driver: step a proof for the editor's Test-play timeline
388
- // ---------------------------------------------------------------------------
414
+ })),
415
+ Effect.provide(proofRunnerLayer),
416
+ )
389
417
 
390
- /** One step of a driven proof: the rendered tree captured right after the
391
- * proof's i-th yielded effect settled. Index 0 is the booted, pre-drive tree. */
392
418
  export interface StepFrame {
393
419
  readonly index: number
394
- readonly captures: ReadonlyArray<UiCapture>
420
+ readonly captures: ReadonlyArray<CapturedRender>
395
421
  }
396
422
 
397
423
  export interface DriveResult {
398
424
  readonly requirement: string
399
425
  readonly frames: ReadonlyArray<StepFrame>
400
426
  readonly ok: boolean
401
- // oxlint-disable-next-line reform-rules/no-optional-fields -- serialized result DTO read as `error ?? …` by the editor timeline (external); optional kept for wire compat
402
- readonly error?: string
427
+ readonly error: string | undefined
403
428
  }
404
429
 
405
430
  export interface ProofDriver {
406
431
  readonly requirement: string
407
- /** Run the proof, snapshotting the captured tree after each yielded step. */
408
432
  readonly run: () => Promise<DriveResult>
409
433
  }
410
434
 
411
- /** A driver per proof in the suite — the editor's Test-play entry point. Each
412
- * `run` resolves the same `ProofRunner` layer and steps that proof. */
413
435
  const driver = (proofSuite: ProofSuite): ReadonlyArray<ProofDriver> =>
414
436
  proofSuite.proofs.map((proof) => ({
415
437
  requirement: proof.requirement.manifest.statement,
@@ -418,7 +440,9 @@ const driver = (proofSuite: ProofSuite): ReadonlyArray<ProofDriver> =>
418
440
 
419
441
  export const Proof: {
420
442
  readonly implement: typeof implement
443
+ readonly implementVia: typeof implementVia
421
444
  readonly suite: typeof suite
422
445
  readonly run: typeof run
446
+ readonly runEffect: typeof runEffect
423
447
  readonly driver: typeof driver
424
- } = { implement, suite, run, driver }
448
+ } = { implement, implementVia, suite, run, runEffect, driver }