@enricai/barnacle 1.12.82 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/ats-field-vocabulary.d.ts.map +1 -1
- package/dist/lib/ats-field-vocabulary.js +86 -3
- package/dist/lib/ats-field-vocabulary.js.map +1 -1
- package/dist/lib/llm/judges/verify-step-advanced.d.ts +1 -1
- package/dist/lib/llm/judges/verify-step-advanced.d.ts.map +1 -1
- package/dist/lib/llm/judges/verify-step-advanced.js +3 -2
- package/dist/lib/llm/judges/verify-step-advanced.js.map +1 -1
- package/dist/lib/llm/judges/verify-submit.d.ts +2 -2
- package/dist/lib/llm/judges/verify-submit.d.ts.map +1 -1
- package/dist/lib/llm/judges/verify-submit.js +17 -5
- package/dist/lib/llm/judges/verify-submit.js.map +1 -1
- package/dist/lib/logging.d.ts +14 -4
- package/dist/lib/logging.d.ts.map +1 -1
- package/dist/lib/logging.js +34 -25
- package/dist/lib/logging.js.map +1 -1
- package/dist/lib/random.d.ts +7 -3
- package/dist/lib/random.d.ts.map +1 -1
- package/dist/lib/random.js +27 -4
- package/dist/lib/random.js.map +1 -1
- package/dist/scraper/click-settle.d.ts +16 -7
- package/dist/scraper/click-settle.d.ts.map +1 -1
- package/dist/scraper/click-settle.js +31 -24
- package/dist/scraper/click-settle.js.map +1 -1
- package/dist/scraper/deep-locator-actuate.d.ts.map +1 -1
- package/dist/scraper/deep-locator-actuate.js +17 -1
- package/dist/scraper/deep-locator-actuate.js.map +1 -1
- package/dist/scraper/deep-locator-fake.js +1 -1
- package/dist/scraper/deep-locator-fake.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts +139 -8
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +710 -316
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/phantom-click.d.ts +45 -1
- package/dist/scraper/phantom-click.d.ts.map +1 -1
- package/dist/scraper/phantom-click.js +114 -10
- package/dist/scraper/phantom-click.js.map +1 -1
- package/dist/scraper/runtime-capture-store.d.ts +38 -0
- package/dist/scraper/runtime-capture-store.d.ts.map +1 -0
- package/dist/scraper/runtime-capture-store.js +103 -0
- package/dist/scraper/runtime-capture-store.js.map +1 -0
- package/dist/scraper/segmented-date-spinbutton.d.ts +13 -0
- package/dist/scraper/segmented-date-spinbutton.d.ts.map +1 -0
- package/dist/scraper/segmented-date-spinbutton.js +71 -0
- package/dist/scraper/segmented-date-spinbutton.js.map +1 -0
- package/dist/scripts/recon-browser.d.ts +0 -8
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +102 -50
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +11 -0
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +180 -53
- package/dist/scripts/recon-generate.js.map +1 -1
- package/package.json +1 -1
|
@@ -34,6 +34,7 @@ exports.classifyStepSubmitIntent = classifyStepSubmitIntent;
|
|
|
34
34
|
exports.isStepDeclaredNonSubmit = isStepDeclaredNonSubmit;
|
|
35
35
|
exports.isStepSubmitShaped = isStepSubmitShaped;
|
|
36
36
|
exports.isStepSettleWaitShaped = isStepSettleWaitShaped;
|
|
37
|
+
exports.isGenuineFinalSubmit = isGenuineFinalSubmit;
|
|
37
38
|
exports.isMidFlowSubmitShapedStep = isMidFlowSubmitShapedStep;
|
|
38
39
|
exports.isCheckboxOrRadioIntentStep = isCheckboxOrRadioIntentStep;
|
|
39
40
|
exports.shouldCaptureSelectionState = shouldCaptureSelectionState;
|
|
@@ -47,6 +48,7 @@ exports.windowHasAdvanceTransition = windowHasAdvanceTransition;
|
|
|
47
48
|
exports.shouldVetoFallbackAdvance = shouldVetoFallbackAdvance;
|
|
48
49
|
exports.isDomOnlyAdvanceVerified = isDomOnlyAdvanceVerified;
|
|
49
50
|
exports.isNullVerdictAdvanceCredited = isNullVerdictAdvanceCredited;
|
|
51
|
+
exports.isMidFlowAdvanceCredited = isMidFlowAdvanceCredited;
|
|
50
52
|
exports.isClickViewSwapVerified = isClickViewSwapVerified;
|
|
51
53
|
exports.selectionCountFromSignature = selectionCountFromSignature;
|
|
52
54
|
exports.shouldReadbackFillOnActSuccess = shouldReadbackFillOnActSuccess;
|
|
@@ -74,6 +76,7 @@ exports.extractGaEventEvidence = extractGaEventEvidence;
|
|
|
74
76
|
exports.normalizeDateValue = normalizeDateValue;
|
|
75
77
|
exports.fillHtml5DateTimeInput = fillHtml5DateTimeInput;
|
|
76
78
|
exports.fillTextDatepickerInput = fillTextDatepickerInput;
|
|
79
|
+
exports.isDateSegmentAtSelector = isDateSegmentAtSelector;
|
|
77
80
|
exports.verifyFillReadback = verifyFillReadback;
|
|
78
81
|
exports.verifyPromptSelectorCommitted = verifyPromptSelectorCommitted;
|
|
79
82
|
exports.tailRetargetPhrasesForStep = tailRetargetPhrasesForStep;
|
|
@@ -92,12 +95,15 @@ exports.parseSegmentedDateValue = parseSegmentedDateValue;
|
|
|
92
95
|
exports.resolveSegmentedDateGroup = resolveSegmentedDateGroup;
|
|
93
96
|
exports.parseSegmentedPartStep = parseSegmentedPartStep;
|
|
94
97
|
exports.trySegmentedDateSpinbuttonPrimitive = trySegmentedDateSpinbuttonPrimitive;
|
|
98
|
+
exports.runSegmentedDateSpinbuttonPrimitive = runSegmentedDateSpinbuttonPrimitive;
|
|
95
99
|
exports.parseRadioStep = parseRadioStep;
|
|
96
100
|
exports.parseWidgetOptionClickStep = parseWidgetOptionClickStep;
|
|
97
101
|
exports.parseCheckStep = parseCheckStep;
|
|
98
102
|
exports.parseCheckStateStep = parseCheckStateStep;
|
|
99
103
|
exports.pollEnumerate = pollEnumerate;
|
|
100
104
|
exports.waitForTransitionBody = waitForTransitionBody;
|
|
105
|
+
exports.settleAndSnapshotPostClick = settleAndSnapshotPostClick;
|
|
106
|
+
exports.readMidFlowPostClickAdvance = readMidFlowPostClickAdvance;
|
|
101
107
|
exports.injectCaptchaTokenAndSubmit = injectCaptchaTokenAndSubmit;
|
|
102
108
|
exports.submitCaptchaGatedForm = submitCaptchaGatedForm;
|
|
103
109
|
exports.trySelectPrimitive = trySelectPrimitive;
|
|
@@ -158,6 +164,8 @@ const errors_2 = require("./errors");
|
|
|
158
164
|
const frame_target_1 = require("./frame-target");
|
|
159
165
|
const phantom_click_1 = require("./phantom-click");
|
|
160
166
|
const quoted_spans_1 = require("./quoted-spans");
|
|
167
|
+
const runtime_capture_store_1 = require("./runtime-capture-store");
|
|
168
|
+
const segmented_date_spinbutton_1 = require("./segmented-date-spinbutton");
|
|
161
169
|
const session_teardown_1 = require("./session-teardown");
|
|
162
170
|
const spa_readiness_1 = require("./spa-readiness");
|
|
163
171
|
const stagehand_guard_1 = require("./stagehand-guard");
|
|
@@ -166,7 +174,6 @@ const target_resolution_diagnostic_1 = require("./target-resolution-diagnostic")
|
|
|
166
174
|
const trusted_click_1 = require("./trusted-click");
|
|
167
175
|
const upload_affordance_expr_1 = require("./upload-affordance-expr");
|
|
168
176
|
const watchdog_1 = require("./watchdog");
|
|
169
|
-
const recon_shared_1 = require("../scripts/recon-shared");
|
|
170
177
|
const client_1 = require("../testmail/client");
|
|
171
178
|
var spa_readiness_2 = require("./spa-readiness");
|
|
172
179
|
Object.defineProperty(exports, "waitForSpaReady", { enumerable: true, get: function () { return spa_readiness_2.waitForSpaReady; } });
|
|
@@ -1109,7 +1116,9 @@ function prepareFailureDumpBody(raw) {
|
|
|
1109
1116
|
function ariaCheckedReadExpr(elVar) {
|
|
1110
1117
|
return `${elVar}.getAttribute("aria-checked")`;
|
|
1111
1118
|
}
|
|
1112
|
-
|
|
1119
|
+
/** Bound on {@link StepSnapshot.visibleText}, keeping a pathological page from bloating snapshots. */
|
|
1120
|
+
const VISIBLE_TEXT_EXCERPT_CHARS = 3000;
|
|
1121
|
+
const DOM_SNAPSHOT_EXPR = `(() => { const b = document.body; if (!b) return { html: 0, text: "", values: "", state: "", full: "", digest: "" }; const t = b.innerText || ""; const controls = Array.from(b.querySelectorAll("input, textarea, select")).filter((el) => el.offsetParent !== null); const values = controls.map((el) => { if (el.type === "checkbox" || el.type === "radio") return el.checked ? "1" : "0"; return el.value || ""; }).join("|").slice(0, 2000); const ariaSel = "[aria-pressed],[aria-checked],[aria-selected],[data-state],[data-selected],[data-checked],[role=option],[role=switch],[role=checkbox],[role=menuitemcheckbox]"; const classSel = ${JSON.stringify(browser_click_expr_1.SELECTION_MARKER_CLASS_SELECTOR_SRC)}; const classRx = ${browser_click_expr_1.SELECTION_MARKER_CLASS_TOKEN_REGEX_SRC}; const skip = (el) => el.closest("[role=dialog],[role=tooltip],[aria-live]") !== null; const seen = new Set(); for (const el of b.querySelectorAll(ariaSel + "," + classSel)) { if (el.offsetParent !== null && !skip(el)) seen.add(el); } let idx = 0; const parts = []; for (const el of seen) { const ap = el.getAttribute("aria-pressed") || ""; const ac = el.getAttribute("aria-checked") || ""; const as = el.getAttribute("aria-selected") || ""; const dsRaw = el.getAttribute("data-state") || ""; const ds = (dsRaw === "open" || dsRaw === "closed") ? "" : dsRaw; const dsel = el.hasAttribute("data-selected") ? "1" : ""; const dchk = el.hasAttribute("data-checked") ? "1" : ""; const clsHit = classRx.test(el.getAttribute("class") || "") ? "1" : "0"; const i = idx++; if (!ap && !ac && !as && !ds && !dsel && !dchk && clsHit === "0") continue; parts.push(i + ":" + ap + "," + ac + "," + as + "," + ds + "," + dsel + "," + dchk + "," + clsHit); } const joined = parts.join("|"); let h = 2166136261; for (let k = 0; k < joined.length; k++) { h ^= joined.charCodeAt(k); h = Math.imul(h, 16777619); } const state = (h >>> 0).toString(36) + ":" + parts.length; const norm = t.replace(/\\s+/g, " ").trim(); let th = 2166136261; for (let tk = 0; tk < norm.length; tk++) { th ^= norm.charCodeAt(tk); th = Math.imul(th, 16777619); } return { html: (b.outerHTML || "").length, text: t.length + ":" + t.slice(0, 200), values, state, full: norm.slice(0, ${VISIBLE_TEXT_EXCERPT_CHARS}), digest: (th >>> 0).toString(36) + ":" + norm.length }; })()`;
|
|
1113
1122
|
/** Caps mirroring {@link DOM_SNAPSHOT_EXPR}'s truncation so a pathological page can't bloat the failure bundle. */
|
|
1114
1123
|
const FIELD_VALUES_AT_FAILURE_MAX_CONTROLS = 200;
|
|
1115
1124
|
const FIELD_VALUES_AT_FAILURE_MAX_VALUE_LENGTH = 500;
|
|
@@ -1175,7 +1184,15 @@ async function readPageQuiescence(target) {
|
|
|
1175
1184
|
: idle;
|
|
1176
1185
|
}
|
|
1177
1186
|
/**
|
|
1178
|
-
*
|
|
1187
|
+
* Installs the quiescence probe before a click, because it only starts counting
|
|
1188
|
+
* requests and mutations from its first read and a post-click install would miss
|
|
1189
|
+
* everything the click triggers immediately.
|
|
1190
|
+
*/
|
|
1191
|
+
async function armPageQuiescence(target) {
|
|
1192
|
+
await readPageQuiescence(target);
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Reads only the visible-text key settle polls on. Going through
|
|
1179
1196
|
* {@link snapshotPage} also read the frame URL, which rejects once a submit
|
|
1180
1197
|
* detaches the OOPIF and would abort the settle mid-poll; a failed read here
|
|
1181
1198
|
* degrades to the empty signature like the snapshot's own text read does.
|
|
@@ -1184,35 +1201,16 @@ async function readSettleTextSignature(target) {
|
|
|
1184
1201
|
const result = await Promise.resolve(target.evaluate(DOM_SNAPSHOT_EXPR)).catch(() => null);
|
|
1185
1202
|
if (result === null || typeof result !== "object")
|
|
1186
1203
|
return "";
|
|
1187
|
-
const { text } = result;
|
|
1188
|
-
return typeof text === "string" ? text : "";
|
|
1204
|
+
const { text, digest } = result;
|
|
1205
|
+
return (0, click_settle_1.settleTextKey)(typeof text === "string" ? text : "", typeof digest === "string" ? digest : undefined);
|
|
1189
1206
|
}
|
|
1190
1207
|
/**
|
|
1191
1208
|
* Settles after a click using the target's own text signature and in-page
|
|
1192
1209
|
* quiescence reader, so every post-click site decides from the same settle
|
|
1193
1210
|
* signal instead of hand-building the readers.
|
|
1194
1211
|
*/
|
|
1195
|
-
async function settleWithPageQuiescence(page, target, signalCounter, preNetworkCount, submitShaped, preVisibleTextSignature,
|
|
1196
|
-
await (0, click_settle_1.settleAfterClick)(page, signalCounter, preNetworkCount, submitShaped, () => readSettleTextSignature(target), preVisibleTextSignature, () => readPageQuiescence(target)
|
|
1197
|
-
}
|
|
1198
|
-
/**
|
|
1199
|
-
* Builds the "has the page finished advancing" test a mid-flow submit settles
|
|
1200
|
-
* against. It is the body-swap half of the {@link hasPageAdvanced} the phantom
|
|
1201
|
-
* classifier credits with, so a header that flipped before its body re-rendered
|
|
1202
|
-
* keeps settle polling while the classifier still credits whatever is left once
|
|
1203
|
-
* the wait is spent.
|
|
1204
|
-
* Undefined for any other step, which keeps the plain quiet-window settle.
|
|
1205
|
-
*/
|
|
1206
|
-
function pageAdvancedSince(preVisibleTextSignature, midFlowSubmit) {
|
|
1207
|
-
if (!midFlowSubmit)
|
|
1208
|
-
return undefined;
|
|
1209
|
-
const pre = {
|
|
1210
|
-
networkCount: 0,
|
|
1211
|
-
url: "",
|
|
1212
|
-
bodyHtmlLength: 0,
|
|
1213
|
-
visibleTextSignature: preVisibleTextSignature,
|
|
1214
|
-
};
|
|
1215
|
-
return (visibleTextSignature) => (0, phantom_click_1.hasPageBodySwapped)(pre, { ...pre, visibleTextSignature });
|
|
1212
|
+
async function settleWithPageQuiescence(page, target, signalCounter, preNetworkCount, submitShaped, preVisibleTextSignature, preVisibleTextDigest) {
|
|
1213
|
+
await (0, click_settle_1.settleAfterClick)(page, signalCounter, preNetworkCount, submitShaped, () => readSettleTextSignature(target), (0, click_settle_1.settleTextKey)(preVisibleTextSignature, preVisibleTextDigest), () => readPageQuiescence(target));
|
|
1216
1214
|
}
|
|
1217
1215
|
/**
|
|
1218
1216
|
* Captures the pre/post signal triple the submit-verify cascade diffs.
|
|
@@ -1231,6 +1229,8 @@ function pageAdvancedSince(preVisibleTextSignature, midFlowSubmit) {
|
|
|
1231
1229
|
async function snapshotPage(target, signalCounter, page, captureSelectionState = false) {
|
|
1232
1230
|
let bodyHtmlLength = 0;
|
|
1233
1231
|
let visibleTextSignature = "";
|
|
1232
|
+
let visibleText = "";
|
|
1233
|
+
let visibleTextDigest = "";
|
|
1234
1234
|
let formValueSignature = "";
|
|
1235
1235
|
let selectionStateSignature = "";
|
|
1236
1236
|
try {
|
|
@@ -1243,6 +1243,10 @@ async function snapshotPage(target, signalCounter, page, captureSelectionState =
|
|
|
1243
1243
|
typeof result.text === "string") {
|
|
1244
1244
|
bodyHtmlLength = result.html;
|
|
1245
1245
|
visibleTextSignature = result.text;
|
|
1246
|
+
const rawFull = result.full;
|
|
1247
|
+
visibleText = typeof rawFull === "string" ? rawFull : "";
|
|
1248
|
+
const rawDigest = result.digest;
|
|
1249
|
+
visibleTextDigest = typeof rawDigest === "string" ? rawDigest : "";
|
|
1246
1250
|
const rawValues = result.values;
|
|
1247
1251
|
formValueSignature = typeof rawValues === "string" ? rawValues : "";
|
|
1248
1252
|
const rawState = result.state;
|
|
@@ -1285,6 +1289,8 @@ async function snapshotPage(target, signalCounter, page, captureSelectionState =
|
|
|
1285
1289
|
url,
|
|
1286
1290
|
bodyHtmlLength,
|
|
1287
1291
|
visibleTextSignature,
|
|
1292
|
+
visibleText,
|
|
1293
|
+
visibleTextDigest: visibleTextDigest === "" ? undefined : visibleTextDigest,
|
|
1288
1294
|
formValueSignature,
|
|
1289
1295
|
selectionStateSignature,
|
|
1290
1296
|
selectionStateByXpath,
|
|
@@ -1584,12 +1590,20 @@ function isStepSettleWaitShaped(params) {
|
|
|
1584
1590
|
return isStepSubmitShaped(params) || isAdvanceStep(params.step);
|
|
1585
1591
|
}
|
|
1586
1592
|
/**
|
|
1587
|
-
*
|
|
1593
|
+
* The single definition of a genuine final submit: the last slot of a flow
|
|
1594
|
+
* that has submit semantics. A final slot without them is mid-flow.
|
|
1595
|
+
*/
|
|
1596
|
+
function isGenuineFinalSubmit(params) {
|
|
1597
|
+
return params.isFinalStep && params.flowHasSubmitSemantics;
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Submit-shaped but NOT the flow's genuine final submit (a final slot without
|
|
1601
|
+
* submit semantics, e.g. after a replan trims the tail, is still mid-flow): a mid-flow advancing submit
|
|
1588
1602
|
* (e.g. account creation) that can be credited on a demonstrable page advance
|
|
1589
1603
|
* instead of needing the final submit's network/URL proof.
|
|
1590
1604
|
*/
|
|
1591
1605
|
function isMidFlowSubmitShapedStep(params) {
|
|
1592
|
-
if (params
|
|
1606
|
+
if (isGenuineFinalSubmit(params))
|
|
1593
1607
|
return false;
|
|
1594
1608
|
return isStepSubmitShaped(params) || params.elementSubmitShaped === true;
|
|
1595
1609
|
}
|
|
@@ -1719,15 +1733,7 @@ function latestCaptureIndex(recentCaptures) {
|
|
|
1719
1733
|
* so callers scan in capture order. Returns [] when the dir is unreadable.
|
|
1720
1734
|
*/
|
|
1721
1735
|
function capturesAfterIndex(preIdx, capturesDir) {
|
|
1722
|
-
|
|
1723
|
-
try {
|
|
1724
|
-
entries = (0, node_fs_1.readdirSync)(capturesDir);
|
|
1725
|
-
}
|
|
1726
|
-
catch {
|
|
1727
|
-
return [];
|
|
1728
|
-
}
|
|
1729
|
-
return entries
|
|
1730
|
-
.filter((f) => f.endsWith(".json") && !f.endsWith(".decoded.json"))
|
|
1736
|
+
return (0, runtime_capture_store_1.listCaptureFiles)(capturesDir)
|
|
1731
1737
|
.map((f) => ({ f, idx: parseCaptureIndex(f) }))
|
|
1732
1738
|
.filter((e) => e.idx !== null && e.idx > preIdx)
|
|
1733
1739
|
.sort((a, b) => a.idx - b.idx)
|
|
@@ -1758,7 +1764,7 @@ function windowHasTransitionBody(params) {
|
|
|
1758
1764
|
const { preIdx, advanceTransitionBodyPattern } = params;
|
|
1759
1765
|
if (!advanceTransitionBodyPattern)
|
|
1760
1766
|
return false;
|
|
1761
|
-
const capturesDir = params.capturesDir ?? (0,
|
|
1767
|
+
const capturesDir = params.capturesDir ?? (0, runtime_capture_store_1.defaultCapturesDir)();
|
|
1762
1768
|
let rx;
|
|
1763
1769
|
try {
|
|
1764
1770
|
rx = new RegExp(advanceTransitionBodyPattern);
|
|
@@ -1768,8 +1774,9 @@ function windowHasTransitionBody(params) {
|
|
|
1768
1774
|
}
|
|
1769
1775
|
for (const filename of capturesAfterIndex(preIdx, capturesDir)) {
|
|
1770
1776
|
try {
|
|
1771
|
-
const
|
|
1772
|
-
|
|
1777
|
+
const capture = (0, runtime_capture_store_1.readCaptureFile)(capturesDir, filename);
|
|
1778
|
+
if (!capture)
|
|
1779
|
+
continue;
|
|
1773
1780
|
const body = typeof capture.requestPostData === "string"
|
|
1774
1781
|
? capture.requestPostData
|
|
1775
1782
|
: JSON.stringify(capture.requestPostData ?? "");
|
|
@@ -1800,7 +1807,7 @@ function windowHasAdvanceTransition(params) {
|
|
|
1800
1807
|
const { preIdx, advanceTransitionBodyPattern } = params;
|
|
1801
1808
|
if (!advanceTransitionBodyPattern)
|
|
1802
1809
|
return false;
|
|
1803
|
-
const capturesDir = params.capturesDir ?? (0,
|
|
1810
|
+
const capturesDir = params.capturesDir ?? (0, runtime_capture_store_1.defaultCapturesDir)();
|
|
1804
1811
|
let rx;
|
|
1805
1812
|
try {
|
|
1806
1813
|
rx = new RegExp(advanceTransitionBodyPattern);
|
|
@@ -1810,8 +1817,9 @@ function windowHasAdvanceTransition(params) {
|
|
|
1810
1817
|
}
|
|
1811
1818
|
for (const filename of capturesAfterIndex(preIdx, capturesDir)) {
|
|
1812
1819
|
try {
|
|
1813
|
-
const
|
|
1814
|
-
|
|
1820
|
+
const capture = (0, runtime_capture_store_1.readCaptureFile)(capturesDir, filename);
|
|
1821
|
+
if (!capture)
|
|
1822
|
+
continue;
|
|
1815
1823
|
const body = typeof capture.requestPostData === "string"
|
|
1816
1824
|
? capture.requestPostData
|
|
1817
1825
|
: JSON.stringify(capture.requestPostData ?? "");
|
|
@@ -1881,13 +1889,29 @@ function isDomOnlyAdvanceVerified(params) {
|
|
|
1881
1889
|
* rejection, so strong structural click signals (a real network advance or a
|
|
1882
1890
|
* demonstrable page advance) credit the step instead of failing it and
|
|
1883
1891
|
* re-clicking at the same position. Final steps and weak-signal cases return
|
|
1884
|
-
* false and keep the DOM-state fallback / failure path.
|
|
1892
|
+
* false and keep the DOM-state fallback / failure path. Invalid markers
|
|
1893
|
+
* describe the page the click landed on, so they only veto a network-only
|
|
1894
|
+
* credit, never a settled page advance.
|
|
1885
1895
|
*/
|
|
1886
1896
|
function isNullVerdictAdvanceCredited(params) {
|
|
1887
|
-
if (!params.midFlowSubmit || !params.judgeUnavailable
|
|
1897
|
+
if (!params.midFlowSubmit || !params.judgeUnavailable)
|
|
1888
1898
|
return false;
|
|
1889
|
-
|
|
1890
|
-
|
|
1899
|
+
if (params.pageAdvanced)
|
|
1900
|
+
return true;
|
|
1901
|
+
return params.networkAdvanced && params.invalidMarkerCount <= 0;
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Single decision both post-click judge sites use to credit a mid-flow submit
|
|
1905
|
+
* on the page's own demonstrable advance, so a stale title or missing network
|
|
1906
|
+
* the judge reads with final-submit eyes cannot undo it. `signalAdvanced` is the
|
|
1907
|
+
* site's own settled-advance signal (the primary click's view-swap credit, the
|
|
1908
|
+
* fallback click's effective verdict); the null-verdict path covers an
|
|
1909
|
+
* unavailable judge.
|
|
1910
|
+
*/
|
|
1911
|
+
function isMidFlowAdvanceCredited(params) {
|
|
1912
|
+
if (params.midFlowSubmit && params.signalAdvanced)
|
|
1913
|
+
return true;
|
|
1914
|
+
return isNullVerdictAdvanceCredited(params);
|
|
1891
1915
|
}
|
|
1892
1916
|
/**
|
|
1893
1917
|
* Client-side view-swap verification gate. Credits a click that produces
|
|
@@ -1980,7 +2004,7 @@ function isNullVerdictAdvanceCredited(params) {
|
|
|
1980
2004
|
*
|
|
1981
2005
|
* **Mid-flow submit:** `midFlowSubmit` (see {@link isMidFlowSubmitShapedStep})
|
|
1982
2006
|
* lets a submit-shaped, non-final step be credited when `pageAdvanced` (the
|
|
1983
|
-
* shared {@link
|
|
2007
|
+
* shared {@link hasSettledPageAdvanced} judgement of the visible text; defaults to
|
|
1984
2008
|
* `textChanged` when omitted) holds — byte delta is not required, since a
|
|
1985
2009
|
* wizard swap can keep the HTML size — with the same invalid-marker and
|
|
1986
2010
|
* destination vetoes. The clicked control being gone is NOT a veto here: a
|
|
@@ -2003,7 +2027,6 @@ function isClickViewSwapVerified(params) {
|
|
|
2003
2027
|
midFlowActRan &&
|
|
2004
2028
|
destinationPlausible !== false &&
|
|
2005
2029
|
!isAdvanceWithPattern &&
|
|
2006
|
-
invalidMarkerDelta <= 0 &&
|
|
2007
2030
|
textChanged &&
|
|
2008
2031
|
pageAdvanced) {
|
|
2009
2032
|
return true;
|
|
@@ -2121,7 +2144,7 @@ async function countNgInvalidContainers(target) {
|
|
|
2121
2144
|
function describeAttemptEffectSignals(pre, post, recentCaptureMeta, preMetaLength) {
|
|
2122
2145
|
const bytesDelta = post.bodyHtmlLength - pre.bodyHtmlLength;
|
|
2123
2146
|
const networkDelta = post.networkCount - pre.networkCount;
|
|
2124
|
-
const textChanged =
|
|
2147
|
+
const textChanged = (0, phantom_click_1.hasVisibleTextChanged)(pre, post);
|
|
2125
2148
|
const valueChanged = post.formValueSignature !== pre.formValueSignature;
|
|
2126
2149
|
const selectionStateChanged = post.selectionStateSignature !== pre.selectionStateSignature;
|
|
2127
2150
|
const windowCaptures = recentCaptureMeta.slice(preMetaLength);
|
|
@@ -2262,9 +2285,11 @@ function shouldSkipTechnique(params) {
|
|
|
2262
2285
|
* navigations, partial DOM reflows, etc.).
|
|
2263
2286
|
*/
|
|
2264
2287
|
function isSubmitRevealedInvalid(params) {
|
|
2265
|
-
const { isFinalStep, requireSubmitEndpoint, resolvedMethod, effectSignals, preSubmitInvalidCount, postAttemptInvalidCount, } = params;
|
|
2288
|
+
const { isFinalStep, requireSubmitEndpoint, resolvedMethod, effectSignals, preSubmitInvalidCount, postAttemptInvalidCount, midFlowAdvanced = false, } = params;
|
|
2266
2289
|
if (!isFinalStep)
|
|
2267
2290
|
return false;
|
|
2291
|
+
if (midFlowAdvanced)
|
|
2292
|
+
return false;
|
|
2268
2293
|
if (!requireSubmitEndpoint)
|
|
2269
2294
|
return false;
|
|
2270
2295
|
if (resolvedMethod !== "click")
|
|
@@ -2955,32 +2980,36 @@ const STEP_REJUDGE_COMPARE_CHARS = 1200;
|
|
|
2955
2980
|
*/
|
|
2956
2981
|
async function judgeStepOutcome(params) {
|
|
2957
2982
|
const { client, captureFn, step, isFinalStep, target } = params;
|
|
2958
|
-
|
|
2983
|
+
const genuineFinal = isGenuineFinalSubmit({
|
|
2984
|
+
isFinalStep,
|
|
2985
|
+
flowHasSubmitSemantics: params.flowHasSubmitSemantics,
|
|
2986
|
+
});
|
|
2987
|
+
if (genuineFinal) {
|
|
2959
2988
|
return (0, verify_submit_1.verifySubmitWithLLM)({
|
|
2960
2989
|
client,
|
|
2961
2990
|
input: {
|
|
2962
|
-
...(await buildSubmitJudgeStepContext({ step, isFinalStep, target })),
|
|
2991
|
+
...(await buildSubmitJudgeStepContext({ step, isFinalStep: genuineFinal, target })),
|
|
2963
2992
|
...params.submitInput,
|
|
2964
2993
|
},
|
|
2965
2994
|
captureFn,
|
|
2966
2995
|
});
|
|
2967
2996
|
}
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
const beforeText = (colon === -1 ? params.preVisibleTextSignature : params.preVisibleTextSignature.slice(colon + 1))
|
|
2997
|
+
const hasSettledRead = params.settledPostVisibleText !== undefined;
|
|
2998
|
+
if (!hasSettledRead) {
|
|
2999
|
+
await settleWithPageQuiescence(params.page, target, params.signalCounter, params.preNetworkCount, isStepSettleWaitShaped({
|
|
3000
|
+
step,
|
|
3001
|
+
submitStep: params.submitStep,
|
|
3002
|
+
isFinalStep,
|
|
3003
|
+
flowHasSubmitSemantics: params.flowHasSubmitSemantics,
|
|
3004
|
+
}) || params.elementSubmitShaped === true, params.preVisibleTextSignature, params.preVisibleTextDigest);
|
|
3005
|
+
}
|
|
3006
|
+
const signatureColon = params.preVisibleTextSignature.indexOf(":");
|
|
3007
|
+
const signatureHead = (signatureColon === -1
|
|
3008
|
+
? params.preVisibleTextSignature
|
|
3009
|
+
: params.preVisibleTextSignature.slice(signatureColon + 1))
|
|
2982
3010
|
.replace(/\s+/g, " ")
|
|
2983
3011
|
.trim();
|
|
3012
|
+
const beforeText = params.preVisibleText ? params.preVisibleText : signatureHead;
|
|
2984
3013
|
const judgeText = (afterText) => (0, verify_step_advanced_1.verifyStepAdvancedWithLLM)({
|
|
2985
3014
|
client,
|
|
2986
3015
|
input: {
|
|
@@ -2992,7 +3021,7 @@ async function judgeStepOutcome(params) {
|
|
|
2992
3021
|
captureFn,
|
|
2993
3022
|
});
|
|
2994
3023
|
const rejudgeIfTextMoved = async (first, judgedText) => {
|
|
2995
|
-
if (first === null || first.advanced)
|
|
3024
|
+
if (first === null || first.advanced || hasSettledRead)
|
|
2996
3025
|
return first;
|
|
2997
3026
|
await params.page.waitForTimeout(STEP_REJUDGE_DELAY_MS);
|
|
2998
3027
|
const laterText = await probeVisibleTextForJudge(target);
|
|
@@ -3002,7 +3031,9 @@ async function judgeStepOutcome(params) {
|
|
|
3002
3031
|
}
|
|
3003
3032
|
return (await judgeText(laterText)) ?? first;
|
|
3004
3033
|
};
|
|
3005
|
-
const afterText =
|
|
3034
|
+
const afterText = hasSettledRead
|
|
3035
|
+
? (params.settledPostVisibleText ?? "")
|
|
3036
|
+
: await probeVisibleTextForJudge(target);
|
|
3006
3037
|
const firstVerdict = await judgeText(afterText);
|
|
3007
3038
|
const verdict = await rejudgeIfTextMoved(firstVerdict, afterText);
|
|
3008
3039
|
if (verdict === null)
|
|
@@ -3116,7 +3147,7 @@ function extractSubmitFailureEvidence(recentCaptureFilenames,
|
|
|
3116
3147
|
* deterministic hostname equality. Empty list / "any-4xx" mode disables
|
|
3117
3148
|
* the host filter and returns any 4xx in the window.
|
|
3118
3149
|
*/
|
|
3119
|
-
ownBackendHostnames, capturesDir = (0,
|
|
3150
|
+
ownBackendHostnames, capturesDir = (0, runtime_capture_store_1.defaultCapturesDir)(), mode = "strict") {
|
|
3120
3151
|
if (recentCaptureFilenames.length === 0)
|
|
3121
3152
|
return "";
|
|
3122
3153
|
if (mode === "strict" && ownBackendHostnames.length === 0)
|
|
@@ -3128,9 +3159,9 @@ ownBackendHostnames, capturesDir = (0, recon_shared_1.resolveReconRunDir)().grap
|
|
|
3128
3159
|
continue;
|
|
3129
3160
|
seen.add(filename);
|
|
3130
3161
|
try {
|
|
3131
|
-
const
|
|
3132
|
-
|
|
3133
|
-
|
|
3162
|
+
const capture = (0, runtime_capture_store_1.readCaptureFile)(capturesDir, filename);
|
|
3163
|
+
if (!capture)
|
|
3164
|
+
continue;
|
|
3134
3165
|
if (typeof capture.url !== "string")
|
|
3135
3166
|
continue;
|
|
3136
3167
|
if (mode === "strict") {
|
|
@@ -3207,7 +3238,7 @@ ownBackendHostnames, capturesDir = (0, recon_shared_1.resolveReconRunDir)().grap
|
|
|
3207
3238
|
* Returns a numbered evidence list. Empty string when no GA collect
|
|
3208
3239
|
* captures are present. Advisory — never load-bearing.
|
|
3209
3240
|
*/
|
|
3210
|
-
function extractGaEventEvidence(recentCaptureFilenames, capturesDir = (0,
|
|
3241
|
+
function extractGaEventEvidence(recentCaptureFilenames, capturesDir = (0, runtime_capture_store_1.defaultCapturesDir)()) {
|
|
3211
3242
|
if (recentCaptureFilenames.length === 0)
|
|
3212
3243
|
return "";
|
|
3213
3244
|
const records = [];
|
|
@@ -3217,9 +3248,9 @@ function extractGaEventEvidence(recentCaptureFilenames, capturesDir = (0, recon_
|
|
|
3217
3248
|
continue;
|
|
3218
3249
|
seen.add(filename);
|
|
3219
3250
|
try {
|
|
3220
|
-
const
|
|
3221
|
-
|
|
3222
|
-
|
|
3251
|
+
const capture = (0, runtime_capture_store_1.readCaptureFile)(capturesDir, filename);
|
|
3252
|
+
if (!capture)
|
|
3253
|
+
continue;
|
|
3223
3254
|
if (typeof capture.url !== "string")
|
|
3224
3255
|
continue;
|
|
3225
3256
|
let url;
|
|
@@ -3591,6 +3622,25 @@ async function fillTextDatepickerInput(target, selector, value) {
|
|
|
3591
3622
|
return { filled: false, postValue: "", strategy: "none" };
|
|
3592
3623
|
}
|
|
3593
3624
|
}
|
|
3625
|
+
/**
|
|
3626
|
+
* Pre-write check for the observe-act fill override: a lone-segment write
|
|
3627
|
+
* (insertText) cannot compose a date, so callers refuse before any write.
|
|
3628
|
+
* An evaluate error propagates so the attempt fails rather than writing blind;
|
|
3629
|
+
* only a selector form with no XPath body (nothing to resolve) answers false.
|
|
3630
|
+
*/
|
|
3631
|
+
async function isDateSegmentAtSelector(target, selector) {
|
|
3632
|
+
const xpath = xpathBodyForEvaluate(selector);
|
|
3633
|
+
if (xpath === null)
|
|
3634
|
+
return false;
|
|
3635
|
+
const expr = `(() => {
|
|
3636
|
+
const result = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
3637
|
+
const el = result.singleNodeValue;
|
|
3638
|
+
if (!el) return false;
|
|
3639
|
+
${segmented_date_spinbutton_1.SEGMENT_OWNER_JS}
|
|
3640
|
+
return isDateSegment(el);
|
|
3641
|
+
})()`;
|
|
3642
|
+
return (await target.evaluate(expr)) === true;
|
|
3643
|
+
}
|
|
3594
3644
|
/**
|
|
3595
3645
|
* Read back an element's value after a fill action and compare to the
|
|
3596
3646
|
* expected value. Catches silent-value-rejection cases that the verifier's
|
|
@@ -3628,7 +3678,7 @@ async function verifyFillReadback(target, selector, expectedValue) {
|
|
|
3628
3678
|
if (!el) return null;
|
|
3629
3679
|
const tag = el.tagName ? el.tagName.toLowerCase() : "";
|
|
3630
3680
|
let actual = "";
|
|
3631
|
-
${SEGMENT_OWNER_JS}
|
|
3681
|
+
${segmented_date_spinbutton_1.SEGMENT_OWNER_JS}
|
|
3632
3682
|
if (isDateSegment(el)) {
|
|
3633
3683
|
return { outcome: "segment", postValue: el.value || el.getAttribute("aria-valuenow") || "", tag };
|
|
3634
3684
|
}
|
|
@@ -4265,6 +4315,47 @@ async function resolvedClickTargetIsSubmitShaped(target, selector, xpathTail = n
|
|
|
4265
4315
|
return false;
|
|
4266
4316
|
}
|
|
4267
4317
|
}
|
|
4318
|
+
/**
|
|
4319
|
+
* Canonical key for an absolute xpath so Stagehand's `/html/body/div/button`
|
|
4320
|
+
* (index omitted when sole of its tag) and this module's `/html[1]/body[1]/
|
|
4321
|
+
* div[1]/button[1]` compare equal: a node's first-of-tag and sole-of-tag
|
|
4322
|
+
* forms can never coexist under one parent, so dropping `[1]` is injective.
|
|
4323
|
+
*/
|
|
4324
|
+
function canonicalAbsoluteXpath(selector) {
|
|
4325
|
+
const body = xpathBodyForEvaluate(selector);
|
|
4326
|
+
return body?.startsWith("/") ? body.replace(/\[1\]/g, "").toLowerCase() : null;
|
|
4327
|
+
}
|
|
4328
|
+
/**
|
|
4329
|
+
* Canonical xpaths of every control on the page that is submit-shaped right
|
|
4330
|
+
* now, read before an act clicks anything. The act performs the click itself,
|
|
4331
|
+
* and a view swap removes the control with the page, so the only read that
|
|
4332
|
+
* cannot be blinded by the transition is one taken beforehand — and one that
|
|
4333
|
+
* does not depend on observe having resolved the same control the act later
|
|
4334
|
+
* clicks. Empty on any read failure: absence never manufactures a shape.
|
|
4335
|
+
*/
|
|
4336
|
+
async function readSubmitShapedControlXpaths(target) {
|
|
4337
|
+
const expr = `(() => {
|
|
4338
|
+
const xpathOf = ${XPATH_OF_FN_SRC};
|
|
4339
|
+
const isSubmitShaped = ${submit_control_1.SUBMIT_SHAPE_EXPR};
|
|
4340
|
+
const out = [];
|
|
4341
|
+
for (const el of document.querySelectorAll("button, input, a, [role='button'], [type='submit'], [onclick], [tabindex]")) {
|
|
4342
|
+
if (isSubmitShaped(el)) out.push(xpathOf(el));
|
|
4343
|
+
}
|
|
4344
|
+
return out;
|
|
4345
|
+
})()`;
|
|
4346
|
+
try {
|
|
4347
|
+
const result = await target.evaluate(expr);
|
|
4348
|
+
if (!Array.isArray(result))
|
|
4349
|
+
return new Set();
|
|
4350
|
+
return new Set(result
|
|
4351
|
+
.filter((x) => typeof x === "string")
|
|
4352
|
+
.map(canonicalAbsoluteXpath)
|
|
4353
|
+
.filter((x) => x !== null));
|
|
4354
|
+
}
|
|
4355
|
+
catch {
|
|
4356
|
+
return new Set();
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4268
4359
|
/**
|
|
4269
4360
|
* True when the primary (Stagehand-recorded) xpath is unresolvable and only
|
|
4270
4361
|
* `xpathTailForRetarget`'s loose tag+same-tag-sibling-position re-anchor
|
|
@@ -5468,7 +5559,7 @@ function pickSegmentedDateGroup(instruction, groups, value, compatible) {
|
|
|
5468
5559
|
// A group under another heading is not the field the step names, however
|
|
5469
5560
|
// well its label matches: the step's own words must find the section.
|
|
5470
5561
|
// A section that only repeats the group's own label is no heading at all
|
|
5471
|
-
// (
|
|
5562
|
+
// (a widget may enumerate "From*" as both), so it cannot name another section.
|
|
5472
5563
|
const namesOtherSection = (group) => {
|
|
5473
5564
|
const section = segmentedDateTokens(group.section ?? "");
|
|
5474
5565
|
const label = segmentedDateTokens(group.label ?? "");
|
|
@@ -5529,7 +5620,7 @@ function pickSegmentedDateGroup(instruction, groups, value, compatible) {
|
|
|
5529
5620
|
if (leaders.length > 1) {
|
|
5530
5621
|
// A nested group is one wrapper's slice of a larger date widget (e.g. the
|
|
5531
5622
|
// Year half of "From MM/YYYY"), so it only ties with a standalone group of
|
|
5532
|
-
// the same label when no section text separates them —
|
|
5623
|
+
// the same label when no section text separates them — a form may render
|
|
5533
5624
|
// "Work Experience 1 From" and "Education 1 From" with no heading the
|
|
5534
5625
|
// enumerator can see. The standalone group is the field the step names.
|
|
5535
5626
|
const standalone = leaders.filter((i) => !groups[i]?.nested);
|
|
@@ -5542,71 +5633,13 @@ function pickSegmentedDateGroup(instruction, groups, value, compatible) {
|
|
|
5542
5633
|
const only = groups.flatMap((g, i) => (compatible(g) && !g.nested ? [i] : []));
|
|
5543
5634
|
return only.length === 1 ? (only[0] ?? null) : null;
|
|
5544
5635
|
}
|
|
5545
|
-
/**
|
|
5546
|
-
* In-page helpers shared by the date-segment enumeration and the fill
|
|
5547
|
-
* readback so "is this spinbutton one segment of a multi-segment widget" has a
|
|
5548
|
-
* single definition; defines `spins`, `ownerOf` and `isDateSegment`.
|
|
5549
|
-
*/
|
|
5550
|
-
const SEGMENT_OWNER_JS = `
|
|
5551
|
-
const hiddenAttr = (node) => node.hasAttribute("hidden") || node.getAttribute("aria-hidden") === "true";
|
|
5552
|
-
const styleOf = (node) => {
|
|
5553
|
-
try { return typeof getComputedStyle === "function" ? getComputedStyle(node) : window.getComputedStyle(node); } catch { return null; }
|
|
5554
|
-
};
|
|
5555
|
-
const isHidden = (el) => {
|
|
5556
|
-
for (let node = el; node && node.nodeType === 1; node = node.parentElement) {
|
|
5557
|
-
if (hiddenAttr(node)) return true;
|
|
5558
|
-
const style = styleOf(node);
|
|
5559
|
-
if (style && (style.display === "none" || style.visibility === "hidden")) return true;
|
|
5560
|
-
}
|
|
5561
|
-
return false;
|
|
5562
|
-
};
|
|
5563
|
-
const spins = Array.from(document.querySelectorAll("input[role='spinbutton']")).filter((el) => !isHidden(el));
|
|
5564
|
-
const norm = (s) => (s || "").replace(/\\s+/g, " ").trim();
|
|
5565
|
-
const idText = (ids) => (ids || "").split(/\\s+/).map((id) => {
|
|
5566
|
-
const ref = id ? document.getElementById(id) : null;
|
|
5567
|
-
return ref ? norm(ref.textContent) : "";
|
|
5568
|
-
}).join(" ");
|
|
5569
|
-
const legendOf = (node) => {
|
|
5570
|
-
const legend = node.tagName === "FIELDSET" ? node.querySelector(":scope > legend") : null;
|
|
5571
|
-
return legend ? norm(legend.textContent) : "";
|
|
5572
|
-
};
|
|
5573
|
-
const namedBy = (node) => [legendOf(node), norm(node.getAttribute("aria-label")), idText(node.getAttribute("aria-labelledby"))].filter(Boolean).join(" ");
|
|
5574
|
-
const tiedToSpin = (l) => l.querySelector("input,[role='spinbutton']") || (l.getAttribute("for") && spins.some((sp) => sp.id === l.getAttribute("for")));
|
|
5575
|
-
const hasCaption = (node) => namedBy(node) !== "" || Array.from(node.querySelectorAll("label,legend")).some((l) => !tiedToSpin(l));
|
|
5576
|
-
const captionOwnerOf = (el) => {
|
|
5577
|
-
const explicit = el.closest("fieldset,[role='group']");
|
|
5578
|
-
if (explicit) return explicit;
|
|
5579
|
-
for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
|
|
5580
|
-
if (hasCaption(node)) return node;
|
|
5581
|
-
}
|
|
5582
|
-
return null;
|
|
5583
|
-
};
|
|
5584
|
-
const bareSpins = spins.filter((sp) => !captionOwnerOf(sp));
|
|
5585
|
-
const hasOtherControl = (node) => Array.from(node.querySelectorAll("input,select,textarea")).some((c) => !spins.includes(c) && c.type !== "hidden");
|
|
5586
|
-
// Segments that each sit in their own uncaptioned wrapper share no
|
|
5587
|
-
// fieldset/caption, so the nearest ancestor holding several bare spinbuttons
|
|
5588
|
-
// (and no other form control) is the widget they belong to.
|
|
5589
|
-
const bareOwnerOf = (el) => {
|
|
5590
|
-
for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
|
|
5591
|
-
if (hasOtherControl(node)) break;
|
|
5592
|
-
if (bareSpins.filter((sp) => node.contains(sp)).length > 1) return node;
|
|
5593
|
-
}
|
|
5594
|
-
return el.parentElement;
|
|
5595
|
-
};
|
|
5596
|
-
const ownerOf = (el) => captionOwnerOf(el) || bareOwnerOf(el);
|
|
5597
|
-
const isDateSegment = (el) => {
|
|
5598
|
-
if (!el.getAttribute || el.getAttribute("role") !== "spinbutton" || !spins.includes(el)) return false;
|
|
5599
|
-
const box = ownerOf(el);
|
|
5600
|
-
return spins.filter((sp) => ownerOf(sp) === box).length > 1;
|
|
5601
|
-
};
|
|
5602
|
-
`;
|
|
5603
5636
|
const SEGMENTED_DATE_MARKER_ATTR = "data-barnacle-segdate";
|
|
5604
5637
|
/** Exported so tests can run the real DOM enumeration against markup. */
|
|
5605
5638
|
exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
5606
5639
|
const attr = ${JSON.stringify(SEGMENTED_DATE_MARKER_ATTR)};
|
|
5607
5640
|
const stale = document.querySelectorAll("[" + attr + "]");
|
|
5608
5641
|
stale.forEach((el) => el.removeAttribute(attr));
|
|
5609
|
-
${SEGMENT_OWNER_JS}
|
|
5642
|
+
${segmented_date_spinbutton_1.SEGMENT_OWNER_JS}
|
|
5610
5643
|
if (spins.length === 0) return { groups: [] };
|
|
5611
5644
|
const headingSibling = (node) => {
|
|
5612
5645
|
let prevSib = node.previousElementSibling;
|
|
@@ -5660,15 +5693,21 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5660
5693
|
const prior = seg.getAttribute(attr);
|
|
5661
5694
|
seg.setAttribute(attr, prior ? prior + " " + token : token);
|
|
5662
5695
|
};
|
|
5663
|
-
const addGroup = (segs, box, nested) => {
|
|
5696
|
+
const addGroup = (segs, box, nested, outer) => {
|
|
5664
5697
|
const gi = groups.length;
|
|
5665
5698
|
segs.forEach((seg, si) => mark(seg, gi + ":" + si));
|
|
5666
5699
|
let inner = null;
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5700
|
+
// A wrapper holding one segment is captioned by the nearest ancestor that
|
|
5701
|
+
// holds that segment and no other: a caption over several segments names
|
|
5702
|
+
// the whole date, never one slice of it.
|
|
5703
|
+
const top = outer || box;
|
|
5704
|
+
const soleOwner = (node) => !outer || spins.filter((sp) => node.contains(sp)).length === segs.length;
|
|
5705
|
+
for (let node = segs[0].parentElement; node && node !== top && top.contains(node) && !inner; node = node.parentElement) {
|
|
5706
|
+
if (segs.every((seg) => node.contains(seg)) && hasCaption(node) && soleOwner(node)) inner = node;
|
|
5707
|
+
}
|
|
5708
|
+
if (inner === box) inner = null;
|
|
5670
5709
|
const labelBox = inner || box;
|
|
5671
|
-
const own = namedBy(labelBox) || ownCaption(labelBox) || segs.map((seg) => idText(seg.getAttribute("aria-labelledby"))).filter(Boolean).join(" ");
|
|
5710
|
+
const own = namedBy(labelBox) || ownCaption(labelBox) || segs.map((seg) => idText(seg.getAttribute("aria-labelledby"))).filter(Boolean).join(" ") || Array.from(new Set(segs.map((seg) => ownKey(seg)).filter(Boolean))).join(" ");
|
|
5672
5711
|
const section = sectionOf(labelBox, Boolean(inner));
|
|
5673
5712
|
groups.push({
|
|
5674
5713
|
segmentMaxes: segs.map((seg, si) => maxOf(seg, si, segs.length)),
|
|
@@ -5691,7 +5730,7 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5691
5730
|
}
|
|
5692
5731
|
const wrap = seg.closest("label");
|
|
5693
5732
|
if (wrap) parts.push(norm(wrap.textContent));
|
|
5694
|
-
return keyOf(parts.filter(Boolean).join(" "));
|
|
5733
|
+
return keyOf(Array.from(new Set(parts.filter(Boolean))).join(" "));
|
|
5695
5734
|
};
|
|
5696
5735
|
// A container's legend names the whole section; the field a segment belongs
|
|
5697
5736
|
// to is the caption naming it, or the caption nearest before it with no other
|
|
@@ -5705,8 +5744,15 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5705
5744
|
if (el === boxLegend) return;
|
|
5706
5745
|
if (el.matches("label,legend")) {
|
|
5707
5746
|
const target = el.getAttribute("for");
|
|
5708
|
-
if (isHidden(el) || el.querySelector("input,select,textarea,[role='spinbutton']")
|
|
5709
|
-
|
|
5747
|
+
if (isHidden(el) || el.querySelector("input,select,textarea,[role='spinbutton']")) return;
|
|
5748
|
+
const text = keyOf(el.textContent);
|
|
5749
|
+
// A label tied to one of these segments names the field that segment
|
|
5750
|
+
// opens; its text is only a part word ("Month") when it names nothing.
|
|
5751
|
+
if (target && box.querySelector("[id='" + target + "']")) {
|
|
5752
|
+
if (own.has(box.querySelector("[id='" + target + "']")) && text) current = text;
|
|
5753
|
+
return;
|
|
5754
|
+
}
|
|
5755
|
+
current = text;
|
|
5710
5756
|
return;
|
|
5711
5757
|
}
|
|
5712
5758
|
if (!own.has(el)) {
|
|
@@ -5729,10 +5775,8 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5729
5775
|
}
|
|
5730
5776
|
// A shared fieldset can hold several independent dates (From and To year
|
|
5731
5777
|
// spinbuttons side by side); each wrapper is then its own candidate group.
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
return;
|
|
5735
|
-
}
|
|
5778
|
+
const split = byParent.size > 1;
|
|
5779
|
+
if (split) byParent.forEach((inner, parent) => nestedCandidates.push([inner, parent, box]));
|
|
5736
5780
|
const keys = fieldKeys(segs, box);
|
|
5737
5781
|
const runs = [];
|
|
5738
5782
|
segs.forEach((seg, si) => {
|
|
@@ -5740,9 +5784,13 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5740
5784
|
if (last && last.key === keys[si]) last.segs.push(seg);
|
|
5741
5785
|
else runs.push({ key: keys[si], segs: [seg] });
|
|
5742
5786
|
});
|
|
5743
|
-
|
|
5787
|
+
// Segments that each sit in their own display wrapper still form one date
|
|
5788
|
+
// when a shared caption names them, so the caption run is a candidate too;
|
|
5789
|
+
// a run that is exactly one wrapper's segments is already that wrapper's group.
|
|
5790
|
+
const isWrapperGroup = (run) => split && byParent.get(run.segs[0].parentElement).length === run.segs.length && run.segs.every((seg) => seg.parentElement === run.segs[0].parentElement);
|
|
5791
|
+
runs.filter((run) => run.key && !isWrapperGroup(run)).forEach((run) => captionRuns.push({ run, box, outerIndex, whole: run.segs.length === segs.length }));
|
|
5744
5792
|
});
|
|
5745
|
-
nestedCandidates.forEach(([inner, parent]) => addGroup(inner, parent, true));
|
|
5793
|
+
nestedCandidates.forEach(([inner, parent, outer]) => addGroup(inner, parent, true, outer));
|
|
5746
5794
|
captionRuns.forEach(({ run, box, outerIndex, whole }) => {
|
|
5747
5795
|
const outer = groups[outerIndex];
|
|
5748
5796
|
const same = (a, b) => a.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim() === b.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
@@ -5771,21 +5819,38 @@ const SEGMENTED_DATE_CLEAR_EXPR = `(() => {
|
|
|
5771
5819
|
*
|
|
5772
5820
|
* Why this exists: such widgets keep each segment's value in component state
|
|
5773
5821
|
* driven by `keydown`/`beforeinput`; `fill`/`insertText` bypasses that and
|
|
5774
|
-
* garbles the segments. Returns a marker id on commit, or null on
|
|
5775
|
-
*
|
|
5776
|
-
*
|
|
5822
|
+
* garbles the segments. Returns a marker id on commit, or null on any
|
|
5823
|
+
* non-commit. Callers that must tell "not applicable" from "refused" use
|
|
5824
|
+
* `runSegmentedDateSpinbuttonPrimitive`.
|
|
5777
5825
|
*/
|
|
5778
5826
|
async function trySegmentedDateSpinbuttonPrimitive(params) {
|
|
5827
|
+
const outcome = await runSegmentedDateSpinbuttonPrimitive(params);
|
|
5828
|
+
return outcome.kind === "committed" ? outcome.targetId : null;
|
|
5829
|
+
}
|
|
5830
|
+
/**
|
|
5831
|
+
* Same as `trySegmentedDateSpinbuttonPrimitive` but distinguishes "this step
|
|
5832
|
+
* is not a segmented date here" (fall through to the cascade) from "a
|
|
5833
|
+
* width-compatible segment group exists and the primitive could not deliver"
|
|
5834
|
+
* (refused: the cascade would write the value via insertText and garble the
|
|
5835
|
+
* segments, so the caller must fail the step instead).
|
|
5836
|
+
*/
|
|
5837
|
+
async function runSegmentedDateSpinbuttonPrimitive(params) {
|
|
5779
5838
|
const { page, target, instruction, logger } = params;
|
|
5839
|
+
const notApplicable = { kind: "not-applicable" };
|
|
5840
|
+
const refuse = (reason) => {
|
|
5841
|
+
logger.warn(`segmented date primitive: refused: ${reason}`);
|
|
5842
|
+
return { kind: "refused", reason };
|
|
5843
|
+
};
|
|
5844
|
+
let groupChosen = false;
|
|
5780
5845
|
const whole = parseSegmentedDateValue(instruction);
|
|
5781
5846
|
const part = parseSegmentedPartStep(instruction);
|
|
5782
5847
|
if (!whole && !part)
|
|
5783
|
-
return
|
|
5848
|
+
return notApplicable;
|
|
5784
5849
|
const sendCDP = frameCdpSender(page, target);
|
|
5785
5850
|
try {
|
|
5786
5851
|
const enumerated = (await target.evaluate(exports.SEGMENTED_DATE_ENUMERATE_EXPR));
|
|
5787
5852
|
if (!enumerated?.groups?.length)
|
|
5788
|
-
return
|
|
5853
|
+
return notApplicable;
|
|
5789
5854
|
const wholeGi = whole ? resolveSegmentedDateGroup(instruction, enumerated.groups) : null;
|
|
5790
5855
|
const partStep = wholeGi === null ? part : null;
|
|
5791
5856
|
const value = (wholeGi !== null ? whole : part);
|
|
@@ -5801,10 +5866,18 @@ async function trySegmentedDateSpinbuttonPrimitive(params) {
|
|
|
5801
5866
|
? pickSegmentedDateGroup(instruction, enumerated.groups, partStep, partFits)
|
|
5802
5867
|
: null);
|
|
5803
5868
|
if (gi === null) {
|
|
5804
|
-
logger.info("segmented date primitive: no unambiguous group; falling through");
|
|
5805
5869
|
await target.evaluate(SEGMENTED_DATE_CLEAR_EXPR);
|
|
5806
|
-
|
|
5870
|
+
const widthCompatible = enumerated.groups.some((group) => (whole !== null &&
|
|
5871
|
+
group.segmentMaxes.reduce((sum, max) => sum + String(max).length, 0) ===
|
|
5872
|
+
whole.digits.length) ||
|
|
5873
|
+
(partStep !== null && partFits(group)));
|
|
5874
|
+
if (widthCompatible) {
|
|
5875
|
+
return refuse("a width-compatible date group exists but the target group is ambiguous");
|
|
5876
|
+
}
|
|
5877
|
+
logger.info("segmented date primitive: no compatible group; falling through");
|
|
5878
|
+
return notApplicable;
|
|
5807
5879
|
}
|
|
5880
|
+
groupChosen = true;
|
|
5808
5881
|
const maxes = enumerated.groups[gi]?.segmentMaxes ?? [];
|
|
5809
5882
|
const widths = maxes.map((max) => String(max).length);
|
|
5810
5883
|
const onlySegment = partStep ? partOf(enumerated.groups[gi]) : null;
|
|
@@ -5901,15 +5974,16 @@ async function trySegmentedDateSpinbuttonPrimitive(params) {
|
|
|
5901
5974
|
actual = await readSegments();
|
|
5902
5975
|
await target.evaluate(SEGMENTED_DATE_CLEAR_EXPR);
|
|
5903
5976
|
if (!matches(actual)) {
|
|
5904
|
-
|
|
5905
|
-
return null;
|
|
5977
|
+
return refuse(`readback ${maskDateDigits(JSON.stringify(actual))} != ${maskDateDigits(JSON.stringify(expected))}`);
|
|
5906
5978
|
}
|
|
5907
5979
|
logger.info(`segmented date primitive: committed segments ${maskDateDigits(JSON.stringify(actual))}`);
|
|
5908
|
-
return `segmented-date-${gi}
|
|
5980
|
+
return { kind: "committed", targetId: `segmented-date-${gi}` };
|
|
5909
5981
|
}
|
|
5910
5982
|
catch (err) {
|
|
5983
|
+
if (groupChosen)
|
|
5984
|
+
return refuse(`delivery threw: ${(0, errors_1.toErrorMessage)(err)}`);
|
|
5911
5985
|
logger.warn(`segmented date primitive: threw: ${(0, errors_1.toErrorMessage)(err)}; falling through`);
|
|
5912
|
-
return
|
|
5986
|
+
return notApplicable;
|
|
5913
5987
|
}
|
|
5914
5988
|
}
|
|
5915
5989
|
/**
|
|
@@ -6275,15 +6349,103 @@ function isNavigatingEvaluateRejection(error) {
|
|
|
6275
6349
|
message.includes("no frame for given id found") ||
|
|
6276
6350
|
GENERIC_CONTENT_FREE_EVAL_REJECTION_MESSAGES.has(message.trim()));
|
|
6277
6351
|
}
|
|
6352
|
+
const DEFAULT_POST_CLICK_SETTLE_IO = {
|
|
6353
|
+
settle: settleWithPageQuiescence,
|
|
6354
|
+
snapshot: snapshotPage,
|
|
6355
|
+
};
|
|
6356
|
+
/**
|
|
6357
|
+
* Re-settle rounds granted to a post-click page whose header flipped but whose
|
|
6358
|
+
* body has not re-rendered yet. Each round waits the settle's own idle
|
|
6359
|
+
* allowance, so the rounds together span the settle's MAX_MS ceiling rather
|
|
6360
|
+
* than a fixed guess; a held-still text ends the loop sooner.
|
|
6361
|
+
*/
|
|
6362
|
+
const HEADER_ONLY_RESETTLE_ROUNDS = Math.floor(click_settle_1.MAX_MS / click_settle_1.IDLE_GIVE_UP_MS);
|
|
6363
|
+
/**
|
|
6364
|
+
* Re-reads the post-click page while only its header has flipped. One
|
|
6365
|
+
* settle-then-snapshot moment can land between a header flip and the body
|
|
6366
|
+
* re-render that follows it, so the verdict waits for the body or for the text
|
|
6367
|
+
* to hold still through the whole re-settle budget, and only then reads the
|
|
6368
|
+
* header flip as the page's final state (`settled`): a single quiet round
|
|
6369
|
+
* cannot distinguish a finished page from a body that re-renders later. Each round settles against the
|
|
6370
|
+
* pre-click text, so the header flip stays movement and the quiet window, not the silent no-op
|
|
6371
|
+
* give-up, decides how long a body re-render is waited for. Bounded; a page whose body already swapped
|
|
6372
|
+
* returns at once, and one showing no change gets a single probe round so a
|
|
6373
|
+
* re-render that lands after the first snapshot is still seen.
|
|
6374
|
+
*/
|
|
6375
|
+
async function resettleThroughHeaderOnlyFlip(page, target, signalCounter, pre, post, io = DEFAULT_POST_CLICK_SETTLE_IO) {
|
|
6376
|
+
let current = post;
|
|
6377
|
+
for (let round = 0; round < HEADER_ONLY_RESETTLE_ROUNDS; round++) {
|
|
6378
|
+
const moved = (0, phantom_click_1.hasVisibleTextChanged)(pre, current);
|
|
6379
|
+
if (moved && (0, phantom_click_1.hasPageBodySwapped)(pre, current))
|
|
6380
|
+
return round > 0 ? { ...current, settled: true } : current;
|
|
6381
|
+
if (!moved) {
|
|
6382
|
+
// A click unseen to move anything gets one settle-free re-read, so a genuine
|
|
6383
|
+
// no-op adds no wait to its give-up bound.
|
|
6384
|
+
if (round > 0)
|
|
6385
|
+
return current;
|
|
6386
|
+
const probe = await io.snapshot(target, signalCounter, page);
|
|
6387
|
+
if (!(0, phantom_click_1.hasVisibleTextChanged)(pre, probe))
|
|
6388
|
+
return current;
|
|
6389
|
+
current = probe;
|
|
6390
|
+
continue;
|
|
6391
|
+
}
|
|
6392
|
+
await io.settle(page, target, signalCounter, current.networkCount, true, pre.visibleTextSignature, pre.visibleTextDigest);
|
|
6393
|
+
const next = await io.snapshot(target, signalCounter, page);
|
|
6394
|
+
const heldStill = !(0, phantom_click_1.hasVisibleTextChanged)(current, next);
|
|
6395
|
+
current = next;
|
|
6396
|
+
if (heldStill && !(0, phantom_click_1.hasPageBodySwapped)(pre, current))
|
|
6397
|
+
return { ...current, settled: true };
|
|
6398
|
+
}
|
|
6399
|
+
return current;
|
|
6400
|
+
}
|
|
6401
|
+
/**
|
|
6402
|
+
* Text baseline of the step-start page with the attempt's own network count, so
|
|
6403
|
+
* a late re-render an earlier attempt already absorbed into `pre` still reads as
|
|
6404
|
+
* movement to the settle while only this click's requests count as its own.
|
|
6405
|
+
*/
|
|
6406
|
+
function withNetworkBaseline(baseline, pre) {
|
|
6407
|
+
return { ...baseline, networkCount: pre.networkCount };
|
|
6408
|
+
}
|
|
6409
|
+
/**
|
|
6410
|
+
* Single owner of the settle -> snapshot -> header-only resettle sequence, so
|
|
6411
|
+
* every post-click site applies the same settled-ness rule. A click that cannot
|
|
6412
|
+
* trigger a round-trip is snapshotted as-is. The resettle does not depend on
|
|
6413
|
+
* `waitForMutations`, since an unflagged step can still advance a page whose
|
|
6414
|
+
* body re-renders late.
|
|
6415
|
+
*/
|
|
6416
|
+
async function settleSnapshotAndResettle(target, signalCounter, page, pre, mayTriggerRoundTrip, waitForMutations, io) {
|
|
6417
|
+
if (page && mayTriggerRoundTrip)
|
|
6418
|
+
await io.settle(page, target, signalCounter, pre.networkCount, waitForMutations, pre.visibleTextSignature, pre.visibleTextDigest);
|
|
6419
|
+
const post = await io.snapshot(target, signalCounter, page);
|
|
6420
|
+
return page && mayTriggerRoundTrip
|
|
6421
|
+
? resettleThroughHeaderOnlyFlip(page, target, signalCounter, pre, post, io)
|
|
6422
|
+
: post;
|
|
6423
|
+
}
|
|
6278
6424
|
/**
|
|
6279
6425
|
* Settles after a click and returns the post-click snapshot, so the settle
|
|
6280
6426
|
* wait and the phantom classifier decide from the same reader and baseline.
|
|
6281
6427
|
* Without a page there is nothing to wait on and the snapshot is taken as-is.
|
|
6428
|
+
* `io` lets a caller that owns its own seam (recon-browser's pre-replan
|
|
6429
|
+
* short-circuit) route the settle and the snapshot through the functions it
|
|
6430
|
+
* already imports, so the settled read there is the same one every other site
|
|
6431
|
+
* makes.
|
|
6432
|
+
*/
|
|
6433
|
+
async function settleAndSnapshotPostClick(target, signalCounter, page, pre, waitForMutations = true, io = DEFAULT_POST_CLICK_SETTLE_IO) {
|
|
6434
|
+
return settleSnapshotAndResettle(target, signalCounter, page, pre, true, waitForMutations, io);
|
|
6435
|
+
}
|
|
6436
|
+
/**
|
|
6437
|
+
* Single mid-flow post-click advance read: picks the advance baseline (the
|
|
6438
|
+
* step-start snapshot for a mid-flow submit, `pre` otherwise), gives it the
|
|
6439
|
+
* attempt's network count, settles, re-settles through a header-only flip and
|
|
6440
|
+
* judges advance against that same baseline, so every site credits a page that
|
|
6441
|
+
* advanced from step start once it is quiescent.
|
|
6282
6442
|
*/
|
|
6283
|
-
async function
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6443
|
+
async function readMidFlowPostClickAdvance(args) {
|
|
6444
|
+
const advanceBaseline = args.midFlowSubmit
|
|
6445
|
+
? withNetworkBaseline(args.stepStartSnapshot, args.pre)
|
|
6446
|
+
: args.pre;
|
|
6447
|
+
const post = await settleSnapshotAndResettle(args.target, args.signalCounter, args.page, advanceBaseline, args.mayTriggerRoundTrip ?? true, args.waitForMutations ?? true, args.io ?? DEFAULT_POST_CLICK_SETTLE_IO);
|
|
6448
|
+
return { post, advanceBaseline, pageAdvanced: (0, phantom_click_1.hasSettledPageAdvanced)(advanceBaseline, post) };
|
|
6287
6449
|
}
|
|
6288
6450
|
/**
|
|
6289
6451
|
* Shared in-page discovery of the widget's callback, used identically by
|
|
@@ -6511,6 +6673,38 @@ async function injectCaptchaTokenAndSubmit(target, token, responseField = "h-cap
|
|
|
6511
6673
|
await target.evaluate(dispatchChangeExpr).catch(() => undefined);
|
|
6512
6674
|
return { injected, hasForm, callbackDiscovered };
|
|
6513
6675
|
}
|
|
6676
|
+
/**
|
|
6677
|
+
* Single assembly point for the mid-flow half of a phantom-click verdict, so
|
|
6678
|
+
* the primary attempt and every bypass/retry site extend the same
|
|
6679
|
+
* pre-URL destination exemption for a mid-flow submit, granted only on a
|
|
6680
|
+
* settled page advance, identically instead of each re-deriving it.
|
|
6681
|
+
*/
|
|
6682
|
+
function midFlowDestinationPlausible(params) {
|
|
6683
|
+
const { step, post, midFlowSubmit } = params;
|
|
6684
|
+
const baseline = params.advanceBaseline;
|
|
6685
|
+
return ((params.destinationPlausible ?? (0, phantom_click_1.isPlausibleStepDestination)(step, post.url)) ||
|
|
6686
|
+
(midFlowSubmit &&
|
|
6687
|
+
(0, phantom_click_1.hasSettledPageAdvanced)(baseline, post) &&
|
|
6688
|
+
(0, phantom_click_1.isPlausibleStepDestination)(step, post.url, baseline.url)));
|
|
6689
|
+
}
|
|
6690
|
+
/**
|
|
6691
|
+
* Classifies a submit-shaped click through the shared mid-flow verdict
|
|
6692
|
+
* assembly used by the primary path and the n+16 / deep-submit / captcha /
|
|
6693
|
+
* child-frame bypass sites.
|
|
6694
|
+
*/
|
|
6695
|
+
function classifySubmitShapedClick(params) {
|
|
6696
|
+
const { pre, post, midFlowSubmit } = params;
|
|
6697
|
+
return (0, phantom_click_1.classifyPhantomClick)({
|
|
6698
|
+
actResultSuccess: params.actResultSuccess ?? true,
|
|
6699
|
+
pre,
|
|
6700
|
+
post,
|
|
6701
|
+
elementStateChanged: params.elementStateChanged,
|
|
6702
|
+
isSubmitShapedStep: midFlowSubmit || (params.isSubmitShapedStep ?? true),
|
|
6703
|
+
isMidFlowSubmitStep: midFlowSubmit,
|
|
6704
|
+
advanceBaseline: params.advanceBaseline,
|
|
6705
|
+
destinationPlausible: midFlowDestinationPlausible(params),
|
|
6706
|
+
});
|
|
6707
|
+
}
|
|
6514
6708
|
/**
|
|
6515
6709
|
* Explicit submit fallback for {@link injectCaptchaTokenAndSubmit} callers:
|
|
6516
6710
|
* used only when the caller's own transition poll observed no advance after
|
|
@@ -6582,6 +6776,8 @@ async function submitCaptchaGatedForm(target, responseField = "h-captcha-respons
|
|
|
6582
6776
|
const pre = verification
|
|
6583
6777
|
? await snapshotPage(target, verification.signalCounter, verification.page)
|
|
6584
6778
|
: null;
|
|
6779
|
+
if (pre)
|
|
6780
|
+
await armPageQuiescence(target);
|
|
6585
6781
|
const clickResult = await target
|
|
6586
6782
|
.evaluate((0, submit_control_1.buildClickByDeepIndexExpr)(top.deepIndex))
|
|
6587
6783
|
.catch((err) => {
|
|
@@ -6592,14 +6788,30 @@ async function submitCaptchaGatedForm(target, responseField = "h-captcha-respons
|
|
|
6592
6788
|
if (clickResult.clicked) {
|
|
6593
6789
|
if (!pre || !verification)
|
|
6594
6790
|
return true;
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6791
|
+
// An omitted flag means the caller could not tell final from mid-flow;
|
|
6792
|
+
// derive from the step like the in-cascade sites rather than going strict.
|
|
6793
|
+
const midFlowSubmit = verification.isMidFlowSubmitStep ??
|
|
6794
|
+
isMidFlowSubmitShapedStep({
|
|
6795
|
+
step: verification.step,
|
|
6796
|
+
submitStep: false,
|
|
6797
|
+
isFinalStep: false,
|
|
6798
|
+
flowHasSubmitSemantics: false,
|
|
6799
|
+
elementSubmitShaped: true,
|
|
6800
|
+
});
|
|
6801
|
+
const { post, advanceBaseline } = await readMidFlowPostClickAdvance({
|
|
6802
|
+
target,
|
|
6803
|
+
signalCounter: verification.signalCounter,
|
|
6804
|
+
page: verification.page,
|
|
6805
|
+
pre,
|
|
6806
|
+
stepStartSnapshot: verification.stepStartSnapshot ?? pre,
|
|
6807
|
+
midFlowSubmit,
|
|
6808
|
+
});
|
|
6809
|
+
const verdict = classifySubmitShapedClick({
|
|
6810
|
+
step: verification.step,
|
|
6598
6811
|
pre,
|
|
6599
6812
|
post,
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
destinationPlausible: (0, phantom_click_1.isPlausibleStepDestination)(verification.step, post.url, verification.isMidFlowSubmitStep ? pre.url : undefined),
|
|
6813
|
+
midFlowSubmit,
|
|
6814
|
+
advanceBaseline,
|
|
6603
6815
|
});
|
|
6604
6816
|
if (verdict !== "phantom")
|
|
6605
6817
|
return true;
|
|
@@ -6613,14 +6825,20 @@ async function submitCaptchaGatedForm(target, responseField = "h-captcha-respons
|
|
|
6613
6825
|
throw err;
|
|
6614
6826
|
});
|
|
6615
6827
|
if (runnerUpClickResult.clicked) {
|
|
6616
|
-
const runnerUpPost = await
|
|
6617
|
-
|
|
6618
|
-
|
|
6828
|
+
const { post: runnerUpPost, advanceBaseline: runnerUpBaseline } = await readMidFlowPostClickAdvance({
|
|
6829
|
+
target,
|
|
6830
|
+
signalCounter: verification.signalCounter,
|
|
6831
|
+
page: verification.page,
|
|
6832
|
+
pre: post,
|
|
6833
|
+
stepStartSnapshot: verification.stepStartSnapshot ?? pre,
|
|
6834
|
+
midFlowSubmit,
|
|
6835
|
+
});
|
|
6836
|
+
const runnerUpVerdict = classifySubmitShapedClick({
|
|
6837
|
+
step: verification.step,
|
|
6619
6838
|
pre: post,
|
|
6620
6839
|
post: runnerUpPost,
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
destinationPlausible: (0, phantom_click_1.isPlausibleStepDestination)(verification.step, runnerUpPost.url, verification.isMidFlowSubmitStep ? post.url : undefined),
|
|
6840
|
+
midFlowSubmit,
|
|
6841
|
+
advanceBaseline: runnerUpBaseline,
|
|
6624
6842
|
});
|
|
6625
6843
|
if (runnerUpVerdict !== "phantom")
|
|
6626
6844
|
return true;
|
|
@@ -9884,6 +10102,7 @@ function primitiveOptionLabel(option, questionLabel, redactValue, fromFill = fal
|
|
|
9884
10102
|
*/
|
|
9885
10103
|
async function probeChildFrameSubmitFallback(params) {
|
|
9886
10104
|
const { page, signalCounter, step, isMidFlowSubmitStep } = params;
|
|
10105
|
+
const midFlowSubmit = isMidFlowSubmitStep === true;
|
|
9887
10106
|
// `page.mainFrameId`/`page.frames` are read defensively: some call sites
|
|
9888
10107
|
// (and their test fakes) model a `Page` that never attaches child frames at
|
|
9889
10108
|
// all and doesn't implement this pair, which is indistinguishable here from
|
|
@@ -9933,17 +10152,27 @@ async function probeChildFrameSubmitFallback(params) {
|
|
|
9933
10152
|
// biome-ignore lint/style/noNonNullAssertion: guarded by the length check above
|
|
9934
10153
|
const top = ranked[0];
|
|
9935
10154
|
const pre = await snapshotPage(target, signalCounter, page);
|
|
10155
|
+
await armPageQuiescence(target);
|
|
9936
10156
|
const clickResult = (await Promise.resolve(target.evaluate((0, submit_control_1.buildClickByDeepIndexExpr)(top.deepIndex))).catch(() => ({ clicked: false })));
|
|
9937
10157
|
if (!clickResult.clicked)
|
|
9938
10158
|
continue;
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
10159
|
+
// The step-start snapshot describes the resolved frame, not this child
|
|
10160
|
+
// frame, so the first click's own pre-click page is the baseline both
|
|
10161
|
+
// the top pick and the runner-up are judged against.
|
|
10162
|
+
const { post, advanceBaseline } = await readMidFlowPostClickAdvance({
|
|
10163
|
+
target,
|
|
10164
|
+
signalCounter,
|
|
10165
|
+
page,
|
|
10166
|
+
pre,
|
|
10167
|
+
stepStartSnapshot: pre,
|
|
10168
|
+
midFlowSubmit,
|
|
10169
|
+
});
|
|
10170
|
+
const verdict = classifySubmitShapedClick({
|
|
10171
|
+
step,
|
|
9942
10172
|
pre,
|
|
9943
10173
|
post,
|
|
9944
|
-
|
|
9945
|
-
|
|
9946
|
-
destinationPlausible: (0, phantom_click_1.isPlausibleStepDestination)(step, post.url, isMidFlowSubmitStep ? pre.url : undefined),
|
|
10174
|
+
midFlowSubmit,
|
|
10175
|
+
advanceBaseline,
|
|
9947
10176
|
});
|
|
9948
10177
|
if (verdict !== "phantom")
|
|
9949
10178
|
return target;
|
|
@@ -9953,14 +10182,20 @@ async function probeChildFrameSubmitFallback(params) {
|
|
|
9953
10182
|
const runnerUpClickResult = (await Promise.resolve(target.evaluate((0, submit_control_1.buildClickByDeepIndexExpr)(runnerUp.deepIndex))).catch(() => ({ clicked: false })));
|
|
9954
10183
|
if (!runnerUpClickResult.clicked)
|
|
9955
10184
|
continue;
|
|
9956
|
-
const runnerUpPost = await
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
10185
|
+
const { post: runnerUpPost } = await readMidFlowPostClickAdvance({
|
|
10186
|
+
target,
|
|
10187
|
+
signalCounter,
|
|
10188
|
+
page,
|
|
10189
|
+
pre: midFlowSubmit ? post : advanceBaseline,
|
|
10190
|
+
stepStartSnapshot: advanceBaseline,
|
|
10191
|
+
midFlowSubmit,
|
|
10192
|
+
});
|
|
10193
|
+
const runnerUpVerdict = classifySubmitShapedClick({
|
|
10194
|
+
step,
|
|
10195
|
+
pre: post,
|
|
9960
10196
|
post: runnerUpPost,
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
destinationPlausible: (0, phantom_click_1.isPlausibleStepDestination)(step, runnerUpPost.url, isMidFlowSubmitStep ? post.url : undefined),
|
|
10197
|
+
midFlowSubmit,
|
|
10198
|
+
advanceBaseline,
|
|
9964
10199
|
});
|
|
9965
10200
|
if (runnerUpVerdict !== "phantom")
|
|
9966
10201
|
return target;
|
|
@@ -9994,7 +10229,14 @@ async function raceAbortSignal(work, signal) {
|
|
|
9994
10229
|
}
|
|
9995
10230
|
}
|
|
9996
10231
|
async function executeStepWithHealing(params) {
|
|
9997
|
-
const { stagehand, optional, upload, submitStep, captchaGated = false, emailStep = false, emailStepConfig, allocatedInbox = null, sessionProxy = null, flowHasSubmitSemantics:
|
|
10232
|
+
const { stagehand, optional, upload, submitStep, captchaGated = false, emailStep = false, emailStepConfig, allocatedInbox = null, sessionProxy = null, flowHasSubmitSemantics: flowHasSubmitSemanticsParam, stepIndex, totalSteps, phase, signalCounter, recentCaptures, recentCaptureMeta, anthropic, rephraseModel, logger, captureFn, uploadFixture, isFinalStep, submitEndpointPattern, submittedStateSelectors, requireSubmitEndpointMatch, advanceTransitionBodyPattern, successUrlFragments, successPageTitleHints, ownBackendHostnames, knownErrorClassPrefixes, wizardExitButtonLabels, getSuppressedAisdkElementIdErrorCount, trajectory, onStepFailure, onStepHeal, onElementStepStartBaseline, } = params;
|
|
10233
|
+
// An explicitly flagged final step is the flow's submit by definition (see
|
|
10234
|
+
// flowHasSubmitSemantics). An omitted flow-level flag is derived from the same
|
|
10235
|
+
// evidence flowHasSubmitSemantics reads (a submit flag, an endpoint pattern or
|
|
10236
|
+
// requirement), so a last slot with none of them is mid-flow rather than a
|
|
10237
|
+
// final submit held to network/URL proof.
|
|
10238
|
+
const flowSubmitEvidence = submitStep || submitEndpointPattern !== null || requireSubmitEndpointMatch;
|
|
10239
|
+
const flowHasSubmitSemanticsFlag = (flowHasSubmitSemanticsParam ?? flowSubmitEvidence) || (isFinalStep && submitStep);
|
|
9998
10240
|
// Mutable: the `emailStep` code-extract path splices the extracted code
|
|
9999
10241
|
// into this instruction (see the emailStep hook block below) so the
|
|
10000
10242
|
// pre-existing fill cascade downstream picks it up as the value to type
|
|
@@ -10174,7 +10416,11 @@ async function executeStepWithHealing(params) {
|
|
|
10174
10416
|
const stepDeclaredNonSubmit = isStepDeclaredNonSubmit({ step, submitStep });
|
|
10175
10417
|
// Single final-step inference every submit-strictness gate below reads, so a
|
|
10176
10418
|
// declared non-submit veto cannot be honoured at one site and missed at another.
|
|
10177
|
-
|
|
10419
|
+
// Finality goes through isGenuineFinalSubmit like the judge routing and
|
|
10420
|
+
// isMidFlowSubmitShapedStep: a last slot without submit semantics is mid-flow
|
|
10421
|
+
// and must not be held to final-submit proof here.
|
|
10422
|
+
const inferredFinalSubmitStep = isGenuineFinalSubmit({ isFinalStep, flowHasSubmitSemantics: flowHasSubmitSemanticsFlag }) &&
|
|
10423
|
+
!stepDeclaredNonSubmit;
|
|
10178
10424
|
const requireSubmitEndpoint = (inferredFinalSubmitStep || submitStep) && submitEndpointPattern !== null;
|
|
10179
10425
|
// Capture the per-element selection baseline (for verifyDomEffect's element-
|
|
10180
10426
|
// scoped click read-back) ONLY for selection/field-answer click steps — never
|
|
@@ -10312,15 +10558,18 @@ async function executeStepWithHealing(params) {
|
|
|
10312
10558
|
}
|
|
10313
10559
|
// Multi-segment date spinbuttons need real key events; the cascade's
|
|
10314
10560
|
// insertText would garble the segments. Falls through on null.
|
|
10315
|
-
const
|
|
10561
|
+
const segmentedDate = await runSegmentedDateSpinbuttonPrimitive({
|
|
10316
10562
|
page,
|
|
10317
10563
|
target: selectFrameTarget,
|
|
10318
10564
|
instruction: step,
|
|
10319
10565
|
logger,
|
|
10320
10566
|
});
|
|
10321
|
-
if (
|
|
10567
|
+
if (segmentedDate.kind === "refused") {
|
|
10568
|
+
throw new errors_2.StepVerificationError(`${formatStepPrefix(stepIndex, totalSteps)} (${(0, pii_redaction_1.redactPii)(step).slice(0, 60)}) segmented date could not be delivered: ${segmentedDate.reason}`, "cascade-exhausted");
|
|
10569
|
+
}
|
|
10570
|
+
if (segmentedDate.kind === "committed") {
|
|
10322
10571
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} resolved by segmented-date primitive`);
|
|
10323
|
-
trajectory?.push({ stepIndex, verifiedBy: "dom", targetId:
|
|
10572
|
+
trajectory?.push({ stepIndex, verifiedBy: "dom", targetId: segmentedDate.targetId });
|
|
10324
10573
|
return "completed";
|
|
10325
10574
|
}
|
|
10326
10575
|
// On a CATCH-ALL step ("for any remaining … question"), fill every
|
|
@@ -10361,6 +10610,9 @@ async function executeStepWithHealing(params) {
|
|
|
10361
10610
|
const isSettleWaitStep = isSubmitOrFinalStep || isAdvanceStep(step);
|
|
10362
10611
|
if (captchaGated && isSubmitOrFinalStep) {
|
|
10363
10612
|
let captchaTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
10613
|
+
// The step-level baseline is declared after this block, so capture the
|
|
10614
|
+
// step-start page here for the fallback submit to judge against.
|
|
10615
|
+
const captchaStepStartSnapshot = await snapshotPage(captchaTarget, signalCounter, page);
|
|
10364
10616
|
const sitekeyProbeExpr = `(() => {
|
|
10365
10617
|
const el = document.querySelector("[data-sitekey]");
|
|
10366
10618
|
if (!el) return { siteKey: null, isInvisible: false };
|
|
@@ -10502,11 +10754,13 @@ async function executeStepWithHealing(params) {
|
|
|
10502
10754
|
signalCounter,
|
|
10503
10755
|
page,
|
|
10504
10756
|
step,
|
|
10757
|
+
stepStartSnapshot: captchaStepStartSnapshot ?? undefined,
|
|
10505
10758
|
isMidFlowSubmitStep: isMidFlowSubmitShapedStep({
|
|
10506
10759
|
step,
|
|
10507
10760
|
submitStep,
|
|
10508
10761
|
isFinalStep,
|
|
10509
10762
|
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
10763
|
+
elementSubmitShaped: !stepDeclaredNonSubmit,
|
|
10510
10764
|
}),
|
|
10511
10765
|
});
|
|
10512
10766
|
}
|
|
@@ -10648,6 +10902,66 @@ async function executeStepWithHealing(params) {
|
|
|
10648
10902
|
// legitimate-transition check (below) scans captures landed during THIS
|
|
10649
10903
|
// step's processing — earlier-step transitions don't count.
|
|
10650
10904
|
const stepStartMetaLength = recentCaptureMeta.length;
|
|
10905
|
+
let firstAttemptPre = null;
|
|
10906
|
+
let stepStartSnapshot = isStepSubmitShaped({
|
|
10907
|
+
step,
|
|
10908
|
+
submitStep,
|
|
10909
|
+
isFinalStep,
|
|
10910
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
10911
|
+
})
|
|
10912
|
+
? await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, page)
|
|
10913
|
+
: null;
|
|
10914
|
+
// A click on a disabled control can still grow the DOM (a validation
|
|
10915
|
+
// re-render), which would read as a step-start advance — so the last
|
|
10916
|
+
// resolved click's disabled veto must also gate the baseline credit.
|
|
10917
|
+
let lastClickBlockedByDisabled = false;
|
|
10918
|
+
// A step whose submit shape is only known from a resolved element got no
|
|
10919
|
+
// step-level baseline; the first attempt's pre-click snapshot is the
|
|
10920
|
+
// step-start page, so every later verdict site judges against it.
|
|
10921
|
+
const adoptElementStepStartBaseline = () => {
|
|
10922
|
+
if (stepStartSnapshot || !firstAttemptPre)
|
|
10923
|
+
return;
|
|
10924
|
+
stepStartSnapshot = firstAttemptPre;
|
|
10925
|
+
onElementStepStartBaseline?.(firstAttemptPre);
|
|
10926
|
+
};
|
|
10927
|
+
// One derivation for every in-cascade verdict site, so the same click is
|
|
10928
|
+
// judged under the same strictness class and baseline whichever site sees it.
|
|
10929
|
+
const deriveMidFlowJudgement = (elementSubmitShaped, pre) => {
|
|
10930
|
+
const midFlowSubmit = isMidFlowSubmitShapedStep({
|
|
10931
|
+
step,
|
|
10932
|
+
submitStep,
|
|
10933
|
+
isFinalStep,
|
|
10934
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
10935
|
+
elementSubmitShaped,
|
|
10936
|
+
});
|
|
10937
|
+
if (elementSubmitShaped)
|
|
10938
|
+
adoptElementStepStartBaseline();
|
|
10939
|
+
return { midFlowSubmit, advanceBaseline: stepStartSnapshot ?? pre };
|
|
10940
|
+
};
|
|
10941
|
+
const creditStepStartAdvance = async (exit) => {
|
|
10942
|
+
const genuineFinal = isGenuineFinalSubmit({
|
|
10943
|
+
isFinalStep,
|
|
10944
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
10945
|
+
});
|
|
10946
|
+
if (!stepStartSnapshot || genuineFinal || lastClickBlockedByDisabled)
|
|
10947
|
+
return false;
|
|
10948
|
+
const { post: settled, pageAdvanced } = await readMidFlowPostClickAdvance({
|
|
10949
|
+
target: frameTarget ?? (0, frame_target_1.mainFrameTarget)(page),
|
|
10950
|
+
signalCounter,
|
|
10951
|
+
page,
|
|
10952
|
+
pre: stepStartSnapshot,
|
|
10953
|
+
stepStartSnapshot,
|
|
10954
|
+
midFlowSubmit: true,
|
|
10955
|
+
});
|
|
10956
|
+
if (!pageAdvanced)
|
|
10957
|
+
return false;
|
|
10958
|
+
// A DOM change that came from bouncing to an auth gate is not an advance.
|
|
10959
|
+
if (!(0, phantom_click_1.isPlausibleStepDestination)(step, settled.url, stepStartSnapshot.url))
|
|
10960
|
+
return false;
|
|
10961
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} ${exit}: settled page advanced from the step-start baseline; crediting the step`);
|
|
10962
|
+
trajectory?.push({ stepIndex, verifiedBy: "dom" });
|
|
10963
|
+
return true;
|
|
10964
|
+
};
|
|
10651
10965
|
// Page-state probe BEFORE the cascade. When the page doesn't have any
|
|
10652
10966
|
// candidate matching the step's instruction we either skip cleanly
|
|
10653
10967
|
// (optional) or escalate straight to replan (required) — far cheaper than
|
|
@@ -10738,6 +11052,7 @@ async function executeStepWithHealing(params) {
|
|
|
10738
11052
|
submitStep,
|
|
10739
11053
|
isFinalStep,
|
|
10740
11054
|
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
11055
|
+
elementSubmitShaped: !stepDeclaredNonSubmit,
|
|
10741
11056
|
}),
|
|
10742
11057
|
});
|
|
10743
11058
|
if (fallbackTarget) {
|
|
@@ -10746,6 +11061,8 @@ async function executeStepWithHealing(params) {
|
|
|
10746
11061
|
return "completed";
|
|
10747
11062
|
}
|
|
10748
11063
|
}
|
|
11064
|
+
if (await creditStepStartAdvance("probe-absent"))
|
|
11065
|
+
return "completed";
|
|
10749
11066
|
// Capture diagnostics + write a failure dump BEFORE throwing so the
|
|
10750
11067
|
// global replan path's `readFailureDumpEvidence` can populate the
|
|
10751
11068
|
// prompt's CURRENTLY VISIBLE / UNFOCUSED OBSERVE / PAGE BODY HTML
|
|
@@ -11076,6 +11393,21 @@ async function executeStepWithHealing(params) {
|
|
|
11076
11393
|
};
|
|
11077
11394
|
let phantomClickAfterAttempt1 = false;
|
|
11078
11395
|
let attempt1UnreachableViaLightDom = false;
|
|
11396
|
+
// Attempt 1 resolved no candidate at all and was skipped before any verdict,
|
|
11397
|
+
// so it never reached the "unresolved" escalation a judged attempt 1 takes.
|
|
11398
|
+
let attempt1FastSkipped = false;
|
|
11399
|
+
// The deep submit-control locator takes attempt 2 when attempt 1 judged its
|
|
11400
|
+
// target unreachable, and attempt 3 when attempt 1 was fast-skipped and the
|
|
11401
|
+
// observe-act retry in attempt 2 did not verify, so a submit-shaped step with
|
|
11402
|
+
// no resolvable light-DOM target is never left without it.
|
|
11403
|
+
const deepSubmitLocatorSlot = (attemptNumber) => isStepSubmitShaped({
|
|
11404
|
+
step,
|
|
11405
|
+
submitStep,
|
|
11406
|
+
isFinalStep,
|
|
11407
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
11408
|
+
}) &&
|
|
11409
|
+
((attemptNumber === 2 && attempt1UnreachableViaLightDom) ||
|
|
11410
|
+
(attemptNumber === 3 && attempt1FastSkipped));
|
|
11079
11411
|
// Last candidate list the deep-submit-locator itself ranked this step, if
|
|
11080
11412
|
// any — reused by the cascade-exhausted diagnostic snapshot below so it
|
|
11081
11413
|
// doesn't re-run buildRankSubmitCandidatesExpr (and inflate the rank-call
|
|
@@ -11090,23 +11422,17 @@ async function executeStepWithHealing(params) {
|
|
|
11090
11422
|
// Skip to the next iteration so the cascade reaches its higher-value
|
|
11091
11423
|
// techniques faster.
|
|
11092
11424
|
if (attempt > 1) {
|
|
11093
|
-
const wouldBeTechnique = attempt
|
|
11094
|
-
?
|
|
11095
|
-
|
|
11096
|
-
|
|
11097
|
-
submitStep,
|
|
11098
|
-
isFinalStep,
|
|
11099
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
11100
|
-
})
|
|
11101
|
-
? "deep-submit-locator"
|
|
11102
|
-
: phantomClickAfterAttempt1
|
|
11425
|
+
const wouldBeTechnique = deepSubmitLocatorSlot(attempt)
|
|
11426
|
+
? "deep-submit-locator"
|
|
11427
|
+
: attempt === 2
|
|
11428
|
+
? phantomClickAfterAttempt1
|
|
11103
11429
|
? "trusted-click-retry"
|
|
11104
11430
|
: "observe-act"
|
|
11105
|
-
|
|
11106
|
-
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11431
|
+
: attempt === 3
|
|
11432
|
+
? "structured-click"
|
|
11433
|
+
: attempt === 4
|
|
11434
|
+
? "observe-act-exclude"
|
|
11435
|
+
: "llm-rephrase";
|
|
11110
11436
|
const decision = shouldSkipTechnique({
|
|
11111
11437
|
technique: wouldBeTechnique,
|
|
11112
11438
|
priorAttempts: attempts.map((a) => ({
|
|
@@ -11136,7 +11462,15 @@ async function executeStepWithHealing(params) {
|
|
|
11136
11462
|
// A click from an earlier attempt that errored after opening a tab leaves the
|
|
11137
11463
|
// run on the stale page; pick the tab up before measuring anything.
|
|
11138
11464
|
await adoptOpenedPage(0);
|
|
11465
|
+
// A prior attempt's click may have advanced the page after its own verdict;
|
|
11466
|
+
// judge against the step-start baseline before `pre` absorbs the advance.
|
|
11467
|
+
if (attempt > 1 && (await creditStepStartAdvance(`attempt-${attempt}-entry`)))
|
|
11468
|
+
return "completed";
|
|
11139
11469
|
const pre = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, page, captureSelectionState);
|
|
11470
|
+
// Recorded before any fast-skip `continue`, so an attempt-1 act that threw
|
|
11471
|
+
// after its click landed still leaves the step-start page behind.
|
|
11472
|
+
firstAttemptPre ??= pre;
|
|
11473
|
+
await armPageQuiescence(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page));
|
|
11140
11474
|
// Snapshot the meta-tail length so the final-step pattern gate can scope
|
|
11141
11475
|
// its URL scan to captures added DURING this attempt (not historical
|
|
11142
11476
|
// tail from earlier steps).
|
|
@@ -11171,6 +11505,15 @@ async function executeStepWithHealing(params) {
|
|
|
11171
11505
|
// whether this attempt's signal should come from the network/URL pair or
|
|
11172
11506
|
// from DOM re-read. Captured here so both branches of the cascade write to it.
|
|
11173
11507
|
let resolvedAction = null;
|
|
11508
|
+
// Submit shape of the control observe resolved BEFORE the act clicked it.
|
|
11509
|
+
// The act performs the click itself, so a page that swaps at once has
|
|
11510
|
+
// already removed the control by the time the post-act read runs; this is
|
|
11511
|
+
// the only read that cannot be blinded by the transition it caused.
|
|
11512
|
+
const submitShapeObservedBeforeClick = new Map();
|
|
11513
|
+
// Every submit-shaped control on the page before the act, so the shape of
|
|
11514
|
+
// the control the act clicked is known even when observe resolved nothing
|
|
11515
|
+
// or a different node than the act did.
|
|
11516
|
+
const submitShapedControlsBeforeClick = new Set();
|
|
11174
11517
|
// Stagehand resolves a natural-language act target internally, so for a
|
|
11175
11518
|
// click-intent instruction resolve it with observe first and centre it;
|
|
11176
11519
|
// a sticky bar otherwise swallows Stagehand's own click. Throws when the
|
|
@@ -11179,10 +11522,17 @@ async function executeStepWithHealing(params) {
|
|
|
11179
11522
|
const preCentreNaturalLanguageClick = async (instruction) => {
|
|
11180
11523
|
if (!(0, stagehand_guard_1.hasClickIntent)(instruction))
|
|
11181
11524
|
return false;
|
|
11525
|
+
if (!stepDeclaredNonSubmit) {
|
|
11526
|
+
for (const xpath of await readSubmitShapedControlXpaths(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page))) {
|
|
11527
|
+
submitShapedControlsBeforeClick.add(xpath);
|
|
11528
|
+
}
|
|
11529
|
+
}
|
|
11182
11530
|
const observed = await (0, stagehand_guard_1.guardedObserve)(stagehand, instruction, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget);
|
|
11183
11531
|
const first = observed[0];
|
|
11184
11532
|
if (!(0, stagehand_guard_1.isTrustedClickMethod)(first?.method) || !first?.selector)
|
|
11185
11533
|
return true;
|
|
11534
|
+
submitShapeObservedBeforeClick.set(first.selector, !stepDeclaredNonSubmit &&
|
|
11535
|
+
(await resolvedClickTargetIsSubmitShaped(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), first.selector)));
|
|
11186
11536
|
const failure = await centreObservedClickTarget(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), first.selector);
|
|
11187
11537
|
if (failure)
|
|
11188
11538
|
throw new Error(failure);
|
|
@@ -11280,14 +11630,7 @@ async function executeStepWithHealing(params) {
|
|
|
11280
11630
|
}
|
|
11281
11631
|
}
|
|
11282
11632
|
}
|
|
11283
|
-
else if (attempt
|
|
11284
|
-
attempt1UnreachableViaLightDom &&
|
|
11285
|
-
isStepSubmitShaped({
|
|
11286
|
-
step,
|
|
11287
|
-
submitStep,
|
|
11288
|
-
isFinalStep,
|
|
11289
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
11290
|
-
})) {
|
|
11633
|
+
else if (deepSubmitLocatorSlot(attempt)) {
|
|
11291
11634
|
// Deep submit-control locator: attempt 1 either phantom-clicked
|
|
11292
11635
|
// (Stagehand reported success but pre/post showed zero effect) or
|
|
11293
11636
|
// came back "unresolved" (act() found zero candidates at all), so the
|
|
@@ -11363,24 +11706,26 @@ async function executeStepWithHealing(params) {
|
|
|
11363
11706
|
const runnerUp = ranked[1];
|
|
11364
11707
|
if (runnerUp) {
|
|
11365
11708
|
const midTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
|
|
11370
|
-
|
|
11371
|
-
|
|
11709
|
+
// Ranked candidates are submit-shaped by construction, so the
|
|
11710
|
+
// resolved element's shape is the step's declared-non-submit gate.
|
|
11711
|
+
const { midFlowSubmit: deepMidFlowSubmit, advanceBaseline: deepStepBaseline } = deriveMidFlowJudgement(!stepDeclaredNonSubmit, pre);
|
|
11712
|
+
const { post: midPost, advanceBaseline: deepAdvanceBaseline } = await readMidFlowPostClickAdvance({
|
|
11713
|
+
target: midTarget,
|
|
11714
|
+
signalCounter,
|
|
11715
|
+
page,
|
|
11716
|
+
pre,
|
|
11717
|
+
stepStartSnapshot: deepStepBaseline,
|
|
11718
|
+
midFlowSubmit: deepMidFlowSubmit,
|
|
11372
11719
|
});
|
|
11373
|
-
|
|
11374
|
-
|
|
11375
|
-
|
|
11720
|
+
// The deep-submit-locator runs only on submit escalation, so a
|
|
11721
|
+
// stray selection flip must not mask a top-pick phantom and skip
|
|
11722
|
+
// the runner-up retry.
|
|
11723
|
+
const topVerdict = classifySubmitShapedClick({
|
|
11724
|
+
step,
|
|
11376
11725
|
pre,
|
|
11377
11726
|
post: midPost,
|
|
11378
|
-
|
|
11379
|
-
|
|
11380
|
-
// the runner-up retry.
|
|
11381
|
-
isSubmitShapedStep: true,
|
|
11382
|
-
isMidFlowSubmitStep: deepMidFlowSubmit,
|
|
11383
|
-
destinationPlausible: (0, phantom_click_1.isPlausibleStepDestination)(step, midPost.url, deepMidFlowSubmit ? pre.url : undefined),
|
|
11727
|
+
midFlowSubmit: deepMidFlowSubmit,
|
|
11728
|
+
advanceBaseline: deepAdvanceBaseline,
|
|
11384
11729
|
});
|
|
11385
11730
|
if (topVerdict === "phantom") {
|
|
11386
11731
|
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})`);
|
|
@@ -11768,6 +12113,14 @@ async function executeStepWithHealing(params) {
|
|
|
11768
12113
|
};
|
|
11769
12114
|
}
|
|
11770
12115
|
}
|
|
12116
|
+
if (overriddenTarget.method === "fill" &&
|
|
12117
|
+
(await isDateSegmentAtSelector(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), overriddenTarget.selector))) {
|
|
12118
|
+
record.errorMessage = `refused fill on a date segment: "${target.description.slice(0, 60)}" (a lone-segment write cannot compose a date)`;
|
|
12119
|
+
attempts.push(record);
|
|
12120
|
+
failureReasons.push(record.errorMessage);
|
|
12121
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${record.errorMessage}`);
|
|
12122
|
+
continue;
|
|
12123
|
+
}
|
|
11771
12124
|
params.abortSignal?.throwIfAborted();
|
|
11772
12125
|
// Stagehand's internal Locator.click() does not scroll past sticky
|
|
11773
12126
|
// bars, so centre + hit-test the observed control first; an
|
|
@@ -12107,6 +12460,7 @@ async function executeStepWithHealing(params) {
|
|
|
12107
12460
|
? `attempt 1 fast-skipped (no resolved action): ${record.errorMessage}`
|
|
12108
12461
|
: "attempt 1 fast-skipped (no resolved action; Stagehand returned no candidates)";
|
|
12109
12462
|
failureReasons.push(reason);
|
|
12463
|
+
attempt1FastSkipped = true;
|
|
12110
12464
|
continue;
|
|
12111
12465
|
}
|
|
12112
12466
|
// A fill/select/type action cannot open a tab, so skip the discovery wait for it.
|
|
@@ -12117,9 +12471,19 @@ async function executeStepWithHealing(params) {
|
|
|
12117
12471
|
// settle: a view swap removes the clicked control, so a post-swap read
|
|
12118
12472
|
// would blind the mid-flow exemption on exactly the case it exists for.
|
|
12119
12473
|
// See resolvedClickTargetIsSubmitShaped's doc comment.
|
|
12474
|
+
// A successful act that listed no actions still clicked what observe
|
|
12475
|
+
// resolved first, so that control's pre-click shape stands in for it.
|
|
12120
12476
|
const resolvedElementIsSubmitShaped = isClick && !stepDeclaredNonSubmit && resolvedAction?.selector
|
|
12121
|
-
?
|
|
12122
|
-
|
|
12477
|
+
? submitShapeObservedBeforeClick.get(resolvedAction.selector) === true ||
|
|
12478
|
+
submitShapedControlsBeforeClick.has(canonicalAbsoluteXpath(resolvedAction.selector) ?? "") ||
|
|
12479
|
+
(await resolvedClickTargetIsSubmitShaped(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), resolvedAction.selector))
|
|
12480
|
+
: resolvedAction === null &&
|
|
12481
|
+
record.actResultSuccess === true &&
|
|
12482
|
+
!stepDeclaredNonSubmit &&
|
|
12483
|
+
submitShapeObservedBeforeClick.values().next().value === true;
|
|
12484
|
+
if (resolvedElementIsSubmitShaped)
|
|
12485
|
+
adoptElementStepStartBaseline();
|
|
12486
|
+
const advanceBaseline = stepStartSnapshot ?? pre;
|
|
12123
12487
|
// Computed once: gates every credit signal that takes no post-click URL
|
|
12124
12488
|
// (network, DOM read-back) and classifyPhantomClick's destinationPlausible.
|
|
12125
12489
|
const stepIsMidFlowSubmit = isMidFlowSubmitShapedStep({
|
|
@@ -12135,11 +12499,19 @@ async function executeStepWithHealing(params) {
|
|
|
12135
12499
|
const mayTriggerRoundTrip = resolvedAction === null
|
|
12136
12500
|
? record.actResultSuccess === true
|
|
12137
12501
|
: !STATE_CLASS_METHODS.has(resolvedAction.method ?? "");
|
|
12138
|
-
|
|
12139
|
-
|
|
12140
|
-
|
|
12141
|
-
}
|
|
12142
|
-
|
|
12502
|
+
// Re-settle is not gated on submit shape: an unflagged step or a no-action
|
|
12503
|
+
// act can still advance a page whose body re-renders late. Credit stays
|
|
12504
|
+
// gated on stepIsMidFlowSubmit, so the final step keeps its strictness.
|
|
12505
|
+
const { post } = await readMidFlowPostClickAdvance({
|
|
12506
|
+
target: frameTarget ?? (0, frame_target_1.mainFrameTarget)(page),
|
|
12507
|
+
signalCounter,
|
|
12508
|
+
page,
|
|
12509
|
+
pre,
|
|
12510
|
+
stepStartSnapshot: advanceBaseline,
|
|
12511
|
+
midFlowSubmit: stepIsMidFlowSubmit,
|
|
12512
|
+
mayTriggerRoundTrip,
|
|
12513
|
+
waitForMutations: isSettleWaitStep || resolvedElementIsSubmitShaped,
|
|
12514
|
+
});
|
|
12143
12515
|
record.post = post;
|
|
12144
12516
|
if (resolvedAction) {
|
|
12145
12517
|
record.resolvedMethod = resolvedAction.method ?? null;
|
|
@@ -12261,23 +12633,42 @@ async function executeStepWithHealing(params) {
|
|
|
12261
12633
|
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
12262
12634
|
elementSubmitShaped: resolvedElementIsSubmitShaped,
|
|
12263
12635
|
});
|
|
12636
|
+
// A submit-worded last slot without submit semantics is mid-flow
|
|
12637
|
+
// (isGenuineFinalSubmit), so the sibling predicates below must not hold it
|
|
12638
|
+
// to genuine-final strictness while the verdict gates already credit it on
|
|
12639
|
+
// a settled advance.
|
|
12640
|
+
const primarySubmitShaped = isStepSubmitShaped({
|
|
12641
|
+
step,
|
|
12642
|
+
submitStep,
|
|
12643
|
+
isFinalStep,
|
|
12644
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
12645
|
+
}) &&
|
|
12646
|
+
(submitStep ||
|
|
12647
|
+
isGenuineFinalSubmit({ isFinalStep, flowHasSubmitSemantics: flowHasSubmitSemanticsFlag }) ||
|
|
12648
|
+
!isFinalStep);
|
|
12264
12649
|
// An element-inferred mid-flow submit joins the exemption the step-derived
|
|
12265
12650
|
// one already got: a click that never left its URL did not bounce to a gate.
|
|
12266
|
-
const
|
|
12267
|
-
|
|
12651
|
+
const midFlowDestinationPlausibleForPost = midFlowDestinationPlausible({
|
|
12652
|
+
step,
|
|
12653
|
+
pre,
|
|
12654
|
+
post,
|
|
12655
|
+
midFlowSubmit,
|
|
12656
|
+
advanceBaseline,
|
|
12657
|
+
destinationPlausible,
|
|
12658
|
+
});
|
|
12268
12659
|
const clickViewSwapVerified = isClickViewSwapVerified({
|
|
12269
12660
|
resolvedAction,
|
|
12270
12661
|
submitStep,
|
|
12271
12662
|
isAdvanceWithPattern: isAdvanceStep(step) && advanceTransitionBodyPattern !== null,
|
|
12272
12663
|
networkDelta: post.networkCount - pre.networkCount,
|
|
12273
12664
|
bytesDelta: post.bodyHtmlLength - pre.bodyHtmlLength,
|
|
12274
|
-
textChanged:
|
|
12665
|
+
textChanged: (0, phantom_click_1.hasVisibleTextChanged)(advanceBaseline, post),
|
|
12275
12666
|
invalidMarkerDelta: postInvalidMarkerCount - preInvalidMarkerCount,
|
|
12276
12667
|
clickedElementStillPresent,
|
|
12277
12668
|
resolvedElementIsSubmitShaped,
|
|
12278
|
-
destinationPlausible:
|
|
12669
|
+
destinationPlausible: midFlowDestinationPlausibleForPost,
|
|
12279
12670
|
midFlowSubmit,
|
|
12280
|
-
pageAdvanced: (0, phantom_click_1.
|
|
12671
|
+
pageAdvanced: (0, phantom_click_1.hasSettledPageAdvanced)(advanceBaseline, post),
|
|
12281
12672
|
actSucceeded: record.actResultSuccess === true,
|
|
12282
12673
|
});
|
|
12283
12674
|
if (clickViewSwapVerified === false &&
|
|
@@ -12311,12 +12702,7 @@ async function executeStepWithHealing(params) {
|
|
|
12311
12702
|
// submitStep, inferred final-step, or an attribute-detected submit
|
|
12312
12703
|
// control) may not ride this signal alone — only when the submit-judge
|
|
12313
12704
|
// gate (requireSubmitEndpoint) will re-litigate the credit below.
|
|
12314
|
-
const formValueWeakSignalAllowed = !(
|
|
12315
|
-
step,
|
|
12316
|
-
submitStep,
|
|
12317
|
-
isFinalStep,
|
|
12318
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
12319
|
-
}) || resolvedElementIsSubmitShaped) || requireSubmitEndpoint;
|
|
12705
|
+
const formValueWeakSignalAllowed = !(primarySubmitShaped || resolvedElementIsSubmitShaped) || requireSubmitEndpoint;
|
|
12320
12706
|
const formValueVerified = isStateClass &&
|
|
12321
12707
|
formValueWeakSignalAllowed &&
|
|
12322
12708
|
destinationPlausible &&
|
|
@@ -12436,7 +12822,8 @@ async function executeStepWithHealing(params) {
|
|
|
12436
12822
|
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
12437
12823
|
}),
|
|
12438
12824
|
isMidFlowSubmitStep: midFlowSubmit,
|
|
12439
|
-
|
|
12825
|
+
advanceBaseline,
|
|
12826
|
+
destinationPlausible: midFlowDestinationPlausibleForPost,
|
|
12440
12827
|
});
|
|
12441
12828
|
// An `"effective"` verdict driven purely by the page-wide byte-delta
|
|
12442
12829
|
// floor (`TRIVIAL_DOM_DELTA_BYTES`, 500B) is intentionally NOT trusted
|
|
@@ -12457,12 +12844,7 @@ async function executeStepWithHealing(params) {
|
|
|
12457
12844
|
// respects the SAME DOM-only-advance veto as the block above: a wizard
|
|
12458
12845
|
// "Next" step on a pattern-configured site whose only signal is a field
|
|
12459
12846
|
// toggle must stay unverified, not get waved through by the verdict.
|
|
12460
|
-
const domEffectiveVerdict = !
|
|
12461
|
-
step,
|
|
12462
|
-
submitStep,
|
|
12463
|
-
isFinalStep,
|
|
12464
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
12465
|
-
}) && domVerifiedForStep;
|
|
12847
|
+
const domEffectiveVerdict = !primarySubmitShaped && domVerifiedForStep;
|
|
12466
12848
|
let verified = networkIsRealAdvance ||
|
|
12467
12849
|
urlChanged ||
|
|
12468
12850
|
domVerifiedForStep ||
|
|
@@ -12552,7 +12934,10 @@ async function executeStepWithHealing(params) {
|
|
|
12552
12934
|
target: frameTarget ?? (0, frame_target_1.mainFrameTarget)(page),
|
|
12553
12935
|
signalCounter,
|
|
12554
12936
|
preNetworkCount: pre.networkCount,
|
|
12555
|
-
preVisibleTextSignature:
|
|
12937
|
+
preVisibleTextSignature: advanceBaseline.visibleTextSignature,
|
|
12938
|
+
preVisibleText: advanceBaseline.visibleText,
|
|
12939
|
+
preVisibleTextDigest: advanceBaseline.visibleTextDigest,
|
|
12940
|
+
settledPostVisibleText: post.visibleText,
|
|
12556
12941
|
submitInput: {
|
|
12557
12942
|
pageUrl: post.url,
|
|
12558
12943
|
pageTitle,
|
|
@@ -12571,14 +12956,14 @@ async function executeStepWithHealing(params) {
|
|
|
12571
12956
|
// A mid-flow transition is credited on the page's own demonstrable
|
|
12572
12957
|
// advance (visible text swapped); the judge's final-submit-flavoured
|
|
12573
12958
|
// view of a stale title or missing network must not undo it.
|
|
12574
|
-
const midFlowAdvanceCredited = (
|
|
12575
|
-
|
|
12576
|
-
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
|
|
12959
|
+
const midFlowAdvanceCredited = isMidFlowAdvanceCredited({
|
|
12960
|
+
midFlowSubmit,
|
|
12961
|
+
signalAdvanced: clickViewSwapVerified,
|
|
12962
|
+
judgeUnavailable: judgeVerdict === null,
|
|
12963
|
+
networkAdvanced: networkIsRealAdvance,
|
|
12964
|
+
pageAdvanced: (0, phantom_click_1.hasSettledPageAdvanced)(advanceBaseline, post),
|
|
12965
|
+
invalidMarkerCount,
|
|
12966
|
+
});
|
|
12582
12967
|
if (midFlowAdvanceCredited && judgeVerdict?.verified !== true) {
|
|
12583
12968
|
logger.info(`mid-flow submit credited on demonstrable page advance (judge ${judgeVerdict === null ? "unavailable" : `rejected: ${judgeVerdict.reason}`})`);
|
|
12584
12969
|
}
|
|
@@ -12614,7 +12999,7 @@ async function executeStepWithHealing(params) {
|
|
|
12614
12999
|
// Any-4xx fallback. The judge said failed; surface any captured 4xx
|
|
12615
13000
|
// body's field-level error JSON for the rephrase prompt downstream
|
|
12616
13001
|
// — same role as before the migration.
|
|
12617
|
-
const fallbackEvidence = extractSubmitFailureEvidence(recentCaptures.slice(-tail.length), [], (0,
|
|
13002
|
+
const fallbackEvidence = extractSubmitFailureEvidence(recentCaptures.slice(-tail.length), [], (0, runtime_capture_store_1.defaultCapturesDir)(), "any-4xx");
|
|
12618
13003
|
if (fallbackEvidence.length > 0) {
|
|
12619
13004
|
failureReasons.push(`any-4xx fallback: ${fallbackEvidence.split("\n")[0]}`.slice(0, 240));
|
|
12620
13005
|
}
|
|
@@ -12789,26 +13174,22 @@ async function executeStepWithHealing(params) {
|
|
|
12789
13174
|
probeResult.checked === true &&
|
|
12790
13175
|
(probeResult.ariaManaged === true || !ancestorStillInvalid);
|
|
12791
13176
|
await page.waitForTimeout(STEP_PAUSE_MS);
|
|
12792
|
-
|
|
12793
|
-
|
|
12794
|
-
|
|
12795
|
-
|
|
12796
|
-
|
|
12797
|
-
|
|
12798
|
-
|
|
12799
|
-
|
|
12800
|
-
|
|
12801
|
-
const retryMidFlowSubmit = isMidFlowSubmitShapedStep({
|
|
12802
|
-
step,
|
|
12803
|
-
submitStep,
|
|
12804
|
-
isFinalStep,
|
|
12805
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
12806
|
-
elementSubmitShaped: retryResolvedElementIsSubmitShaped,
|
|
13177
|
+
const { midFlowSubmit: retryMidFlowSubmit, advanceBaseline: retryAdvanceBaseline } = deriveMidFlowJudgement(retryResolvedElementIsSubmitShaped, pre);
|
|
13178
|
+
const { post: retryPost, pageAdvanced: retryPageAdvanced } = await readMidFlowPostClickAdvance({
|
|
13179
|
+
target: frameTarget ?? (0, frame_target_1.mainFrameTarget)(page),
|
|
13180
|
+
signalCounter,
|
|
13181
|
+
page,
|
|
13182
|
+
pre,
|
|
13183
|
+
stepStartSnapshot: retryAdvanceBaseline,
|
|
13184
|
+
midFlowSubmit: retryMidFlowSubmit,
|
|
13185
|
+
waitForMutations: isSettleWaitStep || retryResolvedElementIsSubmitShaped,
|
|
12807
13186
|
});
|
|
13187
|
+
const retryNetworkFired = retryPost.networkCount > pre.networkCount;
|
|
12808
13188
|
const retryUrlChanged = (0, phantom_click_1.hasOriginOrPathChanged)(pre.url, retryPost.url) &&
|
|
12809
13189
|
(0, phantom_click_1.isPlausibleStepDestination)(step, retryPost.url, retryMidFlowSubmit ? pre.url : undefined);
|
|
12810
|
-
const
|
|
12811
|
-
const
|
|
13190
|
+
const retryDomBaseline = retryMidFlowSubmit ? retryAdvanceBaseline : pre;
|
|
13191
|
+
const retryHtmlDelta = retryPost.bodyHtmlLength - retryDomBaseline.bodyHtmlLength;
|
|
13192
|
+
const retryTextChanged = (0, phantom_click_1.hasVisibleTextChanged)(retryDomBaseline, retryPost);
|
|
12812
13193
|
const retryFormValueChanged = retryPost.formValueSignature !== pre.formValueSignature;
|
|
12813
13194
|
// Element-scoped selection read-back for the n+16 fallback — the same
|
|
12814
13195
|
// authoritative signal the primary verifier uses via `verifyDomEffect`,
|
|
@@ -12878,6 +13259,8 @@ async function executeStepWithHealing(params) {
|
|
|
12878
13259
|
.evaluate(disabledExpr)
|
|
12879
13260
|
.catch(() => false);
|
|
12880
13261
|
})());
|
|
13262
|
+
if (probeResult.kind === "click")
|
|
13263
|
+
lastClickBlockedByDisabled = clickBlockedByDisabled;
|
|
12881
13264
|
const clickWasDomOnly = probeResult.kind === "click" && !retryNetworkFired && !retryUrlChanged;
|
|
12882
13265
|
const clickBlockedByInvalid = clickWasDomOnly &&
|
|
12883
13266
|
(await countNgInvalidContainers(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page)).catch(() => 0)) >
|
|
@@ -12903,10 +13286,14 @@ async function executeStepWithHealing(params) {
|
|
|
12903
13286
|
const retryDestinationPlausible = (0, phantom_click_1.isPlausibleStepDestination)(step, retryPost.url);
|
|
12904
13287
|
// Only the verdict sees the pre.url exemption, and only for a demonstrable
|
|
12905
13288
|
// page advance: bare network/DOM signals stay gated on the landed URL.
|
|
12906
|
-
const retryMidFlowDestinationPlausible =
|
|
12907
|
-
|
|
12908
|
-
|
|
12909
|
-
|
|
13289
|
+
const retryMidFlowDestinationPlausible = midFlowDestinationPlausible({
|
|
13290
|
+
step,
|
|
13291
|
+
pre,
|
|
13292
|
+
post: retryPost,
|
|
13293
|
+
midFlowSubmit: retryMidFlowSubmit,
|
|
13294
|
+
advanceBaseline: retryAdvanceBaseline,
|
|
13295
|
+
destinationPlausible: retryDestinationPlausible,
|
|
13296
|
+
});
|
|
12910
13297
|
const retryNetworkIsRealAdvance = retryNetworkFired &&
|
|
12911
13298
|
retryDestinationPlausible &&
|
|
12912
13299
|
(await waitForTransitionBody({
|
|
@@ -13014,6 +13401,7 @@ async function executeStepWithHealing(params) {
|
|
|
13014
13401
|
elementStateChanged: retrySelectionStateChanged,
|
|
13015
13402
|
isSubmitShapedStep: retryMidFlowSubmit || retrySubmitShaped,
|
|
13016
13403
|
isMidFlowSubmitStep: retryMidFlowSubmit,
|
|
13404
|
+
advanceBaseline: retryAdvanceBaseline,
|
|
13017
13405
|
destinationPlausible: retryMidFlowDestinationPlausible,
|
|
13018
13406
|
});
|
|
13019
13407
|
// Destination-plausibility audit of every remaining disjunct in
|
|
@@ -13050,6 +13438,10 @@ async function executeStepWithHealing(params) {
|
|
|
13050
13438
|
(n16ClickDelivered &&
|
|
13051
13439
|
(!retrySubmitShaped || retryMidFlowSubmit) &&
|
|
13052
13440
|
retryVerdict === "effective") ||
|
|
13441
|
+
(n16ClickDelivered &&
|
|
13442
|
+
retryMidFlowSubmit &&
|
|
13443
|
+
retryPageAdvanced &&
|
|
13444
|
+
retryMidFlowDestinationPlausible) ||
|
|
13053
13445
|
(n16ClickDelivered &&
|
|
13054
13446
|
weakDomSignalsAllowed &&
|
|
13055
13447
|
retryDestinationPlausible &&
|
|
@@ -13149,12 +13541,15 @@ async function executeStepWithHealing(params) {
|
|
|
13149
13541
|
isFinalStep,
|
|
13150
13542
|
submitStep,
|
|
13151
13543
|
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
13152
|
-
elementSubmitShaped:
|
|
13544
|
+
elementSubmitShaped: retryResolvedElementIsSubmitShaped,
|
|
13153
13545
|
page,
|
|
13154
13546
|
target: frameTarget ?? (0, frame_target_1.mainFrameTarget)(page),
|
|
13155
13547
|
signalCounter,
|
|
13156
13548
|
preNetworkCount: pre.networkCount,
|
|
13157
|
-
preVisibleTextSignature:
|
|
13549
|
+
preVisibleTextSignature: retryAdvanceBaseline.visibleTextSignature,
|
|
13550
|
+
preVisibleText: retryAdvanceBaseline.visibleText,
|
|
13551
|
+
preVisibleTextDigest: retryAdvanceBaseline.visibleTextDigest,
|
|
13552
|
+
settledPostVisibleText: retryPost.visibleText,
|
|
13158
13553
|
submitInput: {
|
|
13159
13554
|
pageUrl: retryPost.url,
|
|
13160
13555
|
pageTitle,
|
|
@@ -13170,16 +13565,14 @@ async function executeStepWithHealing(params) {
|
|
|
13170
13565
|
submittedStateSelectors: matchedSubmittedSelectors,
|
|
13171
13566
|
},
|
|
13172
13567
|
});
|
|
13173
|
-
const retryMidFlowAdvanceCredited = (
|
|
13174
|
-
|
|
13175
|
-
|
|
13176
|
-
|
|
13177
|
-
|
|
13178
|
-
|
|
13179
|
-
|
|
13180
|
-
|
|
13181
|
-
invalidMarkerCount,
|
|
13182
|
-
});
|
|
13568
|
+
const retryMidFlowAdvanceCredited = isMidFlowAdvanceCredited({
|
|
13569
|
+
midFlowSubmit: retryMidFlowSubmit,
|
|
13570
|
+
signalAdvanced: retryVerdict === "effective" && retryPageAdvanced,
|
|
13571
|
+
judgeUnavailable: judgeVerdict === null,
|
|
13572
|
+
networkAdvanced: retryNetworkFired && retryDestinationPlausible,
|
|
13573
|
+
pageAdvanced: retryPageAdvanced,
|
|
13574
|
+
invalidMarkerCount,
|
|
13575
|
+
});
|
|
13183
13576
|
if (retryMidFlowAdvanceCredited && judgeVerdict?.verified !== true) {
|
|
13184
13577
|
logger.info(`n+16 fallback mid-flow submit credited on demonstrable page advance (judge ${judgeVerdict === null ? "unavailable" : `rejected: ${judgeVerdict.reason}`})`);
|
|
13185
13578
|
}
|
|
@@ -13358,12 +13751,7 @@ async function executeStepWithHealing(params) {
|
|
|
13358
13751
|
// (triedSelectors), which is empty on an "unresolved" verdict, so a
|
|
13359
13752
|
// non-submit step with that verdict falls through to the normal
|
|
13360
13753
|
// ladder instead of escalating.
|
|
13361
|
-
const escalationTarget =
|
|
13362
|
-
step,
|
|
13363
|
-
submitStep,
|
|
13364
|
-
isFinalStep,
|
|
13365
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
13366
|
-
})
|
|
13754
|
+
const escalationTarget = primarySubmitShaped
|
|
13367
13755
|
? "escalating attempt 2 to deep-submit-locator"
|
|
13368
13756
|
: phantomClickAfterAttempt1
|
|
13369
13757
|
? "non-submit step — escalating attempt 2 to trusted-click-retry (trusted CDP click on the resolved target)"
|
|
@@ -13375,17 +13763,16 @@ async function executeStepWithHealing(params) {
|
|
|
13375
13763
|
// Treat the canonical submit click as "final" for this predicate
|
|
13376
13764
|
// even when it lives mid-flow. See requireSubmitEndpoint derivation
|
|
13377
13765
|
// above for the same gate-widening rationale.
|
|
13378
|
-
isFinalStep:
|
|
13379
|
-
step,
|
|
13380
|
-
submitStep,
|
|
13381
|
-
isFinalStep,
|
|
13382
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
13383
|
-
}),
|
|
13766
|
+
isFinalStep: primarySubmitShaped,
|
|
13384
13767
|
requireSubmitEndpoint,
|
|
13385
13768
|
resolvedMethod: record.resolvedMethod,
|
|
13386
13769
|
effectSignals,
|
|
13387
13770
|
preSubmitInvalidCount,
|
|
13388
13771
|
postAttemptInvalidCount,
|
|
13772
|
+
midFlowAdvanced: !isGenuineFinalSubmit({
|
|
13773
|
+
isFinalStep,
|
|
13774
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
13775
|
+
}) && (0, phantom_click_1.hasSettledPageAdvanced)(advanceBaseline, post),
|
|
13389
13776
|
});
|
|
13390
13777
|
if (earlyExit) {
|
|
13391
13778
|
const exitReason = `submit-revealed-invalid: click surfaced ${postAttemptInvalidCount - preSubmitInvalidCount} new ng-invalid container(s) (was ${preSubmitInvalidCount}, now ${postAttemptInvalidCount}); attempts 2-${MAX_STEP_ATTEMPTS} cannot heal a form that needs answers — routing to replan`;
|
|
@@ -13447,6 +13834,7 @@ async function executeStepWithHealing(params) {
|
|
|
13447
13834
|
submitStep,
|
|
13448
13835
|
isFinalStep,
|
|
13449
13836
|
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
13837
|
+
elementSubmitShaped: !stepDeclaredNonSubmit,
|
|
13450
13838
|
}),
|
|
13451
13839
|
});
|
|
13452
13840
|
if (fallbackTarget) {
|
|
@@ -13455,6 +13843,8 @@ async function executeStepWithHealing(params) {
|
|
|
13455
13843
|
return "completed";
|
|
13456
13844
|
}
|
|
13457
13845
|
}
|
|
13846
|
+
if (await creditStepStartAdvance("cascade-exhausted"))
|
|
13847
|
+
return "completed";
|
|
13458
13848
|
const cascadeExhaustObservedFinal = await (0, stagehand_guard_1.guardedObserve)(stagehand, step, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
13459
13849
|
// observe() is blind to a cross-origin OOPIF, so a frame-scoped empty
|
|
13460
13850
|
// result degrades to the deep-locator resolver for dump evidence — this
|
|
@@ -13550,6 +13940,9 @@ async function executeNavigateStep(params) {
|
|
|
13550
13940
|
* than let the flow report phantom success.
|
|
13551
13941
|
*/
|
|
13552
13942
|
async function runHealingFlow(deps) {
|
|
13943
|
+
return (0, runtime_capture_store_1.withRuntimeCaptureStore)((store) => runHealingFlowScoped(deps, store));
|
|
13944
|
+
}
|
|
13945
|
+
async function runHealingFlowScoped(deps, store) {
|
|
13553
13946
|
const { stagehand, page, steps, logger, anthropic, rephraseModel, uploadFixture, maxFlowMs } = deps;
|
|
13554
13947
|
for (const secret of deps.secrets ?? [])
|
|
13555
13948
|
(0, pii_redaction_1.registerSecretValue)(secret);
|
|
@@ -13579,6 +13972,7 @@ async function runHealingFlow(deps) {
|
|
|
13579
13972
|
recentCaptures,
|
|
13580
13973
|
recentCaptureMeta,
|
|
13581
13974
|
getCurrentPhase: () => "flow",
|
|
13975
|
+
onCapture: (capture, filename) => (0, runtime_capture_store_1.recordRuntimeCapture)(store, filename, capture),
|
|
13582
13976
|
getCurrentPageOrigin: () => {
|
|
13583
13977
|
try {
|
|
13584
13978
|
return new URL(activePage.url()).origin;
|