@enricai/barnacle 1.12.77 → 1.12.79
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/dist/plugins/config-plugin.d.ts.map +1 -1
- package/dist/plugins/config-plugin.js +2 -1
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/scraper/actionable-visibility-expr.d.ts +12 -0
- package/dist/scraper/actionable-visibility-expr.d.ts.map +1 -0
- package/dist/scraper/actionable-visibility-expr.js +22 -0
- package/dist/scraper/actionable-visibility-expr.js.map +1 -0
- package/dist/scraper/browser-click-expr.d.ts +1 -1
- package/dist/scraper/browser-click-expr.d.ts.map +1 -1
- package/dist/scraper/browser-click-expr.js +2 -6
- package/dist/scraper/browser-click-expr.js.map +1 -1
- package/dist/scraper/deep-locator-candidates.d.ts.map +1 -1
- package/dist/scraper/deep-locator-candidates.js +35 -11
- package/dist/scraper/deep-locator-candidates.js.map +1 -1
- package/dist/scraper/deep-locator-scan.d.ts +12 -0
- package/dist/scraper/deep-locator-scan.d.ts.map +1 -1
- package/dist/scraper/deep-locator-scan.js +42 -13
- package/dist/scraper/deep-locator-scan.js.map +1 -1
- package/dist/scraper/errors.d.ts +10 -0
- package/dist/scraper/errors.d.ts.map +1 -1
- package/dist/scraper/errors.js +19 -1
- package/dist/scraper/errors.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts +47 -0
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +310 -61
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/phantom-click.d.ts +3 -3
- package/dist/scraper/phantom-click.d.ts.map +1 -1
- package/dist/scraper/phantom-click.js +6 -14
- package/dist/scraper/phantom-click.js.map +1 -1
- package/dist/scraper/pool.d.ts.map +1 -1
- package/dist/scraper/pool.js +8 -1
- package/dist/scraper/pool.js.map +1 -1
- package/dist/scraper/retry.d.ts +1 -1
- package/dist/scraper/retry.d.ts.map +1 -1
- package/dist/scraper/retry.js +5 -1
- package/dist/scraper/retry.js.map +1 -1
- package/dist/scraper/session-browserbase.d.ts.map +1 -1
- package/dist/scraper/session-browserbase.js +29 -17
- package/dist/scraper/session-browserbase.js.map +1 -1
- package/dist/scraper/session-shared.d.ts +5 -0
- package/dist/scraper/session-shared.d.ts.map +1 -1
- package/dist/scraper/session-shared.js.map +1 -1
- package/dist/scraper/session-teardown.d.ts +30 -1
- package/dist/scraper/session-teardown.d.ts.map +1 -1
- package/dist/scraper/session-teardown.js +48 -3
- package/dist/scraper/session-teardown.js.map +1 -1
- package/dist/scraper/submit-control.d.ts +2 -2
- package/dist/scraper/submit-control.d.ts.map +1 -1
- package/dist/scraper/submit-control.js +5 -21
- package/dist/scraper/submit-control.js.map +1 -1
- package/dist/scraper/target-resolution-diagnostic.d.ts.map +1 -1
- package/dist/scraper/target-resolution-diagnostic.js +6 -11
- package/dist/scraper/target-resolution-diagnostic.js.map +1 -1
- package/dist/scripts/recon-browser.d.ts +4 -1
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +25 -6
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +117 -2
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +801 -166
- package/dist/scripts/recon-generate.js.map +1 -1
- package/package.json +1 -1
|
@@ -27,6 +27,9 @@ exports.findRecentPageTransition = findRecentPageTransition;
|
|
|
27
27
|
exports.isWizardExitAction = isWizardExitAction;
|
|
28
28
|
exports.isAdvanceStep = isAdvanceStep;
|
|
29
29
|
exports.isSubmitIntentStep = isSubmitIntentStep;
|
|
30
|
+
exports.stripNegatedSubmitClauses = stripNegatedSubmitClauses;
|
|
31
|
+
exports.classifyStepSubmitIntent = classifyStepSubmitIntent;
|
|
32
|
+
exports.isStepSubmitShaped = isStepSubmitShaped;
|
|
30
33
|
exports.isCheckboxOrRadioIntentStep = isCheckboxOrRadioIntentStep;
|
|
31
34
|
exports.shouldCaptureSelectionState = shouldCaptureSelectionState;
|
|
32
35
|
exports.flowHasSubmitSemantics = flowHasSubmitSemantics;
|
|
@@ -1141,6 +1144,14 @@ function findRecentBackendError(params) {
|
|
|
1141
1144
|
}
|
|
1142
1145
|
return null;
|
|
1143
1146
|
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Network-capture transitions say a request succeeded, not where the page
|
|
1149
|
+
* landed, so a credit must also see a landed URL the step could plausibly have
|
|
1150
|
+
* targeted (e.g. not a sign-in bounce the instruction never asked for).
|
|
1151
|
+
*/
|
|
1152
|
+
async function isLandedUrlPlausibleForStep(page, target, step) {
|
|
1153
|
+
return (0, phantom_click_1.isPlausibleStepDestination)(step, await (0, frame_target_1.readCurrentFrameUrl)(page, target));
|
|
1154
|
+
}
|
|
1144
1155
|
/**
|
|
1145
1156
|
* Detect whether the page legitimately transitioned within the supplied
|
|
1146
1157
|
* capture-meta window — a 3xx redirect or a same-origin non-GET capture
|
|
@@ -1277,6 +1288,60 @@ function isSubmitIntentStep(instruction) {
|
|
|
1277
1288
|
const haystack = instruction.toLowerCase();
|
|
1278
1289
|
return SUBMIT_INTENT_STEP_PHRASES.some((p) => haystack.includes(p));
|
|
1279
1290
|
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Targets whose negation declares the step is not a submit: a checkout/payment/
|
|
1293
|
+
* order/submit action, or a "proceeds/leads/advances to" phrase naming one.
|
|
1294
|
+
* Bare "do not click Book Now" stays undeclared — the control name alone says
|
|
1295
|
+
* nothing about whether the author means a submit.
|
|
1296
|
+
*/
|
|
1297
|
+
const SUBMIT_TARGET_SRC = "(?:(?:proceed|advance|move|go|lead|take|navigate|continue)(?:s|ing|es)?\\s+(?:you\\s+)?(?:on\\s+)?(?:to|toward|towards|into|with)\\s+(?:the\\s+|a\\s+|your\\s+)?(?:\\w+\\s+)?(?:checkout|check ?out|payments?|purchase|order|booking|reservation)|(?:place|complete|finali[sz]e|confirm)(?:s|ing)?\\s+(?:the\\s+|your\\s+|an?\\s+)?(?:order|purchase|booking|reservation|payment|checkout)|(?:advance|move|go|lead|take|navigate|continue)(?:s|ing|es)?\\s+(?:you\\s+)?(?:on\\s+)?(?:to|toward|towards|into|with|past|through)\\s+(?:the\\s+|a\\s+|your\\s+)?(?:next|following|final|last)\\s+(?:step|page|screen|stage)|(?:advance|advances|advancing)\\s+(?:the\\s+|your\\s+)?(?:booking|order|reservation|purchase)|check ?out|pay(?:ment|ing)?|purchas(?:e|es|ing)|buy(?:s|ing)?|charg(?:e|es|ing)|submit(?:s|ting)?)";
|
|
1298
|
+
/**
|
|
1299
|
+
* A negation followed, in the same clause, by a submit target ("Do NOT click
|
|
1300
|
+
* ... any control that proceeds to checkout"), or an explicit "this is not a
|
|
1301
|
+
* submit click" / "without submitting" declaration.
|
|
1302
|
+
*/
|
|
1303
|
+
const NEGATED_SUBMIT_CLAUSE = new RegExp(`\\b(?:do not|don't|dont|never|must not|should not|shouldn't|does not|doesn't|avoid|refrain from)\\b[^.;\\n]*?\\b${SUBMIT_TARGET_SRC}\\b[^.;\\n]*|\\b(?:(?:this|that|it)\\s+(?:is|are)\\s+not|is not|isn't|not)\\s+(?:a|an|the)?\\s*(?:final\\s+|actual\\s+)?(?:submit|submission|checkout|payment|purchase)\\b[^.;\\n]*|\\bwithout\\s+(?:${SUBMIT_TARGET_SRC})\\b[^.;\\n]*`, "gi");
|
|
1304
|
+
/** A positive submit imperative outside any negated clause ("Submit the form."). */
|
|
1305
|
+
const LEADING_SUBMIT_IMPERATIVE = /(?:^|[.;]\s*)(?:then\s+)?submit\b/i;
|
|
1306
|
+
/**
|
|
1307
|
+
* The instruction with every negated submit clause blanked, so callers that
|
|
1308
|
+
* match submit wording on the step text (flow-load seeding) read only what the
|
|
1309
|
+
* step positively asks for and never a guardrail ("Do NOT ... proceeds to
|
|
1310
|
+
* checkout"). Pure.
|
|
1311
|
+
*/
|
|
1312
|
+
function stripNegatedSubmitClauses(instruction) {
|
|
1313
|
+
return instruction.replace(NEGATED_SUBMIT_CLAUSE, " ");
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Reads the step's own instruction for a declared submit intent, so an
|
|
1317
|
+
* explicit "this is NOT a submit/checkout click" can veto inferred submit
|
|
1318
|
+
* shape. A positive submit phrase outside the negated clause keeps "submit"
|
|
1319
|
+
* so a real submit step that mentions a guardrail is not demoted. Pure.
|
|
1320
|
+
*/
|
|
1321
|
+
function classifyStepSubmitIntent(instruction) {
|
|
1322
|
+
if (!instruction)
|
|
1323
|
+
return "unspecified";
|
|
1324
|
+
const remainder = stripNegatedSubmitClauses(instruction);
|
|
1325
|
+
if (isSubmitIntentStep(remainder) || LEADING_SUBMIT_IMPERATIVE.test(remainder))
|
|
1326
|
+
return "submit";
|
|
1327
|
+
if (remainder !== instruction)
|
|
1328
|
+
return "non-submit";
|
|
1329
|
+
return isSubmitIntentStep(instruction) ? "submit" : "unspecified";
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Single seam answering "is this step submit-shaped?": an explicit `submitStep`
|
|
1333
|
+
* flag is authoritative, a declared non-submit instruction vetoes every
|
|
1334
|
+
* inferred path, otherwise final-step inference or submit-intent wording applies.
|
|
1335
|
+
*/
|
|
1336
|
+
function isStepSubmitShaped(params) {
|
|
1337
|
+
const { step, submitStep, isFinalStep, flowHasSubmitSemantics: flowSubmit } = params;
|
|
1338
|
+
if (submitStep)
|
|
1339
|
+
return true;
|
|
1340
|
+
const intent = classifyStepSubmitIntent(step);
|
|
1341
|
+
if (intent === "non-submit")
|
|
1342
|
+
return false;
|
|
1343
|
+
return (isFinalStep && flowSubmit) || intent === "submit";
|
|
1344
|
+
}
|
|
1280
1345
|
/**
|
|
1281
1346
|
* Is this flow step's intent to check a checkbox or select a radio option?
|
|
1282
1347
|
* Used to veto a weak page-wide DOM signal (htmlDelta/textChanged/formValueChanged)
|
|
@@ -1623,13 +1688,26 @@ function isDomOnlyAdvanceVerified(params) {
|
|
|
1623
1688
|
* when the probe can't run (non-xpath selector, evaluate failure) it stays
|
|
1624
1689
|
* at its default `true` so it never manufactures a veto out of missing
|
|
1625
1690
|
* information. Only a confirmed `false` blocks the credit.
|
|
1691
|
+
*
|
|
1692
|
+
* **Destination-plausibility veto:** none of the signals above look at
|
|
1693
|
+
* WHERE the click landed. A non-submit click that bounces to a page
|
|
1694
|
+
* {@link isPlausibleStepDestination} rejects as implausible for the step
|
|
1695
|
+
* (e.g. a settings-panel tab switcher landing on an unrelated marketing
|
|
1696
|
+
* page) can still clear every byte/text threshold above, crediting a
|
|
1697
|
+
* phantom transition the same way a same-page toggle is credited.
|
|
1698
|
+
* `destinationPlausible` is optional and defaults to the `!== false`
|
|
1699
|
+
* no-veto behavior — matching the sibling `destinationPlausible` param on
|
|
1700
|
+
* `classifyPhantomClick` — so existing/future callers that don't thread it
|
|
1701
|
+
* are unaffected.
|
|
1626
1702
|
*/
|
|
1627
1703
|
function isClickViewSwapVerified(params) {
|
|
1628
1704
|
const VIEW_SWAP_MIN_BYTES = config_1.config.scraper.viewSwapMinBytesThreshold;
|
|
1629
1705
|
const VIEW_SWAP_REVEAL_MIN_BYTES = config_1.config.scraper.viewSwapRevealMinBytesThreshold;
|
|
1630
|
-
const { resolvedAction, submitStep, isAdvanceWithPattern, networkDelta, bytesDelta, textChanged, invalidMarkerDelta = 0, clickedElementStillPresent = true, resolvedElementIsSubmitShaped = false, } = params;
|
|
1706
|
+
const { resolvedAction, submitStep, isAdvanceWithPattern, networkDelta, bytesDelta, textChanged, invalidMarkerDelta = 0, clickedElementStillPresent = true, resolvedElementIsSubmitShaped = false, destinationPlausible, } = params;
|
|
1631
1707
|
if (resolvedAction?.method !== "click")
|
|
1632
1708
|
return false;
|
|
1709
|
+
if (destinationPlausible === false)
|
|
1710
|
+
return false;
|
|
1633
1711
|
// Only the step's own explicit submitStep flag identifies the step that
|
|
1634
1712
|
// actually needs network/URL verification — mirrors the submit-judge
|
|
1635
1713
|
// gate's hasSubmitTransitionSignal discipline (see 5763ac2). Inferring
|
|
@@ -3341,7 +3419,7 @@ function xpathTailForRetarget(xpath) {
|
|
|
3341
3419
|
* bar, preserving the previous behavior for every single-match (the common)
|
|
3342
3420
|
* case.
|
|
3343
3421
|
*/
|
|
3344
|
-
const XPATH_TAIL_RETARGET_RESOLVE_FN_SRC = `((tail) => {
|
|
3422
|
+
const XPATH_TAIL_RETARGET_RESOLVE_FN_SRC = `((tail, preferSubmit) => {
|
|
3345
3423
|
const isNegative = ${submit_control_1.NEGATIVE_TEXT_EXPR};
|
|
3346
3424
|
const accessibleName = (el) =>
|
|
3347
3425
|
(el.getAttribute("aria-label") || el.textContent || "").replace(/\\s+/g, " ").trim().toLowerCase();
|
|
@@ -3350,6 +3428,7 @@ const XPATH_TAIL_RETARGET_RESOLVE_FN_SRC = `((tail) => {
|
|
|
3350
3428
|
const nodes = [];
|
|
3351
3429
|
for (let i = 0; i < r.snapshotLength; i++) nodes.push(r.snapshotItem(i));
|
|
3352
3430
|
if (nodes.length <= 1) return nodes[0] || null;
|
|
3431
|
+
if (!preferSubmit) return nodes[0];
|
|
3353
3432
|
const preferred = nodes.find((el) => isSubmitShaped(el) && !isNegative(accessibleName(el)));
|
|
3354
3433
|
return preferred || nodes[0];
|
|
3355
3434
|
})`;
|
|
@@ -3696,7 +3775,7 @@ async function resolvedClickTargetIsSubmitShaped(target, selector, xpathTail = n
|
|
|
3696
3775
|
const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
3697
3776
|
let el = r.singleNodeValue;
|
|
3698
3777
|
if (!el && ${JSON.stringify(xpathTail)}) {
|
|
3699
|
-
el = ${XPATH_TAIL_RETARGET_RESOLVE_FN_SRC}(${JSON.stringify(xpathTail)});
|
|
3778
|
+
el = ${XPATH_TAIL_RETARGET_RESOLVE_FN_SRC}(${JSON.stringify(xpathTail)}, ${JSON.stringify(true)});
|
|
3700
3779
|
}
|
|
3701
3780
|
const isSubmitShaped = ${submit_control_1.SUBMIT_SHAPE_EXPR};
|
|
3702
3781
|
return isSubmitShaped(el);
|
|
@@ -4873,10 +4952,11 @@ Object.defineProperty(exports, "hasOriginOrPathChanged", { enumerable: true, get
|
|
|
4873
4952
|
* independently of whether `advanceTransitionBodyPattern` is configured.
|
|
4874
4953
|
*/
|
|
4875
4954
|
async function waitForCaptchaNavigation(params) {
|
|
4876
|
-
const { page, captchaTarget, baselineUrl, timeoutMs, intervalMs } = params;
|
|
4955
|
+
const { page, captchaTarget, baselineUrl, step, timeoutMs, intervalMs } = params;
|
|
4877
4956
|
const check = async () => {
|
|
4878
4957
|
const currentUrl = await (0, frame_target_1.readCurrentFrameUrl)(page, captchaTarget);
|
|
4879
|
-
return (0, phantom_click_1.hasOriginOrPathChanged)(baselineUrl, currentUrl)
|
|
4958
|
+
return ((0, phantom_click_1.hasOriginOrPathChanged)(baselineUrl, currentUrl) &&
|
|
4959
|
+
(0, phantom_click_1.isPlausibleStepDestination)(step, currentUrl));
|
|
4880
4960
|
};
|
|
4881
4961
|
if (await check())
|
|
4882
4962
|
return true;
|
|
@@ -5246,6 +5326,7 @@ async function submitCaptchaGatedForm(target, responseField = "h-captcha-respons
|
|
|
5246
5326
|
pre,
|
|
5247
5327
|
post,
|
|
5248
5328
|
isSubmitShapedStep: true,
|
|
5329
|
+
destinationPlausible: (0, phantom_click_1.isPlausibleStepDestination)(verification.step, post.url),
|
|
5249
5330
|
});
|
|
5250
5331
|
if (verdict !== "phantom")
|
|
5251
5332
|
return true;
|
|
@@ -5258,8 +5339,18 @@ async function submitCaptchaGatedForm(target, responseField = "h-captcha-respons
|
|
|
5258
5339
|
return { clicked: false };
|
|
5259
5340
|
throw err;
|
|
5260
5341
|
});
|
|
5261
|
-
if (runnerUpClickResult.clicked)
|
|
5262
|
-
|
|
5342
|
+
if (runnerUpClickResult.clicked) {
|
|
5343
|
+
const runnerUpPost = await snapshotPage(target, verification.signalCounter, verification.page);
|
|
5344
|
+
const runnerUpVerdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
5345
|
+
actResultSuccess: true,
|
|
5346
|
+
pre: post,
|
|
5347
|
+
post: runnerUpPost,
|
|
5348
|
+
isSubmitShapedStep: true,
|
|
5349
|
+
destinationPlausible: (0, phantom_click_1.isPlausibleStepDestination)(verification.step, runnerUpPost.url),
|
|
5350
|
+
});
|
|
5351
|
+
if (runnerUpVerdict !== "phantom")
|
|
5352
|
+
return true;
|
|
5353
|
+
}
|
|
5263
5354
|
}
|
|
5264
5355
|
}
|
|
5265
5356
|
}
|
|
@@ -7973,7 +8064,7 @@ function redactIfSensitive(text, sensitiveValue) {
|
|
|
7973
8064
|
* candidate was actually clickable, or every click phantomed.
|
|
7974
8065
|
*/
|
|
7975
8066
|
async function probeChildFrameSubmitFallback(params) {
|
|
7976
|
-
const { page, signalCounter } = params;
|
|
8067
|
+
const { page, signalCounter, step } = params;
|
|
7977
8068
|
// `page.mainFrameId`/`page.frames` are read defensively: some call sites
|
|
7978
8069
|
// (and their test fakes) model a `Page` that never attaches child frames at
|
|
7979
8070
|
// all and doesn't implement this pair, which is indistinguishable here from
|
|
@@ -8032,6 +8123,7 @@ async function probeChildFrameSubmitFallback(params) {
|
|
|
8032
8123
|
pre,
|
|
8033
8124
|
post,
|
|
8034
8125
|
isSubmitShapedStep: true,
|
|
8126
|
+
destinationPlausible: (0, phantom_click_1.isPlausibleStepDestination)(step, post.url),
|
|
8035
8127
|
});
|
|
8036
8128
|
if (verdict !== "phantom")
|
|
8037
8129
|
return target;
|
|
@@ -8047,6 +8139,7 @@ async function probeChildFrameSubmitFallback(params) {
|
|
|
8047
8139
|
pre,
|
|
8048
8140
|
post: runnerUpPost,
|
|
8049
8141
|
isSubmitShapedStep: true,
|
|
8142
|
+
destinationPlausible: (0, phantom_click_1.isPlausibleStepDestination)(step, runnerUpPost.url),
|
|
8050
8143
|
});
|
|
8051
8144
|
if (runnerUpVerdict !== "phantom")
|
|
8052
8145
|
return target;
|
|
@@ -8228,7 +8321,8 @@ async function executeStepWithHealing(params) {
|
|
|
8228
8321
|
// Submit, and unfilled required fields produce silent submit failures
|
|
8229
8322
|
// (verified 2026-06-15 on one measured tenant's telemetry). Site-agnostic: any flow
|
|
8230
8323
|
// whose submit is mid-list can mark its submit step explicitly.
|
|
8231
|
-
const
|
|
8324
|
+
const stepDeclaredNonSubmit = !submitStep && classifyStepSubmitIntent(step) === "non-submit";
|
|
8325
|
+
const requireSubmitEndpoint = (isFinalStep || submitStep) && !stepDeclaredNonSubmit && submitEndpointPattern !== null;
|
|
8232
8326
|
// Capture the per-element selection baseline (for verifyDomEffect's element-
|
|
8233
8327
|
// scoped click read-back) ONLY for selection/field-answer click steps — never
|
|
8234
8328
|
// for submit/advance steps, whose own self-toggling buttons must not
|
|
@@ -8380,7 +8474,12 @@ async function executeStepWithHealing(params) {
|
|
|
8380
8474
|
// when no solver key is configured or the solve otherwise fails — that
|
|
8381
8475
|
// propagates out of this function unchanged, so the step fails cleanly
|
|
8382
8476
|
// instead of silently falling through as if unverified-but-passing.
|
|
8383
|
-
const isSubmitOrFinalStep =
|
|
8477
|
+
const isSubmitOrFinalStep = isStepSubmitShaped({
|
|
8478
|
+
step,
|
|
8479
|
+
submitStep,
|
|
8480
|
+
isFinalStep,
|
|
8481
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
8482
|
+
});
|
|
8384
8483
|
if (captchaGated && isSubmitOrFinalStep) {
|
|
8385
8484
|
let captchaTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
8386
8485
|
const sitekeyProbeExpr = `(() => {
|
|
@@ -8523,6 +8622,7 @@ async function executeStepWithHealing(params) {
|
|
|
8523
8622
|
fallbackSubmitted = await submitCaptchaGatedForm(captchaTarget, "h-captcha-response", {
|
|
8524
8623
|
signalCounter,
|
|
8525
8624
|
page,
|
|
8625
|
+
step,
|
|
8526
8626
|
});
|
|
8527
8627
|
}
|
|
8528
8628
|
catch (err) {
|
|
@@ -8567,6 +8667,7 @@ async function executeStepWithHealing(params) {
|
|
|
8567
8667
|
page,
|
|
8568
8668
|
captchaTarget,
|
|
8569
8669
|
baselineUrl: pageUrl,
|
|
8670
|
+
step,
|
|
8570
8671
|
timeoutMs: CAPTCHA_TRANSITION_POLL_MS,
|
|
8571
8672
|
intervalMs: ADVANCE_TRANSITION_POLL_INTERVAL_MS,
|
|
8572
8673
|
});
|
|
@@ -8586,7 +8687,8 @@ async function executeStepWithHealing(params) {
|
|
|
8586
8687
|
recentCaptureMeta,
|
|
8587
8688
|
preMetaLength: preCaptchaMetaLength,
|
|
8588
8689
|
});
|
|
8589
|
-
if (networkTransitionUrl !== null
|
|
8690
|
+
if (networkTransitionUrl !== null &&
|
|
8691
|
+
(await isLandedUrlPlausibleForStep(page, captchaTarget, step))) {
|
|
8590
8692
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: post-submit network response confirmed the advance (${networkTransitionUrl})`);
|
|
8591
8693
|
trajectory?.push({ stepIndex, verifiedBy: "network" });
|
|
8592
8694
|
return "completed";
|
|
@@ -8703,7 +8805,8 @@ async function executeStepWithHealing(params) {
|
|
|
8703
8805
|
recentCaptureMeta,
|
|
8704
8806
|
preMetaLength: stepStartMetaLength,
|
|
8705
8807
|
});
|
|
8706
|
-
if (transitionUrl !== null
|
|
8808
|
+
if (transitionUrl !== null &&
|
|
8809
|
+
(await isLandedUrlPlausibleForStep(page, frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), step))) {
|
|
8707
8810
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} skipped (probe absent but recent transition detected: ${transitionUrl})`);
|
|
8708
8811
|
trajectory?.push({ stepIndex, verifiedBy: "url" });
|
|
8709
8812
|
return "completed";
|
|
@@ -8733,8 +8836,13 @@ async function executeStepWithHealing(params) {
|
|
|
8733
8836
|
// a radio/select answer) could complete the step by clicking an unrelated
|
|
8734
8837
|
// submit control in an unrelated same-origin child iframe.
|
|
8735
8838
|
if (!frameTarget?.declaredFrameSelector &&
|
|
8736
|
-
(
|
|
8737
|
-
|
|
8839
|
+
isStepSubmitShaped({
|
|
8840
|
+
step,
|
|
8841
|
+
submitStep,
|
|
8842
|
+
isFinalStep,
|
|
8843
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
8844
|
+
})) {
|
|
8845
|
+
const fallbackTarget = await probeChildFrameSubmitFallback({ page, signalCounter, step });
|
|
8738
8846
|
if (fallbackTarget) {
|
|
8739
8847
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} probe-absent: no candidate in the resolved frame, but a same-origin child iframe surfaced a submit-shaped candidate — clicked and verified there instead of failing`);
|
|
8740
8848
|
trajectory?.push({ stepIndex, verifiedBy: "dom" });
|
|
@@ -8767,7 +8875,12 @@ async function executeStepWithHealing(params) {
|
|
|
8767
8875
|
const unfocusedObserve = probeAbsentObservedUnfocused.length === 0 && frameTarget?.frame
|
|
8768
8876
|
? await deepLocatorCandidatesAsActions(page, frameTarget)
|
|
8769
8877
|
: probeAbsentObservedUnfocused;
|
|
8770
|
-
const targetResolutionDiagnostic =
|
|
8878
|
+
const targetResolutionDiagnostic = isStepSubmitShaped({
|
|
8879
|
+
step,
|
|
8880
|
+
submitStep,
|
|
8881
|
+
isFinalStep,
|
|
8882
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
8883
|
+
})
|
|
8771
8884
|
? await (0, target_resolution_diagnostic_1.captureTargetResolutionDiagnosticSnapshot)(frameTarget, page)
|
|
8772
8885
|
: null;
|
|
8773
8886
|
const dumpPath = onStepFailure?.({
|
|
@@ -9052,6 +9165,7 @@ async function executeStepWithHealing(params) {
|
|
|
9052
9165
|
// already has an up-to-date ranking to report from.
|
|
9053
9166
|
let lastRankedSubmitCandidates = null;
|
|
9054
9167
|
for (let attempt = 1; attempt <= MAX_STEP_ATTEMPTS; attempt++) {
|
|
9168
|
+
params.abortSignal?.throwIfAborted();
|
|
9055
9169
|
// Telemetry-driven technique-skip: when a cascade technique's
|
|
9056
9170
|
// preconditions cannot be met by the prior attempts' state, running
|
|
9057
9171
|
// it would burn the attempt slot without exercising new behaviour.
|
|
@@ -9060,7 +9174,12 @@ async function executeStepWithHealing(params) {
|
|
|
9060
9174
|
if (attempt > 1) {
|
|
9061
9175
|
const wouldBeTechnique = attempt === 2
|
|
9062
9176
|
? attempt1UnreachableViaLightDom &&
|
|
9063
|
-
(
|
|
9177
|
+
isStepSubmitShaped({
|
|
9178
|
+
step,
|
|
9179
|
+
submitStep,
|
|
9180
|
+
isFinalStep,
|
|
9181
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
9182
|
+
})
|
|
9064
9183
|
? "deep-submit-locator"
|
|
9065
9184
|
: phantomClickAfterAttempt1
|
|
9066
9185
|
? "trusted-click-retry"
|
|
@@ -9079,7 +9198,12 @@ async function executeStepWithHealing(params) {
|
|
|
9079
9198
|
})),
|
|
9080
9199
|
advanceUnmovedAfterAttempt1,
|
|
9081
9200
|
attempt1UnreachableViaLightDom,
|
|
9082
|
-
submitShapedStep:
|
|
9201
|
+
submitShapedStep: isStepSubmitShaped({
|
|
9202
|
+
step,
|
|
9203
|
+
submitStep,
|
|
9204
|
+
isFinalStep,
|
|
9205
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
9206
|
+
}),
|
|
9083
9207
|
});
|
|
9084
9208
|
if (decision.skip) {
|
|
9085
9209
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt} (${wouldBeTechnique}) skipped: ${decision.reason}`);
|
|
@@ -9217,7 +9341,12 @@ async function executeStepWithHealing(params) {
|
|
|
9217
9341
|
}
|
|
9218
9342
|
else if (attempt === 2 &&
|
|
9219
9343
|
attempt1UnreachableViaLightDom &&
|
|
9220
|
-
(
|
|
9344
|
+
isStepSubmitShaped({
|
|
9345
|
+
step,
|
|
9346
|
+
submitStep,
|
|
9347
|
+
isFinalStep,
|
|
9348
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
9349
|
+
})) {
|
|
9221
9350
|
// Deep submit-control locator: attempt 1 either phantom-clicked
|
|
9222
9351
|
// (Stagehand reported success but pre/post showed zero effect) or
|
|
9223
9352
|
// came back "unresolved" (act() found zero candidates at all), so the
|
|
@@ -9301,6 +9430,7 @@ async function executeStepWithHealing(params) {
|
|
|
9301
9430
|
// stray selection flip must not mask a top-pick phantom and skip
|
|
9302
9431
|
// the runner-up retry.
|
|
9303
9432
|
isSubmitShapedStep: true,
|
|
9433
|
+
destinationPlausible: (0, phantom_click_1.isPlausibleStepDestination)(step, midPost.url),
|
|
9304
9434
|
});
|
|
9305
9435
|
if (topVerdict === "phantom") {
|
|
9306
9436
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} deep-submit-locator top pick (${top.tag} "${top.accessibleName}") phantom-clicked; retrying runner-up ${runnerUp.tag} "${runnerUp.accessibleName}" (tier ${runnerUp.tier})`);
|
|
@@ -9343,7 +9473,12 @@ async function executeStepWithHealing(params) {
|
|
|
9343
9473
|
}
|
|
9344
9474
|
else if (attempt === 2 &&
|
|
9345
9475
|
phantomClickAfterAttempt1 &&
|
|
9346
|
-
!(
|
|
9476
|
+
!isStepSubmitShaped({
|
|
9477
|
+
step,
|
|
9478
|
+
submitStep,
|
|
9479
|
+
isFinalStep,
|
|
9480
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
9481
|
+
})) {
|
|
9347
9482
|
// Trusted-click retry: attempt 1 phantom-clicked a NON-submit control —
|
|
9348
9483
|
// Stagehand reported success but pre/post showed zero effect. On a
|
|
9349
9484
|
// design-system widget (React synthetic-event delegation, custom
|
|
@@ -9997,7 +10132,10 @@ async function executeStepWithHealing(params) {
|
|
|
9997
10132
|
record.resolvedArguments = (resolvedAction.arguments ?? []).map(String);
|
|
9998
10133
|
}
|
|
9999
10134
|
const networkFired = post.networkCount > pre.networkCount;
|
|
10000
|
-
|
|
10135
|
+
// Computed once: gates every credit signal that takes no post-click URL
|
|
10136
|
+
// (network, DOM read-back) and classifyPhantomClick's destinationPlausible.
|
|
10137
|
+
const destinationPlausible = (0, phantom_click_1.isPlausibleStepDestination)(step, post.url);
|
|
10138
|
+
const urlChanged = (0, phantom_click_1.hasOriginOrPathChanged)(pre.url, post.url) && destinationPlausible;
|
|
10001
10139
|
const isStateClass = resolvedAction !== null && STATE_CLASS_METHODS.has(resolvedAction.method ?? "");
|
|
10002
10140
|
const isClick = resolvedAction !== null && resolvedAction.method === "click";
|
|
10003
10141
|
// State-class actions (fill/check/etc.) never move the network counter or URL,
|
|
@@ -10032,7 +10170,7 @@ async function executeStepWithHealing(params) {
|
|
|
10032
10170
|
// short-circuits when the transition is already in-window (zero added latency
|
|
10033
10171
|
// on the common path). Scoped by preCaptureIdx via an eviction-proof disk scan.
|
|
10034
10172
|
const advanceGateTimeoutMs = captchaGated && isSubmitOrFinalStep ? CAPTCHA_TRANSITION_POLL_MS : ADVANCE_TRANSITION_POLL_MS;
|
|
10035
|
-
const
|
|
10173
|
+
const networkAdvanced = !advanceGateActive
|
|
10036
10174
|
? networkFired
|
|
10037
10175
|
: await waitForTransitionBody({
|
|
10038
10176
|
page,
|
|
@@ -10041,7 +10179,8 @@ async function executeStepWithHealing(params) {
|
|
|
10041
10179
|
timeoutMs: advanceGateTimeoutMs,
|
|
10042
10180
|
intervalMs: ADVANCE_TRANSITION_POLL_INTERVAL_MS,
|
|
10043
10181
|
});
|
|
10044
|
-
|
|
10182
|
+
const networkIsRealAdvance = networkAdvanced && destinationPlausible;
|
|
10183
|
+
if (advanceGateActive && !networkAdvanced) {
|
|
10045
10184
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} network fired but no advance-transition (type=next) body matched within ${advanceGateTimeoutMs}ms poll (non-advancing POST); not treating as verified`);
|
|
10046
10185
|
}
|
|
10047
10186
|
// DOM-only-advance veto (opt-in). A rephrase can turn an advance/"Next" step
|
|
@@ -10059,15 +10198,20 @@ async function executeStepWithHealing(params) {
|
|
|
10059
10198
|
// `advanceTransitionBodyPattern` are unaffected.
|
|
10060
10199
|
const domVerifiedForStep = isDomOnlyAdvanceVerified({
|
|
10061
10200
|
hasPattern: advanceTransitionBodyPattern !== null,
|
|
10062
|
-
isFinalOrSubmit:
|
|
10201
|
+
isFinalOrSubmit: isStepSubmitShaped({
|
|
10202
|
+
step,
|
|
10203
|
+
submitStep,
|
|
10204
|
+
isFinalStep,
|
|
10205
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
10206
|
+
}),
|
|
10063
10207
|
isAdvance: isAdvanceStep(step),
|
|
10064
|
-
domVerified,
|
|
10208
|
+
domVerified: domVerified && destinationPlausible,
|
|
10065
10209
|
networkIsRealAdvance,
|
|
10066
10210
|
urlChanged,
|
|
10067
10211
|
})
|
|
10068
|
-
? domVerified
|
|
10212
|
+
? domVerified && destinationPlausible
|
|
10069
10213
|
: false;
|
|
10070
|
-
if (domVerified && !domVerifiedForStep) {
|
|
10214
|
+
if (domVerified && !domVerifiedForStep && destinationPlausible) {
|
|
10071
10215
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} advance step succeeded only via DOM state change (field toggle / non-advancing POST), not a real transition; not treating as verified`);
|
|
10072
10216
|
}
|
|
10073
10217
|
// Blocked-submit veto input for the view-swap gate below. Read post-click
|
|
@@ -10092,7 +10236,7 @@ async function executeStepWithHealing(params) {
|
|
|
10092
10236
|
// Attribute-based submit-shape read of the resolved click target itself —
|
|
10093
10237
|
// independent of the flow-authored `submitStep` flag. See
|
|
10094
10238
|
// resolvedClickTargetIsSubmitShaped's doc comment.
|
|
10095
|
-
const resolvedElementIsSubmitShaped = isClick && resolvedAction?.selector
|
|
10239
|
+
const resolvedElementIsSubmitShaped = isClick && !stepDeclaredNonSubmit && resolvedAction?.selector
|
|
10096
10240
|
? await resolvedClickTargetIsSubmitShaped(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), resolvedAction.selector)
|
|
10097
10241
|
: false;
|
|
10098
10242
|
// Client-side view-swap gate: credit a click that produces substantial
|
|
@@ -10102,7 +10246,8 @@ async function executeStepWithHealing(params) {
|
|
|
10102
10246
|
// Below that threshold, also credits a smaller text-changing reveal (≥500B) —
|
|
10103
10247
|
// fixes the top-window site Work-History gate-message reveal (+789B) that used to
|
|
10104
10248
|
// cascade to a 5-attempt failure and global replan. Vetoed when the click's
|
|
10105
|
-
// ng-invalid marker count grew
|
|
10249
|
+
// ng-invalid marker count grew, or when the landed destination is implausible
|
|
10250
|
+
// for the step (see isClickViewSwapVerified's doc comment).
|
|
10106
10251
|
const clickViewSwapVerified = isClickViewSwapVerified({
|
|
10107
10252
|
resolvedAction,
|
|
10108
10253
|
submitStep,
|
|
@@ -10113,6 +10258,7 @@ async function executeStepWithHealing(params) {
|
|
|
10113
10258
|
invalidMarkerDelta: postInvalidMarkerCount - preInvalidMarkerCount,
|
|
10114
10259
|
clickedElementStillPresent,
|
|
10115
10260
|
resolvedElementIsSubmitShaped,
|
|
10261
|
+
destinationPlausible,
|
|
10116
10262
|
});
|
|
10117
10263
|
if (clickViewSwapVerified === false &&
|
|
10118
10264
|
isClick &&
|
|
@@ -10145,12 +10291,15 @@ async function executeStepWithHealing(params) {
|
|
|
10145
10291
|
// submitStep, inferred final-step, or an attribute-detected submit
|
|
10146
10292
|
// control) may not ride this signal alone — only when the submit-judge
|
|
10147
10293
|
// gate (requireSubmitEndpoint) will re-litigate the credit below.
|
|
10148
|
-
const formValueWeakSignalAllowed = !(
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10294
|
+
const formValueWeakSignalAllowed = !(isStepSubmitShaped({
|
|
10295
|
+
step,
|
|
10296
|
+
submitStep,
|
|
10297
|
+
isFinalStep,
|
|
10298
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
10299
|
+
}) || resolvedElementIsSubmitShaped) || requireSubmitEndpoint;
|
|
10152
10300
|
const formValueVerified = isStateClass &&
|
|
10153
10301
|
formValueWeakSignalAllowed &&
|
|
10302
|
+
destinationPlausible &&
|
|
10154
10303
|
post.formValueSignature !== pre.formValueSignature;
|
|
10155
10304
|
// Committed-value guard on the act-success path. A controlled datepicker
|
|
10156
10305
|
// (react-datepicker) accepts the typed value, discards it on React's next
|
|
@@ -10253,9 +10402,14 @@ async function executeStepWithHealing(params) {
|
|
|
10253
10402
|
actResultSuccess: record.actResultSuccess,
|
|
10254
10403
|
pre,
|
|
10255
10404
|
post,
|
|
10256
|
-
elementStateChanged: domVerified,
|
|
10257
|
-
isSubmitShapedStep:
|
|
10258
|
-
|
|
10405
|
+
elementStateChanged: domVerified && destinationPlausible,
|
|
10406
|
+
isSubmitShapedStep: isStepSubmitShaped({
|
|
10407
|
+
step,
|
|
10408
|
+
submitStep,
|
|
10409
|
+
isFinalStep,
|
|
10410
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
10411
|
+
}),
|
|
10412
|
+
destinationPlausible,
|
|
10259
10413
|
});
|
|
10260
10414
|
// An `"effective"` verdict driven purely by the page-wide byte-delta
|
|
10261
10415
|
// floor (`TRIVIAL_DOM_DELTA_BYTES`, 500B) is intentionally NOT trusted
|
|
@@ -10276,8 +10430,12 @@ async function executeStepWithHealing(params) {
|
|
|
10276
10430
|
// respects the SAME DOM-only-advance veto as the block above: a wizard
|
|
10277
10431
|
// "Next" step on a pattern-configured site whose only signal is a field
|
|
10278
10432
|
// toggle must stay unverified, not get waved through by the verdict.
|
|
10279
|
-
const domEffectiveVerdict = !(
|
|
10280
|
-
|
|
10433
|
+
const domEffectiveVerdict = !isStepSubmitShaped({
|
|
10434
|
+
step,
|
|
10435
|
+
submitStep,
|
|
10436
|
+
isFinalStep,
|
|
10437
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
10438
|
+
}) && domVerifiedForStep;
|
|
10281
10439
|
let verified = networkIsRealAdvance ||
|
|
10282
10440
|
urlChanged ||
|
|
10283
10441
|
domVerifiedForStep ||
|
|
@@ -10492,11 +10650,15 @@ async function executeStepWithHealing(params) {
|
|
|
10492
10650
|
// tail node `clickExpr` below is about to click, instead of only
|
|
10493
10651
|
// ever checking the (possibly unresolvable) primary xpath — see
|
|
10494
10652
|
// resolvedClickTargetIsSubmitShaped's doc comment.
|
|
10495
|
-
const retryResolvedElementIsSubmitShaped =
|
|
10653
|
+
const retryResolvedElementIsSubmitShaped = stepDeclaredNonSubmit
|
|
10654
|
+
? false
|
|
10655
|
+
: await resolvedClickTargetIsSubmitShaped(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), resolvedAction.selector, xpathTail);
|
|
10496
10656
|
// See resolvedClickUsedXpathTailRetarget's doc comment: flags that
|
|
10497
10657
|
// this click's target identity came only from the loose tail
|
|
10498
10658
|
// heuristic, not Stagehand's own resolved selector.
|
|
10499
|
-
const retryClickUsedXpathTailRetarget =
|
|
10659
|
+
const retryClickUsedXpathTailRetarget = stepDeclaredNonSubmit
|
|
10660
|
+
? false
|
|
10661
|
+
: await resolvedClickUsedXpathTailRetarget(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), resolvedAction.selector, xpathTail);
|
|
10500
10662
|
// Trusted-click delivery for the plain-click branch: attempted
|
|
10501
10663
|
// BEFORE the resolve/activate expression below (so a successful
|
|
10502
10664
|
// delivery can suppress the synthetic `clickActivationExpr`
|
|
@@ -10528,7 +10690,7 @@ async function executeStepWithHealing(params) {
|
|
|
10528
10690
|
const n16TrustedClickFailure = !n16TrustedDelivered && "reason" in n16TrustedClickResult
|
|
10529
10691
|
? n16TrustedClickResult
|
|
10530
10692
|
: null;
|
|
10531
|
-
const clickExpr = `(() => { const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); let el = r.singleNodeValue; if (!el && ${JSON.stringify(xpathTail)}) { el = ${XPATH_TAIL_RETARGET_RESOLVE_FN_SRC}(${JSON.stringify(xpathTail)}); } if (!el || typeof el.click !== "function") return { fired: false }; if (el.tagName === "LABEL") { const wrapped = el.querySelector("input[type=checkbox], input[type=radio]"); if (wrapped) el = wrapped; } if (el.type === "checkbox" || el.type === "radio") { el.checked = true; el.dispatchEvent(new Event("click", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); return { fired: true, kind: "checkbox", checked: el.checked }; } let __n16SmMatched = false; ${(0, browser_click_expr_1.retargetToSelectionMarkerExpr)("el", "__n16SmMatched")} ${n16TrustedDelivered ? "" : (0, browser_click_expr_1.clickActivationExpr)("el")} if (__n16SmMatched) { el.dispatchEvent(new Event("change", { bubbles: true })); } return { fired: true, kind: "click" }; })()`;
|
|
10693
|
+
const clickExpr = `(() => { const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); let el = r.singleNodeValue; if (!el && ${JSON.stringify(xpathTail)}) { el = ${XPATH_TAIL_RETARGET_RESOLVE_FN_SRC}(${JSON.stringify(xpathTail)}, ${JSON.stringify(!stepDeclaredNonSubmit)}); } if (!el || typeof el.click !== "function") return { fired: false }; if (el.tagName === "LABEL") { const wrapped = el.querySelector("input[type=checkbox], input[type=radio]"); if (wrapped) el = wrapped; } if (el.type === "checkbox" || el.type === "radio") { el.checked = true; el.dispatchEvent(new Event("click", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); return { fired: true, kind: "checkbox", checked: el.checked }; } let __n16SmMatched = false; ${(0, browser_click_expr_1.retargetToSelectionMarkerExpr)("el", "__n16SmMatched")} ${n16TrustedDelivered ? "" : (0, browser_click_expr_1.clickActivationExpr)("el")} if (__n16SmMatched) { el.dispatchEvent(new Event("change", { bubbles: true })); } return { fired: true, kind: "click" }; })()`;
|
|
10532
10694
|
const n16FallbackTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
10533
10695
|
const probeResult = (await n16FallbackTarget.evaluate(clickExpr));
|
|
10534
10696
|
const fired = probeResult.fired;
|
|
@@ -10547,7 +10709,7 @@ async function executeStepWithHealing(params) {
|
|
|
10547
10709
|
const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
10548
10710
|
let node = r.singleNodeValue;
|
|
10549
10711
|
if (!node && ${JSON.stringify(xpathTail)}) {
|
|
10550
|
-
node = ${XPATH_TAIL_RETARGET_RESOLVE_FN_SRC}(${JSON.stringify(xpathTail)});
|
|
10712
|
+
node = ${XPATH_TAIL_RETARGET_RESOLVE_FN_SRC}(${JSON.stringify(xpathTail)}, ${JSON.stringify(!stepDeclaredNonSubmit)});
|
|
10551
10713
|
}
|
|
10552
10714
|
if (!node) return false;
|
|
10553
10715
|
for (let depth = 0; depth < 6 && node; depth++) {
|
|
@@ -10631,7 +10793,7 @@ async function executeStepWithHealing(params) {
|
|
|
10631
10793
|
const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
10632
10794
|
let el = r.singleNodeValue;
|
|
10633
10795
|
if (!el && ${JSON.stringify(xpathTail)}) {
|
|
10634
|
-
el = ${XPATH_TAIL_RETARGET_RESOLVE_FN_SRC}(${JSON.stringify(xpathTail)});
|
|
10796
|
+
el = ${XPATH_TAIL_RETARGET_RESOLVE_FN_SRC}(${JSON.stringify(xpathTail)}, ${JSON.stringify(!stepDeclaredNonSubmit)});
|
|
10635
10797
|
}
|
|
10636
10798
|
return el ? isDisabled(el) : false;
|
|
10637
10799
|
})()`;
|
|
@@ -10659,7 +10821,11 @@ async function executeStepWithHealing(params) {
|
|
|
10659
10821
|
// Poll for the real TransitionWorklet(type="next") like the primary
|
|
10660
10822
|
// verifier — the transition POST can land after this snapshot, and a
|
|
10661
10823
|
// one-shot check would false-negative and retry into a back-bounce.
|
|
10824
|
+
// Destination-gated so an implausible landing URL's traffic cannot satisfy
|
|
10825
|
+
// the veto or the submit-transition carve-out downstream.
|
|
10826
|
+
const retryDestinationPlausible = (0, phantom_click_1.isPlausibleStepDestination)(step, retryPost.url);
|
|
10662
10827
|
const retryNetworkIsRealAdvance = retryNetworkFired &&
|
|
10828
|
+
retryDestinationPlausible &&
|
|
10663
10829
|
(await waitForTransitionBody({
|
|
10664
10830
|
page,
|
|
10665
10831
|
preIdx: preCaptureIdx,
|
|
@@ -10671,7 +10837,12 @@ async function executeStepWithHealing(params) {
|
|
|
10671
10837
|
}));
|
|
10672
10838
|
const fallbackDomOnlyAdvance = shouldVetoFallbackAdvance({
|
|
10673
10839
|
hasPattern: advanceTransitionBodyPattern !== null,
|
|
10674
|
-
isFinalOrSubmit:
|
|
10840
|
+
isFinalOrSubmit: isStepSubmitShaped({
|
|
10841
|
+
step,
|
|
10842
|
+
submitStep,
|
|
10843
|
+
isFinalStep,
|
|
10844
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
10845
|
+
}),
|
|
10675
10846
|
isAdvance: isAdvanceStep(step),
|
|
10676
10847
|
retryUrlChanged,
|
|
10677
10848
|
retryNetworkIsRealAdvance,
|
|
@@ -10730,31 +10901,63 @@ async function executeStepWithHealing(params) {
|
|
|
10730
10901
|
// step resolving onto an objectively submit-shaped control is
|
|
10731
10902
|
// treated identically to an explicitly flagged one everywhere this
|
|
10732
10903
|
// value is read below.
|
|
10733
|
-
const retrySubmitShaped =
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10904
|
+
const retrySubmitShaped = isStepSubmitShaped({
|
|
10905
|
+
step,
|
|
10906
|
+
submitStep,
|
|
10907
|
+
isFinalStep,
|
|
10908
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
10909
|
+
}) || retryResolvedElementIsSubmitShaped;
|
|
10910
|
+
const weakDomSignalsAllowed = ((!(isFinalStep && !stepDeclaredNonSubmit) &&
|
|
10911
|
+
!submitStep &&
|
|
10912
|
+
!retryResolvedElementIsSubmitShaped) ||
|
|
10738
10913
|
requireSubmitEndpoint) &&
|
|
10739
10914
|
!isCheckboxOrRadioIntentStep(step) &&
|
|
10740
10915
|
!clickTargetIsSelectionMarker;
|
|
10916
|
+
// `classifyPhantomClick`'s own `elementStateChanged` input is
|
|
10917
|
+
// UNGATED by `destinationPlausible` (phantom-click.ts ~220-229);
|
|
10918
|
+
// the primary attempt gates its credit paths (`domVerifiedForStep`,
|
|
10919
|
+
// `networkIsRealAdvance`) on destination itself. The n+16 fallback's
|
|
10920
|
+
// `retrySelectionStateChanged`, UNLIKE the primary technique's
|
|
10921
|
+
// `domVerified`, is partly driven by this fallback's OWN synthetic
|
|
10922
|
+
// `el.click()`/forced-checkbox probe rather than a genuine site
|
|
10923
|
+
// click handler — pre-gating it here (this call site only; the
|
|
10924
|
+
// primary technique's call above is untouched) closes the n+16-
|
|
10925
|
+
// specific path where `classifyPhantomClick`'s `"effective"`
|
|
10926
|
+
// verdict would otherwise re-credit a destination-implausible
|
|
10927
|
+
// click purely off `elementStateChanged`, bypassing the direct
|
|
10928
|
+
// `retrySelectionStateChanged`/`checkboxStateVerified` gates below.
|
|
10741
10929
|
const retryVerdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
10742
10930
|
actResultSuccess: record.actResultSuccess,
|
|
10743
10931
|
pre,
|
|
10744
10932
|
post: retryPost,
|
|
10745
10933
|
elementStateChanged: retrySelectionStateChanged,
|
|
10746
10934
|
isSubmitShapedStep: retrySubmitShaped,
|
|
10747
|
-
destinationPlausible:
|
|
10935
|
+
destinationPlausible: retryDestinationPlausible,
|
|
10748
10936
|
});
|
|
10937
|
+
// Destination-plausibility audit of every remaining disjunct in
|
|
10938
|
+
// this OR (the recon report's closing instruction, see
|
|
10939
|
+
// flow-runner.retry-fallback-remaining-disjuncts-destination-
|
|
10940
|
+
// audit.test.ts): `retryNetworkFired` is a bare request COUNT with
|
|
10941
|
+
// zero awareness of destination (a request can fire en route to
|
|
10942
|
+
// ANY page, including a wrong one); `checkboxStateVerified` forces
|
|
10943
|
+
// `.checked = true` on whatever element the xpath resolves to on
|
|
10944
|
+
// the CURRENTLY LOADED page, independent of whether the click
|
|
10945
|
+
// fallback navigated there correctly; `retrySelectionStateChanged`
|
|
10946
|
+
// reads a fingerprint for whatever element the xpath/selector
|
|
10947
|
+
// resolves to post-click, same independence from destination. All
|
|
10948
|
+
// three get the same `isPlausibleStepDestination` gate
|
|
10949
|
+
// `weakDomSignalsAllowed` already has (bugfix-002) — `retryUrlChanged`
|
|
10950
|
+
// already folds the same check in at its own computation above.
|
|
10749
10951
|
let retryVerified = !clickBlockedByDisabled &&
|
|
10750
10952
|
!clickBlockedByInvalid &&
|
|
10751
10953
|
!fallbackDomOnlyAdvance &&
|
|
10752
|
-
(retryNetworkFired ||
|
|
10954
|
+
((retryNetworkFired && retryDestinationPlausible) ||
|
|
10753
10955
|
retryUrlChanged ||
|
|
10754
|
-
checkboxStateVerified ||
|
|
10755
|
-
retrySelectionStateChanged ||
|
|
10956
|
+
(checkboxStateVerified && retryDestinationPlausible) ||
|
|
10957
|
+
(retrySelectionStateChanged && retryDestinationPlausible) ||
|
|
10756
10958
|
(!retrySubmitShaped && retryVerdict === "effective") ||
|
|
10757
10959
|
(weakDomSignalsAllowed &&
|
|
10960
|
+
retryDestinationPlausible &&
|
|
10758
10961
|
(retryHtmlDelta !== 0 || retryTextChanged || retryFormValueChanged)));
|
|
10759
10962
|
if (retryVerified) {
|
|
10760
10963
|
record.phantomClickVerdict = retryVerdict;
|
|
@@ -10797,7 +11000,11 @@ async function executeStepWithHealing(params) {
|
|
|
10797
11000
|
// flow-runner.overlay-hidden-target-trusted-click-acceptance.test.ts
|
|
10798
11001
|
// and flow-runner.evidence-table-alternating-verdict-regression.test.ts,
|
|
10799
11002
|
// both of which rely on that carve-out and must stay green).
|
|
10800
|
-
const retryDestinationUnconfirmed = isFinalStep &&
|
|
11003
|
+
const retryDestinationUnconfirmed = isFinalStep &&
|
|
11004
|
+
!submitStep &&
|
|
11005
|
+
!stepDeclaredNonSubmit &&
|
|
11006
|
+
retryUrlChanged &&
|
|
11007
|
+
!retryResolvedElementIsSubmitShaped;
|
|
10801
11008
|
if (retryVerified && retryDestinationUnconfirmed && !requireSubmitEndpoint) {
|
|
10802
11009
|
// Veto directly rather than routing through the judge below: no
|
|
10803
11010
|
// endpoint pattern and no submit-shaped element means there is
|
|
@@ -10885,7 +11092,18 @@ async function executeStepWithHealing(params) {
|
|
|
10885
11092
|
logger.info(`n+16 probe: step=${stepIndex + 1}/${totalSteps?.() ?? "?"} attempt=${attempt} el.click() fallback fired=${fired === true} kind=${probeResult.kind ?? "none"} checkboxStateVerified=${checkboxStateVerified} ancestorStillInvalid=${ancestorStillInvalid}; network=${retryNetworkFired} url=${retryUrlChanged} htmlDelta=${retryHtmlDelta} textChanged=${retryTextChanged} formValueChanged=${retryFormValueChanged} selectionStateChanged=${retrySelectionStateChanged} verified=${retryVerified} delivery=${n16DeliveryBranch}${n16TrustedClickFailureSuffix}`);
|
|
10886
11093
|
if (retryVerified) {
|
|
10887
11094
|
if (record.verifiedBy === null) {
|
|
10888
|
-
|
|
11095
|
+
// Mirror the same `retryDestinationPlausible` gate `retryVerified`
|
|
11096
|
+
// applies to `retryNetworkFired` above — otherwise a step verified
|
|
11097
|
+
// via `retryVerdict === "effective"` (or `weakDomSignalsAllowed`)
|
|
11098
|
+
// while the destination is implausible would still be labeled
|
|
11099
|
+
// "network" here off the raw, ungated flag, misreporting what
|
|
11100
|
+
// actually verified it and desyncing `verifiedBy` consumers (e.g.
|
|
11101
|
+
// the `priorDomVerified` check below) from the real gate.
|
|
11102
|
+
record.verifiedBy = retryUrlChanged
|
|
11103
|
+
? "url"
|
|
11104
|
+
: retryNetworkFired && retryDestinationPlausible
|
|
11105
|
+
? "network"
|
|
11106
|
+
: "dom";
|
|
10889
11107
|
}
|
|
10890
11108
|
record.post = retryPost;
|
|
10891
11109
|
attempts.push(record);
|
|
@@ -10957,7 +11175,12 @@ async function executeStepWithHealing(params) {
|
|
|
10957
11175
|
// dumps in a 2026-06-10 survey had the paired touched+dirty + visible
|
|
10958
11176
|
// error text pattern with 3 distinct rejection messages.
|
|
10959
11177
|
if (record.resolvedMethod === "click" &&
|
|
10960
|
-
(
|
|
11178
|
+
isStepSubmitShaped({
|
|
11179
|
+
step,
|
|
11180
|
+
submitStep,
|
|
11181
|
+
isFinalStep,
|
|
11182
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
11183
|
+
})) {
|
|
10961
11184
|
const live = await extractLivePageFormEvidence(page, frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), {
|
|
10962
11185
|
client: anthropic,
|
|
10963
11186
|
knownErrorClassPrefixes,
|
|
@@ -11018,7 +11241,12 @@ async function executeStepWithHealing(params) {
|
|
|
11018
11241
|
// (triedSelectors), which is empty on an "unresolved" verdict, so a
|
|
11019
11242
|
// non-submit step with that verdict falls through to the normal
|
|
11020
11243
|
// ladder instead of escalating.
|
|
11021
|
-
const escalationTarget =
|
|
11244
|
+
const escalationTarget = isStepSubmitShaped({
|
|
11245
|
+
step,
|
|
11246
|
+
submitStep,
|
|
11247
|
+
isFinalStep,
|
|
11248
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
11249
|
+
})
|
|
11022
11250
|
? "escalating attempt 2 to deep-submit-locator"
|
|
11023
11251
|
: phantomClickAfterAttempt1
|
|
11024
11252
|
? "non-submit step — escalating attempt 2 to trusted-click-retry (trusted CDP click on the resolved target)"
|
|
@@ -11030,7 +11258,12 @@ async function executeStepWithHealing(params) {
|
|
|
11030
11258
|
// Treat the canonical submit click as "final" for this predicate
|
|
11031
11259
|
// even when it lives mid-flow. See requireSubmitEndpoint derivation
|
|
11032
11260
|
// above for the same gate-widening rationale.
|
|
11033
|
-
isFinalStep:
|
|
11261
|
+
isFinalStep: isStepSubmitShaped({
|
|
11262
|
+
step,
|
|
11263
|
+
submitStep,
|
|
11264
|
+
isFinalStep,
|
|
11265
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
11266
|
+
}),
|
|
11034
11267
|
requireSubmitEndpoint,
|
|
11035
11268
|
resolvedMethod: record.resolvedMethod,
|
|
11036
11269
|
effectSignals,
|
|
@@ -11050,7 +11283,12 @@ async function executeStepWithHealing(params) {
|
|
|
11050
11283
|
// can reorder a later step forward — instead of burning the cascade.
|
|
11051
11284
|
const advanceStalled = isAdvanceStalled({
|
|
11052
11285
|
isAdvance: isAdvanceStep(step),
|
|
11053
|
-
isFinalOrSubmit:
|
|
11286
|
+
isFinalOrSubmit: isStepSubmitShaped({
|
|
11287
|
+
step,
|
|
11288
|
+
submitStep,
|
|
11289
|
+
isFinalStep,
|
|
11290
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
11291
|
+
}),
|
|
11054
11292
|
hasPattern: advanceTransitionBodyPattern !== null,
|
|
11055
11293
|
clickFired: record.resolvedMethod === "click" && record.actResultSuccess === true,
|
|
11056
11294
|
networkFired,
|
|
@@ -11077,8 +11315,13 @@ async function executeStepWithHealing(params) {
|
|
|
11077
11315
|
// firing on a non-submit step could complete it by clicking an unrelated
|
|
11078
11316
|
// submit control in an unrelated same-origin child iframe.
|
|
11079
11317
|
if (!frameTarget?.declaredFrameSelector &&
|
|
11080
|
-
(
|
|
11081
|
-
|
|
11318
|
+
isStepSubmitShaped({
|
|
11319
|
+
step,
|
|
11320
|
+
submitStep,
|
|
11321
|
+
isFinalStep,
|
|
11322
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
11323
|
+
})) {
|
|
11324
|
+
const fallbackTarget = await probeChildFrameSubmitFallback({ page, signalCounter, step });
|
|
11082
11325
|
if (fallbackTarget) {
|
|
11083
11326
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} cascade-exhausted: no candidate resolvable in the declared/main frame, but a same-origin child iframe surfaced a submit-shaped candidate — clicked and verified there instead of failing`);
|
|
11084
11327
|
trajectory?.push({ stepIndex, verifiedBy: "dom" });
|
|
@@ -11110,7 +11353,12 @@ async function executeStepWithHealing(params) {
|
|
|
11110
11353
|
const unfocusedObserve = cascadeExhaustObservedUnfocused.length === 0 && frameTarget?.frame
|
|
11111
11354
|
? await deepLocatorCandidatesAsActions(page, frameTarget)
|
|
11112
11355
|
: cascadeExhaustObservedUnfocused;
|
|
11113
|
-
const targetResolutionDiagnostic =
|
|
11356
|
+
const targetResolutionDiagnostic = isStepSubmitShaped({
|
|
11357
|
+
step,
|
|
11358
|
+
submitStep,
|
|
11359
|
+
isFinalStep,
|
|
11360
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
11361
|
+
})
|
|
11114
11362
|
? await (0, target_resolution_diagnostic_1.captureTargetResolutionDiagnosticSnapshot)(frameTarget, page, lastRankedSubmitCandidates)
|
|
11115
11363
|
: null;
|
|
11116
11364
|
const dumpPath = onStepFailure?.({
|
|
@@ -11255,6 +11503,7 @@ async function runHealingFlow(deps) {
|
|
|
11255
11503
|
stepPromise = executeStepWithHealing({
|
|
11256
11504
|
stagehand,
|
|
11257
11505
|
page,
|
|
11506
|
+
abortSignal: deps.abortSignal,
|
|
11258
11507
|
step: s.instruction,
|
|
11259
11508
|
optional: s.optional,
|
|
11260
11509
|
upload: s.upload,
|