@decocms/parity 0.11.13 → 0.11.14
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 +17 -0
- package/dist/cli.js +97 -30
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ 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.14](https://github.com/decocms/parity/compare/v0.11.13...v0.11.14) (2026-06-17)
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* **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`.
|
|
13
|
+
* **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:
|
|
14
|
+
```
|
|
15
|
+
flows breakdown (sides run in parallel within viewport)
|
|
16
|
+
purchase-journey max 1m32s · mobile/prod 52s · mobile/cand 58s · desk/prod 89s · desk/cand 90s
|
|
17
|
+
search max 1m48s · …
|
|
18
|
+
```
|
|
19
|
+
Driven by the existing `FlowCapture.totalDurationMs` (already populated by `finalize` — just wasn't surfaced).
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
* **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.
|
|
24
|
+
|
|
8
25
|
## [0.11.13](https://github.com/decocms/parity/compare/v0.11.12...v0.11.13) (2026-06-17)
|
|
9
26
|
|
|
10
27
|
### Fixed
|
package/dist/cli.js
CHANGED
|
@@ -54522,7 +54522,7 @@ class TimingRegistry {
|
|
|
54522
54522
|
};
|
|
54523
54523
|
}
|
|
54524
54524
|
}
|
|
54525
|
-
function formatTimingsSummary(timings) {
|
|
54525
|
+
function formatTimingsSummary(timings, flowBreakdown) {
|
|
54526
54526
|
if (timings.phases.length === 0) {
|
|
54527
54527
|
return `⏱ Run completed in ${formatMs(timings.totalMs)}`;
|
|
54528
54528
|
}
|
|
@@ -54535,9 +54535,41 @@ function formatTimingsSummary(timings) {
|
|
|
54535
54535
|
const pctOfTotal = timings.totalMs > 0 ? Math.round(p.durationMs / timings.totalMs * 100) : 0;
|
|
54536
54536
|
lines.push(` ${p.phase.padEnd(maxLabel)} ${formatMs(p.durationMs).padStart(7)} ${bar} ${pctOfTotal}%`);
|
|
54537
54537
|
}
|
|
54538
|
+
if (flowBreakdown && flowBreakdown.length > 0) {
|
|
54539
|
+
const maxFlowLabel = Math.max(...flowBreakdown.map((f) => f.flow.length));
|
|
54540
|
+
lines.push("");
|
|
54541
|
+
lines.push(" flows breakdown (sides run in parallel within viewport)");
|
|
54542
|
+
for (const f of flowBreakdown) {
|
|
54543
|
+
const sideDetail = f.sides.map((s) => `${s.viewport}/${s.side} ${formatMs(s.durationMs)}`).join(" · ");
|
|
54544
|
+
lines.push(` ${f.flow.padEnd(maxFlowLabel)} max ${formatMs(f.maxMs).padStart(6)} · ${sideDetail}`);
|
|
54545
|
+
}
|
|
54546
|
+
}
|
|
54538
54547
|
return lines.join(`
|
|
54539
54548
|
`);
|
|
54540
54549
|
}
|
|
54550
|
+
function buildFlowBreakdown(captures) {
|
|
54551
|
+
const byFlow = new Map;
|
|
54552
|
+
for (const c of captures) {
|
|
54553
|
+
const existing = byFlow.get(c.flow);
|
|
54554
|
+
const entry = {
|
|
54555
|
+
viewport: c.viewport,
|
|
54556
|
+
side: c.side,
|
|
54557
|
+
durationMs: c.totalDurationMs
|
|
54558
|
+
};
|
|
54559
|
+
if (!existing) {
|
|
54560
|
+
byFlow.set(c.flow, {
|
|
54561
|
+
flow: c.flow,
|
|
54562
|
+
maxMs: c.totalDurationMs,
|
|
54563
|
+
sides: [entry]
|
|
54564
|
+
});
|
|
54565
|
+
} else {
|
|
54566
|
+
existing.sides.push(entry);
|
|
54567
|
+
if (c.totalDurationMs > existing.maxMs)
|
|
54568
|
+
existing.maxMs = c.totalDurationMs;
|
|
54569
|
+
}
|
|
54570
|
+
}
|
|
54571
|
+
return Array.from(byFlow.values());
|
|
54572
|
+
}
|
|
54541
54573
|
function formatMs(ms) {
|
|
54542
54574
|
const totalSec = Math.floor(ms / 1000);
|
|
54543
54575
|
if (totalSec < 60)
|
|
@@ -55069,6 +55101,21 @@ async function runCommand(rawOpts) {
|
|
|
55069
55101
|
let browser = null;
|
|
55070
55102
|
const allFlowCaptures = [];
|
|
55071
55103
|
const allPageCaptures = [];
|
|
55104
|
+
let currentSpinnerLabel = "Launching browser…";
|
|
55105
|
+
const renderSpinner = () => {
|
|
55106
|
+
const elapsedSec = Math.floor((Date.now() - startedAt) / 1000);
|
|
55107
|
+
const min = Math.floor(elapsedSec / 60);
|
|
55108
|
+
const sec = elapsedSec % 60;
|
|
55109
|
+
const mmss = `${min.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}`;
|
|
55110
|
+
spinner.text = `${chalk16.dim(`⏱ ${mmss} ·`)} ${currentSpinnerLabel}`;
|
|
55111
|
+
};
|
|
55112
|
+
const setSpinnerLabel = (label) => {
|
|
55113
|
+
currentSpinnerLabel = label;
|
|
55114
|
+
renderSpinner();
|
|
55115
|
+
};
|
|
55116
|
+
setSpinnerLabel("Launching browser…");
|
|
55117
|
+
const elapsedTicker = setInterval(renderSpinner, 1000);
|
|
55118
|
+
elapsedTicker.unref?.();
|
|
55072
55119
|
const timings = new TimingRegistry;
|
|
55073
55120
|
let phaseStart = performance.now();
|
|
55074
55121
|
const stampPhase = (label) => {
|
|
@@ -55113,7 +55160,7 @@ async function runCommand(rawOpts) {
|
|
|
55113
55160
|
process.stderr.write(` report parcial em ${paths.reportHtml}
|
|
55114
55161
|
`);
|
|
55115
55162
|
process.stderr.write(`
|
|
55116
|
-
${formatTimingsSummary(partialTimings)}
|
|
55163
|
+
${formatTimingsSummary(partialTimings, buildFlowBreakdown(allFlowCaptures))}
|
|
55117
55164
|
`);
|
|
55118
55165
|
} catch (err) {
|
|
55119
55166
|
process.stderr.write(` falha escrevendo report parcial: ${err.message}
|
|
@@ -55155,26 +55202,27 @@ ${formatTimingsSummary(partialTimings)}
|
|
|
55155
55202
|
warmupSpinner.fail(`Warmup falhou — 0/${result.attempted} requests ok. Cache pode estar stale. Primeira falha: ${firstReason}`);
|
|
55156
55203
|
}
|
|
55157
55204
|
}
|
|
55158
|
-
|
|
55159
|
-
|
|
55160
|
-
|
|
55161
|
-
|
|
55162
|
-
|
|
55163
|
-
|
|
55164
|
-
|
|
55165
|
-
|
|
55166
|
-
|
|
55167
|
-
|
|
55168
|
-
|
|
55169
|
-
|
|
55170
|
-
|
|
55171
|
-
|
|
55205
|
+
const runOneSide = async (viewport, side) => {
|
|
55206
|
+
const baseUrl = side === "prod" ? opts.prod : opts.cand;
|
|
55207
|
+
const harPath = join16(paths.harDir, `${viewport}-${side}.har`);
|
|
55208
|
+
const tracePath = join16(paths.tracesDir, `${viewport}-${side}.zip`);
|
|
55209
|
+
const ctx = await newContext(browser, {
|
|
55210
|
+
viewport,
|
|
55211
|
+
harPath,
|
|
55212
|
+
tracesDir: paths.tracesDir,
|
|
55213
|
+
cohortCookieValue: "control",
|
|
55214
|
+
noCache: opts.bypassCache
|
|
55215
|
+
});
|
|
55216
|
+
await installVitalsCollector(ctx);
|
|
55217
|
+
const captures = [];
|
|
55218
|
+
const pages = [];
|
|
55219
|
+
try {
|
|
55172
55220
|
for (const flow of flows) {
|
|
55173
55221
|
const flowStart = Date.now();
|
|
55174
55222
|
let lastStepTotal = 0;
|
|
55175
55223
|
const sideTag = side === "prod" ? chalk16.cyan("prod") : chalk16.magenta("cand");
|
|
55176
55224
|
const prefix = `${chalk16.dim(`[${viewport}/`)}${sideTag}${chalk16.dim("]")}`;
|
|
55177
|
-
|
|
55225
|
+
setSpinnerLabel(`${prefix} ${flow}: starting…`);
|
|
55178
55226
|
const cap = await runFlow(flow, {
|
|
55179
55227
|
baseUrl,
|
|
55180
55228
|
side,
|
|
@@ -55190,7 +55238,7 @@ ${formatTimingsSummary(partialTimings)}
|
|
|
55190
55238
|
onStep: (event) => {
|
|
55191
55239
|
if (event.phase === "start") {
|
|
55192
55240
|
lastStepTotal = event.total;
|
|
55193
|
-
|
|
55241
|
+
setSpinnerLabel(`${prefix} ${flow} ${chalk16.dim(`${event.index}/${event.total}`)} ${event.name}…`);
|
|
55194
55242
|
}
|
|
55195
55243
|
}
|
|
55196
55244
|
});
|
|
@@ -55199,7 +55247,7 @@ ${formatTimingsSummary(partialTimings)}
|
|
|
55199
55247
|
const target = lastStepTotal || recorded;
|
|
55200
55248
|
const failed = cap.steps?.find((s) => s.status === "failed");
|
|
55201
55249
|
const lastStep = cap.steps?.[cap.steps.length - 1];
|
|
55202
|
-
const
|
|
55250
|
+
const elapsedDim = chalk16.dim(`${((Date.now() - flowStart) / 1000).toFixed(1)}s`);
|
|
55203
55251
|
const counts = target > 0 ? chalk16.dim(`${okSteps}/${target}`) : "";
|
|
55204
55252
|
let glyph;
|
|
55205
55253
|
let detail;
|
|
@@ -55213,22 +55261,38 @@ ${formatTimingsSummary(partialTimings)}
|
|
|
55213
55261
|
glyph = chalk16.green("✓");
|
|
55214
55262
|
detail = "";
|
|
55215
55263
|
}
|
|
55216
|
-
const summary = `${glyph} ${prefix} ${flow} ${counts} ${detail} ${
|
|
55264
|
+
const summary = `${glyph} ${prefix} ${flow} ${counts} ${detail} ${elapsedDim}`.replace(/\s+/g, " ").trim();
|
|
55217
55265
|
spinner.stopAndPersist({ symbol: "", text: summary });
|
|
55218
|
-
|
|
55219
|
-
|
|
55266
|
+
setSpinnerLabel(`${prefix} ${flow} done — next…`);
|
|
55267
|
+
spinner.start();
|
|
55268
|
+
captures.push(cap);
|
|
55220
55269
|
for (const p of cap.pages)
|
|
55221
|
-
|
|
55270
|
+
pages.push(p);
|
|
55271
|
+
}
|
|
55272
|
+
} finally {
|
|
55273
|
+
await stopTracing(ctx, tracePath).catch(() => {
|
|
55274
|
+
return;
|
|
55275
|
+
});
|
|
55276
|
+
await ctx.close();
|
|
55277
|
+
}
|
|
55278
|
+
return { captures, pages };
|
|
55279
|
+
};
|
|
55280
|
+
for (const viewport of viewports) {
|
|
55281
|
+
const [prodResult, candResult] = await Promise.all([
|
|
55282
|
+
runOneSide(viewport, "prod"),
|
|
55283
|
+
runOneSide(viewport, "cand")
|
|
55284
|
+
]);
|
|
55285
|
+
for (const r of [prodResult, candResult]) {
|
|
55286
|
+
for (const cap of r.captures) {
|
|
55287
|
+
allFlowCaptures.push(cap);
|
|
55222
55288
|
if (opts.learn !== false) {
|
|
55223
55289
|
const result = promoteStepsFromFlow(learned, platform, prodHost, cap);
|
|
55224
55290
|
promotedCount += result.promoted;
|
|
55225
55291
|
deprecatedCount += result.deprecated;
|
|
55226
55292
|
}
|
|
55227
55293
|
}
|
|
55228
|
-
|
|
55229
|
-
|
|
55230
|
-
});
|
|
55231
|
-
await ctx.close();
|
|
55294
|
+
for (const p of r.pages)
|
|
55295
|
+
allPageCaptures.push(p);
|
|
55232
55296
|
}
|
|
55233
55297
|
}
|
|
55234
55298
|
stampPhase("collect");
|
|
@@ -55449,7 +55513,8 @@ ${formatTimingsSummary(partialTimings)}
|
|
|
55449
55513
|
}
|
|
55450
55514
|
}
|
|
55451
55515
|
currentPhase = "checks";
|
|
55452
|
-
|
|
55516
|
+
setSpinnerLabel("Rodando checks…");
|
|
55517
|
+
spinner.start();
|
|
55453
55518
|
const checksHb = attachSpinnerHeartbeat(spinner, { baseText: "Rodando checks…" });
|
|
55454
55519
|
const cacheDir = join16(opts.output, "cache");
|
|
55455
55520
|
const checkCtx = {
|
|
@@ -55474,7 +55539,8 @@ ${formatTimingsSummary(partialTimings)}
|
|
|
55474
55539
|
}
|
|
55475
55540
|
const allIssues = checks.flatMap((c) => c.issues);
|
|
55476
55541
|
currentPhase = "llm-aggregate";
|
|
55477
|
-
|
|
55542
|
+
setSpinnerLabel(isLlmAvailable() ? `Agregando issues via LLM (${providerLabel()}, timeout=${llmTimeoutSec}s)…` : "Agregando issues (modo offline — sem LLM keys)…");
|
|
55543
|
+
spinner.start();
|
|
55478
55544
|
const llmHb = attachSpinnerHeartbeat(spinner, {
|
|
55479
55545
|
baseText: isLlmAvailable() ? "Agregando issues via LLM (Sonnet 4.6)…" : "Agregando issues (modo offline)…"
|
|
55480
55546
|
});
|
|
@@ -55556,7 +55622,7 @@ ${formatTimingsSummary(partialTimings)}
|
|
|
55556
55622
|
});
|
|
55557
55623
|
printSummary5(run, paths.reportHtml, { promotedCount, deprecatedCount, platform });
|
|
55558
55624
|
console.log(`
|
|
55559
|
-
${formatTimingsSummary(runTimings)}`);
|
|
55625
|
+
${formatTimingsSummary(runTimings, buildFlowBreakdown(allFlowCaptures))}`);
|
|
55560
55626
|
if (opts.ci) {
|
|
55561
55627
|
const blocking = allIssues.filter((i) => failOn.includes(i.severity));
|
|
55562
55628
|
if (blocking.length > 0) {
|
|
@@ -55586,6 +55652,7 @@ ${formatTimingsSummary(runTimings)}`);
|
|
|
55586
55652
|
});
|
|
55587
55653
|
return 2;
|
|
55588
55654
|
} finally {
|
|
55655
|
+
clearInterval(elapsedTicker);
|
|
55589
55656
|
clearTimeout(globalTimeoutTimer);
|
|
55590
55657
|
process.removeListener("SIGINT", onSignal);
|
|
55591
55658
|
process.removeListener("SIGTERM", onSignal);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/parity",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.14",
|
|
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",
|