fix: rebuild trustworthy local-day evidence (#99)

This commit is contained in:
xavierk
2026-09-28 13:09:00 +05:30
parent 7e392c4ea2
commit 017a562566
15 changed files with 1305 additions and 151 deletions
+26 -26
View File
@@ -1,8 +1,9 @@
"""Historical repair and retention (issue #74).
"""Historical repair and retention (issue #74, #99).
Re-derives hour observations and day aggregates from surviving raw samples,
with idempotent and interruption-safe guarantees. Preserves import markers,
boundary anchors, and valid historical summaries.
rebuilds legacy local-day evidence from surviving samples or trustworthy UTC
hours, and preserves import markers, boundary anchors, and valid summaries.
Repair is idempotent and interruption-safe.
Contracts:
- Idempotent: running repair multiple times produces no duplicates
@@ -71,7 +72,6 @@ def _set_repair_in_progress(conn: sqlite3.Connection, in_progress: bool) -> None
"INSERT OR REPLACE INTO store_metadata (key, value) VALUES (?, ?)",
("repair_in_progress", "true" if in_progress else "false"),
)
conn.commit()
def _update_repair_status(conn: sqlite3.Connection, result: RepairResult) -> None:
@@ -100,8 +100,6 @@ def _update_repair_status(conn: sqlite3.Connection, result: RepairResult) -> Non
"INSERT OR REPLACE INTO store_metadata (key, value) VALUES (?, ?)",
("days_derived", str(days)),
)
conn.commit()
def get_repair_status(conn: sqlite3.Connection) -> RepairStatus:
@@ -364,20 +362,16 @@ def repair_derivation(
elif hasattr(clock, 'utcnow'):
clock = clock.utcnow()
# Check if repair is already in progress
if is_repair_in_progress(conn):
return RepairResult(
ok=False,
error="Repair already in progress",
)
# Mark repair as in progress
_set_repair_in_progress(conn, True)
result = RepairResult(ok=True)
try:
# Begin transaction
# A committed in-progress marker may be stale after process death.
# SQLite's write lock serializes active repairs; retry idempotently.
if conn.in_transaction:
conn.commit()
conn.execute("BEGIN IMMEDIATE")
_set_repair_in_progress(conn, True)
conn.commit()
conn.execute("BEGIN IMMEDIATE")
# 1. Find and derive intervals from sample pairs
@@ -407,7 +401,12 @@ def repair_derivation(
result.days_created += 1
else:
result.days_updated += 1
# Rebuild only legacy or unavailable local-day activity. This shares
# the migration derivation and leaves trustworthy published totals intact.
from .local_day import repair_legacy_local_day_evidence
repair_legacy_local_day_evidence(conn)
# 3. Count boundary anchors retained
now = clock if isinstance(clock, datetime) else datetime.now(timezone.utc)
cursor = conn.execute("SELECT ts FROM samples ORDER BY ts")
@@ -428,21 +427,22 @@ def repair_derivation(
)
result.legacy_summaries_preserved = cursor.fetchone()[0]
# Commit transaction
conn.commit()
# Update repair status
# Publish derived rows and completion status together. A process
# interruption rolls back the in-progress marker with the repair.
_update_repair_status(conn, result)
_set_repair_in_progress(conn, False)
conn.commit()
except Exception as e:
conn.rollback()
try:
_set_repair_in_progress(conn, False)
conn.commit()
except sqlite3.Error:
conn.rollback()
logger.error("Repair failed: %s", e)
return RepairResult(
ok=False,
error=str(e),
)
finally:
# Mark repair as complete
_set_repair_in_progress(conn, False)
return result