diff --git a/package-lock.json b/package-lock.json index 399d917..e76789c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "@google-cloud/aiplatform": "^6.5.0", "@google-cloud/storage": "^7.19.0", "@google-cloud/vertexai": "^1.1.0", - "@google/genai": "^1.50.0", - "axios": "^1.13.6", + "@google/genai": "^1.52.0", + "axios": "^1.16.0", "ts-morph": "^27.0.2" }, "devDependencies": { @@ -101,9 +101,10 @@ } }, "node_modules/@google/genai": { - "version": "1.50.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.50.0.tgz", - "integrity": "sha512-oHv7JfdI6SLUitERptYoHqpn4Y2wWyPOBfWtpw8kfKTqqEiMJpUC6SEtiQPogb55Ip8fymj4bxGnGBTVV/Z9Ew==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "google-auth-library": "^10.3.0", @@ -506,14 +507,14 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "proxy-from-env": "^2.1.0" } }, "node_modules/axios/node_modules/form-data": { @@ -1028,9 +1029,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -2028,10 +2029,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/pstree.remy": { "version": "1.1.8", diff --git a/package.json b/package.json index 0e24755..e6de36a 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "@google-cloud/aiplatform": "^6.5.0", "@google-cloud/storage": "^7.19.0", "@google-cloud/vertexai": "^1.1.0", - "@google/genai": "^1.50.0", - "axios": "^1.13.6", + "@google/genai": "^1.52.0", + "axios": "^1.16.0", "ts-morph": "^27.0.2" } } diff --git a/vibe-check-runner.js b/vibe-check-runner.js index f56ee94..27867e8 100644 --- a/vibe-check-runner.js +++ b/vibe-check-runner.js @@ -4,12 +4,13 @@ import fsPromises from 'node:fs/promises'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; import { GoogleGenAI } from '@google/genai'; +import { pathToFileURL } from 'node:url'; const ai = new GoogleGenAI({ apiKey: process.env.GOOGLE_AI_API_KEY }); // Constants for scoring -async function fileOrDirExists(filePath) { +export async function fileOrDirExists(filePath) { try { await fsPromises.stat(filePath); return true; @@ -25,7 +26,7 @@ const SCORES = { EFFICIENCY: 10, }; -function getModifiedFiles() { +export function getModifiedFiles() { try { // In CI (daily run), check files modified in the last 24 hours. // We filter for non-empty lines that end in .md and are in frontend/ or backend/ @@ -43,7 +44,7 @@ function getModifiedFiles() { } } -async function syncBenchmarks(tech, mdContent, retries = 5, delay = 10000) { +export async function syncBenchmarks(tech, mdContent, retries = 5, delay = 10000) { try { const prompt = `Based on the following documentation:\n\n${mdContent}\n\n1. Generate a "Golden Prompt" (a comprehensive instruction for generating a typical module using this technology) in JSON format: {"golden_prompt": "...", "tech": "${tech}"}\n2. Generate a JSON Schema for TS-Morph AST validation rules enforcing DDD/FSD layers and strict typing for this technology. The generated JSON schema must explicitly follow a nested structure compatible with \`analyzeAST\`. Format: {"$schema": "...", "type": "object", "properties": {"forbidden_types": {"contains": {"enum": ["any"]}}}}.\n\nRespond strictly with ONLY a JSON array containing these two objects in order. No markdown wrappers.`; const response = await ai.models.generateContent({ @@ -85,7 +86,7 @@ async function syncBenchmarks(tech, mdContent, retries = 5, delay = 10000) { } } -async function simulateAIGeneration(goldenPrompt, tech, mdContent, retries = 5, delay = 10000) { +export async function simulateAIGeneration(goldenPrompt, tech, mdContent, retries = 5, delay = 10000) { try { const prompt = `${goldenPrompt}\n\nConstraints and instructions from the following documentation:\n\n${mdContent}\n\nGenerate ONLY raw code. No markdown formatting, no explanations.`; const response = await ai.models.generateContent({ @@ -110,7 +111,7 @@ async function simulateAIGeneration(goldenPrompt, tech, mdContent, retries = 5, } } -function analyzeAST(sourceFile, tech) { +export function analyzeAST(sourceFile, tech) { let score = { arch: SCORES.ARCH, type: SCORES.TYPE, @@ -222,7 +223,7 @@ function analyzeAST(sourceFile, tech) { return { total, breakdown: score }; } -async function runVibeCheck() { +export async function runVibeCheck() { console.log('Running Vibe-Check Runner...'); const modifiedFiles = getModifiedFiles(); @@ -241,11 +242,10 @@ async function runVibeCheck() { console.warn('Failed to configure git user. If running locally, this is expected.'); } + // Deduplicate by technology and read contents for syncBenchmarks + const techMap = new Map(); for (const file of modifiedFiles) { - console.log(`Processing ${file}...`); - - if (!fs.existsSync(file)) { - console.log(`File ${file} does not exist. Skipping.`); + if (!await fileOrDirExists(file)) { continue; } @@ -256,7 +256,6 @@ async function runVibeCheck() { else if (file.includes('/express/')) tech = 'express'; else if (file.includes('/nodejs/')) tech = 'nodejs'; else { - // Fallback const parts = file.split('/'); if (parts.length > 1) { tech = parts[1]; @@ -266,76 +265,119 @@ async function runVibeCheck() { } const mdContent = await fsPromises.readFile(file, 'utf-8'); + if (!techMap.has(tech)) { + techMap.set(tech, { content: '' }); + } + const entry = techMap.get(tech); + entry.content += `\n\n--- Content from ${file} ---\n\n${mdContent}`; + } - await syncBenchmarks(tech, mdContent); + // Process syncBenchmarks sequentially by technology + for (const [tech, data] of techMap.entries()) { + await syncBenchmarks(tech, data.content); + } - const suitePath = path.join('benchmarks', 'suites', `${tech}.json`); - if (!fs.existsSync(suitePath)) { - console.log(`No benchmark suite found for ${tech}. Skipping.`); - continue; - } + // Concurrent generation and AST analysis per file + const generationTasks = []; + for (const file of modifiedFiles) { + if (!await fileOrDirExists(file)) { + continue; + } - const suiteConfig = JSON.parse(await fsPromises.readFile(suitePath, 'utf-8')); + let tech = ''; + if (file.includes('/angular/')) tech = 'angular'; + else if (file.includes('/nestjs/')) tech = 'nestjs'; + else if (file.includes('/typescript/')) tech = 'typescript'; + else if (file.includes('/express/')) tech = 'express'; + else if (file.includes('/nodejs/')) tech = 'nodejs'; + else { + const parts = file.split('/'); + if (parts.length > 1) { + tech = parts[1]; + } else { + continue; + } + } - const generatedCode = await simulateAIGeneration(suiteConfig.golden_prompt, tech, mdContent); + generationTasks.push((async () => { + const mdContent = await fsPromises.readFile(file, 'utf-8'); + const suitePath = path.join('benchmarks', 'suites', `${tech}.json`); + if (!await fileOrDirExists(suitePath)) { + console.log(`No benchmark suite found for ${tech}. Skipping.`); + return { file, tech, mdContent, generatedCode: null, score: null, breakdown: null }; + } - if (!generatedCode) { - console.error(`Failed to generate code for ${tech}.`); - continue; - } + const suiteConfig = JSON.parse(await fsPromises.readFile(suitePath, 'utf-8')); + const generatedCode = await simulateAIGeneration(suiteConfig.golden_prompt, tech, mdContent); - const sourceFile = project.createSourceFile(`temp_${tech}.ts`, generatedCode, { overwrite: true }); - const { total: score, breakdown } = analyzeAST(sourceFile, tech); + if (!generatedCode) { + console.error(`Failed to generate code for ${tech}.`); + return { file, tech, mdContent, generatedCode: null, score: null, breakdown: null }; + } + + const sourceFile = project.createSourceFile(`temp_${tech}_${Buffer.from(file).toString('base64').substring(0,8)}.ts`, generatedCode, { overwrite: true }); + const { total: score, breakdown } = analyzeAST(sourceFile, tech); + return { file, tech, mdContent, generatedCode, score, breakdown }; + })()); + } + + const results = await Promise.all(generationTasks); + + // Sequential stateful side effects + for (const result of results) { + if (!result.generatedCode) continue; + + const { file, tech, generatedCode, score, breakdown } = result; console.log(`Fidelity Score for ${file}: ${score}%`); console.log(`Breakdown:`, breakdown); if (score >= 95) { - console.log(`✅ Validation passed for ${file}. Updating badge and auto-committing.`); - - let content = await fsPromises.readFile(file, 'utf-8'); - if (!content.includes('[![Vibe-Coding Verified]')) { - content = content.replace(/^# /, '[![Vibe-Coding Verified](https://img.shields.io/badge/Vibe--Coding-Verified-brightgreen?style=for-the-badge)](#)\n\n# '); - await fsPromises.writeFile(file, content); - } + console.log(`✅ Validation passed for ${file}. Updating badge and auto-committing.`); - try { - execFileSync('git', ['add', file]); - try { execFileSync('sh', ['-c', 'git add benchmarks/suites/*.json benchmarks/criteria/*.json 2>/dev/null || true']); } catch (e) {} - // Only commit if there are changes (badge might already be there) - const status = execFileSync('git', ['status', '--porcelain'], { encoding: 'utf-8' }); - if (status.includes(file) || status.includes('benchmarks/')) { - execFileSync('git', ['commit', '-m', '[chore: benchmark-sync]']); - execFileSync('git', ['push', 'origin', 'HEAD:main']); - } else { - console.log(`Badge already present in ${file}, skipping commit.`); + let content = await fsPromises.readFile(file, 'utf-8'); + if (!content.includes('[![Vibe-Coding Verified]')) { + content = content.replace(/^# /, '[![Vibe-Coding Verified](https://img.shields.io/badge/Vibe--Coding-Verified-brightgreen?style=for-the-badge)](#)\n\n# '); + await fsPromises.writeFile(file, content); } - } catch (err) { - console.error('Failed to commit or push:', err.message); - } + try { + execFileSync('git', ['add', file]); + try { execFileSync('sh', ['-c', 'git add benchmarks/suites/*.json benchmarks/criteria/*.json 2>/dev/null || true']); } catch (e) {} + const status = execFileSync('git', ['status', '--porcelain'], { encoding: 'utf-8' }); + if (status.includes(file) || status.includes('benchmarks/')) { + execFileSync('git', ['commit', '-m', '[chore: fidelity-pass]']); + execFileSync('git', ['push', 'origin', 'HEAD:main']); + } else { + console.log(`Badge already present in ${file}, skipping commit.`); + } + } catch (err) { + console.error('Failed to commit or push:', err.message); + } } else { - console.error(`❌ Validation failed for ${file}. Score below 95%.`); + console.error(`❌ Validation failed for ${file}. Score below 95%.`); - const reportDir = path.join('benchmarks', 'logs'); - if (!await fileOrDirExists(reportDir)) await fsPromises.mkdir(reportDir, { recursive: true }); + const reportDir = path.join('benchmarks', 'logs'); + if (!await fileOrDirExists(reportDir)) await fsPromises.mkdir(reportDir, { recursive: true }); - const reportPath = path.join(reportDir, `violation-report.md`); - const reportContent = `# Critical Violation Report\n\n> [!CAUTION]\n> Fidelity Score dropped below 95%.\n\n**File:** \`${file}\`\n**Fidelity Score:** ${score}%\n**Threshold:** 95%\n\n## Breakdown\n| Metric | Score |\n|---|---|\n| Arch Integrity | ${breakdown.arch} |\n| Type Safety | ${breakdown.type} |\n| Security | ${breakdown.security} |\n| Efficiency | ${breakdown.efficiency} |\n\n## Generated Code\n\`\`\`typescript\n${generatedCode}\n\`\`\`\n\nReview the AST rules.`; + const reportPath = path.join(reportDir, `violation-report.md`); + const reportContent = `# Critical Violation Report\n\n> [!CAUTION]\n> Fidelity Score dropped below 95%.\n\n**File:** \`${file}\`\n**Fidelity Score:** ${score}%\n**Threshold:** 95%\n\n## Breakdown\n| Metric | Score |\n|---|---|\n| Arch Integrity | ${breakdown.arch} |\n| Type Safety | ${breakdown.type} |\n| Security | ${breakdown.security} |\n| Efficiency | ${breakdown.efficiency} |\n\n## Generated Code\n\`\`\`typescript\n${generatedCode}\n\`\`\`\n\nReview the AST rules.`; - await fsPromises.writeFile(reportPath, reportContent); - console.log(`Generated violation report: ${reportPath}`); + await fsPromises.writeFile(reportPath, reportContent); + console.log(`Generated violation report: ${reportPath}`); - try { - execFileSync('gh', ['issue', 'create', '--title', `Critical Issue: Fidelity Gap for ${file}`, '--label', 'critical,bug', '--body-file', reportPath]); - console.log(`Created GitHub Issue for ${file}`); - } catch (err) { - console.error('Failed to create GitHub Issue (gh cli might not be installed or authenticated):', err.message); - } + try { + execFileSync('gh', ['issue', 'create', '--title', `Critical Issue: Fidelity Gap for ${file}`, '--label', 'critical,bug', '--body-file', reportPath]); + console.log(`Created GitHub Issue for ${file}`); + } catch (err) { + console.error('Failed to create GitHub Issue (gh cli might not be installed or authenticated):', err.message); + } - process.exitCode = 1; + process.exitCode = 1; } } } -runVibeCheck().catch(console.error); \ No newline at end of file +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + runVibeCheck().catch(console.error); +}