@playfast/reform-proof 0.1.0 → 1.1.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,324 +1,110 @@
1
- import { Array as Arr, Effect, Option, Record as Rec } from 'effect'
1
+ import { Effect, type Schema } from 'effect'
2
2
  import type { YieldWrap } from 'effect/Utils'
3
- import { AssertionFailed } from './errors'
4
3
  import { ProofRunner, type ProofRunnerApi, proofRunnerLayer, withProofRunner } from './runner'
4
+ import { type ProductClass, type RequirementClass } from './product'
5
+ import type { ProofDriver } from './driverTypes'
5
6
  import type {
6
7
  CompositionClass,
7
8
  CompositionService,
8
9
  Scene,
9
10
  SlotInstance,
10
11
  Trigger,
11
- UiCapture,
12
12
  UiContract,
13
13
  } from '@playfast/reform'
14
14
 
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.
15
+ type AnyValue = Schema.Schema.Type<Schema.Schema.Any>
16
+ type AnyComposition = CompositionClass<AnyValue, AnyValue>
24
17
 
25
- // The injectable execution seam is re-exported so adapters can resolve it.
26
18
  export { ProofRunner, proofRunnerLayer, withProofRunner }
27
19
  export type { ProofRunnerApi }
28
20
 
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
- // ---------------------------------------------------------------------------
38
-
39
- /** Any composition, used where the contract type is irrelevant. */
40
- type AnyComposition = CompositionClass<any, any>
41
-
42
- export interface RequirementManifest<Comp extends AnyComposition, Statement extends string> {
43
- readonly kind: 'ProductRequirement'
44
- readonly name: Statement
45
- /** The human-readable behavioural statement; doubles as the manifest name. */
46
- readonly statement: Statement
47
- /** The composition this requirement specifies — its contract types the facade. */
48
- 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>>>>
56
- }
57
-
58
- export interface RequirementClass<Comp extends AnyComposition, Statement extends string> {
59
- new (): {}
60
- readonly manifest: RequirementManifest<Comp, Statement>
61
- }
62
-
63
- export interface ProductManifest<Comp extends AnyComposition> {
64
- readonly kind: 'Product'
65
- readonly name: string
66
- readonly composition: Comp
67
- readonly requirements: ReadonlyArray<RequirementClass<Comp, string>>
68
- }
69
-
70
- export interface ProductClass<Comp extends AnyComposition = AnyComposition> {
71
- new (): {}
72
- readonly manifest: ProductManifest<Comp>
73
- }
74
-
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>>>
83
- }
84
-
85
- const makeRequirement = <Comp extends AnyComposition, const Statement extends string>(
86
- composition: Comp,
87
- statement: Statement,
88
- options?: MakeRequirementOptionsExternalApi<Comp>,
89
- ): RequirementClass<Comp, Statement> => {
90
- const manifest: RequirementManifest<Comp, Statement> = {
91
- kind: 'ProductRequirement',
92
- name: statement,
93
- statement,
94
- composition,
95
- events: Option.fromNullable(options?.events),
96
- }
97
- return class {
98
- static readonly manifest = manifest
99
- }
100
- }
101
-
102
- export const ProductRequirement: { readonly make: typeof makeRequirement } = {
103
- make: makeRequirement,
104
- }
105
-
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> {
111
- readonly requirements: ReadonlyArray<RequirementClass<Comp, string>>
112
- }
113
-
114
- const makeProduct = <Comp extends AnyComposition>(
115
- composition: Comp,
116
- config: MakeProductConfig<Comp>,
117
- ): ProductClass<Comp> => {
118
- const manifest: ProductManifest<Comp> = {
119
- kind: 'Product',
120
- name: composition.manifest.name,
121
- composition,
122
- requirements: config.requirements,
123
- }
124
- return class {
125
- static readonly manifest = manifest
126
- }
127
- }
128
-
129
- export const Product: { readonly make: typeof makeProduct } = { make: makeProduct }
130
-
131
- // ---------------------------------------------------------------------------
132
- // Assertions — Effect-returning matchers; a failed match fails the proof.
133
- // ---------------------------------------------------------------------------
134
-
135
- /** Two values to deep-compare — one options object, not positional primitives. */
136
- interface ComparePair {
137
- readonly left: unknown
138
- readonly right: unknown
139
- }
140
-
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
-
146
- const deepEqual = ({ left, right }: ComparePair): boolean =>
147
- Object.is(left, right) || show(left) === show(right)
148
-
149
- const fail = (message: string): Effect.Effect<never> =>
150
- Effect.dieMessage(new AssertionFailed({ detail: message }).message)
151
-
152
- export const expect = <A>(actual: A) => ({
153
- toBe: (expected: A): Effect.Effect<void> =>
154
- Object.is(actual, expected)
155
- ? Effect.void
156
- : fail(`expected ${show(actual)} to be ${show(expected)}`),
157
- toEqual: (expected: A): Effect.Effect<void> =>
158
- deepEqual({ left: actual, right: expected })
159
- ? Effect.void
160
- : fail(`expected ${show(actual)} to equal ${show(expected)}`),
161
- toContain: (expected: A extends ReadonlyArray<infer E> ? E : unknown): Effect.Effect<void> =>
162
- Array.isArray(actual) && actual.some((element) => deepEqual({ left: element, right: expected }))
163
- ? Effect.void
164
- : fail(`expected ${show(actual)} to contain ${show(expected)}`),
165
- toMatchObject: (expected: Partial<A>): Effect.Effect<void> => {
166
- const mismatch = matchPartial({ actual, expected })
167
- return mismatch === undefined ? Effect.void : fail(mismatch)
168
- },
169
- })
170
-
171
- /** A non-null object as an indexable record — the narrowing `unknown` doesn't give. */
172
- const isRecord = (candidate: unknown): candidate is Record<string, unknown> =>
173
- typeof candidate === 'object' && candidate !== null
174
-
175
- /** The pair `matchPartial`/`matchProps` deep-matches: `actual` against the `expected` subset. */
176
- interface MatchInput {
177
- readonly actual: unknown
178
- readonly expected: unknown
179
- }
180
-
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
- const matchPartial = ({ actual, expected }: MatchInput): string | undefined => {
187
- if (!isRecord(expected)) {
188
- return deepEqual({ left: actual, right: expected })
189
- ? undefined
190
- : `expected ${show(actual)} to match ${show(expected)}`
191
- }
192
- if (!isRecord(actual)) {
193
- return `expected ${show(actual)} to be an object matching ${show(expected)}`
194
- }
195
- const mismatch = Arr.findFirst(
196
- Rec.keys(expected),
197
- (key) => !deepEqual({ left: actual[key], right: expected[key] }),
198
- )
199
- return Option.match(mismatch, {
200
- onNone: () => undefined,
201
- onSome: (key) =>
202
- `expected key ${show(key)} to match ${show(expected[key])}, got ${show(actual[key])}`,
203
- })
204
- }
205
-
206
- /** Internal: the facade's `expectProps` reuses the same partial-match logic. */
207
- export const matchProps: (input: MatchInput) => string | undefined = matchPartial
208
-
209
- // ---------------------------------------------------------------------------
210
- // Facade — a headless view over the live composition tree
211
- // ---------------------------------------------------------------------------
212
-
213
- // --- Contract projection: derive the facade's exact shape from a UI contract ---
214
-
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. */
21
+ export {
22
+ makeFacade,
23
+ makeRuntimeHandleFacade,
24
+ makeRuntimeTreeFacade,
25
+ makeSink,
26
+ proofLayer,
27
+ } from './engine'
28
+ export type {
29
+ MountedFacade,
30
+ MountedSlotFacade,
31
+ RuntimeServices,
32
+ RuntimeTreeFacade,
33
+ Sink,
34
+ } from './engine'
35
+
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>
218
48
  type SlotsOf<C extends UiContract> = C extends { slots: infer S } ? S : Record<never, never>
219
- /** The payload a trigger accepts. */
220
49
  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. */
50
+ export type ContractOf<Comp> = Comp extends CompositionClass<AnyValue, infer C> ? C : never
224
51
  type ContractOfSlot<S> = S extends SlotInstance<infer Comp> ? ContractOf<Comp> : never
225
52
 
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
53
  export type ActionsOf<C extends UiContract> = {
233
54
  readonly [K in keyof EventsOf<C>]: (
234
55
  payload: PayloadOf<EventsOf<C>[K]>,
235
56
  ) => Effect.Effect<C['props'], never, CompositionService>
236
57
  }
237
- /** The contract's slots as child facades, each typed by the child's own contract. */
238
58
  export type SlotFacadesOf<C extends UiContract> = {
239
59
  readonly [K in keyof SlotsOf<C>]: SlotFacade<ContractOfSlot<SlotsOf<C>[K]>>
240
60
  }
241
61
 
242
- /** The Effect a facade action returns: dispatch the event, settle, read props. */
243
62
  export type Action = (payload: unknown) => Effect.Effect<unknown, never, CompositionService>
244
63
 
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
64
  export interface Facade<C extends UiContract> {
252
- /** Re-render the tree and read the props this node last computed. */
253
65
  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
66
  readonly expectProps: (partial: Partial<C['props']>) => Effect.Effect<void, never, CompositionService>
260
- /** The contract's events as callables; calling one dispatches and settles. */
261
67
  readonly actions: ActionsOf<C>
262
- /** Child composition facades, keyed by slot name. */
263
68
  readonly slots: SlotFacadesOf<C>
264
- /** Settle the engine and re-render — wait for the next stable frame. */
265
69
  readonly frame: Effect.Effect<void, never, CompositionService>
266
70
  }
267
71
 
268
- /** A slot may hold many instances (a list); it is also usable as its first one. */
269
72
  export interface SlotFacade<C extends UiContract> extends Facade<C> {
270
73
  readonly first: Effect.Effect<Facade<C>, never, CompositionService>
271
74
  readonly at: (index: number) => Effect.Effect<Facade<C>, never, CompositionService>
272
75
  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
76
  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`). */
284
77
  readonly where: (
285
78
  predicate: (props: C['props']) => boolean,
286
79
  ) => Effect.Effect<ReadonlyArray<Facade<C>>, never, CompositionService>
287
- /** How many children this slot mounted this frame (the fill length). */
288
80
  readonly count: Effect.Effect<number, never, CompositionService>
289
81
  }
290
82
 
291
- // ---------------------------------------------------------------------------
292
- // Proofs + suite
293
- // ---------------------------------------------------------------------------
294
-
295
- /** The generator a proof body produces — its yielded effects run on the engine. */
296
83
  type ProofGenerator = Generator<
297
84
  YieldWrap<Effect.Effect<unknown, unknown, CompositionService>>,
298
85
  void,
299
86
  unknown
300
87
  >
301
88
 
302
- /** An erased facade — the runtime shape before the contract type is re-attached. */
303
89
  type AnyFacade = Facade<UiContract>
304
90
 
305
- /** The erased body stored on a `Proof` value; `implement` types the contract in. */
306
91
  export type ProofBody = (app: AnyFacade) => ProofGenerator
307
92
 
308
93
  export interface Proof {
309
94
  readonly requirement: RequirementClass<AnyComposition, string>
310
- /** The scene this proof runs against: its closed wiring + boot events. */
311
95
  readonly scene: Scene
312
96
  readonly body: ProofBody
313
97
  }
314
98
 
315
99
  export interface ProofSuite {
316
- /** Brand so adapters can duck-type a suite among a module's exports. */
317
100
  readonly kind: 'ProofSuite'
318
101
  readonly product: ProductClass
319
102
  readonly proofs: ReadonlyArray<Proof>
320
103
  }
321
104
 
105
+ const isRecord = (candidate: unknown): candidate is Record<string, unknown> =>
106
+ typeof candidate === 'object' && candidate !== null
107
+
322
108
  export interface ProofResult {
323
109
  readonly requirement: string
324
110
  readonly ok: boolean
@@ -332,14 +118,6 @@ export interface SuiteResult {
332
118
  readonly ok: boolean
333
119
  }
334
120
 
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
121
  const implement = <Comp extends AnyComposition>(
344
122
  requirement: RequirementClass<Comp, string>,
345
123
  scene: Scene<ContractOf<Comp>>,
@@ -350,27 +128,33 @@ const implement = <Comp extends AnyComposition>(
350
128
  return { requirement, scene, body: erased }
351
129
  }
352
130
 
353
- /** The proofs a `Proof.suite` binds to its product. */
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>(
136
+ 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
+ }
144
+
354
145
  interface MakeSuiteConfig {
355
146
  readonly proofs: ReadonlyArray<Proof>
356
147
  }
357
148
 
358
- /** Compose proofs with the product whose requirements they prove. */
359
149
  const suite = (product: ProductClass, config: MakeSuiteConfig): ProofSuite => ({
360
150
  kind: 'ProofSuite',
361
151
  product,
362
152
  proofs: config.proofs,
363
153
  })
364
154
 
365
- /** Duck-type a `ProofSuite` among arbitrary module exports (the adapter's seam). */
366
155
  export const isProofSuite = (candidate: unknown): candidate is ProofSuite =>
367
156
  isRecord(candidate) && candidate['kind'] === 'ProofSuite' && Array.isArray(candidate['proofs'])
368
157
 
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
158
  const run = (proofSuite: ProofSuite): Promise<SuiteResult> =>
375
159
  withProofRunner(async (runner: ProofRunnerApi) => {
376
160
  const reports = await Effect.runPromise(
@@ -383,33 +167,7 @@ const run = (proofSuite: ProofSuite): Promise<SuiteResult> =>
383
167
  }
384
168
  })
385
169
 
386
- // ---------------------------------------------------------------------------
387
- // Driver: step a proof for the editor's Test-play timeline
388
- // ---------------------------------------------------------------------------
389
-
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
- export interface StepFrame {
393
- readonly index: number
394
- readonly captures: ReadonlyArray<UiCapture>
395
- }
396
-
397
- export interface DriveResult {
398
- readonly requirement: string
399
- readonly frames: ReadonlyArray<StepFrame>
400
- 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
403
- }
404
-
405
- export interface ProofDriver {
406
- readonly requirement: string
407
- /** Run the proof, snapshotting the captured tree after each yielded step. */
408
- readonly run: () => Promise<DriveResult>
409
- }
410
-
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. */
170
+ export type { DriveResult, ProofDriver, StepFrame } from './driverTypes'
413
171
  const driver = (proofSuite: ProofSuite): ReadonlyArray<ProofDriver> =>
414
172
  proofSuite.proofs.map((proof) => ({
415
173
  requirement: proof.requirement.manifest.statement,
@@ -418,7 +176,8 @@ const driver = (proofSuite: ProofSuite): ReadonlyArray<ProofDriver> =>
418
176
 
419
177
  export const Proof: {
420
178
  readonly implement: typeof implement
179
+ readonly implementVia: typeof implementVia
421
180
  readonly suite: typeof suite
422
181
  readonly run: typeof run
423
182
  readonly driver: typeof driver
424
- } = { implement, suite, run, driver }
183
+ } = { implement, implementVia, suite, run, driver }
package/src/product.ts ADDED
@@ -0,0 +1,77 @@
1
+ import { Option, type Schema } from 'effect'
2
+ import type { CompositionClass } from '@playfast/reform'
3
+ import type { ContractOf, EventsOf } from './index'
4
+
5
+ type AnyValue = Schema.Schema.Type<Schema.Schema.Any>
6
+ type AnyComposition = CompositionClass<AnyValue, AnyValue>
7
+
8
+ export interface RequirementManifest<Comp extends AnyComposition, Statement extends string> {
9
+ readonly kind: 'ProductRequirement'
10
+ readonly name: Statement
11
+ readonly statement: Statement
12
+ readonly composition: Comp
13
+ readonly events: Option.Option<ReadonlyArray<keyof EventsOf<ContractOf<Comp>>>>
14
+ }
15
+
16
+ export interface RequirementClass<Comp extends AnyComposition, Statement extends string> {
17
+ new (): {}
18
+ readonly manifest: RequirementManifest<Comp, Statement>
19
+ }
20
+
21
+ export interface ProductManifest<Comp extends AnyComposition> {
22
+ readonly kind: 'Product'
23
+ readonly name: string
24
+ readonly composition: Comp
25
+ readonly requirements: ReadonlyArray<RequirementClass<Comp, string>>
26
+ }
27
+
28
+ export interface ProductClass<Comp extends AnyComposition = AnyComposition> {
29
+ new (): {}
30
+ readonly manifest: ProductManifest<Comp>
31
+ }
32
+
33
+ interface MakeRequirementOptionsExternalApi<Comp extends AnyComposition> {
34
+ readonly events?: ReadonlyArray<keyof EventsOf<ContractOf<Comp>>>
35
+ }
36
+
37
+ const makeRequirement = <Comp extends AnyComposition, const Statement extends string>(
38
+ composition: Comp,
39
+ statement: Statement,
40
+ options?: MakeRequirementOptionsExternalApi<Comp>,
41
+ ): RequirementClass<Comp, Statement> => {
42
+ const manifest: RequirementManifest<Comp, Statement> = {
43
+ kind: 'ProductRequirement',
44
+ name: statement,
45
+ statement,
46
+ composition,
47
+ events: Option.fromNullable(options?.events),
48
+ }
49
+ return class {
50
+ static readonly manifest = manifest
51
+ }
52
+ }
53
+
54
+ export const ProductRequirement: { readonly make: typeof makeRequirement } = {
55
+ make: makeRequirement,
56
+ }
57
+
58
+ interface MakeProductConfig<Comp extends AnyComposition> {
59
+ readonly requirements: ReadonlyArray<RequirementClass<Comp, string>>
60
+ }
61
+
62
+ const makeProduct = <Comp extends AnyComposition>(
63
+ composition: Comp,
64
+ config: MakeProductConfig<Comp>,
65
+ ): ProductClass<Comp> => {
66
+ const manifest: ProductManifest<Comp> = {
67
+ kind: 'Product',
68
+ name: composition.manifest.name,
69
+ composition,
70
+ requirements: config.requirements,
71
+ }
72
+ return class {
73
+ static readonly manifest = manifest
74
+ }
75
+ }
76
+
77
+ export const Product: { readonly make: typeof makeProduct } = { make: makeProduct }
package/src/runner.ts CHANGED
@@ -1,38 +1,22 @@
1
1
  import { Context, Effect, Layer } from 'effect'
2
- import { driveProof, executeProof } from './engine'
2
+ import { driveProof, executeProof } from './execution'
3
3
  import type { DriveResult, Proof, ProofResult } from './index'
4
4
 
5
- // reform-proof runner — the injectable seam HOW a proof is executed. The proof
6
- // system depends on this layer rather than calling the engine directly, so the
7
- // execution strategy can be swapped (the vitest adapter resolves the same layer to
8
- // run each proof as a native test). The default layer is the headless engine in
9
- // ./engine; mirrors the Tag + Layer house style of reform's `CaptureSink` and
10
- // `Notifications` services.
11
-
12
5
  export interface ProofRunnerApi {
13
- /** Run a proof to completion, returning its pass/fail result. */
14
6
  readonly executeProof: (proof: Proof) => Promise<ProofResult>
15
- /** Step a proof a frame at a time (the editor's Test-play timeline). */
16
7
  readonly driveProof: (proof: Proof) => Promise<DriveResult>
17
8
  }
18
9
 
19
10
  const ProofRunnerBase: Context.TagClass<ProofRunner, 'reform-proof/ProofRunner', ProofRunnerApi> =
20
11
  Context.Tag('reform-proof/ProofRunner')<ProofRunner, ProofRunnerApi>()
21
12
 
22
- /** The service identity for the proof execution strategy. */
23
13
  export class ProofRunner extends ProofRunnerBase {}
24
14
 
25
- /** The default runner: the headless engine that drives the real reduce loop. */
26
15
  export const proofRunnerLayer: Layer.Layer<ProofRunner> = Layer.succeed(ProofRunner, {
27
16
  executeProof,
28
17
  driveProof,
29
18
  })
30
19
 
31
- /**
32
- * Resolve the `ProofRunner` from `proofRunnerLayer` and use it — the single seam
33
- * the proof system (`Proof.run`/`Proof.driver`) and the vitest adapter both run
34
- * through, so every proof executes via the same injectable engine.
35
- */
36
20
  export const withProofRunner = <A>(use: (runner: ProofRunnerApi) => Promise<A>): Promise<A> =>
37
21
  Effect.runPromise(
38
22
  ProofRunner.pipe(