import fs from 'fs/promises'; import path from 'path'; import { Command } from 'commander'; import { bomAgent, BomAgentResult } from './bomAgent'; import { lintAgent, LintAgentResult } from './lintAgent'; import { securityAgent, SecurityAgentResult } from './securityAgent'; import { testAgent, TestAgentResult } from './testAgent'; // Function to generate a synthetic coverage paragraph export function paragrapheCoverage( lines: number, functions: number, branches: number, statements: number, standard: number = 80 ): string { type Niveau = 'IL Y A DU TRAVAIL' | 'PEUT MIEUX FAIRE' | 'PRESQUE CONFORME' | 'OK'; const qualif = (p: number): Niveau => p < 50 ? 'IL Y A DU TRAVAIL' : p < 70 ? 'PEUT MIEUX FAIRE' : p < standard ? 'PRESQUE CONFORME' : 'OK'; const indics = { lignes: { val: lines, niv: qualif(lines) }, fonctions: { val: functions, niv: qualif(functions) }, branches: { val: branches, niv: qualif(branches) }, instructions: { val: statements, niv: qualif(statements) }, }; const ordre: Record = { 'IL Y A DU TRAVAIL': 0, 'PEUT MIEUX FAIRE': 1, 'PRESQUE CONFORME': 2, 'OK': 3, }; const global = (Object.values(indics).map(o => o.niv).sort((a, b) => ordre[a] - ordre[b])[0]) as Niveau; const [faibleCle, faible] = Object.entries(indics).sort(([, a], [, b]) => a.val - b.val)[0]; const intro: Record = { 'IL Y A DU TRAVAIL': 'La couverture globale des tests est très insuffisante.', 'PEUT MIEUX FAIRE': 'Il existe des tests dans l’application, mais la couverture globale reste à améliorer.', 'PRESQUE CONFORME': 'La couverture globale s’approche du seuil visé.', 'OK': 'La couverture globale est conforme au standard en vigueur.', }; const action: Record = { 'IL Y A DU TRAVAIL': 'Une action urgente est requise pour réduire les zones non testées.', 'PEUT MIEUX FAIRE': 'Des efforts complémentaires sont recommandés pour atteindre le niveau attendu.', 'PRESQUE CONFORME': 'Un effort résiduel permettra d’atteindre la conformité totale.', 'OK': 'Il est conseillé de maintenir ce niveau dans la durée.', }; return [ intro[global], `La couverture par ${faibleCle} est actuellement la plus basse avec ${faible.val} %.`, action[global], `Indicateur de test : ${global}.`, ].join(' '); } /** * Génère un graphique SVG simple pour les métriques de coverage */ function generateCoverageChart(metrics: { lines: number; functions: number; branches: number; statements: number }): string { // Chart size slightly enlarged again (increased width) const svgWidth = 240; const svgHeight = 130; // margins adjusted for new size const margin = { top: 12, right: 12, bottom: 22, left: 12 }; const barGap = 12; const barCount = 4; const rawBarWidth = (svgWidth - margin.left - margin.right - barGap * (barCount - 1)) / barCount; // half-thickness bars const barWidth = rawBarWidth / 2; const names = ['Lines', 'Functions', 'Branches', 'Statements']; const values = [metrics.lines, metrics.functions, metrics.branches, metrics.statements]; let svg = ``; svg += ``; const threshold = 80; const maxBarHeight = svgHeight - margin.top - margin.bottom; // Draw bars for (let i = 0; i < barCount; i++) { // Center bars in their slot by halving remaining space const slotWidth = rawBarWidth + barGap; const x = margin.left + i * slotWidth + (slotWidth - barWidth) / 2; const barHeight = (values[i] / 100) * maxBarHeight; const y = margin.top + (maxBarHeight - barHeight); // Color red if below threshold const fillColor = values[i] < threshold ? '#fdd3cb' : '#a7d2ea'; svg += ``; svg += `${values[i]}%`; svg += `${names[i]}`; } // Draw threshold line at 80% const threshY = margin.top + (maxBarHeight - (threshold / 100) * maxBarHeight); svg += ``; // Add threshold label vertically along the y-axis (shifted lower and right) const labelX = margin.left + 10; // Descend le label de 45px (35px + 10px supplémentaire) const labelY = threshY + 45; svg += `Coverage required`; svg += ''; return svg; } export interface FullAgentOptions {} export interface FullAgentResult { test: TestAgentResult; lint: LintAgentResult; security: SecurityAgentResult; bom: BomAgentResult; report: string; } /** * Ordonne et exécute tous les agents de quantimétrie pour générer * un rapport complet en markdown. */ export async function fullAgent( opts?: FullAgentOptions, ): Promise { const test = await testAgent(opts); const lint = await lintAgent(opts); const security = await securityAgent(opts); const bom = await bomAgent(opts); // Construction d'un rapport au format Markdown let report = '# Rapport Qualimétrie\n\n'; report += '## 1. Tests & Couverture\n\n'; if (test.success && test.summary && test.metrics) { // Test summary as narrative sentence with bold numbers report += `Les tests ont été exécutés pour un total de **${test.summary.total}** tests, dont **${test.summary.passed}** réussis et **${test.summary.failed}** échoués, en **${test.summary.duration}**.\n\n`; // Coverage metrics as cards with analysis report += '
\n'; report += '
\n'; report += `
${Math.round(test.metrics.lines)}%
Lines covered
ceci est les contenu de la card
\n`; report += `
${Math.round(test.metrics.functions)}%
Functions covered
ceci est les contenu de la card
\n`; report += `
${Math.round(test.metrics.branches)}%
Branches covered
ceci est les contenu de la card
\n`; report += `
${Math.round(test.metrics.statements)}%
Statements covered
ceci est les contenu de la card
\n`; report += '
\n'; report += '
\n'; report += '
Rapport
\n'; report += '

Analyse

\n'; // Synthèse de la couverture sous forme de bloc HTML report += paragrapheCoverage( Math.round(test.metrics.lines), Math.round(test.metrics.functions), Math.round(test.metrics.branches), Math.round(test.metrics.statements) ) + '\n\n'; // Graphe de couverture report += '
' + generateCoverageChart({ lines: Math.round(test.metrics.lines), functions: Math.round(test.metrics.functions), branches: Math.round(test.metrics.branches), statements: Math.round(test.metrics.statements), }) + '
\n\n'; report += '
\n'; report += '
\n\n'; } else { report += 'Erreur lors de l\'exécution des tests ou de la collecte de la couverture.\n\n'; } report += '## 2. Qualité de code (ESLint)\n\n'; if (lint.metrics) { report += `- Erreurs: **${lint.metrics.errors}**\n`; report += `- Warnings: **${lint.metrics.warnings}**\n`; report += `- Fichiers analysés: **${lint.metrics.files}**\n`; report += `- Total problèmes: **${lint.metrics.total}**\n`; report += `- Problèmes fixables: **${lint.metrics.fixable}**\n\n`; if (lint.summary) { report += '**Top fichiers problématiques**\n'; lint.summary.topFiles.forEach(f => { const rel = path.relative(process.cwd(), f.filePath) || f.filePath; report += `- ${rel}: ${f.total} problèmes (${f.errors} erreurs, ${f.warnings} avertissements)\n`; }); report += '\n**Top règles violées**\n'; lint.summary.topRules.forEach(r => { report += `- ${r.ruleId}: ${r.count} occurrences\n`; }); report += '\n'; } if (lint.detailPath) { report += `> Rapport complet ESLint: ${lint.detailPath}\n\n`; } } else { report += "Erreur lors de l'exécution d'ESLint.\n\n"; } report += '## 3. Sécurité (Vulnérabilités)\n'; report += '```json\n' + JSON.stringify(security.vulnerabilities, null, 2) + '\n```\n\n'; report += '## 4. Bill Of Materials (Licences)\n\n'; report += 'Le tableau ci-dessous présente les 50 premières dépendances du Bill of Materials, ' + 'avec leur nom, version et licence.\n\n'; report += '\n'; report += '\n'; report += '\n'; report += '\n'; report += '\n'; report += '\n'; report += '\n'; report += '\n'; report += '\n'; bom.packages.slice(0, 50).forEach((pkg, index) => { const bgStyle = index % 2 === 0 ? 'background-color: #eef5fb;' : ''; report += `\n`; report += `\n`; report += `\n`; report += `\n`; report += '\n'; }); report += '\n'; report += '
NomVersionLicence
${pkg.name}${pkg.version}${pkg.license}
\n\n'; report += 'Liste complète en pièce jointe de ce document.\n\n'; return { test, lint, security, bom, report }; } export const cli = { command: 'qualimetrie:full', description: 'Exécute tous les agents de qualimétrie et génère un rapport (Markdown ou HTML)', builder: (cmd: Command) => cmd .option('--out ', 'Output format: markdown or html', 'markdown') .option( '--output ', "Base path (without extension) for report (default: 'report/index')", 'report/index' ), handler: async (opts: any) => { // eslint-disable-next-line @typescript-eslint/no-var-requires const { fullAgent: runFullAgent } = require('./fullAgent'); // eslint-disable-next-line @typescript-eslint/no-var-requires const { markdownHtmlAgent: runMdAgent } = require('../markdownHtmlAgent'); const res = await runFullAgent(); const format = (opts.out || 'markdown').toLowerCase(); const base = opts.output || 'report/index'; let outFile: string; if (format === 'html') { outFile = base.endsWith('.html') ? base : `${base}.html`; } else { outFile = base.endsWith('.md') ? base : `${base}.md`; } const outDir = path.dirname(outFile); await fs.mkdir(outDir, { recursive: true }); if (format === 'html') { try { // Append attachments links for detailed reports const attachments = ` ## Attachments - [Coverage HTML report](../coverage/lcov-report/index.html) - [Coverage summary JSON](coverage-summary.json) - [Jest results JSON](jest-results.json) - [ESLint report JSON](eslint-report.json) - [Audit report JSON](audit-report.json) - [BOM report JSON](bom-report.json) `; const html = await runMdAgent({ title: 'Qualimetry Report', markdown: res.report + attachments, }); // Write HTML report await fs.writeFile(outFile, html, 'utf-8'); // Copy detailed JSON reports into report folder await fs.copyFile( path.resolve('coverage', 'coverage-summary.json'), path.join(outDir, 'coverage-summary.json') ); await fs.copyFile( path.resolve('jest-results.json'), path.join(outDir, 'jest-results.json') ); if (res.lint.detailPath) { await fs.copyFile( path.resolve(res.lint.detailPath), path.join(outDir, path.basename(res.lint.detailPath)) ); } if (res.security.detailPath) { await fs.copyFile( path.resolve(res.security.detailPath), path.join(outDir, path.basename(res.security.detailPath)) ); } // Write BOM details await fs.writeFile( path.join(outDir, 'bom-report.json'), JSON.stringify(res.bom.packages, null, 2), 'utf-8' ); console.log(`HTML report generated: ${outFile}`); } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); console.error('Error generating HTML report:', msg); process.exit(1); } } else { // Markdown output await fs.writeFile(outFile, res.report, 'utf-8'); console.log(`Markdown report generated: ${outFile}`); } }, };