@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,598 @@
1
+ // The NATIVE component-boundary gate for the R7 variants surface (REQ-API-032 law f):
2
+ // outer shapes refuse AS A UNIT at the supplying render — `variants` a plain object,
3
+ // `inherit` a boolean, a label ARRAY string-only (a non-string member names its index) —
4
+ // and every dictionary entry validates eagerly through core's entry law (shape, member
5
+ // validity, the embedded-transition law, the dynamic-form refusal) PLUS the native depth
6
+ // the R6 gesture gate pins: member drivability (the numeric/color lanes drive numbers and
7
+ // color strings — a core-valid unit string is native-undrivable) and the property-lane
8
+ // option set (an option the lane never reads — e.g. a tree-lane orchestration key — must
9
+ // refuse loud, never ride as a silent no-op; G-INV-8; `velocity` left this class at T21,
10
+ // the lane seeds springs from it). Development throws; production reports once and refuses the
11
+ // offending UNIT — the whole prop for outer shapes, the single ENTRY within a valid
12
+ // dictionary — while the valid remainder survives (the severity law's granularity).
13
+
14
+ import {
15
+ describeValue,
16
+ hostCapabilities,
17
+ isDiscreteKeyword,
18
+ ORCHESTRATION_OPTION_KEYS,
19
+ parseValue,
20
+ TARGET_PROPERTY_KEYS,
21
+ InvalidTransitionError,
22
+ InvalidVariantError,
23
+ resolveInitialOverlay,
24
+ resolveVariantDefinition,
25
+ validateVariantEntrySnapshot,
26
+ variantsShapeRefusal,
27
+ type Target,
28
+ type VariantApplication,
29
+ type VariantEntry,
30
+ type VariantResolver,
31
+ type Transition,
32
+ type VariantsDictionary,
33
+ } from '@unrulysystems/native-motion-core'
34
+ import {
35
+ executableLengthValueRefusal,
36
+ resolveVariantDefinitionResult,
37
+ } from '@unrulysystems/native-motion-core/internal-driver'
38
+ import {
39
+ isNativeExecutableLengthValue,
40
+ NATIVE_PROPERTY_TRANSITION_OPTIONS,
41
+ nativeMountTargetValueKind,
42
+ } from './shippedSurface'
43
+ import {
44
+ captureBoundedArray,
45
+ capturedArrayDescription,
46
+ keyframeTransitionRefusal,
47
+ resolveTransitionForKey,
48
+ } from '@unrulysystems/native-motion-core/internal-driver'
49
+ import type { StyleValueGate } from './styleValueBinding'
50
+ import type { MotionComponentId } from './motionViewController'
51
+
52
+ const NATIVE_HOST = hostCapabilities('native', ['universal', 'native-extension'])
53
+ const TRANSITION_MAP_KEYS = new Set<string>(['default', 'layout', ...TARGET_PROPERTY_KEYS])
54
+
55
+ /** The label (variant) form of a declarative prop: a string, or an array of strings. */
56
+ export function isVariantLabelForm(value: unknown): value is string | readonly string[] {
57
+ return typeof value === 'string' || Array.isArray(value)
58
+ }
59
+
60
+ /** The pin's runtime-only direct function form plus its public label form. */
61
+ export function isRuntimeVariantDefinitionForm(
62
+ value: unknown,
63
+ ): value is string | readonly string[] | VariantResolver {
64
+ return typeof value === 'function' || isVariantLabelForm(value)
65
+ }
66
+
67
+ /**
68
+ * Law (g): the target keys a dictionary names — the mount-known data that joins the mounted
69
+ * key set regardless of which label (if any) is active at mount.
70
+ */
71
+ export function dictionaryTargetKeys(dictionary: VariantsDictionary | undefined): string[] {
72
+ if (dictionary === undefined) return []
73
+ return [
74
+ ...new Set(
75
+ Object.values(dictionary).flatMap((entry) =>
76
+ Object.entries(entry).flatMap(([key, value]) => {
77
+ if (key === 'transition') return []
78
+ // T23 B3b: the transitionEnd CARRIER is structural, never an animated key — but its
79
+ // SUB-keys are mount-registered animated keys (a value must be mounted to jump).
80
+ if (key === 'transitionEnd') {
81
+ return typeof value === 'object' && value !== null ? Object.keys(value) : []
82
+ }
83
+ return [key]
84
+ }),
85
+ ),
86
+ ),
87
+ ]
88
+ }
89
+
90
+ /**
91
+ * T23 B3b: the EXIT lane is not chartered for settle jumps — T23 B3 charters the
92
+ * animate-lane jump; an exit jump precedes removal. A carrier on an exit target refuses
93
+ * loud naming the deferred lane (development throws; production reports and DROPS the
94
+ * member — sibling exit keys survive). Non-object shapes pass through untouched: the exit
95
+ * shape gates downstream own those refusals.
96
+ */
97
+ export function gateExitTransitionEnd(target: Target, gate: StyleValueGate): Target {
98
+ if (typeof target !== 'object' || target === null || !Object.hasOwn(target, 'transitionEnd')) {
99
+ return target
100
+ }
101
+ const error = new Error(
102
+ `${gate.componentId}: "exit" carries transitionEnd — the presence exit jump is the exit-jump ` +
103
+ 'successor lane (T23 B3 charters the animate-lane settle jump; an exit jump precedes ' +
104
+ 'removal); the member is refused and sibling exit keys survive.',
105
+ )
106
+ if (gate.severity === 'development') throw error
107
+ gate.report(error)
108
+ const { transitionEnd: _dropped, ...rest } = target as Record<string, unknown>
109
+ return rest as Target
110
+ }
111
+
112
+ /**
113
+ * Law (f): the native depth ON TOP of core's entry law — every member must be drivable by
114
+ * the native lanes, and an embedded transition may carry only property-lane options.
115
+ * RETURN-shaped over the already-validated plain SNAPSHOT (M3 r3 major 5cb2381e272c): no
116
+ * foreign object is ever re-enumerated here, so no catch or class discriminant exists —
117
+ * the returned refusal is this boundary's by construction; the caller owns severity.
118
+ */
119
+ const ORCHESTRATION_KEY_SET: ReadonlySet<string> = new Set(ORCHESTRATION_OPTION_KEYS)
120
+
121
+ function entryNativeDepthRefusal(
122
+ label: string,
123
+ entry: VariantEntry,
124
+ componentId: MotionComponentId,
125
+ ): Error | null {
126
+ const { transition, ...target } = entry
127
+ if (transition !== undefined) {
128
+ const source = transition as Record<PropertyKey, unknown>
129
+ const hasMap = Reflect.ownKeys(transition).some(
130
+ (key) => typeof key === 'string' && TRANSITION_MAP_KEYS.has(key),
131
+ )
132
+ const refusalForSelected = (selected: Transition, path: string): Error | null => {
133
+ for (const option of Reflect.ownKeys(selected)) {
134
+ if (typeof option !== 'string' || TRANSITION_MAP_KEYS.has(option)) continue
135
+ // T21 (REQ-API-053): root-level orchestration options are TREE-lane vocabulary — the
136
+ // variant episode consumes them and MotionView strips them before any property-lane
137
+ // command (L1), so they are consumed, not inert. Core's map-entry law upstream still
138
+ // refuses them INSIDE per-value entries; this depth check governs the property lane.
139
+ if (ORCHESTRATION_KEY_SET.has(option)) continue
140
+ const optionValue = (selected as Record<string, unknown>)[option]
141
+ if (optionValue === undefined) continue // explicitly-undefined reads as absent (R6 law)
142
+ if (!NATIVE_PROPERTY_TRANSITION_OPTIONS.has(option)) {
143
+ return new InvalidTransitionError(
144
+ componentId,
145
+ option,
146
+ optionValue,
147
+ `${path}.${option} is not executable by the native property lane (it drives: ` +
148
+ `${NATIVE_PROPERTY_TRANSITION_OPTIONS.options.join(', ')}) — a silently inert ` +
149
+ 'option is refused (G-INV-8, shippedSurface.ts)',
150
+ )
151
+ }
152
+ }
153
+ return null
154
+ }
155
+ if (!hasMap) {
156
+ const refusal = refusalForSelected(transition, `variants.${label}.transition`)
157
+ if (refusal !== null) return refusal
158
+ } else {
159
+ const selectedKeys = Object.keys(target)
160
+ const selectors = selectedKeys.length === 0 ? ['__root__'] : selectedKeys
161
+ const seen = new Set<string>()
162
+ for (const key of selectors) {
163
+ const mapKey =
164
+ TRANSITION_MAP_KEYS.has(key) &&
165
+ Object.hasOwn(source, key) &&
166
+ source[key] !== undefined &&
167
+ source[key] !== null
168
+ ? key
169
+ : Object.hasOwn(source, 'default') && source['default'] !== undefined
170
+ ? 'default'
171
+ : 'root'
172
+ if (seen.has(mapKey)) continue
173
+ seen.add(mapKey)
174
+ const selected = resolveTransitionForKey(transition, key)
175
+ const refusal = refusalForSelected(selected, `variants.${label}.transition.${mapKey}`)
176
+ if (refusal !== null) return refusal
177
+ }
178
+ }
179
+ }
180
+ for (const [memberKey, memberValue] of Object.entries(target)) {
181
+ // T23 B3b: the transitionEnd carrier passes whole — core deep-validated each sub-value
182
+ // (family + path-named diagnostics, B3a), and the controller's jump lane re-gates native
183
+ // executability at jump time (applyTransitionEnd). It is structural, not a drivable member.
184
+ if (memberKey === 'transitionEnd') continue
185
+ const valueKind = nativeMountTargetValueKind(memberKey)
186
+ // Both lanes drive a scalar OR a keyframe ARRAY (REQ-API-033 on variants, majors 31 + 35): core
187
+ // validation already proved a well-formed same-value-type array, and the array-aware controller
188
+ // drives it — the color lane through the progress projection (projectColorSequence), the numeric
189
+ // lane directly. Positional length strings share the controller's measure/defer lane — checked
190
+ // ELEMENT-WISE through the ONE key-aware executable matrix (review 91d41675d511): a scalar or
191
+ // array element outside it refuses eagerly here, never latently at selection.
192
+ const lengthRefusal =
193
+ valueKind === 'color' || valueKind === 'discrete'
194
+ ? null
195
+ : executableLengthValueRefusal(memberKey, memberValue)
196
+ // The discrete lane (T23 B2b) drives a SINGLE keyword of the discrete FAMILY — the
197
+ // controller's own triple guard (isDiscreteKeyword pre-guards the parse; the family verdict
198
+ // refuses 'auto'/named colors); keyframe arrays are not chartered for discrete keys.
199
+ const drivable =
200
+ valueKind === 'color'
201
+ ? typeof memberValue === 'string' || Array.isArray(memberValue)
202
+ : valueKind === 'discrete'
203
+ ? typeof memberValue === 'string' &&
204
+ isDiscreteKeyword(memberValue) &&
205
+ parseValue(memberValue).kind === 'discrete'
206
+ : isNativeExecutableLengthValue(memberKey, memberValue)
207
+ if (!drivable) {
208
+ return new InvalidVariantError(
209
+ label,
210
+ entry,
211
+ `${componentId}: variants.${label}.${memberKey} received ${describeValue(memberValue)} — ${
212
+ lengthRefusal ??
213
+ `the native ${valueKind} lane drives ${
214
+ valueKind === 'color'
215
+ ? 'color strings or a color keyframe array'
216
+ : valueKind === 'discrete'
217
+ ? 'a single CSS keyword string (keyframe arrays are not chartered for discrete keys)'
218
+ : 'numbers, numeric keyframe arrays, or measure-resolved positional lengths'
219
+ }; the supplied value is not executable by the native property lane (REQ-API-032 law f)`
220
+ }`,
221
+ )
222
+ }
223
+ // R8-F1 at the SUPPLYING boundary (review major 42): a keyframe array member under an EMBEDDED
224
+ // spring interpolates exactly two keyframes — a longer array refuses the ENTRY EAGERLY here (an
225
+ // invalid entry can never sit latent until selection, the R7 law) instead of an uncaught controller
226
+ // throw. An array with NO embedded spring rides the controller's own severity-aware compat filter.
227
+ // R8 M2-B (REQ-API-033 law d): validate this variant's EMBEDDED keyframe timing eagerly (the
228
+ // R7 no-latent law). Exact-count `times` override even offsets; other shape-valid counts fall
229
+ // back as a whole. Per-segment easing lists allow missing/extra entries; scalar consumers still
230
+ // refuse keyframe-only timing as inert.
231
+ const timingRefusal = keyframeTransitionRefusal(
232
+ `variants.${label}`,
233
+ memberKey,
234
+ memberValue,
235
+ transition === undefined ? undefined : resolveTransitionForKey(transition, memberKey),
236
+ )
237
+ if (timingRefusal !== null) return new InvalidVariantError(label, entry, timingRefusal.message)
238
+ }
239
+ return null
240
+ }
241
+
242
+ // A dictionary entry with no embedded transition inherits the element transition when a label selects
243
+ // it. That fallback is an effective per-property consumer, not an optional later detail: timing or a
244
+ // spring keyframe cardinality it cannot execute must be refused before the resolver/controller sees it.
245
+ // Core and the existing native-depth gate retain their entry-unit law for embedded transitions. The
246
+ // inherited case is per-property under production severity, matching every other fallback supplier:
247
+ // an incompatible key is absent while a valid sibling remains executable.
248
+ export function gateVariantApplicationFallback(
249
+ application: VariantApplication,
250
+ elementTransition: Transition | undefined,
251
+ gate: StyleValueGate,
252
+ componentId: MotionComponentId = '<Motion.View>',
253
+ ): VariantApplication {
254
+ if (application.transition !== undefined || elementTransition === undefined) return application
255
+ const accepted = Object.create(null) as Record<string, unknown>
256
+ for (const [key, value] of Object.entries(application.target)) {
257
+ const timingRefusal = keyframeTransitionRefusal(
258
+ componentId,
259
+ key,
260
+ value,
261
+ resolveTransitionForKey(elementTransition, key),
262
+ )
263
+ if (timingRefusal !== null) {
264
+ if (gate.severity === 'development') throw timingRefusal
265
+ gate.report(timingRefusal)
266
+ continue
267
+ }
268
+ accepted[key] = value
269
+ }
270
+ return { target: accepted as VariantEntry, transition: undefined }
271
+ }
272
+
273
+ /** Only concrete timing consumers (animate/exit) inherit the element transition. */
274
+ export function gateVariantApplicationFallbacks(
275
+ applications: readonly VariantApplication[],
276
+ elementTransition: Transition | undefined,
277
+ gate: StyleValueGate,
278
+ componentId: MotionComponentId = '<Motion.View>',
279
+ ): readonly VariantApplication[] {
280
+ return gateVariantApplicationFallbacksWithEvidence(
281
+ applications,
282
+ elementTransition,
283
+ gate,
284
+ componentId,
285
+ ).applications
286
+ }
287
+
288
+ /**
289
+ * The accepted projection alone cannot say whether a fallback property was attempted: production may
290
+ * have removed every such key. Keep that evidence beside the same resolved applications so the
291
+ * element-transition owner decision distinguishes a genuinely inert transition from one already refused.
292
+ */
293
+ export function gateVariantApplicationFallbacksWithEvidence(
294
+ applications: readonly VariantApplication[],
295
+ elementTransition: Transition | undefined,
296
+ gate: StyleValueGate,
297
+ componentId: MotionComponentId = '<Motion.View>',
298
+ ): {
299
+ readonly applications: readonly VariantApplication[]
300
+ readonly fallbackAttempted: boolean
301
+ } {
302
+ const fallbackAttempted =
303
+ elementTransition !== undefined &&
304
+ applications.some(
305
+ (application) =>
306
+ application.transition === undefined && Object.keys(application.target).length > 0,
307
+ )
308
+ return {
309
+ applications: applications.map((application) =>
310
+ gateVariantApplicationFallback(application, elementTransition, gate, componentId),
311
+ ),
312
+ fallbackAttempted,
313
+ }
314
+ }
315
+
316
+ /**
317
+ * The dictionary gate: outer shape as a unit, then EVERY entry eagerly (an invalid entry
318
+ * can never sit latent until selection). Returns the SANITIZED dictionary — in production
319
+ * an offending entry is DROPPED (reported once) and the valid remainder survives; a
320
+ * malformed outer shape refuses the whole prop to `undefined`.
321
+ */
322
+ export function gateVariantsDictionary(
323
+ variants: unknown,
324
+ gate: StyleValueGate,
325
+ componentId: MotionComponentId = '<Motion.View>',
326
+ ): VariantsDictionary | undefined {
327
+ if (variants === undefined) return undefined
328
+ // RETURN-shaped refusals, no catch (M3 r3 majors 1b53e9337aec/13af48a64530/5cb2381e272c):
329
+ // the shape probe and the entry READ phase run foreign traps UNGUARDED — their faults
330
+ // (including authentic same-class sentinels) propagate untouched by construction. A
331
+ // returned refusal is boundary-made; severity applies only to those.
332
+ const shapeRefusal = variantsShapeRefusal(variants)
333
+ if (shapeRefusal !== null) {
334
+ const ownedRefusal = new InvalidVariantError(
335
+ '(variants)',
336
+ variants,
337
+ `${componentId}: ${shapeRefusal.message}`,
338
+ shapeRefusal,
339
+ )
340
+ if (gate.severity === 'development') throw ownedRefusal
341
+ gate.report(ownedRefusal)
342
+ return undefined
343
+ }
344
+ // Null-prototype accumulator (M2 r1 major 11): a valid own '__proto__' label must land as
345
+ // an OWN property — plain-object assignment would invoke the legacy prototype setter and
346
+ // silently turn the label into a local miss (and pollute the accumulator's prototype).
347
+ const accepted: Record<string, VariantEntry | VariantResolver> = Object.create(null) as Record<
348
+ string,
349
+ VariantEntry | VariantResolver
350
+ >
351
+ for (const [label, entry] of Object.entries(variants as Record<string, unknown>)) {
352
+ const { refusal, entry: snapshot } = validateVariantEntrySnapshot(label, entry, NATIVE_HOST, {
353
+ componentId,
354
+ })
355
+ // T24 B2: a RESOLVER entry mounts by identity, uninvoked — its keys do not exist until
356
+ // resolution, so BOTH the core entry law and the native depth law apply to its RESULT
357
+ // inside resolveVariantApplicationsGated, never here.
358
+ if (typeof snapshot === 'function') {
359
+ accepted[label] = snapshot
360
+ continue
361
+ }
362
+ // Production: the ENTRY is the refusal unit within a valid dictionary (law f).
363
+ const nativeRefusal = refusal ?? entryNativeDepthRefusal(label, snapshot!, componentId)
364
+ if (nativeRefusal !== null) {
365
+ if (gate.severity === 'development') throw nativeRefusal
366
+ gate.report(nativeRefusal)
367
+ continue
368
+ }
369
+ // The SNAPSHOT is what mounts (one read, one truth): a stateful proxy cannot show the
370
+ // validators one value and the engine another.
371
+ accepted[label] = snapshot!
372
+ }
373
+ return accepted
374
+ }
375
+
376
+ // The fallback state for a value-less/pre-mount native site. Mounted hosts supply their
377
+ // controller's fresh live-state reader at each resolver arm.
378
+ const EMPTY_CURRENT: Readonly<Record<string, unknown>> = Object.freeze({})
379
+ const EMPTY_VELOCITY: Readonly<Record<string, number>> = Object.freeze({})
380
+ const EMPTY_VALUE_STATE = Object.freeze({ current: EMPTY_CURRENT, velocity: EMPTY_VELOCITY })
381
+
382
+ /**
383
+ * T24 B2: THE native resolution seam — every native site resolves labels through this
384
+ * gate, never bare `resolveVariantDefinition`. Static labels are the pinned static law
385
+ * unchanged. A RESOLVER label resolves through the core seam (unguarded invocation — user
386
+ * faults propagate untouched; typed refusals for the two-step bound and the core entry
387
+ * law) and its RESULT then rides the SAME native depth law a static entry rides at the
388
+ * boundary. Severity granularity is the LABEL: development throws; production reports once
389
+ * and skips the offending label while the remainder survives.
390
+ */
391
+ export function resolveVariantApplicationsGated(
392
+ definition: string | readonly string[] | VariantResolver,
393
+ dictionary: VariantsDictionary | undefined,
394
+ gate: StyleValueGate,
395
+ componentId: MotionComponentId,
396
+ custom?: unknown,
397
+ readValueState: () => {
398
+ readonly current: Readonly<Record<string, unknown>>
399
+ readonly velocity: Readonly<Record<string, number>>
400
+ } = () => EMPTY_VALUE_STATE,
401
+ ): readonly VariantApplication[] {
402
+ if (typeof definition === 'function') {
403
+ const resolution = resolveVariantDefinitionResult(definition, dictionary, {
404
+ custom,
405
+ readValueState,
406
+ host: NATIVE_HOST,
407
+ componentId,
408
+ })
409
+ if (resolution.refusal !== null) {
410
+ if (gate.severity === 'development') throw resolution.refusal
411
+ gate.report(resolution.refusal)
412
+ return []
413
+ }
414
+ return resolution.applications.filter((application) => {
415
+ const producedEntry = {
416
+ ...application.target,
417
+ ...(application.transition === undefined ? {} : { transition: application.transition }),
418
+ } as VariantEntry
419
+ const refusal = entryNativeDepthRefusal('(definition)', producedEntry, componentId)
420
+ if (refusal === null) return true
421
+ if (gate.severity === 'development') throw refusal
422
+ gate.report(refusal)
423
+ return false
424
+ })
425
+ }
426
+ const labels = typeof definition === 'string' ? [definition] : definition
427
+ const applications: VariantApplication[] = []
428
+ for (const label of labels) {
429
+ if (dictionary === undefined || !Object.hasOwn(dictionary, label)) continue
430
+ if (typeof dictionary[label] !== 'function') {
431
+ // Static entries cannot refuse here (validated at the boundary) — the bare core
432
+ // resolution is the single static law.
433
+ for (const application of resolveVariantDefinition(label, dictionary)) {
434
+ applications.push(application)
435
+ }
436
+ continue
437
+ }
438
+ const resolution = resolveVariantDefinitionResult(label, dictionary, {
439
+ custom,
440
+ readValueState,
441
+ host: NATIVE_HOST,
442
+ componentId,
443
+ })
444
+ if (resolution.refusal !== null) {
445
+ if (gate.severity === 'development') throw resolution.refusal
446
+ gate.report(resolution.refusal)
447
+ continue
448
+ }
449
+ for (const application of resolution.applications) {
450
+ // The produced entry rides the SAME native depth law a static entry rides.
451
+ const producedEntry = {
452
+ ...application.target,
453
+ ...(application.transition === undefined ? {} : { transition: application.transition }),
454
+ } as VariantEntry
455
+ const depthRefusal = entryNativeDepthRefusal(label, producedEntry, componentId)
456
+ if (depthRefusal !== null) {
457
+ if (gate.severity === 'development') throw depthRefusal
458
+ gate.report(depthRefusal)
459
+ continue
460
+ }
461
+ applications.push(application)
462
+ }
463
+ }
464
+ return applications
465
+ }
466
+
467
+ /**
468
+ * T24 B1: law (c) through the gated seam — the synchronous initial overlay with RESOLVER
469
+ * labels admitted. Composes core `resolveInitialOverlay` PER LABEL (the overlay law is an
470
+ * ordered per-application assign, so per-label calls compose identically) so severity keeps
471
+ * the LABEL granularity: development throws, production reports and skips the offending
472
+ * label while the remainder overlays. Static-only definitions behave byte-identically to
473
+ * the bare core overlay.
474
+ */
475
+ export function resolveInitialOverlayGated(
476
+ definition: string | readonly string[] | VariantResolver,
477
+ dictionary: VariantsDictionary | undefined,
478
+ gate: StyleValueGate,
479
+ componentId: MotionComponentId,
480
+ custom?: unknown,
481
+ readValueState?: () => {
482
+ readonly current: Readonly<Record<string, unknown>>
483
+ readonly velocity: Readonly<Record<string, number>>
484
+ },
485
+ ): Target {
486
+ if (typeof definition === 'function') {
487
+ const overlay: Record<string, unknown> = {}
488
+ for (const application of resolveVariantApplicationsGated(
489
+ definition,
490
+ dictionary,
491
+ gate,
492
+ componentId,
493
+ custom,
494
+ readValueState,
495
+ )) {
496
+ const { transitionEnd: carrier, ...plain } = application.target as Record<string, unknown>
497
+ Object.assign(overlay, plain)
498
+ if (typeof carrier === 'object' && carrier !== null && !Array.isArray(carrier)) {
499
+ Object.assign(overlay, carrier)
500
+ }
501
+ }
502
+ return overlay as Target
503
+ }
504
+ const labels = typeof definition === 'string' ? [definition] : definition
505
+ const overlay: Record<string, unknown> = {}
506
+ for (const label of labels) {
507
+ if (dictionary === undefined || !Object.hasOwn(dictionary, label)) continue
508
+ if (typeof dictionary[label] !== 'function') {
509
+ // Static labels: the bare core overlay is the single law (boundary-validated entries).
510
+ Object.assign(overlay, resolveInitialOverlay(label, dictionary))
511
+ continue
512
+ }
513
+ // Resolver labels resolve through the ONE depth-checked seam, then law (c) applies on
514
+ // top: targets assign in order, transitions discarded, the transitionEnd carrier folds
515
+ // instantly over its own target keys (T23 B3c) and never rides the overlay itself.
516
+ for (const application of resolveVariantApplicationsGated(
517
+ label,
518
+ dictionary,
519
+ gate,
520
+ componentId,
521
+ custom,
522
+ readValueState,
523
+ )) {
524
+ const { transitionEnd: carrier, ...plain } = application.target as Record<string, unknown>
525
+ Object.assign(overlay, plain)
526
+ if (typeof carrier === 'object' && carrier !== null && !Array.isArray(carrier)) {
527
+ Object.assign(overlay, carrier)
528
+ }
529
+ }
530
+ }
531
+ return overlay as Target
532
+ }
533
+
534
+ /** Law (f): `inherit` is a boolean; anything else refuses naming the contract. */
535
+ export function gateInheritProp(
536
+ inherit: unknown,
537
+ gate: StyleValueGate,
538
+ componentId: MotionComponentId = '<Motion.View>',
539
+ ): boolean | undefined {
540
+ if (inherit === undefined || typeof inherit === 'boolean') return inherit
541
+ // describeValue, never JSON.stringify (M3 r2 major 0a96290934e2): the same formatter law
542
+ // the label gate learned — a BigInt crashes the serializer with zero reports.
543
+ const error = new Error(
544
+ `${componentId}: "inherit" must be a boolean — received ${describeValue(inherit)} (REQ-API-032 law f; ` +
545
+ 'the pinned visual-state switch takes true/false only)',
546
+ )
547
+ if (gate.severity === 'development') throw error
548
+ gate.report(error)
549
+ return undefined
550
+ }
551
+
552
+ /**
553
+ * Law (f): normalize a label definition to the ordered label list. A string is one label;
554
+ * an ARRAY must be string-only — a non-string member refuses naming the prop and its index,
555
+ * and production refuses the whole PROP (the outer-shape unit).
556
+ */
557
+ export function gateLabelDefinition(
558
+ prop: string,
559
+ definition: string | readonly unknown[],
560
+ gate: StyleValueGate,
561
+ componentId: MotionComponentId = '<Motion.View>',
562
+ ): readonly string[] | undefined {
563
+ if (typeof definition === 'string') return [definition]
564
+ const capturedDefinition = captureBoundedArray(definition)
565
+ if (capturedDefinition.kind !== 'captured') {
566
+ const error = new Error(
567
+ `${componentId}: "${prop}" label array must have a safe length — received ${capturedArrayDescription(capturedDefinition)} ` +
568
+ '(REQ-API-032 law f)',
569
+ )
570
+ if (gate.severity === 'development') throw error
571
+ gate.report(error)
572
+ return undefined
573
+ }
574
+ const length = capturedDefinition.values.length
575
+ const captured: string[] = []
576
+ captured.length = length
577
+ for (let index = 0; index < length; index++) {
578
+ const member = Object.hasOwn(capturedDefinition.values, index)
579
+ ? capturedDefinition.values[index]
580
+ : undefined
581
+ if (typeof member === 'string') {
582
+ captured[index] = member
583
+ continue
584
+ }
585
+ // describeValue, never JSON.stringify (M3 r1 major cdc42f01c118): a BigInt member
586
+ // would crash the FORMATTER itself — a raw serialize TypeError replacing the typed
587
+ // law-(f) refusal in development and escaping unreported in production.
588
+ const error = new Error(
589
+ `${componentId}: "${prop}" label array member at index ${index} must be a string — received ` +
590
+ `${describeValue(member)} (REQ-API-032 law f; the label array is ` +
591
+ 'string-only)',
592
+ )
593
+ if (gate.severity === 'development') throw error
594
+ gate.report(error)
595
+ return undefined
596
+ }
597
+ return Object.freeze(captured)
598
+ }