Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<p>` 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 `<div class="empty-state" role="status">` for empty states to ensure they act as ARIA live regions and are reliably announced by assistive technologies when content is evaluated or loaded.
15 changes: 6 additions & 9 deletions python/fast_mlsirm/scoring/essay/validation_report_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -108,7 +107,7 @@ def _identifier_list(
) -> str:
"""Render identifier evidence as a semantic list or explicit empty state."""
if not identifiers:
return f'<p class="empty-state">{escape(empty_message)}</p>'
return f'<div class="empty-state" role="status">{escape(empty_message)}</div>'
items = "".join(
f"<li><code>{escape(identifier)}</code></li>" for identifier in identifiers
)
Expand Down Expand Up @@ -191,9 +190,9 @@ def _render_html(report: EssayValidationEvidenceReport, title: str) -> str:
'<section class="review-required" aria-labelledby="review-heading">',
'<h2 id="review-heading">Human interpretation required</h2>',
f'<p class="notice">{escape(_VALIDITY_NOTICE)}</p>',
'<h3>Review triggers</h3>',
"<h3>Review triggers</h3>",
triggers,
'<h3>Interpretation boundaries</h3>',
"<h3>Interpretation boundaries</h3>",
boundaries,
"</section>",
'<section aria-labelledby="provenance-heading">',
Expand All @@ -206,7 +205,7 @@ def _render_html(report: EssayValidationEvidenceReport, title: str) -> str:
"</section>",
'<section aria-labelledby="json-heading">',
'<h2 id="json-heading">Canonical JSON</h2>',
'<p>The complete deterministic evidence payload is available below for audit reconstruction.</p>',
"<p>The complete deterministic evidence payload is available below for audit reconstruction.</p>",
'<pre tabindex="0" role="region" aria-label="Canonical essay validation evidence JSON">',
_canonical_json(report),
"</pre>",
Expand Down Expand Up @@ -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")
Expand Down
22 changes: 14 additions & 8 deletions tests/test_scoring_essay_validation_report_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,24 @@ def test_custom_title_and_identifiers_are_escaped(tmp_path: Path) -> None:

assert "&lt;img src=x onerror=&quot;alert(1)&quot;&gt;" in html
assert "<img src=x" not in html
assert validation_report_html._identifier_list(
("review_<unsafe>",),
empty_message="No review trigger.",
) == '<ul class="trigger-list"><li><code>review_&lt;unsafe&gt;</code></li></ul>'
assert (
validation_report_html._identifier_list(
("review_<unsafe>",),
empty_message="No review trigger.",
)
== '<ul class="trigger-list"><li><code>review_&lt;unsafe&gt;</code></li></ul>'
)


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.",
) == '<p class="empty-state">No boundary is available.</p>'
assert (
validation_report_html._identifier_list(
(),
empty_message="No boundary is available.",
)
== '<div class="empty-state" role="status">No boundary is available.</div>'
)


def test_renderer_rejects_wrong_type_and_wrong_suffix(tmp_path: Path) -> None:
Expand Down
Loading