@enricai/barnacle 1.12.54 → 1.12.56
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/api/schemas/common.d.ts +28 -0
- package/dist/api/schemas/common.d.ts.map +1 -1
- package/dist/api/schemas/common.js +51 -0
- package/dist/api/schemas/common.js.map +1 -1
- package/dist/plugins/config-plugin.d.ts +8 -0
- package/dist/plugins/config-plugin.d.ts.map +1 -1
- package/dist/plugins/config-plugin.js +56 -3
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/plugins/json-schema-to-zod.d.ts +6 -3
- package/dist/plugins/json-schema-to-zod.d.ts.map +1 -1
- package/dist/plugins/json-schema-to-zod.js +18 -6
- package/dist/plugins/json-schema-to-zod.js.map +1 -1
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +7 -1
- package/dist/plugins/loader.js.map +1 -1
- package/dist/recon/capture-filters.d.ts +2 -55
- package/dist/recon/capture-filters.d.ts.map +1 -1
- package/dist/recon/capture-filters.js +449 -4
- package/dist/recon/capture-filters.js.map +1 -1
- package/dist/recon/load-value-constraints.d.ts +22 -0
- package/dist/recon/load-value-constraints.d.ts.map +1 -0
- package/dist/recon/load-value-constraints.js +61 -0
- package/dist/recon/load-value-constraints.js.map +1 -0
- package/dist/recon/value-constraints.d.ts +46 -0
- package/dist/recon/value-constraints.d.ts.map +1 -0
- package/dist/recon/value-constraints.js +31 -0
- package/dist/recon/value-constraints.js.map +1 -0
- package/dist/scraper/errors.d.ts +12 -0
- package/dist/scraper/errors.d.ts.map +1 -1
- package/dist/scraper/errors.js +21 -1
- package/dist/scraper/errors.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts +87 -23
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +440 -71
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/http-client.d.ts +11 -0
- package/dist/scraper/http-client.d.ts.map +1 -1
- package/dist/scraper/http-client.js +96 -2
- package/dist/scraper/http-client.js.map +1 -1
- package/dist/scraper/phantom-click.d.ts +2 -2
- package/dist/scraper/phantom-click.d.ts.map +1 -1
- package/dist/scraper/phantom-click.js +6 -5
- package/dist/scraper/phantom-click.js.map +1 -1
- package/dist/scraper/submit-control.d.ts +11 -6
- package/dist/scraper/submit-control.d.ts.map +1 -1
- package/dist/scraper/submit-control.js +38 -7
- package/dist/scraper/submit-control.js.map +1 -1
- package/dist/scripts/recon-browser.d.ts +45 -8
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +145 -16
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +65 -4
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +389 -51
- package/dist/scripts/recon-generate.js.map +1 -1
- package/dist/site-plugin.d.ts +9 -0
- package/dist/site-plugin.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1207,29 +1207,42 @@ function isCheckboxOrRadioIntentStep(instruction) {
|
|
|
1207
1207
|
* Whether `snapshotPage` should build the per-element selection baseline
|
|
1208
1208
|
* (`StepSnapshot.selectionStateByXpath`) for this step — i.e. whether
|
|
1209
1209
|
* `verifyDomEffect`'s element-scoped click read-back is allowed to credit it.
|
|
1210
|
-
* True ONLY for a field-answer/selection step: a submit
|
|
1211
|
-
*
|
|
1210
|
+
* True ONLY for a field-answer/selection step: a submit or an advance step
|
|
1211
|
+
* must be verified by a real network/URL transition, so its own
|
|
1212
1212
|
* self-toggling button (a submit flipping to a loading/pressed class, a "Next"
|
|
1213
1213
|
* flipping `aria-pressed`) must never earn an element-scoped credit — matching
|
|
1214
|
-
* the `!submit`/`!
|
|
1215
|
-
*
|
|
1214
|
+
* the `!submit`/`!advance` exclusions the former `isClickStateToggleVerified`
|
|
1215
|
+
* gate enforced. A benign final step (one that is NOT itself the flagged
|
|
1216
|
+
* submit step) still captures — `flowHasSubmitSemantics` describes the FLOW,
|
|
1217
|
+
* not this step, so it must not veto capture on its own; `submitStep` alone
|
|
1218
|
+
* (authoritative per {@link flowHasSubmitSemantics}) identifies the step that
|
|
1219
|
+
* actually needs network/URL verification. Pure + exported so the gate the
|
|
1216
1220
|
* cascade depends on is unit-testable, not buried in `executeStepWithHealing`.
|
|
1217
1221
|
*/
|
|
1218
1222
|
function shouldCaptureSelectionState(params) {
|
|
1219
|
-
const { step,
|
|
1220
|
-
return !(submitStep ||
|
|
1221
|
-
}
|
|
1222
|
-
/**
|
|
1223
|
-
* Whether
|
|
1224
|
-
* `submitStep: true`,
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
*
|
|
1228
|
-
*
|
|
1223
|
+
const { step, submitStep } = params;
|
|
1224
|
+
return !(submitStep || isAdvanceStep(step));
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Whether the flow's FINAL step carries submit semantics. A read-only flow
|
|
1228
|
+
* (no step flagged `submitStep: true`, no `submitEndpointPattern`, no
|
|
1229
|
+
* `requireSubmitEndpointMatch`) has no submit shape anywhere, so its final
|
|
1230
|
+
* step is an ordinary read/click. When some step IS explicitly flagged
|
|
1231
|
+
* `submitStep: true`, that flag is authoritative over inference — the final
|
|
1232
|
+
* step only counts as submit-shaped if it is itself the flagged step, not
|
|
1233
|
+
* merely because an earlier, unrelated step claims the submit role. Only
|
|
1234
|
+
* when NO step carries an explicit flag do we fall back to inferring
|
|
1235
|
+
* submit-shape onto the final step from `submitEndpointPattern` /
|
|
1236
|
+
* `requireSubmitEndpointMatch` (the documented fallback for self-heal-
|
|
1237
|
+
* appended final steps that never got the flag set). Pure + exported so
|
|
1238
|
+
* callers can stop inferring submit-shape from `isFinalStep` alone.
|
|
1229
1239
|
*/
|
|
1230
1240
|
function flowHasSubmitSemantics(params) {
|
|
1231
1241
|
const { steps, submitEndpointPattern, requireSubmitEndpointMatch } = params;
|
|
1232
|
-
|
|
1242
|
+
const hasExplicitSubmitStep = steps.some((s) => s.submitStep);
|
|
1243
|
+
return hasExplicitSubmitStep
|
|
1244
|
+
? (steps[steps.length - 1]?.submitStep ?? false)
|
|
1245
|
+
: submitEndpointPattern !== null || requireSubmitEndpointMatch;
|
|
1233
1246
|
}
|
|
1234
1247
|
/**
|
|
1235
1248
|
* Whether a flow should be WARNed that its DOM-only advance guard is disarmed.
|
|
@@ -1471,10 +1484,13 @@ function isDomOnlyAdvanceVerified(params) {
|
|
|
1471
1484
|
* (`VIEW_SWAP_MIN_BYTES` env var) lets a deployment lower the threshold
|
|
1472
1485
|
* without regressing the shared default for other sites.
|
|
1473
1486
|
*
|
|
1474
|
-
* **Scope guards:** Excludes
|
|
1475
|
-
* per isSubmitRevealedInvalid + LLM submit
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
1487
|
+
* **Scope guards:** Excludes steps explicitly flagged `submitStep: true`
|
|
1488
|
+
* (those require real network per isSubmitRevealedInvalid + LLM submit
|
|
1489
|
+
* judge — trusting only the step's own explicit flag, never the flow-level
|
|
1490
|
+
* `flowHasSubmitSemantics` inference: an unflagged final step may genuinely
|
|
1491
|
+
* be a legitimate client-side view swap) and advance-pattern steps (those
|
|
1492
|
+
* require real transition per isDomOnlyAdvanceVerified/isAdvanceStalled to
|
|
1493
|
+
* avoid wizard-ATS autosave-vs-transition ambiguity).
|
|
1478
1494
|
*
|
|
1479
1495
|
* **Small-delta reveal credit:** a sub-section unhiding within an
|
|
1480
1496
|
* already-loaded page (e.g. a validation-triggered "Work History requirement"
|
|
@@ -1505,10 +1521,16 @@ function isDomOnlyAdvanceVerified(params) {
|
|
|
1505
1521
|
function isClickViewSwapVerified(params) {
|
|
1506
1522
|
const VIEW_SWAP_MIN_BYTES = config_1.config.scraper.viewSwapMinBytesThreshold;
|
|
1507
1523
|
const VIEW_SWAP_REVEAL_MIN_BYTES = config_1.config.scraper.viewSwapRevealMinBytesThreshold;
|
|
1508
|
-
const { resolvedAction,
|
|
1524
|
+
const { resolvedAction, submitStep, isAdvanceWithPattern, networkDelta, bytesDelta, textChanged, invalidMarkerDelta = 0, } = params;
|
|
1509
1525
|
if (resolvedAction?.method !== "click")
|
|
1510
1526
|
return false;
|
|
1511
|
-
|
|
1527
|
+
// Only the step's own explicit submitStep flag identifies the step that
|
|
1528
|
+
// actually needs network/URL verification — mirrors the submit-judge
|
|
1529
|
+
// gate's hasSubmitTransitionSignal discipline (see 5763ac2). Inferring
|
|
1530
|
+
// submit-shape from isFinalStep && flowHasSubmitSemantics falsely vetoes
|
|
1531
|
+
// an inferred final same-page toggle step that was never going to receive
|
|
1532
|
+
// a real network/URL transition, leaving it structurally unverifiable.
|
|
1533
|
+
if (submitStep)
|
|
1512
1534
|
return false;
|
|
1513
1535
|
if (isAdvanceWithPattern)
|
|
1514
1536
|
return false;
|
|
@@ -1516,9 +1538,9 @@ function isClickViewSwapVerified(params) {
|
|
|
1516
1538
|
return false;
|
|
1517
1539
|
if (invalidMarkerDelta > 0)
|
|
1518
1540
|
return false;
|
|
1519
|
-
if (bytesDelta >= VIEW_SWAP_MIN_BYTES)
|
|
1541
|
+
if (Math.abs(bytesDelta) >= VIEW_SWAP_MIN_BYTES)
|
|
1520
1542
|
return true;
|
|
1521
|
-
return textChanged && bytesDelta >= VIEW_SWAP_REVEAL_MIN_BYTES;
|
|
1543
|
+
return textChanged && Math.abs(bytesDelta) >= VIEW_SWAP_REVEAL_MIN_BYTES;
|
|
1522
1544
|
}
|
|
1523
1545
|
/**
|
|
1524
1546
|
* Reads the "N settings/items selected" running count that multi-select wizard
|
|
@@ -4564,6 +4586,51 @@ async function waitForCaptchaNavigation(params) {
|
|
|
4564
4586
|
}
|
|
4565
4587
|
return false;
|
|
4566
4588
|
}
|
|
4589
|
+
/**
|
|
4590
|
+
* Stagehand's `StagehandEvalError` message when CDP's `Runtime.evaluate`
|
|
4591
|
+
* reports an exception with no `exception.description` to fall back on:
|
|
4592
|
+
* `exceptionDetails.text` is a content-free classification string ("Uncaught",
|
|
4593
|
+
* "Uncaught (in promise)") Chrome sets on every thrown/rejected evaluate
|
|
4594
|
+
* regardless of what was actually thrown, and Stagehand's own placeholder
|
|
4595
|
+
* ("Evaluation failed") when neither field is present at all. None of these
|
|
4596
|
+
* carry any evidence about what happened in-page — they fire for a clean
|
|
4597
|
+
* teardown-adjacent throw exactly as often as a genuine bug — so a rejection
|
|
4598
|
+
* whose full message is exactly one of these (after bugfix-001's page.js/
|
|
4599
|
+
* frame.js patch already preferred the real `description` when Chrome
|
|
4600
|
+
* provided one) is content-free, not evidence of a real exception.
|
|
4601
|
+
*/
|
|
4602
|
+
const GENERIC_CONTENT_FREE_EVAL_REJECTION_MESSAGES = new Set([
|
|
4603
|
+
"stagehandevalerror: uncaught",
|
|
4604
|
+
"stagehandevalerror: uncaught (in promise)",
|
|
4605
|
+
"stagehandevalerror: evaluation failed",
|
|
4606
|
+
]);
|
|
4607
|
+
/**
|
|
4608
|
+
* Distinguishes a navigating evaluate's expected context-teardown rejection
|
|
4609
|
+
* (the callback/submit dispatch it wraps synchronously navigated the frame,
|
|
4610
|
+
* tearing down the execution context before a return value marshals) — or a
|
|
4611
|
+
* rejection whose message carries no diagnostic content beyond Stagehand's
|
|
4612
|
+
* generic classification wrapper (see
|
|
4613
|
+
* {@link GENERIC_CONTENT_FREE_EVAL_REJECTION_MESSAGES}) — from a genuine
|
|
4614
|
+
* in-page exception with real diagnostic detail: a thrown `TypeError`, a
|
|
4615
|
+
* rejected fetch, or any other real failure inside the site's own
|
|
4616
|
+
* callback/submit logic. All three are indistinguishable to a blanket
|
|
4617
|
+
* `.catch(() => undefined)`, which is exactly why a genuinely
|
|
4618
|
+
* thrown-and-swallowed exception during the callback invoke used to log
|
|
4619
|
+
* `callbackDiscovered=true`/`injected=true` (both computed BEFORE the
|
|
4620
|
+
* invoke) while never actually dispatching anything. Only a context-teardown
|
|
4621
|
+
* or content-free rejection is safe to discard silently; this is what lets
|
|
4622
|
+
* callers keep doing that for those cases while surfacing genuine detail.
|
|
4623
|
+
*/
|
|
4624
|
+
function isNavigatingEvaluateRejection(error) {
|
|
4625
|
+
const message = (0, errors_1.toErrorMessage)(error).toLowerCase();
|
|
4626
|
+
return (message.includes("execution context was destroyed") ||
|
|
4627
|
+
message.includes("execution context is not available") ||
|
|
4628
|
+
message.includes("cannot find context with specified id") ||
|
|
4629
|
+
message.includes("target closed") ||
|
|
4630
|
+
message.includes("frame was detached") ||
|
|
4631
|
+
message.includes("no frame for given id found") ||
|
|
4632
|
+
GENERIC_CONTENT_FREE_EVAL_REJECTION_MESSAGES.has(message.trim()));
|
|
4633
|
+
}
|
|
4567
4634
|
/**
|
|
4568
4635
|
* Shared in-page discovery of the widget's callback, used identically by
|
|
4569
4636
|
* `injectCaptchaTokenAndSubmit`'s precheck (to decide the branch) and its
|
|
@@ -4616,6 +4683,10 @@ function setResponseFieldAndInvokeCallbackExprSrc(responseField, token) {
|
|
|
4616
4683
|
} else if (field) {
|
|
4617
4684
|
field.value = token;
|
|
4618
4685
|
}
|
|
4686
|
+
if (field) {
|
|
4687
|
+
field.dispatchEvent(new Event("input", { bubbles: true }));
|
|
4688
|
+
field.dispatchEvent(new Event("change", { bubbles: true }));
|
|
4689
|
+
}
|
|
4619
4690
|
found.invoke(token);
|
|
4620
4691
|
`;
|
|
4621
4692
|
}
|
|
@@ -4707,8 +4778,8 @@ async function injectCaptchaTokenAndSubmit(target, token, responseField = "h-cap
|
|
|
4707
4778
|
// case: it's an anonymous closure living in the registry, not a named
|
|
4708
4779
|
// `window` global, so it cannot be marshalled back from the precheck's
|
|
4709
4780
|
// evaluate call.
|
|
4710
|
-
await target.evaluate(invokeCallbackExpr).
|
|
4711
|
-
return { injected, hasForm, callbackDiscovered };
|
|
4781
|
+
const callbackInvokeError = await target.evaluate(invokeCallbackExpr).then(() => undefined, (err) => (isNavigatingEvaluateRejection(err) ? undefined : (0, errors_1.toErrorMessage)(err)));
|
|
4782
|
+
return { injected, hasForm, callbackDiscovered, callbackInvokeError };
|
|
4712
4783
|
}
|
|
4713
4784
|
// Belt-and-suspenders: this frame missed session.ts's page-init-script
|
|
4714
4785
|
// install and bugfix-001's per-frame attach install (it either attached
|
|
@@ -4739,8 +4810,8 @@ async function injectCaptchaTokenAndSubmit(target, token, responseField = "h-cap
|
|
|
4739
4810
|
// the non-late invoke above: the callback is free to submit the form
|
|
4740
4811
|
// synchronously off the field value that's now present, tearing down the
|
|
4741
4812
|
// execution context before a return value marshals.
|
|
4742
|
-
await target.evaluate(invokeLateCallbackExpr).
|
|
4743
|
-
return { injected, hasForm, callbackDiscovered: true };
|
|
4813
|
+
const callbackInvokeError = await target.evaluate(invokeLateCallbackExpr).then(() => undefined, (err) => (isNavigatingEvaluateRejection(err) ? undefined : (0, errors_1.toErrorMessage)(err)));
|
|
4814
|
+
return { injected, hasForm, callbackDiscovered: true, callbackInvokeError };
|
|
4744
4815
|
}
|
|
4745
4816
|
const setValueExpr = `(() => {
|
|
4746
4817
|
const responseField = ${JSON.stringify(responseField)};
|
|
@@ -4773,7 +4844,10 @@ async function injectCaptchaTokenAndSubmit(target, token, responseField = "h-cap
|
|
|
4773
4844
|
const dispatchChangeExpr = `(() => {
|
|
4774
4845
|
const responseField = ${JSON.stringify(responseField)};
|
|
4775
4846
|
const field = document.querySelector('[name="' + responseField + '"]');
|
|
4776
|
-
if (field)
|
|
4847
|
+
if (field) {
|
|
4848
|
+
field.dispatchEvent(new Event("input", { bubbles: true }));
|
|
4849
|
+
field.dispatchEvent(new Event("change", { bubbles: true }));
|
|
4850
|
+
}
|
|
4777
4851
|
})()`;
|
|
4778
4852
|
// Dispatching "change" can trigger a page's own submit-on-token callback,
|
|
4779
4853
|
// which navigates the frame synchronously from within this call and tears
|
|
@@ -4788,24 +4862,123 @@ async function injectCaptchaTokenAndSubmit(target, token, responseField = "h-cap
|
|
|
4788
4862
|
* used only when the caller's own transition poll observed no advance after
|
|
4789
4863
|
* the inject, so the widget's callback (if any) evidently didn't submit for
|
|
4790
4864
|
* us. Kept separate from the inject primitive so the caller can gate its use
|
|
4791
|
-
* on an observed transition rather than firing it unconditionally.
|
|
4792
|
-
*
|
|
4793
|
-
*
|
|
4794
|
-
*
|
|
4795
|
-
*
|
|
4796
|
-
|
|
4797
|
-
|
|
4865
|
+
* on an observed transition rather than firing it unconditionally.
|
|
4866
|
+
*
|
|
4867
|
+
* Dispatches a real, framework-observed click via the same
|
|
4868
|
+
* {@link buildRankSubmitCandidatesExpr}/{@link buildClickByDeepIndexExpr}
|
|
4869
|
+
* primitives the phantom-click cascade uses, rather than a synthetic
|
|
4870
|
+
* `form.requestSubmit()`/`form.submit()` call: on a React/SPA site the
|
|
4871
|
+
* submit button's `onClick` handler owns the actual submission (validation,
|
|
4872
|
+
* XHR/fetch dispatch, etc.), and `<form>`-level APIs never fire it, so
|
|
4873
|
+
* "success" here would silently produce zero site-host HTTP traffic for
|
|
4874
|
+
* {@link waitForCaptchaNavigation} to observe. Only when no submit-shaped
|
|
4875
|
+
* control can be found or clicked does this fall back to the form-level API
|
|
4876
|
+
* — a plain server-rendered form with no button-bound JS handler still needs
|
|
4877
|
+
* `requestSubmit()`/`submit()` to actually navigate.
|
|
4878
|
+
*
|
|
4879
|
+
* Resolves the form to submit by name first (the response field's own
|
|
4880
|
+
* closest form), then falls back to the sitekey-anchored form (or the sole
|
|
4881
|
+
* form on the page) the same way {@link injectCaptchaTokenAndSubmit} resolves
|
|
4882
|
+
* its target: an invisible/callback-only widget never creates a named
|
|
4883
|
+
* response field, so a named-field-only lookup here would silently no-op and
|
|
4884
|
+
* no site-host traffic would ever follow the clean callback. Resolves to
|
|
4885
|
+
* whether a form was actually found and acted on; a true no-op is reported
|
|
4886
|
+
* only when the page has no form at all to resolve.
|
|
4887
|
+
*
|
|
4888
|
+
* The click-dispatch and form-level submit evals both tolerate the expected
|
|
4889
|
+
* context-teardown rejection of a click/submit that navigated the frame —
|
|
4890
|
+
* see {@link isNavigatingEvaluateRejection} — but rethrow anything else, so a
|
|
4891
|
+
* genuine in-page exception during the dispatch propagates to the caller
|
|
4892
|
+
* instead of silently resolving as a successful submit.
|
|
4893
|
+
*
|
|
4894
|
+
* The ranked-candidate click is verified the same way the main cascade's
|
|
4895
|
+
* `deep-submit-locator` verifies its top pick (see `executeStepWithHealing`
|
|
4896
|
+
* at flow-runner.ts:10374-10428): a shadow-root/web-component control can
|
|
4897
|
+
* report `clicked: true` while wiring no real handler, so a bare
|
|
4898
|
+
* `clicked: true` here would let a phantom click masquerade as a resolved
|
|
4899
|
+
* submit and this fallback would return `true` with zero site-host traffic
|
|
4900
|
+
* ever following. When `verification` is supplied, snapshots pre/post via
|
|
4901
|
+
* `snapshotPage` and classifies with `classifyPhantomClick({isSubmitShapedStep:
|
|
4902
|
+
* true})`; a `phantom` verdict retries the runner-up candidate once (mirroring
|
|
4903
|
+
* the cascade's single runner-up retry) before falling through to the
|
|
4904
|
+
* form-level submit below. `verification` is optional so callers that only
|
|
4905
|
+
* need the non-polling, non-verifying original contract (and existing tests
|
|
4906
|
+
* exercising the rank/click primitives directly) are unaffected.
|
|
4907
|
+
*/
|
|
4908
|
+
async function submitCaptchaGatedForm(target, responseField = "h-captcha-response", verification) {
|
|
4909
|
+
const findFormExpr = `(() => {
|
|
4910
|
+
const responseField = ${JSON.stringify(responseField)};
|
|
4911
|
+
const field = document.querySelector('[name="' + responseField + '"]');
|
|
4912
|
+
const fieldForm = field ? field.closest("form") : null;
|
|
4913
|
+
if (fieldForm) return true;
|
|
4914
|
+
const forms = Array.from(document.querySelectorAll("form"));
|
|
4915
|
+
const sitekeyForm = forms.find((candidate) => candidate.querySelector("[data-sitekey]")) ?? forms[0];
|
|
4916
|
+
return Boolean(sitekeyForm);
|
|
4917
|
+
})()`;
|
|
4918
|
+
const found = await target.evaluate(findFormExpr).catch(() => false);
|
|
4919
|
+
if (!found)
|
|
4920
|
+
return false;
|
|
4921
|
+
const rankResult = await target
|
|
4922
|
+
.evaluate((0, submit_control_1.buildRankSubmitCandidatesExpr)())
|
|
4923
|
+
.catch(() => []);
|
|
4924
|
+
const ranked = Array.isArray(rankResult) ? rankResult : [];
|
|
4925
|
+
// biome-ignore lint/style/noNonNullAssertion: guarded by the length check
|
|
4926
|
+
const top = ranked.length > 0 ? ranked[0] : null;
|
|
4927
|
+
if (top) {
|
|
4928
|
+
const pre = verification
|
|
4929
|
+
? await snapshotPage(target, verification.signalCounter, verification.page)
|
|
4930
|
+
: null;
|
|
4931
|
+
const clickResult = await target
|
|
4932
|
+
.evaluate((0, submit_control_1.buildClickByDeepIndexExpr)(top.deepIndex))
|
|
4933
|
+
.catch((err) => {
|
|
4934
|
+
if (isNavigatingEvaluateRejection(err))
|
|
4935
|
+
return { clicked: false };
|
|
4936
|
+
throw err;
|
|
4937
|
+
});
|
|
4938
|
+
if (clickResult.clicked) {
|
|
4939
|
+
if (!pre || !verification)
|
|
4940
|
+
return true;
|
|
4941
|
+
const post = await snapshotPage(target, verification.signalCounter, verification.page);
|
|
4942
|
+
const verdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
4943
|
+
actResultSuccess: true,
|
|
4944
|
+
pre,
|
|
4945
|
+
post,
|
|
4946
|
+
isSubmitShapedStep: true,
|
|
4947
|
+
});
|
|
4948
|
+
if (verdict !== "phantom")
|
|
4949
|
+
return true;
|
|
4950
|
+
const runnerUp = ranked[1];
|
|
4951
|
+
if (runnerUp) {
|
|
4952
|
+
const runnerUpClickResult = await target
|
|
4953
|
+
.evaluate((0, submit_control_1.buildClickByDeepIndexExpr)(runnerUp.deepIndex))
|
|
4954
|
+
.catch((err) => {
|
|
4955
|
+
if (isNavigatingEvaluateRejection(err))
|
|
4956
|
+
return { clicked: false };
|
|
4957
|
+
throw err;
|
|
4958
|
+
});
|
|
4959
|
+
if (runnerUpClickResult.clicked)
|
|
4960
|
+
return true;
|
|
4961
|
+
}
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4798
4964
|
const submitExpr = `(() => {
|
|
4799
4965
|
const responseField = ${JSON.stringify(responseField)};
|
|
4800
4966
|
const field = document.querySelector('[name="' + responseField + '"]');
|
|
4801
|
-
const
|
|
4967
|
+
const fieldForm = field ? field.closest("form") : null;
|
|
4968
|
+
const forms = Array.from(document.querySelectorAll("form"));
|
|
4969
|
+
const form = fieldForm ?? forms.find((candidate) => candidate.querySelector("[data-sitekey]")) ?? forms[0];
|
|
4802
4970
|
if (form) (form.requestSubmit ? form.requestSubmit() : form.submit());
|
|
4803
4971
|
})()`;
|
|
4804
4972
|
// form.submit() navigates the frame synchronously, tearing down the execution
|
|
4805
4973
|
// context before Runtime.evaluate can marshal a return value for this call —
|
|
4806
4974
|
// that rejection is the expected outcome of a navigating evaluate, not a
|
|
4807
4975
|
// real failure, so it's discarded here rather than awaited for a result.
|
|
4808
|
-
await target.evaluate(submitExpr).catch(() =>
|
|
4976
|
+
await target.evaluate(submitExpr).catch((err) => {
|
|
4977
|
+
if (isNavigatingEvaluateRejection(err))
|
|
4978
|
+
return undefined;
|
|
4979
|
+
throw err;
|
|
4980
|
+
});
|
|
4981
|
+
return true;
|
|
4809
4982
|
}
|
|
4810
4983
|
/**
|
|
4811
4984
|
* Site-agnostic select primitive: answer a native `<select>` dropdown by
|
|
@@ -7759,7 +7932,7 @@ async function executeStepWithHealing(params) {
|
|
|
7759
7932
|
// instead of silently falling through as if unverified-but-passing.
|
|
7760
7933
|
const isSubmitOrFinalStep = submitStep || (isFinalStep && flowHasSubmitSemanticsFlag);
|
|
7761
7934
|
if (captchaGated && isSubmitOrFinalStep) {
|
|
7762
|
-
|
|
7935
|
+
let captchaTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
7763
7936
|
const sitekeyProbeExpr = `(() => {
|
|
7764
7937
|
const el = document.querySelector("[data-sitekey]");
|
|
7765
7938
|
if (!el) return { siteKey: null, isInvisible: false };
|
|
@@ -7787,26 +7960,56 @@ async function executeStepWithHealing(params) {
|
|
|
7787
7960
|
// no-nav case is the same underlying race from the other side.
|
|
7788
7961
|
for (let captchaAttempt = 1; captchaAttempt <= CAPTCHA_REGISTRY_RETRY_ATTEMPTS; captchaAttempt++) {
|
|
7789
7962
|
if (captchaAttempt > 1) {
|
|
7963
|
+
// Re-resolve the target frame before every retry, mirroring the
|
|
7964
|
+
// main step loop's per-step `resolveFrameTarget` call: a
|
|
7965
|
+
// navigation or detach triggered by the prior attempt (its solve's
|
|
7966
|
+
// ~120s wait, its inject, or its submit) can leave `captchaTarget`
|
|
7967
|
+
// bound to a torn-down frame, and every evaluate below would
|
|
7968
|
+
// otherwise silently keep operating on that stale target for the
|
|
7969
|
+
// rest of the retries. A target with no declared frame selector
|
|
7970
|
+
// (main-frame flows) resolves this immediately with zero polling.
|
|
7971
|
+
captchaTarget = await (0, frame_target_1.resolveFrameTarget)(page, captchaTarget.frameSelector ?? captchaTarget.declaredFrameSelector ?? null);
|
|
7790
7972
|
// Re-assert the capture install in the target frame before
|
|
7791
7973
|
// retrying: a late-attaching registry may not exist yet on the
|
|
7792
7974
|
// first attempt, and re-running this idempotent install gives a
|
|
7793
7975
|
// widget that renders on demand another chance to be caught.
|
|
7794
7976
|
await captchaTarget.evaluate((0, captcha_callback_capture_1.buildHcaptchaCallbackCaptureScript)()).catch(() => undefined);
|
|
7795
7977
|
}
|
|
7796
|
-
const
|
|
7978
|
+
const solveResult = await (0, captcha_solver_1.solveCaptcha)({
|
|
7797
7979
|
type: "hcaptcha",
|
|
7798
7980
|
siteKey,
|
|
7799
7981
|
pageUrl,
|
|
7800
7982
|
isInvisible,
|
|
7801
7983
|
userAgent,
|
|
7802
7984
|
proxy: sessionProxy ?? undefined,
|
|
7803
|
-
})
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7985
|
+
})
|
|
7986
|
+
.then((solved) => ({ ok: true, solved }))
|
|
7987
|
+
.catch((err) => ({ ok: false, err }));
|
|
7988
|
+
if (!solveResult.ok) {
|
|
7989
|
+
const attemptsRemain = captchaAttempt < CAPTCHA_REGISTRY_RETRY_ATTEMPTS;
|
|
7990
|
+
logger.error(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: solve failed on attempt ${captchaAttempt}/${CAPTCHA_REGISTRY_RETRY_ATTEMPTS} (${(0, errors_1.toErrorMessage)(solveResult.err)}); ${attemptsRemain ? "retrying" : "failing the step rather than silently proceeding"}`);
|
|
7991
|
+
if (attemptsRemain)
|
|
7992
|
+
continue;
|
|
7993
|
+
throw solveResult.err;
|
|
7994
|
+
}
|
|
7995
|
+
const solved = solveResult.solved;
|
|
7807
7996
|
const preCaptchaCaptureIdx = latestCaptureIndex(recentCaptures);
|
|
7808
|
-
const
|
|
7809
|
-
|
|
7997
|
+
const preCaptchaMetaLength = recentCaptureMeta.length;
|
|
7998
|
+
// Both the inject-and-submit primitive and the registry-state probe
|
|
7999
|
+
// below evaluate against `captchaTarget` after the solve's ~120s poll
|
|
8000
|
+
// has already elapsed, so a page navigation or frame detach that
|
|
8001
|
+
// happened during that wait can leave the frame stale/wedged and
|
|
8002
|
+
// make either evaluate throw via `withWatchdog` — a possibility
|
|
8003
|
+
// distinct from (and unguarded by) the solveCaptcha rejection handled
|
|
8004
|
+
// above. Fold that into the same attempts-remaining tolerance rather
|
|
8005
|
+
// than letting it escape the loop uncaught on every attempt: retry
|
|
8006
|
+
// when attempts remain, still throw on the final attempt so a
|
|
8007
|
+
// genuine failure fails the step per the comment below.
|
|
8008
|
+
let injectResult;
|
|
8009
|
+
let registryState;
|
|
8010
|
+
try {
|
|
8011
|
+
injectResult = await injectCaptchaTokenAndSubmit(captchaTarget, solved.token);
|
|
8012
|
+
registryState = await captchaTarget.evaluate(`(() => {
|
|
7810
8013
|
const sitekey = ${JSON.stringify(siteKey)};
|
|
7811
8014
|
const findSitekeyEl = function () {
|
|
7812
8015
|
return Array.prototype.find.call(
|
|
@@ -7832,6 +8035,14 @@ async function executeStepWithHealing(params) {
|
|
|
7832
8035
|
if (hcaptchaLoaded && widgetRendered) return "renderedUnmatched";
|
|
7833
8036
|
return "empty";
|
|
7834
8037
|
})()`);
|
|
8038
|
+
}
|
|
8039
|
+
catch (err) {
|
|
8040
|
+
const attemptsRemain = captchaAttempt < CAPTCHA_REGISTRY_RETRY_ATTEMPTS;
|
|
8041
|
+
logger.error(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: token inject/registry-probe threw on attempt ${captchaAttempt}/${CAPTCHA_REGISTRY_RETRY_ATTEMPTS} (${(0, errors_1.toErrorMessage)(err)}); ${attemptsRemain ? "retrying" : "failing the step rather than silently proceeding"}`);
|
|
8042
|
+
if (attemptsRemain)
|
|
8043
|
+
continue;
|
|
8044
|
+
throw err;
|
|
8045
|
+
}
|
|
7835
8046
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: attempt=${captchaAttempt}/${CAPTCHA_REGISTRY_RETRY_ATTEMPTS} token injected=${injectResult.injected} hasForm=${injectResult.hasForm} callbackDiscovered=${injectResult.callbackDiscovered} registryState=${registryState}`);
|
|
7836
8047
|
// Reuse the EXISTING waitForTransitionBody poll (no new captcha-specific
|
|
7837
8048
|
// poll loop), just with a widened budget so the solve+submit round trip
|
|
@@ -7856,9 +8067,32 @@ async function executeStepWithHealing(params) {
|
|
|
7856
8067
|
// only issue an explicit submit when no transition was observed AND a
|
|
7857
8068
|
// form is known to exist to submit — otherwise this would double-submit
|
|
7858
8069
|
// a form the widget's own callback already advanced.
|
|
8070
|
+
let fallbackSubmitted = false;
|
|
7859
8071
|
if (!confirmed && injectResult.hasForm) {
|
|
7860
|
-
|
|
7861
|
-
|
|
8072
|
+
try {
|
|
8073
|
+
fallbackSubmitted = await submitCaptchaGatedForm(captchaTarget, "h-captcha-response", {
|
|
8074
|
+
signalCounter,
|
|
8075
|
+
page,
|
|
8076
|
+
});
|
|
8077
|
+
}
|
|
8078
|
+
catch (err) {
|
|
8079
|
+
// A genuine in-page exception from the click/submit dispatch
|
|
8080
|
+
// (classified via `isNavigatingEvaluateRejection` inside
|
|
8081
|
+
// `submitCaptchaGatedForm`) — not the expected context-teardown
|
|
8082
|
+
// rejection of a submit that navigated the frame. Fold into the
|
|
8083
|
+
// same attempts-remaining tolerance as the inject/registry-probe
|
|
8084
|
+
// catch above rather than letting it silently resolve as a
|
|
8085
|
+
// no-op fallback.
|
|
8086
|
+
const attemptsRemain = captchaAttempt < CAPTCHA_REGISTRY_RETRY_ATTEMPTS;
|
|
8087
|
+
logger.error(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: explicit submit fallback threw a genuine in-page exception on attempt ${captchaAttempt}/${CAPTCHA_REGISTRY_RETRY_ATTEMPTS} (${(0, errors_1.toErrorMessage)(err)}); ${attemptsRemain ? "retrying" : "failing the step rather than silently proceeding"}`);
|
|
8088
|
+
if (attemptsRemain)
|
|
8089
|
+
continue;
|
|
8090
|
+
throw err;
|
|
8091
|
+
}
|
|
8092
|
+
if (!fallbackSubmitted) {
|
|
8093
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: explicit submit fallback found no field/form to act on (widget likely tore down or replaced it after the callback fired); nothing was submitted`);
|
|
8094
|
+
}
|
|
8095
|
+
else if (advanceTransitionBodyPattern) {
|
|
7862
8096
|
confirmed = await waitForTransitionBody({
|
|
7863
8097
|
page,
|
|
7864
8098
|
preIdx: preCaptchaCaptureIdx,
|
|
@@ -7891,13 +8125,53 @@ async function executeStepWithHealing(params) {
|
|
|
7891
8125
|
trajectory?.push({ stepIndex, verifiedBy: "url" });
|
|
7892
8126
|
return "completed";
|
|
7893
8127
|
}
|
|
8128
|
+
// Neither transition poll above can see a same-origin submit whose
|
|
8129
|
+
// response never changes the page's URL (an XHR/fetch-driven submit
|
|
8130
|
+
// with no client-side redirect). `findRecentPageTransition` is the
|
|
8131
|
+
// SAME network-capture detector the probe-absent path below already
|
|
8132
|
+
// relies on for exactly this case — reusing it here, scoped to the
|
|
8133
|
+
// captures landed since this attempt's solve, catches the submit a
|
|
8134
|
+
// clean callback dispatches even when the URL/origin never moves.
|
|
8135
|
+
const networkTransitionUrl = findRecentPageTransition({
|
|
8136
|
+
recentCaptureMeta,
|
|
8137
|
+
preMetaLength: preCaptchaMetaLength,
|
|
8138
|
+
});
|
|
8139
|
+
if (networkTransitionUrl !== null) {
|
|
8140
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: post-submit network response confirmed the advance (${networkTransitionUrl})`);
|
|
8141
|
+
trajectory?.push({ stepIndex, verifiedBy: "network" });
|
|
8142
|
+
return "completed";
|
|
8143
|
+
}
|
|
8144
|
+
// The discovered callback threw a genuine in-page exception during
|
|
8145
|
+
// its invoke (classified via `isNavigatingEvaluateRejection`), and
|
|
8146
|
+
// none of the transition detectors above confirmed an advance
|
|
8147
|
+
// either. Checked BEFORE `shouldRetryCaptchaRegistry` below so the
|
|
8148
|
+
// swallowed exception is always logged and threaded into the retry
|
|
8149
|
+
// decision — never silently absorbed into that check's generic
|
|
8150
|
+
// "callback fired but no nav" retry reasoning, which says nothing
|
|
8151
|
+
// about a real thrown error having occurred. A thrown exception is
|
|
8152
|
+
// direct evidence the token was never actually delivered, not merely
|
|
8153
|
+
// an absence of a poll result, so it must never be folded into the
|
|
8154
|
+
// "clean callback, no evidence either way" fallthrough further below
|
|
8155
|
+
// either. Retry while attempts remain (the exception may be
|
|
8156
|
+
// transient, e.g. a stale frame on this attempt — re-resolved at the
|
|
8157
|
+
// top of the next iteration), else fail loudly naming the real cause
|
|
8158
|
+
// instead of silently reporting `callbackDiscovered=true`/
|
|
8159
|
+
// `injected=true` and falling through to the phantom-click cascade
|
|
8160
|
+
// with no trace of it.
|
|
8161
|
+
if (injectResult.callbackInvokeError) {
|
|
8162
|
+
const attemptsRemain = captchaAttempt < CAPTCHA_REGISTRY_RETRY_ATTEMPTS;
|
|
8163
|
+
logger.error(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: the discovered callback threw a genuine in-page exception on attempt ${captchaAttempt}/${CAPTCHA_REGISTRY_RETRY_ATTEMPTS} (${injectResult.callbackInvokeError}); ${attemptsRemain ? "retrying" : "failing the step rather than silently falling through to the cascade"}`);
|
|
8164
|
+
if (attemptsRemain)
|
|
8165
|
+
continue;
|
|
8166
|
+
throw new errors_2.CaptchaError(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: the discovered callback threw a genuine in-page exception (${injectResult.callbackInvokeError}) instead of delivering the token, and no transition was confirmed after the solve`);
|
|
8167
|
+
}
|
|
7894
8168
|
// The intermittent race this loop exists for: a still-empty/absent
|
|
7895
8169
|
// registry means the callback attach hadn't landed when this attempt
|
|
7896
8170
|
// solved+injected, and a discovered-but-unconfirmed callback means it
|
|
7897
8171
|
// fired without a poll seeing the resulting transition. Both are
|
|
7898
8172
|
// worth another attempt within budget rather than an immediate throw.
|
|
7899
8173
|
if (shouldRetryCaptchaRegistry(captchaAttempt, CAPTCHA_REGISTRY_RETRY_ATTEMPTS, registryState, injectResult.callbackDiscovered, confirmed)) {
|
|
7900
|
-
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: registryState=${registryState} callbackDiscovered=${injectResult.callbackDiscovered} with no confirmed transition on attempt ${captchaAttempt}; retrying`);
|
|
8174
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: registryState=${registryState} callbackDiscovered=${injectResult.callbackDiscovered} fallbackSubmitted=${fallbackSubmitted} with no confirmed transition on attempt ${captchaAttempt}; retrying`);
|
|
7901
8175
|
continue;
|
|
7902
8176
|
}
|
|
7903
8177
|
// No render-config callback was discoverable AND neither the inject's
|
|
@@ -7911,9 +8185,25 @@ async function executeStepWithHealing(params) {
|
|
|
7911
8185
|
// none configured, no network poll ever ran, so there's nothing here
|
|
7912
8186
|
// to contradict the normal cascade/verifier.
|
|
7913
8187
|
if (advanceTransitionBodyPattern && !injectResult.callbackDiscovered) {
|
|
7914
|
-
|
|
8188
|
+
const fallbackDetail = injectResult.hasForm && !fallbackSubmitted
|
|
8189
|
+
? " and the explicit submit fallback found no field/form to act on"
|
|
8190
|
+
: "";
|
|
8191
|
+
throw new errors_2.CaptchaError(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: no render-config callback could be found and delivered, and no transition was confirmed after the solve${fallbackDetail}`);
|
|
7915
8192
|
}
|
|
7916
|
-
|
|
8193
|
+
// The remaining shape: a render-config callback WAS cleanly
|
|
8194
|
+
// discovered and invoked on every attempt (or no pattern was
|
|
8195
|
+
// configured to distinguish that from the branch above), yet
|
|
8196
|
+
// neither the navigation poll nor the network-capture scan ever
|
|
8197
|
+
// confirmed an advance on any attempt. `shouldRetryCaptchaRegistry`
|
|
8198
|
+
// above already exhausted every attempt worth retrying, so this is
|
|
8199
|
+
// not a registry race — the token was solved and handed to the
|
|
8200
|
+
// widget's own callback, but nothing observable ever moved. Fail
|
|
8201
|
+
// loudly here too instead of `break`-ing into the normal
|
|
8202
|
+
// phantom-click cascade as if the solve had worked; that fallthrough
|
|
8203
|
+
// is the exact misleading "callbackDiscovered=true" silent no-op
|
|
8204
|
+
// this hook must never produce, pattern-configured or not.
|
|
8205
|
+
logger.error(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: registryState=${registryState} callbackDiscovered=${injectResult.callbackDiscovered} produced no confirmed transition after exhausting all ${CAPTCHA_REGISTRY_RETRY_ATTEMPTS} attempts; failing the step rather than silently falling through to the cascade`);
|
|
8206
|
+
throw new errors_2.CaptchaError(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: registryState=${registryState} callbackDiscovered=${injectResult.callbackDiscovered} produced no confirmed transition after exhausting all attempts`);
|
|
7917
8207
|
}
|
|
7918
8208
|
}
|
|
7919
8209
|
}
|
|
@@ -9210,9 +9500,7 @@ async function executeStepWithHealing(params) {
|
|
|
9210
9500
|
// ng-invalid marker count grew (see isClickViewSwapVerified's doc comment).
|
|
9211
9501
|
const clickViewSwapVerified = isClickViewSwapVerified({
|
|
9212
9502
|
resolvedAction,
|
|
9213
|
-
isFinalStep,
|
|
9214
9503
|
submitStep,
|
|
9215
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
9216
9504
|
isAdvanceWithPattern: isAdvanceStep(step) && advanceTransitionBodyPattern !== null,
|
|
9217
9505
|
networkDelta: post.networkCount - pre.networkCount,
|
|
9218
9506
|
bytesDelta: post.bodyHtmlLength - pre.bodyHtmlLength,
|
|
@@ -9330,10 +9618,49 @@ async function executeStepWithHealing(params) {
|
|
|
9330
9618
|
}
|
|
9331
9619
|
}
|
|
9332
9620
|
}
|
|
9621
|
+
// Phantom-click verdict, computed from the same pre/post pair the
|
|
9622
|
+
// signals above were derived from. Read HERE — at the point the retry
|
|
9623
|
+
// loop decides whether to keep going — rather than only after the
|
|
9624
|
+
// `verified` gate has already failed the attempt: an `"effective"`
|
|
9625
|
+
// verdict (a real, observable change) must end the attempt loop
|
|
9626
|
+
// immediately regardless of step shape (final, submit, or ordinary
|
|
9627
|
+
// interior toggle), the same as the narrower `verified` boolean does.
|
|
9628
|
+
// Previously the verdict was computed only for post-failure diagnostics/
|
|
9629
|
+
// escalation, so an attempt explicitly classified `"effective"` could
|
|
9630
|
+
// still be followed by wasted further attempts (see
|
|
9631
|
+
// recon-browser-1.12.54-phantom-click-verdict-inconsistent-blocks-terminal-tab-toggle-steps-from-ever-completing.md).
|
|
9632
|
+
record.phantomClickVerdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
9633
|
+
actResultSuccess: record.actResultSuccess,
|
|
9634
|
+
pre,
|
|
9635
|
+
post,
|
|
9636
|
+
elementStateChanged: domVerified,
|
|
9637
|
+
isSubmitShapedStep: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag),
|
|
9638
|
+
});
|
|
9639
|
+
// An `"effective"` verdict driven purely by the page-wide byte-delta
|
|
9640
|
+
// floor (`TRIVIAL_DOM_DELTA_BYTES`, 500B) is intentionally NOT trusted
|
|
9641
|
+
// here: `clickViewSwapVerified`/`formValueVerified` already own that
|
|
9642
|
+
// signal with the correct, much higher view-swap thresholds (5000B, or
|
|
9643
|
+
// 500B+textChanged for a reveal) — see
|
|
9644
|
+
// flow-runner.client-side-view-swap-cascade.test.ts's <5KB fixture,
|
|
9645
|
+
// which must stay unverified so the cascade keeps excluding candidates.
|
|
9646
|
+
// The one verdict-driven signal genuinely missing from `verified` is the
|
|
9647
|
+
// element-scoped selection-state change (`domVerified`): a same-page
|
|
9648
|
+
// toggle whose own committed state flips (Base Web `kind`/class, ARIA,
|
|
9649
|
+
// native `checked`) with no network/URL/advance-pattern match had NO
|
|
9650
|
+
// credit path at all outside the n+16 fallback's own checkbox-specific
|
|
9651
|
+
// check, even though `classifyPhantomClick` already classifies it
|
|
9652
|
+
// `"effective"` (excluded only on submit-shaped steps, to keep a stray
|
|
9653
|
+
// self-toggle from defeating the submit escalation). Gated through
|
|
9654
|
+
// `domVerifiedForStep` (not raw `domVerified`) so this credit path
|
|
9655
|
+
// respects the SAME DOM-only-advance veto as the block above: a wizard
|
|
9656
|
+
// "Next" step on a pattern-configured site whose only signal is a field
|
|
9657
|
+
// toggle must stay unverified, not get waved through by the verdict.
|
|
9658
|
+
const domEffectiveVerdict = !(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag)) && domVerifiedForStep;
|
|
9333
9659
|
let verified = networkIsRealAdvance ||
|
|
9334
9660
|
urlChanged ||
|
|
9335
9661
|
domVerifiedForStep ||
|
|
9336
9662
|
datepickerCommitted ||
|
|
9663
|
+
domEffectiveVerdict ||
|
|
9337
9664
|
(!datepickerRejected &&
|
|
9338
9665
|
!promptSelectorRejected &&
|
|
9339
9666
|
(clickViewSwapVerified || formValueVerified));
|
|
@@ -9351,7 +9678,20 @@ async function executeStepWithHealing(params) {
|
|
|
9351
9678
|
// state. A 2xx network response alone is insufficient (could be
|
|
9352
9679
|
// telemetry). The Haiku judge defaults to verified=false when
|
|
9353
9680
|
// ambiguous — strong evidence, not lax permission.
|
|
9354
|
-
|
|
9681
|
+
//
|
|
9682
|
+
// Only re-litigate an already-`verified` step through the judge when
|
|
9683
|
+
// the step is EXPLICITLY the submit (`submitStep: true`) or a genuine
|
|
9684
|
+
// transition fired (`networkIsRealAdvance` / `urlChanged`). A step that
|
|
9685
|
+
// reached `verified` purely via the element-scoped selection-state
|
|
9686
|
+
// credit (`domVerifiedForStep`) with no explicit `submitStep` flag is
|
|
9687
|
+
// the inferred-final-step case — a same-page toggle the recon slice
|
|
9688
|
+
// happened to end on, not the flow's real submit action (see
|
|
9689
|
+
// flow-runner.final-step-toggle-not-submit-shaped-acceptance.test.ts).
|
|
9690
|
+
// Forcing that toggle through the submit judge (which requires a DOM/
|
|
9691
|
+
// URL/title post-submit signal it will never produce) false-negatives
|
|
9692
|
+
// an already-genuine credit.
|
|
9693
|
+
const hasSubmitTransitionSignal = submitStep || networkIsRealAdvance || urlChanged;
|
|
9694
|
+
if (verified && requireSubmitEndpoint && hasSubmitTransitionSignal) {
|
|
9355
9695
|
// Cap the scan from preMetaLength so we don't accept a historical
|
|
9356
9696
|
// submit-shaped capture from an earlier step as proof for this one.
|
|
9357
9697
|
const tail = recentCaptureMeta.slice(preMetaLength);
|
|
@@ -9698,6 +10038,36 @@ async function executeStepWithHealing(params) {
|
|
|
9698
10038
|
const weakDomSignalsAllowed = ((!isFinalStep && !submitStep) || requireSubmitEndpoint) &&
|
|
9699
10039
|
!isCheckboxOrRadioIntentStep(step) &&
|
|
9700
10040
|
!clickTargetIsSelectionMarker;
|
|
10041
|
+
// Same effective-verdict credit the primary attempt's completion
|
|
10042
|
+
// gate uses (see the `record.phantomClickVerdict` computation
|
|
10043
|
+
// above), re-run against THIS fallback click's own pre/post pair
|
|
10044
|
+
// (`pre`/`retryPost`) rather than the stale pre/post the primary
|
|
10045
|
+
// technique captured — the primary technique's own act() call may
|
|
10046
|
+
// not have produced any DOM effect at all, with the fallback's
|
|
10047
|
+
// `el.click()` being the click that actually landed. Still subject
|
|
10048
|
+
// to the `clickBlockedByDisabled`/`clickBlockedByInvalid`/
|
|
10049
|
+
// `fallbackDomOnlyAdvance` vetoes below, same as every other
|
|
10050
|
+
// fallback signal. `classifyPhantomClick` can still classify a
|
|
10051
|
+
// submit-shaped step "effective" purely off the page-wide
|
|
10052
|
+
// `TRIVIAL_DOM_DELTA_BYTES` floor (elementStateChanged is already
|
|
10053
|
+
// excluded on submit-shaped steps below via `isSubmitShapedStep`),
|
|
10054
|
+
// so the byte-delta-only branch of that verdict must not bypass
|
|
10055
|
+
// the submit-endpoint corroboration gate any more than the raw
|
|
10056
|
+
// `retryHtmlDelta`/`retryTextChanged` signals below may — same
|
|
10057
|
+
// `retrySubmitShaped` exclusion the primary gate's
|
|
10058
|
+
// `domEffectiveVerdict` applies. See
|
|
10059
|
+
// flow-runner.viewswap-blocked-submit-acceptance.test.ts (must stay
|
|
10060
|
+
// gated) and flow-runner.pricing-tab-symmetric-swap-verdict-
|
|
10061
|
+
// acceptance.test.ts (a non-submit-shaped final step must still get
|
|
10062
|
+
// credit here).
|
|
10063
|
+
const retrySubmitShaped = submitStep || (isFinalStep && flowHasSubmitSemanticsFlag);
|
|
10064
|
+
const retryVerdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
10065
|
+
actResultSuccess: record.actResultSuccess,
|
|
10066
|
+
pre,
|
|
10067
|
+
post: retryPost,
|
|
10068
|
+
elementStateChanged: retrySelectionStateChanged,
|
|
10069
|
+
isSubmitShapedStep: retrySubmitShaped,
|
|
10070
|
+
});
|
|
9701
10071
|
let retryVerified = !clickBlockedByDisabled &&
|
|
9702
10072
|
!clickBlockedByInvalid &&
|
|
9703
10073
|
!fallbackDomOnlyAdvance &&
|
|
@@ -9705,14 +10075,25 @@ async function executeStepWithHealing(params) {
|
|
|
9705
10075
|
retryUrlChanged ||
|
|
9706
10076
|
checkboxStateVerified ||
|
|
9707
10077
|
retrySelectionStateChanged ||
|
|
10078
|
+
(!retrySubmitShaped && retryVerdict === "effective") ||
|
|
9708
10079
|
(weakDomSignalsAllowed &&
|
|
9709
10080
|
(retryHtmlDelta !== 0 || retryTextChanged || retryFormValueChanged)));
|
|
10081
|
+
if (retryVerified) {
|
|
10082
|
+
record.phantomClickVerdict = retryVerdict;
|
|
10083
|
+
}
|
|
9710
10084
|
// Apply the same submit-endpoint gate the primary verifier uses.
|
|
9711
10085
|
// Without this, the n+16 fallback would still ride past a
|
|
9712
10086
|
// tracking-pixel-only click on the final step. Same Haiku LLM
|
|
9713
10087
|
// judgment as the primary verifier — multi-signal corroboration
|
|
9714
|
-
// replaces deterministic URL regex matching.
|
|
9715
|
-
|
|
10088
|
+
// replaces deterministic URL regex matching. Same
|
|
10089
|
+
// hasSubmitTransitionSignal carve-out as the primary verifier: an
|
|
10090
|
+
// inferred (non-explicit-submitStep) final step whose n+16 credit
|
|
10091
|
+
// came only from the element-scoped `retrySelectionStateChanged`
|
|
10092
|
+
// signal is the same ordinary-toggle shape, not a real submit —
|
|
10093
|
+
// don't force it through a judge that requires evidence it can
|
|
10094
|
+
// never produce.
|
|
10095
|
+
const retryHasSubmitTransitionSignal = submitStep || retryNetworkIsRealAdvance || retryUrlChanged;
|
|
10096
|
+
if (retryVerified && requireSubmitEndpoint && retryHasSubmitTransitionSignal) {
|
|
9716
10097
|
const tail = recentCaptureMeta.slice(preMetaLength);
|
|
9717
10098
|
// DOM-state probe (deterministic).
|
|
9718
10099
|
let domSubmittedMatch = null;
|
|
@@ -9820,22 +10201,10 @@ async function executeStepWithHealing(params) {
|
|
|
9820
10201
|
return "completed";
|
|
9821
10202
|
}
|
|
9822
10203
|
const effectSignals = describeAttemptEffectSignals(pre, post, recentCaptureMeta, preMetaLength);
|
|
9823
|
-
//
|
|
9824
|
-
//
|
|
9825
|
-
//
|
|
9826
|
-
//
|
|
9827
|
-
// attempt 1's verdict drives the escalation flag below.
|
|
9828
|
-
record.phantomClickVerdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
9829
|
-
actResultSuccess: record.actResultSuccess,
|
|
9830
|
-
pre,
|
|
9831
|
-
post,
|
|
9832
|
-
// Authoritative element-scoped signal: `verifyDomEffect` read the resolved
|
|
9833
|
-
// element's own committed-state delta (Base Web `kind`/class, ARIA, native
|
|
9834
|
-
// checked) into `domVerified`. A registered selection toggle no longer
|
|
9835
|
-
// reads as a phantom just because it moved no network/URL/bytes.
|
|
9836
|
-
elementStateChanged: domVerified,
|
|
9837
|
-
isSubmitShapedStep: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag),
|
|
9838
|
-
});
|
|
10204
|
+
// record.phantomClickVerdict was already computed above, before the
|
|
10205
|
+
// `verified` gate — reaching this point means it is NOT `"effective"`
|
|
10206
|
+
// (an effective verdict would have short-circuited to "completed"
|
|
10207
|
+
// above), so it is either `"phantom"` or `"unresolved"` here.
|
|
9839
10208
|
const reason = record.errorMessage
|
|
9840
10209
|
? effectSignals
|
|
9841
10210
|
? `${record.errorMessage}; ${effectSignals}`
|