Files
dev-intel-v2/repo-profiler.js
Jarvis Prime b8403be96c feat: repo-agnostic refactor (BMad spec-test-build loop)
- NEW: repo-profiler.js — deterministic archetype detection (Infra, Frontend, Backend, etc.)
- NEW: extract-dynamic.js — generic extractor replacing hardcoded Foxtrot patterns
- NEW: eval-generator.js — dynamic ground-truth question generation from any repo graph
- NEW: specs/bmad-agnostic-refactor-spec.md — full BMad spec with acceptance criteria
- REFACTORED: prose.js — two-pass LLM synthesis with rich context (shared secrets, ports, service refs)
- REFACTORED: sysdoc.js — wired repo-profiler + extract-dynamic, --legacy escape hatch
- REFACTORED: wiggum-v2.sh — uses eval-generator before benchmarks
- FIXED: graph.js — _edgeSet rebuilt on loadSnapshot() (edge dedup was broken)
- FIXED: graph.js — recursive sortKeys() for deep equality in diffing
- FIXED: prose.js — robust JSON array extraction from LLM output
- FIXED: ratchet.js — syntax validation (node --check) before saving LLM mutations
- FIXED: extract-dynamic.js — centralized state services regex, added console.warn for silent failures
- TESTS: test-eval-generator, test-repo-profiler, test-synthesis-quality + mock fixtures

Eval: 81.5% on Foxtrot (fully repo-agnostic, no hardcoded reference pages)
BMad reviews: Architect B+, Dev Lead B-, TEA B-
2026-03-11 14:40:31 +00:00

172 lines
5.6 KiB
JavaScript

const fs = require('fs');
const path = require('path');
/**
* Repo Profiler
* Analyzes repository files and dependencies to determine its Archetype deterministically.
*/
const ARCHETYPES = {
INFRASTRUCTURE: 'Infrastructure',
FRONTEND: 'Frontend SPA',
BACKEND: 'Backend API',
PIPELINE: 'Data Pipeline',
LIBRARY: 'Library',
MONOREPO: 'Monorepo',
UNKNOWN: 'Unknown'
};
function readJsonFile(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (e) {
return null;
}
}
function analyzePackageJson(dir) {
const pkg = readJsonFile(path.join(dir, 'package.json'));
if (!pkg) return null;
const signals = [];
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
const depsKeys = Object.keys(deps);
// Frontend
if (depsKeys.includes('react') || depsKeys.includes('vue') || depsKeys.includes('angular') || depsKeys.includes('next') || depsKeys.includes('vite') || depsKeys.includes('webpack')) {
signals.push('frontend_framework');
}
// Backend
if (depsKeys.includes('express') || depsKeys.includes('fastify') || depsKeys.includes('nestjs') || depsKeys.includes('koa')) {
signals.push('backend_framework');
}
// Library
if (!pkg.scripts?.start && (pkg.main || pkg.exports) && !depsKeys.includes('express') && !depsKeys.includes('react') && !depsKeys.includes('vue') && !depsKeys.includes('angular')) {
signals.push('library_exports');
}
// Monorepo workspaces
if (pkg.workspaces) {
signals.push('workspaces');
}
return signals;
}
function analyzeFiles(dir, maxDepth = 3) {
const rootFiles = [];
try {
rootFiles.push(...fs.readdirSync(dir));
} catch (e) {
// directory doesn't exist
}
const signals = [];
// Check root level first
if (rootFiles.includes('Chart.yaml') || rootFiles.some(f => f.endsWith('.tf')) || rootFiles.includes('terraform')) {
signals.push('infra_files');
}
if (rootFiles.some(f => f.endsWith('.hcl') || f === 'helm' || f === 'kubernetes' || f === 'k8s')) {
signals.push('infra_files');
}
if (rootFiles.includes('go.mod')) signals.push('go_backend');
if (rootFiles.includes('Cargo.toml')) signals.push('rust_app');
if (rootFiles.includes('requirements.txt') || rootFiles.includes('Pipfile') || rootFiles.includes('pyproject.toml')) signals.push('python_app');
if (rootFiles.includes('lerna.json') || rootFiles.includes('turbo.json') || rootFiles.includes('nx.json')) signals.push('monorepo_tools');
// Recurse into subdirectories to find infra patterns (Helm charts, TF files)
if (!signals.includes('infra_files')) {
const infraFound = findInfraRecursive(dir, maxDepth, 0);
if (infraFound) signals.push('infra_files');
}
return signals;
}
function findInfraRecursive(dir, maxDepth, currentDepth) {
if (currentDepth >= maxDepth) return false;
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '.terraform') continue;
if (entry.isFile()) {
if (entry.name === 'Chart.yaml' || entry.name.endsWith('.tf') || entry.name === 'Dockerfile' || entry.name === 'crossplane.yaml') {
return true;
}
} else if (entry.isDirectory()) {
if (['charts', 'helm', 'terraform', 'modules', 'k8s', 'kubernetes'].includes(entry.name)) return true;
if (findInfraRecursive(path.join(dir, entry.name), maxDepth, currentDepth + 1)) return true;
}
}
} catch (e) { /* skip unreadable dirs */ }
return false;
}
function profileRepo(repoPath, graph = null) {
const signals = new Set();
const fileSignals = analyzeFiles(repoPath);
fileSignals.forEach(s => signals.add(s));
const pkgSignals = analyzePackageJson(repoPath);
if (pkgSignals) {
pkgSignals.forEach(s => signals.add(s));
}
if (graph && graph.nodes) {
let hasRoutes = false;
let hasComponents = false;
let hasInfraNodes = false;
for (const [id, node] of Object.entries(graph.nodes)) {
if (node.type === 'route' || node.type === 'controller') hasRoutes = true;
if (node.type === 'component') hasComponents = true;
if (node.type === 'resource' || node.type === 'chart' || node.type === 'module') hasInfraNodes = true;
}
if (hasRoutes) signals.add('graph_routes');
if (hasComponents) signals.add('graph_components');
if (hasInfraNodes) signals.add('graph_infra');
}
let archetype = ARCHETYPES.UNKNOWN;
let confidence = 0.0;
if (signals.has('workspaces') || signals.has('monorepo_tools')) {
archetype = ARCHETYPES.MONOREPO;
confidence = 0.9;
} else if (signals.has('infra_files') || signals.has('graph_infra')) {
archetype = ARCHETYPES.INFRASTRUCTURE;
confidence = 0.9;
} else if (signals.has('frontend_framework') || signals.has('graph_components')) {
archetype = ARCHETYPES.FRONTEND;
confidence = 0.85;
} else if (signals.has('backend_framework') || signals.has('graph_routes') || signals.has('go_backend')) {
archetype = ARCHETYPES.BACKEND;
confidence = 0.85;
} else if (signals.has('library_exports')) {
archetype = ARCHETYPES.LIBRARY;
confidence = 0.7;
} else if (signals.has('python_app') || signals.has('rust_app')) {
archetype = ARCHETYPES.BACKEND;
confidence = 0.6;
}
return {
archetype,
confidence,
signals: Array.from(signals)
};
}
module.exports = { profileRepo, ARCHETYPES };
if (require.main === module) {
const targetDir = process.argv[2] || process.cwd();
const profile = profileRepo(targetDir);
console.log(JSON.stringify(profile, null, 2));
}