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
« 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.
3This module provides professional formatting standards, visual emphasis
4systems, and executive summary generation for TraceKit reports.
7Example:
8 >>> from tracekit.reporting.standards import FormatStandards, VisualEmphasis
9 >>> standards = FormatStandards()
10 >>> emphasis = VisualEmphasis()
11 >>> formatted_text = emphasis.format_pass_fail(True)
13References:
14 REPORT-001, REPORT-002,
15"""
17from __future__ import annotations
19from dataclasses import dataclass, field
20from enum import Enum
21from typing import Any, Literal
24class Severity(Enum):
25 """Severity levels for findings and violations."""
27 CRITICAL = "critical"
28 WARNING = "warning"
29 INFO = "info"
32class ColorScheme(Enum):
33 """Available colorblind-safe color schemes."""
35 VIRIDIS = "viridis"
36 CIVIDIS = "cividis"
37 PLASMA = "plasma"
38 INFERNO = "inferno"
39 OKABE_ITO = "okabe_ito" # Colorblind-safe palette
42@dataclass
43class FormatStandards:
44 """Professional formatting standards configuration.
46 Defines typography, page layout, color schemes, and section hierarchy
47 for professional report output.
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).
60 References:
61 REPORT-001: Professional Formatting Standards
62 """
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
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
81 def to_css(self) -> str:
82 """Generate CSS stylesheet from format standards.
84 Returns:
85 CSS stylesheet string.
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;
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}}
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}}
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}}
130h1 {{ font-size: var(--h1-size); }}
131h2 {{ font-size: var(--h2-size); }}
132h3 {{ font-size: var(--h3-size); }}
134.report-title {{
135 font-size: var(--title-size);
136 text-align: center;
137 margin-bottom: 2em;
138}}
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}}
148/* Table styles */
149table {{
150 border-collapse: collapse;
151 width: 100%;
152 margin: 1em 0;
153}}
155th, td {{
156 border: 1px solid #ddd;
157 padding: 8px;
158 text-align: left;
159}}
161th {{
162 background-color: #f2f2f2;
163 font-weight: bold;
164}}
166tr:nth-child(even) {{
167 background-color: #f9f9f9;
168}}
170/* Pass/Fail indicators (REPORT-002) */
171.pass {{
172 color: var(--color-pass);
173}}
175.fail {{
176 color: var(--color-fail);
177}}
179.warning {{
180 color: var(--color-warning);
181}}
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}}
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}}
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}}
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}}
214.callout.key-finding {{
215 border-color: var(--color-info);
216 background-color: var(--color-info-bg);
217}}
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}}
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}}
234.executive-summary h2 {{
235 margin-top: 0;
236}}
238.key-findings {{
239 list-style-type: none;
240 padding-left: 0;
241}}
243.key-findings li {{
244 padding: 5px 0;
245 padding-left: 25px;
246 position: relative;
247}}
249.key-findings li::before {{
250 content: "";
251 position: absolute;
252 left: 0;
253 top: 8px;
254 width: 16px;
255 height: 16px;
256}}
258.key-findings li.critical::before {{
259 content: "!";
260 color: var(--color-fail);
261 font-weight: bold;
262}}
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}}
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"""
291@dataclass
292class VisualEmphasis:
293 """Visual emphasis system for pass/fail indicators and severity levels.
295 Provides WCAG-compliant visual indicators using both symbols and colors
296 for accessibility.
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.
304 References:
305 REPORT-002: Visual Emphasis System
306 """
308 use_unicode_symbols: bool = True
309 colorblind_safe: bool = True
310 highlight_violations: bool = True
311 severity_icons: bool = True
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
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.
329 Args:
330 passed: Whether the test passed.
331 with_text: Include PASS/FAIL text.
332 html: Output as HTML with styling.
334 Returns:
335 Formatted status string.
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"
349 result = f"{symbol} {text}".strip() if with_text else symbol
351 if html:
352 return f'<span class="{css_class}">{result}</span>'
353 return result
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.
364 Args:
365 severity: Severity level.
366 message: Message text.
367 html: Output as HTML with styling.
369 Returns:
370 Formatted message string.
372 References:
373 REPORT-002: Visual Emphasis System
374 """
375 if isinstance(severity, str):
376 severity = Severity(severity.lower())
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"
388 text = f"{symbol} {message}".strip() if symbol else message
390 if html:
391 return f'<div class="{css_class}">{text}</div>'
392 return text
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.
404 Color coding:
405 - Green: margin > 20%
406 - Yellow: 10% < margin <= 20%
407 - Red: margin <= 10%
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.
415 Returns:
416 Formatted margin string.
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
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
446 text = f"{symbol} margin: {margin_pct:.1f}% ({status})"
448 if html:
449 return f'<span class="{css_class}">{text}</span>'
450 return text
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.
461 Args:
462 title: Box title.
463 content: Box content.
464 is_key_finding: Style as key finding.
466 Returns:
467 HTML callout box string.
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>"""
479@dataclass
480class ExecutiveSummary:
481 """Executive summary of analysis results.
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.
492 References:
493 REPORT-004: Executive Summary Auto-Generation
494 """
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 = ""
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", [])
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")
522 # Add margin information
523 margins = results.get("margins", [])
524 min_margin = min(margins) if margins else results.get("min_margin")
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})")
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')}")
535 return key_findings[:max_findings], min_margin
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
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.")
562 # Add critical violations
563 if critical_violations:
564 parts.append(f"Critical: {len(critical_violations)} violation(s) require immediate action.")
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}%.")
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)
575 return " ".join(parts)
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.
586 Automatically extracts key findings, pass/fail status, and critical
587 violations from analysis results.
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).
594 Returns:
595 ExecutiveSummary with generated content.
597 Example:
598 >>> results = {"pass_count": 10, "total_count": 12, "violations": [...]}
599 >>> summary = generate_executive_summary(results)
600 >>> print(summary.summary_text)
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
611 # Extract violations
612 violations = results.get("violations", [])
613 critical_violations = [v for v in violations if v.get("severity", "").lower() == "critical"]
615 # Extract key findings
616 key_findings, min_margin = _extract_key_findings(results, critical_violations, max_findings)
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 )
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 )
640def format_executive_summary_html(summary: ExecutiveSummary) -> str:
641 """Format executive summary as HTML.
643 Args:
644 summary: ExecutiveSummary to format.
646 Returns:
647 HTML string.
649 References:
650 REPORT-004: Executive Summary Auto-Generation
651 """
652 emphasis = VisualEmphasis()
654 status_html = emphasis.format_pass_fail(summary.overall_status, html=True)
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>'
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>"""
672__all__ = [
673 "ColorScheme",
674 "ExecutiveSummary",
675 "FormatStandards",
676 "Severity",
677 "VisualEmphasis",
678 "format_executive_summary_html",
679 "generate_executive_summary",
680]