fix: validate complete-day monitoring coverage

This commit is contained in:
xavierk
2026-09-29 03:34:17 +05:30
parent d045e88043
commit e22b99442e
7 changed files with 130 additions and 65 deletions
+22 -31
View File
@@ -33,6 +33,11 @@ from itertools import pairwise
from typing import Any
from zoneinfo import ZoneInfo
from .monitoring_periods import (
as_utc_datetime,
interval_within_one_monitoring_period,
)
@dataclass(frozen=True)
class LocalDaySummary:
@@ -308,27 +313,13 @@ def persist_local_day(
return created
def _utc_datetime(value: str | datetime) -> datetime:
parsed = value if isinstance(value, datetime) else datetime.fromisoformat(value)
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return parsed.astimezone(timezone.utc)
def _same_monitoring_period(
conn: sqlite3.Connection,
start: datetime,
end: datetime,
) -> bool:
"""Return whether one recorded monitoring period contains the interval."""
for started_at, ended_at in conn.execute(
"SELECT started_at, ended_at FROM monitoring_periods"
):
period_start = _utc_datetime(started_at)
period_end = _utc_datetime(ended_at) if ended_at else None
if period_start <= start and (period_end is None or end <= period_end):
return True
return False
return interval_within_one_monitoring_period(conn, start, end)
def repair_legacy_local_day_evidence(conn: sqlite3.Connection) -> int:
@@ -365,13 +356,13 @@ def repair_legacy_local_day_evidence(conn: sqlite3.Connection) -> int:
segment_windows = {}
if {"monitoring_periods", "controller_segments"}.issubset(tables):
periods = [
(_utc_datetime(start), _utc_datetime(end) if end else None)
(as_utc_datetime(start), as_utc_datetime(end) if end else None)
for start, end in conn.execute(
"SELECT started_at, ended_at FROM monitoring_periods"
)
]
segment_boundaries = [
(segment_id, _utc_datetime(opened_at))
(segment_id, as_utc_datetime(opened_at))
for segment_id, opened_at in conn.execute(
"SELECT id, opened_at FROM controller_segments ORDER BY opened_at, id"
)
@@ -399,7 +390,7 @@ def repair_legacy_local_day_evidence(conn: sqlite3.Connection) -> int:
)
samples = [
{
"id": row[0], "ts": _utc_datetime(row[1]),
"id": row[0], "ts": as_utc_datetime(row[1]),
"bytes_written": row[2], "bytes_read": row[3],
"segment_id": row[4], "local_tz": row[5],
}
@@ -453,8 +444,8 @@ def repair_legacy_local_day_evidence(conn: sqlite3.Connection) -> int:
"id": row[0],
"local_date": row[1],
"tz_name": row[2],
"start": _utc_datetime(row[3]),
"end": _utc_datetime(row[4]),
"start": as_utc_datetime(row[3]),
"end": as_utc_datetime(row[4]),
"precision": row[6],
}
for row in recorded_days
@@ -482,8 +473,8 @@ def repair_legacy_local_day_evidence(conn: sqlite3.Connection) -> int:
local_day_id, local_date, tz_name, utc_start, utc_end,
prior_incomplete,
) = row
start = _utc_datetime(utc_start)
end = _utc_datetime(utc_end)
start = as_utc_datetime(utc_start)
end = as_utc_datetime(utc_end)
matching = matching_by_day[local_day_id]
if not matching:
@@ -595,8 +586,8 @@ def _record_reconstructed_shared_intervals(
day_rows = [
{
"id": row[0], "local_date": row[1], "tz_name": row[2],
"utc_start": _utc_datetime(row[3]),
"utc_end": _utc_datetime(row[4]),
"utc_start": as_utc_datetime(row[3]),
"utc_end": as_utc_datetime(row[4]),
"precision": row[6],
}
for row in recorded_days
@@ -685,7 +676,7 @@ def _coarse_local_day_activity(
full_hours: dict[datetime, tuple] = {}
boundary_hour_found = False
other_day_bounds = [
(_utc_datetime(row[0]), _utc_datetime(row[1]))
(as_utc_datetime(row[0]), as_utc_datetime(row[1]))
for row in conn.execute(
"SELECT utc_start, utc_end FROM local_days WHERE id != ?",
(local_day_id,),
@@ -698,7 +689,7 @@ def _coarse_local_day_activity(
"FROM hour_observations WHERE hour >= ? AND hour < ? ORDER BY hour",
(first_hour.isoformat(), day_end.isoformat()),
):
hour_start = _utc_datetime(row[0])
hour_start = as_utc_datetime(row[0])
hour_end = hour_start + hour
if hour_start >= day_end or hour_end <= day_start:
continue
@@ -856,8 +847,8 @@ def record_local_activity_interval(
both samples share one monitoring period, and the interval stays inside one
recorded local date. Every other valid difference remains one unallocated row.
"""
start = _utc_datetime(previous["ts"])
end = _utc_datetime(current["ts"])
start = as_utc_datetime(previous["ts"])
end = as_utc_datetime(current["ts"])
if end <= start:
return None
@@ -1004,12 +995,12 @@ def mark_local_activity_gap(
previous: dict | None = None,
) -> None:
"""Mark dates around a controller boundary as missing local activity."""
current = _utc_datetime(current_ts)
current = as_utc_datetime(current_ts)
current_date = current.astimezone(ZoneInfo(current_tz)).date().isoformat()
_upsert_activity_day(conn, current_date, current_tz, incomplete=True)
if previous is None or not previous.get("local_tz"):
return
previous_at = _utc_datetime(previous["ts"])
previous_at = as_utc_datetime(previous["ts"])
previous_tz = previous["local_tz"]
previous_date = previous_at.astimezone(ZoneInfo(previous_tz)).date().isoformat()
_upsert_activity_day(conn, previous_date, previous_tz, incomplete=True)
@@ -1127,7 +1118,7 @@ def query_local_day_summary(
== recorded_date
)
day_seconds = int(
(_utc_datetime(utc_end) - _utc_datetime(utc_start)).total_seconds()
(as_utc_datetime(utc_end) - as_utc_datetime(utc_start)).total_seconds()
)
interval_seconds = activity_seconds or 0
full_activity_day = interval_seconds >= day_seconds or (
+41 -1
View File
@@ -9,7 +9,7 @@ Key contracts:
- End causes: user_disabled, migrated, unknown_gap
"""
import sqlite3
from datetime import datetime
from datetime import datetime, timezone
def ensure_period_open(conn: sqlite3.Connection, run_time: datetime) -> None:
@@ -82,6 +82,46 @@ def is_inside_period(conn: sqlite3.Connection, ts: datetime) -> bool:
return cursor.fetchone() is not None
def interval_within_one_monitoring_period(
conn: sqlite3.Connection,
start: str | datetime,
end: str | datetime,
) -> bool:
"""Return whether one monitoring period contains the full interval.
Compare timestamps as UTC instants. Malformed timestamps fail closed.
"""
try:
interval_start = as_utc_datetime(start)
interval_end = as_utc_datetime(end)
except (TypeError, ValueError, OverflowError):
return False
if interval_end <= interval_start:
return False
for started_at, ended_at in conn.execute(
"SELECT started_at, ended_at FROM monitoring_periods"
):
try:
period_start = as_utc_datetime(started_at)
period_end = as_utc_datetime(ended_at) if ended_at is not None else None
except (TypeError, ValueError, OverflowError):
continue
if period_start <= interval_start and (
period_end is None or interval_end <= period_end
):
return True
return False
def as_utc_datetime(value: str | datetime) -> datetime:
"""Parse a timestamp and normalize it to an aware UTC datetime."""
parsed = value if isinstance(value, datetime) else datetime.fromisoformat(value)
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return parsed.astimezone(timezone.utc)
def wall_clock_in_periods(
conn: sqlite3.Connection,
start: datetime,
+9 -10
View File
@@ -28,6 +28,7 @@ from datetime import datetime, timedelta, timezone
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple
from .monitoring_periods import interval_within_one_monitoring_period
# ---------------------------------------------------------------------------
# Constants (spec §6)
@@ -494,18 +495,16 @@ def _has_complete_local_day(conn):
within a monitoring period that has usable observation evidence.
This is the prerequisite for showing an endurance outlook.
"""
return _count_complete_local_days(conn) > 0
def _count_complete_local_days(conn):
"""Count the number of complete local observation days."""
row = conn.execute(
"SELECT COUNT(*) FROM local_days "
local_days = conn.execute(
"SELECT utc_start, utc_end FROM local_days "
"WHERE complete = 1 "
"AND activity_precision IN ('measured', 'coarse') "
"AND activity_intervals > 0"
).fetchone()
return row[0] if row else 0
"AND activity_intervals > 0 ORDER BY id"
).fetchall()
return any(
interval_within_one_monitoring_period(conn, utc_start, utc_end)
for utc_start, utc_end in local_days
)
# ---------------------------------------------------------------------------
+10 -21
View File
@@ -4,25 +4,23 @@ The 14-day cutoff never overrides local-day preservation, shared boundary
evidence, publication recovery, or the newest sample's successor-anchor role.
"""
import sqlite3
from datetime import date, datetime, timedelta, timezone
from datetime import date, datetime, timedelta
from .monitoring_periods import (
as_utc_datetime,
interval_within_one_monitoring_period,
)
RAW_SAMPLE_RETENTION_DAYS = 14
def _utc_datetime(value: datetime) -> datetime:
"""Normalize a clock value to aware UTC."""
if value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc)
def _parse_sample_time(value: str) -> datetime | None:
"""Parse old timestamps conservatively; malformed legacy values stay."""
try:
parsed = datetime.fromisoformat(value)
except (TypeError, ValueError):
return None
return _utc_datetime(parsed)
return as_utc_datetime(parsed)
def _sample_rows(conn: sqlite3.Connection) -> list[tuple]:
@@ -149,16 +147,7 @@ def _monitoring_period_covers(
end: datetime,
) -> bool:
"""Require continuous monitoring before a local-day total replaces detail."""
for started_at, ended_at in conn.execute(
"SELECT started_at, ended_at FROM monitoring_periods"
):
period_start = _parse_sample_time(started_at)
period_end = _parse_sample_time(ended_at) if ended_at is not None else None
if period_start is None or period_start > start:
continue
if ended_at is None or (period_end is not None and end <= period_end):
return True
return False
return interval_within_one_monitoring_period(conn, start, end)
def _has_local_replacement(
@@ -312,7 +301,7 @@ def needs_boundary_anchor(
) -> bool:
"""Return whether an old sample still carries unreplaced evidence."""
sample_time = _parse_sample_time(sample_ts)
cutoff = _utc_datetime(now) - timedelta(days=RAW_SAMPLE_RETENTION_DAYS)
cutoff = as_utc_datetime(now) - timedelta(days=RAW_SAMPLE_RETENTION_DAYS)
if sample_time is None:
return True
if sample_time >= cutoff:
@@ -341,7 +330,7 @@ def prune_old_samples(
neighbour interval, lacks UTC or local-day replacement evidence, or has a
timestamp that cannot be safely interpreted.
"""
cutoff = _utc_datetime(now) - timedelta(days=retention_days)
cutoff = as_utc_datetime(now) - timedelta(days=retention_days)
owns_transaction = not conn.in_transaction
savepoint = "fenris_sample_retention"
if owns_transaction: