@decocms/parity 0.1.0 → 0.2.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 +7 -0
  2. package/dist/cli.js +474 -146
  3. package/package.json +85 -85
package/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.1] — 2026-05-22
11
+
12
+ ### Fixed
13
+
14
+ - `cache-coverage`: classify `decoims.com` (deco image proxy) and `assets.decocache.com` (deco edge cache) as first-party so they remain eligible for cache-opportunity reporting instead of being silently skipped as third-party. ([#5](https://github.com/decocms/parity/pull/5))
15
+ - `purchase-journey-flow`: never silently return `pass` when zero comparable steps were evaluated. The check now returns `skipped` when the flow wasn't requested, and `fail` (critical) when the flow was requested but neither side produced a capture or when capture arrays came back empty. Previously a fully broken cand home would still get a green verdict on the purchase-journey check. ([#6](https://github.com/decocms/parity/pull/6))
16
+
10
17
  ## [0.1.0] — 2026-05-12
11
18
 
12
19
  First public release.
package/dist/cli.js CHANGED
@@ -540,7 +540,7 @@ function isThirdParty(url, baseHost) {
540
540
  return false;
541
541
  if (u.hostname.endsWith(`.${cleanedBase}`))
542
542
  return false;
543
- if (u.hostname.includes("vtexassets") || u.hostname.includes("decoassets") || u.hostname.includes("vteximg"))
543
+ if (u.hostname.includes("vtexassets") || u.hostname.includes("decoassets") || u.hostname.includes("vteximg") || u.hostname.includes("decocache") || u.hostname.includes("decoims"))
544
544
  return false;
545
545
  return true;
546
546
  } catch {
@@ -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
@@ -5655,7 +5924,7 @@ var CRITICAL_STEPS = new Set([
5655
5924
  async function journeyCommand(opts) {
5656
5925
  const viewports = opts.viewports.split(",").map((s) => s.trim()).filter((s) => s === "mobile" || s === "desktop");
5657
5926
  if (viewports.length === 0) {
5658
- console.error(chalk5.red("Nenhum viewport válido (use mobile,desktop)"));
5927
+ console.error(chalk6.red("Nenhum viewport válido (use mobile,desktop)"));
5659
5928
  return 2;
5660
5929
  }
5661
5930
  const rc = loadParityRc();
@@ -5665,13 +5934,13 @@ async function journeyCommand(opts) {
5665
5934
  const runId = newRunId();
5666
5935
  const paths = createRunDir(opts.output, runId);
5667
5936
  if (!opts.json) {
5668
- console.log(chalk5.bold(`
5937
+ console.log(chalk6.bold(`
5669
5938
  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}`));
5939
+ console.log(chalk6.dim(` prod: ${opts.prod}`));
5940
+ console.log(chalk6.dim(` cand: ${opts.cand}`));
5941
+ console.log(chalk6.dim(` viewports: ${viewports.join(", ")} · CEP: ${rc.cep}`));
5673
5942
  if (isLlmAvailable())
5674
- console.log(chalk5.dim(` llm: ${providerLabel()}`));
5943
+ console.log(chalk6.dim(` llm: ${providerLabel()}`));
5675
5944
  console.log("");
5676
5945
  }
5677
5946
  let platform = "custom";
@@ -5715,15 +5984,15 @@ async function journeyCommand(opts) {
5715
5984
  const onStepFor = (viewport, side) => (event) => {
5716
5985
  if (opts.json)
5717
5986
  return;
5718
- const sideTag = side === "prod" ? chalk5.cyan("prod") : chalk5.magenta("cand");
5719
- const prefix = ` ${chalk5.dim(`[${viewport}/`)}${sideTag}${chalk5.dim("]")}`;
5987
+ const sideTag = side === "prod" ? chalk6.cyan("prod") : chalk6.magenta("cand");
5988
+ const prefix = ` ${chalk6.dim(`[${viewport}/`)}${sideTag}${chalk6.dim("]")}`;
5720
5989
  if (event.phase === "start") {
5721
- console.log(`${prefix} ${chalk5.dim(`${event.index}/${event.total}`)} ▶ ${chalk5.bold(stepLabel(event.name))}`);
5990
+ console.log(`${prefix} ${chalk6.dim(`${event.index}/${event.total}`)} ▶ ${chalk6.bold(stepLabel(event.name))}`);
5722
5991
  } 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}`);
5992
+ const glyph = event.status === "ok" ? chalk6.green("✓") : event.status === "skipped" ? chalk6.yellow("○") : chalk6.red("✗");
5993
+ const noteText = event.note ? chalk6.dim(` (${event.note})`) : "";
5994
+ const time = chalk6.dim(`${(event.durationMs / 1000).toFixed(1)}s`);
5995
+ console.log(`${prefix} ${chalk6.dim(`${event.index}/${event.total}`)} ${glyph} ${stepLabel(event.name)} ${time}${noteText}`);
5727
5996
  }
5728
5997
  };
5729
5998
  async function runOneSide(browserInstance, viewport, side) {
@@ -5761,7 +6030,7 @@ async function journeyCommand(opts) {
5761
6030
  spinner?.succeed("Browser pronto");
5762
6031
  for (const viewport of viewports) {
5763
6032
  if (!opts.json)
5764
- console.log(chalk5.bold(`
6033
+ console.log(chalk6.bold(`
5765
6034
  ── ${viewport} ─────────────────────────────────────────────`));
5766
6035
  const [prodCap, candCap] = await Promise.all([
5767
6036
  runOneSide(browser, viewport, "prod"),
@@ -5788,7 +6057,7 @@ async function journeyCommand(opts) {
5788
6057
  if (opts.junit) {
5789
6058
  writeFileSync6(opts.junit, buildJUnit(rows, failures), "utf8");
5790
6059
  if (!opts.json)
5791
- console.log(chalk5.dim(` → JUnit XML: ${opts.junit}`));
6060
+ console.log(chalk6.dim(` → JUnit XML: ${opts.junit}`));
5792
6061
  }
5793
6062
  return failures.some((f) => f.critical) ? 1 : 0;
5794
6063
  } catch (err) {
@@ -5853,14 +6122,14 @@ function collectFailures(rows) {
5853
6122
  function printTable(viewports, rows) {
5854
6123
  console.log("");
5855
6124
  for (const viewport of viewports) {
5856
- console.log(chalk5.bold(` [${viewport}]`));
6125
+ console.log(chalk6.bold(` [${viewport}]`));
5857
6126
  const vrows = rows.filter((r) => r.viewport === viewport);
5858
6127
  for (const r of vrows) {
5859
6128
  const label = STEP_LABELS[r.name] ?? r.name;
5860
6129
  const p = statusGlyph(r.prod?.status);
5861
6130
  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})`) : "";
6131
+ const status = r.prod?.status === "ok" && r.cand?.status !== "ok" ? chalk6.red("FAILED") : "";
6132
+ const note = r.cand?.status === "failed" || r.cand?.status === "skipped" ? chalk6.dim(`(${r.cand?.note ?? r.cand?.status})`) : "";
5864
6133
  console.log(` ${label.padEnd(24)} prod ${p} cand ${c} ${status} ${note}`);
5865
6134
  }
5866
6135
  console.log("");
@@ -5868,12 +6137,12 @@ function printTable(viewports, rows) {
5868
6137
  }
5869
6138
  function statusGlyph(status) {
5870
6139
  if (!status)
5871
- return chalk5.dim("—");
6140
+ return chalk6.dim("—");
5872
6141
  if (status === "ok")
5873
- return chalk5.green("✓");
6142
+ return chalk6.green("✓");
5874
6143
  if (status === "skipped")
5875
- return chalk5.yellow("○");
5876
- return chalk5.red("✗");
6144
+ return chalk6.yellow("○");
6145
+ return chalk6.red("✗");
5877
6146
  }
5878
6147
  function printSummary2(rows, failures, htmlPath) {
5879
6148
  const byViewport = new Map;
@@ -5884,25 +6153,25 @@ function printSummary2(rows, failures, htmlPath) {
5884
6153
  v.passed++;
5885
6154
  byViewport.set(r.viewport, v);
5886
6155
  }
5887
- console.log(chalk5.bold(" Summary:"));
6156
+ console.log(chalk6.bold(" Summary:"));
5888
6157
  for (const [vp, { passed, total }] of byViewport) {
5889
- const stat = passed === total ? chalk5.green("✓") : chalk5.yellow(`${passed}/${total}`);
6158
+ const stat = passed === total ? chalk6.green("✓") : chalk6.yellow(`${passed}/${total}`);
5890
6159
  console.log(` ${vp.padEnd(8)} ${stat} steps em cand`);
5891
6160
  }
5892
6161
  if (failures.length > 0) {
5893
6162
  const crit = failures.filter((f) => f.critical).length;
5894
6163
  console.log("");
5895
- console.log(chalk5.red(` ✗ ${failures.length} step(s) divergent(es) (${crit} crítico)`));
6164
+ console.log(chalk6.red(` ✗ ${failures.length} step(s) divergent(es) (${crit} crítico)`));
5896
6165
  for (const f of failures) {
5897
- const tag = f.critical ? chalk5.red("[critical]") : chalk5.yellow("[warn]");
6166
+ const tag = f.critical ? chalk6.red("[critical]") : chalk6.yellow("[warn]");
5898
6167
  console.log(` ${tag} [${f.viewport}] ${f.step}: ${f.reason}`);
5899
6168
  }
5900
6169
  } else {
5901
6170
  console.log("");
5902
- console.log(chalk5.green(" ✓ jornada completa em cand"));
6171
+ console.log(chalk6.green(" ✓ jornada completa em cand"));
5903
6172
  }
5904
6173
  console.log("");
5905
- console.log(chalk5.dim(` → ${htmlPath}`));
6174
+ console.log(chalk6.dim(` → ${htmlPath}`));
5906
6175
  console.log("");
5907
6176
  }
5908
6177
  function emitGithubAnnotations(failures) {
@@ -6130,23 +6399,23 @@ function escapeHtml(s) {
6130
6399
  }
6131
6400
 
6132
6401
  // src/commands/learned.ts
6133
- import chalk6 from "chalk";
6402
+ import chalk7 from "chalk";
6134
6403
  function learnedStats() {
6135
6404
  const lib = loadLearned();
6136
6405
  const stats = statsFromLib(lib);
6137
6406
  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."));
6407
+ console.log(chalk7.dim(`Biblioteca vazia: ${LEARNED_PATH}`));
6408
+ console.log(chalk7.dim("Rode `parity run ...` em algum site para começar a popular."));
6140
6409
  return 0;
6141
6410
  }
6142
- console.log(chalk6.bold(`
6411
+ console.log(chalk7.bold(`
6143
6412
  learned-selectors stats (${LEARNED_PATH})
6144
6413
  `));
6145
6414
  for (const p of stats.platforms) {
6146
- console.log(`${chalk6.cyan(p.platform)}: ${p.activeSelectors} active · ${chalk6.dim(`${p.deprecatedSelectors} deprecated`)}`);
6415
+ console.log(`${chalk7.cyan(p.platform)}: ${p.activeSelectors} active · ${chalk7.dim(`${p.deprecatedSelectors} deprecated`)}`);
6147
6416
  for (const top of p.topByKey) {
6148
6417
  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}`);
6418
+ console.log(` ${chalk7.dim(top.key.padEnd(18))} ${chalk7.green(sr.padStart(4))} ${chalk7.dim(`(${top.hosts} hosts)`)} ${top.selector}`);
6150
6419
  }
6151
6420
  console.log("");
6152
6421
  }
@@ -6156,7 +6425,7 @@ learned-selectors stats (${LEARNED_PATH})
6156
6425
  // src/commands/vitals.ts
6157
6426
  import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
6158
6427
  import { join as join8 } from "node:path";
6159
- import chalk7 from "chalk";
6428
+ import chalk8 from "chalk";
6160
6429
  import ora3 from "ora";
6161
6430
 
6162
6431
  // src/diff/vitals.ts
@@ -6306,10 +6575,10 @@ async function vitalsCommand(opts) {
6306
6575
  const runId = newRunId();
6307
6576
  const paths = createRunDir(opts.output, runId);
6308
6577
  const t0 = Date.now();
6309
- console.log(chalk7.bold(`
6578
+ console.log(chalk8.bold(`
6310
6579
  parity vitals ${runId}`));
6311
- console.log(chalk7.dim(` prod: ${opts.prod}`));
6312
- console.log(chalk7.dim(` cand: ${opts.cand}`));
6580
+ console.log(chalk8.dim(` prod: ${opts.prod}`));
6581
+ console.log(chalk8.dim(` cand: ${opts.cand}`));
6313
6582
  const discoverSpinner = ora3("Descobrindo páginas…").start();
6314
6583
  const pagePaths = await discoverPagePaths2(opts.prod, opts);
6315
6584
  if (pagePaths.length === 0) {
@@ -6341,7 +6610,7 @@ async function vitalsCommand(opts) {
6341
6610
  completed++;
6342
6611
  const elapsed = ((Date.now() - t0) / 1000).toFixed(0);
6343
6612
  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})`;
6613
+ progress.text = `${completed}/${total} · ${elapsed}s · ETA ${etaSec}s · ${task.side === "prod" ? chalk8.cyan("prod") : chalk8.magenta("cand")} ${task.path} (${task.viewport})`;
6345
6614
  }
6346
6615
  });
6347
6616
  progress.succeed(`${total} capture(s) em ${((Date.now() - t0) / 1000).toFixed(1)}s`);
@@ -6414,8 +6683,8 @@ async function vitalsCommand(opts) {
6414
6683
  `, "utf8");
6415
6684
  printSummary3(vitalsResult, cacheResult);
6416
6685
  console.log("");
6417
- console.log(chalk7.dim(` → ${paths.reportHtml}`));
6418
- console.log(chalk7.dim(` \uD83D\uDCA1 use 'parity serve ${runId}' pra preview iframe`));
6686
+ console.log(chalk8.dim(` → ${paths.reportHtml}`));
6687
+ console.log(chalk8.dim(` \uD83D\uDCA1 use 'parity serve ${runId}' pra preview iframe`));
6419
6688
  console.log("");
6420
6689
  if (opts.open) {
6421
6690
  const { default: open } = await import("open");
@@ -6543,27 +6812,27 @@ function computeVerdict2(checks, issues) {
6543
6812
  }
6544
6813
  function printSummary3(vitals, cache) {
6545
6814
  console.log("");
6546
- console.log(chalk7.bold(" Summary:"));
6815
+ console.log(chalk8.bold(" Summary:"));
6547
6816
  console.log(` ${vitals.summary}`);
6548
6817
  console.log(` ${cache.summary}`);
6549
6818
  }
6550
6819
 
6551
6820
  // src/commands/list.ts
6552
- import chalk8 from "chalk";
6821
+ import chalk9 from "chalk";
6553
6822
  function listCommand(output) {
6554
6823
  const runs = listRuns(output);
6555
6824
  if (runs.length === 0) {
6556
- console.log(chalk8.dim(`Nenhum run salvo em ${output}`));
6825
+ console.log(chalk9.dim(`Nenhum run salvo em ${output}`));
6557
6826
  return 0;
6558
6827
  }
6559
6828
  for (const r of runs) {
6560
6829
  try {
6561
6830
  const run = loadRun(output, r.id);
6562
6831
  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)}`);
6832
+ const status = v.status === "pass" ? chalk9.green(v.status) : v.status === "warn" ? chalk9.yellow(v.status) : chalk9.red(v.status);
6833
+ console.log(` ${chalk9.bold(r.id)} ${status} score=${v.score} critical=${v.critical} high=${v.high} ${chalk9.dim(r.timestamp)}`);
6565
6834
  } catch {
6566
- console.log(` ${chalk8.bold(r.id)} ${chalk8.dim("(report.json ausente)")}`);
6835
+ console.log(` ${chalk9.bold(r.id)} ${chalk9.dim("(report.json ausente)")}`);
6567
6836
  }
6568
6837
  }
6569
6838
  return 0;
@@ -6571,13 +6840,13 @@ function listCommand(output) {
6571
6840
 
6572
6841
  // src/commands/prompt.ts
6573
6842
  import { existsSync as existsSync8, writeFileSync as writeFileSync8 } from "node:fs";
6574
- import chalk9 from "chalk";
6843
+ import chalk10 from "chalk";
6575
6844
  function promptCommand(runId, opts) {
6576
6845
  let run;
6577
6846
  try {
6578
6847
  run = loadRun(opts.output, runId);
6579
6848
  } catch (err) {
6580
- console.error(chalk9.red(`✖ ${err.message}`));
6849
+ console.error(chalk10.red(`✖ ${err.message}`));
6581
6850
  return 1;
6582
6851
  }
6583
6852
  const md = buildLlmPrompt(run, {
@@ -6586,10 +6855,10 @@ function promptCommand(runId, opts) {
6586
6855
  });
6587
6856
  if (opts.out) {
6588
6857
  writeFileSync8(opts.out, md, "utf8");
6589
- console.log(chalk9.green(`✔ prompt salvo em ${opts.out}`));
6858
+ console.log(chalk10.green(`✔ prompt salvo em ${opts.out}`));
6590
6859
  if (existsSync8(opts.out)) {
6591
6860
  const sizeKb = (md.length / 1024).toFixed(1);
6592
- console.log(chalk9.dim(` ${md.length} chars (${sizeKb} KB)`));
6861
+ console.log(chalk10.dim(` ${md.length} chars (${sizeKb} KB)`));
6593
6862
  }
6594
6863
  } else {
6595
6864
  process.stdout.write(md);
@@ -6598,20 +6867,20 @@ function promptCommand(runId, opts) {
6598
6867
  }
6599
6868
 
6600
6869
  // src/commands/report.ts
6601
- import chalk10 from "chalk";
6870
+ import chalk11 from "chalk";
6602
6871
  import open from "open";
6603
6872
  async function reportCommand(runId, output) {
6604
6873
  const paths = getRunPaths(output, runId);
6605
- console.log(chalk10.dim(`opening ${paths.reportHtml}`));
6874
+ console.log(chalk11.dim(`opening ${paths.reportHtml}`));
6606
6875
  await open(paths.reportHtml).catch((err) => {
6607
- console.error(chalk10.red(`failed to open: ${err.message}`));
6876
+ console.error(chalk11.red(`failed to open: ${err.message}`));
6608
6877
  });
6609
6878
  return 0;
6610
6879
  }
6611
6880
 
6612
6881
  // src/commands/run.ts
6613
6882
  import { join as join11 } from "node:path";
6614
- import chalk12 from "chalk";
6883
+ import chalk13 from "chalk";
6615
6884
  import ora4 from "ora";
6616
6885
 
6617
6886
  // src/checks/http-status.ts
@@ -7466,11 +7735,49 @@ function purchaseJourneyFlow(ctx) {
7466
7735
  let totalSteps = 0;
7467
7736
  let failedSteps = 0;
7468
7737
  let asymmetricSkips = 0;
7738
+ const hasAnyPurchaseFlow = ctx.prodFlows.some((f) => f.flow === "purchase-journey") || ctx.candFlows.some((f) => f.flow === "purchase-journey");
7739
+ if (!hasAnyPurchaseFlow) {
7740
+ return {
7741
+ name: "purchase-journey-flow",
7742
+ status: "skipped",
7743
+ severity: "critical",
7744
+ durationMs: Date.now() - start,
7745
+ summary: "purchase-journey não estava no escopo do run (sem captura de flow)",
7746
+ issues: [],
7747
+ data: { totalSteps: 0, failedSteps: 0, asymmetricSkips: 0 }
7748
+ };
7749
+ }
7469
7750
  for (const viewport of ctx.viewports) {
7470
7751
  const prodFlow = findFlow(ctx.prodFlows, viewport);
7471
7752
  const candFlow = findFlow(ctx.candFlows, viewport);
7472
- if (!prodFlow || !candFlow)
7753
+ if (!prodFlow || !candFlow) {
7754
+ if (prodFlow && !candFlow) {
7755
+ issues.push({
7756
+ id: `pj:${viewport}:missing-cand-flow`,
7757
+ severity: "critical",
7758
+ category: "functional",
7759
+ check: "purchase-journey-flow",
7760
+ summary: `[${viewport}] cand não produziu captura da purchase-journey (prod produziu) — checkout indisponível ou cliente nunca hidratou`
7761
+ });
7762
+ } else if (!prodFlow && candFlow) {
7763
+ issues.push({
7764
+ id: `pj:${viewport}:missing-prod-flow`,
7765
+ severity: "high",
7766
+ category: "functional",
7767
+ check: "purchase-journey-flow",
7768
+ summary: `[${viewport}] prod não produziu captura da purchase-journey — selectors podem estar desalinhados com a source-of-truth`
7769
+ });
7770
+ } else {
7771
+ issues.push({
7772
+ id: `pj:${viewport}:no-flow-either-side`,
7773
+ severity: "critical",
7774
+ category: "functional",
7775
+ check: "purchase-journey-flow",
7776
+ summary: `[${viewport}] purchase-journey foi requisitada mas não há captura em prod nem cand — flow falhou completamente`
7777
+ });
7778
+ }
7473
7779
  continue;
7780
+ }
7474
7781
  const prodSteps = indexBy(prodFlow.steps ?? [], (s) => s.name);
7475
7782
  const candSteps = indexBy(candFlow.steps ?? [], (s) => s.name);
7476
7783
  const allNames = new Set([...prodSteps.keys(), ...candSteps.keys()]);
@@ -7518,6 +7825,15 @@ function purchaseJourneyFlow(ctx) {
7518
7825
  } else if (p.status === "skipped" && c.status === "ok") {}
7519
7826
  }
7520
7827
  }
7828
+ if (totalSteps === 0 && issues.length === 0) {
7829
+ issues.push({
7830
+ id: "pj:zero-steps-evaluated",
7831
+ severity: "critical",
7832
+ category: "functional",
7833
+ check: "purchase-journey-flow",
7834
+ summary: "purchase-journey rodou mas avaliou 0 step(s) — capturas existem mas estão vazias dos dois lados (selectors quebrados ou home não hidratou)"
7835
+ });
7836
+ }
7521
7837
  const status = issues.some((i) => i.severity === "critical") ? "fail" : issues.length > 0 ? "warn" : "pass";
7522
7838
  return {
7523
7839
  name: "purchase-journey-flow",
@@ -8858,7 +9174,7 @@ cand: ${JSON.stringify(cand)}`,
8858
9174
 
8859
9175
  // src/commands/serve.ts
8860
9176
  import { existsSync as existsSync11 } from "node:fs";
8861
- import chalk11 from "chalk";
9177
+ import chalk12 from "chalk";
8862
9178
 
8863
9179
  // src/server/proxy-server.ts
8864
9180
  import { existsSync as existsSync10, readFileSync as readFileSync10, statSync } from "node:fs";
@@ -9078,16 +9394,16 @@ async function serveRunAndBlock(runDir, opts = {}) {
9078
9394
  try {
9079
9395
  handle = await startProxyServer(runDir, { port: opts.port });
9080
9396
  } catch (err) {
9081
- console.error(chalk11.red(` ✖ Falha ao iniciar servidor: ${err.message}`));
9397
+ console.error(chalk12.red(` ✖ Falha ao iniciar servidor: ${err.message}`));
9082
9398
  return 2;
9083
9399
  }
9084
9400
  const label = opts.label ?? "parity serve";
9085
9401
  console.log("");
9086
- console.log(chalk11.bold(` ${label}`));
9087
- console.log(` ${chalk11.green("●")} ${handle.url}`);
9088
- console.log(chalk11.dim(` Servindo: ${runDir}`));
9089
- console.log(chalk11.dim(` Proxy de iframes: ${handle.url}proxy?url=<encoded>`));
9090
- console.log(chalk11.dim(` Ctrl+C pra parar (2x pra forçar saída).
9402
+ console.log(chalk12.bold(` ${label}`));
9403
+ console.log(` ${chalk12.green("●")} ${handle.url}`);
9404
+ console.log(chalk12.dim(` Servindo: ${runDir}`));
9405
+ console.log(chalk12.dim(` Proxy de iframes: ${handle.url}proxy?url=<encoded>`));
9406
+ console.log(chalk12.dim(` Ctrl+C pra parar (2x pra forçar saída).
9091
9407
  `));
9092
9408
  if (opts.open !== false) {
9093
9409
  const { default: open2 } = await import("open");
@@ -9120,11 +9436,11 @@ async function serveRunAndBlock(runDir, opts = {}) {
9120
9436
  async function serveCommand(runId, opts) {
9121
9437
  const paths = getRunPaths(opts.output, runId);
9122
9438
  if (!existsSync11(paths.runDir)) {
9123
- console.error(chalk11.red(`✖ Run não encontrado: ${runId} (em ${opts.output})`));
9439
+ console.error(chalk12.red(`✖ Run não encontrado: ${runId} (em ${opts.output})`));
9124
9440
  return 1;
9125
9441
  }
9126
9442
  if (!existsSync11(paths.reportHtml)) {
9127
- console.error(chalk11.red(`✖ report.html não existe em ${paths.runDir}`));
9443
+ console.error(chalk12.red(`✖ report.html não existe em ${paths.runDir}`));
9128
9444
  return 1;
9129
9445
  }
9130
9446
  return serveRunAndBlock(paths.runDir, {
@@ -9184,7 +9500,7 @@ function applyPreset(opts) {
9184
9500
  async function runCommand(rawOpts) {
9185
9501
  const opts = applyPreset(rawOpts);
9186
9502
  if (rawOpts.preset) {
9187
- console.log(chalk12.dim(` preset: ${rawOpts.preset}`));
9503
+ console.log(chalk13.dim(` preset: ${rawOpts.preset}`));
9188
9504
  }
9189
9505
  const flows = opts.flows.split(",").map((s) => s.trim());
9190
9506
  const viewports = opts.viewports.split(",").map((s) => s.trim());
@@ -9194,11 +9510,11 @@ async function runCommand(rawOpts) {
9194
9510
  const ignore = loadParityIgnore();
9195
9511
  const preflight = await preflightCheck(opts.prod, opts.cand);
9196
9512
  if (!preflight.ok) {
9197
- console.error(chalk12.red(`
9513
+ console.error(chalk13.red(`
9198
9514
  ✖ pre-flight falhou:`));
9199
9515
  for (const err of preflight.errors)
9200
- console.error(chalk12.red(` - ${err}`));
9201
- console.error(chalk12.dim(`
9516
+ console.error(chalk13.red(` - ${err}`));
9517
+ console.error(chalk13.dim(`
9202
9518
  dica: verifique se as URLs estão corretas e acessíveis`));
9203
9519
  return 2;
9204
9520
  }
@@ -9209,7 +9525,7 @@ async function runCommand(rawOpts) {
9209
9525
  if (prodHomeHtml) {
9210
9526
  platform = detectPlatform({ url: opts.prod, html: prodHomeHtml });
9211
9527
  if (platform !== "custom") {
9212
- console.log(chalk12.dim(` Detected platform: ${platform}`));
9528
+ console.log(chalk13.dim(` Detected platform: ${platform}`));
9213
9529
  }
9214
9530
  }
9215
9531
  const prodHost = hostOf(opts.prod);
@@ -9251,11 +9567,11 @@ async function runCommand(rawOpts) {
9251
9567
  const paths = createRunDir(opts.output, runId);
9252
9568
  const startedAt = Date.now();
9253
9569
  const timestamp = new Date().toISOString();
9254
- console.log(chalk12.bold(`
9570
+ console.log(chalk13.bold(`
9255
9571
  parity run ${runId}`));
9256
- console.log(chalk12.dim(` prod: ${opts.prod}`));
9257
- console.log(chalk12.dim(` cand: ${opts.cand}`));
9258
- console.log(chalk12.dim(` flows: ${flows.join(", ")} · viewports: ${viewports.join(", ")} · CEP: ${rc.cep}
9572
+ console.log(chalk13.dim(` prod: ${opts.prod}`));
9573
+ console.log(chalk13.dim(` cand: ${opts.cand}`));
9574
+ console.log(chalk13.dim(` flows: ${flows.join(", ")} · viewports: ${viewports.join(", ")} · CEP: ${rc.cep}
9259
9575
  `));
9260
9576
  const spinner = ora4("Launching browser…").start();
9261
9577
  let browser = null;
@@ -9569,7 +9885,7 @@ async function runCommand(rawOpts) {
9569
9885
  if (opts.ci) {
9570
9886
  const blocking = allIssues.filter((i) => failOn.includes(i.severity));
9571
9887
  if (blocking.length > 0) {
9572
- console.log(chalk12.red(`
9888
+ console.log(chalk13.red(`
9573
9889
  ✖ ${blocking.length} issue(s) bloqueante(s) [${failOn.join(", ")}] — exit 1`));
9574
9890
  return 1;
9575
9891
  }
@@ -9708,26 +10024,26 @@ function hostOf(url) {
9708
10024
  }
9709
10025
  function printSummary4(run, htmlPath, meta) {
9710
10026
  const { verdict } = run;
9711
- const emoji = verdict.status === "pass" ? chalk12.green("✔") : verdict.status === "warn" ? chalk12.yellow("⚠") : chalk12.red("✖");
9712
- const score = verdict.status === "pass" ? chalk12.green(verdict.score) : verdict.status === "warn" ? chalk12.yellow(verdict.score) : chalk12.red(verdict.score);
10027
+ const emoji = verdict.status === "pass" ? chalk13.green("✔") : verdict.status === "warn" ? chalk13.yellow("⚠") : chalk13.red("✖");
10028
+ const score = verdict.status === "pass" ? chalk13.green(verdict.score) : verdict.status === "warn" ? chalk13.yellow(verdict.score) : chalk13.red(verdict.score);
9713
10029
  console.log("");
9714
10030
  console.log(` ${emoji} parity ${verdict.status.toUpperCase()} · score ${score}/100`);
9715
- console.log(` checks: ${chalk12.green(verdict.checksPassed)} pass · ${chalk12.red(verdict.checksFailed)} fail · ${chalk12.dim(`${verdict.checksSkipped} skipped`)}`);
9716
- console.log(` issues: ${chalk12.red(verdict.critical)} critical · ${chalk12.yellow(verdict.high)} high · ${verdict.medium} medium · ${chalk12.dim(`${verdict.low} low`)}`);
10031
+ console.log(` checks: ${chalk13.green(verdict.checksPassed)} pass · ${chalk13.red(verdict.checksFailed)} fail · ${chalk13.dim(`${verdict.checksSkipped} skipped`)}`);
10032
+ console.log(` issues: ${chalk13.red(verdict.critical)} critical · ${chalk13.yellow(verdict.high)} high · ${verdict.medium} medium · ${chalk13.dim(`${verdict.low} low`)}`);
9717
10033
  if (run.topIssues.length > 0) {
9718
10034
  console.log("");
9719
- console.log(chalk12.bold(" Top issues:"));
10035
+ console.log(chalk13.bold(" Top issues:"));
9720
10036
  for (const i of run.topIssues.slice(0, 5)) {
9721
- const sev = i.severity === "critical" ? chalk12.red(`[${i.severity}]`) : i.severity === "high" ? chalk12.yellow(`[${i.severity}]`) : chalk12.dim(`[${i.severity}]`);
10037
+ const sev = i.severity === "critical" ? chalk13.red(`[${i.severity}]`) : i.severity === "high" ? chalk13.yellow(`[${i.severity}]`) : chalk13.dim(`[${i.severity}]`);
9722
10038
  console.log(` ${sev} ${i.summary}`);
9723
10039
  }
9724
10040
  }
9725
10041
  if (meta.promotedCount > 0 || meta.deprecatedCount > 0) {
9726
10042
  console.log("");
9727
- console.log(chalk12.dim(` \uD83D\uDCDA learned-selectors [${meta.platform}]: ${chalk12.green(`+${meta.promotedCount}`)} promoted · ${chalk12.yellow(meta.deprecatedCount)} deprecated`));
10043
+ console.log(chalk13.dim(` \uD83D\uDCDA learned-selectors [${meta.platform}]: ${chalk13.green(`+${meta.promotedCount}`)} promoted · ${chalk13.yellow(meta.deprecatedCount)} deprecated`));
9728
10044
  }
9729
10045
  console.log("");
9730
- console.log(chalk12.dim(` → ${htmlPath}`));
10046
+ console.log(chalk13.dim(` → ${htmlPath}`));
9731
10047
  console.log("");
9732
10048
  }
9733
10049
 
@@ -9777,6 +10093,18 @@ program.command("prompt").argument("<runId>", "Run ID").description("Generate a
9777
10093
  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) => {
9778
10094
  process.exit(await explainCommand(runId, issueId, opts.output));
9779
10095
  });
10096
+ 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) => {
10097
+ process.exit(await cssTraceCommand({
10098
+ url: opts.url,
10099
+ prod: opts.prod,
10100
+ cand: opts.cand,
10101
+ selector: opts.selector,
10102
+ filter: opts.filter,
10103
+ viewport: opts.viewport,
10104
+ settleMs: opts.settle,
10105
+ json: opts.json
10106
+ }));
10107
+ });
9780
10108
  program.command("learned").description("Inspect the learned-selectors library").addCommand(new Command("stats").description("Print learned-selectors stats per platform").action(() => {
9781
10109
  process.exit(learnedStats());
9782
10110
  }));
package/package.json CHANGED
@@ -1,87 +1,87 @@
1
1
  {
2
- "name": "@decocms/parity",
3
- "version": "0.1.0",
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
- "type": "module",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/decocms/parity.git"
10
- },
11
- "homepage": "https://github.com/decocms/parity#readme",
12
- "bugs": {
13
- "url": "https://github.com/decocms/parity/issues"
14
- },
15
- "keywords": [
16
- "deco",
17
- "tanstack",
18
- "fresh",
19
- "migration",
20
- "e2e",
21
- "playwright",
22
- "web-vitals",
23
- "visual-regression",
24
- "visual-diff",
25
- "llm",
26
- "claude",
27
- "anthropic",
28
- "cli",
29
- "parity",
30
- "regression-testing"
31
- ],
32
- "bin": {
33
- "parity": "./dist/cli.js"
34
- },
35
- "files": [
36
- "dist",
37
- "README.md",
38
- "AGENTS.md",
39
- "CHANGELOG.md",
40
- "LICENSE"
41
- ],
42
- "scripts": {
43
- "dev": "bun run --hot src/cli.ts",
44
- "build": "bun build src/cli.ts --target=node --outfile=dist/cli.js --packages=external && bun run scripts/postbuild.ts",
45
- "check": "tsc --noEmit",
46
- "test": "vitest run",
47
- "test:watch": "vitest",
48
- "test:coverage": "vitest run --coverage",
49
- "test:e2e": "vitest run tests/e2e",
50
- "deadcode": "knip",
51
- "lint": "biome lint .",
52
- "fmt": "biome format --write .",
53
- "prepublishOnly": "bun run check && bun run build"
54
- },
55
- "engines": {
56
- "node": ">=20",
57
- "bun": ">=1.1"
58
- },
59
- "dependencies": {
60
- "@anthropic-ai/sdk": "^0.40.0",
61
- "chalk": "^5.3.0",
62
- "cheerio": "^1.0.0",
63
- "commander": "^12.1.0",
64
- "open": "^10.1.0",
65
- "ora": "^8.1.1",
66
- "pixelmatch": "^6.0.0",
67
- "playwright": "^1.49.0",
68
- "pngjs": "^7.0.0",
69
- "zod": "^3.24.1"
70
- },
71
- "devDependencies": {
72
- "@biomejs/biome": "^1.9.4",
73
- "@types/node": "^22.10.0",
74
- "@types/pixelmatch": "^5.2.6",
75
- "@types/pngjs": "^6.0.5",
76
- "@vitest/coverage-v8": "^2.1.8",
77
- "knip": "^6.13.1",
78
- "typescript": "^5.7.2",
79
- "vitest": "^2.1.8"
80
- },
81
- "publishConfig": {
82
- "access": "public"
83
- },
84
- "trustedDependencies": [
85
- "@biomejs/biome"
86
- ]
2
+ "name": "@decocms/parity",
3
+ "version": "0.2.0",
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
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/decocms/parity.git"
10
+ },
11
+ "homepage": "https://github.com/decocms/parity#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/decocms/parity/issues"
14
+ },
15
+ "keywords": [
16
+ "deco",
17
+ "tanstack",
18
+ "fresh",
19
+ "migration",
20
+ "e2e",
21
+ "playwright",
22
+ "web-vitals",
23
+ "visual-regression",
24
+ "visual-diff",
25
+ "llm",
26
+ "claude",
27
+ "anthropic",
28
+ "cli",
29
+ "parity",
30
+ "regression-testing"
31
+ ],
32
+ "bin": {
33
+ "parity": "dist/cli.js"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "README.md",
38
+ "AGENTS.md",
39
+ "CHANGELOG.md",
40
+ "LICENSE"
41
+ ],
42
+ "scripts": {
43
+ "dev": "bun run --hot src/cli.ts",
44
+ "build": "bun build src/cli.ts --target=node --outfile=dist/cli.js --packages=external && bun run scripts/postbuild.ts",
45
+ "check": "tsc --noEmit",
46
+ "test": "vitest run",
47
+ "test:watch": "vitest",
48
+ "test:coverage": "vitest run --coverage",
49
+ "test:e2e": "vitest run tests/e2e",
50
+ "deadcode": "knip",
51
+ "lint": "biome lint .",
52
+ "fmt": "biome format --write .",
53
+ "prepublishOnly": "bun run check && bun run build"
54
+ },
55
+ "engines": {
56
+ "node": ">=20",
57
+ "bun": ">=1.1"
58
+ },
59
+ "dependencies": {
60
+ "@anthropic-ai/sdk": "^0.40.0",
61
+ "chalk": "^5.3.0",
62
+ "cheerio": "^1.0.0",
63
+ "commander": "^12.1.0",
64
+ "open": "^10.1.0",
65
+ "ora": "^8.1.1",
66
+ "pixelmatch": "^6.0.0",
67
+ "playwright": "^1.49.0",
68
+ "pngjs": "^7.0.0",
69
+ "zod": "^3.24.1"
70
+ },
71
+ "devDependencies": {
72
+ "@biomejs/biome": "^1.9.4",
73
+ "@types/node": "^22.10.0",
74
+ "@types/pixelmatch": "^5.2.6",
75
+ "@types/pngjs": "^6.0.5",
76
+ "@vitest/coverage-v8": "^2.1.8",
77
+ "knip": "^6.13.1",
78
+ "typescript": "^5.7.2",
79
+ "vitest": "^2.1.8"
80
+ },
81
+ "publishConfig": {
82
+ "access": "public"
83
+ },
84
+ "trustedDependencies": [
85
+ "@biomejs/biome"
86
+ ]
87
87
  }