executable-stories-formatters 0.8.0 → 0.10.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 +1306 -96
- package/dist/cli.js.map +1 -1
- package/dist/{index-BiAYcEiz.d.cts → index-CbWFyoTx.d.cts} +161 -4
- package/dist/{index-BiAYcEiz.d.ts → index-CbWFyoTx.d.ts} +161 -4
- package/dist/index.cjs +1196 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +413 -126
- package/dist/index.d.ts +413 -126
- package/dist/index.js +1178 -55
- package/dist/index.js.map +1 -1
- package/package.json +3 -5
- package/schemas/behavior-manifest-v1.json +65 -0
- package/schemas/examples/dotnet.json +84 -20
- package/schemas/examples/go.json +77 -20
- package/schemas/examples/junit5.json +84 -20
- package/schemas/examples/pytest.json +92 -20
- package/schemas/examples/rust.json +84 -20
- package/schemas/raw-run.schema.json +49 -2
- package/schemas/scenario-index-v1.json +88 -0
- package/schemas/story-report-v1.json +5 -0
- package/bin/intent.js +0 -3
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { parseArgs } from "util";
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
5
|
+
import * as fs9 from "fs";
|
|
6
|
+
import * as path10 from "path";
|
|
7
7
|
|
|
8
8
|
// src/validation/schema-validator.ts
|
|
9
9
|
import Ajv from "ajv/dist/2020.js";
|
|
@@ -414,7 +414,7 @@ var raw_run_schema_default = {
|
|
|
414
414
|
},
|
|
415
415
|
RawAttachment: {
|
|
416
416
|
type: "object",
|
|
417
|
-
description: "A test attachment (screenshot, log, artifact).
|
|
417
|
+
description: "A test attachment (screenshot, log, artifact). Either path-based or inline (body); the ACL decides embed-vs-link.",
|
|
418
418
|
properties: {
|
|
419
419
|
name: {
|
|
420
420
|
type: "string",
|
|
@@ -427,9 +427,40 @@ var raw_run_schema_default = {
|
|
|
427
427
|
path: {
|
|
428
428
|
type: "string",
|
|
429
429
|
description: "File path (relative to projectRoot or absolute)."
|
|
430
|
+
},
|
|
431
|
+
body: {
|
|
432
|
+
type: "string",
|
|
433
|
+
description: "Inline content (e.g., base64-encoded image or UTF-8 text)."
|
|
434
|
+
},
|
|
435
|
+
encoding: {
|
|
436
|
+
type: "string",
|
|
437
|
+
enum: ["BASE64", "IDENTITY"],
|
|
438
|
+
description: "Content encoding for an inline body."
|
|
439
|
+
},
|
|
440
|
+
charset: {
|
|
441
|
+
type: "string",
|
|
442
|
+
description: "Character set for IDENTITY text bodies (default utf-8)."
|
|
443
|
+
},
|
|
444
|
+
fileName: {
|
|
445
|
+
type: "string",
|
|
446
|
+
description: "Actual artifact filename (distinct from the logical name)."
|
|
447
|
+
},
|
|
448
|
+
byteLength: {
|
|
449
|
+
type: "integer",
|
|
450
|
+
minimum: 0,
|
|
451
|
+
description: "Size in bytes, used for embed-vs-link decisions."
|
|
452
|
+
},
|
|
453
|
+
stepIndex: {
|
|
454
|
+
type: "integer",
|
|
455
|
+
minimum: 0,
|
|
456
|
+
description: "Step index this attachment belongs to (undefined = test-case level)."
|
|
457
|
+
},
|
|
458
|
+
stepId: {
|
|
459
|
+
type: "string",
|
|
460
|
+
description: "Stable step ID this attachment belongs to (preferred over stepIndex)."
|
|
430
461
|
}
|
|
431
462
|
},
|
|
432
|
-
required: ["name", "mediaType"
|
|
463
|
+
required: ["name", "mediaType"],
|
|
433
464
|
additionalProperties: false
|
|
434
465
|
},
|
|
435
466
|
RawStepEvent: {
|
|
@@ -475,6 +506,22 @@ var raw_run_schema_default = {
|
|
|
475
506
|
buildNumber: {
|
|
476
507
|
type: "string",
|
|
477
508
|
description: "CI build number or run ID."
|
|
509
|
+
},
|
|
510
|
+
provider: {
|
|
511
|
+
type: "string",
|
|
512
|
+
description: "Typed provider key (e.g., 'github', 'gitlab', 'circleci')."
|
|
513
|
+
},
|
|
514
|
+
branch: {
|
|
515
|
+
type: "string",
|
|
516
|
+
description: "Git branch name."
|
|
517
|
+
},
|
|
518
|
+
commitSha: {
|
|
519
|
+
type: "string",
|
|
520
|
+
description: "Git commit SHA."
|
|
521
|
+
},
|
|
522
|
+
prNumber: {
|
|
523
|
+
type: "string",
|
|
524
|
+
description: "Pull/merge request number."
|
|
478
525
|
}
|
|
479
526
|
},
|
|
480
527
|
required: ["name"],
|
|
@@ -492,17 +539,17 @@ function validateRawRun(data) {
|
|
|
492
539
|
return { valid: true, errors: [] };
|
|
493
540
|
}
|
|
494
541
|
const errors = (validate.errors ?? []).map((err) => {
|
|
495
|
-
const
|
|
542
|
+
const path11 = err.instancePath || "/";
|
|
496
543
|
const message = err.message ?? "unknown error";
|
|
497
544
|
if (err.keyword === "additionalProperties") {
|
|
498
545
|
const extra = err.params.additionalProperty;
|
|
499
|
-
return `${
|
|
546
|
+
return `${path11}: ${message} \u2014 '${extra}'`;
|
|
500
547
|
}
|
|
501
548
|
if (err.keyword === "enum") {
|
|
502
549
|
const allowed = err.params.allowedValues;
|
|
503
|
-
return `${
|
|
550
|
+
return `${path11}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
|
|
504
551
|
}
|
|
505
|
-
return `${
|
|
552
|
+
return `${path11}: ${message}`;
|
|
506
553
|
});
|
|
507
554
|
return { valid: false, errors };
|
|
508
555
|
}
|
|
@@ -845,7 +892,8 @@ function canonicalizeTestCase(raw, options, projectRoot) {
|
|
|
845
892
|
projectName: raw.projectName,
|
|
846
893
|
retry: raw.retry ?? 0,
|
|
847
894
|
retries: raw.retries ?? 0,
|
|
848
|
-
tags
|
|
895
|
+
tags,
|
|
896
|
+
...raw.evidence ? { evidence: raw.evidence } : {}
|
|
849
897
|
};
|
|
850
898
|
}
|
|
851
899
|
function normalizeTags(story) {
|
|
@@ -976,7 +1024,7 @@ ${result.errors.join("\n")}`);
|
|
|
976
1024
|
|
|
977
1025
|
// src/index.ts
|
|
978
1026
|
import "fs";
|
|
979
|
-
import * as
|
|
1027
|
+
import * as path8 from "path";
|
|
980
1028
|
import * as fsPromises from "fs/promises";
|
|
981
1029
|
|
|
982
1030
|
// src/converters/acl/lines.ts
|
|
@@ -1521,6 +1569,9 @@ function buildScenario(tc, featureId) {
|
|
|
1521
1569
|
if (tickets && tickets.length > 0) {
|
|
1522
1570
|
scenario.tickets = tickets.map((t) => t.url ? { id: t.id, url: t.url } : { id: t.id });
|
|
1523
1571
|
}
|
|
1572
|
+
if (tc.story.covers && tc.story.covers.length > 0) {
|
|
1573
|
+
scenario.covers = [...tc.story.covers];
|
|
1574
|
+
}
|
|
1524
1575
|
return scenario;
|
|
1525
1576
|
}
|
|
1526
1577
|
function deriveFeatureTitle(group, relSourceFile) {
|
|
@@ -1644,6 +1695,181 @@ var StoryReportJsonFormatter = class {
|
|
|
1644
1695
|
}
|
|
1645
1696
|
};
|
|
1646
1697
|
|
|
1698
|
+
// src/formatters/scenario-index-json.ts
|
|
1699
|
+
var ScenarioIndexJsonFormatter = class {
|
|
1700
|
+
options;
|
|
1701
|
+
constructor(options = {}) {
|
|
1702
|
+
this.options = {
|
|
1703
|
+
pretty: options.pretty ?? true,
|
|
1704
|
+
filters: options.filters
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
toIndex(run) {
|
|
1708
|
+
return toScenarioIndex(toStoryReport(run), this.options.filters);
|
|
1709
|
+
}
|
|
1710
|
+
format(run) {
|
|
1711
|
+
const index = this.toIndex(run);
|
|
1712
|
+
return this.options.pretty ? JSON.stringify(index, null, 2) : JSON.stringify(index);
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
function toScenarioIndex(report, filters = {}) {
|
|
1716
|
+
const scenarios = report.features.flatMap(
|
|
1717
|
+
(feature) => feature.scenarios.map((scenario) => toScenarioIndexItem(feature, scenario))
|
|
1718
|
+
).filter((scenario) => matchesFilters(scenario, filters));
|
|
1719
|
+
return {
|
|
1720
|
+
schemaVersion: "1.0",
|
|
1721
|
+
runId: report.runId,
|
|
1722
|
+
generatedAtMs: report.finishedAtMs,
|
|
1723
|
+
summary: summarize(scenarios),
|
|
1724
|
+
scenarios
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
function toScenarioIndexItem(feature, scenario) {
|
|
1728
|
+
return {
|
|
1729
|
+
id: scenario.id,
|
|
1730
|
+
title: scenario.title,
|
|
1731
|
+
status: scenario.status,
|
|
1732
|
+
feature: feature.title,
|
|
1733
|
+
sourceFile: feature.sourceFile,
|
|
1734
|
+
sourceLine: scenario.sourceLine,
|
|
1735
|
+
tags: scenario.tags,
|
|
1736
|
+
tickets: scenario.tickets ?? [],
|
|
1737
|
+
covers: scenario.covers ?? [],
|
|
1738
|
+
durationMs: scenario.durationMs,
|
|
1739
|
+
steps: scenario.steps.map((step) => ({
|
|
1740
|
+
id: step.id,
|
|
1741
|
+
index: step.index,
|
|
1742
|
+
keyword: step.keyword,
|
|
1743
|
+
text: step.text,
|
|
1744
|
+
status: step.status,
|
|
1745
|
+
durationMs: step.durationMs,
|
|
1746
|
+
errorMessage: step.errorMessage,
|
|
1747
|
+
docKinds: step.docEntries.map((entry) => entry.kind)
|
|
1748
|
+
})),
|
|
1749
|
+
docKinds: scenario.docEntries.map((entry) => entry.kind),
|
|
1750
|
+
error: scenario.errorMessage ? { message: scenario.errorMessage, stack: scenario.errorStack } : void 0
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
function matchesFilters(scenario, filters) {
|
|
1754
|
+
if (filters.statuses?.length && !filters.statuses.includes(scenario.status)) {
|
|
1755
|
+
return false;
|
|
1756
|
+
}
|
|
1757
|
+
if (filters.tags?.length && !filters.tags.some((tag) => scenario.tags.includes(tag))) {
|
|
1758
|
+
return false;
|
|
1759
|
+
}
|
|
1760
|
+
if (filters.sourceFiles?.length && !filters.sourceFiles.some((sourceFile) => scenario.sourceFile.includes(sourceFile))) {
|
|
1761
|
+
return false;
|
|
1762
|
+
}
|
|
1763
|
+
return true;
|
|
1764
|
+
}
|
|
1765
|
+
function summarize(scenarios) {
|
|
1766
|
+
return {
|
|
1767
|
+
total: scenarios.length,
|
|
1768
|
+
passed: scenarios.filter((scenario) => scenario.status === "passed").length,
|
|
1769
|
+
failed: scenarios.filter((scenario) => scenario.status === "failed").length,
|
|
1770
|
+
skipped: scenarios.filter((scenario) => scenario.status === "skipped").length,
|
|
1771
|
+
pending: scenarios.filter((scenario) => scenario.status === "pending").length,
|
|
1772
|
+
durationMs: scenarios.reduce((total, scenario) => total + scenario.durationMs, 0)
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// src/formatters/behavior-manifest-json.ts
|
|
1777
|
+
var BehaviorManifestJsonFormatter = class {
|
|
1778
|
+
pretty;
|
|
1779
|
+
constructor(options = {}) {
|
|
1780
|
+
this.pretty = options.pretty ?? true;
|
|
1781
|
+
}
|
|
1782
|
+
toManifest(run) {
|
|
1783
|
+
return toBehaviorManifest(toStoryReport(run));
|
|
1784
|
+
}
|
|
1785
|
+
format(run) {
|
|
1786
|
+
const manifest = this.toManifest(run);
|
|
1787
|
+
return this.pretty ? JSON.stringify(manifest, null, 2) : JSON.stringify(manifest);
|
|
1788
|
+
}
|
|
1789
|
+
};
|
|
1790
|
+
function toBehaviorManifest(report) {
|
|
1791
|
+
const index = toScenarioIndex(report);
|
|
1792
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
1793
|
+
const byTag = /* @__PURE__ */ new Map();
|
|
1794
|
+
const docKinds = /* @__PURE__ */ new Set();
|
|
1795
|
+
const debuggerIssues = [];
|
|
1796
|
+
for (const scenario of index.scenarios) {
|
|
1797
|
+
const source = bySource.get(scenario.sourceFile) ?? {
|
|
1798
|
+
path: scenario.sourceFile,
|
|
1799
|
+
scenarioCount: 0,
|
|
1800
|
+
failed: 0,
|
|
1801
|
+
tags: []
|
|
1802
|
+
};
|
|
1803
|
+
source.scenarioCount += 1;
|
|
1804
|
+
if (scenario.status === "failed") source.failed += 1;
|
|
1805
|
+
source.tags = [.../* @__PURE__ */ new Set([...source.tags, ...scenario.tags])].sort();
|
|
1806
|
+
bySource.set(scenario.sourceFile, source);
|
|
1807
|
+
for (const tag of scenario.tags) {
|
|
1808
|
+
const tagEntry = byTag.get(tag) ?? { name: tag, scenarioCount: 0 };
|
|
1809
|
+
tagEntry.scenarioCount += 1;
|
|
1810
|
+
byTag.set(tag, tagEntry);
|
|
1811
|
+
}
|
|
1812
|
+
for (const kind of scenario.docKinds) docKinds.add(kind);
|
|
1813
|
+
for (const step of scenario.steps) {
|
|
1814
|
+
for (const kind of step.docKinds) docKinds.add(kind);
|
|
1815
|
+
}
|
|
1816
|
+
if (!scenarioHasDocs(scenario)) {
|
|
1817
|
+
debuggerIssues.push({
|
|
1818
|
+
severity: "warning",
|
|
1819
|
+
code: "missing-docs",
|
|
1820
|
+
scenarioId: scenario.id,
|
|
1821
|
+
title: scenario.title,
|
|
1822
|
+
message: "Scenario has no doc entries."
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
if (scenario.tags.length === 0) {
|
|
1826
|
+
debuggerIssues.push({
|
|
1827
|
+
severity: "warning",
|
|
1828
|
+
code: "missing-tags",
|
|
1829
|
+
scenarioId: scenario.id,
|
|
1830
|
+
title: scenario.title,
|
|
1831
|
+
message: "Scenario has no tags."
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
if (scenario.covers.length === 0) {
|
|
1835
|
+
debuggerIssues.push({
|
|
1836
|
+
severity: "warning",
|
|
1837
|
+
code: "missing-covers",
|
|
1838
|
+
scenarioId: scenario.id,
|
|
1839
|
+
title: scenario.title,
|
|
1840
|
+
message: "Scenario declares no covers (product-code paths), so code\u2192scenario lookup cannot find it."
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
if (scenario.sourceLine === void 0) {
|
|
1844
|
+
debuggerIssues.push({
|
|
1845
|
+
severity: "warning",
|
|
1846
|
+
code: "missing-source-line",
|
|
1847
|
+
scenarioId: scenario.id,
|
|
1848
|
+
title: scenario.title,
|
|
1849
|
+
message: "Scenario has no source line."
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
const scenariosWithDocs = index.scenarios.filter(scenarioHasDocs).length;
|
|
1854
|
+
return {
|
|
1855
|
+
schemaVersion: "1.0",
|
|
1856
|
+
runId: report.runId,
|
|
1857
|
+
generatedAtMs: report.finishedAtMs,
|
|
1858
|
+
summary: index.summary,
|
|
1859
|
+
sourceFiles: [...bySource.values()].sort((a, b) => a.path.localeCompare(b.path)),
|
|
1860
|
+
tags: [...byTag.values()].sort((a, b) => a.name.localeCompare(b.name)),
|
|
1861
|
+
docCoverage: {
|
|
1862
|
+
scenariosWithDocs,
|
|
1863
|
+
scenariosWithoutDocs: index.scenarios.length - scenariosWithDocs,
|
|
1864
|
+
docKinds: [...docKinds].sort()
|
|
1865
|
+
},
|
|
1866
|
+
debugger: debuggerIssues
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
function scenarioHasDocs(scenario) {
|
|
1870
|
+
return scenario.docKinds.length > 0 || scenario.steps.some((step) => step.docKinds.length > 0);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1647
1873
|
// src/formatters/html/renderers/index.ts
|
|
1648
1874
|
import * as fs2 from "fs";
|
|
1649
1875
|
import * as path3 from "path";
|
|
@@ -14461,7 +14687,7 @@ function renderDocEntry(entry, deps) {
|
|
|
14461
14687
|
// src/formatters/html/renderers/steps.ts
|
|
14462
14688
|
var CONTINUATION_KEYWORDS = ["And", "But", "*"];
|
|
14463
14689
|
function renderStep(step, stepResult, index, deps) {
|
|
14464
|
-
const
|
|
14690
|
+
const statusIcon4 = stepResult ? deps.getStatusIcon(stepResult.status) : "\u25CB";
|
|
14465
14691
|
const statusClass = stepResult ? `status-${stepResult.status}` : "";
|
|
14466
14692
|
const duration = stepResult && stepResult.durationMs > 0 ? `${stepResult.durationMs}ms` : "";
|
|
14467
14693
|
const keywordTrimmed = step.keyword.trim();
|
|
@@ -14470,7 +14696,7 @@ function renderStep(step, stepResult, index, deps) {
|
|
|
14470
14696
|
const stepDocs = deps.renderDocs(step.docs, "step-docs");
|
|
14471
14697
|
const textHtml = deps.highlightStepParams ? deps.highlightStepParams(step.text) : deps.escapeHtml(step.text);
|
|
14472
14698
|
return `<div class="${stepClass}" data-keyword="${deps.escapeHtml(keywordTrimmed)}" data-text="${deps.escapeHtml(step.text)}">
|
|
14473
|
-
<span class="step-status ${statusClass}">${
|
|
14699
|
+
<span class="step-status ${statusClass}">${statusIcon4}</span>
|
|
14474
14700
|
<span class="step-keyword">${deps.escapeHtml(step.keyword)}</span>
|
|
14475
14701
|
<span class="step-text">${textHtml}</span>
|
|
14476
14702
|
<span class="step-duration">${duration}</span>
|
|
@@ -14515,16 +14741,16 @@ function highlightStepParams(text2, deps) {
|
|
|
14515
14741
|
var MIN_METRIC_SAMPLES = 5;
|
|
14516
14742
|
|
|
14517
14743
|
// src/formatters/html/renderers/scenario.ts
|
|
14518
|
-
function renderTicket(ticket, template,
|
|
14744
|
+
function renderTicket(ticket, template, escapeHtml4) {
|
|
14519
14745
|
const url = ticket.url ?? (template ? template.replace("{ticket}", ticket.id) : void 0);
|
|
14520
14746
|
if (url) {
|
|
14521
|
-
return `<a class="tag ticket-tag" href="${
|
|
14747
|
+
return `<a class="tag ticket-tag" href="${escapeHtml4(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml4(ticket.id)}</a>`;
|
|
14522
14748
|
}
|
|
14523
|
-
return `<span class="tag ticket-tag">${
|
|
14749
|
+
return `<span class="tag ticket-tag">${escapeHtml4(ticket.id)}</span>`;
|
|
14524
14750
|
}
|
|
14525
14751
|
function renderScenario(args, deps) {
|
|
14526
14752
|
const { tc } = args;
|
|
14527
|
-
const
|
|
14753
|
+
const statusIcon4 = deps.getStatusIcon(tc.status);
|
|
14528
14754
|
const statusClass = `status-${tc.status}`;
|
|
14529
14755
|
const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
|
|
14530
14756
|
const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
|
|
@@ -14594,7 +14820,7 @@ function renderScenario(args, deps) {
|
|
|
14594
14820
|
<div class="scenario-header" role="button" tabindex="0" aria-expanded="${ariaExpanded}">
|
|
14595
14821
|
<div class="scenario-info">
|
|
14596
14822
|
<div class="scenario-title">
|
|
14597
|
-
<span class="status-icon ${statusClass}">${
|
|
14823
|
+
<span class="status-icon ${statusClass}">${statusIcon4}</span>
|
|
14598
14824
|
<span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
|
|
14599
14825
|
</div>
|
|
14600
14826
|
<div class="scenario-meta">${tags}${tickets}${outcomeBadge}${sourceLink}${traceBadge}${metricBadges}</div>
|
|
@@ -14720,7 +14946,7 @@ function flattenTree(roots) {
|
|
|
14720
14946
|
}
|
|
14721
14947
|
return result;
|
|
14722
14948
|
}
|
|
14723
|
-
function buildTooltip(span,
|
|
14949
|
+
function buildTooltip(span, escapeHtml4) {
|
|
14724
14950
|
const parts = [];
|
|
14725
14951
|
parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
|
|
14726
14952
|
if (span.statusMessage) {
|
|
@@ -14738,7 +14964,7 @@ function buildTooltip(span, escapeHtml3) {
|
|
|
14738
14964
|
if (text2.length > TOOLTIP_MAX_LENGTH) {
|
|
14739
14965
|
text2 = text2.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
|
|
14740
14966
|
}
|
|
14741
|
-
return
|
|
14967
|
+
return escapeHtml4(text2);
|
|
14742
14968
|
}
|
|
14743
14969
|
function renderTraceView(args, deps) {
|
|
14744
14970
|
if (!args.spans || args.spans.length === 0) return "";
|
|
@@ -14961,11 +15187,11 @@ function renderToc(args, deps) {
|
|
|
14961
15187
|
const featureName = suitePaths.length > 0 && suitePaths[0].length > 0 ? suitePaths[0][0] : file.split("/").pop()?.replace(/\.[^.]+$/, "") ?? file;
|
|
14962
15188
|
const featureSlug = `feature-${slugify(file)}`;
|
|
14963
15189
|
const scenarios = testCases.map((tc) => {
|
|
14964
|
-
const
|
|
15190
|
+
const statusIcon4 = deps.getStatusIcon(tc.status);
|
|
14965
15191
|
const statusClass = `status-${tc.status}`;
|
|
14966
15192
|
const failedClass = tc.status === "failed" ? " toc-failed" : "";
|
|
14967
15193
|
return `<a class="toc-scenario${failedClass}" href="#scenario-${tc.id}">
|
|
14968
|
-
<span class="toc-status ${statusClass}">${
|
|
15194
|
+
<span class="toc-status ${statusClass}">${statusIcon4}</span>
|
|
14969
15195
|
${deps.escapeHtml(tc.story.scenario)}
|
|
14970
15196
|
</a>`;
|
|
14971
15197
|
}).join("\n");
|
|
@@ -16478,8 +16704,8 @@ function extractDocAttachments(step) {
|
|
|
16478
16704
|
}
|
|
16479
16705
|
return attachments;
|
|
16480
16706
|
}
|
|
16481
|
-
function guessMediaType(
|
|
16482
|
-
const lower =
|
|
16707
|
+
function guessMediaType(path11) {
|
|
16708
|
+
const lower = path11.toLowerCase();
|
|
16483
16709
|
if (lower.endsWith(".png")) return "image/png";
|
|
16484
16710
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
16485
16711
|
if (lower.endsWith(".gif")) return "image/gif";
|
|
@@ -16620,11 +16846,11 @@ var CucumberHtmlFormatter = class {
|
|
|
16620
16846
|
for (const envelope of envelopes) {
|
|
16621
16847
|
const accepted = htmlStream.write(envelope);
|
|
16622
16848
|
if (!accepted) {
|
|
16623
|
-
await new Promise((
|
|
16849
|
+
await new Promise((resolve9) => htmlStream.once("drain", resolve9));
|
|
16624
16850
|
}
|
|
16625
16851
|
}
|
|
16626
|
-
await new Promise((
|
|
16627
|
-
collector.on("finish",
|
|
16852
|
+
await new Promise((resolve9, reject) => {
|
|
16853
|
+
collector.on("finish", resolve9);
|
|
16628
16854
|
collector.on("error", reject);
|
|
16629
16855
|
htmlStream.end();
|
|
16630
16856
|
});
|
|
@@ -18235,6 +18461,68 @@ function copyMarkdownAssets(options) {
|
|
|
18235
18461
|
};
|
|
18236
18462
|
}
|
|
18237
18463
|
|
|
18464
|
+
// src/watch.ts
|
|
18465
|
+
import * as fs6 from "fs";
|
|
18466
|
+
import * as path7 from "path";
|
|
18467
|
+
function toRun(data, inputType, synthesize) {
|
|
18468
|
+
if (inputType === "canonical") return data;
|
|
18469
|
+
let raw = data;
|
|
18470
|
+
if (synthesize) raw = synthesizeStories(raw);
|
|
18471
|
+
return canonicalizeRun(raw);
|
|
18472
|
+
}
|
|
18473
|
+
async function regenerateArtifacts(options, deps = {}) {
|
|
18474
|
+
const read = deps.readFile ?? ((filePath) => fs6.readFileSync(filePath, "utf8"));
|
|
18475
|
+
const data = JSON.parse(read(path7.resolve(options.input)));
|
|
18476
|
+
const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
|
|
18477
|
+
const generator = new ReportGenerator({
|
|
18478
|
+
formats: options.formats,
|
|
18479
|
+
outputDir: options.outputDir,
|
|
18480
|
+
outputName: options.outputName
|
|
18481
|
+
});
|
|
18482
|
+
const result = await generator.generate(run);
|
|
18483
|
+
return [...result.values()].flat();
|
|
18484
|
+
}
|
|
18485
|
+
function startWatch(options, deps = {}) {
|
|
18486
|
+
const log = deps.log ?? ((message) => console.log(message));
|
|
18487
|
+
const regenerate = deps.regenerate ?? ((input) => regenerateArtifacts({ ...options, input }, deps));
|
|
18488
|
+
const watchFn = deps.watch ?? ((filePath, listener) => fs6.watch(filePath, listener));
|
|
18489
|
+
const debounceMs = options.debounceMs ?? 150;
|
|
18490
|
+
let timer;
|
|
18491
|
+
let running = false;
|
|
18492
|
+
let pending = false;
|
|
18493
|
+
const run = async () => {
|
|
18494
|
+
if (running) {
|
|
18495
|
+
pending = true;
|
|
18496
|
+
return;
|
|
18497
|
+
}
|
|
18498
|
+
running = true;
|
|
18499
|
+
try {
|
|
18500
|
+
const files = await regenerate(options.input);
|
|
18501
|
+
log(`Regenerated ${files.length} artifact file(s) from ${options.input}`);
|
|
18502
|
+
} catch (error) {
|
|
18503
|
+
log(`Watch regeneration failed: ${error.message}`);
|
|
18504
|
+
} finally {
|
|
18505
|
+
running = false;
|
|
18506
|
+
if (pending) {
|
|
18507
|
+
pending = false;
|
|
18508
|
+
trigger();
|
|
18509
|
+
}
|
|
18510
|
+
}
|
|
18511
|
+
};
|
|
18512
|
+
const trigger = () => {
|
|
18513
|
+
if (timer) clearTimeout(timer);
|
|
18514
|
+
timer = setTimeout(() => void run(), debounceMs);
|
|
18515
|
+
};
|
|
18516
|
+
trigger();
|
|
18517
|
+
const watcher = watchFn(path7.resolve(options.input), trigger);
|
|
18518
|
+
return {
|
|
18519
|
+
close: () => {
|
|
18520
|
+
if (timer) clearTimeout(timer);
|
|
18521
|
+
watcher.close();
|
|
18522
|
+
}
|
|
18523
|
+
};
|
|
18524
|
+
}
|
|
18525
|
+
|
|
18238
18526
|
// src/publishers/confluence.ts
|
|
18239
18527
|
function parseAdf(adf) {
|
|
18240
18528
|
let parsed;
|
|
@@ -19403,12 +19691,22 @@ function listScenarios(args, _deps) {
|
|
|
19403
19691
|
const { testCases, format } = args;
|
|
19404
19692
|
if (format === "json") {
|
|
19405
19693
|
const items = testCases.map((tc) => ({
|
|
19694
|
+
id: tc.id,
|
|
19406
19695
|
scenario: tc.story.scenario,
|
|
19407
19696
|
status: tc.status,
|
|
19408
19697
|
sourceFile: tc.sourceFile,
|
|
19409
19698
|
sourceLine: tc.sourceLine,
|
|
19699
|
+
suitePath: tc.story.suitePath ?? tc.titlePath.slice(0, -1),
|
|
19410
19700
|
tags: tc.tags,
|
|
19411
|
-
|
|
19701
|
+
tickets: tc.story.tickets ?? [],
|
|
19702
|
+
covers: tc.story.covers ?? [],
|
|
19703
|
+
durationMs: tc.durationMs,
|
|
19704
|
+
error: tc.errorMessage ? {
|
|
19705
|
+
message: tc.errorMessage,
|
|
19706
|
+
stack: tc.errorStack
|
|
19707
|
+
} : void 0,
|
|
19708
|
+
steps: tc.story.steps.map((step, index) => toScenarioStep(step, index, tc)),
|
|
19709
|
+
docKinds: collectDocKinds(tc)
|
|
19412
19710
|
}));
|
|
19413
19711
|
return JSON.stringify(items, null, 2);
|
|
19414
19712
|
}
|
|
@@ -19481,10 +19779,730 @@ function listScenarios(args, _deps) {
|
|
|
19481
19779
|
];
|
|
19482
19780
|
return lines.join("\n");
|
|
19483
19781
|
}
|
|
19782
|
+
function toScenarioStep(step, index, testCase) {
|
|
19783
|
+
const result = testCase.stepResults.find(
|
|
19784
|
+
(candidate) => candidate.index === index || candidate.stepId === step.id
|
|
19785
|
+
);
|
|
19786
|
+
return {
|
|
19787
|
+
id: step.id,
|
|
19788
|
+
index,
|
|
19789
|
+
keyword: step.keyword,
|
|
19790
|
+
text: step.text,
|
|
19791
|
+
status: result?.status ?? testCase.status,
|
|
19792
|
+
durationMs: result?.durationMs ?? step.durationMs ?? 0,
|
|
19793
|
+
errorMessage: result?.errorMessage,
|
|
19794
|
+
mode: step.mode,
|
|
19795
|
+
docKinds: (step.docs ?? []).map((doc) => doc.kind)
|
|
19796
|
+
};
|
|
19797
|
+
}
|
|
19798
|
+
function collectDocKinds(testCase) {
|
|
19799
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
19800
|
+
for (const doc of testCase.story.docs ?? []) {
|
|
19801
|
+
kinds.add(doc.kind);
|
|
19802
|
+
}
|
|
19803
|
+
for (const step of testCase.story.steps) {
|
|
19804
|
+
for (const doc of step.docs ?? []) {
|
|
19805
|
+
kinds.add(doc.kind);
|
|
19806
|
+
}
|
|
19807
|
+
}
|
|
19808
|
+
return [...kinds].sort();
|
|
19809
|
+
}
|
|
19810
|
+
|
|
19811
|
+
// src/review/conventions.ts
|
|
19812
|
+
var CHANGE_TAG_PREFIX = "change:";
|
|
19813
|
+
var AUDIENCE_TAG_PREFIX = "audience:";
|
|
19814
|
+
var VALID_CHANGE_TYPES = /* @__PURE__ */ new Set([
|
|
19815
|
+
"feature",
|
|
19816
|
+
"bugfix",
|
|
19817
|
+
"refactor",
|
|
19818
|
+
"perf",
|
|
19819
|
+
"deps"
|
|
19820
|
+
]);
|
|
19821
|
+
var STAKEHOLDER_FILE = /(?:\.e2e\.)|(?:^|\/)e2e\/|(?:\.spec\.)/i;
|
|
19822
|
+
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
19823
|
+
"ts",
|
|
19824
|
+
"tsx",
|
|
19825
|
+
"js",
|
|
19826
|
+
"jsx",
|
|
19827
|
+
"mjs",
|
|
19828
|
+
"cjs",
|
|
19829
|
+
"py",
|
|
19830
|
+
"go",
|
|
19831
|
+
"rs",
|
|
19832
|
+
"kt",
|
|
19833
|
+
"kts",
|
|
19834
|
+
"java",
|
|
19835
|
+
"cs",
|
|
19836
|
+
"rb"
|
|
19837
|
+
]);
|
|
19838
|
+
var TEST_INFIX = /\.(?:story\.)?(?:int\.|e2e\.|unit\.)?(?:test|spec|cy)\.[a-z]+$/i;
|
|
19839
|
+
function deriveAudience(sourceFile, tags) {
|
|
19840
|
+
const override = tags.map((t) => t.toLowerCase()).find((t) => t.startsWith(AUDIENCE_TAG_PREFIX));
|
|
19841
|
+
if (override) {
|
|
19842
|
+
const value = override.slice(AUDIENCE_TAG_PREFIX.length);
|
|
19843
|
+
if (value === "stakeholder" || value === "engineer") return value;
|
|
19844
|
+
}
|
|
19845
|
+
return STAKEHOLDER_FILE.test(sourceFile) ? "stakeholder" : "engineer";
|
|
19846
|
+
}
|
|
19847
|
+
function deriveChangeType(tags) {
|
|
19848
|
+
for (const tag of tags) {
|
|
19849
|
+
const lower = tag.toLowerCase();
|
|
19850
|
+
if (lower.startsWith(CHANGE_TAG_PREFIX)) {
|
|
19851
|
+
const value = lower.slice(CHANGE_TAG_PREFIX.length);
|
|
19852
|
+
if (VALID_CHANGE_TYPES.has(value)) return value;
|
|
19853
|
+
}
|
|
19854
|
+
}
|
|
19855
|
+
return "unknown";
|
|
19856
|
+
}
|
|
19857
|
+
function extensionOf(path11) {
|
|
19858
|
+
const base = path11.split("/").pop() ?? path11;
|
|
19859
|
+
const dot = base.lastIndexOf(".");
|
|
19860
|
+
return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
|
|
19861
|
+
}
|
|
19862
|
+
function isTestFile(path11) {
|
|
19863
|
+
return TEST_INFIX.test(path11);
|
|
19864
|
+
}
|
|
19865
|
+
function isReviewableSource(path11) {
|
|
19866
|
+
if (isTestFile(path11)) return false;
|
|
19867
|
+
if (path11.endsWith(".d.ts")) return false;
|
|
19868
|
+
return CODE_EXTENSIONS.has(extensionOf(path11));
|
|
19869
|
+
}
|
|
19870
|
+
function testBaseKey(testFile) {
|
|
19871
|
+
return testFile.replace(TEST_INFIX, "");
|
|
19872
|
+
}
|
|
19873
|
+
function sourceBaseKey(sourceFile) {
|
|
19874
|
+
const dot = sourceFile.lastIndexOf(".");
|
|
19875
|
+
const slash = sourceFile.lastIndexOf("/");
|
|
19876
|
+
return dot > slash ? sourceFile.slice(0, dot) : sourceFile;
|
|
19877
|
+
}
|
|
19878
|
+
|
|
19879
|
+
// src/review/build-review.ts
|
|
19880
|
+
var STRENGTH_RANK = {
|
|
19881
|
+
none: 0,
|
|
19882
|
+
weak: 1,
|
|
19883
|
+
moderate: 2,
|
|
19884
|
+
strong: 3
|
|
19885
|
+
};
|
|
19886
|
+
var INTENT_SECTION_TITLE = /\b(why|intent|approach|rationale|reasoning)\b/i;
|
|
19887
|
+
function findDoc(docs, predicate) {
|
|
19888
|
+
if (!docs) return void 0;
|
|
19889
|
+
for (const doc of docs) {
|
|
19890
|
+
if (predicate(doc)) return doc;
|
|
19891
|
+
const nested = findDoc(doc.children, predicate);
|
|
19892
|
+
if (nested) return nested;
|
|
19893
|
+
}
|
|
19894
|
+
return void 0;
|
|
19895
|
+
}
|
|
19896
|
+
function anyDoc(docs, predicate) {
|
|
19897
|
+
return findDoc(docs, predicate) !== void 0;
|
|
19898
|
+
}
|
|
19899
|
+
function extractIntent(testCase) {
|
|
19900
|
+
const docs = testCase.story.docs;
|
|
19901
|
+
const section = findDoc(
|
|
19902
|
+
docs,
|
|
19903
|
+
(d) => d.kind === "section" && INTENT_SECTION_TITLE.test(d.title)
|
|
19904
|
+
);
|
|
19905
|
+
if (section && section.kind === "section") return section.markdown;
|
|
19906
|
+
const note = findDoc(docs, (d) => d.kind === "note");
|
|
19907
|
+
if (note && note.kind === "note") return note.text;
|
|
19908
|
+
return void 0;
|
|
19909
|
+
}
|
|
19910
|
+
function hasScreenshot(testCase) {
|
|
19911
|
+
if (testCase.attachments.some((a) => a.mediaType.startsWith("image/"))) {
|
|
19912
|
+
return true;
|
|
19913
|
+
}
|
|
19914
|
+
if (anyDoc(testCase.story.docs, (d) => d.kind === "screenshot")) return true;
|
|
19915
|
+
return testCase.story.steps.some(
|
|
19916
|
+
(step) => anyDoc(step.docs, (d) => d.kind === "screenshot")
|
|
19917
|
+
);
|
|
19918
|
+
}
|
|
19919
|
+
function hasOtelTrace(testCase) {
|
|
19920
|
+
return (testCase.story.otelSpans?.length ?? 0) > 0;
|
|
19921
|
+
}
|
|
19922
|
+
function gradeEvidence(testCase, audience) {
|
|
19923
|
+
if (testCase.status !== "passed") {
|
|
19924
|
+
return {
|
|
19925
|
+
strength: "none",
|
|
19926
|
+
reasons: [`test is ${testCase.status} \u2014 the proof does not hold`]
|
|
19927
|
+
};
|
|
19928
|
+
}
|
|
19929
|
+
const ev = testCase.evidence;
|
|
19930
|
+
const screenshot = hasScreenshot(testCase);
|
|
19931
|
+
const otel = hasOtelTrace(testCase);
|
|
19932
|
+
const isIntegration = /\.int\.test\./i.test(testCase.sourceFile);
|
|
19933
|
+
const mutation = ev?.mutationScorePct;
|
|
19934
|
+
const changedCov = ev?.changedLineCoveragePct;
|
|
19935
|
+
const strong2 = [];
|
|
19936
|
+
if (ev?.failingFirstVerified) {
|
|
19937
|
+
strong2.push("failing-first verified (red on base ref, green on head)");
|
|
19938
|
+
}
|
|
19939
|
+
if (typeof mutation === "number" && mutation >= 80) {
|
|
19940
|
+
strong2.push(`mutation score ${mutation}% (\u226580%)`);
|
|
19941
|
+
}
|
|
19942
|
+
if (screenshot && otel) {
|
|
19943
|
+
strong2.push("backed by screenshot + OTEL trace");
|
|
19944
|
+
} else if (audience === "stakeholder" && (screenshot || otel)) {
|
|
19945
|
+
strong2.push(`stakeholder proof: ${screenshot ? "screenshot" : "OTEL trace"}`);
|
|
19946
|
+
}
|
|
19947
|
+
if (strong2.length > 0) return { strength: "strong", reasons: strong2 };
|
|
19948
|
+
const moderate = [];
|
|
19949
|
+
if (screenshot) moderate.push("screenshot attached");
|
|
19950
|
+
if (otel) moderate.push("OTEL trace attached");
|
|
19951
|
+
if (typeof mutation === "number" && mutation >= 50) {
|
|
19952
|
+
moderate.push(`mutation score ${mutation}%`);
|
|
19953
|
+
}
|
|
19954
|
+
if (typeof changedCov === "number" && changedCov >= 80) {
|
|
19955
|
+
moderate.push(`changed-line coverage ${changedCov}%`);
|
|
19956
|
+
}
|
|
19957
|
+
if (isIntegration) moderate.push("integration-level test");
|
|
19958
|
+
if (moderate.length > 0) return { strength: "moderate", reasons: moderate };
|
|
19959
|
+
return {
|
|
19960
|
+
strength: "weak",
|
|
19961
|
+
reasons: [
|
|
19962
|
+
"passing test only \u2014 no corroborating evidence (add e2e proof, mutation score, or failing-first)"
|
|
19963
|
+
]
|
|
19964
|
+
};
|
|
19965
|
+
}
|
|
19966
|
+
function toClaim(testCase, changedSourcePaths) {
|
|
19967
|
+
const audience = deriveAudience(testCase.sourceFile, testCase.tags);
|
|
19968
|
+
const changeType = deriveChangeType(testCase.tags);
|
|
19969
|
+
const { strength, reasons } = gradeEvidence(testCase, audience);
|
|
19970
|
+
const key = testBaseKey(testCase.sourceFile);
|
|
19971
|
+
const coversFiles = changedSourcePaths.filter(
|
|
19972
|
+
(path11) => sourceBaseKey(path11) === key
|
|
19973
|
+
);
|
|
19974
|
+
return {
|
|
19975
|
+
id: testCase.id,
|
|
19976
|
+
scenario: testCase.story.scenario,
|
|
19977
|
+
sourceFile: testCase.sourceFile,
|
|
19978
|
+
sourceLine: testCase.sourceLine,
|
|
19979
|
+
status: testCase.status,
|
|
19980
|
+
audience,
|
|
19981
|
+
changeType,
|
|
19982
|
+
strength,
|
|
19983
|
+
strengthReasons: reasons,
|
|
19984
|
+
intent: extractIntent(testCase),
|
|
19985
|
+
coversFiles,
|
|
19986
|
+
testCase
|
|
19987
|
+
};
|
|
19988
|
+
}
|
|
19989
|
+
function bandFor(claims) {
|
|
19990
|
+
if (claims.length === 0) return "uncovered";
|
|
19991
|
+
const maxRank = Math.max(...claims.map((c) => STRENGTH_RANK[c.strength]));
|
|
19992
|
+
return maxRank >= STRENGTH_RANK.moderate ? "covered" : "weak";
|
|
19993
|
+
}
|
|
19994
|
+
var AUDIENCE_ORDER = {
|
|
19995
|
+
stakeholder: 0,
|
|
19996
|
+
engineer: 1
|
|
19997
|
+
};
|
|
19998
|
+
function buildReview(run, context = { changedFiles: [] }) {
|
|
19999
|
+
const changedSource = context.changedFiles.filter(
|
|
20000
|
+
(f) => isReviewableSource(f.path)
|
|
20001
|
+
);
|
|
20002
|
+
const changedSourcePaths = changedSource.map((f) => f.path);
|
|
20003
|
+
const claims = run.testCases.map((tc) => toClaim(tc, changedSourcePaths));
|
|
20004
|
+
const changedFiles = changedSource.map((file) => {
|
|
20005
|
+
const covering = claims.filter((c) => c.coversFiles.includes(file.path));
|
|
20006
|
+
return {
|
|
20007
|
+
path: file.path,
|
|
20008
|
+
changeKind: file.changeKind,
|
|
20009
|
+
band: bandFor(covering),
|
|
20010
|
+
claims: covering.map((c) => ({
|
|
20011
|
+
id: c.id,
|
|
20012
|
+
scenario: c.scenario,
|
|
20013
|
+
strength: c.strength
|
|
20014
|
+
}))
|
|
20015
|
+
};
|
|
20016
|
+
});
|
|
20017
|
+
const sortedClaims = [...claims].sort((a, b) => {
|
|
20018
|
+
if (AUDIENCE_ORDER[a.audience] !== AUDIENCE_ORDER[b.audience]) {
|
|
20019
|
+
return AUDIENCE_ORDER[a.audience] - AUDIENCE_ORDER[b.audience];
|
|
20020
|
+
}
|
|
20021
|
+
if (STRENGTH_RANK[a.strength] !== STRENGTH_RANK[b.strength]) {
|
|
20022
|
+
return STRENGTH_RANK[a.strength] - STRENGTH_RANK[b.strength];
|
|
20023
|
+
}
|
|
20024
|
+
if (a.sourceFile !== b.sourceFile) {
|
|
20025
|
+
return a.sourceFile.localeCompare(b.sourceFile);
|
|
20026
|
+
}
|
|
20027
|
+
return a.scenario.localeCompare(b.scenario);
|
|
20028
|
+
});
|
|
20029
|
+
const bandRank = { uncovered: 0, weak: 1, covered: 2 };
|
|
20030
|
+
const sortedFiles = [...changedFiles].sort((a, b) => {
|
|
20031
|
+
if (bandRank[a.band] !== bandRank[b.band]) {
|
|
20032
|
+
return bandRank[a.band] - bandRank[b.band];
|
|
20033
|
+
}
|
|
20034
|
+
return a.path.localeCompare(b.path);
|
|
20035
|
+
});
|
|
20036
|
+
const summary = buildSummary2(sortedClaims, sortedFiles);
|
|
20037
|
+
return {
|
|
20038
|
+
run,
|
|
20039
|
+
context,
|
|
20040
|
+
summary,
|
|
20041
|
+
claims: sortedClaims,
|
|
20042
|
+
changedFiles: sortedFiles
|
|
20043
|
+
};
|
|
20044
|
+
}
|
|
20045
|
+
function buildSummary2(claims, changedFiles) {
|
|
20046
|
+
const byAudience = {
|
|
20047
|
+
stakeholder: 0,
|
|
20048
|
+
engineer: 0
|
|
20049
|
+
};
|
|
20050
|
+
const byStrength = {
|
|
20051
|
+
none: 0,
|
|
20052
|
+
weak: 0,
|
|
20053
|
+
moderate: 0,
|
|
20054
|
+
strong: 0
|
|
20055
|
+
};
|
|
20056
|
+
for (const claim of claims) {
|
|
20057
|
+
byAudience[claim.audience] += 1;
|
|
20058
|
+
byStrength[claim.strength] += 1;
|
|
20059
|
+
}
|
|
20060
|
+
return {
|
|
20061
|
+
totalClaims: claims.length,
|
|
20062
|
+
byAudience,
|
|
20063
|
+
byStrength,
|
|
20064
|
+
changedSourceFiles: changedFiles.length,
|
|
20065
|
+
uncovered: changedFiles.filter((f) => f.band === "uncovered").length,
|
|
20066
|
+
weaklyCovered: changedFiles.filter((f) => f.band === "weak").length,
|
|
20067
|
+
covered: changedFiles.filter((f) => f.band === "covered").length
|
|
20068
|
+
};
|
|
20069
|
+
}
|
|
20070
|
+
|
|
20071
|
+
// src/formatters/review-markdown.ts
|
|
20072
|
+
var STRENGTH_BADGE = {
|
|
20073
|
+
strong: "\u{1F7E2} strong",
|
|
20074
|
+
moderate: "\u{1F7E1} moderate",
|
|
20075
|
+
weak: "\u{1F7E0} weak",
|
|
20076
|
+
none: "\u{1F534} none"
|
|
20077
|
+
};
|
|
20078
|
+
function statusIcon2(status) {
|
|
20079
|
+
switch (status) {
|
|
20080
|
+
case "passed":
|
|
20081
|
+
return "\u2705";
|
|
20082
|
+
case "failed":
|
|
20083
|
+
return "\u274C";
|
|
20084
|
+
case "skipped":
|
|
20085
|
+
return "\u2298";
|
|
20086
|
+
default:
|
|
20087
|
+
return "\u2022";
|
|
20088
|
+
}
|
|
20089
|
+
}
|
|
20090
|
+
function escapeCell2(value) {
|
|
20091
|
+
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
20092
|
+
}
|
|
20093
|
+
function intentSummary(intent) {
|
|
20094
|
+
const firstLine = intent.split("\n").find((l) => l.trim().length > 0) ?? "";
|
|
20095
|
+
const trimmed = firstLine.trim();
|
|
20096
|
+
return trimmed.length > 200 ? `${trimmed.slice(0, 197)}\u2026` : trimmed;
|
|
20097
|
+
}
|
|
20098
|
+
function renderTicket2(ticket) {
|
|
20099
|
+
return ticket.url ? `[${ticket.id}](${ticket.url})` : `\`${ticket.id}\``;
|
|
20100
|
+
}
|
|
20101
|
+
function renderUncoveredBand(lines, files) {
|
|
20102
|
+
const uncovered = files.filter((f) => f.band === "uncovered");
|
|
20103
|
+
if (uncovered.length === 0) return;
|
|
20104
|
+
lines.push(`## \u{1F534} Changed code with no evidence (${uncovered.length})`);
|
|
20105
|
+
lines.push("");
|
|
20106
|
+
lines.push("Start here \u2014 these changed source files have no claim or test behind them.");
|
|
20107
|
+
lines.push("");
|
|
20108
|
+
for (const file of uncovered) {
|
|
20109
|
+
lines.push(`- \`${file.path}\` _(${file.changeKind})_`);
|
|
20110
|
+
}
|
|
20111
|
+
lines.push("");
|
|
20112
|
+
}
|
|
20113
|
+
function renderWeakBand(lines, files) {
|
|
20114
|
+
const weak = files.filter((f) => f.band === "weak");
|
|
20115
|
+
if (weak.length === 0) return;
|
|
20116
|
+
lines.push(`## \u{1F7E1} Changed code with weak evidence (${weak.length})`);
|
|
20117
|
+
lines.push("");
|
|
20118
|
+
for (const file of weak) {
|
|
20119
|
+
const covered = file.claims.map((c) => `${escapeCell2(c.scenario)} (${c.strength})`).join(", ");
|
|
20120
|
+
lines.push(`- \`${file.path}\` _(${file.changeKind})_ \u2014 only: ${covered}`);
|
|
20121
|
+
}
|
|
20122
|
+
lines.push("");
|
|
20123
|
+
}
|
|
20124
|
+
function renderClaim(lines, claim) {
|
|
20125
|
+
lines.push(`### ${statusIcon2(claim.status)} ${claim.scenario}`);
|
|
20126
|
+
lines.push("");
|
|
20127
|
+
lines.push(`- File: \`${claim.sourceFile}:${claim.sourceLine}\``);
|
|
20128
|
+
if (claim.changeType !== "unknown") {
|
|
20129
|
+
lines.push(`- Change: \`${claim.changeType}\``);
|
|
20130
|
+
}
|
|
20131
|
+
const tickets = claim.testCase.story.tickets ?? [];
|
|
20132
|
+
if (tickets.length > 0) {
|
|
20133
|
+
lines.push(`- Tickets: ${tickets.map(renderTicket2).join(", ")}`);
|
|
20134
|
+
}
|
|
20135
|
+
lines.push(
|
|
20136
|
+
`- Evidence: ${STRENGTH_BADGE[claim.strength]} \u2014 ${claim.strengthReasons.join("; ")}`
|
|
20137
|
+
);
|
|
20138
|
+
if (claim.coversFiles.length > 0) {
|
|
20139
|
+
lines.push(
|
|
20140
|
+
`- Covers: ${claim.coversFiles.map((f) => `\`${f}\``).join(", ")}`
|
|
20141
|
+
);
|
|
20142
|
+
}
|
|
20143
|
+
if (claim.intent) {
|
|
20144
|
+
lines.push(`- Why: ${escapeCell2(intentSummary(claim.intent))}`);
|
|
20145
|
+
}
|
|
20146
|
+
lines.push("");
|
|
20147
|
+
}
|
|
20148
|
+
function renderAudienceSection(lines, title, claims) {
|
|
20149
|
+
if (claims.length === 0) return;
|
|
20150
|
+
lines.push(`## ${title} (${claims.length})`);
|
|
20151
|
+
lines.push("");
|
|
20152
|
+
for (const claim of claims) {
|
|
20153
|
+
renderClaim(lines, claim);
|
|
20154
|
+
}
|
|
20155
|
+
}
|
|
20156
|
+
var ReviewMarkdownFormatter = class {
|
|
20157
|
+
title;
|
|
20158
|
+
constructor(options = {}) {
|
|
20159
|
+
this.title = options.title ?? "Evidence Review";
|
|
20160
|
+
}
|
|
20161
|
+
format(review) {
|
|
20162
|
+
const lines = [];
|
|
20163
|
+
const { summary, context } = review;
|
|
20164
|
+
lines.push(`# ${this.title}`);
|
|
20165
|
+
lines.push("");
|
|
20166
|
+
if (context.baseRef || context.headRef) {
|
|
20167
|
+
lines.push(
|
|
20168
|
+
`Comparing \`${context.baseRef ?? "base"}\` \u2192 \`${context.headRef ?? "head"}\`.`
|
|
20169
|
+
);
|
|
20170
|
+
lines.push("");
|
|
20171
|
+
}
|
|
20172
|
+
lines.push("## Review priority");
|
|
20173
|
+
lines.push("");
|
|
20174
|
+
if (summary.changedSourceFiles === 0) {
|
|
20175
|
+
lines.push(
|
|
20176
|
+
"No changed source files supplied \u2014 showing claims and evidence only."
|
|
20177
|
+
);
|
|
20178
|
+
} else if (summary.uncovered > 0) {
|
|
20179
|
+
lines.push(
|
|
20180
|
+
`Review the ${summary.uncovered} unaccounted-for file(s) first: changed code with no evidence behind it.`
|
|
20181
|
+
);
|
|
20182
|
+
} else if (summary.weaklyCovered > 0) {
|
|
20183
|
+
lines.push(
|
|
20184
|
+
`No unaccounted-for changes. Review ${summary.weaklyCovered} weakly-covered file(s) next.`
|
|
20185
|
+
);
|
|
20186
|
+
} else {
|
|
20187
|
+
lines.push("Every changed source file is backed by at least moderate evidence.");
|
|
20188
|
+
}
|
|
20189
|
+
lines.push("");
|
|
20190
|
+
if (summary.changedSourceFiles > 0) {
|
|
20191
|
+
lines.push("| \u{1F534} Uncovered | \u{1F7E1} Weak | \u{1F7E2} Covered | Changed files |");
|
|
20192
|
+
lines.push("| ---: | ---: | ---: | ---: |");
|
|
20193
|
+
lines.push(
|
|
20194
|
+
`| ${summary.uncovered} | ${summary.weaklyCovered} | ${summary.covered} | ${summary.changedSourceFiles} |`
|
|
20195
|
+
);
|
|
20196
|
+
lines.push("");
|
|
20197
|
+
}
|
|
20198
|
+
lines.push("| Claims | Stakeholder | Engineer | Strong | Moderate | Weak | None |");
|
|
20199
|
+
lines.push("| ---: | ---: | ---: | ---: | ---: | ---: | ---: |");
|
|
20200
|
+
lines.push(
|
|
20201
|
+
`| ${summary.totalClaims} | ${summary.byAudience.stakeholder} | ${summary.byAudience.engineer} | ${summary.byStrength.strong} | ${summary.byStrength.moderate} | ${summary.byStrength.weak} | ${summary.byStrength.none} |`
|
|
20202
|
+
);
|
|
20203
|
+
lines.push("");
|
|
20204
|
+
renderUncoveredBand(lines, review.changedFiles);
|
|
20205
|
+
renderWeakBand(lines, review.changedFiles);
|
|
20206
|
+
renderAudienceSection(
|
|
20207
|
+
lines,
|
|
20208
|
+
"Stakeholder behaviour",
|
|
20209
|
+
review.claims.filter((c) => c.audience === "stakeholder")
|
|
20210
|
+
);
|
|
20211
|
+
renderAudienceSection(
|
|
20212
|
+
lines,
|
|
20213
|
+
"Engineer changes",
|
|
20214
|
+
review.claims.filter((c) => c.audience === "engineer")
|
|
20215
|
+
);
|
|
20216
|
+
return lines.join("\n").trimEnd();
|
|
20217
|
+
}
|
|
20218
|
+
};
|
|
20219
|
+
|
|
20220
|
+
// src/formatters/review-html.ts
|
|
20221
|
+
function escapeHtml3(value) {
|
|
20222
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
20223
|
+
}
|
|
20224
|
+
var STRENGTH_LABEL = {
|
|
20225
|
+
strong: "Strong",
|
|
20226
|
+
moderate: "Moderate",
|
|
20227
|
+
weak: "Weak",
|
|
20228
|
+
none: "None"
|
|
20229
|
+
};
|
|
20230
|
+
function statusIcon3(status) {
|
|
20231
|
+
switch (status) {
|
|
20232
|
+
case "passed":
|
|
20233
|
+
return "\u2705";
|
|
20234
|
+
case "failed":
|
|
20235
|
+
return "\u274C";
|
|
20236
|
+
case "skipped":
|
|
20237
|
+
return "\u2298";
|
|
20238
|
+
default:
|
|
20239
|
+
return "\u2022";
|
|
20240
|
+
}
|
|
20241
|
+
}
|
|
20242
|
+
function formatStep3(step) {
|
|
20243
|
+
return `<li><strong>${escapeHtml3(step.keyword)}</strong> ${escapeHtml3(step.text)}</li>`;
|
|
20244
|
+
}
|
|
20245
|
+
function inlineDoc(doc) {
|
|
20246
|
+
switch (doc.kind) {
|
|
20247
|
+
case "note":
|
|
20248
|
+
return escapeHtml3(doc.text);
|
|
20249
|
+
case "section":
|
|
20250
|
+
return `<strong>${escapeHtml3(doc.title)}</strong>: ${escapeHtml3(doc.markdown)}`;
|
|
20251
|
+
case "kv":
|
|
20252
|
+
return `${escapeHtml3(doc.label)}: ${escapeHtml3(String(doc.value))}`;
|
|
20253
|
+
case "code":
|
|
20254
|
+
return `${escapeHtml3(doc.label)}: <code>${escapeHtml3(doc.content)}</code>`;
|
|
20255
|
+
case "link":
|
|
20256
|
+
return `${escapeHtml3(doc.label)}: ${escapeHtml3(doc.url)}`;
|
|
20257
|
+
default:
|
|
20258
|
+
return escapeHtml3(doc.kind);
|
|
20259
|
+
}
|
|
20260
|
+
}
|
|
20261
|
+
function renderEvidenceArtifacts(testCase) {
|
|
20262
|
+
const parts = [];
|
|
20263
|
+
for (const att of testCase.attachments) {
|
|
20264
|
+
if (att.mediaType.startsWith("image/") && att.contentEncoding === "BASE64") {
|
|
20265
|
+
parts.push(
|
|
20266
|
+
`<img class="shot" alt="${escapeHtml3(att.name)}" src="data:${escapeHtml3(att.mediaType)};base64,${att.body}" />`
|
|
20267
|
+
);
|
|
20268
|
+
}
|
|
20269
|
+
}
|
|
20270
|
+
if ((testCase.story.otelSpans?.length ?? 0) > 0) {
|
|
20271
|
+
parts.push(
|
|
20272
|
+
`<p class="trace-note">\u{1F4E1} ${testCase.story.otelSpans.length} OTEL span(s) captured</p>`
|
|
20273
|
+
);
|
|
20274
|
+
}
|
|
20275
|
+
return parts.length > 0 ? `<div class="artifacts">${parts.join("")}</div>` : "";
|
|
20276
|
+
}
|
|
20277
|
+
function renderTicketPills(claim) {
|
|
20278
|
+
const tickets = claim.testCase.story.tickets ?? [];
|
|
20279
|
+
if (tickets.length === 0) return "";
|
|
20280
|
+
return `<div class="ticket-row">${tickets.map((ticket) => {
|
|
20281
|
+
const label = escapeHtml3(ticket.id);
|
|
20282
|
+
if (ticket.url) {
|
|
20283
|
+
return `<a class="ticket-pill" href="${escapeHtml3(ticket.url)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
|
|
20284
|
+
}
|
|
20285
|
+
return `<span class="ticket-pill">${label}</span>`;
|
|
20286
|
+
}).join("")}</div>`;
|
|
20287
|
+
}
|
|
20288
|
+
function renderClaimCard(claim) {
|
|
20289
|
+
const ticketSearch = (claim.testCase.story.tickets ?? []).map((ticket) => ticket.id).join(" ");
|
|
20290
|
+
const search = escapeHtml3(
|
|
20291
|
+
`${claim.scenario} ${claim.sourceFile} ${claim.changeType} ${claim.audience} ${claim.strength} ${ticketSearch}`
|
|
20292
|
+
).toLowerCase();
|
|
20293
|
+
const steps = claim.testCase.story.steps.length > 0 ? `<ul class="step-list">${claim.testCase.story.steps.map(formatStep3).join("")}</ul>` : "";
|
|
20294
|
+
const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${escapeHtml3(r)}</li>`).join("")}</ul>`;
|
|
20295
|
+
const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${escapeHtml3(claim.intent)}</div>` : "";
|
|
20296
|
+
const covers = claim.coversFiles.length > 0 ? `<p class="covers">Covers ${claim.coversFiles.map((f) => `<code>${escapeHtml3(f)}</code>`).join(", ")}</p>` : "";
|
|
20297
|
+
const docs = (claim.testCase.story.docs ?? []).filter(
|
|
20298
|
+
(d) => d.kind === "section" || d.kind === "note"
|
|
20299
|
+
);
|
|
20300
|
+
const extraDocs = docs.length > 0 && claim.intent === void 0 ? `<div class="intent">${docs.map(inlineDoc).join("<br>")}</div>` : "";
|
|
20301
|
+
return `
|
|
20302
|
+
<article class="claim-card" data-audience="${claim.audience}" data-strength="${claim.strength}" data-search="${search}">
|
|
20303
|
+
<header class="claim-header">
|
|
20304
|
+
<div>
|
|
20305
|
+
<span class="strength-badge strength-${claim.strength}">${STRENGTH_LABEL[claim.strength]}</span>
|
|
20306
|
+
${claim.changeType !== "unknown" ? `<span class="change-pill">${escapeHtml3(claim.changeType)}</span>` : ""}
|
|
20307
|
+
<h3>${statusIcon3(claim.status)} ${escapeHtml3(claim.scenario)}</h3>
|
|
20308
|
+
<p class="source">${escapeHtml3(`${claim.sourceFile}:${claim.sourceLine}`)}</p>
|
|
20309
|
+
${renderTicketPills(claim)}
|
|
20310
|
+
</div>
|
|
20311
|
+
</header>
|
|
20312
|
+
${intent}${extraDocs}
|
|
20313
|
+
<div class="evidence-block">
|
|
20314
|
+
<span class="evidence-label">Evidence</span>
|
|
20315
|
+
${reasons}
|
|
20316
|
+
</div>
|
|
20317
|
+
${covers}
|
|
20318
|
+
${renderEvidenceArtifacts(claim.testCase)}
|
|
20319
|
+
${steps}
|
|
20320
|
+
</article>`;
|
|
20321
|
+
}
|
|
20322
|
+
function renderChangedFileRow(file) {
|
|
20323
|
+
const claims = file.claims.length > 0 ? file.claims.map((c) => `${escapeHtml3(c.scenario)} <em>(${c.strength})</em>`).join(", ") : "\u2014";
|
|
20324
|
+
return `<tr data-band="${file.band}">
|
|
20325
|
+
<td><span class="band-dot band-${file.band}"></span></td>
|
|
20326
|
+
<td><code>${escapeHtml3(file.path)}</code></td>
|
|
20327
|
+
<td>${escapeHtml3(file.changeKind)}</td>
|
|
20328
|
+
<td>${claims}</td>
|
|
20329
|
+
</tr>`;
|
|
20330
|
+
}
|
|
20331
|
+
function renderAudienceSection2(title, claims) {
|
|
20332
|
+
if (claims.length === 0) return "";
|
|
20333
|
+
return `<section class="audience-section">
|
|
20334
|
+
<h2>${escapeHtml3(title)} <span class="count">${claims.length}</span></h2>
|
|
20335
|
+
<div class="claim-list">${claims.map(renderClaimCard).join("\n")}</div>
|
|
20336
|
+
</section>`;
|
|
20337
|
+
}
|
|
20338
|
+
var REVIEW_CSS = `
|
|
20339
|
+
* { box-sizing: border-box; }
|
|
20340
|
+
body { margin: 0; font-family: var(--font-sans, system-ui, sans-serif); background: var(--background); color: var(--foreground); }
|
|
20341
|
+
main { max-width: 1100px; margin: 0 auto; padding: 32px 20px 80px; }
|
|
20342
|
+
h1, h2, h3, p { margin: 0; }
|
|
20343
|
+
.review-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
|
20344
|
+
.subtle { color: var(--muted-foreground); margin-top: 6px; }
|
|
20345
|
+
.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); }
|
|
20346
|
+
.card, .claim-card, .summary-card, .panel { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius, 16px); }
|
|
20347
|
+
.hero-card { padding: 24px; margin-bottom: 20px; }
|
|
20348
|
+
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; margin-bottom: 20px; }
|
|
20349
|
+
.summary-card { padding: 14px 16px; }
|
|
20350
|
+
.summary-card strong { display: block; font-size: 1.8rem; }
|
|
20351
|
+
.priority-banner { padding: 18px 20px; margin-bottom: 20px; background: linear-gradient(135deg, color-mix(in srgb, var(--destructive) 10%, transparent), var(--card)); }
|
|
20352
|
+
.panel { padding: 18px; margin-bottom: 24px; }
|
|
20353
|
+
table { width: 100%; border-collapse: collapse; }
|
|
20354
|
+
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|
20355
|
+
th { color: var(--muted-foreground); font-weight: 600; }
|
|
20356
|
+
.band-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; }
|
|
20357
|
+
.band-uncovered { background: var(--destructive); }
|
|
20358
|
+
.band-weak { background: var(--warning, #b58900); }
|
|
20359
|
+
.band-covered { background: var(--success, #2e7d32); }
|
|
20360
|
+
.toolbar { position: sticky; top: 12px; z-index: 2; display: flex; flex-wrap: wrap; gap: 10px; padding: 14px; margin-bottom: 20px; }
|
|
20361
|
+
.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); }
|
|
20362
|
+
.toolbar button { border: 1px solid var(--border); background: var(--secondary); border-radius: 999px; padding: 10px 14px; font: inherit; cursor: pointer; color: var(--foreground); }
|
|
20363
|
+
.toolbar button.active { background: var(--foreground); color: var(--background); }
|
|
20364
|
+
.audience-section { margin-bottom: 28px; }
|
|
20365
|
+
.audience-section h2 { margin-bottom: 12px; }
|
|
20366
|
+
.count { color: var(--muted-foreground); font-weight: 400; }
|
|
20367
|
+
.claim-list { display: grid; gap: 14px; }
|
|
20368
|
+
.claim-card { padding: 18px; }
|
|
20369
|
+
.claim-header h3 { margin-top: 8px; }
|
|
20370
|
+
.source { color: var(--muted-foreground); font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.85rem; margin-top: 4px; }
|
|
20371
|
+
.ticket-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
|
20372
|
+
.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; }
|
|
20373
|
+
.ticket-pill:hover { color: var(--foreground); border-color: var(--muted-foreground); }
|
|
20374
|
+
.strength-badge, .change-pill { display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 999px; font-size: 0.8rem; margin-right: 6px; }
|
|
20375
|
+
.change-pill { background: var(--secondary); }
|
|
20376
|
+
.strength-strong { background: color-mix(in srgb, var(--success, #2e7d32) 18%, transparent); color: var(--success, #2e7d32); }
|
|
20377
|
+
.strength-moderate { background: color-mix(in srgb, var(--warning, #b58900) 20%, transparent); color: var(--warning, #b58900); }
|
|
20378
|
+
.strength-weak { background: color-mix(in srgb, #d2691e 20%, transparent); color: #b5530a; }
|
|
20379
|
+
.strength-none { background: color-mix(in srgb, var(--destructive) 16%, transparent); color: var(--destructive); }
|
|
20380
|
+
.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; }
|
|
20381
|
+
.intent-label { font-weight: 700; margin-right: 6px; }
|
|
20382
|
+
.evidence-block { margin-top: 10px; }
|
|
20383
|
+
.evidence-label { font-weight: 600; color: var(--muted-foreground); }
|
|
20384
|
+
.reasons { margin: 6px 0 0; padding-left: 18px; }
|
|
20385
|
+
.covers { color: var(--muted-foreground); margin-top: 8px; font-size: 0.9rem; }
|
|
20386
|
+
.artifacts { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-start; }
|
|
20387
|
+
.shot { max-width: 280px; max-height: 200px; border: 1px solid var(--border); border-radius: 8px; }
|
|
20388
|
+
.trace-note { color: var(--muted-foreground); }
|
|
20389
|
+
.step-list { margin: 12px 0 0; padding-left: 18px; color: var(--muted-foreground); }
|
|
20390
|
+
`;
|
|
20391
|
+
var JS_THEME_TOGGLE2 = `
|
|
20392
|
+
function getSystemTheme() { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; }
|
|
20393
|
+
function getEffectiveTheme() { var s = localStorage.getItem('review-theme'); return (s === 'dark' || s === 'light') ? s : getSystemTheme(); }
|
|
20394
|
+
function toggleTheme() { var n = getEffectiveTheme() === 'dark' ? 'light' : 'dark'; localStorage.setItem('review-theme', n); applyTheme(n); }
|
|
20395
|
+
function applyTheme(t) {
|
|
20396
|
+
document.documentElement.setAttribute('data-theme', t);
|
|
20397
|
+
var b = document.querySelector('.theme-toggle');
|
|
20398
|
+
if (b) { b.textContent = t === 'dark' ? '\\u2600\\ufe0f' : '\\ud83c\\udf19'; }
|
|
20399
|
+
}
|
|
20400
|
+
`;
|
|
20401
|
+
var ReviewHtmlFormatter = class {
|
|
20402
|
+
title;
|
|
20403
|
+
theme;
|
|
20404
|
+
darkMode;
|
|
20405
|
+
constructor(options = {}) {
|
|
20406
|
+
this.title = options.title ?? "Evidence Review";
|
|
20407
|
+
this.theme = resolveTheme(options.theme ?? "default");
|
|
20408
|
+
this.darkMode = options.darkMode ?? true;
|
|
20409
|
+
}
|
|
20410
|
+
format(review) {
|
|
20411
|
+
const { summary, context } = review;
|
|
20412
|
+
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.";
|
|
20413
|
+
const changedFilesPanel = summary.changedSourceFiles > 0 ? `<section class="panel">
|
|
20414
|
+
<h2>Changed files</h2>
|
|
20415
|
+
<table>
|
|
20416
|
+
<thead><tr><th></th><th>File</th><th>Change</th><th>Evidence</th></tr></thead>
|
|
20417
|
+
<tbody>${review.changedFiles.map(renderChangedFileRow).join("")}</tbody>
|
|
20418
|
+
</table>
|
|
20419
|
+
</section>` : "";
|
|
20420
|
+
const themeToggleHtml = this.darkMode ? `<button type="button" class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle theme"></button>` : "";
|
|
20421
|
+
const themeInitJs = this.darkMode ? `${JS_THEME_TOGGLE2}
|
|
20422
|
+
applyTheme(getEffectiveTheme());` : "";
|
|
20423
|
+
const themeAttr = this.darkMode ? ' data-theme="light"' : "";
|
|
20424
|
+
const refsLine = context.baseRef || context.headRef ? `<p class="subtle">Comparing ${escapeHtml3(context.baseRef ?? "base")} \u2192 ${escapeHtml3(context.headRef ?? "head")}</p>` : "";
|
|
20425
|
+
return `<!doctype html>
|
|
20426
|
+
<html lang="en"${themeAttr}>
|
|
20427
|
+
<head>
|
|
20428
|
+
<meta charset="utf-8" />
|
|
20429
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
20430
|
+
<title>${escapeHtml3(this.title)}</title>
|
|
20431
|
+
<style>
|
|
20432
|
+
${this.theme.css}
|
|
20433
|
+
${REVIEW_CSS}
|
|
20434
|
+
</style>
|
|
20435
|
+
</head>
|
|
20436
|
+
<body>
|
|
20437
|
+
<main>
|
|
20438
|
+
<div class="hero-card card">
|
|
20439
|
+
<div class="review-header">
|
|
20440
|
+
<h1>${escapeHtml3(this.title)}</h1>
|
|
20441
|
+
${themeToggleHtml}
|
|
20442
|
+
</div>
|
|
20443
|
+
${refsLine}
|
|
20444
|
+
</div>
|
|
20445
|
+
<section class="summary-grid">
|
|
20446
|
+
<div class="summary-card"><strong>${summary.uncovered}</strong><span>\u{1F534} Uncovered</span></div>
|
|
20447
|
+
<div class="summary-card"><strong>${summary.weaklyCovered}</strong><span>\u{1F7E1} Weak</span></div>
|
|
20448
|
+
<div class="summary-card"><strong>${summary.covered}</strong><span>\u{1F7E2} Covered</span></div>
|
|
20449
|
+
<div class="summary-card"><strong>${summary.totalClaims}</strong><span>Claims</span></div>
|
|
20450
|
+
<div class="summary-card"><strong>${summary.byStrength.strong}</strong><span>Strong</span></div>
|
|
20451
|
+
<div class="summary-card"><strong>${summary.byStrength.weak + summary.byStrength.none}</strong><span>Weak/None</span></div>
|
|
20452
|
+
</section>
|
|
20453
|
+
<section class="card priority-banner">
|
|
20454
|
+
<h2>Review priority</h2>
|
|
20455
|
+
<p class="subtle">${escapeHtml3(priority)}</p>
|
|
20456
|
+
</section>
|
|
20457
|
+
${changedFilesPanel}
|
|
20458
|
+
<section class="toolbar">
|
|
20459
|
+
<input type="search" placeholder="Filter claims by scenario, file, change-type" aria-label="Filter claims" />
|
|
20460
|
+
<button type="button" class="active" data-filter="all">All</button>
|
|
20461
|
+
<button type="button" data-filter="stakeholder">Stakeholder</button>
|
|
20462
|
+
<button type="button" data-filter="engineer">Engineer</button>
|
|
20463
|
+
<button type="button" data-filter="weak">Weak/None</button>
|
|
20464
|
+
</section>
|
|
20465
|
+
${renderAudienceSection2("Stakeholder behaviour", review.claims.filter((c) => c.audience === "stakeholder"))}
|
|
20466
|
+
${renderAudienceSection2("Engineer changes", review.claims.filter((c) => c.audience === "engineer"))}
|
|
20467
|
+
</main>
|
|
20468
|
+
<script>
|
|
20469
|
+
${themeInitJs}
|
|
20470
|
+
const input = document.querySelector('input[type="search"]');
|
|
20471
|
+
const buttons = Array.from(document.querySelectorAll('[data-filter]'));
|
|
20472
|
+
const cards = Array.from(document.querySelectorAll('.claim-card'));
|
|
20473
|
+
let activeFilter = 'all';
|
|
20474
|
+
function applyFilters() {
|
|
20475
|
+
const query = (input.value || '').trim().toLowerCase();
|
|
20476
|
+
cards.forEach((card) => {
|
|
20477
|
+
const audience = card.getAttribute('data-audience');
|
|
20478
|
+
const strength = card.getAttribute('data-strength');
|
|
20479
|
+
const haystack = card.getAttribute('data-search') || '';
|
|
20480
|
+
let matchesFilter = activeFilter === 'all'
|
|
20481
|
+
|| audience === activeFilter
|
|
20482
|
+
|| (activeFilter === 'weak' && (strength === 'weak' || strength === 'none'));
|
|
20483
|
+
const matchesSearch = !query || haystack.includes(query);
|
|
20484
|
+
card.style.display = matchesFilter && matchesSearch ? '' : 'none';
|
|
20485
|
+
});
|
|
20486
|
+
}
|
|
20487
|
+
input.addEventListener('input', applyFilters);
|
|
20488
|
+
buttons.forEach((button) => {
|
|
20489
|
+
button.addEventListener('click', () => {
|
|
20490
|
+
activeFilter = button.getAttribute('data-filter');
|
|
20491
|
+
buttons.forEach((b) => b.classList.toggle('active', b === button));
|
|
20492
|
+
applyFilters();
|
|
20493
|
+
});
|
|
20494
|
+
});
|
|
20495
|
+
applyFilters();
|
|
20496
|
+
</script>
|
|
20497
|
+
</body>
|
|
20498
|
+
</html>`;
|
|
20499
|
+
}
|
|
20500
|
+
};
|
|
19484
20501
|
|
|
19485
20502
|
// src/index.ts
|
|
19486
20503
|
var FORMAT_EXTENSIONS = {
|
|
19487
20504
|
astro: ".md",
|
|
20505
|
+
"behavior-manifest-json": ".behavior-manifest.json",
|
|
19488
20506
|
markdown: ".md",
|
|
19489
20507
|
html: ".html",
|
|
19490
20508
|
"cucumber-html": ".cucumber.html",
|
|
@@ -19492,6 +20510,7 @@ var FORMAT_EXTENSIONS = {
|
|
|
19492
20510
|
"cucumber-json": ".cucumber.json",
|
|
19493
20511
|
"cucumber-messages": ".ndjson",
|
|
19494
20512
|
confluence: ".adf.json",
|
|
20513
|
+
"scenario-index-json": ".scenarios-index.json",
|
|
19495
20514
|
"story-report-json": ".story-report.json"
|
|
19496
20515
|
};
|
|
19497
20516
|
var TEST_EXTENSIONS = [
|
|
@@ -19519,11 +20538,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
19519
20538
|
const ext = FORMAT_EXTENSIONS[format];
|
|
19520
20539
|
const effectiveName = outputName + (outputNameSuffix ?? "");
|
|
19521
20540
|
if (mode === "aggregated") {
|
|
19522
|
-
return toPosix(
|
|
20541
|
+
return toPosix(path8.join(baseOutputDir, `${effectiveName}${ext}`));
|
|
19523
20542
|
}
|
|
19524
20543
|
const normalizedSource = toPosix(sourceFile);
|
|
19525
|
-
const dirOfSource =
|
|
19526
|
-
let baseName =
|
|
20544
|
+
const dirOfSource = path8.posix.dirname(normalizedSource);
|
|
20545
|
+
let baseName = path8.posix.basename(normalizedSource);
|
|
19527
20546
|
for (const testExt of TEST_EXTENSIONS) {
|
|
19528
20547
|
if (baseName.endsWith(testExt)) {
|
|
19529
20548
|
baseName = baseName.slice(0, -testExt.length);
|
|
@@ -19532,9 +20551,9 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
19532
20551
|
}
|
|
19533
20552
|
const fileName = `${baseName}.${effectiveName}${ext}`;
|
|
19534
20553
|
if (colocatedStyle === "adjacent") {
|
|
19535
|
-
return toPosix(
|
|
20554
|
+
return toPosix(path8.posix.join(dirOfSource, fileName));
|
|
19536
20555
|
}
|
|
19537
|
-
return toPosix(
|
|
20556
|
+
return toPosix(path8.posix.join(baseOutputDir, dirOfSource, fileName));
|
|
19538
20557
|
}
|
|
19539
20558
|
function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
|
|
19540
20559
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -19620,6 +20639,12 @@ var ReportGenerator = class {
|
|
|
19620
20639
|
storyReportJson: {
|
|
19621
20640
|
pretty: options.storyReportJson?.pretty ?? true
|
|
19622
20641
|
},
|
|
20642
|
+
scenarioIndexJson: {
|
|
20643
|
+
pretty: options.scenarioIndexJson?.pretty ?? true
|
|
20644
|
+
},
|
|
20645
|
+
behaviorManifestJson: {
|
|
20646
|
+
pretty: options.behaviorManifestJson?.pretty ?? true
|
|
20647
|
+
},
|
|
19623
20648
|
cucumberMessages: {
|
|
19624
20649
|
uriStrategy: options.cucumberMessages?.uriStrategy ?? "sourceFile",
|
|
19625
20650
|
includeSynthetics: options.cucumberMessages?.includeSynthetics ?? true,
|
|
@@ -19735,8 +20760,8 @@ var ReportGenerator = class {
|
|
|
19735
20760
|
if (astroPaths) {
|
|
19736
20761
|
for (const mdPath of astroPaths) {
|
|
19737
20762
|
const content = await fsPromises.readFile(mdPath, "utf8");
|
|
19738
|
-
const mdDir =
|
|
19739
|
-
const assetsDir =
|
|
20763
|
+
const mdDir = path8.dirname(mdPath);
|
|
20764
|
+
const assetsDir = path8.resolve(this.options.astro.assetsDir);
|
|
19740
20765
|
const result = copyMarkdownAssets({
|
|
19741
20766
|
markdown: content,
|
|
19742
20767
|
markdownDir: mdDir,
|
|
@@ -19767,9 +20792,9 @@ var ReportGenerator = class {
|
|
|
19767
20792
|
if (groups.size === 0 && this.options.output.mode === "aggregated") {
|
|
19768
20793
|
const ext = FORMAT_EXTENSIONS[format];
|
|
19769
20794
|
const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
|
|
19770
|
-
const outputPath = toPosix(
|
|
20795
|
+
const outputPath = toPosix(path8.join(this.options.outputDir, `${effectiveName}${ext}`));
|
|
19771
20796
|
const content = await this.formatContent(run, format);
|
|
19772
|
-
const dir =
|
|
20797
|
+
const dir = path8.dirname(outputPath);
|
|
19773
20798
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
19774
20799
|
await this.deps.writeFile(outputPath, content);
|
|
19775
20800
|
return [outputPath];
|
|
@@ -19781,7 +20806,7 @@ var ReportGenerator = class {
|
|
|
19781
20806
|
testCases
|
|
19782
20807
|
};
|
|
19783
20808
|
const content = await this.formatContent(groupRun, format);
|
|
19784
|
-
const dir =
|
|
20809
|
+
const dir = path8.dirname(outputPath);
|
|
19785
20810
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
19786
20811
|
await this.deps.writeFile(outputPath, content);
|
|
19787
20812
|
writtenPaths.push(outputPath);
|
|
@@ -19894,6 +20919,18 @@ var ReportGenerator = class {
|
|
|
19894
20919
|
});
|
|
19895
20920
|
return formatter.format(run);
|
|
19896
20921
|
}
|
|
20922
|
+
case "scenario-index-json": {
|
|
20923
|
+
const formatter = new ScenarioIndexJsonFormatter({
|
|
20924
|
+
pretty: this.options.scenarioIndexJson.pretty
|
|
20925
|
+
});
|
|
20926
|
+
return formatter.format(run);
|
|
20927
|
+
}
|
|
20928
|
+
case "behavior-manifest-json": {
|
|
20929
|
+
const formatter = new BehaviorManifestJsonFormatter({
|
|
20930
|
+
pretty: this.options.behaviorManifestJson.pretty
|
|
20931
|
+
});
|
|
20932
|
+
return formatter.format(run);
|
|
20933
|
+
}
|
|
19897
20934
|
default:
|
|
19898
20935
|
throw new Error(`Unknown format: ${format}`);
|
|
19899
20936
|
}
|
|
@@ -19907,7 +20944,7 @@ async function generateRunComparison(args) {
|
|
|
19907
20944
|
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
19908
20945
|
for (const format of args.formats) {
|
|
19909
20946
|
const ext = format === "html" ? ".html" : ".md";
|
|
19910
|
-
const outputPath = toPosix(
|
|
20947
|
+
const outputPath = toPosix(path8.join(outputDir, `${outputName}${ext}`));
|
|
19911
20948
|
const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
|
|
19912
20949
|
await fsPromises.writeFile(outputPath, content, "utf8");
|
|
19913
20950
|
files.push(outputPath);
|
|
@@ -19916,23 +20953,23 @@ async function generateRunComparison(args) {
|
|
|
19916
20953
|
}
|
|
19917
20954
|
|
|
19918
20955
|
// src/init-astro.ts
|
|
19919
|
-
import * as
|
|
19920
|
-
import * as
|
|
20956
|
+
import * as fs8 from "fs";
|
|
20957
|
+
import * as path9 from "path";
|
|
19921
20958
|
import { fileURLToPath } from "url";
|
|
19922
|
-
var __dirname =
|
|
20959
|
+
var __dirname = path9.dirname(fileURLToPath(import.meta.url));
|
|
19923
20960
|
function initAstro(options = {}) {
|
|
19924
20961
|
const targetDir = options.targetDir ?? "./story-docs";
|
|
19925
20962
|
const force = options.force ?? false;
|
|
19926
|
-
if (
|
|
19927
|
-
const entries =
|
|
20963
|
+
if (fs8.existsSync(targetDir)) {
|
|
20964
|
+
const entries = fs8.readdirSync(targetDir);
|
|
19928
20965
|
if (entries.length > 0 && !force) {
|
|
19929
20966
|
throw new Error(
|
|
19930
20967
|
`Directory "${targetDir}" already exists and is not empty. Use --force to overwrite.`
|
|
19931
20968
|
);
|
|
19932
20969
|
}
|
|
19933
20970
|
}
|
|
19934
|
-
const templateDir =
|
|
19935
|
-
if (!
|
|
20971
|
+
const templateDir = path9.resolve(__dirname, "..", "templates", "astro-starlight");
|
|
20972
|
+
if (!fs8.existsSync(templateDir)) {
|
|
19936
20973
|
throw new Error(
|
|
19937
20974
|
`Template directory not found at ${templateDir}. Ensure the package is installed correctly.`
|
|
19938
20975
|
);
|
|
@@ -19941,24 +20978,24 @@ function initAstro(options = {}) {
|
|
|
19941
20978
|
return { targetDir };
|
|
19942
20979
|
}
|
|
19943
20980
|
function copyDirRecursive(src, dest) {
|
|
19944
|
-
|
|
19945
|
-
const entries =
|
|
20981
|
+
fs8.mkdirSync(dest, { recursive: true });
|
|
20982
|
+
const entries = fs8.readdirSync(src, { withFileTypes: true });
|
|
19946
20983
|
for (const entry of entries) {
|
|
19947
|
-
const srcPath =
|
|
19948
|
-
const destPath =
|
|
20984
|
+
const srcPath = path9.join(src, entry.name);
|
|
20985
|
+
const destPath = path9.join(dest, entry.name);
|
|
19949
20986
|
if (entry.isDirectory()) {
|
|
19950
20987
|
copyDirRecursive(srcPath, destPath);
|
|
19951
20988
|
} else {
|
|
19952
|
-
|
|
20989
|
+
fs8.copyFileSync(srcPath, destPath);
|
|
19953
20990
|
}
|
|
19954
20991
|
}
|
|
19955
20992
|
}
|
|
19956
20993
|
|
|
19957
20994
|
// src/config.ts
|
|
19958
20995
|
import { existsSync as existsSync7 } from "fs";
|
|
19959
|
-
import { resolve as
|
|
20996
|
+
import { resolve as resolve7 } from "path";
|
|
19960
20997
|
async function loadConfig(configPath) {
|
|
19961
|
-
const resolved = configPath ?
|
|
20998
|
+
const resolved = configPath ? resolve7(configPath) : resolve7(process.cwd(), "executable-stories.config.js");
|
|
19962
20999
|
if (!existsSync7(resolved)) return {};
|
|
19963
21000
|
const mod = await import(resolved);
|
|
19964
21001
|
const config = mod.default;
|
|
@@ -19977,6 +21014,7 @@ var EXIT_CANONICAL_VALIDATION = 2;
|
|
|
19977
21014
|
var EXIT_GENERATION = 3;
|
|
19978
21015
|
var EXIT_USAGE = 4;
|
|
19979
21016
|
var EXIT_COMPARE_GATE = 5;
|
|
21017
|
+
var EXIT_REVIEW_GATE = 5;
|
|
19980
21018
|
var HELP_TEXT = `
|
|
19981
21019
|
executable-stories \u2014 Generate reports from test results JSON.
|
|
19982
21020
|
|
|
@@ -19984,6 +21022,7 @@ USAGE
|
|
|
19984
21022
|
executable-stories format <file> [options]
|
|
19985
21023
|
executable-stories format --stdin [options]
|
|
19986
21024
|
executable-stories compare <baseline-file> <current-file> [options]
|
|
21025
|
+
executable-stories review <file> --changed-files <path> [options]
|
|
19987
21026
|
executable-stories list <file> [options]
|
|
19988
21027
|
executable-stories validate <file>
|
|
19989
21028
|
executable-stories validate --stdin
|
|
@@ -19993,7 +21032,9 @@ USAGE
|
|
|
19993
21032
|
|
|
19994
21033
|
SUBCOMMANDS
|
|
19995
21034
|
format Read raw test results and generate reports
|
|
21035
|
+
watch Regenerate reports whenever the raw-run file changes (live agent index)
|
|
19996
21036
|
compare Compare two runs and generate a diff report
|
|
21037
|
+
review Generate an Evidence Review of AI-authored changes (correlate a run to the diff)
|
|
19997
21038
|
list List scenarios from a test run (text table or JSON)
|
|
19998
21039
|
validate Validate a JSON file against the schema (no output generated)
|
|
19999
21040
|
init-astro Scaffold an Astro docs site for story output (Starlight with themed CSS)
|
|
@@ -20001,9 +21042,10 @@ SUBCOMMANDS
|
|
|
20001
21042
|
publish-jira Publish an ADF JSON file to a Jira issue (as comment or description)
|
|
20002
21043
|
|
|
20003
21044
|
OPTIONS
|
|
20004
|
-
--format <formats> Comma-separated formats: html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, astro, confluence, story-report-json, or custom names from config (default: html)
|
|
21045
|
+
--format <formats> Comma-separated formats: html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, astro, confluence, story-report-json, scenario-index-json, behavior-manifest-json, or custom names from config (default: html)
|
|
20005
21046
|
astro Themed Markdown (for Astro docs sites with matching CSS)
|
|
20006
21047
|
confluence Atlassian Document Format (ADF) JSON for Confluence / Jira
|
|
21048
|
+
behavior-manifest-json Agent-readable behavior manifest and debugger warnings
|
|
20007
21049
|
html Custom HTML report (accessible, dark mode, mermaid)
|
|
20008
21050
|
cucumber-html Official Cucumber HTML report
|
|
20009
21051
|
markdown Markdown documentation
|
|
@@ -20011,6 +21053,7 @@ OPTIONS
|
|
|
20011
21053
|
cucumber-json Cucumber JSON
|
|
20012
21054
|
cucumber-messages Raw NDJSON (Cucumber Messages)
|
|
20013
21055
|
story-report-json StoryReport v1 JSON (consumed by executable-stories-react and other UI renderers)
|
|
21056
|
+
scenario-index-json Storybook-like scenario index for agents and explorers
|
|
20014
21057
|
--config <path> Path to executable-stories.config.js (default: ./executable-stories.config.js)
|
|
20015
21058
|
--input-type <type> Input type: raw, canonical, or ndjson (default: raw)
|
|
20016
21059
|
--output-dir <dir> Output directory (default: reports)
|
|
@@ -20044,6 +21087,11 @@ OPTIONS
|
|
|
20044
21087
|
--fail-on-regression Exit non-zero when any regression is detected in compare
|
|
20045
21088
|
--fail-on-added-failures Exit non-zero when newly added scenarios are failing
|
|
20046
21089
|
--max-regressions <n> Exit non-zero when regressions exceed threshold
|
|
21090
|
+
--changed-files <path> (review) Changed files: JSON (ChangedFile[] or {changedFiles,baseRef,headRef}) or "git diff --name-status" text
|
|
21091
|
+
--base-ref <ref> (review) Base ref label shown in the report (informational)
|
|
21092
|
+
--head-ref <ref> (review) Head ref label shown in the report (informational)
|
|
21093
|
+
--fail-on <band> (review) Gate: "uncovered" or "weak" \u2014 exit non-zero when changed code lacks evidence (default: off)
|
|
21094
|
+
--min-evidence <strength> (review) Gate: "weak"|"moderate"|"strong" \u2014 exit non-zero when any claim is below this strength (default: off)
|
|
20047
21095
|
--emit-canonical <path> Write canonical JSON to given path
|
|
20048
21096
|
--help Show this help message
|
|
20049
21097
|
|
|
@@ -20116,9 +21164,9 @@ async function parseCliArgs(argv) {
|
|
|
20116
21164
|
process.exit(EXIT_SUCCESS);
|
|
20117
21165
|
}
|
|
20118
21166
|
const subcommand = args[0];
|
|
20119
|
-
if (subcommand !== "format" && subcommand !== "compare" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
|
|
21167
|
+
if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "review" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
|
|
20120
21168
|
console.error(
|
|
20121
|
-
`Unknown subcommand: "${subcommand}". Use "format", "compare", "list", "validate", "init-astro", "publish-confluence", or "publish-jira".`
|
|
21169
|
+
`Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "review", "list", "validate", "init-astro", "publish-confluence", or "publish-jira".`
|
|
20122
21170
|
);
|
|
20123
21171
|
process.exit(EXIT_USAGE);
|
|
20124
21172
|
}
|
|
@@ -20156,6 +21204,9 @@ async function parseCliArgs(argv) {
|
|
|
20156
21204
|
console.log("");
|
|
20157
21205
|
console.log("Generate story docs with:");
|
|
20158
21206
|
console.log(` executable-stories format run.json --format astro --output-dir ${result.targetDir}/src/content/docs/stories --asset-mode copy`);
|
|
21207
|
+
console.log("");
|
|
21208
|
+
console.log("Generate the Storybook-like scenario explorer data with:");
|
|
21209
|
+
console.log(` executable-stories format run.json --format story-report-json --output-dir ${result.targetDir}/public/stories --output-name story-report`);
|
|
20159
21210
|
process.exit(EXIT_SUCCESS);
|
|
20160
21211
|
} catch (err) {
|
|
20161
21212
|
console.error(`Error: ${err.message}`);
|
|
@@ -20212,6 +21263,11 @@ async function parseCliArgs(argv) {
|
|
|
20212
21263
|
"fail-on-regression": { type: "boolean", default: false },
|
|
20213
21264
|
"fail-on-added-failures": { type: "boolean", default: false },
|
|
20214
21265
|
"max-regressions": { type: "string" },
|
|
21266
|
+
"changed-files": { type: "string" },
|
|
21267
|
+
"base-ref": { type: "string" },
|
|
21268
|
+
"head-ref": { type: "string" },
|
|
21269
|
+
"fail-on": { type: "string" },
|
|
21270
|
+
"min-evidence": { type: "string" },
|
|
20215
21271
|
"config": { type: "string" },
|
|
20216
21272
|
help: { type: "boolean", default: false }
|
|
20217
21273
|
},
|
|
@@ -20252,7 +21308,7 @@ async function parseCliArgs(argv) {
|
|
|
20252
21308
|
}
|
|
20253
21309
|
const pluginConfig = await loadConfig(values["config"]);
|
|
20254
21310
|
const customFormatterNames = new Set(Object.keys(pluginConfig.formatters ?? {}));
|
|
20255
|
-
const builtInFormats = /* @__PURE__ */ new Set(["astro", "confluence", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html", "story-report-json"]);
|
|
21311
|
+
const builtInFormats = /* @__PURE__ */ new Set(["astro", "behavior-manifest-json", "confluence", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html", "scenario-index-json", "story-report-json"]);
|
|
20256
21312
|
const formatStr = values.format;
|
|
20257
21313
|
const allRequestedFormats = formatStr.split(",").map((f) => f.trim());
|
|
20258
21314
|
const builtInRequested = allRequestedFormats.filter((f) => builtInFormats.has(f));
|
|
@@ -20260,7 +21316,7 @@ async function parseCliArgs(argv) {
|
|
|
20260
21316
|
const unknownFormats = allRequestedFormats.filter((f) => !builtInFormats.has(f) && !customFormatterNames.has(f));
|
|
20261
21317
|
if (unknownFormats.length > 0) {
|
|
20262
21318
|
const knownCustom = customFormatterNames.size > 0 ? `, ${[...customFormatterNames].join(", ")}` : "";
|
|
20263
|
-
console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, confluence, html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, story-report-json${knownCustom}.`);
|
|
21319
|
+
console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, behavior-manifest-json, confluence, html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, scenario-index-json, story-report-json${knownCustom}.`);
|
|
20264
21320
|
process.exit(EXIT_USAGE);
|
|
20265
21321
|
}
|
|
20266
21322
|
const formats = builtInRequested;
|
|
@@ -20337,6 +21393,17 @@ async function parseCliArgs(argv) {
|
|
|
20337
21393
|
console.error(`Error: --asset-mode must be "none" or "copy", got "${assetModeRaw}".`);
|
|
20338
21394
|
process.exit(EXIT_USAGE);
|
|
20339
21395
|
}
|
|
21396
|
+
const failOnRaw = values["fail-on"];
|
|
21397
|
+
if (failOnRaw !== void 0 && failOnRaw !== "uncovered" && failOnRaw !== "weak") {
|
|
21398
|
+
console.error(`Error: --fail-on must be "uncovered" or "weak", got "${failOnRaw}".`);
|
|
21399
|
+
process.exit(EXIT_USAGE);
|
|
21400
|
+
}
|
|
21401
|
+
const minEvidenceRaw = values["min-evidence"];
|
|
21402
|
+
const validMinEvidence = /* @__PURE__ */ new Set(["weak", "moderate", "strong"]);
|
|
21403
|
+
if (minEvidenceRaw !== void 0 && !validMinEvidence.has(minEvidenceRaw)) {
|
|
21404
|
+
console.error(`Error: --min-evidence must be "weak", "moderate", or "strong", got "${minEvidenceRaw}".`);
|
|
21405
|
+
process.exit(EXIT_USAGE);
|
|
21406
|
+
}
|
|
20340
21407
|
const cliArgs = {
|
|
20341
21408
|
subcommand,
|
|
20342
21409
|
inputFile,
|
|
@@ -20388,6 +21455,11 @@ async function parseCliArgs(argv) {
|
|
|
20388
21455
|
failOnRegression: values["fail-on-regression"],
|
|
20389
21456
|
failOnAddedFailures: values["fail-on-added-failures"],
|
|
20390
21457
|
maxRegressions,
|
|
21458
|
+
changedFilesPath: values["changed-files"],
|
|
21459
|
+
baseRef: values["base-ref"],
|
|
21460
|
+
headRef: values["head-ref"],
|
|
21461
|
+
failOn: failOnRaw,
|
|
21462
|
+
minEvidence: minEvidenceRaw,
|
|
20391
21463
|
config: values["config"]
|
|
20392
21464
|
};
|
|
20393
21465
|
return { args: cliArgs, pluginConfig, customRequested };
|
|
@@ -20396,27 +21468,27 @@ async function readInput(args) {
|
|
|
20396
21468
|
if (args.stdin) {
|
|
20397
21469
|
return readStdin();
|
|
20398
21470
|
}
|
|
20399
|
-
const filePath =
|
|
20400
|
-
if (!
|
|
21471
|
+
const filePath = path10.resolve(args.inputFile);
|
|
21472
|
+
if (!fs9.existsSync(filePath)) {
|
|
20401
21473
|
console.error(`Error: File not found: ${filePath}`);
|
|
20402
21474
|
process.exit(EXIT_USAGE);
|
|
20403
21475
|
}
|
|
20404
|
-
return
|
|
21476
|
+
return fs9.readFileSync(filePath, "utf8");
|
|
20405
21477
|
}
|
|
20406
21478
|
function readFileInput(filePath) {
|
|
20407
|
-
const resolved =
|
|
20408
|
-
if (!
|
|
21479
|
+
const resolved = path10.resolve(filePath);
|
|
21480
|
+
if (!fs9.existsSync(resolved)) {
|
|
20409
21481
|
console.error(`Error: File not found: ${resolved}`);
|
|
20410
21482
|
process.exit(EXIT_USAGE);
|
|
20411
21483
|
}
|
|
20412
|
-
return
|
|
21484
|
+
return fs9.readFileSync(resolved, "utf8");
|
|
20413
21485
|
}
|
|
20414
21486
|
function readStdin() {
|
|
20415
|
-
return new Promise((
|
|
21487
|
+
return new Promise((resolve9, reject) => {
|
|
20416
21488
|
const chunks = [];
|
|
20417
21489
|
process.stdin.setEncoding("utf8");
|
|
20418
21490
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
20419
|
-
process.stdin.on("end", () =>
|
|
21491
|
+
process.stdin.on("end", () => resolve9(chunks.join("")));
|
|
20420
21492
|
process.stdin.on("error", reject);
|
|
20421
21493
|
});
|
|
20422
21494
|
}
|
|
@@ -20542,14 +21614,14 @@ function tryNormalizeRunFromText(text2, args) {
|
|
|
20542
21614
|
}
|
|
20543
21615
|
}
|
|
20544
21616
|
function listBaselineCandidates(currentFile, args) {
|
|
20545
|
-
const baselineDir =
|
|
20546
|
-
const currentResolved =
|
|
20547
|
-
if (!
|
|
21617
|
+
const baselineDir = path10.resolve(args.baselineDir ?? path10.dirname(currentFile));
|
|
21618
|
+
const currentResolved = path10.resolve(currentFile);
|
|
21619
|
+
if (!fs9.existsSync(baselineDir)) {
|
|
20548
21620
|
console.error(`Error: baseline directory not found: ${baselineDir}`);
|
|
20549
21621
|
process.exit(EXIT_USAGE);
|
|
20550
21622
|
}
|
|
20551
|
-
const entries =
|
|
20552
|
-
return entries.filter((entry) => entry.isFile()).map((entry) =>
|
|
21623
|
+
const entries = fs9.readdirSync(baselineDir, { withFileTypes: true });
|
|
21624
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => path10.join(baselineDir, entry.name)).filter((candidate) => path10.resolve(candidate) !== currentResolved).filter(
|
|
20553
21625
|
(candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
|
|
20554
21626
|
);
|
|
20555
21627
|
}
|
|
@@ -20557,14 +21629,14 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
|
|
|
20557
21629
|
const candidates = listBaselineCandidates(currentFile, args);
|
|
20558
21630
|
const comparable = [];
|
|
20559
21631
|
for (const candidate of candidates) {
|
|
20560
|
-
const run = tryNormalizeRunFromText(
|
|
21632
|
+
const run = tryNormalizeRunFromText(fs9.readFileSync(candidate, "utf8"), args);
|
|
20561
21633
|
if (run) {
|
|
20562
21634
|
comparable.push({ file: candidate, run });
|
|
20563
21635
|
}
|
|
20564
21636
|
}
|
|
20565
21637
|
if (comparable.length === 0) {
|
|
20566
21638
|
console.error(
|
|
20567
|
-
`Error: no compatible baseline files found in ${
|
|
21639
|
+
`Error: no compatible baseline files found in ${path10.resolve(args.baselineDir ?? path10.dirname(currentFile))}.`
|
|
20568
21640
|
);
|
|
20569
21641
|
process.exit(EXIT_USAGE);
|
|
20570
21642
|
}
|
|
@@ -20601,6 +21673,30 @@ async function main() {
|
|
|
20601
21673
|
process.exit(EXIT_GENERATION);
|
|
20602
21674
|
}
|
|
20603
21675
|
}
|
|
21676
|
+
if (args.subcommand === "review") {
|
|
21677
|
+
const text3 = await readInput(args);
|
|
21678
|
+
const run = applySelection(normalizeRunFromText(text3, args).run, args);
|
|
21679
|
+
const context = loadReviewContext(args);
|
|
21680
|
+
const review = buildReview(run, context);
|
|
21681
|
+
try {
|
|
21682
|
+
const files = writeReviewReport(review, args);
|
|
21683
|
+
for (const f of files) {
|
|
21684
|
+
console.log(f);
|
|
21685
|
+
}
|
|
21686
|
+
const gateFailures = evaluateReviewGate(review, args);
|
|
21687
|
+
if (gateFailures.length > 0) {
|
|
21688
|
+
for (const failure of gateFailures) {
|
|
21689
|
+
console.error(`Review gate failed: ${failure}`);
|
|
21690
|
+
}
|
|
21691
|
+
process.exit(EXIT_REVIEW_GATE);
|
|
21692
|
+
}
|
|
21693
|
+
process.exit(EXIT_SUCCESS);
|
|
21694
|
+
} catch (err) {
|
|
21695
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
21696
|
+
console.error(`Review failed: ${msg}`);
|
|
21697
|
+
process.exit(EXIT_GENERATION);
|
|
21698
|
+
}
|
|
21699
|
+
}
|
|
20604
21700
|
if (args.subcommand === "list") {
|
|
20605
21701
|
const text3 = await readInput(args);
|
|
20606
21702
|
const run = applySelection(normalizeRunFromText(text3, args).run, args);
|
|
@@ -20617,6 +21713,24 @@ async function main() {
|
|
|
20617
21713
|
console.log(output);
|
|
20618
21714
|
process.exit(EXIT_SUCCESS);
|
|
20619
21715
|
}
|
|
21716
|
+
if (args.subcommand === "watch") {
|
|
21717
|
+
if (!args.inputFile) {
|
|
21718
|
+
console.error("Error: watch requires an input file (the raw-run JSON the framework writes).");
|
|
21719
|
+
process.exit(EXIT_USAGE);
|
|
21720
|
+
}
|
|
21721
|
+
console.log(
|
|
21722
|
+
`Watching ${args.inputFile} \u2192 regenerating [${args.formats.join(", ")}] into ${args.outputDir}/ (Ctrl+C to stop)`
|
|
21723
|
+
);
|
|
21724
|
+
startWatch({
|
|
21725
|
+
input: args.inputFile,
|
|
21726
|
+
outputDir: args.outputDir,
|
|
21727
|
+
outputName: args.outputName,
|
|
21728
|
+
formats: args.formats,
|
|
21729
|
+
inputType: args.inputType === "canonical" ? "canonical" : "raw",
|
|
21730
|
+
synthesize: args.synthesizeStories
|
|
21731
|
+
});
|
|
21732
|
+
return;
|
|
21733
|
+
}
|
|
20620
21734
|
const text2 = await readInput(args);
|
|
20621
21735
|
if (args.inputType === "ndjson") {
|
|
20622
21736
|
if (args.subcommand === "validate") {
|
|
@@ -20660,9 +21774,9 @@ async function main() {
|
|
|
20660
21774
|
process.exit(EXIT_SCHEMA_VALIDATION);
|
|
20661
21775
|
}
|
|
20662
21776
|
if (args.emitCanonical) {
|
|
20663
|
-
const outPath =
|
|
20664
|
-
|
|
20665
|
-
|
|
21777
|
+
const outPath = path10.resolve(args.emitCanonical);
|
|
21778
|
+
fs9.mkdirSync(path10.dirname(outPath), { recursive: true });
|
|
21779
|
+
fs9.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
20666
21780
|
}
|
|
20667
21781
|
try {
|
|
20668
21782
|
const result = await generateReports(run, args);
|
|
@@ -20719,9 +21833,9 @@ ${msg}`);
|
|
|
20719
21833
|
}
|
|
20720
21834
|
const run = data;
|
|
20721
21835
|
if (args.emitCanonical) {
|
|
20722
|
-
const outPath =
|
|
20723
|
-
|
|
20724
|
-
|
|
21836
|
+
const outPath = path10.resolve(args.emitCanonical);
|
|
21837
|
+
fs9.mkdirSync(path10.dirname(outPath), { recursive: true });
|
|
21838
|
+
fs9.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
20725
21839
|
}
|
|
20726
21840
|
try {
|
|
20727
21841
|
const result = await generateReports(run, args);
|
|
@@ -20777,9 +21891,9 @@ ${msg}`);
|
|
|
20777
21891
|
process.exit(EXIT_CANONICAL_VALIDATION);
|
|
20778
21892
|
}
|
|
20779
21893
|
if (args.emitCanonical) {
|
|
20780
|
-
const outPath =
|
|
20781
|
-
|
|
20782
|
-
|
|
21894
|
+
const outPath = path10.resolve(args.emitCanonical);
|
|
21895
|
+
fs9.mkdirSync(path10.dirname(outPath), { recursive: true });
|
|
21896
|
+
fs9.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
|
|
20783
21897
|
}
|
|
20784
21898
|
try {
|
|
20785
21899
|
const result = await generateReports(canonical, args, droppedMissingStory);
|
|
@@ -20804,9 +21918,9 @@ function runCustomFormatters(run, customRequested, formatters, args) {
|
|
|
20804
21918
|
const ext = formatter.fileExtension ?? formatName;
|
|
20805
21919
|
const baseName = args.outputName ?? "report";
|
|
20806
21920
|
const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
|
|
20807
|
-
const filepath =
|
|
20808
|
-
|
|
20809
|
-
|
|
21921
|
+
const filepath = path10.join(outputDir, filename);
|
|
21922
|
+
fs9.mkdirSync(outputDir, { recursive: true });
|
|
21923
|
+
fs9.writeFileSync(filepath, content, "utf8");
|
|
20810
21924
|
console.log(`Generated: ${filepath}`);
|
|
20811
21925
|
} catch (err) {
|
|
20812
21926
|
console.error(`Error running custom formatter "${formatName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -20856,13 +21970,13 @@ async function dispatchNotifications(run, args) {
|
|
|
20856
21970
|
}
|
|
20857
21971
|
function runHistoryPipeline(run, args) {
|
|
20858
21972
|
if (!args.historyFile) return;
|
|
20859
|
-
const historyPath =
|
|
21973
|
+
const historyPath = path10.resolve(args.historyFile);
|
|
20860
21974
|
const store = loadHistory(
|
|
20861
21975
|
{ filePath: historyPath },
|
|
20862
21976
|
{
|
|
20863
21977
|
readFile: (p) => {
|
|
20864
21978
|
try {
|
|
20865
|
-
return
|
|
21979
|
+
return fs9.readFileSync(p, "utf8");
|
|
20866
21980
|
} catch {
|
|
20867
21981
|
return void 0;
|
|
20868
21982
|
}
|
|
@@ -20875,11 +21989,11 @@ function runHistoryPipeline(run, args) {
|
|
|
20875
21989
|
run,
|
|
20876
21990
|
maxRuns: args.maxHistoryRuns
|
|
20877
21991
|
});
|
|
20878
|
-
const dir =
|
|
20879
|
-
|
|
21992
|
+
const dir = path10.dirname(historyPath);
|
|
21993
|
+
fs9.mkdirSync(dir, { recursive: true });
|
|
20880
21994
|
saveHistory(
|
|
20881
21995
|
{ filePath: historyPath, store: updated },
|
|
20882
|
-
{ writeFile: (p, content) =>
|
|
21996
|
+
{ writeFile: (p, content) => fs9.writeFileSync(p, content, "utf8") }
|
|
20883
21997
|
);
|
|
20884
21998
|
let metricsCount = 0;
|
|
20885
21999
|
for (const testId of Object.keys(updated.tests)) {
|
|
@@ -20959,6 +22073,102 @@ async function generateCompareReports(baseline, current, baselineFile, args) {
|
|
|
20959
22073
|
prSummary: args.prSummary || args.prSummaryFile ? createPrCommentSummary(result.diff) : void 0
|
|
20960
22074
|
};
|
|
20961
22075
|
}
|
|
22076
|
+
var STRENGTH_RANK2 = {
|
|
22077
|
+
none: 0,
|
|
22078
|
+
weak: 1,
|
|
22079
|
+
moderate: 2,
|
|
22080
|
+
strong: 3
|
|
22081
|
+
};
|
|
22082
|
+
function mapStatus(status) {
|
|
22083
|
+
const letter = status.charAt(0).toUpperCase();
|
|
22084
|
+
if (letter === "A") return "added";
|
|
22085
|
+
if (letter === "D") return "deleted";
|
|
22086
|
+
if (letter === "R") return "renamed";
|
|
22087
|
+
if (letter === "C") return "added";
|
|
22088
|
+
return "modified";
|
|
22089
|
+
}
|
|
22090
|
+
function parseNameStatus(text2) {
|
|
22091
|
+
const files = [];
|
|
22092
|
+
for (const raw of text2.split("\n")) {
|
|
22093
|
+
const line = raw.trim();
|
|
22094
|
+
if (!line) continue;
|
|
22095
|
+
const cols = line.includes(" ") ? line.split(" ") : line.split(/\s+/);
|
|
22096
|
+
const status = cols[0];
|
|
22097
|
+
if (!status) continue;
|
|
22098
|
+
const filePath = /^[RC]/i.test(status) && cols.length >= 3 ? cols[cols.length - 1] : cols[1];
|
|
22099
|
+
if (!filePath) continue;
|
|
22100
|
+
files.push({ path: filePath, changeKind: mapStatus(status) });
|
|
22101
|
+
}
|
|
22102
|
+
return files;
|
|
22103
|
+
}
|
|
22104
|
+
var VALID_CHANGE_KINDS = /* @__PURE__ */ new Set(["added", "modified", "deleted", "renamed"]);
|
|
22105
|
+
function coerceChangedFile(value) {
|
|
22106
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
22107
|
+
const obj = value;
|
|
22108
|
+
if (typeof obj.path !== "string") return void 0;
|
|
22109
|
+
const kind = typeof obj.changeKind === "string" && VALID_CHANGE_KINDS.has(obj.changeKind) ? obj.changeKind : "modified";
|
|
22110
|
+
const changedLines = Array.isArray(obj.changedLines) ? obj.changedLines.filter((n) => typeof n === "number") : void 0;
|
|
22111
|
+
return changedLines ? { path: obj.path, changeKind: kind, changedLines } : { path: obj.path, changeKind: kind };
|
|
22112
|
+
}
|
|
22113
|
+
function loadReviewContext(args) {
|
|
22114
|
+
let changedFiles = [];
|
|
22115
|
+
let baseRef = args.baseRef;
|
|
22116
|
+
let headRef = args.headRef;
|
|
22117
|
+
if (args.changedFilesPath) {
|
|
22118
|
+
const text2 = readFileInput(args.changedFilesPath);
|
|
22119
|
+
const parsed = tryParseJson(text2);
|
|
22120
|
+
if (Array.isArray(parsed)) {
|
|
22121
|
+
changedFiles = parsed.map(coerceChangedFile).filter((f) => f !== void 0);
|
|
22122
|
+
} else if (parsed && typeof parsed === "object") {
|
|
22123
|
+
const obj = parsed;
|
|
22124
|
+
if (Array.isArray(obj.changedFiles)) {
|
|
22125
|
+
changedFiles = obj.changedFiles.map(coerceChangedFile).filter((f) => f !== void 0);
|
|
22126
|
+
}
|
|
22127
|
+
if (typeof obj.baseRef === "string") baseRef = baseRef ?? obj.baseRef;
|
|
22128
|
+
if (typeof obj.headRef === "string") headRef = headRef ?? obj.headRef;
|
|
22129
|
+
} else {
|
|
22130
|
+
changedFiles = parseNameStatus(text2);
|
|
22131
|
+
}
|
|
22132
|
+
}
|
|
22133
|
+
return { changedFiles, baseRef, headRef };
|
|
22134
|
+
}
|
|
22135
|
+
function writeReviewReport(review, args) {
|
|
22136
|
+
const title = args.htmlTitle && args.htmlTitle !== "Test Results" ? args.htmlTitle : void 0;
|
|
22137
|
+
const titleOpt = title ? { title } : {};
|
|
22138
|
+
const markdown = new ReviewMarkdownFormatter(titleOpt).format(review);
|
|
22139
|
+
const html = new ReviewHtmlFormatter({ ...titleOpt, theme: args.htmlTheme }).format(review);
|
|
22140
|
+
const outputDir = args.outputDir ?? "reports";
|
|
22141
|
+
const baseName = args.outputName ?? "evidence-review";
|
|
22142
|
+
const suffix = args.outputNameTimestamp ? `-${Math.floor(review.run.startedAtMs / 1e3)}` : "";
|
|
22143
|
+
fs9.mkdirSync(outputDir, { recursive: true });
|
|
22144
|
+
const mdPath = path10.join(outputDir, `${baseName}${suffix}.md`);
|
|
22145
|
+
const htmlPath = path10.join(outputDir, `${baseName}${suffix}.html`);
|
|
22146
|
+
fs9.writeFileSync(mdPath, markdown, "utf8");
|
|
22147
|
+
fs9.writeFileSync(htmlPath, html, "utf8");
|
|
22148
|
+
return [mdPath, htmlPath];
|
|
22149
|
+
}
|
|
22150
|
+
function evaluateReviewGate(review, args) {
|
|
22151
|
+
const failures = [];
|
|
22152
|
+
const { summary } = review;
|
|
22153
|
+
if (args.failOn === "uncovered" && summary.uncovered > 0) {
|
|
22154
|
+
failures.push(`${summary.uncovered} changed source file(s) have no evidence`);
|
|
22155
|
+
}
|
|
22156
|
+
if (args.failOn === "weak" && summary.uncovered + summary.weaklyCovered > 0) {
|
|
22157
|
+
failures.push(
|
|
22158
|
+
`${summary.uncovered + summary.weaklyCovered} changed source file(s) lack moderate+ evidence`
|
|
22159
|
+
);
|
|
22160
|
+
}
|
|
22161
|
+
if (args.minEvidence) {
|
|
22162
|
+
const threshold = STRENGTH_RANK2[args.minEvidence];
|
|
22163
|
+
const below = review.claims.filter((c) => STRENGTH_RANK2[c.strength] < threshold);
|
|
22164
|
+
if (below.length > 0) {
|
|
22165
|
+
failures.push(
|
|
22166
|
+
`${below.length} claim(s) below "${args.minEvidence}" evidence strength`
|
|
22167
|
+
);
|
|
22168
|
+
}
|
|
22169
|
+
}
|
|
22170
|
+
return failures;
|
|
22171
|
+
}
|
|
20962
22172
|
function printResult(result, args, startMs, droppedMissingStory = 0) {
|
|
20963
22173
|
const durationMs = Date.now() - startMs;
|
|
20964
22174
|
if (args.jsonSummary) {
|
|
@@ -20980,9 +22190,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
|
|
|
20980
22190
|
function printCompareResult(result, args, startMs) {
|
|
20981
22191
|
const durationMs = Date.now() - startMs;
|
|
20982
22192
|
if (result.prSummary && args.prSummaryFile) {
|
|
20983
|
-
const outputPath =
|
|
20984
|
-
|
|
20985
|
-
|
|
22193
|
+
const outputPath = path10.resolve(args.prSummaryFile);
|
|
22194
|
+
fs9.mkdirSync(path10.dirname(outputPath), { recursive: true });
|
|
22195
|
+
fs9.writeFileSync(outputPath, result.prSummary, "utf8");
|
|
20986
22196
|
}
|
|
20987
22197
|
if (args.jsonSummary) {
|
|
20988
22198
|
console.log(
|
|
@@ -21073,7 +22283,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
21073
22283
|
console.error("Error: missing ADF file argument. Run with --help for usage.");
|
|
21074
22284
|
process.exit(EXIT_USAGE);
|
|
21075
22285
|
}
|
|
21076
|
-
if (!
|
|
22286
|
+
if (!fs9.existsSync(inputFile)) {
|
|
21077
22287
|
console.error(`Error: file not found: ${inputFile}`);
|
|
21078
22288
|
process.exit(EXIT_USAGE);
|
|
21079
22289
|
}
|
|
@@ -21101,7 +22311,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
21101
22311
|
console.error("Error: --title is required when creating a new page");
|
|
21102
22312
|
process.exit(EXIT_USAGE);
|
|
21103
22313
|
}
|
|
21104
|
-
const adf =
|
|
22314
|
+
const adf = fs9.readFileSync(path10.resolve(inputFile), "utf8");
|
|
21105
22315
|
if (dryRun) {
|
|
21106
22316
|
console.log(
|
|
21107
22317
|
JSON.stringify(
|
|
@@ -21180,7 +22390,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
21180
22390
|
console.error("Error: missing ADF file argument. Run with --help for usage.");
|
|
21181
22391
|
process.exit(EXIT_USAGE);
|
|
21182
22392
|
}
|
|
21183
|
-
if (!
|
|
22393
|
+
if (!fs9.existsSync(inputFile)) {
|
|
21184
22394
|
console.error(`Error: file not found: ${inputFile}`);
|
|
21185
22395
|
process.exit(EXIT_USAGE);
|
|
21186
22396
|
}
|
|
@@ -21207,7 +22417,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
21207
22417
|
process.exit(EXIT_USAGE);
|
|
21208
22418
|
}
|
|
21209
22419
|
const mode = modeRaw;
|
|
21210
|
-
const adf =
|
|
22420
|
+
const adf = fs9.readFileSync(path10.resolve(inputFile), "utf8");
|
|
21211
22421
|
if (dryRun) {
|
|
21212
22422
|
console.log(
|
|
21213
22423
|
JSON.stringify(
|