peachsim 0.1.1635 → 0.1.1640

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 (129) hide show
  1. package/README.md +5 -1
  2. package/cli/app-config.ts +14 -5
  3. package/cli/app-stubs.ts +62 -13
  4. package/cli/bridge-flow-runner.ts +2 -2
  5. package/cli/cloud-client.ts +60 -40
  6. package/cli/commands/doctor.ts +13 -5
  7. package/cli/commands/flow.ts +1 -1
  8. package/cli/commands/inspect/shared.ts +9 -4
  9. package/cli/commands/inspect.ts +65 -47
  10. package/cli/commands/preview.ts +1 -1
  11. package/cli/commands/record.ts +97 -47
  12. package/cli/commands/swift.ts +9 -3
  13. package/cli/commands/upload.ts +48 -42
  14. package/cli/drivers/playwright-sim-host.ts +65 -42
  15. package/cli/main.ts +1 -1
  16. package/cli/outbound-endpoints.ts +7 -5
  17. package/cli/pause-aware-timeout.ts +4 -0
  18. package/cli/ws-bridge.ts +19 -10
  19. package/detox/attempt-events.cjs +62 -5
  20. package/detox/attempt-events.d.cts +7 -0
  21. package/detox/expectations.ts +7 -3
  22. package/detox/index.ts +121 -47
  23. package/detox/motion-change.cjs +170 -21
  24. package/detox/motion-change.d.cts +32 -0
  25. package/detox/navigation-lifecycle.ts +35 -3
  26. package/dist-lib/agent-daemon-client.cjs +1 -1
  27. package/dist-lib/agent-events.cjs +1 -1
  28. package/dist-lib/attached-projects.cjs +1 -1
  29. package/dist-lib/auth/shared-session.cjs +1 -1
  30. package/dist-lib/backend-origin.cjs +8 -7
  31. package/dist-lib/beta.cjs +1 -1
  32. package/dist-lib/beta.mjs +1 -1
  33. package/dist-lib/bridge-constants.cjs +1 -1
  34. package/dist-lib/bridge-contract-input.cjs +1 -1
  35. package/dist-lib/bridge-contract-input.mjs +1 -1
  36. package/dist-lib/bridge-contract.cjs +1 -1
  37. package/dist-lib/bridge-contract.mjs +1 -1
  38. package/dist-lib/bundle-channel.cjs +4038 -1983
  39. package/dist-lib/capture-contract.cjs +1 -1
  40. package/dist-lib/capture-contract.mjs +1 -1
  41. package/dist-lib/cloud-contract.cjs +1 -1
  42. package/dist-lib/cloud-contract.mjs +1 -1
  43. package/dist-lib/cloud-test-args.cjs +1 -1
  44. package/dist-lib/cloud-test-args.mjs +1 -1
  45. package/dist-lib/cloud.cjs +1 -1
  46. package/dist-lib/cloud.mjs +1 -1
  47. package/dist-lib/config.cjs +1 -1
  48. package/dist-lib/config.mjs +1 -1
  49. package/dist-lib/detox/index.cjs +509 -86
  50. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  51. package/dist-lib/expo-swift-bundle.cjs +3551 -1574
  52. package/dist-lib/expo-swift-metadata.cjs +9 -7
  53. package/dist-lib/expo-swift-metadata.mjs +9 -7
  54. package/dist-lib/home-paths.cjs +1 -1
  55. package/dist-lib/host/bridge-host.cjs +43633 -2635
  56. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  57. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  58. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  59. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  60. package/dist-lib/host/websocket-proxy.cjs +1 -1
  61. package/dist-lib/index.cjs +2748 -128
  62. package/dist-lib/jump-to-source-babel.cjs +1 -1
  63. package/dist-lib/jump-to-source-native.cjs +1 -1
  64. package/dist-lib/kotlin-host.cjs +1 -1
  65. package/dist-lib/menu.cjs +1 -1
  66. package/dist-lib/menu.mjs +1 -1
  67. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  68. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  69. package/dist-lib/metro-production-bundle.cjs +1 -1
  70. package/dist-lib/metro-production-bundle.mjs +1 -1
  71. package/dist-lib/metro.cjs +2547 -83
  72. package/dist-lib/native-app-identity-project.cjs +237 -50
  73. package/dist-lib/native-app-identity-project.mjs +237 -50
  74. package/dist-lib/native-app-identity.cjs +29 -30
  75. package/dist-lib/native-app-identity.mjs +29 -30
  76. package/dist-lib/native-firebase-metadata.cjs +1 -1
  77. package/dist-lib/native-firebase-metadata.mjs +1 -1
  78. package/dist-lib/plist.cjs +1 -1
  79. package/dist-lib/plist.mjs +1 -1
  80. package/dist-lib/profiles.cjs +1 -1
  81. package/dist-lib/public-brand.cjs +1 -1
  82. package/dist-lib/react-native-host-modules.cjs +2 -2
  83. package/dist-lib/react-native-host-modules.mjs +2 -2
  84. package/dist-lib/scripts/dev-server-scanner.cjs +8 -6
  85. package/dist-lib/sdk.cjs +1 -1
  86. package/dist-lib/sdk.mjs +1 -1
  87. package/dist-lib/skills.cjs +65403 -22739
  88. package/dist-lib/swift-compile.cjs +93 -32
  89. package/dist-lib/swift-compile.mjs +93 -32
  90. package/dist-lib/swift-package.cjs +1 -1
  91. package/dist-lib/swift-package.mjs +1 -1
  92. package/dist-lib/swift-submit.cjs +7 -5
  93. package/dist-lib/swift-submit.mjs +7 -5
  94. package/dist-lib/swift-toolchain.cjs +316 -41
  95. package/dist-lib/swift.cjs +1 -1
  96. package/dist-lib/vite-swift.cjs +7335 -5432
  97. package/dist-lib/vite.cjs +2857 -12
  98. package/dist-lib/worklet-executable.cjs +14 -2
  99. package/dist-lib/worklet-executable.mjs +14 -2
  100. package/package.json +1 -1
  101. package/src/backend-origin.ts +13 -6
  102. package/src/expo-swift-host.ts +5 -2
  103. package/src/expoKotlinContract.ts +4 -7
  104. package/src/expoSwiftContract.ts +4 -6
  105. package/src/host/staged-host-files.ts +9 -0
  106. package/src/native-app-identity-project.ts +1 -4
  107. package/src/native-app-identity.ts +1 -4
  108. package/src/nativeAppIconContract.ts +1 -4
  109. package/src/output-lock-stdio.ts +36 -0
  110. package/src/react-native-bridge-modules.ts +21 -6
  111. package/src/react-native-host-modules.ts +4 -1
  112. package/src/settings-bundle.ts +1 -4
  113. package/src/swift-compile.ts +91 -33
  114. package/src/swift-distribution.lock.json +22 -12
  115. package/src/swift-distribution.ts +103 -4
  116. package/src/swift-host.ts +89 -26
  117. package/src/swift-icu.ts +16 -10
  118. package/src/swift-objc-compile.ts +355 -0
  119. package/src/swift-objc-sdk.ts +689 -18
  120. package/src/swift-objc-standin.ts +126 -14
  121. package/src/swift-package-manifest.ts +39 -1
  122. package/src/swift-packages.ts +35 -21
  123. package/src/swift-standin-emit.ts +40 -7
  124. package/src/swift-standins.ts +404 -138
  125. package/src/swift-submit.ts +3 -5
  126. package/src/swift-toolchain.ts +378 -46
  127. package/src/worker-system-chunks.ts +11 -12
  128. package/src/worklet-executable.ts +19 -1
  129. package/src/xcode-project.ts +270 -30
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1640 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1147,6 +1147,32 @@ var require_lib = __commonJS({
1147
1147
  }
1148
1148
  });
1149
1149
 
1150
+ // ../peach-engine/src/platform/adapter.ts
1151
+ var IOS_ADAPTER, ANDROID_ADAPTER, RUNTIME_PLATFORM_ADAPTERS;
1152
+ var init_adapter = __esm({
1153
+ "../peach-engine/src/platform/adapter.ts"() {
1154
+ "use strict";
1155
+ IOS_ADAPTER = Object.freeze({
1156
+ id: "ios",
1157
+ displayName: "iOS",
1158
+ isIOS: true,
1159
+ isAndroid: false,
1160
+ select: (registry) => registry.ios
1161
+ });
1162
+ ANDROID_ADAPTER = Object.freeze({
1163
+ id: "android",
1164
+ displayName: "Android",
1165
+ isIOS: false,
1166
+ isAndroid: true,
1167
+ select: (registry) => registry.android
1168
+ });
1169
+ RUNTIME_PLATFORM_ADAPTERS = Object.freeze({
1170
+ ios: IOS_ADAPTER,
1171
+ android: ANDROID_ADAPTER
1172
+ });
1173
+ }
1174
+ });
1175
+
1150
1176
  // detox/motion-change.cjs
1151
1177
  var require_motion_change = __commonJS({
1152
1178
  "detox/motion-change.cjs"(exports2, module2) {
@@ -1220,29 +1246,48 @@ var require_motion_change = __commonJS({
1220
1246
  var SETTLE_BLIP_MAXIMUM_CHANGE_PERCENT = 0.6;
1221
1247
  var GESTURE_MOTION_MIN_TRANSITIONS = 120;
1222
1248
  var GESTURE_TRAVEL_RATIO_RANGE = Object.freeze({ min: 0.8, max: 1.25 });
1249
+ var GESTURE_QUARTER_TRAVEL_RATIO_RANGE = Object.freeze({ min: 0.5, max: 2 });
1223
1250
  function evaluateMotionTransitions(transitions, oracleTarget) {
1224
1251
  const key = (transition) => `${transition.fromLabel}\0${transition.toLabel}`;
1225
1252
  const oracle = transitions.filter((transition) => transition.target === oracleTarget);
1226
1253
  const sootsim = transitions.filter((transition) => transition.target === "sootsim");
1227
- const sootsimByKey = new Map(sootsim.map((transition) => [key(transition), transition]));
1254
+ const paired = oracle.length === sootsim.length && new Set(oracle.map(key)).size === oracle.length && oracle.every((transition, index) => key(transition) === key(sootsim[index]));
1228
1255
  const measured = transitions.every(
1229
1256
  (transition) => transition.dimensionsMatch && transition.changePercent !== null && transition.changePixels !== null && transition.totalPixels !== null
1230
1257
  );
1231
- const moving = oracle.filter((transition) => transition.passedMinimum);
1232
1258
  const travel = (list) => list.filter(
1233
1259
  (transition) => transition.changePercent >= PAIRED_MOTION_MINIMUM_CHANGE_PERCENT
1234
1260
  ).reduce((total, transition) => total + transition.changePercent, 0);
1235
- const sootsimMovesNear = (index) => [index - 1, index, index + 1].some(
1236
- (neighbour) => sootsim[neighbour]?.passedMinimum === true
1261
+ const movesNear = (list, index) => [index - 1, index, index + 1].some((neighbour) => list[neighbour]?.passedMinimum === true);
1262
+ const pairedChange = (transition) => transition.changePercent >= PAIRED_MOTION_MINIMUM_CHANGE_PERCENT;
1263
+ const settleBlip = (list) => {
1264
+ const index = list.findLastIndex(pairedChange);
1265
+ return index >= 0 && list[index].changePercent < SETTLE_BLIP_MAXIMUM_CHANGE_PERCENT && !(list[index - 1]?.passedMinimum ?? true) ? index : -1;
1266
+ };
1267
+ const oracleBlip = settleBlip(oracle);
1268
+ const sootsimBlip = settleBlip(sootsim);
1269
+ const moving = oracle.filter(
1270
+ (transition, index) => pairedChange(transition) && index !== oracleBlip
1237
1271
  );
1238
- const isSettleBlip = (transition, index) => transition.changePercent < SETTLE_BLIP_MAXIMUM_CHANGE_PERCENT && !(oracle[index - 1]?.passedMinimum ?? true);
1239
1272
  const within = (actual, expected, range) => actual >= expected * range.min && actual <= expected * range.max;
1240
- const follows = oracle.length >= GESTURE_MOTION_MIN_TRANSITIONS ? within(travel(sootsim), travel(oracle), GESTURE_TRAVEL_RATIO_RANGE) : oracle.every(
1241
- (transition, index) => !transition.passedMinimum || transition.changePercent < PAIRED_MOTION_MINIMUM_CHANGE_PERCENT || isSettleBlip(transition, index) || sootsimByKey.has(key(transition)) && sootsimMovesNear(index)
1273
+ const quarter = (list, index) => list.slice(
1274
+ Math.floor(list.length * index / 4),
1275
+ Math.floor(list.length * (index + 1) / 4)
1276
+ );
1277
+ const follows = oracle.length >= GESTURE_MOTION_MIN_TRANSITIONS ? within(travel(sootsim), travel(oracle), GESTURE_TRAVEL_RATIO_RANGE) && [0, 1, 2, 3].every(
1278
+ (index) => within(
1279
+ travel(quarter(sootsim, index)),
1280
+ travel(quarter(oracle, index)),
1281
+ GESTURE_QUARTER_TRAVEL_RATIO_RANGE
1282
+ )
1283
+ ) : oracle.every(
1284
+ (transition, index) => !pairedChange(transition) || index === oracleBlip || movesNear(sootsim, index)
1285
+ ) && sootsim.every(
1286
+ (transition, index) => !pairedChange(transition) || index === sootsimBlip || movesNear(oracle, index)
1242
1287
  );
1243
1288
  return {
1244
1289
  moving: moving.length,
1245
- passed: measured && oracle.length > 0 && sootsim.length === oracle.length && moving.length > 0 && follows
1290
+ passed: measured && oracle.length > 0 && paired && moving.length > 0 && follows
1246
1291
  };
1247
1292
  }
1248
1293
  var DISPLAY_LINK_INTERVAL_MS = 1e3 / 60;
@@ -1396,8 +1441,78 @@ var require_motion_change = __commonJS({
1396
1441
  }
1397
1442
  return { mode: "playing", anchorMs: anchor.atMs, holds };
1398
1443
  }
1444
+ var PUBLICATION_DENSE_LADDER_MIN_STAGES = 10;
1445
+ var PUBLICATION_MIN_INTERMEDIATE_PASSES = 2;
1446
+ function motionCycleOnsets(event, eventPath) {
1447
+ if (!Array.isArray(event.anchors)) return [];
1448
+ const cycles = event.anchors.filter(
1449
+ (anchor) => typeof anchor === "object" && anchor !== null && anchor.cycle === true
1450
+ );
1451
+ if (cycles.length === 0) return [];
1452
+ const finite = (value, label) => {
1453
+ if (typeof value !== "number" || !Number.isFinite(value))
1454
+ throw new Error(`motion event cycle ${label} has no onset time: ${eventPath}`);
1455
+ return value;
1456
+ };
1457
+ const android = event.captureMode === "android-screenrecord-sequence";
1458
+ const peach = typeof event.driveStart === "object" && event.driveStart !== null;
1459
+ const basisMs = peach ? finite(event.driveStart.virtualTimestampMs, "transition") : android ? finite(event.triggerProducerSeconds, "transition") * 1e3 : finite(event.triggerInVideoSec, "transition") * 1e3;
1460
+ return [
1461
+ { id: "transition", atMs: basisMs },
1462
+ ...cycles.map((anchor) => {
1463
+ if (typeof anchor.id !== "string")
1464
+ throw new Error(`motion event cycle anchor has no id: ${eventPath}`);
1465
+ const atMs = peach ? finite(anchor.virtualTimestampMs, anchor.id) : android ? finite(anchor.producerSeconds, anchor.id) * 1e3 : finite(anchor.detection?.atSec, anchor.id) * 1e3;
1466
+ return { id: anchor.id, atMs };
1467
+ })
1468
+ ];
1469
+ }
1470
+ var MOTION_CYCLE_PERIOD_TOLERANCE_MS = 1e3 / 60;
1471
+ function motionCyclePeriods(reference, sootsim) {
1472
+ const ids = (onsets) => onsets.map((onset) => onset.id).join(",");
1473
+ if (ids(reference) !== ids(sootsim))
1474
+ throw new Error(
1475
+ `motion cycle anchors differ between lanes: reference [${ids(reference)}], sootsim [${ids(sootsim)}]`
1476
+ );
1477
+ return reference.slice(1).map((onset, index) => {
1478
+ const referenceMs = onset.atMs - reference[index].atMs;
1479
+ const sootsimMs = sootsim[index + 1].atMs - sootsim[index].atMs;
1480
+ return {
1481
+ id: onset.id,
1482
+ passed: Math.abs(referenceMs - sootsimMs) <= MOTION_CYCLE_PERIOD_TOLERANCE_MS,
1483
+ referenceMs,
1484
+ sootsimMs
1485
+ };
1486
+ });
1487
+ }
1488
+ function evaluateMotionPublication(stages, transitions, oracleTarget, cycleOnsets) {
1489
+ const cyclePeriods = motionCyclePeriods(cycleOnsets.reference, cycleOnsets.sootsim);
1490
+ const intermediates = stages.slice(1, -1);
1491
+ const intermediatePassed = intermediates.filter((stage) => stage.passedThreshold).length;
1492
+ const stagesByLabel = new Map(stages.map((stage) => [stage.label, stage]));
1493
+ const oracleMovingInto = new Set(
1494
+ transitions.filter(
1495
+ (transition) => transition.target === oracleTarget && (transition.changePercent >= PAIRED_MOTION_MINIMUM_CHANGE_PERCENT || transition.changePixels > 0 && stagesByLabel.get(transition.toLabel)?.oracleFromFirstPercent >= PAIRED_MOTION_MINIMUM_CHANGE_PERCENT && stagesByLabel.get(transition.toLabel)?.oracleFromLastPercent >= PAIRED_MOTION_MINIMUM_CHANGE_PERCENT)
1496
+ ).map((transition) => transition.toLabel)
1497
+ );
1498
+ const movingPassed = intermediates.filter(
1499
+ (stage) => stage.passedThreshold && oracleMovingInto.has(stage.label)
1500
+ ).length;
1501
+ const needsIntermediates = stages.length >= PUBLICATION_DENSE_LADDER_MIN_STAGES;
1502
+ const first = stages[0];
1503
+ const last = stages[stages.length - 1];
1504
+ return {
1505
+ passed: stages.length > 0 && first.passedThreshold && last.passedThreshold && (!needsIntermediates || movingPassed >= PUBLICATION_MIN_INTERMEDIATE_PASSES) && cyclePeriods.every((period) => period.passed) && evaluateMotionTransitions(transitions, oracleTarget).passed,
1506
+ cyclePeriods,
1507
+ intermediateCount: intermediates.length,
1508
+ intermediatePassed,
1509
+ movingPassed
1510
+ };
1511
+ }
1399
1512
  module2.exports = {
1400
1513
  MOTION_CHANGE_DEFINITION: MOTION_CHANGE_DEFINITION2,
1514
+ evaluateMotionPublication,
1515
+ motionCycleOnsets,
1401
1516
  measureMotionChange: measureMotionChange2,
1402
1517
  measureProofMotionChange,
1403
1518
  evaluateMotionTransitions,
@@ -1757,6 +1872,99 @@ var require_native_frame_receipt = __commonJS({
1757
1872
  };
1758
1873
  }
1759
1874
  module2.exports.nativeUIKitInputReceipt = nativeUIKitInputReceipt;
1875
+ function nativeUIFrameClockReceipt(bytes) {
1876
+ if (!Buffer.isBuffer(bytes))
1877
+ throw new Error("ui frame clock receipt must preserve source bytes");
1878
+ const rows = bytes.toString("utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
1879
+ if (rows.at(-1)?.kind !== "stopped")
1880
+ throw new Error("ui frame clock receipt was not closed by its recorder");
1881
+ const ticks = [];
1882
+ const workletsTicks = [];
1883
+ for (const row of rows.slice(0, -1)) {
1884
+ if (row.kind === "worklets-frame") {
1885
+ if (!Number.isFinite(row.timestampSeconds) || row.timestampSeconds < 0)
1886
+ throw new Error("invalid worklets frame clock row");
1887
+ if (workletsTicks.length > 0 && row.timestampSeconds <= workletsTicks.at(-1).timestampSeconds)
1888
+ throw new Error("worklets frame clock timestamps must increase");
1889
+ workletsTicks.push({ timestampSeconds: row.timestampSeconds });
1890
+ continue;
1891
+ }
1892
+ if (row.kind !== "ui-frame" || !Number.isFinite(row.targetTimestampSeconds) || !Number.isFinite(row.timestampSeconds) || !(row.durationSeconds > 0))
1893
+ throw new Error("invalid ui frame clock row");
1894
+ if (ticks.length > 0 && row.targetTimestampSeconds <= ticks.at(-1).targetTimestampSeconds)
1895
+ throw new Error("ui frame clock target timestamps must increase");
1896
+ ticks.push({
1897
+ targetTimestampSeconds: row.targetTimestampSeconds,
1898
+ timestampSeconds: row.timestampSeconds,
1899
+ durationSeconds: row.durationSeconds
1900
+ });
1901
+ }
1902
+ if (ticks.length < 2)
1903
+ throw new Error("ui frame clock receipt holds fewer than two ticks");
1904
+ return {
1905
+ domain: "ca-media-time-seconds",
1906
+ source: "cadisplaylink-target-timestamp",
1907
+ sourceReceiptSha256: createHash("sha256").update(bytes).digest("hex"),
1908
+ ticks,
1909
+ workletsTicks
1910
+ };
1911
+ }
1912
+ module2.exports.nativeUIFrameClockReceipt = nativeUIFrameClockReceipt;
1913
+ function pairedNativeWorkletFrameClock(root, lifecycle) {
1914
+ const { boundText } = require_attempt_events();
1915
+ if (!lifecycle || typeof lifecycle !== "object" || Array.isArray(lifecycle))
1916
+ throw new Error("paired worklets clock needs a retained native lifecycle");
1917
+ const receipt = lifecycle.uiFrameClock;
1918
+ if (!receipt)
1919
+ throw new Error("paired worklets clock needs a retained native UI clock receipt");
1920
+ const inputReceipt = lifecycle.deliveredUIKitInput;
1921
+ if (!inputReceipt)
1922
+ throw new Error("paired worklets clock needs a delivered UIKit touch-up witness");
1923
+ const clock = nativeUIFrameClockReceipt(
1924
+ Buffer.from(
1925
+ boundText(
1926
+ root,
1927
+ receipt.sourceReceipt,
1928
+ receipt.sourceReceiptSha256,
1929
+ "native UI clock receipt"
1930
+ )
1931
+ )
1932
+ );
1933
+ const input = nativeUIKitInputReceipt(
1934
+ Buffer.from(
1935
+ boundText(
1936
+ root,
1937
+ inputReceipt.sourceReceipt,
1938
+ inputReceipt.sourceReceiptSha256,
1939
+ "delivered UIKit input receipt"
1940
+ )
1941
+ )
1942
+ );
1943
+ const up = input.events.find((event) => event.phase === 3);
1944
+ if (input.status !== "witnessed" || !up)
1945
+ throw new Error("paired worklets clock needs a delivered UIKit touch-up witness");
1946
+ const startIndex = clock.ticks.findIndex(
1947
+ (tick2) => tick2.timestampSeconds > up.deliveredAtSeconds
1948
+ );
1949
+ if (startIndex < 1)
1950
+ throw new Error("native UI clock does not bracket delivered touch-up");
1951
+ const timestampsSeconds = clock.ticks.slice(startIndex - 1).map((tick2) => tick2.targetTimestampSeconds);
1952
+ return {
1953
+ provenance: {
1954
+ sourceReceipt: receipt.sourceReceipt,
1955
+ sourceReceiptSha256: receipt.sourceReceiptSha256,
1956
+ inputReceipt: inputReceipt.sourceReceipt,
1957
+ inputReceiptSha256: inputReceipt.sourceReceiptSha256,
1958
+ deliveredTouchUpSeconds: up.deliveredAtSeconds,
1959
+ startIndex,
1960
+ alignment: "inferred-next-ui-frame-after-delivered-touch-up",
1961
+ frameCount: timestampsSeconds.length - 1
1962
+ },
1963
+ // the trigger reads the previous frame; the first delivery reads startIndex.
1964
+ timestampsSeconds
1965
+ };
1966
+ }
1967
+ module2.exports.pairedNativeWorkletFrameClock = pairedNativeWorkletFrameClock;
1760
1968
  async function encodeNativeFramebufferMovie(rawDir, videoPath) {
1761
1969
  const fs3 = require("node:fs");
1762
1970
  const path5 = require("node:path");
@@ -1783,6 +1991,27 @@ var require_native_frame_receipt = __commonJS({
1783
1991
  });
1784
1992
  if (times.some((value, index) => index > 0 && value <= times[index - 1]))
1785
1993
  throw new Error("nonmonotonic native source arrivals");
1994
+ const reads = rows.map((row) => ({
1995
+ callbackIndex: row.callbackIndex,
1996
+ seedBeforeRead: row.source?.seedBeforeRead,
1997
+ seedAfterRead: row.source?.seedAfterRead,
1998
+ arrival: BigInt(row.consumerArrival.machTicks)
1999
+ }));
2000
+ for (const [index, read] of reads.entries()) {
2001
+ if (!Number.isSafeInteger(read.seedBeforeRead) || !Number.isSafeInteger(read.seedAfterRead))
2002
+ throw new Error(`native callback ${read.callbackIndex} has no surface seed`);
2003
+ const previous = reads[index - 1];
2004
+ if (!previous) continue;
2005
+ const lost = read.seedBeforeRead - previous.seedAfterRead - 1;
2006
+ if (lost > 1) {
2007
+ const gapMs = Number(
2008
+ (read.arrival - previous.arrival) * BigInt(firstClock.timebaseNumer) / BigInt(firstClock.timebaseDenom)
2009
+ ) / 1e6;
2010
+ throw new Error(
2011
+ `native recording skipped ${lost} surface updates between callbacks ${previous.callbackIndex} and ${read.callbackIndex} (surface seed ${previous.seedAfterRead} to ${read.seedBeforeRead}, ${gapMs.toFixed(1)}ms between arrivals): no captured frame shows them`
2012
+ );
2013
+ }
2014
+ }
1786
2015
  function selectTime(start, end) {
1787
2016
  if (end - start === 1) return String(times[start]);
1788
2017
  const middle = Math.floor((start + end) / 2);
@@ -1979,6 +2208,13 @@ var require_attempt_events = __commonJS({
1979
2208
  throw new Error("cadence evidence path escapes its root");
1980
2209
  return resolved;
1981
2210
  }
2211
+ function boundText(root, relative, sha256, label) {
2212
+ const bytes = fs3.readFileSync(proofBoundPath(root, relative));
2213
+ if (proofByteHash(bytes) !== sha256)
2214
+ throw new Error(`${label} does not match its recorded hash`);
2215
+ return bytes.toString("utf8");
2216
+ }
2217
+ module2.exports.boundText = boundText;
1982
2218
  function authenticateDiscreteFrameAsset(root, declaration, calibrate = false) {
1983
2219
  const cadence = parseDiscreteFrameCadence(declaration);
1984
2220
  const fixture = fs3.readFileSync(proofBoundPath(root, cadence.asset.fixturePath));
@@ -2186,8 +2422,18 @@ var require_attempt_events = __commonJS({
2186
2422
  )
2187
2423
  ))
2188
2424
  throw new Error("native cadence has no source-matched framebuffer consumer");
2189
- const frames = fs3.readFileSync(proofBoundPath(root, native.receipt), "utf8").trim().split("\n").map(JSON.parse);
2190
- const media = fs3.readFileSync(proofBoundPath(root, native.mediaFrames), "utf8").trim().split("\n").map(JSON.parse);
2425
+ const frames = boundText(
2426
+ root,
2427
+ native.receipt,
2428
+ native.receiptSha256,
2429
+ "native cadence source receipt"
2430
+ ).trim().split("\n").map(JSON.parse);
2431
+ const media = boundText(
2432
+ root,
2433
+ native.mediaFrames,
2434
+ native.mediaFramesSha256,
2435
+ "native cadence presentation census"
2436
+ ).trim().split("\n").map(JSON.parse);
2191
2437
  if (frames.length !== decoded.observations.length || media.length !== frames.length)
2192
2438
  throw new Error("native cadence acquisition and presentation census differ");
2193
2439
  const {
@@ -2195,10 +2441,25 @@ var require_attempt_events = __commonJS({
2195
2441
  requireNativeRawFrame
2196
2442
  } = require_native_frame_receipt();
2197
2443
  const rawRoot = path5.dirname(proofBoundPath(root, native.receipt));
2444
+ const superseded = new Set(
2445
+ boundText(
2446
+ root,
2447
+ native.superseded,
2448
+ native.supersededSha256,
2449
+ "native cadence superseded receipt"
2450
+ ).split("\n").filter(Boolean).map((line) => {
2451
+ const row = JSON.parse(line);
2452
+ if (!Number.isSafeInteger(row.callbackIndex) || row.seedBeforeRead === row.seedAfterRead)
2453
+ throw new Error("native superseded receipt is not an overwritten read");
2454
+ return row.callbackIndex;
2455
+ })
2456
+ );
2198
2457
  const origin = frames[0].consumerArrival;
2458
+ let expectedCallback = 0;
2199
2459
  for (const [index, frame] of frames.entries()) {
2460
+ while (superseded.delete(expectedCallback)) expectedCallback++;
2200
2461
  const clock = frame.consumerArrival;
2201
- if (frame.callbackIndex !== index || media[index].sourceFrameIndex !== index || !clock || clock.domain !== "host-mach-absolute-time" || clock.domain !== origin.domain || clock.timebaseNumer !== origin.timebaseNumer || clock.timebaseDenom !== origin.timebaseDenom || !Number.isSafeInteger(clock.timebaseNumer) || clock.timebaseNumer <= 0 || !Number.isSafeInteger(clock.timebaseDenom) || clock.timebaseDenom <= 0)
2462
+ if (frame.callbackIndex !== expectedCallback++ || media[index].sourceFrameIndex !== index || !clock || clock.domain !== "host-mach-absolute-time" || clock.domain !== origin.domain || clock.timebaseNumer !== origin.timebaseNumer || clock.timebaseDenom !== origin.timebaseDenom || !Number.isSafeInteger(clock.timebaseNumer) || clock.timebaseNumer <= 0 || !Number.isSafeInteger(clock.timebaseDenom) || clock.timebaseDenom <= 0)
2202
2463
  throw new Error("native cadence source census has invalid clock or order");
2203
2464
  const pts = (BigInt(clock.machTicks) - BigInt(origin.machTicks)) * BigInt(clock.timebaseNumer) / BigInt(clock.timebaseDenom);
2204
2465
  if (pts < 0n || pts !== BigInt(media[index].ptsNanoseconds) || Math.abs(Number(pts) / 1e6 - decoded.observations[index].atMs) > 1e-3)
@@ -2211,6 +2472,9 @@ var require_attempt_events = __commonJS({
2211
2472
  )
2212
2473
  );
2213
2474
  }
2475
+ while (superseded.delete(expectedCallback)) expectedCallback++;
2476
+ if (superseded.size)
2477
+ throw new Error("native superseded receipts fall outside the callback order");
2214
2478
  }
2215
2479
  function readDiscreteFrameReceipt(root, sourceRoot, reference, expected) {
2216
2480
  if (expected.calibration !== true) assertNormalProofAttempt(root);
@@ -2444,9 +2708,20 @@ var require_attempt_events = __commonJS({
2444
2708
  const life = JSON.parse(
2445
2709
  fs3.readFileSync(proofBoundPath(root, receipt.lifecycle.path), "utf8")
2446
2710
  );
2711
+ for (const [label, auxiliary] of [
2712
+ ["ui frame clock", life.uiFrameClock],
2713
+ ["delivered input", life.deliveredUIKitInput]
2714
+ ]) {
2715
+ if (!auxiliary) continue;
2716
+ const bytes = fs3.readFileSync(proofBoundPath(root, auxiliary.sourceReceipt));
2717
+ if (proofByteHash(bytes) !== auxiliary.sourceReceiptSha256)
2718
+ throw new Error(`native ${label} receipt does not match its recorded hash`);
2719
+ paths.add(auxiliary.sourceReceipt);
2720
+ }
2447
2721
  const native = life.nativeFramebuffer;
2448
2722
  if (native) {
2449
2723
  paths.add(native.receipt);
2724
+ paths.add(native.superseded);
2450
2725
  paths.add(native.mediaFrames);
2451
2726
  const rows = fs3.readFileSync(proofBoundPath(root, native.receipt), "utf8").trim().split("\n").map(JSON.parse);
2452
2727
  for (const row of rows) {
@@ -6348,6 +6623,11 @@ function sameNativeAccessibilityShape(a, b, bounds) {
6348
6623
  return true;
6349
6624
  }
6350
6625
 
6626
+ // src/is-record.ts
6627
+ function isRecord(value) {
6628
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6629
+ }
6630
+
6351
6631
  // cli/commands/inspect/core.ts
6352
6632
  function layoutBoundFontFamily(style) {
6353
6633
  const raw = typeof style === "object" && style !== null ? style.fontFamily : void 0;
@@ -7362,9 +7642,12 @@ var AUTOMATION_SIM_IDLE_REAP_TTL_MS = 5 * 6e4;
7362
7642
  // src/home-paths.ts
7363
7643
  var DAEMON_LOCKFILE_MAX_BYTES = 16 * 1024;
7364
7644
 
7365
- // ../peach-engine/src/tenant-bundle-mode.ts
7645
+ // ../peach-engine/src/preview/bundle-layout.ts
7366
7646
  var import_metro_production_bundle = require("peachsim/metro-production-bundle");
7367
7647
 
7648
+ // ../peach-engine/src/tenant-bundle-mode.ts
7649
+ var import_metro_production_bundle2 = require("peachsim/metro-production-bundle");
7650
+
7368
7651
  // ../peach-engine/src/tenant-runtime-environment.ts
7369
7652
  function readBrowserLocation() {
7370
7653
  try {
@@ -7843,7 +8126,7 @@ var PEACH_UPSTREAM_MODULES = {
7843
8126
  };
7844
8127
 
7845
8128
  // ../peach-engine/src/metro-fingerprint.ts
7846
- var import_metro_production_bundle2 = require("peachsim/metro-production-bundle");
8129
+ var import_metro_production_bundle3 = require("peachsim/metro-production-bundle");
7847
8130
  var import_react_native_host_modules = require("peachsim/react-native-host-modules");
7848
8131
 
7849
8132
  // src/cloud-contract.ts
@@ -10636,6 +10919,10 @@ function setPauseAwareTimeout(fn, budgetMs, hostPid) {
10636
10919
  }
10637
10920
  return () => {
10638
10921
  timers.delete(timer);
10922
+ if (timers.size === 0 && poll) {
10923
+ clearInterval(poll);
10924
+ poll = void 0;
10925
+ }
10639
10926
  };
10640
10927
  }
10641
10928
 
@@ -10674,7 +10961,10 @@ var BRIDGE_VALUE_FLAGS_TABLE = [
10674
10961
  {
10675
10962
  names: ["--timeout"],
10676
10963
  apply: (state, value) => {
10677
- if (value !== void 0) state.commandTimeoutMs = Number(value);
10964
+ if (value !== void 0) {
10965
+ state.commandTimeoutMs = Number(value);
10966
+ state.explicitTimeout = true;
10967
+ }
10678
10968
  }
10679
10969
  },
10680
10970
  {
@@ -10814,7 +11104,7 @@ function createBridge(wsPort, opts = {}) {
10814
11104
  return {
10815
11105
  plane: "local",
10816
11106
  async send(cmd, callOpts) {
10817
- const effectiveTimeoutMs = callOpts?.timeoutMs ?? commandTimeoutMs;
11107
+ const effectiveTimeoutMs = callOpts?.timeoutMs ?? (opts.explicitTimeout ? commandTimeoutMs : callOpts?.defaultTimeoutMs) ?? commandTimeoutMs;
10818
11108
  const sendOnce = async (defaultSimId, cmdOverride) => {
10819
11109
  await ready;
10820
11110
  if (terminalError) throw terminalError;
@@ -10827,11 +11117,7 @@ function createBridge(wsPort, opts = {}) {
10827
11117
  const clearTimeoutFn = setPauseAwareTimeout(
10828
11118
  () => {
10829
11119
  pending.delete(id);
10830
- reject(
10831
- new Error(
10832
- `command timed out after ${Math.round(effectiveTimeoutMs / 1e3)}s`
10833
- )
10834
- );
11120
+ reject(new Error(`command timed out after ${effectiveTimeoutMs / 1e3}s`));
10835
11121
  },
10836
11122
  effectiveTimeoutMs,
10837
11123
  lineageBySimId.get(payload.simId)?.hostPid
@@ -11064,25 +11350,8 @@ function usesDirectLookup(matcher) {
11064
11350
  return matcher.regexFlags === void 0 && matcher.index === void 0 && matcher.type !== "text";
11065
11351
  }
11066
11352
 
11067
- // ../peach-engine/src/platform/adapter.ts
11068
- var IOS_ADAPTER = Object.freeze({
11069
- id: "ios",
11070
- displayName: "iOS",
11071
- isIOS: true,
11072
- isAndroid: false,
11073
- select: (registry) => registry.ios
11074
- });
11075
- var ANDROID_ADAPTER = Object.freeze({
11076
- id: "android",
11077
- displayName: "Android",
11078
- isIOS: false,
11079
- isAndroid: true,
11080
- select: (registry) => registry.android
11081
- });
11082
- var RUNTIME_PLATFORM_ADAPTERS = Object.freeze({
11083
- ios: IOS_ADAPTER,
11084
- android: ANDROID_ADAPTER
11085
- });
11353
+ // ../peach-engine/src/settings/device-spec-bus.ts
11354
+ init_adapter();
11086
11355
 
11087
11356
  // ../peach-engine/src/settings/dynamic-type.ts
11088
11357
  function textMetrics(fontSize, lineHeight, leading, ascent) {
@@ -12097,8 +12366,94 @@ async function dispatchTwoFingerGesture(page, startA, startB, endA, endB, option
12097
12366
  await page.waitForTimeout(250);
12098
12367
  }
12099
12368
 
12369
+ // ../contrast-runtime-delivery/src/version.ts
12370
+ function isValidRuntimeVersion(value) {
12371
+ return value.length <= 100 && /^[0-9A-Za-z][0-9A-Za-z._+-]*$/.test(value);
12372
+ }
12373
+
12374
+ // src/nativeViewSourceContract.ts
12375
+ function isNativeViewSource(value) {
12376
+ if (!isRecord(value)) return false;
12377
+ if (value.type === "placeholder")
12378
+ return (value.uri === void 0 || typeof value.uri === "string" && value.uri.length > 0) && (value.color === void 0 || typeof value.color === "string");
12379
+ if (value.type === "web") {
12380
+ if (typeof value.uri !== "string") return false;
12381
+ try {
12382
+ const url = new URL(value.uri);
12383
+ return url.protocol === "http:" || url.protocol === "https:";
12384
+ } catch {
12385
+ return false;
12386
+ }
12387
+ }
12388
+ return value.type === "video" && typeof value.uri === "string" && value.uri.length > 0 && ["paused", "loop", "muted"].every(
12389
+ (key) => value[key] === void 0 || typeof value[key] === "boolean"
12390
+ ) && (value.currentTime === void 0 || typeof value.currentTime === "number" && Number.isFinite(value.currentTime) && value.currentTime >= 0) && (value.resizeMode === void 0 || value.resizeMode === "contain" || value.resizeMode === "cover" || value.resizeMode === "stretch");
12391
+ }
12392
+
12393
+ // src/config.ts
12394
+ function readNativeRenderer2(value) {
12395
+ if (value === void 0) return void 0;
12396
+ if (value === "paper" || value === "fabric") return value;
12397
+ throw new Error("nativeRenderer must be paper or fabric");
12398
+ }
12399
+ function isPackageStubs(value) {
12400
+ if (value === "noop") return true;
12401
+ if (!isRecord(value)) return false;
12402
+ return (value.modules === void 0 || isRecord(value.modules) && Object.values(value.modules).every(
12403
+ (stub) => typeof stub === "string" && stub.length > 0
12404
+ )) && (value.views === void 0 || isRecord(value.views) && Object.values(value.views).every(
12405
+ (stub) => typeof stub === "string" && stub.length > 0 || isNativeViewSource(stub)
12406
+ ));
12407
+ }
12408
+ var PEACH_CONFIG_QUERY_PARAM = "peachConfig";
12409
+ function isPeachConfig(value) {
12410
+ if (!isRecord(value)) return false;
12411
+ return (value.nativeRenderer === void 0 || value.nativeRenderer === "paper" || value.nativeRenderer === "fabric") && (value.mapKit === void 0 || isRecord(value.mapKit) && typeof value.mapKit.token === "string" && value.mapKit.token.length > 0 && value.mapKit.token === value.mapKit.token.trim()) && (value.runtimeVersion === void 0 || typeof value.runtimeVersion === "string" && value.runtimeVersion === value.runtimeVersion.trim() && isValidRuntimeVersion(value.runtimeVersion)) && [
12412
+ "modules",
12413
+ "packages",
12414
+ "turboModules",
12415
+ "nativeModules",
12416
+ "nativeViews",
12417
+ "env",
12418
+ "network",
12419
+ "settings",
12420
+ "initialProps",
12421
+ "initialState"
12422
+ ].every((key) => value[key] === void 0 || isRecord(value[key])) && (value.packages === void 0 || isRecord(value.packages) && Object.values(value.packages).every(isPackageStubs)) && (value.nativeViews === void 0 || isRecord(value.nativeViews) && Object.values(value.nativeViews).every(
12423
+ (entry) => entry === "noop" || isNativeViewSource(entry) || isRecord(entry) && typeof entry.file === "string" && entry.file.length > 0
12424
+ ));
12425
+ }
12426
+ function hasOwnKeys(value) {
12427
+ return !!value && Object.keys(value).length > 0;
12428
+ }
12429
+ function hasPeachConfig(config) {
12430
+ if (!config) return false;
12431
+ return config.nativeRenderer !== void 0 || config.mapKit !== void 0 || typeof config.runtimeVersion === "string" && config.runtimeVersion.length > 0 || hasOwnKeys(config.modules) || hasOwnKeys(config.turboModules) || hasOwnKeys(config.nativeModules) || hasOwnKeys(config.nativeViews) || hasOwnKeys(config.env) || hasOwnKeys(config.network?.remap) || hasOwnKeys(config.settings) || hasOwnKeys(config.initialProps) || hasOwnKeys(config.initialState);
12432
+ }
12433
+ function readPeachConfigFromSearchParams(searchParams) {
12434
+ const raw = searchParams.get(PEACH_CONFIG_QUERY_PARAM);
12435
+ if (!raw) return void 0;
12436
+ let parsed;
12437
+ try {
12438
+ parsed = JSON.parse(raw);
12439
+ } catch (error) {
12440
+ const message = error instanceof Error ? error.message : String(error);
12441
+ console.warn(`[peach] invalid ${PEACH_CONFIG_QUERY_PARAM}: ${message}`);
12442
+ return void 0;
12443
+ }
12444
+ if (isRecord(parsed)) readNativeRenderer2(parsed.nativeRenderer);
12445
+ return isPeachConfig(parsed) && hasPeachConfig(parsed) ? parsed : void 0;
12446
+ }
12447
+
12100
12448
  // detox/navigation-lifecycle.ts
12101
12449
  var import_attempt_events = __toESM(require_attempt_events(), 1);
12450
+ var developerToken = readPeachConfigFromSearchParams(
12451
+ new URL(process.env.PEACH_URL || "http://localhost").searchParams
12452
+ )?.mapKit?.token;
12453
+ function redactDiagnosticText(text) {
12454
+ const withoutConfig = text.replace(/([?&]peachConfig=)[^&\s"'<>]+/g, "$1[redacted]");
12455
+ return developerToken ? withoutConfig.replaceAll(developerToken, "[redacted]") : withoutConfig;
12456
+ }
12102
12457
  var DIAGNOSTIC_LIMIT = 40;
12103
12458
  var pageDiagnostics = /* @__PURE__ */ new WeakMap();
12104
12459
  var activeNavigations = /* @__PURE__ */ new WeakMap();
@@ -12111,6 +12466,21 @@ function persistDiagnostic(diagnostic) {
12111
12466
  (0, import_attempt_events.writeAttemptEvent)(diagnostic);
12112
12467
  }
12113
12468
  function recordPageDiagnostic(page, diagnostic, persist = false) {
12469
+ if ("url" in diagnostic) diagnostic.url = redactDiagnosticText(diagnostic.url);
12470
+ if ("navigationUrl" in diagnostic)
12471
+ diagnostic.navigationUrl = redactDiagnosticText(diagnostic.navigationUrl);
12472
+ if ("message" in diagnostic)
12473
+ diagnostic.message = redactDiagnosticText(diagnostic.message);
12474
+ if ("errorText" in diagnostic && diagnostic.errorText)
12475
+ diagnostic.errorText = redactDiagnosticText(diagnostic.errorText);
12476
+ if ("pageState" in diagnostic && diagnostic.pageState) {
12477
+ if ("url" in diagnostic.pageState)
12478
+ diagnostic.pageState.url = redactDiagnosticText(diagnostic.pageState.url);
12479
+ if ("evaluateError" in diagnostic.pageState)
12480
+ diagnostic.pageState.evaluateError = redactDiagnosticText(
12481
+ diagnostic.pageState.evaluateError
12482
+ );
12483
+ }
12114
12484
  const entries = pageDiagnostics.get(page);
12115
12485
  if (!entries) return;
12116
12486
  entries.push(diagnostic);
@@ -12217,7 +12587,7 @@ function attachPageDiagnostics(page) {
12217
12587
  }
12218
12588
  async function readSootsimPageState(page) {
12219
12589
  try {
12220
- return await page.evaluate(() => {
12590
+ const state = await page.evaluate(() => {
12221
12591
  const bootMarks = Reflect.get(window, "__sootsimBootMarks");
12222
12592
  return {
12223
12593
  bodyText: document.body?.innerText?.slice(0, 500) ?? "",
@@ -12234,6 +12604,8 @@ async function readSootsimPageState(page) {
12234
12604
  url: window.location.href
12235
12605
  };
12236
12606
  });
12607
+ state.url = redactDiagnosticText(state.url);
12608
+ return state;
12237
12609
  } catch (error) {
12238
12610
  return {
12239
12611
  evaluateError: error instanceof Error ? error.message : String(error)
@@ -12258,14 +12630,14 @@ async function describeSootsimBridgeFailure(page, cause, summary = "timed out wa
12258
12630
  `page: ${JSON.stringify(state)}`,
12259
12631
  `recent page diagnostics:
12260
12632
  ${JSON.stringify(getSootsimNavigationDiagnostics(page))}`
12261
- ].join("\n")
12633
+ ].map(redactDiagnosticText).join("\n")
12262
12634
  );
12263
12635
  }
12264
12636
  async function navigateSootsimPage(page, url, identity) {
12265
12637
  attachPageDiagnostics(page);
12266
12638
  const navigation = {
12267
12639
  ...identity,
12268
- navigationUrl: url,
12640
+ navigationUrl: redactDiagnosticText(url),
12269
12641
  navigationId: ++nextNavigationIdentity
12270
12642
  };
12271
12643
  activeNavigations.set(page, navigation);
@@ -12318,12 +12690,13 @@ function visibleFraction(node) {
12318
12690
  function isNodeVisible(node) {
12319
12691
  return visibleFraction(node) >= DEFAULT_VISIBILITY_THRESHOLD;
12320
12692
  }
12321
- function focusIsWithin(focused, nodeId) {
12322
- return focused.nodeId === nodeId || focused.ancestorNodeIds.includes(nodeId);
12693
+ function focusIsWithin(focused, node) {
12694
+ const nativeNodeId = node.nativeTag ?? node.nodeId;
12695
+ return focused.nodeId === nativeNodeId || focused.ancestorNodeIds.includes(nativeNodeId);
12323
12696
  }
12324
12697
  async function isTextInputNodeFocused(node, page) {
12325
12698
  const focused = await page.evaluate(async () => window.__sootsimTest.getFocusedNode());
12326
- return focused !== null && focusIsWithin(focused, node.nodeId);
12699
+ return focused !== null && focusIsWithin(focused, node);
12327
12700
  }
12328
12701
  async function isNodeFocused(node, page) {
12329
12702
  if (node.isTextInput) return isTextInputNodeFocused(node, page);
@@ -12773,6 +13146,7 @@ var _browserLaunch = null;
12773
13146
  var _browserLaunchAbort = null;
12774
13147
  var _reapBrowserNow = null;
12775
13148
  var _context = null;
13149
+ var _appStorageState;
12776
13150
  var _page = null;
12777
13151
  var _synchronizationEnabled = false;
12778
13152
  var _proofFrameObservationActive = false;
@@ -12968,14 +13342,17 @@ async function waitForSootsimWorkletsIdle(page, timeout = 3e3) {
12968
13342
  let readStartedAt = null;
12969
13343
  let tenantAtRead = window.__sootsimRenderHost;
12970
13344
  let shellAtRead = window.SootSim?.bridges?.shellHost;
13345
+ let peerProgress = null;
12971
13346
  let deadlineTimerLateMs = null;
12972
13347
  while (Date.now() < deadline) {
12973
13348
  readStartedAt = Date.now();
12974
13349
  tenantAtRead = window.__sootsimRenderHost;
12975
13350
  shellAtRead = window.SootSim?.bridges?.shellHost;
12976
13351
  let timer;
13352
+ const peerRead = getPeerStats(false);
13353
+ peerProgress = peerRead && "peachPeerProgress" in peerRead ? peerRead.peachPeerProgress ?? null : null;
12977
13354
  const stats = await Promise.race([
12978
- getPeerStats(false),
13355
+ peerRead,
12979
13356
  new Promise((resolve3) => {
12980
13357
  timer = setTimeout(
12981
13358
  () => {
@@ -13012,6 +13389,7 @@ async function waitForSootsimWorkletsIdle(page, timeout = 3e3) {
13012
13389
  outstandingRead: readStartedAt === null ? null : {
13013
13390
  startedAtMs: readStartedAt - start,
13014
13391
  elapsedMs: Date.now() - readStartedAt,
13392
+ peerProgress: peerProgress ? { ...peerProgress } : null,
13015
13393
  testBridgeChanged: window.__sootsimTest !== testBridge,
13016
13394
  tenant: {
13017
13395
  presentAtRequest: !!tenantAtRead,
@@ -13092,7 +13470,8 @@ async function quiesceContextMedia(context) {
13092
13470
  if (timer !== void 0) clearTimeout(timer);
13093
13471
  }
13094
13472
  }
13095
- async function closeContext() {
13473
+ async function closeContext(preserveStorage) {
13474
+ if (!preserveStorage) _appStorageState = void 0;
13096
13475
  if (_browserCompromised) {
13097
13476
  throw new Error(`sootsim browser is compromised: ${_browserCompromised}`);
13098
13477
  }
@@ -13100,6 +13479,13 @@ async function closeContext() {
13100
13479
  const context = _context;
13101
13480
  const closingContextId = contextIdentity;
13102
13481
  try {
13482
+ if (preserveStorage) {
13483
+ _appStorageState = await withLifecycleTimeout(
13484
+ "context.storageState()",
13485
+ closingContextId,
13486
+ () => context.storageState({ indexedDB: true })
13487
+ );
13488
+ }
13103
13489
  await quiesceContextMedia(context);
13104
13490
  await withLifecycleTimeout("context.close()", closingContextId, () => context.close());
13105
13491
  _context = null;
@@ -13113,7 +13499,8 @@ async function closeContext() {
13113
13499
  throw error;
13114
13500
  }
13115
13501
  }
13116
- async function closeBrowser() {
13502
+ async function closeBrowser(preserveStorage) {
13503
+ if (!preserveStorage) _appStorageState = void 0;
13117
13504
  if (!_browser) return;
13118
13505
  const browser = _browser;
13119
13506
  const reapBrowserNow = _reapBrowserNow;
@@ -13122,7 +13509,7 @@ async function closeBrowser() {
13122
13509
  browserIdentity = 0;
13123
13510
  let closeError = null;
13124
13511
  try {
13125
- await closeContext();
13512
+ await closeContext(preserveStorage);
13126
13513
  } catch (error) {
13127
13514
  closeError = error instanceof Error ? error : new Error(String(error));
13128
13515
  }
@@ -13358,7 +13745,7 @@ async function focusElementForTextEntry(el) {
13358
13745
  await el.tap();
13359
13746
  await pollUntil(
13360
13747
  () => page.evaluate(async () => window.__sootsimTest.getFocusedNode()),
13361
- (focused) => focused !== null && focusIsWithin(focused, node.nodeId),
13748
+ (focused) => focused !== null && focusIsWithin(focused, node),
13362
13749
  {
13363
13750
  timeoutMs: 5e3,
13364
13751
  label: `SootSim text input ${node.nodeId} to focus`
@@ -13747,10 +14134,11 @@ function createSootSimElement(matcher) {
13747
14134
  }
13748
14135
  },
13749
14136
  async swipe(direction, speed = "fast", percentage = 0.75, normalizedStartingPointX = 0.5, normalizedStartingPointY = 0.5) {
14137
+ const page = getPage();
14138
+ if (_synchronizationEnabled) await waitForSootsimIdle2(page, 3e3, false, 0);
13750
14139
  const node = await findStableNodeByMatcher(matcher, "swipe");
13751
14140
  if (!node)
13752
14141
  throw new Error(`element not found for swipe: ${JSON.stringify(matcher)}`);
13753
- const page = getPage();
13754
14142
  const { absolutePosition, layout } = node;
13755
14143
  const startSootSimX = absolutePosition.x + layout.width * normalizedStartingPointX;
13756
14144
  const startSootSimY = absolutePosition.y + layout.height * normalizedStartingPointY;
@@ -14127,10 +14515,11 @@ var device = {
14127
14515
  }
14128
14516
  }
14129
14517
  if (!_page || newInstance || opts?.delete || opts?.recordVideoDir) {
14130
- await closeContext();
14518
+ await closeContext(!opts?.delete);
14131
14519
  const contextOpts = {
14132
14520
  hasTouch: true,
14133
- viewport: { width: 500, height: 900 }
14521
+ viewport: { width: 500, height: 900 },
14522
+ storageState: _appStorageState
14134
14523
  };
14135
14524
  if (opts?.recordVideoDir) {
14136
14525
  fs2.mkdirSync(opts.recordVideoDir, { recursive: true });
@@ -14235,7 +14624,7 @@ var device = {
14235
14624
  await waitForSootsimWorkletsIdle(page, 3e3);
14236
14625
  },
14237
14626
  async terminateApp() {
14238
- await closeBrowser();
14627
+ await closeBrowser(true);
14239
14628
  },
14240
14629
  async installApp() {
14241
14630
  },
@@ -14707,7 +15096,18 @@ var device = {
14707
15096
  if (input.hold && (!interact || typeof interact.touchDown !== "function")) {
14708
15097
  throw new Error("peach interact bridge is not installed");
14709
15098
  }
14710
- const controller = await lockstep.begin(60);
15099
+ const pairedPeerStats = async (reset = false) => {
15100
+ const stats = await captureWindow.SootSim?.bridges?.test?.getPeerStats?.(reset);
15101
+ if (!stats || !Number.isSafeInteger(stats.replayedFrameCallbacks))
15102
+ throw new Error(
15103
+ "paired worklets clock needs native frame delivery diagnostics"
15104
+ );
15105
+ return stats;
15106
+ };
15107
+ if (input.workletFrameTimestampsSeconds) await pairedPeerStats(true);
15108
+ const controller = await lockstep.begin(60, {
15109
+ workletFrameTimestampsSeconds: input.workletFrameTimestampsSeconds
15110
+ });
14711
15111
  const moves = input.hold?.moves ?? [];
14712
15112
  const holdState = {
14713
15113
  nextMove: 0,
@@ -14752,10 +15152,21 @@ var device = {
14752
15152
  );
14753
15153
  };
14754
15154
  const keptFrames = /* @__PURE__ */ new Map();
15155
+ const keepFrame = (receipt, bitmap) => {
15156
+ keptFrames.get(receipt.frameIndex)?.bitmap.close();
15157
+ keptFrames.set(receipt.frameIndex, { bitmap, receipt });
15158
+ };
14755
15159
  try {
15160
+ if (input.anchorAtTouch && !input.hold) {
15161
+ throw new Error("peach touch anchoring needs a hold");
15162
+ }
14756
15163
  const baselineBitmap = await captureBitmap();
14757
15164
  const baseline = regionPixels(baselineBitmap);
14758
- baselineBitmap.close();
15165
+ if (input.anchorAtTouch === "down")
15166
+ keepFrame(controller.initialFrame, baselineBitmap);
15167
+ else baselineBitmap.close();
15168
+ if (input.workletFrameTimestampsSeconds && (await pairedPeerStats()).replayedFrameCallbacks > 0)
15169
+ throw new Error("paired worklets callback preceded the Peach trigger");
14759
15170
  state.ready = true;
14760
15171
  if (input.hold && interact) {
14761
15172
  const hit = await interact.touchDown(
@@ -14793,7 +15204,7 @@ var device = {
14793
15204
  if (measurement.passedMinimum) {
14794
15205
  visibleStart = receipt;
14795
15206
  detectedMeasurement = measurement;
14796
- keptFrames.set(visibleStart.frameIndex, candidateBitmap);
15207
+ keepFrame(visibleStart, candidateBitmap);
14797
15208
  } else {
14798
15209
  candidateBitmap.close();
14799
15210
  }
@@ -14804,8 +15215,8 @@ var device = {
14804
15215
  const receipt = await controller.stepFrame();
14805
15216
  await advanceHold(receipt);
14806
15217
  const candidateBitmap = await captureBitmap();
14807
- if (input.anchorAtRelease && holdState.releaseReceipt === receipt) {
14808
- keptFrames.set(receipt.frameIndex, await createImageBitmap(candidateBitmap));
15218
+ if (input.anchorAtTouch === "down" || input.anchorAtTouch === "up" && holdState.releaseReceipt === receipt) {
15219
+ keepFrame(receipt, await createImageBitmap(candidateBitmap));
14809
15220
  }
14810
15221
  const bounds = boundsFor(candidateBitmap);
14811
15222
  const measurement = measureChange({
@@ -14821,8 +15232,7 @@ var device = {
14821
15232
  previousBitmap = null;
14822
15233
  visibleStart = receipt;
14823
15234
  detectedMeasurement = measurement;
14824
- keptFrames.get(receipt.frameIndex)?.close();
14825
- keptFrames.set(receipt.frameIndex, candidateBitmap);
15235
+ keepFrame(receipt, candidateBitmap);
14826
15236
  break;
14827
15237
  }
14828
15238
  previousBitmap?.close();
@@ -14836,18 +15246,14 @@ var device = {
14836
15246
  }
14837
15247
  let driveStart = visibleStart;
14838
15248
  let currentReceipt = visibleStart;
14839
- if (input.anchorAtRelease) {
14840
- if (!input.hold) {
14841
- throw new Error("peach release anchoring needs a hold");
14842
- }
15249
+ if (input.anchorAtTouch === "down") driveStart = controller.initialFrame;
15250
+ if (input.anchorAtTouch === "up") {
14843
15251
  while (!holdState.released) {
14844
15252
  if (state.cancelled) throw new Error("peach motion capture was cancelled");
14845
15253
  const receipt = await controller.stepFrame();
14846
15254
  await advanceHold(receipt);
14847
15255
  currentReceipt = receipt;
14848
- if (holdState.released) {
14849
- keptFrames.set(receipt.frameIndex, await captureBitmap());
14850
- }
15256
+ if (holdState.released) keepFrame(receipt, await captureBitmap());
14851
15257
  }
14852
15258
  if (!holdState.releaseReceipt) {
14853
15259
  throw new Error("peach release anchoring found no release frame");
@@ -14926,8 +15332,7 @@ var device = {
14926
15332
  `peach motion anchor ${anchor.id} found no onset past ${onsetThresholdPercent}% within ${input.detectionTimeoutMs}ms of ${anchor.notBeforeMs}ms (pan peak ${panPeakPercent.toFixed(2)}%, later peak ${latePeakPercent.toFixed(2)}%): the gate or the search start is wrong for this surface`
14927
15333
  );
14928
15334
  }
14929
- keptFrames.get(anchorReceipt.frameIndex)?.close();
14930
- keptFrames.set(anchorReceipt.frameIndex, anchorBitmap);
15335
+ keepFrame(anchorReceipt, anchorBitmap);
14931
15336
  resolvedAnchors.set(anchor.id, anchorReceipt);
14932
15337
  anchorRecords.push({
14933
15338
  changedPixelPercent: onsetMeasurement.changePercent,
@@ -14946,13 +15351,13 @@ var device = {
14946
15351
  const kept = keptFrames.get(stageFrameIndex);
14947
15352
  if (kept && stageFrameIndex <= currentReceipt.frameIndex) {
14948
15353
  snapshots.push({
14949
- actualMs: 0,
15354
+ actualMs: kept.receipt.virtualTimestampMs - anchorReceipt.virtualTimestampMs,
14950
15355
  anchorId,
14951
- bitmap: await createImageBitmap(kept),
14952
- frameIndex: anchorReceipt.frameIndex,
15356
+ bitmap: await createImageBitmap(kept.bitmap),
15357
+ frameIndex: kept.receipt.frameIndex,
14953
15358
  label: stage.label,
14954
15359
  offsetMs: stage.offsetMs,
14955
- virtualTimestampMs: anchorReceipt.virtualTimestampMs
15360
+ virtualTimestampMs: kept.receipt.virtualTimestampMs
14956
15361
  });
14957
15362
  continue;
14958
15363
  }
@@ -14972,6 +15377,7 @@ var device = {
14972
15377
  virtualTimestampMs: receipt.virtualTimestampMs
14973
15378
  });
14974
15379
  }
15380
+ if (input.workletFrameTimestampsSeconds) controller.endWorkletFrameReceipt();
14975
15381
  let releasedOffsetMs = 0;
14976
15382
  while (nextHoldEventFrameIndex() !== null) {
14977
15383
  const receipt = await controller.stepFrame();
@@ -14993,6 +15399,17 @@ var device = {
14993
15399
  await controller.stepFrame();
14994
15400
  await new Promise((resolve3) => setTimeout(resolve3, 0));
14995
15401
  }
15402
+ if (input.workletFrameTimestampsSeconds) {
15403
+ const stats = await pairedPeerStats();
15404
+ const first = stats.nativeFrameFirstTimestampMs;
15405
+ const firstFrameIndex = first === null ? null : Math.round(
15406
+ (first - controller.initialFrame.virtualTimestampMs) / (1e3 / 60)
15407
+ );
15408
+ if (firstFrameIndex !== 1)
15409
+ throw new Error(
15410
+ `paired worklets first delivery did not match the next UI frame: ${firstFrameIndex}`
15411
+ );
15412
+ }
14996
15413
  return {
14997
15414
  inputReceipts: controller.inputReceipts,
14998
15415
  anchors: anchorRecords,
@@ -15013,15 +15430,16 @@ var device = {
15013
15430
  }
15014
15431
  };
15015
15432
  } finally {
15016
- for (const bitmap of keptFrames.values()) bitmap.close();
15433
+ for (const kept of keptFrames.values()) kept.bitmap.close();
15017
15434
  controller.end();
15018
15435
  delete registry[input.token];
15019
15436
  }
15020
15437
  },
15021
15438
  {
15439
+ workletFrameTimestampsSeconds: options.workletFrameTimestampsSeconds,
15022
15440
  detectionTimeoutMs: options.detectionTimeoutMs ?? 3e3,
15023
15441
  frame: options.frame ?? null,
15024
- anchorAtRelease: options.anchorAtRelease ?? false,
15442
+ anchorAtTouch: options.anchorAtTouch ?? null,
15025
15443
  hold: options.hold ?? null,
15026
15444
  motionChangeDefinition: import_motion_change.MOTION_CHANGE_DEFINITION,
15027
15445
  platform: device.getPlatform(),
@@ -15031,14 +15449,19 @@ var device = {
15031
15449
  token
15032
15450
  }
15033
15451
  );
15034
- await page.waitForFunction(
15035
- (captureToken) => {
15036
- const registry = window.__sootsimVisibleChangeCaptures;
15037
- return registry?.[captureToken]?.ready === true;
15038
- },
15039
- token,
15040
- { timeout: 5e3 }
15041
- );
15452
+ await Promise.race([
15453
+ page.waitForFunction(
15454
+ (captureToken) => {
15455
+ const registry = window.__sootsimVisibleChangeCaptures;
15456
+ return registry?.[captureToken]?.ready === true;
15457
+ },
15458
+ token,
15459
+ { timeout: 5e3 }
15460
+ ),
15461
+ capturePromise.then(() => {
15462
+ throw new Error("peach motion capture completed before its trigger");
15463
+ })
15464
+ ]);
15042
15465
  const triggerStartMs = Date.now();
15043
15466
  let triggerCompletedMs = 0;
15044
15467
  try {
@@ -15483,7 +15906,7 @@ async function cleanup() {
15483
15906
  _browserLaunchAbort?.abort(new Error("Peach driver cleaned up"));
15484
15907
  await _browserLaunch?.catch(() => {
15485
15908
  });
15486
- await closeBrowser();
15909
+ await closeBrowser(false);
15487
15910
  }
15488
15911
  // Annotate the CommonJS export names for ESM import in node:
15489
15912
  0 && (module.exports = {