rnxsim 0.1.381 → 0.1.382

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 (138) hide show
  1. package/README.md +19 -6
  2. package/cli/app-fonts.ts +1 -1
  3. package/cli/bin.ts +118 -26
  4. package/cli/bridge-diagnostics.ts +1 -1
  5. package/cli/bridge-flow-runner.ts +9 -9
  6. package/cli/browser-evals.ts +3 -3
  7. package/cli/cli-update.ts +261 -0
  8. package/cli/cloud-client.ts +407 -0
  9. package/cli/cloud-dispatch.ts +165 -0
  10. package/cli/cloud-session.ts +156 -0
  11. package/cli/command-contract.ts +2 -2
  12. package/cli/command-registry.ts +9 -0
  13. package/cli/commands/agent-wrapper.ts +4 -4
  14. package/cli/commands/agent.ts +3 -3
  15. package/cli/commands/box/checkout-plane.ts +16 -8
  16. package/cli/commands/box/rnx-command.ts +2 -2
  17. package/cli/commands/box/search-command.ts +2 -2
  18. package/cli/commands/box.ts +123 -70
  19. package/cli/commands/config.ts +3 -2
  20. package/cli/commands/control.ts +6 -6
  21. package/cli/commands/cpu-profile.ts +2 -2
  22. package/cli/commands/daemon.ts +7 -7
  23. package/cli/commands/debug.ts +2 -2
  24. package/cli/commands/detox.ts +7 -7
  25. package/cli/commands/diagnose.ts +1 -1
  26. package/cli/commands/electron.ts +1 -1
  27. package/cli/commands/film.ts +1 -1
  28. package/cli/commands/flow.ts +4 -4
  29. package/cli/commands/inspect/core.ts +58 -7
  30. package/cli/commands/inspect/env.ts +5 -3
  31. package/cli/commands/inspect/shared.ts +2 -1
  32. package/cli/commands/inspect/wait-event.ts +1 -1
  33. package/cli/commands/inspect/wait-ready.ts +15 -5
  34. package/cli/commands/inspect.ts +31 -9
  35. package/cli/commands/install-desktop.ts +2 -2
  36. package/cli/commands/maestro.ts +3 -3
  37. package/cli/commands/permissions.ts +1 -1
  38. package/cli/commands/platform.ts +91 -0
  39. package/cli/commands/record.ts +3 -3
  40. package/cli/commands/report-issue.ts +1 -1
  41. package/cli/commands/runtime.ts +1 -1
  42. package/cli/commands/screenshot-mode.ts +1 -1
  43. package/cli/commands/screenshot.ts +25 -0
  44. package/cli/commands/serve.ts +1 -1
  45. package/cli/commands/skills.ts +1 -1
  46. package/cli/commands/timeline.ts +1 -1
  47. package/cli/commands/upgrade.ts +8 -34
  48. package/cli/commands/upload.ts +3 -3
  49. package/cli/commands/version.ts +14 -3
  50. package/cli/commands/what-happened.ts +2 -2
  51. package/cli/current-sim.ts +43 -30
  52. package/cli/desktop-companion.ts +1 -1
  53. package/cli/drivers/electron.ts +1 -1
  54. package/cli/drivers/playwright.ts +3 -3
  55. package/cli/drivers/types.ts +4 -4
  56. package/cli/flow-file.ts +3 -3
  57. package/cli/help-core.ts +18 -11
  58. package/cli/hidden-runtime-alias.ts +1 -1
  59. package/cli/hints.ts +2 -2
  60. package/cli/maestro-js.ts +2 -2
  61. package/cli/outbound-endpoints.ts +40 -6
  62. package/cli/parse-args.ts +1 -1
  63. package/cli/registry.ts +1 -1
  64. package/cli/resolve-assets.ts +2 -2
  65. package/cli/secure-file.ts +31 -0
  66. package/cli/shell-init.ts +98 -0
  67. package/cli/ws-bridge.ts +18 -8
  68. package/detox/expectations.ts +1 -1
  69. package/detox/gestures.ts +3 -3
  70. package/detox/index.ts +3 -3
  71. package/dist-lib/agent-daemon-client.cjs +1 -1
  72. package/dist-lib/agent-events.cjs +1 -1
  73. package/dist-lib/agent-identity.cjs +1 -1
  74. package/dist-lib/agent-sessions.cjs +1 -1
  75. package/dist-lib/attached-projects.cjs +1 -1
  76. package/dist-lib/auth/shared-session.cjs +1 -1
  77. package/dist-lib/backend-origin.cjs +1 -1
  78. package/dist-lib/beta.cjs +1 -1
  79. package/dist-lib/beta.mjs +1 -1
  80. package/dist-lib/bridge-constants.cjs +1 -1
  81. package/dist-lib/bridge-contract-input.cjs +15 -8
  82. package/dist-lib/bridge-contract-input.mjs +15 -8
  83. package/dist-lib/bridge-contract.cjs +1 -1
  84. package/dist-lib/bridge-contract.mjs +1 -1
  85. package/dist-lib/capture-contract.cjs +107 -0
  86. package/dist-lib/capture-contract.mjs +80 -0
  87. package/dist-lib/cli-constants.cjs +1 -1
  88. package/dist-lib/cloud-contract.cjs +32 -0
  89. package/dist-lib/cloud-contract.mjs +7 -0
  90. package/dist-lib/config.cjs +1 -1
  91. package/dist-lib/detox/index.cjs +4 -4
  92. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  93. package/dist-lib/home-paths.cjs +9 -1
  94. package/dist-lib/host/bridge-host.cjs +15 -10
  95. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  96. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  97. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  98. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  99. package/dist-lib/host/websocket-proxy.cjs +1 -1
  100. package/dist-lib/index.cjs +248 -73
  101. package/dist-lib/jump-to-source-babel.cjs +1 -1
  102. package/dist-lib/menu.cjs +1 -1
  103. package/dist-lib/menu.mjs +1 -1
  104. package/dist-lib/metro-production-bundle.cjs +1 -1
  105. package/dist-lib/metro-production-bundle.mjs +1 -1
  106. package/dist-lib/metro.cjs +1 -1
  107. package/dist-lib/profiles.cjs +1 -1
  108. package/dist-lib/public-brand.cjs +1 -1
  109. package/dist-lib/react-native-host-modules.cjs +1 -1
  110. package/dist-lib/react-native-host-modules.mjs +1 -1
  111. package/dist-lib/render-mode.cjs +1 -1
  112. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  113. package/dist-lib/sdk.cjs +44 -6
  114. package/dist-lib/sdk.mjs +44 -6
  115. package/dist-lib/skills.cjs +435 -155
  116. package/dist-lib/vite.cjs +1 -1
  117. package/package.json +20 -4
  118. package/scripts/dev-server-scanner.ts +3 -3
  119. package/skills/contrast/SKILL.md +1 -1
  120. package/skills/rnx-perf/SKILL.md +68 -52
  121. package/src/app-url.ts +2 -3
  122. package/src/bridge-contract-input.ts +17 -6
  123. package/src/bridge-contract.ts +25 -4
  124. package/src/capture-contract.ts +423 -0
  125. package/src/cloud-contract.ts +15 -0
  126. package/src/home-paths.ts +9 -3
  127. package/src/host/agent-host.ts +1 -1
  128. package/src/host/bridge-host.ts +13 -11
  129. package/src/host/plane-host.ts +2 -2
  130. package/src/index.ts +21 -0
  131. package/src/native-dev-bundle-url.ts +2 -2
  132. package/src/screenshots/orchestrate.ts +1 -1
  133. package/src/screenshots/org-deck.ts +1 -1
  134. package/src/settings-bundle.ts +1 -1
  135. package/src/sim-client.ts +4 -0
  136. package/src/vite-plugin-one.ts +2 -2
  137. package/src/vite-plugin.ts +1 -1
  138. package/cli/commands/box/cloud-box.ts +0 -294
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.381 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.382 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rnxsim",
3
- "version": "0.1.381",
3
+ "version": "0.1.382",
4
4
  "description": "Vite and Metro plugins, testing drivers, and SDK exports for rnx.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -41,6 +41,7 @@
41
41
  "./metro-production-bundle": {
42
42
  "types": "./src/metro-production-bundle.ts",
43
43
  "source": "./src/metro-production-bundle.ts",
44
+ "bun": "./src/metro-production-bundle.ts",
44
45
  "import": "./dist-lib/metro-production-bundle.mjs",
45
46
  "default": "./dist-lib/metro-production-bundle.cjs"
46
47
  },
@@ -115,6 +116,20 @@
115
116
  "import": "./dist-lib/bridge-contract.mjs",
116
117
  "default": "./dist-lib/bridge-contract.cjs"
117
118
  },
119
+ "./cloud-contract": {
120
+ "types": "./src/cloud-contract.ts",
121
+ "source": "./src/cloud-contract.ts",
122
+ "bun": "./src/cloud-contract.ts",
123
+ "import": "./dist-lib/cloud-contract.mjs",
124
+ "default": "./dist-lib/cloud-contract.cjs"
125
+ },
126
+ "./capture-contract": {
127
+ "types": "./src/capture-contract.ts",
128
+ "source": "./src/capture-contract.ts",
129
+ "bun": "./src/capture-contract.ts",
130
+ "import": "./dist-lib/capture-contract.mjs",
131
+ "default": "./dist-lib/capture-contract.cjs"
132
+ },
118
133
  "./bridge-constants": {
119
134
  "source": "./src/bridge-constants.ts",
120
135
  "default": "./dist-lib/bridge-constants.cjs"
@@ -214,12 +229,13 @@
214
229
  }
215
230
  },
216
231
  "devDependencies": {
217
- "@contrast/harness": "workspace:*",
218
232
  "@contrast/runtime-delivery": "workspace:*",
219
233
  "@sootsim/compat": "workspace:*",
220
- "@sootsim/globals": "workspace:*",
221
- "@sootsim/skills": "workspace:*",
234
+ "@rnx/box": "workspace:*",
235
+ "@rnx/globals": "workspace:*",
236
+ "@rnx/skills": "workspace:*",
222
237
  "@types/ws": "^8.5.13",
238
+ "rnx-cloud-box": "workspace:*",
223
239
  "sootsim-engine": "workspace:*",
224
240
  "typescript": "^5.7.0",
225
241
  "vite": "^8.2.2",
@@ -34,7 +34,7 @@ import {
34
34
  readAppSettingsBundle,
35
35
  } from '../src/settings-bundle.ts'
36
36
  import { loadOptionalDemoApps, optionalDemoAppsSync } from './optional-demo-registry.ts'
37
- import type { AppSettingsSpecifier } from '@sootsim/globals'
37
+ import type { AppSettingsSpecifier } from '@rnx/globals'
38
38
 
39
39
  // how long past a command's own budget we wait before deciding its exit event is
40
40
  // never coming. generous, because this only fires when the child is already
@@ -787,7 +787,7 @@ export async function probePort(
787
787
  buildIconProxyUrl?: (externalUrl: string) => string,
788
788
  listeningProcess?: ListeningProcess,
789
789
  ): Promise<DiscoveredServer | null> {
790
- // callers such as `sootsim open <port>` probe one port directly instead of
790
+ // callers such as `rnx open <port>` probe one port directly instead of
791
791
  // going through scanDevServers. wait for the optional in-repo registry here
792
792
  // too, or a cold CLI process can build the known app's bundle url before its
793
793
  // runtime config is available.
@@ -1074,7 +1074,7 @@ export function __shouldReuseScannerCacheEntry(
1074
1074
  return true
1075
1075
  }
1076
1076
 
1077
- // exported for tests / `sootsim debug` — forces the next scan to re-probe.
1077
+ // exported for tests / `rnx debug` — forces the next scan to re-probe.
1078
1078
  export function __resetScannerCache() {
1079
1079
  portCache.clear()
1080
1080
  knownNonPatched.clear()
@@ -42,7 +42,7 @@ automation, inspecting the render tree, and managing sims.
42
42
  | `rnx debug` | drive __sootsimDebug: channels, snapshots, and inspectors |
43
43
  | `rnx what-happened` | show recent semantic events: toasts, keyboards, screens, alerts, fetches, errors |
44
44
  | `rnx timeline` | control the semantic event timeline (enable opt-in kinds, clear, dump) |
45
- | `rnx box` | start a local box and enter its shell |
45
+ | `rnx box` | start a local Box or create a cloud Box |
46
46
  | `rnx ios` | start an iOS simulator |
47
47
  | `rnx android` | start an Android simulator |
48
48
  | `rnx open` | load a target into the current sim or open a new one |
@@ -24,7 +24,7 @@ indict, remeasure — and every wasted week came from skipping a step.
24
24
  draw call, paint boundary and raster-tier decision is in the compositor's
25
25
  render profile, not the shell's.** `perf shell stop` prints both, labelled
26
26
  by worker — read the compositor block for anything inside an app. for
27
- months it printed only the shell's, and a uniswap token-list fling
27
+ months it printed only the shell's, and a dense token-list fling
28
28
  therefore reported `raster tier: 0 promotions / 0 blits` while the
29
29
  compositor was blitting ~14 rows a frame; the giveaway was `node visits: 7`
30
30
  next to `app:one 1014 paints`. if a render-profile block describes a
@@ -32,26 +32,25 @@ indict, remeasure — and every wasted week came from skipping a step.
32
32
  are reading the wrong worker.
33
33
  browser matters just as much: playwright's bundled headless chromium has
34
34
  NO GPU — CanvasKit falls back to software rendering and every number lies
35
- high. launch GPU-backed Chrome via `launchReapedChrome` from
36
- `scripts/lib/reap-browser`, never a bare `chromium.launch()`. never
37
- `channel: 'chrome'` — that is the user's own browser, and automation
38
- launching it hijacks their Dock events.
35
+ high. launch a dedicated GPU-backed Chrome executable with a disposable
36
+ profile and an owning process supervisor that terminates its exact process
37
+ tree. never automate the user's own browser profile.
39
38
  2. **machinery silently not engaging is the default failure mode.** the
40
39
  raster tier compiled, gated, and did *nothing* for months — zero
41
40
  promotions on its textbook case — until per-gate rejection counters
42
41
  existed. the frame-demand gate was blind to commit storms. the vsync
43
42
  pump flapped 100 msgs/s at rest. never assume a tier works because it's
44
43
  wired; read its counter during the exact workload.
45
- 3. **check free memory and sampled CPU before blaming code.** an OOM or busy
46
- machine mimics every perf and infra failure. read `vm_stat`, then take at
47
- least two `top -l 2` samples and use the busiest sample for admission.
48
- attribute browser processes with `ps -eo pid,ppid,args` before treating
49
- one as abandoned or stopping an exact pid. load average is not an
50
- admission signal on macOS.
51
- 4. **never trade animation fidelity for CPU.** the home clock sweeps
52
- sub-second like real iOS; discrete ticks have been proposed and rejected
53
- repeatedly. attack per-frame cost (layers, caching, damage), never
54
- cadence or fidelity.
44
+ 3. **check reclaimable memory and sampled CPU before blaming code.** an OOM or
45
+ busy machine mimics every perf and infra failure. on macOS, read
46
+ `memory_pressure -Q` and take multiple `iostat -c 3` CPU samples; on Linux,
47
+ read `MemAvailable` from `/proc/meminfo` and sample CPU idle over time. keep
48
+ the busiest sample. raw macOS `freemem()` and load average are not admission
49
+ signals. attribute browser processes with `ps -eo pid,ppid,args` before
50
+ treating one as abandoned or stopping an exact pid.
51
+ 4. **never trade animation fidelity for CPU.** perpetual sub-frame animation
52
+ must keep its authored cadence. attack per-frame cost (layers, caching,
53
+ damage), never cadence or fidelity.
55
54
 
56
55
  ## measuring
57
56
 
@@ -63,14 +62,12 @@ paths add work to the simulator or browser and can change the cadence being
63
62
  measured. Record the visible window with the operating system's native screen
64
63
  capture instead.
65
64
 
66
- On macOS, use the window-object recorder in `~/screen` so the simulator does
67
- not copy or encode its own frames:
68
-
69
- ```sh
70
- bun ~/screen/src/cli.ts list-windows --owner "Google Chrome for Testing" --json
71
- bun ~/screen/src/cli.ts run --window-id <id> --duration <seconds> \
72
- --out <artifact.mov> -- <operating-system input command>
73
- ```
65
+ Use an operating-system-owned window recorder. Target the exact browser window
66
+ rather than the display when the platform supports it, start recording before
67
+ normal user input, preserve the movie, and record the operating system and
68
+ recorder version. Confirm that the recorder process, not the browser or
69
+ simulator, owns capture and encoding. If native window capture is unavailable,
70
+ report the proof as blocked instead of substituting an in-page capture path.
74
71
 
75
72
  The visual run contains only normal user input. Do not query the performance,
76
73
  debug, or test bridge while it records. Treat that video as visual evidence of
@@ -82,15 +79,10 @@ Open the sim with `?debug=framestamp` to make that video machine-readable.
82
79
  Every surface paint then draws its `Date.now()` low 20 bits as a 23-cell
83
80
  barcode — `home` and `app:*` at 45% of the surface height, `shell-overlay` on
84
81
  its own row at 55% so overlay paints (keyboard, native UI) are attributable
85
- separately — and the decoder reports, per presented frame, which paint it
86
- shows and how late that paint presented:
87
-
88
- ```sh
89
- # sim opened with ?debug=framestamp
90
- bun ~/screen/src/cli.ts run --window-id <id> --duration <seconds> \
91
- --out <artifact.mov> -- <operating-system input command>
92
- bun scripts/debug/decode-framestamp.ts <artifact.mov>
93
- ```
82
+ separately. Decode the documented 23-cell barcode with a compatible framestamp
83
+ decoder to report, per presented frame, which paint it shows and how late that
84
+ paint presented. If no decoder is available, keep the recording as qualitative
85
+ evidence and make no numeric presentation-latency claim from it.
94
86
 
95
87
  This is the only way to separate a frame the compositor never produced from one
96
88
  it produced and the browser never presented.
@@ -153,13 +145,13 @@ the report and how to read it:
153
145
  workload.
154
146
  **before widening any skip bucket, read the `also fail a later gate`
155
147
  line under it.** the counters name only the FIRST failed gate, so a big
156
- bucket can be entirely blocked a second time and unwidenable: on a uniswap
148
+ bucket can be entirely blocked a second time and unwidenable: on one dense
157
149
  token-list fling all 8298 `animated` skips also carried a style transform
158
150
  or a non-cacheable chunk, and narrowing the animated gate moved 6007 to
159
151
  `transform` and 2350 to `cacheable` while promoting nothing. a bucket is
160
152
  worth attacking when its also-blocked share is small.
161
- and blits firing is not the same as the tier paying for itself: on the
162
- uniswap token list the tier blits every visible row and costs 70MB of GPU
153
+ and blits firing is not the same as the tier paying for itself: on that same
154
+ token list the tier blits every visible row and costs 70MB of GPU
163
155
  cache for no frame-time change, while the retained row boundaries under it
164
156
  cost 2.4ms a frame. the discriminator is per-row draw density, so check
165
157
  `draw calls` in the same block before concluding a cache is earning its
@@ -198,11 +190,34 @@ not the installed Safari engine.
198
190
 
199
191
  every receipt must carry the exact page and frame URLs, browser user agent,
200
192
  harness-source identity, capture time, engine build identity and generation,
201
- runtime bundle receipt, system memory, runtime errors, and any partial failure.
193
+ runtime bundle receipt, sampled host capacity at run start, runtime errors, and
194
+ any partial failure. use the shared host-capacity sampler; do not record raw
195
+ `freemem()` or decide from macOS load average.
202
196
  partial artifacts remain inadmissible until every requested run number for that
203
197
  scenario exists with one harness, engine, seed, and viewport identity. browser
204
- comparisons use the same css viewport and backing scale; finite but different
205
- device pixel ratios are not comparable.
198
+ comparisons use the same root/target viewport tuple and backing scale. for a
199
+ wrapper or embedded surface, first prove the target has nonzero presented
200
+ geometry; a finite host viewport that responsively hides the target measures a
201
+ different surface. direct and embedded modes may intentionally have different
202
+ tuples, but browsers within one mode may not. finite but different device pixel
203
+ ratios are not comparable. if setup reloads only the child frame, replay the
204
+ parent's presentation control and wait for the new child to acknowledge it. a
205
+ selected parent toggle can outlive its child and is not proof of the child's
206
+ mode.
207
+ the profile intentionally blocks Graphite cache cleanup, so its cache snapshot
208
+ is not a terminal memory verdict. require a capture-local post-profile
209
+ compositor snapshot with before, after, and delta cleanup counters. an
210
+ over-limit profiled Context needs fresh submitted-work attempt and
211
+ successful-completion deltas; an over-limit profiled Recorder also needs a
212
+ fresh cleanup in that capture. classify Context and Recorder independently as
213
+ within-limit, locked-over-limit, or purgeable-over-limit; never compare their
214
+ aggregate, and never admit purgeable overage. a cache with zero purgeable bytes
215
+ can still hold a returned-resource queue: settled maintenance releases completed
216
+ Recorder resources, then budget enforcement evicts
217
+ least-recently-used overage before the next paint or resource insertion can
218
+ inherit the synchronous work. perpetual renderer
219
+ animation is not finite interaction demand; finite motion, readbacks, and
220
+ profiling must still block cleanup.
206
221
  the host-clock `displayFrames`/`timerFrames` and shell-link
207
222
  `displayPumps`/`timerPumps` counters prove whether the visible-rAF fallback
208
223
  engaged. preserve failed receipts instead of retrying them out of the data set.
@@ -255,9 +270,9 @@ invalidates the profile.
255
270
 
256
271
  ### the cadence block: frames that never happened
257
272
 
258
- work-per-painted-frame cannot see a frame that was never delivered — host
259
- rAF starved, shell worker descheduled, or the compositor's shared-frame wait
260
- resumed late. the report shows low avg and zero jank while the user sees stutter.
273
+ work-per-painted-frame cannot see a frame that was never delivered because a
274
+ worker was descheduled or its resilient clock fell back late. the report shows
275
+ low avg and zero jank while the user sees stutter.
261
276
  the `cadence` section of `perf shell stop` measures the delivery side and
262
277
  attributes a missing frame to its hop:
263
278
 
@@ -271,13 +286,14 @@ attributes a missing frame to its hop:
271
286
  - `idle breaks` — gaps >250ms, excluded from all three: demand-gated
272
287
  quiesce is legitimate, not starvation.
273
288
 
274
- the compositor `compositorHostFrameClock` separates display/timer ticks published into
275
- its shared latest-frame cell from ticks consumed during the acknowledged worker
276
- profile window. published timer ticks prove the renderer fallback won; sparse consumed ticks
277
- with low production time identifies worker scheduling starvation. many consumed
278
- ticks with sparse paints can be legitimate dirty-gating; correlate the frame
279
- series before treating it as a regression. shell-owned captures expose the
280
- corresponding `note.frameLink.sharedFrameClock` counters.
289
+ the compositor `compositorHostFrameClock` keeps its historical name and reports
290
+ display/timer ticks consumed by the compositor worker's resilient clock during
291
+ the acknowledged profile window. `published` is null because renderer-main no
292
+ longer publishes compositor frames. sparse consumed ticks with low production
293
+ time identifies worker scheduling starvation. many consumed ticks with sparse
294
+ paints can be legitimate dirty-gating; correlate the frame series before
295
+ treating it as a regression. shell cadence comes from its worker frame and paint
296
+ series.
281
297
 
282
298
  compare baseline vs contended captures of the same scripted window; also
283
299
  compare **painted-frame counts**, the crudest and most robust signal.
@@ -339,9 +355,9 @@ which tier *should* absorb your cost and prove whether it does.
339
355
  | layer compositor (`paint-boundary.ts`) | animating a child never re-records ancestors | replays >> records |
340
356
  | idle boundary pre-record | the first drag replays instead of recording; cold records happen while the surface is quiet | `pre-recorded: N boundaries at idle` in the report, and the first interaction's record cost near its warm cost |
341
357
  | damage rects (`damage-rect.ts`) | repaint clips to changed region (opaque-backdrop gated) | worst-frame render bounded during small updates |
342
- | raster tier | stable rows become GPU textures; scroll skips recording | compositor block: promotions > 0 once warm, blits ≈ one per visible row (uniswap token list: ~13.7/frame) |
358
+ | raster tier | stable rows become GPU textures; scroll skips recording | compositor block: promotions > 0 once warm, blits ≈ one per visible row on a dense feed |
343
359
  | flood guard, pointer coalescing, vsync hysteresis | message-storm and pump-thrash protection | msgs/s sane during gestures and at rest |
344
- | resilient renderer-main frame clock | visible rAF holes do not stall pointer coalescing or worker vsync | compositor `published` / `consumed` display and timer frames; shell `displayPumps` / `timerPumps` |
360
+ | worker-owned resilient frame clocks | renderer-main stalls do not stop shell or compositor motion | compositor consumed display/timer frames plus shell worker frame and paint cadence |
345
361
 
346
362
  there is no whole-screen scroll blit. one existed for a few hours in June
347
363
  2026 and was reverted (`6706f3a014`): it shifted a surface RECTANGLE, so a
@@ -374,5 +390,5 @@ tier fire on feeds came entirely from rejection counters.
374
390
  done means: a named hot path + the counter or split that indicts it + the
375
391
  same capture re-run showing the number moved + no new errors (`get errors`)
376
392
  + nothing torn down left running (close sims you opened, kill load loops).
377
- for engine changes, run the rnx gate (`bun run test:sootsim:gate`) and
378
- screenshot-stable surfaces you touched.
393
+ for engine changes, run the affected integration tests exposed by the installed
394
+ rnx version and capture stable screenshots of surfaces you touched.
package/src/app-url.ts CHANGED
@@ -24,9 +24,8 @@ export function isAppDeepLinkTarget(target: string): boolean {
24
24
  export async function openAppUrl(bridge: InspectBridge, target: string): Promise<string> {
25
25
  const normalized = normalizeAppUrlTarget(target)
26
26
  const result: unknown = await bridge.send({
27
- type: 'call',
28
- path: 'SootSim.bridges.test.openDeepLink',
29
- args: [normalized],
27
+ type: 'openUrl',
28
+ url: normalized,
30
29
  })
31
30
  if (
32
31
  result !== null &&
@@ -208,8 +208,10 @@ export function parseStateOptions(value: unknown): SimStateOptions {
208
208
  const input = recordValue(value, 'state')
209
209
  const route = optionalBoolean(input, 'route', 'state')
210
210
  const errors = optionalBoolean(input, 'errors', 'state')
211
+ const logs = optionalBoolean(input, 'logs', 'state')
211
212
  const depth = optionalNumber(input, 'depth', 'state')
212
213
  const errorLimit = optionalNumber(input, 'errorLimit', 'state')
214
+ const logLimit = optionalNumber(input, 'logLimit', 'state')
213
215
  const layers =
214
216
  input.layers === undefined ? undefined : stringValue(input.layers, 'state.layers')
215
217
  if (
@@ -225,9 +227,11 @@ export function parseStateOptions(value: unknown): SimStateOptions {
225
227
  tree: optionalBoolean(input, 'tree', 'state') ?? true,
226
228
  ...(route === undefined ? {} : { route }),
227
229
  ...(errors === undefined ? {} : { errors }),
230
+ ...(logs === undefined ? {} : { logs }),
228
231
  ...(depth === undefined ? {} : { depth }),
229
232
  ...(layers === undefined ? {} : { layers }),
230
233
  ...(errorLimit === undefined ? {} : { errorLimit }),
234
+ ...(logLimit === undefined ? {} : { logLimit }),
231
235
  }
232
236
  }
233
237
 
@@ -277,15 +281,22 @@ export function parseSemanticQuery(value: unknown): SimSemanticQuery {
277
281
  return validationFailure('query.kind', 'must be tree, find, or count')
278
282
  }
279
283
 
280
- export function parseSemanticResolveSelector(value: unknown): SimSemanticResolveSelector {
281
- const selector = recordValue(value, 'selector')
284
+ function semanticResolveSelector(
285
+ value: unknown,
286
+ path: string,
287
+ ): SimSemanticResolveSelector {
288
+ const selector = recordValue(value, path)
282
289
  if (selector.testID !== undefined) {
283
- return { testID: stringValue(selector.testID, 'selector.testID') }
290
+ return { testID: stringValue(selector.testID, `${path}.testID`) }
284
291
  }
285
292
  if (selector.text !== undefined) {
286
- return { text: stringValue(selector.text, 'selector.text') }
293
+ return { text: stringValue(selector.text, `${path}.text`) }
287
294
  }
288
- return validationFailure('selector', 'must select testID or text')
295
+ return validationFailure(path, 'must select testID or text')
296
+ }
297
+
298
+ export function parseSemanticResolveSelector(value: unknown): SimSemanticResolveSelector {
299
+ return semanticResolveSelector(value, 'selector')
289
300
  }
290
301
 
291
302
  export function parseSemanticWaitOptions(value: unknown): SemanticWaitOptions {
@@ -308,7 +319,7 @@ export function parseSemanticWaitOptions(value: unknown): SemanticWaitOptions {
308
319
  return {
309
320
  condition: {
310
321
  type,
311
- testID: stringValue(condition.testID, 'waitFor.condition.testID'),
322
+ selector: semanticResolveSelector(condition.selector, 'waitFor.condition.selector'),
312
323
  },
313
324
  ...(timeoutMs === undefined ? {} : { timeoutMs }),
314
325
  }
@@ -11,12 +11,16 @@
11
11
  import type {
12
12
  SootSimAgentActionDetail,
13
13
  SootSimExternalAppLifecycleOptions,
14
- } from '@sootsim/globals'
14
+ } from '@rnx/globals'
15
15
 
16
16
  export type BridgeCommandType =
17
17
  | 'evaluate'
18
18
  | 'call'
19
19
  | 'screenshot'
20
+ | 'capture'
21
+ | 'captureRegions'
22
+ | 'setAppearance'
23
+ | 'openUrl'
20
24
  | 'tap'
21
25
  | 'longPress'
22
26
  | 'keyboard'
@@ -48,6 +52,11 @@ export interface WsCommand {
48
52
  target?: SootSimAgentActionDetail['target']
49
53
  action?: string
50
54
  text?: string
55
+ appearance?: 'light' | 'dark'
56
+ url?: string
57
+ includeVisual?: boolean
58
+ includeFonts?: boolean
59
+ regions?: import('./capture-contract').RnxCaptureRegionRequest[]
51
60
  depth?: number
52
61
  // screenshot — which canvas layers to composite. omit for full (today's
53
62
  // default: everything including iOS chrome). 'tenant' excludes the shell
@@ -174,8 +183,8 @@ export interface PerformResult {
174
183
 
175
184
  export type SemanticWaitCondition =
176
185
  | { type: 'ready' }
177
- | { type: 'present'; testID: string }
178
- | { type: 'absent'; testID: string }
186
+ | { type: 'present'; selector: SimSemanticResolveSelector }
187
+ | { type: 'absent'; selector: SimSemanticResolveSelector }
179
188
 
180
189
  export interface SemanticWaitOptions {
181
190
  condition: SemanticWaitCondition
@@ -239,11 +248,22 @@ export interface SimStateOptions {
239
248
  tree?: boolean
240
249
  route?: boolean
241
250
  errors?: boolean
251
+ // include the bounded guest console tail. omitted by the ordinary state
252
+ // command so one-shot state remains compact; `rnx logs` requests it.
253
+ logs?: boolean
242
254
  // tree depth, matching the standalone `tree` command. default 5.
243
255
  depth?: number
244
256
  layers?: WsCommand['layers']
245
257
  // cap on recentErrors entries. default 20.
246
258
  errorLimit?: number
259
+ // cap on recentLogs entries. default and maximum 100.
260
+ logLimit?: number
261
+ }
262
+
263
+ export interface SimStateLogEntry {
264
+ level: string
265
+ text: string
266
+ at?: number
247
267
  }
248
268
 
249
269
  export interface SimStateResult {
@@ -251,7 +271,8 @@ export interface SimStateResult {
251
271
  screenshot?: unknown
252
272
  tree?: SimSemanticNode[]
253
273
  route?: { url?: string; screen?: string | null; stack?: string[] } | null
254
- recentErrors?: Array<{ level: string; text: string; at?: number }>
274
+ recentErrors?: SimStateLogEntry[]
275
+ recentLogs?: SimStateLogEntry[]
255
276
  keyboard?: { visible: boolean; mode?: string | null }
256
277
  capturedAt: number
257
278
  }