@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.
Files changed (117) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +55 -0
  4. package/android/build.gradle +24 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/com/unrulysystems/nativemotion/GestureExclusionModule.kt +55 -0
  7. package/android/src/main/java/com/unrulysystems/nativemotion/NativeMotionPackage.kt +27 -0
  8. package/package.json +57 -0
  9. package/react-native.config.js +14 -0
  10. package/src/native/driver/instantWindow.ts +31 -0
  11. package/src/native/driver/strictModeReplay.ts +20 -0
  12. package/src/native/driver/uiLayoutEngine.ts +845 -0
  13. package/src/native/driver/uiLayoutGraph.ts +338 -0
  14. package/src/native/driver/uiValueChannel.ts +4640 -0
  15. package/src/native/driver/workletDriver.ts +3860 -0
  16. package/src/native/motion/AnimatePresence.tsx +1605 -0
  17. package/src/native/motion/LayoutGroup.tsx +163 -0
  18. package/src/native/motion/MotionConfig.tsx +400 -0
  19. package/src/native/motion/MotionRoot.tsx +250 -0
  20. package/src/native/motion/MotionView.tsx +6761 -0
  21. package/src/native/motion/addScaleCorrector.ts +97 -0
  22. package/src/native/motion/colorEndpointUiFeed.ts +32 -0
  23. package/src/native/motion/colorProjection.ts +39 -0
  24. package/src/native/motion/composeTransform.ts +271 -0
  25. package/src/native/motion/constraintMeasure.ts +157 -0
  26. package/src/native/motion/deferredPendingSnapshots.ts +25 -0
  27. package/src/native/motion/discreteProjection.ts +36 -0
  28. package/src/native/motion/dragAncestorPanContext.ts +28 -0
  29. package/src/native/motion/dragControls.ts +152 -0
  30. package/src/native/motion/dragGestureWiring.ts +1168 -0
  31. package/src/native/motion/dragHandoffBinding.ts +179 -0
  32. package/src/native/motion/dragHubStream.ts +756 -0
  33. package/src/native/motion/dragPropagationLock.ts +57 -0
  34. package/src/native/motion/driverValueChannel.ts +3288 -0
  35. package/src/native/motion/externalDragDriver.ts +214 -0
  36. package/src/native/motion/frameData.ts +31 -0
  37. package/src/native/motion/gestureBinding.ts +107 -0
  38. package/src/native/motion/gestureStateGate.ts +563 -0
  39. package/src/native/motion/gestureStateResolver.ts +286 -0
  40. package/src/native/motion/identityValueChannelControllerAdapter.ts +2043 -0
  41. package/src/native/motion/identityValueChannelControllerReconciliation.ts +176 -0
  42. package/src/native/motion/identityValueChannelLaneMarker.ts +14 -0
  43. package/src/native/motion/imperativeAnimate.ts +816 -0
  44. package/src/native/motion/keyframeTiming.ts +7 -0
  45. package/src/native/motion/layoutIdBinding.ts +261 -0
  46. package/src/native/motion/layoutIdFlightConfig.ts +51 -0
  47. package/src/native/motion/layoutProjection.ts +89 -0
  48. package/src/native/motion/layoutScroll.ts +204 -0
  49. package/src/native/motion/layoutTransition.ts +1302 -0
  50. package/src/native/motion/lengthLayoutContext.tsx +100 -0
  51. package/src/native/motion/lengthLayoutHost.ts +44 -0
  52. package/src/native/motion/mappedKeys.ts +184 -0
  53. package/src/native/motion/motionViewController.ts +3180 -0
  54. package/src/native/motion/nativeHostMarker.ts +22 -0
  55. package/src/native/motion/panSessionWiring.ts +127 -0
  56. package/src/native/motion/pathTransition.ts +101 -0
  57. package/src/native/motion/popLayout.ts +176 -0
  58. package/src/native/motion/presenceBinding.ts +382 -0
  59. package/src/native/motion/scaleCorrectorRegistry.ts +123 -0
  60. package/src/native/motion/scrollValues.ts +260 -0
  61. package/src/native/motion/serializablePayload.ts +151 -0
  62. package/src/native/motion/severity.ts +16 -0
  63. package/src/native/motion/shippedSurface.ts +843 -0
  64. package/src/native/motion/staticLengthGate.ts +86 -0
  65. package/src/native/motion/styleBaseGate.ts +72 -0
  66. package/src/native/motion/styleValueBinding.ts +657 -0
  67. package/src/native/motion/systemGestureExclusion.ts +61 -0
  68. package/src/native/motion/tapGestureWiring.ts +215 -0
  69. package/src/native/motion/transformOrder.ts +38 -0
  70. package/src/native/motion/transformStringBinding.ts +200 -0
  71. package/src/native/motion/transformTemplateGate.ts +45 -0
  72. package/src/native/motion/transitionGate.ts +435 -0
  73. package/src/native/motion/useAnimate.ts +52 -0
  74. package/src/native/motion/useCycle.ts +44 -0
  75. package/src/native/motion/useInstantTransition.ts +68 -0
  76. package/src/native/motion/useReducedMotion.ts +58 -0
  77. package/src/native/motion/useScroll.ts +162 -0
  78. package/src/native/motion/useViewportScroll.ts +29 -0
  79. package/src/native/motion/valueChannel.ts +7121 -0
  80. package/src/native/motion/valueHooks.ts +901 -0
  81. package/src/native/motion/variantChildRegistry.ts +67 -0
  82. package/src/native/motion/variantContext.tsx +172 -0
  83. package/src/native/motion/variantProps.ts +598 -0
  84. package/src/native.ts +159 -0
  85. package/src/verification/harnessMetrics.ts +78 -0
  86. package/src/verification/probe/LayoutIdentityWorkletProbe.tsx +251 -0
  87. package/src/verification/probe/WorkletParityProbe.tsx +162 -0
  88. package/src/verification/probe/layoutIdentityProbeEngine.ts +429 -0
  89. package/src/verification/screens/ArcPathChecksScreen.tsx +273 -0
  90. package/src/verification/screens/BooleanAnimateChecksScreen.tsx +358 -0
  91. package/src/verification/screens/ChoreographyGalleryScreen.tsx +1010 -0
  92. package/src/verification/screens/ColorBindingChecksScreen.tsx +568 -0
  93. package/src/verification/screens/CompletionChecksScreen.tsx +294 -0
  94. package/src/verification/screens/ConformanceScreen.tsx +573 -0
  95. package/src/verification/screens/ContentionProbeScreen.tsx +224 -0
  96. package/src/verification/screens/DriverSmokeScreen.tsx +99 -0
  97. package/src/verification/screens/DurationOnlyTweenChecksScreen.tsx +294 -0
  98. package/src/verification/screens/DynamicDragConfigChecksScreen.tsx +237 -0
  99. package/src/verification/screens/FrameDataChecksScreen.tsx +252 -0
  100. package/src/verification/screens/GestureChecksScreen.tsx +685 -0
  101. package/src/verification/screens/InstantTransitionChecksScreen.tsx +839 -0
  102. package/src/verification/screens/LayoutAnimationStartChecksScreen.tsx +648 -0
  103. package/src/verification/screens/LayoutChecksScreen.tsx +822 -0
  104. package/src/verification/screens/LayoutCommitSpikeScreen.tsx +142 -0
  105. package/src/verification/screens/MotionViewChecksScreen.tsx +930 -0
  106. package/src/verification/screens/PresenceChecksScreen.tsx +614 -0
  107. package/src/verification/screens/ReducedMotionChecksScreen.tsx +614 -0
  108. package/src/verification/screens/RestThresholdChecksScreen.tsx +634 -0
  109. package/src/verification/screens/ScaleCorrectorChecksScreen.tsx +425 -0
  110. package/src/verification/screens/SharedLayoutContinuityChecksScreen.tsx +338 -0
  111. package/src/verification/screens/SharedLayoutCrossfadeChecksScreen.tsx +425 -0
  112. package/src/verification/screens/TransitionDefaultSelectionChecksScreen.tsx +514 -0
  113. package/src/verification/screens/ViewportScrollAliasChecksScreen.tsx +303 -0
  114. package/src/verification/screens/conformanceBanner.ts +21 -0
  115. package/src/verification/screens/proofConsoleTap.ts +18 -0
  116. package/src/verification.ts +47 -0
  117. package/src/web.ts +98 -0
@@ -0,0 +1,648 @@
1
+ // U7e layout-animation-start device checks (the U7e packet's device floor): a NATIVE nested
2
+ // layout card with the start/complete readouts on screen, witnessing live on device the
3
+ // per-flight callback law the W2 web leg pinned against the raw pin (u7e-layout-animation-
4
+ // start.e2e.ts — startAnimation's onPlay/onComplete, create-projection-node.ts:567-574):
5
+ //
6
+ // - start fires ONCE per LOGICAL flight at flight creation (the host-layout worklet's UI
7
+ // commit edge; the public crossing is qualified per React layout-signature generation —
8
+ // the pin's didUpdate analogue — while the internal beginCombinedLayoutFlight reset still
9
+ // runs at every commit, F1), STRICTLY BEFORE that flight's
10
+ // complete (the JS-thread FIFO read: both callbacks land via the same scheduleOnRN
11
+ // crossing, so JS receipt order is the law);
12
+ // - a mid-flight retarget fires start AGAIN for the new flight and the interrupted flight
13
+ // NEVER completes (the pin stops the in-flight animation first — stop never fires
14
+ // complete), so start count > complete count across the interruption and complete catches
15
+ // up only for the final settled flight;
16
+ // - a mount with no layout change fires neither callback.
17
+ //
18
+ // The subject mirrors the pinned catalog Animation-layout-nested-position.tsx (read-only
19
+ // reference, ../motion/dev/react/src/examples/): the three-deep `layout` tree (parent → child
20
+ // → grandchild animating on parent size-delta propagation) on the verbatim 3s circIn tween,
21
+ // useCycle(true, false) toggling open/closed, callbacks on the PARENT. Recorded adaptations
22
+ // (the U7d/U7e-W2 pattern): geometry halved (400/200/150 → 200/100/75, left 400 → 200) to fit
23
+ // the screen, the catalog's onClick-on-parent becomes an explicit toggle button (the on-screen
24
+ // control; a mid-flight press IS the retarget trigger), and the console.log callbacks become
25
+ // count + mount-relative-stamp latches — the same signal a screenshot reads.
26
+ //
27
+ // The screen SELF-DRIVES (the iOS simulator has no tap injection): after a mount-quiet dwell
28
+ // the timeline presses once (flight A), awaits its settle with a fail-closed poll, then fires
29
+ // the retarget pair (flight B, then flight C 150ms after B's start is observed — waiting for
30
+ // the start first avoids the pendingAnimation frame race, the W2 discipline). Four rows:
31
+ //
32
+ // 1. MOUNT QUIET — zero starts and zero completes across the boot dwell (a mount-snap
33
+ // miswire treating the initial measure as a qualifying commit fails here).
34
+ // 2. ONE FLIGHT — exactly one start and one complete for flight A; the start stamp follows
35
+ // the press stamp inside a bounded window (start fires at flight CREATION, not at the
36
+ // first frame or the settle) and strictly precedes the complete; the complete lands a
37
+ // full 3s-tween class after the start (a settle-deferred start reads complete − start ≈
38
+ // 0 and fails the floor; a per-frame start miswire fails starts === 1).
39
+ // 3. RETARGET — across flights B and C: starts reaches 3 while completes holds at 1 (the
40
+ // latched gap: B interrupted + C in flight), then C's settle brings completes to exactly
41
+ // 2 = starts − 1 (B's complete NEVER arrives, verified across a post-settle stability
42
+ // dwell), with C's complete strictly after C's start and a full 3s class after it (a
43
+ // retarget continuing B's clock completes early and fails the floor).
44
+ // 4. CLEAN RUN — zero refusal reports through the console tap (the U5/U7c pattern:
45
+ // console.error is the severity law's production error channel) across the whole
46
+ // timeline.
47
+ //
48
+ // A screenshot is the assertion; every stamp is mount-relative Date.now() at JS receipt, and
49
+ // every wait is a fail-closed poll with a loud deadline — a dropped callback is a timeout,
50
+ // never a silent skip. The console tap is evidence capture, never suppression.
51
+
52
+ import { useCallback, useEffect, useRef, useState } from 'react'
53
+ import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'
54
+ import { MotionView } from '../../native/motion/MotionView'
55
+ import { ambientNativeSeverity } from '../../native/motion/severity'
56
+ import { useCycle } from '../../native/motion/useCycle'
57
+
58
+ const ROW_MOUNT = 'mount quiet: no layout change fires neither start nor complete'
59
+ const ROW_FLIGHT = 'one flight: start fires exactly once, strictly before its complete (3s class)'
60
+ const ROW_RETARGET = 'mid-flight retarget: start re-fires, the stopped flight never completes'
61
+ const ROW_CLEAN = 'clean run: zero refusal reports through the console tap'
62
+ const ROWS = [ROW_MOUNT, ROW_FLIGHT, ROW_RETARGET, ROW_CLEAN] as const
63
+
64
+ // The catalog's transition verbatim (Animation-layout-nested-position.tsx:6).
65
+ const TRANSITION = { duration: 3, ease: 'circIn' } as const
66
+
67
+ // Row 2 bounds (ms): the start stamp must follow its press stamp (JS FIFO) inside a bounded
68
+ // window — the pin's web witness reads 3–5ms against a ~50ms Cypress witness; the native
69
+ // start crosses UI → JS at the commit edge, so the bound carries generous delivery slack
70
+ // while still failing a settle-deferred start (~3000ms late) loud. The flight floor is the
71
+ // W2 leg's verbatim: the 3s tween must run its course (> 2000); a settle-deferred start reads
72
+ // complete − start ≈ 0.
73
+ const START_LATENCY_BOUND_MS = 1000
74
+ const FLIGHT_FLOOR_MS = 2000
75
+ // Row 3's retarget spacing: flight C's press lands 150ms after flight B's START is observed
76
+ // (waiting for the start first avoids the pendingAnimation frame race — a retarget inside it
77
+ // would replace the pending flight and fire start only once, a fixture race, not the law).
78
+ const RETARGET_DELAY_MS = 150
79
+ // Post-settle stability dwell: a miswired late complete (a stop-fires-complete for the
80
+ // interrupted flight) flushes well inside this absence window.
81
+ const STABILITY_MS = 600
82
+
83
+ // Fail-closed deadlines (ms post-arm): a callback that never fires is a loud timeout, never
84
+ // a silent skip. The settle deadline comfortably covers the 3s tween plus delivery.
85
+ const START_DEADLINE_MS = 6000
86
+ const SETTLE_DEADLINE_MS = 12000
87
+
88
+ // Timeline: the mount-quiet dwell doubles as row 1's absence window (2000ms is many times
89
+ // every observed start latency); flight A's press fires at its end. Every later step is
90
+ // chained off fail-closed polls, never fixed sleeps.
91
+ const T_TRIGGER_A = 2000
92
+
93
+ interface RowResult {
94
+ readonly pass: boolean
95
+ readonly detail: string
96
+ }
97
+
98
+ // The latched readouts (the W2 fixture's data attributes, mirrored at native altitude):
99
+ // counts plus mount-relative stamps (-1 = not yet). JS receipt order is the ordering law.
100
+ interface Latches {
101
+ presses: number
102
+ starts: number
103
+ completes: number
104
+ firstPressAt: number
105
+ lastPressAt: number
106
+ firstStartAt: number
107
+ lastStartAt: number
108
+ lastCompleteAt: number
109
+ startAts: number[]
110
+ }
111
+
112
+ const freshLatches = (): Latches => ({
113
+ presses: 0,
114
+ starts: 0,
115
+ completes: 0,
116
+ firstPressAt: -1,
117
+ lastPressAt: -1,
118
+ firstStartAt: -1,
119
+ lastStartAt: -1,
120
+ lastCompleteAt: -1,
121
+ startAts: [],
122
+ })
123
+
124
+ // Latch rendering: mount-relative stamp, — while unfired.
125
+ const stamp = (at: number): string => (at === -1 ? '—' : `+${at.toFixed(0)}ms`)
126
+
127
+ export function LayoutAnimationStartChecksScreen() {
128
+ const severity = ambientNativeSeverity()
129
+
130
+ const [isOpen, toggleIsOpen] = useCycle(true, false)
131
+ const [stage, setStage] = useState('boot: mount-quiet dwell (open tree, no layout change)')
132
+ const [live, setLive] = useState<Latches>(freshLatches())
133
+ const [rows, setRows] = useState<Partial<Record<(typeof ROWS)[number], RowResult>>>({})
134
+
135
+ const mountStampRef = useRef(Date.now())
136
+ const latchesRef = useRef<Latches | null>(null)
137
+ if (latchesRef.current === null) latchesRef.current = freshLatches()
138
+
139
+ // Row 3's evidence: starts − completes latched at flight C's start (expect 2 — flight B
140
+ // interrupted + flight C in flight).
141
+ const retargetGapRef = useRef(-1)
142
+
143
+ const rafsRef = useRef<ReturnType<typeof requestAnimationFrame>[]>([])
144
+ const reportsRef = useRef<string[]>([])
145
+
146
+ const trackRaf = (id: ReturnType<typeof requestAnimationFrame>): void => {
147
+ rafsRef.current.push(id)
148
+ }
149
+
150
+ const now = useCallback((): number => Date.now() - mountStampRef.current, [])
151
+
152
+ const publish = useCallback((): void => {
153
+ setLive({ ...latchesRef.current! })
154
+ }, [])
155
+
156
+ // The toggle IS the retarget trigger: the on-screen control and the auto timeline share it.
157
+ const press = useCallback((): void => {
158
+ const latches = latchesRef.current!
159
+ const at = now()
160
+ latches.presses += 1
161
+ if (latches.firstPressAt === -1) latches.firstPressAt = at
162
+ latches.lastPressAt = at
163
+ publish()
164
+ toggleIsOpen()
165
+ }, [now, publish, toggleIsOpen])
166
+
167
+ const onStart = useCallback((): void => {
168
+ const latches = latchesRef.current!
169
+ const at = now()
170
+ latches.starts += 1
171
+ if (latches.firstStartAt === -1) latches.firstStartAt = at
172
+ latches.lastStartAt = at
173
+ latches.startAts = [...latches.startAts, at]
174
+ publish()
175
+ }, [now, publish])
176
+
177
+ const onComplete = useCallback((): void => {
178
+ const latches = latchesRef.current!
179
+ latches.completes += 1
180
+ latches.lastCompleteAt = now()
181
+ publish()
182
+ }, [now, publish])
183
+
184
+ const finalize = (id: (typeof ROWS)[number], pass: boolean, detail: string): void => {
185
+ setRows((previous) => ({ ...previous, [id]: { pass, detail } }))
186
+ }
187
+
188
+ useEffect(() => {
189
+ const latches = latchesRef.current!
190
+
191
+ // The production error channel tap (the U5/U7c pattern): refusal reports ride
192
+ // consoleReporter → console.error. Installed for this screen's lifetime only; everything
193
+ // passes through.
194
+ const originalError = console.error
195
+ console.error = (...args: unknown[]) => {
196
+ for (const arg of args) {
197
+ const message = arg instanceof Error ? arg.message : typeof arg === 'string' ? arg : null
198
+ if (message !== null && /motion/i.test(message)) reportsRef.current.push(message)
199
+ }
200
+ originalError(...args)
201
+ }
202
+
203
+ const timers: ReturnType<typeof setTimeout>[] = []
204
+ const at = (ms: number, run: () => void): void => {
205
+ timers.push(setTimeout(run, ms))
206
+ }
207
+
208
+ // Fail-closed observation primitive: poll per rAF until the condition holds or the
209
+ // deadline passes. `onResolve` fires exactly once with ok=false on the deadline.
210
+ const pollUntil = (
211
+ condition: () => boolean,
212
+ deadlineAt: number,
213
+ onResolve: (ok: boolean) => void,
214
+ ): void => {
215
+ const step = (): void => {
216
+ if (condition()) {
217
+ onResolve(true)
218
+ return
219
+ }
220
+ if (now() >= deadlineAt) {
221
+ onResolve(false)
222
+ return
223
+ }
224
+ trackRaf(requestAnimationFrame(step))
225
+ }
226
+ trackRaf(requestAnimationFrame(step))
227
+ }
228
+
229
+ // 4. CLEAN RUN + terminal stage, scheduled by whichever chain end lands last.
230
+ const finish = (): void => {
231
+ at(STABILITY_MS, () => {
232
+ const reports = reportsRef.current
233
+ finalize(
234
+ ROW_CLEAN,
235
+ reports.length === 0,
236
+ reports.length === 0
237
+ ? `0 refusal reports through the console tap across ${now().toFixed(0)}ms of ` +
238
+ 'the mount dwell, flight A, and the B/C retarget pair'
239
+ : `${reports.length} report(s): ${reports
240
+ .map((message) => `"${message.slice(0, 80)}…"`)
241
+ .join(' | ')}`,
242
+ )
243
+ setStage('done')
244
+ })
245
+ }
246
+
247
+ const countsLine = (): string =>
248
+ `presses ${latches.presses} · starts ${latches.starts} · completes ${latches.completes}`
249
+
250
+ // 3. RETARGET — flight B, then flight C RETARGET_DELAY_MS after B's start is observed.
251
+ // Finalizes after C's settle plus the stability dwell: starts 3, completes EXACTLY 2
252
+ // (B's complete never arrives), the latched gap 2, C's complete strictly after C's
253
+ // start and a full 3s class after it.
254
+ const runRetargetPhase = (): void => {
255
+ setStage('flight A settled — pressing flight B (the retarget source)')
256
+ press()
257
+ pollUntil(
258
+ () => latches.starts >= 2,
259
+ now() + START_DEADLINE_MS,
260
+ (okB) => {
261
+ if (!okB) {
262
+ finalize(
263
+ ROW_RETARGET,
264
+ false,
265
+ `flight B's start never fired within the ${START_DEADLINE_MS}ms deadline ` +
266
+ `(${countsLine()}) — the per-flight law is unwitnessable (a start-once-per-` +
267
+ 'component miswire leaves starts at 1)',
268
+ )
269
+ setStage('retarget row finalized (FAIL)')
270
+ finish()
271
+ return
272
+ }
273
+ at(RETARGET_DELAY_MS, () => {
274
+ setStage('flight B in flight — pressing flight C (the mid-flight retarget)')
275
+ press()
276
+ pollUntil(
277
+ () => latches.starts >= 3,
278
+ now() + START_DEADLINE_MS,
279
+ (okC) => {
280
+ if (!okC) {
281
+ finalize(
282
+ ROW_RETARGET,
283
+ false,
284
+ `flight C's start never fired within the ${START_DEADLINE_MS}ms deadline ` +
285
+ `(${countsLine()}) — the retarget did not start a new flight`,
286
+ )
287
+ setStage('retarget row finalized (FAIL)')
288
+ finish()
289
+ return
290
+ }
291
+ retargetGapRef.current = latches.starts - latches.completes
292
+ setStage(
293
+ `flight C in flight (starts − completes = ${retargetGapRef.current}: ` +
294
+ 'B interrupted + C in flight) — awaiting its settle',
295
+ )
296
+ pollUntil(
297
+ () => latches.completes >= 2,
298
+ now() + SETTLE_DEADLINE_MS,
299
+ (okSettle) => {
300
+ if (!okSettle) {
301
+ finalize(
302
+ ROW_RETARGET,
303
+ false,
304
+ `flight C never completed within the ${SETTLE_DEADLINE_MS}ms deadline ` +
305
+ `(${countsLine()}) — the retargeted flight never settled`,
306
+ )
307
+ setStage('retarget row finalized (FAIL)')
308
+ finish()
309
+ return
310
+ }
311
+ // The stability dwell: a stop-fires-complete miswire for the interrupted
312
+ // flight B flushes a THIRD complete inside this window.
313
+ at(STABILITY_MS, () => {
314
+ const gap = retargetGapRef.current
315
+ const flightC = latches.lastCompleteAt - latches.lastStartAt
316
+ let bad: string | null = null
317
+ if (latches.starts !== 3 || latches.completes !== 2) {
318
+ bad =
319
+ `post-settle counts read ${countsLine()}, want starts 3 / completes ` +
320
+ '2 — ' +
321
+ (latches.completes > 2
322
+ ? "the interrupted flight B's complete FIRED (a stop()-fires-" +
323
+ 'complete miswire; the pin stops the in-flight animation first ' +
324
+ 'and stop never fires complete)'
325
+ : 'a flight fired start without ever completing or a start was ' +
326
+ 'dropped')
327
+ } else if (gap !== 2) {
328
+ bad =
329
+ `the interruption gap latched ${gap}, want 2 (B interrupted + C in ` +
330
+ 'flight while completes held at 1) — start count never led complete ' +
331
+ 'count across the retarget'
332
+ } else if (latches.lastCompleteAt <= latches.lastStartAt) {
333
+ bad =
334
+ `flight C's complete (${latches.lastCompleteAt.toFixed(0)}ms) did ` +
335
+ `not strictly follow its start (${latches.lastStartAt.toFixed(0)}ms)`
336
+ } else if (flightC <= FLIGHT_FLOOR_MS) {
337
+ bad =
338
+ `flight C ran ${flightC.toFixed(0)}ms (floor ${FLIGHT_FLOOR_MS}ms) — ` +
339
+ "the retarget continued flight B's clock instead of starting a " +
340
+ 'fresh 3s tween'
341
+ }
342
+ finalize(
343
+ ROW_RETARGET,
344
+ bad === null,
345
+ bad ??
346
+ `two flights, two fresh starts: starts 3 / completes 2 = starts − 1 ` +
347
+ `(the interrupted flight B never completed, stable across a ` +
348
+ `${STABILITY_MS}ms post-settle dwell); the gap latched 2 at C's ` +
349
+ `start; C's start +${latches.lastStartAt.toFixed(0)}ms strictly ` +
350
+ `precedes its complete +${latches.lastCompleteAt.toFixed(0)}ms, a ` +
351
+ `${flightC.toFixed(0)}ms flight (floor ${FLIGHT_FLOOR_MS}ms — a ` +
352
+ 'FRESH 3s tween, not B’s clock)',
353
+ )
354
+ setStage('retarget row finalized')
355
+ finish()
356
+ })
357
+ },
358
+ )
359
+ },
360
+ )
361
+ })
362
+ },
363
+ )
364
+ }
365
+
366
+ // THE TRIGGER — row 1 finalizes on the dwell's evidence, then flight A's press fires and
367
+ // row 2 awaits its settle with a fail-closed poll.
368
+ at(T_TRIGGER_A, () => {
369
+ finalize(
370
+ ROW_MOUNT,
371
+ latches.starts === 0 && latches.completes === 0,
372
+ latches.starts === 0 && latches.completes === 0
373
+ ? `0 starts, 0 completes across the ${T_TRIGGER_A}ms boot dwell with the open tree ` +
374
+ 'mounted — the initial measure never qualified as a layout flight'
375
+ : `mount fired ${latches.starts} start(s) / ${latches.completes} complete(s) with ` +
376
+ 'no layout change — the initial measure was treated as a qualifying commit ' +
377
+ '(the mount-snap miswire)',
378
+ )
379
+ setStage('mount-quiet row finalized — pressing flight A (open → closed)')
380
+ const pressAAt = now()
381
+ press()
382
+ // A failed flight A leaves the retarget sequence unwitnessable — fail the row loud,
383
+ // never leave it pending (a pending row holds the banner at RUNNING forever).
384
+ const failRetargetUnwitnessable = (why: string): void => {
385
+ finalize(ROW_RETARGET, false, `unwitnessable — ${why}`)
386
+ }
387
+ pollUntil(
388
+ () => latches.starts >= 1,
389
+ now() + START_DEADLINE_MS,
390
+ (okStart) => {
391
+ if (!okStart) {
392
+ const why =
393
+ `flight A's start never fired within the ${START_DEADLINE_MS}ms deadline ` +
394
+ `(${countsLine()}) — the pre-W1 class (the prop leaking to the host, never ` +
395
+ 'composed at the commit edge)'
396
+ finalize(ROW_FLIGHT, false, why)
397
+ failRetargetUnwitnessable(why)
398
+ setStage('flight row finalized (FAIL)')
399
+ finish()
400
+ return
401
+ }
402
+ pollUntil(
403
+ () => latches.completes >= 1,
404
+ now() + SETTLE_DEADLINE_MS,
405
+ (okSettle) => {
406
+ if (!okSettle) {
407
+ const why =
408
+ `flight A never completed within the ${SETTLE_DEADLINE_MS}ms deadline ` +
409
+ `(${countsLine()}) — the flight never settled`
410
+ finalize(ROW_FLIGHT, false, why)
411
+ failRetargetUnwitnessable(why)
412
+ setStage('flight row finalized (FAIL)')
413
+ finish()
414
+ return
415
+ }
416
+ const startAt = latches.firstStartAt
417
+ const completeAt = latches.lastCompleteAt
418
+ const startLatency = startAt - pressAAt
419
+ const flightMs = completeAt - startAt
420
+ let bad: string | null = null
421
+ if (latches.starts !== 1 || latches.completes !== 1) {
422
+ bad =
423
+ `one flight latched ${countsLine()}, want starts 1 / completes 1 — a ` +
424
+ 'per-frame or re-fire-at-settle start miswire inflates starts'
425
+ } else if (startLatency < 0) {
426
+ bad =
427
+ `the start stamp (${startAt.toFixed(0)}ms) PRECEDES its press stamp ` +
428
+ `(${pressAAt.toFixed(0)}ms) — impossible on the JS thread's FIFO`
429
+ } else if (startLatency >= START_LATENCY_BOUND_MS) {
430
+ bad =
431
+ `start fired ${startLatency.toFixed(0)}ms after the press (bound ` +
432
+ `${START_LATENCY_BOUND_MS}ms) — start must fire at flight CREATION (the ` +
433
+ 'commit edge), not at the first frame or the settle (~3000ms late)'
434
+ } else if (completeAt <= startAt) {
435
+ bad =
436
+ `complete (${completeAt.toFixed(0)}ms) did not strictly follow start ` +
437
+ `(${startAt.toFixed(0)}ms) — start must precede its own flight's complete`
438
+ } else if (flightMs <= FLIGHT_FLOOR_MS) {
439
+ bad =
440
+ `the flight ran ${flightMs.toFixed(0)}ms (floor ${FLIGHT_FLOOR_MS}ms) — the ` +
441
+ '3s circIn tween did not run its course (a settle-deferred start reads ' +
442
+ 'complete − start ≈ 0)'
443
+ }
444
+ finalize(
445
+ ROW_FLIGHT,
446
+ bad === null,
447
+ bad ??
448
+ `flight A: exactly one start (+${startAt.toFixed(0)}ms, ` +
449
+ `${startLatency.toFixed(0)}ms after the press — flight creation, bound ` +
450
+ `${START_LATENCY_BOUND_MS}ms) STRICTLY BEFORE its one complete (` +
451
+ `+${completeAt.toFixed(0)}ms), a ${flightMs.toFixed(0)}ms flight (floor ` +
452
+ `${FLIGHT_FLOOR_MS}ms — the 3s circIn tween ran its course)`,
453
+ )
454
+ setStage('flight row finalized')
455
+ runRetargetPhase()
456
+ },
457
+ )
458
+ },
459
+ )
460
+ })
461
+
462
+ return () => {
463
+ for (const timer of timers) clearTimeout(timer)
464
+ for (const raf of rafsRef.current) cancelAnimationFrame(raf)
465
+ console.error = originalError
466
+ }
467
+ // All captured values are refs or stable callbacks; the timeline runs once per mount.
468
+ // eslint-disable-next-line react-hooks/exhaustive-deps
469
+ }, [])
470
+
471
+ const done = ROWS.every((id) => rows[id] !== undefined)
472
+ const failing = ROWS.filter((id) => rows[id] !== undefined && !rows[id]!.pass)
473
+ const verdict = !done ? 'RUNNING' : failing.length === 0 ? 'PASS' : 'FAIL'
474
+ const passed = Object.keys(rows).length - failing.length
475
+
476
+ return (
477
+ <ScrollView style={styles.scroll} contentContainerStyle={styles.container}>
478
+ <Text style={styles.title}>U7e layout-animation-start checks</Text>
479
+ <Text style={styles.subtitle}>
480
+ the catalog nested-position tree witnesses per-flight start/complete ({severity} severity)
481
+ </Text>
482
+ <View
483
+ style={[
484
+ styles.banner,
485
+ verdict === 'PASS' ? styles.pass : verdict === 'FAIL' ? styles.fail : styles.running,
486
+ ]}
487
+ >
488
+ <Text style={styles.bannerText}>{verdict}</Text>
489
+ </View>
490
+ <Text style={styles.tally}>
491
+ {done
492
+ ? `${passed} PASS / ${failing.length} FAIL of ${ROWS.length} rows`
493
+ : `${Object.keys(rows).length}/${ROWS.length} rows complete — RUNNING`}
494
+ </Text>
495
+ <Text style={styles.tallyDetail}>
496
+ {done
497
+ ? failing.length === 0
498
+ ? 'no failing rows'
499
+ : `FAILING: ${failing.join(' | ')}`
500
+ : `stage: ${stage}`}
501
+ </Text>
502
+ <Text style={styles.status}>
503
+ u7e stage: {stage} · presses={live.presses} starts={live.starts} completes=
504
+ {live.completes}
505
+ </Text>
506
+ <Text style={styles.status}>
507
+ press {stamp(live.firstPressAt)}/{stamp(live.lastPressAt)} · start{' '}
508
+ {stamp(live.firstStartAt)}/{stamp(live.lastStartAt)} · complete {stamp(live.lastCompleteAt)}
509
+ </Text>
510
+ <Text style={styles.status} accessibilityLabel="u7e-layout-start-stamps">
511
+ starts@[{live.startAts.map((at) => `+${at.toFixed(0)}`).join(',')}]
512
+ </Text>
513
+
514
+ {/* The on-screen control (the catalog's onClick-on-parent, made explicit): a mid-flight
515
+ press IS the retarget trigger. The auto timeline drives the same press for the
516
+ hands-free proof. */}
517
+ <Pressable style={styles.button} accessibilityLabel="u7e-layout-start-toggle" onPress={press}>
518
+ <Text style={styles.buttonText}>TOGGLE LAYOUT (a mid-flight press retargets)</Text>
519
+ </Pressable>
520
+
521
+ {/* The catalog's three-deep `layout` tree, geometry halved (the W2 card's adaptation):
522
+ parent 200↔100 at left 200↔0, child 75, grandchild 30% tall at 50%↔100% wide — the
523
+ grandchildren animate on parent size-delta propagation. Callbacks on the PARENT,
524
+ verbatim. The parent is white-on-dark (the catalog reads white-on-white; the screen
525
+ is dark), the blue child and red/yellow grandchild carry the visual. */}
526
+ <View style={styles.stage} accessibilityLabel="u7e-layout-start-stage">
527
+ <MotionView
528
+ layout
529
+ transition={TRANSITION}
530
+ onLayoutAnimationStart={onStart}
531
+ onLayoutAnimationComplete={onComplete}
532
+ style={[styles.parent, isOpen ? styles.parentOpen : styles.parentClosed]}
533
+ accessibilityLabel="u7e-layout-start-parent"
534
+ >
535
+ <MotionView
536
+ layout
537
+ transition={TRANSITION}
538
+ style={[styles.child, isOpen ? styles.childOpen : styles.childClosed]}
539
+ accessibilityLabel="u7e-layout-start-child"
540
+ >
541
+ <MotionView
542
+ layout
543
+ transition={TRANSITION}
544
+ style={[styles.grandchild, isOpen ? styles.grandchildOpen : styles.grandchildClosed]}
545
+ accessibilityLabel="u7e-layout-start-grandchild"
546
+ />
547
+ </MotionView>
548
+ </MotionView>
549
+ </View>
550
+
551
+ {ROWS.map((id) => {
552
+ const row = rows[id]
553
+ return (
554
+ <View key={id} style={styles.row}>
555
+ <Text
556
+ style={[
557
+ styles.verdict,
558
+ row === undefined ? styles.pending : row.pass ? styles.ok : styles.bad,
559
+ ]}
560
+ >
561
+ {row === undefined ? '…' : row.pass ? 'PASS' : 'FAIL'}
562
+ </Text>
563
+ <View style={styles.rowBody}>
564
+ <Text style={styles.rowId}>{id}</Text>
565
+ <Text style={styles.rowDetail} numberOfLines={4}>
566
+ {row === undefined ? 'pending' : row.detail}
567
+ </Text>
568
+ </View>
569
+ </View>
570
+ )
571
+ })}
572
+ </ScrollView>
573
+ )
574
+ }
575
+
576
+ const styles = StyleSheet.create({
577
+ scroll: { flex: 1, backgroundColor: '#0b0b12' },
578
+ container: { paddingVertical: 72, paddingHorizontal: 8 },
579
+ title: { color: '#fff', fontSize: 22, fontWeight: '700', textAlign: 'center' },
580
+ subtitle: { color: '#8a8aa0', fontSize: 12, marginTop: 6, textAlign: 'center' },
581
+ banner: {
582
+ alignSelf: 'center',
583
+ paddingHorizontal: 36,
584
+ paddingVertical: 10,
585
+ borderRadius: 10,
586
+ marginTop: 18,
587
+ marginBottom: 10,
588
+ },
589
+ pass: { backgroundColor: '#1d7a3e' },
590
+ fail: { backgroundColor: '#a32633' },
591
+ running: { backgroundColor: '#7a641d' },
592
+ bannerText: { color: '#fff', fontSize: 22, fontWeight: '800', letterSpacing: 2 },
593
+ tally: { color: '#e6e6f0', fontSize: 14, fontWeight: '700', textAlign: 'center' },
594
+ tallyDetail: { color: '#f87171', fontSize: 11, marginTop: 3, textAlign: 'center' },
595
+ status: {
596
+ color: '#c9c9d8',
597
+ fontSize: 11,
598
+ marginTop: 8,
599
+ textAlign: 'center',
600
+ fontFamily: 'Menlo',
601
+ },
602
+ button: {
603
+ alignSelf: 'center',
604
+ marginTop: 14,
605
+ backgroundColor: '#1f2a44',
606
+ borderRadius: 8,
607
+ paddingHorizontal: 14,
608
+ paddingVertical: 10,
609
+ },
610
+ buttonText: { color: '#e6e6f0', fontSize: 12, fontWeight: '700', letterSpacing: 0.5 },
611
+ // The stage holds the open parent (200px at left 200 within the ~360px column) with room
612
+ // to spare; the parent is absolutely positioned inside it like the catalog's fixed stage.
613
+ stage: { height: 220, marginTop: 14 },
614
+ parent: {
615
+ position: 'absolute',
616
+ top: 0,
617
+ backgroundColor: '#e6e6f0',
618
+ display: 'flex',
619
+ },
620
+ parentOpen: {
621
+ width: 200,
622
+ height: 200,
623
+ left: 200,
624
+ alignItems: 'flex-end',
625
+ justifyContent: 'flex-end',
626
+ },
627
+ parentClosed: {
628
+ width: 100,
629
+ height: 100,
630
+ left: 0,
631
+ alignItems: 'flex-start',
632
+ justifyContent: 'flex-start',
633
+ },
634
+ child: { width: 75, height: 75, backgroundColor: '#3b82f6', display: 'flex' },
635
+ childOpen: { alignItems: 'center', justifyContent: 'center' },
636
+ childClosed: { alignItems: 'flex-start', justifyContent: 'flex-start' },
637
+ grandchild: { height: '30%' },
638
+ grandchildOpen: { width: '50%', backgroundColor: '#eab308' },
639
+ grandchildClosed: { width: '100%', backgroundColor: '#dc2626' },
640
+ row: { flexDirection: 'row', marginTop: 10 },
641
+ verdict: { width: 52, fontSize: 12, fontWeight: '800', paddingTop: 1 },
642
+ ok: { color: '#4ade80' },
643
+ bad: { color: '#f87171' },
644
+ pending: { color: '#8a8aa0' },
645
+ rowBody: { flex: 1 },
646
+ rowId: { color: '#e6e6f0', fontSize: 13, fontWeight: '600' },
647
+ rowDetail: { color: '#6f6f85', fontSize: 11, marginTop: 2 },
648
+ })