@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.
Files changed (3) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/cli.js +120 -40
  3. 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
- if (provider === "anthropic")
23419
- return callAnthropicTool(params, model, timeoutMs);
23420
- if (provider === "openrouter")
23421
- return callOpenRouterTool(params, model, timeoutMs);
23422
- if (provider === "claude-agent-sdk")
23423
- return callToolSdk({ ...params, model, timeoutMs });
23424
- return null;
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
- if (provider === "anthropic")
23633
- return callAnthropicMessage(params, model, timeoutMs);
23634
- if (provider === "openrouter")
23635
- return callOpenRouterMessage(params, model, timeoutMs);
23636
- if (provider === "claude-agent-sdk")
23637
- return callMessageSdk({ ...params, model, timeoutMs });
23638
- return null;
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 issues = [];
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
- issues.push({
43875
- id: `console:${pair.key}:${hash(e.key)}`,
43876
- severity: e.cls === "hydration" ? "critical" : "high",
43877
- category: "console",
43878
- page: pair.key,
43879
- check: "console-errors-baseline",
43880
- summary: `[${e.cls}] novo erro de console em ${pair.key}: ${truncate4(e.entry.text, 160)}`,
43881
- details: e.entry.text,
43882
- evidence: [{ kind: "screenshot", path: pair.cand.screenshotPath, label: "cand" }]
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
- for (const check of ALL_CHECKS) {
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
- for (const viewport of viewports) {
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
- async function probe(label, url) {
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
- new URL(url);
55750
- } catch {
55751
- errors2.push(`${label}: URL inválida (${url})`);
55752
- return;
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.14",
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": ["dist", "scripts/postinstall.cjs", "README.md", "AGENTS.md", "CHANGELOG.md", "LICENSE"],
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": ["@biomejs/biome"]
87
- }
93
+ "trustedDependencies": [
94
+ "@biomejs/biome"
95
+ ]
96
+ }