sootsim 0.1.240 → 0.1.241

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 (144) hide show
  1. package/dist-cli/bin.js +4 -4
  2. package/dist-cli/chunks/{agent-YS355M7O.js → agent-7Z3WG3YJ.js} +2 -2
  3. package/dist-cli/chunks/{agent-wrapper-DJJH2DE5.js → agent-wrapper-IMTGEJ7T.js} +2 -2
  4. package/dist-cli/chunks/{app-fonts-2KURCNO7.js → app-fonts-7E66PSAV.js} +2 -2
  5. package/dist-cli/chunks/{assert-Y7SV6LAY.js → assert-IEBAQZC7.js} +2 -2
  6. package/dist-cli/chunks/{auth-5VMBSD6F.js → auth-FRMHPA2X.js} +2 -2
  7. package/dist-cli/chunks/beta-AMMCLKHD.js +2 -0
  8. package/dist-cli/chunks/{camera-WCQNMZRH.js → camera-O67TQUAH.js} +2 -2
  9. package/dist-cli/chunks/{chunk-ZYMOUSJC.js → chunk-2DY3JHFK.js} +1 -1
  10. package/dist-cli/chunks/{chunk-BWR3AJZ3.js → chunk-4JSCYLLH.js} +1 -1
  11. package/dist-cli/chunks/{chunk-AX56F2Z5.js → chunk-4V6MFWDY.js} +1 -1
  12. package/dist-cli/chunks/{chunk-OBQJXFV3.js → chunk-5COOJUTE.js} +1 -1
  13. package/dist-cli/chunks/{chunk-SFNTUDIP.js → chunk-5ZNYFTOB.js} +2 -2
  14. package/dist-cli/chunks/{chunk-KY2HHVDG.js → chunk-7I3PV2RE.js} +2 -2
  15. package/dist-cli/chunks/{chunk-XVE2O2IR.js → chunk-AROVTS3P.js} +2 -2
  16. package/dist-cli/chunks/{chunk-DQEI7LOC.js → chunk-BIHEU677.js} +3 -3
  17. package/dist-cli/chunks/{chunk-4FUKKXDQ.js → chunk-CAOW2O5V.js} +2 -2
  18. package/dist-cli/chunks/{chunk-2ZEVD46I.js → chunk-CSZOZRKB.js} +3 -3
  19. package/dist-cli/chunks/{chunk-GW7REFUB.js → chunk-DS2KFLBO.js} +1 -1
  20. package/dist-cli/chunks/chunk-ECRSTV5C.js +118 -0
  21. package/dist-cli/chunks/{chunk-OUW757HM.js → chunk-EY3ZQWUQ.js} +52 -52
  22. package/dist-cli/chunks/{chunk-H6GS5C4F.js → chunk-F5YD4BLA.js} +2 -2
  23. package/dist-cli/chunks/{chunk-TFXCEOCW.js → chunk-FR7JCFAR.js} +1 -1
  24. package/dist-cli/chunks/{chunk-LBOSTYWQ.js → chunk-GFZLDVLE.js} +1 -1
  25. package/dist-cli/chunks/{chunk-OKHNWLZ7.js → chunk-GVTSKLLS.js} +2 -2
  26. package/dist-cli/chunks/{chunk-XV4VXQDA.js → chunk-H7G2QGUG.js} +2 -2
  27. package/dist-cli/chunks/{chunk-XSSTKHK7.js → chunk-H7SP6J3H.js} +2 -2
  28. package/dist-cli/chunks/{chunk-C4MSUAGM.js → chunk-HNI6U6M5.js} +1 -1
  29. package/dist-cli/chunks/{chunk-HIFBYPJK.js → chunk-IF5VXL45.js} +2 -2
  30. package/dist-cli/chunks/{chunk-5LCZXDDX.js → chunk-JMTQBHQY.js} +1 -1
  31. package/dist-cli/chunks/{chunk-T7BLBFPG.js → chunk-JX4N376W.js} +2 -2
  32. package/dist-cli/chunks/{chunk-WYT6JDAV.js → chunk-KFI26HW5.js} +1 -1
  33. package/dist-cli/chunks/chunk-KLXLOMI6.js +1 -0
  34. package/dist-cli/chunks/{chunk-DEO54ZY3.js → chunk-KN3Q6VSC.js} +2 -2
  35. package/dist-cli/chunks/{chunk-EJ6ULQFO.js → chunk-LVUOVKJO.js} +2 -2
  36. package/dist-cli/chunks/{chunk-SW5HPJC7.js → chunk-MCAJBAWD.js} +2 -2
  37. package/dist-cli/chunks/{chunk-JDRG4T3G.js → chunk-MGLTIS7P.js} +2 -2
  38. package/dist-cli/chunks/{chunk-NJYDI627.js → chunk-PR6AOTP5.js} +1 -1
  39. package/dist-cli/chunks/{chunk-GYWIGJB7.js → chunk-PVUUG5YW.js} +2 -2
  40. package/dist-cli/chunks/{chunk-6XHVSTMH.js → chunk-R3EZVIEM.js} +2 -2
  41. package/dist-cli/chunks/{chunk-4F63QBYN.js → chunk-U7XJNNGI.js} +3 -3
  42. package/dist-cli/chunks/{chunk-VU5WTUZJ.js → chunk-UF333CGY.js} +2 -2
  43. package/dist-cli/chunks/{chunk-3ULI43M6.js → chunk-VVUANJFA.js} +72 -23
  44. package/dist-cli/chunks/{chunk-2QILUPQJ.js → chunk-WFB3ICCN.js} +1 -1
  45. package/dist-cli/chunks/{chunk-M3VY5LEN.js → chunk-WIHKSZM4.js} +1 -1
  46. package/dist-cli/chunks/{chunk-KENWUOKC.js → chunk-WPWLJFKF.js} +2 -2
  47. package/dist-cli/chunks/{chunk-M2MJXMTO.js → chunk-YLWUYESW.js} +1 -1
  48. package/dist-cli/chunks/{chunk-7ZTOWAQF.js → chunk-YX7S6N42.js} +2 -2
  49. package/dist-cli/chunks/{chunk-M2OTE66X.js → chunk-Z72IF5FC.js} +2 -2
  50. package/dist-cli/chunks/chunk-ZGSGMGE3.js +1 -0
  51. package/dist-cli/chunks/{chunk-ILU6JNUJ.js → chunk-ZUZLQ4ZJ.js} +1 -1
  52. package/dist-cli/chunks/{chunk-GI36DI2I.js → chunk-ZYJCVJFP.js} +1 -1
  53. package/dist-cli/chunks/cli-version-DIYZFJMN.js +2 -0
  54. package/dist-cli/chunks/{compat-LUERFKMW.js → compat-ZRUVBIK7.js} +3 -3
  55. package/dist-cli/chunks/{config-U4Z6TYT6.js → config-VKQDFC5Z.js} +2 -2
  56. package/dist-cli/chunks/{control-I5YB63IZ.js → control-OYK2J4JW.js} +2 -2
  57. package/dist-cli/chunks/daemon-YQLY7PVM.js +2 -0
  58. package/dist-cli/chunks/{debug-HXECWDLV.js → debug-3WEO7ZRW.js} +3 -3
  59. package/dist-cli/chunks/{desktop-NZJLDJIO.js → desktop-DVOFHJKU.js} +3 -3
  60. package/dist-cli/chunks/{detox-LUOT4TIA.js → detox-GAGC276D.js} +2 -2
  61. package/dist-cli/chunks/{device-7TZ7BD36.js → device-256KFP2G.js} +2 -2
  62. package/dist-cli/chunks/{diagnose-DW2ANEGF.js → diagnose-GYKUU3N5.js} +2 -2
  63. package/dist-cli/chunks/drivers-FHWFHVQF.js +2 -0
  64. package/dist-cli/chunks/{film-6MYMKZ3N.js → film-WAV3WPQK.js} +3 -3
  65. package/dist-cli/chunks/flow-WKPPIDM6.js +2 -0
  66. package/dist-cli/chunks/help-P4A7NNSC.js +2 -0
  67. package/dist-cli/chunks/{hidden-runtime-alias-FUDVEMFB.js → hidden-runtime-alias-DODMMNOJ.js} +2 -2
  68. package/dist-cli/chunks/{home-paths-KU4MY22X.js → home-paths-QU5UX3MT.js} +2 -2
  69. package/dist-cli/chunks/inspect-NIHY67NU.js +2 -0
  70. package/dist-cli/chunks/install-desktop-NA53KD6G.js +2 -0
  71. package/dist-cli/chunks/{login-EVS4SZQN.js → login-OURQ3DVM.js} +4 -4
  72. package/dist-cli/chunks/{logout-K5MLSAWM.js → logout-ZYVJAPBO.js} +2 -2
  73. package/dist-cli/chunks/{maestro-OEDT2T7V.js → maestro-H2NGXNF4.js} +3 -3
  74. package/dist-cli/chunks/{maestro-generate-T4RZSVSM.js → maestro-generate-DA2IZPK2.js} +3 -3
  75. package/dist-cli/chunks/{mode-TZU7JML2.js → mode-QTK3ERUW.js} +2 -2
  76. package/dist-cli/chunks/optional-demo-registry-ZLN23OYV.js +2 -0
  77. package/dist-cli/chunks/{perf-YNBU57WH.js → perf-WZ4OPYXV.js} +2 -2
  78. package/dist-cli/chunks/{record-V6T2INPF.js → record-ZQSUHQJU.js} +3 -3
  79. package/dist-cli/chunks/runtime-RM67M6H3.js +2 -0
  80. package/dist-cli/chunks/{screenshot-command-CZZ6IXSD.js → screenshot-command-UDJ6TEAM.js} +3 -3
  81. package/dist-cli/chunks/{screenshots-capture-WVR5FCHX.js → screenshots-capture-4FK4NYXG.js} +2 -2
  82. package/dist-cli/chunks/{serve-6I35UUFI.js → serve-MCT4HKR7.js} +2 -2
  83. package/dist-cli/chunks/{setup-O7GD6GME.js → setup-I2IELNJN.js} +3 -3
  84. package/dist-cli/chunks/{skills-GBTBS2OK.js → skills-PVXI7LM6.js} +2 -2
  85. package/dist-cli/chunks/{storage-FI2YOPQE.js → storage-KZFCVRGD.js} +2 -2
  86. package/dist-cli/chunks/store-CEMC5MTK.js +2 -0
  87. package/dist-cli/chunks/telemetry-LFIQWPZD.js +2 -0
  88. package/dist-cli/chunks/{timeline-E5DVVATK.js → timeline-YAULXFEC.js} +2 -2
  89. package/dist-cli/chunks/{upgrade-QKOI3Q4U.js → upgrade-63TVREBM.js} +2 -2
  90. package/dist-cli/chunks/upload-APW46WK3.js +2 -0
  91. package/dist-cli/chunks/{version-UANAPNZM.js → version-JWWEGZS7.js} +2 -2
  92. package/dist-cli/chunks/{web-4LZ7DUUS.js → web-XO724HJL.js} +2 -2
  93. package/dist-cli/chunks/{what-happened-JOLNICMX.js → what-happened-DQ4MXAPJ.js} +2 -2
  94. package/dist-lib/agent-daemon-client.cjs +1 -1
  95. package/dist-lib/agent-events.cjs +1 -1
  96. package/dist-lib/agent-identity.cjs +1 -1
  97. package/dist-lib/agent-sessions.cjs +1 -1
  98. package/dist-lib/attached-projects.cjs +1 -1
  99. package/dist-lib/auth/shared-session.cjs +1 -1
  100. package/dist-lib/backend-origin.cjs +1 -1
  101. package/dist-lib/beta.cjs +1 -1
  102. package/dist-lib/beta.mjs +1 -1
  103. package/dist-lib/bridge-constants.cjs +1 -1
  104. package/dist-lib/cli-constants.cjs +1 -1
  105. package/dist-lib/config.cjs +1 -1
  106. package/dist-lib/detox/index.cjs +1 -1
  107. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  108. package/dist-lib/home-paths.cjs +1 -1
  109. package/dist-lib/host/bridge-host.cjs +1 -1
  110. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  111. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  112. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  113. package/dist-lib/host/websocket-proxy.cjs +1 -1
  114. package/dist-lib/index.cjs +1 -1
  115. package/dist-lib/jump-to-source-babel.cjs +1 -1
  116. package/dist-lib/menu.cjs +1 -1
  117. package/dist-lib/menu.mjs +1 -1
  118. package/dist-lib/metro.cjs +1 -1
  119. package/dist-lib/profiles.cjs +1 -1
  120. package/dist-lib/render-mode.cjs +1 -1
  121. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  122. package/dist-lib/sdk.cjs +157 -5
  123. package/dist-lib/sdk.mjs +156 -5
  124. package/dist-lib/skills.cjs +144 -4
  125. package/dist-lib/vite.cjs +1 -1
  126. package/package.json +1 -1
  127. package/skills/contrast/SKILL.md +1 -1
  128. package/skills/sootsim-debug/SKILL.md +21 -12
  129. package/dist-cli/chunks/beta-KMI4GCCN.js +0 -2
  130. package/dist-cli/chunks/chunk-3RYTIG4Y.js +0 -118
  131. package/dist-cli/chunks/chunk-R54EGANH.js +0 -1
  132. package/dist-cli/chunks/chunk-U3VMQUN6.js +0 -1
  133. package/dist-cli/chunks/cli-version-2BXTYTLJ.js +0 -2
  134. package/dist-cli/chunks/daemon-VTG3XKWA.js +0 -2
  135. package/dist-cli/chunks/drivers-RDXY5SDO.js +0 -2
  136. package/dist-cli/chunks/flow-KS77CBEA.js +0 -2
  137. package/dist-cli/chunks/help-H3ZD3LVG.js +0 -2
  138. package/dist-cli/chunks/inspect-ACDDO276.js +0 -2
  139. package/dist-cli/chunks/install-desktop-UQVXJAXS.js +0 -2
  140. package/dist-cli/chunks/optional-demo-registry-NIJ3MA5N.js +0 -2
  141. package/dist-cli/chunks/runtime-7XBSKGNO.js +0 -2
  142. package/dist-cli/chunks/store-FU7T5NPV.js +0 -2
  143. package/dist-cli/chunks/telemetry-REPVX6LA.js +0 -2
  144. package/dist-cli/chunks/upload-IFQM3FG4.js +0 -2
package/dist-lib/sdk.mjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! sootsim v0.1.240 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! sootsim v0.1.241 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // cli/commands/inspect/core.ts
4
4
  function resolveMaxMsFlag(args, fallbackMs) {
@@ -146,6 +146,101 @@ async function inspectDescribe(bridge, dumpOpts) {
146
146
  const result = await bridge.send({ type: "evaluate", code });
147
147
  return result ?? {};
148
148
  }
149
+ function scoreLayoutTextContrast(input) {
150
+ const required = input.fontSize >= 18 || input.fontSize >= 14 && (input.fontWeight ?? 400) >= 700 ? 3 : 4.5;
151
+ const { normal, background } = input;
152
+ if (normal.width <= 0 || normal.height <= 0 || normal.width !== background.width || normal.height !== background.height) {
153
+ return { ratio: null, required, reason: "capture dimensions do not match" };
154
+ }
155
+ const left = Math.max(0, Math.floor(input.box.x));
156
+ const top = Math.max(0, Math.floor(input.box.y));
157
+ const right = Math.min(normal.width, Math.ceil(input.box.x + input.box.w));
158
+ const bottom = Math.min(normal.height, Math.ceil(input.box.y + input.box.h));
159
+ if (right <= left || bottom <= top) {
160
+ return { ratio: null, required, reason: "text is outside the captured frame" };
161
+ }
162
+ const linear = (value) => {
163
+ const channel = value / 255;
164
+ return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
165
+ };
166
+ const ratioOf = (foreground, behind) => {
167
+ const foregroundLuminance = 0.2126 * linear(foreground[0]) + 0.7152 * linear(foreground[1]) + 0.0722 * linear(foreground[2]);
168
+ const backgroundLuminance = 0.2126 * linear(behind[0]) + 0.7152 * linear(behind[1]) + 0.0722 * linear(behind[2]);
169
+ return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
170
+ };
171
+ const hex = (color) => `#${color.slice(0, 3).map((value) => Math.round(value).toString(16).padStart(2, "0")).join("")}`;
172
+ const area = (right - left) * (bottom - top);
173
+ const step = Math.max(1, Math.floor(Math.sqrt(area / 12e3)));
174
+ const offsetAt = (x, y) => (y * normal.width + x) * 4;
175
+ const deltaAt = (offset) => Math.max(
176
+ Math.abs(normal.data[offset] - background.data[offset]),
177
+ Math.abs(normal.data[offset + 1] - background.data[offset + 1]),
178
+ Math.abs(normal.data[offset + 2] - background.data[offset + 2])
179
+ );
180
+ let strongestDelta = 0;
181
+ let strongestForeground = null;
182
+ let opaqueBackground = null;
183
+ for (let y = top; y < bottom; y += step) {
184
+ for (let x = left; x < right; x += step) {
185
+ const offset = offsetAt(x, y);
186
+ if (background.data[offset + 3] < 250) continue;
187
+ const behind = [
188
+ background.data[offset],
189
+ background.data[offset + 1],
190
+ background.data[offset + 2]
191
+ ];
192
+ opaqueBackground ?? (opaqueBackground = behind);
193
+ const delta = deltaAt(offset);
194
+ if (delta <= strongestDelta) continue;
195
+ strongestDelta = delta;
196
+ strongestForeground = [
197
+ normal.data[offset],
198
+ normal.data[offset + 1],
199
+ normal.data[offset + 2]
200
+ ];
201
+ }
202
+ }
203
+ let bestRatio = Infinity;
204
+ let bestForeground = null;
205
+ let bestBackground = null;
206
+ const consider = (foreground, behind) => {
207
+ const ratio = ratioOf(foreground, behind);
208
+ if (ratio >= bestRatio) return;
209
+ bestRatio = ratio;
210
+ bestForeground = foreground;
211
+ bestBackground = behind;
212
+ };
213
+ if (strongestDelta >= 2 && strongestForeground) {
214
+ for (let y = top; y < bottom; y += step) {
215
+ for (let x = left; x < right; x += step) {
216
+ const offset = offsetAt(x, y);
217
+ if (background.data[offset + 3] < 250 || deltaAt(offset) < 2) {
218
+ continue;
219
+ }
220
+ consider(strongestForeground, [
221
+ background.data[offset],
222
+ background.data[offset + 1],
223
+ background.data[offset + 2]
224
+ ]);
225
+ }
226
+ }
227
+ } else if (opaqueBackground) {
228
+ consider(opaqueBackground, opaqueBackground);
229
+ }
230
+ if (!bestForeground || !bestBackground || !Number.isFinite(bestRatio)) {
231
+ return {
232
+ ratio: null,
233
+ required,
234
+ reason: "could not isolate opaque rendered text pixels"
235
+ };
236
+ }
237
+ return {
238
+ ratio: Math.round(bestRatio * 100) / 100,
239
+ required,
240
+ foreground: hex(bestForeground),
241
+ background: hex(bestBackground)
242
+ };
243
+ }
149
244
  function layoutGetNativeEval(opts = {}) {
150
245
  const includeStyle = opts.styling === true;
151
246
  const filter = opts.filter?.trim().toLowerCase() ?? "";
@@ -155,6 +250,7 @@ function layoutGetNativeEval(opts = {}) {
155
250
  const list = await t.listInspectable({})
156
251
  if (!Array.isArray(list)) return []
157
252
  const includeStyle = ${JSON.stringify(includeStyle)}
253
+ const scoreContrast = ${scoreLayoutTextContrast.toString()}
158
254
  // narrows by ELEMENT, not by style field: fewer rows, full detail on each.
159
255
  const filter = ${JSON.stringify(filter)}
160
256
  const matchesFilter = (n, text) => {
@@ -199,7 +295,7 @@ function layoutGetNativeEval(opts = {}) {
199
295
  }
200
296
  const weightOf = (style) => {
201
297
  const w = style && style.fontWeight
202
- const n = typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN)
298
+ const n = w === 'bold' ? 700 : (typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN))
203
299
  return Number.isFinite(n) && n >= 600 ? n : undefined
204
300
  }
205
301
  const decoOf = (style) => {
@@ -242,7 +338,7 @@ function layoutGetNativeEval(opts = {}) {
242
338
  return abs.x + layout.width > 0 && abs.y + layout.height > 0 && abs.x < screenW && abs.y < screenH
243
339
  }
244
340
 
245
- return list.filter(isVisible).filter((n) => {
341
+ const elements = list.filter(isVisible).filter((n) => {
246
342
  const t = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
247
343
  return matchesFilter(n, t)
248
344
  }).map((n) => {
@@ -277,7 +373,7 @@ function layoutGetNativeEval(opts = {}) {
277
373
  ...base,
278
374
  borderRadius: radiusOf(style),
279
375
  padding: { t: round(pad.top), r: round(pad.right), b: round(pad.bottom), l: round(pad.left) },
280
- fontSize: fontSizeOf(style),
376
+ fontSize: text ? (fontSizeOf(style) || 14) : 0,
281
377
  color: text ? colorStr(style.color) : undefined,
282
378
  bg: colorStr(style.backgroundColor),
283
379
  opacity: opacityOf(style),
@@ -288,6 +384,50 @@ function layoutGetNativeEval(opts = {}) {
288
384
  borderColor: borderWidth ? borderColorOf(style) : undefined,
289
385
  }
290
386
  })
387
+ const textElements = elements.filter((element) => element.text && element.fontSize > 0)
388
+ if (!includeStyle || textElements.length === 0) return elements
389
+
390
+ const screenshot = window.SootSim && window.SootSim.bridges && window.SootSim.bridges.screenshot
391
+ if (typeof screenshot !== 'function') {
392
+ throw new Error('styled native layout requires the SootSim capture bridge')
393
+ }
394
+ const device = list.find((node) => node && node.device)?.device || {}
395
+ const width = Math.round(Number(device.width) || window.innerWidth || 0)
396
+ const height = Math.round(Number(device.height) || window.innerHeight || 0)
397
+ if (!width || !height) throw new Error('native layout has no capture dimensions')
398
+ const capture = async (suppressText) => {
399
+ const dataUrl = await screenshot({
400
+ format: 'png',
401
+ outputWidth: width,
402
+ outputHeight: height,
403
+ ...(suppressText ? { suppressText: true } : {}),
404
+ })
405
+ const image = await new Promise((resolve, reject) => {
406
+ const next = new Image()
407
+ next.onload = () => resolve(next)
408
+ next.onerror = () => reject(new Error('could not decode native layout pixels'))
409
+ next.src = dataUrl
410
+ })
411
+ const canvas = document.createElement('canvas')
412
+ canvas.width = image.naturalWidth
413
+ canvas.height = image.naturalHeight
414
+ const context = canvas.getContext('2d', { willReadFrequently: true })
415
+ if (!context) throw new Error('could not read native layout pixels')
416
+ context.drawImage(image, 0, 0)
417
+ return context.getImageData(0, 0, canvas.width, canvas.height)
418
+ }
419
+ const normal = await capture(false)
420
+ const background = await capture(true)
421
+ for (const element of textElements) {
422
+ element.contrast = scoreContrast({
423
+ normal,
424
+ background,
425
+ box: element.box,
426
+ fontSize: element.fontSize,
427
+ fontWeight: element.fontWeight,
428
+ })
429
+ }
430
+ return elements
291
431
  })()`;
292
432
  }
293
433
  var LAYOUT_GET_NATIVE_EVAL = layoutGetNativeEval();
@@ -314,6 +454,11 @@ function formatLayoutElements(elements, opts) {
314
454
  if (el.fontWeight) parts.push(`weight:${el.fontWeight}`);
315
455
  if (el.color) parts.push(`color:${el.color}`);
316
456
  if (el.bg) parts.push(`bg:${el.bg}`);
457
+ if (el.contrast) {
458
+ const score = el.contrast.ratio == null ? `?/${el.contrast.required}` : `${el.contrast.ratio}/${el.contrast.required}${el.contrast.ratio < el.contrast.required ? " FAIL" : ""}`;
459
+ parts.push(`contrast:${score}`);
460
+ if (el.contrast.background) parts.push(`on:${el.contrast.background}`);
461
+ }
317
462
  if (el.opacity != null) parts.push(`opacity:${el.opacity}`);
318
463
  if (el.textDecoration) parts.push(`deco:${el.textDecoration}`);
319
464
  if (el.overflow) parts.push(`overflow:${el.overflow}`);
@@ -1114,6 +1259,10 @@ async function inspectScreens(bridge) {
1114
1259
  test && typeof test.getNavigationSnapshot === 'function'
1115
1260
  ? await test.getNavigationSnapshot()
1116
1261
  : null
1262
+ const route =
1263
+ test && typeof test.getRouteInfo === 'function'
1264
+ ? await test.getRouteInfo()
1265
+ : null
1117
1266
  const keyboard =
1118
1267
  kb && typeof kb.getLayout === 'function'
1119
1268
  ? (() => {
@@ -1130,13 +1279,14 @@ async function inspectScreens(bridge) {
1130
1279
  } : null
1131
1280
  })()
1132
1281
  : null
1133
- return { nav: navSnap, keyboard }
1282
+ return { nav: navSnap, route, keyboard }
1134
1283
  })()`
1135
1284
  });
1136
1285
  const shell = await getShellState(bridge, 500).catch(() => null);
1137
1286
  return {
1138
1287
  shell,
1139
1288
  nav: payload?.nav ?? null,
1289
+ route: payload?.route ?? null,
1140
1290
  keyboard: payload?.keyboard ?? null
1141
1291
  };
1142
1292
  }
@@ -1993,6 +2143,7 @@ export {
1993
2143
  resolveFindMode,
1994
2144
  resolveMaxMsFlag,
1995
2145
  scoreInteractive,
2146
+ scoreLayoutTextContrast,
1996
2147
  setDebugChannels,
1997
2148
  shouldSkipAutoSettleForInspectPick,
1998
2149
  tapBest,
@@ -1,4 +1,4 @@
1
- /*! sootsim v0.1.240 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! sootsim v0.1.241 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -16429,6 +16429,101 @@ var init_app_state_reset = __esm({
16429
16429
  });
16430
16430
 
16431
16431
  // cli/commands/inspect/core.ts
16432
+ function scoreLayoutTextContrast(input) {
16433
+ const required = input.fontSize >= 18 || input.fontSize >= 14 && (input.fontWeight ?? 400) >= 700 ? 3 : 4.5;
16434
+ const { normal, background } = input;
16435
+ if (normal.width <= 0 || normal.height <= 0 || normal.width !== background.width || normal.height !== background.height) {
16436
+ return { ratio: null, required, reason: "capture dimensions do not match" };
16437
+ }
16438
+ const left = Math.max(0, Math.floor(input.box.x));
16439
+ const top = Math.max(0, Math.floor(input.box.y));
16440
+ const right = Math.min(normal.width, Math.ceil(input.box.x + input.box.w));
16441
+ const bottom = Math.min(normal.height, Math.ceil(input.box.y + input.box.h));
16442
+ if (right <= left || bottom <= top) {
16443
+ return { ratio: null, required, reason: "text is outside the captured frame" };
16444
+ }
16445
+ const linear = (value) => {
16446
+ const channel = value / 255;
16447
+ return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
16448
+ };
16449
+ const ratioOf = (foreground, behind) => {
16450
+ const foregroundLuminance = 0.2126 * linear(foreground[0]) + 0.7152 * linear(foreground[1]) + 0.0722 * linear(foreground[2]);
16451
+ const backgroundLuminance = 0.2126 * linear(behind[0]) + 0.7152 * linear(behind[1]) + 0.0722 * linear(behind[2]);
16452
+ return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
16453
+ };
16454
+ const hex = (color) => `#${color.slice(0, 3).map((value) => Math.round(value).toString(16).padStart(2, "0")).join("")}`;
16455
+ const area = (right - left) * (bottom - top);
16456
+ const step = Math.max(1, Math.floor(Math.sqrt(area / 12e3)));
16457
+ const offsetAt = (x, y) => (y * normal.width + x) * 4;
16458
+ const deltaAt = (offset) => Math.max(
16459
+ Math.abs(normal.data[offset] - background.data[offset]),
16460
+ Math.abs(normal.data[offset + 1] - background.data[offset + 1]),
16461
+ Math.abs(normal.data[offset + 2] - background.data[offset + 2])
16462
+ );
16463
+ let strongestDelta = 0;
16464
+ let strongestForeground = null;
16465
+ let opaqueBackground = null;
16466
+ for (let y = top; y < bottom; y += step) {
16467
+ for (let x = left; x < right; x += step) {
16468
+ const offset = offsetAt(x, y);
16469
+ if (background.data[offset + 3] < 250) continue;
16470
+ const behind = [
16471
+ background.data[offset],
16472
+ background.data[offset + 1],
16473
+ background.data[offset + 2]
16474
+ ];
16475
+ opaqueBackground ??= behind;
16476
+ const delta = deltaAt(offset);
16477
+ if (delta <= strongestDelta) continue;
16478
+ strongestDelta = delta;
16479
+ strongestForeground = [
16480
+ normal.data[offset],
16481
+ normal.data[offset + 1],
16482
+ normal.data[offset + 2]
16483
+ ];
16484
+ }
16485
+ }
16486
+ let bestRatio = Infinity;
16487
+ let bestForeground = null;
16488
+ let bestBackground = null;
16489
+ const consider = (foreground, behind) => {
16490
+ const ratio = ratioOf(foreground, behind);
16491
+ if (ratio >= bestRatio) return;
16492
+ bestRatio = ratio;
16493
+ bestForeground = foreground;
16494
+ bestBackground = behind;
16495
+ };
16496
+ if (strongestDelta >= 2 && strongestForeground) {
16497
+ for (let y = top; y < bottom; y += step) {
16498
+ for (let x = left; x < right; x += step) {
16499
+ const offset = offsetAt(x, y);
16500
+ if (background.data[offset + 3] < 250 || deltaAt(offset) < 2) {
16501
+ continue;
16502
+ }
16503
+ consider(strongestForeground, [
16504
+ background.data[offset],
16505
+ background.data[offset + 1],
16506
+ background.data[offset + 2]
16507
+ ]);
16508
+ }
16509
+ }
16510
+ } else if (opaqueBackground) {
16511
+ consider(opaqueBackground, opaqueBackground);
16512
+ }
16513
+ if (!bestForeground || !bestBackground || !Number.isFinite(bestRatio)) {
16514
+ return {
16515
+ ratio: null,
16516
+ required,
16517
+ reason: "could not isolate opaque rendered text pixels"
16518
+ };
16519
+ }
16520
+ return {
16521
+ ratio: Math.round(bestRatio * 100) / 100,
16522
+ required,
16523
+ foreground: hex(bestForeground),
16524
+ background: hex(bestBackground)
16525
+ };
16526
+ }
16432
16527
  function layoutGetNativeEval(opts = {}) {
16433
16528
  const includeStyle = opts.styling === true;
16434
16529
  const filter = opts.filter?.trim().toLowerCase() ?? "";
@@ -16438,6 +16533,7 @@ function layoutGetNativeEval(opts = {}) {
16438
16533
  const list = await t.listInspectable({})
16439
16534
  if (!Array.isArray(list)) return []
16440
16535
  const includeStyle = ${JSON.stringify(includeStyle)}
16536
+ const scoreContrast = ${scoreLayoutTextContrast.toString()}
16441
16537
  // narrows by ELEMENT, not by style field: fewer rows, full detail on each.
16442
16538
  const filter = ${JSON.stringify(filter)}
16443
16539
  const matchesFilter = (n, text) => {
@@ -16482,7 +16578,7 @@ function layoutGetNativeEval(opts = {}) {
16482
16578
  }
16483
16579
  const weightOf = (style) => {
16484
16580
  const w = style && style.fontWeight
16485
- const n = typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN)
16581
+ const n = w === 'bold' ? 700 : (typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN))
16486
16582
  return Number.isFinite(n) && n >= 600 ? n : undefined
16487
16583
  }
16488
16584
  const decoOf = (style) => {
@@ -16525,7 +16621,7 @@ function layoutGetNativeEval(opts = {}) {
16525
16621
  return abs.x + layout.width > 0 && abs.y + layout.height > 0 && abs.x < screenW && abs.y < screenH
16526
16622
  }
16527
16623
 
16528
- return list.filter(isVisible).filter((n) => {
16624
+ const elements = list.filter(isVisible).filter((n) => {
16529
16625
  const t = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
16530
16626
  return matchesFilter(n, t)
16531
16627
  }).map((n) => {
@@ -16560,7 +16656,7 @@ function layoutGetNativeEval(opts = {}) {
16560
16656
  ...base,
16561
16657
  borderRadius: radiusOf(style),
16562
16658
  padding: { t: round(pad.top), r: round(pad.right), b: round(pad.bottom), l: round(pad.left) },
16563
- fontSize: fontSizeOf(style),
16659
+ fontSize: text ? (fontSizeOf(style) || 14) : 0,
16564
16660
  color: text ? colorStr(style.color) : undefined,
16565
16661
  bg: colorStr(style.backgroundColor),
16566
16662
  opacity: opacityOf(style),
@@ -16571,6 +16667,50 @@ function layoutGetNativeEval(opts = {}) {
16571
16667
  borderColor: borderWidth ? borderColorOf(style) : undefined,
16572
16668
  }
16573
16669
  })
16670
+ const textElements = elements.filter((element) => element.text && element.fontSize > 0)
16671
+ if (!includeStyle || textElements.length === 0) return elements
16672
+
16673
+ const screenshot = window.SootSim && window.SootSim.bridges && window.SootSim.bridges.screenshot
16674
+ if (typeof screenshot !== 'function') {
16675
+ throw new Error('styled native layout requires the SootSim capture bridge')
16676
+ }
16677
+ const device = list.find((node) => node && node.device)?.device || {}
16678
+ const width = Math.round(Number(device.width) || window.innerWidth || 0)
16679
+ const height = Math.round(Number(device.height) || window.innerHeight || 0)
16680
+ if (!width || !height) throw new Error('native layout has no capture dimensions')
16681
+ const capture = async (suppressText) => {
16682
+ const dataUrl = await screenshot({
16683
+ format: 'png',
16684
+ outputWidth: width,
16685
+ outputHeight: height,
16686
+ ...(suppressText ? { suppressText: true } : {}),
16687
+ })
16688
+ const image = await new Promise((resolve, reject) => {
16689
+ const next = new Image()
16690
+ next.onload = () => resolve(next)
16691
+ next.onerror = () => reject(new Error('could not decode native layout pixels'))
16692
+ next.src = dataUrl
16693
+ })
16694
+ const canvas = document.createElement('canvas')
16695
+ canvas.width = image.naturalWidth
16696
+ canvas.height = image.naturalHeight
16697
+ const context = canvas.getContext('2d', { willReadFrequently: true })
16698
+ if (!context) throw new Error('could not read native layout pixels')
16699
+ context.drawImage(image, 0, 0)
16700
+ return context.getImageData(0, 0, canvas.width, canvas.height)
16701
+ }
16702
+ const normal = await capture(false)
16703
+ const background = await capture(true)
16704
+ for (const element of textElements) {
16705
+ element.contrast = scoreContrast({
16706
+ normal,
16707
+ background,
16708
+ box: element.box,
16709
+ fontSize: element.fontSize,
16710
+ fontWeight: element.fontWeight,
16711
+ })
16712
+ }
16713
+ return elements
16574
16714
  })()`;
16575
16715
  }
16576
16716
  function readyProbeHasContent(probe) {
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! sootsim v0.1.240 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! sootsim v0.1.241 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sootsim",
3
- "version": "0.1.240",
3
+ "version": "0.1.241",
4
4
  "description": "sootsim CLI + vite/metro plugins + skills registry. bridge client for driving the proprietary sootsim-engine over WebSocket.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "MIT",
@@ -29,7 +29,7 @@ automation, inspecting the render tree, and managing sims.
29
29
  | `sootsim find` | locate nodes by text / testID / role / type / predicate |
30
30
  | `sootsim assert` | run any read verb and convert its output into an exit code |
31
31
  | `sootsim screenshot` | capture PNGs and manage App Store screenshot assets |
32
- | `sootsim get` | read runtime state (tree, a11y, url, count, errors, …) |
32
+ | `sootsim get` | read runtime state (tree, a11y, layout, url, count, errors, …) |
33
33
  | `sootsim do` | drive the app (tap, type, scroll, swipe, key, reload, …) |
34
34
  | `sootsim wait` | block until the runtime reaches a known state |
35
35
  | `sootsim shell` | drive shell state and simulator chrome |
@@ -239,17 +239,19 @@ watch out for:
239
239
  ## branch — accessibility
240
240
 
241
241
  ```sh
242
- sootsim get a11y # full a11y tree
243
- sootsim get a11y --issues # only nodes with problems
242
+ sootsim get a11y # roles, labels, hints, and target geometry
243
+ sootsim get layout --styling # boxes, styles, and native pixel contrast
244
244
  sootsim describe --a11y # inline a11y info on every node
245
245
  ```
246
246
 
247
- issue summary format:
247
+ native styled layout reads rendered pixels rather than inferring contrast from
248
+ tokens. it compares the normal frame with one CanvasKit capture where text is
249
+ suppressed and adds each text score to the existing layout row and issue list.
250
+
251
+ contrast summary format:
248
252
 
249
253
  ```
250
- [input] "" role=none label=null ⚠ missing label
251
- [button] "➜" @(340,280) 20x20 (tap) ⚠ touch target <44×44
252
- <text> "Sign in" color=#777 bg=#888 ⚠ contrast 1.4:1 (need 4.5:1)
254
+ #loginButton @(24,347) 120x24 font:16 color:#777 contrast:1.4/4.5 FAIL on:#888888 "Sign in"
253
255
  ```
254
256
 
255
257
  finding offenders fast:
@@ -266,7 +268,8 @@ what gets checked:
266
268
  - **missing roles** — `Pressable`/`Touchable` without `accessibilityRole`
267
269
  - **missing hints** — complex interactions without `accessibilityHint`
268
270
  - **touch target size** — tappable nodes with hitSlop-expanded rect <44×44
269
- - **contrast** — text color vs detectable background
271
+ - **contrast** — run `sootsim get layout --styling`; it samples final text and
272
+ text-suppressed background pixels, including gradients and composited opacity
270
273
  - **label / visible-text mismatch** — label that reads differently from
271
274
  the text VoiceOver would fall back to (often a translation bug)
272
275
  - **duplicate labels** — two reachable nodes with identical labels on
@@ -282,10 +285,13 @@ triage order:
282
285
  fix → verify loop (the same shape as debugging):
283
286
 
284
287
  ```sh
285
- sootsim get a11y --issues > before.txt
288
+ sootsim get a11y > before-a11y.txt
289
+ sootsim get layout --styling --json > before-layout.json
286
290
  # apply the fix
287
- sootsim get a11y --issues > after.txt
288
- diff before.txt after.txt
291
+ sootsim get a11y > after-a11y.txt
292
+ sootsim get layout --styling --json > after-layout.json
293
+ diff before-a11y.txt after-a11y.txt
294
+ diff before-layout.json after-layout.json
289
295
  ```
290
296
 
291
297
  fixing common cases:
@@ -340,8 +346,11 @@ type names sneak in).
340
346
  `--visible`, or both.
341
347
  - **`perf shell` reports zero frames** — you didn't run `perf shell start`
342
348
  first, or the capture window closed before any frame committed.
343
- - **`get a11y --issues` returns 100+ items** — sort by P0 first; fixing
344
- one label often cascades to a parent's a11y subtree.
349
+ - **`get a11y` returns 100+ items** — narrow with `find --role`,
350
+ `find --pressable`, or `find --no-label`; the a11y tree is descriptive and
351
+ intentionally does not invent an `--issues` grading mode.
352
+ - **styled layout cannot capture pixels** — open a tenant app, dismiss system
353
+ overlays, and let the launch transition finish before retrying.
345
354
  - **timeline footer keeps showing 1 error but `get errors` is empty** —
346
355
  the footer reads "since last cursor advance"; `get errors` is absolute.
347
356
  the error already scrolled out of the absolute ring buffer.
@@ -1,2 +0,0 @@
1
- /*! sootsim v0.1.240 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
- import{a,b,c,d}from"./chunk-5LCZXDDX.js";import"./chunk-OBQJXFV3.js";export{d as BETA_ASK_HEADLINE,b as BETA_LABEL,c as BETA_TAGLINE,a as IS_BETA};