v0.2.0: Git-native architecture (#1)

This commit was merged in pull request #1.
This commit is contained in:
2026-05-29 12:59:45 +00:00
parent 9cf5c000d9
commit 6e637e4af0
50 changed files with 5852 additions and 135 deletions
+173 -10
View File
@@ -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;
}
}