Swadhin Biswas Feature/ai code reviewer (#79) · 8/16/2026
cfd400d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/**
* OpenAPI Coverage Audit (WS5-01)
*
* Measures how much of the REST surface (src/pages/api/**) is documented
* in the OpenAPI spec (src/lib/openapi.ts).
*
* Route-file → spec-path mapping follows the existing parity-test
* convention: `[param]` → `{param}`, `index` → parent path.
*
* Usage:
* bun scripts/openapi-coverage.ts # report
* bun scripts/openapi-coverage.ts --fail # exit 1 below threshold
*
* Env:
* OPENAPI_COVERAGE_THRESHOLD — minimum path coverage % (default 60)
*/
import { readdirSync, statSync } from "node:fs";
import { join, relative } from "node:path";
import { openApiSpec } from "@/lib/openapi";
const rootDir = join(import.meta.dirname, "..");
const API_ROOT = join(rootDir, "src", "pages", "api");
const FAIL_MODE = process.argv.includes("--fail");
const THRESHOLD = parseInt(process.env.OPENAPI_COVERAGE_THRESHOLD || "60", 10);
function collectRouteFiles(dir: string): string[] {
const files: string[] = [];
const stack = [dir];
while (stack.length > 0) {
const current = stack.pop()!;
for (const entry of readdirSync(current)) {
const full = join(current, entry);
const stats = statSync(full);
if (stats.isDirectory()) {
stack.push(full);
} else if (entry.endsWith(".ts") && !entry.endsWith(".test.ts")) {
files.push(full);
}
}
}
return files;
}
function routeToSpecPath(routeFile: string): string {
const relativePath = relative(API_ROOT, routeFile).replace(/\.ts$/, "");
const segments = relativePath.split("/");
// Find the param markers and rebuild the URL-style path
let path = "/" + segments.map((s) => s.replace(/\[([^\]]+)\]/g, "{$1}")).join("/");
// index files map to the parent directory path
if (path.endsWith("/index")) path = path.slice(0, -"/index".length);
if (path === "") path = "/";
return path;
}
const routeFiles = collectRouteFiles(API_ROOT);
const specPaths = new Set(Object.keys(openApiSpec.paths ?? {}));
// Also canonicalize spec paths ({owner} → {}) for loose matching
const specPathVariants = new Set<string>();
for (const p of specPaths) {
specPathVariants.add(p);
specPathVariants.add(p.replace(/\{[^}]+\}/g, "{}"));
}
const covered: string[] = [];
const missing: Array<{ file: string; path: string }> = [];
for (const file of routeFiles) {
const path = routeToSpecPath(file);
const canonical = path.replace(/\{[^}]+\}/g, "{}");
const isCovered =
specPaths.has(path) || specPathVariants.has(canonical);
if (isCovered) covered.push(file);
else missing.push({ file: relative(API_ROOT, file), path });
}
const pct = Math.round((covered.length / routeFiles.length) * 1000) / 10;
console.log("═".repeat(72));
console.log(" OPENAPI COVERAGE AUDIT");
console.log("═".repeat(72));
console.log(` route files: ${routeFiles.length}`);
console.log(` documented: ${covered.length} (${pct}%)`);
console.log(` undocumented: ${missing.length}`);
console.log(` spec paths total: ${specPaths.size}`);
console.log(` threshold: ${THRESHOLD}%`);
if (missing.length > 0) {
console.log("\n── Undocumented routes ────────────────────────────");
const byArea = new Map<string, string[]>();
for (const m of missing) {
const area = m.path.split("/")[2] || "root";
byArea.set(area, [...(byArea.get(area) || []), m.path]);
}
for (const [area, paths] of byArea) {
console.log(`\n ${area}/ (${paths.length})`);
for (const p of paths.slice(0, 12)) console.log(` ${p}`);
if (paths.length > 12) console.log(` … and ${paths.length - 12} more`);
}
}
const reportPath = join(rootDir, "test-results", "openapi-coverage.json");
const { writeFileSync } = await import("node:fs");
writeFileSync(
reportPath,
JSON.stringify({ date: new Date().toISOString(), pct, covered: covered.length, total: routeFiles.length, missing: missing.map((m) => m.path) }, null, 2),
);
console.log(`\nReport: ${relative(rootDir, reportPath)}`);
if (FAIL_MODE && pct < THRESHOLD) {
console.error(`\n❌ OpenAPI coverage ${pct}% is below the ${THRESHOLD}% threshold.`);
console.error(" Document missing routes in src/lib/openapi.ts.");
process.exit(1);
}
console.log(`\n✅ OpenAPI coverage ${pct}% (threshold ${THRESHOLD}%)`);