agentic-kdd 3.5.1 → 3.5.3

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/tdd-gate.cjs +474 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-kdd",
3
- "version": "3.5.1",
3
+ "version": "3.5.3",
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,474 @@
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
+ // Strip ANSI color codes — Vitest adds them and break regex matching
120
+ raw = (raw || '').replace(/\x1b\[[0-9;]*m/g, '').replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
121
+
122
+ const result = {
123
+ allPassed: exitCode === 0,
124
+ total: 0, passed: 0, failed: 0,
125
+ failures: [], output: raw, error: null
126
+ };
127
+
128
+ // ── Jest / Vitest ────────────────────────────────────────────────────────
129
+ // "Tests: 5 passed, 2 failed, 7 total"
130
+ // Jest format: "Tests: 177 passed, 2 failed, 192 total"
131
+ const jestSummary = raw.match(/Tests:\s*(?:(\d+)\s+passed,?\s*)?(?:(\d+)\s+failed,?\s*)?(\d+)\s+total/i);
132
+ if (jestSummary) {
133
+ result.passed = parseInt(jestSummary[1] || '0');
134
+ result.failed = parseInt(jestSummary[2] || '0');
135
+ result.total = parseInt(jestSummary[3] || '0');
136
+ }
137
+
138
+ // Vitest format: "Tests 177 passed | 15 skipped (192)"
139
+ const vitestSummary = raw.match(/Tests\s+(\d+)\s+passed(?:\s*\|\s*(\d+)\s+failed)?(?:\s*\|[^(]*)?\s*\((\d+)\)/i);
140
+ if (vitestSummary && result.total === 0) {
141
+ result.passed = parseInt(vitestSummary[1] || '0');
142
+ result.failed = parseInt(vitestSummary[2] || '0');
143
+ result.total = parseInt(vitestSummary[3] || '0');
144
+ }
145
+
146
+ // Vitest format: "Test Files 38 passed | 2 skipped (40)"
147
+ const vitestFiles = raw.match(/Test Files\s+(\d+)\s+passed(?:\s*\|\s*(\d+)\s+failed)?(?:\s*\|[^(]*)?\s*\((\d+)\)/i);
148
+ if (vitestFiles && result.total === 0) {
149
+ result.passed = parseInt(vitestFiles[1] || '0');
150
+ result.failed = parseInt(vitestFiles[2] || '0');
151
+ result.total = parseInt(vitestFiles[3] || '0');
152
+ }
153
+
154
+ // "Test Suites: 1 failed, 2 passed, 3 total"
155
+ const suiteSummary = raw.match(/Test Suites:\s*(?:(\d+)\s+failed,?\s*)?(?:(\d+)\s+passed,?\s*)?(\d+)\s+total/i);
156
+ if (suiteSummary && result.total === 0) {
157
+ result.failed = parseInt(suiteSummary[1] || '0');
158
+ result.passed = parseInt(suiteSummary[2] || '0');
159
+ result.total = parseInt(suiteSummary[3] || '0');
160
+ }
161
+
162
+ // ── Mocha ────────────────────────────────────────────────────────────────
163
+ // " 5 passing" / " 2 failing"
164
+ const mochaPassing = raw.match(/(\d+)\s+passing/i);
165
+ const mochaFailing = raw.match(/(\d+)\s+failing/i);
166
+ if (mochaPassing || mochaFailing) {
167
+ result.passed = parseInt(mochaPassing?.[1] || '0');
168
+ result.failed = parseInt(mochaFailing?.[1] || '0');
169
+ result.total = result.passed + result.failed;
170
+ }
171
+
172
+ // ── pytest (básico) ──────────────────────────────────────────────────────
173
+ // "5 passed, 2 failed in 1.23s"
174
+ const pytestSummary = raw.match(/(\d+)\s+passed(?:,\s*(\d+)\s+failed)?/i);
175
+ if (pytestSummary && result.total === 0) {
176
+ result.passed = parseInt(pytestSummary[1] || '0');
177
+ result.failed = parseInt(pytestSummary[2] || '0');
178
+ result.total = result.passed + result.failed;
179
+ }
180
+
181
+ // ── Extraer nombres de tests fallidos ────────────────────────────────────
182
+ const failurePatterns = [
183
+ /●\s+(.+)$/gm, // jest bullets
184
+ /FAIL\s+.+\n.*›\s+(.+)/gm, // jest FAIL
185
+ /\d+\)\s+(.+)\n.*Error:/gm, // mocha
186
+ /FAILED\s+(test_.+)/gm, // pytest
187
+ /AssertionError.*at\s+(.+):\d+/gm, // generic
188
+ ];
189
+
190
+ for (const pattern of failurePatterns) {
191
+ let match;
192
+ while ((match = pattern.exec(raw)) !== null) {
193
+ const failure = match[1].trim();
194
+ if (failure && !result.failures.includes(failure) && result.failures.length < 20) {
195
+ result.failures.push(failure);
196
+ }
197
+ }
198
+ }
199
+
200
+ // ── Fallback: si exitCode !== 0 y no parseamos nada ─────────────────────
201
+ if (exitCode !== 0 && result.total === 0) {
202
+ result.allPassed = false;
203
+ result.failed = 1;
204
+ if (result.failures.length === 0) {
205
+ // Extraer la primera línea de error
206
+ const errorLine = raw.split('\n').find(l => /error|fail|cannot|unexpected/i.test(l));
207
+ if (errorLine) result.failures.push(errorLine.trim().substring(0, 120));
208
+ else result.failures.push('Error desconocido — revisar output completo');
209
+ }
210
+ }
211
+
212
+ result.allPassed = result.failed === 0 && exitCode === 0;
213
+ return result;
214
+ }
215
+
216
+ /**
217
+ * Encuentra archivos de test en el scope del plan.
218
+ * @param {string} projectRoot
219
+ * @param {string[]} [scope] - archivos/directorios a buscar
220
+ * @returns {string[]}
221
+ */
222
+ function findTestFiles(projectRoot, scope = []) {
223
+ const testPatterns = [
224
+ /\.(test|spec)\.(ts|tsx|js|jsx)$/,
225
+ /__(tests?)__\//,
226
+ /test\/.*\.(ts|js)$/,
227
+ ];
228
+
229
+ const results = [];
230
+
231
+ const searchDir = (dir, maxDepth = 5, depth = 0) => {
232
+ if (depth > maxDepth) return;
233
+ try {
234
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
235
+ for (const e of entries) {
236
+ if (e.name.startsWith('.') || e.name === 'node_modules') continue;
237
+ const fullPath = path.join(dir, e.name);
238
+ if (e.isDirectory()) {
239
+ searchDir(fullPath, maxDepth, depth + 1);
240
+ } else if (testPatterns.some(p => p.test(fullPath))) {
241
+ results.push(path.relative(projectRoot, fullPath));
242
+ }
243
+ }
244
+ } catch {}
245
+ };
246
+
247
+ if (scope.length > 0) {
248
+ // Buscar tests relacionados con los archivos del scope
249
+ for (const f of scope) {
250
+ const base = path.basename(f, path.extname(f));
251
+ const dir = path.dirname(path.join(projectRoot, f));
252
+ searchDir(dir);
253
+ // También buscar en __tests__ relativo
254
+ const testDir = path.join(path.dirname(path.join(projectRoot, f)), '__tests__');
255
+ if (fs.existsSync(testDir)) searchDir(testDir);
256
+ }
257
+ } else {
258
+ searchDir(projectRoot);
259
+ }
260
+
261
+ return [...new Set(results)];
262
+ }
263
+
264
+ // ─── SELF-HEALING LOOP ────────────────────────────────────────────────────────
265
+
266
+ /**
267
+ * Carga el estado del TDD gate desde el archivo de estado.
268
+ */
269
+ function loadState(projectRoot) {
270
+ const statePath = path.join(projectRoot, TDD_STATE_FILE);
271
+ if (fs.existsSync(statePath)) {
272
+ try { return JSON.parse(fs.readFileSync(statePath, 'utf8')); } catch {}
273
+ }
274
+ return null;
275
+ }
276
+
277
+ function saveState(projectRoot, state) {
278
+ const statePath = path.join(projectRoot, TDD_STATE_FILE);
279
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
280
+ }
281
+
282
+ function clearState(projectRoot) {
283
+ const statePath = path.join(projectRoot, TDD_STATE_FILE);
284
+ if (fs.existsSync(statePath)) fs.unlinkSync(statePath);
285
+ }
286
+
287
+ /**
288
+ * LOOP PRINCIPAL DE SELF-HEALING
289
+ *
290
+ * @param {object} opts
291
+ * projectRoot: string
292
+ * area: string (área del módulo)
293
+ * scope: string[] (archivos tocados en la fase actual)
294
+ * testCommand: string|null (null = autodetectar)
295
+ * @returns {object} resultado con allPassed, iterations, etc.
296
+ */
297
+ function runSelfHealingLoop(opts) {
298
+ const { projectRoot = process.cwd(), area = 'global', scope = [], testCommand = null } = opts;
299
+
300
+ const command = testCommand || detectTestCommand(projectRoot);
301
+ if (!command) {
302
+ return {
303
+ success: false,
304
+ allPassed: false,
305
+ reason: 'No se detectó comando de tests. Configurar test: en config.md.',
306
+ tests_found: [],
307
+ iterations: 0,
308
+ };
309
+ }
310
+
311
+ const testFiles = findTestFiles(projectRoot, scope);
312
+
313
+ if (testFiles.length === 0) {
314
+ return {
315
+ success: false,
316
+ allPassed: false,
317
+ reason: 'No se encontraron archivos de test. TDD es OBLIGATORIO — crear tests antes de avanzar.',
318
+ tests_found: [],
319
+ iterations: 0,
320
+ command,
321
+ };
322
+ }
323
+
324
+ console.log(`\n[TDD-GATE] Comando: ${command}`);
325
+ console.log(`[TDD-GATE] Tests encontrados: ${testFiles.length}`);
326
+ console.log(`[TDD-GATE] Área: ${area}`);
327
+ console.log(`[TDD-GATE] Max iteraciones: ${MAX_HEALING_ITERATIONS}\n`);
328
+
329
+ let iteration = 0;
330
+ let lastResult = null;
331
+ const history = [];
332
+
333
+ // ── LOOP ──────────────────────────────────────────────────────────────────
334
+ while (iteration < MAX_HEALING_ITERATIONS) {
335
+ iteration++;
336
+ console.log(`[TDD-GATE] ── Iteración ${iteration}/${MAX_HEALING_ITERATIONS} ──`);
337
+
338
+ const result = runTests(command, projectRoot);
339
+ lastResult = result;
340
+ history.push({ iteration, ...result });
341
+
342
+ console.log(`[TDD-GATE] Resultado: ${result.allPassed ? '✅ PASS' : '❌ FAIL'}`);
343
+ console.log(`[TDD-GATE] Total: ${result.total} | Pasando: ${result.passed} | Fallando: ${result.failed}`);
344
+
345
+ if (result.allPassed) {
346
+ console.log(`\n[TDD-GATE] ✅ PASS en iteración ${iteration}`);
347
+ break;
348
+ }
349
+
350
+ if (iteration < MAX_HEALING_ITERATIONS) {
351
+ console.log(`[TDD-GATE] Fallando tests: ${result.failures.slice(0, 5).join(', ')}`);
352
+ console.log(`[TDD-GATE] 🔄 Señal de healing enviada al agente para iteración ${iteration + 1}`);
353
+ console.log(`[TDD-GATE] Diagnóstico necesario: revisar error → aplicar fix → re-ejecutar\n`);
354
+
355
+ // Guardar estado para que el agente sepa en qué iteración está
356
+ saveState(projectRoot, {
357
+ iteration,
358
+ lastResult: result,
359
+ area,
360
+ command,
361
+ testFiles,
362
+ timestamp: new Date().toISOString(),
363
+ });
364
+ }
365
+ }
366
+
367
+ // ── SUITE COMPLETA (verificar regresiones) ────────────────────────────────
368
+ let regressions = [];
369
+ if (lastResult?.allPassed) {
370
+ console.log('\n[TDD-GATE] Verificando suite completa para detectar regresiones...');
371
+ const suiteResult = runTests(command, projectRoot);
372
+ if (!suiteResult.allPassed) {
373
+ regressions = suiteResult.failures;
374
+ console.log(`[TDD-GATE] ⚠️ Regresiones detectadas: ${regressions.join(', ')}`);
375
+ } else {
376
+ console.log('[TDD-GATE] ✅ Suite completa: sin regresiones');
377
+ }
378
+ }
379
+
380
+ const finalResult = {
381
+ success: lastResult?.allPassed && regressions.length === 0,
382
+ allPassed: lastResult?.allPassed ?? false,
383
+ iterations: iteration,
384
+ tests_found: testFiles,
385
+ tests_passing: lastResult?.passed ?? 0,
386
+ tests_failing: lastResult?.failed ?? 0,
387
+ failing_tests: lastResult?.failures ?? [],
388
+ regressions,
389
+ command,
390
+ area,
391
+ history,
392
+ };
393
+
394
+ if (!finalResult.success) {
395
+ finalResult.stop_reason = lastResult?.allPassed
396
+ ? `Regresiones introducidas: ${regressions.join(', ')}`
397
+ : `Tests fallando después de ${iteration} iteraciones. Requiere intervención humana.`;
398
+ }
399
+
400
+ // Limpiar estado si terminamos
401
+ clearState(projectRoot);
402
+
403
+ // Imprimir reporte
404
+ _printTDDReport(finalResult);
405
+
406
+ return finalResult;
407
+ }
408
+
409
+ function _printTDDReport(r) {
410
+ console.log('\n═══════════════════════════════════════════════════');
411
+ console.log(' 🧪 TDD-GATE REPORTE FINAL');
412
+ console.log('═══════════════════════════════════════════════════');
413
+ console.log(` Resultado: ${r.success ? '✅ PASS' : '🛑 STOP'}`);
414
+ console.log(` Tests encontrados: ${r.tests_found.length}`);
415
+ console.log(` Pasando: ${r.tests_passing}`);
416
+ console.log(` Fallando: ${r.tests_failing}`);
417
+ console.log(` Iteraciones: ${r.iterations} (max ${MAX_HEALING_ITERATIONS})`);
418
+ console.log(` Regresiones: ${r.regressions.length === 0 ? '0 ✓' : r.regressions.join(', ')}`);
419
+ if (!r.success && r.stop_reason) {
420
+ console.log(`\n ⛔ STOP: ${r.stop_reason}`);
421
+ console.log(' Acción requerida: diagnóstico e intervención humana.');
422
+ }
423
+ console.log('═══════════════════════════════════════════════════\n');
424
+ }
425
+
426
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
427
+
428
+ if (require.main === module) {
429
+ const [,, command, ...args] = process.argv;
430
+ const projectRoot = process.cwd();
431
+
432
+ switch (command) {
433
+ case 'run': {
434
+ const area = args[0] || 'global';
435
+ const result = runSelfHealingLoop({ projectRoot, area });
436
+ process.exit(result.success ? 0 : 1);
437
+ break;
438
+ }
439
+ case 'detect': {
440
+ const cmd = detectTestCommand(projectRoot);
441
+ console.log(cmd ? `Comando detectado: ${cmd}` : 'No se detectó comando de tests');
442
+ break;
443
+ }
444
+ case 'status': {
445
+ const state = loadState(projectRoot);
446
+ if (state) {
447
+ console.log('Estado TDD actual:', JSON.stringify(state, null, 2));
448
+ } else {
449
+ console.log('Sin estado TDD activo');
450
+ }
451
+ break;
452
+ }
453
+ case 'clear': {
454
+ clearState(projectRoot);
455
+ console.log('Estado TDD limpiado');
456
+ break;
457
+ }
458
+ case 'find': {
459
+ const files = findTestFiles(projectRoot);
460
+ console.log(`Tests encontrados (${files.length}):\n${files.join('\n')}`);
461
+ break;
462
+ }
463
+ default:
464
+ console.log('Uso: node tdd-gate.cjs [run [area] | detect | status | clear | find]');
465
+ }
466
+ }
467
+
468
+ module.exports = {
469
+ runSelfHealingLoop,
470
+ runTests,
471
+ parseTestOutput,
472
+ findTestFiles,
473
+ detectTestCommand,
474
+ };