executable-stories-formatters 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +305 -158
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +8 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
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 fs16 from "fs";
|
|
6
|
+
import * as path17 from "path";
|
|
7
7
|
|
|
8
8
|
// src/validation/schema-validator.ts
|
|
9
9
|
import Ajv from "ajv/dist/2020.js";
|
|
@@ -588,17 +588,17 @@ function validateRawRun(data) {
|
|
|
588
588
|
return { valid: true, errors: [] };
|
|
589
589
|
}
|
|
590
590
|
const errors = (validate.errors ?? []).map((err) => {
|
|
591
|
-
const
|
|
591
|
+
const path18 = err.instancePath || "/";
|
|
592
592
|
const message = err.message ?? "unknown error";
|
|
593
593
|
if (err.keyword === "additionalProperties") {
|
|
594
594
|
const extra = err.params.additionalProperty;
|
|
595
|
-
return `${
|
|
595
|
+
return `${path18}: ${message} \u2014 '${extra}'`;
|
|
596
596
|
}
|
|
597
597
|
if (err.keyword === "enum") {
|
|
598
598
|
const allowed = err.params.allowedValues;
|
|
599
|
-
return `${
|
|
599
|
+
return `${path18}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
|
|
600
600
|
}
|
|
601
|
-
return `${
|
|
601
|
+
return `${path18}: ${message}`;
|
|
602
602
|
});
|
|
603
603
|
return { valid: false, errors };
|
|
604
604
|
}
|
|
@@ -15864,7 +15864,8 @@ var MarkdownFormatter = class {
|
|
|
15864
15864
|
includeSourceLinks: options.includeSourceLinks ?? true,
|
|
15865
15865
|
customRenderers: options.customRenderers,
|
|
15866
15866
|
scenarioAnchor: options.scenarioAnchor,
|
|
15867
|
-
scenarioBadge: options.scenarioBadge
|
|
15867
|
+
scenarioBadge: options.scenarioBadge,
|
|
15868
|
+
scenarioNoteLink: options.scenarioNoteLink
|
|
15868
15869
|
};
|
|
15869
15870
|
}
|
|
15870
15871
|
/**
|
|
@@ -16075,6 +16076,10 @@ var MarkdownFormatter = class {
|
|
|
16075
16076
|
if (badge2) {
|
|
16076
16077
|
lines.push(badge2);
|
|
16077
16078
|
}
|
|
16079
|
+
const noteLink = this.options.scenarioNoteLink?.(tc);
|
|
16080
|
+
if (noteLink) {
|
|
16081
|
+
lines.push(noteLink);
|
|
16082
|
+
}
|
|
16078
16083
|
if (this.options.includeSourceLinks && this.options.permalinkBaseUrl && tc.sourceFile !== "unknown") {
|
|
16079
16084
|
const permalink = this.buildPermalink(tc);
|
|
16080
16085
|
lines.push(`Source: [${tc.sourceFile}](${permalink})`);
|
|
@@ -16490,14 +16495,14 @@ var TraceabilityMatrixFormatter = class {
|
|
|
16490
16495
|
lines.push("");
|
|
16491
16496
|
lines.push(`Status: ${renderRequirementStatus(req.status)}`);
|
|
16492
16497
|
if (req.covers.length > 0) {
|
|
16493
|
-
lines.push(`Covers: ${req.covers.map((
|
|
16498
|
+
lines.push(`Covers: ${req.covers.map((path18) => `\`${path18}\``).join(", ")}`);
|
|
16494
16499
|
}
|
|
16495
16500
|
lines.push("");
|
|
16496
16501
|
lines.push("| Status | Scenario | Source | Covers |");
|
|
16497
16502
|
lines.push("| --- | --- | --- | --- |");
|
|
16498
16503
|
for (const scenario of req.scenarios) {
|
|
16499
16504
|
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
16500
|
-
const covers = scenario.covers.length > 0 ? scenario.covers.map((
|
|
16505
|
+
const covers = scenario.covers.length > 0 ? scenario.covers.map((path18) => `\`${path18}\``).join(", ") : "";
|
|
16501
16506
|
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
|
|
16502
16507
|
}
|
|
16503
16508
|
lines.push("");
|
|
@@ -16599,8 +16604,8 @@ function extractFeatureName(testCases, uri) {
|
|
|
16599
16604
|
return tc.titlePath[0];
|
|
16600
16605
|
}
|
|
16601
16606
|
}
|
|
16602
|
-
const
|
|
16603
|
-
return
|
|
16607
|
+
const basename5 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
|
|
16608
|
+
return basename5.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
16604
16609
|
}
|
|
16605
16610
|
function synthesizeFeature(uri, testCases) {
|
|
16606
16611
|
const featureName = extractFeatureName(testCases, uri);
|
|
@@ -17212,8 +17217,8 @@ function extractDocAttachments(step) {
|
|
|
17212
17217
|
}
|
|
17213
17218
|
return attachments;
|
|
17214
17219
|
}
|
|
17215
|
-
function guessMediaType(
|
|
17216
|
-
const lower =
|
|
17220
|
+
function guessMediaType(path18) {
|
|
17221
|
+
const lower = path18.toLowerCase();
|
|
17217
17222
|
if (lower.endsWith(".png")) return "image/png";
|
|
17218
17223
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
17219
17224
|
if (lower.endsWith(".gif")) return "image/gif";
|
|
@@ -20747,18 +20752,18 @@ function deriveChangeType(tags) {
|
|
|
20747
20752
|
}
|
|
20748
20753
|
return "unknown";
|
|
20749
20754
|
}
|
|
20750
|
-
function extensionOf(
|
|
20751
|
-
const base =
|
|
20755
|
+
function extensionOf(path18) {
|
|
20756
|
+
const base = path18.split("/").pop() ?? path18;
|
|
20752
20757
|
const dot = base.lastIndexOf(".");
|
|
20753
20758
|
return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
|
|
20754
20759
|
}
|
|
20755
|
-
function isTestFile(
|
|
20756
|
-
return TEST_INFIX.test(
|
|
20760
|
+
function isTestFile(path18) {
|
|
20761
|
+
return TEST_INFIX.test(path18);
|
|
20757
20762
|
}
|
|
20758
|
-
function isReviewableSource(
|
|
20759
|
-
if (isTestFile(
|
|
20760
|
-
if (
|
|
20761
|
-
return CODE_EXTENSIONS.has(extensionOf(
|
|
20763
|
+
function isReviewableSource(path18) {
|
|
20764
|
+
if (isTestFile(path18)) return false;
|
|
20765
|
+
if (path18.endsWith(".d.ts")) return false;
|
|
20766
|
+
return CODE_EXTENSIONS.has(extensionOf(path18));
|
|
20762
20767
|
}
|
|
20763
20768
|
function testBaseKey(testFile) {
|
|
20764
20769
|
return testFile.replace(TEST_INFIX, "");
|
|
@@ -20862,7 +20867,7 @@ function toClaim(testCase, changedSourcePaths) {
|
|
|
20862
20867
|
const { strength, reasons } = gradeEvidence(testCase, audience);
|
|
20863
20868
|
const key = testBaseKey(testCase.sourceFile);
|
|
20864
20869
|
const coversFiles = changedSourcePaths.filter(
|
|
20865
|
-
(
|
|
20870
|
+
(path18) => sourceBaseKey(path18) === key
|
|
20866
20871
|
);
|
|
20867
20872
|
return {
|
|
20868
20873
|
id: testCase.id,
|
|
@@ -21727,7 +21732,8 @@ var ReportGenerator = class {
|
|
|
21727
21732
|
traceUrlTemplate: options.astro?.markdown?.traceUrlTemplate,
|
|
21728
21733
|
customRenderers: options.astro?.markdown?.customRenderers,
|
|
21729
21734
|
scenarioAnchor: options.astro?.markdown?.scenarioAnchor,
|
|
21730
|
-
scenarioBadge: options.astro?.markdown?.scenarioBadge
|
|
21735
|
+
scenarioBadge: options.astro?.markdown?.scenarioBadge,
|
|
21736
|
+
scenarioNoteLink: options.astro?.markdown?.scenarioNoteLink
|
|
21731
21737
|
}
|
|
21732
21738
|
},
|
|
21733
21739
|
assetMode: options.assetMode ?? "none",
|
|
@@ -22064,7 +22070,13 @@ function copyDirRecursive(src, dest, onFile, baseSrc = src) {
|
|
|
22064
22070
|
// src/scaffold-doc.ts
|
|
22065
22071
|
import * as fs10 from "fs";
|
|
22066
22072
|
import * as path11 from "path";
|
|
22067
|
-
var TEMPLATES = [
|
|
22073
|
+
var TEMPLATES = [
|
|
22074
|
+
"adr",
|
|
22075
|
+
"runbook",
|
|
22076
|
+
"decision-log",
|
|
22077
|
+
"incident",
|
|
22078
|
+
"scenario-note"
|
|
22079
|
+
];
|
|
22068
22080
|
function slugify3(input) {
|
|
22069
22081
|
return input.toLowerCase().trim().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
|
|
22070
22082
|
}
|
|
@@ -22193,6 +22205,30 @@ _How it was fixed._
|
|
|
22193
22205
|
|
|
22194
22206
|
- [ ] Add a regression story and link it in \`verifiedBy\` so a silent recurrence
|
|
22195
22207
|
becomes a failing badge.
|
|
22208
|
+
`
|
|
22209
|
+
},
|
|
22210
|
+
"scenario-note": {
|
|
22211
|
+
subdir: "notes",
|
|
22212
|
+
filename: (_slug, ctx) => ctx.scenarioId ?? ctx.slug,
|
|
22213
|
+
content: (ctx) => `---
|
|
22214
|
+
title: 'Business context \u2014 ${ctx.name}'
|
|
22215
|
+
description: 'Stakeholder context for ${ctx.name}'
|
|
22216
|
+
scenarioId: ${ctx.scenarioId}
|
|
22217
|
+
# Link this note back to the scenario it explains so the badge and explorer stay aligned.
|
|
22218
|
+
verifiedBy: [${ctx.scenarioId}]
|
|
22219
|
+
---
|
|
22220
|
+
|
|
22221
|
+
This page is hand-written commentary for a generated scenario. It is never
|
|
22222
|
+
overwritten by \`build-docs\`.
|
|
22223
|
+
|
|
22224
|
+
## Why this behavior matters
|
|
22225
|
+
|
|
22226
|
+
_Describe the business rule, policy, customer promise, or operational nuance._
|
|
22227
|
+
|
|
22228
|
+
## Caveats
|
|
22229
|
+
|
|
22230
|
+
- _What readers should know when this scenario passes_
|
|
22231
|
+
- _Any assumptions, exclusions, or follow-up links_
|
|
22196
22232
|
`
|
|
22197
22233
|
}
|
|
22198
22234
|
};
|
|
@@ -22211,10 +22247,15 @@ function scaffoldDoc(options) {
|
|
|
22211
22247
|
const today = options.today ?? /* @__PURE__ */ new Date();
|
|
22212
22248
|
const name = (options.name ?? "").trim() || defaultName(template);
|
|
22213
22249
|
const slug2 = slugify3(name);
|
|
22250
|
+
const scenarioId = normalizeScenarioId(options.scenarioId);
|
|
22214
22251
|
const dir = path11.join(baseDir, spec.subdir);
|
|
22252
|
+
if (template === "scenario-note" && !scenarioId) {
|
|
22253
|
+
throw new Error(`Template "scenario-note" requires --scenario-id.`);
|
|
22254
|
+
}
|
|
22215
22255
|
const ctx = {
|
|
22216
22256
|
name,
|
|
22217
22257
|
slug: slug2,
|
|
22258
|
+
scenarioId,
|
|
22218
22259
|
isoDate: isoDate(today),
|
|
22219
22260
|
seq: nextSeq(dir)
|
|
22220
22261
|
};
|
|
@@ -22239,6 +22280,8 @@ function defaultName(template) {
|
|
|
22239
22280
|
return "Decisions";
|
|
22240
22281
|
case "incident":
|
|
22241
22282
|
return "Untitled incident";
|
|
22283
|
+
case "scenario-note":
|
|
22284
|
+
return "Untitled scenario note";
|
|
22242
22285
|
}
|
|
22243
22286
|
}
|
|
22244
22287
|
function titleFor2(template, ctx) {
|
|
@@ -22251,12 +22294,43 @@ function titleFor2(template, ctx) {
|
|
|
22251
22294
|
return `Decision log \u2014 ${ctx.name}`;
|
|
22252
22295
|
case "incident":
|
|
22253
22296
|
return `Incident \u2014 ${ctx.name}`;
|
|
22297
|
+
case "scenario-note":
|
|
22298
|
+
return `Business context \u2014 ${ctx.name}`;
|
|
22299
|
+
}
|
|
22300
|
+
}
|
|
22301
|
+
function normalizeScenarioId(input) {
|
|
22302
|
+
const value = input?.trim();
|
|
22303
|
+
if (!value) return void 0;
|
|
22304
|
+
if (value.includes("/") || value.includes("\\")) {
|
|
22305
|
+
throw new Error(`scenarioId must not contain path separators.`);
|
|
22254
22306
|
}
|
|
22307
|
+
return value;
|
|
22255
22308
|
}
|
|
22256
22309
|
|
|
22257
22310
|
// src/check-links.ts
|
|
22311
|
+
import * as fs12 from "fs";
|
|
22312
|
+
import * as path13 from "path";
|
|
22313
|
+
|
|
22314
|
+
// src/utils/markdown-files.ts
|
|
22258
22315
|
import * as fs11 from "fs";
|
|
22259
22316
|
import * as path12 from "path";
|
|
22317
|
+
function collectMarkdownFiles(target) {
|
|
22318
|
+
if (!fs11.existsSync(target)) return [];
|
|
22319
|
+
if (fs11.statSync(target).isFile()) return [target];
|
|
22320
|
+
const out = [];
|
|
22321
|
+
const walk = (dir) => {
|
|
22322
|
+
for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
|
|
22323
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
22324
|
+
const full = path12.join(dir, entry.name);
|
|
22325
|
+
if (entry.isDirectory()) walk(full);
|
|
22326
|
+
else if (/\.mdx?$/u.test(entry.name)) out.push(full);
|
|
22327
|
+
}
|
|
22328
|
+
};
|
|
22329
|
+
walk(target);
|
|
22330
|
+
return out;
|
|
22331
|
+
}
|
|
22332
|
+
|
|
22333
|
+
// src/check-links.ts
|
|
22260
22334
|
function stripCode(markdown) {
|
|
22261
22335
|
let out = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
|
|
22262
22336
|
out = out.replace(/(`+)(?:(?!\1).)+\1/g, "");
|
|
@@ -22286,34 +22360,19 @@ function classifyLink(link2) {
|
|
|
22286
22360
|
function resolutionCandidates(fromFile, link2) {
|
|
22287
22361
|
const withoutAnchor = link2.split("#")[0];
|
|
22288
22362
|
if (!withoutAnchor) return [];
|
|
22289
|
-
const base =
|
|
22363
|
+
const base = path13.resolve(path13.dirname(fromFile), withoutAnchor);
|
|
22290
22364
|
const candidates = [base];
|
|
22291
|
-
if (!
|
|
22365
|
+
if (!path13.extname(base)) {
|
|
22292
22366
|
candidates.push(`${base}.md`, `${base}.mdx`);
|
|
22293
|
-
candidates.push(
|
|
22367
|
+
candidates.push(path13.join(base, "index.md"), path13.join(base, "index.mdx"));
|
|
22294
22368
|
}
|
|
22295
22369
|
return candidates;
|
|
22296
22370
|
}
|
|
22297
22371
|
function resolvesOnDisk(fromFile, link2) {
|
|
22298
22372
|
return resolutionCandidates(fromFile, link2).some(
|
|
22299
|
-
(candidate) =>
|
|
22373
|
+
(candidate) => fs12.existsSync(candidate) && fs12.statSync(candidate).isFile()
|
|
22300
22374
|
);
|
|
22301
22375
|
}
|
|
22302
|
-
function collectDocFiles(target) {
|
|
22303
|
-
const stat = fs11.statSync(target);
|
|
22304
|
-
if (stat.isFile()) return [target];
|
|
22305
|
-
const out = [];
|
|
22306
|
-
const walk = (dir) => {
|
|
22307
|
-
for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
|
|
22308
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
22309
|
-
const full = path12.join(dir, entry.name);
|
|
22310
|
-
if (entry.isDirectory()) walk(full);
|
|
22311
|
-
else if (/\.mdx?$/.test(entry.name)) out.push(full);
|
|
22312
|
-
}
|
|
22313
|
-
};
|
|
22314
|
-
walk(target);
|
|
22315
|
-
return out;
|
|
22316
|
-
}
|
|
22317
22376
|
async function isExternalAlive(url, timeoutMs) {
|
|
22318
22377
|
const attempt = async (method) => {
|
|
22319
22378
|
const controller = new AbortController();
|
|
@@ -22337,17 +22396,17 @@ async function isExternalAlive(url, timeoutMs) {
|
|
|
22337
22396
|
}
|
|
22338
22397
|
async function checkLinks(options) {
|
|
22339
22398
|
const { target, checkExternal = false, externalTimeoutMs = 8e3 } = options;
|
|
22340
|
-
if (!
|
|
22399
|
+
if (!fs12.existsSync(target)) {
|
|
22341
22400
|
throw new Error(`Path not found: ${target}`);
|
|
22342
22401
|
}
|
|
22343
|
-
const files =
|
|
22402
|
+
const files = collectMarkdownFiles(target);
|
|
22344
22403
|
const broken = [];
|
|
22345
22404
|
let linksChecked = 0;
|
|
22346
22405
|
let externalChecked = 0;
|
|
22347
22406
|
let skipped = 0;
|
|
22348
22407
|
const externalCache = /* @__PURE__ */ new Map();
|
|
22349
22408
|
for (const file of files) {
|
|
22350
|
-
const content =
|
|
22409
|
+
const content = fs12.readFileSync(file, "utf8");
|
|
22351
22410
|
for (const link2 of extractLinks(content)) {
|
|
22352
22411
|
const kind = classifyLink(link2);
|
|
22353
22412
|
if (kind === "anchor" || kind === "mail" || kind === "root") {
|
|
@@ -22403,8 +22462,8 @@ function formatLinkReport(report) {
|
|
|
22403
22462
|
}
|
|
22404
22463
|
|
|
22405
22464
|
// src/import-openapi.ts
|
|
22406
|
-
import * as
|
|
22407
|
-
import * as
|
|
22465
|
+
import * as fs13 from "fs";
|
|
22466
|
+
import * as path14 from "path";
|
|
22408
22467
|
import { parse as parseYamlString } from "yaml";
|
|
22409
22468
|
var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head"];
|
|
22410
22469
|
function parseYaml(raw, specPath) {
|
|
@@ -22417,9 +22476,9 @@ function parseYaml(raw, specPath) {
|
|
|
22417
22476
|
}
|
|
22418
22477
|
}
|
|
22419
22478
|
function parseSpec(specPath) {
|
|
22420
|
-
if (!
|
|
22421
|
-
const raw =
|
|
22422
|
-
const ext =
|
|
22479
|
+
if (!fs13.existsSync(specPath)) throw new Error(`Spec not found: ${specPath}`);
|
|
22480
|
+
const raw = fs13.readFileSync(specPath, "utf8");
|
|
22481
|
+
const ext = path14.extname(specPath).toLowerCase();
|
|
22423
22482
|
if (ext === ".json") return JSON.parse(raw);
|
|
22424
22483
|
if (ext === ".yaml" || ext === ".yml") return parseYaml(raw, specPath);
|
|
22425
22484
|
try {
|
|
@@ -22450,8 +22509,8 @@ function extractEndpoints(spec) {
|
|
|
22450
22509
|
}
|
|
22451
22510
|
function loadScenarios(runFile) {
|
|
22452
22511
|
if (!runFile) return [];
|
|
22453
|
-
if (!
|
|
22454
|
-
const report = JSON.parse(
|
|
22512
|
+
if (!fs13.existsSync(runFile)) throw new Error(`Run file not found: ${runFile}`);
|
|
22513
|
+
const report = JSON.parse(fs13.readFileSync(runFile, "utf8"));
|
|
22455
22514
|
return (report.features ?? []).flatMap((f) => f.scenarios ?? []);
|
|
22456
22515
|
}
|
|
22457
22516
|
function endpointRefs(endpoint) {
|
|
@@ -22558,25 +22617,25 @@ async function importOpenApi(options) {
|
|
|
22558
22617
|
list.push(item);
|
|
22559
22618
|
groups.set(item.endpoint.tag, list);
|
|
22560
22619
|
}
|
|
22561
|
-
const outputDir = options.outputDir ??
|
|
22562
|
-
if (
|
|
22563
|
-
const entries =
|
|
22620
|
+
const outputDir = options.outputDir ?? path14.join("src", "content", "docs", "api");
|
|
22621
|
+
if (fs13.existsSync(outputDir) && !options.force) {
|
|
22622
|
+
const entries = fs13.readdirSync(outputDir);
|
|
22564
22623
|
if (entries.length > 0) {
|
|
22565
22624
|
throw new Error(`Output directory "${outputDir}" is not empty. Use --force to overwrite.`);
|
|
22566
22625
|
}
|
|
22567
22626
|
}
|
|
22568
|
-
|
|
22627
|
+
fs13.mkdirSync(outputDir, { recursive: true });
|
|
22569
22628
|
const coveredCount = coverage.filter((c) => c.status === "covered").length;
|
|
22570
22629
|
const uncoveredCount = coverage.filter((c) => c.status === "uncovered").length;
|
|
22571
|
-
|
|
22572
|
-
|
|
22630
|
+
fs13.writeFileSync(
|
|
22631
|
+
path14.join(outputDir, "index.mdx"),
|
|
22573
22632
|
renderIndex(groups, hasRun, { endpointCount: endpoints.length, coveredCount, uncoveredCount }),
|
|
22574
22633
|
"utf8"
|
|
22575
22634
|
);
|
|
22576
22635
|
for (const [tag, rows] of groups) {
|
|
22577
|
-
const dir =
|
|
22578
|
-
|
|
22579
|
-
|
|
22636
|
+
const dir = path14.join(outputDir, slug(tag));
|
|
22637
|
+
fs13.mkdirSync(dir, { recursive: true });
|
|
22638
|
+
fs13.writeFileSync(path14.join(dir, "index.mdx"), renderTagPage(tag, rows, hasRun), "utf8");
|
|
22580
22639
|
}
|
|
22581
22640
|
return {
|
|
22582
22641
|
outputDir,
|
|
@@ -22588,8 +22647,8 @@ async function importOpenApi(options) {
|
|
|
22588
22647
|
}
|
|
22589
22648
|
|
|
22590
22649
|
// src/build-docs.ts
|
|
22591
|
-
import * as
|
|
22592
|
-
import * as
|
|
22650
|
+
import * as fs15 from "fs";
|
|
22651
|
+
import * as path16 from "path";
|
|
22593
22652
|
|
|
22594
22653
|
// src/scenario-links.ts
|
|
22595
22654
|
function scenarioAnchor(title) {
|
|
@@ -22685,6 +22744,58 @@ ${body.join("\n")}
|
|
|
22685
22744
|
`;
|
|
22686
22745
|
}
|
|
22687
22746
|
|
|
22747
|
+
// src/notes-index.ts
|
|
22748
|
+
import * as fs14 from "fs";
|
|
22749
|
+
import * as path15 from "path";
|
|
22750
|
+
import { slug as githubSlug } from "github-slugger";
|
|
22751
|
+
import { parse as parseYaml2 } from "yaml";
|
|
22752
|
+
function buildScenarioNotesIndex(notesDir) {
|
|
22753
|
+
const entries = collectMarkdownFiles(notesDir).map((filePath) => readScenarioNote(filePath, notesDir)).filter((entry) => entry !== null).sort((a, b) => a.slug.localeCompare(b.slug));
|
|
22754
|
+
return {
|
|
22755
|
+
schemaVersion: "1.0",
|
|
22756
|
+
notes: entries
|
|
22757
|
+
};
|
|
22758
|
+
}
|
|
22759
|
+
function writeNotesIndex(index, outPath) {
|
|
22760
|
+
fs14.mkdirSync(path15.dirname(outPath), { recursive: true });
|
|
22761
|
+
fs14.writeFileSync(outPath, JSON.stringify(index, null, 2), "utf8");
|
|
22762
|
+
return index;
|
|
22763
|
+
}
|
|
22764
|
+
function notesByScenarioId(index) {
|
|
22765
|
+
const map = /* @__PURE__ */ new Map();
|
|
22766
|
+
for (const note of index.notes) {
|
|
22767
|
+
if (!map.has(note.scenarioId)) map.set(note.scenarioId, note);
|
|
22768
|
+
}
|
|
22769
|
+
return map;
|
|
22770
|
+
}
|
|
22771
|
+
function noteHref(note) {
|
|
22772
|
+
return `/notes/${note.slug}/`;
|
|
22773
|
+
}
|
|
22774
|
+
function noteLinkMarkdown(note) {
|
|
22775
|
+
return `[Business context \u2192](${noteHref(note)})`;
|
|
22776
|
+
}
|
|
22777
|
+
function readScenarioNote(filePath, notesDir) {
|
|
22778
|
+
const relative5 = path15.relative(notesDir, filePath);
|
|
22779
|
+
const stem = relative5.replace(/\.(?:md|mdx)$/u, "");
|
|
22780
|
+
const frontmatter = parseFrontmatter(fs14.readFileSync(filePath, "utf8"));
|
|
22781
|
+
const scenarioId = typeof frontmatter.scenarioId === "string" && frontmatter.scenarioId.trim().length > 0 ? frontmatter.scenarioId.trim() : path15.basename(stem);
|
|
22782
|
+
const title = typeof frontmatter.title === "string" && frontmatter.title.trim().length > 0 ? frontmatter.title.trim() : `Business context \u2014 ${scenarioId}`;
|
|
22783
|
+
return {
|
|
22784
|
+
scenarioId,
|
|
22785
|
+
slug: toRouteSlug(stem),
|
|
22786
|
+
title
|
|
22787
|
+
};
|
|
22788
|
+
}
|
|
22789
|
+
function parseFrontmatter(source) {
|
|
22790
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/u.exec(source);
|
|
22791
|
+
if (!match) return {};
|
|
22792
|
+
const parsed = parseYaml2(match[1]);
|
|
22793
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
22794
|
+
}
|
|
22795
|
+
function toRouteSlug(stem) {
|
|
22796
|
+
return stem.split(path15.sep).map((segment) => githubSlug(segment)).join("/").replace(/\/index$/u, "");
|
|
22797
|
+
}
|
|
22798
|
+
|
|
22688
22799
|
// src/overview-page.ts
|
|
22689
22800
|
var AUDIENCE_CARDS = [
|
|
22690
22801
|
{
|
|
@@ -22712,11 +22823,12 @@ function yamlScalar3(value) {
|
|
|
22712
22823
|
}
|
|
22713
22824
|
return value;
|
|
22714
22825
|
}
|
|
22715
|
-
function renderOverviewPage(links) {
|
|
22826
|
+
function renderOverviewPage(links, notesIndex) {
|
|
22716
22827
|
const all = Object.values(links.scenarios);
|
|
22717
22828
|
const total = all.length;
|
|
22718
22829
|
const passed = all.filter((s) => s.status === "passed").length;
|
|
22719
22830
|
const failed = all.filter((s) => s.status === "failed").length;
|
|
22831
|
+
const notesById = notesByScenarioId(notesIndex ?? { schemaVersion: "1.0", notes: [] });
|
|
22720
22832
|
const frontmatter = [
|
|
22721
22833
|
"---",
|
|
22722
22834
|
"title: Stories",
|
|
@@ -22740,7 +22852,9 @@ function renderOverviewPage(links) {
|
|
|
22740
22852
|
body.push(`## ${card.icon} ${card.label} (${scenarios.length} \u2014 ${counts})`, "");
|
|
22741
22853
|
body.push(`${card.blurb}`, "");
|
|
22742
22854
|
for (const s of scenariosSorted(scenarios)) {
|
|
22743
|
-
|
|
22855
|
+
const note = notesById.get(s.id);
|
|
22856
|
+
const noteSuffix = note ? ` \xB7 ${noteLinkMarkdown(note)}` : "";
|
|
22857
|
+
body.push(`- ${STATUS_ICON[s.status] ?? "\u2022"} [${s.title}](${s.deepLink})${noteSuffix}`);
|
|
22744
22858
|
}
|
|
22745
22859
|
body.push("");
|
|
22746
22860
|
}
|
|
@@ -22782,22 +22896,22 @@ var BuildDocsError = class extends Error {
|
|
|
22782
22896
|
};
|
|
22783
22897
|
var isRemote = (p) => /^(?:https?:|data:)/i.test(p);
|
|
22784
22898
|
function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets") {
|
|
22785
|
-
if (!
|
|
22786
|
-
const report = JSON.parse(
|
|
22899
|
+
if (!fs15.existsSync(reportPath)) return 0;
|
|
22900
|
+
const report = JSON.parse(fs15.readFileSync(reportPath, "utf8"));
|
|
22787
22901
|
let copied = 0;
|
|
22788
22902
|
const bundle = (value) => {
|
|
22789
|
-
const rel = copyAsset(
|
|
22903
|
+
const rel = copyAsset(path16.resolve(value), assetsDir);
|
|
22790
22904
|
copied++;
|
|
22791
|
-
return `${baseUrl}/${
|
|
22905
|
+
return `${baseUrl}/${path16.basename(rel)}`;
|
|
22792
22906
|
};
|
|
22793
22907
|
const visit = (entries) => {
|
|
22794
22908
|
for (const entry of entries ?? []) {
|
|
22795
22909
|
const e = entry;
|
|
22796
22910
|
if (e.kind === "screenshot" || e.kind === "video" || e.kind === "html") {
|
|
22797
|
-
if (typeof e.path === "string" && !isRemote(e.path) &&
|
|
22911
|
+
if (typeof e.path === "string" && !isRemote(e.path) && fs15.existsSync(e.path)) {
|
|
22798
22912
|
e.path = bundle(e.path);
|
|
22799
22913
|
}
|
|
22800
|
-
if (typeof e.poster === "string" && !isRemote(e.poster) &&
|
|
22914
|
+
if (typeof e.poster === "string" && !isRemote(e.poster) && fs15.existsSync(e.poster)) {
|
|
22801
22915
|
e.poster = bundle(e.poster);
|
|
22802
22916
|
}
|
|
22803
22917
|
}
|
|
@@ -22810,7 +22924,7 @@ function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets"
|
|
|
22810
22924
|
}
|
|
22811
22925
|
}
|
|
22812
22926
|
if (copied > 0) {
|
|
22813
|
-
|
|
22927
|
+
fs15.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
|
|
22814
22928
|
}
|
|
22815
22929
|
return copied;
|
|
22816
22930
|
}
|
|
@@ -22819,50 +22933,69 @@ var CHANGE_BADGE = {
|
|
|
22819
22933
|
fixed: "\u2705 **Fixed** _since last run_",
|
|
22820
22934
|
regressed: "\u26A0\uFE0F **Regressed** _since last run_"
|
|
22821
22935
|
};
|
|
22936
|
+
function scenarioKey(sourceFile, title) {
|
|
22937
|
+
return `${sourceFile}\0${title}`;
|
|
22938
|
+
}
|
|
22822
22939
|
function changeBadgeLookup(diff) {
|
|
22823
22940
|
if (!diff) return void 0;
|
|
22824
22941
|
const byKey = /* @__PURE__ */ new Map();
|
|
22825
22942
|
for (const s of diff.scenarios) {
|
|
22826
22943
|
const badge2 = CHANGE_BADGE[s.kind];
|
|
22827
|
-
if (badge2) byKey.set(
|
|
22944
|
+
if (badge2) byKey.set(scenarioKey(s.sourceFile, s.title), badge2);
|
|
22828
22945
|
}
|
|
22829
22946
|
if (byKey.size === 0) return void 0;
|
|
22830
|
-
return (tc) => byKey.get(
|
|
22947
|
+
return (tc) => byKey.get(scenarioKey(tc.sourceFile, tc.story.scenario));
|
|
22831
22948
|
}
|
|
22832
22949
|
function readStoryReport(reportPath) {
|
|
22833
|
-
if (!
|
|
22950
|
+
if (!fs15.existsSync(reportPath)) return null;
|
|
22834
22951
|
try {
|
|
22835
|
-
return JSON.parse(
|
|
22952
|
+
return JSON.parse(fs15.readFileSync(reportPath, "utf8"));
|
|
22836
22953
|
} catch {
|
|
22837
22954
|
return null;
|
|
22838
22955
|
}
|
|
22839
22956
|
}
|
|
22957
|
+
function noteLinkLookup(report, notesIndex) {
|
|
22958
|
+
if (!report) return void 0;
|
|
22959
|
+
const noteById = notesByScenarioId(notesIndex);
|
|
22960
|
+
if (noteById.size === 0) return void 0;
|
|
22961
|
+
const idByKey = /* @__PURE__ */ new Map();
|
|
22962
|
+
for (const feature of report.features) {
|
|
22963
|
+
for (const scenario of feature.scenarios) {
|
|
22964
|
+
idByKey.set(scenarioKey(feature.sourceFile, scenario.title), scenario.id);
|
|
22965
|
+
}
|
|
22966
|
+
}
|
|
22967
|
+
return (tc) => {
|
|
22968
|
+
const id = idByKey.get(scenarioKey(tc.sourceFile, tc.story.scenario));
|
|
22969
|
+
const note = id ? noteById.get(id) : void 0;
|
|
22970
|
+
return note ? noteLinkMarkdown(note) : void 0;
|
|
22971
|
+
};
|
|
22972
|
+
}
|
|
22840
22973
|
function writeScenarioLinks(reportPath, outDir, options = {}) {
|
|
22841
22974
|
const report = readStoryReport(reportPath);
|
|
22842
22975
|
if (!report) return null;
|
|
22843
22976
|
const index = buildScenarioLinks(report, { audienceSplit: options.audienceSplit });
|
|
22844
|
-
|
|
22845
|
-
|
|
22977
|
+
fs15.writeFileSync(
|
|
22978
|
+
path16.join(outDir, "scenario-links.json"),
|
|
22846
22979
|
JSON.stringify(index, null, 2),
|
|
22847
22980
|
"utf8"
|
|
22848
22981
|
);
|
|
22849
22982
|
return index;
|
|
22850
22983
|
}
|
|
22851
22984
|
function clearGeneratedPages(dir) {
|
|
22852
|
-
if (!
|
|
22853
|
-
for (const entry of
|
|
22854
|
-
const full =
|
|
22985
|
+
if (!fs15.existsSync(dir)) return;
|
|
22986
|
+
for (const entry of fs15.readdirSync(dir, { withFileTypes: true })) {
|
|
22987
|
+
const full = path16.join(dir, entry.name);
|
|
22855
22988
|
if (entry.isDirectory()) {
|
|
22856
22989
|
clearGeneratedPages(full);
|
|
22857
|
-
if (
|
|
22990
|
+
if (fs15.readdirSync(full).length === 0) fs15.rmdirSync(full);
|
|
22858
22991
|
} else if (/\.mdx?$/.test(entry.name)) {
|
|
22859
|
-
|
|
22992
|
+
fs15.rmSync(full);
|
|
22860
22993
|
}
|
|
22861
22994
|
}
|
|
22862
22995
|
}
|
|
22863
22996
|
function loadCanonicalRun(rawRunPath, synthesize) {
|
|
22864
22997
|
try {
|
|
22865
|
-
const data = JSON.parse(
|
|
22998
|
+
const data = JSON.parse(fs15.readFileSync(path16.resolve(rawRunPath), "utf8"));
|
|
22866
22999
|
if (data.schemaVersion !== 1) {
|
|
22867
23000
|
throw new BuildDocsError(`Unsupported schemaVersion ${data.schemaVersion}. Supported: 1.`, "schema");
|
|
22868
23001
|
}
|
|
@@ -22885,12 +23018,13 @@ ${schemaResult.errors.map((e) => ` ${e}`).join("\n")}`,
|
|
|
22885
23018
|
}
|
|
22886
23019
|
}
|
|
22887
23020
|
async function buildDocs(options) {
|
|
22888
|
-
const siteDir =
|
|
22889
|
-
const storiesPublicDir =
|
|
22890
|
-
const assetsDir =
|
|
22891
|
-
const storyPagesDir =
|
|
22892
|
-
const
|
|
22893
|
-
const
|
|
23021
|
+
const siteDir = path16.resolve(options.siteDir);
|
|
23022
|
+
const storiesPublicDir = path16.join(siteDir, "public", "stories");
|
|
23023
|
+
const assetsDir = path16.join(storiesPublicDir, "assets");
|
|
23024
|
+
const storyPagesDir = path16.join(siteDir, "src", "content", "docs", "stories");
|
|
23025
|
+
const notesDir = path16.join(siteDir, "src", "content", "docs", "notes");
|
|
23026
|
+
const apiDir = path16.join(siteDir, "src", "content", "docs", "api");
|
|
23027
|
+
const reportPath = path16.join(storiesPublicDir, "story-report.json");
|
|
22894
23028
|
const canonical = loadCanonicalRun(options.rawRunPath, options.synthesizeStories ?? true);
|
|
22895
23029
|
try {
|
|
22896
23030
|
await new ReportGenerator({
|
|
@@ -22898,9 +23032,10 @@ async function buildDocs(options) {
|
|
|
22898
23032
|
outputDir: storiesPublicDir,
|
|
22899
23033
|
outputName: "story-report"
|
|
22900
23034
|
}).generate(canonical);
|
|
23035
|
+
const currentReport = readStoryReport(reportPath);
|
|
22901
23036
|
let diff;
|
|
22902
23037
|
if (options.baselinePath) {
|
|
22903
|
-
const baselineResolved =
|
|
23038
|
+
const baselineResolved = path16.resolve(options.baselinePath);
|
|
22904
23039
|
const baseline = readStoryReport(baselineResolved);
|
|
22905
23040
|
if (!baseline) {
|
|
22906
23041
|
throw new BuildDocsError(
|
|
@@ -22908,10 +23043,11 @@ async function buildDocs(options) {
|
|
|
22908
23043
|
"input"
|
|
22909
23044
|
);
|
|
22910
23045
|
}
|
|
22911
|
-
|
|
22912
|
-
if (current) diff = diffStoryReports(baseline, current);
|
|
23046
|
+
if (currentReport) diff = diffStoryReports(baseline, currentReport);
|
|
22913
23047
|
}
|
|
22914
23048
|
const scenarioBadge = changeBadgeLookup(diff);
|
|
23049
|
+
const notesIndex = buildScenarioNotesIndex(notesDir);
|
|
23050
|
+
const scenarioNoteLink = noteLinkLookup(currentReport, notesIndex);
|
|
22915
23051
|
clearGeneratedPages(storyPagesDir);
|
|
22916
23052
|
const genPages = (run, outDir) => new ReportGenerator({
|
|
22917
23053
|
formats: ["astro"],
|
|
@@ -22925,7 +23061,8 @@ async function buildDocs(options) {
|
|
|
22925
23061
|
markdown: {
|
|
22926
23062
|
// Emit the same anchor scenario-links.json points at, so fragments resolve.
|
|
22927
23063
|
scenarioAnchor: (tc) => scenarioAnchor(tc.story.scenario),
|
|
22928
|
-
scenarioBadge
|
|
23064
|
+
scenarioBadge,
|
|
23065
|
+
scenarioNoteLink
|
|
22929
23066
|
}
|
|
22930
23067
|
}
|
|
22931
23068
|
}).generate(run);
|
|
@@ -22936,7 +23073,7 @@ async function buildDocs(options) {
|
|
|
22936
23073
|
const sub = partitioned[audience];
|
|
22937
23074
|
audiences[audience] = sub.testCases.length;
|
|
22938
23075
|
if (sub.testCases.length === 0) continue;
|
|
22939
|
-
await genPages(sub,
|
|
23076
|
+
await genPages(sub, path16.join(storyPagesDir, audience));
|
|
22940
23077
|
}
|
|
22941
23078
|
} else {
|
|
22942
23079
|
await genPages(canonical, storyPagesDir);
|
|
@@ -22946,35 +23083,37 @@ async function buildDocs(options) {
|
|
|
22946
23083
|
audienceSplit: options.audienceSplit ?? false
|
|
22947
23084
|
});
|
|
22948
23085
|
const scenarioLinks = linksIndex ? Object.keys(linksIndex.scenarios).length : 0;
|
|
23086
|
+
writeNotesIndex(notesIndex, path16.join(storiesPublicDir, "notes-index.json"));
|
|
23087
|
+
const notesIndexed = notesIndex.notes.length;
|
|
22949
23088
|
if (linksIndex) {
|
|
22950
|
-
|
|
22951
|
-
|
|
22952
|
-
renderOverviewPage(linksIndex),
|
|
23089
|
+
fs15.writeFileSync(
|
|
23090
|
+
path16.join(storyPagesDir, "index.md"),
|
|
23091
|
+
renderOverviewPage(linksIndex, notesIndex),
|
|
22953
23092
|
"utf8"
|
|
22954
23093
|
);
|
|
22955
23094
|
}
|
|
22956
|
-
const changesJsonPath =
|
|
22957
|
-
const changesMdPath =
|
|
23095
|
+
const changesJsonPath = path16.join(storiesPublicDir, "changes.json");
|
|
23096
|
+
const changesMdPath = path16.join(storyPagesDir, "changes.md");
|
|
22958
23097
|
let changes;
|
|
22959
23098
|
if (diff && linksIndex) {
|
|
22960
|
-
|
|
22961
|
-
|
|
23099
|
+
fs15.writeFileSync(changesJsonPath, JSON.stringify(diff, null, 2), "utf8");
|
|
23100
|
+
fs15.writeFileSync(changesMdPath, renderChangesPage(diff, linksIndex), "utf8");
|
|
22962
23101
|
changes = diff.summary;
|
|
22963
23102
|
} else {
|
|
22964
|
-
|
|
22965
|
-
|
|
23103
|
+
fs15.rmSync(changesJsonPath, { force: true });
|
|
23104
|
+
fs15.rmSync(changesMdPath, { force: true });
|
|
22966
23105
|
}
|
|
22967
23106
|
let apiPages = 0;
|
|
22968
23107
|
if (options.openapiPath) {
|
|
22969
23108
|
const res = await importOpenApi({
|
|
22970
|
-
specPath:
|
|
23109
|
+
specPath: path16.resolve(options.openapiPath),
|
|
22971
23110
|
outputDir: apiDir,
|
|
22972
23111
|
runFile: reportPath,
|
|
22973
23112
|
force: true
|
|
22974
23113
|
});
|
|
22975
23114
|
apiPages = res.pageCount;
|
|
22976
23115
|
}
|
|
22977
|
-
return { siteDir, bundledAssets, apiPages, audiences, scenarioLinks, changes };
|
|
23116
|
+
return { siteDir, bundledAssets, apiPages, audiences, scenarioLinks, notesIndexed, changes };
|
|
22978
23117
|
} catch (err) {
|
|
22979
23118
|
if (err instanceof BuildDocsError) throw err;
|
|
22980
23119
|
throw new BuildDocsError(`Generation failed: ${err.message}`, "generation");
|
|
@@ -22982,11 +23121,11 @@ async function buildDocs(options) {
|
|
|
22982
23121
|
}
|
|
22983
23122
|
|
|
22984
23123
|
// src/config.ts
|
|
22985
|
-
import { existsSync as
|
|
23124
|
+
import { existsSync as existsSync13 } from "fs";
|
|
22986
23125
|
import { resolve as resolve10 } from "path";
|
|
22987
23126
|
async function loadConfig(configPath) {
|
|
22988
23127
|
const resolved = configPath ? resolve10(configPath) : resolve10(process.cwd(), "executable-stories.config.js");
|
|
22989
|
-
if (!
|
|
23128
|
+
if (!existsSync13(resolved)) return {};
|
|
22990
23129
|
const mod = await import(resolved);
|
|
22991
23130
|
const config = mod.default;
|
|
22992
23131
|
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
@@ -23044,7 +23183,7 @@ SUBCOMMANDS
|
|
|
23044
23183
|
triage Discovery worklist for agent loops: failing scenarios, regressions first, each with the code it covers
|
|
23045
23184
|
validate Validate a JSON file against the schema (no output generated)
|
|
23046
23185
|
init-astro Scaffold an Astro docs site for story output (Starlight with themed CSS)
|
|
23047
|
-
new Scaffold a docs page from a template (adr, runbook, decision-log, incident)
|
|
23186
|
+
new Scaffold a docs page from a template (adr, runbook, decision-log, incident, scenario-note)
|
|
23048
23187
|
check-links Scan docs for broken internal/external links (CI-friendly exit code)
|
|
23049
23188
|
import-openapi Generate API doc pages from an OpenAPI spec, linked to verifying stories
|
|
23050
23189
|
publish-confluence Publish an ADF JSON file to a Confluence page via REST API
|
|
@@ -23583,20 +23722,20 @@ async function readInput(args) {
|
|
|
23583
23722
|
if (args.stdin) {
|
|
23584
23723
|
return readStdin();
|
|
23585
23724
|
}
|
|
23586
|
-
const filePath =
|
|
23587
|
-
if (!
|
|
23725
|
+
const filePath = path17.resolve(args.inputFile);
|
|
23726
|
+
if (!fs16.existsSync(filePath)) {
|
|
23588
23727
|
console.error(`Error: File not found: ${filePath}`);
|
|
23589
23728
|
process.exit(EXIT_USAGE);
|
|
23590
23729
|
}
|
|
23591
|
-
return
|
|
23730
|
+
return fs16.readFileSync(filePath, "utf8");
|
|
23592
23731
|
}
|
|
23593
23732
|
function readFileInput(filePath) {
|
|
23594
|
-
const resolved =
|
|
23595
|
-
if (!
|
|
23733
|
+
const resolved = path17.resolve(filePath);
|
|
23734
|
+
if (!fs16.existsSync(resolved)) {
|
|
23596
23735
|
console.error(`Error: File not found: ${resolved}`);
|
|
23597
23736
|
process.exit(EXIT_USAGE);
|
|
23598
23737
|
}
|
|
23599
|
-
return
|
|
23738
|
+
return fs16.readFileSync(resolved, "utf8");
|
|
23600
23739
|
}
|
|
23601
23740
|
function readStdin() {
|
|
23602
23741
|
return new Promise((resolve12, reject) => {
|
|
@@ -23729,14 +23868,14 @@ function tryNormalizeRunFromText(text2, args) {
|
|
|
23729
23868
|
}
|
|
23730
23869
|
}
|
|
23731
23870
|
function listBaselineCandidates(currentFile, args) {
|
|
23732
|
-
const baselineDir =
|
|
23733
|
-
const currentResolved =
|
|
23734
|
-
if (!
|
|
23871
|
+
const baselineDir = path17.resolve(args.baselineDir ?? path17.dirname(currentFile));
|
|
23872
|
+
const currentResolved = path17.resolve(currentFile);
|
|
23873
|
+
if (!fs16.existsSync(baselineDir)) {
|
|
23735
23874
|
console.error(`Error: baseline directory not found: ${baselineDir}`);
|
|
23736
23875
|
process.exit(EXIT_USAGE);
|
|
23737
23876
|
}
|
|
23738
|
-
const entries =
|
|
23739
|
-
return entries.filter((entry) => entry.isFile()).map((entry) =>
|
|
23877
|
+
const entries = fs16.readdirSync(baselineDir, { withFileTypes: true });
|
|
23878
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => path17.join(baselineDir, entry.name)).filter((candidate) => path17.resolve(candidate) !== currentResolved).filter(
|
|
23740
23879
|
(candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
|
|
23741
23880
|
);
|
|
23742
23881
|
}
|
|
@@ -23744,14 +23883,14 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
|
|
|
23744
23883
|
const candidates = listBaselineCandidates(currentFile, args);
|
|
23745
23884
|
const comparable = [];
|
|
23746
23885
|
for (const candidate of candidates) {
|
|
23747
|
-
const run = tryNormalizeRunFromText(
|
|
23886
|
+
const run = tryNormalizeRunFromText(fs16.readFileSync(candidate, "utf8"), args);
|
|
23748
23887
|
if (run) {
|
|
23749
23888
|
comparable.push({ file: candidate, run });
|
|
23750
23889
|
}
|
|
23751
23890
|
}
|
|
23752
23891
|
if (comparable.length === 0) {
|
|
23753
23892
|
console.error(
|
|
23754
|
-
`Error: no compatible baseline files found in ${
|
|
23893
|
+
`Error: no compatible baseline files found in ${path17.resolve(args.baselineDir ?? path17.dirname(currentFile))}.`
|
|
23755
23894
|
);
|
|
23756
23895
|
process.exit(EXIT_USAGE);
|
|
23757
23896
|
}
|
|
@@ -23996,9 +24135,9 @@ async function main() {
|
|
|
23996
24135
|
process.exit(EXIT_SCHEMA_VALIDATION);
|
|
23997
24136
|
}
|
|
23998
24137
|
if (args.emitCanonical) {
|
|
23999
|
-
const outPath =
|
|
24000
|
-
|
|
24001
|
-
|
|
24138
|
+
const outPath = path17.resolve(args.emitCanonical);
|
|
24139
|
+
fs16.mkdirSync(path17.dirname(outPath), { recursive: true });
|
|
24140
|
+
fs16.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
24002
24141
|
}
|
|
24003
24142
|
try {
|
|
24004
24143
|
const result = await generateReports(run, args);
|
|
@@ -24055,9 +24194,9 @@ ${msg}`);
|
|
|
24055
24194
|
}
|
|
24056
24195
|
const run = data;
|
|
24057
24196
|
if (args.emitCanonical) {
|
|
24058
|
-
const outPath =
|
|
24059
|
-
|
|
24060
|
-
|
|
24197
|
+
const outPath = path17.resolve(args.emitCanonical);
|
|
24198
|
+
fs16.mkdirSync(path17.dirname(outPath), { recursive: true });
|
|
24199
|
+
fs16.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
24061
24200
|
}
|
|
24062
24201
|
try {
|
|
24063
24202
|
const result = await generateReports(run, args);
|
|
@@ -24113,9 +24252,9 @@ ${msg}`);
|
|
|
24113
24252
|
process.exit(EXIT_CANONICAL_VALIDATION);
|
|
24114
24253
|
}
|
|
24115
24254
|
if (args.emitCanonical) {
|
|
24116
|
-
const outPath =
|
|
24117
|
-
|
|
24118
|
-
|
|
24255
|
+
const outPath = path17.resolve(args.emitCanonical);
|
|
24256
|
+
fs16.mkdirSync(path17.dirname(outPath), { recursive: true });
|
|
24257
|
+
fs16.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
|
|
24119
24258
|
}
|
|
24120
24259
|
try {
|
|
24121
24260
|
const result = await generateReports(canonical, args, droppedMissingStory);
|
|
@@ -24140,9 +24279,9 @@ function runCustomFormatters(run, customRequested, formatters, args) {
|
|
|
24140
24279
|
const ext = formatter.fileExtension ?? formatName;
|
|
24141
24280
|
const baseName = args.outputName ?? "report";
|
|
24142
24281
|
const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
|
|
24143
|
-
const filepath =
|
|
24144
|
-
|
|
24145
|
-
|
|
24282
|
+
const filepath = path17.join(outputDir, filename);
|
|
24283
|
+
fs16.mkdirSync(outputDir, { recursive: true });
|
|
24284
|
+
fs16.writeFileSync(filepath, content, "utf8");
|
|
24146
24285
|
console.log(`Generated: ${filepath}`);
|
|
24147
24286
|
} catch (err) {
|
|
24148
24287
|
console.error(`Error running custom formatter "${formatName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -24192,13 +24331,13 @@ async function dispatchNotifications(run, args) {
|
|
|
24192
24331
|
}
|
|
24193
24332
|
function runHistoryPipeline(run, args) {
|
|
24194
24333
|
if (!args.historyFile) return;
|
|
24195
|
-
const historyPath =
|
|
24334
|
+
const historyPath = path17.resolve(args.historyFile);
|
|
24196
24335
|
const store = loadHistory(
|
|
24197
24336
|
{ filePath: historyPath },
|
|
24198
24337
|
{
|
|
24199
24338
|
readFile: (p) => {
|
|
24200
24339
|
try {
|
|
24201
|
-
return
|
|
24340
|
+
return fs16.readFileSync(p, "utf8");
|
|
24202
24341
|
} catch {
|
|
24203
24342
|
return void 0;
|
|
24204
24343
|
}
|
|
@@ -24211,11 +24350,11 @@ function runHistoryPipeline(run, args) {
|
|
|
24211
24350
|
run,
|
|
24212
24351
|
maxRuns: args.maxHistoryRuns
|
|
24213
24352
|
});
|
|
24214
|
-
const dir =
|
|
24215
|
-
|
|
24353
|
+
const dir = path17.dirname(historyPath);
|
|
24354
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
24216
24355
|
saveHistory(
|
|
24217
24356
|
{ filePath: historyPath, store: updated },
|
|
24218
|
-
{ writeFile: (p, content) =>
|
|
24357
|
+
{ writeFile: (p, content) => fs16.writeFileSync(p, content, "utf8") }
|
|
24219
24358
|
);
|
|
24220
24359
|
let metricsCount = 0;
|
|
24221
24360
|
for (const testId of Object.keys(updated.tests)) {
|
|
@@ -24363,11 +24502,11 @@ function writeReviewReport(review, args) {
|
|
|
24363
24502
|
const outputDir = args.outputDir ?? "reports";
|
|
24364
24503
|
const baseName = args.outputName ?? "evidence-review";
|
|
24365
24504
|
const suffix = args.outputNameTimestamp ? `-${Math.floor(review.run.startedAtMs / 1e3)}` : "";
|
|
24366
|
-
|
|
24367
|
-
const mdPath =
|
|
24368
|
-
const htmlPath =
|
|
24369
|
-
|
|
24370
|
-
|
|
24505
|
+
fs16.mkdirSync(outputDir, { recursive: true });
|
|
24506
|
+
const mdPath = path17.join(outputDir, `${baseName}${suffix}.md`);
|
|
24507
|
+
const htmlPath = path17.join(outputDir, `${baseName}${suffix}.html`);
|
|
24508
|
+
fs16.writeFileSync(mdPath, markdown, "utf8");
|
|
24509
|
+
fs16.writeFileSync(htmlPath, html, "utf8");
|
|
24371
24510
|
return [mdPath, htmlPath];
|
|
24372
24511
|
}
|
|
24373
24512
|
function evaluateReviewGate(review, args) {
|
|
@@ -24413,9 +24552,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
|
|
|
24413
24552
|
function printCompareResult(result, args, startMs) {
|
|
24414
24553
|
const durationMs = Date.now() - startMs;
|
|
24415
24554
|
if (result.prSummary && args.prSummaryFile) {
|
|
24416
|
-
const outputPath =
|
|
24417
|
-
|
|
24418
|
-
|
|
24555
|
+
const outputPath = path17.resolve(args.prSummaryFile);
|
|
24556
|
+
fs16.mkdirSync(path17.dirname(outputPath), { recursive: true });
|
|
24557
|
+
fs16.writeFileSync(outputPath, result.prSummary, "utf8");
|
|
24419
24558
|
}
|
|
24420
24559
|
if (args.jsonSummary) {
|
|
24421
24560
|
console.log(
|
|
@@ -24444,13 +24583,13 @@ function printCompareResult(result, args, startMs) {
|
|
|
24444
24583
|
}
|
|
24445
24584
|
}
|
|
24446
24585
|
function loadReleasePolicy(policyPath) {
|
|
24447
|
-
const resolved =
|
|
24448
|
-
if (!
|
|
24586
|
+
const resolved = path17.resolve(policyPath);
|
|
24587
|
+
if (!fs16.existsSync(resolved)) {
|
|
24449
24588
|
console.error(`Error: release policy file not found: ${resolved}`);
|
|
24450
24589
|
process.exit(EXIT_USAGE);
|
|
24451
24590
|
}
|
|
24452
24591
|
try {
|
|
24453
|
-
const raw = JSON.parse(
|
|
24592
|
+
const raw = JSON.parse(fs16.readFileSync(resolved, "utf8"));
|
|
24454
24593
|
return {
|
|
24455
24594
|
allowedOmissions: Array.isArray(raw.allowedOmissions) ? raw.allowedOmissions : [],
|
|
24456
24595
|
allowedRegressions: Array.isArray(raw.allowedRegressions) ? raw.allowedRegressions : [],
|
|
@@ -24554,7 +24693,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
24554
24693
|
console.error("Error: missing ADF file argument. Run with --help for usage.");
|
|
24555
24694
|
process.exit(EXIT_USAGE);
|
|
24556
24695
|
}
|
|
24557
|
-
if (!
|
|
24696
|
+
if (!fs16.existsSync(inputFile)) {
|
|
24558
24697
|
console.error(`Error: file not found: ${inputFile}`);
|
|
24559
24698
|
process.exit(EXIT_USAGE);
|
|
24560
24699
|
}
|
|
@@ -24582,7 +24721,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
24582
24721
|
console.error("Error: --title is required when creating a new page");
|
|
24583
24722
|
process.exit(EXIT_USAGE);
|
|
24584
24723
|
}
|
|
24585
|
-
const adf =
|
|
24724
|
+
const adf = fs16.readFileSync(path17.resolve(inputFile), "utf8");
|
|
24586
24725
|
if (dryRun) {
|
|
24587
24726
|
console.log(
|
|
24588
24727
|
JSON.stringify(
|
|
@@ -24661,7 +24800,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
24661
24800
|
console.error("Error: missing ADF file argument. Run with --help for usage.");
|
|
24662
24801
|
process.exit(EXIT_USAGE);
|
|
24663
24802
|
}
|
|
24664
|
-
if (!
|
|
24803
|
+
if (!fs16.existsSync(inputFile)) {
|
|
24665
24804
|
console.error(`Error: file not found: ${inputFile}`);
|
|
24666
24805
|
process.exit(EXIT_USAGE);
|
|
24667
24806
|
}
|
|
@@ -24688,7 +24827,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
24688
24827
|
process.exit(EXIT_USAGE);
|
|
24689
24828
|
}
|
|
24690
24829
|
const mode = modeRaw;
|
|
24691
|
-
const adf =
|
|
24830
|
+
const adf = fs16.readFileSync(path17.resolve(inputFile), "utf8");
|
|
24692
24831
|
if (dryRun) {
|
|
24693
24832
|
console.log(
|
|
24694
24833
|
JSON.stringify(
|
|
@@ -24732,14 +24871,20 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
24732
24871
|
function runNew(rawArgs) {
|
|
24733
24872
|
const { values, positionals } = parseArgs({
|
|
24734
24873
|
args: rawArgs,
|
|
24735
|
-
options: {
|
|
24874
|
+
options: {
|
|
24875
|
+
dir: { type: "string" },
|
|
24876
|
+
force: { type: "boolean", default: false },
|
|
24877
|
+
"scenario-id": { type: "string" }
|
|
24878
|
+
},
|
|
24736
24879
|
allowPositionals: true,
|
|
24737
24880
|
strict: true
|
|
24738
24881
|
});
|
|
24739
24882
|
const template = positionals[0];
|
|
24740
24883
|
const name = positionals.slice(1).join(" ");
|
|
24741
24884
|
if (!template) {
|
|
24742
|
-
console.error(
|
|
24885
|
+
console.error(
|
|
24886
|
+
`Usage: executable-stories new <template> "<name>" [--dir <docs-dir>] [--scenario-id <id>] [--force]`
|
|
24887
|
+
);
|
|
24743
24888
|
console.error(`Templates: ${TEMPLATES.join(", ")}`);
|
|
24744
24889
|
return EXIT_USAGE;
|
|
24745
24890
|
}
|
|
@@ -24747,6 +24892,7 @@ function runNew(rawArgs) {
|
|
|
24747
24892
|
const result = scaffoldDoc({
|
|
24748
24893
|
template,
|
|
24749
24894
|
name,
|
|
24895
|
+
scenarioId: values["scenario-id"],
|
|
24750
24896
|
baseDir: values.dir,
|
|
24751
24897
|
force: values.force
|
|
24752
24898
|
});
|
|
@@ -24849,6 +24995,7 @@ async function runBuildDocs(rawArgs) {
|
|
|
24849
24995
|
console.log(`\u2713 Living docs generated in ${result.siteDir}`);
|
|
24850
24996
|
console.log(` \u2022 Explorer data \u2192 public/stories/story-report.json`);
|
|
24851
24997
|
console.log(` \u2022 Deep links \u2192 public/stories/scenario-links.json (${result.scenarioLinks})`);
|
|
24998
|
+
console.log(` \u2022 Note links \u2192 public/stories/notes-index.json (${result.notesIndexed})`);
|
|
24852
24999
|
if (audienceSplit) {
|
|
24853
25000
|
console.log(
|
|
24854
25001
|
` \u2022 Story pages \u2192 src/content/docs/stories/{engineer,stakeholder} (engineer: ${result.audiences.engineer}, stakeholder: ${result.audiences.stakeholder})`
|
|
@@ -24868,7 +25015,7 @@ async function runBuildDocs(rawArgs) {
|
|
|
24868
25015
|
` \u2022 What's changed \u2192 src/content/docs/stories/changes.md (+${c.added} added, ${c.regressed} regressed, ${c.fixed} fixed, ${c.removed} removed)`
|
|
24869
25016
|
);
|
|
24870
25017
|
}
|
|
24871
|
-
const rel =
|
|
25018
|
+
const rel = path17.relative(process.cwd(), result.siteDir) || ".";
|
|
24872
25019
|
console.log(`
|
|
24873
25020
|
Preview: cd ${rel} && npm run dev`);
|
|
24874
25021
|
return EXIT_SUCCESS;
|