From 30e5222ad8b390ad5a60e467379a92fc198d9d4f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 16:35:59 +0000 Subject: [PATCH] fix(ui): include next-action sentences in analysis-run list names aria-label replaced the visible pending/failed sentence, so a screen reader only heard the caption. Fold the AccName pin into ADR 0014 without taking ADR 0020 or 0021. After Request, the stub list now keeps the Pending lineage row so the name can be verified. Co-authored-by: Seongho Bae --- CHANGELOG.d/analysis-run-list-accname.md | 2 ++ CHANGELOG.md | 9 ++++++ CLAUDE.md | 3 +- docs/adr/0014-authorized-analysis-run-read.md | 14 +++++++- .../ANALYSIS_RUN_REGISTRY_REFERENCES.md | 5 ++- frontend/src/App.test.tsx | 32 ++++++++++++------- frontend/src/App.tsx | 17 +++++++++- 7 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 CHANGELOG.d/analysis-run-list-accname.md diff --git a/CHANGELOG.d/analysis-run-list-accname.md b/CHANGELOG.d/analysis-run-list-accname.md new file mode 100644 index 000000000..45ab84d7d --- /dev/null +++ b/CHANGELOG.d/analysis-run-list-accname.md @@ -0,0 +1,2 @@ +List-button accessible names include the kind-specific next-action +sentence so a screen reader hears what to do next (ADR 0014). diff --git a/CHANGELOG.md b/CHANGELOG.md index d30ae14aa..38f220241 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- Analysis-run list buttons include the kind-specific next-action + sentence in the accessible name (ADR 0014). Open a Failed TEPP row + and hear “connect the measurement service” in the list name. A + Pending TEPP name does not say reconstruction. + ## [0.86.2] - 2026-08-16 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index c5a1828f2..be55f52d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,8 @@ theta or a local psychometric substitute. The home list caption stays transport. A failed lineage row retries reconstruction -- it does not mention TEPP. A failed period-report row rebuilds the report. A pending TEPP row does not claim a calibrated measurement. A pending -lineage row says reconstruction has not started yet. +lineage row says reconstruction has not started yet. The list +button name includes the next-action sentence (ADR 0014). Digest prefixes stay audible; hover a prefix to read the full digest. Opening a cutoff title shows the live post -- compare it with the cutoff before treating the body as reconstructed evidence (ADR 0016). diff --git a/docs/adr/0014-authorized-analysis-run-read.md b/docs/adr/0014-authorized-analysis-run-read.md index 500c2bc2a..d246c57c2 100644 --- a/docs/adr/0014-authorized-analysis-run-read.md +++ b/docs/adr/0014-authorized-analysis-run-read.md @@ -49,7 +49,11 @@ reconstruction, not to connect TEPP. A failed period-report row tells the operator to rebuild the report from a current snapshot. A pending or running TEPP row must not claim a calibrated measurement. A pending lineage row says reconstruction has not -started yet. The detail now shows the legal +started yet. The list button accessible name is +`Open analysis run: {caption}. {nextAction}` when a next action +exists, otherwise the caption alone, so `aria-label` does not hide +the sentence a sighted operator already sees (W3C, 2018, 2023). +The detail now shows the legal lifecycle the registry already stored. `POST /api/analysis-runs` now records a Pending run on an authorized cutoff capture (ADR 0017). Reconstruction, a live TEPP transport, and a fuller Analysis Run @@ -65,3 +69,11 @@ Educational Research Association. Lebo, T., Sahoo, S., & McGuinness, D. (Eds.). (2013). *PROV-O: The PROV ontology* (W3C Recommendation). World Wide Web Consortium. https://www.w3.org/TR/2013/REC-prov-o-20130430/ + +World Wide Web Consortium. (2018). *Accessible name and description +computation 1.1* (W3C Recommendation). +https://www.w3.org/TR/accname-1.1/ + +World Wide Web Consortium. (2023). *Web content accessibility +guidelines (WCAG) 2.2* (W3C Recommendation). +https://www.w3.org/TR/WCAG22/ diff --git a/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md b/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md index b41b31c17..bee3d2571 100644 --- a/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md +++ b/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md @@ -9,7 +9,7 @@ |---|---|---| | W3C PROV-DM and PROV-O | Preserve identifiable entities, activities, agents, generation/use, and derivation without flattening provenance into display-only edges. | `analysis_source_snapshot`, `analysis_run`, authenticated requester, append-only status events, immutable digests; later product bindings continue to use the separate `provenance_*` layer from ADR 0011. | | W3C Time Ontology in OWL | Keep temporal concepts explicit and avoid collapsing distinct clocks. | Evidence availability and snapshot capture remain on `analysis_source_snapshot`; analysis knowledge cutoff and request time remain on `analysis_run`; status occurrence and database record time remain distinct. `GET /api/analysis-runs/{id}` visible posts apply `created_at <= knowledge_cutoff` (ADR 0016). Opening a listed title warns that the live body may have changed after that cutoff. | -| W3C Accessible Name and Description Computation 1.1 | Do not let `aria-label` replace visible text the operator must hear. | Analysis-run digest prefixes live in a labeled group; the prefixes remain the accessible contents and the full digest is on `title` for hover verification. | +| W3C Accessible Name and Description Computation 1.1 | Do not let `aria-label` replace visible text the operator must hear. | Analysis-run digest prefixes live in a labeled group; the prefixes remain the accessible contents and the full digest is on `title` for hover verification. List-button names include the kind-specific next-action sentence (ADR 0014; WCAG 2.2 SC 4.1.2). | | ISO 8601-1:2019 | Use unambiguous timestamp representation and timezone-aware persistence. | PostgreSQL `timestamptz` for availability, capture, cutoff, request, occurrence, and record clocks; tests use explicit `Z` offsets. | | PostgreSQL 18 constraints and trigger contracts | Put integrity close to durable truth and use constraints for row shape while triggers enforce cross-row state and serialization. | Digest/check constraints, category allowlists, account-scoped uniqueness, shape constraints, immutable-row triggers, shared snapshot-row locking, and serialized status transitions. | | NIST SP 800-92 | Treat audit records as bounded, protected operational evidence rather than unstructured application logging. | Append-only status events, machine failure codes, actor identity, occurrence/record clocks, fail-closed rollback, and exclusion of raw source/provider payloads. | @@ -102,5 +102,8 @@ Recommendation). https://www.w3.org/TR/prov-o/ World Wide Web Consortium. (2018). *Accessible name and description computation 1.1* (W3C Recommendation). https://www.w3.org/TR/accname-1.1/ +World Wide Web Consortium. (2023). *Web content accessibility +guidelines (WCAG) 2.2* (W3C Recommendation). https://www.w3.org/TR/WCAG22/ + World Wide Web Consortium. (2022). *Time ontology in OWL* (W3C Recommendation). https://www.w3.org/TR/owl-time/ diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 60d06c8de..d3fe344cb 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -85,6 +85,7 @@ describe("App, authenticated", () => { let nextTicketId = 1; const events: { event_id: string; event_type: string; actor_account_id: string; summary: string }[] = []; let nextEventId = 1; + let createdPendingLineage: Record | null = null; const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); @@ -351,12 +352,14 @@ describe("App, authenticated", () => { }, ], }; + createdPendingLineage = created; return Promise.resolve(new Response(JSON.stringify(created), { status: 201 })); } if (url.endsWith("/api/analysis-runs")) { return Promise.resolve( jsonResponse({ analysis_runs: [ + ...(createdPendingLineage ? [createdPendingLineage] : []), { analysis_run_id: "run-demo-lineage", run_kind_code: "analysis_run_lineage", @@ -1704,7 +1707,7 @@ describe("App, authenticated", () => { await userEvent.click( screen.getByRole("button", { - name: "Open analysis run: TEPP measurement · Failed · Demo Corp", + name: "Open analysis run: TEPP measurement · Failed · Demo Corp. Open this run to see why it failed, then connect the measurement service and re-run.", }), ); expect( @@ -1722,10 +1725,10 @@ describe("App, authenticated", () => { await screen.findByRole("list", { name: "Analysis runs" }); const lineageButton = screen.getByRole("button", { - name: "Open analysis run: Lineage reconstruction · Failed · Demo Corp", + name: "Open analysis run: Lineage reconstruction · Failed · Demo Corp. Open this run to see why it failed, then retry reconstruction from a current snapshot.", }); const teppButton = screen.getByRole("button", { - name: "Open analysis run: TEPP measurement · Failed · Demo Corp", + name: "Open analysis run: TEPP measurement · Failed · Demo Corp. Open this run to see why it failed, then connect the measurement service and re-run.", }); expect(lineageButton).toHaveTextContent( "Open this run to see why it failed, then retry reconstruction from a current snapshot.", @@ -1742,7 +1745,7 @@ describe("App, authenticated", () => { render(); const reportButton = await screen.findByRole("button", { - name: "Open analysis run: Period report · Failed · Demo Corp", + name: "Open analysis run: Period report · Failed · Demo Corp. Open this run to see why it failed, then rebuild the period report from a current snapshot.", }); expect(reportButton).toHaveTextContent( "Open this run to see why it failed, then rebuild the period report from a current snapshot.", @@ -1762,11 +1765,11 @@ describe("App, authenticated", () => { stubBackend({ pendingTeppRun: true }); render(); - await userEvent.click( - await screen.findByRole("button", { - name: "Open analysis run: TEPP measurement · Pending · Demo Corp", - }), - ); + const teppButton = await screen.findByRole("button", { + name: "Open analysis run: TEPP measurement · Pending · Demo Corp. Open this run to confirm which posts TEPP will measure. Measurement has not started yet — this is not a calibrated result.", + }); + expect(teppButton).not.toHaveTextContent("Reconstruction has not started yet"); + await userEvent.click(teppButton); expect( await screen.findByText("These posts are the cutoff corpus TEPP will measure once this run finishes."), ).toBeInTheDocument(); @@ -1801,10 +1804,15 @@ describe("App, authenticated", () => { await screen.findByRole("heading", { name: "Lineage reconstruction · Pending · Demo Corp" }), ).toBeInTheDocument(); expect( - screen.getByText( - "Open this run to confirm which posts it will use. Reconstruction has not started yet.", - ), + screen.getByRole("button", { + name: "Open analysis run: Lineage reconstruction · Pending · Demo Corp. Open this run to confirm which posts it will use. Reconstruction has not started yet.", + }), ).toBeInTheDocument(); + expect( + screen.getAllByText( + "Open this run to confirm which posts it will use. Reconstruction has not started yet.", + ).length, + ).toBeGreaterThanOrEqual(2); const postCall = fetchMock.mock.calls.find( (call) => String(call[0]).endsWith("/api/analysis-runs") && call[1]?.method === "POST", ); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d589a3644..35b6b32ee 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1498,6 +1498,21 @@ function analysisRunNextAction(run: AnalysisRun): string | null { } } +/** + * List-button accessible name (WCAG 2.2 SC 4.1.2 / AccName 1.1). + * + * `aria-label` replaces the button contents, so the next-action + * sentence must be in the name or a screen reader only hears the + * caption (ADR 0014). + */ +function analysisRunAccessibleName(run: AnalysisRun): string { + const caption = analysisRunCaption(run); + const nextAction = analysisRunNextAction(run); + return nextAction + ? `Open analysis run: ${caption}. ${nextAction}` + : `Open analysis run: ${caption}`; +} + /** * Empty-corpus copy that tells the operator what to do next. */ @@ -1701,7 +1716,7 @@ function AnalysisRunsPanel({