@unrulysystems/native-motion 0.1.0-alpha.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/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/android/build.gradle +24 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/unrulysystems/nativemotion/GestureExclusionModule.kt +55 -0
- package/android/src/main/java/com/unrulysystems/nativemotion/NativeMotionPackage.kt +27 -0
- package/package.json +57 -0
- package/react-native.config.js +14 -0
- package/src/native/driver/instantWindow.ts +31 -0
- package/src/native/driver/strictModeReplay.ts +20 -0
- package/src/native/driver/uiLayoutEngine.ts +845 -0
- package/src/native/driver/uiLayoutGraph.ts +338 -0
- package/src/native/driver/uiValueChannel.ts +4640 -0
- package/src/native/driver/workletDriver.ts +3860 -0
- package/src/native/motion/AnimatePresence.tsx +1605 -0
- package/src/native/motion/LayoutGroup.tsx +163 -0
- package/src/native/motion/MotionConfig.tsx +400 -0
- package/src/native/motion/MotionRoot.tsx +250 -0
- package/src/native/motion/MotionView.tsx +6761 -0
- package/src/native/motion/addScaleCorrector.ts +97 -0
- package/src/native/motion/colorEndpointUiFeed.ts +32 -0
- package/src/native/motion/colorProjection.ts +39 -0
- package/src/native/motion/composeTransform.ts +271 -0
- package/src/native/motion/constraintMeasure.ts +157 -0
- package/src/native/motion/deferredPendingSnapshots.ts +25 -0
- package/src/native/motion/discreteProjection.ts +36 -0
- package/src/native/motion/dragAncestorPanContext.ts +28 -0
- package/src/native/motion/dragControls.ts +152 -0
- package/src/native/motion/dragGestureWiring.ts +1168 -0
- package/src/native/motion/dragHandoffBinding.ts +179 -0
- package/src/native/motion/dragHubStream.ts +756 -0
- package/src/native/motion/dragPropagationLock.ts +57 -0
- package/src/native/motion/driverValueChannel.ts +3288 -0
- package/src/native/motion/externalDragDriver.ts +214 -0
- package/src/native/motion/frameData.ts +31 -0
- package/src/native/motion/gestureBinding.ts +107 -0
- package/src/native/motion/gestureStateGate.ts +563 -0
- package/src/native/motion/gestureStateResolver.ts +286 -0
- package/src/native/motion/identityValueChannelControllerAdapter.ts +2043 -0
- package/src/native/motion/identityValueChannelControllerReconciliation.ts +176 -0
- package/src/native/motion/identityValueChannelLaneMarker.ts +14 -0
- package/src/native/motion/imperativeAnimate.ts +816 -0
- package/src/native/motion/keyframeTiming.ts +7 -0
- package/src/native/motion/layoutIdBinding.ts +261 -0
- package/src/native/motion/layoutIdFlightConfig.ts +51 -0
- package/src/native/motion/layoutProjection.ts +89 -0
- package/src/native/motion/layoutScroll.ts +204 -0
- package/src/native/motion/layoutTransition.ts +1302 -0
- package/src/native/motion/lengthLayoutContext.tsx +100 -0
- package/src/native/motion/lengthLayoutHost.ts +44 -0
- package/src/native/motion/mappedKeys.ts +184 -0
- package/src/native/motion/motionViewController.ts +3180 -0
- package/src/native/motion/nativeHostMarker.ts +22 -0
- package/src/native/motion/panSessionWiring.ts +127 -0
- package/src/native/motion/pathTransition.ts +101 -0
- package/src/native/motion/popLayout.ts +176 -0
- package/src/native/motion/presenceBinding.ts +382 -0
- package/src/native/motion/scaleCorrectorRegistry.ts +123 -0
- package/src/native/motion/scrollValues.ts +260 -0
- package/src/native/motion/serializablePayload.ts +151 -0
- package/src/native/motion/severity.ts +16 -0
- package/src/native/motion/shippedSurface.ts +843 -0
- package/src/native/motion/staticLengthGate.ts +86 -0
- package/src/native/motion/styleBaseGate.ts +72 -0
- package/src/native/motion/styleValueBinding.ts +657 -0
- package/src/native/motion/systemGestureExclusion.ts +61 -0
- package/src/native/motion/tapGestureWiring.ts +215 -0
- package/src/native/motion/transformOrder.ts +38 -0
- package/src/native/motion/transformStringBinding.ts +200 -0
- package/src/native/motion/transformTemplateGate.ts +45 -0
- package/src/native/motion/transitionGate.ts +435 -0
- package/src/native/motion/useAnimate.ts +52 -0
- package/src/native/motion/useCycle.ts +44 -0
- package/src/native/motion/useInstantTransition.ts +68 -0
- package/src/native/motion/useReducedMotion.ts +58 -0
- package/src/native/motion/useScroll.ts +162 -0
- package/src/native/motion/useViewportScroll.ts +29 -0
- package/src/native/motion/valueChannel.ts +7121 -0
- package/src/native/motion/valueHooks.ts +901 -0
- package/src/native/motion/variantChildRegistry.ts +67 -0
- package/src/native/motion/variantContext.tsx +172 -0
- package/src/native/motion/variantProps.ts +598 -0
- package/src/native.ts +159 -0
- package/src/verification/harnessMetrics.ts +78 -0
- package/src/verification/probe/LayoutIdentityWorkletProbe.tsx +251 -0
- package/src/verification/probe/WorkletParityProbe.tsx +162 -0
- package/src/verification/probe/layoutIdentityProbeEngine.ts +429 -0
- package/src/verification/screens/ArcPathChecksScreen.tsx +273 -0
- package/src/verification/screens/BooleanAnimateChecksScreen.tsx +358 -0
- package/src/verification/screens/ChoreographyGalleryScreen.tsx +1010 -0
- package/src/verification/screens/ColorBindingChecksScreen.tsx +568 -0
- package/src/verification/screens/CompletionChecksScreen.tsx +294 -0
- package/src/verification/screens/ConformanceScreen.tsx +573 -0
- package/src/verification/screens/ContentionProbeScreen.tsx +224 -0
- package/src/verification/screens/DriverSmokeScreen.tsx +99 -0
- package/src/verification/screens/DurationOnlyTweenChecksScreen.tsx +294 -0
- package/src/verification/screens/DynamicDragConfigChecksScreen.tsx +237 -0
- package/src/verification/screens/FrameDataChecksScreen.tsx +252 -0
- package/src/verification/screens/GestureChecksScreen.tsx +685 -0
- package/src/verification/screens/InstantTransitionChecksScreen.tsx +839 -0
- package/src/verification/screens/LayoutAnimationStartChecksScreen.tsx +648 -0
- package/src/verification/screens/LayoutChecksScreen.tsx +822 -0
- package/src/verification/screens/LayoutCommitSpikeScreen.tsx +142 -0
- package/src/verification/screens/MotionViewChecksScreen.tsx +930 -0
- package/src/verification/screens/PresenceChecksScreen.tsx +614 -0
- package/src/verification/screens/ReducedMotionChecksScreen.tsx +614 -0
- package/src/verification/screens/RestThresholdChecksScreen.tsx +634 -0
- package/src/verification/screens/ScaleCorrectorChecksScreen.tsx +425 -0
- package/src/verification/screens/SharedLayoutContinuityChecksScreen.tsx +338 -0
- package/src/verification/screens/SharedLayoutCrossfadeChecksScreen.tsx +425 -0
- package/src/verification/screens/TransitionDefaultSelectionChecksScreen.tsx +514 -0
- package/src/verification/screens/ViewportScrollAliasChecksScreen.tsx +303 -0
- package/src/verification/screens/conformanceBanner.ts +21 -0
- package/src/verification/screens/proofConsoleTap.ts +18 -0
- package/src/verification.ts +47 -0
- package/src/web.ts +98 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
// M2.1 + M2.2 on-device conformance (specs/M2.1-BUILD-PACKET.md / specs/M2.2-BUILD-PACKET.md,
|
|
2
|
+
// Milestone 4 of each). Precisely scoped: the scenario suites construct their OWN in-suite engines
|
|
3
|
+
// (reference driver / nativeAdapter / fake engine) and are not engine-parameterized — running them
|
|
4
|
+
// here is a SUITE-PORTABILITY claim (core + conformance execute unchanged on the device runtime),
|
|
5
|
+
// NOT a Motion.View/worklet-driver rewiring; those are verified by their behavioral checks. The
|
|
6
|
+
// M2.2 suites (SPRING / VALUE-TYPES / COMPONENT — counts rendered from the arrays) show per-suite
|
|
7
|
+
// verdict COUNTS with only non-pass rows expanded, so one screenshot carries the whole table. When
|
|
8
|
+
// the motion-dom oracle loads (it does on Hermes — pinned motion-dom@12.42.2, manual timing), the
|
|
9
|
+
// spring/value-type runs are the REAL cross-engine comparisons from the vitest harness.
|
|
10
|
+
//
|
|
11
|
+
// The per-module conformance subpath imports are deliberate: the package INDEX eagerly re-exports
|
|
12
|
+
// the motion-dom adapter, so importing it here would load the oracle at bundle-import time and
|
|
13
|
+
// turn a Hermes incompatibility into a startup crash instead of the on-screen evidence the lazy
|
|
14
|
+
// probe below produces. Every top-level import therefore names its module and motion-dom is only
|
|
15
|
+
// touched inside `tryMotionDomAdapter`.
|
|
16
|
+
|
|
17
|
+
import { useEffect, useRef, useState } from 'react'
|
|
18
|
+
import { ScrollView, StyleSheet, Text, View } from 'react-native'
|
|
19
|
+
import { createReferenceDriver, type ElementHandle } from '@unrulysystems/native-motion-core'
|
|
20
|
+
import { nativeAdapter } from '@unrulysystems/native-motion-conformance/adapters/native'
|
|
21
|
+
import type { EngineAdapter } from '@unrulysystems/native-motion-conformance/adapter'
|
|
22
|
+
import {
|
|
23
|
+
compareEngines,
|
|
24
|
+
compareValueTypeSeries,
|
|
25
|
+
} from '@unrulysystems/native-motion-conformance/comparator'
|
|
26
|
+
import { SPRING_BAND } from '@unrulysystems/native-motion-conformance/config'
|
|
27
|
+
import {
|
|
28
|
+
runSpringScenario,
|
|
29
|
+
runValueTypeScenario,
|
|
30
|
+
type ScenarioResult,
|
|
31
|
+
} from '@unrulysystems/native-motion-conformance/runner'
|
|
32
|
+
import {
|
|
33
|
+
COMPONENT_SCENARIOS,
|
|
34
|
+
runComponentScenario,
|
|
35
|
+
} from '@unrulysystems/native-motion-conformance/scenarios/component'
|
|
36
|
+
import {
|
|
37
|
+
DRIVER_SCENARIOS,
|
|
38
|
+
runDriverScenario,
|
|
39
|
+
} from '@unrulysystems/native-motion-conformance/scenarios/driver'
|
|
40
|
+
import {
|
|
41
|
+
GESTURE_SCENARIOS,
|
|
42
|
+
runGestureScenario,
|
|
43
|
+
} from '@unrulysystems/native-motion-conformance/scenarios/gesture'
|
|
44
|
+
import {
|
|
45
|
+
LAYOUT_SCENARIOS,
|
|
46
|
+
runLayoutScenario,
|
|
47
|
+
} from '@unrulysystems/native-motion-conformance/scenarios/layout'
|
|
48
|
+
import {
|
|
49
|
+
PRESENCE_SCENARIOS,
|
|
50
|
+
runPresenceScenario,
|
|
51
|
+
} from '@unrulysystems/native-motion-conformance/scenarios/presence'
|
|
52
|
+
import { SPRING_SCENARIOS } from '@unrulysystems/native-motion-conformance/scenarios/spring'
|
|
53
|
+
import { VALUE_TYPE_SCENARIOS } from '@unrulysystems/native-motion-conformance/scenarios/value-types'
|
|
54
|
+
import { createWorkletDriver, useWorkletDriver } from '../../native/driver/workletDriver'
|
|
55
|
+
import { conformanceBannerVerdict } from './conformanceBanner'
|
|
56
|
+
|
|
57
|
+
const SPRING = { type: 'spring', stiffness: 100, damping: 10 } as const
|
|
58
|
+
const DT_MS = 16.67
|
|
59
|
+
const STEPS = 120
|
|
60
|
+
|
|
61
|
+
interface Row {
|
|
62
|
+
readonly id: string
|
|
63
|
+
readonly pass: boolean
|
|
64
|
+
readonly detail: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Manual drivers created by the parity checks, disposed together after the rows are computed —
|
|
68
|
+
// an undisposed driver strands its UI-runtime record (quiesce-before-unmount, review round 4
|
|
69
|
+
// major 5). The checks early-return mid-flow, so the ledger frees them centrally.
|
|
70
|
+
const liveCheckDrivers: ReturnType<typeof createWorkletDriver>[] = []
|
|
71
|
+
|
|
72
|
+
function checkDriver(): ReturnType<typeof createWorkletDriver> {
|
|
73
|
+
const binding = createWorkletDriver({ manualStepping: true })
|
|
74
|
+
liveCheckDrivers.push(binding)
|
|
75
|
+
return binding
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function disposeCheckDrivers(): void {
|
|
79
|
+
for (const binding of liveCheckDrivers) binding.dispose()
|
|
80
|
+
liveCheckDrivers.length = 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Section 1: the five engine-agnostic DRIVER scenarios, verbatim (reference driver on device Hermes).
|
|
84
|
+
function runSuitePortability(): Row[] {
|
|
85
|
+
return DRIVER_SCENARIOS.map((scenario) => {
|
|
86
|
+
const result = runDriverScenario(scenario)
|
|
87
|
+
return {
|
|
88
|
+
id: result.id,
|
|
89
|
+
pass: result.verdict === 'pass',
|
|
90
|
+
detail: result.detail ?? result.verdict,
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Section 2a: the worklet driver and the reference driver run the IDENTICAL command script over the
|
|
96
|
+
// identical manual grid; committed values must agree exactly at every step (the same bit-identical
|
|
97
|
+
// claim the parity probe proved for the raw step, now through the full Driver surface).
|
|
98
|
+
function checkDriverParity(): Row {
|
|
99
|
+
const worklet = checkDriver()
|
|
100
|
+
const reference = createReferenceDriver()
|
|
101
|
+
const wh = worklet.driver.register({ x: 0 })
|
|
102
|
+
const rh = reference.register({ x: 0 })
|
|
103
|
+
const command = {
|
|
104
|
+
kind: 'start',
|
|
105
|
+
targets: { x: { to: 220, from: 0 } },
|
|
106
|
+
transition: SPRING,
|
|
107
|
+
} as const
|
|
108
|
+
worklet.driver.command(wh, command)
|
|
109
|
+
reference.command(rh, command)
|
|
110
|
+
for (let i = 0; i < STEPS; i++) {
|
|
111
|
+
worklet.driver.step(DT_MS)
|
|
112
|
+
reference.step(DT_MS)
|
|
113
|
+
const w = worklet.driver.committed(wh)['x']
|
|
114
|
+
const r = reference.committed(rh)['x']
|
|
115
|
+
if (w !== r) {
|
|
116
|
+
return {
|
|
117
|
+
id: 'worklet-vs-reference parity',
|
|
118
|
+
pass: false,
|
|
119
|
+
detail: `diverged at step ${i}: worklet=${String(w)} reference=${String(r)}`,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const settledWorklet = worklet.driver.isActive(wh)
|
|
124
|
+
const settledReference = reference.isActive(rh)
|
|
125
|
+
if (settledWorklet !== settledReference) {
|
|
126
|
+
return {
|
|
127
|
+
id: 'worklet-vs-reference parity',
|
|
128
|
+
pass: false,
|
|
129
|
+
detail: `quiesce mismatch: worklet active=${String(settledWorklet)} reference active=${String(settledReference)}`,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
id: 'worklet-vs-reference parity',
|
|
134
|
+
pass: true,
|
|
135
|
+
detail: `${STEPS} steps exact through the full Driver surface; both quiesced`,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Section 2b: retarget continuity — the retargeted trajectory seeds from the LIVE value (C0), and the
|
|
140
|
+
// redirected spring settles exactly at its new target.
|
|
141
|
+
function checkRetargetContinuity(): Row {
|
|
142
|
+
const worklet = checkDriver()
|
|
143
|
+
const handle = worklet.driver.register({ x: 0 })
|
|
144
|
+
worklet.driver.command(handle, {
|
|
145
|
+
kind: 'start',
|
|
146
|
+
targets: { x: { to: 220, from: 0 } },
|
|
147
|
+
transition: SPRING,
|
|
148
|
+
})
|
|
149
|
+
for (let i = 0; i < 30; i++) worklet.driver.step(DT_MS)
|
|
150
|
+
const before = worklet.driver.committed(handle)['x']!
|
|
151
|
+
worklet.driver.command(handle, { kind: 'retarget', targets: { x: 0 }, transition: SPRING })
|
|
152
|
+
const seeded = worklet.driver.committed(handle)['x']!
|
|
153
|
+
if (seeded !== before) {
|
|
154
|
+
return {
|
|
155
|
+
id: 'retarget C0 continuity',
|
|
156
|
+
pass: false,
|
|
157
|
+
detail: `seed jumped: before=${before} after-retarget=${seeded}`,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (let i = 0; i < 600; i++) worklet.driver.step(DT_MS)
|
|
161
|
+
const settled = worklet.driver.committed(handle)['x']!
|
|
162
|
+
if (settled !== 0 || worklet.driver.isActive(handle)) {
|
|
163
|
+
return {
|
|
164
|
+
id: 'retarget C0 continuity',
|
|
165
|
+
pass: false,
|
|
166
|
+
detail: `did not settle at retarget target: x=${settled} active=${String(worklet.driver.isActive(handle))}`,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
id: 'retarget C0 continuity',
|
|
171
|
+
pass: true,
|
|
172
|
+
detail: `seeded from live value ${before.toFixed(3)}, settled exactly at 0, quiesced`,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Section 2c (async): live settle-cancel — the real frame loop (the hook-owned useFrameCallback,
|
|
177
|
+
// REQ-DRIVER-011) drives a spring and must fire the settle edge exactly once, after which the element
|
|
178
|
+
// is inactive and committed exactly at target.
|
|
179
|
+
function useSettleCancelCheck(): Row | null {
|
|
180
|
+
const [row, setRow] = useState<Row | null>(null)
|
|
181
|
+
const handleRef = useRef<ElementHandle | null>(null)
|
|
182
|
+
const binding = useWorkletDriver({
|
|
183
|
+
onAllSettled: () => {
|
|
184
|
+
const handle = handleRef.current
|
|
185
|
+
if (handle === null) return
|
|
186
|
+
const committed = binding.driver.committed(handle)['x']
|
|
187
|
+
const active = binding.driver.isActive(handle)
|
|
188
|
+
setRow({
|
|
189
|
+
id: 'live settle-cancel',
|
|
190
|
+
pass: committed === 220 && !active,
|
|
191
|
+
detail: `settle edge fired; committed=${String(committed)} active=${String(active)} (loop stopped)`,
|
|
192
|
+
})
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
const handle = binding.driver.register({ x: 0 })
|
|
197
|
+
handleRef.current = handle
|
|
198
|
+
binding.driver.command(handle, {
|
|
199
|
+
kind: 'start',
|
|
200
|
+
targets: { x: { to: 220, from: 0 } },
|
|
201
|
+
transition: SPRING,
|
|
202
|
+
})
|
|
203
|
+
}, [binding])
|
|
204
|
+
return row
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// A whole scenario suite reduced to verdict counts + expanded non-pass rows, so 54 scenarios fit one
|
|
208
|
+
// screenshot without hiding any deviation ("no silent caps": every non-pass id is listed).
|
|
209
|
+
interface SuiteSummary {
|
|
210
|
+
readonly name: string
|
|
211
|
+
readonly pass: number
|
|
212
|
+
readonly fail: number
|
|
213
|
+
readonly unassertable: number
|
|
214
|
+
readonly nonPass: readonly ScenarioResult[]
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function summarize(name: string, results: readonly ScenarioResult[]): SuiteSummary {
|
|
218
|
+
return {
|
|
219
|
+
name,
|
|
220
|
+
pass: results.filter((r) => r.verdict === 'pass').length,
|
|
221
|
+
fail: results.filter((r) => r.verdict === 'fail').length,
|
|
222
|
+
unassertable: results.filter((r) => r.verdict === 'unassertable').length,
|
|
223
|
+
nonPass: results.filter((r) => r.verdict !== 'pass'),
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// The pinned motion-dom oracle (motion-dom@12.42.2, manual timing, no DOM needed on these paths) —
|
|
228
|
+
// attempted at runtime via a lazy require so a Hermes incompatibility surfaces as ON-SCREEN evidence
|
|
229
|
+
// (single-engine verdicts, reason recorded) instead of a startup crash. If it loads, the device runs
|
|
230
|
+
// the REAL cross-engine comparison the vitest suites run.
|
|
231
|
+
function tryMotionDomAdapter(): { adapter: EngineAdapter } | { error: string } {
|
|
232
|
+
try {
|
|
233
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
234
|
+
const mod = require('@unrulysystems/native-motion-conformance/adapters/motion-dom') as {
|
|
235
|
+
motionDomAdapter: () => EngineAdapter
|
|
236
|
+
}
|
|
237
|
+
return { adapter: mod.motionDomAdapter() }
|
|
238
|
+
} catch (e) {
|
|
239
|
+
return { error: e instanceof Error ? `${e.name}: ${e.message}` : String(e) }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Run one scenario body safely: a thrown scenario becomes a FAIL result carrying the message —
|
|
244
|
+
// render must never die to a probe (the unit-mismatch scenarios throw BY CONTRACT elsewhere).
|
|
245
|
+
function safely(id: string, run: () => ScenarioResult): ScenarioResult {
|
|
246
|
+
try {
|
|
247
|
+
return run()
|
|
248
|
+
} catch (e) {
|
|
249
|
+
return {
|
|
250
|
+
id,
|
|
251
|
+
engine: 'device',
|
|
252
|
+
verdict: 'fail',
|
|
253
|
+
readings: [],
|
|
254
|
+
detail: `threw: ${e instanceof Error ? e.message : String(e)}`,
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Mirror the vitest harness contracts exactly (spring/value-types conformance tests): clean spring
|
|
260
|
+
// scenarios compare native vs the oracle within SPRING_BAND; decision-D-pending ones are excluded as
|
|
261
|
+
// unassertable (the §7 decision is the human's); value-type 'throw' scenarios PASS when native
|
|
262
|
+
// throws (and the oracle does not); 'snap' scenarios PASS when the engines genuinely diverge and
|
|
263
|
+
// native truly mixes. Component scenarios are self-contained.
|
|
264
|
+
//
|
|
265
|
+
// FAIL-CLOSED on the oracle (review cycle 1, major 2): these suites CLAIM cross-engine comparison,
|
|
266
|
+
// so a missing oracle cannot silently degrade to native-only PASS verdicts — every oracle-dependent
|
|
267
|
+
// scenario becomes `unassertable` with the load error, and the screen's banner FAILS (an unavailable
|
|
268
|
+
// independent verifier is a blocked gate, not a pass).
|
|
269
|
+
function runM22Suites(): { suites: SuiteSummary[]; oracleError: string | null } {
|
|
270
|
+
const motion = tryMotionDomAdapter()
|
|
271
|
+
const oracle = 'adapter' in motion ? motion.adapter : null
|
|
272
|
+
const oracleError = oracle === null ? (motion as { error: string }).error : null
|
|
273
|
+
|
|
274
|
+
const oracleMissing = (id: string): ScenarioResult => ({
|
|
275
|
+
id,
|
|
276
|
+
engine: 'device',
|
|
277
|
+
verdict: 'unassertable',
|
|
278
|
+
readings: [],
|
|
279
|
+
detail: `cross-engine leg NOT run — motion-dom oracle unavailable: ${oracleError ?? 'unknown'}`,
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
const springClean = SPRING_SCENARIOS.filter((s) => !s.pendingDecisionD)
|
|
283
|
+
const springPending = SPRING_SCENARIOS.filter((s) => s.pendingDecisionD)
|
|
284
|
+
const springResults: ScenarioResult[] = [
|
|
285
|
+
...springClean.map((sc) =>
|
|
286
|
+
safely(sc.id, () => {
|
|
287
|
+
if (oracle === null) return oracleMissing(sc.id)
|
|
288
|
+
const a = runSpringScenario(sc, nativeAdapter(), SPRING_BAND)
|
|
289
|
+
if (a.verdict !== 'pass') return a
|
|
290
|
+
const b = runSpringScenario(sc, oracle, SPRING_BAND)
|
|
291
|
+
if (b.verdict !== 'pass') return { ...b, detail: `oracle leg: ${b.detail ?? b.verdict}` }
|
|
292
|
+
const cmp = compareEngines(a, b, SPRING_BAND)
|
|
293
|
+
return cmp.agree
|
|
294
|
+
? a
|
|
295
|
+
: {
|
|
296
|
+
...a,
|
|
297
|
+
verdict: 'fail' as const,
|
|
298
|
+
detail: `cross-engine diverged: ${JSON.stringify(cmp.mismatches[0])}`,
|
|
299
|
+
}
|
|
300
|
+
}),
|
|
301
|
+
),
|
|
302
|
+
...springPending.map((sc) => ({
|
|
303
|
+
id: sc.id,
|
|
304
|
+
engine: 'device',
|
|
305
|
+
verdict: 'unassertable' as const,
|
|
306
|
+
readings: [],
|
|
307
|
+
detail:
|
|
308
|
+
'pending decision D (§7, human) — excluded from the clean set, mirroring the vitest harness',
|
|
309
|
+
})),
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
const vtClean = VALUE_TYPE_SCENARIOS.filter((s) => s.divergence === undefined)
|
|
313
|
+
const vtThrowing = VALUE_TYPE_SCENARIOS.filter((s) => s.divergence === 'throw')
|
|
314
|
+
const vtSnapping = VALUE_TYPE_SCENARIOS.filter((s) => s.divergence === 'snap')
|
|
315
|
+
const native = nativeAdapter()
|
|
316
|
+
const vtResults: ScenarioResult[] = [
|
|
317
|
+
...vtClean.map((sc) =>
|
|
318
|
+
safely(sc.id, () => {
|
|
319
|
+
if (oracle === null) return oracleMissing(sc.id)
|
|
320
|
+
const a = runValueTypeScenario(sc, nativeAdapter())
|
|
321
|
+
if (a.verdict !== 'pass') return a
|
|
322
|
+
const b = runValueTypeScenario(sc, oracle)
|
|
323
|
+
const cmp = compareValueTypeSeries(a, b)
|
|
324
|
+
return cmp.agree
|
|
325
|
+
? a
|
|
326
|
+
: {
|
|
327
|
+
...a,
|
|
328
|
+
verdict: 'fail' as const,
|
|
329
|
+
detail: `vs oracle: ${JSON.stringify(cmp.mismatches[0])}`,
|
|
330
|
+
}
|
|
331
|
+
}),
|
|
332
|
+
),
|
|
333
|
+
...vtThrowing.map((sc) =>
|
|
334
|
+
safely(sc.id, () => {
|
|
335
|
+
if (oracle === null) return oracleMissing(sc.id)
|
|
336
|
+
let nativeThrew = false
|
|
337
|
+
try {
|
|
338
|
+
native.mixValueType?.(sc.from, sc.to, 0.5)
|
|
339
|
+
} catch {
|
|
340
|
+
nativeThrew = true
|
|
341
|
+
}
|
|
342
|
+
let oracleThrew = false
|
|
343
|
+
try {
|
|
344
|
+
oracle.mixValueType?.(sc.from, sc.to, 0.5)
|
|
345
|
+
} catch {
|
|
346
|
+
oracleThrew = true
|
|
347
|
+
}
|
|
348
|
+
const pass = nativeThrew && !oracleThrew
|
|
349
|
+
return {
|
|
350
|
+
id: sc.id,
|
|
351
|
+
engine: 'device',
|
|
352
|
+
verdict: pass ? ('pass' as const) : ('fail' as const),
|
|
353
|
+
readings: [],
|
|
354
|
+
detail: pass
|
|
355
|
+
? 'native threw loud; oracle snapped (divergence: throw)'
|
|
356
|
+
: `nativeThrew=${String(nativeThrew)} oracleThrew=${String(oracleThrew)}`,
|
|
357
|
+
}
|
|
358
|
+
}),
|
|
359
|
+
),
|
|
360
|
+
...vtSnapping.map((sc) =>
|
|
361
|
+
safely(sc.id, () => {
|
|
362
|
+
if (oracle === null) return oracleMissing(sc.id)
|
|
363
|
+
const mid = native.mixValueType?.(sc.from, sc.to, 0.5)
|
|
364
|
+
const nativeMixed = mid !== sc.from && mid !== sc.to
|
|
365
|
+
const a = runValueTypeScenario(sc, nativeAdapter())
|
|
366
|
+
const b = runValueTypeScenario(sc, oracle)
|
|
367
|
+
const cmp = compareValueTypeSeries(a, b)
|
|
368
|
+
const pass = !cmp.agree && cmp.mismatches.length > 0 && nativeMixed
|
|
369
|
+
return {
|
|
370
|
+
id: sc.id,
|
|
371
|
+
engine: 'device',
|
|
372
|
+
verdict: pass ? ('pass' as const) : ('fail' as const),
|
|
373
|
+
readings: [],
|
|
374
|
+
detail: pass
|
|
375
|
+
? 'engines diverge as documented; native genuinely mixes named colors'
|
|
376
|
+
: `agree=${String(cmp.agree)} nativeMixed=${String(nativeMixed)}`,
|
|
377
|
+
}
|
|
378
|
+
}),
|
|
379
|
+
),
|
|
380
|
+
]
|
|
381
|
+
|
|
382
|
+
const suites = [
|
|
383
|
+
summarize(
|
|
384
|
+
`SPRING_SCENARIOS (${SPRING_SCENARIOS.length}) — native vs motion-dom oracle`,
|
|
385
|
+
springResults,
|
|
386
|
+
),
|
|
387
|
+
summarize(
|
|
388
|
+
`VALUE_TYPE_SCENARIOS (${VALUE_TYPE_SCENARIOS.length}) — harness contracts`,
|
|
389
|
+
vtResults,
|
|
390
|
+
),
|
|
391
|
+
summarize(
|
|
392
|
+
`COMPONENT_SCENARIOS (${COMPONENT_SCENARIOS.length}) — fake engine`,
|
|
393
|
+
COMPONENT_SCENARIOS.map((s) => safely(s.id, () => runComponentScenario(s))),
|
|
394
|
+
),
|
|
395
|
+
summarize(
|
|
396
|
+
`PRESENCE_SCENARIOS (${PRESENCE_SCENARIOS.length}) — fake engine (M2.3)`,
|
|
397
|
+
PRESENCE_SCENARIOS.map((s) => safely(s.id, () => runPresenceScenario(s))),
|
|
398
|
+
),
|
|
399
|
+
summarize(
|
|
400
|
+
`GESTURE_SCENARIOS (${GESTURE_SCENARIOS.length}) — fake engine (M2.4)`,
|
|
401
|
+
GESTURE_SCENARIOS.map((s) => safely(s.id, () => runGestureScenario(s))),
|
|
402
|
+
),
|
|
403
|
+
summarize(
|
|
404
|
+
`LAYOUT_SCENARIOS (${LAYOUT_SCENARIOS.length}) — fake engine (M2.5)`,
|
|
405
|
+
LAYOUT_SCENARIOS.map((s) => safely(s.id, () => runLayoutScenario(s))),
|
|
406
|
+
),
|
|
407
|
+
]
|
|
408
|
+
return { suites, oracleError }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function ConformanceScreen() {
|
|
412
|
+
const [suiteRows] = useState<Row[]>(() => runSuitePortability())
|
|
413
|
+
const [checkRows] = useState<Row[]>(() => {
|
|
414
|
+
const rows = [checkDriverParity(), checkRetargetContinuity()]
|
|
415
|
+
disposeCheckDrivers() // the parity rigs are read out — free their UI-runtime records
|
|
416
|
+
return rows
|
|
417
|
+
})
|
|
418
|
+
const [m22Run] = useState<{ suites: SuiteSummary[]; oracleError: string | null }>(() =>
|
|
419
|
+
runM22Suites(),
|
|
420
|
+
)
|
|
421
|
+
const m22Suites = m22Run.suites
|
|
422
|
+
const settleRow = useSettleCancelCheck()
|
|
423
|
+
|
|
424
|
+
const allRows = [...suiteRows, ...checkRows, ...(settleRow === null ? [] : [settleRow])]
|
|
425
|
+
const expected = suiteRows.length + checkRows.length + 1
|
|
426
|
+
const allDone = allRows.length === expected
|
|
427
|
+
// Fail-closed: a missing cross-engine oracle FAILS the banner — the suites' claim is cross-engine
|
|
428
|
+
// comparison, and an unavailable independent verifier is a blocked gate, never a pass.
|
|
429
|
+
const suitesClean = m22Suites.every((suite) => suite.fail === 0) && m22Run.oracleError === null
|
|
430
|
+
// REQ-CONFORM-011 (M2 r4 major 6d4e1f7a92c3, r5 major 6e730d4c1a82): unassertable NEVER
|
|
431
|
+
// counts toward a pass. `routed > 0 → FAIL` is UNCONDITIONAL — it holds even for a routed row
|
|
432
|
+
// whose parity is owned at another altitude (CMP-R7-cross-engine-parity is discharged by the
|
|
433
|
+
// web e2e + device proofs, yet that parity does not execute in THIS suite, so it stays routed
|
|
434
|
+
// and still fails the banner). The routed count renders beside the verdict so it names its cause.
|
|
435
|
+
const routedTotal = m22Suites.reduce((total, suite) => total + suite.unassertable, 0)
|
|
436
|
+
const verdict = conformanceBannerVerdict({
|
|
437
|
+
allDone,
|
|
438
|
+
rowsPass: allRows.every((row) => row.pass),
|
|
439
|
+
suitesClean,
|
|
440
|
+
routed: routedTotal,
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<ScrollView style={styles.scroll} contentContainerStyle={styles.container}>
|
|
445
|
+
<Text style={styles.title}>M2 on-device conformance</Text>
|
|
446
|
+
<Text style={styles.subtitle}>
|
|
447
|
+
suite portability (in-suite engines, device Hermes) + worklet-driver behavioral checks
|
|
448
|
+
</Text>
|
|
449
|
+
<View
|
|
450
|
+
style={[
|
|
451
|
+
styles.banner,
|
|
452
|
+
verdict === 'PASS' ? styles.pass : verdict === 'FAIL' ? styles.fail : styles.pending,
|
|
453
|
+
]}
|
|
454
|
+
>
|
|
455
|
+
<Text style={styles.bannerText}>
|
|
456
|
+
{verdict === 'FAIL' && routedTotal > 0 ? `FAIL · ${routedTotal} ROUTED` : verdict}
|
|
457
|
+
</Text>
|
|
458
|
+
</View>
|
|
459
|
+
{m22Run.oracleError !== null ? (
|
|
460
|
+
<RowLine
|
|
461
|
+
row={{
|
|
462
|
+
id: 'motion-dom oracle FAILED TO LOAD (cross-engine legs not run)',
|
|
463
|
+
pass: false,
|
|
464
|
+
detail: m22Run.oracleError,
|
|
465
|
+
}}
|
|
466
|
+
/>
|
|
467
|
+
) : null}
|
|
468
|
+
<Text style={styles.section}>suite verdict counts (non-pass expanded)</Text>
|
|
469
|
+
{m22Suites.map((suite) => (
|
|
470
|
+
<View key={suite.name}>
|
|
471
|
+
<View style={styles.row}>
|
|
472
|
+
<Text
|
|
473
|
+
style={[
|
|
474
|
+
styles.verdict,
|
|
475
|
+
suite.fail > 0 ? styles.bad : suite.unassertable > 0 ? styles.dim : styles.ok,
|
|
476
|
+
]}
|
|
477
|
+
>
|
|
478
|
+
{suite.fail > 0 ? 'FAIL' : suite.unassertable > 0 ? 'ROUTED' : 'PASS'}
|
|
479
|
+
</Text>
|
|
480
|
+
<View style={styles.rowBody}>
|
|
481
|
+
<Text style={styles.rowId}>{suite.name}</Text>
|
|
482
|
+
<Text style={styles.rowDetail}>
|
|
483
|
+
{suite.pass} pass · {suite.fail} fail · {suite.unassertable} unassertable
|
|
484
|
+
</Text>
|
|
485
|
+
</View>
|
|
486
|
+
</View>
|
|
487
|
+
{suite.nonPass.map((result) =>
|
|
488
|
+
result.verdict === 'unassertable' ? (
|
|
489
|
+
// A routed obligation renders DISTINCT — never a green PASS row
|
|
490
|
+
// (REQ-CONFORM-011: unassertable is not a pass).
|
|
491
|
+
<View key={result.id} style={styles.row}>
|
|
492
|
+
<Text style={[styles.verdict, styles.dim]}>ROUTED</Text>
|
|
493
|
+
<View style={styles.rowBody}>
|
|
494
|
+
<Text style={styles.rowId}>{`${result.id} [unassertable]`}</Text>
|
|
495
|
+
<Text style={styles.rowDetail} numberOfLines={3}>
|
|
496
|
+
{result.detail ?? result.verdict}
|
|
497
|
+
</Text>
|
|
498
|
+
</View>
|
|
499
|
+
</View>
|
|
500
|
+
) : (
|
|
501
|
+
<RowLine
|
|
502
|
+
key={result.id}
|
|
503
|
+
row={{
|
|
504
|
+
id: `${result.id} [${result.verdict}]`,
|
|
505
|
+
pass: false,
|
|
506
|
+
detail: result.detail ?? result.verdict,
|
|
507
|
+
}}
|
|
508
|
+
/>
|
|
509
|
+
),
|
|
510
|
+
)}
|
|
511
|
+
</View>
|
|
512
|
+
))}
|
|
513
|
+
<Text style={styles.section}>DRIVER_SCENARIOS — as-is, reference driver (M2.1)</Text>
|
|
514
|
+
{suiteRows.map((row) => (
|
|
515
|
+
<RowLine key={row.id} row={row} />
|
|
516
|
+
))}
|
|
517
|
+
<Text style={styles.section}>worklet driver — behavioral checks (M2.1)</Text>
|
|
518
|
+
{checkRows.map((row) => (
|
|
519
|
+
<RowLine key={row.id} row={row} />
|
|
520
|
+
))}
|
|
521
|
+
<RowLine
|
|
522
|
+
row={
|
|
523
|
+
settleRow ?? { id: 'live settle-cancel', pass: false, detail: 'waiting for settle edge…' }
|
|
524
|
+
}
|
|
525
|
+
pending={settleRow === null}
|
|
526
|
+
/>
|
|
527
|
+
</ScrollView>
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function RowLine({ row, pending = false }: { readonly row: Row; readonly pending?: boolean }) {
|
|
532
|
+
return (
|
|
533
|
+
<View style={styles.row}>
|
|
534
|
+
<Text style={[styles.verdict, pending ? styles.dim : row.pass ? styles.ok : styles.bad]}>
|
|
535
|
+
{pending ? '…' : row.pass ? 'PASS' : 'FAIL'}
|
|
536
|
+
</Text>
|
|
537
|
+
<View style={styles.rowBody}>
|
|
538
|
+
<Text style={styles.rowId}>{row.id}</Text>
|
|
539
|
+
<Text style={styles.rowDetail} numberOfLines={3}>
|
|
540
|
+
{row.detail}
|
|
541
|
+
</Text>
|
|
542
|
+
</View>
|
|
543
|
+
</View>
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const styles = StyleSheet.create({
|
|
548
|
+
scroll: { flex: 1, backgroundColor: '#0b0b12' },
|
|
549
|
+
container: { paddingVertical: 72, paddingHorizontal: 20 },
|
|
550
|
+
title: { color: '#fff', fontSize: 22, fontWeight: '700', textAlign: 'center' },
|
|
551
|
+
subtitle: { color: '#8a8aa0', fontSize: 12, marginTop: 6, textAlign: 'center' },
|
|
552
|
+
banner: {
|
|
553
|
+
alignSelf: 'center',
|
|
554
|
+
paddingHorizontal: 36,
|
|
555
|
+
paddingVertical: 10,
|
|
556
|
+
borderRadius: 10,
|
|
557
|
+
marginTop: 18,
|
|
558
|
+
marginBottom: 10,
|
|
559
|
+
},
|
|
560
|
+
pass: { backgroundColor: '#1d7a3e' },
|
|
561
|
+
fail: { backgroundColor: '#a32633' },
|
|
562
|
+
pending: { backgroundColor: '#6b6b28' },
|
|
563
|
+
bannerText: { color: '#fff', fontSize: 22, fontWeight: '800', letterSpacing: 2 },
|
|
564
|
+
section: { color: '#8a8aa0', fontSize: 13, fontWeight: '700', marginTop: 18, marginBottom: 6 },
|
|
565
|
+
row: { flexDirection: 'row', marginTop: 8 },
|
|
566
|
+
verdict: { width: 52, fontSize: 12, fontWeight: '800', paddingTop: 1 },
|
|
567
|
+
ok: { color: '#4ade80' },
|
|
568
|
+
bad: { color: '#f87171' },
|
|
569
|
+
dim: { color: '#8a8aa0' },
|
|
570
|
+
rowBody: { flex: 1 },
|
|
571
|
+
rowId: { color: '#e6e6f0', fontSize: 13, fontWeight: '600' },
|
|
572
|
+
rowDetail: { color: '#6f6f85', fontSize: 11, marginTop: 2 },
|
|
573
|
+
})
|