diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a876f076d..22bf0c910 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -511,7 +511,9 @@ measurement service) so `tepp_not_available` is not mistaken for a calibrated negative result. A failed lineage row tells the operator to retry reconstruction, not to connect TEPP. A failed period-report row tells the operator to rebuild the report. A pending TEPP row -does not claim a calibrated measurement. A pending lineage row +does not claim a calibrated measurement and does not say +reconstruction. The list button accessible name includes the +next-action sentence; detail repeats it (ADR 0014). A pending lineage row says reconstruction has not started yet; open it and start reconstruction. The payload is lookup labels plus non-negative aggregate counts -- never diff --git a/CHANGELOG.d/2.10.1-analysis-run-accessible-next-action.md b/CHANGELOG.d/2.10.1-analysis-run-accessible-next-action.md new file mode 100644 index 000000000..3a0e27a02 --- /dev/null +++ b/CHANGELOG.d/2.10.1-analysis-run-accessible-next-action.md @@ -0,0 +1,5 @@ +# 2.10.1 Include next-action in analysis-run accessible names + +List button names include the kind-specific next-action sentence. +Open a Failed TEPP row and hear connect the measurement service +(ADR 0014). diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ff4d3d66..85e50fc69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ 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). +## [2.10.1] - 2026-08-17 + +### Fixed + +- Analysis-run list buttons now include the kind-specific next-action + sentence in the accessible name (WCAG 2.2 SC 4.1.2). Open a Failed + TEPP row: a screen reader hears connect the measurement service, not + only the run title. `aria-label` replaces button contents (ADR 0014). + No TEPP theta is invented. + ## [2.10.0] - 2026-08-17 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 1bcf50763..0e15bc773 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,8 +27,10 @@ theta or a local psychometric substitute. The home list caption stays (ADR 0014). Open a Failed TEPP row, then connect a live TEPP 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. +pending TEPP row does not claim a calibrated measurement and does +not say reconstruction. The list button name includes the +next-action sentence. A pending lineage row says reconstruction has +not started yet. Digest prefixes stay audible; hover a prefix to read the full digest. Opening a cutoff title shows the live post. Titles marked updated after cutoff were rewritten after the run; the opened body names diff --git a/docs/adr/0014-authorized-analysis-run-read.md b/docs/adr/0014-authorized-analysis-run-read.md index 10e99d37d..b6d61dcaa 100644 --- a/docs/adr/0014-authorized-analysis-run-read.md +++ b/docs/adr/0014-authorized-analysis-run-read.md @@ -49,8 +49,13 @@ service. A failed lineage row tells the operator to retry 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 +measurement and must not say reconstruction. The list +button accessible name is `Open analysis run: {caption}. {nextAction}` +when a next action exists (WCAG 2.2 SC 4.1.2); otherwise the caption +alone. `aria-label` replaces button contents (W3C Accessible Name and +Description Computation 1.1), so the next-action sentence must live +in that name. Detail repeats that sentence. A pending +lineage row says reconstruction has not started yet. The detail now shows the legal lifecycle the registry already stored. `POST /api/analysis-runs` now records a Pending lineage run on an authorized cutoff capture (ADR 0017). TEPP and period-report kinds are 422. Reconstruction, a @@ -67,3 +72,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 99dfff47b..66f382504 100644 --- a/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md +++ b/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md @@ -10,7 +10,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). Detail compares live `updated_at` with that cutoff and marks titles rewritten after the run. Seed records the built period report as a later Succeeded run on that same snapshot (ADR 0024) without copying a theta onto the registry row. The cutoff-known body is read from `source_post_revision` on the opened post, not from the run payload (ADR 0025). | -| 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, `invoking_session_role` on each retention event, and exclusion of raw source/provider payloads. | @@ -125,3 +125,6 @@ computation 1.1* (W3C Recommendation). https://www.w3.org/TR/accname-1.1/ World Wide Web Consortium. (2022). *Time ontology in OWL* (W3C Recommendation). https://www.w3.org/TR/owl-time/ + +World Wide Web Consortium. (2023). *Web content accessibility guidelines +(WCAG) 2.2* (W3C Recommendation). https://www.w3.org/TR/WCAG22/ diff --git a/frontend/package.json b/frontend/package.json index 58489b343..49b74b3e0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.10.0", + "version": "2.10.1", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 0e35f6832..e5180b90d 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -2137,6 +2137,12 @@ describe("App, authenticated", () => { "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", ); + expect( + screen.getByRole("button", { + 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.", + }), + ).toBeInTheDocument(); + await userEvent.click( screen.getByRole("button", { name: "Open analysis run: Lineage reconstruction · Succeeded · Demo Corp", @@ -2213,7 +2219,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( @@ -2273,12 +2279,29 @@ describe("App, authenticated", () => { expect(screen.queryByRole("heading", { name: "Body this run knew" })).not.toBeInTheDocument(); }); + it("finds a failed TEPP list button by the next-action accessible name", async () => { + stubBackend(); + render(); + + const teppButton = await screen.findByRole("button", { + 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(teppButton).toHaveAccessibleName( + "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( + screen.getByRole("button", { + name: "Open analysis run: Lineage reconstruction · Succeeded · Demo Corp", + }), + ).toHaveAccessibleName("Open analysis run: Lineage reconstruction · Succeeded · Demo Corp"); + }); + it("tells a running lineage run to refresh the durable outbox", async () => { stubBackend({ runningLineageRun: true }); render(); const lineageButton = await screen.findByRole("button", { - name: "Open analysis run: Lineage reconstruction · Running · Demo Corp", + name: "Open analysis run: Lineage reconstruction · Running · Demo Corp. Refresh this run. Start already queued the work on the durable outbox.", }); expect(lineageButton).toHaveTextContent( "Refresh this run. Start already queued the work on the durable outbox.", @@ -2296,10 +2319,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.", @@ -2584,7 +2607,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.", @@ -2608,11 +2631,13 @@ 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.toHaveAccessibleName(/Reconstruction/); + expect(teppButton).not.toHaveAccessibleName(/measured/); + + await userEvent.click(teppButton); expect( await screen.findByText("These posts are the cutoff corpus TEPP will measure once this run finishes."), ).toBeInTheDocument(); @@ -2629,7 +2654,7 @@ describe("App, authenticated", () => { await userEvent.click( await screen.findByRole("button", { - name: "Open analysis run: TEPP measurement · Pending · Demo Corp", + 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.", }), ); await userEvent.click(screen.getByRole("button", { name: "Start TEPP measurement" })); @@ -2651,7 +2676,7 @@ describe("App, authenticated", () => { await userEvent.click( await screen.findByRole("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( @@ -2695,7 +2720,7 @@ describe("App, authenticated", () => { ).toBeInTheDocument(); expect( screen.getByRole("button", { - name: "Open analysis run: Lineage reconstruction · Pending · Demo Corp", + name: "Open analysis run: Lineage reconstruction · Pending · Demo Corp. Open this run, then start reconstruction. Reconstruction has not started yet.", }), ).toBeInTheDocument(); expect( diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c39bcf2f9..684902521 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1801,6 +1801,24 @@ function analysisRunNextAction(run: AnalysisRun): string | null { } } +/** + * Accessible name for an analysis-run list button (ADR 0014). + * + * `aria-label` replaces the button contents (W3C Accessible Name and + * Description Computation 1.1). When a next action exists, the name is + * `Open analysis run: {caption}. {nextAction}` so a screen reader hears + * what to do next, not only the run title (WCAG 2.2 SC 4.1.2). Succeeded + * and cancelled rows keep the caption alone. + */ +function analysisRunAccessibleName(run: AnalysisRun): string { + const caption = analysisRunCaption(run); + const nextAction = analysisRunNextAction(run); + if (nextAction === null) { + return `Open analysis run: ${caption}`; + } + return `Open analysis run: ${caption}. ${nextAction}`; +} + /** * Empty-corpus copy that tells the operator what to do next. */ @@ -2208,7 +2226,7 @@ function AnalysisRunsPanel({