Skip to content
Merged
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
6 changes: 4 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
TEPP list rows add a next-action line (open the run, then connect the
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.d/0.84.1-analysis-run-digest-a11y.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion 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.84.0)
## Analysis-run seed (v0.84.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,3 +14,6 @@ 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.
Opening a cutoff title shows the live post -- compare it with the
cutoff before treating the body as reconstructed evidence (ADR 0016).
20 changes: 17 additions & 3 deletions docs/adr/0016-analysis-run-knowledge-cutoff-posts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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/
6 changes: 5 additions & 1 deletion docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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/
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.84.0",
"version": "0.84.1",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
21 changes: 19 additions & 2 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
31 changes: 29 additions & 2 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,9 @@ describe("App, authenticated", () => {
count_value: 3,
},
],
code_revision_sha: "abcdef0123456789deadbeefcafebabe",
configuration_sha256:
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
},
{
analysis_run_id: "run-demo-tepp",
Expand Down Expand Up @@ -1460,6 +1463,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", {
Expand All @@ -1470,21 +1479,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(
Expand Down
76 changes: 62 additions & 14 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1406,6 +1406,62 @@ function analysisRunCorpusHint(run: AnalysisRun): string | null {
return "These posts are the cutoff corpus this TEPP run measured.";
}

/** 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 (
<div role="group" aria-label="Analysis run reproducibility digests">
<p className="post-meta">
<span className="visually-hidden">
Hover a prefix to read the full digest for verification.{" "}
Comment thread
cursor[bot] marked this conversation as resolved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This visually-hidden sentence is still announced, and it still names a pointer-only action. Keyboard and assistive-technology operators cannot read the full digest on this head. Keep prefixes audible here; the v0.84.3 successor turns each prefix into an APG disclosure (aria-expanded, Enter/Space) so the operator can match the API payload without a hover title.

</span>
{codeRevisionSha ? (
<span title={codeRevisionSha}>{`Code ${analysisRunDigestPrefix(codeRevisionSha)}`}</span>
) : null}
{codeRevisionSha && configurationSha256 ? " · " : null}
{configurationSha256 ? (
<span title={configurationSha256}>
{`Config ${analysisRunDigestPrefix(configurationSha256)}`}
</span>
) : null}
</p>
</div>
);
}

function AnalysisRunsPanel({
accessToken,
onSelectPost,
Expand Down Expand Up @@ -1488,19 +1544,10 @@ function AnalysisRunsPanel({
{" · "}
Requested {selected.requested_at.slice(0, 10)}
</p>
{(selected.code_revision_sha || selected.configuration_sha256) && (
<p className="post-meta" aria-label="Analysis run reproducibility digests">
{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)}`
: ""}
</p>
)}
<AnalysisRunReproducibilityDigests
codeRevisionSha={selected.code_revision_sha}
configurationSha256={selected.configuration_sha256}
/>
<ul>
{selected.source_counts.map((count) => (
<li key={count.count_type_code}>
Expand All @@ -1521,12 +1568,13 @@ function AnalysisRunsPanel({
{selected.visible_posts && selected.visible_posts.length > 0 ? (
<>
{corpusHint && <p className="post-meta">{corpusHint}</p>}
<p className="post-meta">{analysisRunLivePostWarning(selected.knowledge_cutoff)}</p>
<ul aria-label="Posts known at this run cutoff">
{selected.visible_posts.map((post) => (
<li key={post.post_id}>
<button
className="keyman-select"
aria-label={`Open run post: ${post.post_title}`}
aria-label={analysisRunLivePostButtonLabel(post.post_title)}
onClick={() => onSelectPost(post.post_id)}
>
{post.post_title}
Expand Down
2 changes: 1 addition & 1 deletion lineageweave/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@
"sentence_excerpts",
]

__version__ = "0.84.0"
__version__ = "0.84.1"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "lineageweave"
version = "0.84.0"
version = "0.84.1"
description = "Reconstructs git-branch-style lineage DAGs from scattered short records using multi-channel score fusion and LLM adjudication."
readme = "README.md"
license = { text = "MIT" }
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.