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
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ list, then open the Pending row to confirm the cutoff corpus.
unavailable, so that run is Failed rather than a fabricated score.
The home list is clickable: `GET /api/analysis-runs/{id}` fills a
labeled detail (cutoff, requested date, 12-character digest prefixes
with full digests on hover, counts, status history)
that disclose the full digest on activation, counts, status history)
without exposing a DSN or raw record. Opening a cutoff title warns
that the live body may have changed after the run. Status history is detail-only
and uses lookup labels plus occurrence times; a failure event keeps
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.d/0.85.1-analysis-run-digest-disclosure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# 0.85.1 Analysis-run digest disclosure

Activate a prefix on the run detail to read the full digest. Hover is
not the only verification path. The list stays aggregates-only.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ 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).

## [0.85.1] - 2026-08-16

### Fixed

- Analysis-run digest prefixes are disclosure buttons. Open the Demo
Corp lineage run, activate `Code` or `Config`, and match the revealed
digest to the API payload. Keyboard and assistive technology can
complete that check; a hover `title` is no longer the only path
(WCAG 2.2 SC 1.4.13; WAI-ARIA APG Disclosure). The home list still
hides digests even when the list JSON includes them.

## [0.85.0] - 2026-08-16

### Added
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Tool-specific pointer. Policy lives in [AGENTS.md](AGENTS.md) and the
ADRs under `docs/adr/`. Do not fork those rules here.

## Analysis-run seed (v0.85.0)
## Analysis-run seed (v0.85.1)

`make seed` writes a Demo Corp lineage run and a TEPP run on the same
snapshot (ADR 0013). The TEPP path goes through `tepp_client`. A missing
Expand All @@ -14,7 +14,7 @@ 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.
Digest prefixes stay audible; hover a prefix to read the full digest.
Digest prefixes stay audible; activate 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).
`POST /api/analysis-runs` records Pending on an authorized
Expand Down
19 changes: 14 additions & 5 deletions docs/adr/0016-analysis-run-knowledge-cutoff-posts.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ as reconstructed evidence.

Reproducibility digests on the same detail use a labeled group whose
accessible name does not replace the visible prefixes (W3C Accessible
Name and Description Computation 1.1). Full digests stay on `title`
for hover verification and on the API payload; the home list stays
aggregates-only.
Name and Description Computation 1.1). Each prefix is a disclosure
button (WAI-ARIA APG Disclosure; WCAG 2.2 Success Criterion 1.4.13).
The full digest is hidden until activation so keyboard and assistive
technology can verify it the same way a pointer can. The home list
stays aggregates-only; the API payload still carries the full values.

Seed and API fixtures backdate in-cutoff posts. A late own-corp private
post remains on the live post list and stays out of the January 2026
Expand All @@ -46,8 +48,8 @@ run.
post (2026-02-10) does not appear.
- Open the run, read the live-body warning, then open a listed post
and compare it with the cutoff date.
- Hover a digest prefix to read the full code or configuration digest
when you need to match the API payload.
- Activate a digest prefix (Enter, Space, or click) to read the full
code or configuration digest when you need to match the API payload.
- Post-body versioning at the cutoff remains future work.

## References
Expand All @@ -62,3 +64,10 @@ Recommendation). https://www.w3.org/TR/owl-time/
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. (2024). *Web content accessibility guidelines
(WCAG) 2.2* (W3C Recommendation). https://www.w3.org/TR/WCAG22/

World Wide Web Consortium. (n.d.). *Disclosure (show/hide) pattern*.
ARIA Authoring Practices Guide.
https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
11 changes: 10 additions & 1 deletion docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
|---|---|---|
| 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 of disclosure buttons. |
| WCAG 2.2 Success Criterion 1.4.13 | Additional content that appears only on hover or focus must not be the only way to complete a task. | Full digests are hidden until the operator activates a prefix button (Enter, Space, or click). Native `title` tooltips are not the verification path. |
| WAI-ARIA APG Disclosure | Use a button with `aria-expanded` to show and hide the controlled digest. | `AnalysisRunReproducibilityDigests` toggles a `<code>` panel per prefix. |
| 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. |
Expand Down Expand Up @@ -104,3 +106,10 @@ 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. (2024). *Web content accessibility guidelines
(WCAG) 2.2* (W3C Recommendation). https://www.w3.org/TR/WCAG22/

World Wide Web Consortium. (n.d.). *Disclosure (show/hide) pattern*.
ARIA Authoring Practices Guide.
https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
29 changes: 29 additions & 0 deletions docs/goals/analysis-run-operator-loop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Goal — analysis-run operator loop

**Status:** Active
**Date:** 2026-08-16

## Goal

A buyer can open a seeded analysis run, hear the digest prefixes, reveal
the full digests without a pointer, and compare any opened live post
with that run's cutoff before treating the body as reconstructed
evidence.

## Current loop

1. #127 is on `feat/role-responsibility-agent-ontology` (`44912a6`).
Prefixes are audible; the live-body warning is present.
2. Land #135 (v0.85.1) so keyboard and AT operators can match a digest
to the API payload. Do not self-approve or merge from this automation.
3. Keep #131 as the write-clock comparison slice. Do not open a second
write-clock PR.
4. #125 (`POST /api/analysis-runs`) is on the same base. Do not open a
second create PR.
5. Post-body versioning at the cutoff remains later work (ADR 0016).

## Out of this loop

Retention purge and the Storybook runner belong to the approved
frontend-toolchain PR. Failed-run next-action copy already landed with
#124.
21 changes: 21 additions & 0 deletions docs/storybook-inventory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Storybook inventory

Repeating web objects that must stay tokenized and independently
composable. The Storybook runner itself lands with the approved
frontend toolchain PR; this inventory is the product list those
stories must cover.

| Object | Tokens | Next action the story must teach |
|---|---|---|
| Analysis-run digest disclosure | `--lw-opacity-meta`, `--lw-font-size-meta`, `--lw-space-digest-gap`, `--lw-font-family-mono`, `--lw-focus-ring`, `--lw-focus-offset` | Activate a prefix, then match the revealed digest to the API payload. |
| Analysis-run live-post warning | `--lw-opacity-meta`, `--lw-font-size-meta` | Compare the opened body with the run cutoff before treating it as reconstructed evidence. |
| Meta caption (`.post-meta`) | `--lw-opacity-meta`, `--lw-font-size-meta` | Read the clock or count, then take the control beside it. |

## References

World Wide Web Consortium. (2024). *Web content accessibility guidelines
(WCAG) 2.2* (W3C Recommendation). https://www.w3.org/TR/WCAG22/

World Wide Web Consortium. (n.d.). *Disclosure (show/hide) pattern*.
ARIA Authoring Practices Guide.
https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "0.85.0",
"version": "0.85.1",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
74 changes: 74 additions & 0 deletions frontend/src/AnalysisRunReproducibilityDigests.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { AnalysisRunReproducibilityDigests } from "./AnalysisRunReproducibilityDigests";

const CODE_REVISION_SHA = "abcdef0123456789deadbeefcafebabe";
const CONFIGURATION_SHA256 =
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";

describe("AnalysisRunReproducibilityDigests", () => {
it("renders nothing when the run has no digests", () => {
const { container } = render(<AnalysisRunReproducibilityDigests />);
expect(container).toBeEmptyDOMElement();
});

it("keeps prefixes audible and hides full digests until activation", () => {
render(
<AnalysisRunReproducibilityDigests
codeRevisionSha={CODE_REVISION_SHA}
configurationSha256={CONFIGURATION_SHA256}
/>,
);
const group = screen.getByLabelText("Analysis run reproducibility digests");
expect(group).toHaveTextContent("Activate a prefix to read the full digest and match the API payload.");
expect(group).not.toHaveTextContent("Hover");
const codeButton = screen.getByRole("button", { name: "Code abcdef012345" });
const configButton = screen.getByRole("button", { name: "Config 0123456789ab" });
expect(codeButton).toHaveAttribute("aria-expanded", "false");
expect(configButton).toHaveAttribute("aria-expanded", "false");
expect(group).not.toHaveTextContent(CODE_REVISION_SHA);
expect(group).not.toHaveTextContent(CONFIGURATION_SHA256);
});

it("reveals the full code digest with Enter and hides it on the next activation", async () => {
const user = userEvent.setup();
render(
<AnalysisRunReproducibilityDigests
codeRevisionSha={CODE_REVISION_SHA}
configurationSha256={CONFIGURATION_SHA256}
/>,
);
await user.tab();
expect(screen.getByRole("button", { name: "Code abcdef012345" })).toHaveFocus();
await user.keyboard("{Enter}");
expect(screen.getByRole("button", { name: "Code abcdef012345" })).toHaveAttribute(
"aria-expanded",
"true",
);
expect(screen.getByText(CODE_REVISION_SHA)).toBeInTheDocument();
expect(screen.queryByText(CONFIGURATION_SHA256)).not.toBeInTheDocument();
await user.keyboard("{Enter}");
expect(screen.getByRole("button", { name: "Code abcdef012345" })).toHaveAttribute(
"aria-expanded",
"false",
);
expect(screen.queryByText(CODE_REVISION_SHA)).not.toBeInTheDocument();
});

it("reveals the full configuration digest with Space", async () => {
const user = userEvent.setup();
render(
<AnalysisRunReproducibilityDigests
codeRevisionSha={CODE_REVISION_SHA}
configurationSha256={CONFIGURATION_SHA256}
/>,
);
await user.tab();
await user.tab();
expect(screen.getByRole("button", { name: "Config 0123456789ab" })).toHaveFocus();
await user.keyboard(" ");
expect(screen.getByText(CONFIGURATION_SHA256)).toBeInTheDocument();
expect(screen.queryByText(CODE_REVISION_SHA)).not.toBeInTheDocument();
});
});
83 changes: 83 additions & 0 deletions frontend/src/AnalysisRunReproducibilityDigests.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useId, useState } from "react";
import {
analysisRunDigestButtonLabel,
analysisRunDigestNextAction,
type AnalysisRunDigestKind,
} from "./analysisRunDigests";

/**
* One digest disclosure. The button name stays the audible prefix; the
* full value is shown only after Enter, Space, or click (APG Disclosure).
*/
function AnalysisRunDigestDisclosure({
kind,
digest,
panelId,
}: {
kind: AnalysisRunDigestKind;
digest: string;
panelId: string;
}) {
const [open, setOpen] = useState(false);
const label = analysisRunDigestButtonLabel(kind, digest);
return (
<span className="analysis-run-digest">
<button
type="button"
className="analysis-run-digest-toggle"
aria-expanded={open}
aria-controls={panelId}
onClick={() => setOpen((current) => !current)}
>
{label}
</button>
{open ? (
<code id={panelId} className="analysis-run-digest-full">
{digest}
</code>
) : null}
Comment on lines +34 to +38

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When open is false this panel is unmounted, so aria-controls on the button points at an id that is not in the document. WAI-ARIA APG Disclosure keeps the controlled element in the tree and toggles hidden (or equivalent). Keyboard/AT operators who follow the control relationship then have nowhere to land.

#139 keeps <code hidden={!open}> in place so the target exists while collapsed.

</span>
);
}

/**
* Labeled group of analysis-run reproducibility digests.
*
* Prefixes remain the accessible contents of the group. Full digests
* stay off the home list and off the default detail text until the
* operator activates a prefix.
*/
export function AnalysisRunReproducibilityDigests({
codeRevisionSha,
configurationSha256,
}: {
codeRevisionSha?: string;
configurationSha256?: string;
}) {
const id = useId();
if (!codeRevisionSha && !configurationSha256) {
return null;
}
return (
<div role="group" aria-label="Analysis run reproducibility digests">
<p className="post-meta">{analysisRunDigestNextAction()}</p>
<p className="post-meta analysis-run-digest-row">
{codeRevisionSha ? (
<AnalysisRunDigestDisclosure
kind="code"
digest={codeRevisionSha}
panelId={`${id}-code`}
/>
) : null}
{codeRevisionSha && configurationSha256 ? " · " : null}
{configurationSha256 ? (
<AnalysisRunDigestDisclosure
kind="config"
digest={configurationSha256}
panelId={`${id}-config`}
/>
) : null}
</p>
</div>
);
}
47 changes: 38 additions & 9 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,23 +88,52 @@
:root {
--lw-opacity-meta: 0.7;
--lw-font-size-meta: 0.85rem;
--lw-space-digest-gap: 0.35rem;
--lw-font-family-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
--lw-focus-ring: 2px solid currentColor;
--lw-focus-offset: 2px;
}

.post-meta {
opacity: var(--lw-opacity-meta);
font-size: var(--lw-font-size-meta);
}

.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
.analysis-run-digest-row {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: var(--lw-space-digest-gap);
}

.analysis-run-digest {
display: inline-flex;
flex-direction: column;
align-items: flex-start;
gap: var(--lw-space-digest-gap);
}

.analysis-run-digest-toggle {
background: none;
border: none;
padding: 0;
Comment on lines +116 to 119

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

padding: 0 plus .post-meta at 0.85rem lets the pointer target drop under 24×24 CSS pixels. WCAG 2.2 SC 2.5.8 applies here: these are standalone controls, not text in a sentence.

#139 sets --lw-target-min: 24px and a matching inline minimum on the button.

margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
color: inherit;
cursor: pointer;
font: inherit;
text-decoration: underline;
text-underline-offset: 0.15em;
}

.analysis-run-digest-toggle:focus-visible {
outline: var(--lw-focus-ring);
outline-offset: var(--lw-focus-offset);
}

.analysis-run-digest-full {
display: block;
font-family: var(--lw-font-family-mono);
font-size: var(--lw-font-size-meta);
overflow-wrap: anywhere;
}

.post-body {
Expand Down
Loading