From 2d5850ff94f9718b3e256d20dd62a2567794b06e Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 7 Jul 2026 19:00:11 +0000 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20HTML=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=EB=A9=94=ED=8A=B8=EB=A6=AD=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=EC=9D=98=20=EC=A0=91=EA=B7=BC=EC=84=B1=20=EB=B0=8F=20?= =?UTF-8?q?UX=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 메트릭 카드의 HTML 구조를 제네릭한 `
` 대신 `
`, `
`, `
`를 사용하여 스크린 리더에서 의미론적(semantic) 맥락을 유지하도록 개선 - 브라우저 간 기본 `
` 스타일의 불일치를 해결하기 위해 명시적인 CSS `font-weight` 초기화 코드 추가 - 마우스 사용자의 시각적 경험 저하를 방지하기 위해 키보드 포커스 시에만 외곽선이 표시되도록 CSS 선택자를 `:focus`에서 `:focus-visible`로 변경 - 스크린 리더 사용자에게 불필요한 시각적 차트 요소의 음성 출력을 방지하기 위해 `aria-hidden="true"` 속성 추가 - `.jules/palette.md`에 관련된 UX 및 접근성 최적화 지식 기록 - 부수적인 파이썬 사용하지 않는 모듈(import) 정리 및 ruff 린팅 적용 --- .jules/palette.md | 4 ++++ python/fast_mlsirm/report.py | 28 ++++++++++++---------- scripts/build_benchmark_report.py | 1 - scripts/build_buyer_packet.py | 1 - scripts/build_procurement_due_diligence.py | 1 - scripts/build_release_evidence_index.py | 1 - tests/test_diagnostics.py | 6 ++--- tests/test_io.py | 2 -- tests/test_math.py | 1 - tests/test_objective.py | 8 ++----- 10 files changed, 25 insertions(+), 28 deletions(-) create mode 100644 .jules/palette.md diff --git a/.jules/palette.md b/.jules/palette.md new file mode 100644 index 0000000..d7c3c92 --- /dev/null +++ b/.jules/palette.md @@ -0,0 +1,4 @@ +## 2024-07-07 - Add definition lists to Fit Report metric cards + +**Learning:** Key-value pairs for metrics and statistics in HTML reports must use semantic `
`, `
`, and `
` elements to properly associate the label with its value for screen readers. Using generic `
` or `
` with `` and `` separates the semantic relationship. Additionally, default `
` styling varies across browsers, so explicit `font-weight` resets are necessary to maintain cross-browser visual consistency while improving semantics. Also, relying purely on mouse interactions for focused tables (`:focus`) impacts mouse users visually - `:focus-visible` ensures focus outlines appear only during keyboard navigation. Purely visual charting elements (like horizontal bar tracks) inside HTML need `aria-hidden="true"` to prevent redundant, noisy announcements for screen reader users. +**Action:** Always prefer definition lists (`
`) over generic markup for metric pairs, and explicitly reset their font-weight in CSS. Use `:focus-visible` instead of `:focus` for generic focus styles, and add `aria-hidden="true"` to decorative/visual chart tracks. diff --git a/python/fast_mlsirm/report.py b/python/fast_mlsirm/report.py index 9d70dcb..7cc9460 100644 --- a/python/fast_mlsirm/report.py +++ b/python/fast_mlsirm/report.py @@ -69,7 +69,7 @@ def _render_html(payload: dict[str, Any], report_type: str, title: str, source_n "
", '
', '
', - f"

fast-mlsirm diagnostics

", + "

fast-mlsirm diagnostics

", f"

{escape(title)}

", f"Source: {escape(source_name)}", "
", @@ -165,10 +165,10 @@ def _metric_section(heading: str, metrics: dict[str, Any]) -> str | None: cards.append( "\n".join( [ - '
', - f"{escape(_label(key))}", - f"{escape(_format_value(value))}", - "
", + '
', + f"
{escape(_label(key))}
", + f"
{escape(_format_value(value))}
", + "
", ] ) ) @@ -179,9 +179,9 @@ def _metric_section(heading: str, metrics: dict[str, Any]) -> str | None: [ '
', f"

{escape(heading)}

", - '
', + '
', *cards, - "
", + "
", "", ] ) @@ -265,7 +265,7 @@ def _bar_chart(rows: list[dict[str, Any]], value_key: str | None) -> str: [ '
', f'{escape(_row_label(row, index))}', - '
', + '", f'{escape(_format_value(value))}', @@ -477,6 +477,7 @@ def _css() -> str: display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; + margin: 0; } .metric-card { @@ -487,16 +488,19 @@ def _css() -> str: background: #fafafa; } -.metric-card span { +.metric-card dt { display: block; color: var(--muted); font-size: 0.82rem; + font-weight: normal; + margin: 0; } -.metric-card strong { +.metric-card dd { display: block; - margin-top: 8px; + margin: 8px 0 0 0; font-size: 1.45rem; + font-weight: bold; overflow-wrap: anywhere; } @@ -574,7 +578,7 @@ def _css() -> str: border-radius: 8px; } -.table-wrap:focus { +.table-wrap:focus-visible { outline: 3px solid #0f766e; outline-offset: 3px; } diff --git a/scripts/build_benchmark_report.py b/scripts/build_benchmark_report.py index ddd37b4..d87b6b3 100644 --- a/scripts/build_benchmark_report.py +++ b/scripts/build_benchmark_report.py @@ -7,7 +7,6 @@ import hashlib import json import subprocess -import sys from datetime import UTC, datetime from html import escape from pathlib import Path diff --git a/scripts/build_buyer_packet.py b/scripts/build_buyer_packet.py index 5d0512b..90ef338 100644 --- a/scripts/build_buyer_packet.py +++ b/scripts/build_buyer_packet.py @@ -7,7 +7,6 @@ import hashlib import json import subprocess -import sys import zipfile from datetime import UTC, datetime from html import escape diff --git a/scripts/build_procurement_due_diligence.py b/scripts/build_procurement_due_diligence.py index 2caaa35..16127cc 100644 --- a/scripts/build_procurement_due_diligence.py +++ b/scripts/build_procurement_due_diligence.py @@ -7,7 +7,6 @@ import hashlib import json import subprocess -import sys import tarfile import zipfile from datetime import UTC, datetime diff --git a/scripts/build_release_evidence_index.py b/scripts/build_release_evidence_index.py index e79971a..208dd03 100644 --- a/scripts/build_release_evidence_index.py +++ b/scripts/build_release_evidence_index.py @@ -7,7 +7,6 @@ import hashlib import json import subprocess -import sys from datetime import UTC, datetime from html import escape from pathlib import Path diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py index 030431c..ee5d0df 100644 --- a/tests/test_diagnostics.py +++ b/tests/test_diagnostics.py @@ -1,13 +1,16 @@ import numpy as np +import pytest from fast_mlsirm import FitConfig, MLS2PLMConfig, simulate from fast_mlsirm.diagnostics import ( + align_latent_space, dimensionality_diagnostics, fit_diagnostics, predict_proba, response_process_dimensionality_diagnostics, response_process_fit_diagnostics, ) +from fast_mlsirm.types import MLSIRMParams def test_predict_proba_matches_simulation(): @@ -41,9 +44,6 @@ def test_predict_proba_subset_both(): probs = predict_proba(data.truth, data.factor_id, persons=sub_persons, items=sub_items) assert np.allclose(probs, data.probabilities[np.ix_(sub_persons, sub_items)]) -import pytest -from fast_mlsirm.diagnostics import align_latent_space, predict_proba -from fast_mlsirm.types import MLSIRMParams def test_align_latent_space_invalid_method(): with pytest.raises(ValueError, match="only procrustes alignment is supported"): diff --git a/tests/test_io.py b/tests/test_io.py index 88d62a9..f492988 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -1,7 +1,5 @@ import pytest import numpy as np -from pathlib import Path -import json from fast_mlsirm.io import load_factor_csv, load_params diff --git a/tests/test_math.py b/tests/test_math.py index 3b9dc1b..742f3a9 100644 --- a/tests/test_math.py +++ b/tests/test_math.py @@ -1,5 +1,4 @@ import numpy as np -import pytest from fast_mlsirm.math import logit, normalize_latent_positions, sigmoid, softplus, standardize from fast_mlsirm.types import MLSIRMParams diff --git a/tests/test_objective.py b/tests/test_objective.py index 70eb90e..c55ad4c 100644 --- a/tests/test_objective.py +++ b/tests/test_objective.py @@ -2,7 +2,8 @@ import pytest from fast_mlsirm import FitConfig, MLSIRMParams -from fast_mlsirm.objective import neg_loglik_and_grad, validate_factor_id +from fast_mlsirm.objective import neg_loglik_and_grad, validate_factor_id, prepare_response, _add_penalty +from fast_mlsirm.config import PenaltyConfig def test_missing_entries_are_excluded(): @@ -127,11 +128,6 @@ def test_validate_factor_id(): validate_factor_id([0, 2, 0], n_items=3, n_dims=2) -import pytest -from fast_mlsirm.objective import prepare_response, _add_penalty -from fast_mlsirm.config import PenaltyConfig - - def test_prepare_response_errors(): with pytest.raises(ValueError, match="responses must be a 2D matrix"): prepare_response(np.array([1.0, 0.0])) From 9ff24b2919ec16b8e2923f7718e9dfdd3e2b2463 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 8 Jul 2026 09:21:07 +0000 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20HTML=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=EB=A9=94=ED=8A=B8=EB=A6=AD=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=EC=9D=98=20=EC=A0=91=EA=B7=BC=EC=84=B1=20=EB=B0=8F=20?= =?UTF-8?q?UX=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 메트릭 카드의 HTML 구조를 제네릭한 `
` 대신 `
`, `
`, `
`를 사용하여 스크린 리더에서 의미론적(semantic) 맥락을 유지하도록 개선 - 브라우저 간 기본 `
` 스타일의 불일치를 해결하기 위해 명시적인 CSS `font-weight` 초기화 코드 추가 - 마우스 사용자의 시각적 경험 저하를 방지하기 위해 키보드 포커스 시에만 외곽선이 표시되도록 CSS 선택자를 `:focus`에서 `:focus-visible`로 변경 - 스크린 리더 사용자에게 불필요한 시각적 차트 요소의 음성 출력을 방지하기 위해 `aria-hidden="true"` 속성 추가 - `.jules/palette.md`에 관련된 UX 및 접근성 최적화 지식 기록 - 사용하지 않는 모듈(import) 정리 및 ruff 포매팅 적용 From de543b7bd494d12d168de84f2c26638868dd0e58 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 9 Jul 2026 06:48:02 +0000 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20HTML=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=EB=A9=94=ED=8A=B8=EB=A6=AD=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=EC=9D=98=20=EC=A0=91=EA=B7=BC=EC=84=B1=20=EB=B0=8F=20?= =?UTF-8?q?UX=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 메트릭 카드의 HTML 구조를 제네릭한 `
` 대신 `
`, `
`, `
`를 사용하여 스크린 리더에서 의미론적(semantic) 맥락을 유지하도록 개선 - 브라우저 간 기본 `
` 스타일의 불일치를 해결하기 위해 명시적인 CSS `font-weight` 초기화 코드 추가 - 마우스 사용자의 시각적 경험 저하를 방지하기 위해 키보드 포커스 시에만 외곽선이 표시되도록 CSS 선택자를 `:focus`에서 `:focus-visible`로 변경 - 스크린 리더 사용자에게 불필요한 시각적 차트 요소의 음성 출력을 방지하기 위해 `aria-hidden="true"` 속성 추가 - `.jules/palette.md`에 관련된 UX 및 접근성 최적화 지식 기록 --- scripts/build_benchmark_report.py | 1 + scripts/build_buyer_packet.py | 1 + scripts/build_procurement_due_diligence.py | 1 + scripts/build_release_evidence_index.py | 1 + tests/test_diagnostics.py | 6 +++--- tests/test_io.py | 2 ++ tests/test_math.py | 1 + tests/test_objective.py | 8 ++++++-- 8 files changed, 16 insertions(+), 5 deletions(-) diff --git a/scripts/build_benchmark_report.py b/scripts/build_benchmark_report.py index d87b6b3..ddd37b4 100644 --- a/scripts/build_benchmark_report.py +++ b/scripts/build_benchmark_report.py @@ -7,6 +7,7 @@ import hashlib import json import subprocess +import sys from datetime import UTC, datetime from html import escape from pathlib import Path diff --git a/scripts/build_buyer_packet.py b/scripts/build_buyer_packet.py index 90ef338..5d0512b 100644 --- a/scripts/build_buyer_packet.py +++ b/scripts/build_buyer_packet.py @@ -7,6 +7,7 @@ import hashlib import json import subprocess +import sys import zipfile from datetime import UTC, datetime from html import escape diff --git a/scripts/build_procurement_due_diligence.py b/scripts/build_procurement_due_diligence.py index 16127cc..2caaa35 100644 --- a/scripts/build_procurement_due_diligence.py +++ b/scripts/build_procurement_due_diligence.py @@ -7,6 +7,7 @@ import hashlib import json import subprocess +import sys import tarfile import zipfile from datetime import UTC, datetime diff --git a/scripts/build_release_evidence_index.py b/scripts/build_release_evidence_index.py index 208dd03..e79971a 100644 --- a/scripts/build_release_evidence_index.py +++ b/scripts/build_release_evidence_index.py @@ -7,6 +7,7 @@ import hashlib import json import subprocess +import sys from datetime import UTC, datetime from html import escape from pathlib import Path diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py index ee5d0df..030431c 100644 --- a/tests/test_diagnostics.py +++ b/tests/test_diagnostics.py @@ -1,16 +1,13 @@ import numpy as np -import pytest from fast_mlsirm import FitConfig, MLS2PLMConfig, simulate from fast_mlsirm.diagnostics import ( - align_latent_space, dimensionality_diagnostics, fit_diagnostics, predict_proba, response_process_dimensionality_diagnostics, response_process_fit_diagnostics, ) -from fast_mlsirm.types import MLSIRMParams def test_predict_proba_matches_simulation(): @@ -44,6 +41,9 @@ def test_predict_proba_subset_both(): probs = predict_proba(data.truth, data.factor_id, persons=sub_persons, items=sub_items) assert np.allclose(probs, data.probabilities[np.ix_(sub_persons, sub_items)]) +import pytest +from fast_mlsirm.diagnostics import align_latent_space, predict_proba +from fast_mlsirm.types import MLSIRMParams def test_align_latent_space_invalid_method(): with pytest.raises(ValueError, match="only procrustes alignment is supported"): diff --git a/tests/test_io.py b/tests/test_io.py index f492988..88d62a9 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -1,5 +1,7 @@ import pytest import numpy as np +from pathlib import Path +import json from fast_mlsirm.io import load_factor_csv, load_params diff --git a/tests/test_math.py b/tests/test_math.py index 742f3a9..3b9dc1b 100644 --- a/tests/test_math.py +++ b/tests/test_math.py @@ -1,4 +1,5 @@ import numpy as np +import pytest from fast_mlsirm.math import logit, normalize_latent_positions, sigmoid, softplus, standardize from fast_mlsirm.types import MLSIRMParams diff --git a/tests/test_objective.py b/tests/test_objective.py index c55ad4c..70eb90e 100644 --- a/tests/test_objective.py +++ b/tests/test_objective.py @@ -2,8 +2,7 @@ import pytest from fast_mlsirm import FitConfig, MLSIRMParams -from fast_mlsirm.objective import neg_loglik_and_grad, validate_factor_id, prepare_response, _add_penalty -from fast_mlsirm.config import PenaltyConfig +from fast_mlsirm.objective import neg_loglik_and_grad, validate_factor_id def test_missing_entries_are_excluded(): @@ -128,6 +127,11 @@ def test_validate_factor_id(): validate_factor_id([0, 2, 0], n_items=3, n_dims=2) +import pytest +from fast_mlsirm.objective import prepare_response, _add_penalty +from fast_mlsirm.config import PenaltyConfig + + def test_prepare_response_errors(): with pytest.raises(ValueError, match="responses must be a 2D matrix"): prepare_response(np.array([1.0, 0.0])) From 890b0b3df7afd7fbd37be2ba9082435b98db340f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 9 Jul 2026 20:06:29 +0900 Subject: [PATCH 4/7] test: assert report accessibility markup --- tests/test_report.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_report.py b/tests/test_report.py index 0b29e0e..fa6e3f4 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -31,6 +31,9 @@ def test_render_fit_diagnostics_report_has_sections(tmp_path): html = out.read_text(encoding="utf-8") assert "Example Fit" in html assert "Model Fit" in html + assert '
' in html + assert "
Loglik
" in html + assert "
-3.2
" in html assert "Item Fit" in html assert "Diagnostics Coverage" in html assert "No row data" in html @@ -225,4 +228,5 @@ def test_render_table_region_has_keyboard_focus_style(tmp_path): html = out.read_text(encoding="utf-8") assert 'aria-label="Candidate Comparison diagnostics table"' in html assert 'tabindex="0"' in html - assert ".table-wrap:focus" in html + assert ".table-wrap:focus-visible" in html + assert '