@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/errors.ts
CHANGED
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
import { type Cause, Data } from 'effect'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Tagged errors for the proof harness. A failed assertion or a malformed proof
|
|
5
|
-
* navigation throws one of these — tagged and structured, never a bare `Error` —
|
|
6
|
-
* so a runner can distinguish an assertion failure from a harness misuse.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The constructor shape `Data.TaggedError(tag)<A>` produces, named so the
|
|
11
|
-
* generated `.d.ts` can describe the `extends` base under `isolatedDeclarations`
|
|
12
|
-
* (which forbids an inferred expression in an extends clause).
|
|
13
|
-
*/
|
|
14
3
|
type TaggedErrorClass<Tag extends string, A extends Record<string, unknown>> = new (
|
|
15
4
|
args: A,
|
|
16
5
|
) => Cause.YieldableError & { readonly _tag: Tag } & Readonly<A>
|
|
@@ -20,7 +9,6 @@ const AssertionFailedBase: TaggedErrorClass<
|
|
|
20
9
|
{ readonly detail: string }
|
|
21
10
|
> = Data.TaggedError('reform-proof/AssertionFailed')<{ readonly detail: string }>
|
|
22
11
|
|
|
23
|
-
/** An `expect(...)` matcher did not hold. */
|
|
24
12
|
export class AssertionFailed extends AssertionFailedBase {
|
|
25
13
|
override get message(): string {
|
|
26
14
|
return `reform-proof: assertion failed — ${this.detail}`
|
|
@@ -36,7 +24,6 @@ const UnknownActionBase: TaggedErrorClass<
|
|
|
36
24
|
readonly rendered: number
|
|
37
25
|
}>
|
|
38
26
|
|
|
39
|
-
/** A proof referenced an action the rendered composition never exposed. */
|
|
40
27
|
export class UnknownAction extends UnknownActionBase {
|
|
41
28
|
override get message(): string {
|
|
42
29
|
return `reform-proof: '${this.composition}' has no action '${this.action}' (rendered ${this.rendered} instance(s))`
|
|
@@ -48,7 +35,6 @@ const UnknownSlotBase: TaggedErrorClass<
|
|
|
48
35
|
{ readonly parent: string; readonly slot: string }
|
|
49
36
|
> = Data.TaggedError('reform-proof/UnknownSlot')<{ readonly parent: string; readonly slot: string }>
|
|
50
37
|
|
|
51
|
-
/** A proof navigated into a slot the parent composition does not declare. */
|
|
52
38
|
export class UnknownSlot extends UnknownSlotBase {
|
|
53
39
|
override get message(): string {
|
|
54
40
|
return `reform-proof: '${this.parent}' has no slot '${this.slot}'`
|
|
@@ -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())
|