peachsim 0.1.1635 → 0.1.1641

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 (142) hide show
  1. package/README.md +10 -1
  2. package/cli/app-config.ts +14 -5
  3. package/cli/app-stubs.ts +96 -22
  4. package/cli/bridge-flow-runner.ts +2 -2
  5. package/cli/cloud-client.ts +60 -40
  6. package/cli/cloud-session.ts +16 -0
  7. package/cli/commands/assert.ts +17 -4
  8. package/cli/commands/box.ts +33 -19
  9. package/cli/commands/doctor.ts +37 -20
  10. package/cli/commands/flow.ts +1 -1
  11. package/cli/commands/inspect/actions.ts +3 -2
  12. package/cli/commands/inspect/core.ts +0 -8
  13. package/cli/commands/inspect/shared.ts +9 -4
  14. package/cli/commands/inspect.ts +65 -61
  15. package/cli/commands/platform.ts +2 -1
  16. package/cli/commands/preview.ts +1 -1
  17. package/cli/commands/record.ts +154 -68
  18. package/cli/commands/swift.ts +18 -4
  19. package/cli/commands/three-mode.ts +4 -38
  20. package/cli/commands/upload.ts +55 -49
  21. package/cli/drivers/playwright-sim-host.ts +65 -42
  22. package/cli/flow-live-status.ts +0 -2
  23. package/cli/main.ts +2 -2
  24. package/cli/outbound-endpoints.ts +7 -5
  25. package/cli/pause-aware-timeout.ts +4 -0
  26. package/cli/ws-bridge.ts +19 -16
  27. package/detox/attempt-events.cjs +99 -6
  28. package/detox/attempt-events.d.cts +7 -0
  29. package/detox/expectations.ts +7 -3
  30. package/detox/index.ts +259 -139
  31. package/detox/jest-environment.cjs +5 -1
  32. package/detox/motion-change.cjs +170 -21
  33. package/detox/motion-change.d.cts +32 -0
  34. package/detox/navigation-lifecycle.ts +35 -3
  35. package/dist-lib/agent-daemon-client.cjs +1 -1
  36. package/dist-lib/agent-events.cjs +1 -1
  37. package/dist-lib/attached-projects.cjs +1 -1
  38. package/dist-lib/auth/shared-session.cjs +1 -1
  39. package/dist-lib/backend-origin.cjs +17 -7
  40. package/dist-lib/beta.cjs +1 -1
  41. package/dist-lib/beta.mjs +1 -1
  42. package/dist-lib/bridge-constants.cjs +1 -1
  43. package/dist-lib/bridge-contract-input.cjs +1 -1
  44. package/dist-lib/bridge-contract-input.mjs +1 -1
  45. package/dist-lib/bridge-contract.cjs +1 -1
  46. package/dist-lib/bridge-contract.mjs +1 -1
  47. package/dist-lib/bundle-channel.cjs +5014 -2268
  48. package/dist-lib/capture-contract.cjs +1 -1
  49. package/dist-lib/capture-contract.mjs +1 -1
  50. package/dist-lib/cloud-contract.cjs +1 -1
  51. package/dist-lib/cloud-contract.mjs +1 -1
  52. package/dist-lib/cloud-test-args.cjs +1 -1
  53. package/dist-lib/cloud-test-args.mjs +1 -1
  54. package/dist-lib/cloud.cjs +1 -1
  55. package/dist-lib/cloud.mjs +1 -1
  56. package/dist-lib/config.cjs +1 -1
  57. package/dist-lib/config.mjs +1 -1
  58. package/dist-lib/detox/index.cjs +1036 -166
  59. package/dist-lib/detox/jest-environment.cjs +5 -1
  60. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  61. package/dist-lib/expo-swift-bundle.cjs +4613 -1963
  62. package/dist-lib/expo-swift-metadata.cjs +9 -7
  63. package/dist-lib/expo-swift-metadata.mjs +9 -7
  64. package/dist-lib/home-paths.cjs +1 -1
  65. package/dist-lib/host/bridge-host.cjs +58072 -16400
  66. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  67. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  68. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  69. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  70. package/dist-lib/host/websocket-proxy.cjs +1 -1
  71. package/dist-lib/index.cjs +2790 -143
  72. package/dist-lib/jump-to-source-babel.cjs +1 -1
  73. package/dist-lib/jump-to-source-native.cjs +1 -1
  74. package/dist-lib/kotlin-host.cjs +1 -1
  75. package/dist-lib/menu.cjs +1 -1
  76. package/dist-lib/menu.mjs +1 -1
  77. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  78. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  79. package/dist-lib/metro-production-bundle.cjs +1 -1
  80. package/dist-lib/metro-production-bundle.mjs +1 -1
  81. package/dist-lib/metro.cjs +2579 -95
  82. package/dist-lib/native-app-identity-project.cjs +243 -53
  83. package/dist-lib/native-app-identity-project.mjs +243 -53
  84. package/dist-lib/native-app-identity.cjs +29 -30
  85. package/dist-lib/native-app-identity.mjs +29 -30
  86. package/dist-lib/native-firebase-metadata.cjs +1 -1
  87. package/dist-lib/native-firebase-metadata.mjs +1 -1
  88. package/dist-lib/plist.cjs +1 -1
  89. package/dist-lib/plist.mjs +1 -1
  90. package/dist-lib/profiles.cjs +1 -1
  91. package/dist-lib/public-brand.cjs +1 -1
  92. package/dist-lib/react-native-host-modules.cjs +2 -2
  93. package/dist-lib/react-native-host-modules.mjs +2 -2
  94. package/dist-lib/scripts/dev-server-scanner.cjs +8 -6
  95. package/dist-lib/sdk.cjs +4 -3
  96. package/dist-lib/sdk.mjs +4 -3
  97. package/dist-lib/skills.cjs +66475 -23097
  98. package/dist-lib/swift-compile.cjs +452 -260
  99. package/dist-lib/swift-compile.mjs +451 -252
  100. package/dist-lib/swift-package.cjs +1 -1
  101. package/dist-lib/swift-package.mjs +1 -1
  102. package/dist-lib/swift-submit.cjs +14 -7
  103. package/dist-lib/swift-submit.mjs +14 -7
  104. package/dist-lib/swift-toolchain.cjs +316 -41
  105. package/dist-lib/swift.cjs +1 -1
  106. package/dist-lib/vite-swift.cjs +8188 -5636
  107. package/dist-lib/vite.cjs +2857 -12
  108. package/dist-lib/worklet-executable.cjs +14 -2
  109. package/dist-lib/worklet-executable.mjs +14 -2
  110. package/package.json +1 -1
  111. package/src/backend-origin.ts +21 -6
  112. package/src/expo-swift-host.ts +5 -2
  113. package/src/expoKotlinContract.ts +4 -7
  114. package/src/expoSwiftContract.ts +4 -6
  115. package/src/host/staged-host-files.ts +9 -0
  116. package/src/native-app-identity-project.ts +1 -4
  117. package/src/native-app-identity.ts +1 -4
  118. package/src/nativeAppIconContract.ts +1 -4
  119. package/src/output-lock-stdio.ts +36 -0
  120. package/src/react-native-bridge-modules.ts +106 -78
  121. package/src/react-native-host-modules.ts +4 -1
  122. package/src/settings-bundle.ts +1 -4
  123. package/src/swift-compile.ts +224 -139
  124. package/src/swift-distribution.lock.json +23 -12
  125. package/src/swift-distribution.ts +103 -4
  126. package/src/swift-host.ts +95 -30
  127. package/src/swift-icu.ts +16 -10
  128. package/src/swift-objc-compile.ts +547 -0
  129. package/src/swift-objc-metadata.ts +204 -0
  130. package/src/swift-objc-sdk.ts +883 -19
  131. package/src/swift-objc-standin.ts +126 -14
  132. package/src/swift-package-manifest.ts +42 -1
  133. package/src/swift-packages.ts +35 -21
  134. package/src/swift-root-lock.ts +153 -0
  135. package/src/swift-standin-emit.ts +40 -7
  136. package/src/swift-standins.ts +404 -138
  137. package/src/swift-submit.ts +18 -7
  138. package/src/swift-toolchain.ts +378 -46
  139. package/src/swiftContract.ts +12 -0
  140. package/src/worker-system-chunks.ts +11 -12
  141. package/src/worklet-executable.ts +19 -1
  142. package/src/xcode-project.ts +280 -33
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1641 | (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,
@@ -1409,6 +1524,26 @@ var require_motion_change = __commonJS({
1409
1524
  }
1410
1525
  });
1411
1526
 
1527
+ // ../peach-engine/src/capture/frame-offset.ts
1528
+ var frame_offset_exports = {};
1529
+ __export(frame_offset_exports, {
1530
+ frameIndexForOffset: () => frameIndexForOffset
1531
+ });
1532
+ function frameIndexForOffset(offsetMs, frameIntervalMs) {
1533
+ if (!Number.isFinite(offsetMs) || offsetMs < 0)
1534
+ throw new Error(`capture: invalid stage offset ${offsetMs}`);
1535
+ if (!Number.isFinite(frameIntervalMs) || frameIntervalMs <= 0)
1536
+ throw new Error(`capture: invalid frame interval ${frameIntervalMs}`);
1537
+ const quotient = offsetMs / frameIntervalMs;
1538
+ const nearest = Math.round(quotient);
1539
+ return Math.abs(quotient - nearest) <= Number.EPSILON * Math.abs(quotient) ? nearest : Math.ceil(quotient);
1540
+ }
1541
+ var init_frame_offset = __esm({
1542
+ "../peach-engine/src/capture/frame-offset.ts"() {
1543
+ "use strict";
1544
+ }
1545
+ });
1546
+
1412
1547
  // ../../scripts/lib/remote-build-delivery.ts
1413
1548
  var remote_build_delivery_exports = {};
1414
1549
  __export(remote_build_delivery_exports, {
@@ -1540,6 +1675,7 @@ var require_native_frame_receipt = __commonJS({
1540
1675
  "../peach-conformance/e2e/utils/native-frame-receipt.cjs"(exports2, module2) {
1541
1676
  "use strict";
1542
1677
  var { createHash } = require("node:crypto");
1678
+ var { frameIndexForOffset: frameIndexForOffset2 } = (init_frame_offset(), __toCommonJS(frame_offset_exports));
1543
1679
  function nativeVideoFrameReceipt(recordingSha256, sourceFrameIndex, ptsSeconds, inputClock) {
1544
1680
  if (!/^[a-f0-9]{64}$/.test(recordingSha256)) throw new Error("invalid recording hash");
1545
1681
  if (!Number.isSafeInteger(sourceFrameIndex) || sourceFrameIndex < 0)
@@ -1620,25 +1756,29 @@ var require_native_frame_receipt = __commonJS({
1620
1756
  if (output !== bytes.length) throw new Error("incomplete native framebuffer");
1621
1757
  return bytes;
1622
1758
  }
1623
- function writeNativeFramebufferPng(receipt, bytes, outputPath, expectedSize) {
1759
+ function nativeFramebufferPixels(receipt, bytes) {
1624
1760
  requireNativeRawFrame(receipt, bytes);
1625
- const { PNG } = require("pngjs");
1626
- const fs3 = require("node:fs");
1627
1761
  const { width, height, stride } = receipt.rawFramebuffer;
1628
- if (width !== expectedSize.width || height !== expectedSize.height)
1629
- throw new Error("native framebuffer and screenshot dimensions differ");
1630
- const png = new PNG({ width, height });
1762
+ const data = Buffer.allocUnsafe(width * height * 4);
1631
1763
  for (let y = 0; y < height; y++) {
1632
1764
  for (let x = 0; x < width; x++) {
1633
1765
  const source = y * stride + x * 4;
1634
1766
  const destination = (y * width + x) * 4;
1635
- png.data[destination] = bytes[source + 2];
1636
- png.data[destination + 1] = bytes[source + 1];
1637
- png.data[destination + 2] = bytes[source];
1638
- png.data[destination + 3] = bytes[source + 3];
1767
+ data[destination] = bytes[source + 2];
1768
+ data[destination + 1] = bytes[source + 1];
1769
+ data[destination + 2] = bytes[source];
1770
+ data[destination + 3] = bytes[source + 3];
1639
1771
  }
1640
1772
  }
1641
- fs3.writeFileSync(outputPath, PNG.sync.write(png));
1773
+ return { width, height, data };
1774
+ }
1775
+ function writeNativeFramebufferPng(receipt, bytes, outputPath, expectedSize) {
1776
+ const { PNG } = require("pngjs");
1777
+ const fs3 = require("node:fs");
1778
+ const pixels = nativeFramebufferPixels(receipt, bytes);
1779
+ if (pixels.width !== expectedSize.width || pixels.height !== expectedSize.height)
1780
+ throw new Error("native framebuffer and screenshot dimensions differ");
1781
+ fs3.writeFileSync(outputPath, PNG.sync.write(pixels));
1642
1782
  return receipt;
1643
1783
  }
1644
1784
  module2.exports = {
@@ -1646,6 +1786,7 @@ var require_native_frame_receipt = __commonJS({
1646
1786
  requireNativeProducerFrame,
1647
1787
  requireNativeRawFrame,
1648
1788
  decodeNativeFramebuffer,
1789
+ nativeFramebufferPixels,
1649
1790
  writeNativeFramebufferPng
1650
1791
  };
1651
1792
  async function buildNativeFrameConsumer({ root, cacheRoot, waitForAdmission = false }) {
@@ -1757,6 +1898,455 @@ var require_native_frame_receipt = __commonJS({
1757
1898
  };
1758
1899
  }
1759
1900
  module2.exports.nativeUIKitInputReceipt = nativeUIKitInputReceipt;
1901
+ function nativeUIFrameClockReceipt(bytes) {
1902
+ if (!Buffer.isBuffer(bytes))
1903
+ throw new Error("ui frame clock receipt must preserve source bytes");
1904
+ const rows = bytes.toString("utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
1905
+ if (rows.at(-1)?.kind !== "stopped")
1906
+ throw new Error("ui frame clock receipt was not closed by its recorder");
1907
+ const ticks = [];
1908
+ const workletsTicks = [];
1909
+ for (const row of rows.slice(0, -1)) {
1910
+ if (row.kind === "worklets-frame") {
1911
+ if (!Number.isFinite(row.timestampSeconds) || row.timestampSeconds < 0)
1912
+ throw new Error("invalid worklets frame clock row");
1913
+ if (workletsTicks.length > 0 && row.timestampSeconds <= workletsTicks.at(-1).timestampSeconds)
1914
+ throw new Error("worklets frame clock timestamps must increase");
1915
+ workletsTicks.push({ timestampSeconds: row.timestampSeconds });
1916
+ continue;
1917
+ }
1918
+ if (row.kind !== "ui-frame" || !Number.isFinite(row.targetTimestampSeconds) || !Number.isFinite(row.timestampSeconds) || !(row.durationSeconds > 0))
1919
+ throw new Error("invalid ui frame clock row");
1920
+ if (ticks.length > 0 && row.targetTimestampSeconds <= ticks.at(-1).targetTimestampSeconds)
1921
+ throw new Error("ui frame clock target timestamps must increase");
1922
+ ticks.push({
1923
+ targetTimestampSeconds: row.targetTimestampSeconds,
1924
+ timestampSeconds: row.timestampSeconds,
1925
+ durationSeconds: row.durationSeconds,
1926
+ clockBridge: row.clockBridge,
1927
+ callbackThread: row.callbackThread,
1928
+ callbackWasMainThread: row.callbackWasMainThread
1929
+ });
1930
+ }
1931
+ if (ticks.length < 2)
1932
+ throw new Error("ui frame clock receipt holds fewer than two ticks");
1933
+ return {
1934
+ domain: "ca-media-time-seconds",
1935
+ source: "cadisplaylink-target-timestamp",
1936
+ sourceReceiptSha256: createHash("sha256").update(bytes).digest("hex"),
1937
+ ticks,
1938
+ workletsTicks
1939
+ };
1940
+ }
1941
+ module2.exports.nativeUIFrameClockReceipt = nativeUIFrameClockReceipt;
1942
+ function pairedNativeWorkletFrameClock(root, lifecycle, seed = "ui-frame-after-delivered-touch-up") {
1943
+ const { boundText } = require_attempt_events();
1944
+ if (!lifecycle || typeof lifecycle !== "object" || Array.isArray(lifecycle))
1945
+ throw new Error("paired worklets clock needs a retained native lifecycle");
1946
+ const receipt = lifecycle.uiFrameClock;
1947
+ if (!receipt)
1948
+ throw new Error("paired worklets clock needs a retained native UI clock receipt");
1949
+ const inputReceipt = lifecycle.deliveredUIKitInput;
1950
+ if (!inputReceipt)
1951
+ throw new Error("paired worklets clock needs a delivered UIKit touch-up witness");
1952
+ const clock = nativeUIFrameClockReceipt(
1953
+ Buffer.from(
1954
+ boundText(
1955
+ root,
1956
+ receipt.sourceReceipt,
1957
+ receipt.sourceReceiptSha256,
1958
+ "native UI clock receipt"
1959
+ )
1960
+ )
1961
+ );
1962
+ const input = nativeUIKitInputReceipt(
1963
+ Buffer.from(
1964
+ boundText(
1965
+ root,
1966
+ inputReceipt.sourceReceipt,
1967
+ inputReceipt.sourceReceiptSha256,
1968
+ "delivered UIKit input receipt"
1969
+ )
1970
+ )
1971
+ );
1972
+ if (seed === "delivered-touch-drag") {
1973
+ const down = input.events[0];
1974
+ const up2 = input.events.at(-1);
1975
+ if (input.status !== "witnessed" || down?.phase !== 0 || up2?.phase !== 3 || !input.events.slice(1, -1).every((event) => event.phase === 1) || input.events.length < 3 || input.events.some(
1976
+ (event, index) => index > 0 && (event.deliveredAtSeconds < input.events[index - 1].deliveredAtSeconds || event.touchTimestampSeconds < input.events[index - 1].touchTimestampSeconds)
1977
+ ))
1978
+ throw new Error(
1979
+ "paired drag needs one ordered delivered UIKit down/moves/up sequence"
1980
+ );
1981
+ if (!clock.ticks.some((tick2) => tick2.timestampSeconds < down.deliveredAtSeconds) || !clock.ticks.some((tick2) => tick2.timestampSeconds > up2.deliveredAtSeconds))
1982
+ throw new Error("native UI clock does not bracket the delivered drag");
1983
+ return {
1984
+ provenance: {
1985
+ sourceReceipt: receipt.sourceReceipt,
1986
+ sourceReceiptSha256: receipt.sourceReceiptSha256,
1987
+ inputReceipt: inputReceipt.sourceReceipt,
1988
+ inputReceiptSha256: inputReceipt.sourceReceiptSha256,
1989
+ alignment: "delivered-touch-drag",
1990
+ stageOffsetBasis: "declared-from-delivered-down",
1991
+ deliveredTouchDownSeconds: down.deliveredAtSeconds
1992
+ },
1993
+ hold: {
1994
+ x: down.x,
1995
+ y: down.y,
1996
+ release: { x: up2.x, y: up2.y },
1997
+ durationMs: (up2.deliveredAtSeconds - down.deliveredAtSeconds) * 1e3,
1998
+ eventDurationMs: (up2.touchTimestampSeconds - down.touchTimestampSeconds) * 1e3,
1999
+ moves: input.events.slice(1, -1).map((event) => ({
2000
+ x: event.x,
2001
+ y: event.y,
2002
+ afterMs: (event.deliveredAtSeconds - down.deliveredAtSeconds) * 1e3,
2003
+ eventAfterMs: (event.touchTimestampSeconds - down.touchTimestampSeconds) * 1e3
2004
+ }))
2005
+ }
2006
+ };
2007
+ }
2008
+ const up = input.events.find((event) => event.phase === 3);
2009
+ if (input.status !== "witnessed" || !up)
2010
+ throw new Error("paired worklets clock needs a delivered UIKit touch-up witness");
2011
+ const startIndex = clock.ticks.findIndex(
2012
+ (tick2) => tick2.timestampSeconds > up.deliveredAtSeconds
2013
+ );
2014
+ if (startIndex < 1)
2015
+ throw new Error("native UI clock does not bracket delivered touch-up");
2016
+ const timestampsSeconds = clock.ticks.slice(startIndex - 1).map((tick2) => tick2.targetTimestampSeconds);
2017
+ return {
2018
+ provenance: {
2019
+ sourceReceipt: receipt.sourceReceipt,
2020
+ sourceReceiptSha256: receipt.sourceReceiptSha256,
2021
+ inputReceipt: inputReceipt.sourceReceipt,
2022
+ inputReceiptSha256: inputReceipt.sourceReceiptSha256,
2023
+ deliveredTouchUpSeconds: up.deliveredAtSeconds,
2024
+ startIndex,
2025
+ alignment: "inferred-next-ui-frame-after-delivered-touch-up",
2026
+ frameCount: timestampsSeconds.length - 1
2027
+ },
2028
+ // the trigger reads the previous frame; the first delivery reads startIndex.
2029
+ timestampsSeconds
2030
+ };
2031
+ }
2032
+ module2.exports.pairedNativeWorkletFrameClock = pairedNativeWorkletFrameClock;
2033
+ function nativeGradingPlan(sourceRoot, declaration) {
2034
+ const fs3 = require("node:fs"), path5 = require("node:path");
2035
+ const catalogs = [
2036
+ ["packages/peach-conformance/src/cases/catalog.cjs", "CONFORMANCE_CASE_CATALOG"],
2037
+ [
2038
+ "packages/peach-library-conformance/src/cases/catalog.cjs",
2039
+ "LIBRARY_CONFORMANCE_CASE_CATALOG"
2040
+ ]
2041
+ ];
2042
+ let catalog, catalogPath;
2043
+ for (const [relative, key] of catalogs) {
2044
+ const entries = require(path5.join(sourceRoot, relative))[key];
2045
+ const entry = entries.find((item) => item.name === declaration.caseName);
2046
+ const checkpoint = entry?.checkpoints?.find(
2047
+ (item) => item.id === declaration.checkpoint
2048
+ );
2049
+ if (checkpoint) {
2050
+ catalog = checkpoint;
2051
+ catalogPath = relative;
2052
+ break;
2053
+ }
2054
+ }
2055
+ if (!catalog || !Array.isArray(catalog.stages) || JSON.stringify(catalog.stages) !== JSON.stringify(declaration.stages.map((stage) => stage.label)))
2056
+ throw new Error(
2057
+ "native sampled grading plan must include every catalog stage in order"
2058
+ );
2059
+ if (JSON.stringify(catalog.motionCadence ?? null) !== JSON.stringify(declaration.cadence))
2060
+ throw new Error("native sampled grading plan changed the catalog cadence");
2061
+ if (declaration.anchorPlan?.stages?.length !== declaration.stages.length || declaration.stages.some(
2062
+ (stage, index) => declaration.anchorPlan.stages[index].anchorId !== (stage.anchor?.id ?? "transition")
2063
+ ))
2064
+ throw new Error("native sampled grading plan changed a declared stage anchor");
2065
+ if (declaration.stages.some(
2066
+ (stage) => !Number.isFinite(stage.offsetMs) || stage.offsetMs < 0
2067
+ ))
2068
+ throw new Error("native sampled grading plan has invalid stage offsets");
2069
+ return {
2070
+ ...declaration,
2071
+ contract: "continuous-catalog-stage-window-v1",
2072
+ catalogPath,
2073
+ catalogSha256: createHash("sha256").update(fs3.readFileSync(path5.join(sourceRoot, catalogPath))).digest("hex")
2074
+ };
2075
+ }
2076
+ module2.exports.nativeGradingPlan = nativeGradingPlan;
2077
+ function tickValue(value, label = "native sampled recording") {
2078
+ if (typeof value !== "string" || !/^\d+$/.test(value))
2079
+ throw new Error(`${label} has invalid mach ticks`);
2080
+ return BigInt(value);
2081
+ }
2082
+ function nativeResolvedGradingWindow(plan, resolution, frames, markers) {
2083
+ const markerStart = tickValue(markers?.machStart, "native grade-start marker");
2084
+ const markerEnd = tickValue(markers?.machEnd, "native grade-end marker");
2085
+ if (!resolution || !Array.isArray(resolution.stages) || resolution.stages.length !== plan.stages.length || resolution.stages.some(
2086
+ (stage, index) => stage.label !== plan.stages[index].label || stage.offsetMs !== plan.stages[index].offsetMs || stage.anchorId !== plan.anchorPlan.stages[index].anchorId || !Number.isFinite(stage.atSec) || stage.atSec < 0
2087
+ ) || !Number.isFinite(resolution.triggerInVideoSec) || resolution.triggerInVideoSec < 0)
2088
+ throw new Error("native sampled window resolution dropped or changed declared stages");
2089
+ for (const stage of resolution.stages) {
2090
+ const offset = stage.touchOffsetMs ?? stage.offsetMs;
2091
+ const expected = stage.basisSec + frameIndexForOffset2(offset, 1e3 / 60) * (1e3 / 60) / 1e3;
2092
+ if (!Number.isFinite(stage.basisSec) || stage.basisSec < 0 || !Number.isFinite(offset) || offset < 0 || !stage.initialBeforeTrigger && stage.atSec !== expected || stage.anchorId !== "transition" && offset !== stage.offsetMs)
2093
+ throw new Error("native sampled window changed the declared stage timing");
2094
+ }
2095
+ const origin = frames[0]?.consumerArrival;
2096
+ if (!origin || !Number.isSafeInteger(origin.timebaseNumer) || origin.timebaseNumer <= 0 || !Number.isSafeInteger(origin.timebaseDenom) || origin.timebaseDenom <= 0)
2097
+ throw new Error("native sampled window has no source clock");
2098
+ const atTicks = (seconds) => {
2099
+ const nanos = Math.ceil(seconds * 1e9);
2100
+ if (!Number.isSafeInteger(nanos) || nanos < 0)
2101
+ throw new Error("native sampled window exceeds exact source range");
2102
+ return BigInt(origin.machTicks) + (BigInt(nanos) * BigInt(origin.timebaseDenom) + BigInt(origin.timebaseNumer) - 1n) / BigInt(origin.timebaseNumer);
2103
+ };
2104
+ const endSeconds = Math.max(
2105
+ ...resolution.stages.map((stage) => stage.atSec),
2106
+ ...plan.cadence ? [resolution.triggerInVideoSec + 1.9] : []
2107
+ );
2108
+ const firstAnchor = atTicks(
2109
+ Math.min(
2110
+ resolution.triggerInVideoSec,
2111
+ ...resolution.stages.map((stage) => Math.min(stage.atSec, stage.basisSec))
2112
+ )
2113
+ );
2114
+ const start = firstAnchor < markerStart ? firstAnchor : markerStart;
2115
+ const end = atTicks(endSeconds) + 1n;
2116
+ if (start < BigInt(origin.machTicks) || end > markerEnd || end <= start)
2117
+ throw new Error("native recorder does not bracket the complete catalog stage window");
2118
+ return { machStart: String(start), machEnd: String(end) };
2119
+ }
2120
+ module2.exports.nativeResolvedGradingWindow = nativeResolvedGradingWindow;
2121
+ function nativeRefreshBoundaryWindows(ticks, { startSeconds, endSeconds }) {
2122
+ if (!Number.isFinite(startSeconds) || !Number.isFinite(endSeconds) || !(endSeconds > startSeconds))
2123
+ throw new Error("native sampled grading window is empty");
2124
+ const intervals = [], gaps = [];
2125
+ for (const [refresh, tick2] of ticks.entries()) {
2126
+ const start = tick2.timestampSeconds, end = tick2.targetTimestampSeconds;
2127
+ if (!(end > start)) throw new Error("native UI clock has a reversed refresh interval");
2128
+ if (end > startSeconds && start < endSeconds) intervals.push({ refresh, start, end });
2129
+ const next = ticks[refresh + 1];
2130
+ if (!next) continue;
2131
+ const rounding = 8 * Number.EPSILON * Math.max(1, Math.abs(end), Math.abs(next.timestampSeconds));
2132
+ if (Math.abs(end - next.timestampSeconds) <= rounding) continue;
2133
+ const gapStart = Math.min(end, next.timestampSeconds);
2134
+ const gapEnd = Math.max(end, next.timestampSeconds);
2135
+ gaps.push({
2136
+ afterRefresh: refresh,
2137
+ start: gapStart,
2138
+ end: gapEnd,
2139
+ intersectsGrading: gapEnd > startSeconds && gapStart < endSeconds
2140
+ });
2141
+ }
2142
+ return {
2143
+ intervals,
2144
+ gaps,
2145
+ bracketed: intervals.length > 0 && intervals[0].start <= startSeconds && intervals.at(-1).end >= endSeconds
2146
+ };
2147
+ }
2148
+ module2.exports.nativeRefreshBoundaryWindows = nativeRefreshBoundaryWindows;
2149
+ function nativeRefreshSamples({
2150
+ frames,
2151
+ reads: observations,
2152
+ clockBytes,
2153
+ finalFrames,
2154
+ finalSurfaceStatus = "settled",
2155
+ grading
2156
+ }) {
2157
+ const clock = nativeUIFrameClockReceipt(clockBytes);
2158
+ const movingFinalSurface = finalSurfaceStatus === "moving" && finalFrames.length === 0;
2159
+ if (!frames.length || !observations.length || !movingFinalSurface && finalFrames.length < 3)
2160
+ throw new Error("native sampled recording has no observed stable final surface");
2161
+ const origin = frames[0].consumerArrival;
2162
+ if (origin?.domain !== "host-mach-absolute-time" || !/^[0-9a-f-]{36}$/i.test(origin.bootSessionUUID ?? "") || !Number.isSafeInteger(origin.timebaseNumer) || origin.timebaseNumer <= 0 || !Number.isSafeInteger(origin.timebaseDenom) || origin.timebaseDenom <= 0)
2163
+ throw new Error("native sampled recording has no authenticated host clock");
2164
+ const secondsPerTick = origin.timebaseNumer / origin.timebaseDenom / 1e9;
2165
+ const bridgeOrigin = tickValue(clock.ticks[0].clockBridge?.machBefore);
2166
+ const elapsed = (value) => {
2167
+ const ticks = tickValue(value) - bridgeOrigin;
2168
+ if (ticks < -BigInt(Number.MAX_SAFE_INTEGER) || ticks > BigInt(Number.MAX_SAFE_INTEGER))
2169
+ throw new Error("native clock bridge exceeds exact mach range");
2170
+ return Number(ticks) * secondsPerTick;
2171
+ };
2172
+ let lowerOffset = -Infinity;
2173
+ let upperOffset = Infinity;
2174
+ for (const tick2 of clock.ticks) {
2175
+ if (tick2.callbackThread !== "dedicated-user-interactive-run-loop" || tick2.callbackWasMainThread !== false)
2176
+ throw new Error(
2177
+ "native UI clock callback did not run on its independent recorder thread"
2178
+ );
2179
+ const bridge = tick2.clockBridge;
2180
+ if (bridge?.bootSessionUUID !== origin.bootSessionUUID || bridge.timebaseNumer !== origin.timebaseNumer || bridge.timebaseDenom !== origin.timebaseDenom || !Number.isFinite(bridge.mediaSeconds))
2181
+ throw new Error("native clock bridge kernel identity or timebase mismatch");
2182
+ const before = elapsed(bridge.machBefore), after = elapsed(bridge.machAfter);
2183
+ if (after < before) throw new Error("native clock bridge has reversed bracket");
2184
+ const rounding = 8 * Number.EPSILON * Math.max(1, Math.abs(bridge.mediaSeconds));
2185
+ lowerOffset = Math.max(lowerOffset, bridge.mediaSeconds - after - rounding);
2186
+ upperOffset = Math.min(upperOffset, bridge.mediaSeconds - before + rounding);
2187
+ }
2188
+ if (lowerOffset > upperOffset) throw new Error("native clock bridge brackets disagree");
2189
+ const sourceFrames = new Map(
2190
+ frames.map((frame, index) => [frame.callbackIndex, { frame, index }])
2191
+ );
2192
+ if (sourceFrames.size !== frames.length)
2193
+ throw new Error("native source callbacks repeat");
2194
+ const preservedEnds = frames.map((frame) => tickValue(frame.readWindow?.machEnd));
2195
+ if (preservedEnds.some((end, index) => index > 0 && end <= preservedEnds[index - 1]))
2196
+ throw new Error("native preserved source copy windows are not increasing");
2197
+ let latestPreserved = -1;
2198
+ let previousEnd = -Infinity;
2199
+ const reads = observations.map((frame) => {
2200
+ const readEnd = tickValue(frame.readWindow?.machEnd);
2201
+ while (latestPreserved + 1 < frames.length && preservedEnds[latestPreserved + 1] <= readEnd)
2202
+ latestPreserved++;
2203
+ const reference = sourceFrames.get(frame.sourceCallbackIndex);
2204
+ if (!reference || reference.index !== latestPreserved || reference.frame.rawPath !== frame.rawPath || reference.frame.rawFramebuffer?.sha256 !== frame.rawFramebuffer?.sha256 || reference.frame.rawFramebuffer?.width !== frame.rawFramebuffer?.width || reference.frame.rawFramebuffer?.height !== frame.rawFramebuffer?.height || reference.frame.rawFramebuffer?.stride !== frame.rawFramebuffer?.stride || reference.frame.rawFramebuffer?.pixelFormat !== frame.rawFramebuffer?.pixelFormat || reference.frame.source?.screenId !== frame.source?.screenId || tickValue(reference.frame.readWindow?.machEnd) > tickValue(frame.readWindow?.machEnd))
2205
+ throw new Error("native sampled read has no matching immutable source pixels");
2206
+ const host = frame.consumerArrival;
2207
+ if (host?.bootSessionUUID !== origin.bootSessionUUID || host.domain !== origin.domain || host.timebaseNumer !== origin.timebaseNumer || host.timebaseDenom !== origin.timebaseDenom || !Number.isSafeInteger(frame.source?.seedBeforeRead) || frame.source.seedBeforeRead !== frame.source.seedAfterRead || frame.source.surfaceSeed !== frame.source.seedBeforeRead || frame.rawFramebuffer?.status !== "preserved" || !/^[a-f0-9]{64}$/.test(frame.rawFramebuffer.sha256))
2208
+ throw new Error("native sampled read is superseded or has a different clock");
2209
+ const start = elapsed(frame.readWindow?.machStart), end = elapsed(frame.readWindow?.machEnd);
2210
+ if (end < start || start < previousEnd || tickValue(frame.readWindow.machStart) < tickValue(host.machTicks))
2211
+ throw new Error("native sampled read has an invalid copy window");
2212
+ previousEnd = end;
2213
+ return {
2214
+ start: start + lowerOffset,
2215
+ end: end + upperOffset,
2216
+ sourceFrameIndex: reference.index,
2217
+ callbackIndex: frame.callbackIndex
2218
+ };
2219
+ });
2220
+ const gradedStart = elapsed(grading?.machStart) + lowerOffset;
2221
+ const gradedEnd = elapsed(grading?.machEnd) + upperOffset;
2222
+ if (!(gradedEnd > gradedStart))
2223
+ throw new Error("native sampled grading window is empty");
2224
+ const boundaries = nativeRefreshBoundaryWindows(clock.ticks, {
2225
+ startSeconds: gradedStart,
2226
+ endSeconds: gradedEnd
2227
+ });
2228
+ const uncovered = boundaries.gaps.find((gap) => gap.intersectsGrading);
2229
+ if (uncovered)
2230
+ throw new Error(
2231
+ `native UI clock has an uncovered refresh after ${uncovered.afterRefresh}`
2232
+ );
2233
+ if (!boundaries.bracketed)
2234
+ throw new Error("native UI clock does not bracket every graded refresh");
2235
+ const samples = [];
2236
+ let cursor = 0;
2237
+ for (const { refresh, start, end } of boundaries.intervals) {
2238
+ while (cursor < reads.length && reads[cursor].start < start) cursor++;
2239
+ if (cursor === reads.length || reads[cursor].end >= end)
2240
+ throw new Error(`native recording has no surface sampled during refresh ${refresh}`);
2241
+ samples.push({
2242
+ refresh,
2243
+ sourceFrameIndex: reads[cursor].sourceFrameIndex,
2244
+ observationCallbackIndex: reads[cursor].callbackIndex,
2245
+ evidence: `surface sampled during refresh ${refresh}`
2246
+ });
2247
+ }
2248
+ const final = observations.at(-1);
2249
+ let previousFinalEnd = tickValue(final.readWindow.machEnd);
2250
+ for (const settled of finalFrames) {
2251
+ const host = settled.consumerArrival;
2252
+ const source = settled.source, raw = settled.rawFramebuffer;
2253
+ if (host?.bootSessionUUID !== origin.bootSessionUUID || host.domain !== origin.domain || host.timebaseNumer !== origin.timebaseNumer || host.timebaseDenom !== origin.timebaseDenom || !Number.isSafeInteger(source?.seedBeforeRead) || source.surfaceSeed !== source.seedBeforeRead || source.seedBeforeRead !== source.seedAfterRead || raw?.status !== "preserved" || raw.sha256 !== final.rawFramebuffer.sha256 || raw.width !== final.rawFramebuffer.width || raw.height !== final.rawFramebuffer.height || raw.stride !== final.rawFramebuffer.stride || raw.pixelFormat !== final.rawFramebuffer.pixelFormat || source.screenId !== final.source.screenId || tickValue(settled.readWindow?.machStart) <= previousFinalEnd || tickValue(settled.readWindow?.machEnd) < tickValue(settled.readWindow?.machStart))
2254
+ throw new Error(
2255
+ "native recording final surface is unobserved or mismatches stable post-settle reads"
2256
+ );
2257
+ previousFinalEnd = tickValue(settled.readWindow.machEnd);
2258
+ }
2259
+ return {
2260
+ contract: "per-refresh-sampled-surface-v1",
2261
+ samples,
2262
+ displayedWrite: "unknown: may differ from sampled write within refresh",
2263
+ limits: {
2264
+ clockDiscontinuities: boundaries.gaps,
2265
+ ...movingFinalSurface ? { finalSurface: "moving" } : {}
2266
+ },
2267
+ bridge: { bootSessionUUID: origin.bootSessionUUID, lowerOffset, upperOffset }
2268
+ };
2269
+ }
2270
+ module2.exports.nativeRefreshSamples = nativeRefreshSamples;
2271
+ function authenticateNativeRefreshSamples(root, sourceRoot, lifecycle) {
2272
+ const fs3 = require("node:fs");
2273
+ const path5 = require("node:path");
2274
+ const { boundText } = require_attempt_events();
2275
+ const native = lifecycle.nativeFramebuffer;
2276
+ if (!native || native.exitCode !== 0 || native.failure || native.sourceSha256 !== createHash("sha256").update(
2277
+ fs3.readFileSync(
2278
+ path5.join(sourceRoot, "scripts/debug/native-ios-frame-consumer.m")
2279
+ )
2280
+ ).digest("hex"))
2281
+ throw new Error("native sampled recording has no source-matched consumer");
2282
+ const parse2 = (text) => text.split("\n").filter(Boolean).map(JSON.parse);
2283
+ const frames = parse2(
2284
+ boundText(root, native.receipt, native.receiptSha256, "native sampled reads")
2285
+ );
2286
+ const observations = parse2(
2287
+ boundText(root, native.reads, native.readsSha256, "native sampled copy windows")
2288
+ );
2289
+ const clock = lifecycle.uiFrameClock;
2290
+ const clockBytes = Buffer.from(
2291
+ boundText(
2292
+ root,
2293
+ clock?.sourceReceipt,
2294
+ clock?.sourceReceiptSha256,
2295
+ "native sampled UI clock and bridge"
2296
+ )
2297
+ );
2298
+ const finalSurfaceStatus = lifecycle.finalFrameProof?.status;
2299
+ if (!Array.isArray(native.finalReads) || native.finalReads.length < 3 && !(finalSurfaceStatus === "moving" && native.finalReads.length === 0))
2300
+ throw new Error("native sampled recording has no stable post-settle reads");
2301
+ if (new Set(native.finalReads.map((receipt) => receipt.path)).size !== native.finalReads.length)
2302
+ throw new Error("native sampled end-state receipts repeat an observation");
2303
+ const finalFrames = native.finalReads.map((receipt) => {
2304
+ const rows = parse2(
2305
+ boundText(root, receipt.path, receipt.sha256, "native sampled end state")
2306
+ );
2307
+ if (!rows.length) throw new Error("native sampled end state is unobserved");
2308
+ return rows.at(-1);
2309
+ });
2310
+ const plan = JSON.parse(
2311
+ boundText(
2312
+ root,
2313
+ native.gradingPlan,
2314
+ native.gradingPlanSha256,
2315
+ "native sampled pre-capture stage plan"
2316
+ )
2317
+ );
2318
+ const verifiedPlan = nativeGradingPlan(sourceRoot, plan);
2319
+ const { catalogSha256: capturedCatalogSha256, ...capturedCheckpoint } = plan;
2320
+ const { catalogSha256: currentCatalogSha256, ...verifiedCheckpoint } = verifiedPlan;
2321
+ if (!/^[a-f0-9]{64}$/.test(capturedCatalogSha256 ?? "") || !/^[a-f0-9]{64}$/.test(currentCatalogSha256) || JSON.stringify(verifiedCheckpoint) !== JSON.stringify(capturedCheckpoint) || observations.some((row) => row.gradingPlanSha256 !== native.gradingPlanSha256) || frames.some((row) => row.gradingPlanSha256 !== native.gradingPlanSha256))
2322
+ throw new Error("native sampled stage plan was not bound before acquisition");
2323
+ const resolution = JSON.parse(
2324
+ boundText(
2325
+ root,
2326
+ native.windowResolution,
2327
+ native.windowResolutionSha256,
2328
+ "native sampled window resolution"
2329
+ )
2330
+ );
2331
+ const markers = JSON.parse(
2332
+ boundText(
2333
+ root,
2334
+ native.gradingMarkers,
2335
+ native.gradingMarkersSha256,
2336
+ "native sampled trigger markers"
2337
+ )
2338
+ );
2339
+ const grading = nativeResolvedGradingWindow(plan, resolution, frames, markers);
2340
+ return nativeRefreshSamples({
2341
+ frames,
2342
+ reads: observations,
2343
+ clockBytes,
2344
+ finalFrames,
2345
+ finalSurfaceStatus,
2346
+ grading
2347
+ });
2348
+ }
2349
+ module2.exports.authenticateNativeRefreshSamples = authenticateNativeRefreshSamples;
1760
2350
  async function encodeNativeFramebufferMovie(rawDir, videoPath) {
1761
2351
  const fs3 = require("node:fs");
1762
2352
  const path5 = require("node:path");
@@ -1979,6 +2569,13 @@ var require_attempt_events = __commonJS({
1979
2569
  throw new Error("cadence evidence path escapes its root");
1980
2570
  return resolved;
1981
2571
  }
2572
+ function boundText(root, relative, sha256, label) {
2573
+ const bytes = fs3.readFileSync(proofBoundPath(root, relative));
2574
+ if (proofByteHash(bytes) !== sha256)
2575
+ throw new Error(`${label} does not match its recorded hash`);
2576
+ return bytes.toString("utf8");
2577
+ }
2578
+ module2.exports.boundText = boundText;
1982
2579
  function authenticateDiscreteFrameAsset(root, declaration, calibrate = false) {
1983
2580
  const cadence = parseDiscreteFrameCadence(declaration);
1984
2581
  const fixture = fs3.readFileSync(proofBoundPath(root, cadence.asset.fixturePath));
@@ -2179,6 +2776,10 @@ var require_attempt_events = __commonJS({
2179
2776
  };
2180
2777
  }
2181
2778
  function authenticateNativeCadenceCensus(root, sourceRoot, life, decoded) {
2779
+ const {
2780
+ authenticateNativeRefreshSamples
2781
+ } = require_native_frame_receipt();
2782
+ authenticateNativeRefreshSamples(root, sourceRoot, life);
2182
2783
  const native = life.nativeFramebuffer;
2183
2784
  if (!native || native.exitCode !== 0 || native.failure || native.sourceSha256 !== proofByteHash(
2184
2785
  fs3.readFileSync(
@@ -2186,19 +2787,54 @@ var require_attempt_events = __commonJS({
2186
2787
  )
2187
2788
  ))
2188
2789
  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);
2790
+ const frames = boundText(
2791
+ root,
2792
+ native.receipt,
2793
+ native.receiptSha256,
2794
+ "native cadence source receipt"
2795
+ ).trim().split("\n").map(JSON.parse);
2796
+ const media = boundText(
2797
+ root,
2798
+ native.mediaFrames,
2799
+ native.mediaFramesSha256,
2800
+ "native cadence sampled surface census"
2801
+ ).trim().split("\n").map(JSON.parse);
2191
2802
  if (frames.length !== decoded.observations.length || media.length !== frames.length)
2192
- throw new Error("native cadence acquisition and presentation census differ");
2803
+ throw new Error("native cadence acquisition and sampled surface census differ");
2193
2804
  const {
2194
2805
  decodeNativeFramebuffer,
2195
2806
  requireNativeRawFrame
2196
2807
  } = require_native_frame_receipt();
2197
2808
  const rawRoot = path5.dirname(proofBoundPath(root, native.receipt));
2809
+ const superseded = new Set(
2810
+ boundText(
2811
+ root,
2812
+ native.superseded,
2813
+ native.supersededSha256,
2814
+ "native cadence superseded receipt"
2815
+ ).split("\n").filter(Boolean).map((line) => {
2816
+ const row = JSON.parse(line);
2817
+ if (!Number.isSafeInteger(row.callbackIndex) || row.seedBeforeRead === row.seedAfterRead)
2818
+ throw new Error("native superseded receipt is not an overwritten read");
2819
+ return row.callbackIndex;
2820
+ })
2821
+ );
2822
+ const reads = boundText(
2823
+ root,
2824
+ native.reads,
2825
+ native.readsSha256,
2826
+ "native cadence observations"
2827
+ ).split("\n").filter(Boolean).map(JSON.parse);
2828
+ let expectedCallback = 0;
2829
+ for (const read of reads) {
2830
+ while (superseded.delete(expectedCallback)) expectedCallback++;
2831
+ if (read.callbackIndex !== expectedCallback++)
2832
+ throw new Error("native cadence observations have invalid callback order");
2833
+ }
2198
2834
  const origin = frames[0].consumerArrival;
2199
2835
  for (const [index, frame] of frames.entries()) {
2200
2836
  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)
2837
+ if (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
2838
  throw new Error("native cadence source census has invalid clock or order");
2203
2839
  const pts = (BigInt(clock.machTicks) - BigInt(origin.machTicks)) * BigInt(clock.timebaseNumer) / BigInt(clock.timebaseDenom);
2204
2840
  if (pts < 0n || pts !== BigInt(media[index].ptsNanoseconds) || Math.abs(Number(pts) / 1e6 - decoded.observations[index].atMs) > 1e-3)
@@ -2211,6 +2847,9 @@ var require_attempt_events = __commonJS({
2211
2847
  )
2212
2848
  );
2213
2849
  }
2850
+ while (superseded.delete(expectedCallback)) expectedCallback++;
2851
+ if (superseded.size)
2852
+ throw new Error("native superseded receipts fall outside the callback order");
2214
2853
  }
2215
2854
  function readDiscreteFrameReceipt(root, sourceRoot, reference, expected) {
2216
2855
  if (expected.calibration !== true) assertNormalProofAttempt(root);
@@ -2444,10 +3083,41 @@ var require_attempt_events = __commonJS({
2444
3083
  const life = JSON.parse(
2445
3084
  fs3.readFileSync(proofBoundPath(root, receipt.lifecycle.path), "utf8")
2446
3085
  );
3086
+ for (const [label, auxiliary] of [
3087
+ ["ui frame clock", life.uiFrameClock],
3088
+ ["delivered input", life.deliveredUIKitInput]
3089
+ ]) {
3090
+ if (!auxiliary) continue;
3091
+ const bytes = fs3.readFileSync(proofBoundPath(root, auxiliary.sourceReceipt));
3092
+ if (proofByteHash(bytes) !== auxiliary.sourceReceiptSha256)
3093
+ throw new Error(`native ${label} receipt does not match its recorded hash`);
3094
+ paths.add(auxiliary.sourceReceipt);
3095
+ }
2447
3096
  const native = life.nativeFramebuffer;
2448
3097
  if (native) {
2449
3098
  paths.add(native.receipt);
3099
+ paths.add(native.superseded);
2450
3100
  paths.add(native.mediaFrames);
3101
+ paths.add(native.gradingMarkers);
3102
+ paths.add(native.gradingPlan);
3103
+ paths.add(native.windowResolution);
3104
+ paths.add(native.reads);
3105
+ for (const receipt2 of native.finalReads) {
3106
+ const text = boundText(
3107
+ root,
3108
+ receipt2.path,
3109
+ receipt2.sha256,
3110
+ "native sampled end state"
3111
+ );
3112
+ paths.add(receipt2.path);
3113
+ for (const row of text.split("\n").filter(Boolean).map(JSON.parse)) {
3114
+ const file = proofBoundPath(
3115
+ path5.dirname(proofBoundPath(root, receipt2.path)),
3116
+ row.rawPath
3117
+ );
3118
+ paths.add(path5.relative(root, file).split(path5.sep).join("/"));
3119
+ }
3120
+ }
2451
3121
  const rows = fs3.readFileSync(proofBoundPath(root, native.receipt), "utf8").trim().split("\n").map(JSON.parse);
2452
3122
  for (const row of rows) {
2453
3123
  const file = proofBoundPath(
@@ -6348,6 +7018,11 @@ function sameNativeAccessibilityShape(a, b, bounds) {
6348
7018
  return true;
6349
7019
  }
6350
7020
 
7021
+ // src/is-record.ts
7022
+ function isRecord(value) {
7023
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7024
+ }
7025
+
6351
7026
  // cli/commands/inspect/core.ts
6352
7027
  function layoutBoundFontFamily(style) {
6353
7028
  const raw = typeof style === "object" && style !== null ? style.fontFamily : void 0;
@@ -7362,9 +8037,12 @@ var AUTOMATION_SIM_IDLE_REAP_TTL_MS = 5 * 6e4;
7362
8037
  // src/home-paths.ts
7363
8038
  var DAEMON_LOCKFILE_MAX_BYTES = 16 * 1024;
7364
8039
 
7365
- // ../peach-engine/src/tenant-bundle-mode.ts
8040
+ // ../peach-engine/src/preview/bundle-layout.ts
7366
8041
  var import_metro_production_bundle = require("peachsim/metro-production-bundle");
7367
8042
 
8043
+ // ../peach-engine/src/tenant-bundle-mode.ts
8044
+ var import_metro_production_bundle2 = require("peachsim/metro-production-bundle");
8045
+
7368
8046
  // ../peach-engine/src/tenant-runtime-environment.ts
7369
8047
  function readBrowserLocation() {
7370
8048
  try {
@@ -7843,7 +8521,7 @@ var PEACH_UPSTREAM_MODULES = {
7843
8521
  };
7844
8522
 
7845
8523
  // ../peach-engine/src/metro-fingerprint.ts
7846
- var import_metro_production_bundle2 = require("peachsim/metro-production-bundle");
8524
+ var import_metro_production_bundle3 = require("peachsim/metro-production-bundle");
7847
8525
  var import_react_native_host_modules = require("peachsim/react-native-host-modules");
7848
8526
 
7849
8527
  // src/cloud-contract.ts
@@ -10636,6 +11314,10 @@ function setPauseAwareTimeout(fn, budgetMs, hostPid) {
10636
11314
  }
10637
11315
  return () => {
10638
11316
  timers.delete(timer);
11317
+ if (timers.size === 0 && poll) {
11318
+ clearInterval(poll);
11319
+ poll = void 0;
11320
+ }
10639
11321
  };
10640
11322
  }
10641
11323
 
@@ -10674,7 +11356,10 @@ var BRIDGE_VALUE_FLAGS_TABLE = [
10674
11356
  {
10675
11357
  names: ["--timeout"],
10676
11358
  apply: (state, value) => {
10677
- if (value !== void 0) state.commandTimeoutMs = Number(value);
11359
+ if (value !== void 0) {
11360
+ state.commandTimeoutMs = Number(value);
11361
+ state.explicitTimeout = true;
11362
+ }
10678
11363
  }
10679
11364
  },
10680
11365
  {
@@ -10814,7 +11499,7 @@ function createBridge(wsPort, opts = {}) {
10814
11499
  return {
10815
11500
  plane: "local",
10816
11501
  async send(cmd, callOpts) {
10817
- const effectiveTimeoutMs = callOpts?.timeoutMs ?? commandTimeoutMs;
11502
+ const effectiveTimeoutMs = callOpts?.timeoutMs ?? (opts.explicitTimeout ? commandTimeoutMs : callOpts?.defaultTimeoutMs) ?? commandTimeoutMs;
10818
11503
  const sendOnce = async (defaultSimId, cmdOverride) => {
10819
11504
  await ready;
10820
11505
  if (terminalError) throw terminalError;
@@ -10827,11 +11512,7 @@ function createBridge(wsPort, opts = {}) {
10827
11512
  const clearTimeoutFn = setPauseAwareTimeout(
10828
11513
  () => {
10829
11514
  pending.delete(id);
10830
- reject(
10831
- new Error(
10832
- `command timed out after ${Math.round(effectiveTimeoutMs / 1e3)}s`
10833
- )
10834
- );
11515
+ reject(new Error(`command timed out after ${effectiveTimeoutMs / 1e3}s`));
10835
11516
  },
10836
11517
  effectiveTimeoutMs,
10837
11518
  lineageBySimId.get(payload.simId)?.hostPid
@@ -11064,25 +11745,8 @@ function usesDirectLookup(matcher) {
11064
11745
  return matcher.regexFlags === void 0 && matcher.index === void 0 && matcher.type !== "text";
11065
11746
  }
11066
11747
 
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
- });
11748
+ // ../peach-engine/src/settings/device-spec-bus.ts
11749
+ init_adapter();
11086
11750
 
11087
11751
  // ../peach-engine/src/settings/dynamic-type.ts
11088
11752
  function textMetrics(fontSize, lineHeight, leading, ascent) {
@@ -12097,8 +12761,94 @@ async function dispatchTwoFingerGesture(page, startA, startB, endA, endB, option
12097
12761
  await page.waitForTimeout(250);
12098
12762
  }
12099
12763
 
12764
+ // ../contrast-runtime-delivery/src/version.ts
12765
+ function isValidRuntimeVersion(value) {
12766
+ return value.length <= 100 && /^[0-9A-Za-z][0-9A-Za-z._+-]*$/.test(value);
12767
+ }
12768
+
12769
+ // src/nativeViewSourceContract.ts
12770
+ function isNativeViewSource(value) {
12771
+ if (!isRecord(value)) return false;
12772
+ if (value.type === "placeholder")
12773
+ return (value.uri === void 0 || typeof value.uri === "string" && value.uri.length > 0) && (value.color === void 0 || typeof value.color === "string");
12774
+ if (value.type === "web") {
12775
+ if (typeof value.uri !== "string") return false;
12776
+ try {
12777
+ const url = new URL(value.uri);
12778
+ return url.protocol === "http:" || url.protocol === "https:";
12779
+ } catch {
12780
+ return false;
12781
+ }
12782
+ }
12783
+ return value.type === "video" && typeof value.uri === "string" && value.uri.length > 0 && ["paused", "loop", "muted"].every(
12784
+ (key) => value[key] === void 0 || typeof value[key] === "boolean"
12785
+ ) && (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");
12786
+ }
12787
+
12788
+ // src/config.ts
12789
+ function readNativeRenderer2(value) {
12790
+ if (value === void 0) return void 0;
12791
+ if (value === "paper" || value === "fabric") return value;
12792
+ throw new Error("nativeRenderer must be paper or fabric");
12793
+ }
12794
+ function isPackageStubs(value) {
12795
+ if (value === "noop") return true;
12796
+ if (!isRecord(value)) return false;
12797
+ return (value.modules === void 0 || isRecord(value.modules) && Object.values(value.modules).every(
12798
+ (stub) => typeof stub === "string" && stub.length > 0
12799
+ )) && (value.views === void 0 || isRecord(value.views) && Object.values(value.views).every(
12800
+ (stub) => typeof stub === "string" && stub.length > 0 || isNativeViewSource(stub)
12801
+ ));
12802
+ }
12803
+ var PEACH_CONFIG_QUERY_PARAM = "peachConfig";
12804
+ function isPeachConfig(value) {
12805
+ if (!isRecord(value)) return false;
12806
+ 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)) && [
12807
+ "modules",
12808
+ "packages",
12809
+ "turboModules",
12810
+ "nativeModules",
12811
+ "nativeViews",
12812
+ "env",
12813
+ "network",
12814
+ "settings",
12815
+ "initialProps",
12816
+ "initialState"
12817
+ ].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(
12818
+ (entry) => entry === "noop" || isNativeViewSource(entry) || isRecord(entry) && typeof entry.file === "string" && entry.file.length > 0
12819
+ ));
12820
+ }
12821
+ function hasOwnKeys(value) {
12822
+ return !!value && Object.keys(value).length > 0;
12823
+ }
12824
+ function hasPeachConfig(config) {
12825
+ if (!config) return false;
12826
+ 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);
12827
+ }
12828
+ function readPeachConfigFromSearchParams(searchParams) {
12829
+ const raw = searchParams.get(PEACH_CONFIG_QUERY_PARAM);
12830
+ if (!raw) return void 0;
12831
+ let parsed;
12832
+ try {
12833
+ parsed = JSON.parse(raw);
12834
+ } catch (error) {
12835
+ const message = error instanceof Error ? error.message : String(error);
12836
+ console.warn(`[peach] invalid ${PEACH_CONFIG_QUERY_PARAM}: ${message}`);
12837
+ return void 0;
12838
+ }
12839
+ if (isRecord(parsed)) readNativeRenderer2(parsed.nativeRenderer);
12840
+ return isPeachConfig(parsed) && hasPeachConfig(parsed) ? parsed : void 0;
12841
+ }
12842
+
12100
12843
  // detox/navigation-lifecycle.ts
12101
12844
  var import_attempt_events = __toESM(require_attempt_events(), 1);
12845
+ var developerToken = readPeachConfigFromSearchParams(
12846
+ new URL(process.env.PEACH_URL || "http://localhost").searchParams
12847
+ )?.mapKit?.token;
12848
+ function redactDiagnosticText(text) {
12849
+ const withoutConfig = text.replace(/([?&]peachConfig=)[^&\s"'<>]+/g, "$1[redacted]");
12850
+ return developerToken ? withoutConfig.replaceAll(developerToken, "[redacted]") : withoutConfig;
12851
+ }
12102
12852
  var DIAGNOSTIC_LIMIT = 40;
12103
12853
  var pageDiagnostics = /* @__PURE__ */ new WeakMap();
12104
12854
  var activeNavigations = /* @__PURE__ */ new WeakMap();
@@ -12111,6 +12861,21 @@ function persistDiagnostic(diagnostic) {
12111
12861
  (0, import_attempt_events.writeAttemptEvent)(diagnostic);
12112
12862
  }
12113
12863
  function recordPageDiagnostic(page, diagnostic, persist = false) {
12864
+ if ("url" in diagnostic) diagnostic.url = redactDiagnosticText(diagnostic.url);
12865
+ if ("navigationUrl" in diagnostic)
12866
+ diagnostic.navigationUrl = redactDiagnosticText(diagnostic.navigationUrl);
12867
+ if ("message" in diagnostic)
12868
+ diagnostic.message = redactDiagnosticText(diagnostic.message);
12869
+ if ("errorText" in diagnostic && diagnostic.errorText)
12870
+ diagnostic.errorText = redactDiagnosticText(diagnostic.errorText);
12871
+ if ("pageState" in diagnostic && diagnostic.pageState) {
12872
+ if ("url" in diagnostic.pageState)
12873
+ diagnostic.pageState.url = redactDiagnosticText(diagnostic.pageState.url);
12874
+ if ("evaluateError" in diagnostic.pageState)
12875
+ diagnostic.pageState.evaluateError = redactDiagnosticText(
12876
+ diagnostic.pageState.evaluateError
12877
+ );
12878
+ }
12114
12879
  const entries = pageDiagnostics.get(page);
12115
12880
  if (!entries) return;
12116
12881
  entries.push(diagnostic);
@@ -12217,7 +12982,7 @@ function attachPageDiagnostics(page) {
12217
12982
  }
12218
12983
  async function readSootsimPageState(page) {
12219
12984
  try {
12220
- return await page.evaluate(() => {
12985
+ const state = await page.evaluate(() => {
12221
12986
  const bootMarks = Reflect.get(window, "__sootsimBootMarks");
12222
12987
  return {
12223
12988
  bodyText: document.body?.innerText?.slice(0, 500) ?? "",
@@ -12234,6 +12999,8 @@ async function readSootsimPageState(page) {
12234
12999
  url: window.location.href
12235
13000
  };
12236
13001
  });
13002
+ state.url = redactDiagnosticText(state.url);
13003
+ return state;
12237
13004
  } catch (error) {
12238
13005
  return {
12239
13006
  evaluateError: error instanceof Error ? error.message : String(error)
@@ -12258,14 +13025,14 @@ async function describeSootsimBridgeFailure(page, cause, summary = "timed out wa
12258
13025
  `page: ${JSON.stringify(state)}`,
12259
13026
  `recent page diagnostics:
12260
13027
  ${JSON.stringify(getSootsimNavigationDiagnostics(page))}`
12261
- ].join("\n")
13028
+ ].map(redactDiagnosticText).join("\n")
12262
13029
  );
12263
13030
  }
12264
13031
  async function navigateSootsimPage(page, url, identity) {
12265
13032
  attachPageDiagnostics(page);
12266
13033
  const navigation = {
12267
13034
  ...identity,
12268
- navigationUrl: url,
13035
+ navigationUrl: redactDiagnosticText(url),
12269
13036
  navigationId: ++nextNavigationIdentity
12270
13037
  };
12271
13038
  activeNavigations.set(page, navigation);
@@ -12318,12 +13085,13 @@ function visibleFraction(node) {
12318
13085
  function isNodeVisible(node) {
12319
13086
  return visibleFraction(node) >= DEFAULT_VISIBILITY_THRESHOLD;
12320
13087
  }
12321
- function focusIsWithin(focused, nodeId) {
12322
- return focused.nodeId === nodeId || focused.ancestorNodeIds.includes(nodeId);
13088
+ function focusIsWithin(focused, node) {
13089
+ const nativeNodeId = node.nativeTag ?? node.nodeId;
13090
+ return focused.nodeId === nativeNodeId || focused.ancestorNodeIds.includes(nativeNodeId);
12323
13091
  }
12324
13092
  async function isTextInputNodeFocused(node, page) {
12325
13093
  const focused = await page.evaluate(async () => window.__sootsimTest.getFocusedNode());
12326
- return focused !== null && focusIsWithin(focused, node.nodeId);
13094
+ return focused !== null && focusIsWithin(focused, node);
12327
13095
  }
12328
13096
  async function isNodeFocused(node, page) {
12329
13097
  if (node.isTextInput) return isTextInputNodeFocused(node, page);
@@ -12766,6 +13534,7 @@ var by = {
12766
13534
  // detox/index.ts
12767
13535
  var import_motion_change = __toESM(require_motion_change(), 1);
12768
13536
  var import_proof_frame = __toESM(require_proof_frame(), 1);
13537
+ var WORKLET_FRAME_TRACE_LIMIT = 16384;
12769
13538
  var BASE_URL = process.env.PEACH_URL || "http://localhost:5173";
12770
13539
  var SCREENSHOT_DIR = process.env.PEACH_SCREENSHOT_DIR || path4.join(process.cwd(), "test", "detox-driver", "screenshots", String(process.pid));
12771
13540
  var _browser = null;
@@ -12773,6 +13542,7 @@ var _browserLaunch = null;
12773
13542
  var _browserLaunchAbort = null;
12774
13543
  var _reapBrowserNow = null;
12775
13544
  var _context = null;
13545
+ var _appStorageState;
12776
13546
  var _page = null;
12777
13547
  var _synchronizationEnabled = false;
12778
13548
  var _proofFrameObservationActive = false;
@@ -12968,14 +13738,17 @@ async function waitForSootsimWorkletsIdle(page, timeout = 3e3) {
12968
13738
  let readStartedAt = null;
12969
13739
  let tenantAtRead = window.__sootsimRenderHost;
12970
13740
  let shellAtRead = window.SootSim?.bridges?.shellHost;
13741
+ let peerProgress = null;
12971
13742
  let deadlineTimerLateMs = null;
12972
13743
  while (Date.now() < deadline) {
12973
13744
  readStartedAt = Date.now();
12974
13745
  tenantAtRead = window.__sootsimRenderHost;
12975
13746
  shellAtRead = window.SootSim?.bridges?.shellHost;
12976
13747
  let timer;
13748
+ const peerRead = getPeerStats(false);
13749
+ peerProgress = peerRead && "peachPeerProgress" in peerRead ? peerRead.peachPeerProgress ?? null : null;
12977
13750
  const stats = await Promise.race([
12978
- getPeerStats(false),
13751
+ peerRead,
12979
13752
  new Promise((resolve3) => {
12980
13753
  timer = setTimeout(
12981
13754
  () => {
@@ -13012,6 +13785,7 @@ async function waitForSootsimWorkletsIdle(page, timeout = 3e3) {
13012
13785
  outstandingRead: readStartedAt === null ? null : {
13013
13786
  startedAtMs: readStartedAt - start,
13014
13787
  elapsedMs: Date.now() - readStartedAt,
13788
+ peerProgress: peerProgress ? { ...peerProgress } : null,
13015
13789
  testBridgeChanged: window.__sootsimTest !== testBridge,
13016
13790
  tenant: {
13017
13791
  presentAtRequest: !!tenantAtRead,
@@ -13037,8 +13811,12 @@ async function waitForSootsimWorkletsIdle(page, timeout = 3e3) {
13037
13811
  );
13038
13812
  if (!result?.settled) {
13039
13813
  const pending = result?.pending ?? "no peer stats reply";
13040
- throw new Error(
13041
- `sootsim worklet runtime did not settle after ${result?.elapsed ?? timeout}ms (pending=${pending}; observation=${JSON.stringify(result?.observation)})`
13814
+ throw await describeSootsimBridgeFailure(
13815
+ page,
13816
+ new Error(
13817
+ `sootsim worklet runtime did not settle after ${result?.elapsed ?? timeout}ms (pending=${pending}; observation=${JSON.stringify(result?.observation)})`
13818
+ ),
13819
+ "sootsim worklet runtime did not settle"
13042
13820
  );
13043
13821
  }
13044
13822
  }
@@ -13092,7 +13870,8 @@ async function quiesceContextMedia(context) {
13092
13870
  if (timer !== void 0) clearTimeout(timer);
13093
13871
  }
13094
13872
  }
13095
- async function closeContext() {
13873
+ async function closeContext(preserveStorage) {
13874
+ if (!preserveStorage) _appStorageState = void 0;
13096
13875
  if (_browserCompromised) {
13097
13876
  throw new Error(`sootsim browser is compromised: ${_browserCompromised}`);
13098
13877
  }
@@ -13100,6 +13879,13 @@ async function closeContext() {
13100
13879
  const context = _context;
13101
13880
  const closingContextId = contextIdentity;
13102
13881
  try {
13882
+ if (preserveStorage) {
13883
+ _appStorageState = await withLifecycleTimeout(
13884
+ "context.storageState()",
13885
+ closingContextId,
13886
+ () => context.storageState({ indexedDB: true })
13887
+ );
13888
+ }
13103
13889
  await quiesceContextMedia(context);
13104
13890
  await withLifecycleTimeout("context.close()", closingContextId, () => context.close());
13105
13891
  _context = null;
@@ -13113,7 +13899,8 @@ async function closeContext() {
13113
13899
  throw error;
13114
13900
  }
13115
13901
  }
13116
- async function closeBrowser() {
13902
+ async function closeBrowser(preserveStorage) {
13903
+ if (!preserveStorage) _appStorageState = void 0;
13117
13904
  if (!_browser) return;
13118
13905
  const browser = _browser;
13119
13906
  const reapBrowserNow = _reapBrowserNow;
@@ -13122,7 +13909,7 @@ async function closeBrowser() {
13122
13909
  browserIdentity = 0;
13123
13910
  let closeError = null;
13124
13911
  try {
13125
- await closeContext();
13912
+ await closeContext(preserveStorage);
13126
13913
  } catch (error) {
13127
13914
  closeError = error instanceof Error ? error : new Error(String(error));
13128
13915
  }
@@ -13358,7 +14145,7 @@ async function focusElementForTextEntry(el) {
13358
14145
  await el.tap();
13359
14146
  await pollUntil(
13360
14147
  () => page.evaluate(async () => window.__sootsimTest.getFocusedNode()),
13361
- (focused) => focused !== null && focusIsWithin(focused, node.nodeId),
14148
+ (focused) => focused !== null && focusIsWithin(focused, node),
13362
14149
  {
13363
14150
  timeoutMs: 5e3,
13364
14151
  label: `SootSim text input ${node.nodeId} to focus`
@@ -13747,10 +14534,11 @@ function createSootSimElement(matcher) {
13747
14534
  }
13748
14535
  },
13749
14536
  async swipe(direction, speed = "fast", percentage = 0.75, normalizedStartingPointX = 0.5, normalizedStartingPointY = 0.5) {
14537
+ const page = getPage();
14538
+ if (_synchronizationEnabled) await waitForSootsimIdle2(page, 3e3, false, 0);
13750
14539
  const node = await findStableNodeByMatcher(matcher, "swipe");
13751
14540
  if (!node)
13752
14541
  throw new Error(`element not found for swipe: ${JSON.stringify(matcher)}`);
13753
- const page = getPage();
13754
14542
  const { absolutePosition, layout } = node;
13755
14543
  const startSootSimX = absolutePosition.x + layout.width * normalizedStartingPointX;
13756
14544
  const startSootSimY = absolutePosition.y + layout.height * normalizedStartingPointY;
@@ -14127,10 +14915,11 @@ var device = {
14127
14915
  }
14128
14916
  }
14129
14917
  if (!_page || newInstance || opts?.delete || opts?.recordVideoDir) {
14130
- await closeContext();
14918
+ await closeContext(!opts?.delete);
14131
14919
  const contextOpts = {
14132
14920
  hasTouch: true,
14133
- viewport: { width: 500, height: 900 }
14921
+ viewport: { width: 500, height: 900 },
14922
+ storageState: _appStorageState
14134
14923
  };
14135
14924
  if (opts?.recordVideoDir) {
14136
14925
  fs2.mkdirSync(opts.recordVideoDir, { recursive: true });
@@ -14235,11 +15024,12 @@ var device = {
14235
15024
  await waitForSootsimWorkletsIdle(page, 3e3);
14236
15025
  },
14237
15026
  async terminateApp() {
14238
- await closeBrowser();
15027
+ await closeBrowser(true);
14239
15028
  },
14240
15029
  async installApp() {
14241
15030
  },
14242
15031
  async uninstallApp() {
15032
+ await closeBrowser(false);
14243
15033
  },
14244
15034
  async openURL(url) {
14245
15035
  const page = getPage();
@@ -14616,6 +15406,14 @@ var device = {
14616
15406
  definition: import_motion_change.MOTION_CHANGE_DEFINITION,
14617
15407
  installGlobal: true
14618
15408
  });
15409
+ if (options.workletFrameTracePath) {
15410
+ await page.evaluate(async (limit) => {
15411
+ const enable = window.SootSim?.bridges?.test?.setShellReanimatedTraceEnabled;
15412
+ if (typeof enable !== "function")
15413
+ throw new Error("worklet trace bridge is absent");
15414
+ await enable(true, true, limit);
15415
+ }, WORKLET_FRAME_TRACE_LIMIT);
15416
+ }
14619
15417
  const capturePromise = page.evaluate(
14620
15418
  async (input) => {
14621
15419
  const captureWindow = window;
@@ -14707,68 +15505,83 @@ var device = {
14707
15505
  if (input.hold && (!interact || typeof interact.touchDown !== "function")) {
14708
15506
  throw new Error("peach interact bridge is not installed");
14709
15507
  }
14710
- const controller = await lockstep.begin(60);
14711
- const moves = input.hold?.moves ?? [];
14712
- const holdState = {
14713
- nextMove: 0,
14714
- pressFrameIndex: null,
14715
- releaseReceipt: null,
14716
- released: false,
14717
- x: input.hold?.x ?? 0,
14718
- y: input.hold?.y ?? 0
14719
- };
14720
- const holdEventFrameIndex = (afterMs) => (holdState.pressFrameIndex ?? 0) + controller.framesForOffset(afterMs);
14721
- const nextHoldEventFrameIndex = () => {
14722
- if (!input.hold || holdState.released || holdState.pressFrameIndex === null) {
14723
- return null;
14724
- }
14725
- const move = moves[holdState.nextMove];
14726
- return holdEventFrameIndex(move ? move.afterMs : input.hold.durationMs);
14727
- };
14728
- const advanceHold = async (receipt) => {
14729
- if (!input.hold || !interact || holdState.released) return;
14730
- if (holdState.pressFrameIndex === null) {
14731
- holdState.pressFrameIndex = receipt.frameIndex - 1;
14732
- }
14733
- for (let move = moves[holdState.nextMove]; move && receipt.frameIndex >= holdEventFrameIndex(move.afterMs); move = moves[holdState.nextMove]) {
14734
- holdState.nextMove++;
14735
- holdState.x = move.x;
14736
- holdState.y = move.y;
14737
- await interact.touchMove(
14738
- move.x,
14739
- move.y,
14740
- 999,
14741
- controller.initialFrame.virtualTimestampMs + move.afterMs
15508
+ const pairedPeerStats = async (reset = false) => {
15509
+ const stats = await captureWindow.SootSim?.bridges?.test?.getPeerStats?.(reset);
15510
+ if (!stats || !Number.isSafeInteger(stats.replayedFrameCallbacks))
15511
+ throw new Error(
15512
+ "paired worklets clock needs native frame delivery diagnostics"
14742
15513
  );
14743
- }
14744
- if (receipt.frameIndex < holdEventFrameIndex(input.hold.durationMs)) return;
14745
- holdState.released = true;
14746
- holdState.releaseReceipt = receipt;
14747
- await interact.touchUp(
14748
- holdState.x,
14749
- holdState.y,
14750
- 999,
14751
- controller.initialFrame.virtualTimestampMs + input.hold.durationMs
14752
- );
15514
+ return stats;
14753
15515
  };
15516
+ if (input.workletFrameTimestampsSeconds) await pairedPeerStats(true);
15517
+ const controller = await lockstep.begin(60, {
15518
+ workletFrameTimestampsSeconds: input.workletFrameTimestampsSeconds
15519
+ });
15520
+ let heldInput = null;
15521
+ const releaseReceipt = () => controller.inputReceipts.find((receipt) => receipt.phase === "up");
14754
15522
  const keptFrames = /* @__PURE__ */ new Map();
15523
+ const keepFrame = (receipt, bitmap) => {
15524
+ keptFrames.get(receipt.frameIndex)?.bitmap.close();
15525
+ keptFrames.set(receipt.frameIndex, { bitmap, receipt });
15526
+ };
14755
15527
  try {
15528
+ if (input.anchorAtTouch && !input.hold) {
15529
+ throw new Error("peach touch anchoring needs a hold");
15530
+ }
14756
15531
  const baselineBitmap = await captureBitmap();
14757
15532
  const baseline = regionPixels(baselineBitmap);
14758
- baselineBitmap.close();
15533
+ if (input.anchorAtTouch === "down")
15534
+ keepFrame(controller.initialFrame, baselineBitmap);
15535
+ else baselineBitmap.close();
15536
+ if (input.workletFrameTimestampsSeconds && (await pairedPeerStats()).replayedFrameCallbacks > 0)
15537
+ throw new Error("paired worklets callback preceded the Peach trigger");
14759
15538
  state.ready = true;
14760
15539
  if (input.hold && interact) {
14761
- const hit = await interact.touchDown(
14762
- input.hold.x,
14763
- input.hold.y,
14764
- 999,
14765
- controller.initialFrame.virtualTimestampMs
14766
- );
14767
- if (!hit) {
14768
- throw new Error(
14769
- `peach motion hold missed at ${input.hold.x},${input.hold.y}`
14770
- );
14771
- }
15540
+ const hold = input.hold;
15541
+ const moves = hold.moves ?? [];
15542
+ const release = hold.release ?? moves.at(-1) ?? hold;
15543
+ const events = [
15544
+ {
15545
+ phase: "down",
15546
+ x: hold.x,
15547
+ y: hold.y,
15548
+ pointerId: 999,
15549
+ afterMs: 0,
15550
+ async deliver(timestamp2) {
15551
+ const hit = await interact.touchDown(hold.x, hold.y, 999, timestamp2);
15552
+ if (!hit)
15553
+ throw new Error(`peach motion hold missed at ${hold.x},${hold.y}`);
15554
+ }
15555
+ },
15556
+ ...moves.map(
15557
+ (move) => ({
15558
+ phase: "move",
15559
+ x: move.x,
15560
+ y: move.y,
15561
+ pointerId: 999,
15562
+ afterMs: move.afterMs,
15563
+ eventAfterMs: move.eventAfterMs,
15564
+ async deliver(timestamp2) {
15565
+ await interact.touchMove(move.x, move.y, 999, timestamp2);
15566
+ }
15567
+ })
15568
+ ),
15569
+ {
15570
+ phase: "up",
15571
+ x: release.x,
15572
+ y: release.y,
15573
+ pointerId: 999,
15574
+ afterMs: hold.durationMs,
15575
+ eventAfterMs: hold.eventDurationMs,
15576
+ async deliver(timestamp2) {
15577
+ await interact.touchUp(release.x, release.y, 999, timestamp2);
15578
+ }
15579
+ }
15580
+ ];
15581
+ heldInput = controller.scheduleInput(events);
15582
+ heldInput.catch(() => {
15583
+ });
15584
+ await controller.flushFrame();
14772
15585
  } else {
14773
15586
  while (!state.triggerStarted) {
14774
15587
  if (state.cancelled) throw new Error("peach motion capture was cancelled");
@@ -14793,7 +15606,7 @@ var device = {
14793
15606
  if (measurement.passedMinimum) {
14794
15607
  visibleStart = receipt;
14795
15608
  detectedMeasurement = measurement;
14796
- keptFrames.set(visibleStart.frameIndex, candidateBitmap);
15609
+ keepFrame(visibleStart, candidateBitmap);
14797
15610
  } else {
14798
15611
  candidateBitmap.close();
14799
15612
  }
@@ -14802,10 +15615,9 @@ var device = {
14802
15615
  for (let frame = 0; !visibleStart && frame < detectionFrameLimit; frame++) {
14803
15616
  if (state.cancelled) throw new Error("peach motion capture was cancelled");
14804
15617
  const receipt = await controller.stepFrame();
14805
- await advanceHold(receipt);
14806
15618
  const candidateBitmap = await captureBitmap();
14807
- if (input.anchorAtRelease && holdState.releaseReceipt === receipt) {
14808
- keptFrames.set(receipt.frameIndex, await createImageBitmap(candidateBitmap));
15619
+ if (input.anchorAtTouch === "down" || input.anchorAtTouch === "up" && releaseReceipt()?.frameIndex === receipt.frameIndex) {
15620
+ keepFrame(receipt, await createImageBitmap(candidateBitmap));
14809
15621
  }
14810
15622
  const bounds = boundsFor(candidateBitmap);
14811
15623
  const measurement = measureChange({
@@ -14821,8 +15633,7 @@ var device = {
14821
15633
  previousBitmap = null;
14822
15634
  visibleStart = receipt;
14823
15635
  detectedMeasurement = measurement;
14824
- keptFrames.get(receipt.frameIndex)?.close();
14825
- keptFrames.set(receipt.frameIndex, candidateBitmap);
15636
+ keepFrame(receipt, candidateBitmap);
14826
15637
  break;
14827
15638
  }
14828
15639
  previousBitmap?.close();
@@ -14836,23 +15647,19 @@ var device = {
14836
15647
  }
14837
15648
  let driveStart = visibleStart;
14838
15649
  let currentReceipt = visibleStart;
14839
- if (input.anchorAtRelease) {
14840
- if (!input.hold) {
14841
- throw new Error("peach release anchoring needs a hold");
14842
- }
14843
- while (!holdState.released) {
15650
+ if (input.anchorAtTouch === "down") driveStart = controller.initialFrame;
15651
+ if (input.anchorAtTouch === "up") {
15652
+ while (!releaseReceipt()) {
14844
15653
  if (state.cancelled) throw new Error("peach motion capture was cancelled");
14845
15654
  const receipt = await controller.stepFrame();
14846
- await advanceHold(receipt);
14847
15655
  currentReceipt = receipt;
14848
- if (holdState.released) {
14849
- keptFrames.set(receipt.frameIndex, await captureBitmap());
14850
- }
15656
+ if (releaseReceipt()) keepFrame(receipt, await captureBitmap());
14851
15657
  }
14852
- if (!holdState.releaseReceipt) {
15658
+ const released = releaseReceipt();
15659
+ if (!released) {
14853
15660
  throw new Error("peach release anchoring found no release frame");
14854
15661
  }
14855
- driveStart = holdState.releaseReceipt;
15662
+ driveStart = { ...released, tenantFrame: null };
14856
15663
  }
14857
15664
  const resolvedAnchors = /* @__PURE__ */ new Map();
14858
15665
  const anchorRecords = [];
@@ -14877,7 +15684,6 @@ var device = {
14877
15684
  while (searchReceipt.virtualTimestampMs < searchStartVts) {
14878
15685
  if (state.cancelled) throw new Error("peach motion capture was cancelled");
14879
15686
  searchReceipt = await controller.stepFrame();
14880
- await advanceHold(searchReceipt);
14881
15687
  }
14882
15688
  currentReceipt = searchReceipt;
14883
15689
  let anchorBitmap = await captureBitmap();
@@ -14889,7 +15695,6 @@ var device = {
14889
15695
  for (let frame = 0; frame < detectionFrameLimit; frame++) {
14890
15696
  if (state.cancelled) throw new Error("peach motion capture was cancelled");
14891
15697
  const receipt = await controller.stepFrame();
14892
- await advanceHold(receipt);
14893
15698
  const candidateBitmap = await captureBitmap();
14894
15699
  const bounds = boundsFor(candidateBitmap);
14895
15700
  const candidate = regionPixels(candidateBitmap);
@@ -14926,8 +15731,7 @@ var device = {
14926
15731
  `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
15732
  );
14928
15733
  }
14929
- keptFrames.get(anchorReceipt.frameIndex)?.close();
14930
- keptFrames.set(anchorReceipt.frameIndex, anchorBitmap);
15734
+ keepFrame(anchorReceipt, anchorBitmap);
14931
15735
  resolvedAnchors.set(anchor.id, anchorReceipt);
14932
15736
  anchorRecords.push({
14933
15737
  changedPixelPercent: onsetMeasurement.changePercent,
@@ -14946,21 +15750,17 @@ var device = {
14946
15750
  const kept = keptFrames.get(stageFrameIndex);
14947
15751
  if (kept && stageFrameIndex <= currentReceipt.frameIndex) {
14948
15752
  snapshots.push({
14949
- actualMs: 0,
15753
+ actualMs: kept.receipt.virtualTimestampMs - anchorReceipt.virtualTimestampMs,
14950
15754
  anchorId,
14951
- bitmap: await createImageBitmap(kept),
14952
- frameIndex: anchorReceipt.frameIndex,
15755
+ bitmap: await createImageBitmap(kept.bitmap),
15756
+ frameIndex: kept.receipt.frameIndex,
14953
15757
  label: stage.label,
14954
15758
  offsetMs: stage.offsetMs,
14955
- virtualTimestampMs: anchorReceipt.virtualTimestampMs
15759
+ virtualTimestampMs: kept.receipt.virtualTimestampMs
14956
15760
  });
14957
15761
  continue;
14958
15762
  }
14959
- for (let eventFrameIndex = nextHoldEventFrameIndex(); eventFrameIndex !== null && stageFrameIndex > eventFrameIndex; eventFrameIndex = nextHoldEventFrameIndex()) {
14960
- await advanceHold(await controller.stepFrame());
14961
- }
14962
15763
  const receipt = await controller.stepToOffset(anchorReceipt, stage.offsetMs);
14963
- await advanceHold(receipt);
14964
15764
  currentReceipt = receipt;
14965
15765
  snapshots.push({
14966
15766
  actualMs: receipt.virtualTimestampMs - anchorReceipt.virtualTimestampMs,
@@ -14972,12 +15772,13 @@ var device = {
14972
15772
  virtualTimestampMs: receipt.virtualTimestampMs
14973
15773
  });
14974
15774
  }
15775
+ if (input.workletFrameTimestampsSeconds) controller.endWorkletFrameReceipt();
14975
15776
  let releasedOffsetMs = 0;
14976
- while (nextHoldEventFrameIndex() !== null) {
15777
+ while (input.hold && !releaseReceipt()) {
14977
15778
  const receipt = await controller.stepFrame();
14978
- await advanceHold(receipt);
14979
15779
  releasedOffsetMs = receipt.virtualTimestampMs - driveStart.virtualTimestampMs;
14980
15780
  }
15781
+ await heldInput;
14981
15782
  if (input.settleMs > 0) {
14982
15783
  const maximumPrimaryMs = snapshots.reduce(
14983
15784
  (maximum, snapshot2) => Math.max(
@@ -14993,6 +15794,17 @@ var device = {
14993
15794
  await controller.stepFrame();
14994
15795
  await new Promise((resolve3) => setTimeout(resolve3, 0));
14995
15796
  }
15797
+ if (input.workletFrameTimestampsSeconds) {
15798
+ const stats = await pairedPeerStats();
15799
+ const first = stats.nativeFrameFirstTimestampMs;
15800
+ const firstFrameIndex = first === null ? null : Math.round(
15801
+ (first - controller.initialFrame.virtualTimestampMs) / (1e3 / 60)
15802
+ );
15803
+ if (firstFrameIndex !== 1)
15804
+ throw new Error(
15805
+ `paired worklets first delivery did not match the next UI frame: ${firstFrameIndex}`
15806
+ );
15807
+ }
14996
15808
  return {
14997
15809
  inputReceipts: controller.inputReceipts,
14998
15810
  anchors: anchorRecords,
@@ -15013,15 +15825,16 @@ var device = {
15013
15825
  }
15014
15826
  };
15015
15827
  } finally {
15016
- for (const bitmap of keptFrames.values()) bitmap.close();
15828
+ for (const kept of keptFrames.values()) kept.bitmap.close();
15017
15829
  controller.end();
15018
15830
  delete registry[input.token];
15019
15831
  }
15020
15832
  },
15021
15833
  {
15834
+ workletFrameTimestampsSeconds: options.workletFrameTimestampsSeconds,
15022
15835
  detectionTimeoutMs: options.detectionTimeoutMs ?? 3e3,
15023
15836
  frame: options.frame ?? null,
15024
- anchorAtRelease: options.anchorAtRelease ?? false,
15837
+ anchorAtTouch: options.anchorAtTouch ?? null,
15025
15838
  hold: options.hold ?? null,
15026
15839
  motionChangeDefinition: import_motion_change.MOTION_CHANGE_DEFINITION,
15027
15840
  platform: device.getPlatform(),
@@ -15031,17 +15844,24 @@ var device = {
15031
15844
  token
15032
15845
  }
15033
15846
  );
15034
- await page.waitForFunction(
15035
- (captureToken) => {
15036
- const registry = window.__sootsimVisibleChangeCaptures;
15037
- return registry?.[captureToken]?.ready === true;
15038
- },
15039
- token,
15040
- { timeout: 5e3 }
15041
- );
15042
- const triggerStartMs = Date.now();
15847
+ let captureFailure;
15848
+ let triggerStartMs = 0;
15043
15849
  let triggerCompletedMs = 0;
15044
15850
  try {
15851
+ await Promise.race([
15852
+ page.waitForFunction(
15853
+ (captureToken) => {
15854
+ const registry = window.__sootsimVisibleChangeCaptures;
15855
+ return registry?.[captureToken]?.ready === true;
15856
+ },
15857
+ token,
15858
+ { timeout: 5e3 }
15859
+ ),
15860
+ capturePromise.then(() => {
15861
+ throw new Error("peach motion capture completed before its trigger");
15862
+ })
15863
+ ]);
15864
+ triggerStartMs = Date.now();
15045
15865
  const triggerPromise = (async () => {
15046
15866
  if (!options.hold) {
15047
15867
  if (typeof options.trigger !== "function") {
@@ -15077,6 +15897,7 @@ var device = {
15077
15897
  }
15078
15898
  };
15079
15899
  } catch (error) {
15900
+ captureFailure = { error };
15080
15901
  await page.evaluate((captureToken) => {
15081
15902
  const registry = window.__sootsimVisibleChangeCaptures;
15082
15903
  const capture = registry?.[captureToken];
@@ -15086,6 +15907,55 @@ var device = {
15086
15907
  await capturePromise.catch(() => {
15087
15908
  });
15088
15909
  throw error;
15910
+ } finally {
15911
+ if (options.workletFrameTracePath) {
15912
+ try {
15913
+ const events = await page.evaluate(async () => {
15914
+ const test = window.SootSim?.bridges?.test;
15915
+ const read = test?.getShellReanimatedTrace;
15916
+ const enable = test?.setShellReanimatedTraceEnabled;
15917
+ if (typeof read !== "function" || typeof enable !== "function")
15918
+ throw new Error("worklet trace bridge is absent");
15919
+ try {
15920
+ return await read();
15921
+ } finally {
15922
+ await enable(false, false);
15923
+ }
15924
+ });
15925
+ fs2.writeFileSync(
15926
+ options.workletFrameTracePath,
15927
+ JSON.stringify(
15928
+ {
15929
+ source: "peach-native-frame-queue",
15930
+ limit: WORKLET_FRAME_TRACE_LIMIT,
15931
+ events
15932
+ },
15933
+ null,
15934
+ 2
15935
+ )
15936
+ );
15937
+ } catch (traceError) {
15938
+ if (!captureFailure) throw traceError;
15939
+ const primary = captureFailure.error;
15940
+ if (primary && (typeof primary === "object" || typeof primary === "function") && Object.isExtensible(primary)) {
15941
+ try {
15942
+ Object.defineProperty(primary, "workletFrameTraceError", {
15943
+ value: traceError,
15944
+ configurable: true
15945
+ });
15946
+ } catch (attachmentError) {
15947
+ console.error(
15948
+ "[peach motion capture] could not attach trace failure:",
15949
+ attachmentError
15950
+ );
15951
+ }
15952
+ }
15953
+ console.error(
15954
+ "[peach motion capture] secondary worklet trace failure:",
15955
+ traceError
15956
+ );
15957
+ }
15958
+ }
15089
15959
  }
15090
15960
  },
15091
15961
  // FAST multi-stage capture for sub-second animation diffs. each stage
@@ -15483,7 +16353,7 @@ async function cleanup() {
15483
16353
  _browserLaunchAbort?.abort(new Error("Peach driver cleaned up"));
15484
16354
  await _browserLaunch?.catch(() => {
15485
16355
  });
15486
- await closeBrowser();
16356
+ await closeBrowser(false);
15487
16357
  }
15488
16358
  // Annotate the CommonJS export names for ESM import in node:
15489
16359
  0 && (module.exports = {