@decocms/parity 0.11.1 → 0.11.3
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 +21 -0
- package/README.md +1 -1
- package/dist/cli.js +195 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ 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.3](https://github.com/decocms/parity/compare/v0.11.2...v0.11.3) (2026-06-17)
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
* **`productCard` defaults now match Deco TanStack PLPs (#102).** Live testing against `bagaggio-tanstack.deco-cx.workers.dev` showed every purchase-journey aborting at step 3 (`enter-pdp` → "no product card found, recovery exhausted"). Root cause: the Deco TanStack PLP uses `<a aria-label="view product" href="/<product>/p">` (no `/p/` subpath, no `[data-product-card]` attr) — none of the seven baked-in candidates matched. Added five new defaults covering the Deco TanStack pattern: `[data-product-list] a[aria-label='view product']`, `[data-product-list] a[href$='/p']`, `[data-product-list] a[href*='/p?']`, `a[aria-label='view product']`, plus path-suffix variants. Journey on bagaggio now reaches step 6 (add-to-cart) where it surfaces a real bug — variant selection — instead of bailing at step 3.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
* **Default tier for selector-related features back to Sonnet.** PR #66 defaulted selector-discovery / step-recovery / plp-matching / pdp-matching to Haiku 4.5 for cost savings. Live testing showed Sonnet is the safer default for structural-reasoning calls — Haiku-discovered selectors didn't always match on real sites and Haiku-recovery couldn't find alternatives. `search-terms` (pure classification) stays on Haiku. Users who want the previous cheap behavior can opt in with `--llm-tier-default haiku`.
|
|
17
|
+
|
|
18
|
+
## [0.11.2](https://github.com/decocms/parity/compare/v0.11.1...v0.11.2) (2026-06-17)
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
* **Per-flow step timeline in the check detail panel.** When a check is backed by a flow (`purchase-journey-flow`, `cart-interactions-flow`, `search-*`, `login-flow`), the detail page now renders a prod vs cand step-by-step table with status pill, duration, used selector, screenshot link, and skip-reason note per step. Lets you see exactly where the runner stopped instead of just "3/3 ok".
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
* **Journey tile no longer claims "completed in both" when steps were skipped (#100).** The dashboard tile now reads `${ok}/${maxSteps}` (against the actual recorded step count, not just matched steps) and surfaces skipped steps explicitly: `${n} step(s) skipped (recovery exhausted)`. Tile state goes `warn` when a journey aborted early instead of staying `pass`. Found via live testing against bagaggio where step 3 (enter-pdp) silently skipped on both sides and the tile still showed green.
|
|
27
|
+
* **`scripts/regen-report.ts`** — small dev utility for re-rendering `report.html` from a saved `report.json` without re-running the browser. Useful when iterating on the renderer.
|
|
28
|
+
|
|
8
29
|
## [0.11.1](https://github.com/decocms/parity/compare/v0.11.0...v0.11.1) (2026-06-17)
|
|
9
30
|
|
|
10
31
|
### Fixed
|
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ Three providers, auto-detected in this order — **none required** if you have t
|
|
|
68
68
|
2. `OPENROUTER_API_KEY` — OpenRouter
|
|
69
69
|
3. **Local `claude` CLI** — uses [`@anthropic-ai/claude-agent-sdk`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk); goes through your existing Claude plan. No env vars needed
|
|
70
70
|
|
|
71
|
-
Force with `--llm <anthropic|openrouter|claude-code|none|auto>`. Override per-feature with `--llm-model
|
|
71
|
+
Force with `--llm <anthropic|openrouter|claude-code|none|auto>`. Override per-feature with `--llm-model visual-diff=claude-opus-4-7,explain=claude-opus-4-7`, or flatten everything to one tier with `--llm-tier-default <haiku|sonnet|opus>`. Default tiers: **Sonnet** for selector discovery / recovery / matching / vision / aggregation (the structural-reasoning calls), **Haiku** for short copy-list classification (`search-terms`), **Opus** for `explain`. Cost-conscious? Try `--llm-tier-default haiku` and bump features back to Sonnet selectively if you hit selector misses.
|
|
72
72
|
|
|
73
73
|
Without any provider, the CLI still runs and outputs raw check results — only the LLM-tagged tabs (Visual Diff, LLM Prompt) hide themselves and a banner explains why.
|
|
74
74
|
|
package/dist/cli.js
CHANGED
|
@@ -84,11 +84,11 @@ var init_models = __esm(() => {
|
|
|
84
84
|
"explain"
|
|
85
85
|
];
|
|
86
86
|
DEFAULT_FEATURE_TIER = {
|
|
87
|
-
"selector-discovery": "
|
|
88
|
-
"step-recovery": "
|
|
87
|
+
"selector-discovery": "sonnet",
|
|
88
|
+
"step-recovery": "sonnet",
|
|
89
89
|
"search-terms": "haiku",
|
|
90
|
-
"plp-matching": "
|
|
91
|
-
"pdp-matching": "
|
|
90
|
+
"plp-matching": "sonnet",
|
|
91
|
+
"pdp-matching": "sonnet",
|
|
92
92
|
"section-understanding": "sonnet",
|
|
93
93
|
"visual-diff": "sonnet",
|
|
94
94
|
"issue-aggregation": "sonnet",
|
|
@@ -3371,6 +3371,71 @@ var REPORT_CSS = `
|
|
|
3371
3371
|
max-height: 480px;
|
|
3372
3372
|
}
|
|
3373
3373
|
|
|
3374
|
+
/* Per-flow step timeline inside the check detail panel. Issue #98 follow-up. */
|
|
3375
|
+
.step-timeline {
|
|
3376
|
+
margin-top: 16px;
|
|
3377
|
+
padding-top: 16px;
|
|
3378
|
+
border-top: 1px solid var(--border-default);
|
|
3379
|
+
}
|
|
3380
|
+
.step-timeline:first-child { margin-top: 0; padding-top: 0; border-top: none; }
|
|
3381
|
+
.step-timeline-header {
|
|
3382
|
+
display: flex;
|
|
3383
|
+
gap: 16px;
|
|
3384
|
+
align-items: baseline;
|
|
3385
|
+
font-size: 12px;
|
|
3386
|
+
margin-bottom: 8px;
|
|
3387
|
+
flex-wrap: wrap;
|
|
3388
|
+
}
|
|
3389
|
+
.step-timeline-header strong { font-size: 13px; }
|
|
3390
|
+
.step-table {
|
|
3391
|
+
width: 100%;
|
|
3392
|
+
border-collapse: collapse;
|
|
3393
|
+
font-size: 12px;
|
|
3394
|
+
}
|
|
3395
|
+
.step-table th, .step-table td {
|
|
3396
|
+
padding: 8px 10px;
|
|
3397
|
+
border-bottom: 1px solid var(--border-default);
|
|
3398
|
+
vertical-align: top;
|
|
3399
|
+
text-align: left;
|
|
3400
|
+
}
|
|
3401
|
+
.step-table th.num, .step-table td.num { text-align: right; width: 40px; }
|
|
3402
|
+
.step-table th {
|
|
3403
|
+
color: var(--text-muted);
|
|
3404
|
+
font-weight: 600;
|
|
3405
|
+
font-size: 11px;
|
|
3406
|
+
text-transform: uppercase;
|
|
3407
|
+
letter-spacing: 0.04em;
|
|
3408
|
+
}
|
|
3409
|
+
.step-cell.empty { color: var(--text-muted); text-align: center; }
|
|
3410
|
+
.step-cell-head { display: flex; gap: 8px; align-items: center; font-size: 11px; }
|
|
3411
|
+
.step-cell .step-status {
|
|
3412
|
+
font-weight: 600;
|
|
3413
|
+
text-transform: uppercase;
|
|
3414
|
+
font-size: 10px;
|
|
3415
|
+
padding: 1px 6px;
|
|
3416
|
+
border-radius: 4px;
|
|
3417
|
+
background: var(--surface-base);
|
|
3418
|
+
border: 1px solid var(--border-default);
|
|
3419
|
+
}
|
|
3420
|
+
.step-cell.step-ok .step-status { color: var(--state-good); border-color: var(--state-good); }
|
|
3421
|
+
.step-cell.step-skipped .step-status { color: var(--state-warn); border-color: var(--state-warn); }
|
|
3422
|
+
.step-cell.step-failed .step-status { color: var(--state-bad); border-color: var(--state-bad); }
|
|
3423
|
+
.step-shot { color: var(--accent-action); text-decoration: none; font-size: 13px; }
|
|
3424
|
+
.step-shot:hover { opacity: 0.7; }
|
|
3425
|
+
.step-selector {
|
|
3426
|
+
margin-top: 4px;
|
|
3427
|
+
font-size: 10px;
|
|
3428
|
+
color: var(--text-muted);
|
|
3429
|
+
word-break: break-all;
|
|
3430
|
+
}
|
|
3431
|
+
.step-selector code { background: var(--surface-base); padding: 1px 4px; border-radius: 3px; }
|
|
3432
|
+
.step-note {
|
|
3433
|
+
margin-top: 4px;
|
|
3434
|
+
font-size: 11px;
|
|
3435
|
+
color: var(--state-warn);
|
|
3436
|
+
font-style: italic;
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3374
3439
|
/* Network waterfall — per-page SVG bar chart. Issue #78. */
|
|
3375
3440
|
.wf-page {
|
|
3376
3441
|
border: 1px solid var(--border-default);
|
|
@@ -5592,14 +5657,25 @@ function buildTiles(run) {
|
|
|
5592
5657
|
const pj = run.checks.find((c) => c.name === "purchase-journey-flow");
|
|
5593
5658
|
if (pj) {
|
|
5594
5659
|
const data = pj.data ?? {};
|
|
5595
|
-
const total = data.totalSteps ?? 0;
|
|
5596
5660
|
const failed = data.failedSteps ?? 0;
|
|
5661
|
+
const pjCaptures = run.flowCaptures.filter((fc) => fc.flow === "purchase-journey");
|
|
5662
|
+
const maxSteps = pjCaptures.reduce((m, fc) => Math.max(m, fc.steps?.length ?? 0), 0);
|
|
5663
|
+
const skippedSteps = pjCaptures.reduce((n, fc) => n + (fc.steps?.filter((s) => s.status === "skipped").length ?? 0), 0);
|
|
5664
|
+
const okSteps = Math.max(0, maxSteps - failed - skippedSteps);
|
|
5665
|
+
const aborted = skippedSteps > 0;
|
|
5666
|
+
let meta;
|
|
5667
|
+
if (failed > 0)
|
|
5668
|
+
meta = `${failed} step(s) failed`;
|
|
5669
|
+
else if (aborted)
|
|
5670
|
+
meta = `${skippedSteps} step(s) skipped (recovery exhausted)`;
|
|
5671
|
+
else
|
|
5672
|
+
meta = "completed in both";
|
|
5597
5673
|
tiles.push({
|
|
5598
5674
|
icon: "\uD83D\uDED2",
|
|
5599
5675
|
label: "Journey",
|
|
5600
|
-
value:
|
|
5601
|
-
meta
|
|
5602
|
-
state: failed > 0 ? "fail" : "pass",
|
|
5676
|
+
value: maxSteps > 0 ? `${okSteps}/${maxSteps}` : "—",
|
|
5677
|
+
meta,
|
|
5678
|
+
state: failed > 0 ? "fail" : aborted ? "warn" : "pass",
|
|
5603
5679
|
href: "#detail/purchase-journey-flow"
|
|
5604
5680
|
});
|
|
5605
5681
|
}
|
|
@@ -5691,6 +5767,7 @@ function renderCheckDetailPanel(check, run, runDir) {
|
|
|
5691
5767
|
const reproCmd = `parity check ${check.name} --prod ${run.prodUrl} --cand ${run.candUrl}`;
|
|
5692
5768
|
const reproId = `repro-${check.name.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
5693
5769
|
const dataJson = check.data && Object.keys(check.data).length > 0 ? JSON.stringify(check.data, null, 2) : null;
|
|
5770
|
+
const stepTimeline = renderFlowStepTimeline(check.name, run, runDir);
|
|
5694
5771
|
return `
|
|
5695
5772
|
<div class="card">
|
|
5696
5773
|
<div class="check-detail-header">
|
|
@@ -5705,6 +5782,8 @@ function renderCheckDetailPanel(check, run, runDir) {
|
|
|
5705
5782
|
<p class="check-detail-summary">${escapeHtml(check.summary)}</p>
|
|
5706
5783
|
</div>
|
|
5707
5784
|
|
|
5785
|
+
${stepTimeline}
|
|
5786
|
+
|
|
5708
5787
|
<div class="card">
|
|
5709
5788
|
<h2>Reproduce this check</h2>
|
|
5710
5789
|
<div class="prompt-toolbar">
|
|
@@ -5717,6 +5796,107 @@ function renderCheckDetailPanel(check, run, runDir) {
|
|
|
5717
5796
|
|
|
5718
5797
|
${dataJson ? `<div class="card"><h2>Raw check data</h2><pre class="check-data">${escapeHtml(dataJson)}</pre></div>` : ""}`;
|
|
5719
5798
|
}
|
|
5799
|
+
var CHECK_TO_FLOW = {
|
|
5800
|
+
"purchase-journey-flow": "purchase-journey",
|
|
5801
|
+
"cart-interactions-flow": "cart-interactions",
|
|
5802
|
+
"search-presence": "search",
|
|
5803
|
+
"search-autocomplete": "search",
|
|
5804
|
+
"search-results": "search",
|
|
5805
|
+
"search-no-results": "search",
|
|
5806
|
+
"login-flow": "login"
|
|
5807
|
+
};
|
|
5808
|
+
function renderFlowStepTimeline(checkName, run, runDir) {
|
|
5809
|
+
const flowName = CHECK_TO_FLOW[checkName];
|
|
5810
|
+
if (!flowName)
|
|
5811
|
+
return "";
|
|
5812
|
+
const captures = run.flowCaptures.filter((fc) => fc.flow === flowName);
|
|
5813
|
+
if (captures.length === 0)
|
|
5814
|
+
return "";
|
|
5815
|
+
const byViewport = new Map;
|
|
5816
|
+
for (const fc of captures) {
|
|
5817
|
+
const slot = byViewport.get(fc.viewport) ?? {};
|
|
5818
|
+
if (fc.side === "prod")
|
|
5819
|
+
slot.prod = fc;
|
|
5820
|
+
else if (fc.side === "cand")
|
|
5821
|
+
slot.cand = fc;
|
|
5822
|
+
byViewport.set(fc.viewport, slot);
|
|
5823
|
+
}
|
|
5824
|
+
const blocks = [];
|
|
5825
|
+
for (const [viewport, sides] of byViewport) {
|
|
5826
|
+
blocks.push(renderFlowTimelinePair(viewport, sides.prod, sides.cand, runDir));
|
|
5827
|
+
}
|
|
5828
|
+
if (blocks.length === 0)
|
|
5829
|
+
return "";
|
|
5830
|
+
return `
|
|
5831
|
+
<div class="card">
|
|
5832
|
+
<h2>Step timeline</h2>
|
|
5833
|
+
<div class="hint">Step-by-step trace of the flow on prod vs cand. Skipped steps usually mean a selector miss — the note column tells you why the runner gave up.</div>
|
|
5834
|
+
${blocks.join("")}
|
|
5835
|
+
</div>`;
|
|
5836
|
+
}
|
|
5837
|
+
function renderFlowTimelinePair(viewport, prod, cand, runDir) {
|
|
5838
|
+
const stepNumbers = new Set;
|
|
5839
|
+
for (const fc of [prod, cand]) {
|
|
5840
|
+
if (!fc)
|
|
5841
|
+
continue;
|
|
5842
|
+
for (const s of fc.steps ?? [])
|
|
5843
|
+
stepNumbers.add(s.step);
|
|
5844
|
+
}
|
|
5845
|
+
const ordered = Array.from(stepNumbers).sort((a, b) => a - b);
|
|
5846
|
+
const stepBy = (fc, n) => fc?.steps?.find((s) => s.step === n);
|
|
5847
|
+
const rows = ordered.map((n) => {
|
|
5848
|
+
const ps = stepBy(prod, n);
|
|
5849
|
+
const cs = stepBy(cand, n);
|
|
5850
|
+
const stepName = ps?.name ?? cs?.name ?? `step ${n}`;
|
|
5851
|
+
return `<tr>
|
|
5852
|
+
<td class="num">${n}</td>
|
|
5853
|
+
<td><code>${escapeHtml(stepName)}</code></td>
|
|
5854
|
+
${renderStepCell(ps, runDir)}
|
|
5855
|
+
${renderStepCell(cs, runDir)}
|
|
5856
|
+
</tr>`;
|
|
5857
|
+
}).join("");
|
|
5858
|
+
return `
|
|
5859
|
+
<div class="step-timeline">
|
|
5860
|
+
<div class="step-timeline-header">
|
|
5861
|
+
<strong>${escapeHtml(viewport)}</strong>
|
|
5862
|
+
<span class="dim">prod: ${prod ? formatStepSummary(prod) : "—"}</span>
|
|
5863
|
+
<span class="dim">cand: ${cand ? formatStepSummary(cand) : "—"}</span>
|
|
5864
|
+
</div>
|
|
5865
|
+
<table class="step-table">
|
|
5866
|
+
<thead>
|
|
5867
|
+
<tr>
|
|
5868
|
+
<th class="num">#</th>
|
|
5869
|
+
<th>Step</th>
|
|
5870
|
+
<th>prod</th>
|
|
5871
|
+
<th>cand</th>
|
|
5872
|
+
</tr>
|
|
5873
|
+
</thead>
|
|
5874
|
+
<tbody>${rows}</tbody>
|
|
5875
|
+
</table>
|
|
5876
|
+
</div>`;
|
|
5877
|
+
}
|
|
5878
|
+
function formatStepSummary(fc) {
|
|
5879
|
+
const steps = fc.steps ?? [];
|
|
5880
|
+
const ok = steps.filter((s) => s.status === "ok").length;
|
|
5881
|
+
const skipped = steps.filter((s) => s.status === "skipped").length;
|
|
5882
|
+
const failed = steps.filter((s) => s.status === "failed").length;
|
|
5883
|
+
const total = steps.length;
|
|
5884
|
+
const dur = (fc.totalDurationMs / 1000).toFixed(1);
|
|
5885
|
+
return `${ok}/${total} ok · ${skipped} skipped · ${failed} failed · ${dur}s`;
|
|
5886
|
+
}
|
|
5887
|
+
function renderStepCell(s, runDir) {
|
|
5888
|
+
if (!s)
|
|
5889
|
+
return `<td class="step-cell empty">—</td>`;
|
|
5890
|
+
const statusClass = `step-${s.status}`;
|
|
5891
|
+
const screenshot = s.screenshotPath ? `<a class="step-shot" href="${escapeHtml(relPath(runDir, s.screenshotPath))}" target="_blank" title="open screenshot">\uD83D\uDCF7</a>` : "";
|
|
5892
|
+
const selector = s.usedSelector ? `<div class="step-selector"><code>${escapeHtml(s.usedSelector)}</code></div>` : "";
|
|
5893
|
+
const note = s.note ? `<div class="step-note">${escapeHtml(s.note)}</div>` : "";
|
|
5894
|
+
return `<td class="step-cell ${statusClass}">
|
|
5895
|
+
<div class="step-cell-head"><span class="step-status">${s.status}</span>${screenshot}<span class="dim">${s.durationMs}ms</span></div>
|
|
5896
|
+
${selector}
|
|
5897
|
+
${note}
|
|
5898
|
+
</td>`;
|
|
5899
|
+
}
|
|
5720
5900
|
function renderChecksTable(run) {
|
|
5721
5901
|
return `
|
|
5722
5902
|
<div class="card">
|
|
@@ -11696,8 +11876,14 @@ var DEFAULT_SELECTORS = {
|
|
|
11696
11876
|
"[data-deco='view-product'] a",
|
|
11697
11877
|
"[data-testid='product-card'] a",
|
|
11698
11878
|
".product-card a",
|
|
11879
|
+
"[data-product-list] a[aria-label='view product']",
|
|
11880
|
+
"[data-product-list] a[href$='/p']",
|
|
11881
|
+
"[data-product-list] a[href*='/p?']",
|
|
11882
|
+
"a[aria-label='view product']",
|
|
11699
11883
|
"article a[href*='/p/']",
|
|
11700
|
-
"a[href*='/products/']"
|
|
11884
|
+
"a[href*='/products/']",
|
|
11885
|
+
"main a[href$='/p']",
|
|
11886
|
+
"main a[href*='/p?']"
|
|
11701
11887
|
],
|
|
11702
11888
|
buyButton: [
|
|
11703
11889
|
"button:has-text('Comprar')",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/parity",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
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",
|