@decocms/parity 0.11.0 → 0.11.2

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 +17 -0
  2. package/dist/cli.js +184 -5
  3. 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.2](https://github.com/decocms/parity/compare/v0.11.1...v0.11.2) (2026-06-17)
9
+
10
+ ### Added
11
+
12
+ * **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".
13
+
14
+ ### Changed
15
+
16
+ * **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.
17
+ * **`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.
18
+
19
+ ## [0.11.1](https://github.com/decocms/parity/compare/v0.11.0...v0.11.1) (2026-06-17)
20
+
21
+ ### Fixed
22
+
23
+ * **Claude Agent SDK provider — every call failed with `error_max_turns` (#98).** Live testing against `lojabagaggio.deco.site` vs `bagaggio-tanstack.deco-cx.workers.dev` showed `[llm-claude-sdk] call failed: error_max_turns` on every selector-discovery, step-recovery, and aggregation call when running with `--llm claude-code`. Root cause: `maxTurns: 1` in `baseSdkOptions` — Claude Code's harness counts the response emission as turn 2 even when `allowedTools: []` prevents any tool from firing. Removed `maxTurns`; the empty tool whitelist alone guarantees a single round-trip. The SDK provider now actually produces output.
24
+
8
25
  ## [0.11.0](https://github.com/decocms/parity/compare/v0.10.1...v0.11.0) (2026-06-17)
9
26
 
10
27
  ### Added
package/dist/cli.js CHANGED
@@ -144,7 +144,6 @@ function baseSdkOptions(model, controller) {
144
144
  model,
145
145
  allowedTools: [],
146
146
  permissionMode: "dontAsk",
147
- maxTurns: 1,
148
147
  abortController: controller,
149
148
  settingSources: []
150
149
  };
@@ -3372,6 +3371,71 @@ var REPORT_CSS = `
3372
3371
  max-height: 480px;
3373
3372
  }
3374
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
+
3375
3439
  /* Network waterfall — per-page SVG bar chart. Issue #78. */
3376
3440
  .wf-page {
3377
3441
  border: 1px solid var(--border-default);
@@ -5593,14 +5657,25 @@ function buildTiles(run) {
5593
5657
  const pj = run.checks.find((c) => c.name === "purchase-journey-flow");
5594
5658
  if (pj) {
5595
5659
  const data = pj.data ?? {};
5596
- const total = data.totalSteps ?? 0;
5597
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";
5598
5673
  tiles.push({
5599
5674
  icon: "\uD83D\uDED2",
5600
5675
  label: "Journey",
5601
- value: total > 0 ? `${total - failed}/${total}` : "—",
5602
- meta: failed > 0 ? `${failed} step(s) failed` : "completed in both",
5603
- state: failed > 0 ? "fail" : "pass",
5676
+ value: maxSteps > 0 ? `${okSteps}/${maxSteps}` : "—",
5677
+ meta,
5678
+ state: failed > 0 ? "fail" : aborted ? "warn" : "pass",
5604
5679
  href: "#detail/purchase-journey-flow"
5605
5680
  });
5606
5681
  }
@@ -5692,6 +5767,7 @@ function renderCheckDetailPanel(check, run, runDir) {
5692
5767
  const reproCmd = `parity check ${check.name} --prod ${run.prodUrl} --cand ${run.candUrl}`;
5693
5768
  const reproId = `repro-${check.name.replace(/[^a-zA-Z0-9]/g, "_")}`;
5694
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);
5695
5771
  return `
5696
5772
  <div class="card">
5697
5773
  <div class="check-detail-header">
@@ -5706,6 +5782,8 @@ function renderCheckDetailPanel(check, run, runDir) {
5706
5782
  <p class="check-detail-summary">${escapeHtml(check.summary)}</p>
5707
5783
  </div>
5708
5784
 
5785
+ ${stepTimeline}
5786
+
5709
5787
  <div class="card">
5710
5788
  <h2>Reproduce this check</h2>
5711
5789
  <div class="prompt-toolbar">
@@ -5718,6 +5796,107 @@ function renderCheckDetailPanel(check, run, runDir) {
5718
5796
 
5719
5797
  ${dataJson ? `<div class="card"><h2>Raw check data</h2><pre class="check-data">${escapeHtml(dataJson)}</pre></div>` : ""}`;
5720
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
+ }
5721
5900
  function renderChecksTable(run) {
5722
5901
  return `
5723
5902
  <div class="card">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/parity",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
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",