@decocms/parity 0.1.1 → 0.3.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/CHANGELOG.md +30 -0
- package/dist/cli.js +548 -187
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [0.3.0](https://github.com/decocms/parity/compare/v0.2.0...v0.3.0) (2026-05-26)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
* **journey:** retry `go-checkout` via LLM recovery when the default selector clicks the wrong element and the URL never reaches `/checkout` ([5826d30](https://github.com/decocms/parity/commit/5826d309c6a910f8cf8017667cdcdcebd65f65d9))
|
|
14
|
+
* **journey:** LLM recovery on `cep-pdp` + `cep-cart` when defaults miss the CEP input ([624b0f5](https://github.com/decocms/parity/commit/624b0f5acbe1c50fce2bea69faa6906e29d36e64))
|
|
15
|
+
* **journey:** per-flow hard deadline so a single hung flow can't freeze the whole crawl ([e2d4a68](https://github.com/decocms/parity/commit/e2d4a681a5d4f0a9627251eceb1e733d12bdc68d))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
* **journey:** abort in-flight Playwright ops when the deadline fires, instead of letting them mutate the next flow's shared BrowserContext ([a21aa78](https://github.com/decocms/parity/commit/a21aa78a4431e7a2cff9aac26ac34c4ca2fa768c))
|
|
21
|
+
* **journey:** seal the timeout FlowCapture synchronously so Promise.race can't pick up the inner rejection caused by closing pages ([d2a0f1e](https://github.com/decocms/parity/commit/d2a0f1e7a08bf95a28a951bace322c5baf062bea))
|
|
22
|
+
* **journey:** await timeout cleanup before runFlow returns so the next flow on the same context isn't racing in-flight close()s ([a2fd6c2](https://github.com/decocms/parity/commit/a2fd6c2e7e7a9eb493d2cb75d08f3d5cc7cc3f91))
|
|
23
|
+
|
|
24
|
+
## [0.2.0](https://github.com/decocms/parity/compare/v0.1.1...v0.2.0) (2026-05-26)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
* **css-trace:** inspect CSS rules affecting a DOM element ([1e323fa](https://github.com/decocms/parity/commit/1e323fa17564045d5f55bfbc5cbf2fcdb0112877))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
* **capture:** hard outer deadline so capturePage cannot exceed budget+10s ([966b9a5](https://github.com/decocms/parity/commit/966b9a59851536262b88510da6175a215c90d0b2))
|
|
35
|
+
* **capture:** hard outer deadline so capturePage cannot exceed budget+10s ([ea61116](https://github.com/decocms/parity/commit/ea61116c81fb1ec86e86ddb24216214380a81c81))
|
|
36
|
+
* **lint:** replace 3 template literals without interpolation with strings ([c60f19c](https://github.com/decocms/parity/commit/c60f19c4a56c8441ead829fca0998ee36a671872))
|
|
37
|
+
|
|
8
38
|
## [Unreleased]
|
|
9
39
|
|
|
10
40
|
## [0.1.1] — 2026-05-22
|
package/dist/cli.js
CHANGED
|
@@ -1118,63 +1118,7 @@ async function capturePage(page, opts) {
|
|
|
1118
1118
|
const overallBudgetMs = opts.fast ? 25000 : 60000;
|
|
1119
1119
|
const deadline = start + overallBudgetMs;
|
|
1120
1120
|
const remaining = () => Math.max(500, deadline - Date.now());
|
|
1121
|
-
|
|
1122
|
-
let finalUrl = opts.url;
|
|
1123
|
-
let xRobotsTag = null;
|
|
1124
|
-
try {
|
|
1125
|
-
response = await page.goto(opts.url, {
|
|
1126
|
-
waitUntil: "domcontentloaded",
|
|
1127
|
-
timeout: Math.min(opts.timeoutMs ?? 30000, remaining())
|
|
1128
|
-
});
|
|
1129
|
-
finalUrl = page.url();
|
|
1130
|
-
if (response) {
|
|
1131
|
-
const headers = response.headers();
|
|
1132
|
-
xRobotsTag = headers["x-robots-tag"] ?? null;
|
|
1133
|
-
}
|
|
1134
|
-
if (opts.fast) {
|
|
1135
|
-
await page.waitForLoadState("networkidle", { timeout: Math.min(4000, remaining()) }).catch(() => {
|
|
1136
|
-
return;
|
|
1137
|
-
});
|
|
1138
|
-
await page.waitForTimeout(Math.min(opts.settleMs ?? 1200, remaining()));
|
|
1139
|
-
} else {
|
|
1140
|
-
await page.waitForLoadState("load", { timeout: Math.min(12000, remaining()) }).catch(() => {
|
|
1141
|
-
return;
|
|
1142
|
-
});
|
|
1143
|
-
await page.waitForLoadState("networkidle", { timeout: Math.min(6000, remaining()) }).catch(() => {
|
|
1144
|
-
return;
|
|
1145
|
-
});
|
|
1146
|
-
await page.waitForTimeout(Math.min(opts.settleMs ?? 2000, remaining()));
|
|
1147
|
-
if (opts.scrollToLoad !== false && remaining() > 3000) {
|
|
1148
|
-
await Promise.race([
|
|
1149
|
-
scrollFullPage(page).catch(() => {
|
|
1150
|
-
return;
|
|
1151
|
-
}),
|
|
1152
|
-
new Promise((resolve) => setTimeout(resolve, Math.min(1e4, remaining())))
|
|
1153
|
-
]);
|
|
1154
|
-
await page.waitForTimeout(Math.min(600, remaining()));
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
} catch (err) {
|
|
1158
|
-
state.console.push({
|
|
1159
|
-
type: "error",
|
|
1160
|
-
text: `[navigation-error] ${err.message}`
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1163
|
-
const vitals = await page.evaluate(() => window.__parity_vitals).catch(() => null);
|
|
1164
|
-
if (!opts.skipScreenshot) {
|
|
1165
|
-
await Promise.race([
|
|
1166
|
-
page.screenshot({ path: opts.screenshotPath, fullPage: true, animations: "disabled" }).catch(() => {
|
|
1167
|
-
return;
|
|
1168
|
-
}),
|
|
1169
|
-
new Promise((resolve) => setTimeout(resolve, Math.min(15000, remaining())))
|
|
1170
|
-
]);
|
|
1171
|
-
}
|
|
1172
|
-
const html = await Promise.race([
|
|
1173
|
-
page.content().catch(() => ""),
|
|
1174
|
-
new Promise((resolve) => setTimeout(() => resolve(""), Math.min(5000, remaining())))
|
|
1175
|
-
]);
|
|
1176
|
-
await flushCollectors(state, Math.min(3000, remaining()));
|
|
1177
|
-
return {
|
|
1121
|
+
const buildPartial = () => ({
|
|
1178
1122
|
url: opts.url,
|
|
1179
1123
|
finalUrl,
|
|
1180
1124
|
status: response?.status() ?? 0,
|
|
@@ -1189,7 +1133,83 @@ async function capturePage(page, opts) {
|
|
|
1189
1133
|
harPath: opts.harPath,
|
|
1190
1134
|
tracePath: opts.tracePath,
|
|
1191
1135
|
xRobotsTag
|
|
1136
|
+
});
|
|
1137
|
+
let response = null;
|
|
1138
|
+
let finalUrl = opts.url;
|
|
1139
|
+
let xRobotsTag = null;
|
|
1140
|
+
let vitals = null;
|
|
1141
|
+
let html = "";
|
|
1142
|
+
const inner = async () => {
|
|
1143
|
+
try {
|
|
1144
|
+
response = await page.goto(opts.url, {
|
|
1145
|
+
waitUntil: "domcontentloaded",
|
|
1146
|
+
timeout: Math.min(opts.timeoutMs ?? 30000, remaining())
|
|
1147
|
+
});
|
|
1148
|
+
finalUrl = page.url();
|
|
1149
|
+
if (response) {
|
|
1150
|
+
const headers = response.headers();
|
|
1151
|
+
xRobotsTag = headers["x-robots-tag"] ?? null;
|
|
1152
|
+
}
|
|
1153
|
+
if (opts.fast) {
|
|
1154
|
+
await page.waitForLoadState("networkidle", { timeout: Math.min(4000, remaining()) }).catch(() => {
|
|
1155
|
+
return;
|
|
1156
|
+
});
|
|
1157
|
+
await page.waitForTimeout(Math.min(opts.settleMs ?? 1200, remaining()));
|
|
1158
|
+
} else {
|
|
1159
|
+
await page.waitForLoadState("load", { timeout: Math.min(12000, remaining()) }).catch(() => {
|
|
1160
|
+
return;
|
|
1161
|
+
});
|
|
1162
|
+
await page.waitForLoadState("networkidle", { timeout: Math.min(6000, remaining()) }).catch(() => {
|
|
1163
|
+
return;
|
|
1164
|
+
});
|
|
1165
|
+
await page.waitForTimeout(Math.min(opts.settleMs ?? 2000, remaining()));
|
|
1166
|
+
if (opts.scrollToLoad !== false && remaining() > 3000) {
|
|
1167
|
+
await Promise.race([
|
|
1168
|
+
scrollFullPage(page).catch(() => {
|
|
1169
|
+
return;
|
|
1170
|
+
}),
|
|
1171
|
+
new Promise((resolve) => setTimeout(resolve, Math.min(1e4, remaining())))
|
|
1172
|
+
]);
|
|
1173
|
+
await page.waitForTimeout(Math.min(600, remaining()));
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
} catch (err) {
|
|
1177
|
+
state.console.push({
|
|
1178
|
+
type: "error",
|
|
1179
|
+
text: `[navigation-error] ${err.message}`
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
vitals = await Promise.race([
|
|
1183
|
+
page.evaluate(() => window.__parity_vitals).catch(() => null),
|
|
1184
|
+
new Promise((resolve) => setTimeout(() => resolve(null), Math.min(5000, remaining())))
|
|
1185
|
+
]) ?? null;
|
|
1186
|
+
if (!opts.skipScreenshot) {
|
|
1187
|
+
await Promise.race([
|
|
1188
|
+
page.screenshot({ path: opts.screenshotPath, fullPage: true, animations: "disabled" }).catch(() => {
|
|
1189
|
+
return;
|
|
1190
|
+
}),
|
|
1191
|
+
new Promise((resolve) => setTimeout(resolve, Math.min(15000, remaining())))
|
|
1192
|
+
]);
|
|
1193
|
+
}
|
|
1194
|
+
html = await Promise.race([
|
|
1195
|
+
page.content().catch(() => ""),
|
|
1196
|
+
new Promise((resolve) => setTimeout(() => resolve(""), Math.min(5000, remaining())))
|
|
1197
|
+
]);
|
|
1198
|
+
await flushCollectors(state, Math.min(3000, remaining()));
|
|
1199
|
+
return buildPartial();
|
|
1192
1200
|
};
|
|
1201
|
+
const SAFETY_MARGIN_MS = 1e4;
|
|
1202
|
+
const outerDeadlineMs = overallBudgetMs + SAFETY_MARGIN_MS;
|
|
1203
|
+
return Promise.race([
|
|
1204
|
+
inner(),
|
|
1205
|
+
new Promise((resolve) => setTimeout(() => {
|
|
1206
|
+
state.console.push({
|
|
1207
|
+
type: "error",
|
|
1208
|
+
text: `[capture-timeout] capturePage exceeded ${outerDeadlineMs}ms outer deadline — returning partial capture`
|
|
1209
|
+
});
|
|
1210
|
+
resolve(buildPartial());
|
|
1211
|
+
}, outerDeadlineMs))
|
|
1212
|
+
]);
|
|
1193
1213
|
}
|
|
1194
1214
|
|
|
1195
1215
|
// src/report/render.ts
|
|
@@ -4084,8 +4104,257 @@ Regressões (severity piorou):`));
|
|
|
4084
4104
|
}
|
|
4085
4105
|
}
|
|
4086
4106
|
|
|
4087
|
-
// src/commands/
|
|
4107
|
+
// src/commands/css-trace.ts
|
|
4088
4108
|
import chalk4 from "chalk";
|
|
4109
|
+
async function tracePage(page, url, selector, settleMs) {
|
|
4110
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
4111
|
+
await page.waitForLoadState("load", { timeout: 12000 }).catch(() => {
|
|
4112
|
+
return;
|
|
4113
|
+
});
|
|
4114
|
+
await page.waitForLoadState("networkidle", { timeout: 6000 }).catch(() => {
|
|
4115
|
+
return;
|
|
4116
|
+
});
|
|
4117
|
+
await page.waitForTimeout(settleMs);
|
|
4118
|
+
const cdp = await page.context().newCDPSession(page);
|
|
4119
|
+
await cdp.send("DOM.enable");
|
|
4120
|
+
await cdp.send("CSS.enable");
|
|
4121
|
+
const { root } = await cdp.send("DOM.getDocument", { depth: -1 });
|
|
4122
|
+
const found = await cdp.send("DOM.querySelector", {
|
|
4123
|
+
nodeId: root.nodeId,
|
|
4124
|
+
selector
|
|
4125
|
+
});
|
|
4126
|
+
if (!found.nodeId) {
|
|
4127
|
+
await cdp.detach();
|
|
4128
|
+
return { url, selector, found: false, computed: {}, rules: [] };
|
|
4129
|
+
}
|
|
4130
|
+
const computedResult = await cdp.send("CSS.getComputedStyleForNode", {
|
|
4131
|
+
nodeId: found.nodeId
|
|
4132
|
+
});
|
|
4133
|
+
const computed = {};
|
|
4134
|
+
for (const e of computedResult.computedStyle)
|
|
4135
|
+
computed[e.name] = e.value;
|
|
4136
|
+
const matched = await cdp.send("CSS.getMatchedStylesForNode", {
|
|
4137
|
+
nodeId: found.nodeId
|
|
4138
|
+
});
|
|
4139
|
+
const sheetUrlCache = new Map;
|
|
4140
|
+
async function sheetSource(styleSheetId, origin) {
|
|
4141
|
+
if (!styleSheetId) {
|
|
4142
|
+
if (origin === "user-agent")
|
|
4143
|
+
return "user-agent";
|
|
4144
|
+
return "inline";
|
|
4145
|
+
}
|
|
4146
|
+
if (sheetUrlCache.has(styleSheetId))
|
|
4147
|
+
return sheetUrlCache.get(styleSheetId);
|
|
4148
|
+
try {
|
|
4149
|
+
const header = await cdp.send("CSS.getStyleSheetText", {
|
|
4150
|
+
styleSheetId
|
|
4151
|
+
});
|
|
4152
|
+
const preview = header.text.replace(/\s+/g, " ").trim().slice(0, 100);
|
|
4153
|
+
const label = `stylesheet#${styleSheetId} (${preview})`;
|
|
4154
|
+
sheetUrlCache.set(styleSheetId, label);
|
|
4155
|
+
return label;
|
|
4156
|
+
} catch {
|
|
4157
|
+
const label = `stylesheet#${styleSheetId}`;
|
|
4158
|
+
sheetUrlCache.set(styleSheetId, label);
|
|
4159
|
+
return label;
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
const buildRule = async (m, inheritedFromDistance) => {
|
|
4163
|
+
const r = m.rule;
|
|
4164
|
+
const selectors = r.selectorList.selectors.map((s) => s.text);
|
|
4165
|
+
const matchedSelectors = m.matchingSelectors.map((i) => selectors[i]).filter(Boolean);
|
|
4166
|
+
const sel = matchedSelectors.join(", ") || selectors.join(", ") || "(no selector text)";
|
|
4167
|
+
const source = await sheetSource(r.styleSheetId, r.origin);
|
|
4168
|
+
return {
|
|
4169
|
+
source,
|
|
4170
|
+
selector: sel,
|
|
4171
|
+
properties: (r.style.cssProperties ?? []).filter((p) => p.name && p.value && !p.disabled).map((p) => {
|
|
4172
|
+
const trimmed = p.value.replace(/\s*!important\s*$/i, "").trim();
|
|
4173
|
+
return {
|
|
4174
|
+
name: p.name,
|
|
4175
|
+
value: trimmed || p.value,
|
|
4176
|
+
important: p.important === true || /\s*!important\s*$/i.test(p.value),
|
|
4177
|
+
disabled: p.disabled
|
|
4178
|
+
};
|
|
4179
|
+
}),
|
|
4180
|
+
...inheritedFromDistance !== undefined ? { inheritedFromDistance } : {}
|
|
4181
|
+
};
|
|
4182
|
+
};
|
|
4183
|
+
const rules = [];
|
|
4184
|
+
for (const m of matched.matchedCSSRules ?? []) {
|
|
4185
|
+
rules.push(await buildRule(m));
|
|
4186
|
+
}
|
|
4187
|
+
const inheritedGroups = matched.inherited ?? [];
|
|
4188
|
+
for (let i = 0;i < inheritedGroups.length; i++) {
|
|
4189
|
+
const group = inheritedGroups[i];
|
|
4190
|
+
if (!group?.matchedCSSRules?.length)
|
|
4191
|
+
continue;
|
|
4192
|
+
const distance = i + 1;
|
|
4193
|
+
for (const m of group.matchedCSSRules) {
|
|
4194
|
+
rules.push(await buildRule(m, distance));
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
await cdp.detach();
|
|
4198
|
+
return { url, selector, found: true, computed, rules };
|
|
4199
|
+
}
|
|
4200
|
+
function applyFilter(result, filter) {
|
|
4201
|
+
if (!filter)
|
|
4202
|
+
return result;
|
|
4203
|
+
const wanted = new Set(filter.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean));
|
|
4204
|
+
const computed = {};
|
|
4205
|
+
for (const [k, v] of Object.entries(result.computed)) {
|
|
4206
|
+
if (wanted.has(k.toLowerCase()))
|
|
4207
|
+
computed[k] = v;
|
|
4208
|
+
}
|
|
4209
|
+
const rules = result.rules.map((r) => ({
|
|
4210
|
+
...r,
|
|
4211
|
+
properties: r.properties.filter((p) => wanted.has(p.name.toLowerCase()))
|
|
4212
|
+
})).filter((r) => r.properties.length > 0);
|
|
4213
|
+
return { ...result, computed, rules };
|
|
4214
|
+
}
|
|
4215
|
+
function printResult(result, header) {
|
|
4216
|
+
console.log(chalk4.bold(`
|
|
4217
|
+
${header}`));
|
|
4218
|
+
console.log(chalk4.dim(`URL: ${result.url}`));
|
|
4219
|
+
console.log(chalk4.dim(`Selector: ${result.selector}`));
|
|
4220
|
+
if (!result.found) {
|
|
4221
|
+
console.log(chalk4.red("✖ Element not found"));
|
|
4222
|
+
return;
|
|
4223
|
+
}
|
|
4224
|
+
const computedKeys = Object.keys(result.computed);
|
|
4225
|
+
if (computedKeys.length > 0) {
|
|
4226
|
+
console.log(chalk4.bold(`
|
|
4227
|
+
Computed:`));
|
|
4228
|
+
for (const k of computedKeys) {
|
|
4229
|
+
console.log(` ${chalk4.cyan(k)}: ${result.computed[k]}`);
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
if (result.rules.length === 0) {
|
|
4233
|
+
console.log(chalk4.yellow(`
|
|
4234
|
+
No matching rules after filter.`));
|
|
4235
|
+
return;
|
|
4236
|
+
}
|
|
4237
|
+
console.log(chalk4.bold(`
|
|
4238
|
+
Rules (ordered by CDP — most specific last):`));
|
|
4239
|
+
for (const r of result.rules) {
|
|
4240
|
+
const inheritLabel = r.inheritedFromDistance !== undefined ? chalk4.yellow(` ↑ inherited from ancestor (${r.inheritedFromDistance})`) : "";
|
|
4241
|
+
console.log(` ${chalk4.magenta(r.source)}${inheritLabel}`);
|
|
4242
|
+
console.log(` ${chalk4.green(r.selector)} {`);
|
|
4243
|
+
for (const p of r.properties) {
|
|
4244
|
+
const bang = p.important ? chalk4.red(" !important") : "";
|
|
4245
|
+
console.log(` ${p.name}: ${p.value}${bang};`);
|
|
4246
|
+
}
|
|
4247
|
+
console.log(" }");
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
function diffComputed(prod, cand) {
|
|
4251
|
+
const keys = new Set([...Object.keys(prod.computed), ...Object.keys(cand.computed)]);
|
|
4252
|
+
const out = [];
|
|
4253
|
+
for (const k of keys) {
|
|
4254
|
+
const p = prod.computed[k];
|
|
4255
|
+
const c = cand.computed[k];
|
|
4256
|
+
if (p !== c)
|
|
4257
|
+
out.push({ property: k, prod: p, cand: c });
|
|
4258
|
+
}
|
|
4259
|
+
return out.sort((a, b) => a.property.localeCompare(b.property));
|
|
4260
|
+
}
|
|
4261
|
+
function printComparison(prod, cand) {
|
|
4262
|
+
printResult(prod, "── PROD ──────────────────────────────────────────────");
|
|
4263
|
+
printResult(cand, "── CAND ──────────────────────────────────────────────");
|
|
4264
|
+
const diffs = diffComputed(prod, cand);
|
|
4265
|
+
console.log(chalk4.bold(`
|
|
4266
|
+
── DIFF (computed) ───────────────────────────────────`));
|
|
4267
|
+
if (diffs.length === 0) {
|
|
4268
|
+
console.log(chalk4.green(" No computed-style differences in the filtered set."));
|
|
4269
|
+
return;
|
|
4270
|
+
}
|
|
4271
|
+
for (const d of diffs) {
|
|
4272
|
+
console.log(` ${chalk4.cyan(d.property)}`);
|
|
4273
|
+
console.log(` prod: ${chalk4.green(d.prod ?? "(unset)")}`);
|
|
4274
|
+
console.log(` cand: ${chalk4.red(d.cand ?? "(unset)")}`);
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
async function cssTraceCommand(opts) {
|
|
4278
|
+
const viewport = opts.viewport ?? "desktop";
|
|
4279
|
+
const settleMs = opts.settleMs ?? 1500;
|
|
4280
|
+
if (!opts.selector) {
|
|
4281
|
+
console.error(chalk4.red("--selector is required"));
|
|
4282
|
+
return 1;
|
|
4283
|
+
}
|
|
4284
|
+
const hasUrl = !!opts.url;
|
|
4285
|
+
const hasProd = !!opts.prod;
|
|
4286
|
+
const hasCand = !!opts.cand;
|
|
4287
|
+
if (hasUrl && (hasProd || hasCand)) {
|
|
4288
|
+
console.error(chalk4.red("--url is mutually exclusive with --prod / --cand. Pass one mode only."));
|
|
4289
|
+
return 1;
|
|
4290
|
+
}
|
|
4291
|
+
if (hasProd && !hasCand || !hasProd && hasCand) {
|
|
4292
|
+
console.error(chalk4.red("Comparison mode needs both --prod and --cand (got only one)."));
|
|
4293
|
+
return 1;
|
|
4294
|
+
}
|
|
4295
|
+
if (!hasUrl && !hasProd && !hasCand) {
|
|
4296
|
+
console.error(chalk4.red("Provide either --url or both --prod and --cand."));
|
|
4297
|
+
return 1;
|
|
4298
|
+
}
|
|
4299
|
+
const isCompare = hasProd && hasCand;
|
|
4300
|
+
let browser = null;
|
|
4301
|
+
try {
|
|
4302
|
+
browser = await launchBrowser({ headless: true });
|
|
4303
|
+
if (isCompare) {
|
|
4304
|
+
const prodCtx = await newContext(browser, { viewport });
|
|
4305
|
+
const candCtx = await newContext(browser, { viewport });
|
|
4306
|
+
const prodPage = await prodCtx.newPage();
|
|
4307
|
+
const candPage = await candCtx.newPage();
|
|
4308
|
+
try {
|
|
4309
|
+
const [prodRes, candRes] = await Promise.all([
|
|
4310
|
+
tracePage(prodPage, opts.prod, opts.selector, settleMs),
|
|
4311
|
+
tracePage(candPage, opts.cand, opts.selector, settleMs)
|
|
4312
|
+
]);
|
|
4313
|
+
const prodF = applyFilter(prodRes, opts.filter);
|
|
4314
|
+
const candF = applyFilter(candRes, opts.filter);
|
|
4315
|
+
if (opts.json) {
|
|
4316
|
+
console.log(JSON.stringify({ prod: prodF, cand: candF, diff: diffComputed(prodF, candF) }, null, 2));
|
|
4317
|
+
} else {
|
|
4318
|
+
printComparison(prodF, candF);
|
|
4319
|
+
}
|
|
4320
|
+
} finally {
|
|
4321
|
+
await prodCtx.close().catch(() => {
|
|
4322
|
+
return;
|
|
4323
|
+
});
|
|
4324
|
+
await candCtx.close().catch(() => {
|
|
4325
|
+
return;
|
|
4326
|
+
});
|
|
4327
|
+
}
|
|
4328
|
+
} else {
|
|
4329
|
+
const ctx = await newContext(browser, { viewport });
|
|
4330
|
+
const page = await ctx.newPage();
|
|
4331
|
+
try {
|
|
4332
|
+
const res = await tracePage(page, opts.url, opts.selector, settleMs);
|
|
4333
|
+
const filtered = applyFilter(res, opts.filter);
|
|
4334
|
+
if (opts.json) {
|
|
4335
|
+
console.log(JSON.stringify(filtered, null, 2));
|
|
4336
|
+
} else {
|
|
4337
|
+
printResult(filtered, "── RESULT ────────────────────────────────────────────");
|
|
4338
|
+
}
|
|
4339
|
+
} finally {
|
|
4340
|
+
await ctx.close().catch(() => {
|
|
4341
|
+
return;
|
|
4342
|
+
});
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
return 0;
|
|
4346
|
+
} catch (err) {
|
|
4347
|
+
console.error(chalk4.red(`Error: ${err.message}`));
|
|
4348
|
+
return 1;
|
|
4349
|
+
} finally {
|
|
4350
|
+
await browser?.close().catch(() => {
|
|
4351
|
+
return;
|
|
4352
|
+
});
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
|
|
4356
|
+
// src/commands/explain.ts
|
|
4357
|
+
import chalk5 from "chalk";
|
|
4089
4358
|
|
|
4090
4359
|
// src/llm/client.ts
|
|
4091
4360
|
import Anthropic from "@anthropic-ai/sdk";
|
|
@@ -4394,17 +4663,17 @@ Cada issue tem:
|
|
|
4394
4663
|
// src/commands/explain.ts
|
|
4395
4664
|
async function explainCommand(runId, issueId, output) {
|
|
4396
4665
|
if (!isLlmAvailable()) {
|
|
4397
|
-
console.error(
|
|
4666
|
+
console.error(chalk5.red("Nenhuma API key de LLM encontrada — set ANTHROPIC_API_KEY ou OPENROUTER_API_KEY."));
|
|
4398
4667
|
return 1;
|
|
4399
4668
|
}
|
|
4400
4669
|
const run = loadRun(output, runId);
|
|
4401
4670
|
const issue = run.issues.find((i) => i.id === issueId) || run.topIssues.find((i) => i.id === issueId);
|
|
4402
4671
|
if (!issue) {
|
|
4403
|
-
console.error(
|
|
4672
|
+
console.error(chalk5.red(`Issue não encontrada: ${issueId}`));
|
|
4404
4673
|
return 1;
|
|
4405
4674
|
}
|
|
4406
4675
|
const checks = run.checks.filter((c) => c.issues.some((i) => i.id === issueId || i.check === issue.check));
|
|
4407
|
-
console.log(
|
|
4676
|
+
console.log(chalk5.dim(`Consultando LLM…
|
|
4408
4677
|
`));
|
|
4409
4678
|
const text = await callMessage({
|
|
4410
4679
|
systemPrompt: ISSUE_AGGREGATOR_SYSTEM_PROMPT,
|
|
@@ -4425,7 +4694,7 @@ ${JSON.stringify(checks.map((c) => ({ name: c.name, status: c.status, summary: c
|
|
|
4425
4694
|
// src/commands/journey.ts
|
|
4426
4695
|
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
4427
4696
|
import { join as join7 } from "node:path";
|
|
4428
|
-
import
|
|
4697
|
+
import chalk6 from "chalk";
|
|
4429
4698
|
import ora2 from "ora";
|
|
4430
4699
|
|
|
4431
4700
|
// src/llm/pick-plp.ts
|
|
@@ -4790,19 +5059,60 @@ var PURCHASE_JOURNEY_TOTAL_STEPS = 8;
|
|
|
4790
5059
|
function selFor(ctx, key) {
|
|
4791
5060
|
return selectorsFor(key, { rc: ctx.rc, learned: ctx.learned, platform: ctx.platform });
|
|
4792
5061
|
}
|
|
5062
|
+
var FLOW_DEADLINE_MS = {
|
|
5063
|
+
homepage: 90000,
|
|
5064
|
+
plp: 180000,
|
|
5065
|
+
pdp: 240000,
|
|
5066
|
+
"purchase-journey": 360000
|
|
5067
|
+
};
|
|
4793
5068
|
async function runFlow(flow, ctx) {
|
|
4794
5069
|
const start = Date.now();
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
5070
|
+
const deadlineMs = FLOW_DEADLINE_MS[flow];
|
|
5071
|
+
const inner = async () => {
|
|
5072
|
+
switch (flow) {
|
|
5073
|
+
case "homepage":
|
|
5074
|
+
return finalize(flow, ctx, await flowHomepage(ctx), [], start);
|
|
5075
|
+
case "plp":
|
|
5076
|
+
return finalize(flow, ctx, await flowPlp(ctx), [], start);
|
|
5077
|
+
case "pdp":
|
|
5078
|
+
return finalize(flow, ctx, await flowPdp(ctx), [], start);
|
|
5079
|
+
case "purchase-journey": {
|
|
5080
|
+
const { pages, steps } = await flowPurchaseJourney(ctx);
|
|
5081
|
+
return finalize(flow, ctx, pages, steps, start);
|
|
5082
|
+
}
|
|
4805
5083
|
}
|
|
5084
|
+
};
|
|
5085
|
+
const innerPromise = inner();
|
|
5086
|
+
innerPromise.catch(() => {
|
|
5087
|
+
return;
|
|
5088
|
+
});
|
|
5089
|
+
let timer;
|
|
5090
|
+
let cleanup = Promise.resolve();
|
|
5091
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
5092
|
+
timer = setTimeout(() => {
|
|
5093
|
+
const pages = ctx.ctx.pages();
|
|
5094
|
+
resolve(finalize(flow, ctx, [], [
|
|
5095
|
+
{
|
|
5096
|
+
step: 0,
|
|
5097
|
+
name: "visit-home",
|
|
5098
|
+
side: ctx.side,
|
|
5099
|
+
viewport: ctx.viewport,
|
|
5100
|
+
status: "failed",
|
|
5101
|
+
durationMs: deadlineMs,
|
|
5102
|
+
screenshotPath: "",
|
|
5103
|
+
actionDescription: `[flow-timeout] flow "${flow}" excedeu ${deadlineMs}ms — captura abortada pela safety net externa, ${pages.length} page(s) fechada(s) para liberar o contexto. Step interno provavelmente travou em uma operação Playwright que não respeitou seu timeout declarado.`
|
|
5104
|
+
}
|
|
5105
|
+
], start));
|
|
5106
|
+
cleanup = Promise.allSettled(pages.map((p) => p.close()));
|
|
5107
|
+
}, deadlineMs);
|
|
5108
|
+
});
|
|
5109
|
+
try {
|
|
5110
|
+
const result = await Promise.race([innerPromise, timeoutPromise]);
|
|
5111
|
+
await cleanup;
|
|
5112
|
+
return result;
|
|
5113
|
+
} finally {
|
|
5114
|
+
if (timer !== undefined)
|
|
5115
|
+
clearTimeout(timer);
|
|
4806
5116
|
}
|
|
4807
5117
|
}
|
|
4808
5118
|
function finalize(flow, ctx, pages, steps, start) {
|
|
@@ -4995,7 +5305,16 @@ async function flowPurchaseJourney(ctx) {
|
|
|
4995
5305
|
steps[steps.length - 1].actionDescription = `Abriu PDP \`${pdpHit.url}\` (via \`${pdpHit.selector}\`)`;
|
|
4996
5306
|
steps[steps.length - 1].beforeUrl = plpHit.url;
|
|
4997
5307
|
reportStart(4, "shipping-calc-pdp");
|
|
4998
|
-
|
|
5308
|
+
let cepInputPdp = await firstVisible(page, selFor(ctx, "cepInputPdp"));
|
|
5309
|
+
let cepPdpRecovered = false;
|
|
5310
|
+
if (!cepInputPdp && recoveryBudget > 0) {
|
|
5311
|
+
const recovery = await attemptRecovery(page, ctx, "shipping-calc-pdp", "Achar o input de CEP / código postal nesta PDP (deve ser um input visível com label/placeholder relacionado a frete, entrega ou CEP)", selFor(ctx, "cepInputPdp"));
|
|
5312
|
+
if (recovery) {
|
|
5313
|
+
cepInputPdp = recovery.selector;
|
|
5314
|
+
cepPdpRecovered = true;
|
|
5315
|
+
recoveryBudget--;
|
|
5316
|
+
}
|
|
5317
|
+
}
|
|
4999
5318
|
if (cepInputPdp) {
|
|
5000
5319
|
const t4 = Date.now();
|
|
5001
5320
|
const beforeUrl4 = page.url();
|
|
@@ -5020,14 +5339,15 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5020
5339
|
screenshotPath: sp,
|
|
5021
5340
|
screenshotBeforePath: spBefore4,
|
|
5022
5341
|
beforeUrl: beforeUrl4,
|
|
5023
|
-
actionDescription: `Preencheu CEP '${ctx.rc.cep}' no input \`${cepInputPdp}\` e disparou cálculo de frete`,
|
|
5342
|
+
actionDescription: `Preencheu CEP '${ctx.rc.cep}' no input \`${cepInputPdp}\` e disparou cálculo de frete${cepPdpRecovered ? " (selector via LLM recovery)" : ""}`,
|
|
5024
5343
|
detail: { cepUsed: ctx.rc.cep },
|
|
5025
5344
|
selectorKey: "cepInputPdp",
|
|
5026
|
-
usedSelector: cepInputPdp
|
|
5345
|
+
usedSelector: cepInputPdp,
|
|
5346
|
+
recoveredByLlm: cepPdpRecovered || undefined
|
|
5027
5347
|
});
|
|
5028
5348
|
reportEnd(4, "shipping-calc-pdp", step4Status, Date.now() - t4);
|
|
5029
5349
|
} else {
|
|
5030
|
-
steps.push(makeSkipStep(4, "shipping-calc-pdp", ctx, "no CEP input on PDP"));
|
|
5350
|
+
steps.push(makeSkipStep(4, "shipping-calc-pdp", ctx, "no CEP input on PDP (recovery exhausted)"));
|
|
5031
5351
|
reportEnd(4, "shipping-calc-pdp", "skipped", 0, "no CEP input on PDP");
|
|
5032
5352
|
}
|
|
5033
5353
|
reportStart(5, "add-to-cart");
|
|
@@ -5125,7 +5445,16 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5125
5445
|
});
|
|
5126
5446
|
reportEnd(6, "open-minicart", "ok", Date.now() - t6);
|
|
5127
5447
|
reportStart(7, "shipping-calc-cart");
|
|
5128
|
-
|
|
5448
|
+
let cepInputCart = await firstVisible(page, selFor(ctx, "cepInputCart"));
|
|
5449
|
+
let cepCartRecovered = false;
|
|
5450
|
+
if (!cepInputCart && recoveryBudget > 0) {
|
|
5451
|
+
const recovery = await attemptRecovery(page, ctx, "shipping-calc-cart", "Achar o input de CEP / código postal dentro do carrinho ou minicart aberto agora (input visível com label/placeholder de frete, entrega ou CEP)", selFor(ctx, "cepInputCart"));
|
|
5452
|
+
if (recovery) {
|
|
5453
|
+
cepInputCart = recovery.selector;
|
|
5454
|
+
cepCartRecovered = true;
|
|
5455
|
+
recoveryBudget--;
|
|
5456
|
+
}
|
|
5457
|
+
}
|
|
5129
5458
|
if (cepInputCart) {
|
|
5130
5459
|
const t7 = Date.now();
|
|
5131
5460
|
const beforeUrl7 = page.url();
|
|
@@ -5150,14 +5479,15 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5150
5479
|
screenshotPath: sp7,
|
|
5151
5480
|
screenshotBeforePath: spBefore7,
|
|
5152
5481
|
beforeUrl: beforeUrl7,
|
|
5153
|
-
actionDescription: `Preencheu CEP '${ctx.rc.cep}' no carrinho (\`${cepInputCart}\`)`,
|
|
5482
|
+
actionDescription: `Preencheu CEP '${ctx.rc.cep}' no carrinho (\`${cepInputCart}\`)${cepCartRecovered ? " (selector via LLM recovery)" : ""}`,
|
|
5154
5483
|
detail: { cepUsed: ctx.rc.cep },
|
|
5155
5484
|
selectorKey: "cepInputCart",
|
|
5156
|
-
usedSelector: cepInputCart
|
|
5485
|
+
usedSelector: cepInputCart,
|
|
5486
|
+
recoveredByLlm: cepCartRecovered || undefined
|
|
5157
5487
|
});
|
|
5158
5488
|
reportEnd(7, "shipping-calc-cart", step7Status, Date.now() - t7);
|
|
5159
5489
|
} else {
|
|
5160
|
-
steps.push(makeSkipStep(7, "shipping-calc-cart", ctx, "no CEP input in cart"));
|
|
5490
|
+
steps.push(makeSkipStep(7, "shipping-calc-cart", ctx, "no CEP input in cart (recovery exhausted)"));
|
|
5161
5491
|
reportEnd(7, "shipping-calc-cart", "skipped", 0, "no CEP input in cart");
|
|
5162
5492
|
}
|
|
5163
5493
|
reportStart(8, "go-checkout");
|
|
@@ -5176,28 +5506,47 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5176
5506
|
reportEnd(8, "go-checkout", "skipped", 0, "no checkout button found");
|
|
5177
5507
|
return { pages, steps };
|
|
5178
5508
|
}
|
|
5179
|
-
const
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
await page.screenshot({ path: spBefore8, fullPage: false }).catch(() => {
|
|
5183
|
-
return;
|
|
5184
|
-
});
|
|
5185
|
-
const checkoutText = await checkoutHit.locator.innerText().catch(() => "");
|
|
5186
|
-
await Promise.all([
|
|
5187
|
-
page.waitForURL(/checkout/i, { timeout: 1e4 }).catch(() => {
|
|
5509
|
+
const tryCheckoutClick = async (hit, attempt2) => {
|
|
5510
|
+
const spBefore = screenshotPath(ctx, `pj-8-checkout-before-${attempt2}`);
|
|
5511
|
+
await page.screenshot({ path: spBefore, fullPage: false }).catch(() => {
|
|
5188
5512
|
return;
|
|
5189
|
-
})
|
|
5190
|
-
|
|
5513
|
+
});
|
|
5514
|
+
const clickedText2 = await hit.locator.innerText().catch(() => "");
|
|
5515
|
+
await Promise.all([
|
|
5516
|
+
page.waitForURL(/checkout/i, { timeout: 1e4 }).catch(() => {
|
|
5517
|
+
return;
|
|
5518
|
+
}),
|
|
5519
|
+
hit.locator.click({ timeout: 5000 }).catch(() => {
|
|
5520
|
+
return;
|
|
5521
|
+
})
|
|
5522
|
+
]);
|
|
5523
|
+
await page.waitForTimeout(1500);
|
|
5524
|
+
const spAfter = screenshotPath(ctx, `pj-8-checkout-reached-${attempt2}`);
|
|
5525
|
+
await page.screenshot({ path: spAfter, fullPage: false }).catch(() => {
|
|
5191
5526
|
return;
|
|
5192
|
-
})
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
const
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5527
|
+
});
|
|
5528
|
+
return { url: page.url(), spBefore, spAfter, clickedText: clickedText2 };
|
|
5529
|
+
};
|
|
5530
|
+
const t8 = Date.now();
|
|
5531
|
+
const beforeUrl8 = page.url();
|
|
5532
|
+
let attempt = 1;
|
|
5533
|
+
let result = await tryCheckoutClick(checkoutHit, attempt);
|
|
5534
|
+
let reachedCheckout = /\/checkout/i.test(result.url);
|
|
5535
|
+
let usedSelector = checkoutHit.selector;
|
|
5536
|
+
let clickedText = result.clickedText;
|
|
5537
|
+
if (!reachedCheckout && recoveryBudget > 0 && !/\/checkout/i.test(beforeUrl8)) {
|
|
5538
|
+
const retrySuggestion = await attemptRecovery(page, ctx, "go-checkout-retry", `Cliquei em '${clickedText.slice(0, 40).trim()}' (selector \`${usedSelector}\`), mas a URL ficou em ${result.url} e não foi pra /checkout. Achar o botão que de fato navega pra /checkout neste cart/minicart aberto.`, [usedSelector, ...selFor(ctx, "checkoutButton")]);
|
|
5539
|
+
if (retrySuggestion) {
|
|
5540
|
+
recoveryBudget--;
|
|
5541
|
+
checkoutRecovered = true;
|
|
5542
|
+
attempt++;
|
|
5543
|
+
const retryResult = await tryCheckoutClick(retrySuggestion, attempt);
|
|
5544
|
+
result = retryResult;
|
|
5545
|
+
usedSelector = retrySuggestion.selector;
|
|
5546
|
+
clickedText = retryResult.clickedText;
|
|
5547
|
+
reachedCheckout = /\/checkout/i.test(retryResult.url);
|
|
5548
|
+
}
|
|
5549
|
+
}
|
|
5201
5550
|
const step8Status = reachedCheckout ? "ok" : "failed";
|
|
5202
5551
|
steps.push({
|
|
5203
5552
|
step: 8,
|
|
@@ -5206,13 +5555,13 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5206
5555
|
viewport: ctx.viewport,
|
|
5207
5556
|
status: step8Status,
|
|
5208
5557
|
durationMs: Date.now() - t8,
|
|
5209
|
-
url:
|
|
5210
|
-
screenshotPath:
|
|
5211
|
-
screenshotBeforePath:
|
|
5558
|
+
url: result.url,
|
|
5559
|
+
screenshotPath: result.spAfter,
|
|
5560
|
+
screenshotBeforePath: result.spBefore,
|
|
5212
5561
|
beforeUrl: beforeUrl8,
|
|
5213
|
-
actionDescription: `Clicou em${
|
|
5562
|
+
actionDescription: `Clicou em${clickedText ? ` '${clickedText.slice(0, 30).trim()}'` : ""} (\`${usedSelector}\`); URL final: ${result.url}${reachedCheckout ? " ✓ atingiu /checkout" : " ✗ não foi pra checkout"}${attempt > 1 ? ` (após ${attempt} tentativas com recovery LLM)` : ""}`,
|
|
5214
5563
|
selectorKey: "checkoutButton",
|
|
5215
|
-
usedSelector
|
|
5564
|
+
usedSelector,
|
|
5216
5565
|
recoveredByLlm: checkoutRecovered || undefined
|
|
5217
5566
|
});
|
|
5218
5567
|
reportEnd(8, "go-checkout", step8Status, Date.now() - t8);
|
|
@@ -5655,7 +6004,7 @@ var CRITICAL_STEPS = new Set([
|
|
|
5655
6004
|
async function journeyCommand(opts) {
|
|
5656
6005
|
const viewports = opts.viewports.split(",").map((s) => s.trim()).filter((s) => s === "mobile" || s === "desktop");
|
|
5657
6006
|
if (viewports.length === 0) {
|
|
5658
|
-
console.error(
|
|
6007
|
+
console.error(chalk6.red("Nenhum viewport válido (use mobile,desktop)"));
|
|
5659
6008
|
return 2;
|
|
5660
6009
|
}
|
|
5661
6010
|
const rc = loadParityRc();
|
|
@@ -5665,13 +6014,13 @@ async function journeyCommand(opts) {
|
|
|
5665
6014
|
const runId = newRunId();
|
|
5666
6015
|
const paths = createRunDir(opts.output, runId);
|
|
5667
6016
|
if (!opts.json) {
|
|
5668
|
-
console.log(
|
|
6017
|
+
console.log(chalk6.bold(`
|
|
5669
6018
|
parity journey ${runId}`));
|
|
5670
|
-
console.log(
|
|
5671
|
-
console.log(
|
|
5672
|
-
console.log(
|
|
6019
|
+
console.log(chalk6.dim(` prod: ${opts.prod}`));
|
|
6020
|
+
console.log(chalk6.dim(` cand: ${opts.cand}`));
|
|
6021
|
+
console.log(chalk6.dim(` viewports: ${viewports.join(", ")} · CEP: ${rc.cep}`));
|
|
5673
6022
|
if (isLlmAvailable())
|
|
5674
|
-
console.log(
|
|
6023
|
+
console.log(chalk6.dim(` llm: ${providerLabel()}`));
|
|
5675
6024
|
console.log("");
|
|
5676
6025
|
}
|
|
5677
6026
|
let platform = "custom";
|
|
@@ -5715,15 +6064,15 @@ async function journeyCommand(opts) {
|
|
|
5715
6064
|
const onStepFor = (viewport, side) => (event) => {
|
|
5716
6065
|
if (opts.json)
|
|
5717
6066
|
return;
|
|
5718
|
-
const sideTag = side === "prod" ?
|
|
5719
|
-
const prefix = ` ${
|
|
6067
|
+
const sideTag = side === "prod" ? chalk6.cyan("prod") : chalk6.magenta("cand");
|
|
6068
|
+
const prefix = ` ${chalk6.dim(`[${viewport}/`)}${sideTag}${chalk6.dim("]")}`;
|
|
5720
6069
|
if (event.phase === "start") {
|
|
5721
|
-
console.log(`${prefix} ${
|
|
6070
|
+
console.log(`${prefix} ${chalk6.dim(`${event.index}/${event.total}`)} ▶ ${chalk6.bold(stepLabel(event.name))}`);
|
|
5722
6071
|
} else {
|
|
5723
|
-
const glyph = event.status === "ok" ?
|
|
5724
|
-
const noteText = event.note ?
|
|
5725
|
-
const time =
|
|
5726
|
-
console.log(`${prefix} ${
|
|
6072
|
+
const glyph = event.status === "ok" ? chalk6.green("✓") : event.status === "skipped" ? chalk6.yellow("○") : chalk6.red("✗");
|
|
6073
|
+
const noteText = event.note ? chalk6.dim(` (${event.note})`) : "";
|
|
6074
|
+
const time = chalk6.dim(`${(event.durationMs / 1000).toFixed(1)}s`);
|
|
6075
|
+
console.log(`${prefix} ${chalk6.dim(`${event.index}/${event.total}`)} ${glyph} ${stepLabel(event.name)} ${time}${noteText}`);
|
|
5727
6076
|
}
|
|
5728
6077
|
};
|
|
5729
6078
|
async function runOneSide(browserInstance, viewport, side) {
|
|
@@ -5761,7 +6110,7 @@ async function journeyCommand(opts) {
|
|
|
5761
6110
|
spinner?.succeed("Browser pronto");
|
|
5762
6111
|
for (const viewport of viewports) {
|
|
5763
6112
|
if (!opts.json)
|
|
5764
|
-
console.log(
|
|
6113
|
+
console.log(chalk6.bold(`
|
|
5765
6114
|
── ${viewport} ─────────────────────────────────────────────`));
|
|
5766
6115
|
const [prodCap, candCap] = await Promise.all([
|
|
5767
6116
|
runOneSide(browser, viewport, "prod"),
|
|
@@ -5788,7 +6137,7 @@ async function journeyCommand(opts) {
|
|
|
5788
6137
|
if (opts.junit) {
|
|
5789
6138
|
writeFileSync6(opts.junit, buildJUnit(rows, failures), "utf8");
|
|
5790
6139
|
if (!opts.json)
|
|
5791
|
-
console.log(
|
|
6140
|
+
console.log(chalk6.dim(` → JUnit XML: ${opts.junit}`));
|
|
5792
6141
|
}
|
|
5793
6142
|
return failures.some((f) => f.critical) ? 1 : 0;
|
|
5794
6143
|
} catch (err) {
|
|
@@ -5853,14 +6202,14 @@ function collectFailures(rows) {
|
|
|
5853
6202
|
function printTable(viewports, rows) {
|
|
5854
6203
|
console.log("");
|
|
5855
6204
|
for (const viewport of viewports) {
|
|
5856
|
-
console.log(
|
|
6205
|
+
console.log(chalk6.bold(` [${viewport}]`));
|
|
5857
6206
|
const vrows = rows.filter((r) => r.viewport === viewport);
|
|
5858
6207
|
for (const r of vrows) {
|
|
5859
6208
|
const label = STEP_LABELS[r.name] ?? r.name;
|
|
5860
6209
|
const p = statusGlyph(r.prod?.status);
|
|
5861
6210
|
const c = statusGlyph(r.cand?.status);
|
|
5862
|
-
const status = r.prod?.status === "ok" && r.cand?.status !== "ok" ?
|
|
5863
|
-
const note = r.cand?.status === "failed" || r.cand?.status === "skipped" ?
|
|
6211
|
+
const status = r.prod?.status === "ok" && r.cand?.status !== "ok" ? chalk6.red("FAILED") : "";
|
|
6212
|
+
const note = r.cand?.status === "failed" || r.cand?.status === "skipped" ? chalk6.dim(`(${r.cand?.note ?? r.cand?.status})`) : "";
|
|
5864
6213
|
console.log(` ${label.padEnd(24)} prod ${p} cand ${c} ${status} ${note}`);
|
|
5865
6214
|
}
|
|
5866
6215
|
console.log("");
|
|
@@ -5868,12 +6217,12 @@ function printTable(viewports, rows) {
|
|
|
5868
6217
|
}
|
|
5869
6218
|
function statusGlyph(status) {
|
|
5870
6219
|
if (!status)
|
|
5871
|
-
return
|
|
6220
|
+
return chalk6.dim("—");
|
|
5872
6221
|
if (status === "ok")
|
|
5873
|
-
return
|
|
6222
|
+
return chalk6.green("✓");
|
|
5874
6223
|
if (status === "skipped")
|
|
5875
|
-
return
|
|
5876
|
-
return
|
|
6224
|
+
return chalk6.yellow("○");
|
|
6225
|
+
return chalk6.red("✗");
|
|
5877
6226
|
}
|
|
5878
6227
|
function printSummary2(rows, failures, htmlPath) {
|
|
5879
6228
|
const byViewport = new Map;
|
|
@@ -5884,25 +6233,25 @@ function printSummary2(rows, failures, htmlPath) {
|
|
|
5884
6233
|
v.passed++;
|
|
5885
6234
|
byViewport.set(r.viewport, v);
|
|
5886
6235
|
}
|
|
5887
|
-
console.log(
|
|
6236
|
+
console.log(chalk6.bold(" Summary:"));
|
|
5888
6237
|
for (const [vp, { passed, total }] of byViewport) {
|
|
5889
|
-
const stat = passed === total ?
|
|
6238
|
+
const stat = passed === total ? chalk6.green("✓") : chalk6.yellow(`${passed}/${total}`);
|
|
5890
6239
|
console.log(` ${vp.padEnd(8)} ${stat} steps em cand`);
|
|
5891
6240
|
}
|
|
5892
6241
|
if (failures.length > 0) {
|
|
5893
6242
|
const crit = failures.filter((f) => f.critical).length;
|
|
5894
6243
|
console.log("");
|
|
5895
|
-
console.log(
|
|
6244
|
+
console.log(chalk6.red(` ✗ ${failures.length} step(s) divergent(es) (${crit} crítico)`));
|
|
5896
6245
|
for (const f of failures) {
|
|
5897
|
-
const tag = f.critical ?
|
|
6246
|
+
const tag = f.critical ? chalk6.red("[critical]") : chalk6.yellow("[warn]");
|
|
5898
6247
|
console.log(` ${tag} [${f.viewport}] ${f.step}: ${f.reason}`);
|
|
5899
6248
|
}
|
|
5900
6249
|
} else {
|
|
5901
6250
|
console.log("");
|
|
5902
|
-
console.log(
|
|
6251
|
+
console.log(chalk6.green(" ✓ jornada completa em cand"));
|
|
5903
6252
|
}
|
|
5904
6253
|
console.log("");
|
|
5905
|
-
console.log(
|
|
6254
|
+
console.log(chalk6.dim(` → ${htmlPath}`));
|
|
5906
6255
|
console.log("");
|
|
5907
6256
|
}
|
|
5908
6257
|
function emitGithubAnnotations(failures) {
|
|
@@ -6130,23 +6479,23 @@ function escapeHtml(s) {
|
|
|
6130
6479
|
}
|
|
6131
6480
|
|
|
6132
6481
|
// src/commands/learned.ts
|
|
6133
|
-
import
|
|
6482
|
+
import chalk7 from "chalk";
|
|
6134
6483
|
function learnedStats() {
|
|
6135
6484
|
const lib = loadLearned();
|
|
6136
6485
|
const stats = statsFromLib(lib);
|
|
6137
6486
|
if (stats.platforms.length === 0) {
|
|
6138
|
-
console.log(
|
|
6139
|
-
console.log(
|
|
6487
|
+
console.log(chalk7.dim(`Biblioteca vazia: ${LEARNED_PATH}`));
|
|
6488
|
+
console.log(chalk7.dim("Rode `parity run ...` em algum site para começar a popular."));
|
|
6140
6489
|
return 0;
|
|
6141
6490
|
}
|
|
6142
|
-
console.log(
|
|
6491
|
+
console.log(chalk7.bold(`
|
|
6143
6492
|
learned-selectors stats (${LEARNED_PATH})
|
|
6144
6493
|
`));
|
|
6145
6494
|
for (const p of stats.platforms) {
|
|
6146
|
-
console.log(`${
|
|
6495
|
+
console.log(`${chalk7.cyan(p.platform)}: ${p.activeSelectors} active · ${chalk7.dim(`${p.deprecatedSelectors} deprecated`)}`);
|
|
6147
6496
|
for (const top of p.topByKey) {
|
|
6148
6497
|
const sr = `${(top.successRate * 100).toFixed(0)}%`;
|
|
6149
|
-
console.log(` ${
|
|
6498
|
+
console.log(` ${chalk7.dim(top.key.padEnd(18))} ${chalk7.green(sr.padStart(4))} ${chalk7.dim(`(${top.hosts} hosts)`)} ${top.selector}`);
|
|
6150
6499
|
}
|
|
6151
6500
|
console.log("");
|
|
6152
6501
|
}
|
|
@@ -6156,7 +6505,7 @@ learned-selectors stats (${LEARNED_PATH})
|
|
|
6156
6505
|
// src/commands/vitals.ts
|
|
6157
6506
|
import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
6158
6507
|
import { join as join8 } from "node:path";
|
|
6159
|
-
import
|
|
6508
|
+
import chalk8 from "chalk";
|
|
6160
6509
|
import ora3 from "ora";
|
|
6161
6510
|
|
|
6162
6511
|
// src/diff/vitals.ts
|
|
@@ -6306,10 +6655,10 @@ async function vitalsCommand(opts) {
|
|
|
6306
6655
|
const runId = newRunId();
|
|
6307
6656
|
const paths = createRunDir(opts.output, runId);
|
|
6308
6657
|
const t0 = Date.now();
|
|
6309
|
-
console.log(
|
|
6658
|
+
console.log(chalk8.bold(`
|
|
6310
6659
|
parity vitals ${runId}`));
|
|
6311
|
-
console.log(
|
|
6312
|
-
console.log(
|
|
6660
|
+
console.log(chalk8.dim(` prod: ${opts.prod}`));
|
|
6661
|
+
console.log(chalk8.dim(` cand: ${opts.cand}`));
|
|
6313
6662
|
const discoverSpinner = ora3("Descobrindo páginas…").start();
|
|
6314
6663
|
const pagePaths = await discoverPagePaths2(opts.prod, opts);
|
|
6315
6664
|
if (pagePaths.length === 0) {
|
|
@@ -6341,7 +6690,7 @@ async function vitalsCommand(opts) {
|
|
|
6341
6690
|
completed++;
|
|
6342
6691
|
const elapsed = ((Date.now() - t0) / 1000).toFixed(0);
|
|
6343
6692
|
const etaSec = completed > 0 ? Math.round((Date.now() - t0) / completed * (total - completed) / 1000) : 0;
|
|
6344
|
-
progress.text = `${completed}/${total} · ${elapsed}s · ETA ${etaSec}s · ${task.side === "prod" ?
|
|
6693
|
+
progress.text = `${completed}/${total} · ${elapsed}s · ETA ${etaSec}s · ${task.side === "prod" ? chalk8.cyan("prod") : chalk8.magenta("cand")} ${task.path} (${task.viewport})`;
|
|
6345
6694
|
}
|
|
6346
6695
|
});
|
|
6347
6696
|
progress.succeed(`${total} capture(s) em ${((Date.now() - t0) / 1000).toFixed(1)}s`);
|
|
@@ -6414,8 +6763,8 @@ async function vitalsCommand(opts) {
|
|
|
6414
6763
|
`, "utf8");
|
|
6415
6764
|
printSummary3(vitalsResult, cacheResult);
|
|
6416
6765
|
console.log("");
|
|
6417
|
-
console.log(
|
|
6418
|
-
console.log(
|
|
6766
|
+
console.log(chalk8.dim(` → ${paths.reportHtml}`));
|
|
6767
|
+
console.log(chalk8.dim(` \uD83D\uDCA1 use 'parity serve ${runId}' pra preview iframe`));
|
|
6419
6768
|
console.log("");
|
|
6420
6769
|
if (opts.open) {
|
|
6421
6770
|
const { default: open } = await import("open");
|
|
@@ -6543,27 +6892,27 @@ function computeVerdict2(checks, issues) {
|
|
|
6543
6892
|
}
|
|
6544
6893
|
function printSummary3(vitals, cache) {
|
|
6545
6894
|
console.log("");
|
|
6546
|
-
console.log(
|
|
6895
|
+
console.log(chalk8.bold(" Summary:"));
|
|
6547
6896
|
console.log(` ${vitals.summary}`);
|
|
6548
6897
|
console.log(` ${cache.summary}`);
|
|
6549
6898
|
}
|
|
6550
6899
|
|
|
6551
6900
|
// src/commands/list.ts
|
|
6552
|
-
import
|
|
6901
|
+
import chalk9 from "chalk";
|
|
6553
6902
|
function listCommand(output) {
|
|
6554
6903
|
const runs = listRuns(output);
|
|
6555
6904
|
if (runs.length === 0) {
|
|
6556
|
-
console.log(
|
|
6905
|
+
console.log(chalk9.dim(`Nenhum run salvo em ${output}`));
|
|
6557
6906
|
return 0;
|
|
6558
6907
|
}
|
|
6559
6908
|
for (const r of runs) {
|
|
6560
6909
|
try {
|
|
6561
6910
|
const run = loadRun(output, r.id);
|
|
6562
6911
|
const v = run.verdict;
|
|
6563
|
-
const status = v.status === "pass" ?
|
|
6564
|
-
console.log(` ${
|
|
6912
|
+
const status = v.status === "pass" ? chalk9.green(v.status) : v.status === "warn" ? chalk9.yellow(v.status) : chalk9.red(v.status);
|
|
6913
|
+
console.log(` ${chalk9.bold(r.id)} ${status} score=${v.score} critical=${v.critical} high=${v.high} ${chalk9.dim(r.timestamp)}`);
|
|
6565
6914
|
} catch {
|
|
6566
|
-
console.log(` ${
|
|
6915
|
+
console.log(` ${chalk9.bold(r.id)} ${chalk9.dim("(report.json ausente)")}`);
|
|
6567
6916
|
}
|
|
6568
6917
|
}
|
|
6569
6918
|
return 0;
|
|
@@ -6571,13 +6920,13 @@ function listCommand(output) {
|
|
|
6571
6920
|
|
|
6572
6921
|
// src/commands/prompt.ts
|
|
6573
6922
|
import { existsSync as existsSync8, writeFileSync as writeFileSync8 } from "node:fs";
|
|
6574
|
-
import
|
|
6923
|
+
import chalk10 from "chalk";
|
|
6575
6924
|
function promptCommand(runId, opts) {
|
|
6576
6925
|
let run;
|
|
6577
6926
|
try {
|
|
6578
6927
|
run = loadRun(opts.output, runId);
|
|
6579
6928
|
} catch (err) {
|
|
6580
|
-
console.error(
|
|
6929
|
+
console.error(chalk10.red(`✖ ${err.message}`));
|
|
6581
6930
|
return 1;
|
|
6582
6931
|
}
|
|
6583
6932
|
const md = buildLlmPrompt(run, {
|
|
@@ -6586,10 +6935,10 @@ function promptCommand(runId, opts) {
|
|
|
6586
6935
|
});
|
|
6587
6936
|
if (opts.out) {
|
|
6588
6937
|
writeFileSync8(opts.out, md, "utf8");
|
|
6589
|
-
console.log(
|
|
6938
|
+
console.log(chalk10.green(`✔ prompt salvo em ${opts.out}`));
|
|
6590
6939
|
if (existsSync8(opts.out)) {
|
|
6591
6940
|
const sizeKb = (md.length / 1024).toFixed(1);
|
|
6592
|
-
console.log(
|
|
6941
|
+
console.log(chalk10.dim(` ${md.length} chars (${sizeKb} KB)`));
|
|
6593
6942
|
}
|
|
6594
6943
|
} else {
|
|
6595
6944
|
process.stdout.write(md);
|
|
@@ -6598,20 +6947,20 @@ function promptCommand(runId, opts) {
|
|
|
6598
6947
|
}
|
|
6599
6948
|
|
|
6600
6949
|
// src/commands/report.ts
|
|
6601
|
-
import
|
|
6950
|
+
import chalk11 from "chalk";
|
|
6602
6951
|
import open from "open";
|
|
6603
6952
|
async function reportCommand(runId, output) {
|
|
6604
6953
|
const paths = getRunPaths(output, runId);
|
|
6605
|
-
console.log(
|
|
6954
|
+
console.log(chalk11.dim(`opening ${paths.reportHtml}`));
|
|
6606
6955
|
await open(paths.reportHtml).catch((err) => {
|
|
6607
|
-
console.error(
|
|
6956
|
+
console.error(chalk11.red(`failed to open: ${err.message}`));
|
|
6608
6957
|
});
|
|
6609
6958
|
return 0;
|
|
6610
6959
|
}
|
|
6611
6960
|
|
|
6612
6961
|
// src/commands/run.ts
|
|
6613
6962
|
import { join as join11 } from "node:path";
|
|
6614
|
-
import
|
|
6963
|
+
import chalk13 from "chalk";
|
|
6615
6964
|
import ora4 from "ora";
|
|
6616
6965
|
|
|
6617
6966
|
// src/checks/http-status.ts
|
|
@@ -8905,7 +9254,7 @@ cand: ${JSON.stringify(cand)}`,
|
|
|
8905
9254
|
|
|
8906
9255
|
// src/commands/serve.ts
|
|
8907
9256
|
import { existsSync as existsSync11 } from "node:fs";
|
|
8908
|
-
import
|
|
9257
|
+
import chalk12 from "chalk";
|
|
8909
9258
|
|
|
8910
9259
|
// src/server/proxy-server.ts
|
|
8911
9260
|
import { existsSync as existsSync10, readFileSync as readFileSync10, statSync } from "node:fs";
|
|
@@ -9125,16 +9474,16 @@ async function serveRunAndBlock(runDir, opts = {}) {
|
|
|
9125
9474
|
try {
|
|
9126
9475
|
handle = await startProxyServer(runDir, { port: opts.port });
|
|
9127
9476
|
} catch (err) {
|
|
9128
|
-
console.error(
|
|
9477
|
+
console.error(chalk12.red(` ✖ Falha ao iniciar servidor: ${err.message}`));
|
|
9129
9478
|
return 2;
|
|
9130
9479
|
}
|
|
9131
9480
|
const label = opts.label ?? "parity serve";
|
|
9132
9481
|
console.log("");
|
|
9133
|
-
console.log(
|
|
9134
|
-
console.log(` ${
|
|
9135
|
-
console.log(
|
|
9136
|
-
console.log(
|
|
9137
|
-
console.log(
|
|
9482
|
+
console.log(chalk12.bold(` ${label}`));
|
|
9483
|
+
console.log(` ${chalk12.green("●")} ${handle.url}`);
|
|
9484
|
+
console.log(chalk12.dim(` Servindo: ${runDir}`));
|
|
9485
|
+
console.log(chalk12.dim(` Proxy de iframes: ${handle.url}proxy?url=<encoded>`));
|
|
9486
|
+
console.log(chalk12.dim(` Ctrl+C pra parar (2x pra forçar saída).
|
|
9138
9487
|
`));
|
|
9139
9488
|
if (opts.open !== false) {
|
|
9140
9489
|
const { default: open2 } = await import("open");
|
|
@@ -9167,11 +9516,11 @@ async function serveRunAndBlock(runDir, opts = {}) {
|
|
|
9167
9516
|
async function serveCommand(runId, opts) {
|
|
9168
9517
|
const paths = getRunPaths(opts.output, runId);
|
|
9169
9518
|
if (!existsSync11(paths.runDir)) {
|
|
9170
|
-
console.error(
|
|
9519
|
+
console.error(chalk12.red(`✖ Run não encontrado: ${runId} (em ${opts.output})`));
|
|
9171
9520
|
return 1;
|
|
9172
9521
|
}
|
|
9173
9522
|
if (!existsSync11(paths.reportHtml)) {
|
|
9174
|
-
console.error(
|
|
9523
|
+
console.error(chalk12.red(`✖ report.html não existe em ${paths.runDir}`));
|
|
9175
9524
|
return 1;
|
|
9176
9525
|
}
|
|
9177
9526
|
return serveRunAndBlock(paths.runDir, {
|
|
@@ -9231,7 +9580,7 @@ function applyPreset(opts) {
|
|
|
9231
9580
|
async function runCommand(rawOpts) {
|
|
9232
9581
|
const opts = applyPreset(rawOpts);
|
|
9233
9582
|
if (rawOpts.preset) {
|
|
9234
|
-
console.log(
|
|
9583
|
+
console.log(chalk13.dim(` preset: ${rawOpts.preset}`));
|
|
9235
9584
|
}
|
|
9236
9585
|
const flows = opts.flows.split(",").map((s) => s.trim());
|
|
9237
9586
|
const viewports = opts.viewports.split(",").map((s) => s.trim());
|
|
@@ -9241,11 +9590,11 @@ async function runCommand(rawOpts) {
|
|
|
9241
9590
|
const ignore = loadParityIgnore();
|
|
9242
9591
|
const preflight = await preflightCheck(opts.prod, opts.cand);
|
|
9243
9592
|
if (!preflight.ok) {
|
|
9244
|
-
console.error(
|
|
9593
|
+
console.error(chalk13.red(`
|
|
9245
9594
|
✖ pre-flight falhou:`));
|
|
9246
9595
|
for (const err of preflight.errors)
|
|
9247
|
-
console.error(
|
|
9248
|
-
console.error(
|
|
9596
|
+
console.error(chalk13.red(` - ${err}`));
|
|
9597
|
+
console.error(chalk13.dim(`
|
|
9249
9598
|
dica: verifique se as URLs estão corretas e acessíveis`));
|
|
9250
9599
|
return 2;
|
|
9251
9600
|
}
|
|
@@ -9256,7 +9605,7 @@ async function runCommand(rawOpts) {
|
|
|
9256
9605
|
if (prodHomeHtml) {
|
|
9257
9606
|
platform = detectPlatform({ url: opts.prod, html: prodHomeHtml });
|
|
9258
9607
|
if (platform !== "custom") {
|
|
9259
|
-
console.log(
|
|
9608
|
+
console.log(chalk13.dim(` Detected platform: ${platform}`));
|
|
9260
9609
|
}
|
|
9261
9610
|
}
|
|
9262
9611
|
const prodHost = hostOf(opts.prod);
|
|
@@ -9298,11 +9647,11 @@ async function runCommand(rawOpts) {
|
|
|
9298
9647
|
const paths = createRunDir(opts.output, runId);
|
|
9299
9648
|
const startedAt = Date.now();
|
|
9300
9649
|
const timestamp = new Date().toISOString();
|
|
9301
|
-
console.log(
|
|
9650
|
+
console.log(chalk13.bold(`
|
|
9302
9651
|
parity run ${runId}`));
|
|
9303
|
-
console.log(
|
|
9304
|
-
console.log(
|
|
9305
|
-
console.log(
|
|
9652
|
+
console.log(chalk13.dim(` prod: ${opts.prod}`));
|
|
9653
|
+
console.log(chalk13.dim(` cand: ${opts.cand}`));
|
|
9654
|
+
console.log(chalk13.dim(` flows: ${flows.join(", ")} · viewports: ${viewports.join(", ")} · CEP: ${rc.cep}
|
|
9306
9655
|
`));
|
|
9307
9656
|
const spinner = ora4("Launching browser…").start();
|
|
9308
9657
|
let browser = null;
|
|
@@ -9616,7 +9965,7 @@ async function runCommand(rawOpts) {
|
|
|
9616
9965
|
if (opts.ci) {
|
|
9617
9966
|
const blocking = allIssues.filter((i) => failOn.includes(i.severity));
|
|
9618
9967
|
if (blocking.length > 0) {
|
|
9619
|
-
console.log(
|
|
9968
|
+
console.log(chalk13.red(`
|
|
9620
9969
|
✖ ${blocking.length} issue(s) bloqueante(s) [${failOn.join(", ")}] — exit 1`));
|
|
9621
9970
|
return 1;
|
|
9622
9971
|
}
|
|
@@ -9755,26 +10104,26 @@ function hostOf(url) {
|
|
|
9755
10104
|
}
|
|
9756
10105
|
function printSummary4(run, htmlPath, meta) {
|
|
9757
10106
|
const { verdict } = run;
|
|
9758
|
-
const emoji = verdict.status === "pass" ?
|
|
9759
|
-
const score = verdict.status === "pass" ?
|
|
10107
|
+
const emoji = verdict.status === "pass" ? chalk13.green("✔") : verdict.status === "warn" ? chalk13.yellow("⚠") : chalk13.red("✖");
|
|
10108
|
+
const score = verdict.status === "pass" ? chalk13.green(verdict.score) : verdict.status === "warn" ? chalk13.yellow(verdict.score) : chalk13.red(verdict.score);
|
|
9760
10109
|
console.log("");
|
|
9761
10110
|
console.log(` ${emoji} parity ${verdict.status.toUpperCase()} · score ${score}/100`);
|
|
9762
|
-
console.log(` checks: ${
|
|
9763
|
-
console.log(` issues: ${
|
|
10111
|
+
console.log(` checks: ${chalk13.green(verdict.checksPassed)} pass · ${chalk13.red(verdict.checksFailed)} fail · ${chalk13.dim(`${verdict.checksSkipped} skipped`)}`);
|
|
10112
|
+
console.log(` issues: ${chalk13.red(verdict.critical)} critical · ${chalk13.yellow(verdict.high)} high · ${verdict.medium} medium · ${chalk13.dim(`${verdict.low} low`)}`);
|
|
9764
10113
|
if (run.topIssues.length > 0) {
|
|
9765
10114
|
console.log("");
|
|
9766
|
-
console.log(
|
|
10115
|
+
console.log(chalk13.bold(" Top issues:"));
|
|
9767
10116
|
for (const i of run.topIssues.slice(0, 5)) {
|
|
9768
|
-
const sev = i.severity === "critical" ?
|
|
10117
|
+
const sev = i.severity === "critical" ? chalk13.red(`[${i.severity}]`) : i.severity === "high" ? chalk13.yellow(`[${i.severity}]`) : chalk13.dim(`[${i.severity}]`);
|
|
9769
10118
|
console.log(` ${sev} ${i.summary}`);
|
|
9770
10119
|
}
|
|
9771
10120
|
}
|
|
9772
10121
|
if (meta.promotedCount > 0 || meta.deprecatedCount > 0) {
|
|
9773
10122
|
console.log("");
|
|
9774
|
-
console.log(
|
|
10123
|
+
console.log(chalk13.dim(` \uD83D\uDCDA learned-selectors [${meta.platform}]: ${chalk13.green(`+${meta.promotedCount}`)} promoted · ${chalk13.yellow(meta.deprecatedCount)} deprecated`));
|
|
9775
10124
|
}
|
|
9776
10125
|
console.log("");
|
|
9777
|
-
console.log(
|
|
10126
|
+
console.log(chalk13.dim(` → ${htmlPath}`));
|
|
9778
10127
|
console.log("");
|
|
9779
10128
|
}
|
|
9780
10129
|
|
|
@@ -9824,6 +10173,18 @@ program.command("prompt").argument("<runId>", "Run ID").description("Generate a
|
|
|
9824
10173
|
program.command("explain").argument("<runId>", "Run ID").argument("<issueId>", "Issue ID").option("--output <dir>", "Output directory", "./parity-output").description("LLM deep-dive on a specific issue (requires ANTHROPIC_API_KEY)").action(async (runId, issueId, opts) => {
|
|
9825
10174
|
process.exit(await explainCommand(runId, issueId, opts.output));
|
|
9826
10175
|
});
|
|
10176
|
+
program.command("css-trace").description("Inspect which CSS rules (from which stylesheets) are affecting a DOM element. Single URL mode lists every matched rule; --prod + --cand mode diffs computed styles between Fresh and TanStack sides.").option("--url <url>", "Single URL to inspect (mutually exclusive with --prod/--cand)").option("--prod <url>", "Production URL (for comparison mode)").option("--cand <url>", "Candidate URL (for comparison mode)").requiredOption("--selector <sel>", "CSS selector for the target element (e.g. 'html', '[data-aside]', '.drawer-side')").option("--filter <props>", "Comma-separated property names to focus on (e.g. 'scrollbar-gutter,position,width')").option("--viewport <viewport>", "mobile, tablet, or desktop", "desktop").option("--settle <ms>", "Wait this many ms after `load` for hydration", (v) => Number(v), 1500).option("--json", "Output JSON instead of pretty text", false).action(async (opts) => {
|
|
10177
|
+
process.exit(await cssTraceCommand({
|
|
10178
|
+
url: opts.url,
|
|
10179
|
+
prod: opts.prod,
|
|
10180
|
+
cand: opts.cand,
|
|
10181
|
+
selector: opts.selector,
|
|
10182
|
+
filter: opts.filter,
|
|
10183
|
+
viewport: opts.viewport,
|
|
10184
|
+
settleMs: opts.settle,
|
|
10185
|
+
json: opts.json
|
|
10186
|
+
}));
|
|
10187
|
+
});
|
|
9827
10188
|
program.command("learned").description("Inspect the learned-selectors library").addCommand(new Command("stats").description("Print learned-selectors stats per platform").action(() => {
|
|
9828
10189
|
process.exit(learnedStats());
|
|
9829
10190
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/parity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "E2E parity validator for site migrations. Compares prod vs cand and reports UI, functional, SEO, visual, and Web Vitals deltas with an LLM-ranked HTML report.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|