@decocms/parity 0.11.14 → 0.11.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/cli.js +120 -40
- package/package.json +13 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ 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.11.16](https://github.com/decocms/parity/compare/v0.11.15...v0.11.16) (2026-06-17)
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
* **All checks now run in parallel.** `runAllChecks` previously walked the ~27 checks sequentially with `for (const check of ALL_CHECKS) await check(ctx)`, taking ~4m26s on bagaggio. Almost every check is a pure CPU aggregation over already-captured `PageCapture[]` data (string diffs, regex matches, console-entry filtering); the 3 network-bound ones (`seo-deep-audit`, `footer-links-health`, `plp-pagination`) are I/O-bound and parallelize cleanly too. Now uses `Promise.all` — the whole checks phase is dominated by the slowest single check. **Expected speedup: ~4m26s → ~30-60s** on bagaggio.
|
|
13
|
+
* **Console errors now dedupe across pages.** Previously the same error message ("A chave utilizada não corresponde ao domínio…") that appeared on 4 pages produced 4 separate top-level issues, crowding the report. Now `consoleErrorsBaseline` groups by normalized error key across all page pairs and emits ONE issue per unique error with the affected-pages list inline:
|
|
14
|
+
```
|
|
15
|
+
[high] [generic] novo erro de console em 4 páginas (/ · /s · /search · /:::desktop): A chave utilizada não corresponde…
|
|
16
|
+
```
|
|
17
|
+
Direct response to user feedback ("console log não deveria ter um teste pra ele … fazer o dedup e descrever quais páginas tiveram aquele error"). The schema-level Issue shape is unchanged (page = first affected, details = full list).
|
|
18
|
+
|
|
19
|
+
## [0.11.15](https://github.com/decocms/parity/compare/v0.11.14...v0.11.15) (2026-06-17)
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
* **Viewports now run in parallel during collect.** Building on 0.11.14's parallel-sides change, the outer `for (const viewport of viewports)` loop is now wrapped in `runWithConcurrency` so the default `mobile,desktop` set runs fully concurrent — 4 BrowserContexts simultaneously (mobile/prod + mobile/cand + desktop/prod + desktop/cand). **Expected collect speedup: another ~40% on top of 0.11.14** (~9min → ~5–6min on bagaggio).
|
|
24
|
+
* **New `--max-viewport-concurrency <n>` flag (default 2).** Lets memory-constrained machines fall back to serial-viewport behavior, and caps concurrency for runs that add a 3rd viewport (e.g. `--viewports mobile,tablet,desktop`).
|
|
25
|
+
* **LLM call concurrency cap.** With 4 sides running, recovery-budget × side count could push 8–12 simultaneous LLM calls — past Anthropic tier-1's 4 RPS. Added a process-global semaphore in `src/llm/client.ts` (3 concurrent slots, queue thereafter) so calls never blow up with 429s — they queue. Default of 3 leaves headroom for the post-collect aggregate call.
|
|
26
|
+
|
|
8
27
|
## [0.11.14](https://github.com/decocms/parity/compare/v0.11.13...v0.11.14) (2026-06-17)
|
|
9
28
|
|
|
10
29
|
### Added
|
package/dist/cli.js
CHANGED
|
@@ -23409,19 +23409,42 @@ function makeTimeoutSignal(ms) {
|
|
|
23409
23409
|
const t = setTimeout(() => controller.abort(new Error(`LLM call timed out after ${ms}ms`)), ms);
|
|
23410
23410
|
return { signal: controller.signal, clear: () => clearTimeout(t) };
|
|
23411
23411
|
}
|
|
23412
|
+
function acquireLlmSlot() {
|
|
23413
|
+
if (activeLlmCalls < MAX_CONCURRENT_LLM_CALLS) {
|
|
23414
|
+
activeLlmCalls++;
|
|
23415
|
+
return Promise.resolve();
|
|
23416
|
+
}
|
|
23417
|
+
return new Promise((resolve) => {
|
|
23418
|
+
llmQueue.push(() => {
|
|
23419
|
+
activeLlmCalls++;
|
|
23420
|
+
resolve();
|
|
23421
|
+
});
|
|
23422
|
+
});
|
|
23423
|
+
}
|
|
23424
|
+
function releaseLlmSlot() {
|
|
23425
|
+
activeLlmCalls--;
|
|
23426
|
+
const next2 = llmQueue.shift();
|
|
23427
|
+
if (next2)
|
|
23428
|
+
next2();
|
|
23429
|
+
}
|
|
23412
23430
|
async function callTool(params) {
|
|
23413
23431
|
const provider = getProvider();
|
|
23414
23432
|
if (!provider)
|
|
23415
23433
|
return null;
|
|
23416
23434
|
const model = resolveModel(params.feature, provider);
|
|
23417
23435
|
const timeoutMs = params.timeoutMs ?? defaultTimeout(params);
|
|
23418
|
-
|
|
23419
|
-
|
|
23420
|
-
|
|
23421
|
-
|
|
23422
|
-
|
|
23423
|
-
|
|
23424
|
-
|
|
23436
|
+
await acquireLlmSlot();
|
|
23437
|
+
try {
|
|
23438
|
+
if (provider === "anthropic")
|
|
23439
|
+
return await callAnthropicTool(params, model, timeoutMs);
|
|
23440
|
+
if (provider === "openrouter")
|
|
23441
|
+
return await callOpenRouterTool(params, model, timeoutMs);
|
|
23442
|
+
if (provider === "claude-agent-sdk")
|
|
23443
|
+
return await callToolSdk({ ...params, model, timeoutMs });
|
|
23444
|
+
return null;
|
|
23445
|
+
} finally {
|
|
23446
|
+
releaseLlmSlot();
|
|
23447
|
+
}
|
|
23425
23448
|
}
|
|
23426
23449
|
async function callAnthropicTool(params, model, timeoutMs) {
|
|
23427
23450
|
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY ?? "" });
|
|
@@ -23629,13 +23652,18 @@ async function callMessage(params) {
|
|
|
23629
23652
|
return null;
|
|
23630
23653
|
const model = resolveModel(params.feature, provider);
|
|
23631
23654
|
const timeoutMs = params.timeoutMs ?? 60000;
|
|
23632
|
-
|
|
23633
|
-
|
|
23634
|
-
|
|
23635
|
-
|
|
23636
|
-
|
|
23637
|
-
|
|
23638
|
-
|
|
23655
|
+
await acquireLlmSlot();
|
|
23656
|
+
try {
|
|
23657
|
+
if (provider === "anthropic")
|
|
23658
|
+
return await callAnthropicMessage(params, model, timeoutMs);
|
|
23659
|
+
if (provider === "openrouter")
|
|
23660
|
+
return await callOpenRouterMessage(params, model, timeoutMs);
|
|
23661
|
+
if (provider === "claude-agent-sdk")
|
|
23662
|
+
return await callMessageSdk({ ...params, model, timeoutMs });
|
|
23663
|
+
return null;
|
|
23664
|
+
} finally {
|
|
23665
|
+
releaseLlmSlot();
|
|
23666
|
+
}
|
|
23639
23667
|
}
|
|
23640
23668
|
async function callAnthropicMessage(params, model, timeoutMs) {
|
|
23641
23669
|
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY ?? "" });
|
|
@@ -23695,12 +23723,13 @@ async function callOpenRouterMessage(params, model, timeoutMs) {
|
|
|
23695
23723
|
clear();
|
|
23696
23724
|
}
|
|
23697
23725
|
}
|
|
23698
|
-
var LLM_MODEL_ANTHROPIC, LLM_MODEL_OPENROUTER, forcedProvider = null, llmDisabled = false, llmLanguage = "en";
|
|
23726
|
+
var LLM_MODEL_ANTHROPIC, LLM_MODEL_OPENROUTER, forcedProvider = null, llmDisabled = false, llmLanguage = "en", MAX_CONCURRENT_LLM_CALLS = 3, activeLlmCalls = 0, llmQueue;
|
|
23699
23727
|
var init_client = __esm(() => {
|
|
23700
23728
|
init_models();
|
|
23701
23729
|
init_claude_agent_sdk();
|
|
23702
23730
|
LLM_MODEL_ANTHROPIC = PROVIDER_MODELS.anthropic.sonnet;
|
|
23703
23731
|
LLM_MODEL_OPENROUTER = PROVIDER_MODELS.openrouter.sonnet;
|
|
23732
|
+
llmQueue = [];
|
|
23704
23733
|
});
|
|
23705
23734
|
|
|
23706
23735
|
// src/cli.ts
|
|
@@ -43862,7 +43891,7 @@ function detailFor(prod, cand, viewport) {
|
|
|
43862
43891
|
function consoleErrorsBaseline(ctx) {
|
|
43863
43892
|
const start = Date.now();
|
|
43864
43893
|
const { pairs } = pairCaptures(ctx.prodPages, ctx.candPages);
|
|
43865
|
-
const
|
|
43894
|
+
const byKey = new Map;
|
|
43866
43895
|
let totalNew = 0;
|
|
43867
43896
|
for (const pair of pairs) {
|
|
43868
43897
|
const diff = diffConsole(pair.prod.console, pair.cand.console, {
|
|
@@ -43871,24 +43900,45 @@ function consoleErrorsBaseline(ctx) {
|
|
|
43871
43900
|
});
|
|
43872
43901
|
totalNew += diff.newInCand.length;
|
|
43873
43902
|
for (const e of diff.newInCand) {
|
|
43874
|
-
|
|
43875
|
-
|
|
43876
|
-
|
|
43877
|
-
|
|
43878
|
-
|
|
43879
|
-
|
|
43880
|
-
|
|
43881
|
-
|
|
43882
|
-
|
|
43883
|
-
|
|
43903
|
+
const existing = byKey.get(e.key);
|
|
43904
|
+
if (!existing) {
|
|
43905
|
+
byKey.set(e.key, {
|
|
43906
|
+
cls: e.cls,
|
|
43907
|
+
sampleText: e.entry.text,
|
|
43908
|
+
pages: [pair.key],
|
|
43909
|
+
sampleEvidence: [
|
|
43910
|
+
{ kind: "screenshot", path: pair.cand.screenshotPath, label: "cand" }
|
|
43911
|
+
]
|
|
43912
|
+
});
|
|
43913
|
+
} else if (!existing.pages.includes(pair.key)) {
|
|
43914
|
+
existing.pages.push(pair.key);
|
|
43915
|
+
}
|
|
43884
43916
|
}
|
|
43885
43917
|
}
|
|
43918
|
+
const issues = [];
|
|
43919
|
+
for (const [key, agg] of byKey) {
|
|
43920
|
+
const pageList = agg.pages.length <= 5 ? agg.pages.join(" · ") : `${agg.pages.slice(0, 5).join(" · ")} +${agg.pages.length - 5} more`;
|
|
43921
|
+
const pageSuffix = agg.pages.length === 1 ? `em ${agg.pages[0]}` : `em ${agg.pages.length} páginas (${pageList})`;
|
|
43922
|
+
const severity = agg.cls === "hydration" ? "critical" : "high";
|
|
43923
|
+
issues.push({
|
|
43924
|
+
id: `console:${hash(key)}`,
|
|
43925
|
+
severity,
|
|
43926
|
+
category: "console",
|
|
43927
|
+
page: agg.pages[0],
|
|
43928
|
+
check: "console-errors-baseline",
|
|
43929
|
+
summary: `[${agg.cls}] novo erro de console ${pageSuffix}: ${truncate4(agg.sampleText, 160)}`,
|
|
43930
|
+
details: `${agg.sampleText}
|
|
43931
|
+
|
|
43932
|
+
Observed on: ${agg.pages.join(", ")}`,
|
|
43933
|
+
evidence: agg.sampleEvidence
|
|
43934
|
+
});
|
|
43935
|
+
}
|
|
43886
43936
|
return {
|
|
43887
43937
|
name: "console-errors-baseline",
|
|
43888
43938
|
status: issues.length > 0 ? "fail" : "pass",
|
|
43889
43939
|
severity: "critical",
|
|
43890
43940
|
durationMs: Date.now() - start,
|
|
43891
|
-
summary: `${totalNew} novo(s) erro(s) de console em cand não presentes em prod`,
|
|
43941
|
+
summary: issues.length > 0 ? `${issues.length} erro(s) único(s) novo(s) em cand (${totalNew} ocorrência(s) somando todas as páginas)` : `${totalNew} novo(s) erro(s) de console em cand não presentes em prod`,
|
|
43892
43942
|
issues
|
|
43893
43943
|
};
|
|
43894
43944
|
}
|
|
@@ -47155,7 +47205,7 @@ function getCheckByName(name) {
|
|
|
47155
47205
|
}
|
|
47156
47206
|
async function runAllChecks(ctx, outResults) {
|
|
47157
47207
|
const results = outResults ?? [];
|
|
47158
|
-
|
|
47208
|
+
await Promise.all(ALL_CHECKS.map(async (check) => {
|
|
47159
47209
|
const start = Date.now();
|
|
47160
47210
|
try {
|
|
47161
47211
|
const r = await check(ctx);
|
|
@@ -47170,7 +47220,7 @@ async function runAllChecks(ctx, outResults) {
|
|
|
47170
47220
|
issues: []
|
|
47171
47221
|
});
|
|
47172
47222
|
}
|
|
47173
|
-
}
|
|
47223
|
+
}));
|
|
47174
47224
|
return results;
|
|
47175
47225
|
}
|
|
47176
47226
|
|
|
@@ -55277,7 +55327,7 @@ ${formatTimingsSummary(partialTimings, buildFlowBreakdown(allFlowCaptures))}
|
|
|
55277
55327
|
}
|
|
55278
55328
|
return { captures, pages };
|
|
55279
55329
|
};
|
|
55280
|
-
|
|
55330
|
+
const runOneViewport = async (viewport) => {
|
|
55281
55331
|
const [prodResult, candResult] = await Promise.all([
|
|
55282
55332
|
runOneSide(viewport, "prod"),
|
|
55283
55333
|
runOneSide(viewport, "cand")
|
|
@@ -55294,7 +55344,9 @@ ${formatTimingsSummary(partialTimings, buildFlowBreakdown(allFlowCaptures))}
|
|
|
55294
55344
|
for (const p of r.pages)
|
|
55295
55345
|
allPageCaptures.push(p);
|
|
55296
55346
|
}
|
|
55297
|
-
}
|
|
55347
|
+
};
|
|
55348
|
+
const maxViewportConcurrency = Math.max(1, opts.maxViewportConcurrency ?? 2);
|
|
55349
|
+
await runWithConcurrency2(viewports, maxViewportConcurrency, runOneViewport);
|
|
55298
55350
|
stampPhase("collect");
|
|
55299
55351
|
spinner.succeed("Coleta concluída");
|
|
55300
55352
|
if (isLlmAvailable()) {
|
|
@@ -55744,13 +55796,29 @@ async function preflightCheck(prodUrl, candUrl, viewport) {
|
|
|
55744
55796
|
const spinner = ora5("Pre-flight: verificando URLs…").start();
|
|
55745
55797
|
const errors2 = [];
|
|
55746
55798
|
const ua = userAgentFor(viewport);
|
|
55747
|
-
|
|
55799
|
+
const browser = await launchBrowser({ headless: true }).catch(() => null);
|
|
55800
|
+
async function probeWithBrowser(label, url) {
|
|
55801
|
+
const ctx = await browser.newContext({
|
|
55802
|
+
userAgent: ua,
|
|
55803
|
+
ignoreHTTPSErrors: true
|
|
55804
|
+
});
|
|
55805
|
+
const page = await ctx.newPage();
|
|
55748
55806
|
try {
|
|
55749
|
-
|
|
55750
|
-
|
|
55751
|
-
|
|
55752
|
-
|
|
55807
|
+
const res = await page.goto(url, { waitUntil: "commit", timeout: 30000 });
|
|
55808
|
+
const status = res?.status() ?? 0;
|
|
55809
|
+
if (status >= 500)
|
|
55810
|
+
errors2.push(`${label}: HTTP ${status} (${url})`);
|
|
55811
|
+
else if (status >= 400)
|
|
55812
|
+
errors2.push(`${label}: HTTP ${status} (${url})`);
|
|
55813
|
+
} catch (err) {
|
|
55814
|
+
const e = err;
|
|
55815
|
+
errors2.push(`${label}: ${e.message.split(`
|
|
55816
|
+
`)[0]} (${url})`);
|
|
55817
|
+
} finally {
|
|
55818
|
+
await ctx.close().catch(() => {});
|
|
55753
55819
|
}
|
|
55820
|
+
}
|
|
55821
|
+
async function probeWithFetch(label, url) {
|
|
55754
55822
|
const PREFLIGHT_TIMEOUT_MS = 30000;
|
|
55755
55823
|
const controller = new AbortController;
|
|
55756
55824
|
const t = setTimeout(() => controller.abort(), PREFLIGHT_TIMEOUT_MS);
|
|
@@ -55759,9 +55827,7 @@ async function preflightCheck(prodUrl, candUrl, viewport) {
|
|
|
55759
55827
|
method: "GET",
|
|
55760
55828
|
redirect: "follow",
|
|
55761
55829
|
signal: controller.signal,
|
|
55762
|
-
headers: {
|
|
55763
|
-
"User-Agent": ua
|
|
55764
|
-
}
|
|
55830
|
+
headers: { "User-Agent": ua }
|
|
55765
55831
|
});
|
|
55766
55832
|
if (res.status >= 500)
|
|
55767
55833
|
errors2.push(`${label}: HTTP ${res.status} ${res.statusText} (${url})`);
|
|
@@ -55775,7 +55841,21 @@ async function preflightCheck(prodUrl, candUrl, viewport) {
|
|
|
55775
55841
|
clearTimeout(t);
|
|
55776
55842
|
}
|
|
55777
55843
|
}
|
|
55844
|
+
async function probe(label, url) {
|
|
55845
|
+
try {
|
|
55846
|
+
new URL(url);
|
|
55847
|
+
} catch {
|
|
55848
|
+
errors2.push(`${label}: URL inválida (${url})`);
|
|
55849
|
+
return;
|
|
55850
|
+
}
|
|
55851
|
+
if (browser) {
|
|
55852
|
+
await probeWithBrowser(label, url);
|
|
55853
|
+
} else {
|
|
55854
|
+
await probeWithFetch(label, url);
|
|
55855
|
+
}
|
|
55856
|
+
}
|
|
55778
55857
|
await Promise.all([probe("prod", prodUrl), probe("cand", candUrl)]);
|
|
55858
|
+
await browser?.close().catch(() => {});
|
|
55779
55859
|
if (errors2.length > 0) {
|
|
55780
55860
|
spinner.fail("Pre-flight falhou");
|
|
55781
55861
|
return { ok: false, errors: errors2 };
|
|
@@ -56483,7 +56563,7 @@ program.command("run").description([
|
|
|
56483
56563
|
" auto-selectors=ON (if LLM), learn=ON, cache=ON, visual-diff=ON,",
|
|
56484
56564
|
" warmup=OFF, bypass-cache=OFF, ci=OFF."
|
|
56485
56565
|
].join(`
|
|
56486
|
-
`)).requiredOption("--prod <url>", "Production URL (source of truth, e.g. Fresh site)").requiredOption("--cand <url>", "Candidate URL (migrated site, e.g. TanStack)").option("--preset <name>", "Bundle of defaults: smoke (fast, ~30s, no LLM) | full (deep audit) | ci (CI-tuned)").option("--flows <list>", "Comma-separated flows: homepage,plp,pdp,purchase-journey", "purchase-journey").option("--viewports <list>", "Comma-separated viewports: mobile,desktop", "mobile,desktop").option("--cep <cep>", "CEP for shipping calculation", "01310-100").option("--runs <n>", "Repeat each measurement N times (median)", "1").option("--baseline <name>", "Compare against a saved baseline").option("--output <dir>", "Output directory", "./parity-output").option("--ci", "CI mode: stricter exit codes", false).option("--fail-on <severities>", "Comma-separated severities that cause exit 1", "critical").option("--open", "Open the HTML report after the run completes", false).option("--no-auto-selectors", "Disable LLM-based selector discovery (uses defaults instead)").option("--refresh-selectors", "Bypass selector cache and re-run discovery", false).option("--no-learn", "Don't write to learned-selectors.json (read-only mode)").option("--vitals-pages <n>", "Extra pages from sitemap to crawl for Vitals coverage (default 10)", (v) => Number(v), 10).option("--visual-pages <n>", "Pages to compare visually via LLM (home + sampled PLPs/PDPs from sitemap, default 5)", (v) => Number(v), 5).option("--pages <list>", 'Comma-separated paths to compare visually (overrides sitemap discovery). E.g. "/,/account,/p/some-product". Use this when you want deterministic coverage instead of sampled.').option("--pages-file <path>", "Read paths to compare visually from a text file (one path per line). Lines starting with # are ignored. Overrides --pages when both are present.").option("--no-visual-diff", "Skip the visual diff capture pass entirely").option("--no-cache", "Disable the cross-run visual-diff cache (forces a fresh LLM judgment on every page).").option("--clear-cache", "Wipe the visual-diff verdict cache (parity-output/cache/verdicts.json) before the run.", false).option("--bypass-cache", "Bypass CDN/edge caches: append a cache-busting query param and send Cache-Control: no-cache on every request. Use right after a deploy to avoid false failures from stale CF edge content.", false).option("--warmup", "Before measurement, hit each target URL once (per viewport) with a cache-buster so the Worker serves a fresh response. Recommended after deploys.", false).option("--accept-prod-quirks", "Demote prod-side cart-empty journey failures (VTEX session quirk) from failed to skipped. The cart-reveal-mode-divergence check still emits critical if prod/cand markup intents differ, so this flag never masks a real regression. See issue #12.", false).option("--llm-timeout <seconds>", "Hard timeout for the LLM aggregation call (seconds). The run completes in offline mode if the LLM hangs past this. Default: 60.", (v) => Number(v), 60).option("--timeout <minutes>", "Hard wall-clock cap for the whole run (minutes). On expiry, parity writes a partial report and exits 130. Default: 30. Issue #56.", (v) => Number(v), 30).option("--flow <name>", "Alias for --flows when running a single flow (e.g. --flow cart). Issue #53.").option("--json <path|->", "Emit JSON-Lines (one line per check) to the given file path, or '-' for stdout. Schema versioned via leading metadata line. Issue #53.").option("--pt", "Tell the LLM to respond in Brazilian Portuguese. Only affects LLM-generated content (issues, explain, prompts) — the static HTML report stays in English. Issue #67.").option("--llm <provider>", "Force LLM provider: anthropic | openrouter | claude-code | none. Default: auto-detect (anthropic key → openrouter key → local claude CLI → none). Issue #66.").option("--llm-model <overrides>", "Per-feature model override, e.g. visual-diff=claude-opus-4-7,explain=claude-opus-4-7. Features: selector-discovery, step-recovery, search-terms, plp-matching, pdp-matching, section-understanding, visual-diff, issue-aggregation, explain. Issue #66.").option("--llm-tier-default <tier>", "Override the default tier (haiku | sonnet | opus) for every feature that doesn't have a per-feature override. Issue #66.").option("--llm-model-default <model>", "Force every LLM call to use this exact model ID, ignoring per-feature defaults. Issue #66.").option("--no-interactive", "Disable the interactive selector prompt that auto-fires in a TTY without an LLM provider. Use in scripts and CI where stdin is technically a TTY but you don't want to pause. Issue #72.").action(async (opts) => {
|
|
56566
|
+
`)).requiredOption("--prod <url>", "Production URL (source of truth, e.g. Fresh site)").requiredOption("--cand <url>", "Candidate URL (migrated site, e.g. TanStack)").option("--preset <name>", "Bundle of defaults: smoke (fast, ~30s, no LLM) | full (deep audit) | ci (CI-tuned)").option("--flows <list>", "Comma-separated flows: homepage,plp,pdp,purchase-journey", "purchase-journey").option("--viewports <list>", "Comma-separated viewports: mobile,desktop", "mobile,desktop").option("--cep <cep>", "CEP for shipping calculation", "01310-100").option("--runs <n>", "Repeat each measurement N times (median)", "1").option("--baseline <name>", "Compare against a saved baseline").option("--output <dir>", "Output directory", "./parity-output").option("--ci", "CI mode: stricter exit codes", false).option("--fail-on <severities>", "Comma-separated severities that cause exit 1", "critical").option("--open", "Open the HTML report after the run completes", false).option("--no-auto-selectors", "Disable LLM-based selector discovery (uses defaults instead)").option("--refresh-selectors", "Bypass selector cache and re-run discovery", false).option("--no-learn", "Don't write to learned-selectors.json (read-only mode)").option("--vitals-pages <n>", "Extra pages from sitemap to crawl for Vitals coverage (default 10)", (v) => Number(v), 10).option("--max-viewport-concurrency <n>", "How many viewports run concurrently during collect (default 2: mobile+desktop together). Set to 1 if you hit OOM with many viewports.", (v) => Number(v), 2).option("--visual-pages <n>", "Pages to compare visually via LLM (home + sampled PLPs/PDPs from sitemap, default 5)", (v) => Number(v), 5).option("--pages <list>", 'Comma-separated paths to compare visually (overrides sitemap discovery). E.g. "/,/account,/p/some-product". Use this when you want deterministic coverage instead of sampled.').option("--pages-file <path>", "Read paths to compare visually from a text file (one path per line). Lines starting with # are ignored. Overrides --pages when both are present.").option("--no-visual-diff", "Skip the visual diff capture pass entirely").option("--no-cache", "Disable the cross-run visual-diff cache (forces a fresh LLM judgment on every page).").option("--clear-cache", "Wipe the visual-diff verdict cache (parity-output/cache/verdicts.json) before the run.", false).option("--bypass-cache", "Bypass CDN/edge caches: append a cache-busting query param and send Cache-Control: no-cache on every request. Use right after a deploy to avoid false failures from stale CF edge content.", false).option("--warmup", "Before measurement, hit each target URL once (per viewport) with a cache-buster so the Worker serves a fresh response. Recommended after deploys.", false).option("--accept-prod-quirks", "Demote prod-side cart-empty journey failures (VTEX session quirk) from failed to skipped. The cart-reveal-mode-divergence check still emits critical if prod/cand markup intents differ, so this flag never masks a real regression. See issue #12.", false).option("--llm-timeout <seconds>", "Hard timeout for the LLM aggregation call (seconds). The run completes in offline mode if the LLM hangs past this. Default: 60.", (v) => Number(v), 60).option("--timeout <minutes>", "Hard wall-clock cap for the whole run (minutes). On expiry, parity writes a partial report and exits 130. Default: 30. Issue #56.", (v) => Number(v), 30).option("--flow <name>", "Alias for --flows when running a single flow (e.g. --flow cart). Issue #53.").option("--json <path|->", "Emit JSON-Lines (one line per check) to the given file path, or '-' for stdout. Schema versioned via leading metadata line. Issue #53.").option("--pt", "Tell the LLM to respond in Brazilian Portuguese. Only affects LLM-generated content (issues, explain, prompts) — the static HTML report stays in English. Issue #67.").option("--llm <provider>", "Force LLM provider: anthropic | openrouter | claude-code | none. Default: auto-detect (anthropic key → openrouter key → local claude CLI → none). Issue #66.").option("--llm-model <overrides>", "Per-feature model override, e.g. visual-diff=claude-opus-4-7,explain=claude-opus-4-7. Features: selector-discovery, step-recovery, search-terms, plp-matching, pdp-matching, section-understanding, visual-diff, issue-aggregation, explain. Issue #66.").option("--llm-tier-default <tier>", "Override the default tier (haiku | sonnet | opus) for every feature that doesn't have a per-feature override. Issue #66.").option("--llm-model-default <model>", "Force every LLM call to use this exact model ID, ignoring per-feature defaults. Issue #66.").option("--no-interactive", "Disable the interactive selector prompt that auto-fires in a TTY without an LLM provider. Use in scripts and CI where stdin is technically a TTY but you don't want to pause. Issue #72.").action(async (opts) => {
|
|
56487
56567
|
if (opts.flow) {
|
|
56488
56568
|
opts.flows = opts.flow;
|
|
56489
56569
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/parity",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.17",
|
|
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",
|
|
@@ -32,7 +32,14 @@
|
|
|
32
32
|
"bin": {
|
|
33
33
|
"parity": "dist/cli.js"
|
|
34
34
|
},
|
|
35
|
-
"files": [
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"scripts/postinstall.cjs",
|
|
38
|
+
"README.md",
|
|
39
|
+
"AGENTS.md",
|
|
40
|
+
"CHANGELOG.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
36
43
|
"scripts": {
|
|
37
44
|
"dev": "bun run --hot src/cli.ts",
|
|
38
45
|
"build": "bun build src/cli.ts --target=node --outfile=dist/cli.js --external @anthropic-ai/claude-agent-sdk --external @anthropic-ai/sdk --external chalk --external commander --external diff --external open --external ora --external pixelmatch --external playwright --external pngjs --external prettier --external zod && bun run scripts/postbuild.ts",
|
|
@@ -83,5 +90,7 @@
|
|
|
83
90
|
"publishConfig": {
|
|
84
91
|
"access": "public"
|
|
85
92
|
},
|
|
86
|
-
"trustedDependencies": [
|
|
87
|
-
|
|
93
|
+
"trustedDependencies": [
|
|
94
|
+
"@biomejs/biome"
|
|
95
|
+
]
|
|
96
|
+
}
|