@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.
- package/README.md +12 -12
- package/package.json +18 -18
- package/src/engine.ts +1657 -8
- package/src/fingerprint.test.ts +24 -0
- package/src/fingerprint.ts +73 -0
- package/src/index.ts +352 -87
- package/src/nested-feature.test.ts +174 -0
- package/src/runner.ts +5 -1
- package/src/structure-events.test.ts +9 -3
- package/src/structure-locators.test.ts +19 -9
- package/src/typed-seams.test.ts +6 -3
- package/src/assertions.ts +0 -65
- package/src/driverTypes.ts +0 -19
- package/src/execution.ts +0 -100
- package/src/facade.ts +0 -258
- package/src/product.ts +0 -77
- package/src/runtimeFacade.ts +0 -269
- package/src/runtimeTreeDispose.ts +0 -21
- package/src/runtimeTreeDriver.ts +0 -285
- package/src/runtimeTreeFacade.ts +0 -134
- package/src/runtimeTreeSettle.ts +0 -34
- package/src/runtimeTreeTypes.ts +0 -99
- package/src/sink.ts +0 -73
- package/src/treeBindings.ts +0 -121
|
@@ -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,
|
|
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
|
-
|
|
8
|
-
CompositionService,
|
|
7
|
+
CapturedScene,
|
|
9
8
|
Scene,
|
|
10
|
-
|
|
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
|
-
|
|
30
|
+
MountedSlotFacadesOf,
|
|
32
31
|
RuntimeTreeFacade,
|
|
33
32
|
Sink,
|
|
34
33
|
} from './engine'
|
|
35
34
|
|
|
36
|
-
export {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
51
|
-
|
|
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
|
|
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
|
-
|
|
59
|
-
|
|
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,
|
|
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,
|
|
66
|
-
readonly expectProps: (partial: Partial<C['props']>) => Effect.Effect<void, never,
|
|
67
|
-
readonly actions: ActionsOf<C>
|
|
68
|
-
readonly slots: SlotFacadesOf<C>
|
|
69
|
-
readonly frame: Effect.Effect<void, never,
|
|
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,
|
|
74
|
-
readonly at: (index: number) => Effect.Effect<Facade<C>, never,
|
|
75
|
-
readonly all: Effect.Effect<ReadonlyArray<Facade<C>>, never,
|
|
76
|
-
readonly byKey: (key: string) => Effect.Effect<Facade<C>, never,
|
|
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,
|
|
80
|
-
readonly count: Effect.Effect<number, never,
|
|
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
|
-
|
|
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
|
|
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:
|
|
95
|
-
readonly scene:
|
|
96
|
-
readonly
|
|
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
|
-
|
|
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
|
|
316
|
+
const implement = <Comp extends ProductComposition>(
|
|
122
317
|
requirement: RequirementClass<Comp, string>,
|
|
123
|
-
scene: Scene<ContractOf<Comp>>,
|
|
124
|
-
body: (app: Facade<ContractOf<Comp
|
|
125
|
-
): Proof =>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
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 = <
|
|
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:
|
|
138
|
-
body: (app: Facade<C>) => ProofGenerator,
|
|
139
|
-
): Proof =>
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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 }
|