From 296fa3054d2d5db3b615b525fe5f0e2278558c6d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 01:30:33 +0900 Subject: [PATCH] feat: warn that cutoff-rewritten titles open the live body (v0.88.0) Opening a marked analysis-run title still shows the live post. The popup now says so instead of inventing a cutoff snapshot. --- ...0.88.0-analysis-run-cutoff-body-warning.md | 4 + CHANGELOG.md | 12 +++ ...016-analysis-run-knowledge-cutoff-posts.md | 14 ++-- frontend/package.json | 2 +- frontend/src/App.css | 8 ++ frontend/src/App.test.tsx | 36 +++++++++ frontend/src/App.tsx | 74 ++++++++++++++++--- lineageweave/__init__.py | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 10 files changed, 135 insertions(+), 21 deletions(-) create mode 100644 CHANGELOG.d/0.88.0-analysis-run-cutoff-body-warning.md diff --git a/CHANGELOG.d/0.88.0-analysis-run-cutoff-body-warning.md b/CHANGELOG.d/0.88.0-analysis-run-cutoff-body-warning.md new file mode 100644 index 000000000..0562ff696 --- /dev/null +++ b/CHANGELOG.d/0.88.0-analysis-run-cutoff-body-warning.md @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 333616792..037b11287 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/adr/0016-analysis-run-knowledge-cutoff-posts.md b/docs/adr/0016-analysis-run-knowledge-cutoff-posts.md index 274d34dbd..dc855a964 100644 --- a/docs/adr/0016-analysis-run-knowledge-cutoff-posts.md +++ b/docs/adr/0016-analysis-run-knowledge-cutoff-posts.md @@ -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 @@ -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. diff --git a/frontend/package.json b/frontend/package.json index c93670d28..a9cf709cf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.87.0", + "version": "0.88.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.css b/frontend/src/App.css index b3fab25d1..0f24508d1 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -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; + background: color-mix(in srgb, canvas 88%, #b45309 12%); + font-size: 0.85rem; +} + .popup-section { margin-top: 1.5rem; padding-top: 1rem; diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 92c3fa16f..42b99c766 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -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(); + + 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.", + ); + + 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(); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ac5ca7cf7..5ce509519 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1163,6 +1163,7 @@ function PostDetailPopup({ accessToken, canExtract, graph, + liveBodyWarning, onClose, onSelectPost, }: { @@ -1170,6 +1171,7 @@ function PostDetailPopup({ accessToken: string; canExtract: boolean; graph: LineageGraph | null; + liveBodyWarning?: string | null; onClose: () => void; onSelectPost?: (postId: string) => void; }) { @@ -1246,6 +1248,11 @@ function PostDetailPopup({ {post.visibility_label ?? post.visibility_code} ·{" "} {new Date(post.created_at).toLocaleString()}

+ {liveBodyWarning ? ( +

+ {liveBodyWarning} +

+ ) : null}
@@ -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); @@ -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." + ); +} + function analysisRunLivePostButtonLabel(post: { post_title: string; live_after_cutoff?: boolean; @@ -1606,7 +1634,7 @@ function AnalysisRunsPanel({ onSelectPost, }: { accessToken: string; - onSelectPost: (postId: string) => void; + onSelectPost: (postId: string, options?: SelectPostOptions) => void; }) { const [runs, setRuns] = useState(null); const [selected, setSelected] = useState(null); @@ -1742,7 +1770,12 @@ function AnalysisRunsPanel({ @@ -2018,10 +2051,24 @@ function PostList({ accessToken }: { accessToken: string }) { const [graph, setGraph] = useState(null); const [error, setError] = useState(null); const [selectedPostId, setSelectedPostId] = useState(null); + const [openedAfterCutoff, setOpenedAfterCutoff] = useState(false); + const [openedCutoffIso, setOpenedCutoffIso] = useState(null); const [canRebuild, setCanRebuild] = useState(false); const [rebuilding, setRebuilding] = useState(false); const [rebuildError, setRebuildError] = useState(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: [] })); @@ -2049,9 +2096,9 @@ function PostList({ accessToken }: { accessToken: string }) { return ( <> - - - + + +

Event Lineage

@@ -2063,7 +2110,7 @@ function PostList({ accessToken }: { accessToken: string }) {
{rebuildError &&

{rebuildError}

} {!graph &&

Loading lineage graph...

} - {graph && } + {graph && }
    {posts.map((post) => ( @@ -2071,7 +2118,7 @@ function PostList({ accessToken }: { accessToken: string }) {