fix: measure repository-wide risk coverage

This commit is contained in:
DongHyeonka
2026-08-02 07:52:46 +09:00
parent f487823442
commit 5a73f7a1b5
11 changed files with 1361 additions and 242 deletions
+65 -75
View File
@@ -1,100 +1,90 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
type CoverageMetrics = Record<string, { pct: number }>;
type CoveragePolicy = Readonly<{
summary: Record<string, number>;
criticalModules: readonly Readonly<{
path: string;
minimum: Record<string, number>;
}>[];
}>;
type CoverageSummary = Record<string, CoverageMetrics>;
type CoverageResult = Readonly<{
scope: string;
metric: string;
threshold: number;
received: number | undefined;
passed: boolean;
}>;
import {
buildProductionModuleInventory,
evaluateRiskCoverage,
parseRepositoryRiskCoveragePolicy,
} from "./lib/risk-coverage.ts";
function argumentValue(name: string, fallback: string): string {
function argumentValue(name: string, fallback?: string): string | undefined {
const index = process.argv.indexOf(name);
return index >= 0 && process.argv[index + 1]
? process.argv[index + 1]
: fallback;
return index >= 0 ? process.argv[index + 1] : fallback;
}
const policyPath = argumentValue(
function requiredArgument(name: string, fallback: string): string {
const value = argumentValue(name, fallback);
if (!value) throw new TypeError(`${name} requires a path`);
return value;
}
function parseChangedFiles(value: unknown): readonly string[] {
if (
!Array.isArray(value) ||
value.some((entry) => typeof entry !== "string") ||
new Set(value).size !== value.length
) {
throw new TypeError("changed files input must be an array of unique paths");
}
return Object.freeze([...value] as string[]);
}
const repositoryRoot = path.resolve(
requiredArgument("--repository-root", process.cwd()),
);
const policyPath = requiredArgument(
"--policy",
"config/testing/risk-coverage.json",
);
const summaryPath = argumentValue(
const summaryPath = requiredArgument(
"--summary",
"artifacts/tests/coverage/coverage-summary.json",
);
const artifactPath = argumentValue(
const artifactPath = requiredArgument(
"--artifact",
"artifacts/quality/risk-coverage.json",
);
const policy = JSON.parse(
await readFile(policyPath, "utf8"),
) as CoveragePolicy;
const summary = JSON.parse(
await readFile(summaryPath, "utf8"),
) as CoverageSummary;
const failures: string[] = [];
const results: CoverageResult[] = [];
function evaluate(
scope: string,
actual: CoverageMetrics,
minimum: Record<string, number>,
): void {
for (const [metric, threshold] of Object.entries(minimum)) {
const received = actual?.[metric]?.pct;
const passed =
typeof received === "number" &&
Number.isFinite(received) &&
received >= threshold;
results.push({ scope, metric, threshold, received, passed });
if (!passed) {
failures.push(
`${scope}.${metric} expected >= ${threshold}, received ${String(received)}`,
);
}
}
}
evaluate("total", summary.total, policy.summary);
for (const modulePolicy of policy.criticalModules) {
const key = Object.keys(summary).find(
(candidate) =>
candidate !== "total" &&
candidate.replaceAll("\\", "/").endsWith(`/${modulePolicy.path}`),
);
if (!key) {
failures.push(`critical module missing from coverage: ${modulePolicy.path}`);
continue;
}
evaluate(modulePolicy.path, summary[key], modulePolicy.minimum);
}
const changedFilesPath = argumentValue("--changed-files");
const policy = parseRepositoryRiskCoveragePolicy(
JSON.parse(await readFile(path.resolve(repositoryRoot, policyPath), "utf8")) as unknown,
);
const inventory = await buildProductionModuleInventory({
repositoryRoot,
generatedPaths: policy.generatedPaths,
});
const changedFiles = changedFilesPath
? parseChangedFiles(
JSON.parse(
await readFile(path.resolve(repositoryRoot, changedFilesPath), "utf8"),
) as unknown,
)
: [];
const result = evaluateRiskCoverage({
repositoryRoot,
inventory,
policy,
summary: JSON.parse(
await readFile(path.resolve(repositoryRoot, summaryPath), "utf8"),
) as unknown,
changedFiles,
});
const artifact = {
schemaVersion: 1,
schemaVersion: 2,
policy: policyPath,
summary: summaryPath,
status: failures.length === 0 ? "PASS" : "FAIL",
results,
failures,
changedFiles: changedFilesPath ?? null,
...result,
};
await mkdir(path.dirname(artifactPath), { recursive: true });
await writeFile(artifactPath, `${JSON.stringify(artifact, null, 2)}\n`);
const resolvedArtifactPath = path.resolve(repositoryRoot, artifactPath);
await mkdir(path.dirname(resolvedArtifactPath), { recursive: true });
await writeFile(resolvedArtifactPath, `${JSON.stringify(artifact, null, 2)}\n`);
if (failures.length > 0) {
process.stderr.write(`Risk coverage failed:\n- ${failures.join("\n- ")}\n`);
if (result.failures.length > 0) {
process.stderr.write(
`Risk coverage failed:\n- ${result.failures.join("\n- ")}\n`,
);
process.exit(1);
}
process.stdout.write(
`Risk coverage: PASS (${results.length} scoped thresholds)\n`,
`Risk coverage: PASS (${result.selectedTotal}/${result.repositoryTotal} production modules, ${result.results.length} thresholds)\n`,
);