110 lines
3.5 KiB
JavaScript
110 lines
3.5 KiB
JavaScript
|
|
/**
|
||
|
|
* Structural conformance gate.
|
||
|
|
*
|
||
|
|
* Counts production-source code-smells and compares against committed baselines.
|
||
|
|
* Nonzero exit code = a baseline was violated. The script does NOT alter
|
||
|
|
* files; it only audits and reports.
|
||
|
|
*
|
||
|
|
* Usage: node scripts/check-structural.js [--fix-baseline]
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { readFileSync, writeFileSync } from 'fs'
|
||
|
|
import { fileURLToPath } from 'url'
|
||
|
|
import glob from 'glob'
|
||
|
|
import path from 'path'
|
||
|
|
|
||
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||
|
|
const ROOT = path.resolve(__dirname, '..')
|
||
|
|
const BASELINE_PATH = path.join(ROOT, 'scripts', '.structural-baseline.json')
|
||
|
|
|
||
|
|
const PACKAGE_DIRS = glob.sync('packages/*', { cwd: ROOT, absolute: true })
|
||
|
|
.filter(d => !d.match(/(imhotep-fixtures|imhotep-bench|imhotep-cli)$/))
|
||
|
|
|
||
|
|
function srcFiles(pkgDir) {
|
||
|
|
return glob.sync('src/**/*.ts', { cwd: pkgDir, absolute: true })
|
||
|
|
.filter(f => !f.endsWith('.test.ts') && !f.endsWith('.d.ts'))
|
||
|
|
}
|
||
|
|
|
||
|
|
function countPattern(files, pattern, excludeComment = false) {
|
||
|
|
let count = 0
|
||
|
|
const locations = []
|
||
|
|
for (const f of files) {
|
||
|
|
const lines = readFileSync(f, 'utf8').split('\n')
|
||
|
|
for (let i = 0; i < lines.length; i++) {
|
||
|
|
const line = lines[i]
|
||
|
|
if (excludeComment && line.trim().startsWith('//')) continue
|
||
|
|
const matches = line.match(pattern)
|
||
|
|
if (matches) {
|
||
|
|
count += matches.length
|
||
|
|
locations.push(`${path.relative(ROOT, f)}:${i + 1}`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return { count, locations }
|
||
|
|
}
|
||
|
|
|
||
|
|
const allFiles = PACKAGE_DIRS.flatMap(d => srcFiles(d))
|
||
|
|
const pkgNames = PACKAGE_DIRS.map(d => path.basename(d))
|
||
|
|
|
||
|
|
const violations = {
|
||
|
|
asAny: countPattern(allFiles, /\bas any\b/g, true),
|
||
|
|
emptyCatch: countPattern(allFiles, /\bcatch\s*\{/),
|
||
|
|
nullishZero: countPattern(allFiles, /\?\?\s*0(?![.\d])/, true),
|
||
|
|
}
|
||
|
|
|
||
|
|
const baseline = (() => {
|
||
|
|
try { return JSON.parse(readFileSync(BASELINE_PATH, 'utf8')) }
|
||
|
|
catch { return {} }
|
||
|
|
})()
|
||
|
|
|
||
|
|
const results = {
|
||
|
|
timestamp: new Date().toISOString(),
|
||
|
|
packages: pkgNames,
|
||
|
|
violations,
|
||
|
|
baseline,
|
||
|
|
}
|
||
|
|
|
||
|
|
const errors = []
|
||
|
|
|
||
|
|
for (const [key, current] of Object.entries(violations)) {
|
||
|
|
const allowed = baseline[key] ?? 0
|
||
|
|
const delta = current.count - allowed
|
||
|
|
const status = delta <= 0 ? 'PASS' : 'FAIL'
|
||
|
|
const msg = ` ${key.padEnd(18)} current=${String(current.count).padStart(3)} baseline=${String(allowed).padStart(3)} delta=${String(delta).padStart(4)} ${status}`
|
||
|
|
console.log(msg)
|
||
|
|
if (status === 'FAIL') {
|
||
|
|
errors.push(`${key} (${current.count} > baseline ${allowed})`)
|
||
|
|
console.log(` New locations:`)
|
||
|
|
for (const loc of current.locations.slice(0, 10)) {
|
||
|
|
console.log(` ${loc}`)
|
||
|
|
}
|
||
|
|
if (current.locations.length > 10) {
|
||
|
|
console.log(` ... and ${current.locations.length - 10} more`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const fixBaseline = process.argv.includes('--fix-baseline')
|
||
|
|
if (fixBaseline) {
|
||
|
|
const newBaseline = {}
|
||
|
|
for (const [key, v] of Object.entries(violations)) {
|
||
|
|
newBaseline[key] = v.count
|
||
|
|
}
|
||
|
|
writeFileSync(BASELINE_PATH, JSON.stringify(newBaseline, null, 2) + '\n')
|
||
|
|
console.log(`\n Baseline written to ${path.relative(ROOT, BASELINE_PATH)}`)
|
||
|
|
console.log('Structural gate PASSED (baseline updated)')
|
||
|
|
process.exit(0)
|
||
|
|
}
|
||
|
|
|
||
|
|
writeFileSync(BASELINE_PATH, JSON.stringify(baseline, null, 2) + '\n')
|
||
|
|
|
||
|
|
if (errors.length > 0) {
|
||
|
|
console.error(`\nStructural gate FAILED (${errors.length} violation(s)):`)
|
||
|
|
for (const e of errors) console.error(` - ${e}`)
|
||
|
|
console.error(`\nIf these are legitimate, update the baseline with:`)
|
||
|
|
console.error(` node scripts/check-structural.js --fix-baseline`)
|
||
|
|
process.exit(1)
|
||
|
|
} else {
|
||
|
|
console.log('\nStructural gate PASSED')
|
||
|
|
}
|