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.js
CHANGED
|
@@ -281,7 +281,8 @@ function canonicalizeTestCase(raw, options, projectRoot) {
|
|
|
281
281
|
projectName: raw.projectName,
|
|
282
282
|
retry: raw.retry ?? 0,
|
|
283
283
|
retries: raw.retries ?? 0,
|
|
284
|
-
tags
|
|
284
|
+
tags,
|
|
285
|
+
...raw.evidence ? { evidence: raw.evidence } : {}
|
|
285
286
|
};
|
|
286
287
|
}
|
|
287
288
|
function normalizeTags(story) {
|
|
@@ -1321,11 +1322,53 @@ function initKeyboardShortcuts() {
|
|
|
1321
1322
|
});
|
|
1322
1323
|
}
|
|
1323
1324
|
|
|
1324
|
-
// Collapse/expand functionality
|
|
1325
|
+
// Collapse/expand functionality (persisted in localStorage)
|
|
1326
|
+
var COLLAPSE_KEY = 'es-collapsed-ids';
|
|
1327
|
+
|
|
1328
|
+
function loadCollapseState() {
|
|
1329
|
+
try {
|
|
1330
|
+
var raw = localStorage.getItem(COLLAPSE_KEY);
|
|
1331
|
+
return raw ? new Set(JSON.parse(raw)) : new Set();
|
|
1332
|
+
} catch (e) {
|
|
1333
|
+
return new Set();
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function saveCollapseState(set) {
|
|
1338
|
+
try {
|
|
1339
|
+
localStorage.setItem(COLLAPSE_KEY, JSON.stringify(Array.from(set)));
|
|
1340
|
+
} catch (e) { /* quota or disabled */ }
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function persistCollapse(container) {
|
|
1344
|
+
if (!container || !container.id) return;
|
|
1345
|
+
var state = loadCollapseState();
|
|
1346
|
+
if (container.classList.contains('collapsed')) {
|
|
1347
|
+
state.add(container.id);
|
|
1348
|
+
} else {
|
|
1349
|
+
state.delete(container.id);
|
|
1350
|
+
}
|
|
1351
|
+
saveCollapseState(state);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1325
1354
|
function toggleCollapse(header, container) {
|
|
1326
1355
|
container?.classList.toggle('collapsed');
|
|
1327
1356
|
const isCollapsed = container?.classList.contains('collapsed');
|
|
1328
1357
|
header.setAttribute('aria-expanded', !isCollapsed);
|
|
1358
|
+
persistCollapse(container);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function restoreCollapseState() {
|
|
1362
|
+
var state = loadCollapseState();
|
|
1363
|
+
if (state.size === 0) return;
|
|
1364
|
+
state.forEach(function(id) {
|
|
1365
|
+
var el = document.getElementById(id);
|
|
1366
|
+
if (!el) return;
|
|
1367
|
+
if (!el.classList.contains('feature') && !el.classList.contains('scenario')) return;
|
|
1368
|
+
el.classList.add('collapsed');
|
|
1369
|
+
var header = el.querySelector('.feature-header, .scenario-header');
|
|
1370
|
+
if (header) header.setAttribute('aria-expanded', 'false');
|
|
1371
|
+
});
|
|
1329
1372
|
}
|
|
1330
1373
|
|
|
1331
1374
|
function initCollapse() {
|
|
@@ -1372,14 +1415,20 @@ function expandAll() {
|
|
|
1372
1415
|
const header = el.querySelector('.feature-header, .scenario-header, .trace-view-header');
|
|
1373
1416
|
header?.setAttribute('aria-expanded', 'true');
|
|
1374
1417
|
});
|
|
1418
|
+
saveCollapseState(new Set());
|
|
1375
1419
|
}
|
|
1376
1420
|
|
|
1377
1421
|
function collapseAll() {
|
|
1422
|
+
var ids = new Set();
|
|
1378
1423
|
document.querySelectorAll('.feature, .scenario, .trace-view').forEach(el => {
|
|
1379
1424
|
el.classList.add('collapsed');
|
|
1380
1425
|
const header = el.querySelector('.feature-header, .scenario-header, .trace-view-header');
|
|
1381
1426
|
header?.setAttribute('aria-expanded', 'false');
|
|
1427
|
+
if (el.id && (el.classList.contains('feature') || el.classList.contains('scenario'))) {
|
|
1428
|
+
ids.add(el.id);
|
|
1429
|
+
}
|
|
1382
1430
|
});
|
|
1431
|
+
saveCollapseState(ids);
|
|
1383
1432
|
}
|
|
1384
1433
|
|
|
1385
1434
|
// Detail level toggle
|
|
@@ -1506,6 +1555,68 @@ function copyScenarioAsMarkdown(scenarioId) {
|
|
|
1506
1555
|
});
|
|
1507
1556
|
}
|
|
1508
1557
|
|
|
1558
|
+
// Copy scenario as Claude-ready prompt (failure investigation context)
|
|
1559
|
+
function copyScenarioAsPrompt(scenarioId) {
|
|
1560
|
+
var scenario = document.getElementById(scenarioId);
|
|
1561
|
+
if (!scenario) return;
|
|
1562
|
+
|
|
1563
|
+
var feature = scenario.closest('.feature');
|
|
1564
|
+
var featureTitle = feature ? (feature.querySelector('.feature-title') || {}).textContent || '' : '';
|
|
1565
|
+
var title = (scenario.querySelector('.scenario-name') || {}).textContent || '';
|
|
1566
|
+
var statusEl = scenario.querySelector('.status-icon');
|
|
1567
|
+
var status = statusEl && statusEl.classList.contains('status-passed') ? 'passed' :
|
|
1568
|
+
statusEl && statusEl.classList.contains('status-failed') ? 'failed' :
|
|
1569
|
+
statusEl && statusEl.classList.contains('status-skipped') ? 'skipped' : 'pending';
|
|
1570
|
+
var sourceLink = scenario.querySelector('.source-link');
|
|
1571
|
+
var source = sourceLink ? sourceLink.textContent || '' : '';
|
|
1572
|
+
var tags = Array.from(scenario.querySelectorAll('.scenario-meta .tag')).map(function(t) { return t.textContent.trim(); });
|
|
1573
|
+
var steps = scenario.querySelectorAll('.step, .step.continuation');
|
|
1574
|
+
|
|
1575
|
+
var lines = [];
|
|
1576
|
+
lines.push('I need help investigating a failing executable-story scenario.');
|
|
1577
|
+
lines.push('');
|
|
1578
|
+
if (featureTitle.trim()) lines.push('Feature: ' + featureTitle.trim());
|
|
1579
|
+
lines.push('Scenario: ' + title.trim());
|
|
1580
|
+
lines.push('Status: ' + status);
|
|
1581
|
+
if (source.trim()) lines.push('Source: ' + source.trim());
|
|
1582
|
+
if (tags.length > 0) lines.push('Tags: ' + tags.join(', '));
|
|
1583
|
+
lines.push('');
|
|
1584
|
+
lines.push('Steps:');
|
|
1585
|
+
steps.forEach(function(step) {
|
|
1586
|
+
var keyword = step.getAttribute('data-keyword') || '';
|
|
1587
|
+
var text = step.getAttribute('data-text') || '';
|
|
1588
|
+
var stepStatusEl = step.querySelector('.step-status');
|
|
1589
|
+
var marker = ' ';
|
|
1590
|
+
if (stepStatusEl) {
|
|
1591
|
+
if (stepStatusEl.classList.contains('status-failed')) marker = 'x ';
|
|
1592
|
+
else if (stepStatusEl.classList.contains('status-passed')) marker = '+ ';
|
|
1593
|
+
else if (stepStatusEl.classList.contains('status-skipped')) marker = '- ';
|
|
1594
|
+
}
|
|
1595
|
+
lines.push(marker + keyword + ' ' + text);
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
var errorBox = scenario.querySelector('.error-message');
|
|
1599
|
+
if (errorBox) {
|
|
1600
|
+
lines.push('');
|
|
1601
|
+
lines.push('Error:');
|
|
1602
|
+
lines.push((errorBox.textContent || '').trim());
|
|
1603
|
+
}
|
|
1604
|
+
var stackBox = scenario.querySelector('.error-stack');
|
|
1605
|
+
if (stackBox) {
|
|
1606
|
+
lines.push('');
|
|
1607
|
+
lines.push('Stack:');
|
|
1608
|
+
lines.push((stackBox.textContent || '').trim());
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
lines.push('');
|
|
1612
|
+
lines.push('Please read the source file, identify the root cause, and propose a fix.');
|
|
1613
|
+
|
|
1614
|
+
var text = lines.join('\\n');
|
|
1615
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
1616
|
+
showCopyToast(scenario);
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1509
1620
|
// Hash scroll on load
|
|
1510
1621
|
function initHashScroll() {
|
|
1511
1622
|
if (!location.hash) return;
|
|
@@ -1675,6 +1786,7 @@ function generateScript(options) {
|
|
|
1675
1786
|
initCalls.push("initStatusFilter();");
|
|
1676
1787
|
initCalls.push("initKeyboardShortcuts();");
|
|
1677
1788
|
initCalls.push("initCollapse();");
|
|
1789
|
+
initCalls.push("restoreCollapseState();");
|
|
1678
1790
|
initCalls.push("initDetailLevel();");
|
|
1679
1791
|
initCalls.push("applyAllFilters();");
|
|
1680
1792
|
initCalls.push("initHashScroll();");
|
|
@@ -3782,6 +3894,33 @@ body {
|
|
|
3782
3894
|
color: var(--primary);
|
|
3783
3895
|
}
|
|
3784
3896
|
|
|
3897
|
+
.copy-prompt-btn {
|
|
3898
|
+
display: inline-flex;
|
|
3899
|
+
align-items: center;
|
|
3900
|
+
justify-content: center;
|
|
3901
|
+
width: 1.5rem;
|
|
3902
|
+
height: 1.5rem;
|
|
3903
|
+
border: none;
|
|
3904
|
+
background: none;
|
|
3905
|
+
color: var(--muted-foreground);
|
|
3906
|
+
cursor: pointer;
|
|
3907
|
+
opacity: 0.6;
|
|
3908
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
3909
|
+
font-size: 0.95rem;
|
|
3910
|
+
padding: 0;
|
|
3911
|
+
flex-shrink: 0;
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3914
|
+
.scenario-header:hover .copy-prompt-btn,
|
|
3915
|
+
.copy-prompt-btn:focus-visible {
|
|
3916
|
+
opacity: 1;
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3919
|
+
.copy-prompt-btn:hover {
|
|
3920
|
+
color: var(--primary);
|
|
3921
|
+
transform: scale(1.15);
|
|
3922
|
+
}
|
|
3923
|
+
|
|
3785
3924
|
/* ============================================================================
|
|
3786
3925
|
Keyboard Navigation
|
|
3787
3926
|
============================================================================ */
|
|
@@ -4019,6 +4158,82 @@ a.toc-title:hover {
|
|
|
4019
4158
|
outline-offset: 2px;
|
|
4020
4159
|
}
|
|
4021
4160
|
|
|
4161
|
+
/* ============================================================================
|
|
4162
|
+
Mobile responsive refinements
|
|
4163
|
+
============================================================================ */
|
|
4164
|
+
@media (max-width: 640px) {
|
|
4165
|
+
.container {
|
|
4166
|
+
padding: 0.875rem;
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
.header {
|
|
4170
|
+
flex-direction: column;
|
|
4171
|
+
align-items: stretch;
|
|
4172
|
+
gap: 0.75rem;
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
.header-actions {
|
|
4176
|
+
flex-wrap: wrap;
|
|
4177
|
+
gap: 0.5rem;
|
|
4178
|
+
}
|
|
4179
|
+
|
|
4180
|
+
.search-input {
|
|
4181
|
+
width: 100%;
|
|
4182
|
+
flex: 1 1 100%;
|
|
4183
|
+
min-width: 0;
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
.header h1 {
|
|
4187
|
+
font-size: 1.25rem;
|
|
4188
|
+
}
|
|
4189
|
+
|
|
4190
|
+
.scenario-header,
|
|
4191
|
+
.feature-header {
|
|
4192
|
+
flex-wrap: wrap;
|
|
4193
|
+
gap: 0.5rem;
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
.scenario-meta {
|
|
4197
|
+
flex-wrap: wrap;
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
.scenario-actions {
|
|
4201
|
+
flex-wrap: wrap;
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
/* Always-visible action buttons on touch (no hover) */
|
|
4205
|
+
.copy-scenario-btn,
|
|
4206
|
+
.copy-prompt-btn,
|
|
4207
|
+
.permalink-anchor {
|
|
4208
|
+
opacity: 1 !important;
|
|
4209
|
+
}
|
|
4210
|
+
|
|
4211
|
+
.summary-card {
|
|
4212
|
+
padding: 0.75rem 0.875rem;
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
.summary-card .value {
|
|
4216
|
+
font-size: 1.5rem;
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
.tag-bar {
|
|
4220
|
+
overflow-x: auto;
|
|
4221
|
+
-webkit-overflow-scrolling: touch;
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
.shortcuts-overlay {
|
|
4225
|
+
padding: 1rem;
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
|
|
4229
|
+
@media (hover: none) and (pointer: coarse) {
|
|
4230
|
+
.copy-scenario-btn,
|
|
4231
|
+
.copy-prompt-btn,
|
|
4232
|
+
.permalink-anchor {
|
|
4233
|
+
opacity: 1;
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4022
4237
|
`;
|
|
4023
4238
|
|
|
4024
4239
|
// src/formatters/html/themes/default.ts
|
|
@@ -13600,7 +13815,7 @@ function renderDocEntry(entry, deps) {
|
|
|
13600
13815
|
// src/formatters/html/renderers/steps.ts
|
|
13601
13816
|
var CONTINUATION_KEYWORDS = ["And", "But", "*"];
|
|
13602
13817
|
function renderStep(step, stepResult, index, deps) {
|
|
13603
|
-
const
|
|
13818
|
+
const statusIcon4 = stepResult ? deps.getStatusIcon(stepResult.status) : "\u25CB";
|
|
13604
13819
|
const statusClass = stepResult ? `status-${stepResult.status}` : "";
|
|
13605
13820
|
const duration = stepResult && stepResult.durationMs > 0 ? `${stepResult.durationMs}ms` : "";
|
|
13606
13821
|
const keywordTrimmed = step.keyword.trim();
|
|
@@ -13609,7 +13824,7 @@ function renderStep(step, stepResult, index, deps) {
|
|
|
13609
13824
|
const stepDocs = deps.renderDocs(step.docs, "step-docs");
|
|
13610
13825
|
const textHtml = deps.highlightStepParams ? deps.highlightStepParams(step.text) : deps.escapeHtml(step.text);
|
|
13611
13826
|
return `<div class="${stepClass}" data-keyword="${deps.escapeHtml(keywordTrimmed)}" data-text="${deps.escapeHtml(step.text)}">
|
|
13612
|
-
<span class="step-status ${statusClass}">${
|
|
13827
|
+
<span class="step-status ${statusClass}">${statusIcon4}</span>
|
|
13613
13828
|
<span class="step-keyword">${deps.escapeHtml(step.keyword)}</span>
|
|
13614
13829
|
<span class="step-text">${textHtml}</span>
|
|
13615
13830
|
<span class="step-duration">${duration}</span>
|
|
@@ -13659,16 +13874,16 @@ function hasSufficientHistory(entries, min) {
|
|
|
13659
13874
|
}
|
|
13660
13875
|
|
|
13661
13876
|
// src/formatters/html/renderers/scenario.ts
|
|
13662
|
-
function renderTicket(ticket, template,
|
|
13877
|
+
function renderTicket(ticket, template, escapeHtml4) {
|
|
13663
13878
|
const url = ticket.url ?? (template ? template.replace("{ticket}", ticket.id) : void 0);
|
|
13664
13879
|
if (url) {
|
|
13665
|
-
return `<a class="tag ticket-tag" href="${
|
|
13880
|
+
return `<a class="tag ticket-tag" href="${escapeHtml4(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml4(ticket.id)}</a>`;
|
|
13666
13881
|
}
|
|
13667
|
-
return `<span class="tag ticket-tag">${
|
|
13882
|
+
return `<span class="tag ticket-tag">${escapeHtml4(ticket.id)}</span>`;
|
|
13668
13883
|
}
|
|
13669
13884
|
function renderScenario(args, deps) {
|
|
13670
13885
|
const { tc } = args;
|
|
13671
|
-
const
|
|
13886
|
+
const statusIcon4 = deps.getStatusIcon(tc.status);
|
|
13672
13887
|
const statusClass = `status-${tc.status}`;
|
|
13673
13888
|
const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
|
|
13674
13889
|
const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
|
|
@@ -13738,13 +13953,14 @@ function renderScenario(args, deps) {
|
|
|
13738
13953
|
<div class="scenario-header" role="button" tabindex="0" aria-expanded="${ariaExpanded}">
|
|
13739
13954
|
<div class="scenario-info">
|
|
13740
13955
|
<div class="scenario-title">
|
|
13741
|
-
<span class="status-icon ${statusClass}">${
|
|
13956
|
+
<span class="status-icon ${statusClass}">${statusIcon4}</span>
|
|
13742
13957
|
<span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
|
|
13743
13958
|
</div>
|
|
13744
13959
|
<div class="scenario-meta">${tags}${tickets}${outcomeBadge}${sourceLink}${traceBadge}${metricBadges}</div>
|
|
13745
13960
|
</div>
|
|
13746
13961
|
<div class="scenario-actions">
|
|
13747
13962
|
<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>
|
|
13963
|
+
${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>` : ""}
|
|
13748
13964
|
<button class="permalink-anchor" onclick="copyPermalink('scenario-${tc.id}')" aria-label="Copy link to scenario" title="Copy link">#</button>
|
|
13749
13965
|
<span class="scenario-duration">${duration}</span>
|
|
13750
13966
|
</div>
|
|
@@ -13863,7 +14079,7 @@ function flattenTree(roots) {
|
|
|
13863
14079
|
}
|
|
13864
14080
|
return result;
|
|
13865
14081
|
}
|
|
13866
|
-
function buildTooltip(span,
|
|
14082
|
+
function buildTooltip(span, escapeHtml4) {
|
|
13867
14083
|
const parts = [];
|
|
13868
14084
|
parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
|
|
13869
14085
|
if (span.statusMessage) {
|
|
@@ -13881,7 +14097,7 @@ function buildTooltip(span, escapeHtml3) {
|
|
|
13881
14097
|
if (text2.length > TOOLTIP_MAX_LENGTH) {
|
|
13882
14098
|
text2 = text2.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
|
|
13883
14099
|
}
|
|
13884
|
-
return
|
|
14100
|
+
return escapeHtml4(text2);
|
|
13885
14101
|
}
|
|
13886
14102
|
function renderTraceView(args, deps) {
|
|
13887
14103
|
if (!args.spans || args.spans.length === 0) return "";
|
|
@@ -14104,11 +14320,11 @@ function renderToc(args, deps) {
|
|
|
14104
14320
|
const featureName = suitePaths.length > 0 && suitePaths[0].length > 0 ? suitePaths[0][0] : file.split("/").pop()?.replace(/\.[^.]+$/, "") ?? file;
|
|
14105
14321
|
const featureSlug = `feature-${slugify(file)}`;
|
|
14106
14322
|
const scenarios = testCases.map((tc) => {
|
|
14107
|
-
const
|
|
14323
|
+
const statusIcon4 = deps.getStatusIcon(tc.status);
|
|
14108
14324
|
const statusClass = `status-${tc.status}`;
|
|
14109
14325
|
const failedClass = tc.status === "failed" ? " toc-failed" : "";
|
|
14110
14326
|
return `<a class="toc-scenario${failedClass}" href="#scenario-${tc.id}">
|
|
14111
|
-
<span class="toc-status ${statusClass}">${
|
|
14327
|
+
<span class="toc-status ${statusClass}">${statusIcon4}</span>
|
|
14112
14328
|
${deps.escapeHtml(tc.story.scenario)}
|
|
14113
14329
|
</a>`;
|
|
14114
14330
|
}).join("\n");
|
|
@@ -19419,6 +19635,697 @@ function listScenarios(args, _deps) {
|
|
|
19419
19635
|
return lines.join("\n");
|
|
19420
19636
|
}
|
|
19421
19637
|
|
|
19638
|
+
// src/review/conventions.ts
|
|
19639
|
+
var CHANGE_TAG_PREFIX = "change:";
|
|
19640
|
+
var AUDIENCE_TAG_PREFIX = "audience:";
|
|
19641
|
+
var VALID_CHANGE_TYPES = /* @__PURE__ */ new Set([
|
|
19642
|
+
"feature",
|
|
19643
|
+
"bugfix",
|
|
19644
|
+
"refactor",
|
|
19645
|
+
"perf",
|
|
19646
|
+
"deps"
|
|
19647
|
+
]);
|
|
19648
|
+
var STAKEHOLDER_FILE = /(?:\.e2e\.)|(?:^|\/)e2e\/|(?:\.spec\.)/i;
|
|
19649
|
+
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
19650
|
+
"ts",
|
|
19651
|
+
"tsx",
|
|
19652
|
+
"js",
|
|
19653
|
+
"jsx",
|
|
19654
|
+
"mjs",
|
|
19655
|
+
"cjs",
|
|
19656
|
+
"py",
|
|
19657
|
+
"go",
|
|
19658
|
+
"rs",
|
|
19659
|
+
"kt",
|
|
19660
|
+
"kts",
|
|
19661
|
+
"java",
|
|
19662
|
+
"cs",
|
|
19663
|
+
"rb"
|
|
19664
|
+
]);
|
|
19665
|
+
var TEST_INFIX = /\.(?:story\.)?(?:int\.|e2e\.|unit\.)?(?:test|spec|cy)\.[a-z]+$/i;
|
|
19666
|
+
function deriveAudience(sourceFile, tags) {
|
|
19667
|
+
const override = tags.map((t) => t.toLowerCase()).find((t) => t.startsWith(AUDIENCE_TAG_PREFIX));
|
|
19668
|
+
if (override) {
|
|
19669
|
+
const value = override.slice(AUDIENCE_TAG_PREFIX.length);
|
|
19670
|
+
if (value === "stakeholder" || value === "engineer") return value;
|
|
19671
|
+
}
|
|
19672
|
+
return STAKEHOLDER_FILE.test(sourceFile) ? "stakeholder" : "engineer";
|
|
19673
|
+
}
|
|
19674
|
+
function deriveChangeType(tags) {
|
|
19675
|
+
for (const tag of tags) {
|
|
19676
|
+
const lower = tag.toLowerCase();
|
|
19677
|
+
if (lower.startsWith(CHANGE_TAG_PREFIX)) {
|
|
19678
|
+
const value = lower.slice(CHANGE_TAG_PREFIX.length);
|
|
19679
|
+
if (VALID_CHANGE_TYPES.has(value)) return value;
|
|
19680
|
+
}
|
|
19681
|
+
}
|
|
19682
|
+
return "unknown";
|
|
19683
|
+
}
|
|
19684
|
+
function extensionOf(path10) {
|
|
19685
|
+
const base = path10.split("/").pop() ?? path10;
|
|
19686
|
+
const dot = base.lastIndexOf(".");
|
|
19687
|
+
return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
|
|
19688
|
+
}
|
|
19689
|
+
function isTestFile(path10) {
|
|
19690
|
+
return TEST_INFIX.test(path10);
|
|
19691
|
+
}
|
|
19692
|
+
function isReviewableSource(path10) {
|
|
19693
|
+
if (isTestFile(path10)) return false;
|
|
19694
|
+
if (path10.endsWith(".d.ts")) return false;
|
|
19695
|
+
return CODE_EXTENSIONS.has(extensionOf(path10));
|
|
19696
|
+
}
|
|
19697
|
+
function testBaseKey(testFile) {
|
|
19698
|
+
return testFile.replace(TEST_INFIX, "");
|
|
19699
|
+
}
|
|
19700
|
+
function sourceBaseKey(sourceFile) {
|
|
19701
|
+
const dot = sourceFile.lastIndexOf(".");
|
|
19702
|
+
const slash = sourceFile.lastIndexOf("/");
|
|
19703
|
+
return dot > slash ? sourceFile.slice(0, dot) : sourceFile;
|
|
19704
|
+
}
|
|
19705
|
+
|
|
19706
|
+
// src/review/build-review.ts
|
|
19707
|
+
var STRENGTH_RANK = {
|
|
19708
|
+
none: 0,
|
|
19709
|
+
weak: 1,
|
|
19710
|
+
moderate: 2,
|
|
19711
|
+
strong: 3
|
|
19712
|
+
};
|
|
19713
|
+
var INTENT_SECTION_TITLE = /\b(why|intent|approach|rationale|reasoning)\b/i;
|
|
19714
|
+
function findDoc(docs, predicate) {
|
|
19715
|
+
if (!docs) return void 0;
|
|
19716
|
+
for (const doc of docs) {
|
|
19717
|
+
if (predicate(doc)) return doc;
|
|
19718
|
+
const nested = findDoc(doc.children, predicate);
|
|
19719
|
+
if (nested) return nested;
|
|
19720
|
+
}
|
|
19721
|
+
return void 0;
|
|
19722
|
+
}
|
|
19723
|
+
function anyDoc(docs, predicate) {
|
|
19724
|
+
return findDoc(docs, predicate) !== void 0;
|
|
19725
|
+
}
|
|
19726
|
+
function extractIntent(testCase) {
|
|
19727
|
+
const docs = testCase.story.docs;
|
|
19728
|
+
const section = findDoc(
|
|
19729
|
+
docs,
|
|
19730
|
+
(d) => d.kind === "section" && INTENT_SECTION_TITLE.test(d.title)
|
|
19731
|
+
);
|
|
19732
|
+
if (section && section.kind === "section") return section.markdown;
|
|
19733
|
+
const note = findDoc(docs, (d) => d.kind === "note");
|
|
19734
|
+
if (note && note.kind === "note") return note.text;
|
|
19735
|
+
return void 0;
|
|
19736
|
+
}
|
|
19737
|
+
function hasScreenshot(testCase) {
|
|
19738
|
+
if (testCase.attachments.some((a) => a.mediaType.startsWith("image/"))) {
|
|
19739
|
+
return true;
|
|
19740
|
+
}
|
|
19741
|
+
if (anyDoc(testCase.story.docs, (d) => d.kind === "screenshot")) return true;
|
|
19742
|
+
return testCase.story.steps.some(
|
|
19743
|
+
(step) => anyDoc(step.docs, (d) => d.kind === "screenshot")
|
|
19744
|
+
);
|
|
19745
|
+
}
|
|
19746
|
+
function hasOtelTrace(testCase) {
|
|
19747
|
+
return (testCase.story.otelSpans?.length ?? 0) > 0;
|
|
19748
|
+
}
|
|
19749
|
+
function gradeEvidence(testCase, audience) {
|
|
19750
|
+
if (testCase.status !== "passed") {
|
|
19751
|
+
return {
|
|
19752
|
+
strength: "none",
|
|
19753
|
+
reasons: [`test is ${testCase.status} \u2014 the proof does not hold`]
|
|
19754
|
+
};
|
|
19755
|
+
}
|
|
19756
|
+
const ev = testCase.evidence;
|
|
19757
|
+
const screenshot = hasScreenshot(testCase);
|
|
19758
|
+
const otel = hasOtelTrace(testCase);
|
|
19759
|
+
const isIntegration = /\.int\.test\./i.test(testCase.sourceFile);
|
|
19760
|
+
const mutation = ev?.mutationScorePct;
|
|
19761
|
+
const changedCov = ev?.changedLineCoveragePct;
|
|
19762
|
+
const strong2 = [];
|
|
19763
|
+
if (ev?.failingFirstVerified) {
|
|
19764
|
+
strong2.push("failing-first verified (red on base ref, green on head)");
|
|
19765
|
+
}
|
|
19766
|
+
if (typeof mutation === "number" && mutation >= 80) {
|
|
19767
|
+
strong2.push(`mutation score ${mutation}% (\u226580%)`);
|
|
19768
|
+
}
|
|
19769
|
+
if (screenshot && otel) {
|
|
19770
|
+
strong2.push("backed by screenshot + OTEL trace");
|
|
19771
|
+
} else if (audience === "stakeholder" && (screenshot || otel)) {
|
|
19772
|
+
strong2.push(`stakeholder proof: ${screenshot ? "screenshot" : "OTEL trace"}`);
|
|
19773
|
+
}
|
|
19774
|
+
if (strong2.length > 0) return { strength: "strong", reasons: strong2 };
|
|
19775
|
+
const moderate = [];
|
|
19776
|
+
if (screenshot) moderate.push("screenshot attached");
|
|
19777
|
+
if (otel) moderate.push("OTEL trace attached");
|
|
19778
|
+
if (typeof mutation === "number" && mutation >= 50) {
|
|
19779
|
+
moderate.push(`mutation score ${mutation}%`);
|
|
19780
|
+
}
|
|
19781
|
+
if (typeof changedCov === "number" && changedCov >= 80) {
|
|
19782
|
+
moderate.push(`changed-line coverage ${changedCov}%`);
|
|
19783
|
+
}
|
|
19784
|
+
if (isIntegration) moderate.push("integration-level test");
|
|
19785
|
+
if (moderate.length > 0) return { strength: "moderate", reasons: moderate };
|
|
19786
|
+
return {
|
|
19787
|
+
strength: "weak",
|
|
19788
|
+
reasons: [
|
|
19789
|
+
"passing test only \u2014 no corroborating evidence (add e2e proof, mutation score, or failing-first)"
|
|
19790
|
+
]
|
|
19791
|
+
};
|
|
19792
|
+
}
|
|
19793
|
+
function toClaim(testCase, changedSourcePaths) {
|
|
19794
|
+
const audience = deriveAudience(testCase.sourceFile, testCase.tags);
|
|
19795
|
+
const changeType = deriveChangeType(testCase.tags);
|
|
19796
|
+
const { strength, reasons } = gradeEvidence(testCase, audience);
|
|
19797
|
+
const key = testBaseKey(testCase.sourceFile);
|
|
19798
|
+
const coversFiles = changedSourcePaths.filter(
|
|
19799
|
+
(path10) => sourceBaseKey(path10) === key
|
|
19800
|
+
);
|
|
19801
|
+
return {
|
|
19802
|
+
id: testCase.id,
|
|
19803
|
+
scenario: testCase.story.scenario,
|
|
19804
|
+
sourceFile: testCase.sourceFile,
|
|
19805
|
+
sourceLine: testCase.sourceLine,
|
|
19806
|
+
status: testCase.status,
|
|
19807
|
+
audience,
|
|
19808
|
+
changeType,
|
|
19809
|
+
strength,
|
|
19810
|
+
strengthReasons: reasons,
|
|
19811
|
+
intent: extractIntent(testCase),
|
|
19812
|
+
coversFiles,
|
|
19813
|
+
testCase
|
|
19814
|
+
};
|
|
19815
|
+
}
|
|
19816
|
+
function bandFor(claims) {
|
|
19817
|
+
if (claims.length === 0) return "uncovered";
|
|
19818
|
+
const maxRank = Math.max(...claims.map((c) => STRENGTH_RANK[c.strength]));
|
|
19819
|
+
return maxRank >= STRENGTH_RANK.moderate ? "covered" : "weak";
|
|
19820
|
+
}
|
|
19821
|
+
var AUDIENCE_ORDER = {
|
|
19822
|
+
stakeholder: 0,
|
|
19823
|
+
engineer: 1
|
|
19824
|
+
};
|
|
19825
|
+
function buildReview(run, context = { changedFiles: [] }) {
|
|
19826
|
+
const changedSource = context.changedFiles.filter(
|
|
19827
|
+
(f) => isReviewableSource(f.path)
|
|
19828
|
+
);
|
|
19829
|
+
const changedSourcePaths = changedSource.map((f) => f.path);
|
|
19830
|
+
const claims = run.testCases.map((tc) => toClaim(tc, changedSourcePaths));
|
|
19831
|
+
const changedFiles = changedSource.map((file) => {
|
|
19832
|
+
const covering = claims.filter((c) => c.coversFiles.includes(file.path));
|
|
19833
|
+
return {
|
|
19834
|
+
path: file.path,
|
|
19835
|
+
changeKind: file.changeKind,
|
|
19836
|
+
band: bandFor(covering),
|
|
19837
|
+
claims: covering.map((c) => ({
|
|
19838
|
+
id: c.id,
|
|
19839
|
+
scenario: c.scenario,
|
|
19840
|
+
strength: c.strength
|
|
19841
|
+
}))
|
|
19842
|
+
};
|
|
19843
|
+
});
|
|
19844
|
+
const sortedClaims = [...claims].sort((a, b) => {
|
|
19845
|
+
if (AUDIENCE_ORDER[a.audience] !== AUDIENCE_ORDER[b.audience]) {
|
|
19846
|
+
return AUDIENCE_ORDER[a.audience] - AUDIENCE_ORDER[b.audience];
|
|
19847
|
+
}
|
|
19848
|
+
if (STRENGTH_RANK[a.strength] !== STRENGTH_RANK[b.strength]) {
|
|
19849
|
+
return STRENGTH_RANK[a.strength] - STRENGTH_RANK[b.strength];
|
|
19850
|
+
}
|
|
19851
|
+
if (a.sourceFile !== b.sourceFile) {
|
|
19852
|
+
return a.sourceFile.localeCompare(b.sourceFile);
|
|
19853
|
+
}
|
|
19854
|
+
return a.scenario.localeCompare(b.scenario);
|
|
19855
|
+
});
|
|
19856
|
+
const bandRank = { uncovered: 0, weak: 1, covered: 2 };
|
|
19857
|
+
const sortedFiles = [...changedFiles].sort((a, b) => {
|
|
19858
|
+
if (bandRank[a.band] !== bandRank[b.band]) {
|
|
19859
|
+
return bandRank[a.band] - bandRank[b.band];
|
|
19860
|
+
}
|
|
19861
|
+
return a.path.localeCompare(b.path);
|
|
19862
|
+
});
|
|
19863
|
+
const summary = buildSummary2(sortedClaims, sortedFiles);
|
|
19864
|
+
return {
|
|
19865
|
+
run,
|
|
19866
|
+
context,
|
|
19867
|
+
summary,
|
|
19868
|
+
claims: sortedClaims,
|
|
19869
|
+
changedFiles: sortedFiles
|
|
19870
|
+
};
|
|
19871
|
+
}
|
|
19872
|
+
function buildSummary2(claims, changedFiles) {
|
|
19873
|
+
const byAudience = {
|
|
19874
|
+
stakeholder: 0,
|
|
19875
|
+
engineer: 0
|
|
19876
|
+
};
|
|
19877
|
+
const byStrength = {
|
|
19878
|
+
none: 0,
|
|
19879
|
+
weak: 0,
|
|
19880
|
+
moderate: 0,
|
|
19881
|
+
strong: 0
|
|
19882
|
+
};
|
|
19883
|
+
for (const claim of claims) {
|
|
19884
|
+
byAudience[claim.audience] += 1;
|
|
19885
|
+
byStrength[claim.strength] += 1;
|
|
19886
|
+
}
|
|
19887
|
+
return {
|
|
19888
|
+
totalClaims: claims.length,
|
|
19889
|
+
byAudience,
|
|
19890
|
+
byStrength,
|
|
19891
|
+
changedSourceFiles: changedFiles.length,
|
|
19892
|
+
uncovered: changedFiles.filter((f) => f.band === "uncovered").length,
|
|
19893
|
+
weaklyCovered: changedFiles.filter((f) => f.band === "weak").length,
|
|
19894
|
+
covered: changedFiles.filter((f) => f.band === "covered").length
|
|
19895
|
+
};
|
|
19896
|
+
}
|
|
19897
|
+
|
|
19898
|
+
// src/formatters/review-markdown.ts
|
|
19899
|
+
var STRENGTH_BADGE = {
|
|
19900
|
+
strong: "\u{1F7E2} strong",
|
|
19901
|
+
moderate: "\u{1F7E1} moderate",
|
|
19902
|
+
weak: "\u{1F7E0} weak",
|
|
19903
|
+
none: "\u{1F534} none"
|
|
19904
|
+
};
|
|
19905
|
+
function statusIcon2(status) {
|
|
19906
|
+
switch (status) {
|
|
19907
|
+
case "passed":
|
|
19908
|
+
return "\u2705";
|
|
19909
|
+
case "failed":
|
|
19910
|
+
return "\u274C";
|
|
19911
|
+
case "skipped":
|
|
19912
|
+
return "\u2298";
|
|
19913
|
+
default:
|
|
19914
|
+
return "\u2022";
|
|
19915
|
+
}
|
|
19916
|
+
}
|
|
19917
|
+
function escapeCell2(value) {
|
|
19918
|
+
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
19919
|
+
}
|
|
19920
|
+
function intentSummary(intent) {
|
|
19921
|
+
const firstLine = intent.split("\n").find((l) => l.trim().length > 0) ?? "";
|
|
19922
|
+
const trimmed = firstLine.trim();
|
|
19923
|
+
return trimmed.length > 200 ? `${trimmed.slice(0, 197)}\u2026` : trimmed;
|
|
19924
|
+
}
|
|
19925
|
+
function renderTicket2(ticket) {
|
|
19926
|
+
return ticket.url ? `[${ticket.id}](${ticket.url})` : `\`${ticket.id}\``;
|
|
19927
|
+
}
|
|
19928
|
+
function renderUncoveredBand(lines, files) {
|
|
19929
|
+
const uncovered = files.filter((f) => f.band === "uncovered");
|
|
19930
|
+
if (uncovered.length === 0) return;
|
|
19931
|
+
lines.push(`## \u{1F534} Changed code with no evidence (${uncovered.length})`);
|
|
19932
|
+
lines.push("");
|
|
19933
|
+
lines.push("Start here \u2014 these changed source files have no claim or test behind them.");
|
|
19934
|
+
lines.push("");
|
|
19935
|
+
for (const file of uncovered) {
|
|
19936
|
+
lines.push(`- \`${file.path}\` _(${file.changeKind})_`);
|
|
19937
|
+
}
|
|
19938
|
+
lines.push("");
|
|
19939
|
+
}
|
|
19940
|
+
function renderWeakBand(lines, files) {
|
|
19941
|
+
const weak = files.filter((f) => f.band === "weak");
|
|
19942
|
+
if (weak.length === 0) return;
|
|
19943
|
+
lines.push(`## \u{1F7E1} Changed code with weak evidence (${weak.length})`);
|
|
19944
|
+
lines.push("");
|
|
19945
|
+
for (const file of weak) {
|
|
19946
|
+
const covered = file.claims.map((c) => `${escapeCell2(c.scenario)} (${c.strength})`).join(", ");
|
|
19947
|
+
lines.push(`- \`${file.path}\` _(${file.changeKind})_ \u2014 only: ${covered}`);
|
|
19948
|
+
}
|
|
19949
|
+
lines.push("");
|
|
19950
|
+
}
|
|
19951
|
+
function renderClaim(lines, claim) {
|
|
19952
|
+
lines.push(`### ${statusIcon2(claim.status)} ${claim.scenario}`);
|
|
19953
|
+
lines.push("");
|
|
19954
|
+
lines.push(`- File: \`${claim.sourceFile}:${claim.sourceLine}\``);
|
|
19955
|
+
if (claim.changeType !== "unknown") {
|
|
19956
|
+
lines.push(`- Change: \`${claim.changeType}\``);
|
|
19957
|
+
}
|
|
19958
|
+
const tickets = claim.testCase.story.tickets ?? [];
|
|
19959
|
+
if (tickets.length > 0) {
|
|
19960
|
+
lines.push(`- Tickets: ${tickets.map(renderTicket2).join(", ")}`);
|
|
19961
|
+
}
|
|
19962
|
+
lines.push(
|
|
19963
|
+
`- Evidence: ${STRENGTH_BADGE[claim.strength]} \u2014 ${claim.strengthReasons.join("; ")}`
|
|
19964
|
+
);
|
|
19965
|
+
if (claim.coversFiles.length > 0) {
|
|
19966
|
+
lines.push(
|
|
19967
|
+
`- Covers: ${claim.coversFiles.map((f) => `\`${f}\``).join(", ")}`
|
|
19968
|
+
);
|
|
19969
|
+
}
|
|
19970
|
+
if (claim.intent) {
|
|
19971
|
+
lines.push(`- Why: ${escapeCell2(intentSummary(claim.intent))}`);
|
|
19972
|
+
}
|
|
19973
|
+
lines.push("");
|
|
19974
|
+
}
|
|
19975
|
+
function renderAudienceSection(lines, title, claims) {
|
|
19976
|
+
if (claims.length === 0) return;
|
|
19977
|
+
lines.push(`## ${title} (${claims.length})`);
|
|
19978
|
+
lines.push("");
|
|
19979
|
+
for (const claim of claims) {
|
|
19980
|
+
renderClaim(lines, claim);
|
|
19981
|
+
}
|
|
19982
|
+
}
|
|
19983
|
+
var ReviewMarkdownFormatter = class {
|
|
19984
|
+
title;
|
|
19985
|
+
constructor(options = {}) {
|
|
19986
|
+
this.title = options.title ?? "Evidence Review";
|
|
19987
|
+
}
|
|
19988
|
+
format(review) {
|
|
19989
|
+
const lines = [];
|
|
19990
|
+
const { summary, context } = review;
|
|
19991
|
+
lines.push(`# ${this.title}`);
|
|
19992
|
+
lines.push("");
|
|
19993
|
+
if (context.baseRef || context.headRef) {
|
|
19994
|
+
lines.push(
|
|
19995
|
+
`Comparing \`${context.baseRef ?? "base"}\` \u2192 \`${context.headRef ?? "head"}\`.`
|
|
19996
|
+
);
|
|
19997
|
+
lines.push("");
|
|
19998
|
+
}
|
|
19999
|
+
lines.push("## Review priority");
|
|
20000
|
+
lines.push("");
|
|
20001
|
+
if (summary.changedSourceFiles === 0) {
|
|
20002
|
+
lines.push(
|
|
20003
|
+
"No changed source files supplied \u2014 showing claims and evidence only."
|
|
20004
|
+
);
|
|
20005
|
+
} else if (summary.uncovered > 0) {
|
|
20006
|
+
lines.push(
|
|
20007
|
+
`Review the ${summary.uncovered} unaccounted-for file(s) first: changed code with no evidence behind it.`
|
|
20008
|
+
);
|
|
20009
|
+
} else if (summary.weaklyCovered > 0) {
|
|
20010
|
+
lines.push(
|
|
20011
|
+
`No unaccounted-for changes. Review ${summary.weaklyCovered} weakly-covered file(s) next.`
|
|
20012
|
+
);
|
|
20013
|
+
} else {
|
|
20014
|
+
lines.push("Every changed source file is backed by at least moderate evidence.");
|
|
20015
|
+
}
|
|
20016
|
+
lines.push("");
|
|
20017
|
+
if (summary.changedSourceFiles > 0) {
|
|
20018
|
+
lines.push("| \u{1F534} Uncovered | \u{1F7E1} Weak | \u{1F7E2} Covered | Changed files |");
|
|
20019
|
+
lines.push("| ---: | ---: | ---: | ---: |");
|
|
20020
|
+
lines.push(
|
|
20021
|
+
`| ${summary.uncovered} | ${summary.weaklyCovered} | ${summary.covered} | ${summary.changedSourceFiles} |`
|
|
20022
|
+
);
|
|
20023
|
+
lines.push("");
|
|
20024
|
+
}
|
|
20025
|
+
lines.push("| Claims | Stakeholder | Engineer | Strong | Moderate | Weak | None |");
|
|
20026
|
+
lines.push("| ---: | ---: | ---: | ---: | ---: | ---: | ---: |");
|
|
20027
|
+
lines.push(
|
|
20028
|
+
`| ${summary.totalClaims} | ${summary.byAudience.stakeholder} | ${summary.byAudience.engineer} | ${summary.byStrength.strong} | ${summary.byStrength.moderate} | ${summary.byStrength.weak} | ${summary.byStrength.none} |`
|
|
20029
|
+
);
|
|
20030
|
+
lines.push("");
|
|
20031
|
+
renderUncoveredBand(lines, review.changedFiles);
|
|
20032
|
+
renderWeakBand(lines, review.changedFiles);
|
|
20033
|
+
renderAudienceSection(
|
|
20034
|
+
lines,
|
|
20035
|
+
"Stakeholder behaviour",
|
|
20036
|
+
review.claims.filter((c) => c.audience === "stakeholder")
|
|
20037
|
+
);
|
|
20038
|
+
renderAudienceSection(
|
|
20039
|
+
lines,
|
|
20040
|
+
"Engineer changes",
|
|
20041
|
+
review.claims.filter((c) => c.audience === "engineer")
|
|
20042
|
+
);
|
|
20043
|
+
return lines.join("\n").trimEnd();
|
|
20044
|
+
}
|
|
20045
|
+
};
|
|
20046
|
+
|
|
20047
|
+
// src/formatters/review-html.ts
|
|
20048
|
+
function escapeHtml3(value) {
|
|
20049
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
20050
|
+
}
|
|
20051
|
+
var STRENGTH_LABEL = {
|
|
20052
|
+
strong: "Strong",
|
|
20053
|
+
moderate: "Moderate",
|
|
20054
|
+
weak: "Weak",
|
|
20055
|
+
none: "None"
|
|
20056
|
+
};
|
|
20057
|
+
function statusIcon3(status) {
|
|
20058
|
+
switch (status) {
|
|
20059
|
+
case "passed":
|
|
20060
|
+
return "\u2705";
|
|
20061
|
+
case "failed":
|
|
20062
|
+
return "\u274C";
|
|
20063
|
+
case "skipped":
|
|
20064
|
+
return "\u2298";
|
|
20065
|
+
default:
|
|
20066
|
+
return "\u2022";
|
|
20067
|
+
}
|
|
20068
|
+
}
|
|
20069
|
+
function formatStep3(step) {
|
|
20070
|
+
return `<li><strong>${escapeHtml3(step.keyword)}</strong> ${escapeHtml3(step.text)}</li>`;
|
|
20071
|
+
}
|
|
20072
|
+
function inlineDoc(doc) {
|
|
20073
|
+
switch (doc.kind) {
|
|
20074
|
+
case "note":
|
|
20075
|
+
return escapeHtml3(doc.text);
|
|
20076
|
+
case "section":
|
|
20077
|
+
return `<strong>${escapeHtml3(doc.title)}</strong>: ${escapeHtml3(doc.markdown)}`;
|
|
20078
|
+
case "kv":
|
|
20079
|
+
return `${escapeHtml3(doc.label)}: ${escapeHtml3(String(doc.value))}`;
|
|
20080
|
+
case "code":
|
|
20081
|
+
return `${escapeHtml3(doc.label)}: <code>${escapeHtml3(doc.content)}</code>`;
|
|
20082
|
+
case "link":
|
|
20083
|
+
return `${escapeHtml3(doc.label)}: ${escapeHtml3(doc.url)}`;
|
|
20084
|
+
default:
|
|
20085
|
+
return escapeHtml3(doc.kind);
|
|
20086
|
+
}
|
|
20087
|
+
}
|
|
20088
|
+
function renderEvidenceArtifacts(testCase) {
|
|
20089
|
+
const parts = [];
|
|
20090
|
+
for (const att of testCase.attachments) {
|
|
20091
|
+
if (att.mediaType.startsWith("image/") && att.contentEncoding === "BASE64") {
|
|
20092
|
+
parts.push(
|
|
20093
|
+
`<img class="shot" alt="${escapeHtml3(att.name)}" src="data:${escapeHtml3(att.mediaType)};base64,${att.body}" />`
|
|
20094
|
+
);
|
|
20095
|
+
}
|
|
20096
|
+
}
|
|
20097
|
+
if ((testCase.story.otelSpans?.length ?? 0) > 0) {
|
|
20098
|
+
parts.push(
|
|
20099
|
+
`<p class="trace-note">\u{1F4E1} ${testCase.story.otelSpans.length} OTEL span(s) captured</p>`
|
|
20100
|
+
);
|
|
20101
|
+
}
|
|
20102
|
+
return parts.length > 0 ? `<div class="artifacts">${parts.join("")}</div>` : "";
|
|
20103
|
+
}
|
|
20104
|
+
function renderTicketPills(claim) {
|
|
20105
|
+
const tickets = claim.testCase.story.tickets ?? [];
|
|
20106
|
+
if (tickets.length === 0) return "";
|
|
20107
|
+
return `<div class="ticket-row">${tickets.map((ticket) => {
|
|
20108
|
+
const label = escapeHtml3(ticket.id);
|
|
20109
|
+
if (ticket.url) {
|
|
20110
|
+
return `<a class="ticket-pill" href="${escapeHtml3(ticket.url)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
|
|
20111
|
+
}
|
|
20112
|
+
return `<span class="ticket-pill">${label}</span>`;
|
|
20113
|
+
}).join("")}</div>`;
|
|
20114
|
+
}
|
|
20115
|
+
function renderClaimCard(claim) {
|
|
20116
|
+
const ticketSearch = (claim.testCase.story.tickets ?? []).map((ticket) => ticket.id).join(" ");
|
|
20117
|
+
const search = escapeHtml3(
|
|
20118
|
+
`${claim.scenario} ${claim.sourceFile} ${claim.changeType} ${claim.audience} ${claim.strength} ${ticketSearch}`
|
|
20119
|
+
).toLowerCase();
|
|
20120
|
+
const steps = claim.testCase.story.steps.length > 0 ? `<ul class="step-list">${claim.testCase.story.steps.map(formatStep3).join("")}</ul>` : "";
|
|
20121
|
+
const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${escapeHtml3(r)}</li>`).join("")}</ul>`;
|
|
20122
|
+
const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${escapeHtml3(claim.intent)}</div>` : "";
|
|
20123
|
+
const covers = claim.coversFiles.length > 0 ? `<p class="covers">Covers ${claim.coversFiles.map((f) => `<code>${escapeHtml3(f)}</code>`).join(", ")}</p>` : "";
|
|
20124
|
+
const docs = (claim.testCase.story.docs ?? []).filter(
|
|
20125
|
+
(d) => d.kind === "section" || d.kind === "note"
|
|
20126
|
+
);
|
|
20127
|
+
const extraDocs = docs.length > 0 && claim.intent === void 0 ? `<div class="intent">${docs.map(inlineDoc).join("<br>")}</div>` : "";
|
|
20128
|
+
return `
|
|
20129
|
+
<article class="claim-card" data-audience="${claim.audience}" data-strength="${claim.strength}" data-search="${search}">
|
|
20130
|
+
<header class="claim-header">
|
|
20131
|
+
<div>
|
|
20132
|
+
<span class="strength-badge strength-${claim.strength}">${STRENGTH_LABEL[claim.strength]}</span>
|
|
20133
|
+
${claim.changeType !== "unknown" ? `<span class="change-pill">${escapeHtml3(claim.changeType)}</span>` : ""}
|
|
20134
|
+
<h3>${statusIcon3(claim.status)} ${escapeHtml3(claim.scenario)}</h3>
|
|
20135
|
+
<p class="source">${escapeHtml3(`${claim.sourceFile}:${claim.sourceLine}`)}</p>
|
|
20136
|
+
${renderTicketPills(claim)}
|
|
20137
|
+
</div>
|
|
20138
|
+
</header>
|
|
20139
|
+
${intent}${extraDocs}
|
|
20140
|
+
<div class="evidence-block">
|
|
20141
|
+
<span class="evidence-label">Evidence</span>
|
|
20142
|
+
${reasons}
|
|
20143
|
+
</div>
|
|
20144
|
+
${covers}
|
|
20145
|
+
${renderEvidenceArtifacts(claim.testCase)}
|
|
20146
|
+
${steps}
|
|
20147
|
+
</article>`;
|
|
20148
|
+
}
|
|
20149
|
+
function renderChangedFileRow(file) {
|
|
20150
|
+
const claims = file.claims.length > 0 ? file.claims.map((c) => `${escapeHtml3(c.scenario)} <em>(${c.strength})</em>`).join(", ") : "\u2014";
|
|
20151
|
+
return `<tr data-band="${file.band}">
|
|
20152
|
+
<td><span class="band-dot band-${file.band}"></span></td>
|
|
20153
|
+
<td><code>${escapeHtml3(file.path)}</code></td>
|
|
20154
|
+
<td>${escapeHtml3(file.changeKind)}</td>
|
|
20155
|
+
<td>${claims}</td>
|
|
20156
|
+
</tr>`;
|
|
20157
|
+
}
|
|
20158
|
+
function renderAudienceSection2(title, claims) {
|
|
20159
|
+
if (claims.length === 0) return "";
|
|
20160
|
+
return `<section class="audience-section">
|
|
20161
|
+
<h2>${escapeHtml3(title)} <span class="count">${claims.length}</span></h2>
|
|
20162
|
+
<div class="claim-list">${claims.map(renderClaimCard).join("\n")}</div>
|
|
20163
|
+
</section>`;
|
|
20164
|
+
}
|
|
20165
|
+
var REVIEW_CSS = `
|
|
20166
|
+
* { box-sizing: border-box; }
|
|
20167
|
+
body { margin: 0; font-family: var(--font-sans, system-ui, sans-serif); background: var(--background); color: var(--foreground); }
|
|
20168
|
+
main { max-width: 1100px; margin: 0 auto; padding: 32px 20px 80px; }
|
|
20169
|
+
h1, h2, h3, p { margin: 0; }
|
|
20170
|
+
.review-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
|
20171
|
+
.subtle { color: var(--muted-foreground); margin-top: 6px; }
|
|
20172
|
+
.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); }
|
|
20173
|
+
.card, .claim-card, .summary-card, .panel { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius, 16px); }
|
|
20174
|
+
.hero-card { padding: 24px; margin-bottom: 20px; }
|
|
20175
|
+
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; margin-bottom: 20px; }
|
|
20176
|
+
.summary-card { padding: 14px 16px; }
|
|
20177
|
+
.summary-card strong { display: block; font-size: 1.8rem; }
|
|
20178
|
+
.priority-banner { padding: 18px 20px; margin-bottom: 20px; background: linear-gradient(135deg, color-mix(in srgb, var(--destructive) 10%, transparent), var(--card)); }
|
|
20179
|
+
.panel { padding: 18px; margin-bottom: 24px; }
|
|
20180
|
+
table { width: 100%; border-collapse: collapse; }
|
|
20181
|
+
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|
20182
|
+
th { color: var(--muted-foreground); font-weight: 600; }
|
|
20183
|
+
.band-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; }
|
|
20184
|
+
.band-uncovered { background: var(--destructive); }
|
|
20185
|
+
.band-weak { background: var(--warning, #b58900); }
|
|
20186
|
+
.band-covered { background: var(--success, #2e7d32); }
|
|
20187
|
+
.toolbar { position: sticky; top: 12px; z-index: 2; display: flex; flex-wrap: wrap; gap: 10px; padding: 14px; margin-bottom: 20px; }
|
|
20188
|
+
.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); }
|
|
20189
|
+
.toolbar button { border: 1px solid var(--border); background: var(--secondary); border-radius: 999px; padding: 10px 14px; font: inherit; cursor: pointer; color: var(--foreground); }
|
|
20190
|
+
.toolbar button.active { background: var(--foreground); color: var(--background); }
|
|
20191
|
+
.audience-section { margin-bottom: 28px; }
|
|
20192
|
+
.audience-section h2 { margin-bottom: 12px; }
|
|
20193
|
+
.count { color: var(--muted-foreground); font-weight: 400; }
|
|
20194
|
+
.claim-list { display: grid; gap: 14px; }
|
|
20195
|
+
.claim-card { padding: 18px; }
|
|
20196
|
+
.claim-header h3 { margin-top: 8px; }
|
|
20197
|
+
.source { color: var(--muted-foreground); font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.85rem; margin-top: 4px; }
|
|
20198
|
+
.ticket-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
|
20199
|
+
.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; }
|
|
20200
|
+
.ticket-pill:hover { color: var(--foreground); border-color: var(--muted-foreground); }
|
|
20201
|
+
.strength-badge, .change-pill { display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 999px; font-size: 0.8rem; margin-right: 6px; }
|
|
20202
|
+
.change-pill { background: var(--secondary); }
|
|
20203
|
+
.strength-strong { background: color-mix(in srgb, var(--success, #2e7d32) 18%, transparent); color: var(--success, #2e7d32); }
|
|
20204
|
+
.strength-moderate { background: color-mix(in srgb, var(--warning, #b58900) 20%, transparent); color: var(--warning, #b58900); }
|
|
20205
|
+
.strength-weak { background: color-mix(in srgb, #d2691e 20%, transparent); color: #b5530a; }
|
|
20206
|
+
.strength-none { background: color-mix(in srgb, var(--destructive) 16%, transparent); color: var(--destructive); }
|
|
20207
|
+
.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; }
|
|
20208
|
+
.intent-label { font-weight: 700; margin-right: 6px; }
|
|
20209
|
+
.evidence-block { margin-top: 10px; }
|
|
20210
|
+
.evidence-label { font-weight: 600; color: var(--muted-foreground); }
|
|
20211
|
+
.reasons { margin: 6px 0 0; padding-left: 18px; }
|
|
20212
|
+
.covers { color: var(--muted-foreground); margin-top: 8px; font-size: 0.9rem; }
|
|
20213
|
+
.artifacts { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-start; }
|
|
20214
|
+
.shot { max-width: 280px; max-height: 200px; border: 1px solid var(--border); border-radius: 8px; }
|
|
20215
|
+
.trace-note { color: var(--muted-foreground); }
|
|
20216
|
+
.step-list { margin: 12px 0 0; padding-left: 18px; color: var(--muted-foreground); }
|
|
20217
|
+
`;
|
|
20218
|
+
var JS_THEME_TOGGLE2 = `
|
|
20219
|
+
function getSystemTheme() { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; }
|
|
20220
|
+
function getEffectiveTheme() { var s = localStorage.getItem('review-theme'); return (s === 'dark' || s === 'light') ? s : getSystemTheme(); }
|
|
20221
|
+
function toggleTheme() { var n = getEffectiveTheme() === 'dark' ? 'light' : 'dark'; localStorage.setItem('review-theme', n); applyTheme(n); }
|
|
20222
|
+
function applyTheme(t) {
|
|
20223
|
+
document.documentElement.setAttribute('data-theme', t);
|
|
20224
|
+
var b = document.querySelector('.theme-toggle');
|
|
20225
|
+
if (b) { b.textContent = t === 'dark' ? '\\u2600\\ufe0f' : '\\ud83c\\udf19'; }
|
|
20226
|
+
}
|
|
20227
|
+
`;
|
|
20228
|
+
var ReviewHtmlFormatter = class {
|
|
20229
|
+
title;
|
|
20230
|
+
theme;
|
|
20231
|
+
darkMode;
|
|
20232
|
+
constructor(options = {}) {
|
|
20233
|
+
this.title = options.title ?? "Evidence Review";
|
|
20234
|
+
this.theme = resolveTheme(options.theme ?? "default");
|
|
20235
|
+
this.darkMode = options.darkMode ?? true;
|
|
20236
|
+
}
|
|
20237
|
+
format(review) {
|
|
20238
|
+
const { summary, context } = review;
|
|
20239
|
+
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.";
|
|
20240
|
+
const changedFilesPanel = summary.changedSourceFiles > 0 ? `<section class="panel">
|
|
20241
|
+
<h2>Changed files</h2>
|
|
20242
|
+
<table>
|
|
20243
|
+
<thead><tr><th></th><th>File</th><th>Change</th><th>Evidence</th></tr></thead>
|
|
20244
|
+
<tbody>${review.changedFiles.map(renderChangedFileRow).join("")}</tbody>
|
|
20245
|
+
</table>
|
|
20246
|
+
</section>` : "";
|
|
20247
|
+
const themeToggleHtml = this.darkMode ? `<button type="button" class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle theme"></button>` : "";
|
|
20248
|
+
const themeInitJs = this.darkMode ? `${JS_THEME_TOGGLE2}
|
|
20249
|
+
applyTheme(getEffectiveTheme());` : "";
|
|
20250
|
+
const themeAttr = this.darkMode ? ' data-theme="light"' : "";
|
|
20251
|
+
const refsLine = context.baseRef || context.headRef ? `<p class="subtle">Comparing ${escapeHtml3(context.baseRef ?? "base")} \u2192 ${escapeHtml3(context.headRef ?? "head")}</p>` : "";
|
|
20252
|
+
return `<!doctype html>
|
|
20253
|
+
<html lang="en"${themeAttr}>
|
|
20254
|
+
<head>
|
|
20255
|
+
<meta charset="utf-8" />
|
|
20256
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
20257
|
+
<title>${escapeHtml3(this.title)}</title>
|
|
20258
|
+
<style>
|
|
20259
|
+
${this.theme.css}
|
|
20260
|
+
${REVIEW_CSS}
|
|
20261
|
+
</style>
|
|
20262
|
+
</head>
|
|
20263
|
+
<body>
|
|
20264
|
+
<main>
|
|
20265
|
+
<div class="hero-card card">
|
|
20266
|
+
<div class="review-header">
|
|
20267
|
+
<h1>${escapeHtml3(this.title)}</h1>
|
|
20268
|
+
${themeToggleHtml}
|
|
20269
|
+
</div>
|
|
20270
|
+
${refsLine}
|
|
20271
|
+
</div>
|
|
20272
|
+
<section class="summary-grid">
|
|
20273
|
+
<div class="summary-card"><strong>${summary.uncovered}</strong><span>\u{1F534} Uncovered</span></div>
|
|
20274
|
+
<div class="summary-card"><strong>${summary.weaklyCovered}</strong><span>\u{1F7E1} Weak</span></div>
|
|
20275
|
+
<div class="summary-card"><strong>${summary.covered}</strong><span>\u{1F7E2} Covered</span></div>
|
|
20276
|
+
<div class="summary-card"><strong>${summary.totalClaims}</strong><span>Claims</span></div>
|
|
20277
|
+
<div class="summary-card"><strong>${summary.byStrength.strong}</strong><span>Strong</span></div>
|
|
20278
|
+
<div class="summary-card"><strong>${summary.byStrength.weak + summary.byStrength.none}</strong><span>Weak/None</span></div>
|
|
20279
|
+
</section>
|
|
20280
|
+
<section class="card priority-banner">
|
|
20281
|
+
<h2>Review priority</h2>
|
|
20282
|
+
<p class="subtle">${escapeHtml3(priority)}</p>
|
|
20283
|
+
</section>
|
|
20284
|
+
${changedFilesPanel}
|
|
20285
|
+
<section class="toolbar">
|
|
20286
|
+
<input type="search" placeholder="Filter claims by scenario, file, change-type" aria-label="Filter claims" />
|
|
20287
|
+
<button type="button" class="active" data-filter="all">All</button>
|
|
20288
|
+
<button type="button" data-filter="stakeholder">Stakeholder</button>
|
|
20289
|
+
<button type="button" data-filter="engineer">Engineer</button>
|
|
20290
|
+
<button type="button" data-filter="weak">Weak/None</button>
|
|
20291
|
+
</section>
|
|
20292
|
+
${renderAudienceSection2("Stakeholder behaviour", review.claims.filter((c) => c.audience === "stakeholder"))}
|
|
20293
|
+
${renderAudienceSection2("Engineer changes", review.claims.filter((c) => c.audience === "engineer"))}
|
|
20294
|
+
</main>
|
|
20295
|
+
<script>
|
|
20296
|
+
${themeInitJs}
|
|
20297
|
+
const input = document.querySelector('input[type="search"]');
|
|
20298
|
+
const buttons = Array.from(document.querySelectorAll('[data-filter]'));
|
|
20299
|
+
const cards = Array.from(document.querySelectorAll('.claim-card'));
|
|
20300
|
+
let activeFilter = 'all';
|
|
20301
|
+
function applyFilters() {
|
|
20302
|
+
const query = (input.value || '').trim().toLowerCase();
|
|
20303
|
+
cards.forEach((card) => {
|
|
20304
|
+
const audience = card.getAttribute('data-audience');
|
|
20305
|
+
const strength = card.getAttribute('data-strength');
|
|
20306
|
+
const haystack = card.getAttribute('data-search') || '';
|
|
20307
|
+
let matchesFilter = activeFilter === 'all'
|
|
20308
|
+
|| audience === activeFilter
|
|
20309
|
+
|| (activeFilter === 'weak' && (strength === 'weak' || strength === 'none'));
|
|
20310
|
+
const matchesSearch = !query || haystack.includes(query);
|
|
20311
|
+
card.style.display = matchesFilter && matchesSearch ? '' : 'none';
|
|
20312
|
+
});
|
|
20313
|
+
}
|
|
20314
|
+
input.addEventListener('input', applyFilters);
|
|
20315
|
+
buttons.forEach((button) => {
|
|
20316
|
+
button.addEventListener('click', () => {
|
|
20317
|
+
activeFilter = button.getAttribute('data-filter');
|
|
20318
|
+
buttons.forEach((b) => b.classList.toggle('active', b === button));
|
|
20319
|
+
applyFilters();
|
|
20320
|
+
});
|
|
20321
|
+
});
|
|
20322
|
+
applyFilters();
|
|
20323
|
+
</script>
|
|
20324
|
+
</body>
|
|
20325
|
+
</html>`;
|
|
20326
|
+
}
|
|
20327
|
+
};
|
|
20328
|
+
|
|
19422
20329
|
// src/index.ts
|
|
19423
20330
|
var FORMAT_EXTENSIONS = {
|
|
19424
20331
|
astro: ".md",
|
|
@@ -19540,7 +20447,7 @@ var ReportGenerator = class {
|
|
|
19540
20447
|
exclude: options.exclude ?? [],
|
|
19541
20448
|
includeTags: options.includeTags ?? [],
|
|
19542
20449
|
excludeTags: options.excludeTags ?? [],
|
|
19543
|
-
formats: options.formats ?? ["
|
|
20450
|
+
formats: options.formats ?? ["html"],
|
|
19544
20451
|
outputDir: options.outputDir ?? "reports",
|
|
19545
20452
|
outputName: options.outputName ?? "index",
|
|
19546
20453
|
outputNameTimestamp: options.outputNameTimestamp ?? false,
|
|
@@ -19881,6 +20788,8 @@ export {
|
|
|
19881
20788
|
MIN_PERF_SAMPLES,
|
|
19882
20789
|
MarkdownFormatter,
|
|
19883
20790
|
ReportGenerator,
|
|
20791
|
+
ReviewHtmlFormatter,
|
|
20792
|
+
ReviewMarkdownFormatter,
|
|
19884
20793
|
RunDiffHtmlFormatter,
|
|
19885
20794
|
RunDiffMarkdownFormatter,
|
|
19886
20795
|
STORY_META_KEY,
|
|
@@ -19891,6 +20800,7 @@ export {
|
|
|
19891
20800
|
adaptPlaywrightRun,
|
|
19892
20801
|
adaptVitestRun,
|
|
19893
20802
|
assertValidRun,
|
|
20803
|
+
buildReview,
|
|
19894
20804
|
bundleAssets,
|
|
19895
20805
|
calculateFlakiness,
|
|
19896
20806
|
calculateStability,
|
|
@@ -19900,6 +20810,8 @@ export {
|
|
|
19900
20810
|
copyMarkdownAssets,
|
|
19901
20811
|
createPrCommentSummary,
|
|
19902
20812
|
createReportGenerator,
|
|
20813
|
+
deriveAudience,
|
|
20814
|
+
deriveChangeType,
|
|
19903
20815
|
deriveStepResults,
|
|
19904
20816
|
detectCI4 as detectCI,
|
|
19905
20817
|
detectPerformanceTrend,
|
|
@@ -19911,7 +20823,10 @@ export {
|
|
|
19911
20823
|
generateTestCaseId,
|
|
19912
20824
|
getAvailableThemes,
|
|
19913
20825
|
getCssOnlyThemes,
|
|
20826
|
+
gradeEvidence,
|
|
19914
20827
|
hasSufficientHistory,
|
|
20828
|
+
isReviewableSource,
|
|
20829
|
+
isTestFile,
|
|
19915
20830
|
listScenarios,
|
|
19916
20831
|
loadHistory,
|
|
19917
20832
|
mergeStepResults,
|