diff --git a/.jules/palette.md b/.jules/palette.md index ee48ab0ea..1463b6968 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -47,3 +47,7 @@ ## 2025-02-12 - Exposing Unrounded Numeric Representations for Formatted Floats **Learning:** In data-heavy HTML reports, formatting floats to a fixed number of significant digits can obscure the original Python float representation. A native `title` tooltip can help pointer users inspect that unrounded representation, but it is not a reliable keyboard, touch, or assistive-technology disclosure mechanism. **Action:** Add the unrounded Python float representation as a supplemental native `title` tooltip on formatted values. Preserve the report's accessible exact-value disclosure and JSON/CSV exports as the authoritative non-hover paths; never claim that `title` alone provides accessibility. + +## 2025-02-13 - Empty States ARIA Live Regions +**Learning:** In HTML reports, simply using generic tags like `

` for empty states (such as an empty list of triggers) fails to proactively announce the absence of data to screen reader users navigating dynamically. +**Action:** Always use `

` for empty states to ensure they act as ARIA live regions and are reliably announced by assistive technologies when content is evaluated or loaded. diff --git a/python/fast_mlsirm/scoring/essay/validation_report_html.py b/python/fast_mlsirm/scoring/essay/validation_report_html.py index 3c8eea6d3..ee3e1b148 100644 --- a/python/fast_mlsirm/scoring/essay/validation_report_html.py +++ b/python/fast_mlsirm/scoring/essay/validation_report_html.py @@ -74,8 +74,7 @@ def _validated_report( category_count=report.category_count, paired_observation_count=report.paired_observation_count, metrics=tuple( - _replay_metric(metric, index) - for index, metric in enumerate(report.metrics) + _replay_metric(metric, index) for index, metric in enumerate(report.metrics) ), review_trigger_ids=report.review_trigger_ids, metadata=report.metadata, @@ -108,7 +107,7 @@ def _identifier_list( ) -> str: """Render identifier evidence as a semantic list or explicit empty state.""" if not identifiers: - return f'

{escape(empty_message)}

' + return f'
{escape(empty_message)}
' items = "".join( f"
  • {escape(identifier)}
  • " for identifier in identifiers ) @@ -191,9 +190,9 @@ def _render_html(report: EssayValidationEvidenceReport, title: str) -> str: '
    ', '

    Human interpretation required

    ', f'

    {escape(_VALIDITY_NOTICE)}

    ', - '

    Review triggers

    ', + "

    Review triggers

    ", triggers, - '

    Interpretation boundaries

    ', + "

    Interpretation boundaries

    ", boundaries, "
    ", '
    ', @@ -206,7 +205,7 @@ def _render_html(report: EssayValidationEvidenceReport, title: str) -> str: "
    ", '
    ', '

    Canonical JSON

    ', - '

    The complete deterministic evidence payload is available below for audit reconstruction.

    ', + "

    The complete deterministic evidence payload is available below for audit reconstruction.

    ", '
    ',
                 _canonical_json(report),
                 "
    ", @@ -236,9 +235,7 @@ def render_essay_validation_evidence_report_html( if output.suffix.lower() != ".html": raise ValueError("essay validation evidence output path must end with .html") if title is not None and (not isinstance(title, str) or not title.strip()): - raise ValueError( - "essay validation evidence title must be a non-empty string" - ) + raise ValueError("essay validation evidence title must be a non-empty string") resolved_title = _DEFAULT_TITLE if title is None else title output.parent.mkdir(parents=True, exist_ok=True) output.write_text(_render_html(validated, resolved_title), encoding="utf-8") diff --git a/tests/test_scoring_essay_validation_report_html.py b/tests/test_scoring_essay_validation_report_html.py index 27a5739c2..97e12ac88 100644 --- a/tests/test_scoring_essay_validation_report_html.py +++ b/tests/test_scoring_essay_validation_report_html.py @@ -80,18 +80,24 @@ def test_custom_title_and_identifiers_are_escaped(tmp_path: Path) -> None: assert "<img src=x onerror="alert(1)">" in html assert "",), - empty_message="No review trigger.", - ) == '' + assert ( + validation_report_html._identifier_list( + ("review_",), + empty_message="No review trigger.", + ) + == '
    • review_<unsafe>
    ' + ) def test_empty_identifier_list_renders_explicit_state() -> None: """An empty evidence list remains understandable without hidden state.""" - assert validation_report_html._identifier_list( - (), - empty_message="No boundary is available.", - ) == '

    No boundary is available.

    ' + assert ( + validation_report_html._identifier_list( + (), + empty_message="No boundary is available.", + ) + == '
    No boundary is available.
    ' + ) def test_renderer_rejects_wrong_type_and_wrong_suffix(tmp_path: Path) -> None: