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-
This commit is contained in:
95
extract-dynamic.js
Normal file
95
extract-dynamic.js
Normal file
@@ -0,0 +1,95 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const STATE_SERVICES_REGEX = /(redis|postgres|mysql|mongo|kafka|sqs|rabbit|elasticsearch|cassandra|db|cache|queue|database|aurora)/;
|
||||
|
||||
/**
|
||||
* Dynamically extract system configurations and state boundaries using generic heuristics.
|
||||
* Replaces extract-deep.js and extract-patterns.js.
|
||||
* This MUST be deterministic (no LLM calls).
|
||||
*/
|
||||
function extractDynamic(graph, archetype, repoRoot) {
|
||||
const result = {
|
||||
configs: [],
|
||||
stateServices: [],
|
||||
deploymentPatterns: [],
|
||||
networkTopology: []
|
||||
};
|
||||
|
||||
if (!graph) return result;
|
||||
|
||||
const files = graph.files || [];
|
||||
const entities = graph.entities || {};
|
||||
|
||||
// 1. Config Surfaces (find generic config structures: Helm, JSON, YAML)
|
||||
const configFiles = files.filter(f => f.match(/\.(yaml|yml|json|toml|ini)$/i));
|
||||
|
||||
// Extract a sample of configs deterministically from generic entities
|
||||
for (const [id, entity] of Object.entries(entities)) {
|
||||
if (entity.type && (entity.type.includes('Config') || entity.type.includes('Params') || entity.type.includes('HelmValues'))) {
|
||||
result.configs.push({ name: entity.name, file: entity.file, type: entity.type });
|
||||
}
|
||||
|
||||
// 2. State Services (databases, caches, queues)
|
||||
const lowerName = entity.name ? entity.name.toLowerCase() : '';
|
||||
if (lowerName.match(STATE_SERVICES_REGEX)) {
|
||||
if (!result.stateServices.some(s => s.name === entity.name)) {
|
||||
result.stateServices.push({ name: entity.name, type: entity.type || 'State/Database' });
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Network Topology (if infra archetype)
|
||||
if (archetype.toLowerCase().includes('infra')) {
|
||||
if (lowerName.includes('vpc') || lowerName.includes('cidr') || lowerName.includes('subnet') || lowerName.includes('route') || lowerName.includes('nat')) {
|
||||
result.networkTopology.push({ name: entity.name, file: entity.file, type: 'Network Resource' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for imports or dependencies that match state services
|
||||
if (graph.dependencies) {
|
||||
for (const dep of Object.keys(graph.dependencies)) {
|
||||
const lowerDep = dep.toLowerCase();
|
||||
if (lowerDep.match(STATE_SERVICES_REGEX)) {
|
||||
if (!result.stateServices.some(s => s.name === dep)) {
|
||||
result.stateServices.push({ name: dep, type: 'External Dependency' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look at package.json dependencies for state services
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
|
||||
const deps = Object.keys(pkg.dependencies || {});
|
||||
for (const dep of deps) {
|
||||
if (dep.match(STATE_SERVICES_REGEX)) {
|
||||
if (!result.stateServices.some(s => s.name === dep)) {
|
||||
result.stateServices.push({ name: dep, type: 'NPM Dependency' });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse package.json for state services:', e.message);
|
||||
}
|
||||
|
||||
// 3. Deployment Patterns (CI/CD, GitOps)
|
||||
const ciFiles = files.filter(f => f.match(/(\.github\/workflows|jenkinsfile|\.gitlab-ci|argocd|kustomization|dockerfile|helm)/i));
|
||||
for (const cf of ciFiles) {
|
||||
let type = 'CI/CD';
|
||||
if (cf.toLowerCase().includes('argocd')) type = 'GitOps';
|
||||
if (cf.toLowerCase().includes('helm')) type = 'Helm Chart';
|
||||
result.deploymentPatterns.push({ file: cf, type });
|
||||
}
|
||||
|
||||
// Graceful fallback for config files if no config entities were found
|
||||
if (result.configs.length === 0) {
|
||||
for (const cf of configFiles.slice(0, 10)) {
|
||||
result.configs.push({ file: cf, type: 'Configuration File' });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = { extractDynamic };
|
||||
Reference in New Issue
Block a user