Merge origin/main into main

Reconcile the staged-projection and Diagnostics feature work with the CI
quality-gate changes from origin/main. The origin side of every conflict was
lint-only, so the feature imports were kept and the ruff fixes re-applied:

- keep the branch's added imports (timefmt, activity_readout, projection
  ladder helpers) and drop the unused ones ruff reported
- restore `noqa: E402` on the post-bootstrap imports in fenris-monitor and
  keep the `noqa: F401` re-exports in the TUI that tests assert on
- make the TUI monitoring-glyph test clock-relative instead of pinned to a
  fixed date
This commit is contained in:
xavierk
2026-10-06 10:14:09 +05:30
53 changed files with 288 additions and 229 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ if VENV_DIR.exists():
if site_packages:
sys.path.insert(0, str(site_packages))
from fenris.collector import run_collection
from fenris.collector import run_collection # noqa: E402 -- must follow the sys.path bootstrap above
CONFIG_PATH = Path("/etc/fenris/fenris.conf")
+1 -1
View File
@@ -9,7 +9,7 @@ usage-habit classification is known rather than unknown (§5.3).
"""
import sqlite3
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from datetime import datetime, timedelta
@dataclass(frozen=True)
+1 -1
View File
@@ -12,7 +12,7 @@ No proportional allocation, endpoint assignment, or double counting.
"""
import sqlite3
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional
from .hour_classify import classify_hour, HourSplit
-1
View File
@@ -26,7 +26,6 @@ Runit guarantees:
Spec: §8.4, §8.5, §8.6, §8.7, §8.8, ADR 0008
"""
import os
import shutil
import subprocess
import sys
from enum import Enum
+2 -3
View File
@@ -18,11 +18,10 @@ import logging
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional
from .hour_classify import classify_hour
from .segment import open_segment, normalize_identity
from .monitoring_periods import close_period
from .segment import open_segment
logger = logging.getLogger(__name__)
+2 -2
View File
@@ -394,7 +394,7 @@ def repair_legacy_local_day_evidence(conn: sqlite3.Connection) -> int:
"bytes_written": row[2], "bytes_read": row[3],
"segment_id": row[4], "local_tz": row[5],
}
for row in conn.execute(sample_select)
for row in conn.execute(sample_select) # nosemgrep: sqlalchemy-execute-raw-query -- query text is built from constant fragments only; no external input
]
for previous, current in pairwise(samples):
start = previous["ts"]
@@ -1018,7 +1018,7 @@ def query_local_day_summary(
"""
tz_filter = " AND tz_name = ?" if tz_name else ""
params = (local_date, tz_name) if tz_name else (local_date,)
row = conn.execute(
row = conn.execute( # nosemgrep: sqlalchemy-execute-raw-query -- only constant fragments are concatenated; values are bound via ? placeholders
"SELECT local_date, tz_name, tz_offset, utc_start, utc_end, "
" bytes_written, bytes_read, coverage, sample_count, complete, "
" activity_seconds, activity_intervals, activity_incomplete, "
+4 -4
View File
@@ -31,14 +31,14 @@ if VENV_DIR.exists():
if site_packages:
sys.path.insert(0, str(site_packages))
from fenris.store import init_store, get_store_path
from fenris.timefmt import local_timestamp, local_zone_name
from fenris.monitoring_periods import (
from fenris.store import init_store # noqa: E402 -- must follow the sys.path bootstrap above
from fenris.timefmt import local_timestamp, local_zone_name # noqa: E402 -- sys.path bootstrap above
from fenris.monitoring_periods import ( # noqa: E402 -- sys.path bootstrap above
ensure_period_open,
close_period,
get_open_period,
)
from fenris.init_system import (
from fenris.init_system import ( # noqa: E402 -- sys.path bootstrap above
enable_timer,
disable_timer,
collect_now,
+2 -2
View File
@@ -29,9 +29,9 @@ Criteria: PR-1–PR-17, CI-4.
"""
import sqlite3
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from datetime import datetime, timedelta
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional
from .monitoring_periods import interval_within_one_monitoring_period
+5 -5
View File
@@ -336,7 +336,7 @@ def prune_old_samples(
if owns_transaction:
conn.execute("BEGIN IMMEDIATE")
else:
conn.execute(f"SAVEPOINT {savepoint}")
conn.execute(f"SAVEPOINT {savepoint}") # nosemgrep: sqlalchemy-execute-raw-query, formatted-sql-query -- savepoint name is a function-local constant; SQLite cannot bind identifiers
try:
rows = _sample_rows(conn)
@@ -344,7 +344,7 @@ def prune_old_samples(
if owns_transaction:
conn.commit()
else:
conn.execute(f"RELEASE SAVEPOINT {savepoint}")
conn.execute(f"RELEASE SAVEPOINT {savepoint}") # nosemgrep: sqlalchemy-execute-raw-query, formatted-sql-query -- savepoint name is a function-local constant; SQLite cannot bind identifiers
return 0
newest_id = rows[-1][0]
@@ -361,12 +361,12 @@ def prune_old_samples(
if owns_transaction:
conn.commit()
else:
conn.execute(f"RELEASE SAVEPOINT {savepoint}")
conn.execute(f"RELEASE SAVEPOINT {savepoint}") # nosemgrep: sqlalchemy-execute-raw-query, formatted-sql-query -- savepoint name is a function-local constant; SQLite cannot bind identifiers
return len(expired)
except Exception:
if owns_transaction:
conn.rollback()
else:
conn.execute(f"ROLLBACK TO SAVEPOINT {savepoint}")
conn.execute(f"RELEASE SAVEPOINT {savepoint}")
conn.execute(f"ROLLBACK TO SAVEPOINT {savepoint}") # nosemgrep: sqlalchemy-execute-raw-query, formatted-sql-query -- savepoint name is a function-local constant; SQLite cannot bind identifiers
conn.execute(f"RELEASE SAVEPOINT {savepoint}") # nosemgrep: sqlalchemy-execute-raw-query, formatted-sql-query -- savepoint name is a function-local constant; SQLite cannot bind identifiers
raise
+2 -2
View File
@@ -13,11 +13,11 @@ Contracts:
"""
import logging
import sqlite3
from dataclasses import dataclass, field
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Optional, List, Tuple
from .derive import find_previous_sample, derive_hours_from_interval, _parse_ts
from .derive import derive_hours_from_interval, _parse_ts
logger = logging.getLogger(__name__)
-1
View File
@@ -17,7 +17,6 @@ import sqlite3
from datetime import datetime
from typing import Any, Dict, Optional, Tuple
from .collector import normalize_identity
def find_current_segment(conn: sqlite3.Connection) -> Optional[Dict[str, Any]]:
+2 -3
View File
@@ -15,8 +15,7 @@ Criteria: LC-9, CI-2, CI-4, FL-4, FL-5, FL-7.
"""
import sqlite3
from contextlib import contextmanager
import sys
from datetime import datetime, timedelta, timezone
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Tuple, TYPE_CHECKING
@@ -24,7 +23,7 @@ if TYPE_CHECKING:
from .status_composition import StatusComposition
from .projection import (
compute_projection, ConfidenceState, DISCLOSURES, ProjectionStage, ladder_count_text,
compute_projection, DISCLOSURES, ProjectionStage, ladder_count_text,
ladder_lines, write_rate_gb_day,
)
from .store import SCHEMA_VERSION
+5 -10
View File
@@ -11,24 +11,19 @@ Freshness grading uses shared constants from status.py.
import enum
import sqlite3
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
# Status poll interval (AC78-6): lightweight 5s systemctl show poll
STATUS_POLL_INTERVAL_S = 5
from .timefmt import freshness_age_human
from .status import (
FRESH_THRESHOLD_S,
STALENESS_THRESHOLD_S,
grade_freshness,
is_deliberately_paused,
monitoring_continuity,
deliberate_pause_lines,
open_store_readonly,
StoreFault,
NewerSchema,
)
from .timefmt import freshness_age_human
# Status poll interval (AC78-6): lightweight 5s systemctl show poll
STATUS_POLL_INTERVAL_S = 5
# ---------------------------------------------------------------------------
+5 -6
View File
@@ -7,7 +7,6 @@ This module handles:
"""
import sqlite3
from pathlib import Path
from typing import Optional
# Schema version - increment on each migration
@@ -77,7 +76,7 @@ def init_store(store_path: Path) -> sqlite3.Connection:
if current_version == 0:
# New database - create schema
_create_schema(conn)
conn.execute(f"PRAGMA user_version={SCHEMA_VERSION}")
conn.execute(f"PRAGMA user_version={SCHEMA_VERSION}") # nosemgrep: sqlalchemy-execute-raw-query, formatted-sql-query -- SCHEMA_VERSION is an int constant; PRAGMA cannot bind parameters
conn.commit()
elif current_version > SCHEMA_VERSION:
# Unknown newer version - refuse
@@ -339,7 +338,7 @@ def _apply_migrations(conn: sqlite3.Connection, current_version: int):
conn.execute("BEGIN IMMEDIATE")
try:
migration(conn)
conn.execute(f"PRAGMA user_version={target_version}")
conn.execute(f"PRAGMA user_version={target_version}") # nosemgrep: sqlalchemy-execute-raw-query, formatted-sql-query -- target_version is an int key of the static migrations map; PRAGMA cannot bind parameters
conn.commit()
except Exception:
conn.rollback()
@@ -364,7 +363,7 @@ def _migrate_1_to_2(conn: sqlite3.Connection) -> None:
).fetchall()}
for column in ("unattributed_bytes_written", "unattributed_bytes_read"):
if column not in cols:
conn.execute(
conn.execute( # nosemgrep: sqlalchemy-execute-raw-query, formatted-sql-query -- column comes from a hardcoded tuple; DDL cannot bind identifiers
f"ALTER TABLE day_aggregates ADD COLUMN {column} INTEGER DEFAULT 0"
)
@@ -421,7 +420,7 @@ def _migrate_4_to_5(conn: sqlite3.Connection) -> None:
("last_sample_id", "INTEGER"),
):
if column not in local_cols:
conn.execute(
conn.execute( # nosemgrep: sqlalchemy-execute-raw-query, formatted-sql-query -- column and declaration come from a hardcoded tuple; DDL cannot bind identifiers
f"ALTER TABLE local_days ADD COLUMN {column} {declaration}"
)
_create_local_day_shared_evidence(conn)
@@ -479,7 +478,7 @@ def migrate_to_latest(store_path: Path) -> int:
# Version 0 means no schema — create fresh (issue #73)
if current_version == 0:
_create_schema(conn)
conn.execute(f"PRAGMA user_version={SCHEMA_VERSION}")
conn.execute(f"PRAGMA user_version={SCHEMA_VERSION}") # nosemgrep: sqlalchemy-execute-raw-query, formatted-sql-query -- SCHEMA_VERSION is an int constant; PRAGMA cannot bind parameters
conn.commit()
conn.close()
return SCHEMA_VERSION
+4 -7
View File
@@ -15,7 +15,6 @@ from __future__ import annotations
import json
import math
import sqlite3
import sys
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional
@@ -26,7 +25,7 @@ from rich.markup import escape
from rich.text import Text
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Container, Horizontal, VerticalScroll
from textual.containers import Container, VerticalScroll
from textual.screen import ModalScreen
from textual.message import Message
from textual.widget import Widget
@@ -56,7 +55,6 @@ from .projection import (
ConfidenceState,
ProjectionResult,
ProjectionStage,
ScenarioRange,
compute_projection,
ladder_count_text,
ladder_lines,
@@ -64,10 +62,10 @@ from .projection import (
)
from .status import (
CADENCE_DEFAULT_S,
FRESH_THRESHOLD_S,
STALENESS_THRESHOLD_S,
FRESH_THRESHOLD_S, # noqa: F401 -- re-exported; tests/test_acceptance_sweep.py asserts parity with status
STALENESS_THRESHOLD_S, # noqa: F401 -- re-exported (see above)
format_disclosures,
grade_freshness,
grade_freshness, # noqa: F401 -- re-exported (see above)
deliberate_pause_lines,
read_status,
_journalctl_hint,
@@ -75,7 +73,6 @@ from .status import (
from .status_composition import (
StatusComposition,
render_status_tui,
STATUS_POLL_INTERVAL_S,
)
from .control import MONITOR_HELPER, MonitorError, run_monitor
from .derive import _parse_ts
+1 -1
View File
@@ -5,7 +5,7 @@ computation used by local-day derivation. All functions are
stateless and safe to call from the collector and TUI reader.
"""
import os
from datetime import datetime, timezone
from datetime import datetime
from pathlib import Path