agentic-kdd 3.5.1 → 3.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/tdd-gate.cjs +471 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-kdd",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.2",
|
|
4
4
|
"description": "Autonomous development pipeline — aa: · ag: · audit: · AST graph · Harness · Specs · Impact analysis · Decision trail · Metrics · MCP server. Works with Cursor and Claude Code.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/tdd-gate.cjs
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentic KDD — TDD Gate v1.0
|
|
3
|
+
* Loop mecánico de self-healing: ejecuta tests → evalúa → retry → abort
|
|
4
|
+
*
|
|
5
|
+
* Este módulo reemplaza la instrucción markdown de TDD+Self-Healing
|
|
6
|
+
* con un loop determinista en código Node.js.
|
|
7
|
+
*
|
|
8
|
+
* Diferencia clave:
|
|
9
|
+
* ANTES: markdown dice "intenta hasta 3 veces" → el agente decide si lo sigue
|
|
10
|
+
* AHORA: código fuerza el loop, el agente no puede saltárselo
|
|
11
|
+
*
|
|
12
|
+
* Uso:
|
|
13
|
+
* node .agentic/grafo/tdd-gate.cjs run [area]
|
|
14
|
+
* node .agentic/grafo/tdd-gate.cjs status
|
|
15
|
+
* node .agentic/grafo/tdd-gate.cjs clear
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const { execSync, spawnSync } = require('child_process');
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
// ─── CONSTANTES ───────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const MAX_HEALING_ITERATIONS = 3;
|
|
27
|
+
const MAX_REGRESSION_ITERATIONS = 2;
|
|
28
|
+
const TDD_STATE_FILE = '.agentic/_tdd_state.json';
|
|
29
|
+
const TEST_COMMANDS = [
|
|
30
|
+
'npm test',
|
|
31
|
+
'npm run test',
|
|
32
|
+
'npx jest --passWithNoTests',
|
|
33
|
+
'npx vitest run',
|
|
34
|
+
'npx jest',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// ─── TEST RUNNER ──────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Detecta el comando de tests del proyecto.
|
|
41
|
+
* Prueba los comandos en orden hasta encontrar uno que funcione.
|
|
42
|
+
* @returns {string|null}
|
|
43
|
+
*/
|
|
44
|
+
function detectTestCommand(projectRoot) {
|
|
45
|
+
// 1. Leer desde config.md si ya está guardado
|
|
46
|
+
const configPath = path.join(projectRoot, '.agentic/config.md');
|
|
47
|
+
if (fs.existsSync(configPath)) {
|
|
48
|
+
const config = fs.readFileSync(configPath, 'utf8');
|
|
49
|
+
const match = config.match(/^\s*test:\s*(.+)$/m);
|
|
50
|
+
if (match && match[1].trim() !== '—' && match[1].trim() !== '') {
|
|
51
|
+
return match[1].trim();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2. Detectar por package.json
|
|
56
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
57
|
+
if (fs.existsSync(pkgPath)) {
|
|
58
|
+
try {
|
|
59
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
60
|
+
if (pkg.scripts?.test && pkg.scripts.test !== 'echo "Error: no test specified" && exit 1') {
|
|
61
|
+
return 'npm test';
|
|
62
|
+
}
|
|
63
|
+
if (pkg.scripts?.['test:run']) return 'npm run test:run';
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 3. Probar comandos conocidos
|
|
68
|
+
for (const cmd of TEST_COMMANDS) {
|
|
69
|
+
try {
|
|
70
|
+
const result = spawnSync(cmd.split(' ')[0], cmd.split(' ').slice(1), {
|
|
71
|
+
cwd: projectRoot, timeout: 10000, stdio: 'pipe'
|
|
72
|
+
});
|
|
73
|
+
if (result.status !== null) return cmd;
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Ejecuta la suite de tests y retorna el resultado estructurado.
|
|
82
|
+
* @param {string} command
|
|
83
|
+
* @param {string} projectRoot
|
|
84
|
+
* @param {string} [testFile] - archivo específico o null para suite completa
|
|
85
|
+
* @returns {{ allPassed: boolean, total: number, passed: number, failed: number,
|
|
86
|
+
* failures: string[], output: string, error: string|null }}
|
|
87
|
+
*/
|
|
88
|
+
function runTests(command, projectRoot, testFile = null) {
|
|
89
|
+
const fullCmd = testFile ? `${command} -- ${testFile}` : command;
|
|
90
|
+
|
|
91
|
+
let output = '';
|
|
92
|
+
let errorOutput = '';
|
|
93
|
+
let exitCode = 0;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const result = spawnSync(
|
|
97
|
+
'sh', ['-c', fullCmd + ' 2>&1'],
|
|
98
|
+
{ cwd: projectRoot, timeout: 120000, stdio: 'pipe', encoding: 'utf8' }
|
|
99
|
+
);
|
|
100
|
+
output = result.stdout || '';
|
|
101
|
+
errorOutput = result.stderr || '';
|
|
102
|
+
exitCode = result.status ?? 1;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
return {
|
|
105
|
+
allPassed: false, total: 0, passed: 0, failed: 1,
|
|
106
|
+
failures: [`ERROR ejecutando tests: ${err.message}`],
|
|
107
|
+
output: '', error: err.message
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return parseTestOutput(output + errorOutput, exitCode);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Parsea el output de múltiples frameworks de testing.
|
|
116
|
+
* Soporta: jest, vitest, mocha, jasmine, tap, pytest (output básico).
|
|
117
|
+
*/
|
|
118
|
+
function parseTestOutput(raw, exitCode) {
|
|
119
|
+
const result = {
|
|
120
|
+
allPassed: exitCode === 0,
|
|
121
|
+
total: 0, passed: 0, failed: 0,
|
|
122
|
+
failures: [], output: raw, error: null
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// ── Jest / Vitest ────────────────────────────────────────────────────────
|
|
126
|
+
// "Tests: 5 passed, 2 failed, 7 total"
|
|
127
|
+
// Jest format: "Tests: 177 passed, 2 failed, 192 total"
|
|
128
|
+
const jestSummary = raw.match(/Tests:\s*(?:(\d+)\s+passed,?\s*)?(?:(\d+)\s+failed,?\s*)?(\d+)\s+total/i);
|
|
129
|
+
if (jestSummary) {
|
|
130
|
+
result.passed = parseInt(jestSummary[1] || '0');
|
|
131
|
+
result.failed = parseInt(jestSummary[2] || '0');
|
|
132
|
+
result.total = parseInt(jestSummary[3] || '0');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Vitest format: "Tests 177 passed | 15 skipped (192)"
|
|
136
|
+
const vitestSummary = raw.match(/Tests\s+(\d+)\s+passed(?:\s*\|\s*(\d+)\s+failed)?(?:\s*\|[^(]*)?\s*\((\d+)\)/i);
|
|
137
|
+
if (vitestSummary && result.total === 0) {
|
|
138
|
+
result.passed = parseInt(vitestSummary[1] || '0');
|
|
139
|
+
result.failed = parseInt(vitestSummary[2] || '0');
|
|
140
|
+
result.total = parseInt(vitestSummary[3] || '0');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Vitest format: "Test Files 38 passed | 2 skipped (40)"
|
|
144
|
+
const vitestFiles = raw.match(/Test Files\s+(\d+)\s+passed(?:\s*\|\s*(\d+)\s+failed)?(?:\s*\|[^(]*)?\s*\((\d+)\)/i);
|
|
145
|
+
if (vitestFiles && result.total === 0) {
|
|
146
|
+
result.passed = parseInt(vitestFiles[1] || '0');
|
|
147
|
+
result.failed = parseInt(vitestFiles[2] || '0');
|
|
148
|
+
result.total = parseInt(vitestFiles[3] || '0');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// "Test Suites: 1 failed, 2 passed, 3 total"
|
|
152
|
+
const suiteSummary = raw.match(/Test Suites:\s*(?:(\d+)\s+failed,?\s*)?(?:(\d+)\s+passed,?\s*)?(\d+)\s+total/i);
|
|
153
|
+
if (suiteSummary && result.total === 0) {
|
|
154
|
+
result.failed = parseInt(suiteSummary[1] || '0');
|
|
155
|
+
result.passed = parseInt(suiteSummary[2] || '0');
|
|
156
|
+
result.total = parseInt(suiteSummary[3] || '0');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Mocha ────────────────────────────────────────────────────────────────
|
|
160
|
+
// " 5 passing" / " 2 failing"
|
|
161
|
+
const mochaPassing = raw.match(/(\d+)\s+passing/i);
|
|
162
|
+
const mochaFailing = raw.match(/(\d+)\s+failing/i);
|
|
163
|
+
if (mochaPassing || mochaFailing) {
|
|
164
|
+
result.passed = parseInt(mochaPassing?.[1] || '0');
|
|
165
|
+
result.failed = parseInt(mochaFailing?.[1] || '0');
|
|
166
|
+
result.total = result.passed + result.failed;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── pytest (básico) ──────────────────────────────────────────────────────
|
|
170
|
+
// "5 passed, 2 failed in 1.23s"
|
|
171
|
+
const pytestSummary = raw.match(/(\d+)\s+passed(?:,\s*(\d+)\s+failed)?/i);
|
|
172
|
+
if (pytestSummary && result.total === 0) {
|
|
173
|
+
result.passed = parseInt(pytestSummary[1] || '0');
|
|
174
|
+
result.failed = parseInt(pytestSummary[2] || '0');
|
|
175
|
+
result.total = result.passed + result.failed;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Extraer nombres de tests fallidos ────────────────────────────────────
|
|
179
|
+
const failurePatterns = [
|
|
180
|
+
/●\s+(.+)$/gm, // jest bullets
|
|
181
|
+
/FAIL\s+.+\n.*›\s+(.+)/gm, // jest FAIL
|
|
182
|
+
/\d+\)\s+(.+)\n.*Error:/gm, // mocha
|
|
183
|
+
/FAILED\s+(test_.+)/gm, // pytest
|
|
184
|
+
/AssertionError.*at\s+(.+):\d+/gm, // generic
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
for (const pattern of failurePatterns) {
|
|
188
|
+
let match;
|
|
189
|
+
while ((match = pattern.exec(raw)) !== null) {
|
|
190
|
+
const failure = match[1].trim();
|
|
191
|
+
if (failure && !result.failures.includes(failure) && result.failures.length < 20) {
|
|
192
|
+
result.failures.push(failure);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Fallback: si exitCode !== 0 y no parseamos nada ─────────────────────
|
|
198
|
+
if (exitCode !== 0 && result.total === 0) {
|
|
199
|
+
result.allPassed = false;
|
|
200
|
+
result.failed = 1;
|
|
201
|
+
if (result.failures.length === 0) {
|
|
202
|
+
// Extraer la primera línea de error
|
|
203
|
+
const errorLine = raw.split('\n').find(l => /error|fail|cannot|unexpected/i.test(l));
|
|
204
|
+
if (errorLine) result.failures.push(errorLine.trim().substring(0, 120));
|
|
205
|
+
else result.failures.push('Error desconocido — revisar output completo');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
result.allPassed = result.failed === 0 && exitCode === 0;
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Encuentra archivos de test en el scope del plan.
|
|
215
|
+
* @param {string} projectRoot
|
|
216
|
+
* @param {string[]} [scope] - archivos/directorios a buscar
|
|
217
|
+
* @returns {string[]}
|
|
218
|
+
*/
|
|
219
|
+
function findTestFiles(projectRoot, scope = []) {
|
|
220
|
+
const testPatterns = [
|
|
221
|
+
/\.(test|spec)\.(ts|tsx|js|jsx)$/,
|
|
222
|
+
/__(tests?)__\//,
|
|
223
|
+
/test\/.*\.(ts|js)$/,
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
const results = [];
|
|
227
|
+
|
|
228
|
+
const searchDir = (dir, maxDepth = 5, depth = 0) => {
|
|
229
|
+
if (depth > maxDepth) return;
|
|
230
|
+
try {
|
|
231
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
232
|
+
for (const e of entries) {
|
|
233
|
+
if (e.name.startsWith('.') || e.name === 'node_modules') continue;
|
|
234
|
+
const fullPath = path.join(dir, e.name);
|
|
235
|
+
if (e.isDirectory()) {
|
|
236
|
+
searchDir(fullPath, maxDepth, depth + 1);
|
|
237
|
+
} else if (testPatterns.some(p => p.test(fullPath))) {
|
|
238
|
+
results.push(path.relative(projectRoot, fullPath));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} catch {}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
if (scope.length > 0) {
|
|
245
|
+
// Buscar tests relacionados con los archivos del scope
|
|
246
|
+
for (const f of scope) {
|
|
247
|
+
const base = path.basename(f, path.extname(f));
|
|
248
|
+
const dir = path.dirname(path.join(projectRoot, f));
|
|
249
|
+
searchDir(dir);
|
|
250
|
+
// También buscar en __tests__ relativo
|
|
251
|
+
const testDir = path.join(path.dirname(path.join(projectRoot, f)), '__tests__');
|
|
252
|
+
if (fs.existsSync(testDir)) searchDir(testDir);
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
searchDir(projectRoot);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return [...new Set(results)];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ─── SELF-HEALING LOOP ────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Carga el estado del TDD gate desde el archivo de estado.
|
|
265
|
+
*/
|
|
266
|
+
function loadState(projectRoot) {
|
|
267
|
+
const statePath = path.join(projectRoot, TDD_STATE_FILE);
|
|
268
|
+
if (fs.existsSync(statePath)) {
|
|
269
|
+
try { return JSON.parse(fs.readFileSync(statePath, 'utf8')); } catch {}
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function saveState(projectRoot, state) {
|
|
275
|
+
const statePath = path.join(projectRoot, TDD_STATE_FILE);
|
|
276
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function clearState(projectRoot) {
|
|
280
|
+
const statePath = path.join(projectRoot, TDD_STATE_FILE);
|
|
281
|
+
if (fs.existsSync(statePath)) fs.unlinkSync(statePath);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* LOOP PRINCIPAL DE SELF-HEALING
|
|
286
|
+
*
|
|
287
|
+
* @param {object} opts
|
|
288
|
+
* projectRoot: string
|
|
289
|
+
* area: string (área del módulo)
|
|
290
|
+
* scope: string[] (archivos tocados en la fase actual)
|
|
291
|
+
* testCommand: string|null (null = autodetectar)
|
|
292
|
+
* @returns {object} resultado con allPassed, iterations, etc.
|
|
293
|
+
*/
|
|
294
|
+
function runSelfHealingLoop(opts) {
|
|
295
|
+
const { projectRoot = process.cwd(), area = 'global', scope = [], testCommand = null } = opts;
|
|
296
|
+
|
|
297
|
+
const command = testCommand || detectTestCommand(projectRoot);
|
|
298
|
+
if (!command) {
|
|
299
|
+
return {
|
|
300
|
+
success: false,
|
|
301
|
+
allPassed: false,
|
|
302
|
+
reason: 'No se detectó comando de tests. Configurar test: en config.md.',
|
|
303
|
+
tests_found: [],
|
|
304
|
+
iterations: 0,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const testFiles = findTestFiles(projectRoot, scope);
|
|
309
|
+
|
|
310
|
+
if (testFiles.length === 0) {
|
|
311
|
+
return {
|
|
312
|
+
success: false,
|
|
313
|
+
allPassed: false,
|
|
314
|
+
reason: 'No se encontraron archivos de test. TDD es OBLIGATORIO — crear tests antes de avanzar.',
|
|
315
|
+
tests_found: [],
|
|
316
|
+
iterations: 0,
|
|
317
|
+
command,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
console.log(`\n[TDD-GATE] Comando: ${command}`);
|
|
322
|
+
console.log(`[TDD-GATE] Tests encontrados: ${testFiles.length}`);
|
|
323
|
+
console.log(`[TDD-GATE] Área: ${area}`);
|
|
324
|
+
console.log(`[TDD-GATE] Max iteraciones: ${MAX_HEALING_ITERATIONS}\n`);
|
|
325
|
+
|
|
326
|
+
let iteration = 0;
|
|
327
|
+
let lastResult = null;
|
|
328
|
+
const history = [];
|
|
329
|
+
|
|
330
|
+
// ── LOOP ──────────────────────────────────────────────────────────────────
|
|
331
|
+
while (iteration < MAX_HEALING_ITERATIONS) {
|
|
332
|
+
iteration++;
|
|
333
|
+
console.log(`[TDD-GATE] ── Iteración ${iteration}/${MAX_HEALING_ITERATIONS} ──`);
|
|
334
|
+
|
|
335
|
+
const result = runTests(command, projectRoot);
|
|
336
|
+
lastResult = result;
|
|
337
|
+
history.push({ iteration, ...result });
|
|
338
|
+
|
|
339
|
+
console.log(`[TDD-GATE] Resultado: ${result.allPassed ? '✅ PASS' : '❌ FAIL'}`);
|
|
340
|
+
console.log(`[TDD-GATE] Total: ${result.total} | Pasando: ${result.passed} | Fallando: ${result.failed}`);
|
|
341
|
+
|
|
342
|
+
if (result.allPassed) {
|
|
343
|
+
console.log(`\n[TDD-GATE] ✅ PASS en iteración ${iteration}`);
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (iteration < MAX_HEALING_ITERATIONS) {
|
|
348
|
+
console.log(`[TDD-GATE] Fallando tests: ${result.failures.slice(0, 5).join(', ')}`);
|
|
349
|
+
console.log(`[TDD-GATE] 🔄 Señal de healing enviada al agente para iteración ${iteration + 1}`);
|
|
350
|
+
console.log(`[TDD-GATE] Diagnóstico necesario: revisar error → aplicar fix → re-ejecutar\n`);
|
|
351
|
+
|
|
352
|
+
// Guardar estado para que el agente sepa en qué iteración está
|
|
353
|
+
saveState(projectRoot, {
|
|
354
|
+
iteration,
|
|
355
|
+
lastResult: result,
|
|
356
|
+
area,
|
|
357
|
+
command,
|
|
358
|
+
testFiles,
|
|
359
|
+
timestamp: new Date().toISOString(),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── SUITE COMPLETA (verificar regresiones) ────────────────────────────────
|
|
365
|
+
let regressions = [];
|
|
366
|
+
if (lastResult?.allPassed) {
|
|
367
|
+
console.log('\n[TDD-GATE] Verificando suite completa para detectar regresiones...');
|
|
368
|
+
const suiteResult = runTests(command, projectRoot);
|
|
369
|
+
if (!suiteResult.allPassed) {
|
|
370
|
+
regressions = suiteResult.failures;
|
|
371
|
+
console.log(`[TDD-GATE] ⚠️ Regresiones detectadas: ${regressions.join(', ')}`);
|
|
372
|
+
} else {
|
|
373
|
+
console.log('[TDD-GATE] ✅ Suite completa: sin regresiones');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const finalResult = {
|
|
378
|
+
success: lastResult?.allPassed && regressions.length === 0,
|
|
379
|
+
allPassed: lastResult?.allPassed ?? false,
|
|
380
|
+
iterations: iteration,
|
|
381
|
+
tests_found: testFiles,
|
|
382
|
+
tests_passing: lastResult?.passed ?? 0,
|
|
383
|
+
tests_failing: lastResult?.failed ?? 0,
|
|
384
|
+
failing_tests: lastResult?.failures ?? [],
|
|
385
|
+
regressions,
|
|
386
|
+
command,
|
|
387
|
+
area,
|
|
388
|
+
history,
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
if (!finalResult.success) {
|
|
392
|
+
finalResult.stop_reason = lastResult?.allPassed
|
|
393
|
+
? `Regresiones introducidas: ${regressions.join(', ')}`
|
|
394
|
+
: `Tests fallando después de ${iteration} iteraciones. Requiere intervención humana.`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Limpiar estado si terminamos
|
|
398
|
+
clearState(projectRoot);
|
|
399
|
+
|
|
400
|
+
// Imprimir reporte
|
|
401
|
+
_printTDDReport(finalResult);
|
|
402
|
+
|
|
403
|
+
return finalResult;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function _printTDDReport(r) {
|
|
407
|
+
console.log('\n═══════════════════════════════════════════════════');
|
|
408
|
+
console.log(' 🧪 TDD-GATE REPORTE FINAL');
|
|
409
|
+
console.log('═══════════════════════════════════════════════════');
|
|
410
|
+
console.log(` Resultado: ${r.success ? '✅ PASS' : '🛑 STOP'}`);
|
|
411
|
+
console.log(` Tests encontrados: ${r.tests_found.length}`);
|
|
412
|
+
console.log(` Pasando: ${r.tests_passing}`);
|
|
413
|
+
console.log(` Fallando: ${r.tests_failing}`);
|
|
414
|
+
console.log(` Iteraciones: ${r.iterations} (max ${MAX_HEALING_ITERATIONS})`);
|
|
415
|
+
console.log(` Regresiones: ${r.regressions.length === 0 ? '0 ✓' : r.regressions.join(', ')}`);
|
|
416
|
+
if (!r.success && r.stop_reason) {
|
|
417
|
+
console.log(`\n ⛔ STOP: ${r.stop_reason}`);
|
|
418
|
+
console.log(' Acción requerida: diagnóstico e intervención humana.');
|
|
419
|
+
}
|
|
420
|
+
console.log('═══════════════════════════════════════════════════\n');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
if (require.main === module) {
|
|
426
|
+
const [,, command, ...args] = process.argv;
|
|
427
|
+
const projectRoot = process.cwd();
|
|
428
|
+
|
|
429
|
+
switch (command) {
|
|
430
|
+
case 'run': {
|
|
431
|
+
const area = args[0] || 'global';
|
|
432
|
+
const result = runSelfHealingLoop({ projectRoot, area });
|
|
433
|
+
process.exit(result.success ? 0 : 1);
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
case 'detect': {
|
|
437
|
+
const cmd = detectTestCommand(projectRoot);
|
|
438
|
+
console.log(cmd ? `Comando detectado: ${cmd}` : 'No se detectó comando de tests');
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
case 'status': {
|
|
442
|
+
const state = loadState(projectRoot);
|
|
443
|
+
if (state) {
|
|
444
|
+
console.log('Estado TDD actual:', JSON.stringify(state, null, 2));
|
|
445
|
+
} else {
|
|
446
|
+
console.log('Sin estado TDD activo');
|
|
447
|
+
}
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
case 'clear': {
|
|
451
|
+
clearState(projectRoot);
|
|
452
|
+
console.log('Estado TDD limpiado');
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
case 'find': {
|
|
456
|
+
const files = findTestFiles(projectRoot);
|
|
457
|
+
console.log(`Tests encontrados (${files.length}):\n${files.join('\n')}`);
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
default:
|
|
461
|
+
console.log('Uso: node tdd-gate.cjs [run [area] | detect | status | clear | find]');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
module.exports = {
|
|
466
|
+
runSelfHealingLoop,
|
|
467
|
+
runTests,
|
|
468
|
+
parseTestOutput,
|
|
469
|
+
findTestFiles,
|
|
470
|
+
detectTestCommand,
|
|
471
|
+
};
|