fix: measure repository-wide risk coverage
This commit is contained in:
@@ -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`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user