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,408 @@
1
+ /**
2
+ * Agentic KDD — Knowledge Validator v1.0
3
+ * Brecha (d): Validación de conocimiento
4
+ *
5
+ * Problema: la memoria puede volverse obsoleta o corrompida.
6
+ * - Obsolescencia: un patrón correcto hace 6 meses ya no aplica
7
+ * - Memory poisoning: MINJA logra >95% de inyección vía recuperación
8
+ *
9
+ * Solución (patrón SSGM + OWASP):
10
+ * 1. Frontmatter YAML por entrada: fecha, última_validación, estado, hash_contexto
11
+ * 2. hash_contexto = hash de los archivos a los que aplica la entrada
12
+ * 3. Si esos archivos cambiaron desde última_validación → marcar SOSPECHOSO
13
+ * 4. Temporal decay: entradas > 60 días sin validación pierden peso
14
+ * 5. validate_knowledge() MCP tool que agentes llaman antes de aplicar un patrón
15
+ *
16
+ * Estados posibles:
17
+ * ACTIVO → válido, confiable
18
+ * SOSPECHOSO → archivos de referencia cambiaron — revisar antes de aplicar
19
+ * OBSOLETO → no validado en > 90 días Y decay < 10%
20
+ * HISTORICO → invalidado explícitamente, preservado para auditoría
21
+ *
22
+ * Uso:
23
+ * node knowledge-validator.cjs scan — escanear toda la memoria
24
+ * node knowledge-validator.cjs validate <id> — validar una entrada
25
+ * node knowledge-validator.cjs report — reporte de estado
26
+ * node knowledge-validator.cjs revalidate <id> — marcar como revalidado
27
+ */
28
+
29
+ 'use strict';
30
+
31
+ const path = require('path');
32
+ const fs = require('fs');
33
+ const crypto = require('crypto');
34
+
35
+ const DECAY_LAMBDA = 0.05; // mismo que kdd-memory
36
+ const SUSPECT_DAYS = 30; // días sin validar → SOSPECHOSO
37
+ const OBSOLETE_DAYS = 90; // días sin validar → OBSOLETO
38
+ const OBSOLETE_DECAY = 0.10; // si decay < 10% → candidato a OBSOLETO
39
+ const POISON_SIMILARITY = 0.95; // Jaccard para detectar entradas inyectadas
40
+
41
+ // ─── DB ───────────────────────────────────────────────────────────────────────
42
+
43
+ function openDB(projectRoot) {
44
+ const dbPath = path.join(projectRoot, '.agentic/memoria.db');
45
+ let db;
46
+ try { db = new (require('better-sqlite3'))(dbPath); }
47
+ catch { try { const { DatabaseSync } = require('node:sqlite'); db = new DatabaseSync(dbPath); } catch { return null; } }
48
+
49
+ // Migrar schema para campos de validación
50
+ try {
51
+ db.exec(`ALTER TABLE nodos ADD COLUMN hash_contexto TEXT`);
52
+ } catch {}
53
+ try {
54
+ db.exec(`ALTER TABLE nodos ADD COLUMN ultima_validacion TEXT`);
55
+ } catch {}
56
+ try {
57
+ db.exec(`ALTER TABLE nodos ADD COLUMN archivos_aplica TEXT DEFAULT '[]'`);
58
+ } catch {}
59
+ try {
60
+ db.exec(`ALTER TABLE nodos ADD COLUMN validation_score REAL DEFAULT 1.0`);
61
+ } catch {}
62
+ try {
63
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_nodos_vigencia ON nodos(vigencia_tipo)`);
64
+ } catch {}
65
+
66
+ return db;
67
+ }
68
+
69
+ const safe = (fn, fallback = null) => { try { return fn(); } catch { return fallback; } };
70
+
71
+ // ─── HASH DE CONTEXTO ─────────────────────────────────────────────────────────
72
+ /**
73
+ * Genera hash de los archivos referenciados por una entrada.
74
+ * Si los archivos cambian, el hash cambia → SOSPECHOSO.
75
+ */
76
+ function computeContextHash(archivosAplica, projectRoot) {
77
+ if (!archivosAplica || archivosAplica.length === 0) return null;
78
+
79
+ const hasher = crypto.createHash('sha256');
80
+ let anyFound = false;
81
+
82
+ archivosAplica.forEach(filePath => {
83
+ const fullPath = path.isAbsolute(filePath)
84
+ ? filePath
85
+ : path.join(projectRoot, filePath);
86
+
87
+ if (fs.existsSync(fullPath)) {
88
+ try {
89
+ const stat = fs.statSync(fullPath);
90
+ hasher.update(`${filePath}:${stat.size}:${stat.mtimeMs}`);
91
+ anyFound = true;
92
+ } catch {}
93
+ }
94
+ });
95
+
96
+ return anyFound ? hasher.digest('hex').substring(0, 16) : null;
97
+ }
98
+
99
+ // ─── TEMPORAL DECAY ──────────────────────────────────────────────────────────
100
+
101
+ function computeDecay(ultimaValidacion, fechaCreacion) {
102
+ const dateStr = ultimaValidacion || fechaCreacion;
103
+ if (!dateStr) return 0.5;
104
+ const deltaDays = (Date.now() - new Date(dateStr).getTime()) / (1000 * 60 * 60 * 24);
105
+ return Math.exp(-DECAY_LAMBDA * deltaDays);
106
+ }
107
+
108
+ // ─── DETECTAR POSIBLE MEMORY POISONING ───────────────────────────────────────
109
+ /**
110
+ * Detecta entradas sospechosas por similitud extrema con otras entradas.
111
+ * Similitud > 95% Jaccard entre entradas de la misma área = posible inyección.
112
+ * Patrón MINJA/MemoryGraft: inyección de variantes de entradas existentes.
113
+ */
114
+ function detectPoisoning(db) {
115
+ const suspicious = [];
116
+
117
+ const nodes = safe(() =>
118
+ db.prepare(`
119
+ SELECT id, titulo, contenido, area, tipo, fecha_creacion
120
+ FROM nodos
121
+ WHERE estado = 'ACTIVO' AND tipo IN ('patron','error','decision')
122
+ ORDER BY area, tipo
123
+ `).all()
124
+ ) || [];
125
+
126
+ const jaccardSim = (a, b) => {
127
+ const setA = new Set((a || '').toLowerCase().split(/\W+/).filter(Boolean));
128
+ const setB = new Set((b || '').toLowerCase().split(/\W+/).filter(Boolean));
129
+ if (setA.size === 0 || setB.size === 0) return 0;
130
+ const inter = new Set([...setA].filter(x => setB.has(x)));
131
+ const union = new Set([...setA, ...setB]);
132
+ return inter.size / union.size;
133
+ };
134
+
135
+ // Comparar por área y tipo para reducir complejidad
136
+ const byAreaType = {};
137
+ nodes.forEach(n => {
138
+ const key = `${n.area}:${n.tipo}`;
139
+ if (!byAreaType[key]) byAreaType[key] = [];
140
+ byAreaType[key].push(n);
141
+ });
142
+
143
+ Object.values(byAreaType).forEach(group => {
144
+ for (let i = 0; i < group.length; i++) {
145
+ for (let j = i + 1; j < group.length; j++) {
146
+ const textA = `${group[i].titulo} ${group[i].contenido}`;
147
+ const textB = `${group[j].titulo} ${group[j].contenido}`;
148
+ const sim = jaccardSim(textA, textB);
149
+
150
+ if (sim >= POISON_SIMILARITY) {
151
+ // La más reciente es la sospechosa
152
+ const newer = new Date(group[i].fecha_creacion) > new Date(group[j].fecha_creacion)
153
+ ? group[i] : group[j];
154
+ suspicious.push({
155
+ id: newer.id,
156
+ similarity: Math.round(sim * 100),
157
+ reason: `Near-duplicate of existing entry (${Math.round(sim*100)}% similarity) — possible memory injection`,
158
+ compared_to: newer === group[i] ? group[j].id : group[i].id,
159
+ });
160
+ }
161
+ }
162
+ }
163
+ });
164
+
165
+ return suspicious;
166
+ }
167
+
168
+ // ─── VALIDAR UNA ENTRADA ─────────────────────────────────────────────────────
169
+
170
+ function validateEntry(db, nodeId, projectRoot) {
171
+ const node = safe(() =>
172
+ db.prepare(`
173
+ SELECT id, titulo, contenido, area, tipo, confianza, vigencia_tipo,
174
+ hash_contexto, ultima_validacion, archivos_aplica, fecha_creacion, fecha_update
175
+ FROM nodos WHERE id = ?
176
+ `).get(nodeId)
177
+ );
178
+
179
+ if (!node) return { ok: false, reason: 'not_found' };
180
+
181
+ const result = {
182
+ id: node.id,
183
+ titulo: node.titulo,
184
+ area: node.area,
185
+ tipo: node.tipo,
186
+ status: 'ACTIVO',
187
+ decay: 0,
188
+ issues: [],
189
+ recommendation:'',
190
+ };
191
+
192
+ // 1. Temporal decay
193
+ result.decay = computeDecay(node.ultima_validacion, node.fecha_creacion);
194
+
195
+ const daysSinceValidation = node.ultima_validacion
196
+ ? (Date.now() - new Date(node.ultima_validacion).getTime()) / (1000 * 60 * 60 * 24)
197
+ : (Date.now() - new Date(node.fecha_creacion).getTime()) / (1000 * 60 * 60 * 24);
198
+
199
+ // 2. Verificar hash de archivos referenciados
200
+ let archivos = [];
201
+ try { archivos = JSON.parse(node.archivos_aplica || '[]'); } catch {}
202
+
203
+ if (archivos.length > 0) {
204
+ const currentHash = computeContextHash(archivos, projectRoot);
205
+ if (currentHash && node.hash_contexto && currentHash !== node.hash_contexto) {
206
+ result.issues.push({
207
+ type: 'stale_context',
208
+ message: `Referenced files changed since last validation. Hash: ${node.hash_contexto} → ${currentHash}`,
209
+ files: archivos,
210
+ });
211
+ }
212
+ }
213
+
214
+ // 3. Determinar estado
215
+ if (daysSinceValidation > OBSOLETE_DAYS && result.decay < OBSOLETE_DECAY) {
216
+ result.status = 'OBSOLETO';
217
+ result.issues.push({ type: 'obsolete', message: `Not validated in ${Math.round(daysSinceValidation)} days and decay < 10%` });
218
+ } else if (daysSinceValidation > SUSPECT_DAYS || result.issues.some(i => i.type === 'stale_context')) {
219
+ result.status = 'SOSPECHOSO';
220
+ if (daysSinceValidation > SUSPECT_DAYS) {
221
+ result.issues.push({ type: 'stale', message: `Not validated in ${Math.round(daysSinceValidation)} days` });
222
+ }
223
+ }
224
+
225
+ // 4. Score de validación (0-1)
226
+ const issuesPenalty = result.issues.reduce((p, issue) => {
227
+ return p * (issue.type === 'stale_context' ? 0.6 : issue.type === 'obsolete' ? 0.2 : 0.8);
228
+ }, 1.0);
229
+
230
+ result.validation_score = Math.max(0.05, result.decay * issuesPenalty);
231
+
232
+ // 5. Recomendación
233
+ result.recommendation = result.status === 'ACTIVO' ? 'Apply normally'
234
+ : result.status === 'SOSPECHOSO' ? 'Verify before applying — context may have changed'
235
+ : 'Do not apply — revalidate manually or mark as HISTORICO';
236
+
237
+ // 6. Actualizar estado en DB si cambió
238
+ if (result.status !== 'ACTIVO' && node.vigencia_tipo !== result.status) {
239
+ safe(() => db.prepare(`
240
+ UPDATE nodos SET
241
+ vigencia_tipo = ?,
242
+ validation_score = ?,
243
+ fecha_update = datetime('now')
244
+ WHERE id = ?
245
+ `).run(result.status, result.validation_score, nodeId));
246
+ } else {
247
+ safe(() => db.prepare(`UPDATE nodos SET validation_score = ? WHERE id = ?`)
248
+ .run(result.validation_score, nodeId));
249
+ }
250
+
251
+ return result;
252
+ }
253
+
254
+ // ─── SCAN COMPLETO ────────────────────────────────────────────────────────────
255
+
256
+ function scanAll(projectRoot) {
257
+ projectRoot = projectRoot || process.cwd();
258
+ const db = openDB(projectRoot);
259
+ if (!db) return { error: 'DB unavailable' };
260
+
261
+ const nodes = safe(() =>
262
+ db.prepare(`
263
+ SELECT id FROM nodos
264
+ WHERE estado = 'ACTIVO'
265
+ AND tipo IN ('patron','error','decision','regla')
266
+ ORDER BY fecha_update ASC
267
+ LIMIT 500
268
+ `).all()
269
+ ) || [];
270
+
271
+ const results = { total: nodes.length, activo: 0, sospechoso: 0, obsoleto: 0, poison_candidates: 0 };
272
+
273
+ nodes.forEach(n => {
274
+ const r = validateEntry(db, n.id, projectRoot);
275
+ if (r.status === 'ACTIVO') results.activo++;
276
+ else if (r.status === 'SOSPECHOSO') results.sospechoso++;
277
+ else if (r.status === 'OBSOLETO') results.obsoleto++;
278
+ });
279
+
280
+ // Detectar posible memory poisoning
281
+ const poisonCandidates = detectPoisoning(db);
282
+ results.poison_candidates = poisonCandidates.length;
283
+
284
+ // Marcar candidatos sospechosos de poisoning
285
+ poisonCandidates.forEach(p => {
286
+ safe(() => db.prepare(`
287
+ UPDATE nodos SET vigencia_tipo = 'SOSPECHOSO', fecha_update = datetime('now')
288
+ WHERE id = ? AND vigencia_tipo = 'VIGENTE'
289
+ `).run(p.id));
290
+ });
291
+
292
+ results.poison_suspects = poisonCandidates.slice(0, 5);
293
+ db.close();
294
+ return results;
295
+ }
296
+
297
+ // ─── VALIDATE_KNOWLEDGE — MCP TOOL ───────────────────────────────────────────
298
+ /**
299
+ * El agente Analista llama esto antes de aplicar un patrón de memoria.
300
+ * Retorna si el patrón es confiable o debe ser revisado.
301
+ */
302
+ function validateKnowledge(nodeId, projectRoot) {
303
+ projectRoot = projectRoot || process.cwd();
304
+ const db = openDB(projectRoot);
305
+ if (!db) return { trusted: true, reason: 'DB unavailable — proceeding' };
306
+
307
+ const result = validateEntry(db, nodeId, projectRoot);
308
+ db.close();
309
+
310
+ return {
311
+ trusted: result.status === 'ACTIVO',
312
+ status: result.status,
313
+ validation_score: result.validation_score,
314
+ decay: Math.round(result.decay * 100) / 100,
315
+ issues: result.issues,
316
+ recommendation: result.recommendation,
317
+ apply: result.status !== 'OBSOLETO',
318
+ warn: result.status === 'SOSPECHOSO',
319
+ };
320
+ }
321
+
322
+ // ─── REVALIDAR ────────────────────────────────────────────────────────────────
323
+
324
+ function revalidate(nodeId, projectRoot) {
325
+ projectRoot = projectRoot || process.cwd();
326
+ const db = openDB(projectRoot);
327
+ if (!db) return { ok: false };
328
+
329
+ const archivos = safe(() => {
330
+ const n = db.prepare("SELECT archivos_aplica FROM nodos WHERE id = ?").get(nodeId);
331
+ return JSON.parse(n?.archivos_aplica || '[]');
332
+ }) || [];
333
+
334
+ const newHash = computeContextHash(archivos, projectRoot);
335
+
336
+ safe(() => db.prepare(`
337
+ UPDATE nodos SET
338
+ vigencia_tipo = 'VIGENTE',
339
+ ultima_validacion = datetime('now'),
340
+ hash_contexto = ?,
341
+ validation_score = 1.0,
342
+ fecha_update = datetime('now')
343
+ WHERE id = ?
344
+ `).run(newHash, nodeId));
345
+
346
+ db.close();
347
+ return { ok: true, id: nodeId, new_hash: newHash, revalidated_at: new Date().toISOString() };
348
+ }
349
+
350
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
351
+
352
+ if (require.main === module) {
353
+ const [,, cmd, arg] = process.argv;
354
+ const projectRoot = process.cwd();
355
+
356
+ switch (cmd) {
357
+ case 'scan': {
358
+ console.log('\n[VALIDATOR] Scanning memory...');
359
+ const r = scanAll(projectRoot);
360
+ if (r.error) { console.log(`❌ ${r.error}`); break; }
361
+ console.log(`\n Knowledge Validation Report`);
362
+ console.log(` Total scanned: ${r.total}`);
363
+ console.log(` ✅ ACTIVO: ${r.activo}`);
364
+ console.log(` ⚠️ SOSPECHOSO: ${r.sospechoso}`);
365
+ console.log(` ❌ OBSOLETO: ${r.obsoleto}`);
366
+ console.log(` 🔍 Poison suspects: ${r.poison_candidates}`);
367
+ if (r.poison_suspects?.length > 0) {
368
+ console.log('\n Possible injections:');
369
+ r.poison_suspects.forEach(p => console.log(` - ${p.id}: ${p.reason}`));
370
+ }
371
+ console.log('');
372
+ break;
373
+ }
374
+
375
+ case 'validate': {
376
+ if (!arg) { console.log('Uso: knowledge-validator.cjs validate <node_id>'); break; }
377
+ const r = validateKnowledge(arg, projectRoot);
378
+ console.log(`\n ID: ${arg}`);
379
+ console.log(` Status: ${r.status} | Score: ${r.validation_score}`);
380
+ console.log(` Trusted: ${r.trusted} | Apply: ${r.apply}`);
381
+ console.log(` → ${r.recommendation}\n`);
382
+ break;
383
+ }
384
+
385
+ case 'revalidate': {
386
+ if (!arg) { console.log('Uso: knowledge-validator.cjs revalidate <node_id>'); break; }
387
+ const r = revalidate(arg, projectRoot);
388
+ console.log(r.ok ? `✅ Revalidated: ${arg}` : `❌ Failed`);
389
+ break;
390
+ }
391
+
392
+ case 'report': {
393
+ const r = scanAll(projectRoot);
394
+ const health = r.total > 0 ? Math.round((r.activo / r.total) * 100) : 0;
395
+ console.log(`\n Memory Health: ${health}% (${r.activo}/${r.total} entries valid)`);
396
+ if (r.sospechoso > 0) console.log(` ⚠️ ${r.sospechoso} entries need review`);
397
+ if (r.obsoleto > 0) console.log(` ❌ ${r.obsoleto} entries are obsolete`);
398
+ if (r.poison_candidates > 0) console.log(` 🔍 ${r.poison_candidates} possible injections detected`);
399
+ console.log('');
400
+ break;
401
+ }
402
+
403
+ default:
404
+ console.log('Uso: node knowledge-validator.cjs [scan | validate <id> | revalidate <id> | report]');
405
+ }
406
+ }
407
+
408
+ module.exports = { validateKnowledge, validateEntry, revalidate, scanAll, detectPoisoning, computeContextHash };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-kdd",
3
- "version": "3.2.3",
3
+ "version": "3.5.1",
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": {