executable-stories-formatters 1.17.1 → 1.18.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 +253 -12
- package/dist/cli.js.map +1 -1
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -148,8 +148,8 @@ import { formatDuration } from "executable-stories-core/utils/duration";
|
|
|
148
148
|
|
|
149
149
|
// src/scenario-failure.ts
|
|
150
150
|
function failingScenarioMessage(tc) {
|
|
151
|
-
const
|
|
152
|
-
return
|
|
151
|
+
const failingStep2 = tc.stepResults.find((s) => s.status === "failed" && s.errorMessage);
|
|
152
|
+
return failingStep2?.errorMessage ?? tc.errorMessage;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// src/check.ts
|
|
@@ -257,8 +257,8 @@ function renderCheckText(report) {
|
|
|
257
257
|
lines.push(`${marker}${step.keyword} ${step.text}`);
|
|
258
258
|
}
|
|
259
259
|
if (f.errorMessage) {
|
|
260
|
-
const
|
|
261
|
-
lines.push(` \u2192 ${
|
|
260
|
+
const firstLine3 = f.errorMessage.split("\n")[0];
|
|
261
|
+
lines.push(` \u2192 ${firstLine3}`);
|
|
262
262
|
}
|
|
263
263
|
if (f.covers.length > 0) {
|
|
264
264
|
lines.push(` covers: ${f.covers.join(", ")}`);
|
|
@@ -2200,8 +2200,8 @@ function escapeCell(value) {
|
|
|
2200
2200
|
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
2201
2201
|
}
|
|
2202
2202
|
function intentSummary(intent) {
|
|
2203
|
-
const
|
|
2204
|
-
const trimmed =
|
|
2203
|
+
const firstLine3 = intent.split("\n").find((l) => l.trim().length > 0) ?? "";
|
|
2204
|
+
const trimmed = firstLine3.trim();
|
|
2205
2205
|
return trimmed.length > 200 ? `${trimmed.slice(0, 197)}\u2026` : trimmed;
|
|
2206
2206
|
}
|
|
2207
2207
|
function renderTicket(ticket) {
|
|
@@ -2396,6 +2396,138 @@ var ReviewMarkdownFormatter = class {
|
|
|
2396
2396
|
}
|
|
2397
2397
|
};
|
|
2398
2398
|
|
|
2399
|
+
// src/formatters/review-json.ts
|
|
2400
|
+
var SEVERITY_RANK = {
|
|
2401
|
+
blocker: 0,
|
|
2402
|
+
major: 1,
|
|
2403
|
+
minor: 2
|
|
2404
|
+
};
|
|
2405
|
+
function failingStep(testCase) {
|
|
2406
|
+
const failed = testCase.stepResults.find((s) => s.status === "failed");
|
|
2407
|
+
if (!failed) return void 0;
|
|
2408
|
+
const step = testCase.story.steps?.[failed.index];
|
|
2409
|
+
return step ? `${step.keyword} ${step.text}` : void 0;
|
|
2410
|
+
}
|
|
2411
|
+
function firstLine(message) {
|
|
2412
|
+
const line = message.split("\n").find((l) => l.trim().length > 0)?.trim() ?? "";
|
|
2413
|
+
return line.length > 300 ? `${line.slice(0, 297)}\u2026` : line;
|
|
2414
|
+
}
|
|
2415
|
+
function failedFinding(claim) {
|
|
2416
|
+
const testCase = claim.testCase;
|
|
2417
|
+
const evidence = [`the scenario is ${claim.status}`];
|
|
2418
|
+
const step = failingStep(testCase);
|
|
2419
|
+
if (step) evidence.push(`failed at: ${step}`);
|
|
2420
|
+
if (testCase.errorMessage) evidence.push(firstLine(testCase.errorMessage));
|
|
2421
|
+
return {
|
|
2422
|
+
kind: "failed",
|
|
2423
|
+
severity: "blocker",
|
|
2424
|
+
title: `Unproven claim: ${claim.scenario}`,
|
|
2425
|
+
file: claim.sourceFile,
|
|
2426
|
+
line: claim.sourceLine,
|
|
2427
|
+
detail: "This scenario states a claim about the change and does not pass, so the claim is unproven.",
|
|
2428
|
+
evidence,
|
|
2429
|
+
remedy: "Fix the behaviour so the scenario passes, or correct the scenario if it states the wrong claim."
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
function skippedFinding(claim) {
|
|
2433
|
+
return {
|
|
2434
|
+
kind: "skipped",
|
|
2435
|
+
severity: "minor",
|
|
2436
|
+
title: `Claim not exercised: ${claim.scenario}`,
|
|
2437
|
+
file: claim.sourceFile,
|
|
2438
|
+
line: claim.sourceLine,
|
|
2439
|
+
detail: `This scenario is ${claim.status}, so it did not run and its claim about the change is unproven.`,
|
|
2440
|
+
evidence: claim.strengthReasons,
|
|
2441
|
+
remedy: "Run the scenario, or delete it if the claim no longer applies \u2014 a permanently skipped claim is worse than no claim."
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
function unassertedFinding(claim) {
|
|
2445
|
+
return {
|
|
2446
|
+
kind: "unasserted",
|
|
2447
|
+
severity: "major",
|
|
2448
|
+
title: `Green but proves nothing: ${claim.scenario}`,
|
|
2449
|
+
file: claim.sourceFile,
|
|
2450
|
+
line: claim.sourceLine,
|
|
2451
|
+
detail: "This scenario passed without asserting anything, so it cannot fail and proves nothing about the change.",
|
|
2452
|
+
evidence: claim.strengthReasons,
|
|
2453
|
+
remedy: "Assert the outcome the scenario claims, so that breaking the behaviour turns it red."
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
function uncoveredFinding(file) {
|
|
2457
|
+
return {
|
|
2458
|
+
kind: "uncovered",
|
|
2459
|
+
severity: "major",
|
|
2460
|
+
title: "Changed with no evidence",
|
|
2461
|
+
file: file.path,
|
|
2462
|
+
detail: `This file was ${file.changeKind} in the diff and no scenario in the run claims anything about it.`,
|
|
2463
|
+
evidence: ["no claim in this run correlates to this file"],
|
|
2464
|
+
remedy: "Add a scenario covering the behaviour this file changed, or say in the PR why it needs none."
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
function weakFinding(file) {
|
|
2468
|
+
return {
|
|
2469
|
+
kind: "weak",
|
|
2470
|
+
severity: "minor",
|
|
2471
|
+
title: "Weak evidence only",
|
|
2472
|
+
file: file.path,
|
|
2473
|
+
detail: `This file was ${file.changeKind} in the diff and its only claims are weakly evidenced.`,
|
|
2474
|
+
evidence: file.claims.map((c) => `${c.scenario} (${c.strength})`),
|
|
2475
|
+
remedy: "Strengthen the proof: verify the test fails on the base ref, add integration or e2e coverage, or attach a screenshot or trace."
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
function reviewFindings(review) {
|
|
2479
|
+
const findings = [];
|
|
2480
|
+
for (const claim of review.claims) {
|
|
2481
|
+
if (claim.status === "failed") {
|
|
2482
|
+
findings.push(failedFinding(claim));
|
|
2483
|
+
} else if (claim.status === "skipped" || claim.status === "pending") {
|
|
2484
|
+
findings.push(skippedFinding(claim));
|
|
2485
|
+
} else if (claim.strength === "none") {
|
|
2486
|
+
findings.push(unassertedFinding(claim));
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
for (const file of review.changedFiles) {
|
|
2490
|
+
if (file.band === "uncovered") findings.push(uncoveredFinding(file));
|
|
2491
|
+
else if (file.band === "weak") findings.push(weakFinding(file));
|
|
2492
|
+
}
|
|
2493
|
+
return findings.sort(
|
|
2494
|
+
(a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]
|
|
2495
|
+
);
|
|
2496
|
+
}
|
|
2497
|
+
function toJsonClaim(claim) {
|
|
2498
|
+
return {
|
|
2499
|
+
id: claim.id,
|
|
2500
|
+
scenario: claim.scenario,
|
|
2501
|
+
sourceFile: claim.sourceFile,
|
|
2502
|
+
sourceLine: claim.sourceLine,
|
|
2503
|
+
status: claim.status,
|
|
2504
|
+
audience: claim.audience,
|
|
2505
|
+
changeType: claim.changeType,
|
|
2506
|
+
strength: claim.strength,
|
|
2507
|
+
strengthReasons: claim.strengthReasons,
|
|
2508
|
+
coversFiles: claim.coversFiles
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
function buildReviewJson(review) {
|
|
2512
|
+
const run = { total: 0, passed: 0, failed: 0, skipped: 0, pending: 0 };
|
|
2513
|
+
for (const testCase of review.run.testCases) {
|
|
2514
|
+
run.total++;
|
|
2515
|
+
if (testCase.status === "passed" || testCase.status === "failed" || testCase.status === "skipped" || testCase.status === "pending") {
|
|
2516
|
+
run[testCase.status]++;
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
return {
|
|
2520
|
+
version: 1,
|
|
2521
|
+
baseRef: review.context.baseRef,
|
|
2522
|
+
headRef: review.context.headRef,
|
|
2523
|
+
summary: review.summary,
|
|
2524
|
+
run,
|
|
2525
|
+
findings: reviewFindings(review),
|
|
2526
|
+
changedFiles: review.changedFiles,
|
|
2527
|
+
claims: review.claims.map(toJsonClaim)
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2399
2531
|
// src/goal.ts
|
|
2400
2532
|
var ACTIVE = ["passed", "failed"];
|
|
2401
2533
|
function buildGoal(args, _deps = {}) {
|
|
@@ -3045,14 +3177,14 @@ function scenarioLine(diff) {
|
|
|
3045
3177
|
case "moved":
|
|
3046
3178
|
return `- **${title}** moved from \`${diff.baseline?.sourceFile ?? "?"}\` to ${source}`;
|
|
3047
3179
|
case "regressed":
|
|
3048
|
-
return `- **${title}** (${source})${diff.current?.errorMessage ? ` \u2014 ${
|
|
3180
|
+
return `- **${title}** (${source})${diff.current?.errorMessage ? ` \u2014 ${firstLine2(diff.current.errorMessage)}` : ""}`;
|
|
3049
3181
|
case "changed":
|
|
3050
3182
|
return `- **${title}** (${source}) \u2014 ${diff.changedFields.join(", ")}`;
|
|
3051
3183
|
default:
|
|
3052
3184
|
return `- **${title}** (${source})`;
|
|
3053
3185
|
}
|
|
3054
3186
|
}
|
|
3055
|
-
function
|
|
3187
|
+
function firstLine2(text2) {
|
|
3056
3188
|
return text2.split("\n", 1)[0].trim();
|
|
3057
3189
|
}
|
|
3058
3190
|
var RunDiffChangelogFormatter = class {
|
|
@@ -10966,6 +11098,12 @@ Options:
|
|
|
10966
11098
|
cloud can recommend a test scope for the change.
|
|
10967
11099
|
--format <fmt> auto (default), story, junit, playwright or allure.
|
|
10968
11100
|
Only needed when detection guesses wrong.
|
|
11101
|
+
--review-json <path>
|
|
11102
|
+
Write what a CI surface renders \u2014 the cloud run URL, this
|
|
11103
|
+
run's outcome counts, and, when --gate is used, the org's
|
|
11104
|
+
verdict and its blocking reasons \u2014 as a StoryReport
|
|
11105
|
+
ReviewJson. Written on every successful push, not only when
|
|
11106
|
+
a gate runs, so the PR comment can link the run either way.
|
|
10969
11107
|
--gate After pushing, ask the cloud whether this commit is safe
|
|
10970
11108
|
to release and exit 5 if it is blocked. The policy lives
|
|
10971
11109
|
in your organization's settings, not in a file here.
|
|
@@ -10992,6 +11130,7 @@ function defaultDeps() {
|
|
|
10992
11130
|
}
|
|
10993
11131
|
},
|
|
10994
11132
|
appendFile: (filePath, text2) => fs14.appendFileSync(filePath, text2),
|
|
11133
|
+
writeFile: (filePath, text2) => fs14.writeFileSync(filePath, text2, "utf8"),
|
|
10995
11134
|
fetchFn: fetch,
|
|
10996
11135
|
git: (args) => {
|
|
10997
11136
|
try {
|
|
@@ -11105,6 +11244,19 @@ function summaryWriter(deps) {
|
|
|
11105
11244
|
`);
|
|
11106
11245
|
};
|
|
11107
11246
|
}
|
|
11247
|
+
var NO_COUNTS = { total: 0, passed: 0, failed: 0, skipped: 0, pending: 0 };
|
|
11248
|
+
function runCounts(report) {
|
|
11249
|
+
const summary = report?.summary;
|
|
11250
|
+
if (!summary) return NO_COUNTS;
|
|
11251
|
+
const read = (key) => typeof summary[key] === "number" ? summary[key] : 0;
|
|
11252
|
+
return {
|
|
11253
|
+
total: read("total"),
|
|
11254
|
+
passed: read("passed"),
|
|
11255
|
+
failed: read("failed"),
|
|
11256
|
+
skipped: read("skipped"),
|
|
11257
|
+
pending: read("pending")
|
|
11258
|
+
};
|
|
11259
|
+
}
|
|
11108
11260
|
function cell(text2) {
|
|
11109
11261
|
return text2.replaceAll("|", "\\|");
|
|
11110
11262
|
}
|
|
@@ -11124,6 +11276,7 @@ async function runPush(rawArgs, depsOverride = {}) {
|
|
|
11124
11276
|
base: { type: "string" },
|
|
11125
11277
|
format: { type: "string" },
|
|
11126
11278
|
gate: { type: "boolean" },
|
|
11279
|
+
"review-json": { type: "string" },
|
|
11127
11280
|
force: { type: "boolean" },
|
|
11128
11281
|
help: { type: "boolean", short: "h" }
|
|
11129
11282
|
}
|
|
@@ -11205,8 +11358,11 @@ async function runPush(rawArgs, depsOverride = {}) {
|
|
|
11205
11358
|
deps.error(`Warning: no changed files found against ${base}; pushing without change metadata.`);
|
|
11206
11359
|
}
|
|
11207
11360
|
const forced = parsed.values.force === true;
|
|
11208
|
-
const gateArgs = () => ({
|
|
11361
|
+
const gateArgs = (runUrl, counts2 = NO_COUNTS) => ({
|
|
11209
11362
|
wanted: parsed.values.gate === true,
|
|
11363
|
+
reviewJson: parsed.values["review-json"],
|
|
11364
|
+
runUrl,
|
|
11365
|
+
counts: counts2,
|
|
11210
11366
|
forced,
|
|
11211
11367
|
baseUrl,
|
|
11212
11368
|
key,
|
|
@@ -11279,6 +11435,13 @@ async function runPush(rawArgs, depsOverride = {}) {
|
|
|
11279
11435
|
result = JSON.parse(body);
|
|
11280
11436
|
} catch {
|
|
11281
11437
|
}
|
|
11438
|
+
const counts = runCounts(report);
|
|
11439
|
+
writeReviewJson(
|
|
11440
|
+
parsed.values["review-json"],
|
|
11441
|
+
void 0,
|
|
11442
|
+
{ repo, gitSha, runUrl: result.url, counts },
|
|
11443
|
+
deps
|
|
11444
|
+
);
|
|
11282
11445
|
const runId = String(result.runId ?? "");
|
|
11283
11446
|
deps.log(runId ? `Pushed run ${runId} (${repo}${branch ? `@${branch}` : ""})` : "Pushed run.");
|
|
11284
11447
|
if (result.url) deps.log(result.url);
|
|
@@ -11306,7 +11469,7 @@ Recommended scope for this change (${recommendations.length}):`);
|
|
|
11306
11469
|
summary(`| ${cell(item.confidence)} | ${cell(item.title)} | ${cell(item.reason)} |`);
|
|
11307
11470
|
}
|
|
11308
11471
|
}
|
|
11309
|
-
return await gateIfRequested(gateArgs(), deps);
|
|
11472
|
+
return await gateIfRequested(gateArgs(result.url, counts), deps);
|
|
11310
11473
|
}
|
|
11311
11474
|
async function gateIfRequested(args, deps) {
|
|
11312
11475
|
if (!args.wanted) return EXIT_SUCCESS;
|
|
@@ -11317,14 +11480,78 @@ async function gateIfRequested(args, deps) {
|
|
|
11317
11480
|
const code = await runGate({ ...args, gitSha: args.gitSha }, deps);
|
|
11318
11481
|
return args.forced && code !== EXIT_GATE_BLOCKED ? EXIT_SUCCESS : code;
|
|
11319
11482
|
}
|
|
11483
|
+
function gateReviewJson(gate, context) {
|
|
11484
|
+
const where = context.gitSha ? `${context.repo}@${context.gitSha.slice(0, 12)}` : context.repo;
|
|
11485
|
+
const evidence = [`organisation release policy, evaluated for ${where}`];
|
|
11486
|
+
const findings = gate === void 0 ? [] : [
|
|
11487
|
+
...(gate.blocking ?? []).map((reason) => ({
|
|
11488
|
+
kind: "policy",
|
|
11489
|
+
severity: "blocker",
|
|
11490
|
+
title: "Release policy not satisfied",
|
|
11491
|
+
detail: reason,
|
|
11492
|
+
evidence,
|
|
11493
|
+
remedy: (
|
|
11494
|
+
// Named without naming the product: this text reaches a PR comment on
|
|
11495
|
+
// every gated push, and the cloud is not launched yet.
|
|
11496
|
+
"Satisfy the policy this names, or record a decision against the release."
|
|
11497
|
+
)
|
|
11498
|
+
})),
|
|
11499
|
+
...(gate.warnings ?? []).map((reason) => ({
|
|
11500
|
+
kind: "policy",
|
|
11501
|
+
severity: "minor",
|
|
11502
|
+
title: "Release policy warning",
|
|
11503
|
+
detail: reason,
|
|
11504
|
+
evidence,
|
|
11505
|
+
remedy: "Not blocking this release. Worth clearing before the next one."
|
|
11506
|
+
}))
|
|
11507
|
+
];
|
|
11508
|
+
const gateStatus = gate === void 0 ? void 0 : gate.status === "no-release" ? "not-evaluated" : gate.status === "blocked" ? "blocked" : "clear";
|
|
11509
|
+
const review = {
|
|
11510
|
+
version: 1,
|
|
11511
|
+
...context.gitSha ? { headRef: context.gitSha } : {},
|
|
11512
|
+
summary: {
|
|
11513
|
+
totalClaims: 0,
|
|
11514
|
+
byAudience: { stakeholder: 0, engineer: 0 },
|
|
11515
|
+
byStrength: { none: 0, weak: 0, moderate: 0, strong: 0 },
|
|
11516
|
+
changedSourceFiles: 0,
|
|
11517
|
+
uncovered: 0,
|
|
11518
|
+
weaklyCovered: 0,
|
|
11519
|
+
covered: 0
|
|
11520
|
+
},
|
|
11521
|
+
// Taken from the StoryReport this command just pushed, so a comment written
|
|
11522
|
+
// with no gate still says what the run actually did rather than nothing.
|
|
11523
|
+
run: context.counts,
|
|
11524
|
+
findings,
|
|
11525
|
+
changedFiles: [],
|
|
11526
|
+
claims: [],
|
|
11527
|
+
...context.runUrl ? { reportUrl: context.runUrl } : {},
|
|
11528
|
+
...gateStatus ? { gate: gateStatus } : {}
|
|
11529
|
+
};
|
|
11530
|
+
return `${JSON.stringify(review, null, 2)}
|
|
11531
|
+
`;
|
|
11532
|
+
}
|
|
11533
|
+
function writeReviewJson(target, gate, context, deps) {
|
|
11534
|
+
if (!target) return;
|
|
11535
|
+
try {
|
|
11536
|
+
deps.writeFile(target, gateReviewJson(gate, context));
|
|
11537
|
+
} catch (err) {
|
|
11538
|
+
deps.error(
|
|
11539
|
+
`Could not write --review-json to ${target}: ${err instanceof Error ? err.message : String(err)}`
|
|
11540
|
+
);
|
|
11541
|
+
}
|
|
11542
|
+
}
|
|
11320
11543
|
async function runGate({
|
|
11321
11544
|
baseUrl,
|
|
11322
11545
|
key,
|
|
11323
11546
|
repo,
|
|
11324
11547
|
gitSha,
|
|
11325
|
-
onActions
|
|
11548
|
+
onActions,
|
|
11549
|
+
reviewJson,
|
|
11550
|
+
runUrl,
|
|
11551
|
+
counts
|
|
11326
11552
|
}, deps) {
|
|
11327
11553
|
const summary = summaryWriter(deps);
|
|
11554
|
+
const writeGateJson = (gate2) => writeReviewJson(reviewJson, gate2, { repo, gitSha, runUrl, counts }, deps);
|
|
11328
11555
|
const query = `repo=${encodeURIComponent(repo)}&sha=${encodeURIComponent(gitSha)}`;
|
|
11329
11556
|
let response;
|
|
11330
11557
|
try {
|
|
@@ -11359,6 +11586,7 @@ async function runGate({
|
|
|
11359
11586
|
deps.log(`
|
|
11360
11587
|
No release recorded for ${commit} \u2014 nothing to gate on.`);
|
|
11361
11588
|
summary("\n**Release gate: no release recorded for this commit**");
|
|
11589
|
+
writeGateJson(gate);
|
|
11362
11590
|
return EXIT_SUCCESS;
|
|
11363
11591
|
}
|
|
11364
11592
|
if (gate.status === "blocked") {
|
|
@@ -11370,11 +11598,13 @@ Release gate: BLOCKED for ${commit}`);
|
|
|
11370
11598
|
summary(`- ${reason}`);
|
|
11371
11599
|
if (onActions) deps.error(`::error::Release gate: ${reason}`);
|
|
11372
11600
|
}
|
|
11601
|
+
writeGateJson(gate);
|
|
11373
11602
|
return EXIT_GATE_BLOCKED;
|
|
11374
11603
|
}
|
|
11375
11604
|
deps.log(`
|
|
11376
11605
|
Release gate: clear for ${commit}`);
|
|
11377
11606
|
summary("\n**Release gate: clear**");
|
|
11607
|
+
writeGateJson(gate);
|
|
11378
11608
|
return EXIT_SUCCESS;
|
|
11379
11609
|
}
|
|
11380
11610
|
|
|
@@ -13601,6 +13831,10 @@ ${presetHelpLines().map((l) => ` ${l}`).join("\
|
|
|
13601
13831
|
--code-diff <path> (review) Code Diff annotation sidecar (JSON: {title, annotations: [{file, match, text, label?, scenarioIds?}]})
|
|
13602
13832
|
--patch <path> (review) Unified patch for --code-diff; generate with "git diff --histogram"
|
|
13603
13833
|
--strict-code-diff (review) Gate: exit non-zero on orphaned/ambiguous anchors or unverified scenario references (default: off)
|
|
13834
|
+
|
|
13835
|
+
review writes three files: <output-name>.md and .html for people, and
|
|
13836
|
+
<output-name>.review.json \u2014 the machine contract CI surfaces render (ranked
|
|
13837
|
+
findings, evidence bands, per-claim strength). See the ReviewJson type.
|
|
13604
13838
|
--emit-canonical <path> Write canonical JSON to given path
|
|
13605
13839
|
--help Show this help message
|
|
13606
13840
|
|
|
@@ -15485,9 +15719,16 @@ function writeReviewReport(review, args) {
|
|
|
15485
15719
|
fs19.mkdirSync(outputDir, { recursive: true });
|
|
15486
15720
|
const mdPath = path25.join(outputDir, `${baseName}${suffix}.md`);
|
|
15487
15721
|
const htmlPath = path25.join(outputDir, `${baseName}${suffix}.html`);
|
|
15722
|
+
const jsonPath = path25.join(outputDir, `${baseName}${suffix}.review.json`);
|
|
15488
15723
|
fs19.writeFileSync(mdPath, markdown, "utf8");
|
|
15489
15724
|
fs19.writeFileSync(htmlPath, html, "utf8");
|
|
15490
|
-
|
|
15725
|
+
fs19.writeFileSync(
|
|
15726
|
+
jsonPath,
|
|
15727
|
+
`${JSON.stringify(buildReviewJson(review), null, 2)}
|
|
15728
|
+
`,
|
|
15729
|
+
"utf8"
|
|
15730
|
+
);
|
|
15731
|
+
return [mdPath, htmlPath, jsonPath];
|
|
15491
15732
|
}
|
|
15492
15733
|
function evaluateReviewGate(review, args) {
|
|
15493
15734
|
const failures = [];
|