diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index b662b00b1..044ba479c 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -471,8 +471,13 @@ 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, then compares the
+live ``updated_at`` write clock with that cutoff so the operator can
+decide whether to treat the opened text as reconstructed evidence.
+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.d/0.84.2-analysis-run-live-write-clock.md b/CHANGELOG.d/0.84.2-analysis-run-live-write-clock.md
new file mode 100644
index 000000000..22216cf67
--- /dev/null
+++ b/CHANGELOG.d/0.84.2-analysis-run-live-write-clock.md
@@ -0,0 +1,4 @@
+# 0.84.2 Analysis-run live write clock
+
+Open a cutoff title, then read whether the live body was written after
+that run. Do not treat a later rewrite as reconstructed evidence.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 22f4878b4..767a80ecc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,33 @@ 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.2] - 2026-08-16
+
+### Added
+
+- Opening a cutoff title now compares the live `source_post.updated_at`
+ write clock with that run's knowledge cutoff. Open the Demo Corp
+ lineage run, then a listed title: if the body was rewritten after
+ 2026-01-12, do not treat it as reconstructed evidence. A write clock
+ at or before the cutoff tells you the opened text is still the
+ cutoff corpus. Post-body versioning remains later work (ADR 0016).
+- `GET /api/posts` and `GET /api/posts/{id}` include `updated_at`.
+ Seed historical Demo posts keep `updated_at = created_at` so the
+ January run does not look rewritten at seed time.
+
+## [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..ca0564826 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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.84.0)
+## Analysis-run seed (v0.84.2)
`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
@@ -12,3 +12,7 @@ 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 and compares its
+``updated_at`` write clock with the run cutoff before you treat the
+body as reconstructed evidence (ADR 0016).
diff --git a/backend/app/analysis_run_ingestion.py b/backend/app/analysis_run_ingestion.py
index e96c2b7c4..d72d877be 100644
--- a/backend/app/analysis_run_ingestion.py
+++ b/backend/app/analysis_run_ingestion.py
@@ -244,10 +244,12 @@ async def fetch_visible_scope_posts(
``knowledge_cutoff`` is the analysis clock (W3C Time / ISO 8601-1:2019;
ADR 0013/0016). A later live post must not appear inside an earlier run.
+ ``updated_at`` is the live write clock so the operator can compare
+ today's body with that cutoff before treating it as evidence.
"""
if scope_kind_code == "analysis_scope_corporate_entity" and corporate_entity_id:
rows = await conn.fetch(
- "select post_id, post_title, visibility_code, corporate_entity_id "
+ "select post_id, post_title, visibility_code, corporate_entity_id, updated_at "
"from source_post where corporate_entity_id = $1 "
"and created_at <= $2 "
"order by created_at, post_title",
@@ -256,7 +258,7 @@ async def fetch_visible_scope_posts(
)
elif scope_kind_code == "analysis_scope_process_unit" and process_unit_id:
rows = await conn.fetch(
- "select post_id, post_title, visibility_code, corporate_entity_id "
+ "select post_id, post_title, visibility_code, corporate_entity_id, updated_at "
"from source_post where process_unit_id = $1 "
"and created_at <= $2 "
"order by created_at, post_title",
@@ -265,7 +267,7 @@ async def fetch_visible_scope_posts(
)
elif scope_kind_code == "analysis_scope_thread_group" and scope_key:
rows = await conn.fetch(
- "select post_id, post_title, visibility_code, corporate_entity_id "
+ "select post_id, post_title, visibility_code, corporate_entity_id, updated_at "
"from source_post where thread_group_key = $1 "
"and created_at <= $2 "
"order by created_at, post_title",
@@ -274,7 +276,7 @@ async def fetch_visible_scope_posts(
)
elif scope_kind_code == "analysis_scope_all_visible":
rows = await conn.fetch(
- "select post_id, post_title, visibility_code, corporate_entity_id "
+ "select post_id, post_title, visibility_code, corporate_entity_id, updated_at "
"from source_post where created_at <= $1 "
"order by created_at, post_title",
knowledge_cutoff,
@@ -287,5 +289,11 @@ async def fetch_visible_scope_posts(
visible = row["visibility_code"] == "public" or str(row["corporate_entity_id"]) in affiliated
if not visible:
continue
- posts.append({"post_id": str(row["post_id"]), "post_title": row["post_title"]})
+ posts.append(
+ {
+ "post_id": str(row["post_id"]),
+ "post_title": row["post_title"],
+ "updated_at": _iso(row["updated_at"]),
+ }
+ )
return posts
diff --git a/backend/app/main.py b/backend/app/main.py
index e77b173bc..746890162 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -278,7 +278,11 @@ def _can_see_post(account: CurrentAccount, post: asyncpg.Record) -> bool:
def _serialize_post(post: asyncpg.Record, labels: dict[str, str] | None = None) -> dict[str, Any]:
- """Turn a ``source_post`` row into the public JSON shape."""
+ """Turn a ``source_post`` row into the public JSON shape.
+
+ ``updated_at`` is the live write clock the analysis-run popup
+ compares with ``knowledge_cutoff`` (ADR 0016).
+ """
resolved = labels or {}
voc = post["voc_type_code"]
visibility = post["visibility_code"]
@@ -290,6 +294,7 @@ def _serialize_post(post: asyncpg.Record, labels: dict[str, str] | None = None)
"visibility_code": visibility,
"visibility_label": resolved.get(visibility, visibility),
"created_at": post["created_at"].isoformat(),
+ "updated_at": post["updated_at"].isoformat(),
}
@@ -351,7 +356,7 @@ async def list_posts(
_require_post_read(account)
async with pool.acquire() as conn:
rows = await conn.fetch(
- "select post_id, post_title, voc_type_code, visibility_code, corporate_entity_id, created_at "
+ "select post_id, post_title, voc_type_code, visibility_code, corporate_entity_id, created_at, updated_at "
"from source_post order by created_at desc"
)
visible = [row for row in rows if _can_see_post(account, row)]
@@ -369,7 +374,7 @@ async def read_post(
_require_post_read(account)
async with pool.acquire() as conn:
row = await conn.fetchrow(
- "select post_id, post_title, post_body, voc_type_code, visibility_code, corporate_entity_id, created_at "
+ "select post_id, post_title, post_body, voc_type_code, visibility_code, corporate_entity_id, created_at, updated_at "
"from source_post where post_id = $1",
post_id,
)
diff --git a/docs/adr/0016-analysis-run-knowledge-cutoff-posts.md b/docs/adr/0016-analysis-run-knowledge-cutoff-posts.md
index d6ac70db8..dbb48eebd 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,9 +44,14 @@ 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.
-- Post-body versioning at the cutoff remains future work.
+- Open the run, read the live-body warning, then open a listed post.
+ The opened popup compares ``source_post.updated_at`` with this
+ cutoff. If the live row was written after the run, do not treat the
+ body as reconstructed evidence.
+- 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. The write
+ clock comparison is the operator action until that snapshot exists.
## References
@@ -48,3 +61,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..74c308d25 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 compares the live `updated_at` write clock with 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..72de2d8af 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
- "version": "0.84.0",
+ "version": "0.84.2",
"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..bba17b3d7 100644
--- a/frontend/src/App.test.tsx
+++ b/frontend/src/App.test.tsx
@@ -59,6 +59,7 @@ describe("App, authenticated", () => {
chatUnavailable?: boolean;
searchUnavailable?: boolean;
verificationEvidenceUrl?: string | null;
+ postUpdatedAt?: string;
}) {
const statusLabel: Record
+ {analysisRunLiveBodyComparison(knowledgeCutoff, post.updated_at)} +
+ ) : null}{post.post_body}
+ + Hover a prefix to read the full digest for verification.{" "} + + {codeRevisionSha ? ( + {`Code ${analysisRunDigestPrefix(codeRevisionSha)}`} + ) : null} + {codeRevisionSha && configurationSha256 ? " · " : null} + {configurationSha256 ? ( + + {`Config ${analysisRunDigestPrefix(configurationSha256)}`} + + ) : null} +
+- {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)}` - : ""} -
- )} +{corpusHint}
} +{analysisRunLivePostWarning(selected.knowledge_cutoff)}
{rebuildError}
} {!graph &&Loading lineage graph...
} - {graph &&