} chemin du fichier écrit
+ */
+export async function writeMapFile(data, outPath) {
+ const html = renderMapHtml(data);
+ await fs.mkdir(path.dirname(path.resolve(outPath)), { recursive: true });
+ await fs.writeFile(outPath, html, 'utf8');
+ return path.resolve(outPath);
+}
+
+export function renderMapHtml(data) {
+ return `
+
+
+
+Metatron — Carte des erreurs
+
+
+
+
+
+
+
+
+
+
+`;
+}
+
+export { SEV_COLORS };
diff --git a/learning/memory.js b/learning/memory.js
new file mode 100644
index 0000000..23be93e
--- /dev/null
+++ b/learning/memory.js
@@ -0,0 +1,151 @@
+import fs from 'node:fs/promises';
+import path from 'node:path';
+
+const MEMORY_DIR = '.metatron';
+const MEMORY_FILE = 'memory.json';
+
+/**
+ * Charge la mémoire projet (crée une structure vide si absente).
+ * @param {string} projectRoot
+ */
+export async function loadMemory(projectRoot = process.cwd()) {
+ const file = path.join(projectRoot, MEMORY_DIR, MEMORY_FILE);
+ try {
+ return JSON.parse(await fs.readFile(file, 'utf8'));
+ } catch {
+ return { version: 1, entries: {}, scans: [] };
+ }
+}
+
+/**
+ * Sauvegarde la mémoire projet.
+ */
+export async function saveMemory(memory, projectRoot = process.cwd()) {
+ const dir = path.join(projectRoot, MEMORY_DIR);
+ await fs.mkdir(dir, { recursive: true });
+ await fs.writeFile(
+ path.join(dir, MEMORY_FILE),
+ JSON.stringify(memory, null, 2),
+ 'utf8'
+ );
+}
+
+function entryKey(ruleId, file) {
+ return `${ruleId}|${file.replace(/\\/g, '/')}`;
+}
+
+/**
+ * Réconcilie les findings actuels avec la mémoire et met à jour celle-ci.
+ * Classification :
+ * - new : première fois qu'on voit cette erreur (règle + fichier)
+ * - known : déjà vue, toujours présente
+ * - recurring : vue >= 3 fois
+ * - regressed : était corrigée, elle est REVENUE (le pire)
+ * - fixed : présente avant, disparue maintenant
+ *
+ * Un fichier n'est marqué "fixed" que s'il faisait partie du périmètre
+ * scanné (sinon son absence signifie juste "pas analysé cette fois").
+ * @param {Array<{ruleId:string,line:number}>} findings
+ * @param {Object} memory - objet mémoire MUTÉ en place
+ * @param {{scannedFiles?:string[]}} [options] - fichiers effectivement scannés
+ * @returns {{new:Array,known:Array,recurring:Array,regressed:Array,fixed:Array}}
+ */
+export function reconcile(findings, memory, { scannedFiles } = {}) {
+ const now = new Date().toISOString();
+ const result = { new: [], known: [], recurring: [], regressed: [], fixed: [] };
+
+ const seenKeys = new Set();
+
+ for (const f of findings) {
+ const key = entryKey(f.ruleId, f.file ?? f.filePath ?? '');
+ seenKeys.add(key);
+
+ let entry = memory.entries[key];
+ if (!entry) {
+ entry = memory.entries[key] = {
+ ruleId: f.ruleId,
+ file: (f.file ?? f.filePath ?? '').replace(/\\/g, '/'),
+ firstSeen: now,
+ lastSeen: now,
+ occurrences: 1,
+ lines: [f.line],
+ status: 'open'
+ };
+ result.new.push({ ...f, entry });
+ } else {
+ entry.lastSeen = now;
+ entry.occurrences++;
+ entry.lines = [...new Set([...entry.lines, f.line])].slice(-10);
+ if (entry.status === 'fixed') {
+ entry.status = 'open';
+ entry.regressionCount = (entry.regressionCount || 0) + 1;
+ result.regressed.push({ ...f, entry });
+ } else {
+ entry.status = 'open';
+ if (entry.occurrences >= 3) {
+ result.recurring.push({ ...f, entry });
+ } else {
+ result.known.push({ ...f, entry });
+ }
+ }
+ }
+ }
+
+ const scannedSet = scannedFiles
+ ? new Set(scannedFiles.map(f => f.replace(/\\/g, '/')))
+ : null;
+
+ for (const [key, entry] of Object.entries(memory.entries)) {
+ if (entry.status === 'open' && !seenKeys.has(key)) {
+ if (scannedSet && !scannedSet.has(entry.file)) continue;
+ entry.status = 'fixed';
+ entry.fixedAt = now;
+ result.fixed.push(entry);
+ }
+ }
+
+ memory.scans.push({
+ date: now,
+ total: findings.length,
+ bySeverity: countBy(findings, 'severity')
+ });
+ memory.scans = memory.scans.slice(-100);
+
+ return result;
+}
+
+/**
+ * Statistiques d'apprentissage à partir de la mémoire.
+ */
+export function getStats(memory) {
+ const entries = Object.values(memory.entries);
+ const open = entries.filter(e => e.status === 'open');
+ const fixed = entries.filter(e => e.status === 'fixed');
+
+ const topRecurring = open
+ .slice()
+ .sort((a, b) => b.occurrences - a.occurrences || b.regressionCount - a.regressionCount)
+ .slice(0, 10);
+
+ const byRule = {};
+ for (const e of entries) byRule[e.ruleId] = (byRule[e.ruleId] || 0) + 1;
+
+ return {
+ totalDistinct: entries.length,
+ openCount: open.length,
+ fixedCount: fixed.length,
+ regressionTotal: entries.reduce((s, e) => s + (e.regressionCount || 0), 0),
+ topRecurring,
+ byRule,
+ scans: memory.scans.length
+ };
+}
+
+function countBy(arr, field) {
+ const out = {};
+ for (const item of arr) {
+ const k = item[field];
+ out[k] = (out[k] || 0) + 1;
+ }
+ return out;
+}
diff --git a/learning/tutor.js b/learning/tutor.js
new file mode 100644
index 0000000..b5f7cf4
--- /dev/null
+++ b/learning/tutor.js
@@ -0,0 +1,152 @@
+import { ask, closeInterface } from '../cli.js';
+import { getLesson } from '../analyzer/lessons.js';
+import { callAI } from '../ai.js';
+
+const TUTOR_SYSTEM = `Tu es un tuteur de code bienveillant et rigoureux, en français.
+Contexte : l'utilisateur code avec l'aide d'IA et veut APPRENDRE de ses erreurs.
+Tu reçois ses fichiers analysés, les erreurs détectées et leur historique.
+
+Règles :
+- Réponds en français, de façon concise et concrète.
+- Explique le POURQUOI avant le comment : la mécanique du problème d'abord.
+- Illustre avec un mini exemple avant/après quand c'est utile.
+- Si la question porte sur autre chose que les erreurs listées (architecture, design, nommage...), réponds quand même : tu es le mentor du projet entier.
+- Termine parfois par une question courte qui fait progresser (méthode socratique), sans être lourd.`;
+
+/**
+ * Session interactive post-analyse : navigation dans les erreurs,
+ * leçons détaillées et questions libres au tuteur LLM.
+ * @param {{files:Array<{name:string,code:string}>, classified:Object, stats:Object, config:Object|null}} ctx
+ */
+export async function startTutorSession({ files, classified, stats, config }) {
+ const all = [
+ ...classified.regressed.map(f => ({ ...f, status: 'REGRESSION' })),
+ ...classified.new.map(f => ({ ...f, status: 'NOUVEAU' })),
+ ...classified.known.map(f => ({ ...f, status: 'DÉJÀ VU' })),
+ ...classified.recurring.map(f => ({ ...f, status: `RÉCURRENT ×${f.entry.occurrences}` }))
+ ];
+
+ console.log(`
+╔══════════════════════════════════════════════╗
+║ METATRON TUTOR — apprendre de tes erreurs ║
+╚══════════════════════════════════════════════╝
+
+Commandes :
+ Voir la leçon détaillée de l'erreur N
+ liste Re-lister les erreurs
+ stats Progrès et erreurs récurrentes
+ Poser une question au tuteur (code, archi, tout)
+ quitter Sortir
+${config ? '' : '\n⚠️ Pas de clé API détectée : mode lecture seule (pas de questions libres).\n'}
+`);
+
+ while (true) {
+ const raw = await ask('tutor>');
+ if (raw === null) break;
+ const input = raw.trim();
+ if (!input) continue;
+
+ if (/^(quitter|q|quit|exit)$/i.test(input)) break;
+
+ if (/^(liste|l|list)$/i.test(input)) {
+ printFindingList(all);
+ continue;
+ }
+
+ if (/^stats$/i.test(input)) {
+ printStats(stats);
+ continue;
+ }
+
+ const num = parseInt(input, 10);
+ if (!isNaN(num) && num >= 1 && num <= all.length) {
+ printLesson(all[num - 1]);
+ continue;
+ }
+
+ if (!config) {
+ console.log('⚠️ Mode lecture seule : configure GROK_API_KEY / GROQ_API_KEY / CLAUDE_API_KEY ou OLLAMA_MODEL pour poser des questions.\n');
+ continue;
+ }
+
+ await askTutor(input, { files, all, config });
+ }
+
+ closeInterface();
+}
+
+function printFindingList(all) {
+ if (all.length === 0) {
+ console.log('✅ Aucune erreur détectée. Pose tes questions librement !\n');
+ return;
+ }
+ console.log('');
+ all.forEach((f, i) => {
+ console.log(` ${String(i + 1).padStart(2)}. [${f.status}] ${f.title} — ${f.file}:${f.line}`);
+ });
+ console.log('');
+}
+
+function printLesson(f) {
+ const lesson = getLesson(f.ruleId);
+ console.log(`
+┌─ LEÇON — ${lesson.category} ─────────────────────────────
+│ ${f.title}
+│ 📍 ${f.file}:${f.line} · statut: ${f.status} · vu ${f.entry?.occurrences ?? 1}×
+│
+│ QUOI : ${f.excerpt}
+│
+│ POURQUOI C'EST UN PROBLÈME :
+│ ${lesson.explanation}
+${lesson.why ? `│\n│ EN PRATIQUE :\n│ ${lesson.why}` : ''}
+${lesson.badExample ? `\n│ ❌ MAUVAIS :\n${indent(lesson.badExample)}\n│\n│ ✅ MIEUX :\n${indent(lesson.goodExample)}` : ''}
+${lesson.reference ? `\n│ 📚 ${lesson.reference}` : ''}
+└──────────────────────────────────────────────
+`);
+}
+
+function indent(code) {
+ return code.split('\n').map(l => `│ ${l}`).join('\n');
+}
+
+function printStats(stats) {
+ console.log(`
+📈 PROGRÈS
+ Erreurs distinctes rencontrées : ${stats.totalDistinct}
+ Encore ouvertes : ${stats.openCount}
+ Corrigées : ${stats.fixedCount} 🎉
+ Régressions totales : ${stats.regressionTotal}
+
+ Top récidives :`);
+ for (const e of stats.topRecurring.slice(0, 5)) {
+ console.log(` • ${e.ruleId} — ${e.file} (${e.occurrences}×)`);
+ }
+ console.log('');
+}
+
+async function askTutor(question, { files, all, config }) {
+ const codeContext = files.map(f =>
+ `--- ${f.name} ---\n${f.code.length > 6000 ? f.code.slice(0, 6000) + '\n... (tronqué)' : f.code}`
+ ).join('\n\n');
+
+ const findingsSummary = all.map(f =>
+ `- [${f.status}] (${f.severity}) ${f.title} — ${f.file}:${f.line}`
+ ).join('\n') || '- aucune';
+
+ const prompt = `Fichiers analysés :
+${codeContext}
+
+Erreurs détectées (avec historique) :
+${findingsSummary}
+
+Question de l'utilisateur :
+"${question}"`;
+
+ try {
+ console.log('🤔 …\n');
+ const answer = await callAI(prompt, config, { system: TUTOR_SYSTEM });
+ console.log(`${answer}\n`);
+ } catch (err) {
+ console.log(`⚠️ Le tuteur n'a pas pu répondre : ${err.message}\n`);
+ }
+}
diff --git a/metatron.js b/metatron.js
index 32f4ae1..2924d54 100644
--- a/metatron.js
+++ b/metatron.js
@@ -1,48 +1,240 @@
+#!/usr/bin/env node
+import fs from 'node:fs/promises';
+import path from 'node:path';
import { parseArgs, showHelp, ask, closeInterface } from './cli.js';
import { selectProvider, getProviderConfig } from './providers.js';
import { callAI } from './ai.js';
import { parseResponse, isVerificationWeak } from './parser.js';
import { displayStepOutput, displayStepDetails, displayParsingFailure, displayWeakVerificationWarning, displayContextWarning, clearScreen } from './display.js';
import { saveSession, loadSession } from './session.js';
+import { scanSource, checkSyntax, summarize } from './analyzer/static.js';
+import { runFile, formatRunReport } from './analyzer/runner.js';
+import { reviewCode } from './analyzer/review.js';
+import { printAnalyzeReport, printSummary } from './analyzer/report.js';
+import { loadMemory, saveMemory, reconcile, getStats } from './learning/memory.js';
+import { startTutorSession } from './learning/tutor.js';
+import { buildMapData, buildMapDataFromMemory, writeMapFile } from './learning/map.js';
-// Parse command line arguments
-const args = parseArgs();
+const HELP = `
+Metatron - AI Code Debugger & Analyzer + Tuteur d'apprentissage
-// Handle help flag
-if (args.showHelp) {
- showHelp();
- process.exit(0);
-}
+USAGE:
+ node metatron.js learn Analyse + leçons + tuteur interactif
+ node metatron.js analyze Scan statique seul [--review] [--provider=N]
+ node metatron.js run Exécution sandboxée [--timeout=10000]
+ node metatron.js gentest Génère et exécute des tests (LLM)
+ node metatron.js progress Tableau de bord erreurs/progrès
+ node metatron.js map [file|dir] [--out=path] Carte HTML cliquable des erreurs
+ node metatron.js gen [options] Legacy générateur pas-à-pas
+ node metatron.js help
-// Handle test flag
-if (args.runTests) {
- console.log('Running parser tests...\n');
- try {
- await import('./test.js');
- } catch (e) {
- console.error('Test file not found:', e.message);
+LE MODE APPRENTISSAGE :
+ learn Détecte les erreurs, les classe (nouveau / déjà vu / récurrent /
+ RÉGRESSION), affiche la leçon de chacune et ouvre une session
+ tutor où tu poses tes questions en français sur ton code.
+ Mémoire persistante dans .metatron/memory.json.
+ progress Historique : récidives, corrigées, régressions.
+ map Génère metatron-map.html : points d'erreur cliquables par fichier,
+ taille = récurrence, anneau rouge = régression. Sans argument,
+ reconstruit la carte depuis la mémoire.
+
+ Un DOSSIER en argument déclenche un scan récursif de toute la codebase
+ (node_modules, .git, dist… exclus automatiquement).
+
+EXAMPLES:
+ node metatron.js learn .
+ node metatron.js learn src/
+ node metatron.js progress
+ node metatron.js map --out=ma-carte.html
+`;
+
+const PROVIDER_ENV = [
+ ['GROK_API_KEY', 1],
+ ['GROQ_API_KEY', 3],
+ ['CLAUDE_API_KEY', 4],
+ ['OLLAMA_MODEL', 2]
+];
+
+function detectProviderFromEnv() {
+ for (const [envVar, id] of PROVIDER_ENV) {
+ if (process.env[envVar]) return id;
}
- process.exit(0);
+ return null;
}
-// Load session if specified via command line
-let sessionData = null;
-if (args.sessionFile) {
+function flagValue(args, name, fallback) {
+ const hit = args.find(a => a.startsWith(`--${name}=`));
+ return hit ? Number(hit.split('=')[1]) : fallback;
+}
+
+async function readTarget(target) {
try {
- sessionData = await loadSession(args.sessionFile);
- console.log(`📂 Loaded session from ${args.sessionFile}\n`);
+ const code = await fs.readFile(target, 'utf8');
+ return code;
} catch (err) {
- console.error(`❌ Failed to load session: ${err.message}`);
- process.exit(1);
+ console.error(`❌ Cannot read ${target}: ${err.message}`);
+ return null;
}
}
-// Main Application Logic
-async function main() {
+const SKIP_DIRS = new Set(['node_modules', '.git', '.metatron', 'dist', 'build', 'coverage', '.next', '.nuxt']);
+const CODE_EXTS = new Set(['.js', '.mjs', '.cjs']);
+const MAX_FILES = 500;
+
+/**
+ * Résout les arguments en liste de fichiers JS : fichiers directs ou
+ * parcours récursif des dossiers (node_modules etc. ignorés).
+ * @param {string[]} args
+ * @returns {Promise}
+ */
+export async function collectTargets(args) {
+ const targets = [];
+ for (const arg of args) {
+ let stat;
+ try {
+ stat = await fs.stat(arg);
+ } catch {
+ console.error(`⚠️ Introuvable, ignoré : ${arg}`);
+ continue;
+ }
+
+ if (stat.isFile()) {
+ targets.push(arg);
+ } else if (stat.isDirectory()) {
+ const entries = await fs.readdir(arg, { recursive: true, withFileTypes: true });
+ for (const entry of entries) {
+ if (!entry.isFile() || !CODE_EXTS.has(path.extname(entry.name))) continue;
+ const dir = entry.parentPath ?? entry.path ?? arg;
+ const rel = path.relative(arg, dir);
+ if (rel.split(path.sep).some(part => SKIP_DIRS.has(part))) continue;
+ targets.push(path.join(dir, entry.name));
+ }
+ }
+ }
+
+ const unique = [...new Set(targets)];
+ if (unique.length > MAX_FILES) {
+ console.log(`⚠️ ${unique.length} fichiers détectés — analyse limitée aux ${MAX_FILES} premiers.`);
+ return unique.slice(0, MAX_FILES);
+ }
+ return unique;
+}
+
+// ---------- analyze ----------
+async function cmdAnalyze(restArgs) {
+ const targets = await collectTargets(restArgs.filter(a => !a.startsWith('--')));
+ const wantReview = restArgs.includes('--review');
+ const providerOverride = flagValue(restArgs, 'provider', null);
+
+ if (targets.length === 0) {
+ console.log('❌ No file to analyze. Usage: node metatron.js analyze ');
+ process.exitCode = 2;
+ return;
+ }
+
+ let llmConfig = null;
+ if (wantReview) {
+ const providerId = providerOverride ?? detectProviderFromEnv();
+ if (!providerId) {
+ console.log('⚠️ --review requested but no API key found in env (GROK_API_KEY, GROQ_API_KEY, CLAUDE_API_KEY or OLLAMA_MODEL). Skipping LLM layer.');
+ } else {
+ llmConfig = await getProviderConfig(providerId);
+ console.log(`🤖 LLM review enabled (${llmConfig.model})`);
+ }
+ }
+
+ let exitCode = 0;
+ for (const target of targets) {
+ const code = await readTarget(target);
+ if (code === null) { exitCode = 2; continue; }
+
+ const syntax = await checkSyntax(target);
+ const findings = syntax.ok ? scanSource(code) : [];
+ let llmReview = null;
+
+ if (syntax.ok && llmConfig) {
+ try {
+ llmReview = await reviewCode({ code, fileName: target, findings }, llmConfig);
+ } catch (err) {
+ console.log(`⚠️ LLM review failed: ${err.message}`);
+ }
+ }
+
+ printAnalyzeReport(target, syntax, findings, llmReview);
+ exitCode = Math.max(exitCode, printSummary(findings, llmReview));
+ }
+ process.exitCode = exitCode;
+}
+
+// ---------- run ----------
+async function cmdRun(restArgs) {
+ const target = restArgs.find(a => !a.startsWith('--'));
+ if (!target) {
+ console.log('❌ No file to run. Usage: node metatron.js run [--timeout=ms]');
+ process.exitCode = 2;
+ return;
+ }
+ const timeoutMs = flagValue(restArgs, 'timeout', 10000);
+
+ console.log(`▶️ Running ${target} (timeout ${timeoutMs}ms)…\n`);
+ const result = await runFile(target, { timeoutMs });
+ console.log(formatRunReport(result).join('\n'));
+ process.exitCode = result.ok ? 0 : 1;
+}
+
+// ---------- gentest ----------
+async function cmdGentest(restArgs) {
+ const target = restArgs.find(a => !a.startsWith('--'));
+ if (!target) {
+ console.log('❌ No file. Usage: node metatron.js gentest ');
+ process.exitCode = 2;
+ return;
+ }
+
+ const providerId = detectProviderFromEnv() ?? await selectProvider();
+ const config = await getProviderConfig(providerId);
+
+ const code = await readTarget(target);
+ if (code === null) return;
+
+ console.log('\n🧪 Generating test suite…');
+ const prompt = `Generate a complete Node.js test suite using the built-in \`node:test\` module and \`node:assert/strict\` for this file.
+Cover normal cases, edge cases and error cases. Import functions from "${path.basename(target)}".
+Respond ONLY with the test file content, no markdown fences, no explanations.
+
+\`\`\`javascript
+${code}
+\`\`\``;
+
+ const raw = await callAI(prompt, config);
+ const cleaned = raw.replace(/^```(?:javascript|js)?\s*/m, '').replace(/```\s*$/m, '').trim();
+
+ const outFile = target.replace(/\.(js|mjs|cjs)$/, '') + '.test.mjs';
+ await fs.writeFile(outFile, cleaned + '\n', 'utf8');
+ console.log(`💾 Tests written to ${outFile}\n`);
+
+ console.log('▶️ Running generated tests…\n');
+ const result = await runFile(outFile, { timeoutMs: 60000 });
+ console.log(formatRunReport(result).join('\n'));
+ process.exitCode = result.ok ? 0 : 1;
+}
+
+// ---------- legacy stepwise generation ----------
+async function cmdGen(args) {
+ let sessionData = null;
+ if (args.sessionFile) {
+ try {
+ sessionData = await loadSession(args.sessionFile);
+ console.log(`📂 Loaded session from ${args.sessionFile}\n`);
+ } catch (err) {
+ console.error(`❌ Failed to load session: ${err.message}`);
+ process.exit(1);
+ }
+ }
+
clearScreen();
console.log('Metatron – Stepwise Code Generator\n');
- // Select AI provider (skip if loading session)
const provider = sessionData ? sessionData.provider : await selectProvider();
const config = sessionData ? sessionData.config : await getProviderConfig(provider);
@@ -52,78 +244,59 @@ async function main() {
let fullCode = sessionData ? sessionData.fullCode : '';
let context = sessionData ? sessionData.context : `Overall task: ${task}\n\n`;
let step = sessionData ? sessionData.step : 1;
- const MAX_TOKENS = 128000; // Grok-4 limit
+ const MAX_TOKENS = 128000;
while (true) {
const prompt = `Current context so far:\n${context}\n\nWhat is the next SINGLE critical logical step for this task?`;
console.log(`\nStep ${step} – asking AI…\n`);
const raw = await callAI(prompt, config);
-
- // Parse AI response
const parsed = parseResponse(raw);
if (!parsed) {
displayParsingFailure(raw);
const retry = await ask('Parsing failed. Try again with new prompt? (y/n): ');
- if (retry.toLowerCase() === 'y') {
- continue; // Retry same step
- } else {
- console.log('Session ended due to parsing failure');
- break;
- }
+ if (retry.toLowerCase() === 'y') continue;
+ console.log('Session ended due to parsing failure');
+ break;
}
const { explanation, code, verification } = parsed;
- // Check for weak verification
if (isVerificationWeak(verification)) {
displayWeakVerificationWarning(verification);
const confirm = await ask('⚠️ Weak verification (no OWASP/CWE/RFC/MDN/CVE). Continue accumulating code? (y/n): ');
if (confirm.toLowerCase() !== 'y') {
console.log('Step rejected - not accumulating code');
- // Still add to context for continuity
context += raw + '\n\n';
step++;
continue;
}
}
- // Accumulate code
fullCode += code + '\n\n';
-
- // Display step output
displayStepOutput(explanation, code, verification);
- // Context monitoring
const estimatedTokens = context.length / 4;
if (estimatedTokens > MAX_TOKENS * 0.8) {
displayContextWarning(estimatedTokens, MAX_TOKENS);
}
- // User interaction
let answer = await ask('→ Press Enter for next step, "details" to show full content, "save" to save session, "stop" to output full code, "quit" to exit: ');
if (answer.toLowerCase() === 'details') {
displayStepDetails(explanation, code, verification);
- // Re-prompt after showing details
answer = await ask('→ Press Enter for next step, "save" to save session, "stop" to output full code, "quit" to exit: ');
}
if (answer.toLowerCase() === 'quit') break;
if (answer.toLowerCase() === 'save') {
- const sessionToSave = {
- provider,
- config,
- task,
- fullCode,
- context,
- step,
+ await saveSession({
+ provider, config, task, fullCode, context, step,
timestamp: new Date().toISOString()
- };
- await saveSession(sessionToSave);
- continue; // Continue with next step after saving
+ });
+ continue;
}
if (answer.toLowerCase() === 'stop') {
@@ -132,7 +305,6 @@ async function main() {
break;
}
- // Add response to context for next step
context += raw + '\n\n';
step++;
}
@@ -140,7 +312,146 @@ async function main() {
closeInterface();
}
-main().catch(err => {
- console.error('Error:', err.message);
- closeInterface();
-});
+// ---------- learn ----------
+async function cmdLearn(restArgs) {
+ const targets = await collectTargets(restArgs.filter(a => !a.startsWith('--')));
+ if (targets.length === 0) {
+ console.log('❌ Usage: node metatron.js learn ');
+ process.exitCode = 2;
+ return;
+ }
+ if (targets.length > 1) console.log(`📂 ${targets.length} fichier(s) à analyser.`);
+
+ const files = [];
+ const findings = [];
+
+ for (const target of targets) {
+ const code = await readTarget(target);
+ if (code === null) continue;
+ files.push({ name: target, code });
+
+ const syntax = await checkSyntax(target);
+ if (!syntax.ok) {
+ console.log(`⛔ ${target} — erreur de syntaxe :\n${syntax.error}\n`);
+ continue;
+ }
+ for (const f of scanSource(code)) {
+ findings.push({ ...f, file: target });
+ }
+ }
+
+ const memory = await loadMemory();
+ const classified = reconcile(findings, memory, { scannedFiles: targets });
+ const stats = getStats(memory);
+ await saveMemory(memory);
+
+ printFindingOverview(classified);
+
+ let config = null;
+ const providerId = detectProviderFromEnv();
+ if (providerId) {
+ config = await getProviderConfig(providerId);
+ }
+
+ await startTutorSession({ files, classified, stats, config });
+}
+
+function printFindingOverview(classified) {
+ const icons = { regressed: '🚨', recurring: '🔁', new: '🆕', known: '👀', fixed: '✅' };
+ console.log('\n📊 Résultat de l\'analyse :');
+ for (const [kind, label] of [
+ ['regressed', 'RÉGRESSIONS (corrigée puis revenue !)'],
+ ['recurring', 'Récurrences (3 fois ou plus)'],
+ ['new', 'Nouvelles erreurs'],
+ ['known', 'Déjà connues'],
+ ['fixed', 'Corrigées depuis la dernière fois 🎉']
+ ]) {
+ if (classified[kind].length === 0) continue;
+ console.log(`\n${icons[kind]} ${label} (${classified[kind].length}) :`);
+ for (const item of classified[kind]) {
+ const file = item.file ?? item.entry?.file ?? '';
+ const line = item.line ?? '';
+ const title = item.title ?? item.ruleId;
+ console.log(` • ${title} — ${file}${line ? ':' + line : ''}`);
+ }
+ }
+ console.log('');
+}
+
+// ---------- progress ----------
+async function cmdProgress() {
+ const memory = await loadMemory();
+ const stats = getStats(memory);
+
+ console.log('\n📈 METATRON — Progression');
+ console.log('═'.repeat(50));
+ console.log(`Erreurs distinctes rencontrées : ${stats.totalDistinct}`);
+ console.log(`Encore ouvertes : ${stats.openCount}`);
+ console.log(`Corrigées : ${stats.fixedCount} 🎉`);
+ console.log(`Régressions totales : ${stats.regressionTotal}`);
+ console.log(`Scans mémorisés : ${stats.scans}`);
+
+ if (stats.topRecurring.length > 0) {
+ console.log('\nTop récidives (à travailler en priorité) :');
+ for (const e of stats.topRecurring) {
+ console.log(` • [${e.occurrences}×] ${e.ruleId} — ${e.file}`);
+ }
+ }
+ console.log('');
+}
+
+// ---------- map ----------
+async function cmdMap(restArgs) {
+ const outArg = restArgs.find(a => a.startsWith('--out='));
+ const outPath = outArg ? outArg.split('=').slice(1).join('=') : 'metatron-map.html';
+ const targets = await collectTargets(restArgs.filter(a => !a.startsWith('--')));
+
+ let data;
+ if (targets.length > 0) {
+ const findings = [];
+ for (const target of targets) {
+ const code = await readTarget(target);
+ if (code === null) continue;
+ if (!(await checkSyntax(target)).ok) {
+ console.log(`⚠️ ${target} a des erreurs de syntaxe, ignoré pour la carte.`);
+ continue;
+ }
+ for (const f of scanSource(code)) findings.push({ ...f, file: target });
+ }
+ const memory = await loadMemory();
+ data = buildMapData({ files: targets.map(t => ({ name: t })), classified: reconcile(findings, memory, { scannedFiles: targets }) });
+ await saveMemory(memory);
+ } else {
+ const memory = await loadMemory();
+ data = buildMapDataFromMemory(memory);
+ console.log('🗺️ Carte reconstruite depuis la mémoire projet.');
+ }
+
+ const written = await writeMapFile(data, outPath);
+ console.log(`✅ Carte écrite : ${written}`);
+ console.log(` Ouvre-la dans ton navigateur pour explorer les points d'erreur.`);
+}
+
+// ---------- router ----------
+const [,, command = 'help', ...restArgs] = process.argv;
+
+switch (command) {
+ case 'analyze': await cmdAnalyze(restArgs); break;
+ case 'run': await cmdRun(restArgs); break;
+ case 'gentest': await cmdGentest(restArgs); break;
+ case 'learn': await cmdLearn(restArgs); break;
+ case 'progress': await cmdProgress(); break;
+ case 'map': await cmdMap(restArgs); break;
+ case 'gen': {
+ const args = parseArgs();
+ if (args.showHelp) { showHelp(); break; }
+ await cmdGen(args);
+ break;
+ }
+ case '--help': case '-h': case 'help':
+ console.log(HELP);
+ break;
+ default:
+ console.log(`Unknown command: ${command}\n${HELP}`);
+ process.exitCode = 2;
+}
diff --git a/metatron_session_1766164023041.json b/metatron_session_1766164023041.json
deleted file mode 100644
index 06b59f6..0000000
--- a/metatron_session_1766164023041.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "provider": 2,
- "config": {
- "apiKey": null,
- "model": "llama2",
- "endpoint": "http://localhost:11434/v1/chat/completions",
- "format": "openai"
- },
- "task": "a writing app",
- "fullCode": "```php\n// Define a user model for authentication\nclass User {\n public $id;\n public $username;\n public $password;\n // ... other fields ...\n}\n\n// Implement password hashing and verification\nfunction hashPassword($password) {\n // Use a secure password hashing algorithm (e.g. bcrypt, argon2)\n // ... implementation details omitted ...\n}\n\nfunction verifyPassword($username, $password) {\n // Compare the provided password to the stored hashed password\n // ... implementation details omitted ...\n}\n\n// Implement role-based access control (RBAC)\nfunction checkAuthorization($user, $action) {\n // Check if the user has the required permissions for the action\n // ... implementation details omitted ...\n}\n```\n\n```php\n// Define a user profile model\nclass UserProfile {\n public $id;\n public $username;\n public $name;\n public $email;\n // ... other fields ...\n}\n\n// Implement user profile creation and updating\nfunction createUserProfile($user) {\n // Create a new user profile object\n $profile = new UserProfile();\n $profile->id = $user->id;\n $profile->username = $user->username;\n $profile->name = $user->name;\n $profile->email = $user->email;\n // ... other fields ...\n}\n\nfunction updateUserProfile($user, $changes) {\n // Update the user profile with the provided changes\n $profile = UserProfile::findOrCreate($user->id);\n $profile->name = $changes['name'];\n $profile->email = $changes['email'];\n // ... other fields ...\n}\n\n// Implement user profile-based personalization and recommendations\nfunction getRecommendationsForUser($user) {\n // Get the user's profile information\n $profile = UserProfile::findOrCreate($user->id);\n // Use the profile information to generate recommendations\n // ... implementation details omitted ...\n}\n```\n\n",
- "context": "Overall task: a writing app\n\nEXPLANATION:\nThe next critical logical step for our task of creating a writing app is to implement user authentication and authorization. This step exists to ensure that only authorized users have access to the app's features and data, and to prevent unauthorized access or tampering with the app's functionality.\n\nWhy this step exists:\nUser authentication and authorization are crucial for any app that deals with sensitive information, such as writing apps. Without proper authentication and authorization, an attacker could gain unauthorized access to the app's features and data, potentially leading to data breaches or other security incidents.\n\nPitfalls:\n\n* Implementing weak or easily guessable passwords\n* Failing to implement proper password hashing and verification mechanisms\n* Not implementing role-based access control (RBAC) or other authorization models\n* Ignoring the importance of secure password storage and management\n\nCODE:\n```php\n// Define a user model for authentication\nclass User {\n public $id;\n public $username;\n public $password;\n // ... other fields ...\n}\n\n// Implement password hashing and verification\nfunction hashPassword($password) {\n // Use a secure password hashing algorithm (e.g. bcrypt, argon2)\n // ... implementation details omitted ...\n}\n\nfunction verifyPassword($username, $password) {\n // Compare the provided password to the stored hashed password\n // ... implementation details omitted ...\n}\n\n// Implement role-based access control (RBAC)\nfunction checkAuthorization($user, $action) {\n // Check if the user has the required permissions for the action\n // ... implementation details omitted ...\n}\n```\nVERIFICATION:\nThe OWASP Authentication and Authorization Evaluation Guide provides a comprehensive set of guidelines and best practices for implementing authentication and authorization in web applications. The guide covers various aspects, including password hashing and verification, RBAC models, and secure storage and management of user credentials.\n\nReference: OWASP Authentication and Authorization Evaluation Guide ()\n\n",
- "step": 2,
- "timestamp": "2025-12-19T17:07:03.036Z"
-}
\ No newline at end of file
diff --git a/package.json b/package.json
index 6219279..6476e47 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,34 @@
{
"name": "metatron",
- "version": "1.0.0",
+ "version": "2.0.0",
+ "description": "AI Code Debugger & Analyzer - static rules, sandboxed execution, LLM review, interactive learning tutor and error map for your codebases",
"type": "module",
+ "bin": {
+ "metatron": "./metatron.js"
+ },
+ "files": [
+ "metatron.js",
+ "cli.js",
+ "ai.js",
+ "providers.js",
+ "parser.js",
+ "display.js",
+ "session.js",
+ "analyzer/",
+ "learning/"
+ ],
+ "engines": {
+ "node": ">=18"
+ },
+ "keywords": [
+ "code-review",
+ "static-analysis",
+ "ai-generated-code",
+ "debugger",
+ "learning",
+ "cli"
+ ],
+ "license": "SEE LICENSE IN LICENSE",
"scripts": {
"start": "node metatron.js",
"test": "node test.js"
diff --git a/prompts/grok.md b/prompts/grok.md
new file mode 100644
index 0000000..0bcf4eb
--- /dev/null
+++ b/prompts/grok.md
@@ -0,0 +1,59 @@
+# Project-local Grok prompt
+
+Use this prompt for tweets, blogs, forums, founder commentary, and recent public chatter before marketing a new pain point.
+
+```text
+I need pre-marketing pain-point due diligence for a startup idea.
+
+Project:
+- Name: Metatron
+- Geo: TBD
+- Target user: TBD
+- Buyer hypothesis: TBD
+- Pain point hypothesis: TBD
+- Proposed product or wedge: TBD
+- Main alternatives today: TBD
+
+Validation stage:
+- Current stage: L0
+
+Mission:
+- Search recent public signals from 2025-2026.
+- Search in French and English.
+- Prioritize X posts, LinkedIn posts, blog posts, local press, founder commentary, forum threads, Reddit, and app reviews if relevant.
+- Look for:
+ - user complaints
+ - recurring pain language
+ - process failures
+ - integration pain
+ - compliance or platform risk
+ - competitor weakness
+ - public signs of urgency
+
+Rules:
+- Every signal must include a direct URL and date.
+- If a signal is weak or anecdotal, label it weak.
+- Do not present social chatter as proof of willingness to pay.
+- Separate operational pain from generic hype.
+
+Return:
+
+1. Signal log
+- A table with:
+ date | source_type | source_url | actor | country | signal | strength | what_it_suggests | why_it_is_not_enough
+
+2. Pain-point synthesis
+- Say whether public chatter points to a real operational pain, a niche complaint, or generic market noise.
+
+3. Risk and safety synthesis
+- Flag platform dependency, compliance risk, or reputational risk that would make this pain point unsafe to market aggressively.
+
+4. Competitor and substitute synthesis
+- Identify whether the real substitute is an incumbent vendor, internal team, spreadsheet plus email, agency, integrator, or "do nothing".
+
+5. Validation-stage verdict
+- What can public-signal research confirm at the current stage?
+- What can it NOT confirm?
+- Which expert calls are still mandatory before the next stage?
+```
+
diff --git a/prompts/perplexity.md b/prompts/perplexity.md
new file mode 100644
index 0000000..c35fd3d
--- /dev/null
+++ b/prompts/perplexity.md
@@ -0,0 +1,62 @@
+# Project-local Perplexity prompt
+
+Use this prompt for citation-heavy desk research before any marketing, outreach, or strong product claim.
+
+```text
+You are doing pre-marketing pain-point due diligence.
+
+Project:
+- Name: Metatron
+- Geo: TBD
+- Target user: TBD
+- Buyer hypothesis: TBD
+- Pain point hypothesis: TBD
+- Proposed product or wedge: TBD
+- Main alternatives today: TBD
+
+Validation stage:
+- Current stage: L0
+- Goal of this run: move from the current stage to the next stage with evidence, not hype.
+
+Research rules:
+- Prefer sources from 2025-2026.
+- Use a 2024 source only if it is official and structurally important.
+- Search in both French and English.
+- Prioritize regulators, company pages, primary documentation, strong local business press, recent blogs, and public market analysis.
+- Separate verified facts from inference.
+- Do not treat market growth or digitization headlines as proof of willingness to pay.
+
+Return these sections:
+
+1. Executive verdict
+- Is this pain point real enough to keep validating now?
+- Answer: yes, no, or partial.
+
+2. Evidence table
+- Use exactly these columns:
+ venture | claim | source_url | source_date | geo | signal_type | confidence | what_it_proves | open_question
+- Allowed values for signal_type:
+ existence du marche
+ urgence
+ faisabilite
+ ne prouve pas la volonte de payer
+
+3. Buyer and budget signal
+- Identify the likely buyer, budget line, procurement path, and approval signal if visible.
+- Explicitly say what does NOT prove willingness to pay.
+
+4. Competitor and substitute map
+- List direct competitors, internal workflows, integrators, and "do nothing" substitutes.
+
+5. Risk scan
+- Cover compliance risk, platform risk, operational risk, and reputational risk.
+- Explicitly say whether this pain point is safe to market aggressively yet.
+
+6. Validation-stage verdict
+- Say whether the evidence is enough to move from:
+ - L0 to L1
+ - L1 to L2
+ - L2 to L3
+- If not, list the minimum next research or expert calls still required.
+```
+
diff --git a/research/.gitignore b/research/.gitignore
new file mode 100644
index 0000000..a1dde07
--- /dev/null
+++ b/research/.gitignore
@@ -0,0 +1,8 @@
+private/
+raw/
+interviews/
+contact-lists/
+notes-private.md
+contacts.csv
+leads.csv
+prospects.csv
diff --git a/research/README.md b/research/README.md
new file mode 100644
index 0000000..1fa5e3f
--- /dev/null
+++ b/research/README.md
@@ -0,0 +1,32 @@
+# Research system
+
+This folder tracks validation by stages. The idea must become more precise, better sourced, and less hypothetical at each step.
+
+## Validation ladder
+
+1. `L0 - Problem hypothesis`
+ You have a pain-point thesis, a target user, and a narrow wedge.
+2. `L1 - Desk evidence`
+ You have recent public evidence that the pain exists, is urgent enough, and is safe enough to keep validating.
+3. `L2 - Expert confirmation`
+ You have 3-5 expert calls confirming buyer reality, integration reality, and compliance reality.
+4. `L3 - Pilot-ready offer`
+ You have one narrow offer, one measurable outcome, and a realistic approval path.
+5. `L4 - Willingness-to-pay proof`
+ You have a paid pilot, signed LOI, or a clear approval commitment.
+
+## Files
+
+- `evidence-matrix.csv`: dated claims and sources
+- `scorecard.md`: go/no-go scoring against fixed axes
+- `open-questions.md`: what still blocks the next stage
+
+## Private data rule
+
+Do not commit raw interviews, contact lists, or private customer notes.
+Keep those only in ignored paths such as:
+
+- `research/private/`
+- `research/interviews/`
+- `research/raw/`
+- `research/contact-lists/`
diff --git a/research/evidence-matrix.csv b/research/evidence-matrix.csv
new file mode 100644
index 0000000..97723ca
--- /dev/null
+++ b/research/evidence-matrix.csv
@@ -0,0 +1,3 @@
+venture,claim,source_url,source_date,geo,signal_type,confidence,what_it_proves,open_question
+Metatron,TBD,TBD,2026-03-06,TBD,existence du marche,medium,TBD,TBD
+
diff --git a/research/open-questions.md b/research/open-questions.md
new file mode 100644
index 0000000..d7ee4e5
--- /dev/null
+++ b/research/open-questions.md
@@ -0,0 +1,29 @@
+# Open questions and blockers
+
+Date: 2026-03-06
+Project: Metatron
+Current validation stage: L0
+
+## Gate rule
+
+A pain point is only considered validated for the next stage when:
+
+1. There is recent evidence from 2025 or 2026, or an official structural 2024 source.
+2. There is at least one signal of urgency or budget, not just a macro digitization story.
+3. The wedge is feasible in a narrow v1.
+4. If the remaining doubt is about buying behavior, compliance, or integration, expert calls close it before moving forward.
+
+## Open questions
+
+| ID | Question | Why it matters | How to close it | Blocks which stage |
+| --- | --- | --- | --- | --- |
+| Q1 | What is the highest-confidence pain point for this project? | The wedge is undefined until the operational pain is precise. | Run desk research and 3-5 expert calls. | L0 -> L1 |
+| Q2 | Who owns the budget and approval path? | A problem without a buyer is not enough. | Interview likely operators, buyers, and integrators. | L1 -> L2 |
+| Q3 | Can the first pilot avoid heavy integration or compliance work? | Pilot friction determines speed to first contract. | Map current workflow and minimum required controls. | L2 -> L3 |
+
+## Expert-call cap
+
+- Minimum: 3 calls
+- Maximum: 5 calls
+- Trigger: required whenever desk research cannot prove buyer reality, compliance reality, or integration reality
+
diff --git a/research/scorecard.md b/research/scorecard.md
new file mode 100644
index 0000000..75f27cf
--- /dev/null
+++ b/research/scorecard.md
@@ -0,0 +1,33 @@
+# Scorecard
+
+Date: 2026-03-06
+Project: Metatron
+Current validation stage: L0
+Scale: 1 to 5
+Weighted points formula: `weight * score / 5`
+
+## Fixed axes
+
+| Axis | Weight | Score | What is proven | What is still missing |
+| --- | ---: | ---: | --- | --- |
+| Urgence | 20 | 0 | | |
+| Budget signal | 20 | 0 | | |
+| Speed to first contract | 15 | 0 | | |
+| Regulatory friction | 15 | 0 | | |
+| Integration load | 10 | 0 | | |
+| Platform dependency | 10 | 0 | | |
+| Competitive intensity | 5 | 0 | | |
+| Defensability | 5 | 0 | | |
+
+## Stage gates
+
+- L0 -> L1: at least 5 recent dated signals and no critical safety contradiction.
+- L1 -> L2: one credible buyer hypothesis and one credible integration hypothesis.
+- L2 -> L3: expert calls confirm buyer, approval path, and low-friction pilot scope.
+- L3 -> L4: one clear commercial ask and one measurable pilot KPI.
+
+## Decision rule
+
+- Do not move to marketing or code if any of these axes is still `1/5`: regulatory friction, integration load, or platform dependency.
+- Do not claim willingness to pay from desk research alone.
+
diff --git a/test.js b/test.js
index 62c50e9..c70cdc1 100644
--- a/test.js
+++ b/test.js
@@ -1,7 +1,13 @@
-// test.js — Test suite for metatron.js parser
+// test.js — Test suite for parser + analyzer
// Run with: node test.js
import { parseResponse } from './parser.js';
+import { scanSource, summarize, RULES } from './analyzer/static.js';
+import { parseErrors, formatRunReport } from './analyzer/runner.js';
+import { parseReviewResponse } from './analyzer/review.js';
+import { LESSONS, getLesson } from './analyzer/lessons.js';
+import { loadMemory, saveMemory, reconcile, getStats } from './learning/memory.js';
+import { buildMapDataFromMemory, renderMapHtml } from './learning/map.js';
// Mock data for testing parser
const testCases = [
@@ -80,10 +86,213 @@ testCases.forEach((test, i) => {
console.log('');
});
-console.log(`Results: ${passed}/${total} tests passed`);
+console.log(`Results: ${passed}/${total} parser tests passed`);
-if (passed === total) {
+// ---------- Analyzer tests ----------
+console.log('\nRunning analyzer tests...\n');
+
+let analyzerPassed = 0;
+let analyzerTotal = 0;
+
+function assertAnalyzer(name, condition, detail) {
+ analyzerTotal++;
+ if (condition) {
+ console.log(`Test ${analyzerTotal}: ${name}`);
+ console.log(' ✅ PASSED');
+ analyzerPassed++;
+ } else {
+ console.log(`Test ${analyzerTotal}: ${name}`);
+ console.log(' ❌ FAILED', detail || '');
+ }
+ console.log('');
+}
+
+const VULNERABLE_SAMPLE = `
+const apiKey = "sk-1234567890abcdef1234";
+const cmd = \`\${userInput}\`;
+eval(cmd);
+try { risky(); } catch (e) {}
+document.body.innerHTML = userData;
+db.query("SELECT * FROM users WHERE id = " + userId);
+const token = Math.random().toString(36);
+const agent = new https.Agent({ rejectUnauthorized: false });
+res.header("Access-Control-Allow-Origin", "*");
+if (a == b) { }
+var old = 1;
+while (true) { }
+`;
+
+const vulnFindings = scanSource(VULNERABLE_SAMPLE);
+const vulnIds = new Set(vulnFindings.map(f => f.ruleId));
+
+assertAnalyzer('Detects hardcoded secret',
+ vulnIds.has('HARDCODED_SECRET') || vulnIds.has('OPENAI_KEY'),
+ `got: ${[...vulnIds].join(', ')}`);
+
+assertAnalyzer('Detects eval()', vulnIds.has('EVAL_USAGE'));
+assertAnalyzer('Detects SQL concatenation', vulnIds.has('SQL_CONCAT'),
+ `got: ${[...vulnIds].join(', ')}`);
+
+assertAnalyzer('Detects weak random in auth context', vulnIds.has('WEAK_RANDOM_AUTH'));
+assertAnalyzer('Detects TLS bypass', vulnIds.has('TLS_BYPASS'));
+assertAnalyzer('Detects CORS wildcard', vulnIds.has('CORS_WILDCARD'));
+assertAnalyzer('Detects innerHTML sink', vulnIds.has('INNERHTML_ASSIGN'));
+assertAnalyzer('Detects empty catch', vulnIds.has('EMPTY_CATCH'));
+assertAnalyzer('Detects var declaration', vulnIds.has('VAR_DECLARATION'));
+
+const CLEAN_SAMPLE = `
+import crypto from 'node:crypto';
+
+export function makeToken() {
+ return crypto.randomBytes(32).toString('hex');
+}
+
+export function add(a, b) {
+ if (typeof a !== 'number' || typeof b !== 'number') {
+ throw new TypeError('numbers required');
+ }
+ return a + b;
+}
+`;
+
+const cleanFindings = scanSource(CLEAN_SAMPLE).filter(f =>
+ !['DEBUG_LEFTOVER'].includes(f.ruleId));
+
+assertAnalyzer('Clean code has no critical/high findings',
+ !cleanFindings.some(f => f.severity === 'critical' || f.severity === 'high'),
+ JSON.stringify(cleanFindings.map(f => f.ruleId)));
+
+assertAnalyzer('Summary counts match findings',
+ summarize(vulnFindings).critical >= 3);
+
+assertAnalyzer('Rule registry non-empty and ordered severities valid',
+ RULES.length >= 15 && RULES.every(r => r.id && r.pattern instanceof RegExp && r.title));
+
+const SAMPLE_STDERR = `C:\\proj\\app.js:5
+ throw new TypeError('x is not a function');
+ ^
+TypeError: x is not a function
+ at Object. (C:\\proj\\app.js:5:9)
+ at Module._compile (node:internal/modules/cjs/loader:1105:14)`;
+
+const parsedErrors = parseErrors(SAMPLE_STDERR);
+assertAnalyzer('Parses error name/message/line from stderr',
+ parsedErrors.length === 1 &&
+ parsedErrors[0].name === 'TypeError' &&
+ parsedErrors[0].message.includes('not a function') &&
+ parsedErrors[0].line === 5,
+ JSON.stringify(parsedErrors));
+
+assertAnalyzer('Empty stderr yields no errors',
+ parseErrors('').length === 0);
+
+const report = formatRunReport({
+ timedOut: true, ok: false, exitCode: null, durationMs: 1000,
+ stdout: '', stderr: 'TimeoutError: killed', errors: []
+});
+assertAnalyzer('Format flags timeout runs',
+ report[0].includes('TIMED OUT'));
+
+const reviewRaw = 'Sure! Here are my findings:\n```json\n[{"severity":"high","title":"t","line":3,"explanation":"e","suggestion":"s"}]\n```';
+const reviewParsed = parseReviewResponse(reviewRaw);
+assertAnalyzer('Parses fenced JSON LLM review',
+ Array.isArray(reviewParsed) && reviewParsed.length === 1 && reviewParsed[0].line === 3,
+ JSON.stringify(reviewParsed));
+
+assertAnalyzer('Parses bare JSON LLM review',
+ parseReviewResponse('[{"severity":"low","title":"t"}]').length === 1);
+
+assertAnalyzer('Throws on unparseable LLM review',
+ (() => { try { parseReviewResponse('no json here'); return false; } catch { return true; } })());
+
+// ---------- Learning layer tests ----------
+console.log('\nRunning learning tests...\n');
+
+assertAnalyzer('Lexique couvre toutes les règles',
+ RULES.every(r => LESSONS[r.id]),
+ `manquantes: ${RULES.filter(r => !LESSONS[r.id]).map(r => r.id).join(', ')}`);
+
+const fallbackLesson = getLesson('INEXISTANT');
+assertAnalyzer('Leçon générique de secours',
+ typeof fallbackLesson.explanation === 'string' && fallbackLesson.category === 'Général');
+
+{
+ const mem = { version: 1, entries: {}, scans: [] };
+ const f1 = [{ ruleId: 'EVAL_USAGE', line: 3, severity: 'critical', file: 'a.js' }];
+ const r1 = reconcile(f1, mem);
+ assertAnalyzer('Première détection classée NEW',
+ r1.new.length === 1 && mem.entries['EVAL_USAGE|a.js'].occurrences === 1);
+
+ const r2 = reconcile([{ ruleId: 'EVAL_USAGE', line: 5, severity: 'critical', file: 'a.js' }], mem);
+ assertAnalyzer('Deuxième détection classée KNOWN',
+ r2.known.length === 1 && mem.entries['EVAL_USAGE|a.js'].lines.includes(5));
+
+ const r3 = reconcile([], mem);
+ assertAnalyzer('Disparition classée FIXED',
+ r3.fixed.length === 1 && mem.entries['EVAL_USAGE|a.js'].status === 'fixed');
+
+ const r4 = reconcile([{ ruleId: 'EVAL_USAGE', line: 9, severity: 'critical', file: 'a.js' }], mem);
+ assertAnalyzer('Retour après correction = REGRESSION',
+ r4.regressed.length === 1 && mem.entries['EVAL_USAGE|a.js'].regressionCount === 1);
+}
+
+{
+ const mem = { version: 1, entries: {}, scans: [] };
+ reconcile([{ ruleId: 'EVAL_USAGE', line: 3, severity: 'critical', file: 'c.js' }], mem);
+ const outOfScope = reconcile([], mem, { scannedFiles: ['autre.js'] });
+ assertAnalyzer('Fichier hors périmètre non marqué FIXED',
+ outOfScope.fixed.length === 0 && mem.entries['EVAL_USAGE|c.js'].status === 'open');
+
+ const inScope = reconcile([], mem, { scannedFiles: ['c.js'] });
+ assertAnalyzer('Fichier scanné sans erreur marqué FIXED',
+ inScope.fixed.length === 1 && mem.entries['EVAL_USAGE|c.js'].status === 'fixed');
+}
+
+{
+ const mem = { version: 1, entries: {}, scans: [] };
+ for (let i = 0; i < 3; i++) {
+ reconcile([{ ruleId: 'VAR_DECLARATION', line: i + 1, severity: 'info', file: 'b.js' }], mem);
+ }
+ const lastRun = reconcile([{ ruleId: 'VAR_DECLARATION', line: 4, severity: 'info', file: 'b.js' }], mem);
+ assertAnalyzer('Récurrence (>=3) classée RECURRING', lastRun.recurring.length === 1);
+
+ const stats = getStats(mem);
+ assertAnalyzer('Stats comptabilisent ouvert + scans',
+ stats.openCount === 1 && stats.scans === 4 && stats.topRecurring[0].occurrences === 4);
+}
+
+{
+ const mem = {
+ version: 1,
+ entries: {
+ 'EVAL_USAGE|src/x.js': {
+ ruleId: 'EVAL_USAGE', file: 'src/x.js', firstSeen: '2026-01-01', lastSeen: '2026-01-02',
+ occurrences: 2, lines: [3, 8], status: 'open'
+ },
+ 'EMPTY_CATCH|src/y.js': {
+ ruleId: 'EMPTY_CATCH', file: 'src/y.js', firstSeen: '2026-01-01', lastSeen: '2026-01-01',
+ occurrences: 1, lines: [12], status: 'fixed', fixedAt: '2026-01-03'
+ }
+ },
+ scans: []
+ };
+ const data = buildMapDataFromMemory(mem);
+ const html = renderMapHtml(data);
+ assertAnalyzer('Carte depuis mémoire : points ouverts + fixes',
+ data.points.length === 1 && data.fixed.length === 1 && data.files.includes('src/x.js'));
+ assertAnalyzer('HTML de carte contient données embarquées + leçons',
+ html.includes('"points"') &&
+ html.includes('application/json') &&
+ html.includes("Pourquoi c'est un probl") &&
+ html.includes('EVAL_USAGE'));
+}
+
+console.log(`Results: ${analyzerPassed}/${analyzerTotal} analyzer tests passed`);
+
+const allPassed = passed === total && analyzerPassed === analyzerTotal;
+if (allPassed) {
console.log('🎉 All tests passed!');
} else {
console.log('⚠️ Some tests failed. Check the output above.');
+ process.exit(1);
}