humanish 0.75.0 → 0.76.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/README.md +28 -11
- package/dist/chrome-cdp-probe.d.ts +32 -2
- package/dist/chrome-cdp-probe.js +165 -58
- package/dist/chrome-cdp-probe.js.map +1 -1
- package/dist/cua-actor-lab.d.ts +13 -0
- package/dist/cua-actor-lab.js +128 -6
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/feedback.d.ts +20 -5
- package/dist/feedback.js +47 -16
- package/dist/feedback.js.map +1 -1
- package/dist/lab-config.d.ts +21 -0
- package/dist/lab-config.js +30 -0
- package/dist/lab-config.js.map +1 -1
- package/dist/program.js +26 -8
- package/dist/program.js.map +1 -1
- package/dist/run.d.ts +26 -0
- package/dist/run.js.map +1 -1
- package/dist/telemetry.d.ts +10 -0
- package/dist/telemetry.js +12 -0
- package/dist/telemetry.js.map +1 -1
- package/docs/contracts/schemas.md +4 -2
- package/docs/goals/current.md +13 -1
- package/docs/ramp/README.md +1 -1
- package/package.json +1 -1
package/dist/cua-actor-lab.js
CHANGED
|
@@ -736,10 +736,13 @@ async function fillDesktopBrowserWindow(desktop, windowId, resolution, requestTi
|
|
|
736
736
|
})
|
|
737
737
|
.catch(() => undefined);
|
|
738
738
|
}
|
|
739
|
-
async function openDesktopBrowserTarget(desktop, targetUrl, requestTimeoutMs, browserPreference
|
|
739
|
+
async function openDesktopBrowserTarget(desktop, targetUrl, requestTimeoutMs, browserPreference,
|
|
740
|
+
/** Launch-time flags that make mobile fidelity (#221) hold across every tab: the user agent and
|
|
741
|
+
* touch events are browser-wide here, where the CDP holder covers only the launch page. */
|
|
742
|
+
extraChromiumFlags = []) {
|
|
740
743
|
const requestedBrowser = browserPreference ?? "default";
|
|
741
744
|
if (isHttpUrl(targetUrl)) {
|
|
742
|
-
const chromiumFlags = CHROMIUM_EVIDENCE_HYGIENE_FLAGS.map(shellSingleQuote).join(" ");
|
|
745
|
+
const chromiumFlags = [...CHROMIUM_EVIDENCE_HYGIENE_FLAGS, ...extraChromiumFlags].map(shellSingleQuote).join(" ");
|
|
743
746
|
const browserLaunchCommand = [
|
|
744
747
|
"set -euo pipefail",
|
|
745
748
|
`target_url=${shellSingleQuote(targetUrl)}`,
|
|
@@ -937,6 +940,79 @@ export function makeChromeDesktopGeometryObserver(desktop, requestTimeoutMs, end
|
|
|
937
940
|
};
|
|
938
941
|
};
|
|
939
942
|
}
|
|
943
|
+
/** The user agent a mobile-emulated lane presents unless the lab sets its own. */
|
|
944
|
+
export const DEFAULT_MOBILE_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1";
|
|
945
|
+
/**
|
|
946
|
+
* Apply mobile emulation (#221) to the lane's launch page and read back what the page reports.
|
|
947
|
+
* Fails CLOSED: a request that cannot be applied throws, because a desktop run labelled mobile is
|
|
948
|
+
* the over-trust this feature exists to prevent. A read-back that cannot be taken is a warning
|
|
949
|
+
* (the emulation was applied; only the proof is missing).
|
|
950
|
+
*/
|
|
951
|
+
export async function applyMobileEmulation(desktop, requestTimeoutMs, endpoint, targetId, request) {
|
|
952
|
+
const command = (mode) => chromeCdpProbeCommand({ ...endpoint, ...(targetId === undefined ? {} : { targetId }), prefer: "pinned", mode, emulation: request });
|
|
953
|
+
const read = async () => {
|
|
954
|
+
const result = await desktop.commands.run(command("fidelity"), { requestTimeoutMs, timeoutMs: 15_000 });
|
|
955
|
+
if (result.exitCode !== undefined && result.exitCode !== 0) {
|
|
956
|
+
return { unavailable: `probe exited ${result.exitCode}: ${tailOf(result.stderr ?? result.stdout ?? "")}` };
|
|
957
|
+
}
|
|
958
|
+
return parseChromeCdpProbeOutput(result.stdout);
|
|
959
|
+
};
|
|
960
|
+
// The UA / touch / DPR overrides are bound to the DevTools session that set them and lapse the
|
|
961
|
+
// moment its socket closes (measured: only the viewport width survived a one-shot apply). So the
|
|
962
|
+
// applier stays attached for the lane's whole life as a detached process; the sandbox teardown
|
|
963
|
+
// ends it. Its first stdout line says what was applied.
|
|
964
|
+
const holderName = `mobile-emulation-${Date.now().toString(36)}`;
|
|
965
|
+
await startDetachedProcess(desktop, { name: holderName, command: command("hold"), requestTimeoutMs });
|
|
966
|
+
let announced;
|
|
967
|
+
for (let attempt = 0; attempt < 30 && announced === undefined; attempt += 1) {
|
|
968
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
969
|
+
const log = await readDetachedLog(desktop, holderName, requestTimeoutMs).catch(() => "");
|
|
970
|
+
const line = log.split("\n").find((candidate) => candidate.trim().startsWith("{"));
|
|
971
|
+
if (line !== undefined)
|
|
972
|
+
announced = parseChromeCdpProbeOutput(line);
|
|
973
|
+
}
|
|
974
|
+
if (announced === undefined) {
|
|
975
|
+
throw new Error("mobile emulation could not be applied: the in-sandbox applier printed nothing within 15 s");
|
|
976
|
+
}
|
|
977
|
+
if (announced.unavailable !== undefined) {
|
|
978
|
+
throw new Error(`mobile emulation could not be applied (${announced.unavailable}); applied before failing: ${(announced.applied ?? []).join(", ") || "nothing"}`);
|
|
979
|
+
}
|
|
980
|
+
const applied = announced;
|
|
981
|
+
const warnings = [];
|
|
982
|
+
// The reload inside the applier takes a moment; the read-back is retried until the page reports
|
|
983
|
+
// the requested viewport and user agent, so a slow page does not read as "no proof".
|
|
984
|
+
let readBack = await read();
|
|
985
|
+
for (let attempt = 0; attempt < 20 && (readBack.fidelity === undefined || readBack.fidelity.innerWidth !== request.width || !readBack.fidelity.userAgent.includes(request.userAgent.slice(0, 24))); attempt += 1) {
|
|
986
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
987
|
+
readBack = await read();
|
|
988
|
+
}
|
|
989
|
+
const fidelityRead = readBack;
|
|
990
|
+
const requested = {
|
|
991
|
+
width: request.width,
|
|
992
|
+
height: request.height,
|
|
993
|
+
deviceScaleFactor: request.deviceScaleFactor,
|
|
994
|
+
touch: request.touch,
|
|
995
|
+
userAgent: request.userAgent
|
|
996
|
+
};
|
|
997
|
+
if (fidelityRead.fidelity === undefined) {
|
|
998
|
+
warnings.push(`Mobile emulation was applied but the page's own report could not be read (${fidelityRead.unavailable ?? "no fidelity read"}); desktopGeometry.fidelity carries the request without a resolved block.`);
|
|
999
|
+
return { fidelity: { tier: "mobile-emulated", requested, applied: applied.applied ?? [] }, warnings };
|
|
1000
|
+
}
|
|
1001
|
+
const resolved = { ...fidelityRead.fidelity, source: "cdp" };
|
|
1002
|
+
if (resolved.innerWidth !== request.width) {
|
|
1003
|
+
warnings.push(`Mobile emulation requested a ${request.width} px viewport; the page reports ${resolved.innerWidth} px.`);
|
|
1004
|
+
}
|
|
1005
|
+
if (resolved.devicePixelRatio !== request.deviceScaleFactor) {
|
|
1006
|
+
warnings.push(`Mobile emulation requested devicePixelRatio ${request.deviceScaleFactor}; the page reports ${resolved.devicePixelRatio}.`);
|
|
1007
|
+
}
|
|
1008
|
+
if (request.touch && resolved.maxTouchPoints === 0) {
|
|
1009
|
+
warnings.push("Mobile emulation requested touch; the page reports navigator.maxTouchPoints 0.");
|
|
1010
|
+
}
|
|
1011
|
+
if (!resolved.userAgent.includes("Mobile") && !resolved.userAgent.includes("Android") && !resolved.userAgent.includes("iPhone")) {
|
|
1012
|
+
warnings.push("Mobile emulation requested a mobile user agent; the page reports a desktop one.");
|
|
1013
|
+
}
|
|
1014
|
+
return { fidelity: { tier: "mobile-emulated", requested, applied: applied.applied ?? [], resolved }, warnings };
|
|
1015
|
+
}
|
|
940
1016
|
function isMeasuredRect(value) {
|
|
941
1017
|
if (!value || typeof value !== "object")
|
|
942
1018
|
return false;
|
|
@@ -1018,11 +1094,14 @@ export async function captureDesktopBrowserGeometry(args) {
|
|
|
1018
1094
|
: undefined;
|
|
1019
1095
|
const browserWindow = chromeGeometry?.browserWindow ?? xdotoolWindow;
|
|
1020
1096
|
const viewport = chromeGeometry?.viewport;
|
|
1097
|
+
// The fill check reads the X window when it was measured: under mobile emulation (#221) the
|
|
1098
|
+
// page's window.outerWidth reports the EMULATED screen (414), which is not a fill failure.
|
|
1099
|
+
const fillBounds = xdotoolWindow ?? browserWindow;
|
|
1021
1100
|
if (!browserWindow) {
|
|
1022
1101
|
warnings.push(`Browser outer bounds could not be measured for lane ${args.laneId}.`);
|
|
1023
1102
|
}
|
|
1024
|
-
else if (
|
|
1025
|
-
warnings.push(`Browser window fill did not reach the requested ${args.requestedScreen[0]}x${args.requestedScreen[1]} screen for lane ${args.laneId}; measured outer bounds are ${
|
|
1103
|
+
else if (fillBounds !== undefined && (fillBounds.width !== args.requestedScreen[0] || fillBounds.height !== args.requestedScreen[1])) {
|
|
1104
|
+
warnings.push(`Browser window fill did not reach the requested ${args.requestedScreen[0]}x${args.requestedScreen[1]} screen for lane ${args.laneId}; measured outer bounds are ${fillBounds.width}x${fillBounds.height}.`);
|
|
1026
1105
|
}
|
|
1027
1106
|
if (!viewport) {
|
|
1028
1107
|
// Name the cause, not only the symptom: the same dead DevTools channel that loses the viewport
|
|
@@ -1165,8 +1244,17 @@ function hasBlockerLanguage(text) {
|
|
|
1165
1244
|
// study completed without a participant-reported finding". Friction is the INCLUSIVE scan; a
|
|
1166
1245
|
// false positive here adds a candidate a person then reads, which is the cheap direction.
|
|
1167
1246
|
const REPORTED_DEFECT_LANGUAGE = /\b(defects?|bugs?|accessibilit(y|ies)|inaccessible|not (keyboard|screen.?reader)[- ]?accessible|confus(ed|ing)|hesitat(ed|ion)|unexpected(ly)?|unclear|hard to (find|tell|see|read|reach)|no (visible )?focus|overlap(ped|ping|s)?|truncat(ed|es|ion)|cut off|did nothing|nothing happened|no effect)\b/;
|
|
1247
|
+
// The friction scan's own negations (#614). "Nothing was confusing", "no defects", "not unclear"
|
|
1248
|
+
// are what a participant writes when it has NOTHING to report, and until 2026-09-03 each of them
|
|
1249
|
+
// counted as reported friction and became a feedback candidate whose "actual" was a sentence
|
|
1250
|
+
// reporting no problem. Only the report-shaped adjectives are negatable here: "no visible focus",
|
|
1251
|
+
// "not keyboard-accessible" and "did nothing" are defects and stay.
|
|
1252
|
+
const NEGATED_REPORT_LANGUAGE = /\b(?:nothing|no|not|never|without|none)\s+(?:was\s+|were\s+|felt\s+|seemed\s+|really\s+|particularly\s+|especially\s+|major\s+|real\s+|obvious\s+|noticeable\s+)*(?:confus(?:ed|ing|ion)|unclear|unexpected(?:ly)?|hesitat(?:ed|ion|ions)|surpris(?:ed|ing|es)|defects?|bugs?|overlap(?:ped|ping|s)?|truncat(?:ed|es|ion)|hard to (?:find|tell|see|read|reach))\b/g;
|
|
1253
|
+
function stripNegatedReportLanguage(text) {
|
|
1254
|
+
return text.replace(NEGATED_REPORT_LANGUAGE, " ");
|
|
1255
|
+
}
|
|
1168
1256
|
function completionReasonContradictsGoal(reason) {
|
|
1169
|
-
const text = stripQuotedSpans(stripNegatedNonBlockerPhrases(reason.toLowerCase()));
|
|
1257
|
+
const text = stripQuotedSpans(stripNegatedReportLanguage(stripNegatedNonBlockerPhrases(reason.toLowerCase())));
|
|
1170
1258
|
return hasBlockerLanguage(text) || REPORTED_DEFECT_LANGUAGE.test(text);
|
|
1171
1259
|
}
|
|
1172
1260
|
/** The verdict scan (strict): like the friction scan, but resolved-arc segments are stripped
|
|
@@ -1371,6 +1459,7 @@ export async function runCuaLane(spec, deps) {
|
|
|
1371
1459
|
let browserLaunchIdentity;
|
|
1372
1460
|
let browserLaunched = false;
|
|
1373
1461
|
let initialBrowserGeometry;
|
|
1462
|
+
let appliedFidelity;
|
|
1374
1463
|
let browserWindowId;
|
|
1375
1464
|
let browserTargetId;
|
|
1376
1465
|
const declaredScreen = declaredScreenForRender(spec.devicePreset, spec.deviceName, spec.resolution);
|
|
@@ -1562,12 +1651,44 @@ export async function runCuaLane(spec, deps) {
|
|
|
1562
1651
|
});
|
|
1563
1652
|
}
|
|
1564
1653
|
if (!desktopCliRoute) {
|
|
1565
|
-
const
|
|
1654
|
+
const requestedFidelity = config.execution?.desktop?.fidelity;
|
|
1655
|
+
const browserLaunch = await openDesktopBrowserTarget(desktop, targetUrl, deps.requestTimeoutMs, config.execution?.desktop?.browser, requestedFidelity?.mobileEmulation && spec.devicePreset.isMobile
|
|
1656
|
+
? [
|
|
1657
|
+
`--user-agent=${requestedFidelity.userAgent ?? DEFAULT_MOBILE_USER_AGENT}`,
|
|
1658
|
+
...(requestedFidelity.touch === false ? [] : ["--touch-events=enabled"])
|
|
1659
|
+
]
|
|
1660
|
+
: []);
|
|
1566
1661
|
desktopBrowser = browserLaunch.evidence;
|
|
1567
1662
|
launchedBrowserFamily = browserLaunch.family;
|
|
1568
1663
|
browserLaunchIdentity = browserLaunch.identity;
|
|
1569
1664
|
browserLaunched = true;
|
|
1570
1665
|
await desktop.wait(BROWSER_SETTLE_MS).catch(() => undefined);
|
|
1666
|
+
// Mobile fidelity beyond viewport size (#221): applied to the launch page before the
|
|
1667
|
+
// geometry capture and the participant's first observation, OUTSIDE the stream/geometry
|
|
1668
|
+
// try below (whose catch degrades to a warning): a request that cannot be applied fails
|
|
1669
|
+
// the lane closed with the reason.
|
|
1670
|
+
// Only lanes on a mobile preset are emulated: a run-wide flag must not hand a desktop or
|
|
1671
|
+
// tablet lane an iPhone user agent (the first live proof did exactly that to the desktop
|
|
1672
|
+
// newcomer beside the phone lane). Those lanes carry no fidelity block, which is honest.
|
|
1673
|
+
const fidelityRequest = config.execution?.desktop?.fidelity;
|
|
1674
|
+
if (fidelityRequest?.mobileEmulation && spec.devicePreset.isMobile) {
|
|
1675
|
+
if (launchedBrowserFamily !== "chromium") {
|
|
1676
|
+
throw new Error(`execution.desktop.fidelity.mobileEmulation needs Chrome or Chromium on lane ${spec.laneId}; the launched browser family is ${launchedBrowserFamily}. Set execution.desktop.browser: chrome.`);
|
|
1677
|
+
}
|
|
1678
|
+
const applied = await applyMobileEmulation(desktop, deps.requestTimeoutMs, {
|
|
1679
|
+
...(browserLaunchIdentity?.cdpPort === undefined ? {} : { cdpPort: browserLaunchIdentity.cdpPort }),
|
|
1680
|
+
...(browserLaunchIdentity?.profileDir === undefined ? {} : { profileDir: browserLaunchIdentity.profileDir }),
|
|
1681
|
+
targetUrl
|
|
1682
|
+
}, browserTargetId, {
|
|
1683
|
+
width: spec.devicePreset.width,
|
|
1684
|
+
height: spec.devicePreset.height,
|
|
1685
|
+
deviceScaleFactor: fidelityRequest.deviceScaleFactor ?? spec.devicePreset.deviceScaleFactor,
|
|
1686
|
+
touch: fidelityRequest.touch ?? true,
|
|
1687
|
+
userAgent: fidelityRequest.userAgent ?? DEFAULT_MOBILE_USER_AGENT
|
|
1688
|
+
});
|
|
1689
|
+
appliedFidelity = applied.fidelity;
|
|
1690
|
+
warnings.push(...applied.warnings);
|
|
1691
|
+
}
|
|
1571
1692
|
}
|
|
1572
1693
|
else {
|
|
1573
1694
|
// A terminal window, opened the way the browser is opened on every other route: the
|
|
@@ -1792,6 +1913,7 @@ export async function runCuaLane(spec, deps) {
|
|
|
1792
1913
|
screen: desktopGeometry.screen,
|
|
1793
1914
|
...(chosenGeometry.browserWindow === undefined ? {} : { browserWindow: chosenGeometry.browserWindow }),
|
|
1794
1915
|
...(chosenGeometry.viewport === undefined ? {} : { viewport: chosenGeometry.viewport }),
|
|
1916
|
+
...(appliedFidelity === undefined ? {} : { fidelity: appliedFidelity }),
|
|
1795
1917
|
...((desktopGeometry.warnings?.length ?? 0) + geometryWarnings.length === 0
|
|
1796
1918
|
? {}
|
|
1797
1919
|
: { warnings: [...(desktopGeometry.warnings ?? []), ...geometryWarnings] })
|