@enricai/barnacle 1.7.5 → 1.9.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/config.d.ts +2 -2
- package/dist/scraper/behavioral-signals.d.ts +1 -1
- package/dist/scraper/behavioral-signals.js +1 -1
- package/dist/scraper/deep-locator-candidates.d.ts +1 -1
- package/dist/scraper/deep-locator-candidates.js +1 -1
- package/dist/scraper/deep-locator-click.d.ts +1 -1
- package/dist/scraper/deep-locator-click.js +1 -1
- package/dist/scraper/deep-locator-fake.d.ts +2 -2
- package/dist/scraper/deep-locator-fake.d.ts.map +1 -1
- package/dist/scraper/deep-locator-fake.js +2 -2
- package/dist/scraper/deep-locator-fake.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts +56 -3
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +256 -20
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/frame-target.d.ts +1 -1
- package/dist/scraper/frame-target.js +2 -2
- package/dist/scraper/http-client.d.ts +1 -1
- package/dist/scripts/recon-browser.d.ts +7 -4
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +37 -10
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate-multicall-fixture.d.ts +1 -1
- package/dist/scripts/recon-generate-multicall-fixture.js +1 -1
- package/dist/scripts/recon-generate.d.ts +2 -2
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +29 -4
- package/dist/scripts/recon-generate.js.map +1 -1
- package/dist/scripts/recon-replay-jobs.js +2 -2
- package/dist/scripts/recon-replay-jobs.js.map +1 -1
- package/dist/testing/resume-fixture.d.ts +7 -0
- package/dist/testing/resume-fixture.d.ts.map +1 -1
- package/dist/testing/resume-fixture.js +9 -1
- package/dist/testing/resume-fixture.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;
|
|
@@ -51,6 +52,7 @@ exports.extractSubmitFailureEvidence = extractSubmitFailureEvidence;
|
|
|
51
52
|
exports.extractGaEventEvidence = extractGaEventEvidence;
|
|
52
53
|
exports.normalizeDateValue = normalizeDateValue;
|
|
53
54
|
exports.fillHtml5DateTimeInput = fillHtml5DateTimeInput;
|
|
55
|
+
exports.fillTextDatepickerInput = fillTextDatepickerInput;
|
|
54
56
|
exports.verifyFillReadback = verifyFillReadback;
|
|
55
57
|
exports.isUploadAffordanceLabel = isUploadAffordanceLabel;
|
|
56
58
|
exports.writeFixtureToTempFile = writeFixtureToTempFile;
|
|
@@ -78,6 +80,7 @@ const node_fs_1 = require("node:fs");
|
|
|
78
80
|
const node_os_1 = require("node:os");
|
|
79
81
|
const node_path_1 = require("node:path");
|
|
80
82
|
const ai_1 = require("ai");
|
|
83
|
+
const date_fns_1 = require("date-fns");
|
|
81
84
|
const config_1 = require("../config");
|
|
82
85
|
const errors_1 = require("../lib/errors");
|
|
83
86
|
const error_messages_1 = require("../lib/llm/judges/error-messages");
|
|
@@ -530,8 +533,8 @@ async function snapshotPage(target, signalCounter, page) {
|
|
|
530
533
|
* application despite returning a 2xx HTTP status. Many ATSs use a "200 OK
|
|
531
534
|
* with rejection envelope" pattern instead of a 4xx: one JSON-envelope ATS returns
|
|
532
535
|
* `{not_qualified: true, error: "Not qualified reason: <field>"}`,
|
|
533
|
-
*
|
|
534
|
-
* `{qualified: false, reason: "..."}`,
|
|
536
|
+
* others use `{rejected: true, reason: "..."}`,
|
|
537
|
+
* `{qualified: false, reason: "..."}`, or
|
|
535
538
|
* `{status: "rejected"}`. Empirically verified on a JSON-envelope ATS: 4/6 historical
|
|
536
539
|
* /integrated_apply 200s on this codebase had `not_qualified: true` and
|
|
537
540
|
* we treated them as wins because the audit only checked HTTP status.
|
|
@@ -694,6 +697,20 @@ function isAdvanceStep(instruction) {
|
|
|
694
697
|
const haystack = instruction.toLowerCase();
|
|
695
698
|
return ADVANCE_STEP_PHRASES.some((p) => haystack.includes(p));
|
|
696
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* Whether a flow should be WARNed that its DOM-only advance guard is disarmed.
|
|
702
|
+
* `isDomOnlyAdvanceVerified` only vetoes a DOM-only "advance" when
|
|
703
|
+
* `advanceTransitionBodyPattern` is set; a flow with advance steps but no
|
|
704
|
+
* pattern silently loses that veto, so a validation-re-render wizard can desync
|
|
705
|
+
* its step pointer from the page. Pure + exported so the WARN condition is
|
|
706
|
+
* unit-testable without driving the whole runner. Author-side guard-rail only —
|
|
707
|
+
* no behavior change.
|
|
708
|
+
*/
|
|
709
|
+
function shouldWarnMissingAdvancePattern(instructions, advanceTransitionBodyPattern) {
|
|
710
|
+
if (advanceTransitionBodyPattern)
|
|
711
|
+
return false;
|
|
712
|
+
return instructions.some((s) => isAdvanceStep(s));
|
|
713
|
+
}
|
|
697
714
|
/**
|
|
698
715
|
* Parse the monotonic capture index from a capture filename. Every capture is
|
|
699
716
|
* written as `<idx>-<phase>-<unix-ms>-<hash>.json` where `<idx>` is a
|
|
@@ -928,7 +945,7 @@ function isDomOnlyAdvanceVerified(params) {
|
|
|
928
945
|
* **Small-delta reveal credit:** a sub-section unhiding within an
|
|
929
946
|
* already-loaded page (e.g. a validation-triggered "Work History requirement"
|
|
930
947
|
* gate message) grows the DOM by far less than a full screen swap — measured
|
|
931
|
-
* case: +789B on
|
|
948
|
+
* case: +789B on the top-window site, ~63x below the 5000B full-swap default. Below
|
|
932
949
|
* `viewSwapMinBytesThreshold`, credit the click anyway when the growth ALSO
|
|
933
950
|
* changed visible text (`textChanged`) and clears the lower
|
|
934
951
|
* `viewSwapRevealMinBytesThreshold` floor (default 500B, matching
|
|
@@ -960,7 +977,7 @@ function isClickViewSwapVerified(params) {
|
|
|
960
977
|
* questions" — when this count grows from 0 (pre-submit) to ≥1 (post-attempt-1),
|
|
961
978
|
* attempts 2-5 cannot succeed and the cascade should route to replan
|
|
962
979
|
* immediately instead of burning Stagehand calls. Accepts a `FrameTarget` so a
|
|
963
|
-
* wizard embedded in a cross-origin iframe (e.g.
|
|
980
|
+
* wizard embedded in a cross-origin iframe (e.g. an embedded apply-wizard form) is
|
|
964
981
|
* scanned on its own frame; a main-frame target delegates straight to
|
|
965
982
|
* `page.evaluate`, matching today's behavior byte-for-byte.
|
|
966
983
|
*/
|
|
@@ -2150,6 +2167,200 @@ async function fillHtml5DateTimeInput(target, xpath, value) {
|
|
|
2150
2167
|
return null;
|
|
2151
2168
|
}
|
|
2152
2169
|
}
|
|
2170
|
+
/**
|
|
2171
|
+
* Parse a loosely-formatted date string into calendar parts. The recon flow
|
|
2172
|
+
* passes dates in whatever shape the plan text used (ISO `yyyy-MM`, US
|
|
2173
|
+
* `MM/dd/yyyy`, etc.), but a react-datepicker commits through year/month/day
|
|
2174
|
+
* selection, so we need the numeric parts, not a display string. Uses date-fns
|
|
2175
|
+
* per project convention; returns null when no candidate format matches so the
|
|
2176
|
+
* caller skips the datepicker path rather than picking a wrong month.
|
|
2177
|
+
*/
|
|
2178
|
+
function parseDatepickerValue(value) {
|
|
2179
|
+
const candidates = ["yyyy-MM", "yyyy-MM-dd", "MM/dd/yyyy", "MM-dd-yyyy", "MM/yyyy", "M/yyyy"];
|
|
2180
|
+
for (const fmt of candidates) {
|
|
2181
|
+
const parsed = (0, date_fns_1.parse)(value.trim(), fmt, new Date(2000, 0, 1));
|
|
2182
|
+
if ((0, date_fns_1.isValid)(parsed)) {
|
|
2183
|
+
return { year: parsed.getFullYear(), monthIndex: parsed.getMonth(), day: parsed.getDate() };
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
return null;
|
|
2187
|
+
}
|
|
2188
|
+
/** Dispatch a real Enter keydown/keyup on the element (Stagehand's Locator has no `press`). */
|
|
2189
|
+
async function dispatchEnterKey(target, xpath) {
|
|
2190
|
+
const expr = `(() => {
|
|
2191
|
+
const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
2192
|
+
const el = r.singleNodeValue;
|
|
2193
|
+
if (!el) return "no-element";
|
|
2194
|
+
const opts = { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true };
|
|
2195
|
+
el.dispatchEvent(new KeyboardEvent("keydown", opts));
|
|
2196
|
+
el.dispatchEvent(new KeyboardEvent("keyup", opts));
|
|
2197
|
+
return "dispatched";
|
|
2198
|
+
})()`;
|
|
2199
|
+
try {
|
|
2200
|
+
await target.evaluate(expr);
|
|
2201
|
+
}
|
|
2202
|
+
catch {
|
|
2203
|
+
// best-effort: an Enter-dispatch failure shouldn't fail the fill
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
/** A readback whose value is a non-empty, date-shaped string counts as committed. */
|
|
2207
|
+
function isCommittedDateReadback(readback) {
|
|
2208
|
+
if (readback === null)
|
|
2209
|
+
return false;
|
|
2210
|
+
if (readback.outcome === "rejected")
|
|
2211
|
+
return false;
|
|
2212
|
+
// A datepicker reformats what it accepts (e.g. "01/2020" → "2020-01"), so
|
|
2213
|
+
// "differs" is a commit, not a rejection — require only a non-empty value
|
|
2214
|
+
// that contains a 4-digit year, ruling out stray placeholder text.
|
|
2215
|
+
return readback.postValue.trim().length > 0 && /\d{4}/.test(readback.postValue);
|
|
2216
|
+
}
|
|
2217
|
+
/**
|
|
2218
|
+
* Fill a `type="text"` datepicker widget (react-datepicker — the standard
|
|
2219
|
+
* ATS start/end-date control), which `fillHtml5DateTimeInput`
|
|
2220
|
+
* cannot: react-datepicker rejects a programmatic `.value` write and commits
|
|
2221
|
+
* only through its own keyboard/calendar `onChange`. Returns `null` for any
|
|
2222
|
+
* input that is NOT a date-like text control, so every other text field stays
|
|
2223
|
+
* on today's exact fill path.
|
|
2224
|
+
*
|
|
2225
|
+
* Two gestures, in order:
|
|
2226
|
+
* 1. Keyboard entry — real keystrokes via `locator.type` (the proven
|
|
2227
|
+
* framework-control path at {@link verifyDomEffect}) + Enter. Works for
|
|
2228
|
+
* *typeable* datepickers.
|
|
2229
|
+
* 2. Open-and-pick — click to open `.react-datepicker`, then either set the
|
|
2230
|
+
* year/month `.range-select` dropdowns and click the `.react-datepicker__month-N`
|
|
2231
|
+
* cell (month/year-picker variant — the reported ATS widget, which is
|
|
2232
|
+
* picker-only and ignores typed input) or click the matching
|
|
2233
|
+
* `.react-datepicker__day` cell (day-grid variant). Commit fires on the cell
|
|
2234
|
+
* click.
|
|
2235
|
+
*
|
|
2236
|
+
* The committed value is confirmed via {@link verifyFillReadback}; a datepicker
|
|
2237
|
+
* reformats what it accepts, so a non-empty date-shaped readback is success.
|
|
2238
|
+
*
|
|
2239
|
+
* Return contract mirrors {@link fillHtml5DateTimeInput}: `null` means "not a
|
|
2240
|
+
* datepicker — caller falls through to the generic readback verifier"; a
|
|
2241
|
+
* non-null object means "this WAS a datepicker, here's the outcome". A non-null
|
|
2242
|
+
* `filled:false` therefore deliberately TERMINATES the cascade (the caller
|
|
2243
|
+
* records the failure and does NOT run the generic verifier) — a gated-in
|
|
2244
|
+
* datepicker that neither gesture commits is a real fill failure, not an
|
|
2245
|
+
* unrecognized field.
|
|
2246
|
+
*
|
|
2247
|
+
* Site-agnostic: react-datepicker's markup + commit model is universal across
|
|
2248
|
+
* ATS tenants using it.
|
|
2249
|
+
*/
|
|
2250
|
+
async function fillTextDatepickerInput(target, selector, value) {
|
|
2251
|
+
const xpath = xpathBody(selector);
|
|
2252
|
+
if (!xpath)
|
|
2253
|
+
return null;
|
|
2254
|
+
const parts = parseDatepickerValue(value);
|
|
2255
|
+
if (!parts)
|
|
2256
|
+
return null;
|
|
2257
|
+
// Gate: only proceed for a text input that looks like a datepicker. The
|
|
2258
|
+
// definitive signal is a `.react-datepicker__input-container` wrapper; we
|
|
2259
|
+
// also accept id/name/class/aria date hints for non-react libraries.
|
|
2260
|
+
const gateExpr = `(() => {
|
|
2261
|
+
const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
2262
|
+
const el = r.singleNodeValue;
|
|
2263
|
+
if (!el || el.tagName !== "INPUT") return { match: false };
|
|
2264
|
+
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
2265
|
+
if (type !== "text") return { match: false };
|
|
2266
|
+
const inContainer = !!(el.closest && el.closest(".react-datepicker__input-container, .react-datepicker-wrapper"));
|
|
2267
|
+
const hint = ((el.id || "") + " " + (el.name || "") + " " + (el.className || "") + " " + (el.getAttribute("aria-label") || "") + " " + (el.getAttribute("placeholder") || "")).toLowerCase();
|
|
2268
|
+
const looksDate = /date|datepicker/.test(hint) || /\\bmm[\\/-]?(dd[\\/-]?)?yyyy\\b/.test(hint);
|
|
2269
|
+
return { match: inContainer || looksDate };
|
|
2270
|
+
})()`;
|
|
2271
|
+
try {
|
|
2272
|
+
const gate = await target.evaluate(gateExpr);
|
|
2273
|
+
if (!gate || typeof gate !== "object" || gate.match !== true) {
|
|
2274
|
+
return null;
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
catch {
|
|
2278
|
+
return null;
|
|
2279
|
+
}
|
|
2280
|
+
// Strategy 1 — keyboard entry (typeable datepickers). Mirrors the Angular
|
|
2281
|
+
// reactive-form re-type at verifyDomEffect: real CDP keystrokes flow through
|
|
2282
|
+
// the widget's own onChange, unlike a programmatic value write.
|
|
2283
|
+
const typed = (0, date_fns_1.format)(new Date(parts.year, parts.monthIndex, parts.day), "MM/dd/yyyy");
|
|
2284
|
+
try {
|
|
2285
|
+
const locator = target.locator(selector).first();
|
|
2286
|
+
await locator.fill("");
|
|
2287
|
+
await locator.type(typed, { delay: 50 });
|
|
2288
|
+
// react-datepicker commits typed input on Enter/blur. The Stagehand Locator
|
|
2289
|
+
// has no `press`, so dispatch the Enter keydown/keyup from page context
|
|
2290
|
+
// (the widget's own keydown handler listens here) then blur via the shared
|
|
2291
|
+
// jQuery-change helper.
|
|
2292
|
+
await dispatchEnterKey(target, xpath);
|
|
2293
|
+
await dispatchJqueryChangeEvent(target, selector);
|
|
2294
|
+
const readback = await verifyFillReadback(target, xpath, value);
|
|
2295
|
+
if (isCommittedDateReadback(readback)) {
|
|
2296
|
+
return { filled: true, postValue: readback.postValue, strategy: "keyboard" };
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
catch {
|
|
2300
|
+
// fall through to open-and-pick
|
|
2301
|
+
}
|
|
2302
|
+
// Strategy 2 — open-and-pick. Required for picker-only widgets (the reported
|
|
2303
|
+
// month/year picker ignores typed input entirely).
|
|
2304
|
+
try {
|
|
2305
|
+
const locator = target.locator(selector).first();
|
|
2306
|
+
await locator.click();
|
|
2307
|
+
}
|
|
2308
|
+
catch {
|
|
2309
|
+
return { filled: false, postValue: "", strategy: "none" };
|
|
2310
|
+
}
|
|
2311
|
+
const pickExpr = `(() => {
|
|
2312
|
+
const year = ${parts.year};
|
|
2313
|
+
const monthIndex = ${parts.monthIndex};
|
|
2314
|
+
const day = ${parts.day};
|
|
2315
|
+
const cal = document.querySelector(".react-datepicker");
|
|
2316
|
+
if (!cal) return { picked: false, reason: "no-calendar" };
|
|
2317
|
+
const fireChange = (el) => { el.dispatchEvent(new Event("input", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); };
|
|
2318
|
+
const monthCells = cal.querySelectorAll(".react-datepicker__month-text");
|
|
2319
|
+
const dayGrid = cal.querySelectorAll(".react-datepicker__day:not(.react-datepicker__day--outside-month):not(.react-datepicker__day--disabled)");
|
|
2320
|
+
// Month/year-picker variant: click the target month cell (0-based month
|
|
2321
|
+
// index maps to the __month-N class) — THIS is the commit. The year/month
|
|
2322
|
+
// dropdowns are set first only so the calendar shows the right year's month
|
|
2323
|
+
// page. NOTE: "range-select" is a SITE-SPECIFIC class (the ATS site's own
|
|
2324
|
+
// CSS), not a react-datepicker class; the bare "select" fallback covers
|
|
2325
|
+
// other wrappers.
|
|
2326
|
+
// Stock react-datepicker renders its month/year dropdowns as clickable divs,
|
|
2327
|
+
// so on those the select loop is inert and the month-cell click carries it.
|
|
2328
|
+
if (monthCells.length > 0) {
|
|
2329
|
+
const selects = cal.querySelectorAll("select.range-select, select");
|
|
2330
|
+
for (const sel of selects) {
|
|
2331
|
+
const opts = Array.from(sel.options).map((o) => (o.value || "").trim());
|
|
2332
|
+
const yearOpt = opts.indexOf(String(year));
|
|
2333
|
+
if (yearOpt >= 0) { sel.selectedIndex = yearOpt; fireChange(sel); continue; }
|
|
2334
|
+
const monthOpt = opts.indexOf(String(monthIndex));
|
|
2335
|
+
if (monthOpt >= 0) { sel.selectedIndex = monthOpt; fireChange(sel); }
|
|
2336
|
+
}
|
|
2337
|
+
const monthCell = cal.querySelector(".react-datepicker__month-" + monthIndex + ".react-datepicker__month-text");
|
|
2338
|
+
const cell = monthCell || cal.querySelectorAll(".react-datepicker__month-text")[monthIndex];
|
|
2339
|
+
if (cell) { cell.click(); return { picked: true, variant: "month-year" }; }
|
|
2340
|
+
return { picked: false, reason: "no-month-cell" };
|
|
2341
|
+
}
|
|
2342
|
+
// Day-grid variant: click the day cell matching the parsed day number.
|
|
2343
|
+
if (dayGrid.length > 0) {
|
|
2344
|
+
for (const cell of dayGrid) {
|
|
2345
|
+
if ((cell.textContent || "").trim() === String(day)) { cell.click(); return { picked: true, variant: "day-grid" }; }
|
|
2346
|
+
}
|
|
2347
|
+
return { picked: false, reason: "no-day-cell" };
|
|
2348
|
+
}
|
|
2349
|
+
return { picked: false, reason: "empty-calendar" };
|
|
2350
|
+
})()`;
|
|
2351
|
+
try {
|
|
2352
|
+
const picked = await target.evaluate(pickExpr);
|
|
2353
|
+
const didPick = !!picked && typeof picked === "object" && picked.picked === true;
|
|
2354
|
+
const readback = await verifyFillReadback(target, xpath, value);
|
|
2355
|
+
if (didPick && isCommittedDateReadback(readback)) {
|
|
2356
|
+
return { filled: true, postValue: readback.postValue, strategy: "calendar-pick" };
|
|
2357
|
+
}
|
|
2358
|
+
return { filled: false, postValue: readback?.postValue ?? "", strategy: "none" };
|
|
2359
|
+
}
|
|
2360
|
+
catch {
|
|
2361
|
+
return { filled: false, postValue: "", strategy: "none" };
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2153
2364
|
/**
|
|
2154
2365
|
* Read back an element's value after a fill action and compare to the
|
|
2155
2366
|
* expected value. Catches silent-value-rejection cases that the verifier's
|
|
@@ -2217,7 +2428,7 @@ function harvestFieldErrors(body) {
|
|
|
2217
2428
|
return [];
|
|
2218
2429
|
const out = [];
|
|
2219
2430
|
const rec = body;
|
|
2220
|
-
// Singular `{error: "message"}` shape used by JSON-envelope ATSs
|
|
2431
|
+
// Singular `{error: "message"}` shape used by JSON-envelope ATSs
|
|
2221
2432
|
// and any REST API following the {error:string} terse-error convention.
|
|
2222
2433
|
// Verified on a JSON-envelope ATS tenant: /integrated_apply 422 body is
|
|
2223
2434
|
// exactly {"error":"Resume is blank"} — no `errors`, no `message`. Before
|
|
@@ -2586,7 +2797,7 @@ async function surfaceAndUpload(params) {
|
|
|
2586
2797
|
}
|
|
2587
2798
|
}
|
|
2588
2799
|
// The file-chooser CDP interception below must run on the upload target's
|
|
2589
|
-
// OWN session — an OOPIF (e.g.
|
|
2800
|
+
// OWN session — an OOPIF (e.g. a cross-origin OOPIF apply wizard) has its own CDP
|
|
2590
2801
|
// target, and a chooser it opens is only observable via that frame's
|
|
2591
2802
|
// session, not the main session. Main-frame targets fall back to
|
|
2592
2803
|
// page.getSessionForFrame(page.mainFrameId()), matching today's behavior.
|
|
@@ -5721,20 +5932,42 @@ async function executeStepWithHealing(params) {
|
|
|
5721
5932
|
}
|
|
5722
5933
|
}
|
|
5723
5934
|
else {
|
|
5724
|
-
//
|
|
5725
|
-
//
|
|
5726
|
-
//
|
|
5727
|
-
//
|
|
5728
|
-
//
|
|
5729
|
-
//
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5935
|
+
// Not an HTML5 date input. First try the text-datepicker
|
|
5936
|
+
// primitive (react-datepicker — ATS start/end-date
|
|
5937
|
+
// controls): those are `type="text"`, so fillHtml5DateTimeInput
|
|
5938
|
+
// returns null, and the generic fill's value-write never commits
|
|
5939
|
+
// the widget. Returns null for any non-datepicker text input, so
|
|
5940
|
+
// every other field falls through to the readback verifier below
|
|
5941
|
+
// unchanged.
|
|
5942
|
+
const datepickerFill = await fillTextDatepickerInput(dateFillTarget, overriddenTarget.selector, fillValue);
|
|
5943
|
+
if (datepickerFill !== null) {
|
|
5944
|
+
record.errorMessage = datepickerFill.filled
|
|
5945
|
+
? `text-datepicker-fill: filled via ${datepickerFill.strategy} "${datepickerFill.postValue}"`
|
|
5946
|
+
: `text-datepicker-fill: failed to commit "${fillValue.slice(0, 60)}" (post=${datepickerFill.postValue})`;
|
|
5947
|
+
if (datepickerFill.filled) {
|
|
5948
|
+
record.actResultSuccess = true;
|
|
5949
|
+
resolvedAction = overriddenTarget;
|
|
5950
|
+
}
|
|
5951
|
+
else {
|
|
5734
5952
|
record.actResultSuccess = false;
|
|
5735
5953
|
}
|
|
5736
|
-
|
|
5737
|
-
|
|
5954
|
+
}
|
|
5955
|
+
else {
|
|
5956
|
+
// Fix I: not a date input — verify the regular fill landed.
|
|
5957
|
+
// Catches silent-value-rejection cases (HTML5 type validation
|
|
5958
|
+
// on number/email/url/tel inputs, framework-controlled-
|
|
5959
|
+
// component rejection, masked-input library reformatting).
|
|
5960
|
+
// Generic primitive that the verifier's existing signals
|
|
5961
|
+
// (network/url/dom/htmlDelta/textChanged) miss.
|
|
5962
|
+
const readback = await verifyFillReadback(dateFillTarget, overriddenTarget.selector, fillValue);
|
|
5963
|
+
if (readback !== null) {
|
|
5964
|
+
if (readback.outcome === "rejected") {
|
|
5965
|
+
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)`;
|
|
5966
|
+
record.actResultSuccess = false;
|
|
5967
|
+
}
|
|
5968
|
+
else if (readback.outcome === "differs") {
|
|
5969
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} fill-value-differs: tried "${fillValue.slice(0, 60)}" got "${readback.postValue.slice(0, 60)}" (framework reformatted)`);
|
|
5970
|
+
}
|
|
5738
5971
|
}
|
|
5739
5972
|
}
|
|
5740
5973
|
}
|
|
@@ -6052,10 +6285,10 @@ async function executeStepWithHealing(params) {
|
|
|
6052
6285
|
}
|
|
6053
6286
|
// Client-side view-swap gate: credit a click that produces substantial
|
|
6054
6287
|
// DOM growth (≥5KB) with zero network when it's NOT a submit/final step
|
|
6055
|
-
// and NOT an advance-pattern step. Fixes the
|
|
6288
|
+
// and NOT an advance-pattern step. Fixes the top-window site "Manual Application"
|
|
6056
6289
|
// case where a +49KB DOM-only view swap was scored as "no observable effect".
|
|
6057
6290
|
// Below that threshold, also credits a smaller text-changing reveal (≥500B) —
|
|
6058
|
-
// fixes the
|
|
6291
|
+
// fixes the top-window site Work-History gate-message reveal (+789B) that used to
|
|
6059
6292
|
// cascade to a 5-attempt failure and global replan.
|
|
6060
6293
|
const clickViewSwapVerified = isClickViewSwapVerified({
|
|
6061
6294
|
resolvedAction,
|
|
@@ -6707,6 +6940,9 @@ async function runHealingFlow(deps) {
|
|
|
6707
6940
|
}
|
|
6708
6941
|
},
|
|
6709
6942
|
});
|
|
6943
|
+
if (shouldWarnMissingAdvancePattern(steps.map((s) => s.instruction), deps.advanceTransitionBodyPattern ?? null)) {
|
|
6944
|
+
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");
|
|
6945
|
+
}
|
|
6710
6946
|
try {
|
|
6711
6947
|
for (const [i, s] of steps.entries()) {
|
|
6712
6948
|
if (maxFlowMs !== undefined && Date.now() - start > maxFlowMs) {
|