Coverage for src / tracekit / reporting / standards.py: 28%

164 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 23:04 +0000

1"""Professional formatting standards and visual emphasis for reports. 

2 

3This module provides professional formatting standards, visual emphasis 

4systems, and executive summary generation for TraceKit reports. 

5 

6 

7Example: 

8 >>> from tracekit.reporting.standards import FormatStandards, VisualEmphasis 

9 >>> standards = FormatStandards() 

10 >>> emphasis = VisualEmphasis() 

11 >>> formatted_text = emphasis.format_pass_fail(True) 

12 

13References: 

14 REPORT-001, REPORT-002, 

15""" 

16 

17from __future__ import annotations 

18 

19from dataclasses import dataclass, field 

20from enum import Enum 

21from typing import Any, Literal 

22 

23 

24class Severity(Enum): 

25 """Severity levels for findings and violations.""" 

26 

27 CRITICAL = "critical" 

28 WARNING = "warning" 

29 INFO = "info" 

30 

31 

32class ColorScheme(Enum): 

33 """Available colorblind-safe color schemes.""" 

34 

35 VIRIDIS = "viridis" 

36 CIVIDIS = "cividis" 

37 PLASMA = "plasma" 

38 INFERNO = "inferno" 

39 OKABE_ITO = "okabe_ito" # Colorblind-safe palette 

40 

41 

42@dataclass 

43class FormatStandards: 

44 """Professional formatting standards configuration. 

45 

46 Defines typography, page layout, color schemes, and section hierarchy 

47 for professional report output. 

48 

49 Attributes: 

50 heading_font: Font family for headings. 

51 body_font: Font family for body text. 

52 code_font: Font family for code and data. 

53 page_size: Page size (letter or A4). 

54 margins_inches: Page margins in inches. 

55 color_scheme: Colorblind-safe color palette. 

56 line_spacing: Line spacing multiplier. 

57 logo_max_height_inches: Maximum logo height. 

58 watermark_opacity: Watermark opacity (0.0-1.0). 

59 

60 References: 

61 REPORT-001: Professional Formatting Standards 

62 """ 

63 

64 heading_font: str = "Arial, Helvetica, sans-serif" 

65 body_font: str = "Georgia, Times New Roman, serif" 

66 code_font: str = "Consolas, Courier New, monospace" 

67 page_size: Literal["letter", "A4"] = "letter" 

68 margins_inches: float = 1.0 

69 color_scheme: ColorScheme = ColorScheme.VIRIDIS 

70 line_spacing: float = 1.5 

71 logo_max_height_inches: float = 2.0 

72 watermark_opacity: float = 0.3 

73 

74 # Font sizes in points 

75 title_size: int = 24 

76 h1_size: int = 18 

77 h2_size: int = 14 

78 h3_size: int = 12 

79 body_size: int = 10 

80 

81 def to_css(self) -> str: 

82 """Generate CSS stylesheet from format standards. 

83 

84 Returns: 

85 CSS stylesheet string. 

86 

87 References: 

88 REPORT-001: Professional Formatting Standards 

89 """ 

90 return f""" 

91/* TraceKit Professional Report Styles - REPORT-001 */ 

92:root {{ 

93 --heading-font: {self.heading_font}; 

94 --body-font: {self.body_font}; 

95 --code-font: {self.code_font}; 

96 --title-size: {self.title_size}pt; 

97 --h1-size: {self.h1_size}pt; 

98 --h2-size: {self.h2_size}pt; 

99 --h3-size: {self.h3_size}pt; 

100 --body-size: {self.body_size}pt; 

101 --line-spacing: {self.line_spacing}; 

102 --margin: {self.margins_inches}in; 

103 

104 /* Colorblind-safe palette */ 

105 --color-pass: #2e7d32; 

106 --color-fail: #c62828; 

107 --color-warning: #f57c00; 

108 --color-info: #1565c0; 

109 --color-critical-bg: #ffebee; 

110 --color-warning-bg: #fff3e0; 

111 --color-info-bg: #e3f2fd; 

112}} 

113 

114body {{ 

115 font-family: var(--body-font); 

116 font-size: var(--body-size); 

117 line-height: var(--line-spacing); 

118 margin: var(--margin); 

119 max-width: 8.5in; 

120 color: #333; 

121}} 

122 

123h1, h2, h3, h4 {{ 

124 font-family: var(--heading-font); 

125 line-height: 1.2; 

126 margin-top: 1.5em; 

127 margin-bottom: 0.5em; 

128}} 

129 

130h1 {{ font-size: var(--h1-size); }} 

131h2 {{ font-size: var(--h2-size); }} 

132h3 {{ font-size: var(--h3-size); }} 

133 

134.report-title {{ 

135 font-size: var(--title-size); 

136 text-align: center; 

137 margin-bottom: 2em; 

138}} 

139 

140code, pre {{ 

141 font-family: var(--code-font); 

142 font-size: 0.9em; 

143 background-color: #f5f5f5; 

144 padding: 2px 4px; 

145 border-radius: 3px; 

146}} 

147 

148/* Table styles */ 

149table {{ 

150 border-collapse: collapse; 

151 width: 100%; 

152 margin: 1em 0; 

153}} 

154 

155th, td {{ 

156 border: 1px solid #ddd; 

157 padding: 8px; 

158 text-align: left; 

159}} 

160 

161th {{ 

162 background-color: #f2f2f2; 

163 font-weight: bold; 

164}} 

165 

166tr:nth-child(even) {{ 

167 background-color: #f9f9f9; 

168}} 

169 

170/* Pass/Fail indicators (REPORT-002) */ 

171.pass {{ 

172 color: var(--color-pass); 

173}} 

174 

175.fail {{ 

176 color: var(--color-fail); 

177}} 

178 

179.warning {{ 

180 color: var(--color-warning); 

181}} 

182 

183/* Severity indicators (REPORT-002) */ 

184.severity-critical {{ 

185 background-color: var(--color-critical-bg); 

186 border-left: 4px solid var(--color-fail); 

187 padding: 10px; 

188 margin: 10px 0; 

189}} 

190 

191.severity-warning {{ 

192 background-color: var(--color-warning-bg); 

193 border-left: 4px solid var(--color-warning); 

194 padding: 10px; 

195 margin: 10px 0; 

196}} 

197 

198.severity-info {{ 

199 background-color: var(--color-info-bg); 

200 border-left: 4px solid var(--color-info); 

201 padding: 10px; 

202 margin: 10px 0; 

203}} 

204 

205/* Callout box (REPORT-002) */ 

206.callout {{ 

207 border: 1px solid #ddd; 

208 border-radius: 4px; 

209 padding: 15px; 

210 margin: 15px 0; 

211 background-color: #fafafa; 

212}} 

213 

214.callout.key-finding {{ 

215 border-color: var(--color-info); 

216 background-color: var(--color-info-bg); 

217}} 

218 

219/* Highlighting for out-of-spec values */ 

220.out-of-spec {{ 

221 background-color: rgba(255, 235, 59, 0.15); 

222 padding: 2px 4px; 

223 border-radius: 2px; 

224}} 

225 

226/* Executive summary styles (REPORT-004) */ 

227.executive-summary {{ 

228 background-color: #f5f5f5; 

229 padding: 20px; 

230 margin: 20px 0; 

231 border-radius: 4px; 

232}} 

233 

234.executive-summary h2 {{ 

235 margin-top: 0; 

236}} 

237 

238.key-findings {{ 

239 list-style-type: none; 

240 padding-left: 0; 

241}} 

242 

243.key-findings li {{ 

244 padding: 5px 0; 

245 padding-left: 25px; 

246 position: relative; 

247}} 

248 

249.key-findings li::before {{ 

250 content: ""; 

251 position: absolute; 

252 left: 0; 

253 top: 8px; 

254 width: 16px; 

255 height: 16px; 

256}} 

257 

258.key-findings li.critical::before {{ 

259 content: "!"; 

260 color: var(--color-fail); 

261 font-weight: bold; 

262}} 

263 

264/* Watermark */ 

265.watermark {{ 

266 position: fixed; 

267 top: 50%; 

268 left: 50%; 

269 transform: translate(-50%, -50%) rotate(-45deg); 

270 font-size: 72pt; 

271 color: rgba(0, 0, 0, {self.watermark_opacity}); 

272 pointer-events: none; 

273 z-index: 1000; 

274}} 

275 

276/* Print styles */ 

277@media print {{ 

278 body {{ 

279 margin: 0; 

280 }} 

281 .page-break {{ 

282 page-break-before: always; 

283 }} 

284 .no-print {{ 

285 display: none; 

286 }} 

287}} 

288""" 

289 

290 

291@dataclass 

292class VisualEmphasis: 

293 """Visual emphasis system for pass/fail indicators and severity levels. 

294 

295 Provides WCAG-compliant visual indicators using both symbols and colors 

296 for accessibility. 

297 

298 Attributes: 

299 use_unicode_symbols: Use Unicode check/X marks. 

300 colorblind_safe: Always use symbols + colors (never color alone). 

301 highlight_violations: Highlight out-of-spec values. 

302 severity_icons: Show icons for severity levels. 

303 

304 References: 

305 REPORT-002: Visual Emphasis System 

306 """ 

307 

308 use_unicode_symbols: bool = True 

309 colorblind_safe: bool = True 

310 highlight_violations: bool = True 

311 severity_icons: bool = True 

312 

313 # Unicode symbols for status indicators 

314 CHECK_SYMBOL = "\u2713" # Check mark 

315 CROSS_SYMBOL = "\u2717" # X mark 

316 WARNING_SYMBOL = "\u26a0" # Warning triangle 

317 INFO_SYMBOL = "\u2139" # Info circle 

318 CRITICAL_SYMBOL = "\u2757" # Exclamation mark 

319 

320 def format_pass_fail( 

321 self, 

322 passed: bool, 

323 *, 

324 with_text: bool = True, 

325 html: bool = False, 

326 ) -> str: 

327 """Format pass/fail status with visual emphasis. 

328 

329 Args: 

330 passed: Whether the test passed. 

331 with_text: Include PASS/FAIL text. 

332 html: Output as HTML with styling. 

333 

334 Returns: 

335 Formatted status string. 

336 

337 References: 

338 REPORT-002: Visual Emphasis System 

339 """ 

340 if passed: 

341 symbol = self.CHECK_SYMBOL if self.use_unicode_symbols else "[PASS]" 

342 text = "PASS" if with_text else "" 

343 css_class = "pass" 

344 else: 

345 symbol = self.CROSS_SYMBOL if self.use_unicode_symbols else "[FAIL]" 

346 text = "FAIL" if with_text else "" 

347 css_class = "fail" 

348 

349 result = f"{symbol} {text}".strip() if with_text else symbol 

350 

351 if html: 

352 return f'<span class="{css_class}">{result}</span>' 

353 return result 

354 

355 def format_severity( 

356 self, 

357 severity: Severity | str, 

358 message: str, 

359 *, 

360 html: bool = False, 

361 ) -> str: 

362 """Format message with severity indicator. 

363 

364 Args: 

365 severity: Severity level. 

366 message: Message text. 

367 html: Output as HTML with styling. 

368 

369 Returns: 

370 Formatted message string. 

371 

372 References: 

373 REPORT-002: Visual Emphasis System 

374 """ 

375 if isinstance(severity, str): 

376 severity = Severity(severity.lower()) 

377 

378 if severity == Severity.CRITICAL: 

379 symbol = self.CRITICAL_SYMBOL if self.severity_icons else "" 

380 css_class = "severity-critical" 

381 elif severity == Severity.WARNING: 

382 symbol = self.WARNING_SYMBOL if self.severity_icons else "" 

383 css_class = "severity-warning" 

384 else: 

385 symbol = self.INFO_SYMBOL if self.severity_icons else "" 

386 css_class = "severity-info" 

387 

388 text = f"{symbol} {message}".strip() if symbol else message 

389 

390 if html: 

391 return f'<div class="{css_class}">{text}</div>' 

392 return text 

393 

394 def format_margin( 

395 self, 

396 value: float, 

397 limit: float, 

398 *, 

399 limit_type: Literal["upper", "lower"] = "upper", 

400 html: bool = False, 

401 ) -> str: 

402 """Format margin with color-coded indicator. 

403 

404 Color coding: 

405 - Green: margin > 20% 

406 - Yellow: 10% < margin <= 20% 

407 - Red: margin <= 10% 

408 

409 Args: 

410 value: Measured value. 

411 limit: Limit value. 

412 limit_type: Whether limit is upper or lower bound. 

413 html: Output as HTML with styling. 

414 

415 Returns: 

416 Formatted margin string. 

417 

418 References: 

419 REPORT-002: Visual Emphasis System 

420 """ 

421 if limit_type == "upper": 

422 margin = limit - value 

423 margin_pct = (margin / limit * 100) if limit != 0 else 0 

424 else: 

425 margin = value - limit 

426 margin_pct = (margin / limit * 100) if limit != 0 else 0 

427 

428 # Determine status 

429 if margin_pct > 20: 

430 status = "good" 

431 css_class = "pass" 

432 symbol = self.PASS_SYMBOL # type: ignore[attr-defined] 

433 elif margin_pct > 10: 

434 status = "marginal" 

435 css_class = "warning" 

436 symbol = self.WARNING_SYMBOL 

437 elif margin_pct > 0: 

438 status = "tight" 

439 css_class = "warning" 

440 symbol = self.WARNING_SYMBOL 

441 else: 

442 status = "violation" 

443 css_class = "fail" 

444 symbol = self.CROSS_SYMBOL 

445 

446 text = f"{symbol} margin: {margin_pct:.1f}% ({status})" 

447 

448 if html: 

449 return f'<span class="{css_class}">{text}</span>' 

450 return text 

451 

452 def create_callout_box( 

453 self, 

454 title: str, 

455 content: str, 

456 *, 

457 is_key_finding: bool = False, 

458 ) -> str: 

459 """Create a callout box for key findings. 

460 

461 Args: 

462 title: Box title. 

463 content: Box content. 

464 is_key_finding: Style as key finding. 

465 

466 Returns: 

467 HTML callout box string. 

468 

469 References: 

470 REPORT-002: Visual Emphasis System 

471 """ 

472 css_class = "callout key-finding" if is_key_finding else "callout" 

473 return f"""<div class="{css_class}"> 

474<h4>{title}</h4> 

475<p>{content}</p> 

476</div>""" 

477 

478 

479@dataclass 

480class ExecutiveSummary: 

481 """Executive summary of analysis results. 

482 

483 Attributes: 

484 overall_status: Pass/fail status. 

485 pass_count: Number of passing tests. 

486 total_count: Total number of tests. 

487 key_findings: List of key findings. 

488 critical_violations: List of critical violations. 

489 min_margin_pct: Minimum margin percentage. 

490 summary_text: Generated summary text. 

491 

492 References: 

493 REPORT-004: Executive Summary Auto-Generation 

494 """ 

495 

496 overall_status: bool 

497 pass_count: int 

498 total_count: int 

499 key_findings: list[str] = field(default_factory=list) 

500 critical_violations: list[str] = field(default_factory=list) 

501 min_margin_pct: float | None = None 

502 summary_text: str = "" 

503 

504 

505def _extract_key_findings( 

506 results: dict[str, Any], 

507 critical_violations: list[Any], 

508 max_findings: int, 

509) -> tuple[list[str], float | None]: 

510 """Extract key findings from results.""" 

511 key_findings: list[str] = [] 

512 violations = results.get("violations", []) 

513 

514 # Add violation summary 

515 if critical_violations: 

516 key_findings.append( 

517 f"{len(critical_violations)} critical violation(s) require immediate attention" 

518 ) 

519 elif violations: 

520 key_findings.append(f"{len(violations)} violation(s) detected") 

521 

522 # Add margin information 

523 margins = results.get("margins", []) 

524 min_margin = min(margins) if margins else results.get("min_margin") 

525 

526 if min_margin is not None and min_margin < 20: 

527 status = "critical" if min_margin < 10 else "marginal" 

528 key_findings.append(f"Minimum margin is {min_margin:.1f}% ({status})") 

529 

530 # Extract from failed measurements 

531 for name, meas in results.get("measurements", {}).items(): 

532 if not meas.get("passed", True): 

533 key_findings.append(f"{name}: FAIL - {meas.get('message', 'violation')}") 

534 

535 return key_findings[:max_findings], min_margin 

536 

537 

538def _build_summary_text( 

539 overall_status: bool, 

540 total_count: int, 

541 fail_count: int, 

542 critical_violations: list[Any], 

543 min_margin: float | None, 

544 key_findings: list[str], 

545 length: str, 

546) -> str: 

547 """Build the summary text.""" 

548 parts: list[str] = [] 

549 pass_count = total_count - fail_count 

550 

551 # First sentence: overall status 

552 if overall_status and total_count > 0: 

553 parts.append(f"All {pass_count} tests passed.") 

554 elif overall_status: 

555 parts.append("Analysis completed successfully.") 

556 elif total_count > 0: 

557 pct = fail_count / total_count * 100 

558 parts.append(f"{fail_count} of {total_count} tests failed ({pct:.0f}% failure rate).") 

559 else: 

560 parts.append("Analysis completed with failures.") 

561 

562 # Add critical violations 

563 if critical_violations: 

564 parts.append(f"Critical: {len(critical_violations)} violation(s) require immediate action.") 

565 

566 # Add margin note 

567 if min_margin is not None and min_margin < 10: 

568 parts.append(f"Warning: Minimum margin is only {min_margin:.1f}%.") 

569 

570 # Key findings (for detailed mode) 

571 if length == "detailed" and key_findings: 

572 parts.append("\nKey Findings:") 

573 parts.extend(f" - {finding}" for finding in key_findings) 

574 

575 return " ".join(parts) 

576 

577 

578def generate_executive_summary( 

579 results: dict[str, Any], 

580 *, 

581 max_findings: int = 5, 

582 length: Literal["short", "detailed"] = "short", 

583) -> ExecutiveSummary: 

584 """Generate executive summary from analysis results. 

585 

586 Automatically extracts key findings, pass/fail status, and critical 

587 violations from analysis results. 

588 

589 Args: 

590 results: Analysis results dictionary. 

591 max_findings: Maximum number of key findings to include. 

592 length: Summary length (short = 1 paragraph, detailed = 1 page). 

593 

594 Returns: 

595 ExecutiveSummary with generated content. 

596 

597 Example: 

598 >>> results = {"pass_count": 10, "total_count": 12, "violations": [...]} 

599 >>> summary = generate_executive_summary(results) 

600 >>> print(summary.summary_text) 

601 

602 References: 

603 REPORT-004: Executive Summary Auto-Generation 

604 """ 

605 # Extract basic counts 

606 pass_count = results.get("pass_count", 0) 

607 total_count = results.get("total_count", 0) 

608 fail_count = total_count - pass_count if total_count else 0 

609 overall_status = fail_count == 0 

610 

611 # Extract violations 

612 violations = results.get("violations", []) 

613 critical_violations = [v for v in violations if v.get("severity", "").lower() == "critical"] 

614 

615 # Extract key findings 

616 key_findings, min_margin = _extract_key_findings(results, critical_violations, max_findings) 

617 

618 # Generate summary text 

619 summary_text = _build_summary_text( 

620 overall_status, 

621 total_count, 

622 fail_count, 

623 critical_violations, 

624 min_margin, 

625 key_findings, 

626 length, 

627 ) 

628 

629 return ExecutiveSummary( 

630 overall_status=overall_status, 

631 pass_count=pass_count, 

632 total_count=total_count, 

633 key_findings=key_findings, 

634 critical_violations=[str(v) for v in critical_violations], 

635 min_margin_pct=min_margin, 

636 summary_text=summary_text, 

637 ) 

638 

639 

640def format_executive_summary_html(summary: ExecutiveSummary) -> str: 

641 """Format executive summary as HTML. 

642 

643 Args: 

644 summary: ExecutiveSummary to format. 

645 

646 Returns: 

647 HTML string. 

648 

649 References: 

650 REPORT-004: Executive Summary Auto-Generation 

651 """ 

652 emphasis = VisualEmphasis() 

653 

654 status_html = emphasis.format_pass_fail(summary.overall_status, html=True) 

655 

656 findings_html = "" 

657 if summary.key_findings: 

658 items = [] 

659 for finding in summary.key_findings: 

660 css_class = "critical" if "critical" in finding.lower() else "" 

661 items.append(f'<li class="{css_class}">{finding}</li>') 

662 findings_html = f'<ul class="key-findings">{"".join(items)}</ul>' 

663 

664 return f"""<div class="executive-summary"> 

665<h2>Executive Summary</h2> 

666<p><strong>Overall Status:</strong> {status_html}</p> 

667<p>{summary.summary_text}</p> 

668{findings_html} 

669</div>""" 

670 

671 

672__all__ = [ 

673 "ColorScheme", 

674 "ExecutiveSummary", 

675 "FormatStandards", 

676 "Severity", 

677 "VisualEmphasis", 

678 "format_executive_summary_html", 

679 "generate_executive_summary", 

680]