Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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 the v0.86.3 digest-disclosure successor of #139 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 that successor over #135 and over #139 `cf8c2e8` (stale
0.86.1 on `3c17fd3`). 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. 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
Loading