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:
28
graph.js
28
graph.js
@@ -82,15 +82,29 @@ class GraphStore {
|
||||
* @returns {GraphStore}
|
||||
*/
|
||||
static loadSnapshot(inputPath) {
|
||||
const data = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
|
||||
let data;
|
||||
if (typeof inputPath === 'string') {
|
||||
data = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
|
||||
} else {
|
||||
data = inputPath; // Allow passing the parsed object directly
|
||||
}
|
||||
const graph = new GraphStore();
|
||||
|
||||
for (const [id, entity] of Object.entries(data.nodes || {})) {
|
||||
graph.nodes.set(id, entity);
|
||||
const filePath = entity._file || entity.path;
|
||||
if (!data.fileIndex && filePath) {
|
||||
if (!graph.fileIndex.has(filePath)) graph.fileIndex.set(filePath, new Set());
|
||||
graph.fileIndex.get(filePath).add(id);
|
||||
}
|
||||
}
|
||||
|
||||
graph.edges = data.edges || [];
|
||||
|
||||
for (const e of graph.edges) {
|
||||
graph._edgeSet.add(`${e.type}:${e.source}->${e.target}`);
|
||||
}
|
||||
|
||||
for (const [filePath, entityIds] of Object.entries(data.fileIndex || {})) {
|
||||
graph.fileIndex.set(filePath, new Set(entityIds));
|
||||
}
|
||||
@@ -184,7 +198,17 @@ class GraphStore {
|
||||
diff.entities.added.push(newEntity);
|
||||
} else {
|
||||
// Deterministic deep comparison: sort keys, compare canonical JSON
|
||||
const canonicalize = (obj) => JSON.stringify(obj, Object.keys(obj).filter(k => k !== '_file').sort());
|
||||
const sortKeys = (obj) => {
|
||||
if (Array.isArray(obj)) return obj.map(sortKeys);
|
||||
if (obj && typeof obj === 'object') {
|
||||
return Object.keys(obj).filter(k => k !== '_file').sort().reduce((acc, key) => {
|
||||
acc[key] = sortKeys(obj[key]);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
const canonicalize = (obj) => JSON.stringify(sortKeys(obj));
|
||||
if (canonicalize(oldEntity) !== canonicalize(newEntity)) {
|
||||
diff.entities.modified.push({ old: oldEntity, new: newEntity });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user