Skip to content
Closed
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
4 changes: 4 additions & 0 deletions CHANGELOG.d/0.88.0-analysis-run-cutoff-body-warning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# 0.88.0 Analysis-run cutoff body warning

Opening a title marked updated after cutoff now says the popup body is
live. The earlier text is not stored, so the popup does not invent it.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ 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.88.0] - 2026-08-17

### Added

- Opening an analysis-run title marked **Updated after cutoff** now
shows a popup status that the body is live, not a cutoff snapshot
(ADR 0016). After `make seed`, open the Demo Corp lineage run and
click Demo public post: the warning appears above the live body.
Demo private post and the home post list do not. The earlier text
is not stored, so the popup does not invent it. No TEPP theta is
invented.

## [0.87.0] - 2026-08-17

### Added
Expand Down
14 changes: 9 additions & 5 deletions docs/adr/0016-analysis-run-knowledge-cutoff-posts.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ 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. Detail compares the live `updated_at`
write clock with `knowledge_cutoff` and marks titles rewritten after
the run. The next action is specific: only those marked titles need a
cutoff comparison before treating the live body as reconstructed
evidence.
the run. Opening a marked title shows a popup status that the body is
live; the earlier text is not stored, so the popup does not invent it.
The next action is specific: only those marked titles need a cutoff
comparison before treating the live body as reconstructed evidence.

Reproducibility digests on the same detail use a labeled group whose
accessible name does not replace the visible prefixes (W3C Accessible
Expand All @@ -47,11 +48,14 @@ run.
and other in-cutoff Demo Corp titles. The later fixture account-review
post (2026-02-10) does not appear.
- Open the run: Demo public post is marked updated after cutoff
(`updated_at` 2026-01-13). Demo private post is not.
(`updated_at` 2026-01-13). Demo private post is not. Opening the
marked title shows a live-body status; the private title and the
home post list do not.
- 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 is a projection, not a stored cutoff body.
clock is a projection, not a stored cutoff body. The popup states
that honesty instead of inventing the earlier text.
- Thread-group *run list* visibility now uses the same cutoff
(ADR 0018). A later public post cannot surface a previously hidden
thread-group run.
Expand Down
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.87.0",
"version": "0.88.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@
opacity: 0.7;
}

.popup-live-body-warning {
margin: 0.75rem 0 1rem;
padding: 0.65rem 0.75rem;
border-left: 3px solid #b45309;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

New amber hex. --lw-font-size-meta already exists for 0.85rem. Add --lw-color-warning beside the existing --lw-* tokens and use it here. Do not install Storybook on this slice.

background: color-mix(in srgb, canvas 88%, #b45309 12%);
font-size: 0.85rem;
}

.popup-section {
margin-top: 1.5rem;
padding-top: 1rem;
Expand Down
36 changes: 36 additions & 0 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1754,6 +1754,42 @@ describe("App, authenticated", () => {
expect(teppHistory).not.toHaveTextContent("Succeeded");
});

it("warns that a cutoff-rewritten title opens the live body, not a snapshot", async () => {
stubBackend();
render(<App />);

await userEvent.click(
await screen.findByRole("button", {
name: "Open analysis run: Lineage reconstruction · Succeeded · Demo Corp",
}),
);
await userEvent.click(
await screen.findByRole("button", {
name: "Open live post (updated after cutoff): Public post",
}),
);
await waitFor(() => expect(screen.getByText("The full body text.")).toBeInTheDocument());
expect(screen.getByRole("status")).toHaveTextContent(
"This is the live body, not the version known at the 2026-01-12 analysis-run cutoff. The earlier text is not stored, so this popup does not invent it.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This locks the honesty sentence. After the copy change, assert the compare-with-this-run action. Also walk Open post: Linked post after a marked open and assert the status is gone — selectPost without options already clears the flag, but no test covers that path.

);

await userEvent.click(screen.getByRole("button", { name: "Close" }));
await userEvent.click(
screen.getByRole("button", {
name: "Open live post: Private post",
}),
);
await waitFor(() =>
expect(screen.getByText("The evidence panel should show exactly this text.")).toBeInTheDocument(),
);
expect(screen.queryByRole("status")).not.toBeInTheDocument();

await userEvent.click(screen.getByRole("button", { name: "Close" }));
await userEvent.click(screen.getByRole("button", { name: "View post: Public post" }));
await waitFor(() => expect(screen.getByText("The full body text.")).toBeInTheDocument());
expect(screen.queryByRole("status")).not.toBeInTheDocument();
});

it("does not tell a failed lineage run to connect the measurement service", async () => {
stubBackend({ failedLineageRun: true });
render(<App />);
Expand Down
74 changes: 62 additions & 12 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1163,13 +1163,15 @@ function PostDetailPopup({
accessToken,
canExtract,
graph,
liveBodyWarning,
onClose,
onSelectPost,
}: {
postId: string;
accessToken: string;
canExtract: boolean;
graph: LineageGraph | null;
liveBodyWarning?: string | null;
onClose: () => void;
onSelectPost?: (postId: string) => void;
}) {
Expand Down Expand Up @@ -1246,6 +1248,11 @@ function PostDetailPopup({
{post.visibility_label ?? post.visibility_code} &middot;{" "}
{new Date(post.created_at).toLocaleString()}
</p>
{liveBodyWarning ? (
<p className="popup-live-body-warning" role="status">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is the only status today, so uniqueness holds. AccName is name-from-content only, and the new test queries the unnamed role. Add a stable name such as aria-label="Live body warning" and query that name so a second live region cannot steal the assertion.

{liveBodyWarning}
</p>
) : null}
<PostBody body={post.post_body} />

<section className="popup-section">
Expand Down Expand Up @@ -1545,12 +1552,18 @@ function analysisRunDigestPrefix(digest: string): string {
return digest.slice(0, ANALYSIS_RUN_DIGEST_PREFIX_LENGTH);
}

type SelectPostOptions = {
liveAfterCutoff?: boolean;
knowledgeCutoff?: string;
};

/**
* Next action when a cutoff title opens the live post (ADR 0016).
*
* Post-body versioning is a later slice. Titles marked
* `live_after_cutoff` were rewritten after this run; others still
* match the write clock the run knew.
* Titles marked `live_after_cutoff` were rewritten after this run;
* others still match the write clock the run knew. The popup then
* states that the body is live. Cutoff body versioning stays later
* work -- we never invent the earlier text.
*/
function analysisRunLivePostWarning(cutoffIso: string): string {
const cutoffDate = cutoffIso.slice(0, 10);
Expand All @@ -1561,6 +1574,21 @@ function analysisRunLivePostWarning(cutoffIso: string): string {
);
}

/**
* Popup honesty when a marked cutoff title opens the live body.
*
* ADR 0016 does not store a historical snapshot. This copy must not
* invent the earlier text.
*/
function analysisRunOpenedBodyWarning(cutoffIso?: string | null): string {
const cutoffDate = cutoffIso?.slice(0, 10);
const when = cutoffDate ? `the ${cutoffDate} ` : "";
return (
`This is the live body, not the version known at ${when}analysis-run cutoff. ` +
"The earlier text is not stored, so this popup does not invent it."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This sentence is developer honesty, not the next action. The run-list helper already tells the operator to compare marked bodies with this run. The popup should continue that action, for example: “This is the live body, not a cutoff snapshot. Compare it with this 2026-01-12 run before you treat it as reconstructed evidence.” Do not invent the earlier text.

);
}

function analysisRunLivePostButtonLabel(post: {
post_title: string;
live_after_cutoff?: boolean;
Expand Down Expand Up @@ -1606,7 +1634,7 @@ function AnalysisRunsPanel({
onSelectPost,
}: {
accessToken: string;
onSelectPost: (postId: string) => void;
onSelectPost: (postId: string, options?: SelectPostOptions) => void;
}) {
const [runs, setRuns] = useState<AnalysisRun[] | null>(null);
const [selected, setSelected] = useState<AnalysisRun | null>(null);
Expand Down Expand Up @@ -1742,7 +1770,12 @@ function AnalysisRunsPanel({
<button
className="keyman-select"
aria-label={analysisRunLivePostButtonLabel(post)}
onClick={() => onSelectPost(post.post_id)}
onClick={() =>
onSelectPost(post.post_id, {
liveAfterCutoff: Boolean(post.live_after_cutoff),
knowledgeCutoff: selected.knowledge_cutoff,
})
}
>
{post.post_title}
</button>
Expand Down Expand Up @@ -2018,10 +2051,24 @@ function PostList({ accessToken }: { accessToken: string }) {
const [graph, setGraph] = useState<LineageGraph | null>(null);
const [error, setError] = useState<string | null>(null);
const [selectedPostId, setSelectedPostId] = useState<string | null>(null);
const [openedAfterCutoff, setOpenedAfterCutoff] = useState(false);
const [openedCutoffIso, setOpenedCutoffIso] = useState<string | null>(null);
const [canRebuild, setCanRebuild] = useState(false);
const [rebuilding, setRebuilding] = useState(false);
const [rebuildError, setRebuildError] = useState<string | null>(null);

function selectPost(postId: string, options?: SelectPostOptions) {
setSelectedPostId(postId);
setOpenedAfterCutoff(Boolean(options?.liveAfterCutoff));
setOpenedCutoffIso(options?.knowledgeCutoff ?? null);
}

function closeSelectedPost() {
setSelectedPostId(null);
setOpenedAfterCutoff(false);
setOpenedCutoffIso(null);
}

useEffect(() => {
fetchPosts(accessToken).then(setPosts).catch((err) => setError(String(err)));
fetchLineageGraph(accessToken).then(setGraph).catch(() => setGraph({ nodes: [], edges: [] }));
Expand Down Expand Up @@ -2049,9 +2096,9 @@ function PostList({ accessToken }: { accessToken: string }) {

return (
<>
<CalendarPanel accessToken={accessToken} onSelectPost={setSelectedPostId} />
<AnalysisRunsPanel accessToken={accessToken} onSelectPost={setSelectedPostId} />
<ReportsPanel accessToken={accessToken} canRebuild={canRebuild} onSelectPost={setSelectedPostId} />
<CalendarPanel accessToken={accessToken} onSelectPost={selectPost} />
<AnalysisRunsPanel accessToken={accessToken} onSelectPost={selectPost} />
<ReportsPanel accessToken={accessToken} canRebuild={canRebuild} onSelectPost={selectPost} />
<section className="popup-section lineage-home">
<div className="lineage-home-header">
<h2>Event Lineage</h2>
Expand All @@ -2063,15 +2110,15 @@ function PostList({ accessToken }: { accessToken: string }) {
</div>
{rebuildError && <p className="error">{rebuildError}</p>}
{!graph && <p>Loading lineage graph...</p>}
{graph && <LineageDag graph={graph} onSelectPost={setSelectedPostId} />}
{graph && <LineageDag graph={graph} onSelectPost={selectPost} />}
</section>
<ul className="post-list">
{posts.map((post) => (
<li key={post.post_id}>
<button
className="post-list-item"
aria-label={`View post: ${post.post_title}`}
onClick={() => setSelectedPostId(post.post_id)}
onClick={() => selectPost(post.post_id)}
>
<span className="post-title">{post.post_title}</span>
<span className="post-badge">{post.voc_type_label ?? post.voc_type_code}</span>
Expand All @@ -2086,8 +2133,11 @@ function PostList({ accessToken }: { accessToken: string }) {
accessToken={accessToken}
canExtract={canRebuild}
graph={graph}
onClose={() => setSelectedPostId(null)}
onSelectPost={setSelectedPostId}
liveBodyWarning={
openedAfterCutoff ? analysisRunOpenedBodyWarning(openedCutoffIso) : null
}
onClose={closeSelectedPost}
onSelectPost={selectPost}
/>
)}
</>
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.87.0"
__version__ = "0.88.0"
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.87.0"
version = "0.88.0"
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.

Loading