peachsim 0.1.1640 → 0.1.1644

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 (125) hide show
  1. package/README.md +5 -0
  2. package/cli/app-bundle-resources.ts +57 -0
  3. package/cli/app-stubs.ts +38 -13
  4. package/cli/cloud-session.ts +16 -0
  5. package/cli/commands/assert.ts +17 -4
  6. package/cli/commands/box.ts +33 -19
  7. package/cli/commands/control.ts +12 -0
  8. package/cli/commands/doctor.ts +24 -15
  9. package/cli/commands/inspect/actions.ts +3 -2
  10. package/cli/commands/inspect/core.ts +0 -8
  11. package/cli/commands/inspect.ts +0 -14
  12. package/cli/commands/platform.ts +2 -1
  13. package/cli/commands/record.ts +64 -28
  14. package/cli/commands/swift.ts +10 -2
  15. package/cli/commands/three-mode.ts +4 -38
  16. package/cli/commands/upload.ts +8 -8
  17. package/cli/drivers/playwright.ts +39 -0
  18. package/cli/flow-live-status.ts +0 -2
  19. package/cli/main.ts +1 -1
  20. package/cli/outbound-endpoints.ts +4 -2
  21. package/cli/ws-bridge.ts +0 -6
  22. package/detox/attempt-events.cjs +41 -5
  23. package/detox/index.ts +155 -109
  24. package/detox/jest-environment.cjs +5 -1
  25. package/detox/motion-change.cjs +7 -5
  26. package/detox/motion-change.d.cts +1 -0
  27. package/detox/motion-leg-starts.cjs +20 -0
  28. package/detox/motion-leg-starts.d.cts +8 -0
  29. package/dist-lib/agent-daemon-client.cjs +1 -1
  30. package/dist-lib/agent-events.cjs +1 -1
  31. package/dist-lib/attached-projects.cjs +1 -1
  32. package/dist-lib/auth/shared-session.cjs +1 -1
  33. package/dist-lib/backend-origin.cjs +12 -3
  34. package/dist-lib/beta.cjs +1 -1
  35. package/dist-lib/beta.mjs +1 -1
  36. package/dist-lib/bridge-constants.cjs +1 -1
  37. package/dist-lib/bridge-contract-input.cjs +1 -1
  38. package/dist-lib/bridge-contract-input.mjs +1 -1
  39. package/dist-lib/bridge-contract.cjs +1 -1
  40. package/dist-lib/bridge-contract.mjs +1 -1
  41. package/dist-lib/bundle-channel.cjs +2671 -1833
  42. package/dist-lib/capture-contract.cjs +1 -1
  43. package/dist-lib/capture-contract.mjs +1 -1
  44. package/dist-lib/cloud-contract.cjs +1 -1
  45. package/dist-lib/cloud-contract.mjs +1 -1
  46. package/dist-lib/cloud-test-args.cjs +1 -1
  47. package/dist-lib/cloud-test-args.mjs +1 -1
  48. package/dist-lib/cloud.cjs +1 -1
  49. package/dist-lib/cloud.mjs +1 -1
  50. package/dist-lib/config.cjs +1 -1
  51. package/dist-lib/config.mjs +1 -1
  52. package/dist-lib/detox/index.cjs +578 -130
  53. package/dist-lib/detox/jest-environment.cjs +5 -1
  54. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  55. package/dist-lib/expo-swift-bundle.cjs +2374 -1554
  56. package/dist-lib/expo-swift-metadata.cjs +1 -1
  57. package/dist-lib/expo-swift-metadata.mjs +1 -1
  58. package/dist-lib/home-paths.cjs +1 -1
  59. package/dist-lib/host/bridge-host.cjs +2968 -2141
  60. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  61. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  62. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  63. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  64. package/dist-lib/host/websocket-proxy.cjs +1 -1
  65. package/dist-lib/index.cjs +132 -57
  66. package/dist-lib/jump-to-source-babel.cjs +1 -1
  67. package/dist-lib/jump-to-source-native.cjs +1 -1
  68. package/dist-lib/kotlin-host.cjs +1 -1
  69. package/dist-lib/menu.cjs +14 -6
  70. package/dist-lib/menu.mjs +14 -6
  71. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  72. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  73. package/dist-lib/metro-production-bundle.cjs +1 -1
  74. package/dist-lib/metro-production-bundle.mjs +1 -1
  75. package/dist-lib/metro.cjs +120 -53
  76. package/dist-lib/native-app-identity-project.cjs +96 -44
  77. package/dist-lib/native-app-identity-project.mjs +95 -44
  78. package/dist-lib/native-app-identity.cjs +1 -1
  79. package/dist-lib/native-app-identity.mjs +1 -1
  80. package/dist-lib/native-firebase-metadata.cjs +1 -1
  81. package/dist-lib/native-firebase-metadata.mjs +1 -1
  82. package/dist-lib/plist.cjs +1 -1
  83. package/dist-lib/plist.mjs +1 -1
  84. package/dist-lib/profiles.cjs +1 -1
  85. package/dist-lib/public-brand.cjs +1 -1
  86. package/dist-lib/react-native-host-modules.cjs +1 -1
  87. package/dist-lib/react-native-host-modules.mjs +1 -1
  88. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  89. package/dist-lib/sdk.cjs +5 -3
  90. package/dist-lib/sdk.mjs +5 -3
  91. package/dist-lib/skills.cjs +3932 -2868
  92. package/dist-lib/swift-compile.cjs +587 -374
  93. package/dist-lib/swift-compile.mjs +595 -369
  94. package/dist-lib/swift-package.cjs +1 -1
  95. package/dist-lib/swift-package.mjs +1 -1
  96. package/dist-lib/swift-submit.cjs +8 -3
  97. package/dist-lib/swift-submit.mjs +8 -3
  98. package/dist-lib/swift-toolchain.cjs +1 -1
  99. package/dist-lib/swift.cjs +1 -1
  100. package/dist-lib/vite-swift.cjs +2263 -1467
  101. package/dist-lib/vite.cjs +1 -1
  102. package/dist-lib/worklet-executable.cjs +1 -1
  103. package/dist-lib/worklet-executable.mjs +1 -1
  104. package/package.json +1 -1
  105. package/src/appBundleResourcesContract.ts +51 -0
  106. package/src/backend-origin.ts +11 -1
  107. package/src/host/bridge-host.ts +17 -11
  108. package/src/host/staged-host-files.ts +28 -4
  109. package/src/launchMetadataContract.ts +1 -0
  110. package/src/menu.ts +12 -4
  111. package/src/native-app-identity-project.ts +47 -38
  112. package/src/react-native-bridge-modules.ts +89 -76
  113. package/src/runtime-delivery.ts +6 -7
  114. package/src/swift-compile.ts +150 -112
  115. package/src/swift-compiler-digest.ts +5 -3
  116. package/src/swift-distribution.lock.json +9 -8
  117. package/src/swift-host.ts +6 -4
  118. package/src/swift-objc-compile.ts +416 -194
  119. package/src/swift-objc-metadata.ts +204 -0
  120. package/src/swift-objc-sdk.ts +195 -2
  121. package/src/swift-package-manifest.ts +6 -3
  122. package/src/swift-root-lock.ts +302 -0
  123. package/src/swift-submit.ts +15 -2
  124. package/src/swiftContract.ts +12 -0
  125. package/src/xcode-project.ts +77 -7
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1640 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1644 | (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;
@@ -1247,7 +1247,7 @@ var require_motion_change = __commonJS({
1247
1247
  var GESTURE_MOTION_MIN_TRANSITIONS = 120;
1248
1248
  var GESTURE_TRAVEL_RATIO_RANGE = Object.freeze({ min: 0.8, max: 1.25 });
1249
1249
  var GESTURE_QUARTER_TRAVEL_RATIO_RANGE = Object.freeze({ min: 0.5, max: 2 });
1250
- function evaluateMotionTransitions(transitions, oracleTarget) {
1250
+ function evaluateMotionTransitions(transitions, oracleTarget, repeating) {
1251
1251
  const key = (transition) => `${transition.fromLabel}\0${transition.toLabel}`;
1252
1252
  const oracle = transitions.filter((transition) => transition.target === oracleTarget);
1253
1253
  const sootsim = transitions.filter((transition) => transition.target === "sootsim");
@@ -1274,7 +1274,7 @@ var require_motion_change = __commonJS({
1274
1274
  Math.floor(list.length * index / 4),
1275
1275
  Math.floor(list.length * (index + 1) / 4)
1276
1276
  );
1277
- const follows = oracle.length >= GESTURE_MOTION_MIN_TRANSITIONS ? within(travel(sootsim), travel(oracle), GESTURE_TRAVEL_RATIO_RANGE) && [0, 1, 2, 3].every(
1277
+ const follows = !repeating && oracle.length >= GESTURE_MOTION_MIN_TRANSITIONS ? within(travel(sootsim), travel(oracle), GESTURE_TRAVEL_RATIO_RANGE) && [0, 1, 2, 3].every(
1278
1278
  (index) => within(
1279
1279
  travel(quarter(sootsim, index)),
1280
1280
  travel(quarter(oracle, index)),
@@ -1502,7 +1502,7 @@ var require_motion_change = __commonJS({
1502
1502
  const first = stages[0];
1503
1503
  const last = stages[stages.length - 1];
1504
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,
1505
+ passed: stages.length > 0 && first.passedThreshold && last.passedThreshold && (!needsIntermediates || movingPassed >= PUBLICATION_MIN_INTERMEDIATE_PASSES) && cyclePeriods.every((period) => period.passed) && evaluateMotionTransitions(transitions, oracleTarget, cyclePeriods.length > 0).passed,
1506
1506
  cyclePeriods,
1507
1507
  intermediateCount: intermediates.length,
1508
1508
  intermediatePassed,
@@ -1524,6 +1524,26 @@ var require_motion_change = __commonJS({
1524
1524
  }
1525
1525
  });
1526
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
+
1527
1547
  // ../../scripts/lib/remote-build-delivery.ts
1528
1548
  var remote_build_delivery_exports = {};
1529
1549
  __export(remote_build_delivery_exports, {
@@ -1655,6 +1675,7 @@ var require_native_frame_receipt = __commonJS({
1655
1675
  "../peach-conformance/e2e/utils/native-frame-receipt.cjs"(exports2, module2) {
1656
1676
  "use strict";
1657
1677
  var { createHash } = require("node:crypto");
1678
+ var { frameIndexForOffset: frameIndexForOffset2 } = (init_frame_offset(), __toCommonJS(frame_offset_exports));
1658
1679
  function nativeVideoFrameReceipt(recordingSha256, sourceFrameIndex, ptsSeconds, inputClock) {
1659
1680
  if (!/^[a-f0-9]{64}$/.test(recordingSha256)) throw new Error("invalid recording hash");
1660
1681
  if (!Number.isSafeInteger(sourceFrameIndex) || sourceFrameIndex < 0)
@@ -1735,25 +1756,29 @@ var require_native_frame_receipt = __commonJS({
1735
1756
  if (output !== bytes.length) throw new Error("incomplete native framebuffer");
1736
1757
  return bytes;
1737
1758
  }
1738
- function writeNativeFramebufferPng(receipt, bytes, outputPath, expectedSize) {
1759
+ function nativeFramebufferPixels(receipt, bytes) {
1739
1760
  requireNativeRawFrame(receipt, bytes);
1740
- const { PNG } = require("pngjs");
1741
- const fs3 = require("node:fs");
1742
1761
  const { width, height, stride } = receipt.rawFramebuffer;
1743
- if (width !== expectedSize.width || height !== expectedSize.height)
1744
- throw new Error("native framebuffer and screenshot dimensions differ");
1745
- const png = new PNG({ width, height });
1762
+ const data = Buffer.allocUnsafe(width * height * 4);
1746
1763
  for (let y = 0; y < height; y++) {
1747
1764
  for (let x = 0; x < width; x++) {
1748
1765
  const source = y * stride + x * 4;
1749
1766
  const destination = (y * width + x) * 4;
1750
- png.data[destination] = bytes[source + 2];
1751
- png.data[destination + 1] = bytes[source + 1];
1752
- png.data[destination + 2] = bytes[source];
1753
- 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];
1754
1771
  }
1755
1772
  }
1756
- 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));
1757
1782
  return receipt;
1758
1783
  }
1759
1784
  module2.exports = {
@@ -1761,6 +1786,7 @@ var require_native_frame_receipt = __commonJS({
1761
1786
  requireNativeProducerFrame,
1762
1787
  requireNativeRawFrame,
1763
1788
  decodeNativeFramebuffer,
1789
+ nativeFramebufferPixels,
1764
1790
  writeNativeFramebufferPng
1765
1791
  };
1766
1792
  async function buildNativeFrameConsumer({ root, cacheRoot, waitForAdmission = false }) {
@@ -1896,7 +1922,10 @@ var require_native_frame_receipt = __commonJS({
1896
1922
  ticks.push({
1897
1923
  targetTimestampSeconds: row.targetTimestampSeconds,
1898
1924
  timestampSeconds: row.timestampSeconds,
1899
- durationSeconds: row.durationSeconds
1925
+ durationSeconds: row.durationSeconds,
1926
+ clockBridge: row.clockBridge,
1927
+ callbackThread: row.callbackThread,
1928
+ callbackWasMainThread: row.callbackWasMainThread
1900
1929
  });
1901
1930
  }
1902
1931
  if (ticks.length < 2)
@@ -1910,7 +1939,7 @@ var require_native_frame_receipt = __commonJS({
1910
1939
  };
1911
1940
  }
1912
1941
  module2.exports.nativeUIFrameClockReceipt = nativeUIFrameClockReceipt;
1913
- function pairedNativeWorkletFrameClock(root, lifecycle) {
1942
+ function pairedNativeWorkletFrameClock(root, lifecycle, seed = "ui-frame-after-delivered-touch-up") {
1914
1943
  const { boundText } = require_attempt_events();
1915
1944
  if (!lifecycle || typeof lifecycle !== "object" || Array.isArray(lifecycle))
1916
1945
  throw new Error("paired worklets clock needs a retained native lifecycle");
@@ -1940,6 +1969,42 @@ var require_native_frame_receipt = __commonJS({
1940
1969
  )
1941
1970
  )
1942
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
+ }
1943
2008
  const up = input.events.find((event) => event.phase === 3);
1944
2009
  if (input.status !== "witnessed" || !up)
1945
2010
  throw new Error("paired worklets clock needs a delivered UIKit touch-up witness");
@@ -1965,6 +2030,323 @@ var require_native_frame_receipt = __commonJS({
1965
2030
  };
1966
2031
  }
1967
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;
1968
2350
  async function encodeNativeFramebufferMovie(rawDir, videoPath) {
1969
2351
  const fs3 = require("node:fs");
1970
2352
  const path5 = require("node:path");
@@ -1991,27 +2373,6 @@ var require_native_frame_receipt = __commonJS({
1991
2373
  });
1992
2374
  if (times.some((value, index) => index > 0 && value <= times[index - 1]))
1993
2375
  throw new Error("nonmonotonic native source arrivals");
1994
- const reads = rows.map((row) => ({
1995
- callbackIndex: row.callbackIndex,
1996
- seedBeforeRead: row.source?.seedBeforeRead,
1997
- seedAfterRead: row.source?.seedAfterRead,
1998
- arrival: BigInt(row.consumerArrival.machTicks)
1999
- }));
2000
- for (const [index, read] of reads.entries()) {
2001
- if (!Number.isSafeInteger(read.seedBeforeRead) || !Number.isSafeInteger(read.seedAfterRead))
2002
- throw new Error(`native callback ${read.callbackIndex} has no surface seed`);
2003
- const previous = reads[index - 1];
2004
- if (!previous) continue;
2005
- const lost = read.seedBeforeRead - previous.seedAfterRead - 1;
2006
- if (lost > 1) {
2007
- const gapMs = Number(
2008
- (read.arrival - previous.arrival) * BigInt(firstClock.timebaseNumer) / BigInt(firstClock.timebaseDenom)
2009
- ) / 1e6;
2010
- throw new Error(
2011
- `native recording skipped ${lost} surface updates between callbacks ${previous.callbackIndex} and ${read.callbackIndex} (surface seed ${previous.seedAfterRead} to ${read.seedBeforeRead}, ${gapMs.toFixed(1)}ms between arrivals): no captured frame shows them`
2012
- );
2013
- }
2014
- }
2015
2376
  function selectTime(start, end) {
2016
2377
  if (end - start === 1) return String(times[start]);
2017
2378
  const middle = Math.floor((start + end) / 2);
@@ -2415,6 +2776,10 @@ var require_attempt_events = __commonJS({
2415
2776
  };
2416
2777
  }
2417
2778
  function authenticateNativeCadenceCensus(root, sourceRoot, life, decoded) {
2779
+ const {
2780
+ authenticateNativeRefreshSamples
2781
+ } = require_native_frame_receipt();
2782
+ authenticateNativeRefreshSamples(root, sourceRoot, life);
2418
2783
  const native = life.nativeFramebuffer;
2419
2784
  if (!native || native.exitCode !== 0 || native.failure || native.sourceSha256 !== proofByteHash(
2420
2785
  fs3.readFileSync(
@@ -2432,10 +2797,10 @@ var require_attempt_events = __commonJS({
2432
2797
  root,
2433
2798
  native.mediaFrames,
2434
2799
  native.mediaFramesSha256,
2435
- "native cadence presentation census"
2800
+ "native cadence sampled surface census"
2436
2801
  ).trim().split("\n").map(JSON.parse);
2437
2802
  if (frames.length !== decoded.observations.length || media.length !== frames.length)
2438
- throw new Error("native cadence acquisition and presentation census differ");
2803
+ throw new Error("native cadence acquisition and sampled surface census differ");
2439
2804
  const {
2440
2805
  decodeNativeFramebuffer,
2441
2806
  requireNativeRawFrame
@@ -2454,12 +2819,22 @@ var require_attempt_events = __commonJS({
2454
2819
  return row.callbackIndex;
2455
2820
  })
2456
2821
  );
2457
- const origin = frames[0].consumerArrival;
2822
+ const reads = boundText(
2823
+ root,
2824
+ native.reads,
2825
+ native.readsSha256,
2826
+ "native cadence observations"
2827
+ ).split("\n").filter(Boolean).map(JSON.parse);
2458
2828
  let expectedCallback = 0;
2459
- for (const [index, frame] of frames.entries()) {
2829
+ for (const read of reads) {
2460
2830
  while (superseded.delete(expectedCallback)) expectedCallback++;
2831
+ if (read.callbackIndex !== expectedCallback++)
2832
+ throw new Error("native cadence observations have invalid callback order");
2833
+ }
2834
+ const origin = frames[0].consumerArrival;
2835
+ for (const [index, frame] of frames.entries()) {
2461
2836
  const clock = frame.consumerArrival;
2462
- if (frame.callbackIndex !== expectedCallback++ || media[index].sourceFrameIndex !== index || !clock || clock.domain !== "host-mach-absolute-time" || clock.domain !== origin.domain || clock.timebaseNumer !== origin.timebaseNumer || clock.timebaseDenom !== origin.timebaseDenom || !Number.isSafeInteger(clock.timebaseNumer) || clock.timebaseNumer <= 0 || !Number.isSafeInteger(clock.timebaseDenom) || clock.timebaseDenom <= 0)
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)
2463
2838
  throw new Error("native cadence source census has invalid clock or order");
2464
2839
  const pts = (BigInt(clock.machTicks) - BigInt(origin.machTicks)) * BigInt(clock.timebaseNumer) / BigInt(clock.timebaseDenom);
2465
2840
  if (pts < 0n || pts !== BigInt(media[index].ptsNanoseconds) || Math.abs(Number(pts) / 1e6 - decoded.observations[index].atMs) > 1e-3)
@@ -2723,6 +3098,26 @@ var require_attempt_events = __commonJS({
2723
3098
  paths.add(native.receipt);
2724
3099
  paths.add(native.superseded);
2725
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
+ }
2726
3121
  const rows = fs3.readFileSync(proofBoundPath(root, native.receipt), "utf8").trim().split("\n").map(JSON.parse);
2727
3122
  for (const row of rows) {
2728
3123
  const file = proofBoundPath(
@@ -4996,7 +5391,7 @@ async function automateWebViewFrames(context, names, helpers, tenantWorkers) {
4996
5391
  contentType += "; charset=windows-1252";
4997
5392
  }
4998
5393
  const headers = [{ name: "Content-Type", value: contentType }];
4999
- if (document2 && bound.navigation) {
5394
+ if (document2) {
5000
5395
  bound.load = null;
5001
5396
  const config = JSON.parse(bound.config);
5002
5397
  config.documentUrl = url;
@@ -5722,7 +6117,8 @@ ${WEBVIEW_FRAME_COLOR_SCHEME_SOURCE} var webKit = config.webKit || null;
5722
6117
  crossOriginToSim = window.top.location.href === undefined;
5723
6118
  } catch (_) {}
5724
6119
  var cookieProperty = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');
5725
- if (cookieStore) {
6120
+ // file documents keep WebKit's native non-HTTP cookie behavior.
6121
+ if (cookieStore && /^https?:/.test(documentUrl)) {
5726
6122
  var cookieOperations = ${webKitCookieOperationsSource};
5727
6123
  var pageCookies = cookieOperations.restore(JSON.parse(cookieStore.cookies));
5728
6124
  window.__peachUpdateWebKitCookies = function(cookies, policy) {
@@ -13139,6 +13535,7 @@ var by = {
13139
13535
  // detox/index.ts
13140
13536
  var import_motion_change = __toESM(require_motion_change(), 1);
13141
13537
  var import_proof_frame = __toESM(require_proof_frame(), 1);
13538
+ var WORKLET_FRAME_TRACE_LIMIT = 16384;
13142
13539
  var BASE_URL = process.env.PEACH_URL || "http://localhost:5173";
13143
13540
  var SCREENSHOT_DIR = process.env.PEACH_SCREENSHOT_DIR || path4.join(process.cwd(), "test", "detox-driver", "screenshots", String(process.pid));
13144
13541
  var _browser = null;
@@ -13415,8 +13812,12 @@ async function waitForSootsimWorkletsIdle(page, timeout = 3e3) {
13415
13812
  );
13416
13813
  if (!result?.settled) {
13417
13814
  const pending = result?.pending ?? "no peer stats reply";
13418
- throw new Error(
13419
- `sootsim worklet runtime did not settle after ${result?.elapsed ?? timeout}ms (pending=${pending}; observation=${JSON.stringify(result?.observation)})`
13815
+ throw await describeSootsimBridgeFailure(
13816
+ page,
13817
+ new Error(
13818
+ `sootsim worklet runtime did not settle after ${result?.elapsed ?? timeout}ms (pending=${pending}; observation=${JSON.stringify(result?.observation)})`
13819
+ ),
13820
+ "sootsim worklet runtime did not settle"
13420
13821
  );
13421
13822
  }
13422
13823
  }
@@ -14629,6 +15030,7 @@ var device = {
14629
15030
  async installApp() {
14630
15031
  },
14631
15032
  async uninstallApp() {
15033
+ await closeBrowser(false);
14632
15034
  },
14633
15035
  async openURL(url) {
14634
15036
  const page = getPage();
@@ -15005,6 +15407,14 @@ var device = {
15005
15407
  definition: import_motion_change.MOTION_CHANGE_DEFINITION,
15006
15408
  installGlobal: true
15007
15409
  });
15410
+ if (options.workletFrameTracePath) {
15411
+ await page.evaluate(async (limit) => {
15412
+ const enable = window.SootSim?.bridges?.test?.setShellReanimatedTraceEnabled;
15413
+ if (typeof enable !== "function")
15414
+ throw new Error("worklet trace bridge is absent");
15415
+ await enable(true, true, limit);
15416
+ }, WORKLET_FRAME_TRACE_LIMIT);
15417
+ }
15008
15418
  const capturePromise = page.evaluate(
15009
15419
  async (input) => {
15010
15420
  const captureWindow = window;
@@ -15108,49 +15518,8 @@ var device = {
15108
15518
  const controller = await lockstep.begin(60, {
15109
15519
  workletFrameTimestampsSeconds: input.workletFrameTimestampsSeconds
15110
15520
  });
15111
- const moves = input.hold?.moves ?? [];
15112
- const holdState = {
15113
- nextMove: 0,
15114
- pressFrameIndex: null,
15115
- releaseReceipt: null,
15116
- released: false,
15117
- x: input.hold?.x ?? 0,
15118
- y: input.hold?.y ?? 0
15119
- };
15120
- const holdEventFrameIndex = (afterMs) => (holdState.pressFrameIndex ?? 0) + controller.framesForOffset(afterMs);
15121
- const nextHoldEventFrameIndex = () => {
15122
- if (!input.hold || holdState.released || holdState.pressFrameIndex === null) {
15123
- return null;
15124
- }
15125
- const move = moves[holdState.nextMove];
15126
- return holdEventFrameIndex(move ? move.afterMs : input.hold.durationMs);
15127
- };
15128
- const advanceHold = async (receipt) => {
15129
- if (!input.hold || !interact || holdState.released) return;
15130
- if (holdState.pressFrameIndex === null) {
15131
- holdState.pressFrameIndex = receipt.frameIndex - 1;
15132
- }
15133
- for (let move = moves[holdState.nextMove]; move && receipt.frameIndex >= holdEventFrameIndex(move.afterMs); move = moves[holdState.nextMove]) {
15134
- holdState.nextMove++;
15135
- holdState.x = move.x;
15136
- holdState.y = move.y;
15137
- await interact.touchMove(
15138
- move.x,
15139
- move.y,
15140
- 999,
15141
- controller.initialFrame.virtualTimestampMs + move.afterMs
15142
- );
15143
- }
15144
- if (receipt.frameIndex < holdEventFrameIndex(input.hold.durationMs)) return;
15145
- holdState.released = true;
15146
- holdState.releaseReceipt = receipt;
15147
- await interact.touchUp(
15148
- holdState.x,
15149
- holdState.y,
15150
- 999,
15151
- controller.initialFrame.virtualTimestampMs + input.hold.durationMs
15152
- );
15153
- };
15521
+ let heldInput = null;
15522
+ const releaseReceipt = () => controller.inputReceipts.find((receipt) => receipt.phase === "up");
15154
15523
  const keptFrames = /* @__PURE__ */ new Map();
15155
15524
  const keepFrame = (receipt, bitmap) => {
15156
15525
  keptFrames.get(receipt.frameIndex)?.bitmap.close();
@@ -15169,17 +15538,51 @@ var device = {
15169
15538
  throw new Error("paired worklets callback preceded the Peach trigger");
15170
15539
  state.ready = true;
15171
15540
  if (input.hold && interact) {
15172
- const hit = await interact.touchDown(
15173
- input.hold.x,
15174
- input.hold.y,
15175
- 999,
15176
- controller.initialFrame.virtualTimestampMs
15177
- );
15178
- if (!hit) {
15179
- throw new Error(
15180
- `peach motion hold missed at ${input.hold.x},${input.hold.y}`
15181
- );
15182
- }
15541
+ const hold = input.hold;
15542
+ const moves = hold.moves ?? [];
15543
+ const release = hold.release ?? moves.at(-1) ?? hold;
15544
+ const events = [
15545
+ {
15546
+ phase: "down",
15547
+ x: hold.x,
15548
+ y: hold.y,
15549
+ pointerId: 999,
15550
+ afterMs: 0,
15551
+ async deliver(timestamp2) {
15552
+ const hit = await interact.touchDown(hold.x, hold.y, 999, timestamp2);
15553
+ if (!hit)
15554
+ throw new Error(`peach motion hold missed at ${hold.x},${hold.y}`);
15555
+ }
15556
+ },
15557
+ ...moves.map(
15558
+ (move) => ({
15559
+ phase: "move",
15560
+ x: move.x,
15561
+ y: move.y,
15562
+ pointerId: 999,
15563
+ afterMs: move.afterMs,
15564
+ eventAfterMs: move.eventAfterMs,
15565
+ async deliver(timestamp2) {
15566
+ await interact.touchMove(move.x, move.y, 999, timestamp2);
15567
+ }
15568
+ })
15569
+ ),
15570
+ {
15571
+ phase: "up",
15572
+ x: release.x,
15573
+ y: release.y,
15574
+ pointerId: 999,
15575
+ afterMs: hold.durationMs,
15576
+ eventAfterMs: hold.eventDurationMs,
15577
+ async deliver(timestamp2) {
15578
+ await interact.touchUp(release.x, release.y, 999, timestamp2);
15579
+ }
15580
+ }
15581
+ ];
15582
+ heldInput = controller.scheduleInput(events);
15583
+ heldInput.catch(() => {
15584
+ });
15585
+ await controller.flushFrame();
15183
15586
  } else {
15184
15587
  while (!state.triggerStarted) {
15185
15588
  if (state.cancelled) throw new Error("peach motion capture was cancelled");
@@ -15213,9 +15616,8 @@ var device = {
15213
15616
  for (let frame = 0; !visibleStart && frame < detectionFrameLimit; frame++) {
15214
15617
  if (state.cancelled) throw new Error("peach motion capture was cancelled");
15215
15618
  const receipt = await controller.stepFrame();
15216
- await advanceHold(receipt);
15217
15619
  const candidateBitmap = await captureBitmap();
15218
- if (input.anchorAtTouch === "down" || input.anchorAtTouch === "up" && holdState.releaseReceipt === receipt) {
15620
+ if (input.anchorAtTouch === "down" || input.anchorAtTouch === "up" && releaseReceipt()?.frameIndex === receipt.frameIndex) {
15219
15621
  keepFrame(receipt, await createImageBitmap(candidateBitmap));
15220
15622
  }
15221
15623
  const bounds = boundsFor(candidateBitmap);
@@ -15248,17 +15650,17 @@ var device = {
15248
15650
  let currentReceipt = visibleStart;
15249
15651
  if (input.anchorAtTouch === "down") driveStart = controller.initialFrame;
15250
15652
  if (input.anchorAtTouch === "up") {
15251
- while (!holdState.released) {
15653
+ while (!releaseReceipt()) {
15252
15654
  if (state.cancelled) throw new Error("peach motion capture was cancelled");
15253
15655
  const receipt = await controller.stepFrame();
15254
- await advanceHold(receipt);
15255
15656
  currentReceipt = receipt;
15256
- if (holdState.released) keepFrame(receipt, await captureBitmap());
15657
+ if (releaseReceipt()) keepFrame(receipt, await captureBitmap());
15257
15658
  }
15258
- if (!holdState.releaseReceipt) {
15659
+ const released = releaseReceipt();
15660
+ if (!released) {
15259
15661
  throw new Error("peach release anchoring found no release frame");
15260
15662
  }
15261
- driveStart = holdState.releaseReceipt;
15663
+ driveStart = { ...released, tenantFrame: null };
15262
15664
  }
15263
15665
  const resolvedAnchors = /* @__PURE__ */ new Map();
15264
15666
  const anchorRecords = [];
@@ -15283,7 +15685,6 @@ var device = {
15283
15685
  while (searchReceipt.virtualTimestampMs < searchStartVts) {
15284
15686
  if (state.cancelled) throw new Error("peach motion capture was cancelled");
15285
15687
  searchReceipt = await controller.stepFrame();
15286
- await advanceHold(searchReceipt);
15287
15688
  }
15288
15689
  currentReceipt = searchReceipt;
15289
15690
  let anchorBitmap = await captureBitmap();
@@ -15295,7 +15696,6 @@ var device = {
15295
15696
  for (let frame = 0; frame < detectionFrameLimit; frame++) {
15296
15697
  if (state.cancelled) throw new Error("peach motion capture was cancelled");
15297
15698
  const receipt = await controller.stepFrame();
15298
- await advanceHold(receipt);
15299
15699
  const candidateBitmap = await captureBitmap();
15300
15700
  const bounds = boundsFor(candidateBitmap);
15301
15701
  const candidate = regionPixels(candidateBitmap);
@@ -15361,11 +15761,7 @@ var device = {
15361
15761
  });
15362
15762
  continue;
15363
15763
  }
15364
- for (let eventFrameIndex = nextHoldEventFrameIndex(); eventFrameIndex !== null && stageFrameIndex > eventFrameIndex; eventFrameIndex = nextHoldEventFrameIndex()) {
15365
- await advanceHold(await controller.stepFrame());
15366
- }
15367
15764
  const receipt = await controller.stepToOffset(anchorReceipt, stage.offsetMs);
15368
- await advanceHold(receipt);
15369
15765
  currentReceipt = receipt;
15370
15766
  snapshots.push({
15371
15767
  actualMs: receipt.virtualTimestampMs - anchorReceipt.virtualTimestampMs,
@@ -15379,11 +15775,11 @@ var device = {
15379
15775
  }
15380
15776
  if (input.workletFrameTimestampsSeconds) controller.endWorkletFrameReceipt();
15381
15777
  let releasedOffsetMs = 0;
15382
- while (nextHoldEventFrameIndex() !== null) {
15778
+ while (input.hold && !releaseReceipt()) {
15383
15779
  const receipt = await controller.stepFrame();
15384
- await advanceHold(receipt);
15385
15780
  releasedOffsetMs = receipt.virtualTimestampMs - driveStart.virtualTimestampMs;
15386
15781
  }
15782
+ await heldInput;
15387
15783
  if (input.settleMs > 0) {
15388
15784
  const maximumPrimaryMs = snapshots.reduce(
15389
15785
  (maximum, snapshot2) => Math.max(
@@ -15449,22 +15845,24 @@ var device = {
15449
15845
  token
15450
15846
  }
15451
15847
  );
15452
- await Promise.race([
15453
- page.waitForFunction(
15454
- (captureToken) => {
15455
- const registry = window.__sootsimVisibleChangeCaptures;
15456
- return registry?.[captureToken]?.ready === true;
15457
- },
15458
- token,
15459
- { timeout: 5e3 }
15460
- ),
15461
- capturePromise.then(() => {
15462
- throw new Error("peach motion capture completed before its trigger");
15463
- })
15464
- ]);
15465
- const triggerStartMs = Date.now();
15848
+ let captureFailure;
15849
+ let triggerStartMs = 0;
15466
15850
  let triggerCompletedMs = 0;
15467
15851
  try {
15852
+ await Promise.race([
15853
+ page.waitForFunction(
15854
+ (captureToken) => {
15855
+ const registry = window.__sootsimVisibleChangeCaptures;
15856
+ return registry?.[captureToken]?.ready === true;
15857
+ },
15858
+ token,
15859
+ { timeout: 5e3 }
15860
+ ),
15861
+ capturePromise.then(() => {
15862
+ throw new Error("peach motion capture completed before its trigger");
15863
+ })
15864
+ ]);
15865
+ triggerStartMs = Date.now();
15468
15866
  const triggerPromise = (async () => {
15469
15867
  if (!options.hold) {
15470
15868
  if (typeof options.trigger !== "function") {
@@ -15500,6 +15898,7 @@ var device = {
15500
15898
  }
15501
15899
  };
15502
15900
  } catch (error) {
15901
+ captureFailure = { error };
15503
15902
  await page.evaluate((captureToken) => {
15504
15903
  const registry = window.__sootsimVisibleChangeCaptures;
15505
15904
  const capture = registry?.[captureToken];
@@ -15509,6 +15908,55 @@ var device = {
15509
15908
  await capturePromise.catch(() => {
15510
15909
  });
15511
15910
  throw error;
15911
+ } finally {
15912
+ if (options.workletFrameTracePath) {
15913
+ try {
15914
+ const events = await page.evaluate(async () => {
15915
+ const test = window.SootSim?.bridges?.test;
15916
+ const read = test?.getShellReanimatedTrace;
15917
+ const enable = test?.setShellReanimatedTraceEnabled;
15918
+ if (typeof read !== "function" || typeof enable !== "function")
15919
+ throw new Error("worklet trace bridge is absent");
15920
+ try {
15921
+ return await read();
15922
+ } finally {
15923
+ await enable(false, false);
15924
+ }
15925
+ });
15926
+ fs2.writeFileSync(
15927
+ options.workletFrameTracePath,
15928
+ JSON.stringify(
15929
+ {
15930
+ source: "peach-native-frame-queue",
15931
+ limit: WORKLET_FRAME_TRACE_LIMIT,
15932
+ events
15933
+ },
15934
+ null,
15935
+ 2
15936
+ )
15937
+ );
15938
+ } catch (traceError) {
15939
+ if (!captureFailure) throw traceError;
15940
+ const primary = captureFailure.error;
15941
+ if (primary && (typeof primary === "object" || typeof primary === "function") && Object.isExtensible(primary)) {
15942
+ try {
15943
+ Object.defineProperty(primary, "workletFrameTraceError", {
15944
+ value: traceError,
15945
+ configurable: true
15946
+ });
15947
+ } catch (attachmentError) {
15948
+ console.error(
15949
+ "[peach motion capture] could not attach trace failure:",
15950
+ attachmentError
15951
+ );
15952
+ }
15953
+ }
15954
+ console.error(
15955
+ "[peach motion capture] secondary worklet trace failure:",
15956
+ traceError
15957
+ );
15958
+ }
15959
+ }
15512
15960
  }
15513
15961
  },
15514
15962
  // FAST multi-stage capture for sub-second animation diffs. each stage