qa360 1.0.0 → 1.0.2

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.
@@ -1,129 +1,121 @@
1
1
  /**
2
- * QA360 Report Command - Signed proof generation
2
+ * QA360 Report Command - Phase3 Proof Reports
3
+ * Generates PDF/HTML/JSON reports from Phase3Runner proof files
3
4
  */
4
5
  import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
5
6
  import { join } from 'path';
6
- import { createHash, generateKeyPair, sign } from 'crypto';
7
7
  import chalk from 'chalk';
8
8
  import ora from 'ora';
9
9
  export class QA360Reporter {
10
10
  qa360Dir = join(process.cwd(), '.qa360');
11
11
  runsDir = join(this.qa360Dir, 'runs');
12
- keysDir = join(this.qa360Dir, 'keys');
13
- privateKeyPath = join(this.keysDir, 'qa360.key');
14
- publicKeyPath = join(this.keysDir, 'qa360.pub');
15
12
  async generateReport(options = {}) {
16
- console.log(chalk.blue('📋 QA360 Report - Génération de preuve\n'));
13
+ console.log(chalk.blue('📋 QA360 Report - Génération de rapport\n'));
17
14
  try {
18
- // 1. Load latest run results
19
- const runResult = await this.loadRunResults(options.runId);
20
- console.log(chalk.green(`📊 Résultats chargés: ${runResult.pack.name}`));
21
- // 2. Calculate trust score
22
- const trustScore = this.calculateTrustScore(runResult);
23
- console.log(chalk.cyan(`🎯 Trust Score: ${trustScore}%`));
24
- // 3. Generate reports
15
+ // 1. Load proof document
16
+ const proof = await this.loadProofDocument(options.runId);
17
+ console.log(chalk.green(`📊 Preuve chargée: ${proof.pack.name} (${proof.runId})`));
18
+ console.log(chalk.cyan(`🎯 Trust Score: ${proof.execution.trustScore}%`));
19
+ console.log(chalk.gray(`🔏 Signature: ${proof.signature.algorithm}`));
20
+ // 2. Generate reports
25
21
  const outputs = [];
26
- if (options.format === 'json' || options.format === 'both' || !options.format) {
27
- const jsonPath = await this.generateJsonReport(runResult, trustScore);
22
+ const format = options.format || 'both';
23
+ if (format === 'json' || format === 'both') {
24
+ const jsonPath = await this.generateJsonReport(proof);
28
25
  outputs.push(jsonPath);
29
26
  }
30
- if (options.format === 'pdf' || options.format === 'both' || !options.format) {
31
- const pdfPath = await this.generatePdfReport(runResult, trustScore);
32
- outputs.push(pdfPath);
27
+ if (format === 'html') {
28
+ const htmlPath = await this.generateHtmlOnlyReport(proof);
29
+ outputs.push(htmlPath);
33
30
  }
34
- // 4. Sign if requested
35
- if (options.sign) {
36
- await this.signReports(outputs, runResult, trustScore);
31
+ if (format === 'pdf' || format === 'both') {
32
+ const pdfPath = await this.generatePdfReport(proof);
33
+ outputs.push(pdfPath);
37
34
  }
38
35
  return outputs;
39
36
  }
40
37
  catch (error) {
41
- console.error(chalk.red('❌ Erreur lors de la génération:'), error);
38
+ console.error(chalk.red('❌ Erreur:'), error);
42
39
  throw error;
43
40
  }
44
41
  }
45
- async loadRunResults(runId) {
42
+ /**
43
+ * Load proof document from Phase3Runner
44
+ */
45
+ async loadProofDocument(runId) {
46
46
  if (!existsSync(this.runsDir)) {
47
- throw new Error('Aucun résultat d\'exécution trouvé. Utilisez \'qa360 run\' d\'abord');
47
+ throw new Error('Aucune preuve trouvée. Utilisez \'qa360 run\' d\'abord.');
48
48
  }
49
- let targetDir;
49
+ let proofPath;
50
50
  if (runId) {
51
- targetDir = join(this.runsDir, runId);
52
- if (!existsSync(targetDir)) {
53
- throw new Error(`Run ID non trouvé: ${runId}`);
51
+ // Search for specific proof
52
+ proofPath = join(this.runsDir, `${runId}-proof.json`);
53
+ if (!existsSync(proofPath)) {
54
+ // Try with partial match
55
+ const files = readdirSync(this.runsDir).filter(f => f.includes(runId) && f.endsWith('-proof.json'));
56
+ if (files.length === 0) {
57
+ throw new Error(`Preuve non trouvée: ${runId}`);
58
+ }
59
+ proofPath = join(this.runsDir, files[0]);
54
60
  }
55
61
  }
56
62
  else {
57
- // Get latest run
58
- const runs = readdirSync(this.runsDir)
59
- .filter(dir => existsSync(join(this.runsDir, dir, 'results.json')))
63
+ // Find latest proof
64
+ const proofFiles = readdirSync(this.runsDir)
65
+ .filter(f => f.endsWith('-proof.json'))
60
66
  .sort()
61
67
  .reverse();
62
- if (runs.length === 0) {
63
- throw new Error('Aucun résultat d\'exécution trouvé');
68
+ if (proofFiles.length === 0) {
69
+ throw new Error('Aucune preuve trouvée.');
64
70
  }
65
- targetDir = join(this.runsDir, runs[0]);
71
+ proofPath = join(this.runsDir, proofFiles[0]);
66
72
  }
67
- const resultsPath = join(targetDir, 'results.json');
68
- const content = readFileSync(resultsPath, 'utf8');
73
+ const content = readFileSync(proofPath, 'utf8');
69
74
  return JSON.parse(content);
70
75
  }
71
- calculateTrustScore(runResult) {
72
- const { summary } = runResult;
73
- if (summary.total === 0)
74
- return 0;
75
- // Base score from success rate
76
- let score = summary.successRate;
77
- // Penalties and bonuses
78
- if (summary.failed > 0) {
79
- score -= Math.min(20, summary.failed * 5); // -5 points per failure, max -20
80
- }
81
- if (summary.skipped > summary.total * 0.5) {
82
- score -= 10; // -10 if more than 50% skipped
83
- }
84
- // Bonus for comprehensive testing
85
- if (runResult.pack.adapters.length > 2) {
86
- score += 5; // +5 for multi-adapter testing
87
- }
88
- // Duration penalty for very slow tests
89
- if (runResult.duration > 300000) { // 5 minutes
90
- score -= 5;
91
- }
92
- return Math.max(0, Math.min(100, Math.round(score)));
76
+ /**
77
+ * Trust score is already calculated in proof document
78
+ */
79
+ getTrustScore(proof) {
80
+ return proof.execution.trustScore;
93
81
  }
94
- async generateJsonReport(runResult, trustScore) {
82
+ async generateJsonReport(proof) {
95
83
  const spinner = ora('Génération rapport JSON...').start();
96
84
  try {
97
85
  const report = {
98
86
  id: this.generateReportId(),
99
- timestamp: new Date().toISOString(),
100
- pack: {
101
- name: runResult.pack.name,
102
- version: runResult.pack.version,
103
- description: runResult.pack.description
87
+ generatedAt: new Date().toISOString(),
88
+ // Proof data
89
+ proof: {
90
+ runId: proof.runId,
91
+ timestamp: proof.timestamp,
92
+ version: proof.version
104
93
  },
105
- execution: {
106
- timestamp: runResult.timestamp,
107
- duration: runResult.duration,
108
- summary: runResult.summary
94
+ // Pack info
95
+ pack: proof.pack,
96
+ // Execution results
97
+ execution: proof.execution,
98
+ // Gates detail
99
+ gates: proof.gates,
100
+ // Hooks timing
101
+ hooks: proof.hooks,
102
+ // Signature
103
+ signature: {
104
+ algorithm: proof.signature.algorithm,
105
+ verified: proof.signature.algorithm.includes('ed25519'),
106
+ publicKey: proof.signature.publicKey ?
107
+ proof.signature.publicKey.substring(0, 16) + '...' : 'none',
108
+ timestamp: proof.signature.timestamp
109
109
  },
110
- trustScore,
111
- results: runResult.results.map(r => ({
112
- name: r.name,
113
- adapter: r.adapter,
114
- status: r.status,
115
- duration: r.duration,
116
- error: r.error,
117
- output: r.output
118
- })),
110
+ // Report metadata
119
111
  metadata: {
120
- qa360Version: '0.9.0-core',
112
+ qa360Version: '1.0.1',
121
113
  platform: process.platform,
122
114
  nodeVersion: process.version,
123
- generatedAt: new Date().toISOString()
115
+ reportType: 'json'
124
116
  }
125
117
  };
126
- const reportPath = join(this.getLatestRunDir(), 'report.json');
118
+ const reportPath = join(this.runsDir, `${proof.runId}-report.json`);
127
119
  writeFileSync(reportPath, JSON.stringify(report, null, 2));
128
120
  spinner.succeed(`Rapport JSON généré: ${reportPath}`);
129
121
  return reportPath;
@@ -133,21 +125,35 @@ export class QA360Reporter {
133
125
  throw error;
134
126
  }
135
127
  }
136
- async generatePdfReport(runResult, trustScore) {
128
+ async generatePdfReport(proof) {
137
129
  const spinner = ora('Génération rapport PDF...').start();
138
130
  try {
139
- // PDF generation requires Playwright - use @playwright/test if needed
140
- // For now, save HTML as PDF placeholder
141
- const runDir = this.getLatestRunDir();
142
- const htmlPath = join(runDir, 'report.html');
143
- const pdfPath = join(runDir, 'proof.pdf');
144
- // Generate HTML content
145
- const htmlContent = this.generateHtmlReport(runResult, trustScore);
131
+ const htmlContent = this.generateHtmlReport(proof);
132
+ const htmlPath = join(this.runsDir, `${proof.runId}-report.html`);
133
+ const pdfPath = join(this.runsDir, `${proof.runId}-report.pdf`);
134
+ // Save HTML first
146
135
  writeFileSync(htmlPath, htmlContent);
147
- // TODO: Implement actual PDF generation with Playwright
148
- // For now, just save HTML with .pdf extension as placeholder
149
- writeFileSync(pdfPath, htmlContent);
150
- spinner.succeed(`Rapport PDF généré: ${pdfPath}`);
136
+ // Try PDF generation with Playwright
137
+ try {
138
+ // @ts-ignore - Dynamic import, Playwright is optional
139
+ const { chromium } = await import('playwright');
140
+ const browser = await chromium.launch({ headless: true });
141
+ const page = await browser.newPage();
142
+ await page.setContent(htmlContent, { waitUntil: 'networkidle' });
143
+ await page.pdf({
144
+ path: pdfPath,
145
+ format: 'A4',
146
+ margin: { top: '20px', bottom: '20px', left: '20px', right: '20px' },
147
+ printBackground: true
148
+ });
149
+ await browser.close();
150
+ spinner.succeed(`Rapport PDF généré: ${pdfPath}`);
151
+ }
152
+ catch (playwrightError) {
153
+ // Fallback: just keep HTML
154
+ writeFileSync(pdfPath, htmlContent);
155
+ spinner.warn(`Rapport HTML généré (Playwright non disponible): ${htmlPath}`);
156
+ }
151
157
  return pdfPath;
152
158
  }
153
159
  catch (error) {
@@ -155,180 +161,205 @@ export class QA360Reporter {
155
161
  throw error;
156
162
  }
157
163
  }
158
- generateHtmlReport(runResult, trustScore) {
159
- const { pack, summary, results, duration, timestamp } = runResult;
164
+ async generateHtmlOnlyReport(proof) {
165
+ const spinner = ora('Génération rapport HTML...').start();
166
+ const htmlContent = this.generateHtmlReport(proof);
167
+ const htmlPath = join(this.runsDir, `${proof.runId}-report.html`);
168
+ writeFileSync(htmlPath, htmlContent);
169
+ spinner.succeed(`Rapport HTML généré: ${htmlPath}`);
170
+ return htmlPath;
171
+ }
172
+ generateHtmlReport(proof) {
173
+ const { pack, execution, gates, signature } = proof;
174
+ const trustScore = execution.trustScore;
160
175
  const trustColor = trustScore >= 80 ? '#22c55e' : trustScore >= 60 ? '#f59e0b' : '#ef4444';
161
176
  const trustLabel = trustScore >= 80 ? 'ÉLEVÉ' : trustScore >= 60 ? 'MOYEN' : 'FAIBLE';
177
+ const signatureStatus = signature.algorithm.includes('ed25519')
178
+ ? '✅ Signé Ed25519'
179
+ : '⚠️ Non signé';
180
+ const passedGates = gates.filter(g => g.success).length;
162
181
  return `
163
182
  <!DOCTYPE html>
164
183
  <html>
165
184
  <head>
166
185
  <meta charset="UTF-8">
167
- <title>QA360 Proof - ${pack.name}</title>
186
+ <title>QA360 Report - ${pack.name}</title>
168
187
  <style>
169
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 0; padding: 20px; }
170
- .header { text-align: center; border-bottom: 2px solid #3b82f6; padding-bottom: 20px; margin-bottom: 30px; }
171
- .logo { font-size: 24px; font-weight: bold; color: #3b82f6; }
172
- .title { font-size: 20px; margin: 10px 0; }
173
- .trust-score { font-size: 48px; font-weight: bold; color: ${trustColor}; margin: 20px 0; }
174
- .trust-label { background: ${trustColor}; color: white; padding: 5px 15px; border-radius: 20px; font-size: 14px; }
175
- .summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin: 30px 0; }
176
- .metric { text-align: center; padding: 15px; border: 1px solid #e5e7eb; border-radius: 8px; }
177
- .metric-value { font-size: 24px; font-weight: bold; margin-bottom: 5px; }
178
- .metric-label { font-size: 12px; color: #6b7280; text-transform: uppercase; }
179
- .tests { margin: 30px 0; }
180
- .test { display: flex; justify-content: space-between; padding: 10px; border-bottom: 1px solid #f3f4f6; }
181
- .test-name { font-weight: 500; }
182
- .test-status { padding: 2px 8px; border-radius: 4px; font-size: 12px; }
183
- .passed { background: #dcfce7; color: #166534; }
184
- .failed { background: #fecaca; color: #991b1b; }
185
- .skipped { background: #fef3c7; color: #92400e; }
186
- .footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #e5e7eb; font-size: 12px; color: #6b7280; }
187
- .qr-placeholder { width: 100px; height: 100px; border: 2px dashed #d1d5db; margin: 20px auto; display: flex; align-items: center; justify-content: center; }
188
+ * { box-sizing: border-box; margin: 0; padding: 0; }
189
+ body {
190
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
191
+ background: #f8fafc;
192
+ color: #1e293b;
193
+ line-height: 1.6;
194
+ }
195
+ .container { max-width: 900px; margin: 0 auto; padding: 40px 20px; }
196
+
197
+ .header {
198
+ background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
199
+ color: white;
200
+ padding: 40px;
201
+ border-radius: 16px;
202
+ text-align: center;
203
+ margin-bottom: 30px;
204
+ }
205
+ .logo { font-size: 28px; font-weight: 800; margin-bottom: 8px; }
206
+ .subtitle { opacity: 0.9; font-size: 14px; }
207
+
208
+ .trust-card {
209
+ background: white;
210
+ border-radius: 16px;
211
+ padding: 30px;
212
+ text-align: center;
213
+ margin-bottom: 30px;
214
+ box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);
215
+ }
216
+ .trust-score {
217
+ font-size: 72px;
218
+ font-weight: 800;
219
+ color: ${trustColor};
220
+ line-height: 1;
221
+ }
222
+ .trust-label {
223
+ display: inline-block;
224
+ background: ${trustColor};
225
+ color: white;
226
+ padding: 6px 16px;
227
+ border-radius: 20px;
228
+ font-size: 12px;
229
+ font-weight: 600;
230
+ margin-top: 10px;
231
+ }
232
+
233
+ .info-grid {
234
+ display: grid;
235
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
236
+ gap: 20px;
237
+ margin-bottom: 30px;
238
+ }
239
+ .info-card {
240
+ background: white;
241
+ border-radius: 12px;
242
+ padding: 20px;
243
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
244
+ }
245
+ .info-label { font-size: 12px; color: #64748b; text-transform: uppercase; font-weight: 600; }
246
+ .info-value { font-size: 18px; font-weight: 600; margin-top: 4px; }
247
+
248
+ .section {
249
+ background: white;
250
+ border-radius: 12px;
251
+ padding: 24px;
252
+ margin-bottom: 20px;
253
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
254
+ }
255
+ .section-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; }
256
+
257
+ .gate {
258
+ display: flex;
259
+ justify-content: space-between;
260
+ align-items: center;
261
+ padding: 12px 0;
262
+ border-bottom: 1px solid #f1f5f9;
263
+ }
264
+ .gate:last-child { border-bottom: none; }
265
+ .gate-name { font-weight: 500; }
266
+ .gate-meta { font-size: 12px; color: #64748b; }
267
+ .gate-status {
268
+ padding: 4px 12px;
269
+ border-radius: 6px;
270
+ font-size: 12px;
271
+ font-weight: 600;
272
+ }
273
+ .status-pass { background: #dcfce7; color: #166534; }
274
+ .status-fail { background: #fecaca; color: #991b1b; }
275
+
276
+ .signature-box {
277
+ background: #f8fafc;
278
+ border: 1px solid #e2e8f0;
279
+ border-radius: 8px;
280
+ padding: 16px;
281
+ font-family: monospace;
282
+ font-size: 12px;
283
+ word-break: break-all;
284
+ }
285
+
286
+ .footer {
287
+ text-align: center;
288
+ padding: 20px;
289
+ color: #64748b;
290
+ font-size: 12px;
291
+ }
188
292
  </style>
189
293
  </head>
190
294
  <body>
191
- <div class="header">
192
- <div class="logo">QA360 CORE</div>
193
- <div class="title">Preuve de Qualité Vérifiable</div>
194
- <div class="trust-score">${trustScore}%</div>
195
- <div class="trust-label">CONFIANCE ${trustLabel}</div>
196
- </div>
197
-
198
- <div class="pack-info">
199
- <h2>${pack.name}</h2>
200
- <p><strong>Version:</strong> ${pack.version}</p>
201
- <p><strong>Description:</strong> ${pack.description}</p>
202
- <p><strong>Exécuté le:</strong> ${new Date(timestamp).toLocaleString('fr-FR')}</p>
203
- <p><strong>Durée:</strong> ${Math.round(duration / 1000)}s</p>
204
- </div>
205
-
206
- <div class="summary">
207
- <div class="metric">
208
- <div class="metric-value" style="color: #3b82f6;">${summary.total}</div>
209
- <div class="metric-label">Total Tests</div>
210
- </div>
211
- <div class="metric">
212
- <div class="metric-value" style="color: #22c55e;">${summary.passed}</div>
213
- <div class="metric-label">Succès</div>
295
+ <div class="container">
296
+ <div class="header">
297
+ <div class="logo">QA360</div>
298
+ <div class="subtitle">Preuve de Qualité Cryptographique</div>
214
299
  </div>
215
- <div class="metric">
216
- <div class="metric-value" style="color: #ef4444;">${summary.failed}</div>
217
- <div class="metric-label">Échecs</div>
300
+
301
+ <div class="trust-card">
302
+ <div class="trust-score">${trustScore}%</div>
303
+ <div class="trust-label">CONFIANCE ${trustLabel}</div>
218
304
  </div>
219
- <div class="metric">
220
- <div class="metric-value" style="color: #f59e0b;">${summary.skipped}</div>
221
- <div class="metric-label">Ignorés</div>
305
+
306
+ <div class="info-grid">
307
+ <div class="info-card">
308
+ <div class="info-label">Pack</div>
309
+ <div class="info-value">${pack.name}</div>
310
+ </div>
311
+ <div class="info-card">
312
+ <div class="info-label">Version</div>
313
+ <div class="info-value">${pack.version || '1.0.0'}</div>
314
+ </div>
315
+ <div class="info-card">
316
+ <div class="info-label">Durée</div>
317
+ <div class="info-value">${Math.round(execution.duration / 1000)}s</div>
318
+ </div>
319
+ <div class="info-card">
320
+ <div class="info-label">Statut</div>
321
+ <div class="info-value">${execution.success ? '✅ Succès' : '❌ Échec'}</div>
322
+ </div>
222
323
  </div>
223
- </div>
224
-
225
- <div class="tests">
226
- <h3>Détail des Tests</h3>
227
- ${results.map(test => `
228
- <div class="test">
229
- <div>
230
- <div class="test-name">${test.name}</div>
231
- <div style="font-size: 12px; color: #6b7280;">${test.adapter} • ${test.duration}ms</div>
324
+
325
+ <div class="section">
326
+ <div class="section-title">Quality Gates (${passedGates}/${gates.length})</div>
327
+ ${gates.map(gate => `
328
+ <div class="gate">
329
+ <div>
330
+ <div class="gate-name">${gate.gate}</div>
331
+ <div class="gate-meta">${gate.adapter} • ${gate.duration}ms</div>
332
+ </div>
333
+ <div class="gate-status ${gate.success ? 'status-pass' : 'status-fail'}">
334
+ ${gate.success ? 'PASS' : 'FAIL'}
335
+ </div>
232
336
  </div>
233
- <div class="test-status ${test.status}">${this.getStatusLabel(test.status)}</div>
337
+ `).join('')}
338
+ </div>
339
+
340
+ <div class="section">
341
+ <div class="section-title">Signature Cryptographique</div>
342
+ <p style="margin-bottom: 12px; color: #64748b;">
343
+ ${signatureStatus} • ${proof.timestamp}
344
+ </p>
345
+ <div class="signature-box">
346
+ <strong>Algorithm:</strong> ${signature.algorithm}<br>
347
+ <strong>Run ID:</strong> ${proof.runId}<br>
348
+ <strong>Signature:</strong> ${signature.value.substring(0, 64)}...
234
349
  </div>
235
- `).join('')}
236
- </div>
237
-
238
- <div class="qr-placeholder">
239
- QR Code
240
- <br>
241
- (Phase 2)
242
- </div>
243
-
244
- <div class="footer">
245
- <p><strong>Signature:</strong> [Signature Ed25519 - Phase 1 implémentation]</p>
246
- <p><strong>Généré par:</strong> QA360 Core v0.9.0-core</p>
247
- <p><strong>Plateforme:</strong> ${process.platform} ${process.arch}</p>
248
- <p><strong>Hash:</strong> ${this.generateContentHash(runResult)}</p>
350
+ </div>
351
+
352
+ <div class="footer">
353
+ <p>Généré par QA360 v1.0.1 • ${new Date().toLocaleString('fr-FR')}</p>
354
+ <p>Vérifiez cette preuve avec: <code>qa360 verify ${proof.runId}</code></p>
355
+ </div>
249
356
  </div>
250
357
  </body>
251
358
  </html>`;
252
359
  }
253
- getStatusLabel(status) {
254
- switch (status) {
255
- case 'passed': return 'SUCCÈS';
256
- case 'failed': return 'ÉCHEC';
257
- case 'skipped': return 'IGNORÉ';
258
- default: return status.toUpperCase();
259
- }
260
- }
261
- generateContentHash(runResult) {
262
- const content = JSON.stringify(runResult, null, 0);
263
- return createHash('sha256').update(content).digest('hex').substring(0, 16);
264
- }
265
360
  generateReportId() {
266
361
  return 'qa360-' + Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 8);
267
362
  }
268
- getLatestRunDir() {
269
- const runs = readdirSync(this.runsDir).sort().reverse();
270
- return join(this.runsDir, runs[0]);
271
- }
272
- async signReports(reportPaths, runResult, trustScore) {
273
- const spinner = ora('Signature cryptographique...').start();
274
- try {
275
- // Ensure keys exist
276
- await this.ensureKeys();
277
- // Read private key
278
- const privateKey = readFileSync(this.privateKeyPath, 'utf8');
279
- // Create signature payload
280
- const payload = {
281
- reports: reportPaths.map(path => ({
282
- path: path.split('/').pop(),
283
- hash: this.hashFile(path)
284
- })),
285
- trustScore,
286
- timestamp: new Date().toISOString(),
287
- pack: runResult.pack.name
288
- };
289
- const payloadString = JSON.stringify(payload, null, 0);
290
- const signature = sign('sha256', Buffer.from(payloadString), privateKey).toString('base64');
291
- // Save signature
292
- const signaturePath = join(this.getLatestRunDir(), 'signature.json');
293
- writeFileSync(signaturePath, JSON.stringify({
294
- payload,
295
- signature,
296
- algorithm: 'Ed25519',
297
- qa360Version: '0.9.0-core'
298
- }, null, 2));
299
- spinner.succeed(`Signature générée: ${signaturePath}`);
300
- console.log(chalk.green(`🔐 Preuve signée cryptographiquement`));
301
- }
302
- catch (error) {
303
- spinner.fail('Échec signature');
304
- throw error;
305
- }
306
- }
307
- async ensureKeys() {
308
- if (existsSync(this.privateKeyPath) && existsSync(this.publicKeyPath)) {
309
- return;
310
- }
311
- console.log(chalk.yellow('🔑 Génération de la paire de clés Ed25519...'));
312
- return new Promise((resolve, reject) => {
313
- generateKeyPair('ed25519', {
314
- publicKeyEncoding: { type: 'spki', format: 'pem' },
315
- privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
316
- }, (err, publicKey, privateKey) => {
317
- if (err) {
318
- reject(err);
319
- return;
320
- }
321
- writeFileSync(this.publicKeyPath, publicKey);
322
- writeFileSync(this.privateKeyPath, privateKey);
323
- console.log(chalk.green('✅ Clés générées'));
324
- resolve();
325
- });
326
- });
327
- }
328
- hashFile(filePath) {
329
- const content = readFileSync(filePath);
330
- return createHash('sha256').update(content).digest('hex');
331
- }
332
363
  }
333
364
  export async function reportCommand(options = {}) {
334
365
  const reporter = new QA360Reporter();
@@ -338,10 +369,7 @@ export async function reportCommand(options = {}) {
338
369
  outputs.forEach(path => {
339
370
  console.log(chalk.blue(` 📄 ${path}`));
340
371
  });
341
- if (options.sign) {
342
- console.log(chalk.green('\n🔐 Preuve signée et vérifiable'));
343
- console.log(chalk.blue('💡 Utilisez "qa360 verify" pour vérifier la signature'));
344
- }
372
+ console.log(chalk.gray('\n💡 Vérifiez la preuve avec: qa360 verify'));
345
373
  }
346
374
  catch (error) {
347
375
  console.error(chalk.red('❌ Échec génération rapport:'), error);