Files
clean-architecture-frontend…/scripts/lib/risk-coverage.ts
T

580 lines
21 KiB
TypeScript

import { open, readdir, realpath, lstat } from "node:fs/promises";
import type { Dirent, Stats } from "node:fs";
import path from "node:path";
import { normalizeRepositoryRelativePath } from "./repository-file-inventory.ts";
const coverageMetrics = [
"lines",
"statements",
"functions",
"branches",
] as const;
export const REQUIRED_HIGH_RISK_PATHS = Object.freeze([
"src/adapters/http/http-execution-v3.ts",
"src/adapters/http/request-builder.ts",
"src/adapters/http/bounded-body-reader.ts",
"src/adapters/http/bounded-json.ts",
"src/bootstrap/read-bounded-boot-json.ts",
"src/adapters/service-worker/service-worker-lifecycle.ts",
"src/adapters/query-cache/server-state-scope-runtime.ts",
"src/bootstrap/load-release-manifest.ts",
] as const);
type CoverageMetric = (typeof coverageMetrics)[number];
type Thresholds = Readonly<Partial<Record<CoverageMetric, number>>>;
type CoverageMetrics = Readonly<
Record<CoverageMetric, Readonly<{ pct: number }>>
>;
export type RiskCoveragePolicy = Readonly<{
schemaVersion: 2;
repositoryBaseline: number;
generatedPaths: readonly string[];
summary: Thresholds;
criticalModules: readonly Readonly<{
path: string;
owner: string;
minimum: Thresholds;
}>[];
highRiskPaths: readonly string[];
waivers: readonly Readonly<{
path: string;
owner: string;
reason: string;
expiresAt: string;
}>[];
}>;
export type RiskCoverageResult = Readonly<{
status: "PASS" | "FAIL";
selectedTotal: number;
repositoryTotal: number;
uncoveredModules: readonly string[];
ignoredCoveragePaths: readonly string[];
results: readonly Readonly<{
scope: string;
metric: CoverageMetric;
threshold: number;
received: number;
passed: boolean;
}>[];
failures: readonly string[];
}>;
type InventoryOptions = Readonly<{
repositoryRoot?: string;
generatedPaths?: readonly string[];
readDirectory?: (target: string) => Promise<Dirent[]>;
lstatPath?: (target: string) => Promise<Stats>;
realpathPath?: (target: string) => Promise<string>;
assertReadable?: (target: string) => Promise<void>;
}>;
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function assertExactKeys(
value: Record<string, unknown>,
allowed: readonly string[],
label: string,
): void {
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
if (unknown.length > 0) {
throw new TypeError(`${label} has unknown fields: ${unknown.sort().join(", ")}`);
}
}
function exactSourcePath(value: unknown, label: string): string {
if (
typeof value !== "string" ||
["*", "?", "[", "]", "{", "}"].some((character) =>
value.includes(character),
)
) {
throw new TypeError(`${label} must be an exact repository-relative POSIX path`);
}
const normalized = normalizeRepositoryRelativePath(value, label);
if (!normalized.startsWith("src/") || !/\.tsx?$/u.test(normalized)) {
throw new TypeError(`${label} must identify a TypeScript module below src`);
}
return normalized;
}
function nonBlank(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new TypeError(`${label} must be a nonblank string`);
}
return value.trim();
}
function uniquePaths(
value: unknown,
label: string,
options: Readonly<{ allowEmpty: boolean }> = { allowEmpty: true },
): readonly string[] {
if (!Array.isArray(value) || (!options.allowEmpty && value.length === 0)) {
throw new TypeError(`${label} must be an array${options.allowEmpty ? "" : " with at least one path"}`);
}
const paths = value.map((entry) => exactSourcePath(entry, `${label} entry`));
if (new Set(paths).size !== paths.length) {
throw new TypeError(`${label} contains a duplicate path`);
}
return Object.freeze(paths);
}
function thresholds(
value: unknown,
label: string,
options: Readonly<{ requireAll: boolean }>,
): Thresholds {
if (!isRecord(value)) {
throw new TypeError(`${label} must be an object`);
}
assertExactKeys(value, coverageMetrics, label);
if (
Object.keys(value).length === 0 ||
(options.requireAll && coverageMetrics.some((metric) => !(metric in value)))
) {
throw new TypeError(`${label} must define ${options.requireAll ? "all " : "at least one "}coverage metric`);
}
const parsed: Partial<Record<CoverageMetric, number>> = {};
for (const [metric, threshold] of Object.entries(value)) {
if (
typeof threshold !== "number" ||
!Number.isFinite(threshold) ||
threshold < 0 ||
threshold > 100
) {
throw new TypeError(`${label}.${metric} minimum must be a finite number from 0 to 100`);
}
parsed[metric as CoverageMetric] = threshold;
}
return Object.freeze(parsed);
}
export function parseRiskCoveragePolicy(
value: unknown,
options: Readonly<{ now?: number }> = {},
): RiskCoveragePolicy {
if (!isRecord(value)) {
throw new TypeError("risk coverage policy must be an object");
}
assertExactKeys(
value,
[
"schemaVersion",
"repositoryBaseline",
"generatedPaths",
"summary",
"criticalModules",
"highRiskPaths",
"waivers",
],
"risk coverage policy",
);
if (value.schemaVersion !== 2) {
throw new TypeError("risk coverage policy schemaVersion must be 2");
}
if (
typeof value.repositoryBaseline !== "number" ||
!Number.isInteger(value.repositoryBaseline) ||
value.repositoryBaseline <= 0
) {
throw new TypeError("repositoryBaseline must be a positive integer");
}
const generatedPaths = uniquePaths(value.generatedPaths, "generatedPaths");
const highRiskPaths = uniquePaths(value.highRiskPaths, "highRiskPaths", {
allowEmpty: false,
});
if (!Array.isArray(value.criticalModules) || value.criticalModules.length === 0) {
throw new TypeError("criticalModules must be a non-empty array");
}
const criticalModules = value.criticalModules.map((candidate, index) => {
if (!isRecord(candidate)) {
throw new TypeError(`criticalModules[${index}] must be an object`);
}
assertExactKeys(candidate, ["path", "owner", "minimum"], `criticalModules[${index}]`);
return Object.freeze({
path: exactSourcePath(candidate.path, `criticalModules[${index}].path`),
owner: nonBlank(candidate.owner, `criticalModules[${index}].owner`),
minimum: thresholds(candidate.minimum, `criticalModules[${index}].minimum`, {
requireAll: false,
}),
});
});
if (new Set(criticalModules.map((entry) => entry.path)).size !== criticalModules.length) {
throw new TypeError("criticalModules contains a duplicate path");
}
if (!Array.isArray(value.waivers)) {
throw new TypeError("waivers must be an array");
}
const currentTime = options.now ?? Date.now();
const waivers = value.waivers.map((candidate, index) => {
if (!isRecord(candidate)) {
throw new TypeError(`waivers[${index}] must be an object`);
}
assertExactKeys(candidate, ["path", "owner", "reason", "expiresAt"], `waivers[${index}]`);
const waiverPath = exactSourcePath(candidate.path, `waivers[${index}].path`);
const expiresAt = nonBlank(candidate.expiresAt, `waivers[${index}].expiresAt`);
const expiry = Date.parse(expiresAt);
if (!Number.isFinite(expiry) || expiry <= currentTime) {
throw new TypeError(`waivers[${index}] is expired or has an invalid expiry`);
}
if (!highRiskPaths.includes(waiverPath)) {
throw new TypeError(`waivers[${index}] is stale because ${waiverPath} is not high-risk`);
}
return Object.freeze({
path: waiverPath,
owner: nonBlank(candidate.owner, `waivers[${index}].owner`),
reason: nonBlank(candidate.reason, `waivers[${index}].reason`),
expiresAt,
});
});
if (new Set(waivers.map((entry) => entry.path)).size !== waivers.length) {
throw new TypeError("waivers contains a duplicate path");
}
return Object.freeze({
schemaVersion: 2,
repositoryBaseline: value.repositoryBaseline,
generatedPaths,
summary: thresholds(value.summary, "summary", { requireAll: true }),
criticalModules: Object.freeze(criticalModules),
highRiskPaths,
waivers: Object.freeze(waivers),
});
}
export function parseRepositoryRiskCoveragePolicy(
value: unknown,
options: Readonly<{ now?: number }> = {},
): RiskCoveragePolicy {
const policy = parseRiskCoveragePolicy(value, options);
for (const requiredPath of REQUIRED_HIGH_RISK_PATHS) {
if (!policy.highRiskPaths.includes(requiredPath)) {
throw new TypeError(`required high-risk path is missing: ${requiredPath}`);
}
}
return policy;
}
async function defaultAssertReadable(target: string): Promise<void> {
const handle = await open(target, "r");
await handle.close();
}
function isWithin(root: string, target: string): boolean {
const relative = path.relative(root, target);
return (
relative === "" ||
(relative !== ".." &&
!relative.startsWith(`..${path.sep}`) &&
!path.isAbsolute(relative))
);
}
function isProductionModule(relativePath: string): boolean {
return (
/\.tsx?$/u.test(relativePath) &&
!/\.d\.ts$/u.test(relativePath) &&
!/\.stories\.tsx?$/u.test(relativePath)
);
}
export async function buildProductionModuleInventory(
options: InventoryOptions = {},
): Promise<readonly string[]> {
const repositoryRoot = path.resolve(options.repositoryRoot ?? process.cwd());
const sourceRoot = path.join(repositoryRoot, "src");
const readDirectory = options.readDirectory ?? ((target) => readdir(target, { withFileTypes: true }));
const lstatPath = options.lstatPath ?? lstat;
const realpathPath = options.realpathPath ?? realpath;
const assertReadable = options.assertReadable ?? defaultAssertReadable;
const generatedPaths = uniquePaths(options.generatedPaths ?? [], "generatedPaths");
const generated = new Set(generatedPaths);
const repositoryRealpath = await realpathPath(repositoryRoot);
const sourceMetadata = await lstatPath(sourceRoot);
if (!sourceMetadata.isDirectory() || sourceMetadata.isSymbolicLink()) {
throw new TypeError("production source root is not a regular directory: src");
}
const sourceRealpath = await realpathPath(sourceRoot);
if (!isWithin(repositoryRealpath, sourceRealpath)) {
throw new TypeError("production source root is outside repository");
}
const allModules: string[] = [];
async function visit(relativeDirectory: string): Promise<void> {
const absoluteDirectory = path.join(repositoryRoot, relativeDirectory);
const entries = await readDirectory(absoluteDirectory);
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
const relativeTarget = normalizeRepositoryRelativePath(
`${relativeDirectory}/${entry.name}`,
"production inventory path",
);
const absoluteTarget = path.join(repositoryRoot, relativeTarget);
const metadata = await lstatPath(absoluteTarget);
if (metadata.isSymbolicLink() || entry.isSymbolicLink()) {
throw new TypeError(`production inventory path is a symlink: ${relativeTarget}`);
}
if (metadata.isDirectory()) {
await visit(relativeTarget);
continue;
}
if (!isProductionModule(relativeTarget)) continue;
if (!metadata.isFile()) {
throw new TypeError(`production inventory path is not a regular file: ${relativeTarget}`);
}
const resolvedTarget = await realpathPath(absoluteTarget);
if (!isWithin(repositoryRealpath, resolvedTarget)) {
throw new TypeError(`production inventory path is outside repository: ${relativeTarget}`);
}
try {
await assertReadable(absoluteTarget);
} catch (error) {
throw new Error(`production inventory file is unreadable: ${relativeTarget}`, {
cause: error,
});
}
allModules.push(relativeTarget);
}
}
await visit("src");
for (const generatedPath of generatedPaths) {
if (!allModules.includes(generatedPath)) {
throw new TypeError(`generated path is stale or not a production module: ${generatedPath}`);
}
}
const inventory = allModules.filter((file) => !generated.has(file)).sort();
if (inventory.length === 0) {
throw new Error("production module inventory is empty");
}
if (new Set(inventory).size !== inventory.length) {
throw new TypeError("production module inventory contains a duplicate path");
}
return Object.freeze(inventory);
}
function coveragePath(repositoryRoot: string, rawPath: string): string {
if (rawPath.includes("\\") || rawPath.includes("\0")) {
throw new TypeError("coverage path must use POSIX separators");
}
if (path.isAbsolute(rawPath)) {
const relative = path.relative(repositoryRoot, rawPath).split(path.sep).join("/");
if (!relative || relative === ".." || relative.startsWith("../")) {
throw new TypeError(`coverage path is outside repository: ${rawPath}`);
}
return normalizeRepositoryRelativePath(relative, "coverage path");
}
return normalizeRepositoryRelativePath(rawPath, "coverage path");
}
function parseCoverageMetrics(value: unknown, label: string): CoverageMetrics {
if (!isRecord(value)) throw new TypeError(`${label} must be an object`);
const unknownMetrics = Object.keys(value).filter(
(metric) =>
metric !== "branchesTrue" &&
!coverageMetrics.includes(metric as CoverageMetric),
);
if (unknownMetrics.length > 0) {
throw new TypeError(
`${label} has unknown coverage metric keys: ${unknownMetrics.sort().join(", ")}`,
);
}
if (value.branchesTrue !== undefined) {
if (!isRecord(value.branchesTrue)) {
throw new TypeError(`${label}.branchesTrue must be an object`);
}
assertExactKeys(
value.branchesTrue,
["total", "covered", "skipped", "pct"],
`${label}.branchesTrue`,
);
for (const count of ["total", "covered", "skipped"] as const) {
const received = value.branchesTrue[count];
if (
typeof received !== "number" ||
!Number.isSafeInteger(received) ||
received < 0
) {
throw new TypeError(
`${label}.branchesTrue.${count} must be a nonnegative safe integer`,
);
}
}
const pct = value.branchesTrue.pct;
if (typeof pct !== "number" || !Number.isFinite(pct) || pct < 0 || pct > 100) {
throw new TypeError(
`${label}.branchesTrue.pct must be a finite number from 0 to 100`,
);
}
if (
(value.branchesTrue.covered as number) >
(value.branchesTrue.total as number) ||
(value.branchesTrue.skipped as number) >
(value.branchesTrue.total as number)
) {
throw new TypeError(`${label}.branchesTrue counts exceed total`);
}
}
const parsed = {} as Record<CoverageMetric, { pct: number }>;
for (const metric of coverageMetrics) {
const rawMetric = value[metric];
if (!isRecord(rawMetric)) {
throw new TypeError(`${label}.${metric} must be an object`);
}
assertExactKeys(
rawMetric,
["total", "covered", "skipped", "pct"],
`${label}.${metric}`,
);
const suppliedCounts = ["total", "covered", "skipped"].filter(
(count) => rawMetric[count] !== undefined,
);
if (suppliedCounts.length !== 0 && suppliedCounts.length !== 3) {
throw new TypeError(
`${label}.${metric} must define total, covered, and skipped together`,
);
}
for (const count of suppliedCounts) {
const received = rawMetric[count];
if (
typeof received !== "number" ||
!Number.isSafeInteger(received) ||
received < 0
) {
throw new TypeError(
`${label}.${metric}.${count} must be a nonnegative safe integer`,
);
}
}
if (
suppliedCounts.length === 3 &&
((rawMetric.covered as number) > (rawMetric.total as number) ||
(rawMetric.skipped as number) > (rawMetric.total as number))
) {
throw new TypeError(`${label}.${metric} counts exceed total`);
}
const pct = rawMetric.pct;
if (typeof pct !== "number" || !Number.isFinite(pct) || pct < 0 || pct > 100) {
throw new TypeError(`${label}.${metric}.pct must be a finite number from 0 to 100`);
}
parsed[metric] = { pct };
}
return Object.freeze(parsed);
}
export function evaluateRiskCoverage(input: Readonly<{
repositoryRoot?: string;
inventory: readonly string[];
policy: RiskCoveragePolicy;
summary: unknown;
changedFiles?: readonly string[];
now?: number;
}>): RiskCoverageResult {
const repositoryRoot = path.resolve(input.repositoryRoot ?? process.cwd());
if (input.inventory.length === 0) {
throw new TypeError("production module inventory is empty");
}
const inventory = input.inventory.map((file) => exactSourcePath(file, "inventory path"));
if (new Set(inventory).size !== inventory.length) {
throw new TypeError("production module inventory contains a duplicate path");
}
if (!isRecord(input.summary) || !("total" in input.summary)) {
throw new TypeError("coverage summary must contain total metrics");
}
const totalMetrics = parseCoverageMetrics(input.summary.total, "coverage total");
const selected = new Map<string, CoverageMetrics>();
for (const [rawPath, rawMetrics] of Object.entries(input.summary)) {
if (rawPath === "total") continue;
const normalized = coveragePath(repositoryRoot, rawPath);
if (selected.has(normalized)) {
throw new TypeError(`duplicate coverage path: ${normalized}`);
}
selected.set(normalized, parseCoverageMetrics(rawMetrics, `coverage ${normalized}`));
}
const failures: string[] = [];
const results: Array<{
scope: string;
metric: CoverageMetric;
threshold: number;
received: number;
passed: boolean;
}> = [];
function evaluate(scope: string, actual: CoverageMetrics, minimum: Thresholds): void {
for (const metric of coverageMetrics) {
const threshold = minimum[metric];
if (threshold === undefined) continue;
const received = actual[metric].pct;
const passed = received >= threshold;
results.push({ scope, metric, threshold, received, passed });
if (!passed) {
failures.push(`${scope}.${metric} expected >= ${threshold}, received ${received}`);
}
}
}
evaluate("total", totalMetrics, input.policy.summary);
const inventorySet = new Set(inventory);
if (inventory.length < input.policy.repositoryBaseline) {
failures.push(
`repository module baseline expected >= ${input.policy.repositoryBaseline}, received ${inventory.length}`,
);
}
const uncoveredModules = inventory.filter((file) => !selected.has(file)).sort();
const selectedModules = inventory.filter((file) => selected.has(file));
const ignoredCoveragePaths = [...selected.keys()]
.filter((file) => !inventorySet.has(file))
.sort();
failures.push(
...uncoveredModules.map((file) => `production module missing from coverage: ${file}`),
);
for (const modulePolicy of input.policy.criticalModules) {
if (!inventorySet.has(modulePolicy.path)) {
failures.push(`critical module is outside production inventory: ${modulePolicy.path}`);
continue;
}
const actual = selected.get(modulePolicy.path);
if (!actual) {
failures.push(`critical module missing from coverage: ${modulePolicy.path}`);
continue;
}
evaluate(modulePolicy.path, actual, modulePolicy.minimum);
}
const owners = new Set(input.policy.criticalModules.map((entry) => entry.path));
const waivers = new Set(input.policy.waivers.map((entry) => entry.path));
const highRisk = new Set(input.policy.highRiskPaths);
for (const highRiskPath of input.policy.highRiskPaths) {
if (!owners.has(highRiskPath) && !waivers.has(highRiskPath)) {
failures.push(`high-risk module has no owner or waiver: ${highRiskPath}`);
}
}
for (const changedFile of input.changedFiles ?? []) {
const normalized = exactSourcePath(changedFile, "changed file");
if (highRisk.has(normalized) && !owners.has(normalized) && !waivers.has(normalized)) {
failures.push(`changed high-risk module has no owner or waiver: ${normalized}`);
}
}
for (const waiver of input.policy.waivers) {
if (!inventorySet.has(waiver.path) || !highRisk.has(waiver.path)) {
failures.push(`coverage waiver is stale: ${waiver.path}`);
}
const expiry = Date.parse(waiver.expiresAt);
if (!Number.isFinite(expiry) || expiry <= (input.now ?? Date.now())) {
failures.push(`coverage waiver is expired: ${waiver.path}`);
}
}
return Object.freeze({
status: failures.length === 0 ? "PASS" : "FAIL",
selectedTotal: selectedModules.length,
repositoryTotal: inventory.length,
uncoveredModules: Object.freeze(uncoveredModules),
ignoredCoveragePaths: Object.freeze(ignoredCoveragePaths),
results: Object.freeze(results),
failures: Object.freeze(failures),
});
}