executable-stories-formatters 0.7.15 → 0.9.0
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/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +1121 -18
- package/dist/cli.js.map +1 -1
- package/dist/{index-BiAYcEiz.d.cts → index-it3Pkmqv.d.cts} +159 -4
- package/dist/{index-BiAYcEiz.d.ts → index-it3Pkmqv.d.ts} +159 -4
- package/dist/index.cjs +937 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +216 -126
- package/dist/index.d.ts +216 -126
- package/dist/index.js +929 -14
- package/dist/index.js.map +1 -1
- package/package.json +3 -5
- package/schemas/raw-run.schema.json +49 -2
- package/bin/intent.js +0 -3
package/dist/index.cjs
CHANGED
|
@@ -44,6 +44,8 @@ __export(src_exports, {
|
|
|
44
44
|
MIN_PERF_SAMPLES: () => MIN_PERF_SAMPLES,
|
|
45
45
|
MarkdownFormatter: () => MarkdownFormatter,
|
|
46
46
|
ReportGenerator: () => ReportGenerator,
|
|
47
|
+
ReviewHtmlFormatter: () => ReviewHtmlFormatter,
|
|
48
|
+
ReviewMarkdownFormatter: () => ReviewMarkdownFormatter,
|
|
47
49
|
RunDiffHtmlFormatter: () => RunDiffHtmlFormatter,
|
|
48
50
|
RunDiffMarkdownFormatter: () => RunDiffMarkdownFormatter,
|
|
49
51
|
STORY_META_KEY: () => STORY_META_KEY,
|
|
@@ -54,6 +56,7 @@ __export(src_exports, {
|
|
|
54
56
|
adaptPlaywrightRun: () => adaptPlaywrightRun,
|
|
55
57
|
adaptVitestRun: () => adaptVitestRun,
|
|
56
58
|
assertValidRun: () => assertValidRun,
|
|
59
|
+
buildReview: () => buildReview,
|
|
57
60
|
bundleAssets: () => bundleAssets,
|
|
58
61
|
calculateFlakiness: () => calculateFlakiness,
|
|
59
62
|
calculateStability: () => calculateStability,
|
|
@@ -63,6 +66,8 @@ __export(src_exports, {
|
|
|
63
66
|
copyMarkdownAssets: () => copyMarkdownAssets,
|
|
64
67
|
createPrCommentSummary: () => createPrCommentSummary,
|
|
65
68
|
createReportGenerator: () => createReportGenerator,
|
|
69
|
+
deriveAudience: () => deriveAudience,
|
|
70
|
+
deriveChangeType: () => deriveChangeType,
|
|
66
71
|
deriveStepResults: () => deriveStepResults,
|
|
67
72
|
detectCI: () => detectCI4,
|
|
68
73
|
detectPerformanceTrend: () => detectPerformanceTrend,
|
|
@@ -74,7 +79,10 @@ __export(src_exports, {
|
|
|
74
79
|
generateTestCaseId: () => generateTestCaseId,
|
|
75
80
|
getAvailableThemes: () => getAvailableThemes,
|
|
76
81
|
getCssOnlyThemes: () => getCssOnlyThemes,
|
|
82
|
+
gradeEvidence: () => gradeEvidence,
|
|
77
83
|
hasSufficientHistory: () => hasSufficientHistory,
|
|
84
|
+
isReviewableSource: () => isReviewableSource,
|
|
85
|
+
isTestFile: () => isTestFile,
|
|
78
86
|
listScenarios: () => listScenarios,
|
|
79
87
|
loadHistory: () => loadHistory,
|
|
80
88
|
mergeStepResults: () => mergeStepResults,
|
|
@@ -394,7 +402,8 @@ function canonicalizeTestCase(raw, options, projectRoot) {
|
|
|
394
402
|
projectName: raw.projectName,
|
|
395
403
|
retry: raw.retry ?? 0,
|
|
396
404
|
retries: raw.retries ?? 0,
|
|
397
|
-
tags
|
|
405
|
+
tags,
|
|
406
|
+
...raw.evidence ? { evidence: raw.evidence } : {}
|
|
398
407
|
};
|
|
399
408
|
}
|
|
400
409
|
function normalizeTags(story) {
|
|
@@ -1434,11 +1443,53 @@ function initKeyboardShortcuts() {
|
|
|
1434
1443
|
});
|
|
1435
1444
|
}
|
|
1436
1445
|
|
|
1437
|
-
// Collapse/expand functionality
|
|
1446
|
+
// Collapse/expand functionality (persisted in localStorage)
|
|
1447
|
+
var COLLAPSE_KEY = 'es-collapsed-ids';
|
|
1448
|
+
|
|
1449
|
+
function loadCollapseState() {
|
|
1450
|
+
try {
|
|
1451
|
+
var raw = localStorage.getItem(COLLAPSE_KEY);
|
|
1452
|
+
return raw ? new Set(JSON.parse(raw)) : new Set();
|
|
1453
|
+
} catch (e) {
|
|
1454
|
+
return new Set();
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function saveCollapseState(set) {
|
|
1459
|
+
try {
|
|
1460
|
+
localStorage.setItem(COLLAPSE_KEY, JSON.stringify(Array.from(set)));
|
|
1461
|
+
} catch (e) { /* quota or disabled */ }
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function persistCollapse(container) {
|
|
1465
|
+
if (!container || !container.id) return;
|
|
1466
|
+
var state = loadCollapseState();
|
|
1467
|
+
if (container.classList.contains('collapsed')) {
|
|
1468
|
+
state.add(container.id);
|
|
1469
|
+
} else {
|
|
1470
|
+
state.delete(container.id);
|
|
1471
|
+
}
|
|
1472
|
+
saveCollapseState(state);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1438
1475
|
function toggleCollapse(header, container) {
|
|
1439
1476
|
container?.classList.toggle('collapsed');
|
|
1440
1477
|
const isCollapsed = container?.classList.contains('collapsed');
|
|
1441
1478
|
header.setAttribute('aria-expanded', !isCollapsed);
|
|
1479
|
+
persistCollapse(container);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function restoreCollapseState() {
|
|
1483
|
+
var state = loadCollapseState();
|
|
1484
|
+
if (state.size === 0) return;
|
|
1485
|
+
state.forEach(function(id) {
|
|
1486
|
+
var el = document.getElementById(id);
|
|
1487
|
+
if (!el) return;
|
|
1488
|
+
if (!el.classList.contains('feature') && !el.classList.contains('scenario')) return;
|
|
1489
|
+
el.classList.add('collapsed');
|
|
1490
|
+
var header = el.querySelector('.feature-header, .scenario-header');
|
|
1491
|
+
if (header) header.setAttribute('aria-expanded', 'false');
|
|
1492
|
+
});
|
|
1442
1493
|
}
|
|
1443
1494
|
|
|
1444
1495
|
function initCollapse() {
|
|
@@ -1485,14 +1536,20 @@ function expandAll() {
|
|
|
1485
1536
|
const header = el.querySelector('.feature-header, .scenario-header, .trace-view-header');
|
|
1486
1537
|
header?.setAttribute('aria-expanded', 'true');
|
|
1487
1538
|
});
|
|
1539
|
+
saveCollapseState(new Set());
|
|
1488
1540
|
}
|
|
1489
1541
|
|
|
1490
1542
|
function collapseAll() {
|
|
1543
|
+
var ids = new Set();
|
|
1491
1544
|
document.querySelectorAll('.feature, .scenario, .trace-view').forEach(el => {
|
|
1492
1545
|
el.classList.add('collapsed');
|
|
1493
1546
|
const header = el.querySelector('.feature-header, .scenario-header, .trace-view-header');
|
|
1494
1547
|
header?.setAttribute('aria-expanded', 'false');
|
|
1548
|
+
if (el.id && (el.classList.contains('feature') || el.classList.contains('scenario'))) {
|
|
1549
|
+
ids.add(el.id);
|
|
1550
|
+
}
|
|
1495
1551
|
});
|
|
1552
|
+
saveCollapseState(ids);
|
|
1496
1553
|
}
|
|
1497
1554
|
|
|
1498
1555
|
// Detail level toggle
|
|
@@ -1619,6 +1676,68 @@ function copyScenarioAsMarkdown(scenarioId) {
|
|
|
1619
1676
|
});
|
|
1620
1677
|
}
|
|
1621
1678
|
|
|
1679
|
+
// Copy scenario as Claude-ready prompt (failure investigation context)
|
|
1680
|
+
function copyScenarioAsPrompt(scenarioId) {
|
|
1681
|
+
var scenario = document.getElementById(scenarioId);
|
|
1682
|
+
if (!scenario) return;
|
|
1683
|
+
|
|
1684
|
+
var feature = scenario.closest('.feature');
|
|
1685
|
+
var featureTitle = feature ? (feature.querySelector('.feature-title') || {}).textContent || '' : '';
|
|
1686
|
+
var title = (scenario.querySelector('.scenario-name') || {}).textContent || '';
|
|
1687
|
+
var statusEl = scenario.querySelector('.status-icon');
|
|
1688
|
+
var status = statusEl && statusEl.classList.contains('status-passed') ? 'passed' :
|
|
1689
|
+
statusEl && statusEl.classList.contains('status-failed') ? 'failed' :
|
|
1690
|
+
statusEl && statusEl.classList.contains('status-skipped') ? 'skipped' : 'pending';
|
|
1691
|
+
var sourceLink = scenario.querySelector('.source-link');
|
|
1692
|
+
var source = sourceLink ? sourceLink.textContent || '' : '';
|
|
1693
|
+
var tags = Array.from(scenario.querySelectorAll('.scenario-meta .tag')).map(function(t) { return t.textContent.trim(); });
|
|
1694
|
+
var steps = scenario.querySelectorAll('.step, .step.continuation');
|
|
1695
|
+
|
|
1696
|
+
var lines = [];
|
|
1697
|
+
lines.push('I need help investigating a failing executable-story scenario.');
|
|
1698
|
+
lines.push('');
|
|
1699
|
+
if (featureTitle.trim()) lines.push('Feature: ' + featureTitle.trim());
|
|
1700
|
+
lines.push('Scenario: ' + title.trim());
|
|
1701
|
+
lines.push('Status: ' + status);
|
|
1702
|
+
if (source.trim()) lines.push('Source: ' + source.trim());
|
|
1703
|
+
if (tags.length > 0) lines.push('Tags: ' + tags.join(', '));
|
|
1704
|
+
lines.push('');
|
|
1705
|
+
lines.push('Steps:');
|
|
1706
|
+
steps.forEach(function(step) {
|
|
1707
|
+
var keyword = step.getAttribute('data-keyword') || '';
|
|
1708
|
+
var text = step.getAttribute('data-text') || '';
|
|
1709
|
+
var stepStatusEl = step.querySelector('.step-status');
|
|
1710
|
+
var marker = ' ';
|
|
1711
|
+
if (stepStatusEl) {
|
|
1712
|
+
if (stepStatusEl.classList.contains('status-failed')) marker = 'x ';
|
|
1713
|
+
else if (stepStatusEl.classList.contains('status-passed')) marker = '+ ';
|
|
1714
|
+
else if (stepStatusEl.classList.contains('status-skipped')) marker = '- ';
|
|
1715
|
+
}
|
|
1716
|
+
lines.push(marker + keyword + ' ' + text);
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
var errorBox = scenario.querySelector('.error-message');
|
|
1720
|
+
if (errorBox) {
|
|
1721
|
+
lines.push('');
|
|
1722
|
+
lines.push('Error:');
|
|
1723
|
+
lines.push((errorBox.textContent || '').trim());
|
|
1724
|
+
}
|
|
1725
|
+
var stackBox = scenario.querySelector('.error-stack');
|
|
1726
|
+
if (stackBox) {
|
|
1727
|
+
lines.push('');
|
|
1728
|
+
lines.push('Stack:');
|
|
1729
|
+
lines.push((stackBox.textContent || '').trim());
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
lines.push('');
|
|
1733
|
+
lines.push('Please read the source file, identify the root cause, and propose a fix.');
|
|
1734
|
+
|
|
1735
|
+
var text = lines.join('\\n');
|
|
1736
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
1737
|
+
showCopyToast(scenario);
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1622
1741
|
// Hash scroll on load
|
|
1623
1742
|
function initHashScroll() {
|
|
1624
1743
|
if (!location.hash) return;
|
|
@@ -1788,6 +1907,7 @@ function generateScript(options) {
|
|
|
1788
1907
|
initCalls.push("initStatusFilter();");
|
|
1789
1908
|
initCalls.push("initKeyboardShortcuts();");
|
|
1790
1909
|
initCalls.push("initCollapse();");
|
|
1910
|
+
initCalls.push("restoreCollapseState();");
|
|
1791
1911
|
initCalls.push("initDetailLevel();");
|
|
1792
1912
|
initCalls.push("applyAllFilters();");
|
|
1793
1913
|
initCalls.push("initHashScroll();");
|
|
@@ -3895,6 +4015,33 @@ body {
|
|
|
3895
4015
|
color: var(--primary);
|
|
3896
4016
|
}
|
|
3897
4017
|
|
|
4018
|
+
.copy-prompt-btn {
|
|
4019
|
+
display: inline-flex;
|
|
4020
|
+
align-items: center;
|
|
4021
|
+
justify-content: center;
|
|
4022
|
+
width: 1.5rem;
|
|
4023
|
+
height: 1.5rem;
|
|
4024
|
+
border: none;
|
|
4025
|
+
background: none;
|
|
4026
|
+
color: var(--muted-foreground);
|
|
4027
|
+
cursor: pointer;
|
|
4028
|
+
opacity: 0.6;
|
|
4029
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
4030
|
+
font-size: 0.95rem;
|
|
4031
|
+
padding: 0;
|
|
4032
|
+
flex-shrink: 0;
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
.scenario-header:hover .copy-prompt-btn,
|
|
4036
|
+
.copy-prompt-btn:focus-visible {
|
|
4037
|
+
opacity: 1;
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
.copy-prompt-btn:hover {
|
|
4041
|
+
color: var(--primary);
|
|
4042
|
+
transform: scale(1.15);
|
|
4043
|
+
}
|
|
4044
|
+
|
|
3898
4045
|
/* ============================================================================
|
|
3899
4046
|
Keyboard Navigation
|
|
3900
4047
|
============================================================================ */
|
|
@@ -4132,6 +4279,82 @@ a.toc-title:hover {
|
|
|
4132
4279
|
outline-offset: 2px;
|
|
4133
4280
|
}
|
|
4134
4281
|
|
|
4282
|
+
/* ============================================================================
|
|
4283
|
+
Mobile responsive refinements
|
|
4284
|
+
============================================================================ */
|
|
4285
|
+
@media (max-width: 640px) {
|
|
4286
|
+
.container {
|
|
4287
|
+
padding: 0.875rem;
|
|
4288
|
+
}
|
|
4289
|
+
|
|
4290
|
+
.header {
|
|
4291
|
+
flex-direction: column;
|
|
4292
|
+
align-items: stretch;
|
|
4293
|
+
gap: 0.75rem;
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
.header-actions {
|
|
4297
|
+
flex-wrap: wrap;
|
|
4298
|
+
gap: 0.5rem;
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4301
|
+
.search-input {
|
|
4302
|
+
width: 100%;
|
|
4303
|
+
flex: 1 1 100%;
|
|
4304
|
+
min-width: 0;
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4307
|
+
.header h1 {
|
|
4308
|
+
font-size: 1.25rem;
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
.scenario-header,
|
|
4312
|
+
.feature-header {
|
|
4313
|
+
flex-wrap: wrap;
|
|
4314
|
+
gap: 0.5rem;
|
|
4315
|
+
}
|
|
4316
|
+
|
|
4317
|
+
.scenario-meta {
|
|
4318
|
+
flex-wrap: wrap;
|
|
4319
|
+
}
|
|
4320
|
+
|
|
4321
|
+
.scenario-actions {
|
|
4322
|
+
flex-wrap: wrap;
|
|
4323
|
+
}
|
|
4324
|
+
|
|
4325
|
+
/* Always-visible action buttons on touch (no hover) */
|
|
4326
|
+
.copy-scenario-btn,
|
|
4327
|
+
.copy-prompt-btn,
|
|
4328
|
+
.permalink-anchor {
|
|
4329
|
+
opacity: 1 !important;
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
.summary-card {
|
|
4333
|
+
padding: 0.75rem 0.875rem;
|
|
4334
|
+
}
|
|
4335
|
+
|
|
4336
|
+
.summary-card .value {
|
|
4337
|
+
font-size: 1.5rem;
|
|
4338
|
+
}
|
|
4339
|
+
|
|
4340
|
+
.tag-bar {
|
|
4341
|
+
overflow-x: auto;
|
|
4342
|
+
-webkit-overflow-scrolling: touch;
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4345
|
+
.shortcuts-overlay {
|
|
4346
|
+
padding: 1rem;
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
|
|
4350
|
+
@media (hover: none) and (pointer: coarse) {
|
|
4351
|
+
.copy-scenario-btn,
|
|
4352
|
+
.copy-prompt-btn,
|
|
4353
|
+
.permalink-anchor {
|
|
4354
|
+
opacity: 1;
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
|
|
4135
4358
|
`;
|
|
4136
4359
|
|
|
4137
4360
|
// src/formatters/html/themes/default.ts
|
|
@@ -13713,7 +13936,7 @@ function renderDocEntry(entry, deps) {
|
|
|
13713
13936
|
// src/formatters/html/renderers/steps.ts
|
|
13714
13937
|
var CONTINUATION_KEYWORDS = ["And", "But", "*"];
|
|
13715
13938
|
function renderStep(step, stepResult, index, deps) {
|
|
13716
|
-
const
|
|
13939
|
+
const statusIcon4 = stepResult ? deps.getStatusIcon(stepResult.status) : "\u25CB";
|
|
13717
13940
|
const statusClass = stepResult ? `status-${stepResult.status}` : "";
|
|
13718
13941
|
const duration = stepResult && stepResult.durationMs > 0 ? `${stepResult.durationMs}ms` : "";
|
|
13719
13942
|
const keywordTrimmed = step.keyword.trim();
|
|
@@ -13722,7 +13945,7 @@ function renderStep(step, stepResult, index, deps) {
|
|
|
13722
13945
|
const stepDocs = deps.renderDocs(step.docs, "step-docs");
|
|
13723
13946
|
const textHtml = deps.highlightStepParams ? deps.highlightStepParams(step.text) : deps.escapeHtml(step.text);
|
|
13724
13947
|
return `<div class="${stepClass}" data-keyword="${deps.escapeHtml(keywordTrimmed)}" data-text="${deps.escapeHtml(step.text)}">
|
|
13725
|
-
<span class="step-status ${statusClass}">${
|
|
13948
|
+
<span class="step-status ${statusClass}">${statusIcon4}</span>
|
|
13726
13949
|
<span class="step-keyword">${deps.escapeHtml(step.keyword)}</span>
|
|
13727
13950
|
<span class="step-text">${textHtml}</span>
|
|
13728
13951
|
<span class="step-duration">${duration}</span>
|
|
@@ -13772,16 +13995,16 @@ function hasSufficientHistory(entries, min) {
|
|
|
13772
13995
|
}
|
|
13773
13996
|
|
|
13774
13997
|
// src/formatters/html/renderers/scenario.ts
|
|
13775
|
-
function renderTicket(ticket, template,
|
|
13998
|
+
function renderTicket(ticket, template, escapeHtml4) {
|
|
13776
13999
|
const url = ticket.url ?? (template ? template.replace("{ticket}", ticket.id) : void 0);
|
|
13777
14000
|
if (url) {
|
|
13778
|
-
return `<a class="tag ticket-tag" href="${
|
|
14001
|
+
return `<a class="tag ticket-tag" href="${escapeHtml4(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml4(ticket.id)}</a>`;
|
|
13779
14002
|
}
|
|
13780
|
-
return `<span class="tag ticket-tag">${
|
|
14003
|
+
return `<span class="tag ticket-tag">${escapeHtml4(ticket.id)}</span>`;
|
|
13781
14004
|
}
|
|
13782
14005
|
function renderScenario(args, deps) {
|
|
13783
14006
|
const { tc } = args;
|
|
13784
|
-
const
|
|
14007
|
+
const statusIcon4 = deps.getStatusIcon(tc.status);
|
|
13785
14008
|
const statusClass = `status-${tc.status}`;
|
|
13786
14009
|
const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
|
|
13787
14010
|
const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
|
|
@@ -13851,13 +14074,14 @@ function renderScenario(args, deps) {
|
|
|
13851
14074
|
<div class="scenario-header" role="button" tabindex="0" aria-expanded="${ariaExpanded}">
|
|
13852
14075
|
<div class="scenario-info">
|
|
13853
14076
|
<div class="scenario-title">
|
|
13854
|
-
<span class="status-icon ${statusClass}">${
|
|
14077
|
+
<span class="status-icon ${statusClass}">${statusIcon4}</span>
|
|
13855
14078
|
<span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
|
|
13856
14079
|
</div>
|
|
13857
14080
|
<div class="scenario-meta">${tags}${tickets}${outcomeBadge}${sourceLink}${traceBadge}${metricBadges}</div>
|
|
13858
14081
|
</div>
|
|
13859
14082
|
<div class="scenario-actions">
|
|
13860
14083
|
<button class="copy-scenario-btn" onclick="copyScenarioAsMarkdown('scenario-${tc.id}')" aria-label="Copy scenario as markdown" title="Copy as Markdown"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
|
|
14084
|
+
${tc.status === "failed" ? `<button class="copy-prompt-btn" onclick="copyScenarioAsPrompt('scenario-${tc.id}')" aria-label="Copy as Claude prompt" title="Copy as AI prompt">\u2728</button>` : ""}
|
|
13861
14085
|
<button class="permalink-anchor" onclick="copyPermalink('scenario-${tc.id}')" aria-label="Copy link to scenario" title="Copy link">#</button>
|
|
13862
14086
|
<span class="scenario-duration">${duration}</span>
|
|
13863
14087
|
</div>
|
|
@@ -13976,7 +14200,7 @@ function flattenTree(roots) {
|
|
|
13976
14200
|
}
|
|
13977
14201
|
return result;
|
|
13978
14202
|
}
|
|
13979
|
-
function buildTooltip(span,
|
|
14203
|
+
function buildTooltip(span, escapeHtml4) {
|
|
13980
14204
|
const parts = [];
|
|
13981
14205
|
parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
|
|
13982
14206
|
if (span.statusMessage) {
|
|
@@ -13994,7 +14218,7 @@ function buildTooltip(span, escapeHtml3) {
|
|
|
13994
14218
|
if (text2.length > TOOLTIP_MAX_LENGTH) {
|
|
13995
14219
|
text2 = text2.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
|
|
13996
14220
|
}
|
|
13997
|
-
return
|
|
14221
|
+
return escapeHtml4(text2);
|
|
13998
14222
|
}
|
|
13999
14223
|
function renderTraceView(args, deps) {
|
|
14000
14224
|
if (!args.spans || args.spans.length === 0) return "";
|
|
@@ -14217,11 +14441,11 @@ function renderToc(args, deps) {
|
|
|
14217
14441
|
const featureName = suitePaths.length > 0 && suitePaths[0].length > 0 ? suitePaths[0][0] : file.split("/").pop()?.replace(/\.[^.]+$/, "") ?? file;
|
|
14218
14442
|
const featureSlug = `feature-${slugify(file)}`;
|
|
14219
14443
|
const scenarios = testCases.map((tc) => {
|
|
14220
|
-
const
|
|
14444
|
+
const statusIcon4 = deps.getStatusIcon(tc.status);
|
|
14221
14445
|
const statusClass = `status-${tc.status}`;
|
|
14222
14446
|
const failedClass = tc.status === "failed" ? " toc-failed" : "";
|
|
14223
14447
|
return `<a class="toc-scenario${failedClass}" href="#scenario-${tc.id}">
|
|
14224
|
-
<span class="toc-status ${statusClass}">${
|
|
14448
|
+
<span class="toc-status ${statusClass}">${statusIcon4}</span>
|
|
14225
14449
|
${deps.escapeHtml(tc.story.scenario)}
|
|
14226
14450
|
</a>`;
|
|
14227
14451
|
}).join("\n");
|
|
@@ -19533,6 +19757,697 @@ function listScenarios(args, _deps) {
|
|
|
19533
19757
|
return lines.join("\n");
|
|
19534
19758
|
}
|
|
19535
19759
|
|
|
19760
|
+
// src/review/conventions.ts
|
|
19761
|
+
var CHANGE_TAG_PREFIX = "change:";
|
|
19762
|
+
var AUDIENCE_TAG_PREFIX = "audience:";
|
|
19763
|
+
var VALID_CHANGE_TYPES = /* @__PURE__ */ new Set([
|
|
19764
|
+
"feature",
|
|
19765
|
+
"bugfix",
|
|
19766
|
+
"refactor",
|
|
19767
|
+
"perf",
|
|
19768
|
+
"deps"
|
|
19769
|
+
]);
|
|
19770
|
+
var STAKEHOLDER_FILE = /(?:\.e2e\.)|(?:^|\/)e2e\/|(?:\.spec\.)/i;
|
|
19771
|
+
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
19772
|
+
"ts",
|
|
19773
|
+
"tsx",
|
|
19774
|
+
"js",
|
|
19775
|
+
"jsx",
|
|
19776
|
+
"mjs",
|
|
19777
|
+
"cjs",
|
|
19778
|
+
"py",
|
|
19779
|
+
"go",
|
|
19780
|
+
"rs",
|
|
19781
|
+
"kt",
|
|
19782
|
+
"kts",
|
|
19783
|
+
"java",
|
|
19784
|
+
"cs",
|
|
19785
|
+
"rb"
|
|
19786
|
+
]);
|
|
19787
|
+
var TEST_INFIX = /\.(?:story\.)?(?:int\.|e2e\.|unit\.)?(?:test|spec|cy)\.[a-z]+$/i;
|
|
19788
|
+
function deriveAudience(sourceFile, tags) {
|
|
19789
|
+
const override = tags.map((t) => t.toLowerCase()).find((t) => t.startsWith(AUDIENCE_TAG_PREFIX));
|
|
19790
|
+
if (override) {
|
|
19791
|
+
const value = override.slice(AUDIENCE_TAG_PREFIX.length);
|
|
19792
|
+
if (value === "stakeholder" || value === "engineer") return value;
|
|
19793
|
+
}
|
|
19794
|
+
return STAKEHOLDER_FILE.test(sourceFile) ? "stakeholder" : "engineer";
|
|
19795
|
+
}
|
|
19796
|
+
function deriveChangeType(tags) {
|
|
19797
|
+
for (const tag of tags) {
|
|
19798
|
+
const lower = tag.toLowerCase();
|
|
19799
|
+
if (lower.startsWith(CHANGE_TAG_PREFIX)) {
|
|
19800
|
+
const value = lower.slice(CHANGE_TAG_PREFIX.length);
|
|
19801
|
+
if (VALID_CHANGE_TYPES.has(value)) return value;
|
|
19802
|
+
}
|
|
19803
|
+
}
|
|
19804
|
+
return "unknown";
|
|
19805
|
+
}
|
|
19806
|
+
function extensionOf(path10) {
|
|
19807
|
+
const base = path10.split("/").pop() ?? path10;
|
|
19808
|
+
const dot = base.lastIndexOf(".");
|
|
19809
|
+
return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
|
|
19810
|
+
}
|
|
19811
|
+
function isTestFile(path10) {
|
|
19812
|
+
return TEST_INFIX.test(path10);
|
|
19813
|
+
}
|
|
19814
|
+
function isReviewableSource(path10) {
|
|
19815
|
+
if (isTestFile(path10)) return false;
|
|
19816
|
+
if (path10.endsWith(".d.ts")) return false;
|
|
19817
|
+
return CODE_EXTENSIONS.has(extensionOf(path10));
|
|
19818
|
+
}
|
|
19819
|
+
function testBaseKey(testFile) {
|
|
19820
|
+
return testFile.replace(TEST_INFIX, "");
|
|
19821
|
+
}
|
|
19822
|
+
function sourceBaseKey(sourceFile) {
|
|
19823
|
+
const dot = sourceFile.lastIndexOf(".");
|
|
19824
|
+
const slash = sourceFile.lastIndexOf("/");
|
|
19825
|
+
return dot > slash ? sourceFile.slice(0, dot) : sourceFile;
|
|
19826
|
+
}
|
|
19827
|
+
|
|
19828
|
+
// src/review/build-review.ts
|
|
19829
|
+
var STRENGTH_RANK = {
|
|
19830
|
+
none: 0,
|
|
19831
|
+
weak: 1,
|
|
19832
|
+
moderate: 2,
|
|
19833
|
+
strong: 3
|
|
19834
|
+
};
|
|
19835
|
+
var INTENT_SECTION_TITLE = /\b(why|intent|approach|rationale|reasoning)\b/i;
|
|
19836
|
+
function findDoc(docs, predicate) {
|
|
19837
|
+
if (!docs) return void 0;
|
|
19838
|
+
for (const doc of docs) {
|
|
19839
|
+
if (predicate(doc)) return doc;
|
|
19840
|
+
const nested = findDoc(doc.children, predicate);
|
|
19841
|
+
if (nested) return nested;
|
|
19842
|
+
}
|
|
19843
|
+
return void 0;
|
|
19844
|
+
}
|
|
19845
|
+
function anyDoc(docs, predicate) {
|
|
19846
|
+
return findDoc(docs, predicate) !== void 0;
|
|
19847
|
+
}
|
|
19848
|
+
function extractIntent(testCase) {
|
|
19849
|
+
const docs = testCase.story.docs;
|
|
19850
|
+
const section = findDoc(
|
|
19851
|
+
docs,
|
|
19852
|
+
(d) => d.kind === "section" && INTENT_SECTION_TITLE.test(d.title)
|
|
19853
|
+
);
|
|
19854
|
+
if (section && section.kind === "section") return section.markdown;
|
|
19855
|
+
const note = findDoc(docs, (d) => d.kind === "note");
|
|
19856
|
+
if (note && note.kind === "note") return note.text;
|
|
19857
|
+
return void 0;
|
|
19858
|
+
}
|
|
19859
|
+
function hasScreenshot(testCase) {
|
|
19860
|
+
if (testCase.attachments.some((a) => a.mediaType.startsWith("image/"))) {
|
|
19861
|
+
return true;
|
|
19862
|
+
}
|
|
19863
|
+
if (anyDoc(testCase.story.docs, (d) => d.kind === "screenshot")) return true;
|
|
19864
|
+
return testCase.story.steps.some(
|
|
19865
|
+
(step) => anyDoc(step.docs, (d) => d.kind === "screenshot")
|
|
19866
|
+
);
|
|
19867
|
+
}
|
|
19868
|
+
function hasOtelTrace(testCase) {
|
|
19869
|
+
return (testCase.story.otelSpans?.length ?? 0) > 0;
|
|
19870
|
+
}
|
|
19871
|
+
function gradeEvidence(testCase, audience) {
|
|
19872
|
+
if (testCase.status !== "passed") {
|
|
19873
|
+
return {
|
|
19874
|
+
strength: "none",
|
|
19875
|
+
reasons: [`test is ${testCase.status} \u2014 the proof does not hold`]
|
|
19876
|
+
};
|
|
19877
|
+
}
|
|
19878
|
+
const ev = testCase.evidence;
|
|
19879
|
+
const screenshot = hasScreenshot(testCase);
|
|
19880
|
+
const otel = hasOtelTrace(testCase);
|
|
19881
|
+
const isIntegration = /\.int\.test\./i.test(testCase.sourceFile);
|
|
19882
|
+
const mutation = ev?.mutationScorePct;
|
|
19883
|
+
const changedCov = ev?.changedLineCoveragePct;
|
|
19884
|
+
const strong2 = [];
|
|
19885
|
+
if (ev?.failingFirstVerified) {
|
|
19886
|
+
strong2.push("failing-first verified (red on base ref, green on head)");
|
|
19887
|
+
}
|
|
19888
|
+
if (typeof mutation === "number" && mutation >= 80) {
|
|
19889
|
+
strong2.push(`mutation score ${mutation}% (\u226580%)`);
|
|
19890
|
+
}
|
|
19891
|
+
if (screenshot && otel) {
|
|
19892
|
+
strong2.push("backed by screenshot + OTEL trace");
|
|
19893
|
+
} else if (audience === "stakeholder" && (screenshot || otel)) {
|
|
19894
|
+
strong2.push(`stakeholder proof: ${screenshot ? "screenshot" : "OTEL trace"}`);
|
|
19895
|
+
}
|
|
19896
|
+
if (strong2.length > 0) return { strength: "strong", reasons: strong2 };
|
|
19897
|
+
const moderate = [];
|
|
19898
|
+
if (screenshot) moderate.push("screenshot attached");
|
|
19899
|
+
if (otel) moderate.push("OTEL trace attached");
|
|
19900
|
+
if (typeof mutation === "number" && mutation >= 50) {
|
|
19901
|
+
moderate.push(`mutation score ${mutation}%`);
|
|
19902
|
+
}
|
|
19903
|
+
if (typeof changedCov === "number" && changedCov >= 80) {
|
|
19904
|
+
moderate.push(`changed-line coverage ${changedCov}%`);
|
|
19905
|
+
}
|
|
19906
|
+
if (isIntegration) moderate.push("integration-level test");
|
|
19907
|
+
if (moderate.length > 0) return { strength: "moderate", reasons: moderate };
|
|
19908
|
+
return {
|
|
19909
|
+
strength: "weak",
|
|
19910
|
+
reasons: [
|
|
19911
|
+
"passing test only \u2014 no corroborating evidence (add e2e proof, mutation score, or failing-first)"
|
|
19912
|
+
]
|
|
19913
|
+
};
|
|
19914
|
+
}
|
|
19915
|
+
function toClaim(testCase, changedSourcePaths) {
|
|
19916
|
+
const audience = deriveAudience(testCase.sourceFile, testCase.tags);
|
|
19917
|
+
const changeType = deriveChangeType(testCase.tags);
|
|
19918
|
+
const { strength, reasons } = gradeEvidence(testCase, audience);
|
|
19919
|
+
const key = testBaseKey(testCase.sourceFile);
|
|
19920
|
+
const coversFiles = changedSourcePaths.filter(
|
|
19921
|
+
(path10) => sourceBaseKey(path10) === key
|
|
19922
|
+
);
|
|
19923
|
+
return {
|
|
19924
|
+
id: testCase.id,
|
|
19925
|
+
scenario: testCase.story.scenario,
|
|
19926
|
+
sourceFile: testCase.sourceFile,
|
|
19927
|
+
sourceLine: testCase.sourceLine,
|
|
19928
|
+
status: testCase.status,
|
|
19929
|
+
audience,
|
|
19930
|
+
changeType,
|
|
19931
|
+
strength,
|
|
19932
|
+
strengthReasons: reasons,
|
|
19933
|
+
intent: extractIntent(testCase),
|
|
19934
|
+
coversFiles,
|
|
19935
|
+
testCase
|
|
19936
|
+
};
|
|
19937
|
+
}
|
|
19938
|
+
function bandFor(claims) {
|
|
19939
|
+
if (claims.length === 0) return "uncovered";
|
|
19940
|
+
const maxRank = Math.max(...claims.map((c) => STRENGTH_RANK[c.strength]));
|
|
19941
|
+
return maxRank >= STRENGTH_RANK.moderate ? "covered" : "weak";
|
|
19942
|
+
}
|
|
19943
|
+
var AUDIENCE_ORDER = {
|
|
19944
|
+
stakeholder: 0,
|
|
19945
|
+
engineer: 1
|
|
19946
|
+
};
|
|
19947
|
+
function buildReview(run, context = { changedFiles: [] }) {
|
|
19948
|
+
const changedSource = context.changedFiles.filter(
|
|
19949
|
+
(f) => isReviewableSource(f.path)
|
|
19950
|
+
);
|
|
19951
|
+
const changedSourcePaths = changedSource.map((f) => f.path);
|
|
19952
|
+
const claims = run.testCases.map((tc) => toClaim(tc, changedSourcePaths));
|
|
19953
|
+
const changedFiles = changedSource.map((file) => {
|
|
19954
|
+
const covering = claims.filter((c) => c.coversFiles.includes(file.path));
|
|
19955
|
+
return {
|
|
19956
|
+
path: file.path,
|
|
19957
|
+
changeKind: file.changeKind,
|
|
19958
|
+
band: bandFor(covering),
|
|
19959
|
+
claims: covering.map((c) => ({
|
|
19960
|
+
id: c.id,
|
|
19961
|
+
scenario: c.scenario,
|
|
19962
|
+
strength: c.strength
|
|
19963
|
+
}))
|
|
19964
|
+
};
|
|
19965
|
+
});
|
|
19966
|
+
const sortedClaims = [...claims].sort((a, b) => {
|
|
19967
|
+
if (AUDIENCE_ORDER[a.audience] !== AUDIENCE_ORDER[b.audience]) {
|
|
19968
|
+
return AUDIENCE_ORDER[a.audience] - AUDIENCE_ORDER[b.audience];
|
|
19969
|
+
}
|
|
19970
|
+
if (STRENGTH_RANK[a.strength] !== STRENGTH_RANK[b.strength]) {
|
|
19971
|
+
return STRENGTH_RANK[a.strength] - STRENGTH_RANK[b.strength];
|
|
19972
|
+
}
|
|
19973
|
+
if (a.sourceFile !== b.sourceFile) {
|
|
19974
|
+
return a.sourceFile.localeCompare(b.sourceFile);
|
|
19975
|
+
}
|
|
19976
|
+
return a.scenario.localeCompare(b.scenario);
|
|
19977
|
+
});
|
|
19978
|
+
const bandRank = { uncovered: 0, weak: 1, covered: 2 };
|
|
19979
|
+
const sortedFiles = [...changedFiles].sort((a, b) => {
|
|
19980
|
+
if (bandRank[a.band] !== bandRank[b.band]) {
|
|
19981
|
+
return bandRank[a.band] - bandRank[b.band];
|
|
19982
|
+
}
|
|
19983
|
+
return a.path.localeCompare(b.path);
|
|
19984
|
+
});
|
|
19985
|
+
const summary = buildSummary2(sortedClaims, sortedFiles);
|
|
19986
|
+
return {
|
|
19987
|
+
run,
|
|
19988
|
+
context,
|
|
19989
|
+
summary,
|
|
19990
|
+
claims: sortedClaims,
|
|
19991
|
+
changedFiles: sortedFiles
|
|
19992
|
+
};
|
|
19993
|
+
}
|
|
19994
|
+
function buildSummary2(claims, changedFiles) {
|
|
19995
|
+
const byAudience = {
|
|
19996
|
+
stakeholder: 0,
|
|
19997
|
+
engineer: 0
|
|
19998
|
+
};
|
|
19999
|
+
const byStrength = {
|
|
20000
|
+
none: 0,
|
|
20001
|
+
weak: 0,
|
|
20002
|
+
moderate: 0,
|
|
20003
|
+
strong: 0
|
|
20004
|
+
};
|
|
20005
|
+
for (const claim of claims) {
|
|
20006
|
+
byAudience[claim.audience] += 1;
|
|
20007
|
+
byStrength[claim.strength] += 1;
|
|
20008
|
+
}
|
|
20009
|
+
return {
|
|
20010
|
+
totalClaims: claims.length,
|
|
20011
|
+
byAudience,
|
|
20012
|
+
byStrength,
|
|
20013
|
+
changedSourceFiles: changedFiles.length,
|
|
20014
|
+
uncovered: changedFiles.filter((f) => f.band === "uncovered").length,
|
|
20015
|
+
weaklyCovered: changedFiles.filter((f) => f.band === "weak").length,
|
|
20016
|
+
covered: changedFiles.filter((f) => f.band === "covered").length
|
|
20017
|
+
};
|
|
20018
|
+
}
|
|
20019
|
+
|
|
20020
|
+
// src/formatters/review-markdown.ts
|
|
20021
|
+
var STRENGTH_BADGE = {
|
|
20022
|
+
strong: "\u{1F7E2} strong",
|
|
20023
|
+
moderate: "\u{1F7E1} moderate",
|
|
20024
|
+
weak: "\u{1F7E0} weak",
|
|
20025
|
+
none: "\u{1F534} none"
|
|
20026
|
+
};
|
|
20027
|
+
function statusIcon2(status) {
|
|
20028
|
+
switch (status) {
|
|
20029
|
+
case "passed":
|
|
20030
|
+
return "\u2705";
|
|
20031
|
+
case "failed":
|
|
20032
|
+
return "\u274C";
|
|
20033
|
+
case "skipped":
|
|
20034
|
+
return "\u2298";
|
|
20035
|
+
default:
|
|
20036
|
+
return "\u2022";
|
|
20037
|
+
}
|
|
20038
|
+
}
|
|
20039
|
+
function escapeCell2(value) {
|
|
20040
|
+
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
20041
|
+
}
|
|
20042
|
+
function intentSummary(intent) {
|
|
20043
|
+
const firstLine = intent.split("\n").find((l) => l.trim().length > 0) ?? "";
|
|
20044
|
+
const trimmed = firstLine.trim();
|
|
20045
|
+
return trimmed.length > 200 ? `${trimmed.slice(0, 197)}\u2026` : trimmed;
|
|
20046
|
+
}
|
|
20047
|
+
function renderTicket2(ticket) {
|
|
20048
|
+
return ticket.url ? `[${ticket.id}](${ticket.url})` : `\`${ticket.id}\``;
|
|
20049
|
+
}
|
|
20050
|
+
function renderUncoveredBand(lines, files) {
|
|
20051
|
+
const uncovered = files.filter((f) => f.band === "uncovered");
|
|
20052
|
+
if (uncovered.length === 0) return;
|
|
20053
|
+
lines.push(`## \u{1F534} Changed code with no evidence (${uncovered.length})`);
|
|
20054
|
+
lines.push("");
|
|
20055
|
+
lines.push("Start here \u2014 these changed source files have no claim or test behind them.");
|
|
20056
|
+
lines.push("");
|
|
20057
|
+
for (const file of uncovered) {
|
|
20058
|
+
lines.push(`- \`${file.path}\` _(${file.changeKind})_`);
|
|
20059
|
+
}
|
|
20060
|
+
lines.push("");
|
|
20061
|
+
}
|
|
20062
|
+
function renderWeakBand(lines, files) {
|
|
20063
|
+
const weak = files.filter((f) => f.band === "weak");
|
|
20064
|
+
if (weak.length === 0) return;
|
|
20065
|
+
lines.push(`## \u{1F7E1} Changed code with weak evidence (${weak.length})`);
|
|
20066
|
+
lines.push("");
|
|
20067
|
+
for (const file of weak) {
|
|
20068
|
+
const covered = file.claims.map((c) => `${escapeCell2(c.scenario)} (${c.strength})`).join(", ");
|
|
20069
|
+
lines.push(`- \`${file.path}\` _(${file.changeKind})_ \u2014 only: ${covered}`);
|
|
20070
|
+
}
|
|
20071
|
+
lines.push("");
|
|
20072
|
+
}
|
|
20073
|
+
function renderClaim(lines, claim) {
|
|
20074
|
+
lines.push(`### ${statusIcon2(claim.status)} ${claim.scenario}`);
|
|
20075
|
+
lines.push("");
|
|
20076
|
+
lines.push(`- File: \`${claim.sourceFile}:${claim.sourceLine}\``);
|
|
20077
|
+
if (claim.changeType !== "unknown") {
|
|
20078
|
+
lines.push(`- Change: \`${claim.changeType}\``);
|
|
20079
|
+
}
|
|
20080
|
+
const tickets = claim.testCase.story.tickets ?? [];
|
|
20081
|
+
if (tickets.length > 0) {
|
|
20082
|
+
lines.push(`- Tickets: ${tickets.map(renderTicket2).join(", ")}`);
|
|
20083
|
+
}
|
|
20084
|
+
lines.push(
|
|
20085
|
+
`- Evidence: ${STRENGTH_BADGE[claim.strength]} \u2014 ${claim.strengthReasons.join("; ")}`
|
|
20086
|
+
);
|
|
20087
|
+
if (claim.coversFiles.length > 0) {
|
|
20088
|
+
lines.push(
|
|
20089
|
+
`- Covers: ${claim.coversFiles.map((f) => `\`${f}\``).join(", ")}`
|
|
20090
|
+
);
|
|
20091
|
+
}
|
|
20092
|
+
if (claim.intent) {
|
|
20093
|
+
lines.push(`- Why: ${escapeCell2(intentSummary(claim.intent))}`);
|
|
20094
|
+
}
|
|
20095
|
+
lines.push("");
|
|
20096
|
+
}
|
|
20097
|
+
function renderAudienceSection(lines, title, claims) {
|
|
20098
|
+
if (claims.length === 0) return;
|
|
20099
|
+
lines.push(`## ${title} (${claims.length})`);
|
|
20100
|
+
lines.push("");
|
|
20101
|
+
for (const claim of claims) {
|
|
20102
|
+
renderClaim(lines, claim);
|
|
20103
|
+
}
|
|
20104
|
+
}
|
|
20105
|
+
var ReviewMarkdownFormatter = class {
|
|
20106
|
+
title;
|
|
20107
|
+
constructor(options = {}) {
|
|
20108
|
+
this.title = options.title ?? "Evidence Review";
|
|
20109
|
+
}
|
|
20110
|
+
format(review) {
|
|
20111
|
+
const lines = [];
|
|
20112
|
+
const { summary, context } = review;
|
|
20113
|
+
lines.push(`# ${this.title}`);
|
|
20114
|
+
lines.push("");
|
|
20115
|
+
if (context.baseRef || context.headRef) {
|
|
20116
|
+
lines.push(
|
|
20117
|
+
`Comparing \`${context.baseRef ?? "base"}\` \u2192 \`${context.headRef ?? "head"}\`.`
|
|
20118
|
+
);
|
|
20119
|
+
lines.push("");
|
|
20120
|
+
}
|
|
20121
|
+
lines.push("## Review priority");
|
|
20122
|
+
lines.push("");
|
|
20123
|
+
if (summary.changedSourceFiles === 0) {
|
|
20124
|
+
lines.push(
|
|
20125
|
+
"No changed source files supplied \u2014 showing claims and evidence only."
|
|
20126
|
+
);
|
|
20127
|
+
} else if (summary.uncovered > 0) {
|
|
20128
|
+
lines.push(
|
|
20129
|
+
`Review the ${summary.uncovered} unaccounted-for file(s) first: changed code with no evidence behind it.`
|
|
20130
|
+
);
|
|
20131
|
+
} else if (summary.weaklyCovered > 0) {
|
|
20132
|
+
lines.push(
|
|
20133
|
+
`No unaccounted-for changes. Review ${summary.weaklyCovered} weakly-covered file(s) next.`
|
|
20134
|
+
);
|
|
20135
|
+
} else {
|
|
20136
|
+
lines.push("Every changed source file is backed by at least moderate evidence.");
|
|
20137
|
+
}
|
|
20138
|
+
lines.push("");
|
|
20139
|
+
if (summary.changedSourceFiles > 0) {
|
|
20140
|
+
lines.push("| \u{1F534} Uncovered | \u{1F7E1} Weak | \u{1F7E2} Covered | Changed files |");
|
|
20141
|
+
lines.push("| ---: | ---: | ---: | ---: |");
|
|
20142
|
+
lines.push(
|
|
20143
|
+
`| ${summary.uncovered} | ${summary.weaklyCovered} | ${summary.covered} | ${summary.changedSourceFiles} |`
|
|
20144
|
+
);
|
|
20145
|
+
lines.push("");
|
|
20146
|
+
}
|
|
20147
|
+
lines.push("| Claims | Stakeholder | Engineer | Strong | Moderate | Weak | None |");
|
|
20148
|
+
lines.push("| ---: | ---: | ---: | ---: | ---: | ---: | ---: |");
|
|
20149
|
+
lines.push(
|
|
20150
|
+
`| ${summary.totalClaims} | ${summary.byAudience.stakeholder} | ${summary.byAudience.engineer} | ${summary.byStrength.strong} | ${summary.byStrength.moderate} | ${summary.byStrength.weak} | ${summary.byStrength.none} |`
|
|
20151
|
+
);
|
|
20152
|
+
lines.push("");
|
|
20153
|
+
renderUncoveredBand(lines, review.changedFiles);
|
|
20154
|
+
renderWeakBand(lines, review.changedFiles);
|
|
20155
|
+
renderAudienceSection(
|
|
20156
|
+
lines,
|
|
20157
|
+
"Stakeholder behaviour",
|
|
20158
|
+
review.claims.filter((c) => c.audience === "stakeholder")
|
|
20159
|
+
);
|
|
20160
|
+
renderAudienceSection(
|
|
20161
|
+
lines,
|
|
20162
|
+
"Engineer changes",
|
|
20163
|
+
review.claims.filter((c) => c.audience === "engineer")
|
|
20164
|
+
);
|
|
20165
|
+
return lines.join("\n").trimEnd();
|
|
20166
|
+
}
|
|
20167
|
+
};
|
|
20168
|
+
|
|
20169
|
+
// src/formatters/review-html.ts
|
|
20170
|
+
function escapeHtml3(value) {
|
|
20171
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
20172
|
+
}
|
|
20173
|
+
var STRENGTH_LABEL = {
|
|
20174
|
+
strong: "Strong",
|
|
20175
|
+
moderate: "Moderate",
|
|
20176
|
+
weak: "Weak",
|
|
20177
|
+
none: "None"
|
|
20178
|
+
};
|
|
20179
|
+
function statusIcon3(status) {
|
|
20180
|
+
switch (status) {
|
|
20181
|
+
case "passed":
|
|
20182
|
+
return "\u2705";
|
|
20183
|
+
case "failed":
|
|
20184
|
+
return "\u274C";
|
|
20185
|
+
case "skipped":
|
|
20186
|
+
return "\u2298";
|
|
20187
|
+
default:
|
|
20188
|
+
return "\u2022";
|
|
20189
|
+
}
|
|
20190
|
+
}
|
|
20191
|
+
function formatStep3(step) {
|
|
20192
|
+
return `<li><strong>${escapeHtml3(step.keyword)}</strong> ${escapeHtml3(step.text)}</li>`;
|
|
20193
|
+
}
|
|
20194
|
+
function inlineDoc(doc) {
|
|
20195
|
+
switch (doc.kind) {
|
|
20196
|
+
case "note":
|
|
20197
|
+
return escapeHtml3(doc.text);
|
|
20198
|
+
case "section":
|
|
20199
|
+
return `<strong>${escapeHtml3(doc.title)}</strong>: ${escapeHtml3(doc.markdown)}`;
|
|
20200
|
+
case "kv":
|
|
20201
|
+
return `${escapeHtml3(doc.label)}: ${escapeHtml3(String(doc.value))}`;
|
|
20202
|
+
case "code":
|
|
20203
|
+
return `${escapeHtml3(doc.label)}: <code>${escapeHtml3(doc.content)}</code>`;
|
|
20204
|
+
case "link":
|
|
20205
|
+
return `${escapeHtml3(doc.label)}: ${escapeHtml3(doc.url)}`;
|
|
20206
|
+
default:
|
|
20207
|
+
return escapeHtml3(doc.kind);
|
|
20208
|
+
}
|
|
20209
|
+
}
|
|
20210
|
+
function renderEvidenceArtifacts(testCase) {
|
|
20211
|
+
const parts = [];
|
|
20212
|
+
for (const att of testCase.attachments) {
|
|
20213
|
+
if (att.mediaType.startsWith("image/") && att.contentEncoding === "BASE64") {
|
|
20214
|
+
parts.push(
|
|
20215
|
+
`<img class="shot" alt="${escapeHtml3(att.name)}" src="data:${escapeHtml3(att.mediaType)};base64,${att.body}" />`
|
|
20216
|
+
);
|
|
20217
|
+
}
|
|
20218
|
+
}
|
|
20219
|
+
if ((testCase.story.otelSpans?.length ?? 0) > 0) {
|
|
20220
|
+
parts.push(
|
|
20221
|
+
`<p class="trace-note">\u{1F4E1} ${testCase.story.otelSpans.length} OTEL span(s) captured</p>`
|
|
20222
|
+
);
|
|
20223
|
+
}
|
|
20224
|
+
return parts.length > 0 ? `<div class="artifacts">${parts.join("")}</div>` : "";
|
|
20225
|
+
}
|
|
20226
|
+
function renderTicketPills(claim) {
|
|
20227
|
+
const tickets = claim.testCase.story.tickets ?? [];
|
|
20228
|
+
if (tickets.length === 0) return "";
|
|
20229
|
+
return `<div class="ticket-row">${tickets.map((ticket) => {
|
|
20230
|
+
const label = escapeHtml3(ticket.id);
|
|
20231
|
+
if (ticket.url) {
|
|
20232
|
+
return `<a class="ticket-pill" href="${escapeHtml3(ticket.url)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
|
|
20233
|
+
}
|
|
20234
|
+
return `<span class="ticket-pill">${label}</span>`;
|
|
20235
|
+
}).join("")}</div>`;
|
|
20236
|
+
}
|
|
20237
|
+
function renderClaimCard(claim) {
|
|
20238
|
+
const ticketSearch = (claim.testCase.story.tickets ?? []).map((ticket) => ticket.id).join(" ");
|
|
20239
|
+
const search = escapeHtml3(
|
|
20240
|
+
`${claim.scenario} ${claim.sourceFile} ${claim.changeType} ${claim.audience} ${claim.strength} ${ticketSearch}`
|
|
20241
|
+
).toLowerCase();
|
|
20242
|
+
const steps = claim.testCase.story.steps.length > 0 ? `<ul class="step-list">${claim.testCase.story.steps.map(formatStep3).join("")}</ul>` : "";
|
|
20243
|
+
const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${escapeHtml3(r)}</li>`).join("")}</ul>`;
|
|
20244
|
+
const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${escapeHtml3(claim.intent)}</div>` : "";
|
|
20245
|
+
const covers = claim.coversFiles.length > 0 ? `<p class="covers">Covers ${claim.coversFiles.map((f) => `<code>${escapeHtml3(f)}</code>`).join(", ")}</p>` : "";
|
|
20246
|
+
const docs = (claim.testCase.story.docs ?? []).filter(
|
|
20247
|
+
(d) => d.kind === "section" || d.kind === "note"
|
|
20248
|
+
);
|
|
20249
|
+
const extraDocs = docs.length > 0 && claim.intent === void 0 ? `<div class="intent">${docs.map(inlineDoc).join("<br>")}</div>` : "";
|
|
20250
|
+
return `
|
|
20251
|
+
<article class="claim-card" data-audience="${claim.audience}" data-strength="${claim.strength}" data-search="${search}">
|
|
20252
|
+
<header class="claim-header">
|
|
20253
|
+
<div>
|
|
20254
|
+
<span class="strength-badge strength-${claim.strength}">${STRENGTH_LABEL[claim.strength]}</span>
|
|
20255
|
+
${claim.changeType !== "unknown" ? `<span class="change-pill">${escapeHtml3(claim.changeType)}</span>` : ""}
|
|
20256
|
+
<h3>${statusIcon3(claim.status)} ${escapeHtml3(claim.scenario)}</h3>
|
|
20257
|
+
<p class="source">${escapeHtml3(`${claim.sourceFile}:${claim.sourceLine}`)}</p>
|
|
20258
|
+
${renderTicketPills(claim)}
|
|
20259
|
+
</div>
|
|
20260
|
+
</header>
|
|
20261
|
+
${intent}${extraDocs}
|
|
20262
|
+
<div class="evidence-block">
|
|
20263
|
+
<span class="evidence-label">Evidence</span>
|
|
20264
|
+
${reasons}
|
|
20265
|
+
</div>
|
|
20266
|
+
${covers}
|
|
20267
|
+
${renderEvidenceArtifacts(claim.testCase)}
|
|
20268
|
+
${steps}
|
|
20269
|
+
</article>`;
|
|
20270
|
+
}
|
|
20271
|
+
function renderChangedFileRow(file) {
|
|
20272
|
+
const claims = file.claims.length > 0 ? file.claims.map((c) => `${escapeHtml3(c.scenario)} <em>(${c.strength})</em>`).join(", ") : "\u2014";
|
|
20273
|
+
return `<tr data-band="${file.band}">
|
|
20274
|
+
<td><span class="band-dot band-${file.band}"></span></td>
|
|
20275
|
+
<td><code>${escapeHtml3(file.path)}</code></td>
|
|
20276
|
+
<td>${escapeHtml3(file.changeKind)}</td>
|
|
20277
|
+
<td>${claims}</td>
|
|
20278
|
+
</tr>`;
|
|
20279
|
+
}
|
|
20280
|
+
function renderAudienceSection2(title, claims) {
|
|
20281
|
+
if (claims.length === 0) return "";
|
|
20282
|
+
return `<section class="audience-section">
|
|
20283
|
+
<h2>${escapeHtml3(title)} <span class="count">${claims.length}</span></h2>
|
|
20284
|
+
<div class="claim-list">${claims.map(renderClaimCard).join("\n")}</div>
|
|
20285
|
+
</section>`;
|
|
20286
|
+
}
|
|
20287
|
+
var REVIEW_CSS = `
|
|
20288
|
+
* { box-sizing: border-box; }
|
|
20289
|
+
body { margin: 0; font-family: var(--font-sans, system-ui, sans-serif); background: var(--background); color: var(--foreground); }
|
|
20290
|
+
main { max-width: 1100px; margin: 0 auto; padding: 32px 20px 80px; }
|
|
20291
|
+
h1, h2, h3, p { margin: 0; }
|
|
20292
|
+
.review-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
|
20293
|
+
.subtle { color: var(--muted-foreground); margin-top: 6px; }
|
|
20294
|
+
.theme-toggle { background: var(--secondary); border: 1px solid var(--border); border-radius: 8px; padding: 8px 12px; cursor: pointer; font-size: 1.1rem; color: var(--foreground); }
|
|
20295
|
+
.card, .claim-card, .summary-card, .panel { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius, 16px); }
|
|
20296
|
+
.hero-card { padding: 24px; margin-bottom: 20px; }
|
|
20297
|
+
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; margin-bottom: 20px; }
|
|
20298
|
+
.summary-card { padding: 14px 16px; }
|
|
20299
|
+
.summary-card strong { display: block; font-size: 1.8rem; }
|
|
20300
|
+
.priority-banner { padding: 18px 20px; margin-bottom: 20px; background: linear-gradient(135deg, color-mix(in srgb, var(--destructive) 10%, transparent), var(--card)); }
|
|
20301
|
+
.panel { padding: 18px; margin-bottom: 24px; }
|
|
20302
|
+
table { width: 100%; border-collapse: collapse; }
|
|
20303
|
+
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|
20304
|
+
th { color: var(--muted-foreground); font-weight: 600; }
|
|
20305
|
+
.band-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; }
|
|
20306
|
+
.band-uncovered { background: var(--destructive); }
|
|
20307
|
+
.band-weak { background: var(--warning, #b58900); }
|
|
20308
|
+
.band-covered { background: var(--success, #2e7d32); }
|
|
20309
|
+
.toolbar { position: sticky; top: 12px; z-index: 2; display: flex; flex-wrap: wrap; gap: 10px; padding: 14px; margin-bottom: 20px; }
|
|
20310
|
+
.toolbar input { flex: 1 1 240px; border: 1px solid var(--border); border-radius: 999px; padding: 10px 14px; font: inherit; background: var(--background); color: var(--foreground); }
|
|
20311
|
+
.toolbar button { border: 1px solid var(--border); background: var(--secondary); border-radius: 999px; padding: 10px 14px; font: inherit; cursor: pointer; color: var(--foreground); }
|
|
20312
|
+
.toolbar button.active { background: var(--foreground); color: var(--background); }
|
|
20313
|
+
.audience-section { margin-bottom: 28px; }
|
|
20314
|
+
.audience-section h2 { margin-bottom: 12px; }
|
|
20315
|
+
.count { color: var(--muted-foreground); font-weight: 400; }
|
|
20316
|
+
.claim-list { display: grid; gap: 14px; }
|
|
20317
|
+
.claim-card { padding: 18px; }
|
|
20318
|
+
.claim-header h3 { margin-top: 8px; }
|
|
20319
|
+
.source { color: var(--muted-foreground); font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.85rem; margin-top: 4px; }
|
|
20320
|
+
.ticket-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
|
20321
|
+
.ticket-pill { display: inline-flex; align-items: center; border: 1px solid var(--border); border-radius: 999px; padding: 3px 9px; color: var(--muted-foreground); background: var(--background); font-size: 0.78rem; text-decoration: none; }
|
|
20322
|
+
.ticket-pill:hover { color: var(--foreground); border-color: var(--muted-foreground); }
|
|
20323
|
+
.strength-badge, .change-pill { display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 999px; font-size: 0.8rem; margin-right: 6px; }
|
|
20324
|
+
.change-pill { background: var(--secondary); }
|
|
20325
|
+
.strength-strong { background: color-mix(in srgb, var(--success, #2e7d32) 18%, transparent); color: var(--success, #2e7d32); }
|
|
20326
|
+
.strength-moderate { background: color-mix(in srgb, var(--warning, #b58900) 20%, transparent); color: var(--warning, #b58900); }
|
|
20327
|
+
.strength-weak { background: color-mix(in srgb, #d2691e 20%, transparent); color: #b5530a; }
|
|
20328
|
+
.strength-none { background: color-mix(in srgb, var(--destructive) 16%, transparent); color: var(--destructive); }
|
|
20329
|
+
.intent { margin: 12px 0; padding: 10px 12px; border-left: 3px solid var(--border); background: color-mix(in srgb, var(--card) 60%, var(--background)); border-radius: 6px; }
|
|
20330
|
+
.intent-label { font-weight: 700; margin-right: 6px; }
|
|
20331
|
+
.evidence-block { margin-top: 10px; }
|
|
20332
|
+
.evidence-label { font-weight: 600; color: var(--muted-foreground); }
|
|
20333
|
+
.reasons { margin: 6px 0 0; padding-left: 18px; }
|
|
20334
|
+
.covers { color: var(--muted-foreground); margin-top: 8px; font-size: 0.9rem; }
|
|
20335
|
+
.artifacts { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-start; }
|
|
20336
|
+
.shot { max-width: 280px; max-height: 200px; border: 1px solid var(--border); border-radius: 8px; }
|
|
20337
|
+
.trace-note { color: var(--muted-foreground); }
|
|
20338
|
+
.step-list { margin: 12px 0 0; padding-left: 18px; color: var(--muted-foreground); }
|
|
20339
|
+
`;
|
|
20340
|
+
var JS_THEME_TOGGLE2 = `
|
|
20341
|
+
function getSystemTheme() { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; }
|
|
20342
|
+
function getEffectiveTheme() { var s = localStorage.getItem('review-theme'); return (s === 'dark' || s === 'light') ? s : getSystemTheme(); }
|
|
20343
|
+
function toggleTheme() { var n = getEffectiveTheme() === 'dark' ? 'light' : 'dark'; localStorage.setItem('review-theme', n); applyTheme(n); }
|
|
20344
|
+
function applyTheme(t) {
|
|
20345
|
+
document.documentElement.setAttribute('data-theme', t);
|
|
20346
|
+
var b = document.querySelector('.theme-toggle');
|
|
20347
|
+
if (b) { b.textContent = t === 'dark' ? '\\u2600\\ufe0f' : '\\ud83c\\udf19'; }
|
|
20348
|
+
}
|
|
20349
|
+
`;
|
|
20350
|
+
var ReviewHtmlFormatter = class {
|
|
20351
|
+
title;
|
|
20352
|
+
theme;
|
|
20353
|
+
darkMode;
|
|
20354
|
+
constructor(options = {}) {
|
|
20355
|
+
this.title = options.title ?? "Evidence Review";
|
|
20356
|
+
this.theme = resolveTheme(options.theme ?? "default");
|
|
20357
|
+
this.darkMode = options.darkMode ?? true;
|
|
20358
|
+
}
|
|
20359
|
+
format(review) {
|
|
20360
|
+
const { summary, context } = review;
|
|
20361
|
+
const priority = summary.changedSourceFiles === 0 ? "No changed source files supplied \u2014 showing claims and evidence only." : summary.uncovered > 0 ? `${summary.uncovered} changed file(s) have no evidence. Review them first.` : summary.weaklyCovered > 0 ? `No unaccounted-for changes. ${summary.weaklyCovered} file(s) are weakly covered.` : "Every changed source file is backed by at least moderate evidence.";
|
|
20362
|
+
const changedFilesPanel = summary.changedSourceFiles > 0 ? `<section class="panel">
|
|
20363
|
+
<h2>Changed files</h2>
|
|
20364
|
+
<table>
|
|
20365
|
+
<thead><tr><th></th><th>File</th><th>Change</th><th>Evidence</th></tr></thead>
|
|
20366
|
+
<tbody>${review.changedFiles.map(renderChangedFileRow).join("")}</tbody>
|
|
20367
|
+
</table>
|
|
20368
|
+
</section>` : "";
|
|
20369
|
+
const themeToggleHtml = this.darkMode ? `<button type="button" class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle theme"></button>` : "";
|
|
20370
|
+
const themeInitJs = this.darkMode ? `${JS_THEME_TOGGLE2}
|
|
20371
|
+
applyTheme(getEffectiveTheme());` : "";
|
|
20372
|
+
const themeAttr = this.darkMode ? ' data-theme="light"' : "";
|
|
20373
|
+
const refsLine = context.baseRef || context.headRef ? `<p class="subtle">Comparing ${escapeHtml3(context.baseRef ?? "base")} \u2192 ${escapeHtml3(context.headRef ?? "head")}</p>` : "";
|
|
20374
|
+
return `<!doctype html>
|
|
20375
|
+
<html lang="en"${themeAttr}>
|
|
20376
|
+
<head>
|
|
20377
|
+
<meta charset="utf-8" />
|
|
20378
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
20379
|
+
<title>${escapeHtml3(this.title)}</title>
|
|
20380
|
+
<style>
|
|
20381
|
+
${this.theme.css}
|
|
20382
|
+
${REVIEW_CSS}
|
|
20383
|
+
</style>
|
|
20384
|
+
</head>
|
|
20385
|
+
<body>
|
|
20386
|
+
<main>
|
|
20387
|
+
<div class="hero-card card">
|
|
20388
|
+
<div class="review-header">
|
|
20389
|
+
<h1>${escapeHtml3(this.title)}</h1>
|
|
20390
|
+
${themeToggleHtml}
|
|
20391
|
+
</div>
|
|
20392
|
+
${refsLine}
|
|
20393
|
+
</div>
|
|
20394
|
+
<section class="summary-grid">
|
|
20395
|
+
<div class="summary-card"><strong>${summary.uncovered}</strong><span>\u{1F534} Uncovered</span></div>
|
|
20396
|
+
<div class="summary-card"><strong>${summary.weaklyCovered}</strong><span>\u{1F7E1} Weak</span></div>
|
|
20397
|
+
<div class="summary-card"><strong>${summary.covered}</strong><span>\u{1F7E2} Covered</span></div>
|
|
20398
|
+
<div class="summary-card"><strong>${summary.totalClaims}</strong><span>Claims</span></div>
|
|
20399
|
+
<div class="summary-card"><strong>${summary.byStrength.strong}</strong><span>Strong</span></div>
|
|
20400
|
+
<div class="summary-card"><strong>${summary.byStrength.weak + summary.byStrength.none}</strong><span>Weak/None</span></div>
|
|
20401
|
+
</section>
|
|
20402
|
+
<section class="card priority-banner">
|
|
20403
|
+
<h2>Review priority</h2>
|
|
20404
|
+
<p class="subtle">${escapeHtml3(priority)}</p>
|
|
20405
|
+
</section>
|
|
20406
|
+
${changedFilesPanel}
|
|
20407
|
+
<section class="toolbar">
|
|
20408
|
+
<input type="search" placeholder="Filter claims by scenario, file, change-type" aria-label="Filter claims" />
|
|
20409
|
+
<button type="button" class="active" data-filter="all">All</button>
|
|
20410
|
+
<button type="button" data-filter="stakeholder">Stakeholder</button>
|
|
20411
|
+
<button type="button" data-filter="engineer">Engineer</button>
|
|
20412
|
+
<button type="button" data-filter="weak">Weak/None</button>
|
|
20413
|
+
</section>
|
|
20414
|
+
${renderAudienceSection2("Stakeholder behaviour", review.claims.filter((c) => c.audience === "stakeholder"))}
|
|
20415
|
+
${renderAudienceSection2("Engineer changes", review.claims.filter((c) => c.audience === "engineer"))}
|
|
20416
|
+
</main>
|
|
20417
|
+
<script>
|
|
20418
|
+
${themeInitJs}
|
|
20419
|
+
const input = document.querySelector('input[type="search"]');
|
|
20420
|
+
const buttons = Array.from(document.querySelectorAll('[data-filter]'));
|
|
20421
|
+
const cards = Array.from(document.querySelectorAll('.claim-card'));
|
|
20422
|
+
let activeFilter = 'all';
|
|
20423
|
+
function applyFilters() {
|
|
20424
|
+
const query = (input.value || '').trim().toLowerCase();
|
|
20425
|
+
cards.forEach((card) => {
|
|
20426
|
+
const audience = card.getAttribute('data-audience');
|
|
20427
|
+
const strength = card.getAttribute('data-strength');
|
|
20428
|
+
const haystack = card.getAttribute('data-search') || '';
|
|
20429
|
+
let matchesFilter = activeFilter === 'all'
|
|
20430
|
+
|| audience === activeFilter
|
|
20431
|
+
|| (activeFilter === 'weak' && (strength === 'weak' || strength === 'none'));
|
|
20432
|
+
const matchesSearch = !query || haystack.includes(query);
|
|
20433
|
+
card.style.display = matchesFilter && matchesSearch ? '' : 'none';
|
|
20434
|
+
});
|
|
20435
|
+
}
|
|
20436
|
+
input.addEventListener('input', applyFilters);
|
|
20437
|
+
buttons.forEach((button) => {
|
|
20438
|
+
button.addEventListener('click', () => {
|
|
20439
|
+
activeFilter = button.getAttribute('data-filter');
|
|
20440
|
+
buttons.forEach((b) => b.classList.toggle('active', b === button));
|
|
20441
|
+
applyFilters();
|
|
20442
|
+
});
|
|
20443
|
+
});
|
|
20444
|
+
applyFilters();
|
|
20445
|
+
</script>
|
|
20446
|
+
</body>
|
|
20447
|
+
</html>`;
|
|
20448
|
+
}
|
|
20449
|
+
};
|
|
20450
|
+
|
|
19536
20451
|
// src/index.ts
|
|
19537
20452
|
var FORMAT_EXTENSIONS = {
|
|
19538
20453
|
astro: ".md",
|
|
@@ -19654,7 +20569,7 @@ var ReportGenerator = class {
|
|
|
19654
20569
|
exclude: options.exclude ?? [],
|
|
19655
20570
|
includeTags: options.includeTags ?? [],
|
|
19656
20571
|
excludeTags: options.excludeTags ?? [],
|
|
19657
|
-
formats: options.formats ?? ["
|
|
20572
|
+
formats: options.formats ?? ["html"],
|
|
19658
20573
|
outputDir: options.outputDir ?? "reports",
|
|
19659
20574
|
outputName: options.outputName ?? "index",
|
|
19660
20575
|
outputNameTimestamp: options.outputNameTimestamp ?? false,
|
|
@@ -19996,6 +20911,8 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
19996
20911
|
MIN_PERF_SAMPLES,
|
|
19997
20912
|
MarkdownFormatter,
|
|
19998
20913
|
ReportGenerator,
|
|
20914
|
+
ReviewHtmlFormatter,
|
|
20915
|
+
ReviewMarkdownFormatter,
|
|
19999
20916
|
RunDiffHtmlFormatter,
|
|
20000
20917
|
RunDiffMarkdownFormatter,
|
|
20001
20918
|
STORY_META_KEY,
|
|
@@ -20006,6 +20923,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
20006
20923
|
adaptPlaywrightRun,
|
|
20007
20924
|
adaptVitestRun,
|
|
20008
20925
|
assertValidRun,
|
|
20926
|
+
buildReview,
|
|
20009
20927
|
bundleAssets,
|
|
20010
20928
|
calculateFlakiness,
|
|
20011
20929
|
calculateStability,
|
|
@@ -20015,6 +20933,8 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
20015
20933
|
copyMarkdownAssets,
|
|
20016
20934
|
createPrCommentSummary,
|
|
20017
20935
|
createReportGenerator,
|
|
20936
|
+
deriveAudience,
|
|
20937
|
+
deriveChangeType,
|
|
20018
20938
|
deriveStepResults,
|
|
20019
20939
|
detectCI,
|
|
20020
20940
|
detectPerformanceTrend,
|
|
@@ -20026,7 +20946,10 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
20026
20946
|
generateTestCaseId,
|
|
20027
20947
|
getAvailableThemes,
|
|
20028
20948
|
getCssOnlyThemes,
|
|
20949
|
+
gradeEvidence,
|
|
20029
20950
|
hasSufficientHistory,
|
|
20951
|
+
isReviewableSource,
|
|
20952
|
+
isTestFile,
|
|
20030
20953
|
listScenarios,
|
|
20031
20954
|
loadHistory,
|
|
20032
20955
|
mergeStepResults,
|