v0.2.0: Git-native architecture (#1)
This commit was merged in pull request #1.
This commit is contained in:
+220
-12
@@ -1,5 +1,52 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { VerificationLayer, VerificationResult, VerificationCheck } from "./types.js";
|
||||
|
||||
interface CodeFinding {
|
||||
severity: "P0" | "P1" | "P2" | "P3";
|
||||
category: string;
|
||||
message: string;
|
||||
file?: string;
|
||||
}
|
||||
|
||||
const CODE_QUALITY_PATTERNS: Array<{
|
||||
pattern: RegExp;
|
||||
severity: "P0" | "P1" | "P2" | "P3";
|
||||
category: string;
|
||||
message: string;
|
||||
}> = [
|
||||
{
|
||||
pattern: /catch\s*\(\w*\)\s*\{\s*\}/g,
|
||||
severity: "P0",
|
||||
category: "error_handling",
|
||||
message: "Empty catch block — errors silently swallowed",
|
||||
},
|
||||
{
|
||||
pattern: /console\.(log|warn|error)\s*\(/g,
|
||||
severity: "P2",
|
||||
category: "logging",
|
||||
message: "Direct console.log usage — consider structured logging",
|
||||
},
|
||||
{
|
||||
pattern: /any\b/g,
|
||||
severity: "P1",
|
||||
category: "type_safety",
|
||||
message: "Use of 'any' type — loses type safety",
|
||||
},
|
||||
{
|
||||
pattern: /TODO|FIXME|HACK|XXX/g,
|
||||
severity: "P2",
|
||||
category: "tech_debt",
|
||||
message: "Technical debt marker found",
|
||||
},
|
||||
{
|
||||
pattern: /\bvar\s+/g,
|
||||
severity: "P1",
|
||||
category: "modern_js",
|
||||
message: "Use of 'var' — prefer 'const' or 'let'",
|
||||
},
|
||||
];
|
||||
|
||||
export class QualityVerification extends VerificationLayer {
|
||||
readonly layer = 4;
|
||||
readonly name = "Code Quality";
|
||||
@@ -8,25 +55,186 @@ export class QualityVerification extends VerificationLayer {
|
||||
const start = Date.now();
|
||||
const checks: VerificationCheck[] = [];
|
||||
|
||||
checks.push({
|
||||
name: "P0 findings auto-applied",
|
||||
status: "skipped",
|
||||
message: "Code review auto-fix not yet implemented",
|
||||
});
|
||||
const findings = this.scanForFindings(projectPath);
|
||||
|
||||
checks.push({
|
||||
name: "P1+ findings flagged for review",
|
||||
status: "skipped",
|
||||
message: "Multi-persona review not yet implemented",
|
||||
});
|
||||
const p0Findings = findings.filter((f) => f.severity === "P0");
|
||||
const p1Findings = findings.filter((f) => f.severity === "P1");
|
||||
const p2p3Findings = findings.filter((f) => f.severity === "P2" || f.severity === "P3");
|
||||
|
||||
checks.push(this.checkP0Findings(p0Findings));
|
||||
checks.push(this.checkP1Findings(p1Findings));
|
||||
checks.push(this.checkP2P3Findings(p2p3Findings));
|
||||
checks.push(this.checkTypeScriptStrictness(projectPath));
|
||||
checks.push(this.checkConsistentNaming(projectPath));
|
||||
|
||||
const hasP0Fail = p0Findings.length > 3;
|
||||
const passed = !hasP0Fail;
|
||||
|
||||
return {
|
||||
layer: this.layer,
|
||||
name: this.name,
|
||||
passed: true,
|
||||
passed,
|
||||
checks,
|
||||
summary: `Code quality verification layer (placeholder)`,
|
||||
summary: `${findings.length} findings (P0: ${p0Findings.length}, P1: ${p1Findings.length}, P2/P3: ${p2p3Findings.length})`,
|
||||
duration_ms: Date.now() - start,
|
||||
};
|
||||
}
|
||||
|
||||
private scanForFindings(projectPath: string): CodeFinding[] {
|
||||
const findings: CodeFinding[] = [];
|
||||
const srcDir = path.join(projectPath, "src");
|
||||
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
return findings;
|
||||
}
|
||||
|
||||
this.scanDirectory(srcDir, projectPath, findings);
|
||||
return findings;
|
||||
}
|
||||
|
||||
private scanDirectory(dir: string, projectPath: string, findings: CodeFinding[]): void {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory() && entry.name !== "node_modules") {
|
||||
this.scanDirectory(fullPath, projectPath, findings);
|
||||
} else if (
|
||||
entry.isFile() &&
|
||||
entry.name.endsWith(".ts") &&
|
||||
!entry.name.endsWith(".test.ts") &&
|
||||
!entry.name.endsWith(".d.ts")
|
||||
) {
|
||||
const content = fs.readFileSync(fullPath, "utf-8");
|
||||
for (const { pattern, severity, category, message } of CODE_QUALITY_PATTERNS) {
|
||||
pattern.lastIndex = 0;
|
||||
const matches = pattern.test(content);
|
||||
if (matches) {
|
||||
findings.push({
|
||||
severity,
|
||||
category,
|
||||
message: `${message} (${path.relative(projectPath, fullPath)})`,
|
||||
file: path.relative(projectPath, fullPath),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkP0Findings(p0Findings: CodeFinding[]): VerificationCheck {
|
||||
if (p0Findings.length === 0) {
|
||||
return this.check(
|
||||
"P0 findings (auto-fix)",
|
||||
"pass",
|
||||
"No critical code quality issues found"
|
||||
);
|
||||
}
|
||||
return this.check(
|
||||
"P0 findings (auto-fix)",
|
||||
p0Findings.length > 3 ? "fail" : "warning",
|
||||
`${p0Findings.length} P0 finding(s) — should be auto-fixed`,
|
||||
p0Findings.map((f) => `[${f.category}] ${f.message}`).join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
private checkP1Findings(p1Findings: CodeFinding[]): VerificationCheck {
|
||||
if (p1Findings.length === 0) {
|
||||
return this.check(
|
||||
"P1 findings (review)",
|
||||
"pass",
|
||||
"No P1 findings"
|
||||
);
|
||||
}
|
||||
return this.check(
|
||||
"P1 findings (review)",
|
||||
"pass",
|
||||
`${p1Findings.length} P1 finding(s) flagged for post-hoc review`,
|
||||
p1Findings.map((f) => `[${f.category}] ${f.message}`).join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
private checkP2P3Findings(findings: CodeFinding[]): VerificationCheck {
|
||||
if (findings.length === 0) {
|
||||
return this.check(
|
||||
"P2/P3 findings (informational)",
|
||||
"pass",
|
||||
"No P2/P3 findings"
|
||||
);
|
||||
}
|
||||
return this.check(
|
||||
"P2/P3 findings (informational)",
|
||||
"pass",
|
||||
`${findings.length} informational finding(s)`,
|
||||
findings.map((f) => `[${f.category}] ${f.message}`).join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
private checkTypeScriptStrictness(projectPath: string): VerificationCheck {
|
||||
const tsconfigPath = path.join(projectPath, "tsconfig.json");
|
||||
if (!fs.existsSync(tsconfigPath)) {
|
||||
return this.check("TypeScript strictness", "warning", "No tsconfig.json found");
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(tsconfigPath, "utf-8");
|
||||
const jsonContent = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
|
||||
try {
|
||||
const tsconfig = JSON.parse(jsonContent);
|
||||
const strict = tsconfig.compilerOptions?.strict;
|
||||
const noImplicitAny = tsconfig.compilerOptions?.noImplicitAny;
|
||||
|
||||
if (strict) {
|
||||
return this.check("TypeScript strictness", "pass", "TypeScript strict mode is enabled");
|
||||
}
|
||||
if (noImplicitAny) {
|
||||
return this.check("TypeScript strictness", "pass", "noImplicitAny is enabled");
|
||||
}
|
||||
return this.check(
|
||||
"TypeScript strictness",
|
||||
"warning",
|
||||
"TypeScript strict mode is not enabled — consider enabling for better type safety"
|
||||
);
|
||||
} catch {
|
||||
return this.check("TypeScript strictness", "warning", "Could not parse tsconfig.json");
|
||||
}
|
||||
}
|
||||
|
||||
private checkConsistentNaming(projectPath: string): VerificationCheck {
|
||||
const srcDir = path.join(projectPath, "src");
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
return this.check("Consistent naming", "skipped", "No src/ directory found");
|
||||
}
|
||||
|
||||
const tsFiles: string[] = [];
|
||||
this.collectFiles(srcDir, tsFiles);
|
||||
|
||||
const kebabCaseFiles = tsFiles.filter((f) => path.basename(f).includes("-") && !path.basename(f).includes(".test."));
|
||||
const camelCaseFiles = tsFiles.filter((f) => /^[a-z][a-zA-Z0-9]*\.ts$/.test(path.basename(f)) && !path.basename(f).includes("-"));
|
||||
const pascalCaseFiles = tsFiles.filter((f) => /^[A-Z]/.test(path.basename(f)));
|
||||
|
||||
const conventions: string[] = [];
|
||||
if (kebabCaseFiles.length > camelCaseFiles.length && kebabCaseFiles.length > pascalCaseFiles.length) {
|
||||
conventions.push("kebab-case dominant");
|
||||
} else if (camelCaseFiles.length > 0) {
|
||||
conventions.push("camelCase files present");
|
||||
}
|
||||
|
||||
return this.check(
|
||||
"Consistent naming",
|
||||
"pass",
|
||||
`${tsFiles.length} source files, naming conventions: ${conventions.join(", ") || "mixed"}`
|
||||
);
|
||||
}
|
||||
|
||||
private collectFiles(dir: string, files: string[]): void {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory() && entry.name !== "node_modules") {
|
||||
this.collectFiles(fullPath, files);
|
||||
} else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user