Skip to content

feat: add React Native UIKit runtime primitives - #46

Open
DjDeveloperr wants to merge 250 commits into
refactorfrom
codex/rn-module-fabric-turbomodule-worklets
Open

feat: add React Native UIKit runtime primitives#46
DjDeveloperr wants to merge 250 commits into
refactorfrom
codex/rn-module-fabric-turbomodule-worklets

Conversation

@DjDeveloperr

@DjDeveloperr DjDeveloperr commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

React Native UIKit runtime primitives

Adds the runtime primitives that let React Native drive real UIKit from NativeScript, plus a thin @nativescript/react-native-screens adapter built on stock @react-navigation/native-stack.

What's in it

  • RN runtime primitives — Fabric / TurboModule / Worklets interop for hosting native UIKit views and view controllers.
  • @nativescript/react-native-screens — a single-engine adapter that drives a real UINavigationController, with react-native-screens parity: native stack, headers, modals & sheets, transitions, and gestures — behaving like upstream RNS.

Tests

  • Jest unit: 238/238
  • React Navigation native-stack: 19/19
  • On-simulator integration (itest): core + slide suites green

Stacked on top of refactor (#43).

- Updated package.json files for iOS variants to set IOS_VARIANT environment variable during builds.
- Modified NativeScriptNativeApi.podspec to include new native-api structure and adjusted header search paths.
- Changed references from native-api-jsi to native-api in package.json and build scripts.
- Enhanced build_metadata_generator.sh to include source hash verification for metadata generator.
- Refined build_nativescript.sh to streamline FFI backend handling and ensure proper engine checks.
- Updated check_ffi_boundaries.sh to enforce restrictions on FFI layer usage and prevent stale references.
- Improved react_native_app_utils.sh to handle marker file content changes more effectively.
- Adjusted run-tests-macos.js to support additional FFI backends and ensure proper artifact management.
- Enhanced test scripts for React Native FFI compatibility to reflect changes in backend handling.
- Updated NativeApiJsiTests.js to reflect the transition from direct to engine-based API bridging.
Restore correct behavior in the V8 direct FFI backend after the per-engine
reorg. macOS V8 suite: 600+ assertion failures + ~30 failing specs + SIGSEGV
down to 3 failing specs.

- host-object interceptor: kNonMasking -> kNone (Pointer/Reference toString,
  native member interception)
- reconcileObjCMethodRuntimeType: stop overwriting named struct metadata
  types with anonymous ObjC encodings (fixes 588 VersionDiff assertions)
- installClassMembers: stop installing runtime ObjC members on prototypes
  (member enumeration, isKindOfClass); resolve runtime members lazily
- makeNativeObjectValue: drop consumed (object_==nil) wrappers from the
  round-trip cache (alloc placeholder singleton reuse / NSString init)
- RuntimeState::localContext: fall back to current context when empty
- unichar: single-char string<->ushort in slow arg path; exclude ushort
  from the fast-path integer kinds so it uses the unichar conversion
- instance get: invoke runtime ObjC properties as getters; defer JS-subclass
  members to the JS prototype
- selector-group dispatch: use immediate native superclass for derived
  receivers so native overrides are honored
- HostObject::set returns bool so the SET interceptor can defer to JS
  prototype accessors
Move the 8 duplicated bridge fragments into a single engine-neutral
shared/bridge set with a generic NativeApi token, included by each
engine entrypoint. Eliminates ~26k lines of duplication and propagates
the V8 backend fixes to JSC and QuickJS.
… property reads

- Register NSError-out selectors (trailing error:) at both arities so
  error-omitted calls resolve; reword arg-count error to match.
- For JS-extended instances, defer metadata property gets to the prototype
  chain so JS accessor overrides win over native property values.
Block dispose can run during an autorelease-pool drain outside any JS
engine scope; entering a NativeApiRuntimeScope before touching the
round-trip root object fixes a SIGSEGV in Runtime::global().
Restore the proven instance-method dispatch: resolve metadata methods to
a freshly created host function (with NSError-omitted arity support)
instead of injecting a prototype selector-group function through the
property interceptor, which was not reliably callable and mis-resolved
Swift class objects.
A callback can outlive the scope where its function argument was created
(async blocks, completion handlers). Round-trip the function through the
engine value copy so borrowed/scope-bound handles are promoted, fixing
'is not a function'/'number 0 is not a function' in block-retaining and
NSURLSession completion-handler tests.
+[Factory create] returning a different class type was tagged with the
factory's class wrapper, so constructor resolved to the wrong class.
Only remember the class wrapper when the object is an instance of it.
Update JSC/QuickJS HostObject::set to return bool (matching shared
bridge) and defer to the engine when unhandled; use
dispatchSuperclassForEngineDerivedReceiver so native-derived overrides
are honored. JSC macOS: 713 pass, 0 fail.
… return

Consolidate the Hermes backend onto ffi/shared/bridge, removing ~13k
duplicated lines. JSI's HostObject::set returns void, so the shared set
overrides use a NATIVESCRIPT_NATIVE_API_HOST_SET_VOID-gated return type.
Hermes builds and runs on macOS with no crashes.
QuickJS exotic property storage doesn't fall back to own properties when
the host set handler defers, and invokes prototype accessors with the
wrong receiver. Gate explicit prototype getter/setter resolution (with
the instance as receiver) and own-data expando storage behind
NATIVESCRIPT_NATIVE_API_HOST_EXPLICIT_OVERRIDE. quickjs macOS: 713 tests,
1 failure.
…call

Surface the actual thrown error (e.g. block-callback errors) instead of a
generic message, so callback exceptions propagate correctly. quickjs
macOS: 713 tests, 0 failures.
Enable NATIVESCRIPT_NATIVE_API_HOST_EXPLICIT_OVERRIDE for Hermes so JSI
host objects resolve JS-prototype getters/setters with the correct
receiver and store own data as expandos. hermes macOS: 13 failures -> 1.
… registry

The Runtime destructor can run at process teardown after file-scope
statics are destroyed; locking the destroyed promise-runloop mutex threw
std::system_error ('mutex lock failed'). Use leaked, never-destroyed
singletons. Fixes intermittent teardown crash (quickjs/hermes).
Block disposal can run on an arbitrary thread (e.g. NSOperationQueue);
forgetting the round-trip value touches the JS engine global, which is
unsafe off-thread for single-threaded engines (JSI). Marshal the cleanup
to the JS thread. Fixes intermittent hermes Promise cross-thread crash.
The Hermes turbomodule now includes the consolidated ffi/shared/bridge
fragments instead of the removed per-engine hermes copies.
Avoid reallocating the dispatch host function on every method access.
…t fn

Parse the metadata/ObjC signatures once per (method, arg count) and
reuse the prepared invocation, instead of re-parsing on every call.
…rimitive

The Value type previously used std::shared_ptr<ValueStorage> for every
value, causing a heap allocation + atomic ref count on every Value
creation. In the benchmark hot path (250k iterations), this meant
millions of unnecessary heap allocations for simple primitives like
booleans and doubles.

Now Value stores kind/bool/number/borrowedLocal inline (stack-based)
and only allocates a shared_ptr when holding a v8::Global handle or
when sharing storage with Object/Function/Array types.

This eliminates heap allocation for:
- Value() (undefined)
- Value(bool)
- Value(double/int)
- Value::null()
- Value::borrowed(runtime, local)

Tests: macOS v8 713/0
…this

When a JS override calls super.init() via prototype and returns `this`,
the host object's native pointer was nil because callObjectSelector
disowns the receiver after init. This caused hermes (and potentially
other engines without interceptor-based property access) to fail the
ConstructorOverrides: prototype test.

Fix: after disowning the receiver, re-adopt the init result object on
the original host object. This ensures that when the JS override returns
`this`, the host object still wraps a valid native object.

Tests: all engines 713/0 (hermes was 712/1, now fixed)
…ion per primitive

Same optimization as V8: Value stores kind/bool/number/borrowed inline
on the stack and only allocates shared_ptr for owned engine handles.

Tests: jsc 713/0, quickjs 713/0
The V8 HostObject interceptor was creating and caching host functions
for every method access, adding expando lookup + Value copy overhead
on every call. Methods are already installed as selector group functions
on the prototype chain. Removing the interceptor's method resolution
lets V8 fall through to the prototype for method access.

Tests: v8 710/0 (3 skipped due to unrelated build issue)
Move the expando lookup to the very first check in
NativeApiObjectHostObject::get(), before the 18 string comparisons
for special properties. This eliminates ~100-180ns of wasted string
comparisons on every method call (the hot path).

Also skip Symbol properties early in the V8 interceptor callback
to avoid unnecessary UTF8 conversion.

Benchmark: 1232ms total (was 1372ms) — 10% improvement.
Per-case: respondsToSelector 207ns (was 246ns), characterAtIndex 200ns (was 228ns).
Tests: v8 713/0
- Skip redundant sel_registerName + class_getInstanceMethod when the
  prepared invocation is already cached (first-call-only overhead).
- Use raw pointer for receiver host object lookup (avoids atomic
  ref count increment on every method call).
- Only acquire shared_ptr for init methods that need disown handling.
- Add v8HostObjectRaw<T> template for zero-overhead receiver access.

Tests: v8 713/0
Switch V8 HostObject interceptor from kNone to kNonMasking. With
kNonMasking, V8 checks own properties and prototype chain BEFORE
calling the interceptor. This means method calls and property getters
installed on the prototype (by installClassMembers) are found directly
by V8's inline caches without any C++ interceptor overhead.

Add toString to the host object template so it overrides
Object.prototype.toString (which would otherwise shadow it with
kNonMasking).

Benchmark: 732ms total (was 1250ms) — 42% improvement, now matching
legacy iOS V8 performance (728ms).

Known: 9 test failures related to function pointer resolution,
instanceof, and readonly property error messages. These are edge cases
that need the interceptor but aren't on the hot path.

Tests: 713 total, 9 failures (704 pass)
Add a separate V8 object template for NativeApiObjectHostObject that
uses kNonMasking interceptor flag. This allows V8 to check the
prototype chain before calling the interceptor for native object
instances, enabling faster property access for methods and getters
installed on the prototype.

Also skip superclass/class/constructor/debugDescription from prototype
property installation so the interceptor's special handling is used
(these properties need to return wrapped class constructors).

Install toString on the native object template to override
Object.prototype.toString with kNonMasking.

Tests: v8 713/0
DjDeveloperr and others added 28 commits July 25, 2026 15:09
…holds

Promotes the cold-launch first-navigate repro to an asserting scenario. It
issues EXACTLY ONE `navigate` from a cold-launched, nav-ready, settled Home
with NO within-launch retry (the footer/overlay retry carry is precisely what
HID this defect), samples the route for ~3s to catch a transient advance-then-
revert, and FAILS unless the route advanced AND held at `[Home,Footer]`.

`first-nav` carries `maxAttempts:1` (suites.js) so a relaunch can never mask a
per-launch revert. Kept out of the auto-run core/parity suites (invoke via
`--scenario first-nav --reps N`; each rep is one independent cold launch) so
those baselines stay 11/11 and 14/14. Pre-fix this failed ~50% of launches;
post-fix 22/22 clean across two builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The itest harness measured routes/pixels/frame-presence but never
main-thread responsiveness, so a multi-second launch hang and a hang on
every header "Ping" tap passed as green. Add an objective in-app
main-thread stall meter and make every scenario gate on it.

- main-thread-watchdog.ts: a CADisplayLink armed on the MAIN runloop via
  a NativeScript-minted ObjC target/selector (the proven display-link
  machinery). A display link is serviced by the main runloop, so a
  main-thread block shows up as the inter-tick gap on the next tick —
  that gap IS the hang. Records maxGapMs + a timestamped stall list;
  stalls classify into launch vs steady-state by wall time.
- runner.tsx: after every scenario, read the watchdog and FAIL if a
  steady-state interaction stalled >=250ms (Instruments' own hang bar) or
  the cold-launch window stalled >=1200ms (below the 1.838s serialization
  hang, above the sim's environmental first-mount noise). A scenario that
  hangs but "passes on routes/pixels" now FAILS.
- App.tsx: arm the watchdog at module eval (before first mount) so the
  launch window is measured.
- hang-trace.js: device/sim xctrace Time Profiler cross-check that
  exports the Instruments potential-hangs table and asserts no Hang>=250ms
  — objective, independent of the in-app watchdog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…read hang)

Instrumented before-timings (on-sim, per the new hang watchdog) proved the
launch/reconcile hang was NOT the JSON.stringify signatures or the
UINavigationBarAppearance build (all ~0ms — already memoized). It was the
hosted-view LAYOUT traversal:

  configureScreenController.total  954ms/launch (20 screens), 437ms/ping
    configureNavigationAppearance  319ms  (its internal layout pass)
    layoutHostedReactSubviews      310ms
    layoutNavigationStackViews     309ms

layoutNavigationStackViews lays out EVERY view controller (and each one's
whole hosted-subview subtree via a depth-8/12 native-proxy walk) and ran
twice per per-screen configure, per screen -> O(n^2) on launch and a full
re-walk on every header reconcile ("ping"). Both passes are idempotent for
a fixed (bounds, child count), so memoize them by a primitive signature
(controller native-hash keyed, NOT a JS expando — expandos never round-trip
on NativeScript proxies), busting on bounds change (rotation/tab) and on a
direct-/first-child-subview add/remove so late content mounts still lay out.

Controller hash is inlined in both functions: they are declared before
`controllerHash`, and a module worklet capturing a later-declared function
serializes a dead (undefined) closure.

After (same build, on-sim):
  header ping           smax 639ms -> 0ms
  launch configure      954ms -> ~93ms  (engine share of the mount)
Behaviour identical (all itest host route/pixel/frame checks still pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…memos (device .description hang)

Device Time Profiler (potential-hangs) traced the residual launch/reconcile
HANG to the NativeScript runtime's UIAppearance-proxy path: every property SET
on a UINavigationBar / UIView proxy calls
`appearanceProxyCustomizableClassFromDescription` -> `[obj description]`, which
RECURSIVELY serializes the view graph (subviews, NSArray/NSDictionary of items
and attributes) to a string. `configureNavigationAppearance` set ~9 bar scalars
UNCONDITIONALLY on every per-screen configure, and `configureScreenController`
set `controller.view` autoresizingMask/background every configure -> hundreds of
`[view description]` calls across a cold launch. Invisible on the simulator
(description is ~free there), a 0.8-1.8s Main Thread hang on device.

Fix: a combined value-signature over EVERYTHING the nav-bar block writes
(appearance + hidden + largeTitle + tint + direction) so an unchanged reconcile
writes NOTHING to the bar; and a (presentation, background) memo for the two
`controller.view` writes. Both keyed by the controller's native hash (not a JS
expando). Behaviour identical; the runtime description path is simply not tripped
on an unchanged reconcile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…itch hang guard)

Adds `tab-switch-storm` — UIKit<->React Nav switched N times — invokable via
`--scenario tab-switch-storm` (maxAttempts:1). A re-entrant/looping tab reconcile
shows as an unbounded main-thread stall, which the STEP-1 hang gate now FAILs; a
switch that never lands FAILs on AWAIT_TIMEOUT.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…parity)

Finish the main-thread-perf work end-to-end on the simulator.

Harness — env-var scenario launch (fixes a hard blocker on iOS 26 sims):
iOS 26 simulators show an "Open in <App>?" confirmation for custom-scheme
`simctl openurl` that SEVERS URL delivery (getInitialURL never fires, the
scenario never starts -> every scenario HOST_TIMEOUTs). The app now also
accepts the scenario from SIMCTL_CHILD_ITEST_SCENARIO, read off NSProcessInfo
at launch via the same proven getClass/runOnUI machinery as the watchdog;
session.js cold-launches with that env var. Still a cold launch, so the
watchdog's launch-window measurement is unaffected; openurl stays as the
fallback. Absent the env var, app behavior is 100% unchanged.

Gate calibration — cold-launch + first-reveal budget (part B: PARITY):
Measured on the SAME simulator, the port reveals the heavy React-Nav tree in
~1.66s vs upstream react-native-screens 4.25.2's ~2.26-2.98s (upstream never
faster) — mounting a heavy Fabric tree once is inherently this expensive, so
this is works-same, not a port regression. Per that finding:
- STEADY (repeated micro-interactions: header ping, push/pop, back-swipe)
  stays gated at 250ms — the sharp detector for the user's headline hang bug;
  measured 0-249ms, PASS.
- FIRST_MOUNT_BUDGET_MS = 3000 (upstream first-mount + margin) governs the
  cold-launch window and, via an opt-in per-scenario steadyStallBudgetMs, the
  one-time heavy first-reveals (modal sheet present, tab switch to the heavy
  stack) that legitimately mount a heavy subtree once. Applied to
  swipe-down-syncs, sheet-detents, tab-switch-storm.

itest --suite core is 11/11 green; unit 235/235 and native-stack 19/19
unregressed; the tab-switch-storm scenario completes bounded (the 1eda95f
re-entrancy fix ships in the bundle — verified by grep of __nsTabReconcileGuards
in the shipped Hermes main.jsbundle).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REyr3qLVtKK3SZ1pphdGis
…ace content-blank (RED)

The historical itest gated on the ROUTE STACK (+ retries) and tested actions in
isolation, so a BLANK screen with a correct route passed. This adds a
CONTENT-PRESENT primitive and the reconcile-race repro scenarios that were
structurally invisible before.

- content-present.ts: an in-app MAIN-THREAD native read (runOnUI) that, for the
  revealed top controller, asserts (1) the expected route's root + deep-leaf
  accessibility-label markers are attached to a window with a non-degenerate
  frame (w*h>0) and (2) the top controller's view is covered >50% by a live,
  non-hidden, user-interactive subview. Marker-absence catches exactly the blank
  a coverage-only check would false-pass.
- react-navigation.tsx: dedicated root + deep-leaf content markers on Home /
  Detail / Modal.
- scenarios.tsx: `reveal-sequence` (push Detail -> pop -> present Modal ->
  dismiss x10, jittered, content-present after every settle) and the
  `cold-first-*` family (first interaction after a cold launch at swept delays,
  content present within 2s; a re-tap must not be what heals it).
- runner.tsx / scenario-types.ts: `initialSettleMs` + `coldFirst` let cold-first
  scenarios drive their first interaction immediately (racing the cold install).
- lib/exec.js: `ITEST_SESSION` env pins agent-device to a named session so an
  itest lane coexists with another agent-device session on a different sim
  (otherwise every snapshot came back empty).
- lib/suites.js: `reveal` suite + maxAttempts:1 budgets (a blank / dropped nav
  can never be masked by a lucky retry).

REPRODUCES RED on mainline: reveal-sequence fails cycle-0 pop with
CONTENT_BLANK root-absent (Home content detached after push->pop).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eal-sequence diagnostics

The first cut of settleForRoute keyed on the React-Nav route, which flips
SYNCHRONOUSLY on dispatch — before the native present/pop completes — so the
probe fired mid-transition and produced FALSE blanks (a modal that was in fact
fully rendered read as root-absent). Fix:

- settleForRoute now waits for a NEW `transitionEnd` lifecycle event (from the
  transition this action started) that has been quiet for a beat, AND the top
  route to match — i.e. observe the POST-reveal state.
- runRevealCycle runs the FULL push->pop->present->dismiss every cycle (no
  short-circuit on first blank) so a modal-step failure never leaves a presented
  modal covering (hiding) the ProbeOverlay the host reads the verdict from.
- probeContentPresent gains a diagnostic-on-failure dump: the top controller
  subtree, and the engine reconcile state (stackTransitioning, active ids, nav
  vc count, whether the revealed controller has content, is the nav's top VC, is
  on-window, and its superview chain). This is what localised the bug to a view
  parented off-window in a 0x0 orphaned transition container.

With this, reveal-sequence reliably reproduces the REAL content-blank (verified
independently by screenshot + agent-device a11y snapshot: only the nav bar +
tab bar remain, all Home content absent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nded off-window)

Root cause (code-grounded + reproduced): after a rapid pop, UIKit can fail to
move the revealed top view controller's view back into the on-window navigation
content — it is left orphaned in a collapsed (0x0), off-window transition
container even though the nav controller itself is on-window and still reports
that VC as its top. The screen shows only the nav bar + tab bar; all content is
gone from the window (and the a11y tree). The engine's model-based reconcile can
NEVER see this: setNavigationControllerViewControllers early-returns on
viewControllersEqual, because from the stack MODEL nothing changed — only the
VIEW is stranded. Neither layout memos nor the transition latch nor react-freeze
are involved (freeze is off; the content is mounted in the correct controller).

THE HEALER — `reattachStrandedTopView(registry, stackId)`: when the stack is
SETTLED (not transitioning) and the nav is on-window but its top VC's view is
off-window, force UIKit to re-host it by clearing then restoring the same stack
non-animated (bypassing the equal-guard). Idempotent — a no-op on every healthy
reveal. Armed as a small, non-re-arming deferred fan (0/140/340ms) from BOTH
didShow arms so a pop whose didShow resolves no screenId is still covered.

Defense-in-depth in the same coordinated reveal region:
- `bustLayoutMemos(nav, registry)` at the pop-reveal (didShow, closing) and the
  modal-dismiss reveal, so the layout FILL re-stamps the revealed subtree instead
  of hitting a stale "skip" signature (the memory's zero-frame reveal variant);
  fold the first subview's rounded frame into layoutHostedSignatures and skip
  caching a signature mid-transition (poison guard). Bounded: once per reveal
  boundary, O(vcCount) — the O(n^2) steady-state skip is untouched.
- Cancel the pending `scheduleTransitionFallback` timer in `finishTransition`:
  its 420ms non-animated re-install fired after EVERY (healthy) transition and
  raced UIKit's just-completed reparent — a latent spurious install.

Adds `bustLayoutMemos` unit tests. reveal-sequence: 10x push/pop/present/dismiss
now content-present after every settle (was a permanent Home blank).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…er decisions)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ition-uncache hang

Two corrections to the content-blank fix after on-sim verification isolated the
real mechanism and a self-inflicted hang:

1. THE ACTUAL BUG is a UIKit view STRAND, not a layout-memo skip. After a pop the
   revealed screen's view is orphaned in a collapsed (0x0), off-window transition
   container while the nav controller is on-window and still reports that VC as
   its top; the model reconcile is blind to it (setViewControllers early-returns
   on viewControllersEqual). `reattachStrandedTopView` now re-hosts it CHEAPLY —
   inserting the view directly below the nav bar of the on-window nav view —
   instead of a full setViewControllers rebuild (kept only as a fallback). Gated
   by `isTopStranded` (settled + nav on-window + top view off-window) and armed
   as a persistence-confirmed fan (`armStrandHealFan`, both didShow arms) so it
   never fires on the ~1-frame off-window transient a HEALTHY reveal shows.

2. REVERT the "skip the layout memo cache while transitioning" change (A3) from
   the prior commit: it re-ran the hosted-subview FILL uncached on every configure
   during a transition — reintroducing the O(n^2)-class main-thread stall the memo
   exists to prevent (~500ms per push/pop), which failed the 250ms steady-state
   hang gate on every core push/pop scenario. The reveal chokepoint bustLayoutMemos
   + the frame-fold in the hosted signature are retained (they do not regress).

Verified on-sim: core 11/11 green (steady stalls <150ms), reveal-sequence green
(heal ~140ms), unit 242/242, test:rnav 19/19. reveal-sequence is RED without the
heal (strand confirmed real, independently by screenshot + a11y snapshot).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…'s minimal set

Remove dead + test-only exports from @nativescript/react-native and demote the
internally load-bearing helpers to module-private. The consumers that define
"keep" (the react-native-screens adapter, the tabs-snapshot subtree, the
shipped examples, and the demo) use only ~30 of the previous exports; the rest
were exercised only by the source-text pinning tests.

Deleted exports (no consumer references them; verified by grep):
  uiInvoker, invokeOnJS, createEventBridge, createRetainer, retain, release,
  assertUIKitThread, warnIfNotUIKitThread, isMainThread,
  getAssociatedNativeObject, registerUIRuntimeGlobalSync, nearestViewController,
  uikitHostOwnerHandlesForView, attachViewControllerToNearestParent(+Handle),
  refreshUIKitHostView{Handle,Owner,OwnerHandle,DirectOwner,DirectOwnerHandle},
  invalidateUIKitHostReadyOwner(+Handle),
  notifyUIKitAccessibilityLayoutChangedHandle,
  flushUIKitHostView{,Handle,Owner,OwnerHandle}

Demoted to module-private (bodies kept; still called internally):
  installGlobals, installWorklets, isInstalled, defaultMetadataPath,
  getRuntimeBackend, eventBridge, jsInvoker, runtimeInvoker, runOnUISync,
  setAssociatedNativeObject, getProtocol, isFrameworkLoaded, and the standalone
  action-target family (canCreateNativeActionTarget / createNativeActionTarget /
  invokeNativeActionTarget / canCreateNativeUIAction / createNativeUIAction).

Also dropped the redundant `install` alias (init is the single entry point).

Kept: init, defineUIKitView/Container, defineUIViewController, createDelegate,
runOnUI, registerUIRuntimeGlobal, dispatchAsyncOnMainQueue, nativeMethodPolicy,
getClass, isClassAvailable, loadFramework, nativeHandleForObject,
nativeObjectFromHandle, invokeObjCSelector, nativeArrayLength/Item,
nativeSubviews, collectedUIKitHostChildren, uikitHostHandlesForView,
refreshUIKitHostView, notifyUIKitAccessibilityLayoutChanged, loadImage,
reactNativeFabricViewLayoutTraits(ForHandle).

The source-text pinning tests and the adapter jest mock are updated in the same
commit. Native/codegen surface is untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…op drifted index.d.ts

The hand-written src/index.d.ts had drifted from the implementation. Point
package.json "types" at src/index.ts and delete the d.ts.

index.ts is authored for babel/metro (intentional loose casts, worklet-runtime
globals) and was never strict type-checked, so it carries a top-of-file
`// @ts-nocheck`: consumers still get every exported public declaration and the
generated iOS interop globals (via the moved
`/// <reference path="../types/ios/index.d.ts" />`) without inheriting the
file's internal strict-mode noise. The `NativeApiHost` type is now exported
from index.ts.

Repointed each source-text pinning test's `declarations` read to src/index.ts
and converted the d.ts-only assertions (the `X: typeof X` default-export
entries and the removed-symbol signatures) to their index.ts equivalents.

Verified on the simulator host (no native/codegen changes): react-native-screens
tsc holds at its pre-existing baseline — pointing types at index.ts adds no new
consumer type errors thanks to @ts-nocheck; adapter jest 235 + 19 green; pinning
suite unchanged (only the 3 pre-existing native-source-drift tests remain red).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…urces; restore base flushUIKitHostView

The three source-text pinning tests (uikit-host-ready/refresh/transaction) were
red because they pinned native behaviors whose implementation drifted after the
surface trim, and because the index.d.ts->index.ts repoint let them reach those
assertions for the first time. Realign them to the current sources without
weakening what they assert:

- ready-api: the "no RNS-specific trace hooks" guard now strips comments before
  checking for RNSScreen, so shipped code is still forbidden from referencing the
  RNS classes while parity documentation may cite RNSScreen.mm line numbers.
  NS_RNS_TRACE / NSRNS (the actual trace macro + symbol prefix) stay forbidden
  everywhere, comments included.
- refresh-api: NativeScriptRestoreFabricChildrenForUnmount gained two trailing
  nil params (behavior unchanged per its own comment); pin the 5-arg call.
- transaction-api: the props-commit and mount fallback paths were unified onto
  the shared, monotonic _fabricTransactionDeliveryToken /
  advanceFabricTransactionDeliveryToken (already pinned by the delivery-token
  test); the immediate-commit branch gained a && !transactionHasRemovalMutation
  removal-defer gate. Pin the current mechanism.

Also restore base flushUIKitHostView: the tabs-snapshot reveal path
(flushSelectedTabDisplay) consumes it through the same guarded
typeof-=== 'function' pattern as the kept refreshUIKitHostView right beside it,
and the snapshot's own suite pins that usage. The trim dropped base flush while
keeping base refresh -- an asymmetry that silently no-ops the tabs display flush.
Restore only the base export (the native __nativeScriptFlushUIKitHostView entry
point was already retained); the handle/owner variants stay trimmed. Sync the
adapter jest mock. Tabs live code now references zero removed names.

JSDoc the ~30 kept exports and the UIKitViewContext / UIKitViewDefinition /
UIKitHostViewProps members (plus sizing/layout/host-ready types) -- contract +
hazard only; index.ts stays @ts-nocheck so consumers inherit no new strict errors.

Verified: 38/38 pins, adapter jest 235 + native-stack 19, ffi-boundaries, and the
react-native-screens tsc baseline (9 pre-existing errors, unchanged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDFVSJwNBb2wXjBVzZREHW
…README; demote RN_API to worklog

Rewrite packages/react-native/README.md into the authoritative surface doc for
the trimmed runtime: what it is, init() + the two Babel plugins, the threading
model (RN JS thread / worklet UI runtime / main queue), the three host factories
with lifecycle hooks + ctx + layout.sizing, the kept UIKitHostViewProps one line
each, every interop utility with its contract + one hazard, and the __extendClass
contract with its two shipped hazards (property override needs an accessor
descriptor; typeof proxy.sel === 'function' is not an availability check).

Add a one-page packages/react-native-screens/README.md documenting the thin
react-native-screens adapter: how it aliases onto stock React Navigation, the
deliberately tiny engine slice it consumes, and its test wiring.

Demote the RN_API.md worklog to docs/audits/RN_API-worklog.md -- src/index.ts is
now the type surface of record (package.json "types"), so the hand-maintained API
dump is history, not doc.

Also add tsconfig.verify.json, which aliases @nativescript/react-native to the
runtime's src/index.ts so the adapter can be type-checked against the live
surface (holds at the 9-error pre-existing baseline).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDFVSJwNBb2wXjBVzZREHW
…re-host at didShow

Stage 1 of reconcile-parity-close. The merged daed719 only RECOVERED the
content-blank: after a pop the revealed top view is stranded off-window in a
collapsed 0x0 transition container, and a persistence-confirmed heal fan
re-hosted it ~0.85-1.6s later — a visible blank FLASH before recovery.

Root cause of the flash: the prevention hook was gated on `isClosing`/
`revealClosing`, but on-sim the pop's didShow takes the no-screenId placeholder
arm with the closing flag NOT set (strandVisited=0, strandProbed=1 proved the
fan saw a strand prevention never reached). Call `hostRevealedTopViewNow`
UNCONDITIONALLY in both didShow arms and on the modal-dismiss restore: it
self-guards (re-hosts ONLY when the nav is on-window and the revealed top view
is off-window; no-op on a healthy on-window reveal), so it prevents the strand
at its source, synchronously, before the heal fan's first probe. No blank is
ever painted.

The existing armStrandHealFan stays as a pure safety net (it must ~never fire);
the WIP band-aid that armed ANOTHER fan on the modal-dismiss path is dropped —
prevention, not more recovery.

Harness: content-present probe now snapshots IMMEDIATELY at the native
transitionEnd (was sleeping 1500ms, which let the heal fan mask the flash), and
a terminal heal-audit asserts the RECOVERY counter delta is 0. Instrumentation
counters (strandHealFired/strandPrevented/strandProbed/strandVisited) exposed on
the registry and folded into the probe regState.

Verified headless (sim itest-lane1, iOS 26.5): reveal-sequence x10 jittered,
2 runs, content-present at the FIRST snapshot for BOTH pop-reveal and
modal-dismiss-reveal — healFired+0, prevented+10 (prevention engaged), 0 blanks.
Package unit suite 246/246 (+4 hostRevealedTopViewNow tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDFVSJwNBb2wXjBVzZREHW
…ch watchdog

Stage 2 of reconcile-parity-close. idb-companion won't install (formula needs
Xcode 27; we're on 26.6, no bottle), so the cancelled interactive pop is driven
fully headless via a slow, short left-edge agent-device drag (`swipeCancel`,
~40% width over 1.5s) — released below UIKit's commit threshold so it CANCELS
and snaps back. No idb, no host cursor.

New `cancel-swipe-wedge` gate: push Detail, drive the cancelled swipe, then
assert (a) Detail is still content-present, (b) the latch is NOT stuck, (c) a
programmatic push grows the NATIVE stack, and (d) a modal actually presents
(unambiguous modal markers). A `probeNavState` native read exposes vc-count /
active-count / stuck-latch / the diagnostic counters.

Finding: on iOS 26.5 the wedge does NOT reproduce. Instrumentation proves why —
the interactive pop begins (nativeWillShowMarks=2) and UIKit DELIVERS a didShow
on the cancel that the existing wasCancelledGesture path already clears
(cancelDidShowClears=1, latchStuck=0). The memory's Bug B root cause ("UIKit
never calls didShow on a cancelled interactive pop") does not hold here.

Still landed the fix as a belt-and-braces safety net for a runtime that DOES
skip that didShow: `armInteractiveCancelWatchdog` — a ONE-SHOT, token-guarded
watchdog armed at native-driven willShow (closing only). It clears a LEAKED
latch as a cancel iff this exact transition never finished (token unchanged) and
no native transition is still animating (transitionCoordinator == null).
Deliberately NOT a bridged notifyWhenInteractionChangesUsingBlock: (a returning
coordinator block risks hanging the worklet bridge). It stays dormant on iOS
26.5 (interactiveCancelRecovered=0), proven green by the gate.

Verified headless (sim itest-lane1, iOS 26.5): cancel-swipe-wedge PASS — cancel
drives, push advances (vc=3), modal presents, watchdog dormant. Unit 246/246.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDFVSJwNBb2wXjBVzZREHW
…source)

The content-blank band-aid (hostRevealedTopViewNow / reattachStrandedTopView /
armStrandHealFan / isTopStranded + strand* counters) fired at didShow on HEALTHY
reveals and insertSubviewBelowSubview'd the top VC view as a RAW subview of
nav.view — OUTSIDE UIKit's UINavigationTransitionView->UIViewControllerWrapperView
chain. That desynced the nav-bar item stack from viewControllers and stopped it
updating: with the demo's headerTransparent:true an item-less bar is INVISIBLE
(the "2nd+ push no nav bar" render loss, catalog #1). The setViewControllersAnimated
([],false) fallback emptied the bar item stack as a second corruption source.

Deleted all four functions + call sites (modal-dismiss-restore, both didShow arms,
the healer fan) + the strand* registry fields/inits. KEPT bustLayoutMemos (correct
memo hygiene). The reveal-time content strand this masked is fixed at its source by
the transition-coordinator gate (Stage C), not re-hosted.

RED->GREEN (sim 6590BA99, push->pop(button)->push): 2nd-push nav-bar chrome band
0/0/0 -> 6503/2245/7752 (pixel-identical to the 1st push, Back chevron + "Native
Detail" title + Tap all present). Content-blank after button-pop remains, handled
by Stage C.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaVDuTDBj6oz3yfnT3yMhn
…trand heal

B (single-mutation primitive): setNavigationControllerViewControllers now returns
after the shrink branch and keeps a single viewControllers mutation per branch
(popTo OR one setViewControllersAnimated), mirroring RNSScreenStack.mm — was doing
2-3 mutations/frame.

C (transition-coordinator gate): applyStackModel parks the latest intent
(stackPendingModel, latest-wins) and returns instead of mutating while a native
transition is live (transitionCoordinator != null — a plain property read, no
bridged block) or the latch is set; drains token-guarded at finishTransition,
completeModalDismissalToDepth, and a coordinator-aware fallback settle probe. This
keeps every repair/fallback path from mutating mid-transition. Instrumented
(stackGateDeferred / stackGateBypassed) for the UIKit-parity harness gate.

D (guaranteed-animated pop): classify the animated pop on the LIVE
arrayCount(viewControllers) rather than nativeIds.length (which silently drops a
VC whose screen id fails to resolve in pop-first dispose ordering), so a button
pop always takes popViewControllerAnimated; dispose also retains the screen record
while the parent stack is transitioning.

F (mid-transition write hygiene): layoutNavigationStackViews /
layoutHostedReactSubviews skip their frame stamps while a coordinator is live
(force=true re-run once at finishTransition), so a content-revision bump timer can
never stamp controller.view.frame on a view UIKit is animating.

Reveal re-host (root correction): the pre-fix theory (strand from a mid-transition
mutation) does NOT hold on this runtime — verified on-sim that on a CLEAN pop with
zero mid-transition mutation the coordinator clears yet the revealed screen's view
stays off-window (buried screens are fully detached, superview=null). UIKit's own
setViewControllers rebuild re-hosts it AND keeps the nav-bar item stack in sync
(unlike the deleted raw insertSubview band-aid). Armed as a bounded, coordinator-
guarded fan on closing/cancelled + modal-dismiss reveals; self-guards to a no-op
on a healthy on-window reveal.

Verified (sim 6590BA99, push->pop(button) x3): header 6503/2245/7752 + "Native
Detail" title on every push (no bar corruption); Home content present after every
pop (permanent strand blank GONE); no crash. KNOWN RESIDUAL: buried-screen content
does not SLIDE during the pop (nav bar cross-fades; content snaps in at transition
end via the re-host) — a pre-existing detached-children hosting limitation
(catalog #3/#5, present in tip + reverted builds), left for a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaVDuTDBj6oz3yfnT3yMhn
… slot race)

Two low-risk hardenings so a custom `headerTitle` titleView is never silently
dropped:
- configureHeaderTitleView now nudges the navigation bar (setNeedsLayout, plus
  layoutIfNeeded when the stack is settled) after installing/changing a titleView,
  so a titleView installed on an ALREADY-DISPLAYED navigation item is measured and
  drawn (iOS does not lay out a late titleView on its own).
- the screen host `mounted` re-applies header slots when a slot already exists for
  the screen, covering the race where a slot host's `hostReady` fired before
  `mounted` registered the screen (so the earlier `applyHeaderSlots` no-oped).

Verified: "Native Detail" title present on cold first push and every subsequent
push after a button-pop; no regression to header chrome (6503/2245/7752) or
content-present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaVDuTDBj6oz3yfnT3yMhn
…aused a stall

Verified on-sim that the button-pop content strand is architectural (the revealed
detached-children view is off-window regardless of frame-stamp timing), so the
reveal re-host — not F — is what fixes it. Meanwhile F's finishTransition
force-layout (bustLayoutMemos + a forced whole-stack fill on EVERY transition) plus
its coordinator-gated layout added main-thread work that regressed the anim-none
steady-stall gate (baseline smax 121ms -> 279ms > 250ms budget). Removing F drops
it back under budget (smax 201ms) with the content fix fully intact (header
6503/2245/7752 + "Native Detail" title on every push, content present after every
pop). No F benefit was lost.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaVDuTDBj6oz3yfnT3yMhn
…gate audit

New CORE scenario `header-after-button-pop` (maxAttempts:1): the exact RED repro —
push Detail -> pop (goBack) -> push x3. Asserts, in-app, content-present after
EVERY push and pop (probeContentPresent — §5 gate 5); host-side, that the custom
"Native Detail" title AND the right header action ARE present on the 2nd + 3rd push
and the re-push after the cycles (§5 gates 1/3 — the RED bug rendered chrome 0/0/0
here); and that the transition-coordinator gate never bypassed mid-transition
(stackGateBypassed == 0 — §5 gate 4, the UIKit-parity invariant that replaces the
retired strand* heal counters).

probeHealCounters now also reads the new gate counters (stackGateBypassed /
stackGateDeferred / revealRehostFired); the reveal-sequence terminal audit asserts
gateBypassed == 0 instead of the removed strandHealFired.

Gate 2 (pop content ANIMATES >=3 positions) is intentionally NOT added as a passing
gate: the detached-children content does not slide during a pop on this runtime
(the nav bar cross-fades, content snaps in at transition end via the re-host) — a
pre-existing architectural limitation, not a regression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaVDuTDBj6oz3yfnT3yMhn
…tack content (+22pt)

Catalog #4 attributed a +22pt (65-66px @3x) extra TOP content inset to the
native-stack transparent-header path (Home titleTop 490 vs upstream 425, Detail
491 vs 425). It is NOT an engine bug. Raw a11y frames from BOTH apps on the same
iPhone 16 Pro show the scroll content container at y=116pt in BOTH the port and
upstream `nativescript-uikit-demo-original` — identical `contentInsetAdjustment`
result; the engine's transparent-header inset is already pixel-exact.

The whole divergence is the harness's own content-presence probes: the port demo
places `<View accessibilityLabel="itest {home,detail} root" style={{width:6,
height:6}}/>` as the FIRST child of each screen's ScrollView. In the flex flow it
consumed a row plus the content `gap:16`, pushing the visible content 6+16 = 22pt
below upstream (which has no such probe). Making the root probes
`position:'absolute'` keeps them in the a11y tree (found + on-window + 6x6) while
removing them from layout, so the first in-flow child is the headerBlock again.

Verified on the booted iPhone 16 Pro: Home titleTop 490 -> 424, Detail 491 -> 425
(upstream 425); both content titles land at frame y=134 == upstream. The engine,
UIKit tab, modal page-sheet, and non-transparent-header paths are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaVDuTDBj6oz3yfnT3yMhn
On EVERY React-Nav tab activation the port re-ran a full screen appearance
"opening" transition while the content was already on screen. Profiled on the
sim, the sequence was: tabSelected -> (native-driven willShow marks a fresh
opening transition at +3ms) -> ~600-2600ms with the main thread IDLE waiting for
UIKit's didShow -> transitionEnd. react-navigation therefore saw the tab stuck
"opening" for up to ~2.6s per switch (measured dispatch->transitionEnd
[849,1506,2589,1240,2162,1440]ms). Upstream re-selects a tab by unhiding a
persistent UINavigationController (O(1), ~80-120ms).

The willShow delegate could not tell a real navigation from a bare re-appearance:
when a tab is unhidden UIKit runs an appearance cycle that fires willShow for the
already-top view controller, and the engine marked it as an opening transition
and then dwelled on the lagging didShow. Fix: in the native-driven willShow path
(a JS push/pop or an interactive gesture-begin already returned above it) detect a
bare re-appearance -- not closing, the incoming screen is already the top of an
UNCHANGED, previously-appeared stack -- and emit the transitionStart/End pair
IMMEDIATELY without marking the stack transitioning, so the trailing didShow
settles as a no-op. A real push/pop (identity change), a first mount (never
appeared) and an interactive cancel (latched above) all skip it.

Result: per-activation dispatch->transitionEnd collapses to ~3ms for repeated
(warm) re-selections; the first cold re-reveal round-trip is ~150-263ms
(tabs-snapshot cold path, one-time). The engine work at didShow is ~2ms
(layout 1ms + config 2ms) -- the old cost was purely the willShow->didShow dwell.

Harness: tab-switch-storm now records each activation's dispatch->transitionEnd
(a `tabSelected` marker in App.tsx + the demo screens' existing transitionEnd
events) and GATES the repeated (warm) re-selections < 250ms -- the user's
"switching tabs hangs" complaint. The 3000ms main-thread stall tolerance masked
this per-activation dwell; the transition-idle gate is now the sharp detector.

Not applied: the memory-prescribed watchdog CommonModes change. It is moot now
that the dwell is gone (nothing to starve the display link on) and empirically
REGRESSED core (tabs, back-swipe newly FAILed MAIN_THREAD_STALL_STEADY by
surfacing simulator display-link artifacts during ordinary UIKit transitions as
steady stalls) without reducing starvation (link still starved, ticks~=395). The
JS-thread transition-idle gate replaces its intent.

Verified: jest 238/238, rnav 19/19, itest core 12/12, tab-switch-storm PASS
(warm ~3ms), reveal-sequence + cancel-swipe-wedge PASS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaVDuTDBj6oz3yfnT3yMhn
…ing a pop

The port did a BARE popViewControllerAnimated in animateStackPop. On this
runtime UIKit then left the revealed VC's view off-window for the whole ~350ms
pop; the engine only re-hosted it at transitionEnd via armRevealRehostFan, so
the buried screen's content SNAPPED IN instead of sliding (and an edge-swipe
had nothing to dim/parallax).

Upstream RNS does reseat-then-pop (RNSScreenStack.mm:652-653):
setViewControllers(new, animated:NO) THEN popViewControllerAnimated(YES) in the
same turn — the non-animated same-array reseat rebuilds hosting BEFORE the
animation. Mirror that: reseatControllersBeforePop() runs the proven-safe
same-array setViewControllers non-animated reseat (the same primitive
armRevealRehostFan already uses, bar-sync-preserving) before the animated pop,
in all three pop branches (single-step, pop-to-root, pop-to-controller),
scoped to animated pops. Runs pre-transition (coordinator null — §4C gate).

Result: over button-pops the armRevealRehostFan heal is no longer needed
(revealRehostFired stays 0) — the pop is now a genuine UIKit parallax pop with
the content on-window from the first frame. header-after-button-pop gate stays
PASS, coordinator gate never bypassed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a presentation-layer pop-slide sampler + the pop-slide scenario (the
`slide` suite). Over 10 push/button-pop cycles it arms a CADisplayLink (the
proven display-link-spike primitive) that each frame DFS-locks the revealed
content root the instant UIKit hosts it, then reads its WINDOW-space origin.x
via the PRESENTATION layers (convertPoint:fromLayer: across presentation
copies) — the mid-animation position Core Animation is actually drawing.

Asserts, per pop: the revealed content is on-window throughout (no strand),
sweeps across >=3 distinct intermediate positions with >=20pt monotonic travel
to rest; and, in aggregate: revealRehostFired delta == 0 (the transitionEnd
heal is no longer needed — reseat-then-pop rebuilt hosting BEFORE the
animation), coordinator gate never bypassed, strands == 0.

Result on Stage 1: slides=10/10, strands=0, rehost+0, bypass+0 — each pop
~12 distinct positions, startX ~-73pt (parallax from the left) monotonic
(inv=0) to endX=0, on-window ~205/205 frames.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ayout silence)

The stock left-edge interactive back-swipe is UIKit-initiated and never hits
animateStackPop, so reseat-then-pop (Stage 1) does not cover it. It stranded
the revealed screen off-window for the whole scrub (blank, no dim/parallax),
healed only at transitionEnd. Root = two mid-transition layout writes on the
CLOSING nav view, one native + one JS, that re-strand the revealed view each
frame:

- native: pushAdoptedSizeFeedbackIfNeeded runs every frame via layoutSubviews
  and force-re-lays nav.view. Skip that trailing [nav.view setNeedsLayout] when
  nav.transitionCoordinator != nil (keep the shadow-tree size push). The §4F
  JS-side layout silence never covered this native path.
- JS: the reveal didShow fires while the interactive coordinator is still live;
  its layoutNavigationStackViews is the same mid-transition write. Defer it when
  transitionCoordinator != null (park stackPendingRevealLayout) and re-run it
  from the reveal-rehost fan the instant the coordinator clears. A programmatic
  push/pop reaches didShow with the coordinator already gone, so it lays out
  immediately as before — button-pop / anim-none paths unchanged.

Result: edge-swipe-slide gate = slides 5/5, strands 0, rehost 0 — the revealed
screen sweeps ~121pt of native parallax across 44-46 distinct on-window
positions (monotonic) during the swipe. pop-slide stays 10/10.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ive reveal slides

Adds the edge-swipe-slide scenario: 5 push + host left-edge back-swipe cycles,
arming the presentation-layer sampler during each swipe. Asserts the revealed
content is on-window throughout (no strand) and sweeps >=3 distinct window-x
positions with monotonic parallax travel. Registered in the slide suite next to
pop-slide. Verifies STAGE 2 (edge-swipe mid-transition layout silence):
slides 5/5, strands 0, ~121pt travel, 44-46 distinct positions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@DjDeveloperr
DjDeveloperr marked this pull request as ready for review August 5, 2026 20:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant