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
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ R&R chips read the catalog id stored on `post_summary_role`
backfill leaves a role unbound when two same-named mentions already
exist on the post.

A listed analysis-run that then 404s must stay generic: do not name the thread or the cutoff,
and do not say the run is not visible (ADR 0014 / ADR 0018). After
that 404, re-read the authorized list so the stale row does not stay
clickable. Announce the next action with `role="alert"` without
moving focus. Remaining visible runs stay clickable. Request remains
the named reconstruction control.

## CI gates

`.github/workflows/tests.yml` runs the full suite on every PR to `main`.
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.d/2.10.3-stale-hidden-run-list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# 2.10.3 Drop a stale analysis-run row after its detail 404s

Opening a listed analysis-run that then 404s drops that stale row
from the home list after an authorized re-read, announces the next
action with a status alert, and leaves Request as the named
reconstruction control. The message still does not name the thread
or the cutoff (ADR 0014 / ADR 0018).
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ 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).

## [2.10.3] - 2026-08-17

### Fixed

- Opening a listed analysis-run that then 404s drops that stale row
from the home list after an authorized re-read. The next action is
announced as a status alert: open a remaining visible run, or
request a lineage reconstruction. The message still does not name
the thread or the cutoff (ADR 0014 / ADR 0018).

## [2.10.2] - 2026-08-17

### Fixed
Expand Down
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,7 @@ cited source. After that next action, the popup lands the first cited
evidence. Changing the week first still
focuses the report period field. Mean θ stays on the period-report
panel.
A listed analysis-run that then 404s stays generic: do not name the
thread or the cutoff. After that 404, re-read the authorized list so
the stale row does not stay clickable. Announce the next action with
`role="alert"` without moving focus.
9 changes: 8 additions & 1 deletion docs/adr/0014-authorized-analysis-run-read.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,14 @@ lifecycle the registry already stored. `POST /api/analysis-runs` now
records a Pending lineage run on an authorized cutoff capture
(ADR 0017). TEPP and period-report kinds are 422. Reconstruction, a
live TEPP transport, and a fuller Analysis Run Console remain later
slices.
slices. A 404 on a hidden run (including a thread-group row that
still lacks an in-cutoff visible post, ADR 0018) must stay generic:
do not name the thread or the cutoff, and do not say the run is not
visible. Tell the operator to open a visible run from the home list,
or request a lineage reconstruction for a corporation they already
walk. After that 404, re-read `GET /api/analysis-runs` so the stale
list row does not stay clickable, and announce the status with
`role="alert"` (WCAG 2.2 SC 4.1.3) without moving focus.

## References

Expand Down
3 changes: 3 additions & 0 deletions docs/adr/0018-related-nodes-team-org-walk.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ Thread-group run list visibility requires at least one ABAC-visible
organization chip.
- A later public post in a thread group no longer lists a January run
that could not have known that post.
- A 404 on that hidden row stays generic: do not name the thread or
the cutoff. After that 404, re-read the authorized home list so the
stale row does not stay clickable (ADR 0014).

## References

Expand Down
1 change: 1 addition & 0 deletions docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
| 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). Detail compares live `updated_at` with that cutoff and marks titles rewritten after the run. Seed records the built period report as a later Succeeded run on that same snapshot (ADR 0024) without copying a theta onto the registry row. The cutoff-known body is read from `source_post_revision` on the opened post, not from the run payload (ADR 0025). |
| 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. List-button names include the kind-specific next-action sentence (ADR 0014; WCAG 2.2 SC 4.1.2). |
| WCAG 2.2 SC 4.1.3 Status Messages | Announce a hidden-run 404 without confirming why the row is hidden, then drop the stale list row. | `StatusAlert` (`role="alert"`) plus a post-404 `GET /api/analysis-runs` re-read (ADR 0014 / ADR 0018). |
| 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, `invoking_session_role` on each retention event, and exclusion of raw source/provider payloads. |
Expand Down
7 changes: 6 additions & 1 deletion docs/doctoring/DESIGN_TOKEN_REFERENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ the Storybook inventory.

| Source | Product implication | Implemented evidence |
|---|---|---|
| W3C Design Tokens Format Module 1.0 | Name color, space, type, and radius once; consume those names from repeated objects. | `frontend/src/styles/tokens.css` defines `--color-*`, `--space-*`, `--size-control-min`, `--radius-chip`, `--radius-control`, `--radius-panel`, and `--font-*`. `CitationChip`, `PopupCloseButton`, `CutoffKnownBody`, and `LineageEntityPicker` read those names through `App.css`. |
| W3C Design Tokens Format Module 1.0 | Name color, space, type, and radius once; consume those names from repeated objects. | `frontend/src/styles/tokens.css` defines `--color-*`, `--space-*`, `--size-control-min`, `--radius-chip`, `--radius-control`, `--radius-panel`, and `--font-*`. `CitationChip`, `PopupCloseButton`, `CutoffKnownBody`, `LineageEntityPicker`, and `StatusAlert` read those names through `App.css`. |
| Storybook for React & Vite | Catalog repeated controls so a buyer can try the next click without reading `App.tsx`. | `frontend/src/components/*.stories.tsx` and `docs/storybook-inventory.md`. |
| WCAG 2.2 SC 4.1.3 Status Messages | Announce a fail-closed status without moving focus, so the operator hears the next action. | `StatusAlert` uses `role="alert"` and `--color-status-alert`. |

## APA 7th references

Expand All @@ -18,3 +19,7 @@ Design Tokens Community Group. (2025). *Design Tokens Format Module 1.0*

Storybook. (2026). *Storybook for React & Vite*.
https://storybook.js.org/docs/get-started/frameworks/react-vite

World Wide Web Consortium. (2023). *Web content accessibility
guidelines (WCAG) 2.2* (W3C Recommendation).
https://www.w3.org/TR/WCAG22/
5 changes: 5 additions & 0 deletions docs/storybook-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ buyer-facing control you can click before changing product CSS.
| `AnalysisRun/CutoffKnownBody` | Read the cutoff-known sentence, then compare it with the live body below. | `--color-accent-border`, `--space-panel-block`, `--radius-panel`, `CutoffKnownBody` |
| `Analysis/LineageEntityPicker` | Choose which corp to reconstruct, then click Request a lineage reconstruction. | `--space-control-gap`, `--size-control-min`, `--radius-control`, `LineageEntityPicker` |
| `Chrome/PopupCloseButton` | Close the evidence panel or post popup. | `--space-close-inset`, `--font-size-close`, `PopupCloseButton` |
| `Chrome/StatusAlert` | Hear the next action after a hidden-run 404, then open a visible run or request a reconstruction. | `--color-status-alert`, `StatusAlert` |

Repeated web objects must use `frontend/src/styles/tokens.css` and a module
under `frontend/src/components/`. Do not add a second Node package manager;
Expand All @@ -21,3 +22,7 @@ Design Tokens Community Group. (2025). *Design Tokens Format Module 1.0*

Storybook. (2026). *Storybook for React & Vite*.
https://storybook.js.org/docs/get-started/frameworks/react-vite

World Wide Web Consortium. (2023). *Web content accessibility
guidelines (WCAG) 2.2* (W3C Recommendation).
https://www.w3.org/TR/WCAG22/
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": "2.10.2",
"version": "2.10.3",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
color: #b91c1c;
}

.status-alert {
color: var(--color-status-alert);
}

.post-list {
list-style: none;
padding: 0;
Expand Down
61 changes: 59 additions & 2 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ describe("App, authenticated", () => {
succeededReportRun?: boolean;
succeededTeppRun?: boolean;
pendingTeppRun?: boolean;
hiddenAnalysisRun?: boolean;
pluralAffiliations?: boolean;
deferMe?: boolean;
meFailed?: boolean;
Expand Down Expand Up @@ -101,6 +102,7 @@ describe("App, authenticated", () => {
let nextEventId = 1;
let createdPendingLineage: Record<string, unknown> | null = null;
let createdPendingTepp: Record<string, unknown> | null = null;
let analysisRunListCalls = 0;

let releaseMe = () => {};
const meReady = options?.deferMe
Expand Down Expand Up @@ -371,6 +373,14 @@ describe("App, authenticated", () => {
);
}
if (url.endsWith("/api/analysis-runs/run-demo-lineage")) {
if (options?.hiddenAnalysisRun) {
return Promise.resolve(
new Response(JSON.stringify({ detail: "Not found" }), {
status: 404,
headers: { "Content-Type": "application/json" },
}),
);
}
return Promise.resolve(
jsonResponse({
analysis_run_id: "run-demo-lineage",
Expand Down Expand Up @@ -623,12 +633,18 @@ describe("App, authenticated", () => {
return Promise.resolve(new Response(JSON.stringify(created), { status: 201 }));
}
if (url.endsWith("/api/analysis-runs")) {
analysisRunListCalls += 1;
const includeStaleLineageRow = !(
options?.hiddenAnalysisRun && analysisRunListCalls > 1
);
return Promise.resolve(
jsonResponse({
analysis_runs: [
...(createdPendingLineage ? [createdPendingLineage] : []),
...(createdPendingTepp ? [createdPendingTepp] : []),
{
...(includeStaleLineageRow
? [
{
analysis_run_id: "run-demo-lineage",
run_kind_code: "analysis_run_lineage",
run_kind_label: "Lineage reconstruction",
Expand Down Expand Up @@ -657,7 +673,9 @@ describe("App, authenticated", () => {
code_revision_sha: "abcdef0123456789deadbeefcafebabe",
configuration_sha256:
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
},
},
]
: []),
{
analysis_run_id: "run-demo-tepp",
run_kind_code: "analysis_run_tepp",
Expand Down Expand Up @@ -2279,6 +2297,45 @@ describe("App, authenticated", () => {
expect(screen.queryByRole("heading", { name: "Body this run knew" })).not.toBeInTheDocument();
});

it("drops a stale listed run after its detail 404s and names the next action", async () => {
stubBackend({ hiddenAnalysisRun: true });
render(<App />);

await screen.findByRole("list", { name: "Analysis runs" });
await userEvent.click(
screen.getByRole("button", {
name: "Open analysis run: Lineage reconstruction · Succeeded · Demo Corp",
}),
);

expect(await screen.findByRole("alert")).toHaveTextContent(
"This run is not on your list. Open a visible run from the home list, or request a lineage reconstruction for a corporation you already walk.",
);
await waitFor(() => {
expect(
screen.queryByRole("button", {
name: "Open analysis run: Lineage reconstruction · Succeeded · Demo Corp",
}),
).not.toBeInTheDocument();
});
expect(
screen.getByRole("button", {
name: "Open analysis run: TEPP measurement · Failed · Demo Corp. Open this run to see why it failed, then connect the measurement service and re-run.",
}),
).toBeInTheDocument();
expect(
screen.getByRole("button", {
name: "Open analysis run: Period report · Succeeded · Demo Corp",
}),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Request a lineage reconstruction" }),
).toBeInTheDocument();
expect(screen.queryByText(/not visible/i)).not.toBeInTheDocument();
expect(screen.queryByText(/thread-group/i)).not.toBeInTheDocument();
expect(screen.queryByText(/knowledge_cutoff/i)).not.toBeInTheDocument();
});

it("finds a failed TEPP list button by the next-action accessible name", async () => {
stubBackend();
render(<App />);
Expand Down
24 changes: 21 additions & 3 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import { CitationChip } from "./components/CitationChip";
import { CutoffKnownBody } from "./components/CutoffKnownBody";
import { LineageEntityPicker } from "./components/LineageEntityPicker";
import { PopupCloseButton } from "./components/PopupCloseButton";
import { StatusAlert } from "./components/StatusAlert";
import { LineageDag } from "./LineageDag";
import { PostBody } from "./PostBody";
import { subgraphForPost } from "./lineageLayout";
Expand Down Expand Up @@ -1819,6 +1820,17 @@ function analysisRunAccessibleName(run: AnalysisRun): string {
return `Open analysis run: ${caption}. ${nextAction}`;
}

/**
* Next action when detail 404s. Stay generic: do not name the thread or the cutoff.
* Naming either would confirm a hidden row (ADR 0018).
*/
function analysisRunHiddenNextAction(): string {
return (
"This run is not on your list. Open a visible run from the home list, " +
"or request a lineage reconstruction for a corporation you already walk."
);
}

/**
* Empty-corpus copy that tells the operator what to do next.
*/
Expand Down Expand Up @@ -2172,18 +2184,24 @@ function AnalysisRunsPanel({
} catch (err) {
setSelected(null);
if (err instanceof BackendError && err.status === 404) {
setError("This analysis run is not visible.");
setError(analysisRunHiddenNextAction());
try {
setRuns((await fetchAnalysisRuns(accessToken)).analysis_runs);
} catch {
// Keep the last authorized list if the re-read fails.
}
return;
}
setError(String(err));
}
}

if (error && runs === null) return <p className="error">{error}</p>;
if (error && runs === null) return <StatusAlert>{error}</StatusAlert>;
if (runs === null) return <p>Loading analysis runs...</p>;

const corpusHint = selected ? analysisRunCorpusHint(selected) : null;
const selectedNextAction = selected ? analysisRunNextAction(selected) : null;
const statusMessage = error ?? entitiesLoadError;

return (
<section className="popup-section lineage-home">
Expand All @@ -2208,7 +2226,7 @@ function AnalysisRunsPanel({
{requestLabel}
</button>
</div>
{(error || entitiesLoadError) && <p className="error">{error ?? entitiesLoadError}</p>}
{statusMessage ? <StatusAlert>{statusMessage}</StatusAlert> : null}
{runs.length === 0 ? (
<p className="popup-placeholder">
No analysis runs visible to this account yet. Request a lineage
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/components/StatusAlert.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { StatusAlert } from "./StatusAlert";

const meta = {
title: "Chrome/StatusAlert",
component: StatusAlert,
args: {
children:
"This run is not on your list. Open a visible run from the home list, or request a lineage reconstruction for a corporation you already walk.",
},
} satisfies Meta<typeof StatusAlert>;

export default meta;

type Story = StoryObj<typeof meta>;

export const HiddenAnalysisRun: Story = {};

export const ListLoadFailure: Story = {
args: {
children: "BackendError: 503 Service Unavailable",
},
};
23 changes: 23 additions & 0 deletions frontend/src/components/StatusAlert.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { StatusAlert } from "./StatusAlert";

describe("StatusAlert", () => {
it("announces the next action as an alert without moving focus", () => {
render(
<StatusAlert>
This run is not on your list. Open a visible run from the home list, or request a lineage reconstruction for a corporation you already walk.
</StatusAlert>,
);

const alert = screen.getByRole("alert");
expect(alert).toHaveTextContent(
"This run is not on your list. Open a visible run from the home list, or request a lineage reconstruction for a corporation you already walk.",
);
expect(alert.tagName).toBe("P");
expect(document.activeElement).not.toBe(alert);
expect(screen.queryByText(/not visible/i)).not.toBeInTheDocument();
expect(screen.queryByText(/thread-group/i)).not.toBeInTheDocument();
expect(screen.queryByText(/knowledge_cutoff/i)).not.toBeInTheDocument();
});
});
18 changes: 18 additions & 0 deletions frontend/src/components/StatusAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export type StatusAlertProps = {
children: string;
};

/**
* Announces a fail-closed status so the operator hears the next action.
*
* Uses `role="alert"` (WCAG 2.2 SC 4.1.3) without moving focus.
* Next action: read the sentence, then use the control it names
* (open a visible run, or request a lineage reconstruction).
*/
export function StatusAlert({ children }: StatusAlertProps) {
return (
<p className="status-alert" role="alert">
{children}
</p>
);
}
2 changes: 2 additions & 0 deletions frontend/src/styles/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
--color-accent-background: rgba(170, 59, 255, 0.1);
--color-accent-border: rgba(170, 59, 255, 0.5);
--color-chip-border: #3335;
--color-status-alert: #b91c1c;
--space-chip-inline: 0.6rem;
--space-chip-block: 0.1rem;
--space-chip-gap: 0.3rem;
Expand All @@ -34,5 +35,6 @@
--color-accent-background: rgba(192, 132, 252, 0.15);
--color-accent-border: rgba(192, 132, 252, 0.5);
--color-chip-border: #9ca3af;
--color-status-alert: #f87171;
}
}
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__ = "2.10.2"
__version__ = "2.10.3"
Loading