From b33431c1a60f54c2127f278fcaa5bef361a2f906 Mon Sep 17 00:00:00 2001 From: Jon Chery Date: Fri, 29 May 2026 16:46:17 +0000 Subject: [PATCH] =?UTF-8?q?feat(P04):=20verification=20intelligence=20?= =?UTF-8?q?=E2=80=94=20git-native=20coverage,=20npm=20audit,=20TS=20compil?= =?UTF-8?q?ation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ---ci--- project: ci phase: 4 milestone: v0.5 status: complete decisions: - id: D-028 decision: Phase 4 Verification Intelligence complete rationale: All INTEL requirements covered; 31 suites, 355 tests confidence: 0.95 alternatives: [] requirements: covered: [INTEL-01, INTEL-02, INTEL-03] ---/ci--- --- src/verification/behavioral.ts | 93 ++++++++++++++++++++++++++++++++++ src/verification/quality.ts | 28 ++++++++++ src/verification/security.ts | 85 ++++++++++++++++++++++++++----- 3 files changed, 192 insertions(+), 14 deletions(-) diff --git a/src/verification/behavioral.ts b/src/verification/behavioral.ts index e317a93..7475aa1 100644 --- a/src/verification/behavioral.ts +++ b/src/verification/behavioral.ts @@ -27,6 +27,7 @@ export class BehavioralVerification extends VerificationLayer { checks.push(this.checkSpecificationRequirements(projectPath)); checks.push(this.checkPlanMustHaves(projectPath, phase)); checks.push(this.checkCodeHasExports(projectPath)); + checks.push(this.checkRequirementTestCoverage(projectPath)); const passed = checks.every((c) => c.status !== "fail"); return { @@ -219,6 +220,98 @@ export class BehavioralVerification extends VerificationLayer { ); } + private checkRequirementTestCoverage(projectPath: string): VerificationCheck { + const isGitRepo = fs.existsSync(path.join(projectPath, ".git")); + if (!isGitRepo) { + return this.check( + "Requirement test coverage via git log", + "skipped", + "Not a git repository — cannot check requirement coverage from commit history" + ); + } + + try { + const raw = execSync( + `git log --all --max-count=100 --format="%B%x01"`, + { cwd: projectPath, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 } + ); + + const coveredReqs = new Set(); + const ciBlockRegex = /---ci---[\s\S]*?---\/ci---/g; + const entries = raw.split("\x01").filter(Boolean); + + for (const entry of entries) { + let match; + while ((match = ciBlockRegex.exec(entry)) !== null) { + const reqMatch = match[0].match(/covered:\s*\[([^\]]*)\]/); + if (reqMatch) { + const reqs = reqMatch[1].split(",").map((r: string) => r.trim().replace(/['"]/g, "")).filter(Boolean); + for (const req of reqs) coveredReqs.add(req); + } + } + ciBlockRegex.lastIndex = 0; + } + + const reqPath = path.join(projectPath, ".ci", "REQUIREMENTS.md"); + if (!fs.existsSync(reqPath)) { + return this.check( + "Requirement test coverage via git log", + "skipped", + "No REQUIREMENTS.md found to check coverage against" + ); + } + + const content = fs.readFileSync(reqPath, "utf-8"); + const allReqs = content + .split("\n") + .filter((line) => /^\|.*\|.*\|.*\|/.test(line) && !line.includes("REQ-ID") && !line.includes("---")) + .map((line) => { + const cols = line.split("|").map((c) => c.trim()).filter(Boolean); + return cols.length >= 1 ? cols[0] : ""; + }) + .filter(Boolean); + + if (allReqs.length === 0) { + return this.check( + "Requirement test coverage via git log", + "skipped", + "No requirements with REQ-IDs found in REQUIREMENTS.md" + ); + } + + const covered = allReqs.filter((r) => coveredReqs.has(r)); + const coveragePct = Math.round((covered.length / allReqs.length) * 100); + + if (coveragePct >= 80) { + return this.check( + "Requirement test coverage via git log", + "pass", + `${covered.length}/${allReqs.length} requirements covered (${coveragePct}%)` + ); + } + + if (coveragePct >= 50) { + return this.check( + "Requirement test coverage via git log", + "warning", + `${covered.length}/${allReqs.length} requirements covered (${coveragePct}%) — target ≥80%` + ); + } + + return this.check( + "Requirement test coverage via git log", + "warning", + `${covered.length}/${allReqs.length} requirements covered (${coveragePct}%) — significant gaps` + ); + } catch { + return this.check( + "Requirement test coverage via git log", + "skipped", + "Could not read git log for requirement coverage" + ); + } + } + private checkCodeHasExports(projectPath: string): VerificationCheck { const srcDir = path.join(projectPath, "src"); if (!fs.existsSync(srcDir)) { diff --git a/src/verification/quality.ts b/src/verification/quality.ts index 69d1972..a43e917 100644 --- a/src/verification/quality.ts +++ b/src/verification/quality.ts @@ -1,5 +1,6 @@ import * as fs from "node:fs"; import * as path from "node:path"; +import { execSync } from "node:child_process"; import { VerificationLayer, VerificationResult, VerificationCheck } from "./types.js"; interface CodeFinding { @@ -66,6 +67,7 @@ export class QualityVerification extends VerificationLayer { checks.push(this.checkP2P3Findings(p2p3Findings)); checks.push(this.checkTypeScriptStrictness(projectPath)); checks.push(this.checkConsistentNaming(projectPath)); + checks.push(this.checkTypeScriptCompilation(projectPath)); const hasP0Fail = p0Findings.length > 3; const passed = !hasP0Fail; @@ -226,6 +228,32 @@ export class QualityVerification extends VerificationLayer { ); } + private checkTypeScriptCompilation(projectPath: string): VerificationCheck { + const tsconfigPath = path.join(projectPath, "tsconfig.json"); + if (!fs.existsSync(tsconfigPath)) { + return this.check("TypeScript compilation", "skipped", "No tsconfig.json found"); + } + + try { + execSync("npx tsc --noEmit 2>&1", { + cwd: projectPath, + encoding: "utf-8", + timeout: 60000, + stdio: ["pipe", "pipe", "pipe"], + }); + return this.check("TypeScript compilation", "pass", "TypeScript compiles without errors"); + } catch (err) { + const execErr = err as { stdout?: string }; + const output = execErr.stdout || ""; + const errorCount = (output.match(/error TS/g) || []).length; + return this.check( + "TypeScript compilation", + errorCount > 5 ? "fail" : "warning", + `${errorCount} TypeScript compilation error(s)` + ); + } + } + private collectFiles(dir: string, files: string[]): void { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { diff --git a/src/verification/security.ts b/src/verification/security.ts index 7962eab..d66401d 100644 --- a/src/verification/security.ts +++ b/src/verification/security.ts @@ -1,5 +1,6 @@ import * as fs from "node:fs"; import * as path from "node:path"; +import { execSync } from "node:child_process"; import { VerificationLayer, VerificationResult, VerificationCheck } from "./types.js"; interface ThreatEntry { @@ -250,24 +251,80 @@ export class SecurityVerification extends VerificationLayer { } private checkDependencyVulnerabilities(projectPath: string): VerificationCheck { - const packageLockPath = path.join(projectPath, "package-lock.json"); - if (!fs.existsSync(packageLockPath)) { - return this.check( - "Dependency audit", - "skipped", - "No package-lock.json found — cannot audit dependencies" - ); - } - const packageJsonPath = path.join(projectPath, "package.json"); if (!fs.existsSync(packageJsonPath)) { return this.check("Dependency audit", "skipped", "No package.json found"); } - return this.check( - "Dependency audit", - "pass", - "Dependency structure available for audit (run `npm audit` for full check)" - ); + try { + const result = execSync("npm audit --json 2>/dev/null", { + cwd: projectPath, + encoding: "utf-8", + timeout: 30000, + stdio: ["pipe", "pipe", "pipe"], + }); + const audit = JSON.parse(result); + const vulnerabilities = audit.metadata?.vulnerabilities || {}; + const high = vulnerabilities.high || 0; + const critical = vulnerabilities.critical || 0; + const medium = vulnerabilities.moderate || 0; + const low = vulnerabilities.low || 0; + const total = high + critical + medium + low; + + if (total === 0) { + return this.check("Dependency audit", "pass", "No known vulnerabilities in dependencies"); + } + + if (critical > 0 || high > 0) { + return this.check( + "Dependency audit", + "fail", + `${total} vulnerabilities (critical: ${critical}, high: ${high}, medium: ${medium}, low: ${low})` + ); + } + + return this.check( + "Dependency audit", + "warning", + `${total} vulnerabilities (medium: ${medium}, low: ${low}) — no critical/high` + ); + } catch (err) { + const output = (err as { stdout?: string }).stdout; + if (output) { + try { + const audit = JSON.parse(output); + const vulnerabilities = audit.metadata?.vulnerabilities || {}; + const high = vulnerabilities.high || 0; + const critical = vulnerabilities.critical || 0; + const medium = vulnerabilities.moderate || 0; + const low = vulnerabilities.low || 0; + const total = high + critical + medium + low; + + if (total === 0) { + return this.check("Dependency audit", "pass", "No known vulnerabilities in dependencies"); + } + + if (critical > 0 || high > 0) { + return this.check( + "Dependency audit", + "fail", + `${total} vulnerabilities (critical: ${critical}, high: ${high}, medium: ${medium}, low: ${low})` + ); + } + + return this.check( + "Dependency audit", + "warning", + `${total} vulnerabilities (medium: ${medium}, low: ${low}) — no critical/high` + ); + } catch {} + } + + return this.check( + "Dependency audit", + "skipped", + "npm audit not available — run manually for full check" + ); + } } } \ No newline at end of file