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.
- package/README.md +181 -2
- package/data/vocabulary/en.json +148 -0
- package/native/ocr.swift +13 -1
- package/native/rank.swift +87 -0
- package/native/simframed/Sources/PrivateAPI/CoreSimulatorPlatform.swift +72 -4
- package/native/simframed/Sources/PrivateAPI/PrivateAPI.swift +27 -0
- package/native/simframed/Sources/PrivateAPI/StubPlatform.swift +4 -0
- package/native/simframed/Sources/SimframeCore/CaptureRecovery.swift +19 -0
- package/native/simframed/Sources/simframed/main.swift +33 -3
- package/native/simframed/Tests/SimframeCoreTests/HashingTests.swift +41 -0
- package/native/supervise.swift +216 -0
- package/package.json +4 -1
- package/scripts/check-package.mjs +22 -2
- package/scripts/check-private.mjs +9 -0
- package/scripts/ci-integration-local.sh +79 -0
- package/scripts/ci-memory.mjs +104 -20
- package/scripts/collect-rulings.mjs +312 -0
- package/scripts/eval-fingerprint.mjs +100 -23
- package/scripts/eval-perception.mjs +33 -0
- package/scripts/phase17-corpus.mjs +176 -0
- package/scripts/probe-network.mjs +118 -0
- package/scripts/soak-capture.mjs +72 -0
- package/skills/simframe/SKILL.md +257 -7
- package/src/actions.js +1791 -44
- package/src/cli.js +216 -14
- package/src/control.js +1 -0
- package/src/fingerprint.js +43 -1
- package/src/graph.js +136 -7
- package/src/index.js +252 -14
- package/src/input.js +66 -3
- package/src/localhelper.js +161 -0
- package/src/matching.js +64 -1
- package/src/mcp.js +333 -32
- package/src/metrics.js +148 -3
- package/src/ocr.js +18 -1
- package/src/planner.js +195 -0
- package/src/platform/android.js +25 -1
- package/src/platform/index.js +11 -1
- package/src/platform/ios.js +25 -1
- package/src/png.js +26 -0
- package/src/refs.js +51 -8
- package/src/regions.js +215 -1
- package/src/screenmap.js +89 -9
- package/src/supervisor.js +161 -0
- package/src/view.js +375 -11
- package/src/vocabulary.js +134 -0
- 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) {
|