-
Notifications
You must be signed in to change notification settings - Fork 1
feat: compare live post write clock with analysis-run cutoff (v0.87.0) #150
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # 0.87.0 Analysis-run live write clock | ||
|
|
||
| In-cutoff titles now say whether the live row was rewritten after the | ||
| run. Open Demo public post as the edited counter-example; Demo private | ||
| post still matches the January cutoff. Bodies stay live. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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.87.0] - 2026-08-17 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Current #74 tip |
||
|
|
||
| ### Added | ||
|
|
||
| - Analysis-run detail now compares each in-cutoff title's live | ||
| `updated_at` with that run's knowledge cutoff. After `make seed`, | ||
| open the Demo Corp lineage run: Demo public post is marked | ||
| **Updated after cutoff**; Demo private post is not. Opening a | ||
| marked title still shows the live body -- cutoff body versioning | ||
| stays a later slice (ADR 0016). The list stays aggregates-only. | ||
| No TEPP theta is invented. | ||
|
|
||
| ## [0.86.2] - 2026-08-16 | ||
|
|
||
| ### Fixed | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,8 @@ transport. A failed lineage row retries reconstruction -- it does not | |
| mention TEPP. A failed period-report row rebuilds the report. A | ||
| pending TEPP row does not claim a calibrated measurement. | ||
| 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). | ||
| Opening a cutoff title shows the live post. Titles marked updated | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #148 already landed a pending-lineage sentence on #74 Next action: rebase onto
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #74 |
||
| after cutoff were rewritten after the run; compare those bodies | ||
| before treating them as reconstructed evidence (ADR 0016). | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep this write-clock sentence, but rebase onto live #74 |
||
| `POST /api/analysis-runs` records Pending on an authorized | ||
| cutoff capture (ADR 0017) and does not reconstruct lineage. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -93,6 +93,22 @@ def _iso(value: Any) -> str: | |
| return value.isoformat() if hasattr(value, "isoformat") else str(value) | ||
|
|
||
|
|
||
| def _as_utc(value: datetime) -> datetime: | ||
| """Treat a naive clock as UTC so cutoff comparison stays timezone-aware.""" | ||
| if value.tzinfo is None: | ||
| return value.replace(tzinfo=timezone.utc) | ||
| return value.astimezone(timezone.utc) | ||
|
|
||
|
|
||
| def live_write_after_cutoff(updated_at: datetime, knowledge_cutoff: datetime) -> bool: | ||
| """True when the live row was rewritten after the run's analysis clock. | ||
|
|
||
| ``created_at <= knowledge_cutoff`` admits the title. ``updated_at`` is | ||
| the live write clock (ADR 0016). Equal times stay in-cutoff evidence. | ||
| """ | ||
| return _as_utc(updated_at) > _as_utc(knowledge_cutoff) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Admission ( Next action after rebase onto
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
|
|
||
| async def _counts_by_run( | ||
| conn: asyncpg.Connection, | ||
| run_ids: list[str], | ||
|
|
@@ -258,15 +274,21 @@ async def fetch_visible_scope_posts( | |
| scope_key: str | None, | ||
| affiliated_entity_ids: list[str], | ||
| knowledge_cutoff: Any, | ||
| ) -> list[dict[str, str]]: | ||
| ) -> list[dict[str, Any]]: | ||
| """ABAC-visible post titles known at the run cutoff -- never a hidden body. | ||
|
|
||
| ``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 compared separately so the operator can see which | ||
| in-cutoff titles were rewritten after that clock. The live body is | ||
| still not returned. | ||
| """ | ||
| columns = ( | ||
| "post_id, post_title, visibility_code, corporate_entity_id, updated_at" | ||
| ) | ||
| 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 " | ||
| f"select {columns} " | ||
| "from source_post where corporate_entity_id = $1 " | ||
| "and created_at <= $2 " | ||
| "order by created_at, post_title", | ||
|
|
@@ -275,7 +297,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 " | ||
| f"select {columns} " | ||
| "from source_post where process_unit_id = $1 " | ||
| "and created_at <= $2 " | ||
| "order by created_at, post_title", | ||
|
|
@@ -284,7 +306,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 " | ||
| f"select {columns} " | ||
| "from source_post where thread_group_key = $1 " | ||
| "and created_at <= $2 " | ||
| "order by created_at, post_title", | ||
|
|
@@ -293,20 +315,30 @@ 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 " | ||
| f"select {columns} " | ||
| "from source_post where created_at <= $1 " | ||
| "order by created_at, post_title", | ||
| knowledge_cutoff, | ||
| ) | ||
| else: | ||
| return [] | ||
| affiliated = {str(entity_id) for entity_id in affiliated_entity_ids} | ||
| posts: list[dict[str, str]] = [] | ||
| posts: list[dict[str, Any]] = [] | ||
| for row in rows: | ||
| 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"]}) | ||
| updated_at = row["updated_at"] | ||
| posts.append( | ||
| { | ||
| "post_id": str(row["post_id"]), | ||
| "post_title": row["post_title"], | ||
| "updated_at": _iso(updated_at), | ||
| "live_after_cutoff": live_write_after_cutoff( | ||
| updated_at, knowledge_cutoff | ||
| ), | ||
| } | ||
| ) | ||
| return posts | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -303,11 +303,21 @@ def _insert_post( | |
| visibility_code: str, | ||
| body: str = "body", | ||
| created_at: str = "2026-01-10T12:00:00Z", | ||
| updated_at: str | None = None, | ||
| ) -> str: | ||
| written_at = updated_at if updated_at is not None else created_at | ||
| cur.execute( | ||
| "insert into source_post (author_account_id, corporate_entity_id, post_title, post_body, voc_type_code, visibility_code, created_at) " | ||
| "values (%s, %s, %s, %s, 'voc', %s, %s) returning post_id", | ||
| (account_id, corporate_entity_id, title, body, visibility_code, created_at), | ||
| "insert into source_post (author_account_id, corporate_entity_id, post_title, post_body, voc_type_code, visibility_code, created_at, updated_at) " | ||
| "values (%s, %s, %s, %s, 'voc', %s, %s, %s) returning post_id", | ||
| ( | ||
| account_id, | ||
| corporate_entity_id, | ||
| title, | ||
| body, | ||
| visibility_code, | ||
| created_at, | ||
| written_at, | ||
| ), | ||
| ) | ||
| return str(cur.fetchone()[0]) | ||
|
|
||
|
|
@@ -327,6 +337,14 @@ def _insert_post( | |
| "A follow-up written after the January 2026 run cutoff.", | ||
| created_at="2026-01-20T12:00:00Z", | ||
| ) | ||
| _insert_post( | ||
| "Edited own-corp private post", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| own_corp_id, | ||
| "private", | ||
| "A January post rewritten after the run cutoff.", | ||
| created_at="2026-01-10T12:00:00Z", | ||
| updated_at="2026-01-13T09:00:00Z", | ||
| ) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This own-corp private insert is visible on
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This edited own-corp row is the buyer proof for |
||
|
|
||
| cur.execute( | ||
| "insert into cataloged_person (person_name, person_side_code) values " | ||
|
|
@@ -492,8 +510,14 @@ def test_analysis_runs_are_labeled_aggregates_and_hide_other_scopes( | |
| assert all("failure_code" not in event for event in history) | ||
| titles = {post["post_title"] for post in body["visible_posts"]} | ||
| assert "Own-corp private post" in titles | ||
| assert "Edited own-corp private post" in titles | ||
| assert "Late own-corp private post" not in titles | ||
| assert "Other-corp private post" not in titles | ||
| posts_by_title = {post["post_title"]: post for post in body["visible_posts"]} | ||
| assert posts_by_title["Own-corp private post"]["live_after_cutoff"] is False | ||
| assert posts_by_title["Edited own-corp private post"]["live_after_cutoff"] is True | ||
| assert posts_by_title["Edited own-corp private post"]["updated_at"].startswith("2026-01-13") | ||
| assert "post_body" not in posts_by_title["Edited own-corp private post"] | ||
| assert "postgresql://" not in str(body) | ||
| assert "visible_posts" not in visible | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1548,20 +1548,27 @@ function analysisRunDigestPrefix(digest: string): string { | |
| /** | ||
| * 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. | ||
| * 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. | ||
| */ | ||
| 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." | ||
| `Opening a title shows the live post. Titles marked updated after cutoff ` + | ||
| `were rewritten after ${cutoffDate}. Compare those bodies with this run ` + | ||
| "before you treat them as reconstructed evidence." | ||
| ); | ||
| } | ||
|
|
||
| function analysisRunLivePostButtonLabel(postTitle: string): string { | ||
| return `Open live post (may have changed after cutoff): ${postTitle}`; | ||
| function analysisRunLivePostButtonLabel(post: { | ||
| post_title: string; | ||
| live_after_cutoff?: boolean; | ||
| }): string { | ||
| if (post.live_after_cutoff) { | ||
| return `Open live post (updated after cutoff): ${post.post_title}`; | ||
| } | ||
| return `Open live post: ${post.post_title}`; | ||
| } | ||
|
|
||
| function AnalysisRunReproducibilityDigests({ | ||
|
|
@@ -1734,11 +1741,14 @@ function AnalysisRunsPanel({ | |
| <li key={post.post_id}> | ||
| <button | ||
| className="keyman-select" | ||
| aria-label={analysisRunLivePostButtonLabel(post.post_title)} | ||
| aria-label={analysisRunLivePostButtonLabel(post)} | ||
| onClick={() => onSelectPost(post.post_id)} | ||
| > | ||
| {post.post_title} | ||
| </button> | ||
| {post.live_after_cutoff && ( | ||
| <span className="post-badge">Updated after cutoff</span> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The list mark disappears as soon as the operator opens the title. Pass
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The list mark disappears as soon as the operator opens the title. Pass |
||
| )} | ||
| </li> | ||
| ))} | ||
| </ul> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Current #74 tip
69c035balready published## [0.87.0]for granted retention purge and Storybook tokens. Rebase onto that tip and move this write-clock entry to 0.87.1 so both buyer slices stay in the changelog. Merging this0.87.0block as-is will conflict and collapse two releases into one version.