@arcadialdev/arcality 4.1.0 → 4.1.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.
Files changed (36) hide show
  1. package/.agents/skills/db-validation-evidence/SKILL.md +43 -0
  2. package/.agents/skills/db-validation-evidence/agents/openai.yaml +4 -0
  3. package/.agents/skills/db-validation-evidence/references/mission-patterns.md +130 -0
  4. package/.agents/skills/evidence-synthesis-reporting/SKILL.md +31 -0
  5. package/.agents/skills/form-expert/SKILL.md +98 -0
  6. package/.agents/skills/investigation-protocol/SKILL.md +56 -0
  7. package/.agents/skills/mission-generation-reviewer/SKILL.md +25 -0
  8. package/.agents/skills/modal-master/SKILL.md +46 -0
  9. package/.agents/skills/native-control-expert/SKILL.md +74 -0
  10. package/.agents/skills/qa-context-governance/SKILL.md +23 -0
  11. package/.agents/skills/security-evidence-triage/SKILL.md +23 -0
  12. package/.agents/skills/test-data-lifecycle/SKILL.md +24 -0
  13. package/README.md +103 -163
  14. package/bin/arcality.mjs +25 -25
  15. package/package.json +75 -75
  16. package/scripts/edit-config.mjs +843 -0
  17. package/scripts/gen-and-run.mjs +237 -169
  18. package/scripts/generate.mjs +236 -236
  19. package/scripts/init.mjs +47 -47
  20. package/src/configManager.mjs +13 -13
  21. package/src/envSetup.ts +229 -205
  22. package/src/services/codebaseAnalyzer.mjs +59 -59
  23. package/src/services/databaseValidationService.mjs +598 -0
  24. package/src/services/executionEvidenceService.mjs +124 -0
  25. package/src/services/generatedMissionSchema.mjs +76 -76
  26. package/src/services/generatedMissionStore.mjs +117 -117
  27. package/src/services/generationContext.mjs +242 -242
  28. package/src/services/mcpStdioClient.mjs +204 -0
  29. package/src/services/missionGenerator.mjs +329 -329
  30. package/src/services/routeDiscovery.mjs +762 -762
  31. package/tests/_helpers/ArcalityReporter.js +1342 -1255
  32. package/tests/_helpers/agentic-runner.bundle.spec.js +1354 -243
  33. package/.agent/skills/form-expert.md +0 -102
  34. package/.agent/skills/investigation-protocol.md +0 -61
  35. package/.agent/skills/modal-master.md +0 -41
  36. package/.agent/skills/native-control-expert.md +0 -82
@@ -1,1255 +1,1342 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const fs_1 = __importDefault(require("fs"));
7
- const path_1 = __importDefault(require("path"));
8
- class ArcalityReporter {
9
- constructor(options = {}) {
10
- this.results = [];
11
- this.totalDuration = 0;
12
- this.startTime = new Date();
13
- this.outputDir = options.outputDir || 'arcality-report';
14
- }
15
- onBegin(config, suite) {
16
- // Clean output directory to avoid stale reports/attachments.
17
- if (fs_1.default.existsSync(this.outputDir)) {
18
- try {
19
- fs_1.default.rmSync(this.outputDir, { recursive: true, force: true });
20
- console.log(`Cleaned old report data at: ${this.outputDir}`);
21
- }
22
- catch (err) {
23
- console.warn(`Could not fully clean report dir: ${err.message}`);
24
- }
25
- }
26
- if (!fs_1.default.existsSync(this.outputDir)) {
27
- fs_1.default.mkdirSync(this.outputDir, { recursive: true });
28
- }
29
- this.startTime = new Date();
30
- console.log(`Arcality Mission started at ${this.startTime.toLocaleTimeString()}`);
31
- }
32
- onTestEnd(test, result) {
33
- const stepsHtml = result.steps.map(step => this.renderStep(step, 0)).join('');
34
- const processedAttachments = result.attachments.map(att => {
35
- let extension = '.png';
36
- if (att.contentType === 'application/json')
37
- extension = '.json';
38
- else if (att.contentType === 'text/plain')
39
- extension = '.txt';
40
- else if (att.contentType === 'text/yaml')
41
- extension = '.yaml';
42
- const fileName = att.path ? path_1.default.basename(att.path) : `${att.name || 'attachment'}-${Date.now()}${extension}`;
43
- const attachmentsDir = path_1.default.join(this.outputDir, 'attachments');
44
- const destPath = path_1.default.join(attachmentsDir, fileName);
45
- if (!fs_1.default.existsSync(attachmentsDir)) {
46
- fs_1.default.mkdirSync(attachmentsDir, { recursive: true });
47
- }
48
- try {
49
- let copied = false;
50
- if (att.path && fs_1.default.existsSync(att.path)) {
51
- fs_1.default.copyFileSync(att.path, destPath);
52
- copied = true;
53
- }
54
- else if (att.body) {
55
- fs_1.default.writeFileSync(destPath, att.body);
56
- copied = true;
57
- }
58
- return copied ? { ...att, path: `attachments/${fileName}` } : att;
59
- }
60
- catch (e) {
61
- console.error(`Failed to process attachment ${att.name}:`, e);
62
- return att;
63
- }
64
- });
65
- if (result.status === 'failed' || result.error) {
66
- const isAppBug = result.error?.message?.includes('APPLICATION BUG');
67
- const prefix = isAppBug ? '[APPLICATION BUG FOUND]' : '[TEST ENGINE FAILURE]';
68
- console.log(`\n${prefix} ${test.title} failed:`);
69
- console.log(`${result.error?.message || result.error?.stack || 'Unknown error'}\n`);
70
- if (result.error?.stack && !isAppBug) {
71
- console.log(result.error.stack);
72
- }
73
- }
74
- this.results.push({ test, result, stepsHtml, attachments: processedAttachments });
75
- }
76
- async onEnd(result) {
77
- this.totalDuration = result.duration;
78
- const htmlContent = this.generateHtml();
79
- const reportPath = path_1.default.join(this.outputDir, 'index.html');
80
- fs_1.default.writeFileSync(reportPath, htmlContent);
81
- console.log(`Arcality Mission Report generated at: ${reportPath}`);
82
- }
83
- renderStep(step, depth) {
84
- if (step.category === 'hook' || step.category === 'fixture')
85
- return '';
86
- const statusClass = step.error ? 'step-error' : 'step-passed';
87
- const duration = step.duration >= 0 ? `<span class="step-duration">${this.formatDuration(step.duration)}</span>` : '';
88
- let title = this.escapeHtml(step.title);
89
- title = title.replace(/^(click|fill|navigate|wait|expect|goto|press)/i, '<span class="step-action">$1</span>');
90
- const children = step.steps && step.steps.length > 0
91
- ? `<div class="step-children">${step.steps.map(s => this.renderStep(s, depth + 1)).join('')}</div>`
92
- : '';
93
- const expandableAttr = step.steps && step.steps.length > 0 ? 'data-expandable="true"' : '';
94
- return `
95
- <div class="step-wrapper ${statusClass}" style="--step-depth: ${Math.min(depth, 8)}" ${expandableAttr}>
96
- <div class="step-header">
97
- <span class="step-marker" aria-hidden="true"></span>
98
- <span class="step-title">${title}</span>
99
- ${duration}
100
- </div>
101
- ${children}
102
- </div>
103
- `;
104
- }
105
- formatDuration(milliseconds) {
106
- if (milliseconds >= 1000) {
107
- const seconds = milliseconds / 1000;
108
- return `${seconds >= 10 ? seconds.toFixed(0) : seconds.toFixed(1)}s`;
109
- }
110
- return `${Math.max(0, Math.round(milliseconds))}ms`;
111
- }
112
- getStatusLabel(status) {
113
- switch (status) {
114
- case 'passed': return 'Exitosa';
115
- case 'failed': return 'Error';
116
- case 'timedOut': return 'Tiempo agotado';
117
- case 'skipped': return 'Omitida';
118
- case 'interrupted': return 'Interrumpida';
119
- default: return status;
120
- }
121
- }
122
- getStatusTone(status) {
123
- if (status === 'failed' || status === 'timedOut' || status === 'interrupted')
124
- return 'failed';
125
- if (status === 'passed')
126
- return 'passed';
127
- return 'neutral';
128
- }
129
- getEngineVersion() {
130
- try {
131
- const packageJsonPath = path_1.default.join(process.cwd(), 'package.json');
132
- if (fs_1.default.existsSync(packageJsonPath)) {
133
- const pkg = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf8'));
134
- return pkg.version || '2.2.0';
135
- }
136
- }
137
- catch (e) {
138
- console.error('Error reading package.json version:', e);
139
- }
140
- return '2.2.0';
141
- }
142
- getBrandLogoDataUri() {
143
- const roots = [
144
- process.env.ARCALITY_ROOT,
145
- path_1.default.resolve(__dirname, '..', '..'),
146
- process.cwd()
147
- ].filter(Boolean);
148
- const logoNames = ['logo.png', 'logo-arcadial-blanco.png'];
149
- for (const root of roots) {
150
- for (const logoName of logoNames) {
151
- const logoPath = path_1.default.join(root, 'public', logoName);
152
- if (!fs_1.default.existsSync(logoPath))
153
- continue;
154
- try {
155
- const logoBuffer = fs_1.default.readFileSync(logoPath);
156
- return `data:image/png;base64,${logoBuffer.toString('base64')}`;
157
- }
158
- catch (e) {
159
- console.warn(`Could not load Arcality logo from ${logoPath}`);
160
- }
161
- }
162
- }
163
- return null;
164
- }
165
- generateHtml() {
166
- const stats = {
167
- total: this.results.length,
168
- passed: this.results.filter(r => r.result.status === 'passed').length,
169
- failed: this.results.filter(r => r.result.status === 'failed' || r.result.status === 'timedOut').length,
170
- skipped: this.results.filter(r => r.result.status === 'skipped').length,
171
- flaky: this.results.filter(r => r.result.status === 'passed' && r.result.retry > 0).length
172
- };
173
- const evidenceCount = this.results.reduce((count, r) => {
174
- return count + r.attachments.filter(att => att.path && (att.contentType.startsWith('image/') || att.contentType.startsWith('video/'))).length;
175
- }, 0);
176
- const reportTone = stats.failed > 0 ? 'failed' : stats.total === 0 ? 'neutral' : 'passed';
177
- const reportStatus = stats.failed > 0 ? 'Ejecucion con errores' : stats.total === 0 ? 'Sin ejecuciones' : 'Ejecucion exitosa';
178
- const reportSummary = stats.failed > 0
179
- ? `${stats.failed} mision${stats.failed === 1 ? '' : 'es'} fallida${stats.failed === 1 ? '' : 's'}. Revisa el diagnostico y la evidencia visual.`
180
- : stats.total === 0
181
- ? 'No se registraron misiones en esta ejecucion.'
182
- : 'Todas las misiones ejecutadas finalizaron correctamente.';
183
- const generatedAt = new Date();
184
- const reportTitle = stats.total > 1 ? 'Reporte de coleccion' : 'Reporte de ejecucion';
185
- const brandLogo = this.getBrandLogoDataUri();
186
- return `
187
- <!DOCTYPE html>
188
- <html lang="es">
189
- <head>
190
- <meta charset="UTF-8">
191
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
192
- <title>Arcality Mission Report</title>
193
- <link rel="shortcut icon" href="https://raw.githubusercontent.com/microsoft/playwright/main/packages/playwright-core/src/server/chromium/test-results/favicon.ico">
194
- <style>
195
- @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
196
-
197
- :root {
198
- --bg: #07080d;
199
- --surface: #10131b;
200
- --surface-soft: #151925;
201
- --line: #293042;
202
- --line-soft: rgba(255,255,255,0.08);
203
- --text: #f7f4ff;
204
- --muted: #a9afc0;
205
- --faint: #72798c;
206
- --brand: #b56cff;
207
- --brand-soft: rgba(181, 108, 255, 0.16);
208
- --blue: #6bbcff;
209
- --success: #39d99f;
210
- --success-soft: rgba(57, 217, 159, 0.12);
211
- --error: #ff5f7a;
212
- --error-soft: rgba(255, 95, 122, 0.12);
213
- --warning: #ffc857;
214
- --warning-soft: rgba(255, 200, 87, 0.12);
215
- --shadow: 0 18px 60px rgba(0,0,0,0.28);
216
- }
217
-
218
- * { box-sizing: border-box; }
219
- html { background: var(--bg); }
220
- body {
221
- font-family: 'Plus Jakarta Sans', sans-serif;
222
- background: linear-gradient(180deg, #080910 0%, #10121a 44%, #07080d 100%);
223
- color: var(--text);
224
- margin: 0;
225
- min-height: 100vh;
226
- line-height: 1.55;
227
- }
228
- body::before {
229
- content: "";
230
- position: fixed;
231
- inset: 0;
232
- pointer-events: none;
233
- background-image:
234
- linear-gradient(rgba(255,255,255,0.025) 1px, transparent 1px),
235
- linear-gradient(90deg, rgba(255,255,255,0.025) 1px, transparent 1px);
236
- background-size: 52px 52px;
237
- mask-image: linear-gradient(to bottom, rgba(0,0,0,0.75), transparent 68%);
238
- }
239
-
240
- button, a { font: inherit; }
241
- a { color: inherit; }
242
- .container {
243
- position: relative;
244
- max-width: 1480px;
245
- margin: 0 auto;
246
- padding: 32px;
247
- }
248
-
249
- .mission-brief {
250
- display: grid;
251
- grid-template-columns: minmax(0, 1fr) auto;
252
- gap: 28px;
253
- align-items: start;
254
- padding: 6px 0 28px;
255
- border-bottom: 1px solid var(--line);
256
- margin-bottom: 22px;
257
- }
258
- .brand-row {
259
- display: flex;
260
- align-items: center;
261
- gap: 14px;
262
- margin-bottom: 26px;
263
- }
264
- .brand-mark {
265
- width: 38px;
266
- height: 38px;
267
- border: 1px solid rgba(181,108,255,0.45);
268
- border-radius: 8px;
269
- background: linear-gradient(135deg, rgba(181,108,255,0.92), rgba(107,188,255,0.62)), #141720;
270
- box-shadow: 0 0 28px rgba(181,108,255,0.22);
271
- }
272
- .brand-mark.has-logo {
273
- width: 42px;
274
- height: 42px;
275
- padding: 5px;
276
- background: rgba(255,255,255,0.035);
277
- box-shadow: 0 0 24px rgba(181,108,255,0.12);
278
- }
279
- .brand-mark img {
280
- display: block;
281
- width: 100%;
282
- height: 100%;
283
- object-fit: contain;
284
- filter: drop-shadow(0 0 10px rgba(181,108,255,0.28));
285
- }
286
- .brand-name {
287
- font-size: 0.74rem;
288
- color: var(--muted);
289
- letter-spacing: 0.16em;
290
- text-transform: uppercase;
291
- font-weight: 800;
292
- }
293
- .brand-subtitle {
294
- color: var(--faint);
295
- font-size: 0.86rem;
296
- margin-top: 1px;
297
- }
298
- .mission-title {
299
- font-size: clamp(2rem, 4vw, 4.6rem);
300
- line-height: 0.96;
301
- letter-spacing: 0;
302
- max-width: 900px;
303
- margin: 0;
304
- font-weight: 800;
305
- }
306
- .mission-copy {
307
- margin: 18px 0 0;
308
- max-width: 720px;
309
- color: var(--muted);
310
- font-size: 1rem;
311
- }
312
- .status-card {
313
- width: min(380px, 100%);
314
- border: 1px solid var(--line);
315
- background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.018));
316
- border-radius: 8px;
317
- padding: 18px;
318
- box-shadow: var(--shadow);
319
- }
320
- .status-card.passed { border-color: rgba(57,217,159,0.42); }
321
- .status-card.failed { border-color: rgba(255,95,122,0.48); }
322
- .status-card.neutral { border-color: rgba(169,175,192,0.28); }
323
- .status-eyebrow {
324
- display: flex;
325
- align-items: center;
326
- justify-content: space-between;
327
- gap: 16px;
328
- color: var(--faint);
329
- font-size: 0.72rem;
330
- font-family: 'JetBrains Mono', monospace;
331
- letter-spacing: 0.08em;
332
- text-transform: uppercase;
333
- }
334
- .status-pill {
335
- display: inline-flex;
336
- align-items: center;
337
- gap: 7px;
338
- border: 1px solid var(--line-soft);
339
- border-radius: 999px;
340
- padding: 5px 9px;
341
- color: var(--text);
342
- background: rgba(255,255,255,0.04);
343
- }
344
- .status-pill::before {
345
- content: "";
346
- width: 7px;
347
- height: 7px;
348
- border-radius: 50%;
349
- background: var(--faint);
350
- }
351
- .status-card.passed .status-pill::before { background: var(--success); }
352
- .status-card.failed .status-pill::before { background: var(--error); }
353
- .status-title {
354
- font-size: 1.7rem;
355
- line-height: 1.05;
356
- margin: 24px 0 10px;
357
- font-weight: 800;
358
- }
359
- .status-summary {
360
- color: var(--muted);
361
- margin: 0;
362
- font-size: 0.95rem;
363
- }
364
- .brief-grid {
365
- display: grid;
366
- grid-template-columns: repeat(6, minmax(120px, 1fr));
367
- gap: 1px;
368
- background: var(--line);
369
- border: 1px solid var(--line);
370
- border-radius: 8px;
371
- overflow: hidden;
372
- margin-bottom: 18px;
373
- }
374
- .brief-metric {
375
- background: rgba(16,19,27,0.94);
376
- padding: 16px 18px;
377
- min-height: 92px;
378
- }
379
- .metric-value {
380
- display: block;
381
- font-size: 1.7rem;
382
- line-height: 1;
383
- font-weight: 800;
384
- }
385
- .metric-label {
386
- display: block;
387
- margin-top: 10px;
388
- color: var(--faint);
389
- font-size: 0.72rem;
390
- letter-spacing: 0.1em;
391
- text-transform: uppercase;
392
- font-weight: 800;
393
- }
394
- .val-passed { color: var(--success); }
395
- .val-failed { color: var(--error); }
396
- .val-flaky { color: var(--warning); }
397
- .val-blue { color: var(--blue); }
398
-
399
- .test-entry {
400
- border-top: 1px solid var(--line);
401
- }
402
- .test-entry:last-child {
403
- border-bottom: 1px solid var(--line);
404
- }
405
- .test-header {
406
- width: 100%;
407
- appearance: none;
408
- border: 0;
409
- color: var(--text);
410
- background: transparent;
411
- padding: 22px 0;
412
- display: flex;
413
- justify-content: space-between;
414
- align-items: center;
415
- gap: 24px;
416
- text-align: left;
417
- cursor: pointer;
418
- }
419
- .test-header:hover .test-name { color: var(--brand); }
420
- .test-info {
421
- min-width: 0;
422
- display: flex;
423
- align-items: center;
424
- gap: 14px;
425
- }
426
- .status-dot {
427
- width: 11px;
428
- height: 44px;
429
- border-radius: 999px;
430
- background: var(--faint);
431
- flex: 0 0 auto;
432
- }
433
- .status-passed .status-dot { background: var(--success); }
434
- .status-failed .status-dot { background: var(--error); }
435
- .status-neutral .status-dot { background: var(--faint); }
436
- .test-name {
437
- font-weight: 800;
438
- font-size: 1.08rem;
439
- line-height: 1.25;
440
- transition: color 0.16s ease;
441
- }
442
- .test-meta {
443
- display: flex;
444
- flex-wrap: wrap;
445
- gap: 8px;
446
- margin-top: 8px;
447
- color: var(--muted);
448
- font-size: 0.78rem;
449
- font-family: 'JetBrains Mono', monospace;
450
- }
451
- .test-chevron {
452
- width: 32px;
453
- height: 32px;
454
- border: 1px solid var(--line);
455
- border-radius: 8px;
456
- display: inline-grid;
457
- place-items: center;
458
- color: var(--muted);
459
- flex: 0 0 auto;
460
- transition: transform 0.16s ease, border-color 0.16s ease;
461
- }
462
- .test-header[aria-expanded="true"] .test-chevron {
463
- transform: rotate(180deg);
464
- border-color: rgba(181,108,255,0.48);
465
- color: var(--text);
466
- }
467
- .test-details {
468
- padding: 0 0 28px;
469
- }
470
- .test-details-grid {
471
- display: grid;
472
- grid-template-columns: minmax(0, 1.5fr) minmax(320px, 0.8fr);
473
- gap: 24px;
474
- align-items: start;
475
- }
476
- .primary-column,
477
- .context-column {
478
- min-width: 0;
479
- }
480
- .context-column {
481
- display: flex;
482
- flex-direction: column;
483
- gap: 14px;
484
- }
485
- .outcome-block,
486
- .side-panel,
487
- .timeline-wrap {
488
- border: 1px solid var(--line);
489
- border-radius: 8px;
490
- background: rgba(16,19,27,0.78);
491
- }
492
- .outcome-block {
493
- padding: 18px;
494
- margin-bottom: 18px;
495
- }
496
- .outcome-block.success {
497
- border-color: rgba(57,217,159,0.36);
498
- background: var(--success-soft);
499
- }
500
- .outcome-block.error {
501
- border-color: rgba(255,95,122,0.4);
502
- background: var(--error-soft);
503
- }
504
- .outcome-label {
505
- display: block;
506
- color: var(--faint);
507
- font-size: 0.72rem;
508
- letter-spacing: 0.12em;
509
- text-transform: uppercase;
510
- font-weight: 800;
511
- margin-bottom: 8px;
512
- }
513
- .success-block {
514
- margin: 0;
515
- color: #bff7df;
516
- font-size: 1rem;
517
- font-weight: 700;
518
- }
519
- .error-block {
520
- font-family: 'JetBrains Mono', monospace;
521
- font-size: 0.86rem;
522
- color: #ffd0d8;
523
- white-space: pre-wrap;
524
- margin: 0;
525
- }
526
- .panel-header {
527
- display: flex;
528
- justify-content: space-between;
529
- align-items: center;
530
- gap: 12px;
531
- padding: 14px 16px;
532
- border-bottom: 1px solid var(--line-soft);
533
- }
534
- .panel-title {
535
- color: var(--text);
536
- font-size: 0.8rem;
537
- letter-spacing: 0.1em;
538
- text-transform: uppercase;
539
- font-weight: 800;
540
- }
541
- .panel-kicker {
542
- color: var(--faint);
543
- font-size: 0.72rem;
544
- font-family: 'JetBrains Mono', monospace;
545
- text-transform: uppercase;
546
- }
547
- .panel-body {
548
- padding: 16px;
549
- }
550
- .info-block {
551
- font-family: 'JetBrains Mono', monospace;
552
- font-size: 0.8rem;
553
- color: #d7dcff;
554
- white-space: pre-wrap;
555
- margin: 0;
556
- }
557
- .metadata-list {
558
- display: grid;
559
- gap: 12px;
560
- margin: 0;
561
- }
562
- .metadata-row {
563
- display: flex;
564
- justify-content: space-between;
565
- gap: 16px;
566
- border-bottom: 1px solid var(--line-soft);
567
- padding-bottom: 10px;
568
- }
569
- .metadata-row:last-child {
570
- border-bottom: 0;
571
- padding-bottom: 0;
572
- }
573
- .metadata-row dt {
574
- color: var(--faint);
575
- margin: 0;
576
- font-size: 0.74rem;
577
- text-transform: uppercase;
578
- letter-spacing: 0.08em;
579
- font-weight: 800;
580
- }
581
- .metadata-row dd {
582
- margin: 0;
583
- color: var(--text);
584
- font-family: 'JetBrains Mono', monospace;
585
- font-size: 0.8rem;
586
- text-align: right;
587
- }
588
-
589
- .steps-section { margin-top: 0; }
590
- .section-title {
591
- display: block;
592
- padding: 15px 18px;
593
- border-bottom: 1px solid var(--line-soft);
594
- color: var(--muted);
595
- font-size: 0.76rem;
596
- text-transform: uppercase;
597
- letter-spacing: 0.12em;
598
- font-weight: 800;
599
- }
600
- .timeline-wrap {
601
- overflow: hidden;
602
- }
603
- .steps-container {
604
- padding: 12px 0;
605
- }
606
- .step-wrapper {
607
- margin-left: calc(var(--step-depth) * 18px);
608
- padding: 7px 18px 7px 20px;
609
- position: relative;
610
- }
611
- .step-wrapper::before {
612
- content: "";
613
- position: absolute;
614
- left: 5px;
615
- top: 0;
616
- bottom: 0;
617
- width: 1px;
618
- background: var(--line-soft);
619
- }
620
- .step-header {
621
- display: grid;
622
- grid-template-columns: 14px minmax(0, 1fr) auto;
623
- align-items: start;
624
- gap: 10px;
625
- font-size: 0.9rem;
626
- }
627
- .step-marker {
628
- width: 8px;
629
- height: 8px;
630
- border-radius: 50%;
631
- background: var(--success);
632
- margin-top: 7px;
633
- box-shadow: 0 0 0 4px rgba(57,217,159,0.08);
634
- }
635
- .step-title {
636
- color: #e6e9f5;
637
- min-width: 0;
638
- overflow-wrap: anywhere;
639
- }
640
- .step-action {
641
- color: var(--blue);
642
- font-weight: 800;
643
- }
644
- .step-duration {
645
- color: var(--faint);
646
- font-size: 0.72rem;
647
- font-family: 'JetBrains Mono', monospace;
648
- white-space: nowrap;
649
- padding-top: 2px;
650
- }
651
- .step-error .step-title { color: #ffd0d8; }
652
- .step-error .step-marker {
653
- background: var(--error);
654
- box-shadow: 0 0 0 4px rgba(255,95,122,0.09);
655
- }
656
- .step-children {
657
- margin-top: 2px;
658
- }
659
-
660
- .evidence-hero {
661
- position: relative;
662
- overflow: hidden;
663
- background: #080910;
664
- }
665
- .evidence-hero img,
666
- .evidence-hero video {
667
- width: 100%;
668
- max-height: 320px;
669
- object-fit: contain;
670
- display: block;
671
- cursor: zoom-in;
672
- }
673
- .evidence-link,
674
- .evidence-thumb {
675
- text-decoration: none;
676
- }
677
- .evidence-link {
678
- display: flex;
679
- justify-content: space-between;
680
- gap: 12px;
681
- padding: 11px 13px;
682
- color: var(--muted);
683
- border-top: 1px solid var(--line-soft);
684
- font-size: 0.78rem;
685
- font-family: 'JetBrains Mono', monospace;
686
- }
687
- .evidence-link:hover,
688
- .evidence-thumb:hover {
689
- color: var(--text);
690
- }
691
- .evidence-thumbs {
692
- display: grid;
693
- grid-template-columns: repeat(2, minmax(0, 1fr));
694
- gap: 10px;
695
- padding: 12px;
696
- border-top: 1px solid var(--line-soft);
697
- }
698
- .evidence-thumb {
699
- border: 1px solid var(--line-soft);
700
- border-radius: 8px;
701
- overflow: hidden;
702
- color: var(--muted);
703
- background: rgba(255,255,255,0.03);
704
- font-size: 0.72rem;
705
- }
706
- .evidence-thumb img,
707
- .evidence-thumb video {
708
- width: 100%;
709
- aspect-ratio: 16 / 9;
710
- object-fit: cover;
711
- display: block;
712
- background: #07080d;
713
- }
714
- .thumb-name {
715
- display: block;
716
- padding: 8px;
717
- white-space: nowrap;
718
- overflow: hidden;
719
- text-overflow: ellipsis;
720
- }
721
-
722
- footer {
723
- margin-top: 34px;
724
- text-align: center;
725
- padding: 22px 0 6px;
726
- font-size: 0.76rem;
727
- color: var(--faint);
728
- font-family: 'JetBrains Mono', monospace;
729
- }
730
-
731
- .tag {
732
- display: inline-flex;
733
- align-items: center;
734
- min-height: 22px;
735
- padding: 3px 8px;
736
- border-radius: 999px;
737
- border: 1px solid var(--line-soft);
738
- background: rgba(255,255,255,0.035);
739
- color: var(--muted);
740
- font-size: 0.68rem;
741
- font-weight: 800;
742
- text-transform: uppercase;
743
- letter-spacing: 0.06em;
744
- }
745
- .tag-project {
746
- color: #e2c7ff;
747
- background: var(--brand-soft);
748
- border-color: rgba(181,108,255,0.26);
749
- }
750
- .tag-bug {
751
- color: #b7e4ff;
752
- background: rgba(80,170,255,0.12);
753
- border-color: rgba(80,170,255,0.3);
754
- }
755
- .tag-retry {
756
- color: #ffe2a6;
757
- background: var(--warning-soft);
758
- border-color: rgba(255,200,87,0.28);
759
- }
760
- .tag-status-passed {
761
- color: #bff7df;
762
- background: var(--success-soft);
763
- border-color: rgba(57,217,159,0.28);
764
- }
765
- .tag-status-failed {
766
- color: #ffd0d8;
767
- background: var(--error-soft);
768
- border-color: rgba(255,95,122,0.28);
769
- }
770
- .tag-status-neutral {
771
- color: var(--muted);
772
- }
773
-
774
- .security-section {
775
- overflow: hidden;
776
- }
777
- .security-header {
778
- width: 100%;
779
- border: 0;
780
- background: transparent;
781
- color: var(--text);
782
- cursor: pointer;
783
- }
784
- .security-risk-badge {
785
- padding: 5px 9px;
786
- border-radius: 999px;
787
- font-size: 0.72rem;
788
- font-weight: 800;
789
- color: #07080d;
790
- }
791
- .security-summary {
792
- display: grid;
793
- grid-template-columns: repeat(4, 1fr);
794
- gap: 1px;
795
- background: var(--line-soft);
796
- text-align: center;
797
- border-bottom: 1px solid var(--line-soft);
798
- }
799
- .summary-item {
800
- background: rgba(7,8,13,0.48);
801
- padding: 12px 8px;
802
- }
803
- .summary-item .count {
804
- font-size: 1.2rem;
805
- font-weight: 800;
806
- }
807
- .summary-item .label {
808
- font-size: 0.66rem;
809
- text-transform: uppercase;
810
- color: var(--faint);
811
- letter-spacing: 0.08em;
812
- }
813
- .count-Critical { color: var(--error); }
814
- .count-High { color: #ff9d66; }
815
- .count-Medium { color: var(--warning); }
816
- .count-Low { color: var(--blue); }
817
-
818
- .vuln-list {
819
- display: flex;
820
- flex-direction: column;
821
- gap: 12px;
822
- padding: 14px;
823
- }
824
- .vuln-card {
825
- background: rgba(7,8,13,0.52);
826
- border: 1px solid var(--line-soft);
827
- border-left-width: 4px;
828
- border-radius: 8px;
829
- padding: 14px;
830
- }
831
- .vuln-card-title {
832
- font-weight: 800;
833
- margin-bottom: 8px;
834
- display: flex;
835
- align-items: center;
836
- gap: 10px;
837
- flex-wrap: wrap;
838
- }
839
- .vuln-card-desc {
840
- color: var(--muted);
841
- font-size: 0.88rem;
842
- margin: 10px 0;
843
- }
844
- .vuln-card-evidence {
845
- background: #07080d;
846
- padding: 10px;
847
- border-radius: 4px;
848
- font-family: 'JetBrains Mono', monospace;
849
- font-size: 0.74rem;
850
- white-space: pre-wrap;
851
- word-break: break-all;
852
- margin: 10px 0;
853
- border: 1px solid var(--line-soft);
854
- max-height: 200px;
855
- overflow-y: auto;
856
- }
857
- .vuln-card-remediation {
858
- font-size: 0.88rem;
859
- color: #bff7df;
860
- }
861
- pre { margin: 0; }
862
-
863
- @media (max-width: 1040px) {
864
- .mission-brief,
865
- .test-details-grid {
866
- grid-template-columns: 1fr;
867
- }
868
- .status-card {
869
- width: 100%;
870
- }
871
- .brief-grid {
872
- grid-template-columns: repeat(3, minmax(0, 1fr));
873
- }
874
- }
875
- @media (max-width: 680px) {
876
- .container {
877
- padding: 20px;
878
- }
879
- .mission-title {
880
- font-size: 2.3rem;
881
- }
882
- .brief-grid {
883
- grid-template-columns: repeat(2, minmax(0, 1fr));
884
- }
885
- .test-header {
886
- align-items: flex-start;
887
- }
888
- .test-details-grid {
889
- gap: 16px;
890
- }
891
- .step-header {
892
- grid-template-columns: 14px minmax(0, 1fr);
893
- }
894
- .step-duration {
895
- grid-column: 2;
896
- justify-self: start;
897
- padding-top: 0;
898
- }
899
- .evidence-thumbs,
900
- .security-summary {
901
- grid-template-columns: 1fr 1fr;
902
- }
903
- }
904
- </style>
905
- </head>
906
- <body>
907
- <div class="container">
908
- <header class="mission-brief">
909
- <div>
910
- <div class="brand-row">
911
- <span class="brand-mark ${brandLogo ? 'has-logo' : ''}" aria-hidden="true">
912
- ${brandLogo ? `<img src="${brandLogo}" alt="">` : ''}
913
- </span>
914
- <div>
915
- <div class="brand-name">Arcality QA Engine</div>
916
- <div class="brand-subtitle">Consola de diagnostico autonomo</div>
917
- </div>
918
- </div>
919
- <h1 class="mission-title">${reportTitle}</h1>
920
- <p class="mission-copy">${reportSummary}</p>
921
- </div>
922
- <div class="status-card ${reportTone}">
923
- <div class="status-eyebrow">
924
- <span>Estado general</span>
925
- <span class="status-pill">${reportStatus}</span>
926
- </div>
927
- <div class="status-title">${reportStatus}</div>
928
- <p class="status-summary">Inicio ${this.startTime.toLocaleString()} | Engine ${this.getEngineVersion()}</p>
929
- </div>
930
- </header>
931
-
932
- <div class="brief-grid" aria-label="Resumen de ejecucion">
933
- <div class="brief-metric"><span class="metric-value">${stats.total}</span><span class="metric-label">Misiones</span></div>
934
- <div class="brief-metric"><span class="metric-value val-passed">${stats.passed}</span><span class="metric-label">Exitosas</span></div>
935
- <div class="brief-metric"><span class="metric-value val-failed">${stats.failed}</span><span class="metric-label">Errores</span></div>
936
- <div class="brief-metric"><span class="metric-value val-flaky">${stats.flaky}</span><span class="metric-label">Reintentos</span></div>
937
- <div class="brief-metric"><span class="metric-value val-blue">${evidenceCount}</span><span class="metric-label">Evidencias</span></div>
938
- <div class="brief-metric"><span class="metric-value">${this.formatDuration(this.totalDuration)}</span><span class="metric-label">Duracion</span></div>
939
- </div>
940
-
941
- <div class="test-list">
942
- ${this.results.map((r, i) => {
943
- const tone = this.getStatusTone(r.result.status);
944
- const statusClass = `status-${tone}`;
945
- const projectName = r.test.parent.project()?.name || 'default';
946
- const isOpen = this.results.length === 1 || tone === 'failed' || i === 0;
947
- const successSummary = r.attachments.find(a => a.name === 'success_summary');
948
- const successText = this.getAttachmentText(successSummary);
949
- const bugClassificationAtt = r.attachments.find(a => a.name === 'bug_classification');
950
- const bugClassification = (this.getAttachmentText(bugClassificationAtt) || '').trim();
951
- const evidenceHtml = this.renderVisualEvidence(r.attachments);
952
- const contextHtml = this.renderTextAttachmentSection(r.attachments, 'qa_context_summary', 'Contexto QA aplicado');
953
- const securityHtml = this._renderSecuritySection(r.attachments, i);
954
- return `
955
- <div class="test-entry ${statusClass}">
956
- <button class="test-header" type="button" onclick="toggleDetails('details-${i}', this)" aria-expanded="${isOpen ? 'true' : 'false'}">
957
- <div class="test-info">
958
- <span class="status-dot"></span>
959
- <div>
960
- <div class="test-name">${this.escapeHtml(r.test.title)}</div>
961
- <div class="test-meta">
962
- <span class="tag tag-status-${tone}">${this.getStatusLabel(r.result.status)}</span>
963
- <span class="tag tag-project">${this.escapeHtml(projectName)}</span>
964
- ${bugClassification ? `<span class="tag tag-bug">${this.escapeHtml(bugClassification)}</span>` : ''}
965
- ${r.result.retry > 0 ? `<span class="tag tag-retry">Retry #${r.result.retry}</span>` : ''}
966
- <span class="tag">${this.formatDuration(r.result.duration)}</span>
967
- </div>
968
- </div>
969
- </div>
970
- <span class="test-chevron" aria-hidden="true">v</span>
971
- </button>
972
-
973
- <div class="test-details" id="details-${i}" style="display: ${isOpen ? 'block' : 'none'};">
974
- <div class="test-details-grid">
975
- <main class="primary-column">
976
- ${r.result.status === 'passed' && successText ? `
977
- <div class="outcome-block success">
978
- <span class="outcome-label">Resultado</span>
979
- <p class="success-block">${this.escapeHtml(successText)}</p>
980
- </div>
981
- ` : ''}
982
-
983
- ${r.result.error && bugClassification ? `
984
- <div class="outcome-block error">
985
- <span class="outcome-label">Clasificacion AI</span>
986
- <pre class="error-block">${this.escapeHtml(bugClassification)}</pre>
987
- </div>
988
- ` : ''}
989
-
990
- ${r.result.error ? `
991
- <div class="outcome-block error">
992
- <span class="outcome-label">Diagnostico del bloqueo</span>
993
- <pre class="error-block">${this.escapeHtml(r.result.error.message || 'Unknown Failure')}</pre>
994
- </div>
995
- ` : ''}
996
-
997
- <section class="steps-section timeline-wrap">
998
- <span class="section-title">Proceso cognitivo del agente</span>
999
- <div class="steps-container">
1000
- ${r.stepsHtml || '<p style="opacity:0.5; padding: 0 18px;">No hay pasos registrados.</p>'}
1001
- </div>
1002
- </section>
1003
- </main>
1004
-
1005
- <aside class="context-column">
1006
- ${evidenceHtml}
1007
- ${contextHtml}
1008
- ${securityHtml}
1009
- <section class="side-panel">
1010
- <div class="panel-header">
1011
- <span class="panel-title">Metadata</span>
1012
- </div>
1013
- <div class="panel-body">
1014
- <dl class="metadata-list">
1015
- <div class="metadata-row"><dt>Proyecto</dt><dd>${this.escapeHtml(projectName)}</dd></div>
1016
- <div class="metadata-row"><dt>Estado</dt><dd>${this.getStatusLabel(r.result.status)}</dd></div>
1017
- <div class="metadata-row"><dt>Duracion</dt><dd>${this.formatDuration(r.result.duration)}</dd></div>
1018
- <div class="metadata-row"><dt>Retry</dt><dd>${r.result.retry}</dd></div>
1019
- </dl>
1020
- </div>
1021
- </section>
1022
- </aside>
1023
- </div>
1024
- </div>
1025
- </div>
1026
- `;
1027
- }).join('')}
1028
- </div>
1029
-
1030
- <footer>
1031
- ARCALITY ENGINE v${this.getEngineVersion()} | ${generatedAt.toISOString()} | GENERATED FOR ARCADIAL
1032
- </footer>
1033
- </div>
1034
-
1035
- <script>
1036
- function toggleDetails(id, trigger) {
1037
- const el = document.getElementById(id);
1038
- if (!el) return;
1039
- const isHidden = el.style.display === 'none' || el.style.display === '';
1040
- el.style.display = isHidden ? 'block' : 'none';
1041
- if (trigger) trigger.setAttribute('aria-expanded', isHidden ? 'true' : 'false');
1042
- }
1043
- </script>
1044
- </body>
1045
- </html>
1046
- `;
1047
- }
1048
- renderVisualEvidence(attachments) {
1049
- const media = attachments.filter(att => att.path && (att.contentType.startsWith('image/') || att.contentType.startsWith('video/')));
1050
- if (media.length === 0)
1051
- return '';
1052
- const primary = media[0];
1053
- const primaryPath = this.escapeHtml(primary.path);
1054
- const primaryName = this.escapeHtml(primary.name);
1055
- const primaryMedia = primary.contentType.startsWith('image/')
1056
- ? `<img src="${primaryPath}" alt="${primaryName}" onclick="window.open(this.src)">`
1057
- : `<video controls src="${primaryPath}"></video>`;
1058
- return `
1059
- <section class="side-panel evidence-panel">
1060
- <div class="panel-header">
1061
- <span class="panel-title">Evidencia visual</span>
1062
- <span class="panel-kicker">${media.length} archivo${media.length === 1 ? '' : 's'}</span>
1063
- </div>
1064
- <div class="evidence-hero">
1065
- ${primaryMedia}
1066
- <a class="evidence-link" href="${primaryPath}" target="_blank" rel="noreferrer">
1067
- <span>${primaryName}</span>
1068
- <span>Abrir</span>
1069
- </a>
1070
- </div>
1071
- ${media.length > 1 ? `
1072
- <div class="evidence-thumbs">
1073
- ${media.map(att => {
1074
- const mediaPath = this.escapeHtml(att.path);
1075
- const mediaName = this.escapeHtml(att.name);
1076
- const thumb = att.contentType.startsWith('image/')
1077
- ? `<img src="${mediaPath}" alt="${mediaName}">`
1078
- : `<video src="${mediaPath}"></video>`;
1079
- return `
1080
- <a class="evidence-thumb" href="${mediaPath}" target="_blank" rel="noreferrer">
1081
- ${thumb}
1082
- <span class="thumb-name">${mediaName}</span>
1083
- </a>
1084
- `;
1085
- }).join('')}
1086
- </div>
1087
- ` : ''}
1088
- </section>
1089
- `;
1090
- }
1091
- getAttachmentText(att) {
1092
- if (!att)
1093
- return null;
1094
- if (att.body)
1095
- return att.body.toString();
1096
- if (att.path) {
1097
- const actualPath = path_1.default.isAbsolute(att.path) ? att.path : path_1.default.join(this.outputDir, att.path);
1098
- if (fs_1.default.existsSync(actualPath)) {
1099
- return fs_1.default.readFileSync(actualPath, 'utf8');
1100
- }
1101
- }
1102
- return null;
1103
- }
1104
- renderTextAttachmentSection(attachments, attachmentName, title) {
1105
- const attachment = attachments.find(att => att.name === attachmentName);
1106
- const text = this.getAttachmentText(attachment);
1107
- if (!text)
1108
- return '';
1109
- return `
1110
- <section class="side-panel">
1111
- <div class="panel-header">
1112
- <span class="panel-title">${this.escapeHtml(title)}</span>
1113
- </div>
1114
- <div class="panel-body">
1115
- <pre class="info-block">${this.escapeHtml(text)}</pre>
1116
- </div>
1117
- </section>
1118
- `;
1119
- }
1120
- _renderSecuritySection(attachments, index) {
1121
- let vulnerabilities = [];
1122
- const reportAtt = attachments.find(a => a.name === 'security_report');
1123
- if (reportAtt) {
1124
- const text = this.getAttachmentText(reportAtt);
1125
- if (text) {
1126
- try {
1127
- const parsed = JSON.parse(text);
1128
- if (Array.isArray(parsed))
1129
- vulnerabilities.push(...parsed);
1130
- }
1131
- catch (e) { }
1132
- }
1133
- }
1134
- const findingAtts = attachments.filter(a => a.name === 'security_finding');
1135
- for (const findingAtt of findingAtts) {
1136
- const text = this.getAttachmentText(findingAtt);
1137
- if (text) {
1138
- try {
1139
- const parsed = JSON.parse(text);
1140
- vulnerabilities.push(parsed);
1141
- }
1142
- catch (e) { }
1143
- }
1144
- }
1145
- if (vulnerabilities.length === 0)
1146
- return '';
1147
- const severityOrder = { 'Critical': 0, 'High': 1, 'Medium': 2, 'Low': 3, 'Info': 4 };
1148
- const severityCounts = { 'Critical': 0, 'High': 0, 'Medium': 0, 'Low': 0, 'Info': 0 };
1149
- let worstSeverity = 'Info';
1150
- const normalizeSeverity = (value) => {
1151
- const severity = String(value || 'Info');
1152
- return severityCounts[severity] !== undefined ? severity : 'Info';
1153
- };
1154
- vulnerabilities.forEach(v => {
1155
- const severity = normalizeSeverity(v.severity);
1156
- severityCounts[severity]++;
1157
- if (severityOrder[severity] < severityOrder[worstSeverity]) {
1158
- worstSeverity = severity;
1159
- }
1160
- });
1161
- const getSeverityColor = (sev) => {
1162
- switch (sev) {
1163
- case 'Critical': return 'var(--error)';
1164
- case 'High': return '#ff9d66';
1165
- case 'Medium': return 'var(--warning)';
1166
- case 'Low': return 'var(--blue)';
1167
- case 'Info': return 'var(--faint)';
1168
- }
1169
- };
1170
- const riskColor = getSeverityColor(worstSeverity);
1171
- return `
1172
- <section class="side-panel security-section">
1173
- <button type="button" class="security-header panel-header" onclick="toggleDetails('security-details-${index}', this)" aria-expanded="false">
1174
- <span class="panel-title">Evaluacion de seguridad</span>
1175
- <span class="security-risk-badge" style="background-color: ${riskColor};">${worstSeverity}</span>
1176
- </button>
1177
- <div class="security-summary">
1178
- <div class="summary-item">
1179
- <div class="count count-Critical">${severityCounts.Critical}</div>
1180
- <div class="label">Critical</div>
1181
- </div>
1182
- <div class="summary-item">
1183
- <div class="count count-High">${severityCounts.High}</div>
1184
- <div class="label">High</div>
1185
- </div>
1186
- <div class="summary-item">
1187
- <div class="count count-Medium">${severityCounts.Medium}</div>
1188
- <div class="label">Medium</div>
1189
- </div>
1190
- <div class="summary-item">
1191
- <div class="count count-Low">${severityCounts.Low}</div>
1192
- <div class="label">Low</div>
1193
- </div>
1194
- </div>
1195
- <div class="security-details" id="security-details-${index}" style="display: none;">
1196
- <div class="vuln-list">
1197
- ${vulnerabilities.map(v => {
1198
- const sev = normalizeSeverity(v.severity);
1199
- const cardColor = getSeverityColor(sev);
1200
- return `
1201
- <div class="vuln-card" style="border-left-color: ${cardColor};">
1202
- <div class="vuln-card-title">
1203
- <span class="tag" style="background-color: ${cardColor}20; color: ${cardColor};">${sev}</span>
1204
- <span>${this.escapeHtml(v.category)} ${v.subcategory ? `<span style="opacity:0.7">- ${this.escapeHtml(v.subcategory)}</span>` : ''}</span>
1205
- </div>
1206
- <div style="font-size: 0.78rem; opacity: 0.82; margin: 6px 0 10px; display: flex; flex-wrap: wrap; gap: 10px;">
1207
- ${v.type ? `<span><strong style="color:var(--brand);">Type:</strong> ${this.escapeHtml(v.type)}</span>` : ''}
1208
- ${v.confidence ? `<span><strong>Confidence:</strong> ${this.escapeHtml(v.confidence)}</span>` : ''}
1209
- ${v.exploitability ? `<span><strong>Exploitability:</strong> ${this.escapeHtml(v.exploitability)}</span>` : ''}
1210
- </div>
1211
- <p class="vuln-card-desc">${this.escapeHtml(v.description)}</p>
1212
- ${v.impact ? `<p class="vuln-card-desc" style="color:#ffd89a;"><strong>Impact:</strong> ${this.escapeHtml(v.impact)}</p>` : ''}
1213
-
1214
- <div class="vuln-card-evidence">
1215
- <strong>Evidence:</strong>
1216
- <pre>${this.escapeHtml(JSON.stringify(v.evidence, null, 2))}</pre>
1217
- </div>
1218
-
1219
- ${v.reproductionSteps && v.reproductionSteps.length > 0 ? `
1220
- <div style="margin: 10px 0; background: rgba(0,0,0,0.18); padding: 10px; border-radius: 6px; border-left: 2px solid ${cardColor}80;">
1221
- <strong style="font-size: 0.82rem; color: var(--muted);">Reproduction Steps:</strong>
1222
- <ol style="margin: 5px 0 0 20px; font-size: 0.82rem; color: var(--text);">
1223
- ${v.reproductionSteps.map((s) => `<li>${this.escapeHtml(s)}</li>`).join('')}
1224
- </ol>
1225
- </div>` : ''}
1226
-
1227
- <div class="vuln-card-remediation">
1228
- <strong>Remediation:</strong> ${this.escapeHtml(v.remediation)}
1229
- </div>
1230
-
1231
- ${v.classification ? `
1232
- <div style="font-size: 0.72rem; opacity: 0.55; margin-top: 12px; display: flex; gap: 15px; font-family: 'JetBrains Mono', monospace; flex-wrap: wrap;">
1233
- ${v.classification.owaspTop10 ? `<span>[OWASP: ${this.escapeHtml(v.classification.owaspTop10)}]</span>` : ''}
1234
- ${v.classification.cwe ? `<span>[CWE: ${this.escapeHtml(v.classification.cwe)}]</span>` : ''}
1235
- </div>` : ''}
1236
- </div>
1237
- `;
1238
- }).join('')}
1239
- </div>
1240
- </div>
1241
- </section>
1242
- `;
1243
- }
1244
- escapeHtml(unsafe) {
1245
- if (!unsafe)
1246
- return '';
1247
- return unsafe
1248
- .replace(/&/g, "&amp;")
1249
- .replace(/</g, "&lt;")
1250
- .replace(/>/g, "&gt;")
1251
- .replace(/"/g, "&quot;")
1252
- .replace(/'/g, "&#039;");
1253
- }
1254
- }
1255
- exports.default = ArcalityReporter;
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const path_1 = __importDefault(require("path"));
8
+ class ArcalityReporter {
9
+ constructor(options = {}) {
10
+ this.results = [];
11
+ this.totalDuration = 0;
12
+ this.startTime = new Date();
13
+ this.outputDir = options.outputDir || 'arcality-report';
14
+ }
15
+ onBegin(config, suite) {
16
+ // Clean output directory to avoid stale reports/attachments.
17
+ if (fs_1.default.existsSync(this.outputDir)) {
18
+ try {
19
+ fs_1.default.rmSync(this.outputDir, { recursive: true, force: true });
20
+ console.log(`Cleaned old report data at: ${this.outputDir}`);
21
+ }
22
+ catch (err) {
23
+ console.warn(`Could not fully clean report dir: ${err.message}`);
24
+ }
25
+ }
26
+ if (!fs_1.default.existsSync(this.outputDir)) {
27
+ fs_1.default.mkdirSync(this.outputDir, { recursive: true });
28
+ }
29
+ this.startTime = new Date();
30
+ console.log(`Arcality Mission started at ${this.startTime.toLocaleTimeString()}`);
31
+ }
32
+ onTestEnd(test, result) {
33
+ const stepsHtml = result.steps.map(step => this.renderStep(step, 0)).join('');
34
+ const processedAttachments = result.attachments.map(att => {
35
+ let extension = '.png';
36
+ if (att.contentType === 'application/json')
37
+ extension = '.json';
38
+ else if (att.contentType === 'text/plain')
39
+ extension = '.txt';
40
+ else if (att.contentType === 'text/yaml')
41
+ extension = '.yaml';
42
+ const fileName = att.path ? path_1.default.basename(att.path) : `${att.name || 'attachment'}-${Date.now()}${extension}`;
43
+ const attachmentsDir = path_1.default.join(this.outputDir, 'attachments');
44
+ const destPath = path_1.default.join(attachmentsDir, fileName);
45
+ if (!fs_1.default.existsSync(attachmentsDir)) {
46
+ fs_1.default.mkdirSync(attachmentsDir, { recursive: true });
47
+ }
48
+ try {
49
+ let copied = false;
50
+ if (att.path && fs_1.default.existsSync(att.path)) {
51
+ fs_1.default.copyFileSync(att.path, destPath);
52
+ copied = true;
53
+ }
54
+ else if (att.body) {
55
+ fs_1.default.writeFileSync(destPath, att.body);
56
+ copied = true;
57
+ }
58
+ return copied ? { ...att, path: `attachments/${fileName}` } : att;
59
+ }
60
+ catch (e) {
61
+ console.error(`Failed to process attachment ${att.name}:`, e);
62
+ return att;
63
+ }
64
+ });
65
+ if (result.status === 'failed' || result.error) {
66
+ const isAppBug = result.error?.message?.includes('APPLICATION BUG');
67
+ const prefix = isAppBug ? '[APPLICATION BUG FOUND]' : '[TEST ENGINE FAILURE]';
68
+ console.log(`\n${prefix} ${test.title} failed:`);
69
+ console.log(`${result.error?.message || result.error?.stack || 'Unknown error'}\n`);
70
+ if (result.error?.stack && !isAppBug) {
71
+ console.log(result.error.stack);
72
+ }
73
+ }
74
+ this.results.push({ test, result, stepsHtml, attachments: processedAttachments });
75
+ }
76
+ async onEnd(result) {
77
+ this.totalDuration = result.duration;
78
+ const htmlContent = this.generateHtml();
79
+ const reportPath = path_1.default.join(this.outputDir, 'index.html');
80
+ fs_1.default.writeFileSync(reportPath, htmlContent);
81
+ console.log(`Arcality Mission Report generated at: ${reportPath}`);
82
+ }
83
+ renderStep(step, depth) {
84
+ if (step.category === 'hook' || step.category === 'fixture')
85
+ return '';
86
+ const statusClass = step.error ? 'step-error' : 'step-passed';
87
+ const duration = step.duration >= 0 ? `<span class="step-duration">${this.formatDuration(step.duration)}</span>` : '';
88
+ let title = this.escapeHtml(step.title);
89
+ title = title.replace(/^(click|fill|navigate|wait|expect|goto|press)/i, '<span class="step-action">$1</span>');
90
+ const children = step.steps && step.steps.length > 0
91
+ ? `<div class="step-children">${step.steps.map(s => this.renderStep(s, depth + 1)).join('')}</div>`
92
+ : '';
93
+ const expandableAttr = step.steps && step.steps.length > 0 ? 'data-expandable="true"' : '';
94
+ return `
95
+ <div class="step-wrapper ${statusClass}" style="--step-depth: ${Math.min(depth, 8)}" ${expandableAttr}>
96
+ <div class="step-header">
97
+ <span class="step-marker" aria-hidden="true"></span>
98
+ <span class="step-title">${title}</span>
99
+ ${duration}
100
+ </div>
101
+ ${children}
102
+ </div>
103
+ `;
104
+ }
105
+ formatDuration(milliseconds) {
106
+ if (milliseconds >= 1000) {
107
+ const seconds = milliseconds / 1000;
108
+ return `${seconds >= 10 ? seconds.toFixed(0) : seconds.toFixed(1)}s`;
109
+ }
110
+ return `${Math.max(0, Math.round(milliseconds))}ms`;
111
+ }
112
+ getStatusLabel(status) {
113
+ switch (status) {
114
+ case 'passed': return 'Exitosa';
115
+ case 'failed': return 'Error';
116
+ case 'timedOut': return 'Tiempo agotado';
117
+ case 'skipped': return 'Omitida';
118
+ case 'interrupted': return 'Interrumpida';
119
+ default: return status;
120
+ }
121
+ }
122
+ getStatusTone(status) {
123
+ if (status === 'failed' || status === 'timedOut' || status === 'interrupted')
124
+ return 'failed';
125
+ if (status === 'passed')
126
+ return 'passed';
127
+ return 'neutral';
128
+ }
129
+ getEngineVersion() {
130
+ try {
131
+ const packageJsonPath = path_1.default.join(process.cwd(), 'package.json');
132
+ if (fs_1.default.existsSync(packageJsonPath)) {
133
+ const pkg = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf8'));
134
+ return pkg.version || '2.2.0';
135
+ }
136
+ }
137
+ catch (e) {
138
+ console.error('Error reading package.json version:', e);
139
+ }
140
+ return '2.2.0';
141
+ }
142
+ getBrandLogoDataUri() {
143
+ const roots = [
144
+ process.env.ARCALITY_ROOT,
145
+ path_1.default.resolve(__dirname, '..', '..'),
146
+ process.cwd()
147
+ ].filter(Boolean);
148
+ const logoNames = ['logo.png', 'logo-arcadial-blanco.png'];
149
+ for (const root of roots) {
150
+ for (const logoName of logoNames) {
151
+ const logoPath = path_1.default.join(root, 'public', logoName);
152
+ if (!fs_1.default.existsSync(logoPath))
153
+ continue;
154
+ try {
155
+ const logoBuffer = fs_1.default.readFileSync(logoPath);
156
+ return `data:image/png;base64,${logoBuffer.toString('base64')}`;
157
+ }
158
+ catch (e) {
159
+ console.warn(`Could not load Arcality logo from ${logoPath}`);
160
+ }
161
+ }
162
+ }
163
+ return null;
164
+ }
165
+ generateHtml() {
166
+ const stats = {
167
+ total: this.results.length,
168
+ passed: this.results.filter(r => r.result.status === 'passed').length,
169
+ failed: this.results.filter(r => r.result.status === 'failed' || r.result.status === 'timedOut').length,
170
+ skipped: this.results.filter(r => r.result.status === 'skipped').length,
171
+ flaky: this.results.filter(r => r.result.status === 'passed' && r.result.retry > 0).length
172
+ };
173
+ const evidenceCount = this.results.reduce((count, r) => {
174
+ return count + r.attachments.filter(att => att.path && (att.contentType.startsWith('image/') || att.contentType.startsWith('video/'))).length;
175
+ }, 0);
176
+ const reportTone = stats.failed > 0 ? 'failed' : stats.total === 0 ? 'neutral' : 'passed';
177
+ const reportStatus = stats.failed > 0 ? 'Ejecucion con errores' : stats.total === 0 ? 'Sin ejecuciones' : 'Ejecucion exitosa';
178
+ const reportSummary = stats.failed > 0
179
+ ? `${stats.failed} mision${stats.failed === 1 ? '' : 'es'} fallida${stats.failed === 1 ? '' : 's'}. Revisa el diagnostico y la evidencia visual.`
180
+ : stats.total === 0
181
+ ? 'No se registraron misiones en esta ejecucion.'
182
+ : 'Todas las misiones ejecutadas finalizaron correctamente.';
183
+ const generatedAt = new Date();
184
+ const reportTitle = stats.total > 1 ? 'Reporte de coleccion' : 'Reporte de ejecucion';
185
+ const brandLogo = this.getBrandLogoDataUri();
186
+ return `
187
+ <!DOCTYPE html>
188
+ <html lang="es">
189
+ <head>
190
+ <meta charset="UTF-8">
191
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
192
+ <title>Arcality Mission Report</title>
193
+ <link rel="shortcut icon" href="https://raw.githubusercontent.com/microsoft/playwright/main/packages/playwright-core/src/server/chromium/test-results/favicon.ico">
194
+ <style>
195
+ @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
196
+
197
+ :root {
198
+ --bg: #07080d;
199
+ --surface: #10131b;
200
+ --surface-soft: #151925;
201
+ --line: #293042;
202
+ --line-soft: rgba(255,255,255,0.08);
203
+ --text: #f7f4ff;
204
+ --muted: #a9afc0;
205
+ --faint: #72798c;
206
+ --brand: #b56cff;
207
+ --brand-soft: rgba(181, 108, 255, 0.16);
208
+ --blue: #6bbcff;
209
+ --success: #39d99f;
210
+ --success-soft: rgba(57, 217, 159, 0.12);
211
+ --error: #ff5f7a;
212
+ --error-soft: rgba(255, 95, 122, 0.12);
213
+ --warning: #ffc857;
214
+ --warning-soft: rgba(255, 200, 87, 0.12);
215
+ --shadow: 0 18px 60px rgba(0,0,0,0.28);
216
+ }
217
+
218
+ * { box-sizing: border-box; }
219
+ html { background: var(--bg); }
220
+ body {
221
+ font-family: 'Plus Jakarta Sans', sans-serif;
222
+ background: linear-gradient(180deg, #080910 0%, #10121a 44%, #07080d 100%);
223
+ color: var(--text);
224
+ margin: 0;
225
+ min-height: 100vh;
226
+ line-height: 1.55;
227
+ }
228
+ body::before {
229
+ content: "";
230
+ position: fixed;
231
+ inset: 0;
232
+ pointer-events: none;
233
+ background-image:
234
+ linear-gradient(rgba(255,255,255,0.025) 1px, transparent 1px),
235
+ linear-gradient(90deg, rgba(255,255,255,0.025) 1px, transparent 1px);
236
+ background-size: 52px 52px;
237
+ mask-image: linear-gradient(to bottom, rgba(0,0,0,0.75), transparent 68%);
238
+ }
239
+
240
+ button, a { font: inherit; }
241
+ a { color: inherit; }
242
+ .container {
243
+ position: relative;
244
+ max-width: 1480px;
245
+ margin: 0 auto;
246
+ padding: 32px;
247
+ }
248
+
249
+ .mission-brief {
250
+ display: grid;
251
+ grid-template-columns: minmax(0, 1fr) auto;
252
+ gap: 28px;
253
+ align-items: start;
254
+ padding: 6px 0 28px;
255
+ border-bottom: 1px solid var(--line);
256
+ margin-bottom: 22px;
257
+ }
258
+ .brand-row {
259
+ display: flex;
260
+ align-items: center;
261
+ gap: 14px;
262
+ margin-bottom: 26px;
263
+ }
264
+ .brand-mark {
265
+ width: 38px;
266
+ height: 38px;
267
+ border: 1px solid rgba(181,108,255,0.45);
268
+ border-radius: 8px;
269
+ background: linear-gradient(135deg, rgba(181,108,255,0.92), rgba(107,188,255,0.62)), #141720;
270
+ box-shadow: 0 0 28px rgba(181,108,255,0.22);
271
+ }
272
+ .brand-mark.has-logo {
273
+ width: 42px;
274
+ height: 42px;
275
+ padding: 5px;
276
+ background: rgba(255,255,255,0.035);
277
+ box-shadow: 0 0 24px rgba(181,108,255,0.12);
278
+ }
279
+ .brand-mark img {
280
+ display: block;
281
+ width: 100%;
282
+ height: 100%;
283
+ object-fit: contain;
284
+ filter: drop-shadow(0 0 10px rgba(181,108,255,0.28));
285
+ }
286
+ .brand-name {
287
+ font-size: 0.74rem;
288
+ color: var(--muted);
289
+ letter-spacing: 0.16em;
290
+ text-transform: uppercase;
291
+ font-weight: 800;
292
+ }
293
+ .brand-subtitle {
294
+ color: var(--faint);
295
+ font-size: 0.86rem;
296
+ margin-top: 1px;
297
+ }
298
+ .mission-title {
299
+ font-size: clamp(2rem, 4vw, 4.6rem);
300
+ line-height: 0.96;
301
+ letter-spacing: 0;
302
+ max-width: 900px;
303
+ margin: 0;
304
+ font-weight: 800;
305
+ }
306
+ .mission-copy {
307
+ margin: 18px 0 0;
308
+ max-width: 720px;
309
+ color: var(--muted);
310
+ font-size: 1rem;
311
+ }
312
+ .status-card {
313
+ width: min(380px, 100%);
314
+ border: 1px solid var(--line);
315
+ background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.018));
316
+ border-radius: 8px;
317
+ padding: 18px;
318
+ box-shadow: var(--shadow);
319
+ }
320
+ .status-card.passed { border-color: rgba(57,217,159,0.42); }
321
+ .status-card.failed { border-color: rgba(255,95,122,0.48); }
322
+ .status-card.neutral { border-color: rgba(169,175,192,0.28); }
323
+ .status-eyebrow {
324
+ display: flex;
325
+ align-items: center;
326
+ justify-content: space-between;
327
+ gap: 16px;
328
+ color: var(--faint);
329
+ font-size: 0.72rem;
330
+ font-family: 'JetBrains Mono', monospace;
331
+ letter-spacing: 0.08em;
332
+ text-transform: uppercase;
333
+ }
334
+ .status-pill {
335
+ display: inline-flex;
336
+ align-items: center;
337
+ gap: 7px;
338
+ border: 1px solid var(--line-soft);
339
+ border-radius: 999px;
340
+ padding: 5px 9px;
341
+ color: var(--text);
342
+ background: rgba(255,255,255,0.04);
343
+ }
344
+ .status-pill::before {
345
+ content: "";
346
+ width: 7px;
347
+ height: 7px;
348
+ border-radius: 50%;
349
+ background: var(--faint);
350
+ }
351
+ .status-card.passed .status-pill::before { background: var(--success); }
352
+ .status-card.failed .status-pill::before { background: var(--error); }
353
+ .status-title {
354
+ font-size: 1.7rem;
355
+ line-height: 1.05;
356
+ margin: 24px 0 10px;
357
+ font-weight: 800;
358
+ }
359
+ .status-summary {
360
+ color: var(--muted);
361
+ margin: 0;
362
+ font-size: 0.95rem;
363
+ }
364
+ .brief-grid {
365
+ display: grid;
366
+ grid-template-columns: repeat(6, minmax(120px, 1fr));
367
+ gap: 1px;
368
+ background: var(--line);
369
+ border: 1px solid var(--line);
370
+ border-radius: 8px;
371
+ overflow: hidden;
372
+ margin-bottom: 18px;
373
+ }
374
+ .brief-metric {
375
+ background: rgba(16,19,27,0.94);
376
+ padding: 16px 18px;
377
+ min-height: 92px;
378
+ }
379
+ .metric-value {
380
+ display: block;
381
+ font-size: 1.7rem;
382
+ line-height: 1;
383
+ font-weight: 800;
384
+ }
385
+ .metric-label {
386
+ display: block;
387
+ margin-top: 10px;
388
+ color: var(--faint);
389
+ font-size: 0.72rem;
390
+ letter-spacing: 0.1em;
391
+ text-transform: uppercase;
392
+ font-weight: 800;
393
+ }
394
+ .val-passed { color: var(--success); }
395
+ .val-failed { color: var(--error); }
396
+ .val-flaky { color: var(--warning); }
397
+ .val-blue { color: var(--blue); }
398
+
399
+ .test-entry {
400
+ border-top: 1px solid var(--line);
401
+ }
402
+ .test-entry:last-child {
403
+ border-bottom: 1px solid var(--line);
404
+ }
405
+ .test-header {
406
+ width: 100%;
407
+ appearance: none;
408
+ border: 0;
409
+ color: var(--text);
410
+ background: transparent;
411
+ padding: 22px 0;
412
+ display: flex;
413
+ justify-content: space-between;
414
+ align-items: center;
415
+ gap: 24px;
416
+ text-align: left;
417
+ cursor: pointer;
418
+ }
419
+ .test-header:hover .test-name { color: var(--brand); }
420
+ .test-info {
421
+ min-width: 0;
422
+ display: flex;
423
+ align-items: center;
424
+ gap: 14px;
425
+ }
426
+ .status-dot {
427
+ width: 11px;
428
+ height: 44px;
429
+ border-radius: 999px;
430
+ background: var(--faint);
431
+ flex: 0 0 auto;
432
+ }
433
+ .status-passed .status-dot { background: var(--success); }
434
+ .status-failed .status-dot { background: var(--error); }
435
+ .status-neutral .status-dot { background: var(--faint); }
436
+ .test-name {
437
+ font-weight: 800;
438
+ font-size: 1.08rem;
439
+ line-height: 1.25;
440
+ transition: color 0.16s ease;
441
+ }
442
+ .test-meta {
443
+ display: flex;
444
+ flex-wrap: wrap;
445
+ gap: 8px;
446
+ margin-top: 8px;
447
+ color: var(--muted);
448
+ font-size: 0.78rem;
449
+ font-family: 'JetBrains Mono', monospace;
450
+ }
451
+ .test-chevron {
452
+ width: 32px;
453
+ height: 32px;
454
+ border: 1px solid var(--line);
455
+ border-radius: 8px;
456
+ display: inline-grid;
457
+ place-items: center;
458
+ color: var(--muted);
459
+ flex: 0 0 auto;
460
+ transition: transform 0.16s ease, border-color 0.16s ease;
461
+ }
462
+ .test-header[aria-expanded="true"] .test-chevron {
463
+ transform: rotate(180deg);
464
+ border-color: rgba(181,108,255,0.48);
465
+ color: var(--text);
466
+ }
467
+ .test-details {
468
+ padding: 0 0 28px;
469
+ }
470
+ .test-details-grid {
471
+ display: grid;
472
+ grid-template-columns: minmax(0, 1.5fr) minmax(320px, 0.8fr);
473
+ gap: 24px;
474
+ align-items: start;
475
+ }
476
+ .primary-column,
477
+ .context-column {
478
+ min-width: 0;
479
+ }
480
+ .context-column {
481
+ display: flex;
482
+ flex-direction: column;
483
+ gap: 14px;
484
+ }
485
+ .outcome-block,
486
+ .side-panel,
487
+ .timeline-wrap {
488
+ border: 1px solid var(--line);
489
+ border-radius: 8px;
490
+ background: rgba(16,19,27,0.78);
491
+ }
492
+ .outcome-block {
493
+ padding: 18px;
494
+ margin-bottom: 18px;
495
+ }
496
+ .outcome-block.success {
497
+ border-color: rgba(57,217,159,0.36);
498
+ background: var(--success-soft);
499
+ }
500
+ .outcome-block.error {
501
+ border-color: rgba(255,95,122,0.4);
502
+ background: var(--error-soft);
503
+ }
504
+ .outcome-label {
505
+ display: block;
506
+ color: var(--faint);
507
+ font-size: 0.72rem;
508
+ letter-spacing: 0.12em;
509
+ text-transform: uppercase;
510
+ font-weight: 800;
511
+ margin-bottom: 8px;
512
+ }
513
+ .success-block {
514
+ margin: 0;
515
+ color: #bff7df;
516
+ font-size: 1rem;
517
+ font-weight: 700;
518
+ }
519
+ .error-block {
520
+ font-family: 'JetBrains Mono', monospace;
521
+ font-size: 0.86rem;
522
+ color: #ffd0d8;
523
+ white-space: pre-wrap;
524
+ margin: 0;
525
+ }
526
+ .panel-header {
527
+ display: flex;
528
+ justify-content: space-between;
529
+ align-items: center;
530
+ gap: 12px;
531
+ padding: 14px 16px;
532
+ border-bottom: 1px solid var(--line-soft);
533
+ }
534
+ .panel-title {
535
+ color: var(--text);
536
+ font-size: 0.8rem;
537
+ letter-spacing: 0.1em;
538
+ text-transform: uppercase;
539
+ font-weight: 800;
540
+ }
541
+ .panel-kicker {
542
+ color: var(--faint);
543
+ font-size: 0.72rem;
544
+ font-family: 'JetBrains Mono', monospace;
545
+ text-transform: uppercase;
546
+ }
547
+ .panel-body {
548
+ padding: 16px;
549
+ }
550
+ .info-block {
551
+ font-family: 'JetBrains Mono', monospace;
552
+ font-size: 0.8rem;
553
+ color: #d7dcff;
554
+ white-space: pre-wrap;
555
+ margin: 0;
556
+ }
557
+ .metadata-list {
558
+ display: grid;
559
+ gap: 12px;
560
+ margin: 0;
561
+ }
562
+ .metadata-row {
563
+ display: flex;
564
+ justify-content: space-between;
565
+ gap: 16px;
566
+ border-bottom: 1px solid var(--line-soft);
567
+ padding-bottom: 10px;
568
+ }
569
+ .metadata-row:last-child {
570
+ border-bottom: 0;
571
+ padding-bottom: 0;
572
+ }
573
+ .metadata-row dt {
574
+ color: var(--faint);
575
+ margin: 0;
576
+ font-size: 0.74rem;
577
+ text-transform: uppercase;
578
+ letter-spacing: 0.08em;
579
+ font-weight: 800;
580
+ }
581
+ .metadata-row dd {
582
+ margin: 0;
583
+ color: var(--text);
584
+ font-family: 'JetBrains Mono', monospace;
585
+ font-size: 0.8rem;
586
+ text-align: right;
587
+ }
588
+
589
+ .steps-section { margin-top: 0; }
590
+ .section-title {
591
+ display: block;
592
+ padding: 15px 18px;
593
+ border-bottom: 1px solid var(--line-soft);
594
+ color: var(--muted);
595
+ font-size: 0.76rem;
596
+ text-transform: uppercase;
597
+ letter-spacing: 0.12em;
598
+ font-weight: 800;
599
+ }
600
+ .timeline-wrap {
601
+ overflow: hidden;
602
+ }
603
+ .steps-container {
604
+ padding: 12px 0;
605
+ }
606
+ .step-wrapper {
607
+ margin-left: calc(var(--step-depth) * 18px);
608
+ padding: 7px 18px 7px 20px;
609
+ position: relative;
610
+ }
611
+ .step-wrapper::before {
612
+ content: "";
613
+ position: absolute;
614
+ left: 5px;
615
+ top: 0;
616
+ bottom: 0;
617
+ width: 1px;
618
+ background: var(--line-soft);
619
+ }
620
+ .step-header {
621
+ display: grid;
622
+ grid-template-columns: 14px minmax(0, 1fr) auto;
623
+ align-items: start;
624
+ gap: 10px;
625
+ font-size: 0.9rem;
626
+ }
627
+ .step-marker {
628
+ width: 8px;
629
+ height: 8px;
630
+ border-radius: 50%;
631
+ background: var(--success);
632
+ margin-top: 7px;
633
+ box-shadow: 0 0 0 4px rgba(57,217,159,0.08);
634
+ }
635
+ .step-title {
636
+ color: #e6e9f5;
637
+ min-width: 0;
638
+ overflow-wrap: anywhere;
639
+ }
640
+ .step-action {
641
+ color: var(--blue);
642
+ font-weight: 800;
643
+ }
644
+ .step-duration {
645
+ color: var(--faint);
646
+ font-size: 0.72rem;
647
+ font-family: 'JetBrains Mono', monospace;
648
+ white-space: nowrap;
649
+ padding-top: 2px;
650
+ }
651
+ .step-error .step-title { color: #ffd0d8; }
652
+ .step-error .step-marker {
653
+ background: var(--error);
654
+ box-shadow: 0 0 0 4px rgba(255,95,122,0.09);
655
+ }
656
+ .step-children {
657
+ margin-top: 2px;
658
+ }
659
+
660
+ .evidence-hero {
661
+ position: relative;
662
+ overflow: hidden;
663
+ background: #080910;
664
+ }
665
+ .evidence-hero img,
666
+ .evidence-hero video {
667
+ width: 100%;
668
+ max-height: 320px;
669
+ object-fit: contain;
670
+ display: block;
671
+ cursor: zoom-in;
672
+ }
673
+ .evidence-link,
674
+ .evidence-thumb {
675
+ text-decoration: none;
676
+ }
677
+ .evidence-link {
678
+ display: flex;
679
+ justify-content: space-between;
680
+ gap: 12px;
681
+ padding: 11px 13px;
682
+ color: var(--muted);
683
+ border-top: 1px solid var(--line-soft);
684
+ font-size: 0.78rem;
685
+ font-family: 'JetBrains Mono', monospace;
686
+ }
687
+ .evidence-link:hover,
688
+ .evidence-thumb:hover {
689
+ color: var(--text);
690
+ }
691
+ .evidence-thumbs {
692
+ display: grid;
693
+ grid-template-columns: repeat(2, minmax(0, 1fr));
694
+ gap: 10px;
695
+ padding: 12px;
696
+ border-top: 1px solid var(--line-soft);
697
+ }
698
+ .evidence-thumb {
699
+ border: 1px solid var(--line-soft);
700
+ border-radius: 8px;
701
+ overflow: hidden;
702
+ color: var(--muted);
703
+ background: rgba(255,255,255,0.03);
704
+ font-size: 0.72rem;
705
+ }
706
+ .evidence-thumb img,
707
+ .evidence-thumb video {
708
+ width: 100%;
709
+ aspect-ratio: 16 / 9;
710
+ object-fit: cover;
711
+ display: block;
712
+ background: #07080d;
713
+ }
714
+ .thumb-name {
715
+ display: block;
716
+ padding: 8px;
717
+ white-space: nowrap;
718
+ overflow: hidden;
719
+ text-overflow: ellipsis;
720
+ }
721
+
722
+ footer {
723
+ margin-top: 34px;
724
+ text-align: center;
725
+ padding: 22px 0 6px;
726
+ font-size: 0.76rem;
727
+ color: var(--faint);
728
+ font-family: 'JetBrains Mono', monospace;
729
+ }
730
+
731
+ .tag {
732
+ display: inline-flex;
733
+ align-items: center;
734
+ min-height: 22px;
735
+ padding: 3px 8px;
736
+ border-radius: 999px;
737
+ border: 1px solid var(--line-soft);
738
+ background: rgba(255,255,255,0.035);
739
+ color: var(--muted);
740
+ font-size: 0.68rem;
741
+ font-weight: 800;
742
+ text-transform: uppercase;
743
+ letter-spacing: 0.06em;
744
+ }
745
+ .tag-project {
746
+ color: #e2c7ff;
747
+ background: var(--brand-soft);
748
+ border-color: rgba(181,108,255,0.26);
749
+ }
750
+ .tag-bug {
751
+ color: #b7e4ff;
752
+ background: rgba(80,170,255,0.12);
753
+ border-color: rgba(80,170,255,0.3);
754
+ }
755
+ .tag-retry {
756
+ color: #ffe2a6;
757
+ background: var(--warning-soft);
758
+ border-color: rgba(255,200,87,0.28);
759
+ }
760
+ .tag-status-passed {
761
+ color: #bff7df;
762
+ background: var(--success-soft);
763
+ border-color: rgba(57,217,159,0.28);
764
+ }
765
+ .tag-status-failed {
766
+ color: #ffd0d8;
767
+ background: var(--error-soft);
768
+ border-color: rgba(255,95,122,0.28);
769
+ }
770
+ .tag-status-neutral {
771
+ color: var(--muted);
772
+ }
773
+
774
+ .security-section {
775
+ overflow: hidden;
776
+ }
777
+ .security-header {
778
+ width: 100%;
779
+ border: 0;
780
+ background: transparent;
781
+ color: var(--text);
782
+ cursor: pointer;
783
+ }
784
+ .security-risk-badge {
785
+ padding: 5px 9px;
786
+ border-radius: 999px;
787
+ font-size: 0.72rem;
788
+ font-weight: 800;
789
+ color: #07080d;
790
+ }
791
+ .security-summary {
792
+ display: grid;
793
+ grid-template-columns: repeat(4, 1fr);
794
+ gap: 1px;
795
+ background: var(--line-soft);
796
+ text-align: center;
797
+ border-bottom: 1px solid var(--line-soft);
798
+ }
799
+ .summary-item {
800
+ background: rgba(7,8,13,0.48);
801
+ padding: 12px 8px;
802
+ }
803
+ .summary-item .count {
804
+ font-size: 1.2rem;
805
+ font-weight: 800;
806
+ }
807
+ .summary-item .label {
808
+ font-size: 0.66rem;
809
+ text-transform: uppercase;
810
+ color: var(--faint);
811
+ letter-spacing: 0.08em;
812
+ }
813
+ .count-Critical { color: var(--error); }
814
+ .count-High { color: #ff9d66; }
815
+ .count-Medium { color: var(--warning); }
816
+ .count-Low { color: var(--blue); }
817
+
818
+ .vuln-list {
819
+ display: flex;
820
+ flex-direction: column;
821
+ gap: 12px;
822
+ padding: 14px;
823
+ }
824
+ .vuln-card {
825
+ background: rgba(7,8,13,0.52);
826
+ border: 1px solid var(--line-soft);
827
+ border-left-width: 4px;
828
+ border-radius: 8px;
829
+ padding: 14px;
830
+ }
831
+ .vuln-card-title {
832
+ font-weight: 800;
833
+ margin-bottom: 8px;
834
+ display: flex;
835
+ align-items: center;
836
+ gap: 10px;
837
+ flex-wrap: wrap;
838
+ }
839
+ .vuln-card-desc {
840
+ color: var(--muted);
841
+ font-size: 0.88rem;
842
+ margin: 10px 0;
843
+ }
844
+ .vuln-card-evidence {
845
+ background: #07080d;
846
+ padding: 10px;
847
+ border-radius: 4px;
848
+ font-family: 'JetBrains Mono', monospace;
849
+ font-size: 0.74rem;
850
+ white-space: pre-wrap;
851
+ word-break: break-all;
852
+ margin: 10px 0;
853
+ border: 1px solid var(--line-soft);
854
+ max-height: 200px;
855
+ overflow-y: auto;
856
+ }
857
+ .vuln-card-remediation {
858
+ font-size: 0.88rem;
859
+ color: #bff7df;
860
+ }
861
+ pre { margin: 0; }
862
+
863
+ @media (max-width: 1040px) {
864
+ .mission-brief,
865
+ .test-details-grid {
866
+ grid-template-columns: 1fr;
867
+ }
868
+ .status-card {
869
+ width: 100%;
870
+ }
871
+ .brief-grid {
872
+ grid-template-columns: repeat(3, minmax(0, 1fr));
873
+ }
874
+ }
875
+ @media (max-width: 680px) {
876
+ .container {
877
+ padding: 20px;
878
+ }
879
+ .mission-title {
880
+ font-size: 2.3rem;
881
+ }
882
+ .brief-grid {
883
+ grid-template-columns: repeat(2, minmax(0, 1fr));
884
+ }
885
+ .test-header {
886
+ align-items: flex-start;
887
+ }
888
+ .test-details-grid {
889
+ gap: 16px;
890
+ }
891
+ .step-header {
892
+ grid-template-columns: 14px minmax(0, 1fr);
893
+ }
894
+ .step-duration {
895
+ grid-column: 2;
896
+ justify-self: start;
897
+ padding-top: 0;
898
+ }
899
+ .evidence-thumbs,
900
+ .security-summary {
901
+ grid-template-columns: 1fr 1fr;
902
+ }
903
+ }
904
+ </style>
905
+ </head>
906
+ <body>
907
+ <div class="container">
908
+ <header class="mission-brief">
909
+ <div>
910
+ <div class="brand-row">
911
+ <span class="brand-mark ${brandLogo ? 'has-logo' : ''}" aria-hidden="true">
912
+ ${brandLogo ? `<img src="${brandLogo}" alt="">` : ''}
913
+ </span>
914
+ <div>
915
+ <div class="brand-name">Arcality QA Engine</div>
916
+ <div class="brand-subtitle">Consola de diagnostico autonomo</div>
917
+ </div>
918
+ </div>
919
+ <h1 class="mission-title">${reportTitle}</h1>
920
+ <p class="mission-copy">${reportSummary}</p>
921
+ </div>
922
+ <div class="status-card ${reportTone}">
923
+ <div class="status-eyebrow">
924
+ <span>Estado general</span>
925
+ <span class="status-pill">${reportStatus}</span>
926
+ </div>
927
+ <div class="status-title">${reportStatus}</div>
928
+ <p class="status-summary">Inicio ${this.startTime.toLocaleString()} | Engine ${this.getEngineVersion()}</p>
929
+ </div>
930
+ </header>
931
+
932
+ <div class="brief-grid" aria-label="Resumen de ejecucion">
933
+ <div class="brief-metric"><span class="metric-value">${stats.total}</span><span class="metric-label">Misiones</span></div>
934
+ <div class="brief-metric"><span class="metric-value val-passed">${stats.passed}</span><span class="metric-label">Exitosas</span></div>
935
+ <div class="brief-metric"><span class="metric-value val-failed">${stats.failed}</span><span class="metric-label">Errores</span></div>
936
+ <div class="brief-metric"><span class="metric-value val-flaky">${stats.flaky}</span><span class="metric-label">Reintentos</span></div>
937
+ <div class="brief-metric"><span class="metric-value val-blue">${evidenceCount}</span><span class="metric-label">Evidencias</span></div>
938
+ <div class="brief-metric"><span class="metric-value">${this.formatDuration(this.totalDuration)}</span><span class="metric-label">Duracion</span></div>
939
+ </div>
940
+
941
+ <div class="test-list">
942
+ ${this.results.map((r, i) => {
943
+ const tone = this.getStatusTone(r.result.status);
944
+ const statusClass = `status-${tone}`;
945
+ const projectName = r.test.parent.project()?.name || 'default';
946
+ const isOpen = this.results.length === 1 || tone === 'failed' || i === 0;
947
+ const successSummary = r.attachments.find(a => a.name === 'success_summary');
948
+ const successText = this.getAttachmentText(successSummary);
949
+ const bugClassificationAtt = r.attachments.find(a => a.name === 'bug_classification');
950
+ const bugClassification = (this.getAttachmentText(bugClassificationAtt) || '').trim();
951
+ const evidenceHtml = this.renderVisualEvidence(r.attachments);
952
+ const contextHtml = this.renderTextAttachmentSection(r.attachments, 'qa_context_summary', 'Contexto QA aplicado');
953
+ const securityHtml = this._renderSecuritySection(r.attachments, i);
954
+ const databasePreviewHtml = this.renderDatabasePreviewSection(r.attachments);
955
+ const databaseHtml = this.renderDatabaseValidationSection(r.attachments);
956
+ return `
957
+ <div class="test-entry ${statusClass}">
958
+ <button class="test-header" type="button" onclick="toggleDetails('details-${i}', this)" aria-expanded="${isOpen ? 'true' : 'false'}">
959
+ <div class="test-info">
960
+ <span class="status-dot"></span>
961
+ <div>
962
+ <div class="test-name">${this.escapeHtml(r.test.title)}</div>
963
+ <div class="test-meta">
964
+ <span class="tag tag-status-${tone}">${this.getStatusLabel(r.result.status)}</span>
965
+ <span class="tag tag-project">${this.escapeHtml(projectName)}</span>
966
+ ${bugClassification ? `<span class="tag tag-bug">${this.escapeHtml(bugClassification)}</span>` : ''}
967
+ ${r.result.retry > 0 ? `<span class="tag tag-retry">Retry #${r.result.retry}</span>` : ''}
968
+ <span class="tag">${this.formatDuration(r.result.duration)}</span>
969
+ </div>
970
+ </div>
971
+ </div>
972
+ <span class="test-chevron" aria-hidden="true">v</span>
973
+ </button>
974
+
975
+ <div class="test-details" id="details-${i}" style="display: ${isOpen ? 'block' : 'none'};">
976
+ <div class="test-details-grid">
977
+ <main class="primary-column">
978
+ ${r.result.status === 'passed' && successText ? `
979
+ <div class="outcome-block success">
980
+ <span class="outcome-label">Resultado</span>
981
+ <p class="success-block">${this.escapeHtml(successText)}</p>
982
+ </div>
983
+ ` : ''}
984
+
985
+ ${r.result.error && bugClassification ? `
986
+ <div class="outcome-block error">
987
+ <span class="outcome-label">Clasificacion AI</span>
988
+ <pre class="error-block">${this.escapeHtml(bugClassification)}</pre>
989
+ </div>
990
+ ` : ''}
991
+
992
+ ${r.result.error ? `
993
+ <div class="outcome-block error">
994
+ <span class="outcome-label">Diagnostico del bloqueo</span>
995
+ <pre class="error-block">${this.escapeHtml(r.result.error.message || 'Unknown Failure')}</pre>
996
+ </div>
997
+ ` : ''}
998
+
999
+ <section class="steps-section timeline-wrap">
1000
+ <span class="section-title">Proceso cognitivo del agente</span>
1001
+ <div class="steps-container">
1002
+ ${r.stepsHtml || '<p style="opacity:0.5; padding: 0 18px;">No hay pasos registrados.</p>'}
1003
+ </div>
1004
+ </section>
1005
+ </main>
1006
+
1007
+ <aside class="context-column">
1008
+ ${evidenceHtml}
1009
+ ${contextHtml}
1010
+ ${databasePreviewHtml}
1011
+ ${databaseHtml}
1012
+ ${securityHtml}
1013
+ <section class="side-panel">
1014
+ <div class="panel-header">
1015
+ <span class="panel-title">Metadata</span>
1016
+ </div>
1017
+ <div class="panel-body">
1018
+ <dl class="metadata-list">
1019
+ <div class="metadata-row"><dt>Proyecto</dt><dd>${this.escapeHtml(projectName)}</dd></div>
1020
+ <div class="metadata-row"><dt>Estado</dt><dd>${this.getStatusLabel(r.result.status)}</dd></div>
1021
+ <div class="metadata-row"><dt>Duracion</dt><dd>${this.formatDuration(r.result.duration)}</dd></div>
1022
+ <div class="metadata-row"><dt>Retry</dt><dd>${r.result.retry}</dd></div>
1023
+ </dl>
1024
+ </div>
1025
+ </section>
1026
+ </aside>
1027
+ </div>
1028
+ </div>
1029
+ </div>
1030
+ `;
1031
+ }).join('')}
1032
+ </div>
1033
+
1034
+ <footer>
1035
+ ARCALITY ENGINE v${this.getEngineVersion()} | ${generatedAt.toISOString()} | GENERATED FOR ARCADIAL
1036
+ </footer>
1037
+ </div>
1038
+
1039
+ <script>
1040
+ function toggleDetails(id, trigger) {
1041
+ const el = document.getElementById(id);
1042
+ if (!el) return;
1043
+ const isHidden = el.style.display === 'none' || el.style.display === '';
1044
+ el.style.display = isHidden ? 'block' : 'none';
1045
+ if (trigger) trigger.setAttribute('aria-expanded', isHidden ? 'true' : 'false');
1046
+ }
1047
+ </script>
1048
+ </body>
1049
+ </html>
1050
+ `;
1051
+ }
1052
+ renderVisualEvidence(attachments) {
1053
+ const media = attachments.filter(att => att.path && (att.contentType.startsWith('image/') || att.contentType.startsWith('video/')));
1054
+ if (media.length === 0)
1055
+ return '';
1056
+ const primary = media[0];
1057
+ const primaryPath = this.escapeHtml(primary.path);
1058
+ const primaryName = this.escapeHtml(primary.name);
1059
+ const primaryMedia = primary.contentType.startsWith('image/')
1060
+ ? `<img src="${primaryPath}" alt="${primaryName}" onclick="window.open(this.src)">`
1061
+ : `<video controls src="${primaryPath}"></video>`;
1062
+ return `
1063
+ <section class="side-panel evidence-panel">
1064
+ <div class="panel-header">
1065
+ <span class="panel-title">Evidencia visual</span>
1066
+ <span class="panel-kicker">${media.length} archivo${media.length === 1 ? '' : 's'}</span>
1067
+ </div>
1068
+ <div class="evidence-hero">
1069
+ ${primaryMedia}
1070
+ <a class="evidence-link" href="${primaryPath}" target="_blank" rel="noreferrer">
1071
+ <span>${primaryName}</span>
1072
+ <span>Abrir</span>
1073
+ </a>
1074
+ </div>
1075
+ ${media.length > 1 ? `
1076
+ <div class="evidence-thumbs">
1077
+ ${media.map(att => {
1078
+ const mediaPath = this.escapeHtml(att.path);
1079
+ const mediaName = this.escapeHtml(att.name);
1080
+ const thumb = att.contentType.startsWith('image/')
1081
+ ? `<img src="${mediaPath}" alt="${mediaName}">`
1082
+ : `<video src="${mediaPath}"></video>`;
1083
+ return `
1084
+ <a class="evidence-thumb" href="${mediaPath}" target="_blank" rel="noreferrer">
1085
+ ${thumb}
1086
+ <span class="thumb-name">${mediaName}</span>
1087
+ </a>
1088
+ `;
1089
+ }).join('')}
1090
+ </div>
1091
+ ` : ''}
1092
+ </section>
1093
+ `;
1094
+ }
1095
+ getAttachmentText(att) {
1096
+ if (!att)
1097
+ return null;
1098
+ if (att.body)
1099
+ return att.body.toString();
1100
+ if (att.path) {
1101
+ const actualPath = path_1.default.isAbsolute(att.path) ? att.path : path_1.default.join(this.outputDir, att.path);
1102
+ if (fs_1.default.existsSync(actualPath)) {
1103
+ return fs_1.default.readFileSync(actualPath, 'utf8');
1104
+ }
1105
+ }
1106
+ return null;
1107
+ }
1108
+ renderTextAttachmentSection(attachments, attachmentName, title) {
1109
+ const attachment = attachments.find(att => att.name === attachmentName);
1110
+ const text = this.getAttachmentText(attachment);
1111
+ if (!text)
1112
+ return '';
1113
+ return `
1114
+ <section class="side-panel">
1115
+ <div class="panel-header">
1116
+ <span class="panel-title">${this.escapeHtml(title)}</span>
1117
+ </div>
1118
+ <div class="panel-body">
1119
+ <pre class="info-block">${this.escapeHtml(text)}</pre>
1120
+ </div>
1121
+ </section>
1122
+ `;
1123
+ }
1124
+ renderDatabasePreviewSection(attachments) {
1125
+ const previews = [];
1126
+ const previewAtts = attachments.filter(a => a.name === 'database_validation_preview');
1127
+ for (const previewAtt of previewAtts) {
1128
+ const text = this.getAttachmentText(previewAtt);
1129
+ if (!text)
1130
+ continue;
1131
+ try {
1132
+ previews.push(JSON.parse(text));
1133
+ }
1134
+ catch (e) { }
1135
+ }
1136
+ if (previews.length === 0)
1137
+ return '';
1138
+ const preview = previews[previews.length - 1];
1139
+ const provider = this.escapeHtml(String(preview.provider || 'unknown'));
1140
+ const queryId = this.escapeHtml(String(preview.query_id || ''));
1141
+ const rowCount = Number(preview.row_count || 0);
1142
+ const sampleRows = this.escapeHtml(JSON.stringify(preview.rows_sample || [], null, 2));
1143
+ return `
1144
+ <section class="side-panel">
1145
+ <div class="panel-header">
1146
+ <span class="panel-title">Preview BD</span>
1147
+ <span class="panel-kicker">${provider}</span>
1148
+ </div>
1149
+ <div class="panel-body">
1150
+ <dl class="metadata-list">
1151
+ <div class="metadata-row"><dt>Query ID</dt><dd>${queryId || 'N/A'}</dd></div>
1152
+ <div class="metadata-row"><dt>Filas</dt><dd>${rowCount}</dd></div>
1153
+ </dl>
1154
+ <pre class="info-block">${sampleRows}</pre>
1155
+ </div>
1156
+ </section>
1157
+ `;
1158
+ }
1159
+ renderDatabaseValidationSection(attachments) {
1160
+ const reports = [];
1161
+ const reportAtts = attachments.filter(a => a.name === 'database_validation_report');
1162
+ for (const reportAtt of reportAtts) {
1163
+ const text = this.getAttachmentText(reportAtt);
1164
+ if (!text)
1165
+ continue;
1166
+ try {
1167
+ reports.push(JSON.parse(text));
1168
+ }
1169
+ catch (e) { }
1170
+ }
1171
+ if (reports.length === 0)
1172
+ return '';
1173
+ const report = reports[reports.length - 1];
1174
+ const summary = report.summary || { total: 0, passed: 0, failed: 0 };
1175
+ const passed = report.passed === true;
1176
+ const statusText = passed ? 'PASS' : 'FAIL';
1177
+ const statusColor = passed ? 'var(--success)' : 'var(--error)';
1178
+ const validations = Array.isArray(report.validations) ? report.validations : [];
1179
+ const validationRows = validations.map((validation) => {
1180
+ const validationPassed = validation.passed === true;
1181
+ const checks = Array.isArray(validation.checks) ? validation.checks : [];
1182
+ return `
1183
+ <div class="metadata-row"><dt>${this.escapeHtml(validation.name || validation.query_id || 'validacion')}</dt><dd style="color:${validationPassed ? 'var(--success)' : 'var(--error)'}">${validationPassed ? 'PASS' : 'FAIL'}</dd></div>
1184
+ ${checks.slice(0, 6).map((check) => `
1185
+ <div class="metadata-row"><dt>${this.escapeHtml(check.type || 'check')}${check.field ? `.${this.escapeHtml(check.field)}` : ''}</dt><dd>${check.passed ? 'OK' : 'Mismatch'}</dd></div>
1186
+ `).join('')}
1187
+ `;
1188
+ }).join('');
1189
+ return `
1190
+ <section class="side-panel">
1191
+ <div class="panel-header">
1192
+ <span class="panel-title">Validacion BD</span>
1193
+ <span class="panel-kicker" style="color:${statusColor}">${statusText}</span>
1194
+ </div>
1195
+ <div class="panel-body">
1196
+ <dl class="metadata-list">
1197
+ <div class="metadata-row"><dt>Total</dt><dd>${summary.total || 0}</dd></div>
1198
+ <div class="metadata-row"><dt>Exitosas</dt><dd>${summary.passed || 0}</dd></div>
1199
+ <div class="metadata-row"><dt>Fallidas</dt><dd>${summary.failed || 0}</dd></div>
1200
+ ${validationRows}
1201
+ </dl>
1202
+ <pre class="info-block">${this.escapeHtml(JSON.stringify(report, null, 2))}</pre>
1203
+ </div>
1204
+ </section>
1205
+ `;
1206
+ }
1207
+ _renderSecuritySection(attachments, index) {
1208
+ let vulnerabilities = [];
1209
+ const reportAtt = attachments.find(a => a.name === 'security_report');
1210
+ if (reportAtt) {
1211
+ const text = this.getAttachmentText(reportAtt);
1212
+ if (text) {
1213
+ try {
1214
+ const parsed = JSON.parse(text);
1215
+ if (Array.isArray(parsed))
1216
+ vulnerabilities.push(...parsed);
1217
+ }
1218
+ catch (e) { }
1219
+ }
1220
+ }
1221
+ const findingAtts = attachments.filter(a => a.name === 'security_finding');
1222
+ for (const findingAtt of findingAtts) {
1223
+ const text = this.getAttachmentText(findingAtt);
1224
+ if (text) {
1225
+ try {
1226
+ const parsed = JSON.parse(text);
1227
+ vulnerabilities.push(parsed);
1228
+ }
1229
+ catch (e) { }
1230
+ }
1231
+ }
1232
+ if (vulnerabilities.length === 0)
1233
+ return '';
1234
+ const severityOrder = { 'Critical': 0, 'High': 1, 'Medium': 2, 'Low': 3, 'Info': 4 };
1235
+ const severityCounts = { 'Critical': 0, 'High': 0, 'Medium': 0, 'Low': 0, 'Info': 0 };
1236
+ let worstSeverity = 'Info';
1237
+ const normalizeSeverity = (value) => {
1238
+ const severity = String(value || 'Info');
1239
+ return severityCounts[severity] !== undefined ? severity : 'Info';
1240
+ };
1241
+ vulnerabilities.forEach(v => {
1242
+ const severity = normalizeSeverity(v.severity);
1243
+ severityCounts[severity]++;
1244
+ if (severityOrder[severity] < severityOrder[worstSeverity]) {
1245
+ worstSeverity = severity;
1246
+ }
1247
+ });
1248
+ const getSeverityColor = (sev) => {
1249
+ switch (sev) {
1250
+ case 'Critical': return 'var(--error)';
1251
+ case 'High': return '#ff9d66';
1252
+ case 'Medium': return 'var(--warning)';
1253
+ case 'Low': return 'var(--blue)';
1254
+ case 'Info': return 'var(--faint)';
1255
+ }
1256
+ };
1257
+ const riskColor = getSeverityColor(worstSeverity);
1258
+ return `
1259
+ <section class="side-panel security-section">
1260
+ <button type="button" class="security-header panel-header" onclick="toggleDetails('security-details-${index}', this)" aria-expanded="false">
1261
+ <span class="panel-title">Evaluacion de seguridad</span>
1262
+ <span class="security-risk-badge" style="background-color: ${riskColor};">${worstSeverity}</span>
1263
+ </button>
1264
+ <div class="security-summary">
1265
+ <div class="summary-item">
1266
+ <div class="count count-Critical">${severityCounts.Critical}</div>
1267
+ <div class="label">Critical</div>
1268
+ </div>
1269
+ <div class="summary-item">
1270
+ <div class="count count-High">${severityCounts.High}</div>
1271
+ <div class="label">High</div>
1272
+ </div>
1273
+ <div class="summary-item">
1274
+ <div class="count count-Medium">${severityCounts.Medium}</div>
1275
+ <div class="label">Medium</div>
1276
+ </div>
1277
+ <div class="summary-item">
1278
+ <div class="count count-Low">${severityCounts.Low}</div>
1279
+ <div class="label">Low</div>
1280
+ </div>
1281
+ </div>
1282
+ <div class="security-details" id="security-details-${index}" style="display: none;">
1283
+ <div class="vuln-list">
1284
+ ${vulnerabilities.map(v => {
1285
+ const sev = normalizeSeverity(v.severity);
1286
+ const cardColor = getSeverityColor(sev);
1287
+ return `
1288
+ <div class="vuln-card" style="border-left-color: ${cardColor};">
1289
+ <div class="vuln-card-title">
1290
+ <span class="tag" style="background-color: ${cardColor}20; color: ${cardColor};">${sev}</span>
1291
+ <span>${this.escapeHtml(v.category)} ${v.subcategory ? `<span style="opacity:0.7">- ${this.escapeHtml(v.subcategory)}</span>` : ''}</span>
1292
+ </div>
1293
+ <div style="font-size: 0.78rem; opacity: 0.82; margin: 6px 0 10px; display: flex; flex-wrap: wrap; gap: 10px;">
1294
+ ${v.type ? `<span><strong style="color:var(--brand);">Type:</strong> ${this.escapeHtml(v.type)}</span>` : ''}
1295
+ ${v.confidence ? `<span><strong>Confidence:</strong> ${this.escapeHtml(v.confidence)}</span>` : ''}
1296
+ ${v.exploitability ? `<span><strong>Exploitability:</strong> ${this.escapeHtml(v.exploitability)}</span>` : ''}
1297
+ </div>
1298
+ <p class="vuln-card-desc">${this.escapeHtml(v.description)}</p>
1299
+ ${v.impact ? `<p class="vuln-card-desc" style="color:#ffd89a;"><strong>Impact:</strong> ${this.escapeHtml(v.impact)}</p>` : ''}
1300
+
1301
+ <div class="vuln-card-evidence">
1302
+ <strong>Evidence:</strong>
1303
+ <pre>${this.escapeHtml(JSON.stringify(v.evidence, null, 2))}</pre>
1304
+ </div>
1305
+
1306
+ ${v.reproductionSteps && v.reproductionSteps.length > 0 ? `
1307
+ <div style="margin: 10px 0; background: rgba(0,0,0,0.18); padding: 10px; border-radius: 6px; border-left: 2px solid ${cardColor}80;">
1308
+ <strong style="font-size: 0.82rem; color: var(--muted);">Reproduction Steps:</strong>
1309
+ <ol style="margin: 5px 0 0 20px; font-size: 0.82rem; color: var(--text);">
1310
+ ${v.reproductionSteps.map((s) => `<li>${this.escapeHtml(s)}</li>`).join('')}
1311
+ </ol>
1312
+ </div>` : ''}
1313
+
1314
+ <div class="vuln-card-remediation">
1315
+ <strong>Remediation:</strong> ${this.escapeHtml(v.remediation)}
1316
+ </div>
1317
+
1318
+ ${v.classification ? `
1319
+ <div style="font-size: 0.72rem; opacity: 0.55; margin-top: 12px; display: flex; gap: 15px; font-family: 'JetBrains Mono', monospace; flex-wrap: wrap;">
1320
+ ${v.classification.owaspTop10 ? `<span>[OWASP: ${this.escapeHtml(v.classification.owaspTop10)}]</span>` : ''}
1321
+ ${v.classification.cwe ? `<span>[CWE: ${this.escapeHtml(v.classification.cwe)}]</span>` : ''}
1322
+ </div>` : ''}
1323
+ </div>
1324
+ `;
1325
+ }).join('')}
1326
+ </div>
1327
+ </div>
1328
+ </section>
1329
+ `;
1330
+ }
1331
+ escapeHtml(unsafe) {
1332
+ if (!unsafe)
1333
+ return '';
1334
+ return unsafe
1335
+ .replace(/&/g, "&amp;")
1336
+ .replace(/</g, "&lt;")
1337
+ .replace(/>/g, "&gt;")
1338
+ .replace(/"/g, "&quot;")
1339
+ .replace(/'/g, "&#039;");
1340
+ }
1341
+ }
1342
+ exports.default = ArcalityReporter;