@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.
Files changed (3) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/cli.js +548 -187
  3. 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
- let response = null;
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/explain.ts
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(chalk4.red("Nenhuma API key de LLM encontrada — set ANTHROPIC_API_KEY ou OPENROUTER_API_KEY."));
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(chalk4.red(`Issue não encontrada: ${issueId}`));
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(chalk4.dim(`Consultando LLM…
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 chalk5 from "chalk";
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
- switch (flow) {
4796
- case "homepage":
4797
- return finalize(flow, ctx, await flowHomepage(ctx), [], start);
4798
- case "plp":
4799
- return finalize(flow, ctx, await flowPlp(ctx), [], start);
4800
- case "pdp":
4801
- return finalize(flow, ctx, await flowPdp(ctx), [], start);
4802
- case "purchase-journey": {
4803
- const { pages, steps } = await flowPurchaseJourney(ctx);
4804
- return finalize(flow, ctx, pages, steps, start);
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
- const cepInputPdp = await firstVisible(page, selFor(ctx, "cepInputPdp"));
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
- const cepInputCart = await firstVisible(page, selFor(ctx, "cepInputCart"));
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 t8 = Date.now();
5180
- const beforeUrl8 = page.url();
5181
- const spBefore8 = screenshotPath(ctx, "pj-8-checkout-before");
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
- checkoutHit.locator.click({ timeout: 5000 }).catch(() => {
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
- await page.waitForTimeout(1500);
5195
- const sp8 = screenshotPath(ctx, "pj-8-checkout-reached");
5196
- await page.screenshot({ path: sp8, fullPage: false }).catch(() => {
5197
- return;
5198
- });
5199
- const checkoutUrl = page.url();
5200
- const reachedCheckout = /\/checkout/i.test(checkoutUrl);
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: checkoutUrl,
5210
- screenshotPath: sp8,
5211
- screenshotBeforePath: spBefore8,
5558
+ url: result.url,
5559
+ screenshotPath: result.spAfter,
5560
+ screenshotBeforePath: result.spBefore,
5212
5561
  beforeUrl: beforeUrl8,
5213
- actionDescription: `Clicou em${checkoutText ? ` '${checkoutText.slice(0, 30).trim()}'` : ""} (\`${checkoutHit.selector}\`); URL final: ${checkoutUrl}${reachedCheckout ? " ✓ atingiu /checkout" : " ✗ não foi pra checkout"}`,
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: checkoutHit.selector,
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(chalk5.red("Nenhum viewport válido (use mobile,desktop)"));
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(chalk5.bold(`
6017
+ console.log(chalk6.bold(`
5669
6018
  parity journey ${runId}`));
5670
- console.log(chalk5.dim(` prod: ${opts.prod}`));
5671
- console.log(chalk5.dim(` cand: ${opts.cand}`));
5672
- console.log(chalk5.dim(` viewports: ${viewports.join(", ")} · CEP: ${rc.cep}`));
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(chalk5.dim(` llm: ${providerLabel()}`));
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" ? chalk5.cyan("prod") : chalk5.magenta("cand");
5719
- const prefix = ` ${chalk5.dim(`[${viewport}/`)}${sideTag}${chalk5.dim("]")}`;
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} ${chalk5.dim(`${event.index}/${event.total}`)} ▶ ${chalk5.bold(stepLabel(event.name))}`);
6070
+ console.log(`${prefix} ${chalk6.dim(`${event.index}/${event.total}`)} ▶ ${chalk6.bold(stepLabel(event.name))}`);
5722
6071
  } else {
5723
- const glyph = event.status === "ok" ? chalk5.green("✓") : event.status === "skipped" ? chalk5.yellow("○") : chalk5.red("✗");
5724
- const noteText = event.note ? chalk5.dim(` (${event.note})`) : "";
5725
- const time = chalk5.dim(`${(event.durationMs / 1000).toFixed(1)}s`);
5726
- console.log(`${prefix} ${chalk5.dim(`${event.index}/${event.total}`)} ${glyph} ${stepLabel(event.name)} ${time}${noteText}`);
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(chalk5.bold(`
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(chalk5.dim(` → JUnit XML: ${opts.junit}`));
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(chalk5.bold(` [${viewport}]`));
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" ? chalk5.red("FAILED") : "";
5863
- const note = r.cand?.status === "failed" || r.cand?.status === "skipped" ? chalk5.dim(`(${r.cand?.note ?? r.cand?.status})`) : "";
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 chalk5.dim("—");
6220
+ return chalk6.dim("—");
5872
6221
  if (status === "ok")
5873
- return chalk5.green("✓");
6222
+ return chalk6.green("✓");
5874
6223
  if (status === "skipped")
5875
- return chalk5.yellow("○");
5876
- return chalk5.red("✗");
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(chalk5.bold(" Summary:"));
6236
+ console.log(chalk6.bold(" Summary:"));
5888
6237
  for (const [vp, { passed, total }] of byViewport) {
5889
- const stat = passed === total ? chalk5.green("✓") : chalk5.yellow(`${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(chalk5.red(` ✗ ${failures.length} step(s) divergent(es) (${crit} crítico)`));
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 ? chalk5.red("[critical]") : chalk5.yellow("[warn]");
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(chalk5.green(" ✓ jornada completa em cand"));
6251
+ console.log(chalk6.green(" ✓ jornada completa em cand"));
5903
6252
  }
5904
6253
  console.log("");
5905
- console.log(chalk5.dim(` → ${htmlPath}`));
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 chalk6 from "chalk";
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(chalk6.dim(`Biblioteca vazia: ${LEARNED_PATH}`));
6139
- console.log(chalk6.dim("Rode `parity run ...` em algum site para começar a popular."));
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(chalk6.bold(`
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(`${chalk6.cyan(p.platform)}: ${p.activeSelectors} active · ${chalk6.dim(`${p.deprecatedSelectors} deprecated`)}`);
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(` ${chalk6.dim(top.key.padEnd(18))} ${chalk6.green(sr.padStart(4))} ${chalk6.dim(`(${top.hosts} hosts)`)} ${top.selector}`);
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 chalk7 from "chalk";
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(chalk7.bold(`
6658
+ console.log(chalk8.bold(`
6310
6659
  parity vitals ${runId}`));
6311
- console.log(chalk7.dim(` prod: ${opts.prod}`));
6312
- console.log(chalk7.dim(` cand: ${opts.cand}`));
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" ? chalk7.cyan("prod") : chalk7.magenta("cand")} ${task.path} (${task.viewport})`;
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(chalk7.dim(` → ${paths.reportHtml}`));
6418
- console.log(chalk7.dim(` \uD83D\uDCA1 use 'parity serve ${runId}' pra preview iframe`));
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(chalk7.bold(" Summary:"));
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 chalk8 from "chalk";
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(chalk8.dim(`Nenhum run salvo em ${output}`));
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" ? chalk8.green(v.status) : v.status === "warn" ? chalk8.yellow(v.status) : chalk8.red(v.status);
6564
- console.log(` ${chalk8.bold(r.id)} ${status} score=${v.score} critical=${v.critical} high=${v.high} ${chalk8.dim(r.timestamp)}`);
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(` ${chalk8.bold(r.id)} ${chalk8.dim("(report.json ausente)")}`);
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 chalk9 from "chalk";
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(chalk9.red(`✖ ${err.message}`));
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(chalk9.green(`✔ prompt salvo em ${opts.out}`));
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(chalk9.dim(` ${md.length} chars (${sizeKb} KB)`));
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 chalk10 from "chalk";
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(chalk10.dim(`opening ${paths.reportHtml}`));
6954
+ console.log(chalk11.dim(`opening ${paths.reportHtml}`));
6606
6955
  await open(paths.reportHtml).catch((err) => {
6607
- console.error(chalk10.red(`failed to open: ${err.message}`));
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 chalk12 from "chalk";
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 chalk11 from "chalk";
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(chalk11.red(` ✖ Falha ao iniciar servidor: ${err.message}`));
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(chalk11.bold(` ${label}`));
9134
- console.log(` ${chalk11.green("●")} ${handle.url}`);
9135
- console.log(chalk11.dim(` Servindo: ${runDir}`));
9136
- console.log(chalk11.dim(` Proxy de iframes: ${handle.url}proxy?url=<encoded>`));
9137
- console.log(chalk11.dim(` Ctrl+C pra parar (2x pra forçar saída).
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(chalk11.red(`✖ Run não encontrado: ${runId} (em ${opts.output})`));
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(chalk11.red(`✖ report.html não existe em ${paths.runDir}`));
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(chalk12.dim(` preset: ${rawOpts.preset}`));
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(chalk12.red(`
9593
+ console.error(chalk13.red(`
9245
9594
  ✖ pre-flight falhou:`));
9246
9595
  for (const err of preflight.errors)
9247
- console.error(chalk12.red(` - ${err}`));
9248
- console.error(chalk12.dim(`
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(chalk12.dim(` Detected platform: ${platform}`));
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(chalk12.bold(`
9650
+ console.log(chalk13.bold(`
9302
9651
  parity run ${runId}`));
9303
- console.log(chalk12.dim(` prod: ${opts.prod}`));
9304
- console.log(chalk12.dim(` cand: ${opts.cand}`));
9305
- console.log(chalk12.dim(` flows: ${flows.join(", ")} · viewports: ${viewports.join(", ")} · CEP: ${rc.cep}
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(chalk12.red(`
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" ? chalk12.green("✔") : verdict.status === "warn" ? chalk12.yellow("⚠") : chalk12.red("✖");
9759
- const score = verdict.status === "pass" ? chalk12.green(verdict.score) : verdict.status === "warn" ? chalk12.yellow(verdict.score) : chalk12.red(verdict.score);
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: ${chalk12.green(verdict.checksPassed)} pass · ${chalk12.red(verdict.checksFailed)} fail · ${chalk12.dim(`${verdict.checksSkipped} skipped`)}`);
9763
- console.log(` issues: ${chalk12.red(verdict.critical)} critical · ${chalk12.yellow(verdict.high)} high · ${verdict.medium} medium · ${chalk12.dim(`${verdict.low} low`)}`);
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(chalk12.bold(" Top issues:"));
10115
+ console.log(chalk13.bold(" Top issues:"));
9767
10116
  for (const i of run.topIssues.slice(0, 5)) {
9768
- const sev = i.severity === "critical" ? chalk12.red(`[${i.severity}]`) : i.severity === "high" ? chalk12.yellow(`[${i.severity}]`) : chalk12.dim(`[${i.severity}]`);
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(chalk12.dim(` \uD83D\uDCDA learned-selectors [${meta.platform}]: ${chalk12.green(`+${meta.promotedCount}`)} promoted · ${chalk12.yellow(meta.deprecatedCount)} deprecated`));
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(chalk12.dim(` → ${htmlPath}`));
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.1.1",
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",