executable-stories-formatters 0.9.0 → 0.11.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/README.md +35 -0
- package/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +1437 -126
- package/dist/cli.js.map +1 -1
- package/dist/{index-it3Pkmqv.d.cts → index-DF16Xl5i.d.cts} +9 -0
- package/dist/{index-it3Pkmqv.d.ts → index-DF16Xl5i.d.ts} +9 -0
- package/dist/index.cjs +613 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +240 -7
- package/dist/index.d.ts +240 -7
- package/dist/index.js +602 -54
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- 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 +19 -0
- package/schemas/scenario-index-v1.json +88 -0
- package/schemas/story-report-v1.json +22 -0
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 fs13 from "fs";
|
|
6
|
+
import * as path14 from "path";
|
|
7
7
|
|
|
8
8
|
// src/validation/schema-validator.ts
|
|
9
9
|
import Ajv from "ajv/dist/2020.js";
|
|
@@ -209,6 +209,11 @@ var raw_run_schema_default = {
|
|
|
209
209
|
type: "integer",
|
|
210
210
|
minimum: 0,
|
|
211
211
|
description: "Order in which the story was defined in source (for stable sorting)."
|
|
212
|
+
},
|
|
213
|
+
otelSpans: {
|
|
214
|
+
type: "array",
|
|
215
|
+
items: { type: "object" },
|
|
216
|
+
description: "OpenTelemetry spans captured for this scenario (from autotel), used to render a trace waterfall."
|
|
212
217
|
}
|
|
213
218
|
},
|
|
214
219
|
required: ["scenario"],
|
|
@@ -397,6 +402,20 @@ var raw_run_schema_default = {
|
|
|
397
402
|
required: ["kind", "path", "phase"],
|
|
398
403
|
additionalProperties: false
|
|
399
404
|
},
|
|
405
|
+
{
|
|
406
|
+
type: "object",
|
|
407
|
+
description: "Video reference with optional caption and poster image.",
|
|
408
|
+
properties: {
|
|
409
|
+
kind: { const: "video" },
|
|
410
|
+
path: { type: "string" },
|
|
411
|
+
caption: { type: "string" },
|
|
412
|
+
poster: { type: "string" },
|
|
413
|
+
phase: { $ref: "#/$defs/DocPhase" },
|
|
414
|
+
children: { type: "array", items: { $ref: "#/$defs/DocEntry" }, description: "Nested child doc entries for grouping." }
|
|
415
|
+
},
|
|
416
|
+
required: ["kind", "path", "phase"],
|
|
417
|
+
additionalProperties: false
|
|
418
|
+
},
|
|
400
419
|
{
|
|
401
420
|
type: "object",
|
|
402
421
|
description: "Custom documentation entry with arbitrary data.",
|
|
@@ -539,17 +558,17 @@ function validateRawRun(data) {
|
|
|
539
558
|
return { valid: true, errors: [] };
|
|
540
559
|
}
|
|
541
560
|
const errors = (validate.errors ?? []).map((err) => {
|
|
542
|
-
const
|
|
561
|
+
const path15 = err.instancePath || "/";
|
|
543
562
|
const message = err.message ?? "unknown error";
|
|
544
563
|
if (err.keyword === "additionalProperties") {
|
|
545
564
|
const extra = err.params.additionalProperty;
|
|
546
|
-
return `${
|
|
565
|
+
return `${path15}: ${message} \u2014 '${extra}'`;
|
|
547
566
|
}
|
|
548
567
|
if (err.keyword === "enum") {
|
|
549
568
|
const allowed = err.params.allowedValues;
|
|
550
|
-
return `${
|
|
569
|
+
return `${path15}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
|
|
551
570
|
}
|
|
552
|
-
return `${
|
|
571
|
+
return `${path15}: ${message}`;
|
|
553
572
|
});
|
|
554
573
|
return { valid: false, errors };
|
|
555
574
|
}
|
|
@@ -1024,7 +1043,7 @@ ${result.errors.join("\n")}`);
|
|
|
1024
1043
|
|
|
1025
1044
|
// src/index.ts
|
|
1026
1045
|
import "fs";
|
|
1027
|
-
import * as
|
|
1046
|
+
import * as path8 from "path";
|
|
1028
1047
|
import * as fsPromises from "fs/promises";
|
|
1029
1048
|
|
|
1030
1049
|
// src/converters/acl/lines.ts
|
|
@@ -1486,6 +1505,15 @@ function copyDocEntry(entry) {
|
|
|
1486
1505
|
phase: entry.phase,
|
|
1487
1506
|
...children
|
|
1488
1507
|
};
|
|
1508
|
+
case "video":
|
|
1509
|
+
return {
|
|
1510
|
+
kind: "video",
|
|
1511
|
+
path: entry.path,
|
|
1512
|
+
...entry.caption ? { caption: entry.caption } : {},
|
|
1513
|
+
...entry.poster ? { poster: entry.poster } : {},
|
|
1514
|
+
phase: entry.phase,
|
|
1515
|
+
...children
|
|
1516
|
+
};
|
|
1489
1517
|
case "custom":
|
|
1490
1518
|
return {
|
|
1491
1519
|
kind: "custom",
|
|
@@ -1569,6 +1597,9 @@ function buildScenario(tc, featureId) {
|
|
|
1569
1597
|
if (tickets && tickets.length > 0) {
|
|
1570
1598
|
scenario.tickets = tickets.map((t) => t.url ? { id: t.id, url: t.url } : { id: t.id });
|
|
1571
1599
|
}
|
|
1600
|
+
if (tc.story.covers && tc.story.covers.length > 0) {
|
|
1601
|
+
scenario.covers = [...tc.story.covers];
|
|
1602
|
+
}
|
|
1572
1603
|
return scenario;
|
|
1573
1604
|
}
|
|
1574
1605
|
function deriveFeatureTitle(group, relSourceFile) {
|
|
@@ -1692,6 +1723,181 @@ var StoryReportJsonFormatter = class {
|
|
|
1692
1723
|
}
|
|
1693
1724
|
};
|
|
1694
1725
|
|
|
1726
|
+
// src/formatters/scenario-index-json.ts
|
|
1727
|
+
var ScenarioIndexJsonFormatter = class {
|
|
1728
|
+
options;
|
|
1729
|
+
constructor(options = {}) {
|
|
1730
|
+
this.options = {
|
|
1731
|
+
pretty: options.pretty ?? true,
|
|
1732
|
+
filters: options.filters
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
toIndex(run) {
|
|
1736
|
+
return toScenarioIndex(toStoryReport(run), this.options.filters);
|
|
1737
|
+
}
|
|
1738
|
+
format(run) {
|
|
1739
|
+
const index = this.toIndex(run);
|
|
1740
|
+
return this.options.pretty ? JSON.stringify(index, null, 2) : JSON.stringify(index);
|
|
1741
|
+
}
|
|
1742
|
+
};
|
|
1743
|
+
function toScenarioIndex(report, filters = {}) {
|
|
1744
|
+
const scenarios = report.features.flatMap(
|
|
1745
|
+
(feature) => feature.scenarios.map((scenario) => toScenarioIndexItem(feature, scenario))
|
|
1746
|
+
).filter((scenario) => matchesFilters(scenario, filters));
|
|
1747
|
+
return {
|
|
1748
|
+
schemaVersion: "1.0",
|
|
1749
|
+
runId: report.runId,
|
|
1750
|
+
generatedAtMs: report.finishedAtMs,
|
|
1751
|
+
summary: summarize(scenarios),
|
|
1752
|
+
scenarios
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
function toScenarioIndexItem(feature, scenario) {
|
|
1756
|
+
return {
|
|
1757
|
+
id: scenario.id,
|
|
1758
|
+
title: scenario.title,
|
|
1759
|
+
status: scenario.status,
|
|
1760
|
+
feature: feature.title,
|
|
1761
|
+
sourceFile: feature.sourceFile,
|
|
1762
|
+
sourceLine: scenario.sourceLine,
|
|
1763
|
+
tags: scenario.tags,
|
|
1764
|
+
tickets: scenario.tickets ?? [],
|
|
1765
|
+
covers: scenario.covers ?? [],
|
|
1766
|
+
durationMs: scenario.durationMs,
|
|
1767
|
+
steps: scenario.steps.map((step) => ({
|
|
1768
|
+
id: step.id,
|
|
1769
|
+
index: step.index,
|
|
1770
|
+
keyword: step.keyword,
|
|
1771
|
+
text: step.text,
|
|
1772
|
+
status: step.status,
|
|
1773
|
+
durationMs: step.durationMs,
|
|
1774
|
+
errorMessage: step.errorMessage,
|
|
1775
|
+
docKinds: step.docEntries.map((entry) => entry.kind)
|
|
1776
|
+
})),
|
|
1777
|
+
docKinds: scenario.docEntries.map((entry) => entry.kind),
|
|
1778
|
+
error: scenario.errorMessage ? { message: scenario.errorMessage, stack: scenario.errorStack } : void 0
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
function matchesFilters(scenario, filters) {
|
|
1782
|
+
if (filters.statuses?.length && !filters.statuses.includes(scenario.status)) {
|
|
1783
|
+
return false;
|
|
1784
|
+
}
|
|
1785
|
+
if (filters.tags?.length && !filters.tags.some((tag) => scenario.tags.includes(tag))) {
|
|
1786
|
+
return false;
|
|
1787
|
+
}
|
|
1788
|
+
if (filters.sourceFiles?.length && !filters.sourceFiles.some((sourceFile) => scenario.sourceFile.includes(sourceFile))) {
|
|
1789
|
+
return false;
|
|
1790
|
+
}
|
|
1791
|
+
return true;
|
|
1792
|
+
}
|
|
1793
|
+
function summarize(scenarios) {
|
|
1794
|
+
return {
|
|
1795
|
+
total: scenarios.length,
|
|
1796
|
+
passed: scenarios.filter((scenario) => scenario.status === "passed").length,
|
|
1797
|
+
failed: scenarios.filter((scenario) => scenario.status === "failed").length,
|
|
1798
|
+
skipped: scenarios.filter((scenario) => scenario.status === "skipped").length,
|
|
1799
|
+
pending: scenarios.filter((scenario) => scenario.status === "pending").length,
|
|
1800
|
+
durationMs: scenarios.reduce((total, scenario) => total + scenario.durationMs, 0)
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
// src/formatters/behavior-manifest-json.ts
|
|
1805
|
+
var BehaviorManifestJsonFormatter = class {
|
|
1806
|
+
pretty;
|
|
1807
|
+
constructor(options = {}) {
|
|
1808
|
+
this.pretty = options.pretty ?? true;
|
|
1809
|
+
}
|
|
1810
|
+
toManifest(run) {
|
|
1811
|
+
return toBehaviorManifest(toStoryReport(run));
|
|
1812
|
+
}
|
|
1813
|
+
format(run) {
|
|
1814
|
+
const manifest = this.toManifest(run);
|
|
1815
|
+
return this.pretty ? JSON.stringify(manifest, null, 2) : JSON.stringify(manifest);
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
function toBehaviorManifest(report) {
|
|
1819
|
+
const index = toScenarioIndex(report);
|
|
1820
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
1821
|
+
const byTag = /* @__PURE__ */ new Map();
|
|
1822
|
+
const docKinds = /* @__PURE__ */ new Set();
|
|
1823
|
+
const debuggerIssues = [];
|
|
1824
|
+
for (const scenario of index.scenarios) {
|
|
1825
|
+
const source = bySource.get(scenario.sourceFile) ?? {
|
|
1826
|
+
path: scenario.sourceFile,
|
|
1827
|
+
scenarioCount: 0,
|
|
1828
|
+
failed: 0,
|
|
1829
|
+
tags: []
|
|
1830
|
+
};
|
|
1831
|
+
source.scenarioCount += 1;
|
|
1832
|
+
if (scenario.status === "failed") source.failed += 1;
|
|
1833
|
+
source.tags = [.../* @__PURE__ */ new Set([...source.tags, ...scenario.tags])].sort();
|
|
1834
|
+
bySource.set(scenario.sourceFile, source);
|
|
1835
|
+
for (const tag of scenario.tags) {
|
|
1836
|
+
const tagEntry = byTag.get(tag) ?? { name: tag, scenarioCount: 0 };
|
|
1837
|
+
tagEntry.scenarioCount += 1;
|
|
1838
|
+
byTag.set(tag, tagEntry);
|
|
1839
|
+
}
|
|
1840
|
+
for (const kind of scenario.docKinds) docKinds.add(kind);
|
|
1841
|
+
for (const step of scenario.steps) {
|
|
1842
|
+
for (const kind of step.docKinds) docKinds.add(kind);
|
|
1843
|
+
}
|
|
1844
|
+
if (!scenarioHasDocs(scenario)) {
|
|
1845
|
+
debuggerIssues.push({
|
|
1846
|
+
severity: "warning",
|
|
1847
|
+
code: "missing-docs",
|
|
1848
|
+
scenarioId: scenario.id,
|
|
1849
|
+
title: scenario.title,
|
|
1850
|
+
message: "Scenario has no doc entries."
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
if (scenario.tags.length === 0) {
|
|
1854
|
+
debuggerIssues.push({
|
|
1855
|
+
severity: "warning",
|
|
1856
|
+
code: "missing-tags",
|
|
1857
|
+
scenarioId: scenario.id,
|
|
1858
|
+
title: scenario.title,
|
|
1859
|
+
message: "Scenario has no tags."
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
if (scenario.covers.length === 0) {
|
|
1863
|
+
debuggerIssues.push({
|
|
1864
|
+
severity: "warning",
|
|
1865
|
+
code: "missing-covers",
|
|
1866
|
+
scenarioId: scenario.id,
|
|
1867
|
+
title: scenario.title,
|
|
1868
|
+
message: "Scenario declares no covers (product-code paths), so code\u2192scenario lookup cannot find it."
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
if (scenario.sourceLine === void 0) {
|
|
1872
|
+
debuggerIssues.push({
|
|
1873
|
+
severity: "warning",
|
|
1874
|
+
code: "missing-source-line",
|
|
1875
|
+
scenarioId: scenario.id,
|
|
1876
|
+
title: scenario.title,
|
|
1877
|
+
message: "Scenario has no source line."
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
const scenariosWithDocs = index.scenarios.filter(scenarioHasDocs).length;
|
|
1882
|
+
return {
|
|
1883
|
+
schemaVersion: "1.0",
|
|
1884
|
+
runId: report.runId,
|
|
1885
|
+
generatedAtMs: report.finishedAtMs,
|
|
1886
|
+
summary: index.summary,
|
|
1887
|
+
sourceFiles: [...bySource.values()].sort((a, b) => a.path.localeCompare(b.path)),
|
|
1888
|
+
tags: [...byTag.values()].sort((a, b) => a.name.localeCompare(b.name)),
|
|
1889
|
+
docCoverage: {
|
|
1890
|
+
scenariosWithDocs,
|
|
1891
|
+
scenariosWithoutDocs: index.scenarios.length - scenariosWithDocs,
|
|
1892
|
+
docKinds: [...docKinds].sort()
|
|
1893
|
+
},
|
|
1894
|
+
debugger: debuggerIssues
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
function scenarioHasDocs(scenario) {
|
|
1898
|
+
return scenario.docKinds.length > 0 || scenario.steps.some((step) => step.docKinds.length > 0);
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1695
1901
|
// src/formatters/html/renderers/index.ts
|
|
1696
1902
|
import * as fs2 from "fs";
|
|
1697
1903
|
import * as path3 from "path";
|
|
@@ -14422,10 +14628,10 @@ function renderDocMermaid(entry, deps) {
|
|
|
14422
14628
|
function renderDocScreenshot(entry, deps) {
|
|
14423
14629
|
const alt = entry.alt ?? "Screenshot";
|
|
14424
14630
|
const embedEnabled = deps.embedScreenshots ?? true;
|
|
14425
|
-
const
|
|
14426
|
-
const embedAttempted = !
|
|
14631
|
+
const isRemote2 = /^(?:https?:|data:)/i.test(entry.path);
|
|
14632
|
+
const embedAttempted = !isRemote2 && embedEnabled && !!deps.readScreenshot;
|
|
14427
14633
|
const inlined = embedAttempted ? deps.readScreenshot(entry.path) : void 0;
|
|
14428
|
-
const isAbsoluteFsPath = !
|
|
14634
|
+
const isAbsoluteFsPath = !isRemote2 && /^(?:[/\\]|[A-Za-z]:[/\\])/.test(entry.path);
|
|
14429
14635
|
if (embedAttempted && inlined === void 0 && isAbsoluteFsPath) {
|
|
14430
14636
|
const captionHtml = entry.alt ? `<div class="doc-screenshot-caption">${deps.escapeHtml(entry.alt)}</div>` : "";
|
|
14431
14637
|
return `<div class="doc-screenshot doc-screenshot-missing">
|
|
@@ -14440,6 +14646,23 @@ function renderDocScreenshot(entry, deps) {
|
|
|
14440
14646
|
${entry.alt ? `<div class="doc-screenshot-caption">${deps.escapeHtml(entry.alt)}</div>` : ""}
|
|
14441
14647
|
</div>`;
|
|
14442
14648
|
}
|
|
14649
|
+
function renderDocVideo(entry, deps) {
|
|
14650
|
+
const isRemote2 = /^(?:https?:|data:)/i.test(entry.path);
|
|
14651
|
+
const isAbsoluteFsPath = !isRemote2 && /^(?:[/\\]|[A-Za-z]:[/\\])/.test(entry.path);
|
|
14652
|
+
const captionHtml = entry.caption ? `<div class="doc-video-caption">${deps.escapeHtml(entry.caption)}</div>` : "";
|
|
14653
|
+
if ((deps.embedScreenshots ?? true) && isAbsoluteFsPath) {
|
|
14654
|
+
return `<div class="doc-video doc-video-missing">
|
|
14655
|
+
<div class="doc-video-missing-label">Video unavailable</div>
|
|
14656
|
+
<div class="doc-video-missing-path">${deps.escapeHtml(entry.path)}</div>
|
|
14657
|
+
${captionHtml}
|
|
14658
|
+
</div>`;
|
|
14659
|
+
}
|
|
14660
|
+
const poster = entry.poster ? ` poster="${deps.escapeHtml(entry.poster)}"` : "";
|
|
14661
|
+
return `<div class="doc-video">
|
|
14662
|
+
<video class="doc-video-player" controls preload="metadata"${poster} src="${deps.escapeHtml(entry.path)}"></video>
|
|
14663
|
+
${captionHtml}
|
|
14664
|
+
</div>`;
|
|
14665
|
+
}
|
|
14443
14666
|
function renderDocCustom(entry, deps) {
|
|
14444
14667
|
if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
|
|
14445
14668
|
const data = entry.data;
|
|
@@ -14493,6 +14716,9 @@ function renderDocEntry(entry, deps) {
|
|
|
14493
14716
|
case "screenshot":
|
|
14494
14717
|
html = renderDocScreenshot(entry, deps);
|
|
14495
14718
|
break;
|
|
14719
|
+
case "video":
|
|
14720
|
+
html = renderDocVideo(entry, deps);
|
|
14721
|
+
break;
|
|
14496
14722
|
case "custom":
|
|
14497
14723
|
html = renderDocCustom(entry, deps);
|
|
14498
14724
|
break;
|
|
@@ -15814,6 +16040,19 @@ var MarkdownFormatter = class {
|
|
|
15814
16040
|
case "screenshot":
|
|
15815
16041
|
lines.push(`${indent}`);
|
|
15816
16042
|
break;
|
|
16043
|
+
case "video": {
|
|
16044
|
+
const poster = entry.poster ? ` poster="${entry.poster}"` : "";
|
|
16045
|
+
lines.push(`${indent}`);
|
|
16046
|
+
lines.push(`${indent}<video controls preload="metadata"${poster} class="doc-video">`);
|
|
16047
|
+
lines.push(`${indent} <source src="${entry.path}" />`);
|
|
16048
|
+
lines.push(`${indent}</video>`);
|
|
16049
|
+
if (entry.caption) {
|
|
16050
|
+
lines.push(`${indent}`);
|
|
16051
|
+
lines.push(`${indent}*${entry.caption}*`);
|
|
16052
|
+
}
|
|
16053
|
+
lines.push(`${indent}`);
|
|
16054
|
+
break;
|
|
16055
|
+
}
|
|
15817
16056
|
case "custom":
|
|
15818
16057
|
if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
|
|
15819
16058
|
const data = entry.data;
|
|
@@ -15913,8 +16152,8 @@ function extractFeatureName(testCases, uri) {
|
|
|
15913
16152
|
return tc.titlePath[0];
|
|
15914
16153
|
}
|
|
15915
16154
|
}
|
|
15916
|
-
const
|
|
15917
|
-
return
|
|
16155
|
+
const basename4 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
|
|
16156
|
+
return basename4.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
15918
16157
|
}
|
|
15919
16158
|
function synthesizeFeature(uri, testCases) {
|
|
15920
16159
|
const featureName = extractFeatureName(testCases, uri);
|
|
@@ -16526,8 +16765,8 @@ function extractDocAttachments(step) {
|
|
|
16526
16765
|
}
|
|
16527
16766
|
return attachments;
|
|
16528
16767
|
}
|
|
16529
|
-
function guessMediaType(
|
|
16530
|
-
const lower =
|
|
16768
|
+
function guessMediaType(path15) {
|
|
16769
|
+
const lower = path15.toLowerCase();
|
|
16531
16770
|
if (lower.endsWith(".png")) return "image/png";
|
|
16532
16771
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
16533
16772
|
if (lower.endsWith(".gif")) return "image/gif";
|
|
@@ -16668,11 +16907,11 @@ var CucumberHtmlFormatter = class {
|
|
|
16668
16907
|
for (const envelope of envelopes) {
|
|
16669
16908
|
const accepted = htmlStream.write(envelope);
|
|
16670
16909
|
if (!accepted) {
|
|
16671
|
-
await new Promise((
|
|
16910
|
+
await new Promise((resolve11) => htmlStream.once("drain", resolve11));
|
|
16672
16911
|
}
|
|
16673
16912
|
}
|
|
16674
|
-
await new Promise((
|
|
16675
|
-
collector.on("finish",
|
|
16913
|
+
await new Promise((resolve11, reject) => {
|
|
16914
|
+
collector.on("finish", resolve11);
|
|
16676
16915
|
collector.on("error", reject);
|
|
16677
16916
|
htmlStream.end();
|
|
16678
16917
|
});
|
|
@@ -17003,6 +17242,8 @@ function formatDocEntry(doc) {
|
|
|
17003
17242
|
return `${escapeHtml2(doc.title ?? "mermaid diagram")}: <code>${escapeHtml2(doc.code)}</code>`;
|
|
17004
17243
|
case "screenshot":
|
|
17005
17244
|
return `${doc.alt ? `${escapeHtml2(doc.alt)}: ` : ""}${escapeHtml2(doc.path)}`;
|
|
17245
|
+
case "video":
|
|
17246
|
+
return `${doc.caption ? `${escapeHtml2(doc.caption)}: ` : ""}${escapeHtml2(doc.path)}`;
|
|
17006
17247
|
case "custom":
|
|
17007
17248
|
return `${escapeHtml2(doc.type)}: ${escapeHtml2(JSON.stringify(doc.data))}`;
|
|
17008
17249
|
}
|
|
@@ -17459,6 +17700,8 @@ function formatDocEntry2(doc) {
|
|
|
17459
17700
|
return `${doc.title ?? "mermaid diagram"}: \`${doc.code}\``;
|
|
17460
17701
|
case "screenshot":
|
|
17461
17702
|
return `${doc.alt ? `${doc.alt}: ` : ""}${doc.path}`;
|
|
17703
|
+
case "video":
|
|
17704
|
+
return `${doc.caption ? `${doc.caption}: ` : ""}${doc.path}`;
|
|
17462
17705
|
case "custom":
|
|
17463
17706
|
return `${doc.type}: ${JSON.stringify(doc.data)}`;
|
|
17464
17707
|
}
|
|
@@ -17704,19 +17947,35 @@ function replaceAssetRef(html, original, replacement) {
|
|
|
17704
17947
|
return html;
|
|
17705
17948
|
}
|
|
17706
17949
|
|
|
17950
|
+
// src/utils/source-file.ts
|
|
17951
|
+
function cleanTestStem(fileName) {
|
|
17952
|
+
const base = fileName.split(/[\\/]/).pop() ?? fileName;
|
|
17953
|
+
const stripped = base.replace(/\.(story\.)?(test|spec|cy)\.[cm]?[jt]sx?$/i, "");
|
|
17954
|
+
if (stripped !== base) return stripped;
|
|
17955
|
+
return base.replace(/\.[^.]+$/, "");
|
|
17956
|
+
}
|
|
17957
|
+
function humanizeSourceFile(fileName) {
|
|
17958
|
+
return cleanTestStem(fileName).split(/[-_.\s]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
17959
|
+
}
|
|
17960
|
+
|
|
17707
17961
|
// src/formatters/astro.ts
|
|
17708
17962
|
var AstroFormatter = class _AstroFormatter {
|
|
17709
17963
|
markdownFormatter;
|
|
17710
17964
|
title;
|
|
17965
|
+
perFileTitle;
|
|
17711
17966
|
constructor(options = {}) {
|
|
17712
17967
|
this.title = options.markdown?.title ?? "User Stories";
|
|
17968
|
+
this.perFileTitle = options.perFileTitle ?? false;
|
|
17713
17969
|
this.markdownFormatter = new MarkdownFormatter({
|
|
17714
17970
|
...options.markdown,
|
|
17715
17971
|
title: this.title,
|
|
17716
17972
|
stepStyle: "gherkin",
|
|
17717
17973
|
includeFrontMatter: false,
|
|
17718
17974
|
includeSummaryTable: false,
|
|
17719
|
-
includeMetadata: false
|
|
17975
|
+
includeMetadata: false,
|
|
17976
|
+
// A per-file page is one file already — group by suite/describe so the
|
|
17977
|
+
// body shows clean section headings, not the redundant source path.
|
|
17978
|
+
groupBy: this.perFileTitle ? "suite" : options.markdown?.groupBy ?? "file"
|
|
17720
17979
|
});
|
|
17721
17980
|
}
|
|
17722
17981
|
format(run) {
|
|
@@ -17726,13 +17985,31 @@ var AstroFormatter = class _AstroFormatter {
|
|
|
17726
17985
|
return `${frontmatter}
|
|
17727
17986
|
${body}`;
|
|
17728
17987
|
}
|
|
17988
|
+
/**
|
|
17989
|
+
* Title for the page. A per-file page (one source file — i.e. colocated mode)
|
|
17990
|
+
* is titled by its suite/describe name, falling back to a humanized filename,
|
|
17991
|
+
* so the docs nav reads "Convert Currency" not "User Stories" six times over.
|
|
17992
|
+
* Multi-file (aggregated) pages keep the configured title.
|
|
17993
|
+
*/
|
|
17994
|
+
deriveTitle(run) {
|
|
17995
|
+
if (!this.perFileTitle) return this.title;
|
|
17996
|
+
const sourceFiles = new Set(
|
|
17997
|
+
run.testCases.map((tc) => tc.sourceFile).filter((f) => f && f !== "unknown")
|
|
17998
|
+
);
|
|
17999
|
+
if (sourceFiles.size !== 1) return this.title;
|
|
18000
|
+
const suites = new Set(
|
|
18001
|
+
run.testCases.map((tc) => tc.titlePath?.[0]).filter((s) => Boolean(s))
|
|
18002
|
+
);
|
|
18003
|
+
if (suites.size === 1) return [...suites][0];
|
|
18004
|
+
return humanizeSourceFile([...sourceFiles][0]) || this.title;
|
|
18005
|
+
}
|
|
17729
18006
|
buildFrontmatter(run) {
|
|
17730
18007
|
const badge = _AstroFormatter.computeBadge(run.testCases);
|
|
17731
18008
|
const count = run.testCases.length;
|
|
17732
18009
|
const description = `${count} scenario${count !== 1 ? "s" : ""} \u2014 ${badge.text.toLowerCase()}`;
|
|
17733
18010
|
const lines = [
|
|
17734
18011
|
"---",
|
|
17735
|
-
`title: ${this.
|
|
18012
|
+
`title: ${yamlScalar(this.deriveTitle(run))}`,
|
|
17736
18013
|
`description: ${description}`,
|
|
17737
18014
|
"sidebar:",
|
|
17738
18015
|
" badge:",
|
|
@@ -17750,6 +18027,12 @@ ${body}`;
|
|
|
17750
18027
|
return { text: "Passed", variant: "success" };
|
|
17751
18028
|
}
|
|
17752
18029
|
};
|
|
18030
|
+
function yamlScalar(value) {
|
|
18031
|
+
if (/[:#[\]{}&*!|>'"%@`]|^[\s-]|\s$/.test(value)) {
|
|
18032
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
18033
|
+
}
|
|
18034
|
+
return value;
|
|
18035
|
+
}
|
|
17753
18036
|
|
|
17754
18037
|
// src/formatters/confluence.ts
|
|
17755
18038
|
var ConfluenceFormatter = class {
|
|
@@ -18026,6 +18309,15 @@ ${tc.errorStack}` : "");
|
|
|
18026
18309
|
])
|
|
18027
18310
|
);
|
|
18028
18311
|
break;
|
|
18312
|
+
case "video":
|
|
18313
|
+
content.push(
|
|
18314
|
+
paragraph([
|
|
18315
|
+
text(entry.caption ?? "Video", strong()),
|
|
18316
|
+
text(": "),
|
|
18317
|
+
link(entry.path, entry.path)
|
|
18318
|
+
])
|
|
18319
|
+
);
|
|
18320
|
+
break;
|
|
18029
18321
|
case "custom":
|
|
18030
18322
|
content.push(paragraph([text(`[${entry.type}]`, strong())]));
|
|
18031
18323
|
content.push(codeBlock(JSON.stringify(entry.data ?? null, null, 2), "json"));
|
|
@@ -18195,6 +18487,13 @@ function scanMarkdownAssets(markdown) {
|
|
|
18195
18487
|
found.add(src);
|
|
18196
18488
|
}
|
|
18197
18489
|
}
|
|
18490
|
+
const posterRe = /<video[^>]+\bposter=["']([^"']+)["'][^>]*>/gi;
|
|
18491
|
+
while ((match = posterRe.exec(stripped)) !== null) {
|
|
18492
|
+
const src = match[1].trim();
|
|
18493
|
+
if (isLocalPath(src)) {
|
|
18494
|
+
found.add(src);
|
|
18495
|
+
}
|
|
18496
|
+
}
|
|
18198
18497
|
return Array.from(found);
|
|
18199
18498
|
}
|
|
18200
18499
|
function splitByCode(markdown) {
|
|
@@ -18245,6 +18544,19 @@ function rewriteProseSegment(prose, assetsBaseUrl, pathMap) {
|
|
|
18245
18544
|
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
18246
18545
|
}
|
|
18247
18546
|
);
|
|
18547
|
+
result = result.replace(
|
|
18548
|
+
/(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi,
|
|
18549
|
+
(full, pre, src, post) => {
|
|
18550
|
+
const trimmed = src.trim();
|
|
18551
|
+
if (!isLocalPath(trimmed)) return full;
|
|
18552
|
+
if (pathMap) {
|
|
18553
|
+
const mapped = pathMap.get(trimmed);
|
|
18554
|
+
if (mapped === void 0) return full;
|
|
18555
|
+
return `${pre}${assetsBaseUrl}/${mapped}${post}`;
|
|
18556
|
+
}
|
|
18557
|
+
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
18558
|
+
}
|
|
18559
|
+
);
|
|
18248
18560
|
return result;
|
|
18249
18561
|
}
|
|
18250
18562
|
function rewriteAssetPaths(markdown, assetsBaseUrl, pathMap) {
|
|
@@ -18283,6 +18595,68 @@ function copyMarkdownAssets(options) {
|
|
|
18283
18595
|
};
|
|
18284
18596
|
}
|
|
18285
18597
|
|
|
18598
|
+
// src/watch.ts
|
|
18599
|
+
import * as fs6 from "fs";
|
|
18600
|
+
import * as path7 from "path";
|
|
18601
|
+
function toRun(data, inputType, synthesize) {
|
|
18602
|
+
if (inputType === "canonical") return data;
|
|
18603
|
+
let raw = data;
|
|
18604
|
+
if (synthesize) raw = synthesizeStories(raw);
|
|
18605
|
+
return canonicalizeRun(raw);
|
|
18606
|
+
}
|
|
18607
|
+
async function regenerateArtifacts(options, deps = {}) {
|
|
18608
|
+
const read = deps.readFile ?? ((filePath) => fs6.readFileSync(filePath, "utf8"));
|
|
18609
|
+
const data = JSON.parse(read(path7.resolve(options.input)));
|
|
18610
|
+
const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
|
|
18611
|
+
const generator = new ReportGenerator({
|
|
18612
|
+
formats: options.formats,
|
|
18613
|
+
outputDir: options.outputDir,
|
|
18614
|
+
outputName: options.outputName
|
|
18615
|
+
});
|
|
18616
|
+
const result = await generator.generate(run);
|
|
18617
|
+
return [...result.values()].flat();
|
|
18618
|
+
}
|
|
18619
|
+
function startWatch(options, deps = {}) {
|
|
18620
|
+
const log = deps.log ?? ((message) => console.log(message));
|
|
18621
|
+
const regenerate = deps.regenerate ?? ((input) => regenerateArtifacts({ ...options, input }, deps));
|
|
18622
|
+
const watchFn = deps.watch ?? ((filePath, listener) => fs6.watch(filePath, listener));
|
|
18623
|
+
const debounceMs = options.debounceMs ?? 150;
|
|
18624
|
+
let timer;
|
|
18625
|
+
let running = false;
|
|
18626
|
+
let pending = false;
|
|
18627
|
+
const run = async () => {
|
|
18628
|
+
if (running) {
|
|
18629
|
+
pending = true;
|
|
18630
|
+
return;
|
|
18631
|
+
}
|
|
18632
|
+
running = true;
|
|
18633
|
+
try {
|
|
18634
|
+
const files = await regenerate(options.input);
|
|
18635
|
+
log(`Regenerated ${files.length} artifact file(s) from ${options.input}`);
|
|
18636
|
+
} catch (error) {
|
|
18637
|
+
log(`Watch regeneration failed: ${error.message}`);
|
|
18638
|
+
} finally {
|
|
18639
|
+
running = false;
|
|
18640
|
+
if (pending) {
|
|
18641
|
+
pending = false;
|
|
18642
|
+
trigger();
|
|
18643
|
+
}
|
|
18644
|
+
}
|
|
18645
|
+
};
|
|
18646
|
+
const trigger = () => {
|
|
18647
|
+
if (timer) clearTimeout(timer);
|
|
18648
|
+
timer = setTimeout(() => void run(), debounceMs);
|
|
18649
|
+
};
|
|
18650
|
+
trigger();
|
|
18651
|
+
const watcher = watchFn(path7.resolve(options.input), trigger);
|
|
18652
|
+
return {
|
|
18653
|
+
close: () => {
|
|
18654
|
+
if (timer) clearTimeout(timer);
|
|
18655
|
+
watcher.close();
|
|
18656
|
+
}
|
|
18657
|
+
};
|
|
18658
|
+
}
|
|
18659
|
+
|
|
18286
18660
|
// src/publishers/confluence.ts
|
|
18287
18661
|
function parseAdf(adf) {
|
|
18288
18662
|
let parsed;
|
|
@@ -18980,10 +19354,10 @@ _...and ${summary.failedTests.length - maxFailedTests} more_`;
|
|
|
18980
19354
|
}
|
|
18981
19355
|
async function sendSlackNotification(args, deps) {
|
|
18982
19356
|
const { summary, webhookUrl, maxFailedTests = 5 } = args;
|
|
18983
|
-
const { fetch, logger } = deps;
|
|
19357
|
+
const { fetch: fetch2, logger } = deps;
|
|
18984
19358
|
const payload = buildSlackPayload(summary, maxFailedTests);
|
|
18985
19359
|
try {
|
|
18986
|
-
const response = await
|
|
19360
|
+
const response = await fetch2(webhookUrl, {
|
|
18987
19361
|
method: "POST",
|
|
18988
19362
|
headers: { "Content-Type": "application/json" },
|
|
18989
19363
|
body: JSON.stringify(payload)
|
|
@@ -19134,10 +19508,10 @@ function buildTeamsPayload(summary, maxFailedTests) {
|
|
|
19134
19508
|
}
|
|
19135
19509
|
async function sendTeamsNotification(args, deps) {
|
|
19136
19510
|
const { summary, webhookUrl, maxFailedTests = 5 } = args;
|
|
19137
|
-
const { fetch, logger } = deps;
|
|
19511
|
+
const { fetch: fetch2, logger } = deps;
|
|
19138
19512
|
const payload = buildTeamsPayload(summary, maxFailedTests);
|
|
19139
19513
|
try {
|
|
19140
|
-
const response = await
|
|
19514
|
+
const response = await fetch2(webhookUrl, {
|
|
19141
19515
|
method: "POST",
|
|
19142
19516
|
headers: { "Content-Type": "application/json" },
|
|
19143
19517
|
body: JSON.stringify(payload)
|
|
@@ -19185,7 +19559,7 @@ function signBody(args) {
|
|
|
19185
19559
|
// src/notifiers/webhook.ts
|
|
19186
19560
|
async function sendWebhookNotification(args, deps) {
|
|
19187
19561
|
const { summary, options } = args;
|
|
19188
|
-
const { fetch, logger } = deps;
|
|
19562
|
+
const { fetch: fetch2, logger } = deps;
|
|
19189
19563
|
const payload = {
|
|
19190
19564
|
schemaVersion: 1,
|
|
19191
19565
|
event: "test_run_finished",
|
|
@@ -19207,7 +19581,7 @@ async function sendWebhookNotification(args, deps) {
|
|
|
19207
19581
|
}
|
|
19208
19582
|
}
|
|
19209
19583
|
try {
|
|
19210
|
-
const response = await
|
|
19584
|
+
const response = await fetch2(options.url, {
|
|
19211
19585
|
method: options.method ?? "POST",
|
|
19212
19586
|
headers,
|
|
19213
19587
|
body
|
|
@@ -19286,7 +19660,7 @@ async function sendNotifications(args, deps) {
|
|
|
19286
19660
|
logger.warn("notifications: skipped (fetch unavailable)");
|
|
19287
19661
|
return;
|
|
19288
19662
|
}
|
|
19289
|
-
const
|
|
19663
|
+
const fetch2 = deps.fetch;
|
|
19290
19664
|
const slackWebhookUrl = notification?.slackWebhookUrl ?? env.SLACK_WEBHOOK_URL;
|
|
19291
19665
|
const teamsWebhookUrl = notification?.teamsWebhookUrl ?? env.TEAMS_WEBHOOK_URL;
|
|
19292
19666
|
const globalCondition = notification?.condition ?? "on-failure";
|
|
@@ -19302,7 +19676,7 @@ async function sendNotifications(args, deps) {
|
|
|
19302
19676
|
promises.push(
|
|
19303
19677
|
sendSlackNotification(
|
|
19304
19678
|
{ summary, webhookUrl: slackWebhookUrl, maxFailedTests },
|
|
19305
|
-
{ fetch, logger }
|
|
19679
|
+
{ fetch: fetch2, logger }
|
|
19306
19680
|
).then(() => void 0)
|
|
19307
19681
|
);
|
|
19308
19682
|
}
|
|
@@ -19310,7 +19684,7 @@ async function sendNotifications(args, deps) {
|
|
|
19310
19684
|
promises.push(
|
|
19311
19685
|
sendTeamsNotification(
|
|
19312
19686
|
{ summary, webhookUrl: teamsWebhookUrl, maxFailedTests },
|
|
19313
|
-
{ fetch, logger }
|
|
19687
|
+
{ fetch: fetch2, logger }
|
|
19314
19688
|
).then(() => void 0)
|
|
19315
19689
|
);
|
|
19316
19690
|
}
|
|
@@ -19320,7 +19694,7 @@ async function sendNotifications(args, deps) {
|
|
|
19320
19694
|
promises.push(
|
|
19321
19695
|
sendWebhookNotification(
|
|
19322
19696
|
{ summary, options: webhook, maxFailedTests },
|
|
19323
|
-
{ fetch, logger }
|
|
19697
|
+
{ fetch: fetch2, logger }
|
|
19324
19698
|
).then(() => void 0)
|
|
19325
19699
|
);
|
|
19326
19700
|
}
|
|
@@ -19451,12 +19825,22 @@ function listScenarios(args, _deps) {
|
|
|
19451
19825
|
const { testCases, format } = args;
|
|
19452
19826
|
if (format === "json") {
|
|
19453
19827
|
const items = testCases.map((tc) => ({
|
|
19828
|
+
id: tc.id,
|
|
19454
19829
|
scenario: tc.story.scenario,
|
|
19455
19830
|
status: tc.status,
|
|
19456
19831
|
sourceFile: tc.sourceFile,
|
|
19457
19832
|
sourceLine: tc.sourceLine,
|
|
19833
|
+
suitePath: tc.story.suitePath ?? tc.titlePath.slice(0, -1),
|
|
19458
19834
|
tags: tc.tags,
|
|
19459
|
-
|
|
19835
|
+
tickets: tc.story.tickets ?? [],
|
|
19836
|
+
covers: tc.story.covers ?? [],
|
|
19837
|
+
durationMs: tc.durationMs,
|
|
19838
|
+
error: tc.errorMessage ? {
|
|
19839
|
+
message: tc.errorMessage,
|
|
19840
|
+
stack: tc.errorStack
|
|
19841
|
+
} : void 0,
|
|
19842
|
+
steps: tc.story.steps.map((step, index) => toScenarioStep(step, index, tc)),
|
|
19843
|
+
docKinds: collectDocKinds(tc)
|
|
19460
19844
|
}));
|
|
19461
19845
|
return JSON.stringify(items, null, 2);
|
|
19462
19846
|
}
|
|
@@ -19529,6 +19913,34 @@ function listScenarios(args, _deps) {
|
|
|
19529
19913
|
];
|
|
19530
19914
|
return lines.join("\n");
|
|
19531
19915
|
}
|
|
19916
|
+
function toScenarioStep(step, index, testCase) {
|
|
19917
|
+
const result = testCase.stepResults.find(
|
|
19918
|
+
(candidate) => candidate.index === index || candidate.stepId === step.id
|
|
19919
|
+
);
|
|
19920
|
+
return {
|
|
19921
|
+
id: step.id,
|
|
19922
|
+
index,
|
|
19923
|
+
keyword: step.keyword,
|
|
19924
|
+
text: step.text,
|
|
19925
|
+
status: result?.status ?? testCase.status,
|
|
19926
|
+
durationMs: result?.durationMs ?? step.durationMs ?? 0,
|
|
19927
|
+
errorMessage: result?.errorMessage,
|
|
19928
|
+
mode: step.mode,
|
|
19929
|
+
docKinds: (step.docs ?? []).map((doc) => doc.kind)
|
|
19930
|
+
};
|
|
19931
|
+
}
|
|
19932
|
+
function collectDocKinds(testCase) {
|
|
19933
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
19934
|
+
for (const doc of testCase.story.docs ?? []) {
|
|
19935
|
+
kinds.add(doc.kind);
|
|
19936
|
+
}
|
|
19937
|
+
for (const step of testCase.story.steps) {
|
|
19938
|
+
for (const doc of step.docs ?? []) {
|
|
19939
|
+
kinds.add(doc.kind);
|
|
19940
|
+
}
|
|
19941
|
+
}
|
|
19942
|
+
return [...kinds].sort();
|
|
19943
|
+
}
|
|
19532
19944
|
|
|
19533
19945
|
// src/review/conventions.ts
|
|
19534
19946
|
var CHANGE_TAG_PREFIX = "change:";
|
|
@@ -19576,18 +19988,18 @@ function deriveChangeType(tags) {
|
|
|
19576
19988
|
}
|
|
19577
19989
|
return "unknown";
|
|
19578
19990
|
}
|
|
19579
|
-
function extensionOf(
|
|
19580
|
-
const base =
|
|
19991
|
+
function extensionOf(path15) {
|
|
19992
|
+
const base = path15.split("/").pop() ?? path15;
|
|
19581
19993
|
const dot = base.lastIndexOf(".");
|
|
19582
19994
|
return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
|
|
19583
19995
|
}
|
|
19584
|
-
function isTestFile(
|
|
19585
|
-
return TEST_INFIX.test(
|
|
19996
|
+
function isTestFile(path15) {
|
|
19997
|
+
return TEST_INFIX.test(path15);
|
|
19586
19998
|
}
|
|
19587
|
-
function isReviewableSource(
|
|
19588
|
-
if (isTestFile(
|
|
19589
|
-
if (
|
|
19590
|
-
return CODE_EXTENSIONS.has(extensionOf(
|
|
19999
|
+
function isReviewableSource(path15) {
|
|
20000
|
+
if (isTestFile(path15)) return false;
|
|
20001
|
+
if (path15.endsWith(".d.ts")) return false;
|
|
20002
|
+
return CODE_EXTENSIONS.has(extensionOf(path15));
|
|
19591
20003
|
}
|
|
19592
20004
|
function testBaseKey(testFile) {
|
|
19593
20005
|
return testFile.replace(TEST_INFIX, "");
|
|
@@ -19691,7 +20103,7 @@ function toClaim(testCase, changedSourcePaths) {
|
|
|
19691
20103
|
const { strength, reasons } = gradeEvidence(testCase, audience);
|
|
19692
20104
|
const key = testBaseKey(testCase.sourceFile);
|
|
19693
20105
|
const coversFiles = changedSourcePaths.filter(
|
|
19694
|
-
(
|
|
20106
|
+
(path15) => sourceBaseKey(path15) === key
|
|
19695
20107
|
);
|
|
19696
20108
|
return {
|
|
19697
20109
|
id: testCase.id,
|
|
@@ -20224,6 +20636,7 @@ applyTheme(getEffectiveTheme());` : "";
|
|
|
20224
20636
|
// src/index.ts
|
|
20225
20637
|
var FORMAT_EXTENSIONS = {
|
|
20226
20638
|
astro: ".md",
|
|
20639
|
+
"behavior-manifest-json": ".behavior-manifest.json",
|
|
20227
20640
|
markdown: ".md",
|
|
20228
20641
|
html: ".html",
|
|
20229
20642
|
"cucumber-html": ".cucumber.html",
|
|
@@ -20231,8 +20644,13 @@ var FORMAT_EXTENSIONS = {
|
|
|
20231
20644
|
"cucumber-json": ".cucumber.json",
|
|
20232
20645
|
"cucumber-messages": ".ndjson",
|
|
20233
20646
|
confluence: ".adf.json",
|
|
20647
|
+
"scenario-index-json": ".scenarios-index.json",
|
|
20234
20648
|
"story-report-json": ".story-report.json"
|
|
20235
20649
|
};
|
|
20650
|
+
function joinNameAndExt(name, ext) {
|
|
20651
|
+
const stutter = `.${name}.`;
|
|
20652
|
+
return ext.startsWith(stutter) ? `${name}.${ext.slice(stutter.length)}` : `${name}${ext}`;
|
|
20653
|
+
}
|
|
20236
20654
|
var TEST_EXTENSIONS = [
|
|
20237
20655
|
".test.ts",
|
|
20238
20656
|
".test.tsx",
|
|
@@ -20258,11 +20676,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
20258
20676
|
const ext = FORMAT_EXTENSIONS[format];
|
|
20259
20677
|
const effectiveName = outputName + (outputNameSuffix ?? "");
|
|
20260
20678
|
if (mode === "aggregated") {
|
|
20261
|
-
return toPosix(
|
|
20679
|
+
return toPosix(path8.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
|
|
20262
20680
|
}
|
|
20263
20681
|
const normalizedSource = toPosix(sourceFile);
|
|
20264
|
-
const dirOfSource =
|
|
20265
|
-
let baseName =
|
|
20682
|
+
const dirOfSource = path8.posix.dirname(normalizedSource);
|
|
20683
|
+
let baseName = path8.posix.basename(normalizedSource);
|
|
20266
20684
|
for (const testExt of TEST_EXTENSIONS) {
|
|
20267
20685
|
if (baseName.endsWith(testExt)) {
|
|
20268
20686
|
baseName = baseName.slice(0, -testExt.length);
|
|
@@ -20271,9 +20689,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
20271
20689
|
}
|
|
20272
20690
|
const fileName = `${baseName}.${effectiveName}${ext}`;
|
|
20273
20691
|
if (colocatedStyle === "adjacent") {
|
|
20274
|
-
return toPosix(
|
|
20692
|
+
return toPosix(path8.posix.join(dirOfSource, fileName));
|
|
20275
20693
|
}
|
|
20276
|
-
|
|
20694
|
+
if (colocatedStyle === "flat") {
|
|
20695
|
+
return toPosix(path8.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
|
|
20696
|
+
}
|
|
20697
|
+
return toPosix(path8.posix.join(baseOutputDir, dirOfSource, fileName));
|
|
20277
20698
|
}
|
|
20278
20699
|
function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
|
|
20279
20700
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -20359,6 +20780,12 @@ var ReportGenerator = class {
|
|
|
20359
20780
|
storyReportJson: {
|
|
20360
20781
|
pretty: options.storyReportJson?.pretty ?? true
|
|
20361
20782
|
},
|
|
20783
|
+
scenarioIndexJson: {
|
|
20784
|
+
pretty: options.scenarioIndexJson?.pretty ?? true
|
|
20785
|
+
},
|
|
20786
|
+
behaviorManifestJson: {
|
|
20787
|
+
pretty: options.behaviorManifestJson?.pretty ?? true
|
|
20788
|
+
},
|
|
20362
20789
|
cucumberMessages: {
|
|
20363
20790
|
uriStrategy: options.cucumberMessages?.uriStrategy ?? "sourceFile",
|
|
20364
20791
|
includeSynthetics: options.cucumberMessages?.includeSynthetics ?? true,
|
|
@@ -20474,8 +20901,8 @@ var ReportGenerator = class {
|
|
|
20474
20901
|
if (astroPaths) {
|
|
20475
20902
|
for (const mdPath of astroPaths) {
|
|
20476
20903
|
const content = await fsPromises.readFile(mdPath, "utf8");
|
|
20477
|
-
const mdDir =
|
|
20478
|
-
const assetsDir =
|
|
20904
|
+
const mdDir = path8.dirname(mdPath);
|
|
20905
|
+
const assetsDir = path8.resolve(this.options.astro.assetsDir);
|
|
20479
20906
|
const result = copyMarkdownAssets({
|
|
20480
20907
|
markdown: content,
|
|
20481
20908
|
markdownDir: mdDir,
|
|
@@ -20506,9 +20933,9 @@ var ReportGenerator = class {
|
|
|
20506
20933
|
if (groups.size === 0 && this.options.output.mode === "aggregated") {
|
|
20507
20934
|
const ext = FORMAT_EXTENSIONS[format];
|
|
20508
20935
|
const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
|
|
20509
|
-
const outputPath = toPosix(
|
|
20936
|
+
const outputPath = toPosix(path8.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
|
|
20510
20937
|
const content = await this.formatContent(run, format);
|
|
20511
|
-
const dir =
|
|
20938
|
+
const dir = path8.dirname(outputPath);
|
|
20512
20939
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
20513
20940
|
await this.deps.writeFile(outputPath, content);
|
|
20514
20941
|
return [outputPath];
|
|
@@ -20520,7 +20947,7 @@ var ReportGenerator = class {
|
|
|
20520
20947
|
testCases
|
|
20521
20948
|
};
|
|
20522
20949
|
const content = await this.formatContent(groupRun, format);
|
|
20523
|
-
const dir =
|
|
20950
|
+
const dir = path8.dirname(outputPath);
|
|
20524
20951
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
20525
20952
|
await this.deps.writeFile(outputPath, content);
|
|
20526
20953
|
writtenPaths.push(outputPath);
|
|
@@ -20586,6 +21013,8 @@ var ReportGenerator = class {
|
|
|
20586
21013
|
case "astro": {
|
|
20587
21014
|
const formatter = new AstroFormatter({
|
|
20588
21015
|
assetsBaseUrl: this.options.astro.assetsBaseUrl,
|
|
21016
|
+
// Colocated = one page per file, so title each by its own suite/file.
|
|
21017
|
+
perFileTitle: this.options.output.mode === "colocated",
|
|
20589
21018
|
markdown: this.options.astro.markdown
|
|
20590
21019
|
});
|
|
20591
21020
|
return formatter.format(run);
|
|
@@ -20633,6 +21062,18 @@ var ReportGenerator = class {
|
|
|
20633
21062
|
});
|
|
20634
21063
|
return formatter.format(run);
|
|
20635
21064
|
}
|
|
21065
|
+
case "scenario-index-json": {
|
|
21066
|
+
const formatter = new ScenarioIndexJsonFormatter({
|
|
21067
|
+
pretty: this.options.scenarioIndexJson.pretty
|
|
21068
|
+
});
|
|
21069
|
+
return formatter.format(run);
|
|
21070
|
+
}
|
|
21071
|
+
case "behavior-manifest-json": {
|
|
21072
|
+
const formatter = new BehaviorManifestJsonFormatter({
|
|
21073
|
+
pretty: this.options.behaviorManifestJson.pretty
|
|
21074
|
+
});
|
|
21075
|
+
return formatter.format(run);
|
|
21076
|
+
}
|
|
20636
21077
|
default:
|
|
20637
21078
|
throw new Error(`Unknown format: ${format}`);
|
|
20638
21079
|
}
|
|
@@ -20646,7 +21087,7 @@ async function generateRunComparison(args) {
|
|
|
20646
21087
|
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
20647
21088
|
for (const format of args.formats) {
|
|
20648
21089
|
const ext = format === "html" ? ".html" : ".md";
|
|
20649
|
-
const outputPath = toPosix(
|
|
21090
|
+
const outputPath = toPosix(path8.join(outputDir, `${outputName}${ext}`));
|
|
20650
21091
|
const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
|
|
20651
21092
|
await fsPromises.writeFile(outputPath, content, "utf8");
|
|
20652
21093
|
files.push(outputPath);
|
|
@@ -20655,50 +21096,745 @@ async function generateRunComparison(args) {
|
|
|
20655
21096
|
}
|
|
20656
21097
|
|
|
20657
21098
|
// src/init-astro.ts
|
|
20658
|
-
import * as
|
|
20659
|
-
import * as
|
|
21099
|
+
import * as fs8 from "fs";
|
|
21100
|
+
import * as path9 from "path";
|
|
20660
21101
|
import { fileURLToPath } from "url";
|
|
20661
|
-
var __dirname =
|
|
21102
|
+
var __dirname = path9.dirname(fileURLToPath(import.meta.url));
|
|
21103
|
+
var FRAMEWORK_DIRS = ["src/components", "src/lib", "src/styles", "src/pages"];
|
|
21104
|
+
var FRAMEWORK_FILES = ["tsconfig.json"];
|
|
20662
21105
|
function initAstro(options = {}) {
|
|
20663
21106
|
const targetDir = options.targetDir ?? "./story-docs";
|
|
20664
21107
|
const force = options.force ?? false;
|
|
20665
|
-
|
|
20666
|
-
|
|
21108
|
+
const update = options.update ?? false;
|
|
21109
|
+
const templateDir = path9.resolve(__dirname, "..", "templates", "astro-starlight");
|
|
21110
|
+
if (!fs8.existsSync(templateDir)) {
|
|
21111
|
+
throw new Error(
|
|
21112
|
+
`Template directory not found at ${templateDir}. Ensure the package is installed correctly.`
|
|
21113
|
+
);
|
|
21114
|
+
}
|
|
21115
|
+
if (update) {
|
|
21116
|
+
return updateFrameworkFiles(templateDir, targetDir);
|
|
21117
|
+
}
|
|
21118
|
+
if (fs8.existsSync(targetDir)) {
|
|
21119
|
+
const entries = fs8.readdirSync(targetDir);
|
|
20667
21120
|
if (entries.length > 0 && !force) {
|
|
20668
21121
|
throw new Error(
|
|
20669
|
-
`Directory "${targetDir}" already exists and is not empty. Use --force to overwrite.`
|
|
21122
|
+
`Directory "${targetDir}" already exists and is not empty. Use --force to overwrite, or --update to refresh framework files only.`
|
|
20670
21123
|
);
|
|
20671
21124
|
}
|
|
20672
21125
|
}
|
|
20673
|
-
const templateDir = path8.resolve(__dirname, "..", "templates", "astro-starlight");
|
|
20674
|
-
if (!fs7.existsSync(templateDir)) {
|
|
20675
|
-
throw new Error(
|
|
20676
|
-
`Template directory not found at ${templateDir}. Ensure the package is installed correctly.`
|
|
20677
|
-
);
|
|
20678
|
-
}
|
|
20679
21126
|
copyDirRecursive(templateDir, targetDir);
|
|
20680
21127
|
return { targetDir };
|
|
20681
21128
|
}
|
|
20682
|
-
function
|
|
20683
|
-
|
|
20684
|
-
|
|
21129
|
+
function updateFrameworkFiles(templateDir, targetDir) {
|
|
21130
|
+
if (!fs8.existsSync(targetDir) || !fs8.existsSync(path9.join(targetDir, "astro.config.mjs"))) {
|
|
21131
|
+
throw new Error(
|
|
21132
|
+
`"${targetDir}" does not look like a scaffolded docs site. Run init-astro (without --update) first.`
|
|
21133
|
+
);
|
|
21134
|
+
}
|
|
21135
|
+
const updated = [];
|
|
21136
|
+
for (const dir of FRAMEWORK_DIRS) {
|
|
21137
|
+
const src = path9.join(templateDir, dir);
|
|
21138
|
+
if (!fs8.existsSync(src)) continue;
|
|
21139
|
+
copyDirRecursive(src, path9.join(targetDir, dir), (rel) => updated.push(path9.join(dir, rel)));
|
|
21140
|
+
}
|
|
21141
|
+
for (const file of FRAMEWORK_FILES) {
|
|
21142
|
+
const src = path9.join(templateDir, file);
|
|
21143
|
+
if (!fs8.existsSync(src)) continue;
|
|
21144
|
+
fs8.copyFileSync(src, path9.join(targetDir, file));
|
|
21145
|
+
updated.push(file);
|
|
21146
|
+
}
|
|
21147
|
+
if (mergeDependencies(templateDir, targetDir)) updated.push("package.json (deps)");
|
|
21148
|
+
return { targetDir, updatedFiles: updated };
|
|
21149
|
+
}
|
|
21150
|
+
function mergeDependencies(templateDir, targetDir) {
|
|
21151
|
+
const tmplPkgPath = path9.join(templateDir, "package.json");
|
|
21152
|
+
const userPkgPath = path9.join(targetDir, "package.json");
|
|
21153
|
+
if (!fs8.existsSync(tmplPkgPath) || !fs8.existsSync(userPkgPath)) return false;
|
|
21154
|
+
const tmpl = JSON.parse(fs8.readFileSync(tmplPkgPath, "utf8"));
|
|
21155
|
+
const user = JSON.parse(fs8.readFileSync(userPkgPath, "utf8"));
|
|
21156
|
+
user.dependencies = user.dependencies ?? {};
|
|
21157
|
+
let changed = false;
|
|
21158
|
+
for (const [name, version] of Object.entries(tmpl.dependencies ?? {})) {
|
|
21159
|
+
if (!(name in user.dependencies)) {
|
|
21160
|
+
user.dependencies[name] = version;
|
|
21161
|
+
changed = true;
|
|
21162
|
+
}
|
|
21163
|
+
}
|
|
21164
|
+
if (changed) {
|
|
21165
|
+
fs8.writeFileSync(userPkgPath, `${JSON.stringify(user, null, 2)}
|
|
21166
|
+
`, "utf8");
|
|
21167
|
+
}
|
|
21168
|
+
return changed;
|
|
21169
|
+
}
|
|
21170
|
+
function copyDirRecursive(src, dest, onFile, baseSrc = src) {
|
|
21171
|
+
fs8.mkdirSync(dest, { recursive: true });
|
|
21172
|
+
const entries = fs8.readdirSync(src, { withFileTypes: true });
|
|
20685
21173
|
for (const entry of entries) {
|
|
20686
|
-
const srcPath =
|
|
20687
|
-
const destPath =
|
|
21174
|
+
const srcPath = path9.join(src, entry.name);
|
|
21175
|
+
const destPath = path9.join(dest, entry.name);
|
|
20688
21176
|
if (entry.isDirectory()) {
|
|
20689
|
-
copyDirRecursive(srcPath, destPath);
|
|
21177
|
+
copyDirRecursive(srcPath, destPath, onFile, baseSrc);
|
|
20690
21178
|
} else {
|
|
20691
|
-
|
|
21179
|
+
fs8.copyFileSync(srcPath, destPath);
|
|
21180
|
+
onFile?.(path9.relative(baseSrc, srcPath));
|
|
21181
|
+
}
|
|
21182
|
+
}
|
|
21183
|
+
}
|
|
21184
|
+
|
|
21185
|
+
// src/scaffold-doc.ts
|
|
21186
|
+
import * as fs9 from "fs";
|
|
21187
|
+
import * as path10 from "path";
|
|
21188
|
+
var TEMPLATES = ["adr", "runbook", "decision-log", "incident"];
|
|
21189
|
+
function slugify3(input) {
|
|
21190
|
+
return input.toLowerCase().trim().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
|
|
21191
|
+
}
|
|
21192
|
+
function isoDate(today) {
|
|
21193
|
+
return today.toISOString().slice(0, 10);
|
|
21194
|
+
}
|
|
21195
|
+
function nextSeq(dir) {
|
|
21196
|
+
let max = 0;
|
|
21197
|
+
try {
|
|
21198
|
+
for (const entry of fs9.readdirSync(dir)) {
|
|
21199
|
+
const match = /^(\d{1,4})-/.exec(entry);
|
|
21200
|
+
if (match) max = Math.max(max, Number.parseInt(match[1], 10));
|
|
20692
21201
|
}
|
|
21202
|
+
} catch {
|
|
21203
|
+
}
|
|
21204
|
+
return String(max + 1).padStart(4, "0");
|
|
21205
|
+
}
|
|
21206
|
+
var COMPONENTS = "../../../components";
|
|
21207
|
+
var TEMPLATE_SPECS = {
|
|
21208
|
+
adr: {
|
|
21209
|
+
subdir: "adr",
|
|
21210
|
+
filename: (slug2, ctx) => `${ctx.seq}-${slug2}`,
|
|
21211
|
+
content: (ctx) => `---
|
|
21212
|
+
title: 'ADR ${ctx.seq} \u2014 ${ctx.name}'
|
|
21213
|
+
description: '${ctx.name}'
|
|
21214
|
+
# Link the stories that prove this decision. The badge under the title turns
|
|
21215
|
+
# red the moment any of them fail, so this record can't drift from the code.
|
|
21216
|
+
verifiedBy: []
|
|
21217
|
+
---
|
|
21218
|
+
|
|
21219
|
+
## Status
|
|
21220
|
+
|
|
21221
|
+
**Proposed** \u2014 proposed \xB7 accepted \xB7 superseded \xB7 deprecated
|
|
21222
|
+
|
|
21223
|
+
## Context
|
|
21224
|
+
|
|
21225
|
+
_What problem are we solving? What constraints and forces apply?_
|
|
21226
|
+
|
|
21227
|
+
## Decision
|
|
21228
|
+
|
|
21229
|
+
_What did we decide to do?_
|
|
21230
|
+
|
|
21231
|
+
## Consequences
|
|
21232
|
+
|
|
21233
|
+
_What becomes easier, and what becomes harder, as a result?_
|
|
21234
|
+
|
|
21235
|
+
## Verified by
|
|
21236
|
+
|
|
21237
|
+
Add the story ids or tags that exercise this decision to \`verifiedBy\` in the
|
|
21238
|
+
frontmatter above. Until you do, the badge reads **Unverified** \u2014 by design.
|
|
21239
|
+
`
|
|
21240
|
+
},
|
|
21241
|
+
runbook: {
|
|
21242
|
+
subdir: "runbooks",
|
|
21243
|
+
filename: (slug2) => slug2,
|
|
21244
|
+
content: (ctx) => `---
|
|
21245
|
+
title: 'Runbook \u2014 ${ctx.name}'
|
|
21246
|
+
description: 'Operational runbook for ${ctx.name}'
|
|
21247
|
+
---
|
|
21248
|
+
|
|
21249
|
+
import Checklist from '${COMPONENTS}/Checklist.astro';
|
|
21250
|
+
import VerifiedStep from '${COMPONENTS}/VerifiedStep.astro';
|
|
21251
|
+
|
|
21252
|
+
_When to use this runbook, prerequisites, and who to contact._
|
|
21253
|
+
|
|
21254
|
+
## Steps
|
|
21255
|
+
|
|
21256
|
+
Each step linked with \`story=\` shows a live green check when its test passed in
|
|
21257
|
+
the last run \u2014 so this runbook is trustworthy, not aspirational.
|
|
21258
|
+
|
|
21259
|
+
<Checklist>
|
|
21260
|
+
<VerifiedStep story="">Describe the first action, and link the story that verifies it.</VerifiedStep>
|
|
21261
|
+
<VerifiedStep>A manual step with no automated check.</VerifiedStep>
|
|
21262
|
+
</Checklist>
|
|
21263
|
+
|
|
21264
|
+
## Rollback
|
|
21265
|
+
|
|
21266
|
+
_How to safely undo if something goes wrong._
|
|
21267
|
+
`
|
|
21268
|
+
},
|
|
21269
|
+
"decision-log": {
|
|
21270
|
+
subdir: "decisions",
|
|
21271
|
+
filename: (slug2) => slug2,
|
|
21272
|
+
content: (ctx) => `---
|
|
21273
|
+
title: 'Decision log \u2014 ${ctx.name}'
|
|
21274
|
+
description: 'Running log of decisions for ${ctx.name}'
|
|
21275
|
+
---
|
|
21276
|
+
|
|
21277
|
+
A lightweight running log. For weightier decisions, scaffold a full ADR with
|
|
21278
|
+
\`executable-stories new adr\`.
|
|
21279
|
+
|
|
21280
|
+
| Date | Decision | Owner | Verified by |
|
|
21281
|
+
| ---- | -------- | ----- | ----------- |
|
|
21282
|
+
| ${ctx.isoDate} | _What was decided_ | _Who_ | _story id or tag_ |
|
|
21283
|
+
`
|
|
21284
|
+
},
|
|
21285
|
+
incident: {
|
|
21286
|
+
subdir: "incidents",
|
|
21287
|
+
filename: (slug2, ctx) => `${ctx.isoDate}-${slug2}`,
|
|
21288
|
+
content: (ctx) => `---
|
|
21289
|
+
title: 'Incident \u2014 ${ctx.name}'
|
|
21290
|
+
description: 'Post-mortem for ${ctx.name}'
|
|
21291
|
+
# Link the regression story added to stop this recurring.
|
|
21292
|
+
verifiedBy: []
|
|
21293
|
+
---
|
|
21294
|
+
|
|
21295
|
+
## Summary
|
|
21296
|
+
|
|
21297
|
+
_What happened, who was affected, and for how long._
|
|
21298
|
+
|
|
21299
|
+
## Timeline
|
|
21300
|
+
|
|
21301
|
+
| Time | Event |
|
|
21302
|
+
| ---- | ----- |
|
|
21303
|
+
| ${ctx.isoDate} | Detected |
|
|
21304
|
+
|
|
21305
|
+
## Root cause
|
|
21306
|
+
|
|
21307
|
+
_The underlying cause, not just the trigger._
|
|
21308
|
+
|
|
21309
|
+
## Resolution
|
|
21310
|
+
|
|
21311
|
+
_How it was fixed._
|
|
21312
|
+
|
|
21313
|
+
## Action items
|
|
21314
|
+
|
|
21315
|
+
- [ ] Add a regression story and link it in \`verifiedBy\` so a silent recurrence
|
|
21316
|
+
becomes a failing badge.
|
|
21317
|
+
`
|
|
21318
|
+
}
|
|
21319
|
+
};
|
|
21320
|
+
function isTemplateName(value) {
|
|
21321
|
+
return TEMPLATES.includes(value);
|
|
21322
|
+
}
|
|
21323
|
+
function scaffoldDoc(options) {
|
|
21324
|
+
const { template } = options;
|
|
21325
|
+
if (!isTemplateName(template)) {
|
|
21326
|
+
throw new Error(
|
|
21327
|
+
`Unknown template "${template}". Available: ${TEMPLATES.join(", ")}.`
|
|
21328
|
+
);
|
|
21329
|
+
}
|
|
21330
|
+
const spec = TEMPLATE_SPECS[template];
|
|
21331
|
+
const baseDir = options.baseDir ?? path10.join("src", "content", "docs");
|
|
21332
|
+
const today = options.today ?? /* @__PURE__ */ new Date();
|
|
21333
|
+
const name = (options.name ?? "").trim() || defaultName(template);
|
|
21334
|
+
const slug2 = slugify3(name);
|
|
21335
|
+
const dir = path10.join(baseDir, spec.subdir);
|
|
21336
|
+
const ctx = {
|
|
21337
|
+
name,
|
|
21338
|
+
slug: slug2,
|
|
21339
|
+
isoDate: isoDate(today),
|
|
21340
|
+
seq: nextSeq(dir)
|
|
21341
|
+
};
|
|
21342
|
+
const filename = `${spec.filename(slug2, ctx)}.mdx`;
|
|
21343
|
+
const filePath = path10.join(dir, filename);
|
|
21344
|
+
if (fs9.existsSync(filePath) && !options.force) {
|
|
21345
|
+
throw new Error(
|
|
21346
|
+
`File "${filePath}" already exists. Use --force to overwrite.`
|
|
21347
|
+
);
|
|
21348
|
+
}
|
|
21349
|
+
fs9.mkdirSync(dir, { recursive: true });
|
|
21350
|
+
fs9.writeFileSync(filePath, spec.content(ctx), "utf8");
|
|
21351
|
+
return { template, path: filePath, title: titleFor2(template, ctx) };
|
|
21352
|
+
}
|
|
21353
|
+
function defaultName(template) {
|
|
21354
|
+
switch (template) {
|
|
21355
|
+
case "adr":
|
|
21356
|
+
return "Untitled decision";
|
|
21357
|
+
case "runbook":
|
|
21358
|
+
return "Untitled runbook";
|
|
21359
|
+
case "decision-log":
|
|
21360
|
+
return "Decisions";
|
|
21361
|
+
case "incident":
|
|
21362
|
+
return "Untitled incident";
|
|
21363
|
+
}
|
|
21364
|
+
}
|
|
21365
|
+
function titleFor2(template, ctx) {
|
|
21366
|
+
switch (template) {
|
|
21367
|
+
case "adr":
|
|
21368
|
+
return `ADR ${ctx.seq} \u2014 ${ctx.name}`;
|
|
21369
|
+
case "runbook":
|
|
21370
|
+
return `Runbook \u2014 ${ctx.name}`;
|
|
21371
|
+
case "decision-log":
|
|
21372
|
+
return `Decision log \u2014 ${ctx.name}`;
|
|
21373
|
+
case "incident":
|
|
21374
|
+
return `Incident \u2014 ${ctx.name}`;
|
|
21375
|
+
}
|
|
21376
|
+
}
|
|
21377
|
+
|
|
21378
|
+
// src/check-links.ts
|
|
21379
|
+
import * as fs10 from "fs";
|
|
21380
|
+
import * as path11 from "path";
|
|
21381
|
+
function stripCode(markdown) {
|
|
21382
|
+
let out = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
|
|
21383
|
+
out = out.replace(/(`+)(?:(?!\1).)+\1/g, "");
|
|
21384
|
+
return out;
|
|
21385
|
+
}
|
|
21386
|
+
function extractLinks(markdown) {
|
|
21387
|
+
const stripped = stripCode(markdown);
|
|
21388
|
+
const found = [];
|
|
21389
|
+
const mdRe = /!?\[[^\]]*\]\(\s*<?([^)\s"'<>]+)>?(?:\s+["'][^"']*["'])?\s*\)/g;
|
|
21390
|
+
let match;
|
|
21391
|
+
while ((match = mdRe.exec(stripped)) !== null) {
|
|
21392
|
+
found.push(match[1].trim());
|
|
21393
|
+
}
|
|
21394
|
+
const htmlRe = /<[a-z][^>]*\b(?:href|src)=["']([^"']+)["']/gi;
|
|
21395
|
+
while ((match = htmlRe.exec(stripped)) !== null) {
|
|
21396
|
+
found.push(match[1].trim());
|
|
21397
|
+
}
|
|
21398
|
+
return found.filter(Boolean);
|
|
21399
|
+
}
|
|
21400
|
+
function classifyLink(link2) {
|
|
21401
|
+
if (/^(?:https?:)?\/\//i.test(link2)) return "external";
|
|
21402
|
+
if (/^mailto:/i.test(link2)) return "mail";
|
|
21403
|
+
if (link2.startsWith("#")) return "anchor";
|
|
21404
|
+
if (link2.startsWith("/")) return "root";
|
|
21405
|
+
return "internal";
|
|
21406
|
+
}
|
|
21407
|
+
function resolutionCandidates(fromFile, link2) {
|
|
21408
|
+
const withoutAnchor = link2.split("#")[0];
|
|
21409
|
+
if (!withoutAnchor) return [];
|
|
21410
|
+
const base = path11.resolve(path11.dirname(fromFile), withoutAnchor);
|
|
21411
|
+
const candidates = [base];
|
|
21412
|
+
if (!path11.extname(base)) {
|
|
21413
|
+
candidates.push(`${base}.md`, `${base}.mdx`);
|
|
21414
|
+
candidates.push(path11.join(base, "index.md"), path11.join(base, "index.mdx"));
|
|
21415
|
+
}
|
|
21416
|
+
return candidates;
|
|
21417
|
+
}
|
|
21418
|
+
function resolvesOnDisk(fromFile, link2) {
|
|
21419
|
+
return resolutionCandidates(fromFile, link2).some(
|
|
21420
|
+
(candidate) => fs10.existsSync(candidate) && fs10.statSync(candidate).isFile()
|
|
21421
|
+
);
|
|
21422
|
+
}
|
|
21423
|
+
function collectDocFiles(target) {
|
|
21424
|
+
const stat = fs10.statSync(target);
|
|
21425
|
+
if (stat.isFile()) return [target];
|
|
21426
|
+
const out = [];
|
|
21427
|
+
const walk = (dir) => {
|
|
21428
|
+
for (const entry of fs10.readdirSync(dir, { withFileTypes: true })) {
|
|
21429
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
21430
|
+
const full = path11.join(dir, entry.name);
|
|
21431
|
+
if (entry.isDirectory()) walk(full);
|
|
21432
|
+
else if (/\.mdx?$/.test(entry.name)) out.push(full);
|
|
21433
|
+
}
|
|
21434
|
+
};
|
|
21435
|
+
walk(target);
|
|
21436
|
+
return out;
|
|
21437
|
+
}
|
|
21438
|
+
async function isExternalAlive(url, timeoutMs) {
|
|
21439
|
+
const attempt = async (method) => {
|
|
21440
|
+
const controller = new AbortController();
|
|
21441
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
21442
|
+
try {
|
|
21443
|
+
const res = await fetch(url, { method, signal: controller.signal, redirect: "follow" });
|
|
21444
|
+
return res.status < 400;
|
|
21445
|
+
} finally {
|
|
21446
|
+
clearTimeout(timer);
|
|
21447
|
+
}
|
|
21448
|
+
};
|
|
21449
|
+
try {
|
|
21450
|
+
return await attempt("HEAD");
|
|
21451
|
+
} catch {
|
|
21452
|
+
try {
|
|
21453
|
+
return await attempt("GET");
|
|
21454
|
+
} catch {
|
|
21455
|
+
return false;
|
|
21456
|
+
}
|
|
21457
|
+
}
|
|
21458
|
+
}
|
|
21459
|
+
async function checkLinks(options) {
|
|
21460
|
+
const { target, checkExternal = false, externalTimeoutMs = 8e3 } = options;
|
|
21461
|
+
if (!fs10.existsSync(target)) {
|
|
21462
|
+
throw new Error(`Path not found: ${target}`);
|
|
21463
|
+
}
|
|
21464
|
+
const files = collectDocFiles(target);
|
|
21465
|
+
const broken = [];
|
|
21466
|
+
let linksChecked = 0;
|
|
21467
|
+
let externalChecked = 0;
|
|
21468
|
+
let skipped = 0;
|
|
21469
|
+
const externalCache = /* @__PURE__ */ new Map();
|
|
21470
|
+
for (const file of files) {
|
|
21471
|
+
const content = fs10.readFileSync(file, "utf8");
|
|
21472
|
+
for (const link2 of extractLinks(content)) {
|
|
21473
|
+
const kind = classifyLink(link2);
|
|
21474
|
+
if (kind === "anchor" || kind === "mail" || kind === "root") {
|
|
21475
|
+
skipped += 1;
|
|
21476
|
+
continue;
|
|
21477
|
+
}
|
|
21478
|
+
if (kind === "external") {
|
|
21479
|
+
if (!checkExternal) {
|
|
21480
|
+
skipped += 1;
|
|
21481
|
+
continue;
|
|
21482
|
+
}
|
|
21483
|
+
externalChecked += 1;
|
|
21484
|
+
linksChecked += 1;
|
|
21485
|
+
let pending = externalCache.get(link2);
|
|
21486
|
+
if (!pending) {
|
|
21487
|
+
pending = isExternalAlive(link2, externalTimeoutMs);
|
|
21488
|
+
externalCache.set(link2, pending);
|
|
21489
|
+
}
|
|
21490
|
+
if (!await pending) {
|
|
21491
|
+
broken.push({ file, link: link2, reason: "external URL unreachable" });
|
|
21492
|
+
}
|
|
21493
|
+
continue;
|
|
21494
|
+
}
|
|
21495
|
+
linksChecked += 1;
|
|
21496
|
+
if (!resolvesOnDisk(file, link2)) {
|
|
21497
|
+
broken.push({ file, link: link2, reason: "target file not found" });
|
|
21498
|
+
}
|
|
21499
|
+
}
|
|
21500
|
+
}
|
|
21501
|
+
return {
|
|
21502
|
+
filesScanned: files.length,
|
|
21503
|
+
linksChecked,
|
|
21504
|
+
brokenCount: broken.length,
|
|
21505
|
+
broken,
|
|
21506
|
+
externalChecked,
|
|
21507
|
+
skipped
|
|
21508
|
+
};
|
|
21509
|
+
}
|
|
21510
|
+
function formatLinkReport(report) {
|
|
21511
|
+
const lines = [];
|
|
21512
|
+
lines.push(
|
|
21513
|
+
`Scanned ${report.filesScanned} file(s), checked ${report.linksChecked} link(s) (${report.skipped} skipped).`
|
|
21514
|
+
);
|
|
21515
|
+
if (report.brokenCount === 0) {
|
|
21516
|
+
lines.push("\u2713 No broken links.");
|
|
21517
|
+
} else {
|
|
21518
|
+
lines.push(`\u2715 ${report.brokenCount} broken link(s):`);
|
|
21519
|
+
for (const b of report.broken) {
|
|
21520
|
+
lines.push(` ${b.file}: ${b.link} \u2014 ${b.reason}`);
|
|
21521
|
+
}
|
|
21522
|
+
}
|
|
21523
|
+
return lines.join("\n");
|
|
21524
|
+
}
|
|
21525
|
+
|
|
21526
|
+
// src/import-openapi.ts
|
|
21527
|
+
import * as fs11 from "fs";
|
|
21528
|
+
import * as path12 from "path";
|
|
21529
|
+
import { parse as parseYamlString } from "yaml";
|
|
21530
|
+
var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head"];
|
|
21531
|
+
function parseYaml(raw, specPath) {
|
|
21532
|
+
try {
|
|
21533
|
+
return parseYamlString(raw);
|
|
21534
|
+
} catch (err) {
|
|
21535
|
+
throw new Error(`Could not parse YAML spec ${specPath}: ${err.message}`, {
|
|
21536
|
+
cause: err
|
|
21537
|
+
});
|
|
21538
|
+
}
|
|
21539
|
+
}
|
|
21540
|
+
function parseSpec(specPath) {
|
|
21541
|
+
if (!fs11.existsSync(specPath)) throw new Error(`Spec not found: ${specPath}`);
|
|
21542
|
+
const raw = fs11.readFileSync(specPath, "utf8");
|
|
21543
|
+
const ext = path12.extname(specPath).toLowerCase();
|
|
21544
|
+
if (ext === ".json") return JSON.parse(raw);
|
|
21545
|
+
if (ext === ".yaml" || ext === ".yml") return parseYaml(raw, specPath);
|
|
21546
|
+
try {
|
|
21547
|
+
return JSON.parse(raw);
|
|
21548
|
+
} catch {
|
|
21549
|
+
return parseYaml(raw, specPath);
|
|
21550
|
+
}
|
|
21551
|
+
}
|
|
21552
|
+
function extractEndpoints(spec) {
|
|
21553
|
+
const paths = spec.paths ?? {};
|
|
21554
|
+
const endpoints = [];
|
|
21555
|
+
for (const [route, item] of Object.entries(paths)) {
|
|
21556
|
+
if (!item || typeof item !== "object") continue;
|
|
21557
|
+
for (const method of HTTP_METHODS) {
|
|
21558
|
+
const op = item[method];
|
|
21559
|
+
if (!op || typeof op !== "object") continue;
|
|
21560
|
+
const tags = Array.isArray(op.tags) && op.tags.length > 0 ? op.tags : ["API"];
|
|
21561
|
+
endpoints.push({
|
|
21562
|
+
method: method.toUpperCase(),
|
|
21563
|
+
path: route,
|
|
21564
|
+
operationId: typeof op.operationId === "string" ? op.operationId : void 0,
|
|
21565
|
+
summary: typeof op.summary === "string" && op.summary || typeof op.description === "string" && op.description || "",
|
|
21566
|
+
tag: String(tags[0])
|
|
21567
|
+
});
|
|
21568
|
+
}
|
|
21569
|
+
}
|
|
21570
|
+
return endpoints;
|
|
21571
|
+
}
|
|
21572
|
+
function loadScenarios(runFile) {
|
|
21573
|
+
if (!runFile) return [];
|
|
21574
|
+
if (!fs11.existsSync(runFile)) throw new Error(`Run file not found: ${runFile}`);
|
|
21575
|
+
const report = JSON.parse(fs11.readFileSync(runFile, "utf8"));
|
|
21576
|
+
return (report.features ?? []).flatMap((f) => f.scenarios ?? []);
|
|
21577
|
+
}
|
|
21578
|
+
function endpointRefs(endpoint) {
|
|
21579
|
+
const refs = [
|
|
21580
|
+
endpoint.operationId,
|
|
21581
|
+
`${endpoint.method} ${endpoint.path}`,
|
|
21582
|
+
endpoint.path
|
|
21583
|
+
].filter((r) => Boolean(r));
|
|
21584
|
+
return refs;
|
|
21585
|
+
}
|
|
21586
|
+
function scenarioMatchesEndpoint(scenario, refs) {
|
|
21587
|
+
const tags = scenario.tags ?? [];
|
|
21588
|
+
return refs.some(
|
|
21589
|
+
(ref) => scenario.id === ref || scenario.title === ref || tags.includes(ref)
|
|
21590
|
+
);
|
|
21591
|
+
}
|
|
21592
|
+
function computeCoverage(endpoints, scenarios) {
|
|
21593
|
+
return endpoints.map((endpoint) => {
|
|
21594
|
+
const refs = endpointRefs(endpoint);
|
|
21595
|
+
const matched = scenarios.filter((s) => scenarioMatchesEndpoint(s, refs));
|
|
21596
|
+
let status;
|
|
21597
|
+
if (matched.length === 0) status = "uncovered";
|
|
21598
|
+
else if (matched.some((s) => s.status === "failed")) status = "failing";
|
|
21599
|
+
else status = "covered";
|
|
21600
|
+
return {
|
|
21601
|
+
endpoint,
|
|
21602
|
+
status,
|
|
21603
|
+
stories: matched.map((s) => ({
|
|
21604
|
+
id: s.id ?? s.title ?? "",
|
|
21605
|
+
title: s.title ?? s.id ?? "story",
|
|
21606
|
+
status: s.status ?? "passed"
|
|
21607
|
+
}))
|
|
21608
|
+
};
|
|
21609
|
+
});
|
|
21610
|
+
}
|
|
21611
|
+
function slug(input) {
|
|
21612
|
+
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "api";
|
|
21613
|
+
}
|
|
21614
|
+
function yamlQuote(value) {
|
|
21615
|
+
return value.replace(/'/g, "''");
|
|
21616
|
+
}
|
|
21617
|
+
function coverageSummary(rows) {
|
|
21618
|
+
return {
|
|
21619
|
+
total: rows.length,
|
|
21620
|
+
covered: rows.filter((r) => r.status === "covered").length,
|
|
21621
|
+
failing: rows.filter((r) => r.status === "failing").length,
|
|
21622
|
+
uncovered: rows.filter((r) => r.status === "uncovered").length
|
|
21623
|
+
};
|
|
21624
|
+
}
|
|
21625
|
+
function renderTagPage(tag, rows, hasRun) {
|
|
21626
|
+
const endpoints = rows.map((r) => ({
|
|
21627
|
+
method: r.endpoint.method,
|
|
21628
|
+
path: r.endpoint.path,
|
|
21629
|
+
summary: r.endpoint.summary || "",
|
|
21630
|
+
status: r.status,
|
|
21631
|
+
stories: r.stories
|
|
21632
|
+
}));
|
|
21633
|
+
const summary = coverageSummary(rows);
|
|
21634
|
+
return `---
|
|
21635
|
+
title: 'API \u2014 ${yamlQuote(tag)}'
|
|
21636
|
+
description: 'Endpoints for ${yamlQuote(tag)}, linked to the stories that exercise them.'
|
|
21637
|
+
---
|
|
21638
|
+
|
|
21639
|
+
import ApiOperations from '@components/ApiOperations.astro';
|
|
21640
|
+
|
|
21641
|
+
<ApiOperations
|
|
21642
|
+
tag={${JSON.stringify(tag)}}
|
|
21643
|
+
hasRun={${hasRun}}
|
|
21644
|
+
summary={${JSON.stringify(summary)}}
|
|
21645
|
+
endpoints={${JSON.stringify(endpoints)}}
|
|
21646
|
+
/>
|
|
21647
|
+
`;
|
|
21648
|
+
}
|
|
21649
|
+
function renderIndex(groups, hasRun, totals) {
|
|
21650
|
+
const rows = [...groups.entries()].map(([tag, eps]) => {
|
|
21651
|
+
const covered = eps.filter((e) => e.status === "covered").length;
|
|
21652
|
+
const cov = hasRun ? ` | ${covered}/${eps.length} covered` : "";
|
|
21653
|
+
return `- [${tag}](./${slug(tag)}/) \u2014 ${eps.length} endpoint(s)${cov}`;
|
|
21654
|
+
}).join("\n");
|
|
21655
|
+
const coverageNote = hasRun ? `
|
|
21656
|
+
**${totals.coveredCount} of ${totals.endpointCount} endpoints** are covered by a passing story.` + (totals.uncoveredCount > 0 ? ` \u26A0 ${totals.uncoveredCount} endpoint(s) have no verifying test.` : "") : "\nRe-run with `--run <story-report.json>` to show per-endpoint test coverage.";
|
|
21657
|
+
return `---
|
|
21658
|
+
title: 'API reference'
|
|
21659
|
+
description: 'API endpoints generated from OpenAPI, linked to verifying stories.'
|
|
21660
|
+
---
|
|
21661
|
+
|
|
21662
|
+
${coverageNote}
|
|
21663
|
+
|
|
21664
|
+
${rows}
|
|
21665
|
+
`;
|
|
21666
|
+
}
|
|
21667
|
+
async function importOpenApi(options) {
|
|
21668
|
+
const spec = parseSpec(options.specPath);
|
|
21669
|
+
const endpoints = extractEndpoints(spec);
|
|
21670
|
+
if (endpoints.length === 0) {
|
|
21671
|
+
throw new Error(`No endpoints found in ${options.specPath} (expected an OpenAPI "paths" object).`);
|
|
21672
|
+
}
|
|
21673
|
+
const scenarios = loadScenarios(options.runFile);
|
|
21674
|
+
const hasRun = Boolean(options.runFile);
|
|
21675
|
+
const coverage = computeCoverage(endpoints, scenarios);
|
|
21676
|
+
const groups = /* @__PURE__ */ new Map();
|
|
21677
|
+
for (const item of coverage) {
|
|
21678
|
+
const list = groups.get(item.endpoint.tag) ?? [];
|
|
21679
|
+
list.push(item);
|
|
21680
|
+
groups.set(item.endpoint.tag, list);
|
|
21681
|
+
}
|
|
21682
|
+
const outputDir = options.outputDir ?? path12.join("src", "content", "docs", "api");
|
|
21683
|
+
if (fs11.existsSync(outputDir) && !options.force) {
|
|
21684
|
+
const entries = fs11.readdirSync(outputDir);
|
|
21685
|
+
if (entries.length > 0) {
|
|
21686
|
+
throw new Error(`Output directory "${outputDir}" is not empty. Use --force to overwrite.`);
|
|
21687
|
+
}
|
|
21688
|
+
}
|
|
21689
|
+
fs11.mkdirSync(outputDir, { recursive: true });
|
|
21690
|
+
const coveredCount = coverage.filter((c) => c.status === "covered").length;
|
|
21691
|
+
const uncoveredCount = coverage.filter((c) => c.status === "uncovered").length;
|
|
21692
|
+
fs11.writeFileSync(
|
|
21693
|
+
path12.join(outputDir, "index.mdx"),
|
|
21694
|
+
renderIndex(groups, hasRun, { endpointCount: endpoints.length, coveredCount, uncoveredCount }),
|
|
21695
|
+
"utf8"
|
|
21696
|
+
);
|
|
21697
|
+
for (const [tag, rows] of groups) {
|
|
21698
|
+
const dir = path12.join(outputDir, slug(tag));
|
|
21699
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
21700
|
+
fs11.writeFileSync(path12.join(dir, "index.mdx"), renderTagPage(tag, rows, hasRun), "utf8");
|
|
21701
|
+
}
|
|
21702
|
+
return {
|
|
21703
|
+
outputDir,
|
|
21704
|
+
pageCount: groups.size + 1,
|
|
21705
|
+
endpointCount: endpoints.length,
|
|
21706
|
+
coveredCount,
|
|
21707
|
+
uncoveredCount
|
|
21708
|
+
};
|
|
21709
|
+
}
|
|
21710
|
+
|
|
21711
|
+
// src/build-docs.ts
|
|
21712
|
+
import * as fs12 from "fs";
|
|
21713
|
+
import * as path13 from "path";
|
|
21714
|
+
var BuildDocsError = class extends Error {
|
|
21715
|
+
constructor(message, kind) {
|
|
21716
|
+
super(message);
|
|
21717
|
+
this.kind = kind;
|
|
21718
|
+
this.name = "BuildDocsError";
|
|
21719
|
+
}
|
|
21720
|
+
};
|
|
21721
|
+
var isRemote = (p) => /^(?:https?:|data:)/i.test(p);
|
|
21722
|
+
function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets") {
|
|
21723
|
+
if (!fs12.existsSync(reportPath)) return 0;
|
|
21724
|
+
const report = JSON.parse(fs12.readFileSync(reportPath, "utf8"));
|
|
21725
|
+
let copied = 0;
|
|
21726
|
+
const bundle = (value) => {
|
|
21727
|
+
const rel = copyAsset(path13.resolve(value), assetsDir);
|
|
21728
|
+
copied++;
|
|
21729
|
+
return `${baseUrl}/${path13.basename(rel)}`;
|
|
21730
|
+
};
|
|
21731
|
+
const visit = (entries) => {
|
|
21732
|
+
for (const entry of entries ?? []) {
|
|
21733
|
+
const e = entry;
|
|
21734
|
+
if (e.kind === "screenshot" || e.kind === "video") {
|
|
21735
|
+
if (typeof e.path === "string" && !isRemote(e.path) && fs12.existsSync(e.path)) {
|
|
21736
|
+
e.path = bundle(e.path);
|
|
21737
|
+
}
|
|
21738
|
+
if (typeof e.poster === "string" && !isRemote(e.poster) && fs12.existsSync(e.poster)) {
|
|
21739
|
+
e.poster = bundle(e.poster);
|
|
21740
|
+
}
|
|
21741
|
+
}
|
|
21742
|
+
if (Array.isArray(e.children)) visit(e.children);
|
|
21743
|
+
}
|
|
21744
|
+
};
|
|
21745
|
+
for (const feature of report.features ?? []) {
|
|
21746
|
+
for (const scenario of feature.scenarios ?? []) {
|
|
21747
|
+
visit(scenario.docEntries);
|
|
21748
|
+
}
|
|
21749
|
+
}
|
|
21750
|
+
if (copied > 0) {
|
|
21751
|
+
fs12.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
|
|
21752
|
+
}
|
|
21753
|
+
return copied;
|
|
21754
|
+
}
|
|
21755
|
+
function clearGeneratedPages(dir) {
|
|
21756
|
+
if (!fs12.existsSync(dir)) return;
|
|
21757
|
+
for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
|
|
21758
|
+
const full = path13.join(dir, entry.name);
|
|
21759
|
+
if (entry.isDirectory()) {
|
|
21760
|
+
clearGeneratedPages(full);
|
|
21761
|
+
if (fs12.readdirSync(full).length === 0) fs12.rmdirSync(full);
|
|
21762
|
+
} else if (/\.mdx?$/.test(entry.name)) {
|
|
21763
|
+
fs12.rmSync(full);
|
|
21764
|
+
}
|
|
21765
|
+
}
|
|
21766
|
+
}
|
|
21767
|
+
function loadCanonicalRun(rawRunPath, synthesize) {
|
|
21768
|
+
try {
|
|
21769
|
+
const data = JSON.parse(fs12.readFileSync(path13.resolve(rawRunPath), "utf8"));
|
|
21770
|
+
if (data.schemaVersion !== 1) {
|
|
21771
|
+
throw new BuildDocsError(`Unsupported schemaVersion ${data.schemaVersion}. Supported: 1.`, "schema");
|
|
21772
|
+
}
|
|
21773
|
+
const schemaResult = validateRawRun(data);
|
|
21774
|
+
if (!schemaResult.valid) {
|
|
21775
|
+
throw new BuildDocsError(
|
|
21776
|
+
`Schema validation failed:
|
|
21777
|
+
${schemaResult.errors.map((e) => ` ${e}`).join("\n")}`,
|
|
21778
|
+
"schema"
|
|
21779
|
+
);
|
|
21780
|
+
}
|
|
21781
|
+
let raw = data;
|
|
21782
|
+
if (synthesize) raw = synthesizeStories(raw);
|
|
21783
|
+
const canonical = canonicalizeRun(raw);
|
|
21784
|
+
assertValidRun(canonical);
|
|
21785
|
+
return canonical;
|
|
21786
|
+
} catch (err) {
|
|
21787
|
+
if (err instanceof BuildDocsError) throw err;
|
|
21788
|
+
throw new BuildDocsError(`Could not read raw run "${rawRunPath}": ${err.message}`, "input");
|
|
21789
|
+
}
|
|
21790
|
+
}
|
|
21791
|
+
async function buildDocs(options) {
|
|
21792
|
+
const siteDir = path13.resolve(options.siteDir);
|
|
21793
|
+
const storiesPublicDir = path13.join(siteDir, "public", "stories");
|
|
21794
|
+
const assetsDir = path13.join(storiesPublicDir, "assets");
|
|
21795
|
+
const storyPagesDir = path13.join(siteDir, "src", "content", "docs", "stories");
|
|
21796
|
+
const apiDir = path13.join(siteDir, "src", "content", "docs", "api");
|
|
21797
|
+
const reportPath = path13.join(storiesPublicDir, "story-report.json");
|
|
21798
|
+
const canonical = loadCanonicalRun(options.rawRunPath, options.synthesizeStories ?? true);
|
|
21799
|
+
try {
|
|
21800
|
+
await new ReportGenerator({
|
|
21801
|
+
formats: ["story-report-json"],
|
|
21802
|
+
outputDir: storiesPublicDir,
|
|
21803
|
+
outputName: "story-report"
|
|
21804
|
+
}).generate(canonical);
|
|
21805
|
+
clearGeneratedPages(storyPagesDir);
|
|
21806
|
+
await new ReportGenerator({
|
|
21807
|
+
formats: ["astro"],
|
|
21808
|
+
outputDir: storyPagesDir,
|
|
21809
|
+
outputName: "index",
|
|
21810
|
+
output: { mode: "colocated", colocatedStyle: "flat" },
|
|
21811
|
+
assetMode: "copy",
|
|
21812
|
+
astro: { assetsDir, assetsBaseUrl: "/stories/assets" }
|
|
21813
|
+
}).generate(canonical);
|
|
21814
|
+
const bundledAssets = bundleExplorerAssets(reportPath, assetsDir);
|
|
21815
|
+
let apiPages = 0;
|
|
21816
|
+
if (options.openapiPath) {
|
|
21817
|
+
const res = await importOpenApi({
|
|
21818
|
+
specPath: path13.resolve(options.openapiPath),
|
|
21819
|
+
outputDir: apiDir,
|
|
21820
|
+
runFile: reportPath,
|
|
21821
|
+
force: true
|
|
21822
|
+
});
|
|
21823
|
+
apiPages = res.pageCount;
|
|
21824
|
+
}
|
|
21825
|
+
return { siteDir, bundledAssets, apiPages };
|
|
21826
|
+
} catch (err) {
|
|
21827
|
+
if (err instanceof BuildDocsError) throw err;
|
|
21828
|
+
throw new BuildDocsError(`Generation failed: ${err.message}`, "generation");
|
|
20693
21829
|
}
|
|
20694
21830
|
}
|
|
20695
21831
|
|
|
20696
21832
|
// src/config.ts
|
|
20697
|
-
import { existsSync as
|
|
20698
|
-
import { resolve as
|
|
21833
|
+
import { existsSync as existsSync11 } from "fs";
|
|
21834
|
+
import { resolve as resolve9 } from "path";
|
|
20699
21835
|
async function loadConfig(configPath) {
|
|
20700
|
-
const resolved = configPath ?
|
|
20701
|
-
if (!
|
|
21836
|
+
const resolved = configPath ? resolve9(configPath) : resolve9(process.cwd(), "executable-stories.config.js");
|
|
21837
|
+
if (!existsSync11(resolved)) return {};
|
|
20702
21838
|
const mod = await import(resolved);
|
|
20703
21839
|
const config = mod.default;
|
|
20704
21840
|
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
@@ -20729,23 +21865,31 @@ USAGE
|
|
|
20729
21865
|
executable-stories validate <file>
|
|
20730
21866
|
executable-stories validate --stdin
|
|
20731
21867
|
executable-stories init-astro [directory]
|
|
21868
|
+
executable-stories new <template> "<name>" [options]
|
|
21869
|
+
executable-stories check-links <dir> [options]
|
|
21870
|
+
executable-stories import-openapi <spec> [options]
|
|
20732
21871
|
executable-stories publish-confluence <file.adf.json> [options]
|
|
20733
21872
|
executable-stories publish-jira <file.adf.json> [options]
|
|
20734
21873
|
|
|
20735
21874
|
SUBCOMMANDS
|
|
20736
21875
|
format Read raw test results and generate reports
|
|
21876
|
+
watch Regenerate reports whenever the raw-run file changes (live agent index)
|
|
20737
21877
|
compare Compare two runs and generate a diff report
|
|
20738
21878
|
review Generate an Evidence Review of AI-authored changes (correlate a run to the diff)
|
|
20739
21879
|
list List scenarios from a test run (text table or JSON)
|
|
20740
21880
|
validate Validate a JSON file against the schema (no output generated)
|
|
20741
21881
|
init-astro Scaffold an Astro docs site for story output (Starlight with themed CSS)
|
|
21882
|
+
new Scaffold a docs page from a template (adr, runbook, decision-log, incident)
|
|
21883
|
+
check-links Scan docs for broken internal/external links (CI-friendly exit code)
|
|
21884
|
+
import-openapi Generate API doc pages from an OpenAPI spec, linked to verifying stories
|
|
20742
21885
|
publish-confluence Publish an ADF JSON file to a Confluence page via REST API
|
|
20743
21886
|
publish-jira Publish an ADF JSON file to a Jira issue (as comment or description)
|
|
20744
21887
|
|
|
20745
21888
|
OPTIONS
|
|
20746
|
-
--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)
|
|
21889
|
+
--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)
|
|
20747
21890
|
astro Themed Markdown (for Astro docs sites with matching CSS)
|
|
20748
21891
|
confluence Atlassian Document Format (ADF) JSON for Confluence / Jira
|
|
21892
|
+
behavior-manifest-json Agent-readable behavior manifest and debugger warnings
|
|
20749
21893
|
html Custom HTML report (accessible, dark mode, mermaid)
|
|
20750
21894
|
cucumber-html Official Cucumber HTML report
|
|
20751
21895
|
markdown Markdown documentation
|
|
@@ -20753,6 +21897,7 @@ OPTIONS
|
|
|
20753
21897
|
cucumber-json Cucumber JSON
|
|
20754
21898
|
cucumber-messages Raw NDJSON (Cucumber Messages)
|
|
20755
21899
|
story-report-json StoryReport v1 JSON (consumed by executable-stories-react and other UI renderers)
|
|
21900
|
+
scenario-index-json Storybook-like scenario index for agents and explorers
|
|
20756
21901
|
--config <path> Path to executable-stories.config.js (default: ./executable-stories.config.js)
|
|
20757
21902
|
--input-type <type> Input type: raw, canonical, or ndjson (default: raw)
|
|
20758
21903
|
--output-dir <dir> Output directory (default: reports)
|
|
@@ -20863,9 +22008,9 @@ async function parseCliArgs(argv) {
|
|
|
20863
22008
|
process.exit(EXIT_SUCCESS);
|
|
20864
22009
|
}
|
|
20865
22010
|
const subcommand = args[0];
|
|
20866
|
-
if (subcommand !== "format" && subcommand !== "compare" && subcommand !== "review" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
|
|
22011
|
+
if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "review" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "build-docs" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
|
|
20867
22012
|
console.error(
|
|
20868
|
-
`Unknown subcommand: "${subcommand}". Use "format", "compare", "review", "list", "validate", "init-astro", "publish-confluence", or "publish-jira".`
|
|
22013
|
+
`Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "review", "list", "validate", "init-astro", "build-docs", "new", "check-links", "import-openapi", "publish-confluence", or "publish-jira".`
|
|
20869
22014
|
);
|
|
20870
22015
|
process.exit(EXIT_USAGE);
|
|
20871
22016
|
}
|
|
@@ -20881,8 +22026,14 @@ async function parseCliArgs(argv) {
|
|
|
20881
22026
|
const initArgs = args.slice(1);
|
|
20882
22027
|
const targetDir = initArgs.find((a) => !a.startsWith("--")) ?? "./story-docs";
|
|
20883
22028
|
const force = initArgs.includes("--force");
|
|
22029
|
+
const update = initArgs.includes("--update");
|
|
20884
22030
|
try {
|
|
20885
|
-
const result = initAstro({ targetDir, force });
|
|
22031
|
+
const result = initAstro({ targetDir, force, update });
|
|
22032
|
+
if (update) {
|
|
22033
|
+
console.log(`Updated framework files in ${result.targetDir} (content left untouched)`);
|
|
22034
|
+
console.log(` Refreshed: ${result.updatedFiles?.length ?? 0} file(s)`);
|
|
22035
|
+
process.exit(EXIT_SUCCESS);
|
|
22036
|
+
}
|
|
20886
22037
|
console.log(`Scaffolded Astro docs site at ${result.targetDir}`);
|
|
20887
22038
|
console.log("");
|
|
20888
22039
|
console.log("Themes available in src/styles/themes/:");
|
|
@@ -20895,20 +22046,26 @@ async function parseCliArgs(argv) {
|
|
|
20895
22046
|
console.log("");
|
|
20896
22047
|
console.log("To change theme, edit astro.config.mjs customCss array.");
|
|
20897
22048
|
console.log("");
|
|
20898
|
-
console.log("");
|
|
20899
22049
|
console.log("Next steps:");
|
|
20900
22050
|
console.log(` cd ${result.targetDir}`);
|
|
20901
22051
|
console.log(" pnpm install # or npm install");
|
|
20902
22052
|
console.log(" pnpm dev # start the dev server");
|
|
20903
22053
|
console.log("");
|
|
20904
|
-
console.log("Generate story
|
|
20905
|
-
console.log(` executable-stories
|
|
22054
|
+
console.log("Generate everything (story pages, explorer data, API pages) in one step:");
|
|
22055
|
+
console.log(` executable-stories build-docs run.json --site-dir ${result.targetDir} [--openapi spec.json]`);
|
|
22056
|
+
console.log("");
|
|
22057
|
+
console.log("Later, pull template/design improvements without losing your content:");
|
|
22058
|
+
console.log(` executable-stories init-astro ${result.targetDir} --update`);
|
|
20906
22059
|
process.exit(EXIT_SUCCESS);
|
|
20907
22060
|
} catch (err) {
|
|
20908
22061
|
console.error(`Error: ${err.message}`);
|
|
20909
22062
|
process.exit(EXIT_USAGE);
|
|
20910
22063
|
}
|
|
20911
22064
|
}
|
|
22065
|
+
if (subcommand === "new") process.exit(runNew(args.slice(1)));
|
|
22066
|
+
if (subcommand === "check-links") process.exit(await runCheckLinks(args.slice(1)));
|
|
22067
|
+
if (subcommand === "import-openapi") process.exit(await runImportOpenApi(args.slice(1)));
|
|
22068
|
+
if (subcommand === "build-docs") process.exit(await runBuildDocs(args.slice(1)));
|
|
20912
22069
|
const { values, positionals } = parseArgs({
|
|
20913
22070
|
args: args.slice(1),
|
|
20914
22071
|
options: {
|
|
@@ -21004,7 +22161,7 @@ async function parseCliArgs(argv) {
|
|
|
21004
22161
|
}
|
|
21005
22162
|
const pluginConfig = await loadConfig(values["config"]);
|
|
21006
22163
|
const customFormatterNames = new Set(Object.keys(pluginConfig.formatters ?? {}));
|
|
21007
|
-
const builtInFormats = /* @__PURE__ */ new Set(["astro", "confluence", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html", "story-report-json"]);
|
|
22164
|
+
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"]);
|
|
21008
22165
|
const formatStr = values.format;
|
|
21009
22166
|
const allRequestedFormats = formatStr.split(",").map((f) => f.trim());
|
|
21010
22167
|
const builtInRequested = allRequestedFormats.filter((f) => builtInFormats.has(f));
|
|
@@ -21012,7 +22169,7 @@ async function parseCliArgs(argv) {
|
|
|
21012
22169
|
const unknownFormats = allRequestedFormats.filter((f) => !builtInFormats.has(f) && !customFormatterNames.has(f));
|
|
21013
22170
|
if (unknownFormats.length > 0) {
|
|
21014
22171
|
const knownCustom = customFormatterNames.size > 0 ? `, ${[...customFormatterNames].join(", ")}` : "";
|
|
21015
|
-
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}.`);
|
|
22172
|
+
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}.`);
|
|
21016
22173
|
process.exit(EXIT_USAGE);
|
|
21017
22174
|
}
|
|
21018
22175
|
const formats = builtInRequested;
|
|
@@ -21164,27 +22321,27 @@ async function readInput(args) {
|
|
|
21164
22321
|
if (args.stdin) {
|
|
21165
22322
|
return readStdin();
|
|
21166
22323
|
}
|
|
21167
|
-
const filePath =
|
|
21168
|
-
if (!
|
|
22324
|
+
const filePath = path14.resolve(args.inputFile);
|
|
22325
|
+
if (!fs13.existsSync(filePath)) {
|
|
21169
22326
|
console.error(`Error: File not found: ${filePath}`);
|
|
21170
22327
|
process.exit(EXIT_USAGE);
|
|
21171
22328
|
}
|
|
21172
|
-
return
|
|
22329
|
+
return fs13.readFileSync(filePath, "utf8");
|
|
21173
22330
|
}
|
|
21174
22331
|
function readFileInput(filePath) {
|
|
21175
|
-
const resolved =
|
|
21176
|
-
if (!
|
|
22332
|
+
const resolved = path14.resolve(filePath);
|
|
22333
|
+
if (!fs13.existsSync(resolved)) {
|
|
21177
22334
|
console.error(`Error: File not found: ${resolved}`);
|
|
21178
22335
|
process.exit(EXIT_USAGE);
|
|
21179
22336
|
}
|
|
21180
|
-
return
|
|
22337
|
+
return fs13.readFileSync(resolved, "utf8");
|
|
21181
22338
|
}
|
|
21182
22339
|
function readStdin() {
|
|
21183
|
-
return new Promise((
|
|
22340
|
+
return new Promise((resolve11, reject) => {
|
|
21184
22341
|
const chunks = [];
|
|
21185
22342
|
process.stdin.setEncoding("utf8");
|
|
21186
22343
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
21187
|
-
process.stdin.on("end", () =>
|
|
22344
|
+
process.stdin.on("end", () => resolve11(chunks.join("")));
|
|
21188
22345
|
process.stdin.on("error", reject);
|
|
21189
22346
|
});
|
|
21190
22347
|
}
|
|
@@ -21310,14 +22467,14 @@ function tryNormalizeRunFromText(text2, args) {
|
|
|
21310
22467
|
}
|
|
21311
22468
|
}
|
|
21312
22469
|
function listBaselineCandidates(currentFile, args) {
|
|
21313
|
-
const baselineDir =
|
|
21314
|
-
const currentResolved =
|
|
21315
|
-
if (!
|
|
22470
|
+
const baselineDir = path14.resolve(args.baselineDir ?? path14.dirname(currentFile));
|
|
22471
|
+
const currentResolved = path14.resolve(currentFile);
|
|
22472
|
+
if (!fs13.existsSync(baselineDir)) {
|
|
21316
22473
|
console.error(`Error: baseline directory not found: ${baselineDir}`);
|
|
21317
22474
|
process.exit(EXIT_USAGE);
|
|
21318
22475
|
}
|
|
21319
|
-
const entries =
|
|
21320
|
-
return entries.filter((entry) => entry.isFile()).map((entry) =>
|
|
22476
|
+
const entries = fs13.readdirSync(baselineDir, { withFileTypes: true });
|
|
22477
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => path14.join(baselineDir, entry.name)).filter((candidate) => path14.resolve(candidate) !== currentResolved).filter(
|
|
21321
22478
|
(candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
|
|
21322
22479
|
);
|
|
21323
22480
|
}
|
|
@@ -21325,14 +22482,14 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
|
|
|
21325
22482
|
const candidates = listBaselineCandidates(currentFile, args);
|
|
21326
22483
|
const comparable = [];
|
|
21327
22484
|
for (const candidate of candidates) {
|
|
21328
|
-
const run = tryNormalizeRunFromText(
|
|
22485
|
+
const run = tryNormalizeRunFromText(fs13.readFileSync(candidate, "utf8"), args);
|
|
21329
22486
|
if (run) {
|
|
21330
22487
|
comparable.push({ file: candidate, run });
|
|
21331
22488
|
}
|
|
21332
22489
|
}
|
|
21333
22490
|
if (comparable.length === 0) {
|
|
21334
22491
|
console.error(
|
|
21335
|
-
`Error: no compatible baseline files found in ${
|
|
22492
|
+
`Error: no compatible baseline files found in ${path14.resolve(args.baselineDir ?? path14.dirname(currentFile))}.`
|
|
21336
22493
|
);
|
|
21337
22494
|
process.exit(EXIT_USAGE);
|
|
21338
22495
|
}
|
|
@@ -21409,6 +22566,24 @@ async function main() {
|
|
|
21409
22566
|
console.log(output);
|
|
21410
22567
|
process.exit(EXIT_SUCCESS);
|
|
21411
22568
|
}
|
|
22569
|
+
if (args.subcommand === "watch") {
|
|
22570
|
+
if (!args.inputFile) {
|
|
22571
|
+
console.error("Error: watch requires an input file (the raw-run JSON the framework writes).");
|
|
22572
|
+
process.exit(EXIT_USAGE);
|
|
22573
|
+
}
|
|
22574
|
+
console.log(
|
|
22575
|
+
`Watching ${args.inputFile} \u2192 regenerating [${args.formats.join(", ")}] into ${args.outputDir}/ (Ctrl+C to stop)`
|
|
22576
|
+
);
|
|
22577
|
+
startWatch({
|
|
22578
|
+
input: args.inputFile,
|
|
22579
|
+
outputDir: args.outputDir,
|
|
22580
|
+
outputName: args.outputName,
|
|
22581
|
+
formats: args.formats,
|
|
22582
|
+
inputType: args.inputType === "canonical" ? "canonical" : "raw",
|
|
22583
|
+
synthesize: args.synthesizeStories
|
|
22584
|
+
});
|
|
22585
|
+
return;
|
|
22586
|
+
}
|
|
21412
22587
|
const text2 = await readInput(args);
|
|
21413
22588
|
if (args.inputType === "ndjson") {
|
|
21414
22589
|
if (args.subcommand === "validate") {
|
|
@@ -21452,9 +22627,9 @@ async function main() {
|
|
|
21452
22627
|
process.exit(EXIT_SCHEMA_VALIDATION);
|
|
21453
22628
|
}
|
|
21454
22629
|
if (args.emitCanonical) {
|
|
21455
|
-
const outPath =
|
|
21456
|
-
|
|
21457
|
-
|
|
22630
|
+
const outPath = path14.resolve(args.emitCanonical);
|
|
22631
|
+
fs13.mkdirSync(path14.dirname(outPath), { recursive: true });
|
|
22632
|
+
fs13.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
21458
22633
|
}
|
|
21459
22634
|
try {
|
|
21460
22635
|
const result = await generateReports(run, args);
|
|
@@ -21511,9 +22686,9 @@ ${msg}`);
|
|
|
21511
22686
|
}
|
|
21512
22687
|
const run = data;
|
|
21513
22688
|
if (args.emitCanonical) {
|
|
21514
|
-
const outPath =
|
|
21515
|
-
|
|
21516
|
-
|
|
22689
|
+
const outPath = path14.resolve(args.emitCanonical);
|
|
22690
|
+
fs13.mkdirSync(path14.dirname(outPath), { recursive: true });
|
|
22691
|
+
fs13.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
21517
22692
|
}
|
|
21518
22693
|
try {
|
|
21519
22694
|
const result = await generateReports(run, args);
|
|
@@ -21569,9 +22744,9 @@ ${msg}`);
|
|
|
21569
22744
|
process.exit(EXIT_CANONICAL_VALIDATION);
|
|
21570
22745
|
}
|
|
21571
22746
|
if (args.emitCanonical) {
|
|
21572
|
-
const outPath =
|
|
21573
|
-
|
|
21574
|
-
|
|
22747
|
+
const outPath = path14.resolve(args.emitCanonical);
|
|
22748
|
+
fs13.mkdirSync(path14.dirname(outPath), { recursive: true });
|
|
22749
|
+
fs13.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
|
|
21575
22750
|
}
|
|
21576
22751
|
try {
|
|
21577
22752
|
const result = await generateReports(canonical, args, droppedMissingStory);
|
|
@@ -21596,9 +22771,9 @@ function runCustomFormatters(run, customRequested, formatters, args) {
|
|
|
21596
22771
|
const ext = formatter.fileExtension ?? formatName;
|
|
21597
22772
|
const baseName = args.outputName ?? "report";
|
|
21598
22773
|
const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
|
|
21599
|
-
const filepath =
|
|
21600
|
-
|
|
21601
|
-
|
|
22774
|
+
const filepath = path14.join(outputDir, filename);
|
|
22775
|
+
fs13.mkdirSync(outputDir, { recursive: true });
|
|
22776
|
+
fs13.writeFileSync(filepath, content, "utf8");
|
|
21602
22777
|
console.log(`Generated: ${filepath}`);
|
|
21603
22778
|
} catch (err) {
|
|
21604
22779
|
console.error(`Error running custom formatter "${formatName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -21648,13 +22823,13 @@ async function dispatchNotifications(run, args) {
|
|
|
21648
22823
|
}
|
|
21649
22824
|
function runHistoryPipeline(run, args) {
|
|
21650
22825
|
if (!args.historyFile) return;
|
|
21651
|
-
const historyPath =
|
|
22826
|
+
const historyPath = path14.resolve(args.historyFile);
|
|
21652
22827
|
const store = loadHistory(
|
|
21653
22828
|
{ filePath: historyPath },
|
|
21654
22829
|
{
|
|
21655
22830
|
readFile: (p) => {
|
|
21656
22831
|
try {
|
|
21657
|
-
return
|
|
22832
|
+
return fs13.readFileSync(p, "utf8");
|
|
21658
22833
|
} catch {
|
|
21659
22834
|
return void 0;
|
|
21660
22835
|
}
|
|
@@ -21667,11 +22842,11 @@ function runHistoryPipeline(run, args) {
|
|
|
21667
22842
|
run,
|
|
21668
22843
|
maxRuns: args.maxHistoryRuns
|
|
21669
22844
|
});
|
|
21670
|
-
const dir =
|
|
21671
|
-
|
|
22845
|
+
const dir = path14.dirname(historyPath);
|
|
22846
|
+
fs13.mkdirSync(dir, { recursive: true });
|
|
21672
22847
|
saveHistory(
|
|
21673
22848
|
{ filePath: historyPath, store: updated },
|
|
21674
|
-
{ writeFile: (p, content) =>
|
|
22849
|
+
{ writeFile: (p, content) => fs13.writeFileSync(p, content, "utf8") }
|
|
21675
22850
|
);
|
|
21676
22851
|
let metricsCount = 0;
|
|
21677
22852
|
for (const testId of Object.keys(updated.tests)) {
|
|
@@ -21818,11 +22993,11 @@ function writeReviewReport(review, args) {
|
|
|
21818
22993
|
const outputDir = args.outputDir ?? "reports";
|
|
21819
22994
|
const baseName = args.outputName ?? "evidence-review";
|
|
21820
22995
|
const suffix = args.outputNameTimestamp ? `-${Math.floor(review.run.startedAtMs / 1e3)}` : "";
|
|
21821
|
-
|
|
21822
|
-
const mdPath =
|
|
21823
|
-
const htmlPath =
|
|
21824
|
-
|
|
21825
|
-
|
|
22996
|
+
fs13.mkdirSync(outputDir, { recursive: true });
|
|
22997
|
+
const mdPath = path14.join(outputDir, `${baseName}${suffix}.md`);
|
|
22998
|
+
const htmlPath = path14.join(outputDir, `${baseName}${suffix}.html`);
|
|
22999
|
+
fs13.writeFileSync(mdPath, markdown, "utf8");
|
|
23000
|
+
fs13.writeFileSync(htmlPath, html, "utf8");
|
|
21826
23001
|
return [mdPath, htmlPath];
|
|
21827
23002
|
}
|
|
21828
23003
|
function evaluateReviewGate(review, args) {
|
|
@@ -21868,9 +23043,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
|
|
|
21868
23043
|
function printCompareResult(result, args, startMs) {
|
|
21869
23044
|
const durationMs = Date.now() - startMs;
|
|
21870
23045
|
if (result.prSummary && args.prSummaryFile) {
|
|
21871
|
-
const outputPath =
|
|
21872
|
-
|
|
21873
|
-
|
|
23046
|
+
const outputPath = path14.resolve(args.prSummaryFile);
|
|
23047
|
+
fs13.mkdirSync(path14.dirname(outputPath), { recursive: true });
|
|
23048
|
+
fs13.writeFileSync(outputPath, result.prSummary, "utf8");
|
|
21874
23049
|
}
|
|
21875
23050
|
if (args.jsonSummary) {
|
|
21876
23051
|
console.log(
|
|
@@ -21961,7 +23136,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
21961
23136
|
console.error("Error: missing ADF file argument. Run with --help for usage.");
|
|
21962
23137
|
process.exit(EXIT_USAGE);
|
|
21963
23138
|
}
|
|
21964
|
-
if (!
|
|
23139
|
+
if (!fs13.existsSync(inputFile)) {
|
|
21965
23140
|
console.error(`Error: file not found: ${inputFile}`);
|
|
21966
23141
|
process.exit(EXIT_USAGE);
|
|
21967
23142
|
}
|
|
@@ -21989,7 +23164,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
21989
23164
|
console.error("Error: --title is required when creating a new page");
|
|
21990
23165
|
process.exit(EXIT_USAGE);
|
|
21991
23166
|
}
|
|
21992
|
-
const adf =
|
|
23167
|
+
const adf = fs13.readFileSync(path14.resolve(inputFile), "utf8");
|
|
21993
23168
|
if (dryRun) {
|
|
21994
23169
|
console.log(
|
|
21995
23170
|
JSON.stringify(
|
|
@@ -22068,7 +23243,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
22068
23243
|
console.error("Error: missing ADF file argument. Run with --help for usage.");
|
|
22069
23244
|
process.exit(EXIT_USAGE);
|
|
22070
23245
|
}
|
|
22071
|
-
if (!
|
|
23246
|
+
if (!fs13.existsSync(inputFile)) {
|
|
22072
23247
|
console.error(`Error: file not found: ${inputFile}`);
|
|
22073
23248
|
process.exit(EXIT_USAGE);
|
|
22074
23249
|
}
|
|
@@ -22095,7 +23270,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
22095
23270
|
process.exit(EXIT_USAGE);
|
|
22096
23271
|
}
|
|
22097
23272
|
const mode = modeRaw;
|
|
22098
|
-
const adf =
|
|
23273
|
+
const adf = fs13.readFileSync(path14.resolve(inputFile), "utf8");
|
|
22099
23274
|
if (dryRun) {
|
|
22100
23275
|
console.log(
|
|
22101
23276
|
JSON.stringify(
|
|
@@ -22136,6 +23311,142 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
22136
23311
|
process.exit(EXIT_GENERATION);
|
|
22137
23312
|
}
|
|
22138
23313
|
}
|
|
23314
|
+
function runNew(rawArgs) {
|
|
23315
|
+
const { values, positionals } = parseArgs({
|
|
23316
|
+
args: rawArgs,
|
|
23317
|
+
options: { dir: { type: "string" }, force: { type: "boolean", default: false } },
|
|
23318
|
+
allowPositionals: true,
|
|
23319
|
+
strict: true
|
|
23320
|
+
});
|
|
23321
|
+
const template = positionals[0];
|
|
23322
|
+
const name = positionals.slice(1).join(" ");
|
|
23323
|
+
if (!template) {
|
|
23324
|
+
console.error(`Usage: executable-stories new <template> "<name>" [--dir <docs-dir>] [--force]`);
|
|
23325
|
+
console.error(`Templates: ${TEMPLATES.join(", ")}`);
|
|
23326
|
+
return EXIT_USAGE;
|
|
23327
|
+
}
|
|
23328
|
+
try {
|
|
23329
|
+
const result = scaffoldDoc({
|
|
23330
|
+
template,
|
|
23331
|
+
name,
|
|
23332
|
+
baseDir: values.dir,
|
|
23333
|
+
force: values.force
|
|
23334
|
+
});
|
|
23335
|
+
console.log(`Created ${result.template}: ${result.path}`);
|
|
23336
|
+
console.log(` Title: ${result.title}`);
|
|
23337
|
+
console.log("");
|
|
23338
|
+
console.log("Next: fill in the content and link verifying stories in `verifiedBy`.");
|
|
23339
|
+
return EXIT_SUCCESS;
|
|
23340
|
+
} catch (err) {
|
|
23341
|
+
console.error(`Error: ${err.message}`);
|
|
23342
|
+
return EXIT_USAGE;
|
|
23343
|
+
}
|
|
23344
|
+
}
|
|
23345
|
+
async function runCheckLinks(rawArgs) {
|
|
23346
|
+
const { values, positionals } = parseArgs({
|
|
23347
|
+
args: rawArgs,
|
|
23348
|
+
options: {
|
|
23349
|
+
external: { type: "boolean", default: false },
|
|
23350
|
+
json: { type: "boolean", default: false }
|
|
23351
|
+
},
|
|
23352
|
+
allowPositionals: true,
|
|
23353
|
+
strict: true
|
|
23354
|
+
});
|
|
23355
|
+
try {
|
|
23356
|
+
const report = await checkLinks({
|
|
23357
|
+
target: positionals[0] ?? ".",
|
|
23358
|
+
checkExternal: values.external
|
|
23359
|
+
});
|
|
23360
|
+
console.log(values.json ? JSON.stringify(report, null, 2) : formatLinkReport(report));
|
|
23361
|
+
return report.brokenCount > 0 ? EXIT_GENERATION : EXIT_SUCCESS;
|
|
23362
|
+
} catch (err) {
|
|
23363
|
+
console.error(`Error: ${err.message}`);
|
|
23364
|
+
return EXIT_USAGE;
|
|
23365
|
+
}
|
|
23366
|
+
}
|
|
23367
|
+
async function runImportOpenApi(rawArgs) {
|
|
23368
|
+
const { values, positionals } = parseArgs({
|
|
23369
|
+
args: rawArgs,
|
|
23370
|
+
options: {
|
|
23371
|
+
"output-dir": { type: "string" },
|
|
23372
|
+
run: { type: "string" },
|
|
23373
|
+
force: { type: "boolean", default: false }
|
|
23374
|
+
},
|
|
23375
|
+
allowPositionals: true,
|
|
23376
|
+
strict: true
|
|
23377
|
+
});
|
|
23378
|
+
const spec = positionals[0];
|
|
23379
|
+
if (!spec) {
|
|
23380
|
+
console.error(`Usage: executable-stories import-openapi <spec.json|yaml> [--output-dir <dir>] [--run <story-report.json>] [--force]`);
|
|
23381
|
+
return EXIT_USAGE;
|
|
23382
|
+
}
|
|
23383
|
+
try {
|
|
23384
|
+
const result = await importOpenApi({
|
|
23385
|
+
specPath: spec,
|
|
23386
|
+
outputDir: values["output-dir"],
|
|
23387
|
+
runFile: values.run,
|
|
23388
|
+
force: values.force
|
|
23389
|
+
});
|
|
23390
|
+
console.log(`Generated ${result.pageCount} API page(s) at ${result.outputDir}`);
|
|
23391
|
+
console.log(` Covered endpoints: ${result.coveredCount} / ${result.endpointCount}`);
|
|
23392
|
+
if (result.uncoveredCount > 0) {
|
|
23393
|
+
console.log(` \u26A0 ${result.uncoveredCount} endpoint(s) have no verifying story`);
|
|
23394
|
+
}
|
|
23395
|
+
return EXIT_SUCCESS;
|
|
23396
|
+
} catch (err) {
|
|
23397
|
+
console.error(`Error: ${err.message}`);
|
|
23398
|
+
return EXIT_USAGE;
|
|
23399
|
+
}
|
|
23400
|
+
}
|
|
23401
|
+
async function runBuildDocs(rawArgs) {
|
|
23402
|
+
const { values, positionals } = parseArgs({
|
|
23403
|
+
args: rawArgs,
|
|
23404
|
+
options: {
|
|
23405
|
+
"site-dir": { type: "string" },
|
|
23406
|
+
openapi: { type: "string" },
|
|
23407
|
+
"no-synthesize-stories": { type: "boolean", default: false }
|
|
23408
|
+
},
|
|
23409
|
+
allowPositionals: true,
|
|
23410
|
+
strict: true
|
|
23411
|
+
});
|
|
23412
|
+
const rawRunPath = positionals[0];
|
|
23413
|
+
if (!rawRunPath) {
|
|
23414
|
+
console.error(
|
|
23415
|
+
`Usage: executable-stories build-docs <raw-run.json> [--site-dir <dir>] [--openapi <spec>]`
|
|
23416
|
+
);
|
|
23417
|
+
return EXIT_USAGE;
|
|
23418
|
+
}
|
|
23419
|
+
try {
|
|
23420
|
+
const result = await buildDocs({
|
|
23421
|
+
rawRunPath,
|
|
23422
|
+
siteDir: values["site-dir"] ?? ".",
|
|
23423
|
+
openapiPath: values.openapi,
|
|
23424
|
+
synthesizeStories: !values["no-synthesize-stories"]
|
|
23425
|
+
});
|
|
23426
|
+
console.log(`\u2713 Living docs generated in ${result.siteDir}`);
|
|
23427
|
+
console.log(` \u2022 Explorer data \u2192 public/stories/story-report.json`);
|
|
23428
|
+
console.log(` \u2022 Story pages \u2192 src/content/docs/stories`);
|
|
23429
|
+
if (result.bundledAssets > 0) {
|
|
23430
|
+
console.log(` \u2022 Bundled assets \u2192 public/stories/assets (${result.bundledAssets})`);
|
|
23431
|
+
}
|
|
23432
|
+
if (result.apiPages > 0) {
|
|
23433
|
+
console.log(` \u2022 API pages \u2192 src/content/docs/api (${result.apiPages})`);
|
|
23434
|
+
}
|
|
23435
|
+
const rel = path14.relative(process.cwd(), result.siteDir) || ".";
|
|
23436
|
+
console.log(`
|
|
23437
|
+
Preview: cd ${rel} && npm run dev`);
|
|
23438
|
+
return EXIT_SUCCESS;
|
|
23439
|
+
} catch (err) {
|
|
23440
|
+
if (err instanceof BuildDocsError) {
|
|
23441
|
+
console.error(err.message);
|
|
23442
|
+
if (err.kind === "schema") return EXIT_SCHEMA_VALIDATION;
|
|
23443
|
+
if (err.kind === "generation") return EXIT_GENERATION;
|
|
23444
|
+
return EXIT_USAGE;
|
|
23445
|
+
}
|
|
23446
|
+
console.error(`Error: ${err.message}`);
|
|
23447
|
+
return EXIT_USAGE;
|
|
23448
|
+
}
|
|
23449
|
+
}
|
|
22139
23450
|
main().catch((err) => {
|
|
22140
23451
|
console.error(err);
|
|
22141
23452
|
process.exit(EXIT_USAGE);
|