From 43b246e17b31df997312bb27f1264736ab359a0b Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sun, 16 Aug 2026 15:34:47 +0000
Subject: [PATCH 1/4] fix(ui): show analysis-run digest prefixes on detail
The #89 review asked for 12-character code and config prefixes so an
operator can match the approved revision. Full digests stay on the API
only. Do not merge until this review item is checked.
Co-authored-by: Seongho Bae
---
frontend/src/App.test.tsx | 10 ++++++++++
frontend/src/App.tsx | 13 +++++++++++++
frontend/src/api.ts | 2 ++
3 files changed, 25 insertions(+)
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
index a3da5de6b..570d9a684 100644
--- a/frontend/src/App.test.tsx
+++ b/frontend/src/App.test.tsx
@@ -190,6 +190,9 @@ describe("App, authenticated", () => {
},
],
visible_posts: [{ post_id: "post-1", post_title: "Public post" }],
+ code_revision_sha: "abcdef0123456789deadbeefcafebabe",
+ configuration_sha256:
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
status_history: [
{
status_ordinal: 1,
@@ -1386,6 +1389,13 @@ describe("App, authenticated", () => {
expect(await screen.findByRole("heading", { name: "Lineage reconstruction · Succeeded · Demo Corp" })).toBeInTheDocument();
expect(screen.getByText(/Cutoff 2026-01-12/)).toBeInTheDocument();
expect(screen.getByText(/Requested 2026-01-12/)).toBeInTheDocument();
+ const digests = screen.getByLabelText("Analysis run reproducibility digests");
+ expect(digests).toHaveTextContent("Code abcdef012345");
+ expect(digests).toHaveTextContent("Config 0123456789ab");
+ expect(digests).not.toHaveTextContent("abcdef0123456789deadbeefcafebabe");
+ expect(digests).not.toHaveTextContent(
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
+ );
const history = screen.getByRole("list", { name: "Analysis run status history" });
expect(history).toHaveTextContent("Pending 2026-01-12 12:31");
expect(history).toHaveTextContent("Running 2026-01-12 12:32");
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index f866bb302..3952de96c 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1431,6 +1431,19 @@ function AnalysisRunsPanel({
{" · "}
Requested {selected.requested_at.slice(0, 10)}
+ {(selected.code_revision_sha || selected.configuration_sha256) && (
+
+ {selected.code_revision_sha
+ ? `Code ${selected.code_revision_sha.slice(0, 12)}`
+ : ""}
+ {selected.code_revision_sha && selected.configuration_sha256
+ ? " · "
+ : ""}
+ {selected.configuration_sha256
+ ? `Config ${selected.configuration_sha256.slice(0, 12)}`
+ : ""}
+
+ )}
{selected.source_counts.map((count) => (
-
diff --git a/frontend/src/api.ts b/frontend/src/api.ts
index 3dacb054c..740529cba 100644
--- a/frontend/src/api.ts
+++ b/frontend/src/api.ts
@@ -521,6 +521,8 @@ export interface AnalysisRun {
source_counts: AnalysisRunCount[];
status_history?: AnalysisRunStatusEvent[];
visible_posts?: { post_id: string; post_title: string }[];
+ code_revision_sha?: string;
+ configuration_sha256?: string;
}
export function fetchAnalysisRuns(accessToken: string): Promise<{ analysis_runs: AnalysisRun[] }> {
From d2574d25536c7be5c1297d5f68856b2eb2059ee3 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sun, 16 Aug 2026 15:44:03 +0000
Subject: [PATCH 2/4] fix(ui): keep analysis-run digests audible and warn on
live posts
aria-label on the digest paragraph hid the prefixes from assistive
technology. Move the label to a group, keep prefixes as visible text,
and put the full digest on hover. Tell the operator that a cutoff
title opens the live body so they compare it with the run clock.
Co-authored-by: Seongho Bae
---
ARCHITECTURE.md | 6 +-
.../0.84.1-analysis-run-digest-a11y.md | 5 ++
CHANGELOG.md | 13 ++++
CLAUDE.md | 3 +
...016-analysis-run-knowledge-cutoff-posts.md | 20 ++++-
.../ANALYSIS_RUN_REGISTRY_REFERENCES.md | 6 +-
frontend/package.json | 2 +-
frontend/src/App.css | 21 ++++-
frontend/src/App.test.tsx | 31 +++++++-
frontend/src/App.tsx | 76 +++++++++++++++----
lineageweave/__init__.py | 2 +-
pyproject.toml | 2 +-
uv.lock | 2 +-
13 files changed, 161 insertions(+), 28 deletions(-)
create mode 100644 CHANGELOG.d/0.84.1-analysis-run-digest-a11y.md
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index b662b00b1..ef6799146 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -471,8 +471,10 @@ revision and configuration digest prefixes.
`tepp_client` on that same snapshot; the default transport is
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, counts, status history)
-without exposing a DSN or raw record. Status history is detail-only
+labeled detail (cutoff, requested date, 12-character digest prefixes
+with full digests on hover, 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
its machine `failure_code` rather than an invented caption. Failed
list rows add a next-action line (open the run, then connect the
diff --git a/CHANGELOG.d/0.84.1-analysis-run-digest-a11y.md b/CHANGELOG.d/0.84.1-analysis-run-digest-a11y.md
new file mode 100644
index 000000000..213eb5451
--- /dev/null
+++ b/CHANGELOG.d/0.84.1-analysis-run-digest-a11y.md
@@ -0,0 +1,5 @@
+# 0.84.1 Analysis-run digest a11y and live-body warning
+
+Detail prefixes stay audible and hoverable. Open a cutoff title only
+after reading that the live body may have changed since the run.
+The list stays aggregates-only.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 22f4878b4..167fcdea0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,19 @@ 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.84.1] - 2026-08-16
+
+### Fixed
+
+- Analysis-run detail keeps 12-character digest prefixes as visible
+ text (so assistive technology hears `Code` / `Config` values) and
+ puts the full digest on hover. Open the Demo Corp lineage run, hover
+ a prefix, and match it to the API payload. The home list still hides
+ digests even when the list JSON includes them.
+- Opening a cutoff title now says the live body may have changed after
+ that run. Compare the opened post with the cutoff date before you
+ treat it as reconstructed evidence (ADR 0016).
+
## [0.84.0] - 2026-08-16
### Added
diff --git a/CLAUDE.md b/CLAUDE.md
index 3af72ad45..bfbd099ae 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -12,3 +12,6 @@ transport or an unused accepted envelope is Failed
theta or a local psychometric substitute. The home list caption stays
`kind · status · entity`; the machine failure code is detail-only
(ADR 0014). Open the Failed row, then connect a live TEPP transport.
+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/0016-analysis-run-knowledge-cutoff-posts.md b/docs/adr/0016-analysis-run-knowledge-cutoff-posts.md
index d6ac70db8..f9c82a86d 100644
--- a/docs/adr/0016-analysis-run-knowledge-cutoff-posts.md
+++ b/docs/adr/0016-analysis-run-knowledge-cutoff-posts.md
@@ -25,7 +25,15 @@ every scope branch (corporate entity, process unit, thread group, and
all-visible). ABAC visibility is applied after that temporal gate.
Click-through still opens the live post body -- post versioning is a
later slice -- but the run list itself must not advertise a post the
-run was not allowed to know.
+run was not allowed to know. The detail must say that next action
+plainly: compare the opened body with this cutoff before treating it
+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.
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
@@ -36,8 +44,10 @@ run.
- After `make seed`, the Demo Corp lineage run lists Demo public post
and other in-cutoff Demo Corp titles. The later fixture account-review
post (2026-02-10) does not appear.
-- Open the run, then open a listed post, to inspect what that cutoff
- actually reconstructed.
+- 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.
- Post-body versioning at the cutoff remains future work.
## References
@@ -48,3 +58,7 @@ rules* (confirmed 2024; Amendment 1:2022).
World Wide Web Consortium. (2022). *Time ontology in OWL* (W3C
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/
diff --git a/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md b/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md
index a1dc73957..b41b31c17 100644
--- a/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md
+++ b/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md
@@ -8,7 +8,8 @@
| Source | Product implication | Implemented evidence |
|---|---|---|
| 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). |
+| 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. |
| 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. |
@@ -98,5 +99,8 @@ PostgreSQL Global Development Group. (2026). *PostgreSQL 18 documentation:
World Wide Web Consortium. (2013). *PROV-O: The PROV ontology* (W3C
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. (2022). *Time ontology in OWL* (W3C Recommendation).
https://www.w3.org/TR/owl-time/
diff --git a/frontend/package.json b/frontend/package.json
index c21ed209f..8ce5f334b 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
- "version": "0.84.0",
+ "version": "0.84.1",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/App.css b/frontend/src/App.css
index dfd0f2e81..8f38b4dd0 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -85,9 +85,26 @@
cursor: pointer;
}
+:root {
+ --lw-opacity-meta: 0.7;
+ --lw-font-size-meta: 0.85rem;
+}
+
.post-meta {
- opacity: 0.7;
- font-size: 0.85rem;
+ opacity: var(--lw-opacity-meta);
+ font-size: var(--lw-font-size-meta);
+}
+
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip-path: inset(50%);
+ white-space: nowrap;
+ border: 0;
}
.post-body {
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
index 65763b7fa..927a13144 100644
--- a/frontend/src/App.test.tsx
+++ b/frontend/src/App.test.tsx
@@ -283,6 +283,9 @@ describe("App, authenticated", () => {
count_value: 3,
},
],
+ code_revision_sha: "abcdef0123456789deadbeefcafebabe",
+ configuration_sha256:
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
},
{
analysis_run_id: "run-demo-tepp",
@@ -1448,6 +1451,12 @@ describe("App, authenticated", () => {
expect(list).toHaveTextContent("3 documents");
expect(list).not.toHaveTextContent("postgresql://");
expect(list).not.toHaveTextContent("select ");
+ expect(list).not.toHaveTextContent("Code abcdef012345");
+ expect(list).not.toHaveTextContent("Config 0123456789ab");
+ expect(list).not.toHaveTextContent("abcdef0123456789deadbeefcafebabe");
+ expect(list).not.toHaveTextContent(
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
+ );
await userEvent.click(
screen.getByRole("button", {
@@ -1458,21 +1467,39 @@ describe("App, authenticated", () => {
expect(screen.getByText(/Cutoff 2026-01-12/)).toBeInTheDocument();
expect(screen.getByText(/Requested 2026-01-12/)).toBeInTheDocument();
const digests = screen.getByLabelText("Analysis run reproducibility digests");
+ expect(digests).toHaveTextContent("Hover a prefix to read the full digest for verification.");
expect(digests).toHaveTextContent("Code abcdef012345");
expect(digests).toHaveTextContent("Config 0123456789ab");
expect(digests).not.toHaveTextContent("abcdef0123456789deadbeefcafebabe");
expect(digests).not.toHaveTextContent(
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
);
+ expect(screen.getByTitle("abcdef0123456789deadbeefcafebabe")).toHaveTextContent("Code abcdef012345");
+ expect(
+ screen.getByTitle("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"),
+ ).toHaveTextContent("Config 0123456789ab");
const history = screen.getByRole("list", { name: "Analysis run status history" });
expect(history).toHaveTextContent("Pending 2026-01-12 12:31");
expect(history).toHaveTextContent("Running 2026-01-12 12:32");
expect(history).toHaveTextContent("Succeeded 2026-01-12 12:33");
expect(screen.getByRole("list", { name: "Posts known at this run cutoff" })).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "Open run post: Public post" })).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ "Opening a title shows the live post. Compare it with cutoff 2026-01-12 before you treat the body as reconstructed evidence — it may have changed after this run.",
+ ),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", {
+ name: "Open live post (may have changed after cutoff): Public post",
+ }),
+ ).toBeInTheDocument();
expect(screen.queryByText(/postgresql:\/\//)).not.toBeInTheDocument();
- await userEvent.click(screen.getByRole("button", { name: "Open run post: Public post" }));
+ await userEvent.click(
+ screen.getByRole("button", {
+ name: "Open live post (may have changed after cutoff): Public post",
+ }),
+ );
await waitFor(() => expect(screen.getByText("The full body text.")).toBeInTheDocument());
await userEvent.click(
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 9d947ee47..fda1cdd57 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1395,6 +1395,62 @@ function analysisRunCorpusHint(run: AnalysisRun): string | null {
);
}
+/** Git-style prefix. The full digest stays on `title` for verification. */
+const ANALYSIS_RUN_DIGEST_PREFIX_LENGTH = 12;
+
+function analysisRunDigestPrefix(digest: string): string {
+ return digest.slice(0, ANALYSIS_RUN_DIGEST_PREFIX_LENGTH);
+}
+
+/**
+ * Next action when a cutoff title opens the live post (ADR 0016).
+ *
+ * Post-body versioning is a later slice. Until then the operator must
+ * compare the opened body with this run's cutoff instead of treating
+ * today's text as reconstructed evidence.
+ */
+function analysisRunLivePostWarning(cutoffIso: string): string {
+ const cutoffDate = cutoffIso.slice(0, 10);
+ return (
+ `Opening a title shows the live post. Compare it with cutoff ${cutoffDate} ` +
+ "before you treat the body as reconstructed evidence — it may have changed after this run."
+ );
+}
+
+function analysisRunLivePostButtonLabel(postTitle: string): string {
+ return `Open live post (may have changed after cutoff): ${postTitle}`;
+}
+
+function AnalysisRunReproducibilityDigests({
+ codeRevisionSha,
+ configurationSha256,
+}: {
+ codeRevisionSha?: string;
+ configurationSha256?: string;
+}) {
+ if (!codeRevisionSha && !configurationSha256) {
+ return null;
+ }
+ return (
+
+
+
+ Hover a prefix to read the full digest for verification.{" "}
+
+ {codeRevisionSha ? (
+ {`Code ${analysisRunDigestPrefix(codeRevisionSha)}`}
+ ) : null}
+ {codeRevisionSha && configurationSha256 ? " · " : null}
+ {configurationSha256 ? (
+
+ {`Config ${analysisRunDigestPrefix(configurationSha256)}`}
+
+ ) : null}
+
+
+ );
+}
+
function AnalysisRunsPanel({
accessToken,
onSelectPost,
@@ -1477,19 +1533,10 @@ function AnalysisRunsPanel({
{" · "}
Requested {selected.requested_at.slice(0, 10)}
- {(selected.code_revision_sha || selected.configuration_sha256) && (
-
- {selected.code_revision_sha
- ? `Code ${selected.code_revision_sha.slice(0, 12)}`
- : ""}
- {selected.code_revision_sha && selected.configuration_sha256
- ? " · "
- : ""}
- {selected.configuration_sha256
- ? `Config ${selected.configuration_sha256.slice(0, 12)}`
- : ""}
-
- )}
+