@enricai/barnacle 1.8.0 → 1.9.1
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/scraper/flow-runner.d.ts +89 -0
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +363 -26
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scripts/recon-browser.d.ts +1 -1
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +3 -1
- package/dist/scripts/recon-browser.js.map +1 -1
- package/package.json +1 -1
|
@@ -22,6 +22,7 @@ exports.findRecentBackendError = findRecentBackendError;
|
|
|
22
22
|
exports.findRecentPageTransition = findRecentPageTransition;
|
|
23
23
|
exports.isWizardExitAction = isWizardExitAction;
|
|
24
24
|
exports.isAdvanceStep = isAdvanceStep;
|
|
25
|
+
exports.shouldWarnMissingAdvancePattern = shouldWarnMissingAdvancePattern;
|
|
25
26
|
exports.parseCaptureIndex = parseCaptureIndex;
|
|
26
27
|
exports.latestCaptureIndex = latestCaptureIndex;
|
|
27
28
|
exports.capturesAfterIndex = capturesAfterIndex;
|
|
@@ -30,6 +31,7 @@ exports.windowHasAdvanceTransition = windowHasAdvanceTransition;
|
|
|
30
31
|
exports.shouldVetoFallbackAdvance = shouldVetoFallbackAdvance;
|
|
31
32
|
exports.isDomOnlyAdvanceVerified = isDomOnlyAdvanceVerified;
|
|
32
33
|
exports.isClickViewSwapVerified = isClickViewSwapVerified;
|
|
34
|
+
exports.shouldReadbackFillOnActSuccess = shouldReadbackFillOnActSuccess;
|
|
33
35
|
exports.countNgInvalidContainers = countNgInvalidContainers;
|
|
34
36
|
exports.describeAttemptEffectSignals = describeAttemptEffectSignals;
|
|
35
37
|
exports.shouldSkipTechnique = shouldSkipTechnique;
|
|
@@ -51,6 +53,7 @@ exports.extractSubmitFailureEvidence = extractSubmitFailureEvidence;
|
|
|
51
53
|
exports.extractGaEventEvidence = extractGaEventEvidence;
|
|
52
54
|
exports.normalizeDateValue = normalizeDateValue;
|
|
53
55
|
exports.fillHtml5DateTimeInput = fillHtml5DateTimeInput;
|
|
56
|
+
exports.fillTextDatepickerInput = fillTextDatepickerInput;
|
|
54
57
|
exports.verifyFillReadback = verifyFillReadback;
|
|
55
58
|
exports.isUploadAffordanceLabel = isUploadAffordanceLabel;
|
|
56
59
|
exports.writeFixtureToTempFile = writeFixtureToTempFile;
|
|
@@ -58,6 +61,7 @@ exports.attachToSurfacedInput = attachToSurfacedInput;
|
|
|
58
61
|
exports.surfaceAndUpload = surfaceAndUpload;
|
|
59
62
|
exports.parseSelectStep = parseSelectStep;
|
|
60
63
|
exports.parseFillStep = parseFillStep;
|
|
64
|
+
exports.parseFillValueIntent = parseFillValueIntent;
|
|
61
65
|
exports.parseRadioStep = parseRadioStep;
|
|
62
66
|
exports.pollEnumerate = pollEnumerate;
|
|
63
67
|
exports.waitForTransitionBody = waitForTransitionBody;
|
|
@@ -78,6 +82,7 @@ const node_fs_1 = require("node:fs");
|
|
|
78
82
|
const node_os_1 = require("node:os");
|
|
79
83
|
const node_path_1 = require("node:path");
|
|
80
84
|
const ai_1 = require("ai");
|
|
85
|
+
const date_fns_1 = require("date-fns");
|
|
81
86
|
const config_1 = require("../config");
|
|
82
87
|
const errors_1 = require("../lib/errors");
|
|
83
88
|
const error_messages_1 = require("../lib/llm/judges/error-messages");
|
|
@@ -694,6 +699,20 @@ function isAdvanceStep(instruction) {
|
|
|
694
699
|
const haystack = instruction.toLowerCase();
|
|
695
700
|
return ADVANCE_STEP_PHRASES.some((p) => haystack.includes(p));
|
|
696
701
|
}
|
|
702
|
+
/**
|
|
703
|
+
* Whether a flow should be WARNed that its DOM-only advance guard is disarmed.
|
|
704
|
+
* `isDomOnlyAdvanceVerified` only vetoes a DOM-only "advance" when
|
|
705
|
+
* `advanceTransitionBodyPattern` is set; a flow with advance steps but no
|
|
706
|
+
* pattern silently loses that veto, so a validation-re-render wizard can desync
|
|
707
|
+
* its step pointer from the page. Pure + exported so the WARN condition is
|
|
708
|
+
* unit-testable without driving the whole runner. Author-side guard-rail only —
|
|
709
|
+
* no behavior change.
|
|
710
|
+
*/
|
|
711
|
+
function shouldWarnMissingAdvancePattern(instructions, advanceTransitionBodyPattern) {
|
|
712
|
+
if (advanceTransitionBodyPattern)
|
|
713
|
+
return false;
|
|
714
|
+
return instructions.some((s) => isAdvanceStep(s));
|
|
715
|
+
}
|
|
697
716
|
/**
|
|
698
717
|
* Parse the monotonic capture index from a capture filename. Every capture is
|
|
699
718
|
* written as `<idx>-<phase>-<unix-ms>-<hash>.json` where `<idx>` is a
|
|
@@ -952,6 +971,22 @@ function isClickViewSwapVerified(params) {
|
|
|
952
971
|
return true;
|
|
953
972
|
return textChanged && bytesDelta >= VIEW_SWAP_REVEAL_MIN_BYTES;
|
|
954
973
|
}
|
|
974
|
+
/**
|
|
975
|
+
* Whether an act-success step warrants a committed-value read-back before the
|
|
976
|
+
* weak view-swap/form-value signals are allowed to accept it. A controlled
|
|
977
|
+
* datepicker (react-datepicker) accepts the keystrokes, discards the value on
|
|
978
|
+
* its next render, and — because the resolved action opened its calendar popup
|
|
979
|
+
* as a `click` — rides `isClickViewSwapVerified` to a phantom `dom=false`
|
|
980
|
+
* success without ever committing the date. The read-back (and, on a rejected
|
|
981
|
+
* value, the datepicker fill primitive) is only worth running when the step's
|
|
982
|
+
* PROSE asked for a fill AND no strong signal (network/url/dom) already verified
|
|
983
|
+
* it — so the happy path (a real fill, a real click, an advance) is untouched.
|
|
984
|
+
* Keyed on step intent, not the resolved method, because the widget resolves as
|
|
985
|
+
* a click and its accessible name identifies the popup, not the field.
|
|
986
|
+
*/
|
|
987
|
+
function shouldReadbackFillOnActSuccess(params) {
|
|
988
|
+
return params.actReportedSuccess && params.isFillIntent && !params.hasStrongSignal;
|
|
989
|
+
}
|
|
955
990
|
/**
|
|
956
991
|
* Read-only count of ng-invalid form controls on the resolved frame. Side-effect-free
|
|
957
992
|
* counterpart to `probeFormValidityBeforeSubmit` (which also auto-fills
|
|
@@ -2150,6 +2185,200 @@ async function fillHtml5DateTimeInput(target, xpath, value) {
|
|
|
2150
2185
|
return null;
|
|
2151
2186
|
}
|
|
2152
2187
|
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Parse a loosely-formatted date string into calendar parts. The recon flow
|
|
2190
|
+
* passes dates in whatever shape the plan text used (ISO `yyyy-MM`, US
|
|
2191
|
+
* `MM/dd/yyyy`, etc.), but a react-datepicker commits through year/month/day
|
|
2192
|
+
* selection, so we need the numeric parts, not a display string. Uses date-fns
|
|
2193
|
+
* per project convention; returns null when no candidate format matches so the
|
|
2194
|
+
* caller skips the datepicker path rather than picking a wrong month.
|
|
2195
|
+
*/
|
|
2196
|
+
function parseDatepickerValue(value) {
|
|
2197
|
+
const candidates = ["yyyy-MM", "yyyy-MM-dd", "MM/dd/yyyy", "MM-dd-yyyy", "MM/yyyy", "M/yyyy"];
|
|
2198
|
+
for (const fmt of candidates) {
|
|
2199
|
+
const parsed = (0, date_fns_1.parse)(value.trim(), fmt, new Date(2000, 0, 1));
|
|
2200
|
+
if ((0, date_fns_1.isValid)(parsed)) {
|
|
2201
|
+
return { year: parsed.getFullYear(), monthIndex: parsed.getMonth(), day: parsed.getDate() };
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
return null;
|
|
2205
|
+
}
|
|
2206
|
+
/** Dispatch a real Enter keydown/keyup on the element (Stagehand's Locator has no `press`). */
|
|
2207
|
+
async function dispatchEnterKey(target, xpath) {
|
|
2208
|
+
const expr = `(() => {
|
|
2209
|
+
const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
2210
|
+
const el = r.singleNodeValue;
|
|
2211
|
+
if (!el) return "no-element";
|
|
2212
|
+
const opts = { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true };
|
|
2213
|
+
el.dispatchEvent(new KeyboardEvent("keydown", opts));
|
|
2214
|
+
el.dispatchEvent(new KeyboardEvent("keyup", opts));
|
|
2215
|
+
return "dispatched";
|
|
2216
|
+
})()`;
|
|
2217
|
+
try {
|
|
2218
|
+
await target.evaluate(expr);
|
|
2219
|
+
}
|
|
2220
|
+
catch {
|
|
2221
|
+
// best-effort: an Enter-dispatch failure shouldn't fail the fill
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
/** A readback whose value is a non-empty, date-shaped string counts as committed. */
|
|
2225
|
+
function isCommittedDateReadback(readback) {
|
|
2226
|
+
if (readback === null)
|
|
2227
|
+
return false;
|
|
2228
|
+
if (readback.outcome === "rejected")
|
|
2229
|
+
return false;
|
|
2230
|
+
// A datepicker reformats what it accepts (e.g. "01/2020" → "2020-01"), so
|
|
2231
|
+
// "differs" is a commit, not a rejection — require only a non-empty value
|
|
2232
|
+
// that contains a 4-digit year, ruling out stray placeholder text.
|
|
2233
|
+
return readback.postValue.trim().length > 0 && /\d{4}/.test(readback.postValue);
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* Fill a `type="text"` datepicker widget (react-datepicker — the standard
|
|
2237
|
+
* ATS start/end-date control), which `fillHtml5DateTimeInput`
|
|
2238
|
+
* cannot: react-datepicker rejects a programmatic `.value` write and commits
|
|
2239
|
+
* only through its own keyboard/calendar `onChange`. Returns `null` for any
|
|
2240
|
+
* input that is NOT a date-like text control, so every other text field stays
|
|
2241
|
+
* on today's exact fill path.
|
|
2242
|
+
*
|
|
2243
|
+
* Two gestures, in order:
|
|
2244
|
+
* 1. Keyboard entry — real keystrokes via `locator.type` (the proven
|
|
2245
|
+
* framework-control path at {@link verifyDomEffect}) + Enter. Works for
|
|
2246
|
+
* *typeable* datepickers.
|
|
2247
|
+
* 2. Open-and-pick — click to open `.react-datepicker`, then either set the
|
|
2248
|
+
* year/month `.range-select` dropdowns and click the `.react-datepicker__month-N`
|
|
2249
|
+
* cell (month/year-picker variant — the reported ATS widget, which is
|
|
2250
|
+
* picker-only and ignores typed input) or click the matching
|
|
2251
|
+
* `.react-datepicker__day` cell (day-grid variant). Commit fires on the cell
|
|
2252
|
+
* click.
|
|
2253
|
+
*
|
|
2254
|
+
* The committed value is confirmed via {@link verifyFillReadback}; a datepicker
|
|
2255
|
+
* reformats what it accepts, so a non-empty date-shaped readback is success.
|
|
2256
|
+
*
|
|
2257
|
+
* Return contract mirrors {@link fillHtml5DateTimeInput}: `null` means "not a
|
|
2258
|
+
* datepicker — caller falls through to the generic readback verifier"; a
|
|
2259
|
+
* non-null object means "this WAS a datepicker, here's the outcome". A non-null
|
|
2260
|
+
* `filled:false` therefore deliberately TERMINATES the cascade (the caller
|
|
2261
|
+
* records the failure and does NOT run the generic verifier) — a gated-in
|
|
2262
|
+
* datepicker that neither gesture commits is a real fill failure, not an
|
|
2263
|
+
* unrecognized field.
|
|
2264
|
+
*
|
|
2265
|
+
* Site-agnostic: react-datepicker's markup + commit model is universal across
|
|
2266
|
+
* ATS tenants using it.
|
|
2267
|
+
*/
|
|
2268
|
+
async function fillTextDatepickerInput(target, selector, value) {
|
|
2269
|
+
const xpath = xpathBody(selector);
|
|
2270
|
+
if (!xpath)
|
|
2271
|
+
return null;
|
|
2272
|
+
const parts = parseDatepickerValue(value);
|
|
2273
|
+
if (!parts)
|
|
2274
|
+
return null;
|
|
2275
|
+
// Gate: only proceed for a text input that looks like a datepicker. The
|
|
2276
|
+
// definitive signal is a `.react-datepicker__input-container` wrapper; we
|
|
2277
|
+
// also accept id/name/class/aria date hints for non-react libraries.
|
|
2278
|
+
const gateExpr = `(() => {
|
|
2279
|
+
const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
2280
|
+
const el = r.singleNodeValue;
|
|
2281
|
+
if (!el || el.tagName !== "INPUT") return { match: false };
|
|
2282
|
+
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
2283
|
+
if (type !== "text") return { match: false };
|
|
2284
|
+
const inContainer = !!(el.closest && el.closest(".react-datepicker__input-container, .react-datepicker-wrapper"));
|
|
2285
|
+
const hint = ((el.id || "") + " " + (el.name || "") + " " + (el.className || "") + " " + (el.getAttribute("aria-label") || "") + " " + (el.getAttribute("placeholder") || "")).toLowerCase();
|
|
2286
|
+
const looksDate = /date|datepicker/.test(hint) || /\\bmm[\\/-]?(dd[\\/-]?)?yyyy\\b/.test(hint);
|
|
2287
|
+
return { match: inContainer || looksDate };
|
|
2288
|
+
})()`;
|
|
2289
|
+
try {
|
|
2290
|
+
const gate = await target.evaluate(gateExpr);
|
|
2291
|
+
if (!gate || typeof gate !== "object" || gate.match !== true) {
|
|
2292
|
+
return null;
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
catch {
|
|
2296
|
+
return null;
|
|
2297
|
+
}
|
|
2298
|
+
// Strategy 1 — keyboard entry (typeable datepickers). Mirrors the Angular
|
|
2299
|
+
// reactive-form re-type at verifyDomEffect: real CDP keystrokes flow through
|
|
2300
|
+
// the widget's own onChange, unlike a programmatic value write.
|
|
2301
|
+
const typed = (0, date_fns_1.format)(new Date(parts.year, parts.monthIndex, parts.day), "MM/dd/yyyy");
|
|
2302
|
+
try {
|
|
2303
|
+
const locator = target.locator(selector).first();
|
|
2304
|
+
await locator.fill("");
|
|
2305
|
+
await locator.type(typed, { delay: 50 });
|
|
2306
|
+
// react-datepicker commits typed input on Enter/blur. The Stagehand Locator
|
|
2307
|
+
// has no `press`, so dispatch the Enter keydown/keyup from page context
|
|
2308
|
+
// (the widget's own keydown handler listens here) then blur via the shared
|
|
2309
|
+
// jQuery-change helper.
|
|
2310
|
+
await dispatchEnterKey(target, xpath);
|
|
2311
|
+
await dispatchJqueryChangeEvent(target, selector);
|
|
2312
|
+
const readback = await verifyFillReadback(target, xpath, value);
|
|
2313
|
+
if (isCommittedDateReadback(readback)) {
|
|
2314
|
+
return { filled: true, postValue: readback.postValue, strategy: "keyboard" };
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
catch {
|
|
2318
|
+
// fall through to open-and-pick
|
|
2319
|
+
}
|
|
2320
|
+
// Strategy 2 — open-and-pick. Required for picker-only widgets (the reported
|
|
2321
|
+
// month/year picker ignores typed input entirely).
|
|
2322
|
+
try {
|
|
2323
|
+
const locator = target.locator(selector).first();
|
|
2324
|
+
await locator.click();
|
|
2325
|
+
}
|
|
2326
|
+
catch {
|
|
2327
|
+
return { filled: false, postValue: "", strategy: "none" };
|
|
2328
|
+
}
|
|
2329
|
+
const pickExpr = `(() => {
|
|
2330
|
+
const year = ${parts.year};
|
|
2331
|
+
const monthIndex = ${parts.monthIndex};
|
|
2332
|
+
const day = ${parts.day};
|
|
2333
|
+
const cal = document.querySelector(".react-datepicker");
|
|
2334
|
+
if (!cal) return { picked: false, reason: "no-calendar" };
|
|
2335
|
+
const fireChange = (el) => { el.dispatchEvent(new Event("input", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); };
|
|
2336
|
+
const monthCells = cal.querySelectorAll(".react-datepicker__month-text");
|
|
2337
|
+
const dayGrid = cal.querySelectorAll(".react-datepicker__day:not(.react-datepicker__day--outside-month):not(.react-datepicker__day--disabled)");
|
|
2338
|
+
// Month/year-picker variant: click the target month cell (0-based month
|
|
2339
|
+
// index maps to the __month-N class) — THIS is the commit. The year/month
|
|
2340
|
+
// dropdowns are set first only so the calendar shows the right year's month
|
|
2341
|
+
// page. NOTE: "range-select" is a SITE-SPECIFIC class (the ATS site's own
|
|
2342
|
+
// CSS), not a react-datepicker class; the bare "select" fallback covers
|
|
2343
|
+
// other wrappers.
|
|
2344
|
+
// Stock react-datepicker renders its month/year dropdowns as clickable divs,
|
|
2345
|
+
// so on those the select loop is inert and the month-cell click carries it.
|
|
2346
|
+
if (monthCells.length > 0) {
|
|
2347
|
+
const selects = cal.querySelectorAll("select.range-select, select");
|
|
2348
|
+
for (const sel of selects) {
|
|
2349
|
+
const opts = Array.from(sel.options).map((o) => (o.value || "").trim());
|
|
2350
|
+
const yearOpt = opts.indexOf(String(year));
|
|
2351
|
+
if (yearOpt >= 0) { sel.selectedIndex = yearOpt; fireChange(sel); continue; }
|
|
2352
|
+
const monthOpt = opts.indexOf(String(monthIndex));
|
|
2353
|
+
if (monthOpt >= 0) { sel.selectedIndex = monthOpt; fireChange(sel); }
|
|
2354
|
+
}
|
|
2355
|
+
const monthCell = cal.querySelector(".react-datepicker__month-" + monthIndex + ".react-datepicker__month-text");
|
|
2356
|
+
const cell = monthCell || cal.querySelectorAll(".react-datepicker__month-text")[monthIndex];
|
|
2357
|
+
if (cell) { cell.click(); return { picked: true, variant: "month-year" }; }
|
|
2358
|
+
return { picked: false, reason: "no-month-cell" };
|
|
2359
|
+
}
|
|
2360
|
+
// Day-grid variant: click the day cell matching the parsed day number.
|
|
2361
|
+
if (dayGrid.length > 0) {
|
|
2362
|
+
for (const cell of dayGrid) {
|
|
2363
|
+
if ((cell.textContent || "").trim() === String(day)) { cell.click(); return { picked: true, variant: "day-grid" }; }
|
|
2364
|
+
}
|
|
2365
|
+
return { picked: false, reason: "no-day-cell" };
|
|
2366
|
+
}
|
|
2367
|
+
return { picked: false, reason: "empty-calendar" };
|
|
2368
|
+
})()`;
|
|
2369
|
+
try {
|
|
2370
|
+
const picked = await target.evaluate(pickExpr);
|
|
2371
|
+
const didPick = !!picked && typeof picked === "object" && picked.picked === true;
|
|
2372
|
+
const readback = await verifyFillReadback(target, xpath, value);
|
|
2373
|
+
if (didPick && isCommittedDateReadback(readback)) {
|
|
2374
|
+
return { filled: true, postValue: readback.postValue, strategy: "calendar-pick" };
|
|
2375
|
+
}
|
|
2376
|
+
return { filled: false, postValue: readback?.postValue ?? "", strategy: "none" };
|
|
2377
|
+
}
|
|
2378
|
+
catch {
|
|
2379
|
+
return { filled: false, postValue: "", strategy: "none" };
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2153
2382
|
/**
|
|
2154
2383
|
* Read back an element's value after a fill action and compare to the
|
|
2155
2384
|
* expected value. Catches silent-value-rejection cases that the verifier's
|
|
@@ -2811,6 +3040,30 @@ function parseFillStep(instruction) {
|
|
|
2811
3040
|
return null;
|
|
2812
3041
|
return { fieldLabel, value };
|
|
2813
3042
|
}
|
|
3043
|
+
/**
|
|
3044
|
+
* Extract the quoted VALUE a fill step wants written, tolerant of the field-
|
|
3045
|
+
* label phrasing `parseFillStep` is strict about. `parseFillStep` requires the
|
|
3046
|
+
* canonical `<label> field with '<value>'` shape, so real-world prose that
|
|
3047
|
+
* inserts words between the field noun and `with` — e.g. "Fill in the Start
|
|
3048
|
+
* Date field FOR WORK EXPERIENCE with '01/2020'" — parses to `null`, missing
|
|
3049
|
+
* the fill intent entirely. This looser parser recognizes a fill verb plus a
|
|
3050
|
+
* `with '<value>'` clause and returns just the value, so the act-success
|
|
3051
|
+
* datepicker guard can key off intent that survives that phrasing drift.
|
|
3052
|
+
*
|
|
3053
|
+
* Deliberately excludes select steps (checked first, mirroring
|
|
3054
|
+
* `resolveDeepLocatorActuation`'s precedence) so a `select '…'` step whose
|
|
3055
|
+
* prose happens to trail a quoted value is never mistaken for a fill. Returns
|
|
3056
|
+
* `null` for clicks, selects, and any step without a quoted `with '…'` value.
|
|
3057
|
+
*/
|
|
3058
|
+
function parseFillValueIntent(instruction) {
|
|
3059
|
+
if (parseSelectStep(instruction))
|
|
3060
|
+
return null;
|
|
3061
|
+
if (!/\bfill(?:\s+in)?\b/i.test(instruction))
|
|
3062
|
+
return null;
|
|
3063
|
+
const match = instruction.match(/\bwith\s+'([^']+)'/i);
|
|
3064
|
+
const value = match?.[1]?.trim();
|
|
3065
|
+
return value ? { value } : null;
|
|
3066
|
+
}
|
|
2814
3067
|
/**
|
|
2815
3068
|
* Parse a single-choice RADIO flow step into the option to click and (when
|
|
2816
3069
|
* present) the question label that scopes which radio group it targets.
|
|
@@ -5721,20 +5974,42 @@ async function executeStepWithHealing(params) {
|
|
|
5721
5974
|
}
|
|
5722
5975
|
}
|
|
5723
5976
|
else {
|
|
5724
|
-
//
|
|
5725
|
-
//
|
|
5726
|
-
//
|
|
5727
|
-
//
|
|
5728
|
-
//
|
|
5729
|
-
//
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5977
|
+
// Not an HTML5 date input. First try the text-datepicker
|
|
5978
|
+
// primitive (react-datepicker — ATS start/end-date
|
|
5979
|
+
// controls): those are `type="text"`, so fillHtml5DateTimeInput
|
|
5980
|
+
// returns null, and the generic fill's value-write never commits
|
|
5981
|
+
// the widget. Returns null for any non-datepicker text input, so
|
|
5982
|
+
// every other field falls through to the readback verifier below
|
|
5983
|
+
// unchanged.
|
|
5984
|
+
const datepickerFill = await fillTextDatepickerInput(dateFillTarget, overriddenTarget.selector, fillValue);
|
|
5985
|
+
if (datepickerFill !== null) {
|
|
5986
|
+
record.errorMessage = datepickerFill.filled
|
|
5987
|
+
? `text-datepicker-fill: filled via ${datepickerFill.strategy} "${datepickerFill.postValue}"`
|
|
5988
|
+
: `text-datepicker-fill: failed to commit "${fillValue.slice(0, 60)}" (post=${datepickerFill.postValue})`;
|
|
5989
|
+
if (datepickerFill.filled) {
|
|
5990
|
+
record.actResultSuccess = true;
|
|
5991
|
+
resolvedAction = overriddenTarget;
|
|
5992
|
+
}
|
|
5993
|
+
else {
|
|
5734
5994
|
record.actResultSuccess = false;
|
|
5735
5995
|
}
|
|
5736
|
-
|
|
5737
|
-
|
|
5996
|
+
}
|
|
5997
|
+
else {
|
|
5998
|
+
// Fix I: not a date input — verify the regular fill landed.
|
|
5999
|
+
// Catches silent-value-rejection cases (HTML5 type validation
|
|
6000
|
+
// on number/email/url/tel inputs, framework-controlled-
|
|
6001
|
+
// component rejection, masked-input library reformatting).
|
|
6002
|
+
// Generic primitive that the verifier's existing signals
|
|
6003
|
+
// (network/url/dom/htmlDelta/textChanged) miss.
|
|
6004
|
+
const readback = await verifyFillReadback(dateFillTarget, overriddenTarget.selector, fillValue);
|
|
6005
|
+
if (readback !== null) {
|
|
6006
|
+
if (readback.outcome === "rejected") {
|
|
6007
|
+
record.errorMessage = `fill-value-rejected: tried "${fillValue.slice(0, 60)}" on <${readback.tag}>; element value remains empty (silent rejection — HTML5 type validation, framework controlled-component, or masked-input library)`;
|
|
6008
|
+
record.actResultSuccess = false;
|
|
6009
|
+
}
|
|
6010
|
+
else if (readback.outcome === "differs") {
|
|
6011
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} fill-value-differs: tried "${fillValue.slice(0, 60)}" got "${readback.postValue.slice(0, 60)}" (framework reformatted)`);
|
|
6012
|
+
}
|
|
5738
6013
|
}
|
|
5739
6014
|
}
|
|
5740
6015
|
}
|
|
@@ -6074,11 +6349,66 @@ async function executeStepWithHealing(params) {
|
|
|
6074
6349
|
// actions (fill/check/etc.), same as domVerified, so it never lets an
|
|
6075
6350
|
// advance/submit step ride a stray value mutation elsewhere on the page.
|
|
6076
6351
|
const formValueVerified = isStateClass && post.formValueSignature !== pre.formValueSignature;
|
|
6352
|
+
// Committed-value guard on the act-success path. A controlled datepicker
|
|
6353
|
+
// (react-datepicker) accepts the typed value, discards it on React's next
|
|
6354
|
+
// render, and — since the act resolved as a `click` that opened the calendar
|
|
6355
|
+
// popup — rides `clickViewSwapVerified` to a phantom `dom=false` success
|
|
6356
|
+
// without ever committing the date. The 1.9.0 `fillTextDatepickerInput`
|
|
6357
|
+
// primitive (the open-and-pick gesture that DOES commit) sits in a fallback
|
|
6358
|
+
// block only reached when the act "technically failed", so it never runs on
|
|
6359
|
+
// this path. Route a fill-intent step whose value did NOT land (read-back
|
|
6360
|
+
// rejected) into that primitive before the weak view-swap/form-value signals
|
|
6361
|
+
// accept it — keyed on step PROSE, since the widget resolves as a click and
|
|
6362
|
+
// its accessible name identifies the popup, not the field.
|
|
6363
|
+
const fillIntent = parseFillValueIntent(step);
|
|
6364
|
+
const hasStrongSignal = networkIsRealAdvance || urlChanged || domVerifiedForStep;
|
|
6365
|
+
let datepickerCommitted = false;
|
|
6366
|
+
let datepickerRejected = false;
|
|
6367
|
+
if (shouldReadbackFillOnActSuccess({
|
|
6368
|
+
actReportedSuccess: record.actResultSuccess === true,
|
|
6369
|
+
isFillIntent: fillIntent !== null,
|
|
6370
|
+
hasStrongSignal,
|
|
6371
|
+
})) {
|
|
6372
|
+
const readbackSelector = resolvedAction?.selector ?? triedSelectors[triedSelectors.length - 1] ?? null;
|
|
6373
|
+
if (readbackSelector && fillIntent !== null) {
|
|
6374
|
+
const readbackTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
6375
|
+
const readback = await verifyFillReadback(readbackTarget, readbackSelector, fillIntent.value);
|
|
6376
|
+
if (readback?.outcome === "rejected") {
|
|
6377
|
+
// Value didn't land. First try the datepicker primitive; it self-gates
|
|
6378
|
+
// on react-datepicker's `.react-datepicker__input-container` and returns
|
|
6379
|
+
// null for any non-datepicker input, so a plain rejected fill falls
|
|
6380
|
+
// through to the failure path below unchanged.
|
|
6381
|
+
const datepickerFill = await fillTextDatepickerInput(readbackTarget, readbackSelector, fillIntent.value);
|
|
6382
|
+
if (datepickerFill !== null) {
|
|
6383
|
+
if (datepickerFill.filled) {
|
|
6384
|
+
datepickerCommitted = true;
|
|
6385
|
+
record.errorMessage = `text-datepicker-fill: filled via ${datepickerFill.strategy} "${datepickerFill.postValue}"`;
|
|
6386
|
+
}
|
|
6387
|
+
else {
|
|
6388
|
+
// A gated-in datepicker that neither gesture commits is a real fill
|
|
6389
|
+
// failure (per fillTextDatepickerInput's contract) — authoritative,
|
|
6390
|
+
// so suppress the weak view-swap/form-value acceptance and escalate.
|
|
6391
|
+
datepickerRejected = true;
|
|
6392
|
+
record.actResultSuccess = false;
|
|
6393
|
+
record.errorMessage = `text-datepicker-fill: failed to commit "${fillIntent.value.slice(0, 60)}" (post=${datepickerFill.postValue})`;
|
|
6394
|
+
}
|
|
6395
|
+
}
|
|
6396
|
+
else {
|
|
6397
|
+
// Not a datepicker — the fill was genuinely rejected. Same message
|
|
6398
|
+
// shape as the observe-act fill-fallback's readback verifier so the
|
|
6399
|
+
// failure dump reads identically regardless of which path caught it.
|
|
6400
|
+
datepickerRejected = true;
|
|
6401
|
+
record.actResultSuccess = false;
|
|
6402
|
+
record.errorMessage = `fill-value-rejected: tried "${fillIntent.value.slice(0, 60)}" on <${readback.tag}>; element value remains empty (silent rejection — HTML5 type validation, framework controlled-component, or masked-input library)`;
|
|
6403
|
+
}
|
|
6404
|
+
}
|
|
6405
|
+
}
|
|
6406
|
+
}
|
|
6077
6407
|
let verified = networkIsRealAdvance ||
|
|
6078
6408
|
urlChanged ||
|
|
6079
6409
|
domVerifiedForStep ||
|
|
6080
|
-
|
|
6081
|
-
formValueVerified;
|
|
6410
|
+
datepickerCommitted ||
|
|
6411
|
+
(!datepickerRejected && (clickViewSwapVerified || formValueVerified));
|
|
6082
6412
|
// Final-step submit-verification gate. Replaces the deterministic
|
|
6083
6413
|
// submitEndpointPattern regex with a Haiku 4.5 LLM judgment over
|
|
6084
6414
|
// multi-signal evidence (network captures, page URL/title, DOM
|
|
@@ -6190,18 +6520,22 @@ async function executeStepWithHealing(params) {
|
|
|
6190
6520
|
}
|
|
6191
6521
|
if (verified && record.verifiedBy === null) {
|
|
6192
6522
|
// Preserve a richer verdict set earlier in this attempt (e.g.
|
|
6193
|
-
// "submitted-state-dom" from the final-step DOM fallback).
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6523
|
+
// "submitted-state-dom" from the final-step DOM fallback). A datepicker
|
|
6524
|
+
// commit is a real DOM value change, so it ranks ahead of the weaker
|
|
6525
|
+
// view-swap signal that the same calendar-open click also produces.
|
|
6526
|
+
record.verifiedBy = datepickerCommitted
|
|
6527
|
+
? "dom"
|
|
6528
|
+
: clickViewSwapVerified
|
|
6529
|
+
? "view-swap"
|
|
6530
|
+
: urlChanged
|
|
6531
|
+
? "url"
|
|
6532
|
+
: networkFired
|
|
6533
|
+
? "network"
|
|
6534
|
+
: domVerifiedForStep
|
|
6535
|
+
? "dom"
|
|
6536
|
+
: formValueVerified
|
|
6537
|
+
? "form-value"
|
|
6538
|
+
: "dom";
|
|
6205
6539
|
}
|
|
6206
6540
|
// N+16 probe: Stagehand's CDP click sometimes lands on the button without
|
|
6207
6541
|
// triggering React's SyntheticEvent layer (or jQuery delegated handlers).
|
|
@@ -6707,6 +7041,9 @@ async function runHealingFlow(deps) {
|
|
|
6707
7041
|
}
|
|
6708
7042
|
},
|
|
6709
7043
|
});
|
|
7044
|
+
if (shouldWarnMissingAdvancePattern(steps.map((s) => s.instruction), deps.advanceTransitionBodyPattern ?? null)) {
|
|
7045
|
+
logger.warn("flow has advance steps but no advanceTransitionBodyPattern — DOM-only advance guard is disarmed; pointer/page desync is possible on validation-re-render wizards");
|
|
7046
|
+
}
|
|
6710
7047
|
try {
|
|
6711
7048
|
for (const [i, s] of steps.entries()) {
|
|
6712
7049
|
if (maxFlowMs !== undefined && Date.now() - start > maxFlowMs) {
|