agentic-kdd 3.2.3 → 3.5.1

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.
@@ -0,0 +1,915 @@
1
+ /**
2
+ * Agentic KDD — Autonomous Decision Engine v1.0
3
+ *
4
+ * Motor de decisión autónoma L4.
5
+ *
6
+ * Lo que resuelve:
7
+ * - El agente detecta situaciones que afectan lo que se está trabajando
8
+ * y decide autónomamente si implementar, advertir, o diferir
9
+ * - Prerequisite chain: detecta si algo que hay que tocar tiene un
10
+ * prerequisito roto y lo resuelve primero
11
+ * - Cross-module pattern: si el mismo error ya ocurrió en otro módulo
12
+ * y fue resuelto allá, aplica la misma solución aquí
13
+ * - Full spectrum: no solo mira errores — también protege lo que funciona
14
+ *
15
+ * Decisiones posibles:
16
+ * STOP → blast CRITICAL o PROTECTED contract roto
17
+ * No implementar. Reportar exactamente qué rompería.
18
+ * WARN → blast HIGH o VERIFIED contract en riesgo
19
+ * Implementar pero avisar. El dev decide si continuar.
20
+ * IMPLEMENT → blast MEDIUM/LOW + sin rotura de contratos
21
+ * Implementar ahora dentro del ciclo.
22
+ * IMPLEMENT_CAUTIOUS→ prerequisito resuelto primero, luego la tarea
23
+ * Implementar en orden: prerequisito → tarea original
24
+ * DEFER → blast LOW + sin historial + sin contratos en riesgo
25
+ * No tocar ahora. Agregar a cola de sugerencias al final.
26
+ *
27
+ * Uso desde el pipeline (harness.cjs lo llama automáticamente):
28
+ * const { analyze } = require('./autonomous-decision.cjs');
29
+ * const decision = await analyze({ files: ['src/auth.ts'], task: 'fix session' });
30
+ *
31
+ * Uso manual:
32
+ * node autonomous-decision.cjs analyze src/auth.ts
33
+ * node autonomous-decision.cjs queue — ver cola diferida
34
+ * node autonomous-decision.cjs flush — mostrar cola y limpiarla
35
+ */
36
+
37
+ 'use strict';
38
+
39
+ const path = require('path');
40
+ const fs = require('fs');
41
+
42
+ // ─── CONSTANTES ───────────────────────────────────────────────────────────────
43
+
44
+ const DECISIONS = {
45
+ STOP: 'STOP',
46
+ WARN: 'WARN',
47
+ IMPLEMENT: 'IMPLEMENT',
48
+ IMPLEMENT_CAUTIOUS: 'IMPLEMENT_CAUTIOUS',
49
+ DEFER: 'DEFER',
50
+ };
51
+
52
+ const BLAST_LEVELS = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
53
+
54
+ const DEFERRED_QUEUE_PATH = '.agentic/deferred_queue.json';
55
+
56
+ // ─── FILE RISK TABLE ──────────────────────────────────────────────────────────
57
+
58
+ const DEFAULT_RISK_TABLE = {
59
+ CRITICAL: ['auth.ts', 'middleware/', '.env', 'prisma/schema', 'secrets'],
60
+ SENSITIVE: ['routes/auth', 'lib/prisma', 'collab-manager', 'harness'],
61
+ NORMAL: ['routes/', 'lib/', 'services/', 'controllers/'],
62
+ FREE: ['tests/', '__tests__/', 'utils/', 'validators', 'constants'],
63
+ };
64
+
65
+ function classifyFileRisk(filePath, projectRoot) {
66
+ let riskTable = DEFAULT_RISK_TABLE;
67
+ try {
68
+ const configPath = path.join(projectRoot || process.cwd(), '.agentic/config.md');
69
+ const config = fs.readFileSync(configPath, 'utf8');
70
+ const tableMatch = config.match(/\[file_risk_table\]([\s\S]*?)(?=\[|$)/);
71
+ if (tableMatch) {
72
+ const lines = tableMatch[1].trim().split(/\r?\n/).filter(Boolean);
73
+ const custom = { CRITICAL: [], SENSITIVE: [], NORMAL: [], FREE: [] };
74
+ lines.forEach(function(line) {
75
+ const colonIdx = line.indexOf(':');
76
+ if (colonIdx < 0) return;
77
+ const lvl = line.substring(0, colonIdx).trim().toUpperCase();
78
+ const vals = line.substring(colonIdx + 1).split(',').map(function(p) { return p.trim(); }).filter(Boolean);
79
+ if (custom[lvl]) custom[lvl].push.apply(custom[lvl], vals);
80
+ });
81
+ riskTable = {
82
+ CRITICAL: DEFAULT_RISK_TABLE.CRITICAL.concat(custom.CRITICAL),
83
+ SENSITIVE: DEFAULT_RISK_TABLE.SENSITIVE.concat(custom.SENSITIVE),
84
+ NORMAL: DEFAULT_RISK_TABLE.NORMAL.concat(custom.NORMAL),
85
+ FREE: DEFAULT_RISK_TABLE.FREE.concat(custom.FREE),
86
+ };
87
+ }
88
+ } catch(e) {}
89
+
90
+ const fp = (filePath || '').replace(/\\/g, '/').toLowerCase();
91
+ for (var i = 0; i < riskTable.CRITICAL.length; i++) if (fp.includes(riskTable.CRITICAL[i].toLowerCase())) return 'CRITICAL';
92
+ for (var i = 0; i < riskTable.SENSITIVE.length; i++) if (fp.includes(riskTable.SENSITIVE[i].toLowerCase())) return 'SENSITIVE';
93
+ for (var i = 0; i < riskTable.FREE.length; i++) if (fp.includes(riskTable.FREE[i].toLowerCase())) return 'FREE';
94
+ for (var i = 0; i < riskTable.NORMAL.length; i++) if (fp.includes(riskTable.NORMAL[i].toLowerCase())) return 'NORMAL';
95
+ return 'NORMAL';
96
+ }
97
+
98
+ // ─── PATTERN DETECTION ACROSS MODULES ────────────────────────────────────────
99
+
100
+ function detectPatternAcrossModules(db, errorSignatures, currentFiles, projectRoot) {
101
+ if (!db || !errorSignatures || errorSignatures.length === 0) return [];
102
+
103
+ const findings = [];
104
+ const currentAreas = (currentFiles || []).map(function(f) {
105
+ return path.basename(f, path.extname(f)).toLowerCase();
106
+ });
107
+
108
+ const sfn = function(fn) { try { return fn(); } catch(e) { return []; } };
109
+
110
+ errorSignatures.forEach(function(sig) {
111
+ if (!sig || sig.length < 3) return;
112
+
113
+ const likeQuery = '%' + sig + '%';
114
+ const placeholders = currentAreas.length > 0
115
+ ? 'AND area NOT IN (' + currentAreas.map(function() { return '?'; }).join(',') + ')'
116
+ : '';
117
+
118
+ let otherModules = [];
119
+ try {
120
+ otherModules = db.prepare(
121
+ 'SELECT DISTINCT area, id, titulo, confianza, aplicado FROM nodos ' +
122
+ 'WHERE tipo IN (\'error\', \'patron\') ' +
123
+ 'AND (titulo LIKE ? OR contenido LIKE ?) ' +
124
+ 'AND estado = \'ACTIVO\' ' +
125
+ placeholders + ' ORDER BY aplicado DESC LIMIT 8'
126
+ ).all.apply(null, [likeQuery, likeQuery].concat(currentAreas));
127
+ } catch(e) {}
128
+
129
+ otherModules.forEach(function(mod) {
130
+ const hasHistory = (mod.aplicado || 0) > 0;
131
+ const riskLevel = classifyFileRisk(mod.area, projectRoot);
132
+
133
+ let knownFix = null;
134
+ try {
135
+ const fixRow = db.prepare(
136
+ 'SELECT descripcion FROM relaciones_semanticas ' +
137
+ 'WHERE desde_entidad LIKE ? AND tipo = \'was_fixed_by\' ' +
138
+ 'AND (invalid_at IS NULL OR invalid_at = \'\') LIMIT 1'
139
+ ).get('%' + mod.area + '%');
140
+ if (fixRow) knownFix = fixRow.descripcion;
141
+ } catch(e) {}
142
+
143
+ let action;
144
+ if (riskLevel === 'CRITICAL') {
145
+ action = 'WARN';
146
+ } else if (riskLevel === 'SENSITIVE' && !knownFix) {
147
+ action = 'WARN';
148
+ } else if (hasHistory || knownFix) {
149
+ action = 'AUTO_FIX';
150
+ } else {
151
+ action = riskLevel === 'FREE' ? 'AUTO_FIX' : 'WARN';
152
+ }
153
+
154
+ findings.push({
155
+ module: mod.area,
156
+ signature: sig,
157
+ titulo: mod.titulo,
158
+ risk_level: riskLevel,
159
+ has_history: hasHistory,
160
+ known_fix: knownFix,
161
+ action: action,
162
+ reason: action === 'AUTO_FIX'
163
+ ? 'Pattern in ' + mod.area + ' (' + (mod.aplicado || 0) + 'x) — auto-fixing'
164
+ : riskLevel + ' file — requires confirmation before touching ' + mod.area,
165
+ });
166
+ });
167
+ });
168
+
169
+ const seen = {};
170
+ return findings.filter(function(f) {
171
+ const key = f.module + ':' + f.signature;
172
+ if (seen[key]) return false;
173
+ seen[key] = true;
174
+ return true;
175
+ });
176
+ }
177
+
178
+ // ─── PREREQUISITE CHECK FOR FINDINGS ─────────────────────────────────────────
179
+
180
+ function prerequisiteCheckFindings(db, findings, currentFiles) {
181
+ if (!db || !findings || !findings.length) {
182
+ return { prerequisites: [], non_blockers: findings || [], can_proceed: true, fix_order: [{ step: 'MAIN_TASK' }] };
183
+ }
184
+
185
+ const prerequisites = [];
186
+ const nonBlockers = [];
187
+
188
+ findings.forEach(function(finding) {
189
+ var isPrerequisite = false;
190
+
191
+ try {
192
+ (currentFiles || []).forEach(function(file) {
193
+ const bn = path.basename(file);
194
+ const deps = db.prepare(
195
+ 'SELECT hacia_entidad FROM relaciones_semanticas ' +
196
+ 'WHERE desde_entidad LIKE ? ' +
197
+ 'AND tipo IN (\'depende_de\', \'importa\', \'usa\', \'llama\') ' +
198
+ 'AND (invalid_at IS NULL OR invalid_at = \'\') LIMIT 10'
199
+ ).all('%' + bn + '%').map(function(r) { return r.hacia_entidad; });
200
+
201
+ if (deps.some(function(d) {
202
+ return d && d.toLowerCase().includes(finding.module.toLowerCase());
203
+ })) {
204
+ isPrerequisite = true;
205
+ }
206
+ });
207
+ } catch(e) {}
208
+
209
+ if (isPrerequisite) {
210
+ prerequisites.push(Object.assign({}, finding, {
211
+ prerequisite_reason: finding.module + ' is upstream of current task — fix first',
212
+ }));
213
+ } else {
214
+ nonBlockers.push(finding);
215
+ }
216
+ });
217
+
218
+ const autoFixable = nonBlockers.filter(function(f) { return f.action === 'AUTO_FIX'; });
219
+ const warnOnly = prerequisites.concat(nonBlockers.filter(function(f) { return f.action === 'WARN'; }));
220
+
221
+ const fixOrder = prerequisites.map(function(p) { return Object.assign({}, p, { when: 'BEFORE_MAIN_TASK' }); });
222
+ fixOrder.push({ step: 'MAIN_TASK' });
223
+ autoFixable.forEach(function(n) { fixOrder.push(Object.assign({}, n, { when: 'AFTER_MAIN_TASK' })); });
224
+
225
+ return {
226
+ prerequisites: prerequisites,
227
+ non_blockers: nonBlockers,
228
+ can_proceed: prerequisites.length === 0,
229
+ auto_fixable: autoFixable,
230
+ warn_only: warnOnly,
231
+ fix_order: fixOrder,
232
+ };
233
+ }
234
+
235
+
236
+ // ─── DB ───────────────────────────────────────────────────────────────────────
237
+
238
+ function openDB(projectRoot) {
239
+ const dbPath = path.join(projectRoot, '.agentic/memoria.db');
240
+ try { return new (require('better-sqlite3'))(dbPath); } catch {}
241
+ try { const { DatabaseSync } = require('node:sqlite'); return new DatabaseSync(dbPath); } catch {}
242
+ return null;
243
+ }
244
+
245
+ function safe(fn, fallback = null) {
246
+ try { return fn(); } catch { return fallback; }
247
+ }
248
+
249
+ // ─── PREREQUISITE CHAIN DETECTION ────────────────────────────────────────────
250
+ /**
251
+ * Dado un set de archivos a modificar, detecta si algún prerequisito
252
+ * en la cadena de dependencias tiene contratos rotos.
253
+ *
254
+ * Ejemplo:
255
+ * Quieres tocar dashboard.ts
256
+ * dashboard.ts importa authMiddleware.ts
257
+ * authMiddleware.ts tiene contrato INVALIDATED
258
+ * → authMiddleware.ts es prerequisito roto
259
+ */
260
+ function detectPrerequisiteChain(db, targetFiles, projectRoot) {
261
+ const broken = [];
262
+
263
+ if (!db) return broken;
264
+
265
+ targetFiles.forEach(file => {
266
+ const basename = path.basename(file);
267
+
268
+ // Obtener dependencias via AST edges
269
+ const deps = safe(() =>
270
+ db.prepare(`
271
+ SELECT DISTINCT hacia_entidad as dep
272
+ FROM relaciones_semanticas
273
+ WHERE (desde_entidad LIKE ? OR desde_entidad = ?)
274
+ AND tipo IN ('depende_de', 'importa', 'usa', 'llama')
275
+ AND (invalid_at IS NULL OR invalid_at = '')
276
+ LIMIT 20
277
+ `).all(`%${basename}%`, file)
278
+ ) || [];
279
+
280
+ deps.forEach(({ dep }) => {
281
+ if (!dep) return;
282
+
283
+ // Verificar si esa dependencia tiene contratos fallidos o invalidados
284
+ const brokenContracts = safe(() =>
285
+ db.prepare(`
286
+ SELECT id, name, status, module
287
+ FROM verified_contracts
288
+ WHERE (test_file LIKE ? OR name LIKE ? OR module LIKE ?)
289
+ AND status IN ('invalidated')
290
+ LIMIT 5
291
+ `).all(`%${dep}%`, `%${dep}%`, `%${path.basename(dep, path.extname(dep))}%`)
292
+ ) || [];
293
+
294
+ if (brokenContracts.length > 0) {
295
+ broken.push({
296
+ prerequisite: dep,
297
+ broken_contracts: brokenContracts,
298
+ affects: file,
299
+ reason: `${dep} has ${brokenContracts.length} broken contract(s) — must be fixed before ${basename}`,
300
+ });
301
+ }
302
+
303
+ // También verificar errores HIGH confidence no resueltos en la dependencia
304
+ const depArea = path.basename(dep, path.extname(dep)).toLowerCase();
305
+ const unresolvedErrors = safe(() =>
306
+ db.prepare(`
307
+ SELECT titulo, confianza FROM nodos
308
+ WHERE tipo = 'error'
309
+ AND confianza IN ('ALTA', 'MEDIA')
310
+ AND estado = 'ACTIVO'
311
+ AND area LIKE ?
312
+ AND (vigencia_tipo = 'VIGENTE' OR vigencia_tipo IS NULL)
313
+ LIMIT 3
314
+ `).all(`%${depArea}%`)
315
+ ) || [];
316
+
317
+ if (unresolvedErrors.length > 0 && brokenContracts.length === 0) {
318
+ broken.push({
319
+ prerequisite: dep,
320
+ broken_contracts: [],
321
+ unresolved_errors: unresolvedErrors,
322
+ affects: file,
323
+ reason: `${dep} has ${unresolvedErrors.length} HIGH/MEDIUM unresolved error(s)`,
324
+ severity: 'soft', // no es STOP, es WARN
325
+ });
326
+ }
327
+ });
328
+ });
329
+
330
+ return broken;
331
+ }
332
+
333
+ // ─── CROSS-MODULE ERROR CHECK ─────────────────────────────────────────────────
334
+ /**
335
+ * ¿Este error ya ocurrió en otro módulo?
336
+ * Si sí y fue resuelto → propone misma solución
337
+ * Si sí y no fue resuelto → es sistémico, escalar
338
+ */
339
+ function crossModuleCheck(db, errorSignatures, currentFiles) {
340
+ if (!db || !errorSignatures || errorSignatures.length === 0) return [];
341
+
342
+ const findings = [];
343
+ const currentAreas = currentFiles.map(f => path.basename(f, path.extname(f)).toLowerCase());
344
+
345
+ errorSignatures.forEach(sig => {
346
+ if (!sig) return;
347
+
348
+ // Buscar el mismo error en otras áreas
349
+ const otherInstances = safe(() =>
350
+ db.prepare(`
351
+ SELECT id, titulo, area, contenido, aplicado
352
+ FROM nodos
353
+ WHERE tipo = 'error'
354
+ AND (titulo LIKE ? OR contenido LIKE ?)
355
+ AND estado = 'ACTIVO'
356
+ AND area NOT IN (${currentAreas.map(() => '?').join(',')})
357
+ ORDER BY aplicado DESC
358
+ LIMIT 5
359
+ `).all(`%${sig}%`, `%${sig}%`, ...currentAreas)
360
+ ) || [];
361
+
362
+ otherInstances.forEach(inst => {
363
+ // ¿Fue resuelto allá?
364
+ const fixEdge = safe(() =>
365
+ db.prepare(`
366
+ SELECT descripcion, hacia_entidad
367
+ FROM relaciones_semanticas
368
+ WHERE desde_entidad LIKE ?
369
+ AND tipo = 'was_fixed_by'
370
+ AND (invalid_at IS NULL OR invalid_at = '')
371
+ LIMIT 1
372
+ `).get(`%${inst.area}%`)
373
+ );
374
+
375
+ findings.push({
376
+ signature: sig,
377
+ found_in: inst.area,
378
+ titulo: inst.titulo,
379
+ was_fixed: !!fixEdge,
380
+ fix_applied: fixEdge?.descripcion || null,
381
+ fix_target: fixEdge?.hacia_entidad || null,
382
+ is_systemic: !fixEdge,
383
+ recommendation: fixEdge
384
+ ? `Apply same fix as in ${inst.area}: ${fixEdge.descripcion?.substring(0, 80)}`
385
+ : `Systemic error — also present in ${inst.area} without resolution. Escalate priority.`,
386
+ });
387
+ });
388
+ });
389
+
390
+ return findings;
391
+ }
392
+
393
+ // ─── BLAST RADIUS CHECK ───────────────────────────────────────────────────────
394
+
395
+ function getBlastLevel(db, targetFiles) {
396
+ if (!db) return { level: 'LOW', level_int: 0, contracts_at_risk: 0, protected: 0 };
397
+
398
+ let contractsAtRisk = 0;
399
+ let protectedAtRisk = 0;
400
+
401
+ try {
402
+ const allContracts = db.prepare(
403
+ "SELECT * FROM verified_contracts WHERE status IN ('protected','verified')"
404
+ ).all();
405
+
406
+ targetFiles.forEach(file => {
407
+ const basename = path.basename(file);
408
+ allContracts.forEach(c => {
409
+ const testFile = c.test_file || '';
410
+ const isAtRisk = testFile.includes(basename) ||
411
+ (c.module && file.toLowerCase().includes(c.module.toLowerCase()));
412
+ if (isAtRisk) {
413
+ contractsAtRisk++;
414
+ if (c.status === 'protected') protectedAtRisk++;
415
+ }
416
+ });
417
+ });
418
+ } catch {}
419
+
420
+ const level = contractsAtRisk === 0 ? 'LOW'
421
+ : contractsAtRisk <= 3 ? 'LOW'
422
+ : contractsAtRisk <= 10 ? 'MEDIUM'
423
+ : contractsAtRisk <= 20 ? 'HIGH'
424
+ : 'CRITICAL';
425
+
426
+ return {
427
+ level,
428
+ level_int: BLAST_LEVELS[level],
429
+ contracts_at_risk: contractsAtRisk,
430
+ protected: protectedAtRisk,
431
+ };
432
+ }
433
+
434
+ // ─── DEFERRED QUEUE ───────────────────────────────────────────────────────────
435
+
436
+ function loadDeferredQueue(projectRoot) {
437
+ const qPath = path.join(projectRoot, DEFERRED_QUEUE_PATH);
438
+ if (!fs.existsSync(qPath)) return [];
439
+ try { return JSON.parse(fs.readFileSync(qPath, 'utf8')); } catch { return []; }
440
+ }
441
+
442
+ function saveDeferredQueue(projectRoot, queue) {
443
+ const qPath = path.join(projectRoot, DEFERRED_QUEUE_PATH);
444
+ try { fs.writeFileSync(qPath, JSON.stringify(queue, null, 2)); } catch {}
445
+ }
446
+
447
+ function addToDeferred(projectRoot, item) {
448
+ const queue = loadDeferredQueue(projectRoot);
449
+ queue.push({ ...item, deferred_at: new Date().toISOString() });
450
+ // Máx 50 items en cola
451
+ if (queue.length > 50) queue.splice(0, queue.length - 50);
452
+ saveDeferredQueue(projectRoot, queue);
453
+ }
454
+
455
+ function flushDeferredQueue(projectRoot) {
456
+ const queue = loadDeferredQueue(projectRoot);
457
+ if (queue.length === 0) return [];
458
+ saveDeferredQueue(projectRoot, []);
459
+ return queue;
460
+ }
461
+
462
+ // ─── MOTOR DE DECISIÓN PRINCIPAL ─────────────────────────────────────────────
463
+ /**
464
+ * Analiza un cambio propuesto y decide qué hacer.
465
+ *
466
+ * @param {Object} params
467
+ * @param {string[]} params.files Archivos a modificar
468
+ * @param {string} params.task Descripción de la tarea
469
+ * @param {string[]} params.errorSignatures Firmas de errores detectados (opcional)
470
+ * @param {string} params.projectRoot
471
+ */
472
+ function analyze(params = {}) {
473
+ const {
474
+ files = [],
475
+ task = '',
476
+ errorSignatures= [],
477
+ projectRoot = process.cwd(),
478
+ } = params;
479
+
480
+ const db = openDB(projectRoot);
481
+ const result = {
482
+ decision: DECISIONS.IMPLEMENT,
483
+ files,
484
+ task,
485
+ blast: null,
486
+ prerequisites: [],
487
+ cross_module: [],
488
+ reasons: [],
489
+ deferred: [],
490
+ action_plan: [],
491
+ summary: '',
492
+ };
493
+
494
+ // ── 1. BLAST RADIUS ────────────────────────────────────────────────────────
495
+ if (files.length > 0) {
496
+ result.blast = getBlastLevel(db, files);
497
+
498
+ if (result.blast.protected > 0) {
499
+ // Hay contratos PROTECTED en riesgo → STOP
500
+ result.decision = DECISIONS.STOP;
501
+ result.reasons.push(
502
+ `PROTECTED contracts at risk: ${result.blast.protected}. ` +
503
+ `These represent verified behavior that must not break.`
504
+ );
505
+ } else if (result.blast.level === 'CRITICAL') {
506
+ result.decision = DECISIONS.STOP;
507
+ result.reasons.push(
508
+ `Blast radius CRITICAL: ${result.blast.contracts_at_risk} contracts at risk. ` +
509
+ `Too many verified behaviors could break.`
510
+ );
511
+ } else if (result.blast.level === 'HIGH') {
512
+ result.decision = DECISIONS.WARN;
513
+ result.reasons.push(
514
+ `Blast radius HIGH: ${result.blast.contracts_at_risk} contracts at risk. ` +
515
+ `Proceed with caution. Run akdd contracts gate after changes.`
516
+ );
517
+ }
518
+ }
519
+
520
+ // ── 2. PREREQUISITE CHAIN ──────────────────────────────────────────────────
521
+ if (files.length > 0 && result.decision !== DECISIONS.STOP) {
522
+ const prereqs = detectPrerequisiteChain(db, files, projectRoot);
523
+ const hardPrereqs = prereqs.filter(p => !p.severity || p.severity === 'hard');
524
+ const softPrereqs = prereqs.filter(p => p.severity === 'soft');
525
+
526
+ if (hardPrereqs.length > 0) {
527
+ result.prerequisites = hardPrereqs;
528
+ result.decision = DECISIONS.IMPLEMENT_CAUTIOUS;
529
+ result.reasons.push(
530
+ `Prerequisite chain broken: ${hardPrereqs.length} dependency(ies) have broken contracts. ` +
531
+ `Must fix prerequisites first.`
532
+ );
533
+ result.action_plan = [
534
+ ...hardPrereqs.map(p => ({
535
+ step: 'FIX_PREREQUISITE',
536
+ target: p.prerequisite,
537
+ reason: p.reason,
538
+ before_main_task: true,
539
+ })),
540
+ { step: 'EXECUTE_ORIGINAL_TASK', target: files, task },
541
+ ];
542
+ } else if (softPrereqs.length > 0) {
543
+ result.prerequisites = softPrereqs;
544
+ if (result.decision === DECISIONS.IMPLEMENT) {
545
+ result.decision = DECISIONS.WARN;
546
+ }
547
+ result.reasons.push(
548
+ `Soft prerequisite warning: ${softPrereqs.length} dependency(ies) have unresolved HIGH errors.`
549
+ );
550
+ }
551
+ }
552
+
553
+ // ── 3. CROSS-MODULE CHECK ──────────────────────────────────────────────────
554
+ if (errorSignatures.length > 0) {
555
+ const crossModuleFindings = crossModuleCheck(db, errorSignatures, files);
556
+ result.cross_module = crossModuleFindings;
557
+
558
+ const systemicErrors = crossModuleFindings.filter(f => f.is_systemic);
559
+ const fixableErrors = crossModuleFindings.filter(f => f.was_fixed);
560
+
561
+ if (systemicErrors.length > 0 && result.decision === DECISIONS.IMPLEMENT) {
562
+ result.decision = DECISIONS.WARN;
563
+ result.reasons.push(
564
+ `Systemic error detected: ${systemicErrors.length} error(s) also present in other modules ` +
565
+ `without resolution. Consider addressing root cause.`
566
+ );
567
+ }
568
+
569
+ if (fixableErrors.length > 0) {
570
+ result.reasons.push(
571
+ `Cross-module pattern found: ${fixableErrors.length} error(s) were already ` +
572
+ `resolved in other modules. Applying same fix pattern.`
573
+ );
574
+ result.action_plan.push(
575
+ ...fixableErrors.map(f => ({
576
+ step: 'APPLY_KNOWN_FIX',
577
+ signature: f.signature,
578
+ from_module: f.found_in,
579
+ fix: f.fix_applied,
580
+ }))
581
+ );
582
+ }
583
+ }
584
+
585
+ // ── 4. LOW BLAST + SIN HISTORIAL → DEFER ──────────────────────────────────
586
+ if (result.decision === DECISIONS.IMPLEMENT &&
587
+ result.blast?.level === 'LOW' &&
588
+ result.cross_module.length === 0 &&
589
+ result.prerequisites.length === 0 &&
590
+ files.length > 0) {
591
+
592
+ // ¿Hay historial causal para estos archivos?
593
+ let hasHistory = false;
594
+ if (db) {
595
+ files.forEach(f => {
596
+ const basename = path.basename(f);
597
+ const history = safe(() =>
598
+ db.prepare(`
599
+ SELECT COUNT(*) as n FROM relaciones_semanticas
600
+ WHERE (desde_entidad LIKE ? OR hacia_entidad LIKE ?)
601
+ AND tipo IN ('caused_failure', 'was_fixed_by', 'regressed_by')
602
+ AND (invalid_at IS NULL OR invalid_at = '')
603
+ `).get(`%${basename}%`, `%${basename}%`)?.n
604
+ ) || 0;
605
+ if (history > 0) hasHistory = true;
606
+ });
607
+ }
608
+
609
+ if (!hasHistory && task && task.toLowerCase().includes('minor')) {
610
+ // Solo diferir si explícitamente se indica que es menor
611
+ result.decision = DECISIONS.DEFER;
612
+ result.deferred = files;
613
+ addToDeferred(projectRoot, { files, task, reason: 'Low blast, no history, minor scope' });
614
+ result.reasons.push('Low blast radius, no causal history, minor scope — deferred to end-of-cycle suggestions.');
615
+ }
616
+ }
617
+
618
+ // ── 5. RESUMEN EJECUTIVO ───────────────────────────────────────────────────
619
+ result.summary = buildSummary(result);
620
+
621
+ if (db) { try { db.close(); } catch {} }
622
+ return result;
623
+ }
624
+
625
+ // ─── RESUMEN EJECUTIVO ────────────────────────────────────────────────────────
626
+
627
+ function buildSummary(result) {
628
+ const icons = {
629
+ STOP: '🛑',
630
+ WARN: '⚠️',
631
+ IMPLEMENT: '✅',
632
+ IMPLEMENT_CAUTIOUS: '🔄',
633
+ DEFER: '📋',
634
+ };
635
+
636
+ const icon = icons[result.decision] || '?';
637
+ let summary = `${icon} Decision: ${result.decision}`;
638
+
639
+ if (result.blast) {
640
+ summary += ` | Blast: ${result.blast.level} (${result.blast.contracts_at_risk} contracts)`;
641
+ }
642
+
643
+ if (result.prerequisites.length > 0) {
644
+ summary += ` | Prerequisites: ${result.prerequisites.length} broken`;
645
+ }
646
+
647
+ if (result.cross_module.length > 0) {
648
+ const fixed = result.cross_module.filter(f => f.was_fixed).length;
649
+ const systemic = result.cross_module.filter(f => f.is_systemic).length;
650
+ if (fixed > 0) summary += ` | ${fixed} known fix(es) available`;
651
+ if (systemic > 0) summary += ` | ${systemic} systemic error(s)`;
652
+ }
653
+
654
+ return summary;
655
+ }
656
+
657
+ // ─── PRINT RESULT ─────────────────────────────────────────────────────────────
658
+
659
+ function printAnalysis(result) {
660
+ console.log('\n' + '═'.repeat(60));
661
+ console.log(' Autonomous Decision Engine');
662
+ console.log('═'.repeat(60));
663
+ console.log(`\n ${result.summary}\n`);
664
+
665
+ if (result.reasons.length > 0) {
666
+ console.log(' Reasons:');
667
+ result.reasons.forEach(r => console.log(` • ${r}`));
668
+ console.log('');
669
+ }
670
+
671
+ if (result.prerequisites.length > 0) {
672
+ console.log(' Prerequisite chain:');
673
+ result.prerequisites.forEach(p => {
674
+ console.log(` ⚡ ${p.prerequisite}`);
675
+ console.log(` ${p.reason}`);
676
+ });
677
+ console.log('');
678
+ }
679
+
680
+ if (result.cross_module.length > 0) {
681
+ console.log(' Cross-module findings:');
682
+ result.cross_module.forEach(f => {
683
+ const icon = f.was_fixed ? '✅' : '⚠️';
684
+ console.log(` ${icon} "${f.signature}" also in ${f.found_in}`);
685
+ console.log(` ${f.recommendation}`);
686
+ });
687
+ console.log('');
688
+ }
689
+
690
+ if (result.action_plan.length > 0) {
691
+ console.log(' Action plan:');
692
+ result.action_plan.forEach((step, i) => {
693
+ console.log(` ${i + 1}. [${step.step}] ${step.target || step.signature || ''}`);
694
+ if (step.reason) console.log(` Reason: ${step.reason}`);
695
+ if (step.fix) console.log(` Apply: ${step.fix}`);
696
+ });
697
+ console.log('');
698
+ }
699
+
700
+ console.log('═'.repeat(60) + '\n');
701
+ }
702
+
703
+ // ─── SPRINT PLANNER ───────────────────────────────────────────────────────────
704
+ /**
705
+ * Punto 3: Sprint planning con contexto explícito del dev.
706
+ *
707
+ * El dev pasa el contexto una vez. Agentic cruza contra datos técnicos.
708
+ * Produce un plan ejecutable por prioridad real (negocio + técnica).
709
+ *
710
+ * @param {Object} context
711
+ * @param {string} context.objective "Terminar el módulo de pagos antes del viernes"
712
+ * @param {string[]} context.constraints ["no tocar auth.ts esta semana"]
713
+ * @param {string[]} context.priorities ["pagos bloquea al cliente X"]
714
+ * @param {string} projectRoot
715
+ */
716
+ function planSprint(context = {}, projectRoot) {
717
+ projectRoot = projectRoot || process.cwd();
718
+ const db = openDB(projectRoot);
719
+ if (!db) return { error: 'DB no disponible' };
720
+
721
+ const { objective = '', constraints = [], priorities = [] } = context;
722
+
723
+ const plan = {
724
+ objective,
725
+ constraints,
726
+ business_priorities: priorities,
727
+ technical_findings: [],
728
+ combined_priority: [],
729
+ sprint_blocks: [],
730
+ };
731
+
732
+ // Detectar deuda técnica por blast radius
733
+ try {
734
+ const highRisk = db.prepare(`
735
+ SELECT DISTINCT module, COUNT(*) as contract_count
736
+ FROM verified_contracts
737
+ WHERE status IN ('invalidated', 'verified')
738
+ AND failure_count > 0
739
+ GROUP BY module
740
+ ORDER BY failure_count DESC, contract_count DESC
741
+ LIMIT 10
742
+ `).all();
743
+
744
+ plan.technical_findings = highRisk.map(r => ({
745
+ module: r.module,
746
+ contracts: r.contract_count,
747
+ priority: r.contract_count > 5 ? 'HIGH' : r.contract_count > 2 ? 'MEDIUM' : 'LOW',
748
+ }));
749
+ } catch {}
750
+
751
+ // Errores sin resolver de alta confianza
752
+ try {
753
+ const unresolvedErrors = db.prepare(`
754
+ SELECT area, COUNT(*) as n, MAX(confianza) as max_conf
755
+ FROM nodos
756
+ WHERE tipo = 'error'
757
+ AND estado = 'ACTIVO'
758
+ AND confianza IN ('ALTA', 'MEDIA')
759
+ GROUP BY area
760
+ ORDER BY n DESC
761
+ LIMIT 8
762
+ `).all();
763
+
764
+ unresolvedErrors.forEach(e => {
765
+ plan.technical_findings.push({
766
+ module: e.area,
767
+ errors: e.n,
768
+ priority: e.max_conf === 'ALTA' ? 'HIGH' : 'MEDIUM',
769
+ type: 'unresolved_errors',
770
+ });
771
+ });
772
+ } catch {}
773
+
774
+ // Combinar prioridad de negocio con prioridad técnica
775
+ const businessKeywords = [...priorities, objective].join(' ').toLowerCase();
776
+
777
+ plan.technical_findings.forEach(f => {
778
+ const moduleStr = (f.module || '').toLowerCase();
779
+ const businessMatch = businessKeywords.includes(moduleStr) ||
780
+ priorities.some(p => p.toLowerCase().includes(moduleStr));
781
+
782
+ const isConstrained = constraints.some(c =>
783
+ c.toLowerCase().includes(moduleStr)
784
+ );
785
+
786
+ plan.combined_priority.push({
787
+ module: f.module,
788
+ technical_priority: f.priority,
789
+ business_match: businessMatch,
790
+ constrained: isConstrained,
791
+ final_priority: isConstrained ? 'BLOCKED'
792
+ : businessMatch && f.priority === 'HIGH' ? 'P1_CRITICAL'
793
+ : businessMatch ? 'P2_HIGH'
794
+ : f.priority === 'HIGH' ? 'P3_MEDIUM'
795
+ : 'P4_LOW',
796
+ });
797
+ });
798
+
799
+ // Ordenar plan
800
+ const order = { P1_CRITICAL:0, P2_HIGH:1, P3_MEDIUM:2, P4_LOW:3, BLOCKED:4 };
801
+ plan.combined_priority.sort((a, b) =>
802
+ (order[a.final_priority] || 3) - (order[b.final_priority] || 3)
803
+ );
804
+
805
+ // Bloques del sprint
806
+ plan.sprint_blocks = [
807
+ {
808
+ block: 'Immediate — P1/P2',
809
+ items: plan.combined_priority.filter(p => ['P1_CRITICAL','P2_HIGH'].includes(p.final_priority)),
810
+ },
811
+ {
812
+ block: 'This cycle — P3',
813
+ items: plan.combined_priority.filter(p => p.final_priority === 'P3_MEDIUM'),
814
+ },
815
+ {
816
+ block: 'Next cycle — P4',
817
+ items: plan.combined_priority.filter(p => p.final_priority === 'P4_LOW'),
818
+ },
819
+ {
820
+ block: 'BLOCKED — do not touch',
821
+ items: plan.combined_priority.filter(p => p.final_priority === 'BLOCKED'),
822
+ },
823
+ ].filter(b => b.items.length > 0);
824
+
825
+ if (db) { try { db.close(); } catch {} }
826
+ return plan;
827
+ }
828
+
829
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
830
+
831
+ if (require.main === module) {
832
+ const [,, cmd, ...args] = process.argv;
833
+ const projectRoot = process.cwd();
834
+
835
+ switch (cmd) {
836
+ case 'analyze': {
837
+ const files = args.filter(a => !a.startsWith('--'));
838
+ const result = analyze({ files, task: args.join(' '), projectRoot });
839
+ printAnalysis(result);
840
+ process.exit(result.decision === DECISIONS.STOP ? 1 : 0);
841
+ }
842
+
843
+ case 'queue': {
844
+ const queue = loadDeferredQueue(projectRoot);
845
+ if (queue.length === 0) {
846
+ console.log('\n Cola diferida vacía.\n');
847
+ } else {
848
+ console.log(`\n Cola diferida (${queue.length} items):\n`);
849
+ queue.forEach((item, i) => {
850
+ console.log(` ${i+1}. [${item.deferred_at?.split('T')[0]}] ${item.task}`);
851
+ console.log(` Files: ${(item.files || []).join(', ')}`);
852
+ });
853
+ console.log('');
854
+ }
855
+ break;
856
+ }
857
+
858
+ case 'flush': {
859
+ const flushed = flushDeferredQueue(projectRoot);
860
+ if (flushed.length === 0) {
861
+ console.log('\n Sin items diferidos.\n');
862
+ } else {
863
+ console.log(`\n ${flushed.length} items de la cola para revisar:\n`);
864
+ flushed.forEach((item, i) => {
865
+ console.log(` ${i+1}. ${item.task}`);
866
+ if (item.reason) console.log(` ${item.reason}`);
867
+ });
868
+ console.log('');
869
+ }
870
+ break;
871
+ }
872
+
873
+ case 'sprint': {
874
+ // Uso: node autonomous-decision.cjs sprint --objective "..." --priority "..." --constraint "..."
875
+ const getArg = (flag) => {
876
+ const idx = process.argv.indexOf(flag);
877
+ return idx >= 0 ? process.argv[idx + 1] : null;
878
+ };
879
+ const objective = getArg('--objective') || 'Complete current sprint tasks';
880
+ const priorities = process.argv.filter((_, i) => process.argv[i-1] === '--priority');
881
+ const constraints = process.argv.filter((_, i) => process.argv[i-1] === '--constraint');
882
+
883
+ const plan = planSprint({ objective, priorities, constraints }, projectRoot);
884
+
885
+ if (plan.error) { console.log(`\n Error: ${plan.error}\n`); break; }
886
+
887
+ console.log('\n Sprint Plan\n ' + '─'.repeat(40));
888
+ console.log(` Objective: ${plan.objective}\n`);
889
+
890
+ plan.sprint_blocks.forEach(block => {
891
+ console.log(` ${block.block}:`);
892
+ block.items.forEach(item => {
893
+ const marker = item.business_match ? '★' : '○';
894
+ console.log(` ${marker} ${item.module} [${item.final_priority}]`);
895
+ });
896
+ console.log('');
897
+ });
898
+ break;
899
+ }
900
+
901
+ default:
902
+ console.log('Uso: node autonomous-decision.cjs [analyze <files> | queue | flush | sprint --objective "..."]');
903
+ }
904
+ }
905
+
906
+ module.exports = {
907
+ analyze,
908
+ planSprint,
909
+ detectPrerequisiteChain,
910
+ crossModuleCheck,
911
+ getBlastLevel,
912
+ loadDeferredQueue,
913
+ flushDeferredQueue,
914
+ DECISIONS,
915
+ };