@decocms/parity 0.11.13 → 0.11.16
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 +36 -0
- package/dist/cli.js +180 -61
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,42 @@ 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
|
+
|
|
27
|
+
## [0.11.14](https://github.com/decocms/parity/compare/v0.11.13...v0.11.14) (2026-06-17)
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
* **Always-visible elapsed counter.** A 1-second ticker rewrites the spinner with `⏱ 04:32 · <current label>` from the moment "Launching browser…" appears until the report writes. No more "is it stuck?" minutes of silence — the user knows exactly how long the run has been going at any moment. Cleared in `finally` so it can't survive past `runCommand`.
|
|
32
|
+
* **Per-flow timing in the bottom summary.** New `flows breakdown` block lists each flow's `max` time (the parallel-wall-clock contribution) plus per-side detail:
|
|
33
|
+
```
|
|
34
|
+
flows breakdown (sides run in parallel within viewport)
|
|
35
|
+
purchase-journey max 1m32s · mobile/prod 52s · mobile/cand 58s · desk/prod 89s · desk/cand 90s
|
|
36
|
+
search max 1m48s · …
|
|
37
|
+
```
|
|
38
|
+
Driven by the existing `FlowCapture.totalDurationMs` (already populated by `finalize` — just wasn't surfaced).
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
* **Prod + cand now run in parallel within each viewport.** `parity run`'s collect loop was 100% sequential: `for viewport { for side { for flow {…} } }`. The two sides for a given viewport are already independent (separate BrowserContexts, separate HAR/trace paths), so the same `Promise.all([prod, cand])` pattern that `parity journey` has used for months now applies to `parity run` too. Extracted a `runOneSide(viewport, side)` helper, replaced the inner side loop with `Promise.all`, and deferred `promoteStepsFromFlow` (which mutates the shared `learned` object) until after `Promise.all` resolves so there's no race on selector promotion. **Expected speedup: ~50% on collect phase** (25m → ~17m total on bagaggio). Viewports still serialize for now — PR #2 will parallelize those too.
|
|
43
|
+
|
|
8
44
|
## [0.11.13](https://github.com/decocms/parity/compare/v0.11.12...v0.11.13) (2026-06-17)
|
|
9
45
|
|
|
10
46
|
### Fixed
|
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
|
|
|
@@ -54522,7 +54572,7 @@ class TimingRegistry {
|
|
|
54522
54572
|
};
|
|
54523
54573
|
}
|
|
54524
54574
|
}
|
|
54525
|
-
function formatTimingsSummary(timings) {
|
|
54575
|
+
function formatTimingsSummary(timings, flowBreakdown) {
|
|
54526
54576
|
if (timings.phases.length === 0) {
|
|
54527
54577
|
return `⏱ Run completed in ${formatMs(timings.totalMs)}`;
|
|
54528
54578
|
}
|
|
@@ -54535,9 +54585,41 @@ function formatTimingsSummary(timings) {
|
|
|
54535
54585
|
const pctOfTotal = timings.totalMs > 0 ? Math.round(p.durationMs / timings.totalMs * 100) : 0;
|
|
54536
54586
|
lines.push(` ${p.phase.padEnd(maxLabel)} ${formatMs(p.durationMs).padStart(7)} ${bar} ${pctOfTotal}%`);
|
|
54537
54587
|
}
|
|
54588
|
+
if (flowBreakdown && flowBreakdown.length > 0) {
|
|
54589
|
+
const maxFlowLabel = Math.max(...flowBreakdown.map((f) => f.flow.length));
|
|
54590
|
+
lines.push("");
|
|
54591
|
+
lines.push(" flows breakdown (sides run in parallel within viewport)");
|
|
54592
|
+
for (const f of flowBreakdown) {
|
|
54593
|
+
const sideDetail = f.sides.map((s) => `${s.viewport}/${s.side} ${formatMs(s.durationMs)}`).join(" · ");
|
|
54594
|
+
lines.push(` ${f.flow.padEnd(maxFlowLabel)} max ${formatMs(f.maxMs).padStart(6)} · ${sideDetail}`);
|
|
54595
|
+
}
|
|
54596
|
+
}
|
|
54538
54597
|
return lines.join(`
|
|
54539
54598
|
`);
|
|
54540
54599
|
}
|
|
54600
|
+
function buildFlowBreakdown(captures) {
|
|
54601
|
+
const byFlow = new Map;
|
|
54602
|
+
for (const c of captures) {
|
|
54603
|
+
const existing = byFlow.get(c.flow);
|
|
54604
|
+
const entry = {
|
|
54605
|
+
viewport: c.viewport,
|
|
54606
|
+
side: c.side,
|
|
54607
|
+
durationMs: c.totalDurationMs
|
|
54608
|
+
};
|
|
54609
|
+
if (!existing) {
|
|
54610
|
+
byFlow.set(c.flow, {
|
|
54611
|
+
flow: c.flow,
|
|
54612
|
+
maxMs: c.totalDurationMs,
|
|
54613
|
+
sides: [entry]
|
|
54614
|
+
});
|
|
54615
|
+
} else {
|
|
54616
|
+
existing.sides.push(entry);
|
|
54617
|
+
if (c.totalDurationMs > existing.maxMs)
|
|
54618
|
+
existing.maxMs = c.totalDurationMs;
|
|
54619
|
+
}
|
|
54620
|
+
}
|
|
54621
|
+
return Array.from(byFlow.values());
|
|
54622
|
+
}
|
|
54541
54623
|
function formatMs(ms) {
|
|
54542
54624
|
const totalSec = Math.floor(ms / 1000);
|
|
54543
54625
|
if (totalSec < 60)
|
|
@@ -55069,6 +55151,21 @@ async function runCommand(rawOpts) {
|
|
|
55069
55151
|
let browser = null;
|
|
55070
55152
|
const allFlowCaptures = [];
|
|
55071
55153
|
const allPageCaptures = [];
|
|
55154
|
+
let currentSpinnerLabel = "Launching browser…";
|
|
55155
|
+
const renderSpinner = () => {
|
|
55156
|
+
const elapsedSec = Math.floor((Date.now() - startedAt) / 1000);
|
|
55157
|
+
const min = Math.floor(elapsedSec / 60);
|
|
55158
|
+
const sec = elapsedSec % 60;
|
|
55159
|
+
const mmss = `${min.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}`;
|
|
55160
|
+
spinner.text = `${chalk16.dim(`⏱ ${mmss} ·`)} ${currentSpinnerLabel}`;
|
|
55161
|
+
};
|
|
55162
|
+
const setSpinnerLabel = (label) => {
|
|
55163
|
+
currentSpinnerLabel = label;
|
|
55164
|
+
renderSpinner();
|
|
55165
|
+
};
|
|
55166
|
+
setSpinnerLabel("Launching browser…");
|
|
55167
|
+
const elapsedTicker = setInterval(renderSpinner, 1000);
|
|
55168
|
+
elapsedTicker.unref?.();
|
|
55072
55169
|
const timings = new TimingRegistry;
|
|
55073
55170
|
let phaseStart = performance.now();
|
|
55074
55171
|
const stampPhase = (label) => {
|
|
@@ -55113,7 +55210,7 @@ async function runCommand(rawOpts) {
|
|
|
55113
55210
|
process.stderr.write(` report parcial em ${paths.reportHtml}
|
|
55114
55211
|
`);
|
|
55115
55212
|
process.stderr.write(`
|
|
55116
|
-
${formatTimingsSummary(partialTimings)}
|
|
55213
|
+
${formatTimingsSummary(partialTimings, buildFlowBreakdown(allFlowCaptures))}
|
|
55117
55214
|
`);
|
|
55118
55215
|
} catch (err) {
|
|
55119
55216
|
process.stderr.write(` falha escrevendo report parcial: ${err.message}
|
|
@@ -55155,26 +55252,27 @@ ${formatTimingsSummary(partialTimings)}
|
|
|
55155
55252
|
warmupSpinner.fail(`Warmup falhou — 0/${result.attempted} requests ok. Cache pode estar stale. Primeira falha: ${firstReason}`);
|
|
55156
55253
|
}
|
|
55157
55254
|
}
|
|
55158
|
-
|
|
55159
|
-
|
|
55160
|
-
|
|
55161
|
-
|
|
55162
|
-
|
|
55163
|
-
|
|
55164
|
-
|
|
55165
|
-
|
|
55166
|
-
|
|
55167
|
-
|
|
55168
|
-
|
|
55169
|
-
|
|
55170
|
-
|
|
55171
|
-
|
|
55255
|
+
const runOneSide = async (viewport, side) => {
|
|
55256
|
+
const baseUrl = side === "prod" ? opts.prod : opts.cand;
|
|
55257
|
+
const harPath = join16(paths.harDir, `${viewport}-${side}.har`);
|
|
55258
|
+
const tracePath = join16(paths.tracesDir, `${viewport}-${side}.zip`);
|
|
55259
|
+
const ctx = await newContext(browser, {
|
|
55260
|
+
viewport,
|
|
55261
|
+
harPath,
|
|
55262
|
+
tracesDir: paths.tracesDir,
|
|
55263
|
+
cohortCookieValue: "control",
|
|
55264
|
+
noCache: opts.bypassCache
|
|
55265
|
+
});
|
|
55266
|
+
await installVitalsCollector(ctx);
|
|
55267
|
+
const captures = [];
|
|
55268
|
+
const pages = [];
|
|
55269
|
+
try {
|
|
55172
55270
|
for (const flow of flows) {
|
|
55173
55271
|
const flowStart = Date.now();
|
|
55174
55272
|
let lastStepTotal = 0;
|
|
55175
55273
|
const sideTag = side === "prod" ? chalk16.cyan("prod") : chalk16.magenta("cand");
|
|
55176
55274
|
const prefix = `${chalk16.dim(`[${viewport}/`)}${sideTag}${chalk16.dim("]")}`;
|
|
55177
|
-
|
|
55275
|
+
setSpinnerLabel(`${prefix} ${flow}: starting…`);
|
|
55178
55276
|
const cap = await runFlow(flow, {
|
|
55179
55277
|
baseUrl,
|
|
55180
55278
|
side,
|
|
@@ -55190,7 +55288,7 @@ ${formatTimingsSummary(partialTimings)}
|
|
|
55190
55288
|
onStep: (event) => {
|
|
55191
55289
|
if (event.phase === "start") {
|
|
55192
55290
|
lastStepTotal = event.total;
|
|
55193
|
-
|
|
55291
|
+
setSpinnerLabel(`${prefix} ${flow} ${chalk16.dim(`${event.index}/${event.total}`)} ${event.name}…`);
|
|
55194
55292
|
}
|
|
55195
55293
|
}
|
|
55196
55294
|
});
|
|
@@ -55199,7 +55297,7 @@ ${formatTimingsSummary(partialTimings)}
|
|
|
55199
55297
|
const target = lastStepTotal || recorded;
|
|
55200
55298
|
const failed = cap.steps?.find((s) => s.status === "failed");
|
|
55201
55299
|
const lastStep = cap.steps?.[cap.steps.length - 1];
|
|
55202
|
-
const
|
|
55300
|
+
const elapsedDim = chalk16.dim(`${((Date.now() - flowStart) / 1000).toFixed(1)}s`);
|
|
55203
55301
|
const counts = target > 0 ? chalk16.dim(`${okSteps}/${target}`) : "";
|
|
55204
55302
|
let glyph;
|
|
55205
55303
|
let detail;
|
|
@@ -55213,24 +55311,42 @@ ${formatTimingsSummary(partialTimings)}
|
|
|
55213
55311
|
glyph = chalk16.green("✓");
|
|
55214
55312
|
detail = "";
|
|
55215
55313
|
}
|
|
55216
|
-
const summary = `${glyph} ${prefix} ${flow} ${counts} ${detail} ${
|
|
55314
|
+
const summary = `${glyph} ${prefix} ${flow} ${counts} ${detail} ${elapsedDim}`.replace(/\s+/g, " ").trim();
|
|
55217
55315
|
spinner.stopAndPersist({ symbol: "", text: summary });
|
|
55218
|
-
|
|
55219
|
-
|
|
55316
|
+
setSpinnerLabel(`${prefix} ${flow} done — next…`);
|
|
55317
|
+
spinner.start();
|
|
55318
|
+
captures.push(cap);
|
|
55220
55319
|
for (const p of cap.pages)
|
|
55221
|
-
|
|
55320
|
+
pages.push(p);
|
|
55321
|
+
}
|
|
55322
|
+
} finally {
|
|
55323
|
+
await stopTracing(ctx, tracePath).catch(() => {
|
|
55324
|
+
return;
|
|
55325
|
+
});
|
|
55326
|
+
await ctx.close();
|
|
55327
|
+
}
|
|
55328
|
+
return { captures, pages };
|
|
55329
|
+
};
|
|
55330
|
+
const runOneViewport = async (viewport) => {
|
|
55331
|
+
const [prodResult, candResult] = await Promise.all([
|
|
55332
|
+
runOneSide(viewport, "prod"),
|
|
55333
|
+
runOneSide(viewport, "cand")
|
|
55334
|
+
]);
|
|
55335
|
+
for (const r of [prodResult, candResult]) {
|
|
55336
|
+
for (const cap of r.captures) {
|
|
55337
|
+
allFlowCaptures.push(cap);
|
|
55222
55338
|
if (opts.learn !== false) {
|
|
55223
55339
|
const result = promoteStepsFromFlow(learned, platform, prodHost, cap);
|
|
55224
55340
|
promotedCount += result.promoted;
|
|
55225
55341
|
deprecatedCount += result.deprecated;
|
|
55226
55342
|
}
|
|
55227
55343
|
}
|
|
55228
|
-
|
|
55229
|
-
|
|
55230
|
-
});
|
|
55231
|
-
await ctx.close();
|
|
55344
|
+
for (const p of r.pages)
|
|
55345
|
+
allPageCaptures.push(p);
|
|
55232
55346
|
}
|
|
55233
|
-
}
|
|
55347
|
+
};
|
|
55348
|
+
const maxViewportConcurrency = Math.max(1, opts.maxViewportConcurrency ?? 2);
|
|
55349
|
+
await runWithConcurrency2(viewports, maxViewportConcurrency, runOneViewport);
|
|
55234
55350
|
stampPhase("collect");
|
|
55235
55351
|
spinner.succeed("Coleta concluída");
|
|
55236
55352
|
if (isLlmAvailable()) {
|
|
@@ -55449,7 +55565,8 @@ ${formatTimingsSummary(partialTimings)}
|
|
|
55449
55565
|
}
|
|
55450
55566
|
}
|
|
55451
55567
|
currentPhase = "checks";
|
|
55452
|
-
|
|
55568
|
+
setSpinnerLabel("Rodando checks…");
|
|
55569
|
+
spinner.start();
|
|
55453
55570
|
const checksHb = attachSpinnerHeartbeat(spinner, { baseText: "Rodando checks…" });
|
|
55454
55571
|
const cacheDir = join16(opts.output, "cache");
|
|
55455
55572
|
const checkCtx = {
|
|
@@ -55474,7 +55591,8 @@ ${formatTimingsSummary(partialTimings)}
|
|
|
55474
55591
|
}
|
|
55475
55592
|
const allIssues = checks.flatMap((c) => c.issues);
|
|
55476
55593
|
currentPhase = "llm-aggregate";
|
|
55477
|
-
|
|
55594
|
+
setSpinnerLabel(isLlmAvailable() ? `Agregando issues via LLM (${providerLabel()}, timeout=${llmTimeoutSec}s)…` : "Agregando issues (modo offline — sem LLM keys)…");
|
|
55595
|
+
spinner.start();
|
|
55478
55596
|
const llmHb = attachSpinnerHeartbeat(spinner, {
|
|
55479
55597
|
baseText: isLlmAvailable() ? "Agregando issues via LLM (Sonnet 4.6)…" : "Agregando issues (modo offline)…"
|
|
55480
55598
|
});
|
|
@@ -55556,7 +55674,7 @@ ${formatTimingsSummary(partialTimings)}
|
|
|
55556
55674
|
});
|
|
55557
55675
|
printSummary5(run, paths.reportHtml, { promotedCount, deprecatedCount, platform });
|
|
55558
55676
|
console.log(`
|
|
55559
|
-
${formatTimingsSummary(runTimings)}`);
|
|
55677
|
+
${formatTimingsSummary(runTimings, buildFlowBreakdown(allFlowCaptures))}`);
|
|
55560
55678
|
if (opts.ci) {
|
|
55561
55679
|
const blocking = allIssues.filter((i) => failOn.includes(i.severity));
|
|
55562
55680
|
if (blocking.length > 0) {
|
|
@@ -55586,6 +55704,7 @@ ${formatTimingsSummary(runTimings)}`);
|
|
|
55586
55704
|
});
|
|
55587
55705
|
return 2;
|
|
55588
55706
|
} finally {
|
|
55707
|
+
clearInterval(elapsedTicker);
|
|
55589
55708
|
clearTimeout(globalTimeoutTimer);
|
|
55590
55709
|
process.removeListener("SIGINT", onSignal);
|
|
55591
55710
|
process.removeListener("SIGTERM", onSignal);
|
|
@@ -56416,7 +56535,7 @@ program.command("run").description([
|
|
|
56416
56535
|
" auto-selectors=ON (if LLM), learn=ON, cache=ON, visual-diff=ON,",
|
|
56417
56536
|
" warmup=OFF, bypass-cache=OFF, ci=OFF."
|
|
56418
56537
|
].join(`
|
|
56419
|
-
`)).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) => {
|
|
56538
|
+
`)).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) => {
|
|
56420
56539
|
if (opts.flow) {
|
|
56421
56540
|
opts.flows = opts.flow;
|
|
56422
56541
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/parity",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.16",
|
|
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",
|