Compare commits

...

1 Commits

Author SHA1 Message Date
184ea8245d fix: add gate file rotation to prevent unbounded growth (#628)
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 31s
Smoke Test / smoke (pull_request) Failing after 20s
Validate Config / YAML Lint (pull_request) Failing after 21s
Validate Config / JSON Validate (pull_request) Successful in 23s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 2m8s
Validate Config / Shell Script Lint (pull_request) Failing after 1m12s
Validate Config / Cron Syntax Check (pull_request) Successful in 14s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 17s
PR Checklist / pr-checklist (pull_request) Failing after 5m27s
Validate Config / Playbook Schema Validation (pull_request) Successful in 24s
Architecture Lint / Lint Repository (pull_request) Has been cancelled
Validate Config / Python Test Suite (pull_request) Has been cancelled
The quality gate stores SHA-256 hashes and eval results in gate files.
Without rotation, these files accumulate indefinitely.

Changes:
- Added _rotate_gate_files() function
- Deletes files older than 7 days (GATE_FILE_MAX_AGE_DAYS)
- Caps directory at 50 historical files (GATE_FILE_MAX_COUNT)
- Always preserves eval_gate_latest.json
- Called automatically after each evaluate_candidate()

Closes #628
2026-04-15 01:26:14 +00:00

View File

@@ -25,9 +25,11 @@ Usage:
result = evaluate_candidate(scores_path, baseline_path, candidate_id) result = evaluate_candidate(scores_path, baseline_path, candidate_id)
""" """
import glob
import json import json
import os
import sys import sys
from datetime import datetime, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@@ -63,6 +65,10 @@ MAX_METRIC_REGRESSION = -0.15
# Default paths # Default paths
DEFAULT_GATE_DIR = Path.home() / ".timmy" / "training-data" / "eval-gates" DEFAULT_GATE_DIR = Path.home() / ".timmy" / "training-data" / "eval-gates"
# Gate file rotation settings (fixes #628: hash dedup growth)
GATE_FILE_MAX_AGE_DAYS = 7 # Delete gate files older than this
GATE_FILE_MAX_COUNT = 50 # Keep at most this many gate files (excluding latest)
def evaluate_candidate( def evaluate_candidate(
scores_path: str | Path, scores_path: str | Path,
@@ -239,6 +245,9 @@ def evaluate_candidate(
latest_file = gate_dir / "eval_gate_latest.json" latest_file = gate_dir / "eval_gate_latest.json"
latest_file.write_text(json.dumps(result, indent=2)) latest_file.write_text(json.dumps(result, indent=2))
# Rotate old gate files to prevent unbounded growth (#628)
_rotate_gate_files(gate_dir)
return result return result
@@ -287,6 +296,58 @@ def _find_category_score(
return None return None
def _rotate_gate_files(gate_dir: Path) -> int:
"""Rotate and clean up old eval gate files.
Prevents unbounded growth of the gate file directory by:
1. Deleting files older than GATE_FILE_MAX_AGE_DAYS
2. Keeping at most GATE_FILE_MAX_COUNT historical files
3. Always preserving eval_gate_latest.json
Returns the number of files deleted.
"""
if not gate_dir.exists():
return 0
deleted = 0
now = datetime.now(timezone.utc)
cutoff = now - timedelta(days=GATE_FILE_MAX_AGE_DAYS)
# Find all eval_gate_*.json files, excluding latest
pattern = str(gate_dir / "eval_gate_*.json")
all_files = glob.glob(pattern)
gate_files = [f for f in all_files if not f.endswith("eval_gate_latest.json")]
# Sort by modification time (oldest first)
gate_files.sort(key=lambda f: os.path.getmtime(f))
for filepath in gate_files:
try:
mtime = datetime.fromtimestamp(os.path.getmtime(filepath), tz=timezone.utc)
# Delete if older than max age
if mtime < cutoff:
os.remove(filepath)
deleted += 1
continue
except OSError:
pass
# Enforce max count (delete oldest first)
remaining = [f for f in gate_files if os.path.exists(f)]
if len(remaining) > GATE_FILE_MAX_COUNT:
excess = remaining[:len(remaining) - GATE_FILE_MAX_COUNT]
for filepath in excess:
try:
os.remove(filepath)
deleted += 1
except OSError:
pass
return deleted
# ── CLI ────────────────────────────────────────────────────────────── # ── CLI ──────────────────────────────────────────────────────────────
def main(): def main():