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 @@ -481,7 +481,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
5 changes: 5 additions & 0 deletions CHANGELOG.d/0.86.3-analysis-run-digest-disclosure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 0.86.3 Analysis-run digest disclosure

Activate a prefix on the run detail to read the full digest. The
closed panel stays in the document with `hidden`. Hover is not the
only verification path. The list stays aggregates-only.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ 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.86.3] - 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. The closed panel stays in the document
with `hidden` so `aria-controls` has a target, and each prefix meets
the 24px pointer target (WCAG 2.2 SC 1.4.13 and 2.5.8; WAI-ARIA APG
Disclosure). A hover `title` is no longer the only path. The home
list still hides digests even when the list JSON includes them.
Requesting a lineage reconstruction now keeps the pending next action
on that opened detail: reconstruction has not started yet.

## [0.86.2] - 2026-08-16

### Fixed
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.86.3)

`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 @@ -15,7 +15,7 @@ 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.
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
21 changes: 16 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,13 @@ 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 stays in the document with `hidden` until activation
so `aria-controls` has a target and keyboard and assistive technology
can verify it the same way a pointer can. Each prefix button meets the
WCAG 2.2 SC 2.5.8 24px minimum target. 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 +50,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.
- Thread-group *run list* visibility now uses the same cutoff
(ADR 0018). A later public post cannot surface a previously hidden
Expand All @@ -65,3 +69,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/
12 changes: 11 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,10 @@
|---|---|---|
| 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. |
| WCAG 2.2 Success Criterion 2.5.8 | Pointer targets must be at least 24 by 24 CSS pixels. | Each digest prefix button uses `--lw-target-min: 24px` and an inline 24px floor. |
| WAI-ARIA APG Disclosure | Use a button with `aria-expanded` to show and hide the controlled digest. | `AnalysisRunReproducibilityDigests` keeps the closed `<code>` panel in the document with `hidden` so `aria-controls` always has a target. |
| 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 +107,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/
34 changes: 34 additions & 0 deletions docs/goals/analysis-run-operator-loop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 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 #155 (v0.86.3) so keyboard and AT operators can match a digest
to the API payload. Closed panels stay in the document with `hidden`;
each prefix is a 24px target. Prefer #155 over #139 `cf8c2e8` and
over #135. Do not self-approve or merge from this automation.
3. Write-clock landing is #150. Prefer it over #131. Do not open a
second write-clock PR. Kind-specific pending copy is #149 — do not
open a second pending-copy PR. Retention purge + Storybook tokens
landing is #154. Prefer it over #145/#134/#137.
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. Embedded `data:image` rendering landed as 0.86.1. R&R catalog-id
walks landed as 0.86.2 (#141).
22 changes: 22 additions & 0 deletions docs/storybook-inventory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 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`, `--lw-target-min` | 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. |
| Embedded post image | `--post-body-gap`, `--post-image-padding`, `--post-image-border`, `--post-image-radius`, `--post-image-bg` | Read the picture in document order, then run Extract or Ask if you need the OCR text. |
| 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.86.2",
"version": "0.86.3",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
105 changes: 105 additions & 0 deletions frontend/src/AnalysisRunReproducibilityDigests.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { ANALYSIS_RUN_DIGEST_TARGET_MIN_PX } from "./analysisRunDigests";
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(screen.getByText(CODE_REVISION_SHA)).not.toBeVisible();
expect(screen.getByText(CONFIGURATION_SHA256)).not.toBeVisible();
const codePanelId = codeButton.getAttribute("aria-controls");
const configPanelId = configButton.getAttribute("aria-controls");
expect(codePanelId).toBeTruthy();
expect(configPanelId).toBeTruthy();
expect(document.getElementById(codePanelId ?? "")).toHaveAttribute("hidden");
expect(document.getElementById(configPanelId ?? "")).toHaveAttribute("hidden");
expect(codeButton).toHaveStyle({
minHeight: `${ANALYSIS_RUN_DIGEST_TARGET_MIN_PX}px`,
minWidth: `${ANALYSIS_RUN_DIGEST_TARGET_MIN_PX}px`,
});
});

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)).toBeVisible();
expect(screen.getByText(CONFIGURATION_SHA256)).not.toBeVisible();
await user.keyboard("{Enter}");
expect(screen.getByRole("button", { name: "Code abcdef012345" })).toHaveAttribute(
"aria-expanded",
"false",
);
expect(screen.getByText(CODE_REVISION_SHA)).not.toBeVisible();
});

it("tells the operator to match the revealed digest after activation", async () => {
const user = userEvent.setup();
render(
<AnalysisRunReproducibilityDigests
codeRevisionSha={CODE_REVISION_SHA}
configurationSha256={CONFIGURATION_SHA256}
/>,
);
expect(
screen.getByText("Activate a prefix to read the full digest and match the API payload."),
).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Code abcdef012345" }));
expect(
screen.getByText("Match the revealed digest to the API payload."),
).toBeInTheDocument();
expect(
screen.queryByText("Activate a prefix to read the full digest and match the API payload."),
).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)).toBeVisible();
expect(screen.getByText(CODE_REVISION_SHA)).not.toBeVisible();
});
});
104 changes: 104 additions & 0 deletions frontend/src/AnalysisRunReproducibilityDigests.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useId, useState } from "react";
import {
ANALYSIS_RUN_DIGEST_TARGET_MIN_PX,
analysisRunDigestButtonLabel,
analysisRunDigestNextAction,
analysisRunDigestRevealedNextAction,
type AnalysisRunDigestKind,
} from "./analysisRunDigests";

/**
* One digest disclosure. The button name stays the audible prefix; the
* full value stays in the document with `hidden` until Enter, Space, or
* click so `aria-controls` always has a target (APG Disclosure).
*/
function AnalysisRunDigestDisclosure({
kind,
digest,
panelId,
open,
onOpenChange,
}: {
kind: AnalysisRunDigestKind;
digest: string;
panelId: string;
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
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}
style={{
minHeight: ANALYSIS_RUN_DIGEST_TARGET_MIN_PX,
minWidth: ANALYSIS_RUN_DIGEST_TARGET_MIN_PX,
}}
onClick={() => onOpenChange(!open)}
>
{label}
</button>
<code id={panelId} className="analysis-run-digest-full" hidden={!open}>
{digest}
</code>
</span>
);
}

/**
* Labeled group of analysis-run reproducibility digests.
*
* Prefixes remain the accessible contents of the group. Full digests
* stay off the home list and stay `hidden` on the detail until the
* operator activates a prefix.
*/
export function AnalysisRunReproducibilityDigests({
codeRevisionSha,
configurationSha256,
}: {
codeRevisionSha?: string;
configurationSha256?: string;
}) {
const id = useId();
const [openCode, setOpenCode] = useState(false);
const [openConfig, setOpenConfig] = useState(false);
if (!codeRevisionSha && !configurationSha256) {
return null;
}
const anyOpen =
(Boolean(codeRevisionSha) && openCode) ||
(Boolean(configurationSha256) && openConfig);
return (
<div role="group" aria-label="Analysis run reproducibility digests">
<p className="post-meta">
{anyOpen
? analysisRunDigestRevealedNextAction()
: analysisRunDigestNextAction()}
</p>
<p className="post-meta analysis-run-digest-row">
{codeRevisionSha ? (
<AnalysisRunDigestDisclosure
kind="code"
digest={codeRevisionSha}
panelId={`${id}-code`}
open={openCode}
onOpenChange={setOpenCode}
/>
) : null}
{codeRevisionSha && configurationSha256 ? " · " : null}
{configurationSha256 ? (
<AnalysisRunDigestDisclosure
kind="config"
digest={configurationSha256}
panelId={`${id}-config`}
open={openConfig}
onOpenChange={setOpenConfig}
/>
) : null}
</p>
</div>
);
}
Loading