v0.2.0: Git-native architecture (#1)
This commit was merged in pull request #1.
This commit is contained in:
+173
-10
@@ -1,6 +1,20 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { VerificationLayer, VerificationResult } from "./types.js";
|
||||
import { VerificationLayer, VerificationResult, VerificationCheck } from "./types.js";
|
||||
|
||||
const STUB_PATTERNS = [
|
||||
/\bTODO\b/i,
|
||||
/\bFIXME\b/i,
|
||||
/\bHACK\b/i,
|
||||
/\bXXX\b/i,
|
||||
/\bstub\b/i,
|
||||
/\bplaceholder\b/i,
|
||||
/not\s+yet\s+implemented/i,
|
||||
/not\s+implemented/i,
|
||||
];
|
||||
|
||||
const TODO_PATTERN = /\bTODO\b/gi;
|
||||
const FIXME_PATTERN = /\bFIXME\b/gi;
|
||||
|
||||
export class StructuralVerification extends VerificationLayer {
|
||||
readonly layer = 1;
|
||||
@@ -12,8 +26,11 @@ export class StructuralVerification extends VerificationLayer {
|
||||
|
||||
checks.push(this.checkPhaseDir(projectPath, phase));
|
||||
checks.push(this.checkPlanExists(projectPath, phase));
|
||||
checks.push(this.checkCIConfig(projectPath));
|
||||
checks.push(this.checkSpecification(projectPath));
|
||||
checks.push(this.checkNoStubs(projectPath));
|
||||
checks.push(this.checkImportsWired(projectPath));
|
||||
checks.push(this.checkNoEmptyFiles(projectPath));
|
||||
|
||||
const passed = checks.every((c) => c.status !== "fail");
|
||||
return {
|
||||
@@ -54,21 +71,167 @@ export class StructuralVerification extends VerificationLayer {
|
||||
);
|
||||
}
|
||||
|
||||
private checkNoStubs(projectPath: string) {
|
||||
private checkCIConfig(projectPath: string) {
|
||||
const configPath = path.join(projectPath, ".ci", "config.json");
|
||||
const exists = fs.existsSync(configPath);
|
||||
if (!exists) {
|
||||
return this.check("CI config exists", "fail", ".ci/config.json not found", configPath);
|
||||
}
|
||||
try {
|
||||
const content = fs.readFileSync(configPath, "utf-8");
|
||||
JSON.parse(content);
|
||||
return this.check("CI config valid", "pass", ".ci/config.json is valid JSON");
|
||||
} catch (e) {
|
||||
return this.check("CI config valid", "fail", `.ci/config.json has invalid JSON: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private checkSpecification(projectPath: string) {
|
||||
const specPath = path.join(projectPath, ".ci", "specification.md");
|
||||
const exists = fs.existsSync(specPath);
|
||||
if (!exists) {
|
||||
return this.check("Specification exists", "warning", ".ci/specification.md not found — specification may not be loaded yet");
|
||||
}
|
||||
const content = fs.readFileSync(specPath, "utf-8");
|
||||
if (content.trim().length < 10) {
|
||||
return this.check("Specification substantive", "fail", "Specification file is too short to be meaningful");
|
||||
}
|
||||
return this.check("Specification exists", "pass", "Specification file found and substantive");
|
||||
}
|
||||
|
||||
private checkNoStubs(projectPath: string): VerificationCheck {
|
||||
const srcDir = path.join(projectPath, "src");
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
return this.check("No stubs/TODOs", "skipped", "No src/ directory found to check for stubs");
|
||||
}
|
||||
|
||||
const issues: string[] = [];
|
||||
this.scanForStubs(srcDir, issues);
|
||||
|
||||
if (issues.length === 0) {
|
||||
return this.check("No stubs/TODOs", "pass", "No stub patterns found in source files");
|
||||
}
|
||||
|
||||
const status = issues.length > 10 ? "fail" : "warning";
|
||||
return this.check(
|
||||
"No stubs or TODOs",
|
||||
"skipped",
|
||||
"Stub/TODO detection not yet implemented for source files"
|
||||
"No stubs/TODOs",
|
||||
status,
|
||||
`Found ${issues.length} stub/TODO pattern(s)`,
|
||||
issues.slice(0, 20).join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
private checkImportsWired(projectPath: string) {
|
||||
private scanForStubs(dir: string, issues: string[]): void {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git") {
|
||||
this.scanForStubs(path.join(dir, entry.name), issues);
|
||||
} else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) {
|
||||
const filePath = path.join(dir, entry.name);
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
for (const pattern of STUB_PATTERNS) {
|
||||
if (pattern.test(content)) {
|
||||
const lineNum = content.split("\n").findIndex((line) => pattern.test(line)) + 1;
|
||||
issues.push(`${path.relative(dir, filePath)}:${lineNum}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkImportsWired(projectPath: string): VerificationCheck {
|
||||
const srcDir = path.join(projectPath, "src");
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
return this.check("Imports/exports wired", "skipped", "No src/ directory found");
|
||||
}
|
||||
|
||||
const tsFiles = this.collectTsFiles(srcDir);
|
||||
if (tsFiles.length === 0) {
|
||||
return this.check("Imports/exports wired", "skipped", "No TypeScript files found");
|
||||
}
|
||||
|
||||
const importPattern = /import\s+.*from\s+['"]\.\/([^'"]+)['"]/g;
|
||||
const issues: string[] = [];
|
||||
|
||||
for (const file of tsFiles) {
|
||||
const content = fs.readFileSync(file, "utf-8");
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = importPattern.exec(content)) !== null) {
|
||||
const importPath = match[1];
|
||||
const resolvedPath = this.resolveImport(file, importPath);
|
||||
if (resolvedPath && !fs.existsSync(resolvedPath)) {
|
||||
issues.push(`${path.relative(projectPath, file)}: unresolved import '${importPath}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (issues.length === 0) {
|
||||
return this.check("Imports/exports wired", "pass", `All local imports resolved (${tsFiles.length} files checked)`);
|
||||
}
|
||||
|
||||
return this.check(
|
||||
"Imports/exports wired",
|
||||
"skipped",
|
||||
"Import/export analysis not yet implemented"
|
||||
issues.length > 5 ? "fail" : "warning",
|
||||
`${issues.length} unresolved import(s)`,
|
||||
issues.join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
import { VerificationCheck } from "./types.js";
|
||||
private checkNoEmptyFiles(projectPath: string): VerificationCheck {
|
||||
const srcDir = path.join(projectPath, "src");
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
return this.check("No empty files", "skipped", "No src/ directory found");
|
||||
}
|
||||
|
||||
const tsFiles = this.collectTsFiles(srcDir);
|
||||
const emptyFiles: string[] = [];
|
||||
|
||||
for (const file of tsFiles) {
|
||||
const content = fs.readFileSync(file, "utf-8").trim();
|
||||
if (content.length === 0 || (content.length < 20 && !content.includes("export"))) {
|
||||
emptyFiles.push(path.relative(projectPath, file));
|
||||
}
|
||||
}
|
||||
|
||||
if (emptyFiles.length === 0) {
|
||||
return this.check("No empty files", "pass", "All source files have substantive content");
|
||||
}
|
||||
|
||||
return this.check(
|
||||
"No empty files",
|
||||
"warning",
|
||||
`${emptyFiles.length} potentially empty file(s)`,
|
||||
emptyFiles.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
private collectTsFiles(dir: string): string[] {
|
||||
const files: string[] = [];
|
||||
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") {
|
||||
files.push(...this.collectTsFiles(fullPath));
|
||||
} else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts") && !entry.name.endsWith(".test.ts")) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
private resolveImport(fromFile: string, importPath: string): string | null {
|
||||
if (!importPath.startsWith(".")) return null;
|
||||
const dir = path.dirname(fromFile);
|
||||
const candidates = [
|
||||
path.resolve(dir, importPath + ".ts"),
|
||||
path.resolve(dir, importPath + ".js"),
|
||||
path.resolve(dir, importPath, "index.ts"),
|
||||
path.resolve(dir, importPath, "index.js"),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user