peachsim 0.1.1640 → 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 (109) hide show
  1. package/README.md +5 -0
  2. package/cli/app-stubs.ts +38 -13
  3. package/cli/cloud-session.ts +16 -0
  4. package/cli/commands/assert.ts +17 -4
  5. package/cli/commands/box.ts +33 -19
  6. package/cli/commands/doctor.ts +24 -15
  7. package/cli/commands/inspect/actions.ts +3 -2
  8. package/cli/commands/inspect/core.ts +0 -8
  9. package/cli/commands/inspect.ts +0 -14
  10. package/cli/commands/platform.ts +2 -1
  11. package/cli/commands/record.ts +64 -28
  12. package/cli/commands/swift.ts +10 -2
  13. package/cli/commands/three-mode.ts +4 -38
  14. package/cli/commands/upload.ts +8 -8
  15. package/cli/flow-live-status.ts +0 -2
  16. package/cli/main.ts +1 -1
  17. package/cli/ws-bridge.ts +0 -6
  18. package/detox/attempt-events.cjs +41 -5
  19. package/detox/index.ts +155 -109
  20. package/detox/jest-environment.cjs +5 -1
  21. package/dist-lib/agent-daemon-client.cjs +1 -1
  22. package/dist-lib/agent-events.cjs +1 -1
  23. package/dist-lib/attached-projects.cjs +1 -1
  24. package/dist-lib/auth/shared-session.cjs +1 -1
  25. package/dist-lib/backend-origin.cjs +10 -1
  26. package/dist-lib/beta.cjs +1 -1
  27. package/dist-lib/beta.mjs +1 -1
  28. package/dist-lib/bridge-constants.cjs +1 -1
  29. package/dist-lib/bridge-contract-input.cjs +1 -1
  30. package/dist-lib/bridge-contract-input.mjs +1 -1
  31. package/dist-lib/bridge-contract.cjs +1 -1
  32. package/dist-lib/bridge-contract.mjs +1 -1
  33. package/dist-lib/bundle-channel.cjs +2059 -1368
  34. package/dist-lib/capture-contract.cjs +1 -1
  35. package/dist-lib/capture-contract.mjs +1 -1
  36. package/dist-lib/cloud-contract.cjs +1 -1
  37. package/dist-lib/cloud-contract.mjs +1 -1
  38. package/dist-lib/cloud-test-args.cjs +1 -1
  39. package/dist-lib/cloud-test-args.mjs +1 -1
  40. package/dist-lib/cloud.cjs +1 -1
  41. package/dist-lib/cloud.mjs +1 -1
  42. package/dist-lib/config.cjs +1 -1
  43. package/dist-lib/config.mjs +1 -1
  44. package/dist-lib/detox/index.cjs +572 -125
  45. package/dist-lib/detox/jest-environment.cjs +5 -1
  46. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  47. package/dist-lib/expo-swift-bundle.cjs +1837 -1164
  48. package/dist-lib/expo-swift-metadata.cjs +1 -1
  49. package/dist-lib/expo-swift-metadata.mjs +1 -1
  50. package/dist-lib/home-paths.cjs +1 -1
  51. package/dist-lib/host/bridge-host.cjs +2290 -1616
  52. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  53. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  54. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  55. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  56. package/dist-lib/host/websocket-proxy.cjs +1 -1
  57. package/dist-lib/index.cjs +44 -17
  58. package/dist-lib/jump-to-source-babel.cjs +1 -1
  59. package/dist-lib/jump-to-source-native.cjs +1 -1
  60. package/dist-lib/kotlin-host.cjs +1 -1
  61. package/dist-lib/menu.cjs +1 -1
  62. package/dist-lib/menu.mjs +1 -1
  63. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  64. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  65. package/dist-lib/metro-production-bundle.cjs +1 -1
  66. package/dist-lib/metro-production-bundle.mjs +1 -1
  67. package/dist-lib/metro.cjs +33 -13
  68. package/dist-lib/native-app-identity-project.cjs +7 -4
  69. package/dist-lib/native-app-identity-project.mjs +7 -4
  70. package/dist-lib/native-app-identity.cjs +1 -1
  71. package/dist-lib/native-app-identity.mjs +1 -1
  72. package/dist-lib/native-firebase-metadata.cjs +1 -1
  73. package/dist-lib/native-firebase-metadata.mjs +1 -1
  74. package/dist-lib/plist.cjs +1 -1
  75. package/dist-lib/plist.mjs +1 -1
  76. package/dist-lib/profiles.cjs +1 -1
  77. package/dist-lib/public-brand.cjs +1 -1
  78. package/dist-lib/react-native-host-modules.cjs +1 -1
  79. package/dist-lib/react-native-host-modules.mjs +1 -1
  80. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  81. package/dist-lib/sdk.cjs +4 -3
  82. package/dist-lib/sdk.mjs +4 -3
  83. package/dist-lib/skills.cjs +2823 -2109
  84. package/dist-lib/swift-compile.cjs +370 -239
  85. package/dist-lib/swift-compile.mjs +368 -230
  86. package/dist-lib/swift-package.cjs +1 -1
  87. package/dist-lib/swift-package.mjs +1 -1
  88. package/dist-lib/swift-submit.cjs +8 -3
  89. package/dist-lib/swift-submit.mjs +8 -3
  90. package/dist-lib/swift-toolchain.cjs +1 -1
  91. package/dist-lib/swift.cjs +1 -1
  92. package/dist-lib/vite-swift.cjs +1757 -1108
  93. package/dist-lib/vite.cjs +1 -1
  94. package/dist-lib/worklet-executable.cjs +1 -1
  95. package/dist-lib/worklet-executable.mjs +1 -1
  96. package/package.json +1 -1
  97. package/src/backend-origin.ts +8 -0
  98. package/src/react-native-bridge-modules.ts +89 -76
  99. package/src/swift-compile.ts +134 -107
  100. package/src/swift-distribution.lock.json +9 -8
  101. package/src/swift-host.ts +6 -4
  102. package/src/swift-objc-compile.ts +385 -193
  103. package/src/swift-objc-metadata.ts +204 -0
  104. package/src/swift-objc-sdk.ts +195 -2
  105. package/src/swift-package-manifest.ts +6 -3
  106. package/src/swift-root-lock.ts +153 -0
  107. package/src/swift-submit.ts +15 -2
  108. package/src/swiftContract.ts +12 -0
  109. package/src/xcode-project.ts +10 -3
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1640 | (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;
@@ -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(
@@ -13139,6 +13534,7 @@ var by = {
13139
13534
  // detox/index.ts
13140
13535
  var import_motion_change = __toESM(require_motion_change(), 1);
13141
13536
  var import_proof_frame = __toESM(require_proof_frame(), 1);
13537
+ var WORKLET_FRAME_TRACE_LIMIT = 16384;
13142
13538
  var BASE_URL = process.env.PEACH_URL || "http://localhost:5173";
13143
13539
  var SCREENSHOT_DIR = process.env.PEACH_SCREENSHOT_DIR || path4.join(process.cwd(), "test", "detox-driver", "screenshots", String(process.pid));
13144
13540
  var _browser = null;
@@ -13415,8 +13811,12 @@ async function waitForSootsimWorkletsIdle(page, timeout = 3e3) {
13415
13811
  );
13416
13812
  if (!result?.settled) {
13417
13813
  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)})`
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"
13420
13820
  );
13421
13821
  }
13422
13822
  }
@@ -14629,6 +15029,7 @@ var device = {
14629
15029
  async installApp() {
14630
15030
  },
14631
15031
  async uninstallApp() {
15032
+ await closeBrowser(false);
14632
15033
  },
14633
15034
  async openURL(url) {
14634
15035
  const page = getPage();
@@ -15005,6 +15406,14 @@ var device = {
15005
15406
  definition: import_motion_change.MOTION_CHANGE_DEFINITION,
15006
15407
  installGlobal: true
15007
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
+ }
15008
15417
  const capturePromise = page.evaluate(
15009
15418
  async (input) => {
15010
15419
  const captureWindow = window;
@@ -15108,49 +15517,8 @@ var device = {
15108
15517
  const controller = await lockstep.begin(60, {
15109
15518
  workletFrameTimestampsSeconds: input.workletFrameTimestampsSeconds
15110
15519
  });
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
- };
15520
+ let heldInput = null;
15521
+ const releaseReceipt = () => controller.inputReceipts.find((receipt) => receipt.phase === "up");
15154
15522
  const keptFrames = /* @__PURE__ */ new Map();
15155
15523
  const keepFrame = (receipt, bitmap) => {
15156
15524
  keptFrames.get(receipt.frameIndex)?.bitmap.close();
@@ -15169,17 +15537,51 @@ var device = {
15169
15537
  throw new Error("paired worklets callback preceded the Peach trigger");
15170
15538
  state.ready = true;
15171
15539
  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
- }
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();
15183
15585
  } else {
15184
15586
  while (!state.triggerStarted) {
15185
15587
  if (state.cancelled) throw new Error("peach motion capture was cancelled");
@@ -15213,9 +15615,8 @@ var device = {
15213
15615
  for (let frame = 0; !visibleStart && frame < detectionFrameLimit; frame++) {
15214
15616
  if (state.cancelled) throw new Error("peach motion capture was cancelled");
15215
15617
  const receipt = await controller.stepFrame();
15216
- await advanceHold(receipt);
15217
15618
  const candidateBitmap = await captureBitmap();
15218
- if (input.anchorAtTouch === "down" || input.anchorAtTouch === "up" && holdState.releaseReceipt === receipt) {
15619
+ if (input.anchorAtTouch === "down" || input.anchorAtTouch === "up" && releaseReceipt()?.frameIndex === receipt.frameIndex) {
15219
15620
  keepFrame(receipt, await createImageBitmap(candidateBitmap));
15220
15621
  }
15221
15622
  const bounds = boundsFor(candidateBitmap);
@@ -15248,17 +15649,17 @@ var device = {
15248
15649
  let currentReceipt = visibleStart;
15249
15650
  if (input.anchorAtTouch === "down") driveStart = controller.initialFrame;
15250
15651
  if (input.anchorAtTouch === "up") {
15251
- while (!holdState.released) {
15652
+ while (!releaseReceipt()) {
15252
15653
  if (state.cancelled) throw new Error("peach motion capture was cancelled");
15253
15654
  const receipt = await controller.stepFrame();
15254
- await advanceHold(receipt);
15255
15655
  currentReceipt = receipt;
15256
- if (holdState.released) keepFrame(receipt, await captureBitmap());
15656
+ if (releaseReceipt()) keepFrame(receipt, await captureBitmap());
15257
15657
  }
15258
- if (!holdState.releaseReceipt) {
15658
+ const released = releaseReceipt();
15659
+ if (!released) {
15259
15660
  throw new Error("peach release anchoring found no release frame");
15260
15661
  }
15261
- driveStart = holdState.releaseReceipt;
15662
+ driveStart = { ...released, tenantFrame: null };
15262
15663
  }
15263
15664
  const resolvedAnchors = /* @__PURE__ */ new Map();
15264
15665
  const anchorRecords = [];
@@ -15283,7 +15684,6 @@ var device = {
15283
15684
  while (searchReceipt.virtualTimestampMs < searchStartVts) {
15284
15685
  if (state.cancelled) throw new Error("peach motion capture was cancelled");
15285
15686
  searchReceipt = await controller.stepFrame();
15286
- await advanceHold(searchReceipt);
15287
15687
  }
15288
15688
  currentReceipt = searchReceipt;
15289
15689
  let anchorBitmap = await captureBitmap();
@@ -15295,7 +15695,6 @@ var device = {
15295
15695
  for (let frame = 0; frame < detectionFrameLimit; frame++) {
15296
15696
  if (state.cancelled) throw new Error("peach motion capture was cancelled");
15297
15697
  const receipt = await controller.stepFrame();
15298
- await advanceHold(receipt);
15299
15698
  const candidateBitmap = await captureBitmap();
15300
15699
  const bounds = boundsFor(candidateBitmap);
15301
15700
  const candidate = regionPixels(candidateBitmap);
@@ -15361,11 +15760,7 @@ var device = {
15361
15760
  });
15362
15761
  continue;
15363
15762
  }
15364
- for (let eventFrameIndex = nextHoldEventFrameIndex(); eventFrameIndex !== null && stageFrameIndex > eventFrameIndex; eventFrameIndex = nextHoldEventFrameIndex()) {
15365
- await advanceHold(await controller.stepFrame());
15366
- }
15367
15763
  const receipt = await controller.stepToOffset(anchorReceipt, stage.offsetMs);
15368
- await advanceHold(receipt);
15369
15764
  currentReceipt = receipt;
15370
15765
  snapshots.push({
15371
15766
  actualMs: receipt.virtualTimestampMs - anchorReceipt.virtualTimestampMs,
@@ -15379,11 +15774,11 @@ var device = {
15379
15774
  }
15380
15775
  if (input.workletFrameTimestampsSeconds) controller.endWorkletFrameReceipt();
15381
15776
  let releasedOffsetMs = 0;
15382
- while (nextHoldEventFrameIndex() !== null) {
15777
+ while (input.hold && !releaseReceipt()) {
15383
15778
  const receipt = await controller.stepFrame();
15384
- await advanceHold(receipt);
15385
15779
  releasedOffsetMs = receipt.virtualTimestampMs - driveStart.virtualTimestampMs;
15386
15780
  }
15781
+ await heldInput;
15387
15782
  if (input.settleMs > 0) {
15388
15783
  const maximumPrimaryMs = snapshots.reduce(
15389
15784
  (maximum, snapshot2) => Math.max(
@@ -15449,22 +15844,24 @@ var device = {
15449
15844
  token
15450
15845
  }
15451
15846
  );
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();
15847
+ let captureFailure;
15848
+ let triggerStartMs = 0;
15466
15849
  let triggerCompletedMs = 0;
15467
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();
15468
15865
  const triggerPromise = (async () => {
15469
15866
  if (!options.hold) {
15470
15867
  if (typeof options.trigger !== "function") {
@@ -15500,6 +15897,7 @@ var device = {
15500
15897
  }
15501
15898
  };
15502
15899
  } catch (error) {
15900
+ captureFailure = { error };
15503
15901
  await page.evaluate((captureToken) => {
15504
15902
  const registry = window.__sootsimVisibleChangeCaptures;
15505
15903
  const capture = registry?.[captureToken];
@@ -15509,6 +15907,55 @@ var device = {
15509
15907
  await capturePromise.catch(() => {
15510
15908
  });
15511
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
+ }
15512
15959
  }
15513
15960
  },
15514
15961
  // FAST multi-stage capture for sub-second animation diffs. each stage