@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/README.md +12 -12
- package/package.json +18 -18
- package/src/engine.ts +1405 -337
- package/src/errors.ts +0 -14
- package/src/fingerprint.test.ts +24 -0
- package/src/fingerprint.ts +73 -0
- package/src/index.ts +240 -216
- package/src/nested-feature.test.ts +174 -0
- package/src/runner.ts +5 -17
- package/src/structure-events.test.ts +9 -14
- package/src/structure-locators.test.ts +19 -32
- package/src/typed-seams.test.ts +6 -14
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
|
-
|
|
7
|
-
CompositionService,
|
|
7
|
+
CapturedScene,
|
|
8
8
|
Scene,
|
|
9
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
|
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<
|
|
59
|
-
|
|
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
|
|
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<
|
|
80
|
+
readonly requirements: ReadonlyArray<AnyRequirementClass>
|
|
68
81
|
}
|
|
69
82
|
|
|
70
|
-
export interface ProductClass<Comp extends
|
|
83
|
+
export interface ProductClass<Comp extends ProductComposition = ProductComposition> {
|
|
71
84
|
new (): {}
|
|
72
85
|
readonly manifest: ProductManifest<Comp>
|
|
73
86
|
}
|
|
74
87
|
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 } = {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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,
|
|
287
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
306
|
-
|
|
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:
|
|
310
|
-
|
|
311
|
-
readonly
|
|
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
|
-
|
|
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
|
|
347
|
-
): Proof =>
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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<
|
|
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
|
-
|
|
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 }
|