executable-stories-formatters 1.17.0 → 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 +283 -30
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +30 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +30 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 {
|
|
@@ -9187,27 +9319,39 @@ var ReportGenerator = class {
|
|
|
9187
9319
|
bundleAssets(htmlPath, { allowMissing: this.options.allowMissingAssets });
|
|
9188
9320
|
}
|
|
9189
9321
|
}
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
assetsDir,
|
|
9200
|
-
assetsBaseUrl: this.options.astro.assetsBaseUrl,
|
|
9201
|
-
allowMissing: this.options.allowMissingAssets
|
|
9202
|
-
});
|
|
9203
|
-
if (result.copiedCount > 0 || result.missingCount > 0) {
|
|
9204
|
-
await this.deps.writeFile(mdPath, result.markdown);
|
|
9205
|
-
}
|
|
9206
|
-
}
|
|
9207
|
-
}
|
|
9322
|
+
await this.bundleMarkdownAssets(results.get("markdown"), (markdownDir) => ({
|
|
9323
|
+
assetsDir: path14.join(markdownDir, "assets"),
|
|
9324
|
+
assetsBaseUrl: "assets"
|
|
9325
|
+
}));
|
|
9326
|
+
await this.bundleMarkdownAssets(results.get("astro-markdown"), () => ({
|
|
9327
|
+
// assetsDir is resolved from CWD (same as outputDir), not relative to outputDir
|
|
9328
|
+
assetsDir: path14.resolve(this.options.astro.assetsDir),
|
|
9329
|
+
assetsBaseUrl: this.options.astro.assetsBaseUrl
|
|
9330
|
+
}));
|
|
9208
9331
|
}
|
|
9209
9332
|
return results;
|
|
9210
9333
|
}
|
|
9334
|
+
/**
|
|
9335
|
+
* Copy every local asset these markdown reports reference into an assets
|
|
9336
|
+
* directory and rewrite the refs. The destination is computed per report
|
|
9337
|
+
* because colocated output writes one per source file.
|
|
9338
|
+
*/
|
|
9339
|
+
async bundleMarkdownAssets(markdownPaths, target) {
|
|
9340
|
+
if (!markdownPaths) return;
|
|
9341
|
+
for (const markdownPath of markdownPaths) {
|
|
9342
|
+
const markdown = await fsPromises2.readFile(markdownPath, "utf8");
|
|
9343
|
+
const markdownDir = path14.dirname(markdownPath);
|
|
9344
|
+
const result = copyMarkdownAssets({
|
|
9345
|
+
markdown,
|
|
9346
|
+
markdownDir,
|
|
9347
|
+
allowMissing: this.options.allowMissingAssets,
|
|
9348
|
+
...target(markdownDir)
|
|
9349
|
+
});
|
|
9350
|
+
if (result.copiedCount > 0 || result.missingCount > 0) {
|
|
9351
|
+
await this.deps.writeFile(markdownPath, result.markdown);
|
|
9352
|
+
}
|
|
9353
|
+
}
|
|
9354
|
+
}
|
|
9211
9355
|
/**
|
|
9212
9356
|
* Whether any output is colocated — the global mode, or any per-rule mode.
|
|
9213
9357
|
* A colocated rule under a global aggregated mode still writes per-file
|
|
@@ -10954,6 +11098,12 @@ Options:
|
|
|
10954
11098
|
cloud can recommend a test scope for the change.
|
|
10955
11099
|
--format <fmt> auto (default), story, junit, playwright or allure.
|
|
10956
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.
|
|
10957
11107
|
--gate After pushing, ask the cloud whether this commit is safe
|
|
10958
11108
|
to release and exit 5 if it is blocked. The policy lives
|
|
10959
11109
|
in your organization's settings, not in a file here.
|
|
@@ -10980,6 +11130,7 @@ function defaultDeps() {
|
|
|
10980
11130
|
}
|
|
10981
11131
|
},
|
|
10982
11132
|
appendFile: (filePath, text2) => fs14.appendFileSync(filePath, text2),
|
|
11133
|
+
writeFile: (filePath, text2) => fs14.writeFileSync(filePath, text2, "utf8"),
|
|
10983
11134
|
fetchFn: fetch,
|
|
10984
11135
|
git: (args) => {
|
|
10985
11136
|
try {
|
|
@@ -11093,6 +11244,19 @@ function summaryWriter(deps) {
|
|
|
11093
11244
|
`);
|
|
11094
11245
|
};
|
|
11095
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
|
+
}
|
|
11096
11260
|
function cell(text2) {
|
|
11097
11261
|
return text2.replaceAll("|", "\\|");
|
|
11098
11262
|
}
|
|
@@ -11112,6 +11276,7 @@ async function runPush(rawArgs, depsOverride = {}) {
|
|
|
11112
11276
|
base: { type: "string" },
|
|
11113
11277
|
format: { type: "string" },
|
|
11114
11278
|
gate: { type: "boolean" },
|
|
11279
|
+
"review-json": { type: "string" },
|
|
11115
11280
|
force: { type: "boolean" },
|
|
11116
11281
|
help: { type: "boolean", short: "h" }
|
|
11117
11282
|
}
|
|
@@ -11193,8 +11358,11 @@ async function runPush(rawArgs, depsOverride = {}) {
|
|
|
11193
11358
|
deps.error(`Warning: no changed files found against ${base}; pushing without change metadata.`);
|
|
11194
11359
|
}
|
|
11195
11360
|
const forced = parsed.values.force === true;
|
|
11196
|
-
const gateArgs = () => ({
|
|
11361
|
+
const gateArgs = (runUrl, counts2 = NO_COUNTS) => ({
|
|
11197
11362
|
wanted: parsed.values.gate === true,
|
|
11363
|
+
reviewJson: parsed.values["review-json"],
|
|
11364
|
+
runUrl,
|
|
11365
|
+
counts: counts2,
|
|
11198
11366
|
forced,
|
|
11199
11367
|
baseUrl,
|
|
11200
11368
|
key,
|
|
@@ -11267,6 +11435,13 @@ async function runPush(rawArgs, depsOverride = {}) {
|
|
|
11267
11435
|
result = JSON.parse(body);
|
|
11268
11436
|
} catch {
|
|
11269
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
|
+
);
|
|
11270
11445
|
const runId = String(result.runId ?? "");
|
|
11271
11446
|
deps.log(runId ? `Pushed run ${runId} (${repo}${branch ? `@${branch}` : ""})` : "Pushed run.");
|
|
11272
11447
|
if (result.url) deps.log(result.url);
|
|
@@ -11294,7 +11469,7 @@ Recommended scope for this change (${recommendations.length}):`);
|
|
|
11294
11469
|
summary(`| ${cell(item.confidence)} | ${cell(item.title)} | ${cell(item.reason)} |`);
|
|
11295
11470
|
}
|
|
11296
11471
|
}
|
|
11297
|
-
return await gateIfRequested(gateArgs(), deps);
|
|
11472
|
+
return await gateIfRequested(gateArgs(result.url, counts), deps);
|
|
11298
11473
|
}
|
|
11299
11474
|
async function gateIfRequested(args, deps) {
|
|
11300
11475
|
if (!args.wanted) return EXIT_SUCCESS;
|
|
@@ -11305,14 +11480,78 @@ async function gateIfRequested(args, deps) {
|
|
|
11305
11480
|
const code = await runGate({ ...args, gitSha: args.gitSha }, deps);
|
|
11306
11481
|
return args.forced && code !== EXIT_GATE_BLOCKED ? EXIT_SUCCESS : code;
|
|
11307
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
|
+
}
|
|
11308
11543
|
async function runGate({
|
|
11309
11544
|
baseUrl,
|
|
11310
11545
|
key,
|
|
11311
11546
|
repo,
|
|
11312
11547
|
gitSha,
|
|
11313
|
-
onActions
|
|
11548
|
+
onActions,
|
|
11549
|
+
reviewJson,
|
|
11550
|
+
runUrl,
|
|
11551
|
+
counts
|
|
11314
11552
|
}, deps) {
|
|
11315
11553
|
const summary = summaryWriter(deps);
|
|
11554
|
+
const writeGateJson = (gate2) => writeReviewJson(reviewJson, gate2, { repo, gitSha, runUrl, counts }, deps);
|
|
11316
11555
|
const query = `repo=${encodeURIComponent(repo)}&sha=${encodeURIComponent(gitSha)}`;
|
|
11317
11556
|
let response;
|
|
11318
11557
|
try {
|
|
@@ -11347,6 +11586,7 @@ async function runGate({
|
|
|
11347
11586
|
deps.log(`
|
|
11348
11587
|
No release recorded for ${commit} \u2014 nothing to gate on.`);
|
|
11349
11588
|
summary("\n**Release gate: no release recorded for this commit**");
|
|
11589
|
+
writeGateJson(gate);
|
|
11350
11590
|
return EXIT_SUCCESS;
|
|
11351
11591
|
}
|
|
11352
11592
|
if (gate.status === "blocked") {
|
|
@@ -11358,11 +11598,13 @@ Release gate: BLOCKED for ${commit}`);
|
|
|
11358
11598
|
summary(`- ${reason}`);
|
|
11359
11599
|
if (onActions) deps.error(`::error::Release gate: ${reason}`);
|
|
11360
11600
|
}
|
|
11601
|
+
writeGateJson(gate);
|
|
11361
11602
|
return EXIT_GATE_BLOCKED;
|
|
11362
11603
|
}
|
|
11363
11604
|
deps.log(`
|
|
11364
11605
|
Release gate: clear for ${commit}`);
|
|
11365
11606
|
summary("\n**Release gate: clear**");
|
|
11607
|
+
writeGateJson(gate);
|
|
11366
11608
|
return EXIT_SUCCESS;
|
|
11367
11609
|
}
|
|
11368
11610
|
|
|
@@ -13589,6 +13831,10 @@ ${presetHelpLines().map((l) => ` ${l}`).join("\
|
|
|
13589
13831
|
--code-diff <path> (review) Code Diff annotation sidecar (JSON: {title, annotations: [{file, match, text, label?, scenarioIds?}]})
|
|
13590
13832
|
--patch <path> (review) Unified patch for --code-diff; generate with "git diff --histogram"
|
|
13591
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.
|
|
13592
13838
|
--emit-canonical <path> Write canonical JSON to given path
|
|
13593
13839
|
--help Show this help message
|
|
13594
13840
|
|
|
@@ -15473,9 +15719,16 @@ function writeReviewReport(review, args) {
|
|
|
15473
15719
|
fs19.mkdirSync(outputDir, { recursive: true });
|
|
15474
15720
|
const mdPath = path25.join(outputDir, `${baseName}${suffix}.md`);
|
|
15475
15721
|
const htmlPath = path25.join(outputDir, `${baseName}${suffix}.html`);
|
|
15722
|
+
const jsonPath = path25.join(outputDir, `${baseName}${suffix}.review.json`);
|
|
15476
15723
|
fs19.writeFileSync(mdPath, markdown, "utf8");
|
|
15477
15724
|
fs19.writeFileSync(htmlPath, html, "utf8");
|
|
15478
|
-
|
|
15725
|
+
fs19.writeFileSync(
|
|
15726
|
+
jsonPath,
|
|
15727
|
+
`${JSON.stringify(buildReviewJson(review), null, 2)}
|
|
15728
|
+
`,
|
|
15729
|
+
"utf8"
|
|
15730
|
+
);
|
|
15731
|
+
return [mdPath, htmlPath, jsonPath];
|
|
15479
15732
|
}
|
|
15480
15733
|
function evaluateReviewGate(review, args) {
|
|
15481
15734
|
const failures = [];
|