Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 58 additions & 23 deletions vibe-check-runner.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { pathToFileURL } from 'node:url';
import { Project, SyntaxKind } from 'ts-morph';
import fs from 'node:fs';
import fsPromises from 'node:fs/promises';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
Expand All @@ -9,7 +9,7 @@ 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;
Expand All @@ -25,7 +25,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/
Expand All @@ -43,7 +43,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({
Expand Down Expand Up @@ -85,7 +85,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({
Expand All @@ -110,7 +110,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,
Expand Down Expand Up @@ -222,7 +222,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();
Expand All @@ -231,8 +231,6 @@ async function runVibeCheck() {
return;
}

const project = new Project();

// Configure git user for commits
try {
execFileSync('git', ['config', '--global', 'user.name', 'github-actions[bot]']);
Expand All @@ -241,10 +239,10 @@ async function runVibeCheck() {
console.warn('Failed to configure git user. If running locally, this is expected.');
}

// Group files by tech for deduplication
const filesByTech = {};
for (const file of modifiedFiles) {
console.log(`Processing ${file}...`);

if (!fs.existsSync(file)) {
if (!await fileOrDirExists(file)) {
console.log(`File ${file} does not exist. Skipping.`);
continue;
}
Expand All @@ -256,7 +254,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];
Expand All @@ -265,28 +262,66 @@ async function runVibeCheck() {
}
}

const mdContent = await fsPromises.readFile(file, 'utf-8');
if (!filesByTech[tech]) filesByTech[tech] = [];
filesByTech[tech].push(file);
}

await syncBenchmarks(tech, mdContent);
// Sequentially sync benchmarks per tech with concatenated content
for (const [tech, files] of Object.entries(filesByTech)) {
const contents = await Promise.all(files.map(f => fsPromises.readFile(f, 'utf-8')));
const concatenatedContent = contents.join('\n\n--- \n\n');
await syncBenchmarks(tech, concatenatedContent);
}

// Prepare concurrent analysis tasks
const analysisTasks = modifiedFiles.map(async (file) => {
if (!await fileOrDirExists(file)) return null;

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 {
return null;
}
}

const suitePath = path.join('benchmarks', 'suites', `${tech}.json`);
if (!fs.existsSync(suitePath)) {
if (!await fileOrDirExists(suitePath)) {
console.log(`No benchmark suite found for ${tech}. Skipping.`);
continue;
return null;
}

const suiteConfig = JSON.parse(await fsPromises.readFile(suitePath, 'utf-8'));

const mdContent = await fsPromises.readFile(file, 'utf-8');
const generatedCode = await simulateAIGeneration(suiteConfig.golden_prompt, tech, mdContent);

if (!generatedCode) {
console.error(`Failed to generate code for ${tech}.`);
continue;
return null;
}

const sourceFile = project.createSourceFile(`temp_${tech}.ts`, generatedCode, { overwrite: true });
// Isolate Project instance per task
const project = new Project();
const tempFileName = `temp_${tech}_${Math.random().toString(36).substring(7)}.ts`;
const sourceFile = project.createSourceFile(tempFileName, generatedCode, { overwrite: true });
const { total: score, breakdown } = analyzeAST(sourceFile, tech);

return { file, tech, score, breakdown, generatedCode };
});

// Execute analysis concurrently
const analysisResults = (await Promise.all(analysisTasks)).filter(r => r !== null);

// Process Git/GitHub operations sequentially to avoid conflicts
for (const result of analysisResults) {
const { file, tech, score, breakdown, generatedCode } = result;
console.log(`Fidelity Score for ${file}: ${score}%`);
console.log(`Breakdown:`, breakdown);

Expand All @@ -302,7 +337,6 @@ async function runVibeCheck() {
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]']);
Expand All @@ -313,7 +347,6 @@ async function runVibeCheck() {
} catch (err) {
console.error('Failed to commit or push:', err.message);
}

} else {
console.error(`❌ Validation failed for ${file}. Score below 95%.`);

Expand All @@ -338,4 +371,6 @@ async function runVibeCheck() {
}
}

runVibeCheck().catch(console.error);
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
runVibeCheck().catch(console.error);
}
Loading