@mercuryo-ai/agentbrowse 0.2.60 → 0.2.63

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 (105) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/README.md +132 -14
  3. package/dist/browser-session-state.d.ts +40 -10
  4. package/dist/browser-session-state.d.ts.map +1 -1
  5. package/dist/browser-session-state.js +63 -5
  6. package/dist/commands/act.d.ts.map +1 -1
  7. package/dist/commands/act.js +548 -535
  8. package/dist/commands/attach.d.ts +1 -3
  9. package/dist/commands/attach.d.ts.map +1 -1
  10. package/dist/commands/attach.js +5 -12
  11. package/dist/commands/browser-connection-failure.d.ts +9 -0
  12. package/dist/commands/browser-connection-failure.d.ts.map +1 -0
  13. package/dist/commands/browser-connection-failure.js +15 -0
  14. package/dist/commands/browser-status.d.ts +0 -2
  15. package/dist/commands/browser-status.d.ts.map +1 -1
  16. package/dist/commands/browser-status.js +27 -37
  17. package/dist/commands/close.d.ts.map +1 -1
  18. package/dist/commands/close.js +5 -0
  19. package/dist/commands/extract.d.ts.map +1 -1
  20. package/dist/commands/extract.js +147 -144
  21. package/dist/commands/interaction-kernel.d.ts +1 -1
  22. package/dist/commands/interaction-kernel.d.ts.map +1 -1
  23. package/dist/commands/interaction-kernel.js +1 -1
  24. package/dist/commands/launch.d.ts +0 -1
  25. package/dist/commands/launch.d.ts.map +1 -1
  26. package/dist/commands/launch.js +11 -12
  27. package/dist/commands/navigate.d.ts.map +1 -1
  28. package/dist/commands/navigate.js +79 -73
  29. package/dist/commands/observe-accessibility.d.ts.map +1 -1
  30. package/dist/commands/observe-accessibility.js +36 -2
  31. package/dist/commands/observe-inventory.d.ts +50 -7
  32. package/dist/commands/observe-inventory.d.ts.map +1 -1
  33. package/dist/commands/observe-inventory.js +822 -99
  34. package/dist/commands/observe-persistence.d.ts.map +1 -1
  35. package/dist/commands/observe-persistence.js +49 -6
  36. package/dist/commands/observe-projection.d.ts +6 -2
  37. package/dist/commands/observe-projection.d.ts.map +1 -1
  38. package/dist/commands/observe-projection.js +251 -27
  39. package/dist/commands/observe-semantics.d.ts +1 -0
  40. package/dist/commands/observe-semantics.d.ts.map +1 -1
  41. package/dist/commands/observe-semantics.js +541 -135
  42. package/dist/commands/observe-signals.d.ts +4 -4
  43. package/dist/commands/observe-signals.d.ts.map +1 -1
  44. package/dist/commands/observe-signals.js +2 -2
  45. package/dist/commands/observe-surfaces.d.ts +2 -1
  46. package/dist/commands/observe-surfaces.d.ts.map +1 -1
  47. package/dist/commands/observe-surfaces.js +143 -45
  48. package/dist/commands/observe.d.ts +5 -1
  49. package/dist/commands/observe.d.ts.map +1 -1
  50. package/dist/commands/observe.js +266 -274
  51. package/dist/commands/screenshot.d.ts.map +1 -1
  52. package/dist/commands/screenshot.js +50 -64
  53. package/dist/commands/semantic-observe.d.ts.map +1 -1
  54. package/dist/commands/semantic-observe.js +43 -0
  55. package/dist/library.d.ts +3 -1
  56. package/dist/library.d.ts.map +1 -1
  57. package/dist/library.js +3 -1
  58. package/dist/match-resolve-fill.d.ts +196 -0
  59. package/dist/match-resolve-fill.d.ts.map +1 -0
  60. package/dist/match-resolve-fill.js +700 -0
  61. package/dist/match-resolve-fill.test-support.d.ts +34 -0
  62. package/dist/match-resolve-fill.test-support.d.ts.map +1 -0
  63. package/dist/match-resolve-fill.test-support.js +81 -0
  64. package/dist/protected-fill.d.ts.map +1 -1
  65. package/dist/protected-fill.js +46 -7
  66. package/dist/runtime-protected-state.d.ts.map +1 -1
  67. package/dist/runtime-protected-state.js +12 -0
  68. package/dist/runtime-state.d.ts +6 -0
  69. package/dist/runtime-state.d.ts.map +1 -1
  70. package/dist/runtime-state.js +6 -0
  71. package/dist/secrets/form-matcher.d.ts.map +1 -1
  72. package/dist/secrets/form-matcher.js +76 -27
  73. package/dist/secrets/protected-exact-value-redaction.d.ts.map +1 -1
  74. package/dist/secrets/protected-exact-value-redaction.js +6 -0
  75. package/dist/secrets/protected-fill.js +3 -3
  76. package/dist/session.d.ts +3 -3
  77. package/dist/session.d.ts.map +1 -1
  78. package/dist/session.js +2 -2
  79. package/dist/solver/browser-launcher.d.ts.map +1 -1
  80. package/dist/solver/browser-launcher.js +2 -1
  81. package/dist/sticky-owner-host-entry.d.ts +2 -0
  82. package/dist/sticky-owner-host-entry.d.ts.map +1 -0
  83. package/dist/sticky-owner-host-entry.js +97 -0
  84. package/dist/sticky-owner.d.ts +15 -0
  85. package/dist/sticky-owner.d.ts.map +1 -0
  86. package/dist/sticky-owner.js +431 -0
  87. package/dist/testing.d.ts +1 -0
  88. package/dist/testing.d.ts.map +1 -1
  89. package/dist/testing.js +1 -0
  90. package/docs/README.md +28 -11
  91. package/docs/api-reference.md +311 -19
  92. package/docs/assistive-runtime.md +41 -16
  93. package/docs/configuration.md +36 -4
  94. package/docs/getting-started.md +73 -5
  95. package/docs/integration-checklist.md +32 -3
  96. package/docs/match-resolve-fill.md +699 -0
  97. package/docs/protected-fill.md +373 -91
  98. package/docs/testing.md +147 -15
  99. package/docs/troubleshooting.md +47 -6
  100. package/examples/README.md +7 -0
  101. package/examples/match-resolve-fill.ts +107 -0
  102. package/package.json +4 -2
  103. package/dist/protected-fill-browser.d.ts +0 -22
  104. package/dist/protected-fill-browser.d.ts.map +0 -1
  105. package/dist/protected-fill-browser.js +0 -52
@@ -14,10 +14,13 @@ export function inferStructuredCellVariantFromEvidence(evidence) {
14
14
  const hasSeatMetadata = Boolean(evidence.hasSeatAttribute || evidence.hasSeatRowAttribute || evidence.hasSeatColumnAttribute);
15
15
  const seatIdentityLike = /(?:\bseat\s*\d{1,3}[a-z]?\b|место\s*\d{1,3}[a-z]?\b|\b\d{1,3}[a-z]\b|\b[a-z]\d{1,3}\b)/i.test(normalizedLabel);
16
16
  const seatClassLike = /seat|cabin|fare|row/.test(className);
17
- const compactDateCellLabel = /^(?:\d{1,2}|january|february|march|april|may|june|july|august|september|october|november|december|январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)$/i.test(normalizedLabel) || /^(?:\d{1,2}[./-]\d{1,2}(?:[./-]\d{2,4})?)$/.test(normalizedLabel);
17
+ const compactDateCellLabel = /^\d{1,2}$/.test(normalizedLabel) ||
18
+ /^(?:\d{1,2}[./-]\d{1,2}(?:[./-]\d{2,4})?)$/.test(normalizedLabel);
19
+ const leadingNumericDayToken = /^\d{1,2}\b/.test(normalizedLabel);
18
20
  const dateLike = surfaceKind === 'datepicker' ||
19
21
  (hasDateMetadata && role === 'gridcell') ||
20
- ((role === 'gridcell' || structuredSurface || hasDateMetadata) && compactDateCellLabel);
22
+ ((role === 'gridcell' || structuredSurface || hasDateMetadata) &&
23
+ (compactDateCellLabel || (hasDateMetadata && leadingNumericDayToken)));
21
24
  if (dateLike) {
22
25
  return 'date-cell';
23
26
  }
@@ -62,12 +65,22 @@ export function inferDirectionalControlFallbackFromEvidence(evidence) {
62
65
  }
63
66
  return undefined;
64
67
  }
68
+ export function sanitizeClassNameForStateInference(value) {
69
+ return (value ?? '')
70
+ .replace(/\s+/g, ' ')
71
+ .trim()
72
+ .toLowerCase()
73
+ .split(' ')
74
+ .filter(Boolean)
75
+ .filter((token) => !token.includes(':'))
76
+ .join(' ');
77
+ }
65
78
  export function inferDisabledStateFromSemanticEvidence(evidence) {
66
79
  const normalize = (value) => (value ?? '').replace(/\s+/g, ' ').trim().toLowerCase();
67
80
  const tagName = normalize(evidence.tagName);
68
81
  const role = normalize(evidence.role);
69
82
  const inputType = normalize(evidence.inputType);
70
- const className = normalize(evidence.className);
83
+ const className = sanitizeClassNameForStateInference(evidence.className);
71
84
  const nonClassBlob = [
72
85
  evidence.datasetText,
73
86
  evidence.dataState,
@@ -98,6 +111,7 @@ export function inferDisabledStateFromSemanticEvidence(evidence) {
98
111
  return disabledLikeRe.test(collectionOrButtonLike ? fullSemanticBlob : nonClassBlob);
99
112
  }
100
113
  const INFER_STRUCTURED_CELL_VARIANT_HELPER_SCRIPT = `const inferStructuredCellVariantFromEvidence = ${inferStructuredCellVariantFromEvidence.toString()};`;
114
+ const SANITIZE_CLASS_NAME_FOR_STATE_INFERENCE_HELPER_SCRIPT = `const sanitizeClassNameForStateInference = ${sanitizeClassNameForStateInference.toString()};`;
101
115
  const INFER_DISABLED_STATE_FROM_SEMANTIC_EVIDENCE_HELPER_SCRIPT = String.raw `
102
116
  const inferDisabledStateFromSemanticEvidence = (evidence) => {
103
117
  const normalizeDisabledSemanticValue = (value) =>
@@ -106,7 +120,7 @@ const INFER_DISABLED_STATE_FROM_SEMANTIC_EVIDENCE_HELPER_SCRIPT = String.raw `
106
120
  const tagName = normalizeDisabledSemanticValue(evidence?.tagName);
107
121
  const role = normalizeDisabledSemanticValue(evidence?.role);
108
122
  const inputType = normalizeDisabledSemanticValue(evidence?.inputType);
109
- const className = normalizeDisabledSemanticValue(evidence?.className);
123
+ const className = sanitizeClassNameForStateInference(evidence?.className);
110
124
  const nonClassBlob = [
111
125
  evidence?.datasetText,
112
126
  evidence?.dataState,
@@ -404,14 +418,16 @@ export function normalizeStagehandSelector(selector) {
404
418
  }
405
419
  : { selector };
406
420
  }
407
- async function collectDomTargetsFromDocument(context, options) {
421
+ async function collectDomTargetsFromDocumentRaw(context, options) {
408
422
  const includeActivationAffordances = options?.includeActivationAffordances === true;
423
+ const debugStructuralProfileStats = options?.debugStructuralProfileStats === true;
409
424
  const inheritedFramePath = JSON.stringify(options?.framePath ?? []);
410
425
  const inheritedFrameUrl = JSON.stringify(options?.frameUrl ?? '');
411
426
  const inheritedPageSignature = JSON.stringify(options?.pageSignature ?? '');
412
427
  const inheritedPageFormSelector = JSON.stringify(options?.pageFormSelector ?? '');
413
- const observedTargets = await context.evaluate(String.raw `(() => {
428
+ const observedPayload = await context.evaluate(String.raw `(() => {
414
429
  const includeActivationAffordances = ${includeActivationAffordances ? 'true' : 'false'};
430
+ const debugStructuralProfileStats = ${debugStructuralProfileStats ? 'true' : 'false'};
415
431
  const inheritedFramePath = ${inheritedFramePath};
416
432
  const inheritedFrameUrl = ${inheritedFrameUrl};
417
433
  const inheritedPageSignature = ${inheritedPageSignature};
@@ -569,6 +585,7 @@ async function collectDomTargetsFromDocument(context, options) {
569
585
  ${TRANSPARENT_ACTIONABLE_CONTROL_HELPER_SCRIPT}
570
586
  ${OBSERVE_DOM_LABEL_CONTRACT_HELPER_SCRIPT}
571
587
  ${INFER_STRUCTURED_CELL_VARIANT_HELPER_SCRIPT}
588
+ ${SANITIZE_CLASS_NAME_FOR_STATE_INFERENCE_HELPER_SCRIPT}
572
589
  ${INFER_DISABLED_STATE_FROM_SEMANTIC_EVIDENCE_HELPER_SCRIPT}
573
590
  ${INFER_DIRECTIONAL_CONTROL_FALLBACK_HELPER_SCRIPT}
574
591
 
@@ -1096,7 +1113,26 @@ async function collectDomTargetsFromDocument(context, options) {
1096
1113
  return inferRole(element) || tag;
1097
1114
  };
1098
1115
 
1116
+ const selectorCache = new WeakMap();
1117
+
1099
1118
  const buildSelector = (element) => {
1119
+ if (!isHTMLElementNode(element)) {
1120
+ return undefined;
1121
+ }
1122
+
1123
+ const cachedSelector = selectorCache.get(element);
1124
+ if (typeof cachedSelector === 'string') {
1125
+ return cachedSelector;
1126
+ }
1127
+ if (cachedSelector === null) {
1128
+ return undefined;
1129
+ }
1130
+
1131
+ const finalizeSelector = (value) => {
1132
+ selectorCache.set(element, value ?? null);
1133
+ return value;
1134
+ };
1135
+
1100
1136
  const testIdAttributeOf = (candidate) => {
1101
1137
  if (candidate.hasAttribute('data-testid')) return 'data-testid';
1102
1138
  if (candidate.hasAttribute('data-test-id')) return 'data-test-id';
@@ -1123,7 +1159,7 @@ async function collectDomTargetsFromDocument(context, options) {
1123
1159
  };
1124
1160
 
1125
1161
  if (element.id && isSelectorUniqueFor(element, '#' + cssEscape(element.id))) {
1126
- return '#' + cssEscape(element.id);
1162
+ return finalizeSelector('#' + cssEscape(element.id));
1127
1163
  }
1128
1164
 
1129
1165
  const testId =
@@ -1131,7 +1167,7 @@ async function collectDomTargetsFromDocument(context, options) {
1131
1167
  if (testId) {
1132
1168
  const selectorValue = testIdSelectorOf(element, testId);
1133
1169
  if (isSelectorUniqueFor(element, selectorValue)) {
1134
- return selectorValue;
1170
+ return finalizeSelector(selectorValue);
1135
1171
  }
1136
1172
  }
1137
1173
 
@@ -1140,7 +1176,7 @@ async function collectDomTargetsFromDocument(context, options) {
1140
1176
  if (name) {
1141
1177
  const selectorValue = tag + '[name="' + cssEscape(name) + '"]';
1142
1178
  if (isSelectorUniqueFor(element, selectorValue)) {
1143
- return selectorValue;
1179
+ return finalizeSelector(selectorValue);
1144
1180
  }
1145
1181
  }
1146
1182
 
@@ -1189,10 +1225,14 @@ async function collectDomTargetsFromDocument(context, options) {
1189
1225
  }
1190
1226
  current = current.parentElement;
1191
1227
  }
1192
- if (path.length === 0) return undefined;
1228
+ if (path.length === 0) return finalizeSelector(undefined);
1193
1229
 
1194
1230
  const structuralSelector = path.join(' > ');
1195
- return isSelectorUniqueFor(element, structuralSelector) ? structuralSelector : undefined;
1231
+ return finalizeSelector(
1232
+ isSelectorUniqueFor(element, structuralSelector)
1233
+ ? structuralSelector
1234
+ : undefined
1235
+ );
1196
1236
  };
1197
1237
 
1198
1238
  const selectorFromRelation = (element, attribute) => {
@@ -1341,10 +1381,44 @@ async function collectDomTargetsFromDocument(context, options) {
1341
1381
  return false;
1342
1382
  };
1343
1383
 
1384
+ const visibleInteractiveDescendantCountCache = new WeakMap();
1385
+
1344
1386
  const visibleInteractiveDescendantCountOf = (element) => {
1345
- return Array.from(element.querySelectorAll(selector)).filter((candidate) => {
1387
+ if (!isHTMLElementNode(element)) {
1388
+ return 0;
1389
+ }
1390
+
1391
+ const cachedCount = visibleInteractiveDescendantCountCache.get(element);
1392
+ if (typeof cachedCount === 'number') {
1393
+ return cachedCount;
1394
+ }
1395
+
1396
+ const count = Array.from(element.querySelectorAll(selector)).filter((candidate) => {
1346
1397
  return isHTMLElementNode(candidate) && isVisible(candidate);
1347
1398
  }).length;
1399
+ visibleInteractiveDescendantCountCache.set(element, count);
1400
+ return count;
1401
+ };
1402
+
1403
+ const descendantEditableCountCache = new WeakMap();
1404
+
1405
+ const descendantEditableCountOf = (element) => {
1406
+ if (!isHTMLElementNode(element)) {
1407
+ return 0;
1408
+ }
1409
+
1410
+ const cachedCount = descendantEditableCountCache.get(element);
1411
+ if (typeof cachedCount === 'number') {
1412
+ return cachedCount;
1413
+ }
1414
+
1415
+ const count = Array.from(
1416
+ element.querySelectorAll(
1417
+ 'input:not([type="hidden"]), textarea, select, [contenteditable="true"]'
1418
+ )
1419
+ ).filter((candidate) => isHTMLElementNode(candidate) && isVisible(candidate)).length;
1420
+ descendantEditableCountCache.set(element, count);
1421
+ return count;
1348
1422
  };
1349
1423
 
1350
1424
  const clickableSemanticBlobOf = (element) => {
@@ -2139,7 +2213,7 @@ async function collectDomTargetsFromDocument(context, options) {
2139
2213
  states.hasSelection = true;
2140
2214
  }
2141
2215
 
2142
- const className = (element.getAttribute('class') || '').toLowerCase();
2216
+ const className = sanitizeClassNameForStateInference(element.getAttribute('class') || '');
2143
2217
  const dataset = Object.values(element.dataset || {})
2144
2218
  .join(' ')
2145
2219
  .toLowerCase();
@@ -2218,6 +2292,87 @@ async function collectDomTargetsFromDocument(context, options) {
2218
2292
  return Object.keys(states).length > 0 ? states : undefined;
2219
2293
  };
2220
2294
 
2295
+ const canonicalDateIdentityFromParts = (yearValue, monthValue, dayValue) => {
2296
+ const year = Number.parseInt(String(yearValue || '').trim(), 10);
2297
+ const month = Number.parseInt(String(monthValue || '').trim(), 10);
2298
+ const day = Number.parseInt(String(dayValue || '').trim(), 10);
2299
+ if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
2300
+ return undefined;
2301
+ }
2302
+ if (month < 1 || month > 12 || day < 1 || day > 31) {
2303
+ return undefined;
2304
+ }
2305
+
2306
+ const candidate = new Date(Date.UTC(year, month - 1, day));
2307
+ if (
2308
+ candidate.getUTCFullYear() !== year ||
2309
+ candidate.getUTCMonth() !== month - 1 ||
2310
+ candidate.getUTCDate() !== day
2311
+ ) {
2312
+ return undefined;
2313
+ }
2314
+
2315
+ return {
2316
+ dateIso:
2317
+ year.toString().padStart(4, '0') +
2318
+ '-' +
2319
+ month.toString().padStart(2, '0') +
2320
+ '-' +
2321
+ day.toString().padStart(2, '0'),
2322
+ dateYear: year,
2323
+ dateMonth: month,
2324
+ dateDay: day,
2325
+ };
2326
+ };
2327
+
2328
+ const canonicalDateIdentityFromValue = (value) => {
2329
+ const normalized = normalizeDescriptorText(value || '');
2330
+ const match = normalized?.match(/\b(\d{4})-(\d{1,2})-(\d{1,2})\b/);
2331
+ if (!match) {
2332
+ return undefined;
2333
+ }
2334
+
2335
+ return canonicalDateIdentityFromParts(match[1], match[2], match[3]);
2336
+ };
2337
+
2338
+ const canonicalDateIdentityOf = (element, normalizedLabel) => {
2339
+ const metadataValues = [
2340
+ element.getAttribute('data-iso'),
2341
+ element.getAttribute('data-date'),
2342
+ element.getAttribute('data-day'),
2343
+ element.getAttribute('data-testid'),
2344
+ element.getAttribute('data-test-id'),
2345
+ element.getAttribute('id'),
2346
+ element.getAttribute('class'),
2347
+ element.getAttribute('title'),
2348
+ element.getAttribute('aria-label'),
2349
+ ];
2350
+
2351
+ for (const candidateValue of metadataValues) {
2352
+ const directIdentity = canonicalDateIdentityFromValue(candidateValue);
2353
+ if (directIdentity) {
2354
+ return directIdentity;
2355
+ }
2356
+ }
2357
+
2358
+ const leadingDayToken = normalizedLabel.match(/^(\d{1,2})\b/)?.[1];
2359
+ const dayValue = element.getAttribute('data-day')?.trim() || leadingDayToken;
2360
+ const monthValue =
2361
+ element.getAttribute('data-month')?.trim() ||
2362
+ element.getAttribute('data-date-month')?.trim() ||
2363
+ composedClosest(element, '[data-month], [data-date-month]')?.getAttribute?.('data-month')?.trim() ||
2364
+ composedClosest(element, '[data-month], [data-date-month]')?.getAttribute?.('data-date-month')?.trim() ||
2365
+ undefined;
2366
+ const yearValue =
2367
+ element.getAttribute('data-year')?.trim() ||
2368
+ element.getAttribute('data-date-year')?.trim() ||
2369
+ composedClosest(element, '[data-year], [data-date-year]')?.getAttribute?.('data-year')?.trim() ||
2370
+ composedClosest(element, '[data-year], [data-date-year]')?.getAttribute?.('data-date-year')?.trim() ||
2371
+ undefined;
2372
+
2373
+ return canonicalDateIdentityFromParts(yearValue, monthValue, dayValue);
2374
+ };
2375
+
2221
2376
  const inferStructuredCell = (element, surface) => {
2222
2377
  if (!isHTMLElementNode(element)) return undefined;
2223
2378
 
@@ -2231,10 +2386,22 @@ async function collectDomTargetsFromDocument(context, options) {
2231
2386
  const label =
2232
2387
  explicitLabelOf(element) || textOf(element) || seatValueLabel || syntheticLabelOf(element) || '';
2233
2388
  const normalizedLabel = label.replace(/\s+/g, ' ').trim();
2389
+ const dateIdentity = canonicalDateIdentityOf(element, normalizedLabel);
2234
2390
  const explicitDateCellMetadata =
2391
+ Boolean(dateIdentity) ||
2235
2392
  element.hasAttribute('data-day') ||
2236
2393
  element.hasAttribute('data-date') ||
2237
2394
  element.hasAttribute('data-iso') ||
2395
+ /\bdate[-_ ]?cell\b|\bcalendar[-_ ]?day\b|\b\d{4}-\d{2}-\d{2}\b/i.test(
2396
+ [
2397
+ element.getAttribute('data-testid') || '',
2398
+ element.getAttribute('data-test-id') || '',
2399
+ element.getAttribute('id') || '',
2400
+ element.getAttribute('class') || '',
2401
+ ]
2402
+ .join(' ')
2403
+ .trim()
2404
+ ) ||
2238
2405
  element.hasAttribute('aria-rowindex') ||
2239
2406
  element.hasAttribute('aria-colindex') ||
2240
2407
  Boolean(
@@ -2279,6 +2446,7 @@ async function collectDomTargetsFromDocument(context, options) {
2279
2446
  return {
2280
2447
  family: 'structured-grid',
2281
2448
  variant: structuredCellVariant,
2449
+ ...(dateIdentity ?? {}),
2282
2450
  row,
2283
2451
  column,
2284
2452
  zone,
@@ -2357,9 +2525,19 @@ async function collectDomTargetsFromDocument(context, options) {
2357
2525
  return undefined;
2358
2526
  };
2359
2527
 
2528
+ const surfacePositionTraitsCache = new WeakMap();
2529
+
2360
2530
  const surfacePositionTraitsOf = (surface) => {
2361
2531
  if (!isHTMLElementNode(surface) || !isVisible(surface)) return undefined;
2362
2532
 
2533
+ const cachedTraits = surfacePositionTraitsCache.get(surface);
2534
+ if (cachedTraits) {
2535
+ return cachedTraits;
2536
+ }
2537
+ if (cachedTraits === null) {
2538
+ return undefined;
2539
+ }
2540
+
2363
2541
  const style = window.getComputedStyle(surface);
2364
2542
  const position = (style.position || '').toLowerCase();
2365
2543
  const rect = surface.getBoundingClientRect();
@@ -2378,19 +2556,26 @@ async function collectDomTargetsFromDocument(context, options) {
2378
2556
  style.backgroundColor !== 'rgba(0, 0, 0, 0)');
2379
2557
 
2380
2558
  if (rect.width < 180 || rect.height < 72 || interactiveCount < 1 || coverage > 0.45) {
2559
+ surfacePositionTraitsCache.set(surface, null);
2381
2560
  return undefined;
2382
2561
  }
2383
2562
 
2384
2563
  if (position === 'fixed') {
2385
- return { kind: 'floating-panel', priority: 92 };
2564
+ const traits = { kind: 'floating-panel', priority: 92 };
2565
+ surfacePositionTraitsCache.set(surface, traits);
2566
+ return traits;
2386
2567
  }
2387
2568
 
2388
2569
  if (position === 'sticky') {
2389
- return { kind: 'sticky-panel', priority: 88 };
2570
+ const traits = { kind: 'sticky-panel', priority: 88 };
2571
+ surfacePositionTraitsCache.set(surface, traits);
2572
+ return traits;
2390
2573
  }
2391
2574
 
2392
2575
  if (position === 'absolute' && hasCardChrome && zIndexValue > 0) {
2393
- return { kind: 'floating-panel', priority: 82 };
2576
+ const traits = { kind: 'floating-panel', priority: 82 };
2577
+ surfacePositionTraitsCache.set(surface, traits);
2578
+ return traits;
2394
2579
  }
2395
2580
 
2396
2581
  if (
@@ -2399,33 +2584,67 @@ async function collectDomTargetsFromDocument(context, options) {
2399
2584
  interactiveCount >= 1 &&
2400
2585
  (modalBackdropAncestorOf(surface) || siblingModalBackdropOf(surface))
2401
2586
  ) {
2402
- return { kind: 'floating-panel', priority: 90 };
2587
+ const traits = { kind: 'floating-panel', priority: 90 };
2588
+ surfacePositionTraitsCache.set(surface, traits);
2589
+ return traits;
2403
2590
  }
2404
2591
 
2592
+ surfacePositionTraitsCache.set(surface, null);
2405
2593
  return undefined;
2406
2594
  };
2407
2595
 
2596
+ const inferredSurfaceKindCache = new WeakMap();
2597
+
2408
2598
  const inferSurfaceKind = (surface) => {
2409
2599
  if (!isHTMLElementNode(surface)) return undefined;
2600
+
2601
+ const cachedSurfaceKind = inferredSurfaceKindCache.get(surface);
2602
+ if (typeof cachedSurfaceKind === 'string') {
2603
+ return cachedSurfaceKind;
2604
+ }
2605
+ if (cachedSurfaceKind === null) {
2606
+ return undefined;
2607
+ }
2608
+
2410
2609
  const positionedSurface = surfacePositionTraitsOf(surface);
2411
- if (positionedSurface?.kind) return positionedSurface.kind;
2610
+ if (positionedSurface?.kind) {
2611
+ inferredSurfaceKindCache.set(surface, positionedSurface.kind);
2612
+ return positionedSurface.kind;
2613
+ }
2412
2614
  const role = surface.getAttribute('role')?.trim();
2413
- if (role === 'dialog') return 'dialog';
2414
- if (role === 'listbox') return 'listbox';
2415
- if (role === 'menu') return 'menu';
2416
- if (role === 'grid') return 'grid';
2417
- if (role === 'tabpanel') return 'tabpanel';
2418
- if (isGenericClickableElement(surface) && isStructuredContainer(surface)) return 'card';
2615
+ if (role === 'dialog') return inferredSurfaceKindCache.set(surface, 'dialog'), 'dialog';
2616
+ if (role === 'listbox') return inferredSurfaceKindCache.set(surface, 'listbox'), 'listbox';
2617
+ if (role === 'menu') return inferredSurfaceKindCache.set(surface, 'menu'), 'menu';
2618
+ if (role === 'grid') return inferredSurfaceKindCache.set(surface, 'grid'), 'grid';
2619
+ if (role === 'tabpanel') return inferredSurfaceKindCache.set(surface, 'tabpanel'), 'tabpanel';
2620
+ if (isGenericClickableElement(surface) && isStructuredContainer(surface)) {
2621
+ inferredSurfaceKindCache.set(surface, 'card');
2622
+ return 'card';
2623
+ }
2419
2624
  const className = (surface.getAttribute('class') || '').toLowerCase();
2420
- if (className.includes('calendar') || className.includes('datepicker')) return 'datepicker';
2421
- if (className.includes('popover')) return 'popover';
2422
- if (className.includes('dropdown')) return 'dropdown';
2625
+ if (className.includes('calendar') || className.includes('datepicker')) {
2626
+ inferredSurfaceKindCache.set(surface, 'datepicker');
2627
+ return 'datepicker';
2628
+ }
2629
+ if (className.includes('popover')) {
2630
+ inferredSurfaceKindCache.set(surface, 'popover');
2631
+ return 'popover';
2632
+ }
2633
+ if (className.includes('dropdown')) {
2634
+ inferredSurfaceKindCache.set(surface, 'dropdown');
2635
+ return 'dropdown';
2636
+ }
2423
2637
  const tag = surface.tagName.toLowerCase();
2424
- if (tag === 'article') return 'card';
2425
- if (tag === 'fieldset') return 'group';
2426
- if (tag === 'li') return 'listitem';
2427
- if (tag === 'section' || tag === 'form' || tag === 'aside') return tag;
2428
- return role || surface.tagName.toLowerCase();
2638
+ if (tag === 'article') return inferredSurfaceKindCache.set(surface, 'card'), 'card';
2639
+ if (tag === 'fieldset') return inferredSurfaceKindCache.set(surface, 'group'), 'group';
2640
+ if (tag === 'li') return inferredSurfaceKindCache.set(surface, 'listitem'), 'listitem';
2641
+ if (tag === 'section' || tag === 'form' || tag === 'aside') {
2642
+ inferredSurfaceKindCache.set(surface, tag);
2643
+ return tag;
2644
+ }
2645
+ const resolvedSurfaceKind = role || surface.tagName.toLowerCase();
2646
+ inferredSurfaceKindCache.set(surface, resolvedSurfaceKind || null);
2647
+ return resolvedSurfaceKind;
2429
2648
  };
2430
2649
 
2431
2650
  const labelledByTextOf = (element) => {
@@ -2599,15 +2818,29 @@ async function collectDomTargetsFromDocument(context, options) {
2599
2818
  return text.length <= 140 ? text : text.slice(0, 140);
2600
2819
  };
2601
2820
 
2821
+ const contextNodeCache = new WeakMap();
2822
+
2602
2823
  const contextNodeOf = (element) => {
2603
- if (!element) return undefined;
2824
+ if (!isHTMLElementNode(element)) return undefined;
2825
+
2826
+ const cachedNode = contextNodeCache.get(element);
2827
+ if (cachedNode) {
2828
+ return cachedNode;
2829
+ }
2830
+ if (cachedNode === null) {
2831
+ return undefined;
2832
+ }
2604
2833
 
2605
- const kind = element.getAttribute?.('role')?.trim() || element.tagName?.toLowerCase?.();
2834
+ const kind = element.getAttribute('role')?.trim() || element.tagName?.toLowerCase?.();
2606
2835
  const fallbackLabel = contextLabelOf(element);
2607
2836
  const text = contextTextOf(element);
2608
2837
  const selector = buildSelector(element);
2609
- if (!kind && !fallbackLabel && !text && !selector) return undefined;
2610
- return {
2838
+ if (!kind && !fallbackLabel && !text && !selector) {
2839
+ contextNodeCache.set(element, null);
2840
+ return undefined;
2841
+ }
2842
+
2843
+ const contextNode = {
2611
2844
  kind: kind || undefined,
2612
2845
  label: fallbackLabel,
2613
2846
  text:
@@ -2617,6 +2850,8 @@ async function collectDomTargetsFromDocument(context, options) {
2617
2850
  selector,
2618
2851
  fallbackLabel,
2619
2852
  };
2853
+ contextNodeCache.set(element, contextNode);
2854
+ return contextNode;
2620
2855
  };
2621
2856
 
2622
2857
  const pageSignature =
@@ -2634,6 +2869,23 @@ async function collectDomTargetsFromDocument(context, options) {
2634
2869
  const seenElements = new Set();
2635
2870
  const overlaySurfaceSelector =
2636
2871
  '[role="dialog"], [aria-modal="true"], [role="listbox"], [role="menu"], [role="grid"], [role="tabpanel"], [class*="popover"], [class*="dropdown"], [class*="listbox"], [class*="calendar"], [class*="datepicker"]';
2872
+ // Keep structural scans bounded without assuming shallow wrapper trees.
2873
+ const structuralAncestorSearchDepth = 16;
2874
+ const structuralProfileDebugStats = debugStructuralProfileStats
2875
+ ? {
2876
+ structuralProfileBuilds: 0,
2877
+ ownerCandidateAncestorVisits: 0,
2878
+ explicitSurfaceAncestorVisits: 0,
2879
+ surfaceSelectorAncestorVisits: 0,
2880
+ }
2881
+ : undefined;
2882
+ const structuralProfileCache = new WeakMap();
2883
+
2884
+ const incrementStructuralProfileDebugStat = (key) => {
2885
+ if (structuralProfileDebugStats) {
2886
+ structuralProfileDebugStats[key] += 1;
2887
+ }
2888
+ };
2637
2889
 
2638
2890
  const compactText = (value) => (value || '').replace(/\s+/g, ' ').trim();
2639
2891
 
@@ -2855,7 +3107,7 @@ async function collectDomTargetsFromDocument(context, options) {
2855
3107
  let preferredFallback = undefined;
2856
3108
  let current = surface;
2857
3109
  let depth = 0;
2858
- while (current && depth < 8) {
3110
+ while (current && depth < structuralAncestorSearchDepth) {
2859
3111
  if (!isHTMLElementNode(current) || !isVisible(current)) {
2860
3112
  current = current?.parentElement ?? null;
2861
3113
  depth += 1;
@@ -3126,7 +3378,7 @@ async function collectDomTargetsFromDocument(context, options) {
3126
3378
  for (const tokenNode of tokenNodes) {
3127
3379
  let current = composedParentElement(tokenNode);
3128
3380
  let depth = 0;
3129
- while (current && depth < 8) {
3381
+ while (current && depth < structuralAncestorSearchDepth) {
3130
3382
  if (isPotentialVisualGridSurface(current)) {
3131
3383
  candidateSurfaces.set(
3132
3384
  current,
@@ -3178,66 +3430,67 @@ async function collectDomTargetsFromDocument(context, options) {
3178
3430
  .filter((element) => isHTMLElementNode(element))
3179
3431
  .filter((element) => isVisible(element));
3180
3432
 
3181
- const localSurfaceCandidateOf = (element) => {
3182
- const ranked = [];
3183
- const pushCandidate = (candidate, depth) => {
3184
- if (
3185
- !isHTMLElementNode(candidate) ||
3186
- candidate === element ||
3187
- !isVisible(candidate)
3188
- ) {
3189
- return;
3190
- }
3433
+ const explicitSurfaceKindOf = (surface) => {
3434
+ if (!isHTMLElementNode(surface)) {
3435
+ return undefined;
3436
+ }
3191
3437
 
3192
- const priority = surfacePriorityOf(candidate);
3193
- if (priority <= 0) {
3194
- return;
3195
- }
3438
+ const positionedSurface = surfacePositionTraitsOf(surface);
3439
+ if (positionedSurface?.kind) {
3440
+ return positionedSurface.kind;
3441
+ }
3196
3442
 
3197
- if (ranked.some((entry) => entry.element === candidate)) {
3198
- return;
3199
- }
3443
+ const role = surface.getAttribute('role')?.trim() || '';
3444
+ if (role === 'dialog') return 'dialog';
3445
+ if (role === 'listbox') return 'listbox';
3446
+ if (role === 'menu') return 'menu';
3447
+ if (role === 'grid') return 'grid';
3448
+ if (role === 'tabpanel') return 'tabpanel';
3200
3449
 
3201
- ranked.push({ element: candidate, priority, depth });
3202
- };
3450
+ const className = (surface.getAttribute('class') || '').toLowerCase();
3451
+ if (className.includes('calendar') || className.includes('datepicker')) return 'datepicker';
3452
+ if (className.includes('popover')) return 'popover';
3453
+ if (className.includes('dropdown')) return 'dropdown';
3203
3454
 
3204
- pushCandidate(itemOf(element), 0);
3205
- pushCandidate(containerOf(element), 1);
3455
+ const tag = surface.tagName.toLowerCase();
3456
+ if (tag === 'form') return 'form';
3457
+ return undefined;
3458
+ };
3206
3459
 
3460
+ const explicitSurfaceOf = (element) => {
3207
3461
  let current = composedParentElement(element);
3208
3462
  let depth = 0;
3209
- while (current && depth < 16) {
3210
- pushCandidate(current, depth + 2);
3463
+
3464
+ while (current && depth < structuralAncestorSearchDepth) {
3465
+ incrementStructuralProfileDebugStat('explicitSurfaceAncestorVisits');
3466
+ if (structuralProfileOf(current)?.explicitSurfaceKind) {
3467
+ return current;
3468
+ }
3211
3469
  current = composedParentElement(current);
3212
3470
  depth += 1;
3213
3471
  }
3214
3472
 
3215
- ranked.sort((left, right) => {
3216
- if (left.priority !== right.priority) {
3217
- return right.priority - left.priority;
3218
- }
3219
- return left.depth - right.depth;
3220
- });
3221
-
3222
- return ranked[0]?.element;
3473
+ return undefined;
3223
3474
  };
3224
3475
 
3225
- const surfaceSelectorsOf = (element, localSurface) => {
3476
+ const surfaceSelectorsOf = (element, explicitSurface) => {
3226
3477
  const selectors = [];
3227
- let current = element.parentElement;
3478
+ let current = composedParentElement(element);
3228
3479
 
3229
3480
  while (current) {
3230
- if (current.matches?.(overlaySurfaceSelector)) {
3231
- const selector = buildSelector(current);
3481
+ incrementStructuralProfileDebugStat('surfaceSelectorAncestorVisits');
3482
+ const currentProfile = structuralProfileOf(current);
3483
+ if (currentProfile?.explicitSurfaceKind) {
3484
+ const selector = currentProfile.selector;
3232
3485
  if (selector && !selectors.includes(selector)) {
3233
3486
  selectors.push(selector);
3234
3487
  }
3235
3488
  }
3236
- current = current.parentElement;
3489
+ current = composedParentElement(current);
3237
3490
  }
3238
3491
 
3239
- if (localSurface) {
3240
- const selector = buildSelector(localSurface);
3492
+ if (explicitSurface) {
3493
+ const selector = structuralProfileOf(explicitSurface)?.selector;
3241
3494
  if (selector && !selectors.includes(selector)) {
3242
3495
  selectors.push(selector);
3243
3496
  }
@@ -3246,6 +3499,413 @@ async function collectDomTargetsFromDocument(context, options) {
3246
3499
  return selectors.length > 0 ? selectors : undefined;
3247
3500
  };
3248
3501
 
3502
+ const directContextTextProfileCache = new WeakMap();
3503
+
3504
+ const directContextTextProfileOf = (candidate) => {
3505
+ if (!isHTMLElementNode(candidate)) {
3506
+ return undefined;
3507
+ }
3508
+
3509
+ const cachedProfile = directContextTextProfileCache.get(candidate);
3510
+ if (cachedProfile) {
3511
+ return cachedProfile;
3512
+ }
3513
+ if (cachedProfile === null) {
3514
+ return undefined;
3515
+ }
3516
+
3517
+ const children = [];
3518
+ for (const child of Array.from(candidate.children).slice(0, 12)) {
3519
+ if (!isHTMLElementNode(child) || !isVisible(child)) {
3520
+ continue;
3521
+ }
3522
+
3523
+ const tag = child.tagName.toLowerCase();
3524
+ if (
3525
+ ['button', 'input', 'textarea', 'select', 'a', 'svg', 'img'].includes(tag) ||
3526
+ child.matches?.(selector)
3527
+ ) {
3528
+ continue;
3529
+ }
3530
+
3531
+ const normalized = normalizeDescriptorText(textOf(child, { container: true }));
3532
+ if (!normalized) {
3533
+ continue;
3534
+ }
3535
+
3536
+ const semanticText = normalized
3537
+ .replace(observedLooseActionTextRe, ' ')
3538
+ .replace(/\s+/g, ' ')
3539
+ .trim();
3540
+ if (!semanticText || semanticText.length < 2) {
3541
+ continue;
3542
+ }
3543
+
3544
+ children.push(child);
3545
+ }
3546
+
3547
+ const profile = {
3548
+ count: children.length,
3549
+ children,
3550
+ };
3551
+ directContextTextProfileCache.set(candidate, profile);
3552
+ return profile;
3553
+ };
3554
+
3555
+ const directContextTextBlockCountOf = (candidate, excludedTarget) => {
3556
+ const profile = directContextTextProfileOf(candidate);
3557
+ if (!profile) {
3558
+ return 0;
3559
+ }
3560
+
3561
+ if (
3562
+ isHTMLElementNode(excludedTarget) &&
3563
+ profile.children.includes(excludedTarget)
3564
+ ) {
3565
+ return Math.max(0, profile.count - 1);
3566
+ }
3567
+
3568
+ return profile.count;
3569
+ };
3570
+
3571
+ const ownerCandidateShapeKeyCache = new WeakMap();
3572
+
3573
+ const ownerCandidateShapeKeyOf = (candidate) => {
3574
+ const cachedShapeKey = ownerCandidateShapeKeyCache.get(candidate);
3575
+ if (typeof cachedShapeKey === 'string') {
3576
+ return cachedShapeKey;
3577
+ }
3578
+
3579
+ const tag = candidate.tagName.toLowerCase();
3580
+ const role = candidate.getAttribute('role')?.trim().toLowerCase() || '';
3581
+ const className = (candidate.getAttribute('class') || '')
3582
+ .trim()
3583
+ .toLowerCase()
3584
+ .split(/\s+/)
3585
+ .filter(Boolean)
3586
+ .sort()
3587
+ .join('.');
3588
+ const shapeKey = tag + '|' + role + '|' + className;
3589
+ ownerCandidateShapeKeyCache.set(candidate, shapeKey);
3590
+ return shapeKey;
3591
+ };
3592
+
3593
+ const ownerCandidatePeerCountOf = (
3594
+ candidate,
3595
+ directTextBlockCount,
3596
+ descendantInteractiveCount
3597
+ ) => {
3598
+ const parent = composedParentElement(candidate);
3599
+ if (!isHTMLElementNode(parent)) {
3600
+ return 0;
3601
+ }
3602
+
3603
+ const candidateShapeKey = ownerCandidateShapeKeyOf(candidate);
3604
+ const candidateChildCount = candidate.children.length;
3605
+ let peerCount = 0;
3606
+
3607
+ for (const sibling of Array.from(parent.children)) {
3608
+ if (!isHTMLElementNode(sibling) || sibling === candidate || !isVisible(sibling)) {
3609
+ continue;
3610
+ }
3611
+ if (ownerCandidateShapeKeyOf(sibling) !== candidateShapeKey) {
3612
+ continue;
3613
+ }
3614
+
3615
+ const siblingInteractiveCount = visibleInteractiveDescendantCountOf(sibling);
3616
+ const siblingTextBlockCount = directContextTextBlockCountOf(sibling);
3617
+ if (Math.abs(siblingInteractiveCount - descendantInteractiveCount) > 1) {
3618
+ continue;
3619
+ }
3620
+ if (Math.abs(siblingTextBlockCount - directTextBlockCount) > 1) {
3621
+ continue;
3622
+ }
3623
+ if (Math.abs(sibling.children.length - candidateChildCount) > 1) {
3624
+ continue;
3625
+ }
3626
+
3627
+ peerCount += 1;
3628
+ }
3629
+
3630
+ return peerCount;
3631
+ };
3632
+
3633
+ const ownerBoundaryMetaOf = (candidate, coverage, descendantInteractiveCount) => {
3634
+ if (!isHTMLElementNode(candidate)) {
3635
+ return undefined;
3636
+ }
3637
+
3638
+ const positionedSurface = surfacePositionTraitsOf(candidate);
3639
+ if (positionedSurface?.kind) {
3640
+ return {
3641
+ kind: positionedSurface.kind,
3642
+ stop: 'hard',
3643
+ };
3644
+ }
3645
+
3646
+ const role = candidate.getAttribute('role')?.trim().toLowerCase() || '';
3647
+ if (role === 'dialog' || candidate.getAttribute('aria-modal') === 'true') {
3648
+ return {
3649
+ kind: 'dialog',
3650
+ stop: 'hard',
3651
+ };
3652
+ }
3653
+ if (['listbox', 'menu', 'grid', 'tabpanel'].includes(role)) {
3654
+ return {
3655
+ kind: role,
3656
+ stop: 'soft',
3657
+ };
3658
+ }
3659
+
3660
+ const tag = candidate.tagName.toLowerCase();
3661
+ if (tag === 'form') {
3662
+ return {
3663
+ kind: 'form',
3664
+ stop: 'hard',
3665
+ };
3666
+ }
3667
+
3668
+ const className = (candidate.getAttribute('class') || '').toLowerCase();
3669
+ if (className.includes('calendar') || className.includes('datepicker')) {
3670
+ return {
3671
+ kind: 'datepicker',
3672
+ stop: 'hard',
3673
+ };
3674
+ }
3675
+ if (className.includes('popover')) {
3676
+ return {
3677
+ kind: 'popover',
3678
+ stop: 'hard',
3679
+ };
3680
+ }
3681
+ if (className.includes('dropdown')) {
3682
+ return {
3683
+ kind: 'dropdown',
3684
+ stop: 'hard',
3685
+ };
3686
+ }
3687
+
3688
+ if (
3689
+ ['main', 'aside', 'section'].includes(tag) &&
3690
+ (coverage >= 0.24 || descendantInteractiveCount > 4)
3691
+ ) {
3692
+ return {
3693
+ kind: tag,
3694
+ stop: 'hard',
3695
+ };
3696
+ }
3697
+
3698
+ return undefined;
3699
+ };
3700
+
3701
+ const structuralProfileOf = (element) => {
3702
+ if (!isHTMLElementNode(element) || !isVisible(element)) {
3703
+ return undefined;
3704
+ }
3705
+
3706
+ const cachedProfile = structuralProfileCache.get(element);
3707
+ if (cachedProfile) {
3708
+ return cachedProfile;
3709
+ }
3710
+ if (cachedProfile === null) {
3711
+ return undefined;
3712
+ }
3713
+
3714
+ const rect = element.getBoundingClientRect();
3715
+ const area = Math.max(rect.width * rect.height, 1);
3716
+ const coverage = area / viewportArea();
3717
+ const descendantInteractiveCount = visibleInteractiveDescendantCountOf(element);
3718
+ const descendantEditableCount = descendantEditableCountOf(element);
3719
+ const boundaryMeta = ownerBoundaryMetaOf(
3720
+ element,
3721
+ coverage,
3722
+ descendantInteractiveCount
3723
+ );
3724
+ const profile = {
3725
+ selector: buildSelector(element),
3726
+ area,
3727
+ coverage,
3728
+ descendantInteractiveCount,
3729
+ descendantEditableCount,
3730
+ directTextBlockCount: directContextTextBlockCountOf(element),
3731
+ boundaryMeta,
3732
+ explicitSurfaceKind: explicitSurfaceKindOf(element),
3733
+ inferredSurfaceKind: inferSurfaceKind(element),
3734
+ contextNode: contextNodeOf(element),
3735
+ surfacePriority: surfacePriorityOf(element),
3736
+ tag: element.tagName.toLowerCase(),
3737
+ role: element.getAttribute('role')?.trim().toLowerCase() || '',
3738
+ };
3739
+ structuralProfileCache.set(element, profile);
3740
+ incrementStructuralProfileDebugStat('structuralProfileBuilds');
3741
+ return profile;
3742
+ };
3743
+
3744
+ const ownerCandidateSurfacePriorityOf = (surfaceKind, fallbackPriority) => {
3745
+ if (typeof fallbackPriority === 'number' && fallbackPriority > 0) {
3746
+ return fallbackPriority;
3747
+ }
3748
+
3749
+ switch ((surfaceKind || '').toLowerCase()) {
3750
+ case 'dialog':
3751
+ return 100;
3752
+ case 'listbox':
3753
+ case 'menu':
3754
+ return 95;
3755
+ case 'floating-panel':
3756
+ return 92;
3757
+ case 'datepicker':
3758
+ return 90;
3759
+ case 'sticky-panel':
3760
+ return 88;
3761
+ case 'popover':
3762
+ case 'dropdown':
3763
+ return 85;
3764
+ case 'grid':
3765
+ case 'tabpanel':
3766
+ return 80;
3767
+ case 'form':
3768
+ return 70;
3769
+ case 'card':
3770
+ return 58;
3771
+ case 'group':
3772
+ return 55;
3773
+ case 'section':
3774
+ case 'aside':
3775
+ return 52;
3776
+ case 'listitem':
3777
+ case 'row':
3778
+ return 48;
3779
+ default:
3780
+ return undefined;
3781
+ }
3782
+ };
3783
+
3784
+ const ownerCandidateSurfaceKindOf = (candidateProfile, metrics) => {
3785
+ if (!candidateProfile) {
3786
+ return undefined;
3787
+ }
3788
+
3789
+ if (metrics.boundaryKind) {
3790
+ return metrics.boundaryKind;
3791
+ }
3792
+
3793
+ const inferredSurfaceKind = candidateProfile.inferredSurfaceKind;
3794
+ if (inferredSurfaceKind && inferredSurfaceKind !== 'div') {
3795
+ return inferredSurfaceKind;
3796
+ }
3797
+
3798
+ const tag = candidateProfile.tag;
3799
+ const role = candidateProfile.role;
3800
+
3801
+ if (tag === 'article') {
3802
+ return 'card';
3803
+ }
3804
+ if (tag === 'fieldset') {
3805
+ return 'group';
3806
+ }
3807
+ if (tag === 'li' || role === 'listitem') {
3808
+ return 'listitem';
3809
+ }
3810
+ if (role === 'row') {
3811
+ return 'row';
3812
+ }
3813
+
3814
+ const itemLikeCard =
3815
+ metrics.directTextBlockCount >= 2 &&
3816
+ metrics.descendantInteractiveCount >= 1 &&
3817
+ metrics.descendantInteractiveCount <= 4 &&
3818
+ metrics.coverage <= 0.45 &&
3819
+ metrics.relativeAreaToTarget >= 2 &&
3820
+ (metrics.peerCount >= 1 || metrics.descendantInteractiveCount === 1);
3821
+ if (itemLikeCard) {
3822
+ return 'card';
3823
+ }
3824
+
3825
+ return inferredSurfaceKind;
3826
+ };
3827
+
3828
+ const ownerCandidatesOf = (target) => {
3829
+ const targetRect = target.getBoundingClientRect();
3830
+ const targetArea = Math.max(targetRect.width * targetRect.height, 1);
3831
+ const candidates = [];
3832
+ let current = composedParentElement(target);
3833
+ let depth = 0;
3834
+
3835
+ while (current && depth < structuralAncestorSearchDepth) {
3836
+ incrementStructuralProfileDebugStat('ownerCandidateAncestorVisits');
3837
+ const candidateProfile = structuralProfileOf(current);
3838
+ if (!candidateProfile) {
3839
+ current = composedParentElement(current);
3840
+ depth += 1;
3841
+ continue;
3842
+ }
3843
+
3844
+ const selectorValue = candidateProfile.selector;
3845
+ if (!selectorValue) {
3846
+ current = composedParentElement(current);
3847
+ depth += 1;
3848
+ continue;
3849
+ }
3850
+
3851
+ const candidateArea = candidateProfile.area;
3852
+ const coverage = candidateProfile.coverage;
3853
+ const descendantInteractiveCount = candidateProfile.descendantInteractiveCount;
3854
+ const descendantEditableCount = candidateProfile.descendantEditableCount;
3855
+ const directTextBlockCount = directContextTextBlockCountOf(current, target);
3856
+ const peerCount = ownerCandidatePeerCountOf(
3857
+ current,
3858
+ directTextBlockCount,
3859
+ descendantInteractiveCount
3860
+ );
3861
+ const boundaryMeta = candidateProfile.boundaryMeta;
3862
+ const boundaryKind = boundaryMeta?.kind;
3863
+ const surfaceKind = ownerCandidateSurfaceKindOf(candidateProfile, {
3864
+ boundaryKind,
3865
+ coverage,
3866
+ directTextBlockCount,
3867
+ descendantInteractiveCount,
3868
+ peerCount,
3869
+ relativeAreaToTarget: candidateArea / targetArea,
3870
+ });
3871
+ const shell =
3872
+ surfaceKind === 'sticky-panel' ||
3873
+ surfaceKind === 'floating-panel' ||
3874
+ (['main', 'aside', 'section'].includes(candidateProfile.tag) &&
3875
+ (coverage >= 0.24 || descendantInteractiveCount > 4));
3876
+ const boundary = Boolean(boundaryKind) || shell;
3877
+ const stopSearch = shell || boundaryMeta?.stop === 'hard';
3878
+ const contextNode = candidateProfile.contextNode;
3879
+ candidates.push({
3880
+ ...contextNode,
3881
+ selector: selectorValue,
3882
+ depth,
3883
+ surfaceKind,
3884
+ surfacePriority: ownerCandidateSurfacePriorityOf(
3885
+ surfaceKind,
3886
+ candidateProfile.surfacePriority
3887
+ ),
3888
+ directTextBlockCount,
3889
+ peerCount,
3890
+ descendantInteractiveCount,
3891
+ descendantEditableCount,
3892
+ viewportCoverage: coverage,
3893
+ relativeAreaToTarget: candidateArea / targetArea,
3894
+ boundary,
3895
+ shell,
3896
+ });
3897
+
3898
+ if (stopSearch) {
3899
+ break;
3900
+ }
3901
+
3902
+ current = composedParentElement(current);
3903
+ depth += 1;
3904
+ }
3905
+
3906
+ return candidates.length > 0 ? candidates : undefined;
3907
+ };
3908
+
3249
3909
  const targets = elements.map((element, ordinal) => {
3250
3910
  const visualSeatGrid = visualSeatGridMeta.get(element);
3251
3911
  const associatedChoiceControl = associatedChoiceControlOf(element);
@@ -3277,16 +3937,9 @@ async function collectDomTargetsFromDocument(context, options) {
3277
3937
  const item =
3278
3938
  (compactClickableAffordance ? compactClickableItemOf(element) : undefined) ||
3279
3939
  itemOf(element);
3280
- const overlaySurface = composedClosest(element, overlaySurfaceSelector);
3281
- const localSurface = localSurfaceCandidateOf(element);
3282
- const selfSurface =
3283
- genericClickable && !associatedChoiceControl && isStructuredContainer(element)
3284
- ? element
3285
- : undefined;
3286
- const surface =
3287
- visualSeatGrid?.surface ||
3288
- (isHTMLElementNode(overlaySurface) ? overlaySurface : localSurface || selfSurface);
3289
- const surfaceSelectors = surfaceSelectorsOf(element, localSurface || selfSurface);
3940
+ const explicitSurface = explicitSurfaceOf(element);
3941
+ const surface = visualSeatGrid?.surface || explicitSurface;
3942
+ const surfaceSelectors = surfaceSelectorsOf(element, explicitSurface);
3290
3943
  const structure = visualSeatGrid?.structure || inferStructuredCell(element, surface);
3291
3944
  const directFallbackLabel = explicitLabelOf(element) || looseFieldLabelOf(element);
3292
3945
  const directionalFallbackLabel = directionalControlFallbackLabelOf(
@@ -3297,10 +3950,23 @@ async function collectDomTargetsFromDocument(context, options) {
3297
3950
  const currentValue = popupCurrentValueOf(element);
3298
3951
  const selection = selectionOf(element);
3299
3952
  const role = inferRole(element) || inferRole(associatedChoiceControl);
3300
- const surfaceKind = visualSeatGrid?.surfaceKind || surfaceKindOf(surface);
3953
+ const elementProfile = structuralProfileOf(element);
3954
+ const surfaceProfile = surface ? structuralProfileOf(surface) : undefined;
3955
+ const explicitSurfaceProfile = explicitSurface
3956
+ ? structuralProfileOf(explicitSurface)
3957
+ : undefined;
3958
+ const surfaceKind =
3959
+ visualSeatGrid?.surfaceKind ||
3960
+ surfaceProfile?.inferredSurfaceKind ||
3961
+ surfaceKindOf(surface);
3301
3962
  const fallbackSurfaceLabel =
3302
3963
  (visualSeatGrid?.hintText ? 'Seat map' : undefined) ||
3303
3964
  surfaceFallbackLabelOf(surface, surfaceKind);
3965
+ const explicitSurfaceKind = explicitSurfaceProfile?.explicitSurfaceKind;
3966
+ const explicitFallbackSurfaceLabel =
3967
+ explicitSurface && explicitSurfaceKind
3968
+ ? surfaceFallbackLabelOf(explicitSurface, explicitSurfaceKind)
3969
+ : undefined;
3304
3970
  const form = composedClosest(element, 'form');
3305
3971
  const testIdAttribute = element.hasAttribute('data-testid')
3306
3972
  ? 'data-testid'
@@ -3350,17 +4016,28 @@ async function collectDomTargetsFromDocument(context, options) {
3350
4016
  surfaceKind,
3351
4017
  surfaceLabel: fallbackSurfaceLabel,
3352
4018
  fallbackSurfaceLabel,
3353
- surfaceSelector: surface ? buildSelector(surface) : undefined,
4019
+ surfaceSelector: surfaceProfile?.selector,
3354
4020
  surfaceSelectors,
3355
- surfacePriority: surfacePriorityOf(surface),
4021
+ surfacePriority: surfaceProfile?.surfacePriority ?? surfacePriorityOf(surface),
4022
+ explicitSurfaceKind,
4023
+ explicitSurfaceLabel: explicitFallbackSurfaceLabel,
4024
+ explicitFallbackSurfaceLabel,
4025
+ explicitSurfaceSelector: explicitSurfaceProfile?.selector,
4026
+ explicitSurfaceSelectors: surfaceSelectors,
4027
+ explicitSurfacePriority:
4028
+ explicitSurfaceProfile?.surfacePriority ??
4029
+ (explicitSurface ? surfacePriorityOf(explicitSurface) : undefined),
3356
4030
  controlsSurfaceSelector:
3357
4031
  selectorFromRelation(element, 'aria-controls') || selectorFromRelation(element, 'aria-owns'),
3358
4032
  formSelector: form ? buildSelector(form) : undefined,
3359
4033
  pageFormSelector: form ? buildSelector(form) : inheritedPageFormSelector || undefined,
3360
- descendantInteractiveCount: element.querySelectorAll(selector).length,
3361
- descendantEditableCount: element.querySelectorAll(
3362
- 'input:not([type="hidden"]), textarea, select, [contenteditable="true"]'
3363
- ).length,
4034
+ descendantInteractiveCount:
4035
+ elementProfile?.descendantInteractiveCount ?? element.querySelectorAll(selector).length,
4036
+ descendantEditableCount:
4037
+ elementProfile?.descendantEditableCount ??
4038
+ element.querySelectorAll(
4039
+ 'input:not([type="hidden"]), textarea, select, [contenteditable="true"]'
4040
+ ).length,
3364
4041
  structure,
3365
4042
  ordinal,
3366
4043
  context: {
@@ -3375,11 +4052,15 @@ async function collectDomTargetsFromDocument(context, options) {
3375
4052
  hintText: describedByTextOf(element),
3376
4053
  fallbackHintText: visualSeatGrid?.hintText,
3377
4054
  visual: visualOf(element),
4055
+ ownerCandidates: ownerCandidatesOf(element),
3378
4056
  },
3379
4057
  };
3380
4058
  });
3381
4059
 
3382
- return targets;
4060
+ return {
4061
+ targets,
4062
+ debugStats: structuralProfileDebugStats,
4063
+ };
3383
4064
  };
3384
4065
 
3385
4066
  const enrichDisplayLabels = (targets) => {
@@ -3482,8 +4163,9 @@ async function collectDomTargetsFromDocument(context, options) {
3482
4163
  return score;
3483
4164
  };
3484
4165
 
4166
+ const collectionResult = collectTargets(document);
3485
4167
  const seen = new Set();
3486
- return enrichDisplayLabels(collectTargets(document))
4168
+ const finalTargets = enrichDisplayLabels(collectionResult.targets)
3487
4169
  .filter((candidate) => {
3488
4170
  const frameKey = candidate.framePath ? candidate.framePath.join(' -> ') : 'top';
3489
4171
  const key = frameKey + '|' + (candidate.selector || candidate.domSignature || '');
@@ -3512,18 +4194,55 @@ async function collectDomTargetsFromDocument(context, options) {
3512
4194
  return left.index - right.index;
3513
4195
  })
3514
4196
  .map(({ candidate }) => candidate);
4197
+
4198
+ return debugStructuralProfileStats
4199
+ ? {
4200
+ targets: finalTargets,
4201
+ debugStats: collectionResult.debugStats,
4202
+ }
4203
+ : finalTargets;
3515
4204
  })()`);
3516
- if (!Array.isArray(observedTargets)) {
3517
- return [];
4205
+ let observedTargets = [];
4206
+ let debugStats;
4207
+ if (Array.isArray(observedPayload)) {
4208
+ observedTargets = observedPayload;
4209
+ }
4210
+ else if (observedPayload &&
4211
+ typeof observedPayload === 'object' &&
4212
+ Array.isArray(observedPayload.targets)) {
4213
+ observedTargets = observedPayload.targets;
4214
+ const candidateDebugStats = observedPayload.debugStats;
4215
+ if (candidateDebugStats &&
4216
+ typeof candidateDebugStats === 'object' &&
4217
+ typeof candidateDebugStats
4218
+ .structuralProfileBuilds === 'number' &&
4219
+ typeof candidateDebugStats
4220
+ .ownerCandidateAncestorVisits === 'number' &&
4221
+ typeof candidateDebugStats
4222
+ .explicitSurfaceAncestorVisits === 'number' &&
4223
+ typeof candidateDebugStats
4224
+ .surfaceSelectorAncestorVisits === 'number') {
4225
+ debugStats = candidateDebugStats;
4226
+ }
3518
4227
  }
3519
- return observedTargets
3520
- .filter((target) => Boolean(target && typeof target === 'object' && typeof target.kind === 'string'))
4228
+ const targets = observedTargets
4229
+ .filter((target) => Boolean(target &&
4230
+ typeof target === 'object' &&
4231
+ typeof target.kind === 'string'))
3521
4232
  .map((target) => enrichObservedTargetSemantics(applyInheritedDomTargetMetadata(target, {
3522
4233
  framePath: options?.framePath,
3523
4234
  frameUrl: options?.frameUrl,
3524
4235
  pageSignature: options?.pageSignature,
3525
4236
  pageFormSelector: options?.pageFormSelector,
3526
4237
  })));
4238
+ return {
4239
+ targets,
4240
+ debugStats,
4241
+ };
4242
+ }
4243
+ async function collectDomTargetsFromDocument(context, options) {
4244
+ const { targets } = await collectDomTargetsFromDocumentRaw(context, options);
4245
+ return targets;
3527
4246
  }
3528
4247
  const FRAME_HOST_DESCRIPTOR_SCRIPT = String.raw `
3529
4248
  const ownerWindowOf = (node) => node?.ownerDocument?.defaultView || window;
@@ -3844,6 +4563,10 @@ export async function collectDomTargets(page, options) {
3844
4563
  }
3845
4564
  export const __testDomTargetCollection = {
3846
4565
  collectDomTargetsFromDocument,
4566
+ collectDomTargetsDebugFromDocument: (context, options) => collectDomTargetsFromDocumentRaw(context, {
4567
+ ...options,
4568
+ debugStructuralProfileStats: true,
4569
+ }),
3847
4570
  inferStructuredCellVariantFromEvidence,
3848
4571
  inferDirectionalControlFallbackFromEvidence,
3849
4572
  locatorDomSignatureScript: LOCATOR_DOM_SIGNATURE_SCRIPT,