From ccd2d05062e47dce7f69d17bd419e0ee00350997 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:19:20 +0000 Subject: [PATCH 1/6] fix: stamp analysis-run status from one PostgreSQL clock (v2.12.19) Start a Pending lineage or TEPP run no longer fails analysis_run_status_time_check when the process clock is 15-20ms ahead of PostgreSQL. Occurrence and recording share one clock_timestamp(); the transition trigger raises recorded_at when a caller occurrence is already ahead. Never invent a theta. --- AGENTS.md | 4 +- .../2.12.19-analysis-run-status-same-clock.md | 9 ++ CHANGELOG.md | 21 +++- CLAUDE.md | 3 +- backend/app/analysis_run_ingestion.py | 6 +- backend/app/analysis_run_start.py | 17 ++-- docker/postgres-init/migrate.sh | 2 +- .../0013-normalized-analysis-run-registry.md | 4 +- .../0167-analysis-run-status-same-clock.md | 67 +++++++++++++ .../ANALYSIS_RUN_REGISTRY_REFERENCES.md | 4 +- frontend/package.json | 2 +- migrations/0018_analysis_run_registry.sql | 14 ++- .../0104_analysis_run_status_same_clock.sql | 99 +++++++++++++++++++ pyproject.toml | 2 +- tests/test_analysis_run_registry_schema.py | 44 +++++++++ tests/test_analysis_run_start.py | 24 ++++- uv.lock | 2 +- 17 files changed, 300 insertions(+), 24 deletions(-) create mode 100644 CHANGELOG.d/2.12.19-analysis-run-status-same-clock.md create mode 100644 docs/adr/0167-analysis-run-status-same-clock.md create mode 100644 migrations/0104_analysis_run_status_same_clock.sql diff --git a/AGENTS.md b/AGENTS.md index 1728f9e61..08b5ffd90 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -212,7 +212,9 @@ expose purge on a public HTTP route. v2.7.1). TEPP and period-report kinds 422 before any snapshot write. `POST /api/analysis-runs/{id}/start` reconstructs a Pending lineage cutoff bag through `reconstruct()` / `lineage_edge_specs` (ADR 0021 / -v0.88.0). Do not invent a theta. +v0.88.0). Status events stamp `occurred_at` and `recorded_at` from +one PostgreSQL `clock_timestamp()` (ADR 0167 / v2.12.19). Do not +bind Python `datetime.now` as occurrence. Do not invent a theta. Opening a cutoff-rewritten title shows **Body this run knew** from `source_post_revision` beside the live rewrite (ADR 0025 / v2.1.0). Do not invent the earlier sentence when no revision covers the cutoff. diff --git a/CHANGELOG.d/2.12.19-analysis-run-status-same-clock.md b/CHANGELOG.d/2.12.19-analysis-run-status-same-clock.md new file mode 100644 index 000000000..e0d9e4de5 --- /dev/null +++ b/CHANGELOG.d/2.12.19-analysis-run-status-same-clock.md @@ -0,0 +1,9 @@ +## 2.12.19 — Analysis-run status same PostgreSQL clock + +- Status events now stamp `occurred_at` and `recorded_at` from one + PostgreSQL `clock_timestamp()` (ADR 0167). The transition trigger + still records database time and raises `recorded_at` when a caller + clock is already ahead, so `analysis_run_status_time_check` no + longer rejects Start on a 15–20 ms Python-vs-PostgreSQL skew. + After `make seed`, Open the Demo Corp lineage run and Start still + recovers the designed A-100 fork. Never invent a theta. diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ed1a099..0c38f8c8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,19 @@ All notable changes to this project are documented here. Format follows environment, so local OIDC and synthetic-data workflows resolve the same pinned dependencies as CI. +## [2.12.19] - 2026-08-24 + +### Fixed + +- Starting a Pending lineage reconstruction or TEPP measurement no + longer fails on `analysis_run_status_time_check` when the process + clock is 15–20 ms ahead of PostgreSQL (ADR 0167). Status events + stamp `occurred_at` and `recorded_at` from one `clock_timestamp()`, + and the transition trigger raises `recorded_at` if a caller + occurrence is already ahead. After `make seed`, Open the Demo Corp + lineage run and Start still recovers the designed A-100 fork. + Never invent a theta. + ## [2.12.6] - 2026-08-20 ### Added @@ -68,10 +81,10 @@ All notable changes to this project are documented here. Format follows concurrency/load artifact) and entirely pre-existing (verified via `git diff` that no file in this change touches `analysis_run_start.py` or the 0018 migration that defines this - constraint). Root cause not yet conclusively identified; deferred - as out of scope for this migration-catchup change (a different - feature area -- analysis-run/TEPP lifecycle, not R&R/summary/ - verification) rather than rushed. 553 other tests unaffected. + constraint). Root cause identified in ADR 0167 / v2.12.19: the + supplied occurrence can sit ahead of the trigger write clock, so + `recorded_at` must share that clock or be raised to `occurred_at`. + Fixed in [2.12.19](#21219---2026-08-24). 553 other tests unaffected. - `get_or_create_corporate_entity`'s post-lock duplicate-create re-check fuzzy-matched against every cataloged entity, not just an exact diff --git a/CLAUDE.md b/CLAUDE.md index 1bcf50763..da3e22579 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,8 @@ are 422. The Request button waits until affiliated corps load; choose a corp if the token walks more than one. `POST /api/analysis-runs/{id}/start` commits Running plus a durable outbox row, then reconstructs that frozen cutoff bag (ADR 0021 / ADR 0023) or submits TEPP through -`tepp_client` (ADR 0022). A missing transport or unused accepted +`tepp_client` (ADR 0022). Status events share one PostgreSQL +`clock_timestamp()` for `occurred_at` and `recorded_at` (ADR 0167). A missing transport or unused accepted envelope is Failed. Failed TEPP is terminal — connect a TEPP transport from that Failed row. Create does not invent a Pending TEPP row. Do not invent a theta. Hover the Result prefix to read diff --git a/backend/app/analysis_run_ingestion.py b/backend/app/analysis_run_ingestion.py index f7da2969b..71f700bd6 100644 --- a/backend/app/analysis_run_ingestion.py +++ b/backend/app/analysis_run_ingestion.py @@ -947,8 +947,10 @@ async def create_pending_analysis_run( await conn.execute( """ insert into analysis_run_status_event - (analysis_run_id, status_ordinal, status_code, occurred_at) - values ($1, 1, 'analysis_status_pending', clock_timestamp()) + (analysis_run_id, status_ordinal, status_code, + occurred_at, recorded_at) + select $1, 1, 'analysis_status_pending', write_clock, write_clock + from (select clock_timestamp() as write_clock) same_clock """, run_id, ) diff --git a/backend/app/analysis_run_start.py b/backend/app/analysis_run_start.py index 2387d940b..1f7b88b30 100644 --- a/backend/app/analysis_run_start.py +++ b/backend/app/analysis_run_start.py @@ -284,15 +284,21 @@ async def _append_status( analysis_run_id: str, status_ordinal: int, status_code: str, - occurred_at: datetime, failure_code: str | None = None, ) -> None: - """Append one legal lifecycle event. Failed rows carry a machine code.""" + """Append one legal lifecycle event. Failed rows carry a machine code. + + Occurrence and recording share one PostgreSQL ``clock_timestamp()`` + so ``analysis_run_status_time_check`` cannot see a Python clock that + is ahead of the trigger write clock (ADR 0167). + """ await conn.execute( """ insert into analysis_run_status_event - (analysis_run_id, status_ordinal, status_code, occurred_at, failure_code) - values ($1, $2, $3, clock_timestamp(), $4) + (analysis_run_id, status_ordinal, status_code, + occurred_at, recorded_at, failure_code) + select $1, $2, $3, write_clock, write_clock, $4 + from (select clock_timestamp() as write_clock) same_clock """, analysis_run_id, status_ordinal, @@ -528,7 +534,6 @@ async def enqueue_pending_analysis_run( analysis_run_id, await _next_status_ordinal(conn, analysis_run_id), _RUNNING, - now, ) await conn.execute( """ @@ -747,7 +752,6 @@ async def _deliver_lineage_reconstruction( analysis_run_id, await _next_status_ordinal(conn, analysis_run_id), _SUCCEEDED, - finished, ) @@ -783,6 +787,5 @@ async def _deliver_tepp_measurement( analysis_run_id, await _next_status_ordinal(conn, analysis_run_id), status_code, - finished, failure_code, ) diff --git a/docker/postgres-init/migrate.sh b/docker/postgres-init/migrate.sh index f329117d6..d85f14358 100644 --- a/docker/postgres-init/migrate.sh +++ b/docker/postgres-init/migrate.sh @@ -18,7 +18,7 @@ for migration in /opt/lineageweave/migrations/*.sql; do migration_name=${migration##*/} case "$migration_name" in 0012_*|0013_*|0014_*|0015_*|0016_*|0017_*|0018_*|0019_*|0020_*|0021_*|0022_*|0023_*|0024_*|0025_*|0026_*|0027_*|0028_*|0029_*|0030_*|0031_*|0032_*|0033_*|0034_*|0035_*|0036_*|0037_*|0038_*|0039_*|0040_*|0041_*|0042_*|0043_*|0044_*|0045_*|0046_*|0047_*|0048_*|0049_*|0050_*) ;; - 0060_*|0100_*|0101_*|0102_*) ;; + 0060_*|0100_*|0101_*|0102_*|0104_*) ;; *) continue ;; esac printf 'Applying %s\n' "$migration_name" diff --git a/docs/adr/0013-normalized-analysis-run-registry.md b/docs/adr/0013-normalized-analysis-run-registry.md index 15fd040d6..737d0db6a 100644 --- a/docs/adr/0013-normalized-analysis-run-registry.md +++ b/docs/adr/0013-normalized-analysis-run-registry.md @@ -136,7 +136,9 @@ succeeded | failed | cancelled -> terminal The first event must be `pending`, requires an immutable scope, and cannot predate the run request. Failed events require a lowercase machine-code identifier; raw exception text is prohibited. `recorded_at` is overwritten with database system -time on every insert and cannot precede `occurred_at`. +time on every insert and cannot precede `occurred_at`. When a caller supplies +an occurrence already ahead of that write clock, `recorded_at` is raised to +`occurred_at` so the check holds without rewriting occurrence (ADR 0167). `analysis_run_current_status` is a view, not a second mutable state authority. ### Authorization scope diff --git a/docs/adr/0167-analysis-run-status-same-clock.md b/docs/adr/0167-analysis-run-status-same-clock.md new file mode 100644 index 000000000..f1d482508 --- /dev/null +++ b/docs/adr/0167-analysis-run-status-same-clock.md @@ -0,0 +1,67 @@ +# ADR 0167 — Analysis-run status events share one PostgreSQL write clock + +**Decision status:** Accepted +**Date:** 2026-08-24 + +Amends [ADR 0013](0013-normalized-analysis-run-registry.md). Independent of +leftover-map persist, leftover UI, leftover two-axis distance, TEPP +arithmetic, and Valkey outbox payload shape. + +## Context + +ADR 0013 distinguishes lifecycle `occurred_at` from durable +`recorded_at` and requires `occurred_at <= recorded_at`. The 0018 +trigger overwrote `recorded_at` with `clock_timestamp()` after a +`FOR UPDATE`. Callers that bound Python `datetime.now(timezone.utc)` +as `occurred_at` reproducibly landed 15–20 ms *after* that write +clock, so `analysis_run_status_time_check` rejected the row +(`CheckViolationError`). Live +`test_start_analysis_run_recovers_the_a100_fork` and +`test_tepp_start_persists_published_accepted_evidence` then could not +Start a Pending lineage or TEPP run against a real PostgreSQL. + +The product start path later switched `occurred_at` to +`clock_timestamp()` while still omitting `recorded_at` (DEFAULT plus +trigger). That still leaves a two-clock window: VALUES vs DEFAULT vs +trigger each call `clock_timestamp()` separately, and any remaining +Python-ahead caller (seed, live test helper, or a future bind) fails +the same check. + +## Decision + +1. Application inserts of `analysis_run_status_event` stamp + `occurred_at` and `recorded_at` from **one** PostgreSQL + `clock_timestamp()` (a `SELECT ... FROM (SELECT clock_timestamp() + AS write_clock)` row). Do not bind Python `datetime.now` as + occurrence. +2. `enforce_analysis_run_status_transition` still overwrites + `recorded_at` with database `clock_timestamp()`. If the supplied + `occurred_at` is already ahead of that clock, raise `recorded_at` + to `occurred_at` so the check holds. Do not rewrite occurrence: + monotonicity and "cannot predate the request" stay on the + caller-supplied instant. +3. Migration 0104 replaces the trigger on databases that already + applied 0018. The 0018 function body matches so a fresh install + is the same contract. + +Do not invent a leftover score. Do not invent a theta. + +## Consequences + +Starting a Pending lineage reconstruction or TEPP measurement no +longer fails closed on a 15–20 ms Python-vs-PostgreSQL skew. After +`make seed`, Open the Demo Corp lineage run and Start still recovers +the designed A-100 fork. A synthetic insert whose `occurred_at` is +50 ms ahead of `clock_timestamp()` persists with +`recorded_at >= occurred_at`. + +## References + +Allen, J. F. (1983). Maintaining knowledge about temporal intervals. +*Communications of the ACM, 26*(11), 832–843. +https://doi.org/10.1145/182.358434 + +Lebo, T., Sahoo, S., McGuinness, D., Belhajjame, K., Cheney, J., +Corsar, D., Garijo, D., Soiland-Reyes, S., Zednik, S., & Zhao, J. +(2013). *PROV-O: The PROV ontology* (W3C Recommendation). World Wide +Web Consortium. https://www.w3.org/TR/prov-o/ diff --git a/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md b/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md index 99dfff47b..5d554e803 100644 --- a/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md +++ b/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md @@ -29,7 +29,9 @@ general-purpose bitemporal database: - `knowledge_cutoff` answers what a specific analysis was allowed to know; - `requested_at` answers when that analysis was requested; - `occurred_at` and `recorded_at` distinguish lifecycle occurrence from durable - database recording. + database recording. Product inserts stamp both from one PostgreSQL + `clock_timestamp()`; the transition trigger raises `recorded_at` when a + caller occurrence is already ahead (ADR 0167). The database requires the aggregate leakage boundary: diff --git a/frontend/package.json b/frontend/package.json index e2e996bbe..78ce628e2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.12.6", + "version": "2.12.19", "type": "module", "scripts": { "dev": "vite", diff --git a/migrations/0018_analysis_run_registry.sql b/migrations/0018_analysis_run_registry.sql index b08d80b3b..ece5d02e1 100644 --- a/migrations/0018_analysis_run_registry.sql +++ b/migrations/0018_analysis_run_registry.sql @@ -471,6 +471,7 @@ declare previous_status_code text; previous_occurred_at timestamptz; run_requested_at timestamptz; + write_clock timestamptz; begin -- The immutable parent row is a per-run serialization lock. It prevents -- concurrent writers from both accepting the same next ordinal. @@ -492,7 +493,15 @@ begin if new.occurred_at < run_requested_at then raise exception 'analysis_run_status_before_request'; end if; - new.recorded_at := clock_timestamp(); + -- One database clock for the durable write. If the supplied occurrence + -- is already ahead of that clock (Python datetime.now skew), raise + -- recorded_at to occurred_at so analysis_run_status_time_check holds + -- without rewriting occurrence or breaking monotonicity. + write_clock := clock_timestamp(); + new.recorded_at := write_clock; + if new.recorded_at < new.occurred_at then + new.recorded_at := new.occurred_at; + end if; select status_ordinal, status_code, occurred_at into previous_ordinal, previous_status_code, previous_occurred_at @@ -541,7 +550,8 @@ $$; comment on function enforce_analysis_run_status_transition() is 'Serializes status appends and requires immutable scope, request-time ' - 'ordering, database-recorded time, legal transitions, and terminal finality.'; + 'ordering, database-recorded time that cannot precede occurrence, ' + 'legal transitions, and terminal finality.'; drop trigger if exists analysis_run_status_transition_guard on analysis_run_status_event; diff --git a/migrations/0104_analysis_run_status_same_clock.sql b/migrations/0104_analysis_run_status_same_clock.sql new file mode 100644 index 000000000..b10c435bc --- /dev/null +++ b/migrations/0104_analysis_run_status_same_clock.sql @@ -0,0 +1,99 @@ +-- Analysis-run status events share one PostgreSQL write clock (ADR 0167). +-- +-- Replaces enforce_analysis_run_status_transition so recorded_at cannot +-- precede occurred_at when a caller supplies a clock that is slightly +-- ahead of PostgreSQL (the live CheckViolationError on +-- analysis_run_status_time_check). Occurrence is not rewritten; the +-- durable write clock is raised to the supplied occurrence when needed. +-- Idempotent: create or replace. Does not invent a theta. + +create or replace function enforce_analysis_run_status_transition() +returns trigger +language plpgsql +as $$ +declare + previous_ordinal integer; + previous_status_code text; + previous_occurred_at timestamptz; + run_requested_at timestamptz; + write_clock timestamptz; +begin + -- The immutable parent row is a per-run serialization lock. It prevents + -- concurrent writers from both accepting the same next ordinal. + select requested_at + into run_requested_at + from analysis_run + where analysis_run_id = new.analysis_run_id + for update; + + if not found then + raise exception 'analysis_run_not_found'; + end if; + if not exists ( + select 1 from analysis_run_scope + where analysis_run_id = new.analysis_run_id + ) then + raise exception 'analysis_run_scope_required'; + end if; + if new.occurred_at < run_requested_at then + raise exception 'analysis_run_status_before_request'; + end if; + -- One database clock for the durable write. If the supplied occurrence + -- is already ahead of that clock (Python datetime.now skew), raise + -- recorded_at to occurred_at so analysis_run_status_time_check holds + -- without rewriting occurrence or breaking monotonicity. + write_clock := clock_timestamp(); + new.recorded_at := write_clock; + if new.recorded_at < new.occurred_at then + new.recorded_at := new.occurred_at; + end if; + + select status_ordinal, status_code, occurred_at + into previous_ordinal, previous_status_code, previous_occurred_at + from analysis_run_status_event + where analysis_run_id = new.analysis_run_id + order by status_ordinal desc + limit 1; + + if previous_ordinal is null then + if new.status_ordinal <> 1 + or new.status_code <> 'analysis_status_pending' then + raise exception 'analysis_run_first_status_must_be_pending'; + end if; + return new; + end if; + + if new.status_ordinal <> previous_ordinal + 1 then + raise exception 'analysis_run_status_ordinal_not_contiguous'; + end if; + if new.occurred_at < previous_occurred_at then + raise exception 'analysis_run_status_time_not_monotonic'; + end if; + + if previous_status_code = 'analysis_status_pending' then + if new.status_code not in ( + 'analysis_status_running', + 'analysis_status_cancelled' + ) then + raise exception 'analysis_run_status_transition_invalid'; + end if; + elsif previous_status_code = 'analysis_status_running' then + if new.status_code not in ( + 'analysis_status_succeeded', + 'analysis_status_failed', + 'analysis_status_cancelled' + ) then + raise exception 'analysis_run_status_transition_invalid'; + end if; + else + raise exception 'analysis_run_terminal_status_has_no_successor'; + end if; + + return new; +end +$$; + +comment on function enforce_analysis_run_status_transition() is + 'Serializes status appends and requires immutable scope, request-time ' + 'ordering, database-recorded time that cannot precede occurrence, ' + 'legal transitions, and terminal finality.'; diff --git a/pyproject.toml b/pyproject.toml index cb4be2916..8b01c9850 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.12.6" +version = "2.12.19" 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" } diff --git a/tests/test_analysis_run_registry_schema.py b/tests/test_analysis_run_registry_schema.py index 4161f2452..64f5c6b5b 100644 --- a/tests/test_analysis_run_registry_schema.py +++ b/tests/test_analysis_run_registry_schema.py @@ -755,6 +755,50 @@ def test_status_requires_scope_and_cannot_predate_request(registry_db) -> None: assert recorded_at.year < 2099 +def test_status_same_clock_migration_is_idempotent_and_allowlisted() -> None: + """Compose must replay the same-clock trigger; 0018 matches 0104.""" + migration = (_ROOT / "migrations" / "0104_analysis_run_status_same_clock.sql").read_text( + encoding="utf-8" + ) + registry = _REGISTRY_MIGRATION.read_text(encoding="utf-8") + migrate = (_ROOT / "docker/postgres-init/migrate.sh").read_text(encoding="utf-8") + assert "create or replace function enforce_analysis_run_status_transition" in migration + assert "if new.recorded_at < new.occurred_at then" in migration + assert "new.recorded_at := new.occurred_at" in migration + assert "if new.recorded_at < new.occurred_at then" in registry + assert "0104_*" in migrate + assert "Do not invent a theta" in migration or "invent a theta" in migration + + +def test_status_accepts_occurrence_ahead_of_the_write_clock(registry_db) -> None: + """A 50ms-ahead occurrence must persist; recorded_at cannot precede it.""" + with registry_db.cursor() as cursor: + snapshot_id = _insert_snapshot(cursor) + account_id = _insert_account(cursor) + run_id = _insert_run( + cursor, + snapshot_id=snapshot_id, + account_id=account_id, + idempotency_key="ahead-clock", + ) + cursor.execute( + "insert into analysis_run_scope " + "(analysis_run_id, scope_kind_code) " + "values (%s, 'analysis_scope_all_visible')", + (run_id,), + ) + cursor.execute( + "insert into analysis_run_status_event " + "(analysis_run_id, status_ordinal, status_code, occurred_at) " + "values (%s, 1, 'analysis_status_pending', " + "clock_timestamp() + interval '50 milliseconds') " + "returning occurred_at, recorded_at", + (run_id,), + ) + occurred_at, recorded_at = cursor.fetchone() + assert recorded_at >= occurred_at + + def test_machine_codes_and_canonical_idempotency_are_fail_closed(registry_db) -> None: """Audit identifiers are canonical and failure details stay machine-safe.""" diff --git a/tests/test_analysis_run_start.py b/tests/test_analysis_run_start.py index e46aa4a0c..ce60bec41 100644 --- a/tests/test_analysis_run_start.py +++ b/tests/test_analysis_run_start.py @@ -1,12 +1,17 @@ """Start-reconstruction contracts: digest, freeze, 422/409, designed tree.""" +import inspect from datetime import datetime, timezone import pytest -from backend.app.analysis_run_ingestion import reconstructed_edge_is_visible +from backend.app.analysis_run_ingestion import ( + create_pending_analysis_run, + reconstructed_edge_is_visible, +) from backend.app.analysis_run_start import ( AnalysisRunStartError, + _append_status, configured_tepp_client, reconstruction_member_ids, reconstruction_result_digest, @@ -171,3 +176,20 @@ def test_running_restart_conflicts_and_succeeded_replay_is_documented() -> None: ) assert running.status_code == 409 assert "Pending" in running.detail + + +def test_append_status_stamps_occurred_and_recorded_from_one_clock() -> None: + """Start must not bind a Python clock that can sit ahead of PostgreSQL.""" + source = inspect.getsource(_append_status) + assert "from (select clock_timestamp() as write_clock) same_clock" in source + assert "write_clock, write_clock" in source + assert "datetime.now" not in source + assert "occurred_at: datetime" not in source + + +def test_pending_create_stamps_occurred_and_recorded_from_one_clock() -> None: + """Create Pending uses the same one-clock insert as Start.""" + source = inspect.getsource(create_pending_analysis_run) + assert "from (select clock_timestamp() as write_clock) same_clock" in source + assert "analysis_status_pending" in source + assert "write_clock, write_clock" in source diff --git a/uv.lock b/uv.lock index 10bcf9ff1..095fa6696 100644 --- a/uv.lock +++ b/uv.lock @@ -454,7 +454,7 @@ wheels = [ [[package]] name = "lineageweave" -version = "2.12.6" +version = "2.12.19" source = { editable = "." } dependencies = [ { name = "certifi" }, From fdb8f5631eddb6785c4ec9c70f45666d551f88cf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 04:37:22 +0900 Subject: [PATCH 2/6] fix: allocate status clock migration and restore build --- docker/postgres-init/migrate.sh | 2 +- docs/adr/0167-analysis-run-status-same-clock.md | 2 +- frontend/src/App.test.tsx | 2 ++ frontend/src/App.tsx | 4 ++-- ...me_clock.sql => 0173_analysis_run_status_same_clock.sql} | 2 +- tests/test_analysis_run_registry_schema.py | 6 +++--- 6 files changed, 10 insertions(+), 8 deletions(-) rename migrations/{0104_analysis_run_status_same_clock.sql => 0173_analysis_run_status_same_clock.sql} (97%) diff --git a/docker/postgres-init/migrate.sh b/docker/postgres-init/migrate.sh index d85f14358..92b668bb2 100644 --- a/docker/postgres-init/migrate.sh +++ b/docker/postgres-init/migrate.sh @@ -18,7 +18,7 @@ for migration in /opt/lineageweave/migrations/*.sql; do migration_name=${migration##*/} case "$migration_name" in 0012_*|0013_*|0014_*|0015_*|0016_*|0017_*|0018_*|0019_*|0020_*|0021_*|0022_*|0023_*|0024_*|0025_*|0026_*|0027_*|0028_*|0029_*|0030_*|0031_*|0032_*|0033_*|0034_*|0035_*|0036_*|0037_*|0038_*|0039_*|0040_*|0041_*|0042_*|0043_*|0044_*|0045_*|0046_*|0047_*|0048_*|0049_*|0050_*) ;; - 0060_*|0100_*|0101_*|0102_*|0104_*) ;; + 0060_*|0100_*|0101_*|0102_*|0173_*) ;; *) continue ;; esac printf 'Applying %s\n' "$migration_name" diff --git a/docs/adr/0167-analysis-run-status-same-clock.md b/docs/adr/0167-analysis-run-status-same-clock.md index f1d482508..159c3198b 100644 --- a/docs/adr/0167-analysis-run-status-same-clock.md +++ b/docs/adr/0167-analysis-run-status-same-clock.md @@ -40,7 +40,7 @@ the same check. to `occurred_at` so the check holds. Do not rewrite occurrence: monotonicity and "cannot predate the request" stay on the caller-supplied instant. -3. Migration 0104 replaces the trigger on databases that already +3. Migration 0173 replaces the trigger on databases that already applied 0018. The 0018 function body matches so a fresh install is the same contract. diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 7462abd2c..ccbcce454 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -32,6 +32,7 @@ afterEach(() => { describe("App, unauthenticated", () => { it("shows a login button that starts the real OIDC redirect", async () => { + window.sessionStorage.clear(); render(); const button = screen.getByRole("button", { name: /log in/i }); await userEvent.click(button); @@ -41,6 +42,7 @@ describe("App, unauthenticated", () => { state: expect.objectContaining({ returnUrl: expect.stringMatching(/^\//) }), }), ); + expect(window.sessionStorage.getItem("lineageweave.oidc.returnUrl")).toBe("/"); }); }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6fba0dd41..1b5b351ab 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4610,7 +4610,8 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
- {destination === "admin" ? : null}