Files
self/scripts/audit/tech-debt-baseline.mjs
Justin Hernandez a632727fee Add tech debt baseline snapshot generator and baseline docs (#1743)
* Add cruft baseline snapshot

* pr feedback

* rename cruft to tech deb

* improve baseline
2026-02-12 19:29:56 -08:00

326 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
import { promises as fs } from 'fs';
import path from 'path';
const ROOT_DIR = process.cwd();
const ROOT_PACKAGE_JSON_PATH = path.join(ROOT_DIR, 'package.json');
const OUTPUT_JSON_PATH = path.join(
ROOT_DIR,
'docs',
'maintenance',
'tech-debt-baseline.json',
);
const OUTPUT_MARKDOWN_PATH = path.join(
ROOT_DIR,
'docs',
'maintenance',
'tech-debt-baseline.md',
);
const IGNORED_DIRECTORIES = new Set([
'__generated__',
'.cache',
'.git',
'.gradle',
'.next',
'.turbo',
'.yarn',
'android',
'artifacts',
'build',
'cache',
'Carthage',
'coverage',
'DerivedData',
'dist',
'generated',
'ios',
'node_modules',
'out',
'Pods',
'typechain-types',
'vendor',
]);
const SOURCE_EXTENSIONS = new Set([
'.cjs',
'.circom',
'.css',
'.go',
'.h',
'.hpp',
'.java',
'.js',
'.jsx',
'.kt',
'.kts',
'.mjs',
'.noir',
'.py',
'.rb',
'.rs',
'.sh',
'.sol',
'.swift',
'.ts',
'.tsx',
'.vue',
]);
function sortObjectKeys(obj = {}) {
const sortedEntries = Object.entries(obj).sort(([a], [b]) =>
a.localeCompare(b),
);
return Object.fromEntries(sortedEntries);
}
function wildcardToRegex(segment) {
const escaped = segment
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '[^/]*');
return new RegExp(`^${escaped}$`);
}
async function expandWorkspacePattern(rootDir, pattern) {
const segments = pattern.split('/').filter(Boolean);
async function walkSegments(currentDir, segmentIndex) {
if (segmentIndex >= segments.length) {
return [currentDir];
}
const currentSegment = segments[segmentIndex];
const hasWildcard = currentSegment.includes('*');
if (!hasWildcard) {
const nextDir = path.join(currentDir, currentSegment);
try {
const stat = await fs.stat(nextDir);
if (!stat.isDirectory()) return [];
} catch {
return [];
}
return walkSegments(nextDir, segmentIndex + 1);
}
const matcher = wildcardToRegex(currentSegment);
const entries = await fs.readdir(currentDir, { withFileTypes: true });
const matches = entries
.filter(entry => entry.isDirectory() && matcher.test(entry.name))
.map(entry => path.join(currentDir, entry.name));
const expanded = await Promise.all(
matches.map(matchedDir => walkSegments(matchedDir, segmentIndex + 1)),
);
return expanded.flat();
}
return walkSegments(rootDir, 0);
}
async function getWorkspaceDirectories(rootDir, workspacePatterns) {
const allMatches = await Promise.all(
workspacePatterns.map(pattern => expandWorkspacePattern(rootDir, pattern)),
);
const candidateDirs = [...new Set(allMatches.flat())];
const workspaceDirs = [];
for (const dir of candidateDirs) {
const packageJsonPath = path.join(dir, 'package.json');
try {
await fs.access(packageJsonPath);
workspaceDirs.push(dir);
} catch {
// Skip directories without package.json.
}
}
return workspaceDirs.sort((a, b) => a.localeCompare(b));
}
async function collectSourceFileCounts(workspaceDir) {
const extensionCounts = {};
async function walk(currentDir) {
const entries = await fs.readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
if (IGNORED_DIRECTORIES.has(entry.name)) continue;
await walk(fullPath);
continue;
}
if (!entry.isFile()) continue;
const extension = path.extname(entry.name).toLowerCase();
if (!SOURCE_EXTENSIONS.has(extension)) continue;
extensionCounts[extension] = (extensionCounts[extension] || 0) + 1;
}
}
await walk(workspaceDir);
const sortedExtensionCounts = sortObjectKeys(extensionCounts);
const totalSourceFiles = Object.values(sortedExtensionCounts).reduce(
(sum, count) => sum + count,
0,
);
return { extensionCounts: sortedExtensionCounts, totalSourceFiles };
}
function buildMarkdownReport(report) {
const lines = [];
const topLargest = [...report.workspaces]
.sort((a, b) => b.sourceFiles.total - a.sourceFiles.total)
.slice(0, 10);
const noTestScript = report.workspaces.filter(
workspace => !workspace.scripts.includes('test'),
);
const averageDeps =
report.workspaces.reduce((sum, ws) => sum + ws.dependencyCount.total, 0) /
Math.max(report.workspaces.length, 1);
const variance =
report.workspaces.reduce(
(sum, ws) => sum + (ws.dependencyCount.total - averageDeps) ** 2,
0,
) / Math.max(report.workspaces.length, 1);
const standardDeviation = Math.sqrt(variance);
const unusualThreshold = Math.max(
50,
Math.round(averageDeps + standardDeviation),
);
const unusuallyLargeDeps = report.workspaces.filter(
workspace => workspace.dependencyCount.total >= unusualThreshold,
);
lines.push('# Tech Debt Baseline Snapshot');
lines.push('');
lines.push(
'Generated from `package.json` workspaces. This file is intended as an immutable baseline for cleanup PRs.',
);
lines.push('');
lines.push('## Top 10 largest workspaces by source-file count');
lines.push('');
for (const workspace of topLargest) {
lines.push(
`- \`${workspace.path}\` (${workspace.sourceFiles.total} source files, ${workspace.dependencyCount.total} deps)`,
);
}
lines.push('');
lines.push('## Workspaces with no `test` script');
lines.push('');
if (noTestScript.length === 0) {
lines.push('- None');
} else {
for (const workspace of noTestScript) {
lines.push(`- \`${workspace.path}\``);
}
}
lines.push('');
lines.push('## Workspaces with unusually large dependency sets');
lines.push('');
lines.push(
`- Threshold: >= ${unusualThreshold} total dependencies (mean + 1σ, minimum 50).`,
);
if (unusuallyLargeDeps.length === 0) {
lines.push('- None');
} else {
for (const workspace of unusuallyLargeDeps) {
lines.push(
`- \`${workspace.path}\`: ${workspace.dependencyCount.total} total (${workspace.dependencyCount.dependencies} deps, ${workspace.dependencyCount.devDependencies} devDeps, ${workspace.dependencyCount.peerDependencies} peerDeps)`,
);
}
}
lines.push('');
return `${lines.join('\n')}\n`;
}
async function main() {
const rootPackageJson = JSON.parse(
await fs.readFile(ROOT_PACKAGE_JSON_PATH, 'utf8'),
);
const workspacePatterns = rootPackageJson.workspaces?.packages;
if (!Array.isArray(workspacePatterns) || workspacePatterns.length === 0) {
throw new Error('Root package.json does not define workspaces.packages.');
}
const workspaceDirs = await getWorkspaceDirectories(
ROOT_DIR,
workspacePatterns,
);
const workspaces = [];
for (const workspaceDir of workspaceDirs) {
const packageJsonPath = path.join(workspaceDir, 'package.json');
const packageData = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
const relativePath = path.relative(ROOT_DIR, workspaceDir) || '.';
const sourceFiles = await collectSourceFileCounts(workspaceDir);
const dependencies = sortObjectKeys(packageData.dependencies || {});
const devDependencies = sortObjectKeys(packageData.devDependencies || {});
const peerDependencies = sortObjectKeys(packageData.peerDependencies || {});
workspaces.push({
name: packageData.name || relativePath,
path: relativePath,
dependencies,
devDependencies,
peerDependencies,
dependencyCount: {
dependencies: Object.keys(dependencies).length,
devDependencies: Object.keys(devDependencies).length,
peerDependencies: Object.keys(peerDependencies).length,
total:
Object.keys(dependencies).length +
Object.keys(devDependencies).length +
Object.keys(peerDependencies).length,
},
scripts: Object.keys(packageData.scripts || {}).sort((a, b) =>
a.localeCompare(b),
),
sourceFiles: {
byExtension: sourceFiles.extensionCounts,
total: sourceFiles.totalSourceFiles,
},
});
}
const report = {
workspacePatterns,
workspaceCount: workspaces.length,
workspaces,
};
const markdown = buildMarkdownReport(report);
await fs.mkdir(path.dirname(OUTPUT_JSON_PATH), { recursive: true });
await fs.writeFile(OUTPUT_JSON_PATH, `${JSON.stringify(report, null, 2)}\n`);
await fs.writeFile(OUTPUT_MARKDOWN_PATH, markdown);
console.log(`Wrote ${path.relative(ROOT_DIR, OUTPUT_JSON_PATH)}`);
console.log(`Wrote ${path.relative(ROOT_DIR, OUTPUT_MARKDOWN_PATH)}`);
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});