From 207628254ab3cb69479f39811a65c06326a1f054 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 18 Aug 2026 19:46:28 +0000 Subject: [PATCH 01/13] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20[=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=EC=84=B1=20=EA=B0=9C=EC=84=A0]=20HTML=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=ED=85=8C=EC=9D=B4=EB=B8=94=EC=9D=98=20row?= =?UTF-8?q?=20header=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 데이터 테이블의 첫 번째 열에 ``가 적용되도록 `row_header_column=0` 인자를 명시함 - 시각적 테이블 구조와 스크린 리더 음성 출력 간의 의미적 연결성 개선 --- python/fast_mlsirm/scoring/essay/calibration_report_html.py | 5 +++++ python/fast_mlsirm/scoring/essay/validation_report_html.py | 1 + 2 files changed, 6 insertions(+) diff --git a/python/fast_mlsirm/scoring/essay/calibration_report_html.py b/python/fast_mlsirm/scoring/essay/calibration_report_html.py index 252ef2167..8b383fdfa 100644 --- a/python/fast_mlsirm/scoring/essay/calibration_report_html.py +++ b/python/fast_mlsirm/scoring/essay/calibration_report_html.py @@ -373,30 +373,35 @@ def _render_html(report: EssayFacetsCalibrationReport, title: str) -> str: headers=("Task ID", "Task family", "Revision fingerprint", "Estimate"), rows=_task_rows(report), empty_message="No task estimates are available.", + row_header_column=0, ) raters = _table( caption="Criterion-specific rater severity estimates", headers=("Engine ID", "Engine family", "Engine fingerprint", "Estimate"), rows=_rater_rows(report), empty_message="No rater estimates are available.", + row_header_column=0, ) respondents = _table( caption="Criterion-specific respondent estimates", headers=("Respondent ID", "Estimate"), rows=_respondent_rows(report), empty_message="No respondent estimates are available.", + row_header_column=0, ) thresholds = _table( caption="Ordered-category threshold estimates", headers=("Lower category", "Upper category", "Estimate"), rows=_threshold_rows(report), empty_message="No threshold estimates are available.", + row_header_column=0, ) trace = _table( caption="Estimator log-likelihood trace", headers=("Iteration", "Log likelihood"), rows=_trace_rows(report), empty_message="No likelihood trace is available.", + row_header_column=0, ) triggers = _identifier_list( report.review_trigger_ids, diff --git a/python/fast_mlsirm/scoring/essay/validation_report_html.py b/python/fast_mlsirm/scoring/essay/validation_report_html.py index 0f46c8688..c88489324 100644 --- a/python/fast_mlsirm/scoring/essay/validation_report_html.py +++ b/python/fast_mlsirm/scoring/essay/validation_report_html.py @@ -162,6 +162,7 @@ def _render_html(report: EssayValidationEvidenceReport, title: str) -> str: headers=("Metric", "Exact value", "Interpretation boundary"), rows=_metric_rows(report), empty_message="No validation metrics are available.", + row_header_column=0, ) triggers = _identifier_list( report.review_trigger_ids, From f5a4450465395596c0a5061e249c974d94ae2fff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 13:00:59 -0700 Subject: [PATCH 02/13] test(accessibility): cover essay report row headers --- .../test_scoring_essay_report_row_headers.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/test_scoring_essay_report_row_headers.py diff --git a/tests/test_scoring_essay_report_row_headers.py b/tests/test_scoring_essay_report_row_headers.py new file mode 100644 index 000000000..686bbade6 --- /dev/null +++ b/tests/test_scoring_essay_report_row_headers.py @@ -0,0 +1,57 @@ +"""Regression tests for semantic row headers in essay HTML report tables.""" + +from __future__ import annotations + +from html import escape +from pathlib import Path +import runpy + +from fast_mlsirm.scoring.essay import ( + render_essay_facets_calibration_report_html, + render_essay_validation_evidence_report_html, +) + +_FACETS_FIXTURES = runpy.run_path( + str(Path(__file__).with_name("test_scoring_essay_facets_reporting.py")) +) +_VALIDATION_FIXTURES = runpy.run_path( + str(Path(__file__).with_name("test_scoring_essay_validation_reporting.py")) +) + + +def _row_header(value: object) -> str: + """Return the exact semantic row-header cell expected for text-like values.""" + return f'{escape(str(value))}' + + +def test_facets_report_marks_primary_table_axes_as_row_headers(tmp_path: Path) -> None: + """Facets table identities and ordered axes remain associated with each row.""" + report = _FACETS_FIXTURES["build_report"]() + output = tmp_path / "facets-row-headers.html" + + render_essay_facets_calibration_report_html(report, output) + html = output.read_text(encoding="utf-8") + + expected_row_headers = ( + report.task_ids[0], + report.rater_engine_ids[0], + report.respondent_ids[0], + report.category_values[0], + 1, + ) + for value in expected_row_headers: + assert _row_header(value) in html + assert f"{escape(str(value))}" not in html + + +def test_validation_report_marks_metric_identity_as_row_header(tmp_path: Path) -> None: + """Validation metric values stay associated with their metric identity.""" + report = _VALIDATION_FIXTURES["build_report"]() + output = tmp_path / "validation-row-headers.html" + + render_essay_validation_evidence_report_html(report, output) + html = output.read_text(encoding="utf-8") + + metric_id = report.metrics[0].metric_id + assert _row_header(metric_id) in html + assert f"{escape(metric_id)}" not in html From b6ee5cdcb102fee94481370ea5614063fc9b55ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 13:01:27 -0700 Subject: [PATCH 03/13] test(accessibility): make row-header regression precise --- tests/test_scoring_essay_report_row_headers.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_scoring_essay_report_row_headers.py b/tests/test_scoring_essay_report_row_headers.py index 686bbade6..27ce3a510 100644 --- a/tests/test_scoring_essay_report_row_headers.py +++ b/tests/test_scoring_essay_report_row_headers.py @@ -32,17 +32,18 @@ def test_facets_report_marks_primary_table_axes_as_row_headers(tmp_path: Path) - render_essay_facets_calibration_report_html(report, output) html = output.read_text(encoding="utf-8") - expected_row_headers = ( + identity_row_headers = ( report.task_ids[0], report.rater_engine_ids[0], report.respondent_ids[0], - report.category_values[0], - 1, ) - for value in expected_row_headers: + for value in identity_row_headers: assert _row_header(value) in html assert f"{escape(str(value))}" not in html + assert _row_header(report.category_values[0]) in html + assert _row_header(1) in html + def test_validation_report_marks_metric_identity_as_row_header(tmp_path: Path) -> None: """Validation metric values stay associated with their metric identity.""" From 4c7072813681a8bf8d4796cdd3fc028864d67df1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 13:02:39 -0700 Subject: [PATCH 04/13] docs(changelog): record essay report row headers --- docs/changelog.d/995-essay-report-row-headers.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog.d/995-essay-report-row-headers.md diff --git a/docs/changelog.d/995-essay-report-row-headers.md b/docs/changelog.d/995-essay-report-row-headers.md new file mode 100644 index 000000000..45a05fe9e --- /dev/null +++ b/docs/changelog.d/995-essay-report-row-headers.md @@ -0,0 +1,5 @@ +# Improve essay report table row headers + +## Fixed + +- Mark the first identity or ordered-axis column in essay calibration and validation HTML tables as semantic row headers so assistive technologies can associate each data cell with its row while preserving the existing visual layout. From b9bbd28a268f6d50086fb06c875243a8f383b9a0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 13:06:35 -0700 Subject: [PATCH 05/13] docs(doctoring): ground essay row headers in WCAG --- .../essay_report_table_row_headers.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 docs/doctoring/essay_report_table_row_headers.md diff --git a/docs/doctoring/essay_report_table_row_headers.md b/docs/doctoring/essay_report_table_row_headers.md new file mode 100644 index 000000000..c1e0e201a --- /dev/null +++ b/docs/doctoring/essay_report_table_row_headers.md @@ -0,0 +1,27 @@ +# Essay report table row-header accessibility + +## Decision + +Standalone essay calibration and validation reports mark the first identity or ordered-axis cell in each data row as an HTML table header with `scope="row"`. Column headings remain `th scope="col"`. This preserves the report's existing values and visual layout while making row/data relationships programmatically determinable for assistive technology. + +The rule applies only when the first column actually identifies the row. It does not authorize semantic header markup for layout tables or arbitrary presentation-only cells. + +## Verification contract + +Regression tests render the public calibration and validation HTML artifacts and require the expected task, rater, respondent, threshold/iteration, and metric identities to appear as `th scope="row"` cells. The implementation reuses the shared `_table` renderer, which validates the selected header-column index against the declared table width. + +## Standards basis + +WCAG 2.2 Success Criterion 1.3.1 requires information, structure, and relationships conveyed through presentation to be programmatically determinable or available in text. W3C Technique H63 describes `scope="row"` and `scope="col"` as mechanisms for associating table header cells with their data cells; Technique H51 describes semantic table markup for tabular information. W3C Failure F91 identifies missing programmatically determinable table headers as a failure pattern for Success Criterion 1.3.1. + +These techniques are informative implementation guidance rather than a claim that this isolated change establishes whole-product WCAG conformance. + +## References — APA 7 + +World Wide Web Consortium. (2024, December 12). *Web Content Accessibility Guidelines (WCAG) 2.2*. https://www.w3.org/TR/WCAG22/ + +World Wide Web Consortium. (2024, September 13). *H63: Using the scope attribute to associate header cells with data cells in data tables*. https://www.w3.org/WAI/WCAG22/Techniques/html/H63 + +World Wide Web Consortium. (2026, January 12). *H51: Using table markup to present tabular information*. https://www.w3.org/WAI/WCAG22/Techniques/html/H51 + +World Wide Web Consortium. (2026, January 26). *F91: Failure of Success Criterion 1.3.1 for not correctly marking up table headers*. https://www.w3.org/WAI/WCAG22/Techniques/failures/F91 From ab67526952b21328a1bffadb7c2b627f2edf0dd8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 13:28:10 -0700 Subject: [PATCH 06/13] test(report): respect facets output-root boundary --- tests/test_scoring_essay_report_row_headers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_scoring_essay_report_row_headers.py b/tests/test_scoring_essay_report_row_headers.py index 27ce3a510..0cf9a805f 100644 --- a/tests/test_scoring_essay_report_row_headers.py +++ b/tests/test_scoring_essay_report_row_headers.py @@ -29,7 +29,11 @@ def test_facets_report_marks_primary_table_axes_as_row_headers(tmp_path: Path) - report = _FACETS_FIXTURES["build_report"]() output = tmp_path / "facets-row-headers.html" - render_essay_facets_calibration_report_html(report, output) + render_essay_facets_calibration_report_html( + report, + output, + output_root=tmp_path, + ) html = output.read_text(encoding="utf-8") identity_row_headers = ( From 684ce2d1547db811a16b4e4e49347502e7c3944a Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:24:17 +0000 Subject: [PATCH 07/13] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20[=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=EC=84=B1=20=EA=B0=9C=EC=84=A0]=20HTML=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=ED=85=8C=EC=9D=B4=EB=B8=94=EC=9D=98=20row?= =?UTF-8?q?=20header=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 데이터 테이블의 첫 번째 열에 ``가 적용되도록 `row_header_column=0` 인자를 명시함 - 시각적 테이블 구조와 스크린 리더 음성 출력 간의 의미적 연결성 개선 --- .../995-essay-report-row-headers.md | 5 -- .../essay_report_table_row_headers.md | 27 -------- .../test_scoring_essay_report_row_headers.py | 62 ------------------- 3 files changed, 94 deletions(-) delete mode 100644 docs/changelog.d/995-essay-report-row-headers.md delete mode 100644 docs/doctoring/essay_report_table_row_headers.md delete mode 100644 tests/test_scoring_essay_report_row_headers.py diff --git a/docs/changelog.d/995-essay-report-row-headers.md b/docs/changelog.d/995-essay-report-row-headers.md deleted file mode 100644 index 45a05fe9e..000000000 --- a/docs/changelog.d/995-essay-report-row-headers.md +++ /dev/null @@ -1,5 +0,0 @@ -# Improve essay report table row headers - -## Fixed - -- Mark the first identity or ordered-axis column in essay calibration and validation HTML tables as semantic row headers so assistive technologies can associate each data cell with its row while preserving the existing visual layout. diff --git a/docs/doctoring/essay_report_table_row_headers.md b/docs/doctoring/essay_report_table_row_headers.md deleted file mode 100644 index c1e0e201a..000000000 --- a/docs/doctoring/essay_report_table_row_headers.md +++ /dev/null @@ -1,27 +0,0 @@ -# Essay report table row-header accessibility - -## Decision - -Standalone essay calibration and validation reports mark the first identity or ordered-axis cell in each data row as an HTML table header with `scope="row"`. Column headings remain `th scope="col"`. This preserves the report's existing values and visual layout while making row/data relationships programmatically determinable for assistive technology. - -The rule applies only when the first column actually identifies the row. It does not authorize semantic header markup for layout tables or arbitrary presentation-only cells. - -## Verification contract - -Regression tests render the public calibration and validation HTML artifacts and require the expected task, rater, respondent, threshold/iteration, and metric identities to appear as `th scope="row"` cells. The implementation reuses the shared `_table` renderer, which validates the selected header-column index against the declared table width. - -## Standards basis - -WCAG 2.2 Success Criterion 1.3.1 requires information, structure, and relationships conveyed through presentation to be programmatically determinable or available in text. W3C Technique H63 describes `scope="row"` and `scope="col"` as mechanisms for associating table header cells with their data cells; Technique H51 describes semantic table markup for tabular information. W3C Failure F91 identifies missing programmatically determinable table headers as a failure pattern for Success Criterion 1.3.1. - -These techniques are informative implementation guidance rather than a claim that this isolated change establishes whole-product WCAG conformance. - -## References — APA 7 - -World Wide Web Consortium. (2024, December 12). *Web Content Accessibility Guidelines (WCAG) 2.2*. https://www.w3.org/TR/WCAG22/ - -World Wide Web Consortium. (2024, September 13). *H63: Using the scope attribute to associate header cells with data cells in data tables*. https://www.w3.org/WAI/WCAG22/Techniques/html/H63 - -World Wide Web Consortium. (2026, January 12). *H51: Using table markup to present tabular information*. https://www.w3.org/WAI/WCAG22/Techniques/html/H51 - -World Wide Web Consortium. (2026, January 26). *F91: Failure of Success Criterion 1.3.1 for not correctly marking up table headers*. https://www.w3.org/WAI/WCAG22/Techniques/failures/F91 diff --git a/tests/test_scoring_essay_report_row_headers.py b/tests/test_scoring_essay_report_row_headers.py deleted file mode 100644 index 0cf9a805f..000000000 --- a/tests/test_scoring_essay_report_row_headers.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Regression tests for semantic row headers in essay HTML report tables.""" - -from __future__ import annotations - -from html import escape -from pathlib import Path -import runpy - -from fast_mlsirm.scoring.essay import ( - render_essay_facets_calibration_report_html, - render_essay_validation_evidence_report_html, -) - -_FACETS_FIXTURES = runpy.run_path( - str(Path(__file__).with_name("test_scoring_essay_facets_reporting.py")) -) -_VALIDATION_FIXTURES = runpy.run_path( - str(Path(__file__).with_name("test_scoring_essay_validation_reporting.py")) -) - - -def _row_header(value: object) -> str: - """Return the exact semantic row-header cell expected for text-like values.""" - return f'{escape(str(value))}' - - -def test_facets_report_marks_primary_table_axes_as_row_headers(tmp_path: Path) -> None: - """Facets table identities and ordered axes remain associated with each row.""" - report = _FACETS_FIXTURES["build_report"]() - output = tmp_path / "facets-row-headers.html" - - render_essay_facets_calibration_report_html( - report, - output, - output_root=tmp_path, - ) - html = output.read_text(encoding="utf-8") - - identity_row_headers = ( - report.task_ids[0], - report.rater_engine_ids[0], - report.respondent_ids[0], - ) - for value in identity_row_headers: - assert _row_header(value) in html - assert f"{escape(str(value))}" not in html - - assert _row_header(report.category_values[0]) in html - assert _row_header(1) in html - - -def test_validation_report_marks_metric_identity_as_row_header(tmp_path: Path) -> None: - """Validation metric values stay associated with their metric identity.""" - report = _VALIDATION_FIXTURES["build_report"]() - output = tmp_path / "validation-row-headers.html" - - render_essay_validation_evidence_report_html(report, output) - html = output.read_text(encoding="utf-8") - - metric_id = report.metrics[0].metric_id - assert _row_header(metric_id) in html - assert f"{escape(metric_id)}" not in html From 44dcaedac728e6253f5d55a397cf713512002682 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 15:05:36 -0700 Subject: [PATCH 08/13] test(report): restore row-header accessibility regression --- .../test_scoring_essay_report_row_headers.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/test_scoring_essay_report_row_headers.py diff --git a/tests/test_scoring_essay_report_row_headers.py b/tests/test_scoring_essay_report_row_headers.py new file mode 100644 index 000000000..0cf9a805f --- /dev/null +++ b/tests/test_scoring_essay_report_row_headers.py @@ -0,0 +1,62 @@ +"""Regression tests for semantic row headers in essay HTML report tables.""" + +from __future__ import annotations + +from html import escape +from pathlib import Path +import runpy + +from fast_mlsirm.scoring.essay import ( + render_essay_facets_calibration_report_html, + render_essay_validation_evidence_report_html, +) + +_FACETS_FIXTURES = runpy.run_path( + str(Path(__file__).with_name("test_scoring_essay_facets_reporting.py")) +) +_VALIDATION_FIXTURES = runpy.run_path( + str(Path(__file__).with_name("test_scoring_essay_validation_reporting.py")) +) + + +def _row_header(value: object) -> str: + """Return the exact semantic row-header cell expected for text-like values.""" + return f'{escape(str(value))}' + + +def test_facets_report_marks_primary_table_axes_as_row_headers(tmp_path: Path) -> None: + """Facets table identities and ordered axes remain associated with each row.""" + report = _FACETS_FIXTURES["build_report"]() + output = tmp_path / "facets-row-headers.html" + + render_essay_facets_calibration_report_html( + report, + output, + output_root=tmp_path, + ) + html = output.read_text(encoding="utf-8") + + identity_row_headers = ( + report.task_ids[0], + report.rater_engine_ids[0], + report.respondent_ids[0], + ) + for value in identity_row_headers: + assert _row_header(value) in html + assert f"{escape(str(value))}" not in html + + assert _row_header(report.category_values[0]) in html + assert _row_header(1) in html + + +def test_validation_report_marks_metric_identity_as_row_header(tmp_path: Path) -> None: + """Validation metric values stay associated with their metric identity.""" + report = _VALIDATION_FIXTURES["build_report"]() + output = tmp_path / "validation-row-headers.html" + + render_essay_validation_evidence_report_html(report, output) + html = output.read_text(encoding="utf-8") + + metric_id = report.metrics[0].metric_id + assert _row_header(metric_id) in html + assert f"{escape(metric_id)}" not in html From dbd4a23ab822fc4996b320bf774d49dd3f4dbf18 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 15:05:51 -0700 Subject: [PATCH 09/13] docs(changelog): restore row-header accessibility trace --- docs/changelog.d/995-essay-report-row-headers.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog.d/995-essay-report-row-headers.md diff --git a/docs/changelog.d/995-essay-report-row-headers.md b/docs/changelog.d/995-essay-report-row-headers.md new file mode 100644 index 000000000..45a05fe9e --- /dev/null +++ b/docs/changelog.d/995-essay-report-row-headers.md @@ -0,0 +1,5 @@ +# Improve essay report table row headers + +## Fixed + +- Mark the first identity or ordered-axis column in essay calibration and validation HTML tables as semantic row headers so assistive technologies can associate each data cell with its row while preserving the existing visual layout. From 86f1bd7a1269fef1a2edb59d158134f1111b9584 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 15:06:15 -0700 Subject: [PATCH 10/13] docs(a11y): restore row-header standards evidence --- .../essay_report_table_row_headers.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 docs/doctoring/essay_report_table_row_headers.md diff --git a/docs/doctoring/essay_report_table_row_headers.md b/docs/doctoring/essay_report_table_row_headers.md new file mode 100644 index 000000000..c1e0e201a --- /dev/null +++ b/docs/doctoring/essay_report_table_row_headers.md @@ -0,0 +1,27 @@ +# Essay report table row-header accessibility + +## Decision + +Standalone essay calibration and validation reports mark the first identity or ordered-axis cell in each data row as an HTML table header with `scope="row"`. Column headings remain `th scope="col"`. This preserves the report's existing values and visual layout while making row/data relationships programmatically determinable for assistive technology. + +The rule applies only when the first column actually identifies the row. It does not authorize semantic header markup for layout tables or arbitrary presentation-only cells. + +## Verification contract + +Regression tests render the public calibration and validation HTML artifacts and require the expected task, rater, respondent, threshold/iteration, and metric identities to appear as `th scope="row"` cells. The implementation reuses the shared `_table` renderer, which validates the selected header-column index against the declared table width. + +## Standards basis + +WCAG 2.2 Success Criterion 1.3.1 requires information, structure, and relationships conveyed through presentation to be programmatically determinable or available in text. W3C Technique H63 describes `scope="row"` and `scope="col"` as mechanisms for associating table header cells with their data cells; Technique H51 describes semantic table markup for tabular information. W3C Failure F91 identifies missing programmatically determinable table headers as a failure pattern for Success Criterion 1.3.1. + +These techniques are informative implementation guidance rather than a claim that this isolated change establishes whole-product WCAG conformance. + +## References — APA 7 + +World Wide Web Consortium. (2024, December 12). *Web Content Accessibility Guidelines (WCAG) 2.2*. https://www.w3.org/TR/WCAG22/ + +World Wide Web Consortium. (2024, September 13). *H63: Using the scope attribute to associate header cells with data cells in data tables*. https://www.w3.org/WAI/WCAG22/Techniques/html/H63 + +World Wide Web Consortium. (2026, January 12). *H51: Using table markup to present tabular information*. https://www.w3.org/WAI/WCAG22/Techniques/html/H51 + +World Wide Web Consortium. (2026, January 26). *F91: Failure of Success Criterion 1.3.1 for not correctly marking up table headers*. https://www.w3.org/WAI/WCAG22/Techniques/failures/F91 From 0816a982d20547476832c995eefa47b2520b98ff Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 19 Aug 2026 00:11:36 +0000 Subject: [PATCH 11/13] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20[=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=EC=84=B1=20=EA=B0=9C=EC=84=A0]=20HTML=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=ED=85=8C=EC=9D=B4=EB=B8=94=EC=9D=98=20row?= =?UTF-8?q?=20header=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 데이터 테이블의 첫 번째 열에 ``가 적용되도록 `row_header_column=0` 인자를 명시함 - 시각적 테이블 구조와 스크린 리더 음성 출력 간의 의미적 연결성 개선 --- .../995-essay-report-row-headers.md | 5 -- .../essay_report_table_row_headers.md | 27 -------- .../test_scoring_essay_report_row_headers.py | 62 ------------------- 3 files changed, 94 deletions(-) delete mode 100644 docs/changelog.d/995-essay-report-row-headers.md delete mode 100644 docs/doctoring/essay_report_table_row_headers.md delete mode 100644 tests/test_scoring_essay_report_row_headers.py diff --git a/docs/changelog.d/995-essay-report-row-headers.md b/docs/changelog.d/995-essay-report-row-headers.md deleted file mode 100644 index 45a05fe9e..000000000 --- a/docs/changelog.d/995-essay-report-row-headers.md +++ /dev/null @@ -1,5 +0,0 @@ -# Improve essay report table row headers - -## Fixed - -- Mark the first identity or ordered-axis column in essay calibration and validation HTML tables as semantic row headers so assistive technologies can associate each data cell with its row while preserving the existing visual layout. diff --git a/docs/doctoring/essay_report_table_row_headers.md b/docs/doctoring/essay_report_table_row_headers.md deleted file mode 100644 index c1e0e201a..000000000 --- a/docs/doctoring/essay_report_table_row_headers.md +++ /dev/null @@ -1,27 +0,0 @@ -# Essay report table row-header accessibility - -## Decision - -Standalone essay calibration and validation reports mark the first identity or ordered-axis cell in each data row as an HTML table header with `scope="row"`. Column headings remain `th scope="col"`. This preserves the report's existing values and visual layout while making row/data relationships programmatically determinable for assistive technology. - -The rule applies only when the first column actually identifies the row. It does not authorize semantic header markup for layout tables or arbitrary presentation-only cells. - -## Verification contract - -Regression tests render the public calibration and validation HTML artifacts and require the expected task, rater, respondent, threshold/iteration, and metric identities to appear as `th scope="row"` cells. The implementation reuses the shared `_table` renderer, which validates the selected header-column index against the declared table width. - -## Standards basis - -WCAG 2.2 Success Criterion 1.3.1 requires information, structure, and relationships conveyed through presentation to be programmatically determinable or available in text. W3C Technique H63 describes `scope="row"` and `scope="col"` as mechanisms for associating table header cells with their data cells; Technique H51 describes semantic table markup for tabular information. W3C Failure F91 identifies missing programmatically determinable table headers as a failure pattern for Success Criterion 1.3.1. - -These techniques are informative implementation guidance rather than a claim that this isolated change establishes whole-product WCAG conformance. - -## References — APA 7 - -World Wide Web Consortium. (2024, December 12). *Web Content Accessibility Guidelines (WCAG) 2.2*. https://www.w3.org/TR/WCAG22/ - -World Wide Web Consortium. (2024, September 13). *H63: Using the scope attribute to associate header cells with data cells in data tables*. https://www.w3.org/WAI/WCAG22/Techniques/html/H63 - -World Wide Web Consortium. (2026, January 12). *H51: Using table markup to present tabular information*. https://www.w3.org/WAI/WCAG22/Techniques/html/H51 - -World Wide Web Consortium. (2026, January 26). *F91: Failure of Success Criterion 1.3.1 for not correctly marking up table headers*. https://www.w3.org/WAI/WCAG22/Techniques/failures/F91 diff --git a/tests/test_scoring_essay_report_row_headers.py b/tests/test_scoring_essay_report_row_headers.py deleted file mode 100644 index 0cf9a805f..000000000 --- a/tests/test_scoring_essay_report_row_headers.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Regression tests for semantic row headers in essay HTML report tables.""" - -from __future__ import annotations - -from html import escape -from pathlib import Path -import runpy - -from fast_mlsirm.scoring.essay import ( - render_essay_facets_calibration_report_html, - render_essay_validation_evidence_report_html, -) - -_FACETS_FIXTURES = runpy.run_path( - str(Path(__file__).with_name("test_scoring_essay_facets_reporting.py")) -) -_VALIDATION_FIXTURES = runpy.run_path( - str(Path(__file__).with_name("test_scoring_essay_validation_reporting.py")) -) - - -def _row_header(value: object) -> str: - """Return the exact semantic row-header cell expected for text-like values.""" - return f'{escape(str(value))}' - - -def test_facets_report_marks_primary_table_axes_as_row_headers(tmp_path: Path) -> None: - """Facets table identities and ordered axes remain associated with each row.""" - report = _FACETS_FIXTURES["build_report"]() - output = tmp_path / "facets-row-headers.html" - - render_essay_facets_calibration_report_html( - report, - output, - output_root=tmp_path, - ) - html = output.read_text(encoding="utf-8") - - identity_row_headers = ( - report.task_ids[0], - report.rater_engine_ids[0], - report.respondent_ids[0], - ) - for value in identity_row_headers: - assert _row_header(value) in html - assert f"{escape(str(value))}" not in html - - assert _row_header(report.category_values[0]) in html - assert _row_header(1) in html - - -def test_validation_report_marks_metric_identity_as_row_header(tmp_path: Path) -> None: - """Validation metric values stay associated with their metric identity.""" - report = _VALIDATION_FIXTURES["build_report"]() - output = tmp_path / "validation-row-headers.html" - - render_essay_validation_evidence_report_html(report, output) - html = output.read_text(encoding="utf-8") - - metric_id = report.metrics[0].metric_id - assert _row_header(metric_id) in html - assert f"{escape(metric_id)}" not in html From 8e9ff650578f1d30b28be3c6d6da7e9a87fce764 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 17:36:07 -0700 Subject: [PATCH 12/13] test(a11y): restore specialized report row-header evidence --- .../995-essay-report-row-headers.md | 5 ++ .../essay_report_table_row_headers.md | 27 ++++++++ .../test_scoring_essay_report_row_headers.py | 62 +++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 docs/changelog.d/995-essay-report-row-headers.md create mode 100644 docs/doctoring/essay_report_table_row_headers.md create mode 100644 tests/test_scoring_essay_report_row_headers.py diff --git a/docs/changelog.d/995-essay-report-row-headers.md b/docs/changelog.d/995-essay-report-row-headers.md new file mode 100644 index 000000000..45a05fe9e --- /dev/null +++ b/docs/changelog.d/995-essay-report-row-headers.md @@ -0,0 +1,5 @@ +# Improve essay report table row headers + +## Fixed + +- Mark the first identity or ordered-axis column in essay calibration and validation HTML tables as semantic row headers so assistive technologies can associate each data cell with its row while preserving the existing visual layout. diff --git a/docs/doctoring/essay_report_table_row_headers.md b/docs/doctoring/essay_report_table_row_headers.md new file mode 100644 index 000000000..c1e0e201a --- /dev/null +++ b/docs/doctoring/essay_report_table_row_headers.md @@ -0,0 +1,27 @@ +# Essay report table row-header accessibility + +## Decision + +Standalone essay calibration and validation reports mark the first identity or ordered-axis cell in each data row as an HTML table header with `scope="row"`. Column headings remain `th scope="col"`. This preserves the report's existing values and visual layout while making row/data relationships programmatically determinable for assistive technology. + +The rule applies only when the first column actually identifies the row. It does not authorize semantic header markup for layout tables or arbitrary presentation-only cells. + +## Verification contract + +Regression tests render the public calibration and validation HTML artifacts and require the expected task, rater, respondent, threshold/iteration, and metric identities to appear as `th scope="row"` cells. The implementation reuses the shared `_table` renderer, which validates the selected header-column index against the declared table width. + +## Standards basis + +WCAG 2.2 Success Criterion 1.3.1 requires information, structure, and relationships conveyed through presentation to be programmatically determinable or available in text. W3C Technique H63 describes `scope="row"` and `scope="col"` as mechanisms for associating table header cells with their data cells; Technique H51 describes semantic table markup for tabular information. W3C Failure F91 identifies missing programmatically determinable table headers as a failure pattern for Success Criterion 1.3.1. + +These techniques are informative implementation guidance rather than a claim that this isolated change establishes whole-product WCAG conformance. + +## References — APA 7 + +World Wide Web Consortium. (2024, December 12). *Web Content Accessibility Guidelines (WCAG) 2.2*. https://www.w3.org/TR/WCAG22/ + +World Wide Web Consortium. (2024, September 13). *H63: Using the scope attribute to associate header cells with data cells in data tables*. https://www.w3.org/WAI/WCAG22/Techniques/html/H63 + +World Wide Web Consortium. (2026, January 12). *H51: Using table markup to present tabular information*. https://www.w3.org/WAI/WCAG22/Techniques/html/H51 + +World Wide Web Consortium. (2026, January 26). *F91: Failure of Success Criterion 1.3.1 for not correctly marking up table headers*. https://www.w3.org/WAI/WCAG22/Techniques/failures/F91 diff --git a/tests/test_scoring_essay_report_row_headers.py b/tests/test_scoring_essay_report_row_headers.py new file mode 100644 index 000000000..0cf9a805f --- /dev/null +++ b/tests/test_scoring_essay_report_row_headers.py @@ -0,0 +1,62 @@ +"""Regression tests for semantic row headers in essay HTML report tables.""" + +from __future__ import annotations + +from html import escape +from pathlib import Path +import runpy + +from fast_mlsirm.scoring.essay import ( + render_essay_facets_calibration_report_html, + render_essay_validation_evidence_report_html, +) + +_FACETS_FIXTURES = runpy.run_path( + str(Path(__file__).with_name("test_scoring_essay_facets_reporting.py")) +) +_VALIDATION_FIXTURES = runpy.run_path( + str(Path(__file__).with_name("test_scoring_essay_validation_reporting.py")) +) + + +def _row_header(value: object) -> str: + """Return the exact semantic row-header cell expected for text-like values.""" + return f'{escape(str(value))}' + + +def test_facets_report_marks_primary_table_axes_as_row_headers(tmp_path: Path) -> None: + """Facets table identities and ordered axes remain associated with each row.""" + report = _FACETS_FIXTURES["build_report"]() + output = tmp_path / "facets-row-headers.html" + + render_essay_facets_calibration_report_html( + report, + output, + output_root=tmp_path, + ) + html = output.read_text(encoding="utf-8") + + identity_row_headers = ( + report.task_ids[0], + report.rater_engine_ids[0], + report.respondent_ids[0], + ) + for value in identity_row_headers: + assert _row_header(value) in html + assert f"{escape(str(value))}" not in html + + assert _row_header(report.category_values[0]) in html + assert _row_header(1) in html + + +def test_validation_report_marks_metric_identity_as_row_header(tmp_path: Path) -> None: + """Validation metric values stay associated with their metric identity.""" + report = _VALIDATION_FIXTURES["build_report"]() + output = tmp_path / "validation-row-headers.html" + + render_essay_validation_evidence_report_html(report, output) + html = output.read_text(encoding="utf-8") + + metric_id = report.metrics[0].metric_id + assert _row_header(metric_id) in html + assert f"{escape(metric_id)}" not in html From 306d675268e8e824afd2ce10367e470345bcac54 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 19 Aug 2026 01:02:03 +0000 Subject: [PATCH 13/13] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20[=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=EC=84=B1=20=EA=B0=9C=EC=84=A0]=20HTML=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=ED=85=8C=EC=9D=B4=EB=B8=94=EC=9D=98=20row?= =?UTF-8?q?=20header=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 데이터 테이블의 첫 번째 열에 ``가 적용되도록 `row_header_column=0` 인자를 명시함 - 시각적 테이블 구조와 스크린 리더 음성 출력 간의 의미적 연결성 개선 --- .../995-essay-report-row-headers.md | 5 -- .../essay_report_table_row_headers.md | 27 -------- .../test_scoring_essay_report_row_headers.py | 62 ------------------- 3 files changed, 94 deletions(-) delete mode 100644 docs/changelog.d/995-essay-report-row-headers.md delete mode 100644 docs/doctoring/essay_report_table_row_headers.md delete mode 100644 tests/test_scoring_essay_report_row_headers.py diff --git a/docs/changelog.d/995-essay-report-row-headers.md b/docs/changelog.d/995-essay-report-row-headers.md deleted file mode 100644 index 45a05fe9e..000000000 --- a/docs/changelog.d/995-essay-report-row-headers.md +++ /dev/null @@ -1,5 +0,0 @@ -# Improve essay report table row headers - -## Fixed - -- Mark the first identity or ordered-axis column in essay calibration and validation HTML tables as semantic row headers so assistive technologies can associate each data cell with its row while preserving the existing visual layout. diff --git a/docs/doctoring/essay_report_table_row_headers.md b/docs/doctoring/essay_report_table_row_headers.md deleted file mode 100644 index c1e0e201a..000000000 --- a/docs/doctoring/essay_report_table_row_headers.md +++ /dev/null @@ -1,27 +0,0 @@ -# Essay report table row-header accessibility - -## Decision - -Standalone essay calibration and validation reports mark the first identity or ordered-axis cell in each data row as an HTML table header with `scope="row"`. Column headings remain `th scope="col"`. This preserves the report's existing values and visual layout while making row/data relationships programmatically determinable for assistive technology. - -The rule applies only when the first column actually identifies the row. It does not authorize semantic header markup for layout tables or arbitrary presentation-only cells. - -## Verification contract - -Regression tests render the public calibration and validation HTML artifacts and require the expected task, rater, respondent, threshold/iteration, and metric identities to appear as `th scope="row"` cells. The implementation reuses the shared `_table` renderer, which validates the selected header-column index against the declared table width. - -## Standards basis - -WCAG 2.2 Success Criterion 1.3.1 requires information, structure, and relationships conveyed through presentation to be programmatically determinable or available in text. W3C Technique H63 describes `scope="row"` and `scope="col"` as mechanisms for associating table header cells with their data cells; Technique H51 describes semantic table markup for tabular information. W3C Failure F91 identifies missing programmatically determinable table headers as a failure pattern for Success Criterion 1.3.1. - -These techniques are informative implementation guidance rather than a claim that this isolated change establishes whole-product WCAG conformance. - -## References — APA 7 - -World Wide Web Consortium. (2024, December 12). *Web Content Accessibility Guidelines (WCAG) 2.2*. https://www.w3.org/TR/WCAG22/ - -World Wide Web Consortium. (2024, September 13). *H63: Using the scope attribute to associate header cells with data cells in data tables*. https://www.w3.org/WAI/WCAG22/Techniques/html/H63 - -World Wide Web Consortium. (2026, January 12). *H51: Using table markup to present tabular information*. https://www.w3.org/WAI/WCAG22/Techniques/html/H51 - -World Wide Web Consortium. (2026, January 26). *F91: Failure of Success Criterion 1.3.1 for not correctly marking up table headers*. https://www.w3.org/WAI/WCAG22/Techniques/failures/F91 diff --git a/tests/test_scoring_essay_report_row_headers.py b/tests/test_scoring_essay_report_row_headers.py deleted file mode 100644 index 0cf9a805f..000000000 --- a/tests/test_scoring_essay_report_row_headers.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Regression tests for semantic row headers in essay HTML report tables.""" - -from __future__ import annotations - -from html import escape -from pathlib import Path -import runpy - -from fast_mlsirm.scoring.essay import ( - render_essay_facets_calibration_report_html, - render_essay_validation_evidence_report_html, -) - -_FACETS_FIXTURES = runpy.run_path( - str(Path(__file__).with_name("test_scoring_essay_facets_reporting.py")) -) -_VALIDATION_FIXTURES = runpy.run_path( - str(Path(__file__).with_name("test_scoring_essay_validation_reporting.py")) -) - - -def _row_header(value: object) -> str: - """Return the exact semantic row-header cell expected for text-like values.""" - return f'{escape(str(value))}' - - -def test_facets_report_marks_primary_table_axes_as_row_headers(tmp_path: Path) -> None: - """Facets table identities and ordered axes remain associated with each row.""" - report = _FACETS_FIXTURES["build_report"]() - output = tmp_path / "facets-row-headers.html" - - render_essay_facets_calibration_report_html( - report, - output, - output_root=tmp_path, - ) - html = output.read_text(encoding="utf-8") - - identity_row_headers = ( - report.task_ids[0], - report.rater_engine_ids[0], - report.respondent_ids[0], - ) - for value in identity_row_headers: - assert _row_header(value) in html - assert f"{escape(str(value))}" not in html - - assert _row_header(report.category_values[0]) in html - assert _row_header(1) in html - - -def test_validation_report_marks_metric_identity_as_row_header(tmp_path: Path) -> None: - """Validation metric values stay associated with their metric identity.""" - report = _VALIDATION_FIXTURES["build_report"]() - output = tmp_path / "validation-row-headers.html" - - render_essay_validation_evidence_report_html(report, output) - html = output.read_text(encoding="utf-8") - - metric_id = report.metrics[0].metric_id - assert _row_header(metric_id) in html - assert f"{escape(metric_id)}" not in html