@holmdigital/engine 1.4.12 → 2.0.0

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.
package/dist/cli/index.js CHANGED
@@ -45,6 +45,8 @@ var init_en = __esm({
45
45
  pdf_saved: "PDF Report saved to {path}",
46
46
  scan_failed: "Scan failed",
47
47
  critical_failure: "\nCI/CD Failure: Critical regulatory violations found.",
48
+ generating_statement: "Generating Accessibility Statement...",
49
+ statement_saved: "Accessibility Statement saved to {path}",
48
50
  score: "\nCompliance Score: {score}/100",
49
51
  status: "Compliance Status: {status}",
50
52
  not_compliant: " (Not compliant with legal requirements)",
@@ -52,7 +54,8 @@ var init_en = __esm({
52
54
  prescriptive_fix: "\n\u{1F4A1} Prescriptive Fix:",
53
55
  use_component: "Use component: {component}",
54
56
  pseudo_tests: "\n\u{1F9EC} Generating Pseudo-Automation Tests...\n",
55
- test_for: "Test for {ruleId}:"
57
+ test_for: "Test for {ruleId}:",
58
+ junit_saved: "JUnit XML report saved to {path}"
56
59
  },
57
60
  report: {
58
61
  title: "Accessibility Report - {url}",
@@ -87,6 +90,8 @@ var init_sv = __esm({
87
90
  pdf_saved: "PDF-rapport sparad till {path}",
88
91
  scan_failed: "Skanning misslyckades",
89
92
  critical_failure: "\nCI/CD Fel: Kritiska regulatoriska brister hittades.",
93
+ generating_statement: "Genererar tillg\xE4nglighetsredog\xF6relse...",
94
+ statement_saved: "Tillg\xE4nglighetsredog\xF6relse sparad till {path}",
90
95
  score: "\nEfterlevnadspo\xE4ng: {score}/100",
91
96
  status: "Efterlevnadsstatus: {status}",
92
97
  not_compliant: " (Uppfyller ej legala krav)",
@@ -94,7 +99,8 @@ var init_sv = __esm({
94
99
  prescriptive_fix: "\n\u{1F4A1} Preskriptiv L\xF6sning:",
95
100
  use_component: "Anv\xE4nd komponent: {component}",
96
101
  pseudo_tests: "\n\u{1F9EC} Genererar Pseudo-automationstester...\n",
97
- test_for: "Test f\xF6r {ruleId}:"
102
+ test_for: "Test f\xF6r {ruleId}:",
103
+ junit_saved: "JUnit XML-rapport sparad till {path}"
98
104
  },
99
105
  report: {
100
106
  title: "Tillg\xE4nglighetsrapport - {url}",
@@ -129,6 +135,8 @@ var init_de = __esm({
129
135
  pdf_saved: "PDF-Bericht gespeichert unter {path}",
130
136
  scan_failed: "Scan fehlgeschlagen",
131
137
  critical_failure: "\nCI/CD Fehler: Kritische regulatorische Verst\xF6\xDFe gefunden.",
138
+ generating_statement: "Generiere Barrierefreiheitserkl\xE4rung...",
139
+ statement_saved: "Barrierefreiheitserkl\xE4rung gespeichert unter {path}",
132
140
  score: "\nKonformit\xE4tsbewertung: {score}/100",
133
141
  status: "Konformit\xE4tsstatus: {status}",
134
142
  not_compliant: " (Nicht konform mit gesetzlichen Anforderungen - BITV 2.0)",
@@ -136,7 +144,8 @@ var init_de = __esm({
136
144
  prescriptive_fix: "\n\u{1F4A1} Vorschriftsm\xE4\xDFige L\xF6sung:",
137
145
  use_component: "Verwenden Sie die Komponente: {component}",
138
146
  pseudo_tests: "\n\u{1F9EC} Generiere Pseudo-Automatisierungstests...\n",
139
- test_for: "Test f\xFCr {ruleId}:"
147
+ test_for: "Test f\xFCr {ruleId}:",
148
+ junit_saved: "JUnit XML-Bericht gespeichert unter {path}"
140
149
  },
141
150
  report: {
142
151
  title: "Barrierefreiheitsbericht - {url}",
@@ -171,6 +180,8 @@ var init_fr = __esm({
171
180
  pdf_saved: "Rapport PDF enregistr\xE9 sous {path}",
172
181
  scan_failed: "\xC9chec du scan",
173
182
  critical_failure: "\n\xC9chec CI/CD : Violations r\xE9glementaires critiques d\xE9tect\xE9es.",
183
+ generating_statement: "G\xE9n\xE9ration de la d\xE9claration d'accessibilit\xE9...",
184
+ statement_saved: "D\xE9claration d'accessibilit\xE9 enregistr\xE9e sous {path}",
174
185
  score: "\nScore de Conformit\xE9 : {score}/100",
175
186
  status: "Statut de Conformit\xE9 : {status}",
176
187
  not_compliant: " (Non conforme aux exigences l\xE9gales - RGAA)",
@@ -178,7 +189,8 @@ var init_fr = __esm({
178
189
  prescriptive_fix: "\n\u{1F4A1} Solution Prescriptive :",
179
190
  use_component: "Utilisez le composant : {component}",
180
191
  pseudo_tests: "\n\u{1F9EC} G\xE9n\xE9ration de tests de pseudo-automatisation...\n",
181
- test_for: "Test pour {ruleId} :"
192
+ test_for: "Test pour {ruleId} :",
193
+ junit_saved: "Rapport XML JUnit enregistr\xE9 sous {path}"
182
194
  },
183
195
  report: {
184
196
  title: "Rapport d'Accessibilit\xE9 - {url}",
@@ -213,6 +225,8 @@ var init_es = __esm({
213
225
  pdf_saved: "Informe PDF guardado en {path}",
214
226
  scan_failed: "Escaneo fallido",
215
227
  critical_failure: "\nFallo de CI/CD: Se encontraron violaciones regulatorias cr\xEDticas.",
228
+ generating_statement: "Generando Declaraci\xF3n de Accesibilidad...",
229
+ statement_saved: "Declaraci\xF3n de Accesibilidad guardada en {path}",
216
230
  score: "\nPuntuaci\xF3n de Cumplimiento: {score}/100",
217
231
  status: "Estado de Cumplimiento: {status}",
218
232
  not_compliant: " (No cumple con los requisitos legales - UNE 139803)",
@@ -220,7 +234,8 @@ var init_es = __esm({
220
234
  prescriptive_fix: "\n\u{1F4A1} Soluci\xF3n Prescriptiva:",
221
235
  use_component: "Use el componente: {component}",
222
236
  pseudo_tests: "\n\u{1F9EC} Generando pruebas de pseudo-automatizaci\xF3n...\n",
223
- test_for: "Prueba para {ruleId}:"
237
+ test_for: "Prueba para {ruleId}:",
238
+ junit_saved: "Informe JUnit XML guardado en {path}"
224
239
  },
225
240
  report: {
226
241
  title: "Informe de Accesibilidad - {url}",
@@ -240,6 +255,141 @@ var init_es = __esm({
240
255
  }
241
256
  });
242
257
 
258
+ // src/locales/fi.json
259
+ var fi_default;
260
+ var init_fi = __esm({
261
+ "src/locales/fi.json"() {
262
+ fi_default = {
263
+ cli: {
264
+ title: "\n\u{1F310} HolmDigital S\xE4\xE4ntelyskanneri\n",
265
+ scanning: "Skannataan {url}...\n",
266
+ initializing: "Alustetaan skanneria...",
267
+ analyzing: "Analysoidaan DOM & Shadow DOM...",
268
+ complete: "Skannaus valmis!",
269
+ generating_pdf: "Luodaan PDF-raporttia...",
270
+ pdf_saved: "PDF-raportti tallennettu kohteeseen {path}",
271
+ scan_failed: "Skannaus ep\xE4onnistui",
272
+ critical_failure: "\nCI/CD Virhe: Kriittisi\xE4 s\xE4\xE4ntelyrikkomuksia havaittu.",
273
+ generating_statement: "Luodaan saavutettavuusselostetta...",
274
+ statement_saved: "Saavutettavuusseloste tallennettu kohteeseen {path}",
275
+ score: "\nVaatimustenmukaisuus: {score}/100",
276
+ status: "Tila: {status}",
277
+ not_compliant: " (Ei t\xE4yt\xE4 lakis\xE4\xE4teisi\xE4 vaatimuksia - Laki digitaalisten palvelujen tarjoamisesta)",
278
+ viewport: "N\xE4kym\xE4: {width}x{height}",
279
+ prescriptive_fix: "\n\u{1F4A1} Ehdotettu korjaus:",
280
+ use_component: "K\xE4yt\xE4 komponenttia: {component}",
281
+ pseudo_tests: "\n\u{1F9EC} Luodaan Pseudo-automaatiotestej\xE4...\n",
282
+ test_for: "Testi {ruleId}:lle:",
283
+ junit_saved: "JUnit XML -raportti tallennettu kohteeseen {path}"
284
+ },
285
+ report: {
286
+ title: "Saavutettavuusraportti - {url}",
287
+ generated: "Luotu: {date}",
288
+ scan_target: "Skannauksen kohde: {url}",
289
+ overall_score: "Kokonaispisteet",
290
+ critical_issues: "Kriittiset ongelmat",
291
+ high_issues: "Vakavat ongelmat",
292
+ total_issues: "Ongelmat yhteens\xE4",
293
+ html_errors: "HTML-virheet",
294
+ detailed_violations: "Yksityiskohtaiset rikkomukset",
295
+ prescriptive_fix: "\u{1F4A1} Ehdotettu korjaus",
296
+ use: "K\xE4yt\xE4",
297
+ footer: "Luonut @holmdigital/engine v0.1.0 \u2022 Standardit: WCAG 2.1 AA, EN 301 549, Digipalvelulaki"
298
+ }
299
+ };
300
+ }
301
+ });
302
+
303
+ // src/locales/dk.json
304
+ var dk_default;
305
+ var init_dk = __esm({
306
+ "src/locales/dk.json"() {
307
+ dk_default = {
308
+ cli: {
309
+ title: "\n\u{1F310} HolmDigital Regulatorisk Scanner\n",
310
+ scanning: "Scanner {url}...\n",
311
+ initializing: "Initialiserer scanner...",
312
+ analyzing: "Analyserer DOM & Shadow DOM...",
313
+ complete: "Scanning f\xE6rdig!",
314
+ generating_pdf: "Genererer PDF-rapport...",
315
+ pdf_saved: "PDF-rapport gemt i {path}",
316
+ scan_failed: "Scanning mislykkedes",
317
+ critical_failure: "\nCI/CD Fejl: Kritiske regulatoriske overtr\xE6delser fundet.",
318
+ generating_statement: "Genererer tilg\xE6ngelighedserkl\xE6ring...",
319
+ statement_saved: "Tilg\xE6ngelighedserkl\xE6ring gemt i {path}",
320
+ score: "\nOverholdelsesscore: {score}/100",
321
+ status: "Overholdelsesstatus: {status}",
322
+ not_compliant: " (Opfylder ikke lovkrav - Lov om tilg\xE6ngelighed)",
323
+ viewport: "Viewport: {width}x{height}",
324
+ prescriptive_fix: "\n\u{1F4A1} Foresl\xE5et l\xF8sning:",
325
+ use_component: "Brug komponent: {component}",
326
+ pseudo_tests: "\n\u{1F9EC} Genererer Pseudo-automatiseringstests...\n",
327
+ test_for: "Test for {ruleId}:",
328
+ junit_saved: "JUnit XML-rapport gemt i {path}"
329
+ },
330
+ report: {
331
+ title: "Tilg\xE6ngelighedsrapport - {url}",
332
+ generated: "Genereret: {date}",
333
+ scan_target: "Scanningsm\xE5l: {url}",
334
+ overall_score: "Samlet Score",
335
+ critical_issues: "Kritiske Fejl",
336
+ high_issues: "Alvorlige Fejl",
337
+ total_issues: "Totale Fejl",
338
+ html_errors: "HTML Fejl",
339
+ detailed_violations: "Detaljerede Overtr\xE6delser",
340
+ prescriptive_fix: "\u{1F4A1} Foresl\xE5et L\xF8sning",
341
+ use: "Brug",
342
+ footer: "Genereret af @holmdigital/engine v0.1.0 \u2022 Standarder: WCAG 2.1 AA, EN 301 549, Lov om tilg\xE6ngelighed"
343
+ }
344
+ };
345
+ }
346
+ });
347
+
348
+ // src/locales/no.json
349
+ var no_default;
350
+ var init_no = __esm({
351
+ "src/locales/no.json"() {
352
+ no_default = {
353
+ cli: {
354
+ title: "\n\u{1F310} HolmDigital Regulatorisk Skanner\n",
355
+ scanning: "Skanner {url}...\n",
356
+ initializing: "Initialiserer skanner...",
357
+ analyzing: "Analyserer DOM & Shadow DOM...",
358
+ complete: "Skanning ferdig!",
359
+ generating_pdf: "Genererer PDF-rapport...",
360
+ pdf_saved: "PDF-rapport lagret i {path}",
361
+ scan_failed: "Skanning mislyktes",
362
+ critical_failure: "\nCI/CD Feil: Kritiske regulatoriske brudd funnet.",
363
+ generating_statement: "Genererer tilgjengelighetserkl\xE6ring...",
364
+ statement_saved: "Tilgjengelighetserkl\xE6ring lagret i {path}",
365
+ score: "\nEtterlevelsespoeng: {score}/100",
366
+ status: "Etterlevelsesstatus: {status}",
367
+ not_compliant: " (Oppfyller ikke lovkrav - Diskriminerings- og tilgjengelighetsloven)",
368
+ viewport: "Visningsomr\xE5de: {width}x{height}",
369
+ prescriptive_fix: "\n\u{1F4A1} Foresl\xE5tt l\xF8sning:",
370
+ use_component: "Bruk komponent: {component}",
371
+ pseudo_tests: "\n\u{1F9EC} Genererer Pseudo-automatiseringstester...\n",
372
+ test_for: "Test for {ruleId}:",
373
+ junit_saved: "JUnit XML-rapport lagret i {path}"
374
+ },
375
+ report: {
376
+ title: "Tilgjengelighetsrapport - {url}",
377
+ generated: "Generert: {date}",
378
+ scan_target: "Skannet m\xE5l: {url}",
379
+ overall_score: "Total Score",
380
+ critical_issues: "Kritiske Feil",
381
+ high_issues: "Alvorlige Feil",
382
+ total_issues: "Totale Feil",
383
+ html_errors: "HTML Feil",
384
+ detailed_violations: "Detaljerte Brudd",
385
+ prescriptive_fix: "\u{1F4A1} Foresl\xE5tt L\xF8sning",
386
+ use: "Bruk",
387
+ footer: "Generert av @holmdigital/engine v0.1.0 \u2022 Standarder: WCAG 2.1 AA, EN 301 549, Uu-tilsynet"
388
+ }
389
+ };
390
+ }
391
+ });
392
+
243
393
  // src/locales/nl.json
244
394
  var nl_default;
245
395
  var init_nl = __esm({
@@ -255,6 +405,8 @@ var init_nl = __esm({
255
405
  pdf_saved: "PDF-rapport opgeslagen op {path}",
256
406
  scan_failed: "Scan mislukt",
257
407
  critical_failure: "\nCI/CD Fout: Kritieke wettelijke overtredingen gevonden.",
408
+ generating_statement: "Toegankelijkheidsverklaring genereren...",
409
+ statement_saved: "Toegankelijkheidsverklaring opgeslagen op {path}",
258
410
  score: "\nNalevingsscore: {score}/100",
259
411
  status: "Nalevingsstatus: {status}",
260
412
  not_compliant: " (Niet in overeenstemming met wettelijke vereisten)",
@@ -262,7 +414,8 @@ var init_nl = __esm({
262
414
  prescriptive_fix: "\n\u{1F4A1} Voorgeschreven oplossing:",
263
415
  use_component: "Gebruik component: {component}",
264
416
  pseudo_tests: "\n\u{1F9EC} Pseudo-automatiseringstests genereren...\n",
265
- test_for: "Test voor {ruleId}:"
417
+ test_for: "Test voor {ruleId}:",
418
+ junit_saved: "JUnit XML-rapport opgeslagen op {path}"
266
419
  },
267
420
  report: {
268
421
  title: "Toegankelijkheidsrapport - {url}",
@@ -336,6 +489,9 @@ var init_i18n = __esm({
336
489
  init_de();
337
490
  init_fr();
338
491
  init_es();
492
+ init_fi();
493
+ init_dk();
494
+ init_no();
339
495
  init_nl();
340
496
  locales = {
341
497
  en: en_default,
@@ -344,14 +500,101 @@ var init_i18n = __esm({
344
500
  fr: fr_default,
345
501
  es: es_default,
346
502
  nl: nl_default,
503
+ fi: fi_default,
504
+ dk: dk_default,
505
+ no: no_default,
347
506
  "en-gb": en_default,
348
507
  "en-us": en_default,
349
- "en-ca": en_default
508
+ "en-ca": en_default,
509
+ "da": dk_default,
510
+ "nb": no_default
511
+ // Norwegian Bokmål alias
350
512
  };
351
513
  currentLang = "en";
352
514
  }
353
515
  });
354
516
 
517
+ // src/reporting/github-actions.ts
518
+ var github_actions_exports = {};
519
+ __export(github_actions_exports, {
520
+ generateGitHubActionsAnnotations: () => generateGitHubActionsAnnotations
521
+ });
522
+ function generateGitHubActionsAnnotations(reports) {
523
+ reports.forEach((report) => {
524
+ let level = "warning";
525
+ if (report.holmdigitalInsight.diggRisk === "critical") {
526
+ level = "error";
527
+ }
528
+ const title = `[${report.ruleId}] ${report.wcagCriteria}`;
529
+ const message = `${report.holmdigitalInsight.reasoning}
530
+
531
+ Legitimacy: ${report.dosLagenReference}
532
+ Fix: ${report.remediation.component ? `Use <${report.remediation.component} />` : "Manual remediation required"}`;
533
+ if (report.failingNodes && report.failingNodes.length > 0) {
534
+ report.failingNodes.forEach((node) => {
535
+ console.log(`::${level} title=${title}::${message} (Target: ${node.target})`);
536
+ });
537
+ } else {
538
+ console.log(`::${level} title=${title}::${message}`);
539
+ }
540
+ });
541
+ }
542
+ var init_github_actions = __esm({
543
+ "src/reporting/github-actions.ts"() {
544
+ "use strict";
545
+ }
546
+ });
547
+
548
+ // src/reporting/junit-generator.ts
549
+ var junit_generator_exports = {};
550
+ __export(junit_generator_exports, {
551
+ generateJUnitXML: () => generateJUnitXML
552
+ });
553
+ function generateJUnitXML(reports, url, duration) {
554
+ const failures = reports.length;
555
+ const testSuites = [
556
+ `<?xml version="1.0" encoding="UTF-8"?>`,
557
+ `<testsuites name="HolmDigital Accessibility Scan" time="${duration / 1e3}">`,
558
+ ` <testsuite name="${url}" tests="${failures}" failures="${failures}" errors="0" time="${duration / 1e3}">`
559
+ ];
560
+ reports.forEach((report) => {
561
+ const severity = report.holmdigitalInsight.diggRisk;
562
+ const message = escapeXML(report.holmdigitalInsight.reasoning);
563
+ const criteria = escapeXML(`WCAG ${report.wcagCriteria} | EN 301 549 ${report.en301549Criteria}`);
564
+ const help = escapeXML(`Ref: ${report.dosLagenReference}. Remediation: ${report.remediation.component || "Manual"}`);
565
+ testSuites.push(` <testcase name="[${severity.toUpperCase()}] ${report.ruleId}" classname="${report.wcagCriteria}" time="0">`);
566
+ testSuites.push(` <failure message="${message}" type="${severity}">${criteria}
567
+ ${help}</failure>`);
568
+ testSuites.push(` </testcase>`);
569
+ });
570
+ testSuites.push(` </testsuite>`);
571
+ testSuites.push(`</testsuites>`);
572
+ return testSuites.join("\n");
573
+ }
574
+ function escapeXML(unsafe) {
575
+ return unsafe.replace(/[<>&'"]/g, (c) => {
576
+ switch (c) {
577
+ case "<":
578
+ return "&lt;";
579
+ case ">":
580
+ return "&gt;";
581
+ case "&":
582
+ return "&amp;";
583
+ case "'":
584
+ return "&apos;";
585
+ case '"':
586
+ return "&quot;";
587
+ default:
588
+ return c;
589
+ }
590
+ });
591
+ }
592
+ var init_junit_generator = __esm({
593
+ "src/reporting/junit-generator.ts"() {
594
+ "use strict";
595
+ }
596
+ });
597
+
355
598
  // src/cli/index.ts
356
599
  var import_commander = require("commander");
357
600
  var import_chalk = __toESM(require("chalk"));
@@ -611,7 +854,7 @@ var RegulatoryScanner = class {
611
854
  }
612
855
  async enrichResults(axeResults) {
613
856
  const reports = [];
614
- const { searchRulesByTags, generateRegulatoryReport } = await import("@holmdigital/standards");
857
+ const { searchRulesByTags, generateRegulatoryReport, getConvergenceRule } = await import("@holmdigital/standards");
615
858
  const { getCurrentLang: getCurrentLang2 } = await Promise.resolve().then(() => (init_i18n(), i18n_exports));
616
859
  const lang = getCurrentLang2();
617
860
  for (const violation of axeResults.violations) {
@@ -623,6 +866,7 @@ var RegulatoryScanner = class {
623
866
  }
624
867
  }
625
868
  if (report) {
869
+ const fullRule = getConvergenceRule(report.ruleId, lang);
626
870
  reports.push({
627
871
  ...report,
628
872
  holmdigitalInsight: {
@@ -630,6 +874,8 @@ var RegulatoryScanner = class {
630
874
  reasoning: violation.help
631
875
  // Använd Axe's hjälptext som specifik anledning
632
876
  },
877
+ // Include legal context from the full rule
878
+ legalContext: fullRule?.legalContext,
633
879
  // Attach extra debug info for the CLI
634
880
  failingNodes: violation.nodes.map((node) => ({
635
881
  html: node.html,
@@ -706,6 +952,18 @@ var RegulatoryScanner = class {
706
952
  pageTitle,
707
953
  pageLanguage
708
954
  };
955
+ const reportsWithContext = reports.filter((r) => r.legalContext);
956
+ const legalSummary = {
957
+ wadApplicable: reportsWithContext.filter(
958
+ (r) => r.legalContext?.appliesTo?.includes("WAD")
959
+ ).length,
960
+ eaaApplicable: reportsWithContext.filter(
961
+ (r) => r.legalContext?.appliesTo?.includes("EAA")
962
+ ).length,
963
+ eaaDeadlineViolations: reportsWithContext.filter(
964
+ (r) => r.legalContext?.eaaDeadline
965
+ ).length
966
+ };
709
967
  return {
710
968
  url: this.options.url,
711
969
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -713,7 +971,8 @@ var RegulatoryScanner = class {
713
971
  reports,
714
972
  stats,
715
973
  score,
716
- complianceStatus
974
+ complianceStatus,
975
+ legalSummary
717
976
  };
718
977
  }
719
978
  /**
@@ -934,6 +1193,37 @@ function generateReportHTML(result) {
934
1193
  .badge-high { background: #fffbeb; color: #d97706; border: 1px solid #fde68a; }
935
1194
  .badge-medium { background: #fefce8; color: #ca8a04; border: 1px solid #fef08a; }
936
1195
  .badge-low { background: #f8fafc; color: #475569; border: 1px solid #e2e8f0; }
1196
+ .badge-wad { background: #eff6ff; color: #1d4ed8; border: 1px solid #bfdbfe; margin-left: 0.5rem; }
1197
+ .badge-eaa { background: #faf5ff; color: #7c3aed; border: 1px solid #ddd6fe; margin-left: 0.5rem; }
1198
+ .legal-summary {
1199
+ background: linear-gradient(135deg, #eff6ff 0%, #faf5ff 100%);
1200
+ border: 1px solid #c7d2fe;
1201
+ border-radius: 12px;
1202
+ padding: 1.5rem;
1203
+ margin-bottom: 2rem;
1204
+ }
1205
+ .legal-summary-title {
1206
+ font-size: 1rem;
1207
+ font-weight: 600;
1208
+ color: #3730a3;
1209
+ margin-bottom: 1rem;
1210
+ }
1211
+ .legal-grid {
1212
+ display: grid;
1213
+ grid-template-columns: repeat(3, 1fr);
1214
+ gap: 1rem;
1215
+ }
1216
+ .legal-stat {
1217
+ text-align: center;
1218
+ }
1219
+ .legal-stat-value {
1220
+ font-size: 1.5rem;
1221
+ font-weight: 700;
1222
+ }
1223
+ .legal-stat-label {
1224
+ font-size: 0.75rem;
1225
+ color: #64748b;
1226
+ }
937
1227
 
938
1228
  .violation-meta {
939
1229
  font-size: 0.875rem;
@@ -1002,6 +1292,26 @@ function generateReportHTML(result) {
1002
1292
  </div>
1003
1293
  </div>
1004
1294
 
1295
+ ${result.legalSummary ? `
1296
+ <div class="legal-summary">
1297
+ <div class="legal-summary-title">\u{1F1EA}\u{1F1FA} EU Legal Framework Impact</div>
1298
+ <div class="legal-grid">
1299
+ <div class="legal-stat">
1300
+ <div class="legal-stat-value" style="color: #1d4ed8;">${result.legalSummary.wadApplicable}</div>
1301
+ <div class="legal-stat-label">WAD (Public Sector) Violations</div>
1302
+ </div>
1303
+ <div class="legal-stat">
1304
+ <div class="legal-stat-value" style="color: #7c3aed;">${result.legalSummary.eaaApplicable}</div>
1305
+ <div class="legal-stat-label">EAA (Private Sector) Violations</div>
1306
+ </div>
1307
+ <div class="legal-stat">
1308
+ <div class="legal-stat-value" style="color: #dc2626;">${result.legalSummary.eaaDeadlineViolations}</div>
1309
+ <div class="legal-stat-label">EAA 2025 Deadline Issues</div>
1310
+ </div>
1311
+ </div>
1312
+ </div>
1313
+ ` : ""}
1314
+
1005
1315
  <div class="section-title">${t("report.detailed_violations")}</div>
1006
1316
 
1007
1317
  ${[...result.reports].sort((a, b) => {
@@ -1015,11 +1325,16 @@ function generateReportHTML(result) {
1015
1325
  <div class="violation-card">
1016
1326
  <div class="violation-header">
1017
1327
  <div class="violation-title">${report.ruleId}</div>
1018
- <span class="badge ${riskClass}">${report.holmdigitalInsight.diggRisk}</span>
1328
+ <div>
1329
+ <span class="badge ${riskClass}">${report.holmdigitalInsight.diggRisk}</span>
1330
+ ${report.legalContext?.appliesTo?.includes("WAD") ? '<span class="badge badge-wad">WAD</span>' : ""}
1331
+ ${report.legalContext?.appliesTo?.includes("EAA") ? '<span class="badge badge-eaa">EAA</span>' : ""}
1332
+ </div>
1019
1333
  </div>
1020
1334
  <div class="violation-meta">
1021
1335
  WCAG ${report.wcagCriteria} \u2022 EN 301 549 ${report.en301549Criteria}
1022
1336
  ${report.dosLagenReference ? `\u2022 ${report.dosLagenReference}` : ""}
1337
+ ${report.legalContext?.eaaDeadline ? `<br/><strong>\u26A0\uFE0F EAA Deadline:</strong> ${report.legalContext.eaaDeadline}` : ""}
1023
1338
  </div>
1024
1339
  <div style="font-size: 0.95rem; color: #334155; line-height: 1.5;">
1025
1340
  ${report.holmdigitalInsight.swedishInterpretation}
@@ -1072,6 +1387,142 @@ async function generatePDF(htmlContent, outputPath) {
1072
1387
  }
1073
1388
  }
1074
1389
 
1390
+ // src/reporting/statement-generator.ts
1391
+ var import_react = __toESM(require("react"));
1392
+ var import_server = require("react-dom/server");
1393
+ var import_components = require("@holmdigital/components");
1394
+ var import_promises = __toESM(require("fs/promises"));
1395
+ var import_path = __toESM(require("path"));
1396
+ async function generateStatementContent(result, lang = "en", format = "html", metadata) {
1397
+ let complianceLevel = "full";
1398
+ if (result.stats.critical > 0 || result.legalSummary && result.legalSummary.eaaDeadlineViolations > 0) {
1399
+ complianceLevel = "non-compliant";
1400
+ } else if (result.score < 100) {
1401
+ complianceLevel = "partial";
1402
+ }
1403
+ const issuesMap = /* @__PURE__ */ new Map();
1404
+ result.reports.forEach((report) => {
1405
+ if (!issuesMap.has(report.ruleId)) {
1406
+ const issueText = `${report.ruleId} (${report.wcagCriteria})`;
1407
+ issuesMap.set(report.ruleId, issueText);
1408
+ }
1409
+ });
1410
+ const nonComplianceItems = Array.from(issuesMap.values());
1411
+ let country = metadata?.country || "SE";
1412
+ if (!metadata?.country) {
1413
+ if (result.url.endsWith(".no")) country = "NO";
1414
+ if (result.url.endsWith(".dk")) country = "DK";
1415
+ if (result.url.endsWith(".fi")) country = "FI";
1416
+ if (result.url.endsWith(".de")) country = "DE";
1417
+ }
1418
+ const sector = "public";
1419
+ let logoUrl;
1420
+ try {
1421
+ const possiblePaths = [
1422
+ import_path.default.join(process.cwd(), "src/assets/logo.jpg"),
1423
+ // run from package root
1424
+ import_path.default.join(process.cwd(), "packages/engine/src/assets/logo.jpg")
1425
+ // run from monorepo root
1426
+ ];
1427
+ for (const p of possiblePaths) {
1428
+ try {
1429
+ const logoBuffer = await import_promises.default.readFile(p);
1430
+ logoUrl = `data:image/jpeg;base64,${logoBuffer.toString("base64")}`;
1431
+ break;
1432
+ } catch (e) {
1433
+ }
1434
+ }
1435
+ } catch (e) {
1436
+ console.warn("Could not load logo.jpg", e);
1437
+ }
1438
+ const props = {
1439
+ country,
1440
+ sector,
1441
+ organizationName: metadata?.organizationName || new URL(result.url).hostname.replace(/^www\./, ""),
1442
+ websiteUrl: result.url,
1443
+ complianceLevel,
1444
+ lastReviewDate: /* @__PURE__ */ new Date(),
1445
+ assessmentDate: /* @__PURE__ */ new Date(),
1446
+ evaluationMethod: lang === "sv" ? "Automatiserad granskning via @holmdigital/engine" : "Automated Scan via @holmdigital/engine",
1447
+ generatorTool: {
1448
+ name: "HolmDigital Regulatory Engine",
1449
+ url: "https://holmdigital.se"
1450
+ },
1451
+ logoUrl,
1452
+ contactEmail: metadata?.contactEmail || "hej@holmdigital.se",
1453
+ phoneNumber: metadata?.phoneNumber || "070-123 45 67",
1454
+ responseTime: metadata?.responseTime || (lang === "sv" ? "2 dagar" : "2 days"),
1455
+ nonComplianceItems,
1456
+ locale: lang,
1457
+ badgeUrl: generateBadgeUrl(result.score) || void 0,
1458
+ publishDate: metadata?.publishDate ? new Date(metadata.publishDate) : void 0
1459
+ };
1460
+ let content = "";
1461
+ if (format === "html") {
1462
+ const element = import_react.default.createElement(import_components.AccessibilityStatement, props);
1463
+ const markup = (0, import_server.renderToStaticMarkup)(element);
1464
+ content = `<!DOCTYPE html>
1465
+ <html lang="${lang}">
1466
+ <head>
1467
+ <meta charset="UTF-8">
1468
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1469
+ <title>Accessibility Statement - ${props.organizationName}</title>
1470
+ <style>
1471
+ body { font-family: system-ui, -apple-system, sans-serif; background: #f8f9fa; padding: 2rem; margin: 0; }
1472
+ </style>
1473
+ </head>
1474
+ <body>
1475
+ ${markup}
1476
+ </body>
1477
+ </html>`;
1478
+ } else {
1479
+ const dateStr = props.lastReviewDate.toISOString().split("T")[0];
1480
+ const statusMap = {
1481
+ "full": "Full",
1482
+ "partial": "Partial",
1483
+ "non-compliant": "Non-Compliant"
1484
+ };
1485
+ content = `# Accessibility Statement for ${props.organizationName}
1486
+
1487
+ This accessibility statement applies to [${props.websiteUrl}](${props.websiteUrl}).
1488
+
1489
+ ## Compliance Status
1490
+
1491
+ **Status:** ${statusMap[complianceLevel] || complianceLevel}
1492
+
1493
+ This website is ${complianceLevel} compliant with ${sector === "public" ? "EN 301 549 (WAD)" : "EAA"}.
1494
+
1495
+ ## Non-accessible Content
1496
+
1497
+ The following content is non-accessible for the following reasons:
1498
+
1499
+ ${nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `- ${item}`).join("\n") : "_No known issues._"}
1500
+
1501
+ ## Preparation of this statement
1502
+
1503
+ This statement was prepared on ${dateStr}.
1504
+ Method used: **Automated Scan** via @holmdigital/engine.
1505
+
1506
+ ## Feedback and contact information
1507
+
1508
+ If you need information on this website in a different format like accessible PDF, large print, easy-to-read, audio recording or braille:
1509
+
1510
+ - email: [${props.contactEmail}](mailto:${props.contactEmail})
1511
+
1512
+ ## Enforcement procedure
1513
+
1514
+ The enforcement body for ${country} is responsible for enforcing these regulations.
1515
+ `;
1516
+ }
1517
+ return content;
1518
+ }
1519
+ async function generateStatement(result, outputPath, lang = "en", format = "html", metadata) {
1520
+ const content = await generateStatementContent(result, lang, format, metadata);
1521
+ const dir = import_path.default.dirname(outputPath);
1522
+ await import_promises.default.mkdir(dir, { recursive: true });
1523
+ await import_promises.default.writeFile(outputPath, content, "utf-8");
1524
+ }
1525
+
1075
1526
  // src/cli/index.ts
1076
1527
  init_i18n();
1077
1528
 
@@ -1155,6 +1606,7 @@ async function sendToCloud(config, result) {
1155
1606
  }
1156
1607
 
1157
1608
  // src/cli/index.ts
1609
+ var import_cosmiconfig = require("cosmiconfig");
1158
1610
  function isValidUrl(urlString) {
1159
1611
  try {
1160
1612
  const url = new URL(urlString);
@@ -1165,22 +1617,46 @@ function isValidUrl(urlString) {
1165
1617
  }
1166
1618
  var program = new import_commander.Command();
1167
1619
  program.name("hd-a11y-scan").description("HolmDigital Regulatory Scanner").version("0.1.0");
1168
- program.argument("<url>", "URL to scan").option("--lang <code>", "Language code (en, sv)", "en").option("--ci", "Run in CI/CD mode (exit code 1 on critical failures)").option("--generate-tests", "Generate Pseudo-Automation tests").option("--json", "Output as JSON").option("--pdf <path>", "Generate PDF report to path").option("--viewport <size>", 'Set viewport (e.g. "mobile", "desktop", "1024x768")').option("--threshold <level>", "Severity threshold for compliance (critical, high, medium, low)", "high").option("--api-key <key>", "API key for HolmDigital Cloud authentication").option("--cloud-url <url>", "Cloud API URL", "https://cloud.holmdigital.se").option("--invalid-https-cert", "Allow scanning on pages with invalid https certificate").action(async (url, options) => {
1620
+ program.argument("<url>", "URL to scan").option("--lang <code>", "Language code (en, sv)").option("--ci", "Run in CI/CD mode (exit code 1 on critical failures)").option("--generate-tests", "Generate Pseudo-Automation tests").option("--json", "Output as JSON").option("--junit <path>", "Generate JUnit XML report").option("--pdf <path>", "Generate PDF report to path").option("--statement <path>", "Generate accessibility statement HTML to path").option("--format <type>", "Output format for statement (html, md)", "html").option("--viewport <size>", 'Set viewport (e.g. "mobile", "desktop", "1024x768")').option("--threshold <level>", "Severity threshold (critical, high, medium, low)").option("--api-key <key>", "API key for HolmDigital Cloud").option("--cloud-url <url>", "Cloud API URL").option("--invalid-https-cert", "Allow scanning on pages with invalid https certificate").option("--email <email>", "Contact email for accessibility statement").option("--phone <number>", "Contact phone number for accessibility statement").option("--org <name>", "Organization name for accessibility statement").option("--response-time <time>", "Expected response time for accessibility statement").option("--country <code>", "Country code for accessibility statement enforcement body").option("--publish-date <date>", "Publish date for the website (YYYY-MM-DD)").action(async (url, cliOptions) => {
1621
+ const explorer = (0, import_cosmiconfig.cosmiconfig)("a11y");
1622
+ const configResult = await explorer.search();
1623
+ const fileConfig = configResult ? configResult.config : {};
1624
+ const options = {
1625
+ lang: cliOptions.lang || fileConfig.lang || "en",
1626
+ ci: cliOptions.ci ?? fileConfig.ci ?? false,
1627
+ generateTests: cliOptions.generateTests ?? fileConfig.generateTests ?? false,
1628
+ json: cliOptions.json ?? fileConfig.json ?? false,
1629
+ junit: cliOptions.junit || fileConfig.junit,
1630
+ pdf: cliOptions.pdf || fileConfig.pdf,
1631
+ statement: cliOptions.statement || fileConfig.statement,
1632
+ format: cliOptions.format || fileConfig.format || "html",
1633
+ viewport: cliOptions.viewport || fileConfig.viewport,
1634
+ // Handled specifically below
1635
+ threshold: cliOptions.threshold || fileConfig.threshold || "high",
1636
+ apiKey: cliOptions.apiKey || fileConfig.apiKey,
1637
+ cloudUrl: cliOptions.cloudUrl || fileConfig.cloudUrl || "https://cloud.holmdigital.se",
1638
+ invalidHttpsCert: cliOptions.invalidHttpsCert ?? fileConfig.invalidHttpsCert ?? false,
1639
+ // Metadata for accessibility statement
1640
+ email: cliOptions.email || fileConfig.email,
1641
+ phone: cliOptions.phone || fileConfig.phone,
1642
+ org: cliOptions.org || fileConfig.org,
1643
+ responseTime: cliOptions.responseTime || fileConfig.responseTime,
1644
+ country: cliOptions.country || fileConfig.country,
1645
+ publishDate: cliOptions.publishDate || fileConfig.publishDate
1646
+ };
1169
1647
  setLanguage(options.lang);
1170
1648
  if (!isValidUrl(url)) {
1171
- console.error(import_chalk.default.red(`Error: Invalid URL format '${url}'`));
1172
- console.error(import_chalk.default.gray("URL must start with http:// or https://"));
1173
- process.exit(1);
1174
1649
  }
1175
1650
  if (!options.json) {
1176
1651
  console.log(import_chalk.default.blue.bold(t("cli.title")));
1652
+ if (configResult) console.log(import_chalk.default.gray(`Loaded config from ${configResult.filepath}`));
1177
1653
  console.log(import_chalk.default.gray(t("cli.scanning", { url })));
1178
1654
  }
1179
1655
  const spinner = !options.json ? (0, import_ora.default)(t("cli.initializing")).start() : null;
1180
1656
  let scanner;
1181
1657
  try {
1182
1658
  let viewport = { width: 1280, height: 720 };
1183
- if (options.viewport) {
1659
+ if (options.viewport && typeof options.viewport === "string") {
1184
1660
  if (options.viewport === "mobile") viewport = { width: 375, height: 667 };
1185
1661
  else if (options.viewport === "desktop") viewport = { width: 1920, height: 1080 };
1186
1662
  else if (options.viewport === "tablet") viewport = { width: 768, height: 1024 };
@@ -1188,6 +1664,8 @@ program.argument("<url>", "URL to scan").option("--lang <code>", "Language code
1188
1664
  const [w, h] = options.viewport.split("x").map(Number);
1189
1665
  if (w && h) viewport = { width: w, height: h };
1190
1666
  }
1667
+ } else if (options.viewport && typeof options.viewport === "object") {
1668
+ viewport = options.viewport;
1191
1669
  }
1192
1670
  scanner = new RegulatoryScanner({
1193
1671
  url,
@@ -1207,15 +1685,30 @@ program.argument("<url>", "URL to scan").option("--lang <code>", "Language code
1207
1685
  await generatePDF(html, options.pdf);
1208
1686
  if (spinner) spinner.succeed(t("cli.pdf_saved", { path: options.pdf }));
1209
1687
  }
1688
+ if (options.statement) {
1689
+ if (spinner) spinner.start(t("cli.generating_statement", { path: options.statement }));
1690
+ const metadata = {
1691
+ contactEmail: options.email,
1692
+ phoneNumber: options.phone,
1693
+ organizationName: options.org,
1694
+ responseTime: options.responseTime,
1695
+ country: options.country,
1696
+ publishDate: options.publishDate
1697
+ };
1698
+ if (options.statement.toLowerCase().endsWith(".pdf")) {
1699
+ const htmlContent = await generateStatementContent(result, options.lang, "html", metadata);
1700
+ await generatePDF(htmlContent, options.statement);
1701
+ } else {
1702
+ await generateStatement(result, options.statement, options.lang, options.format, metadata);
1703
+ }
1704
+ if (spinner) spinner.succeed(t("cli.statement_saved", { path: options.statement }));
1705
+ }
1210
1706
  if (options.json) {
1211
1707
  console.log(JSON.stringify(result, null, 2));
1212
1708
  } else {
1213
- console.log(import_chalk.default.bold(t("cli.score", { score: result.score })));
1214
- const statusColor = result.complianceStatus === "PASS" ? import_chalk.default.green : import_chalk.default.red;
1215
- console.log(statusColor.bold(t("cli.status", { status: result.complianceStatus })));
1216
- if (result.complianceStatus === "FAIL") {
1217
- console.log(import_chalk.default.red(t("cli.not_compliant")));
1218
- }
1709
+ console.log("\n");
1710
+ const scoreColor = result.score >= 90 ? import_chalk.default.green : result.score >= 70 ? import_chalk.default.yellow : import_chalk.default.red;
1711
+ console.log(import_chalk.default.bold(`[ Compliance Score: ${scoreColor(result.score + "/100")} ] ${result.score >= 90 ? "\u{1F7E2}" : result.score >= 70 ? "\u{1F7E1}" : "\u{1F534}"}`));
1219
1712
  if (result.score === 100) {
1220
1713
  const badge = generateBadgeMarkdown(result.score);
1221
1714
  if (badge) {
@@ -1224,46 +1717,87 @@ program.argument("<url>", "URL to scan").option("--lang <code>", "Language code
1224
1717
  }
1225
1718
  }
1226
1719
  console.log(import_chalk.default.gray("----------------------------------------"));
1227
- if (options.viewport) {
1228
- console.log(import_chalk.default.blue(t("cli.viewport", { width: viewport.width, height: viewport.height })));
1720
+ const calculateCategoryScore = (filterFn) => {
1721
+ const failures = result.reports.filter(filterFn).length;
1722
+ return Math.max(10, 100 - failures * 20);
1723
+ };
1724
+ const cats = [
1725
+ {
1726
+ name: "HTML Structure",
1727
+ score: calculateCategoryScore((r) => ["1.3.1", "4.1.1", "4.1.2"].some((c) => r.wcagCriteria.includes(c))),
1728
+ critical: false
1729
+ },
1730
+ {
1731
+ name: "Keyboard Nav ",
1732
+ score: calculateCategoryScore((r) => ["2.1.1", "2.1.2", "2.4.3", "2.4.7"].some((c) => r.wcagCriteria.includes(c))),
1733
+ critical: result.reports.some((r) => ["2.1.1", "2.1.2"].some((c) => r.wcagCriteria.includes(c)) && r.holmdigitalInsight.diggRisk === "critical")
1734
+ },
1735
+ {
1736
+ name: "Contrast ",
1737
+ score: calculateCategoryScore((r) => r.wcagCriteria.includes("1.4.3") || r.ruleId === "color-contrast"),
1738
+ critical: false
1739
+ }
1740
+ ];
1741
+ cats.forEach((cat) => {
1742
+ const width = 10;
1743
+ const filled = Math.round(cat.score / 100 * width);
1744
+ const empty = width - filled;
1745
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
1746
+ let statusText = `${cat.score}%`;
1747
+ if (cat.critical) statusText += import_chalk.default.red(" (CRITICAL FAIL)");
1748
+ else if (cat.score < 100) statusText += import_chalk.default.gray(" (Minor issues)");
1749
+ console.log(`${cat.name} [${cat.score >= 80 ? import_chalk.default.green(bar) : cat.score >= 50 ? import_chalk.default.yellow(bar) : import_chalk.default.red(bar)}] ${statusText}`);
1750
+ });
1751
+ console.log(import_chalk.default.gray("----------------------------------------"));
1752
+ let legalRisk = "LOW";
1753
+ let riskReason = "No critical violations found.";
1754
+ let riskIcon = "\u2705";
1755
+ if (result.legalSummary && result.legalSummary.eaaDeadlineViolations > 0) {
1756
+ legalRisk = "HIGH";
1757
+ const hasKeyboardIssues = result.reports.some((r) => ["2.1.1", "2.1.2"].some((c) => r.wcagCriteria.includes(c)));
1758
+ const hasContrastIssues = result.reports.some((r) => r.wcagCriteria.includes("1.4.3") || r.ruleId === "color-contrast");
1759
+ const hasStructureIssues = result.reports.some((r) => r.wcagCriteria.includes("1.3.1"));
1760
+ if (hasKeyboardIssues) riskReason = "Keyboard accessibility blocks EAA compliance";
1761
+ else if (hasContrastIssues) riskReason = "Contrast issues block EAA compliance";
1762
+ else if (hasStructureIssues) riskReason = "HTML structure issues block EAA compliance";
1763
+ else riskReason = `${result.legalSummary.eaaDeadlineViolations} EAA deadline violation(s) found`;
1764
+ riskIcon = "\u2696\uFE0F ";
1765
+ } else if (result.stats.critical > 0) {
1766
+ legalRisk = "MEDIUM";
1767
+ riskReason = "Critical violations found.";
1768
+ riskIcon = "\u26A0\uFE0F ";
1229
1769
  }
1770
+ console.log(`${riskIcon} Legal Risk: ${legalRisk === "HIGH" ? import_chalk.default.red.bold(legalRisk) : legalRisk === "MEDIUM" ? import_chalk.default.yellow.bold(legalRisk) : import_chalk.default.green(legalRisk)} (${import_chalk.default.white(riskReason)})`);
1771
+ console.log("\n");
1230
1772
  if (result.htmlValidation && !result.htmlValidation.valid) {
1231
- console.log(import_chalk.default.red.bold("\n\u26A0\uFE0F Structural HTML Issues Detected"));
1773
+ console.log(import_chalk.default.red.bold("\u26A0\uFE0F Structural HTML Issues Detected"));
1232
1774
  console.log(import_chalk.default.yellow(" These issues may affect accessibility tool accuracy (e.g. contrast calculations)\n"));
1233
1775
  result.htmlValidation.errors.forEach((error) => {
1234
1776
  console.log(import_chalk.default.red(` [${error.rule}] ${error.message}`));
1235
- if (error.selector) console.log(import_chalk.default.gray(` ${error.selector}`));
1236
- console.log(import_chalk.default.gray(` Line: ${error.line}, Col: ${error.column}
1237
- `));
1238
1777
  });
1778
+ console.log(import_chalk.default.gray(" ... and more (run with --json for full details)"));
1239
1779
  console.log(import_chalk.default.gray("----------------------------------------"));
1240
1780
  }
1241
- result.reports.forEach((report) => {
1781
+ if (result.reports.length > 0) {
1782
+ console.log(import_chalk.default.bold("Top Violations:"));
1783
+ }
1784
+ result.reports.forEach((report, i) => {
1785
+ if (i > 5) return;
1242
1786
  const color = report.holmdigitalInsight.diggRisk === "critical" ? import_chalk.default.red : import_chalk.default.yellow;
1243
1787
  console.log(color.bold(`
1244
1788
  [${report.holmdigitalInsight.diggRisk.toUpperCase()}] ${report.ruleId}`));
1245
1789
  console.log(import_chalk.default.white(`WCAG: ${report.wcagCriteria} | EN 301 549: ${report.en301549Criteria}`));
1246
- console.log(import_chalk.default.gray(`Legitimitet: ${report.dosLagenReference}`));
1247
1790
  if (report.remediation.component) {
1248
- console.log(import_chalk.default.green(t("cli.prescriptive_fix")));
1249
- console.log(t("cli.use_component", { component: import_chalk.default.bold(report.remediation.component) }));
1250
- }
1251
- if (report.failingNodes && report.failingNodes.length > 0) {
1252
- console.log(import_chalk.default.gray("\nAffected Elements:"));
1253
- report.failingNodes.forEach((node, index) => {
1254
- if (index < 5) {
1255
- console.log(import_chalk.default.cyan(`\u279C ${node.target}`));
1256
- console.log(import_chalk.default.gray(` ${node.html}`));
1257
- }
1258
- });
1259
- if (report.failingNodes.length > 5) {
1260
- console.log(import_chalk.default.gray(` ...and ${report.failingNodes.length - 5} more`));
1261
- }
1791
+ console.log(import_chalk.default.green(`Fix: Use component <${report.remediation.component} />`));
1262
1792
  }
1263
1793
  });
1794
+ if (result.reports.length > 5) {
1795
+ console.log(import_chalk.default.gray(`
1796
+ ... and ${result.reports.length - 5} more issues.`));
1797
+ }
1264
1798
  console.log(import_chalk.default.gray("\n----------------------------------------"));
1265
- console.log(`Critical: ${result.stats.critical} | High: ${result.stats.high} | Medium: ${result.stats.medium} | Total: ${result.stats.total}
1266
- `);
1799
+ console.log(`Scan Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`);
1800
+ console.log("\n");
1267
1801
  if (options.generateTests) {
1268
1802
  console.log(import_chalk.default.magenta.bold(t("cli.pseudo_tests")));
1269
1803
  const automation = new PseudoAutomationEngine();
@@ -1275,6 +1809,18 @@ program.argument("<url>", "URL to scan").option("--lang <code>", "Language code
1275
1809
  });
1276
1810
  }
1277
1811
  }
1812
+ if (options.ci && result.reports.length > 0) {
1813
+ const { generateGitHubActionsAnnotations: generateGitHubActionsAnnotations2 } = await Promise.resolve().then(() => (init_github_actions(), github_actions_exports));
1814
+ generateGitHubActionsAnnotations2(result.reports);
1815
+ }
1816
+ if (options.junit) {
1817
+ const { generateJUnitXML: generateJUnitXML2 } = await Promise.resolve().then(() => (init_junit_generator(), junit_generator_exports));
1818
+ const fs2 = await import("fs/promises");
1819
+ const xml = generateJUnitXML2(result.reports, url, result.metadata.scanDuration);
1820
+ await fs2.writeFile(options.junit, xml);
1821
+ if (spinner) spinner.succeed(t("cli.junit_saved", { path: options.junit }));
1822
+ else if (!options.json) console.log(import_chalk.default.green(`\u2713 JUnit report saved to ${options.junit}`));
1823
+ }
1278
1824
  if (options.ci && result.stats.critical > 0) {
1279
1825
  if (!options.json) console.error(import_chalk.default.red(t("cli.critical_failure")));
1280
1826
  process.exit(1);