simframe 0.10.0 → 0.12.0

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 (47) hide show
  1. package/README.md +181 -2
  2. package/data/vocabulary/en.json +148 -0
  3. package/native/ocr.swift +13 -1
  4. package/native/rank.swift +87 -0
  5. package/native/simframed/Sources/PrivateAPI/CoreSimulatorPlatform.swift +72 -4
  6. package/native/simframed/Sources/PrivateAPI/PrivateAPI.swift +27 -0
  7. package/native/simframed/Sources/PrivateAPI/StubPlatform.swift +4 -0
  8. package/native/simframed/Sources/SimframeCore/CaptureRecovery.swift +19 -0
  9. package/native/simframed/Sources/simframed/main.swift +33 -3
  10. package/native/simframed/Tests/SimframeCoreTests/HashingTests.swift +41 -0
  11. package/native/supervise.swift +216 -0
  12. package/package.json +4 -1
  13. package/scripts/check-package.mjs +22 -2
  14. package/scripts/check-private.mjs +9 -0
  15. package/scripts/ci-integration-local.sh +79 -0
  16. package/scripts/ci-memory.mjs +104 -20
  17. package/scripts/collect-rulings.mjs +312 -0
  18. package/scripts/eval-fingerprint.mjs +100 -23
  19. package/scripts/eval-perception.mjs +33 -0
  20. package/scripts/phase17-corpus.mjs +176 -0
  21. package/scripts/probe-network.mjs +118 -0
  22. package/scripts/soak-capture.mjs +72 -0
  23. package/skills/simframe/SKILL.md +257 -7
  24. package/src/actions.js +1791 -44
  25. package/src/cli.js +216 -14
  26. package/src/control.js +1 -0
  27. package/src/fingerprint.js +43 -1
  28. package/src/graph.js +136 -7
  29. package/src/index.js +252 -14
  30. package/src/input.js +66 -3
  31. package/src/localhelper.js +161 -0
  32. package/src/matching.js +64 -1
  33. package/src/mcp.js +333 -32
  34. package/src/metrics.js +148 -3
  35. package/src/ocr.js +18 -1
  36. package/src/planner.js +195 -0
  37. package/src/platform/android.js +25 -1
  38. package/src/platform/index.js +11 -1
  39. package/src/platform/ios.js +25 -1
  40. package/src/png.js +26 -0
  41. package/src/refs.js +51 -8
  42. package/src/regions.js +215 -1
  43. package/src/screenmap.js +89 -9
  44. package/src/supervisor.js +161 -0
  45. package/src/view.js +375 -11
  46. package/src/vocabulary.js +134 -0
  47. package/src/wrote.js +136 -0
package/src/matching.js CHANGED
@@ -337,9 +337,67 @@ function sameControl(a, b) {
337
337
  // and a map built before that still resolves.
338
338
  if (isAxTarget(a) && !isAxTarget(b) && sameElementSeenTwice(a, b)) return true;
339
339
  if (isAxTarget(b) && !isAxTarget(a) && sameElementSeenTwice(b, a)) return true;
340
+ // And a caption sitting on the control it names. Containment above requires
341
+ // the container to be a recognised hit target, and a React Native composite
342
+ // is a generic element — so a form row published its caption and its select
343
+ // with the same label, 152 points apart, and nothing merged them. They are
344
+ // one control seen twice, not two candidates: answering `ambiguous` here
345
+ // costs a round trip to choose between a thing and its own name.
346
+ if (isCaptionFor(a, b) || isCaptionFor(b, a)) return true;
340
347
  return false;
341
348
  }
342
349
 
350
+ /** Does `caption` merely name `control`, overlapping it, with the same label? */
351
+ function isCaptionFor(caption, control) {
352
+ if (!namesOnly(caption) || namesOnly(control)) return false;
353
+ if (norm(caption.label) !== norm(control.label)) return false;
354
+ return overlaps(caption.frame, control.frame);
355
+ }
356
+
357
+ /** Any shared area at all — a caption's box often clears its control's by a point or two. */
358
+ function overlaps(a, b) {
359
+ if (!a || !b) return false;
360
+ return a.x < b.x + b.width && b.x < a.x + a.width
361
+ && a.y < b.y + b.height && b.y < a.y + a.height;
362
+ }
363
+
364
+ /**
365
+ * Roles that only ever *name* a control, never are one.
366
+ *
367
+ * The tree publishes a caption and the control it captions with the same label,
368
+ * and a React Native composite (a `native-base` Select, say) surfaces as a
369
+ * generic element that `INTERACTIVE_ROLE` does not recognise. So `tap "Problem"`
370
+ * resolved to the caption at (49,486) and did nothing, while the select sat at
371
+ * (201,496) — reported as half of the single largest cost in a real-app run,
372
+ * because it means neither selector can be trusted.
373
+ *
374
+ * `collapseSamePlace` already prefers a hit target over the text printed on it,
375
+ * but only once `sameControl` has decided they are the same place. These two
376
+ * were 152 points apart with a generic role, so nothing merged them.
377
+ */
378
+ const NAMING_ONLY_ROLE = /^(statictext|text|label|heading|image)$/i;
379
+
380
+ const namesOnly = (t) => NAMING_ONLY_ROLE.test(t?.type ?? '');
381
+
382
+ /**
383
+ * A caption never wins over a control that answers the same name.
384
+ *
385
+ * Applied only when the two are close enough in score to be answering the same
386
+ * question — a heading is still reachable when nothing else matches, which is
387
+ * why this promotes rather than filters. Anything further apart than
388
+ * `CAPTION_MARGIN` is a different match, not the same match seen twice.
389
+ */
390
+ export const CAPTION_MARGIN = 0.2;
391
+
392
+ export function preferTheControl(ranked) {
393
+ if (!ranked.length || !namesOnly(ranked[0].target)) return ranked;
394
+ const lead = ranked[0].score;
395
+ const i = ranked.findIndex((c, idx) => idx > 0 && !namesOnly(c.target) && lead - c.score <= CAPTION_MARGIN);
396
+ if (i < 0) return ranked;
397
+ const promoted = { ...ranked[i], reasons: [...ranked[i].reasons, 'the control, not the caption naming it'] };
398
+ return [promoted, ...ranked.filter((_, idx) => idx !== i)];
399
+ }
400
+
343
401
  function collapseSamePlace(ranked) {
344
402
  const kept = [];
345
403
  for (const c of ranked) {
@@ -351,6 +409,11 @@ function collapseSamePlace(ranked) {
351
409
  // Prefer the real hit target: an accessibility element over OCR's reading of
352
410
  // it, and an interactive role over a caption sitting inside it.
353
411
  const better = (candidate, incumbent) => {
412
+ // A caption loses to what it names before anything else is considered:
413
+ // a `StaticText` is never the tap target when the control it labels is
414
+ // right there, whatever either one's source.
415
+ if (namesOnly(incumbent.target) && !namesOnly(candidate.target)) return true;
416
+ if (namesOnly(candidate.target) && !namesOnly(incumbent.target)) return false;
354
417
  if (isAxTarget(candidate.target) && !isAxTarget(incumbent.target)) return true;
355
418
  if (!isAxTarget(candidate.target) && isAxTarget(incumbent.target)) return false;
356
419
  return INTERACTIVE_ROLE.test(candidate.target.type ?? '')
@@ -368,7 +431,7 @@ function collapseSamePlace(ranked) {
368
431
  * @returns {{status: 'ok'|'ambiguous'|'none', target?, score?, reasons?, alternatives?}}
369
432
  */
370
433
  export function resolve(targets, intent, options = {}) {
371
- const ranked = collapseSamePlace(rank(targets, intent, options).filter((c) => c.score >= MINIMUM_SCORE));
434
+ const ranked = preferTheControl(collapseSamePlace(rank(targets, intent, options).filter((c) => c.score >= MINIMUM_SCORE)));
372
435
  if (!ranked.length) return { status: 'none', alternatives: [] };
373
436
  const [best, second] = ranked;
374
437
  if (second && best.score - second.score < AMBIGUITY_MARGIN) {