97 lines
3.4 KiB
Python
97 lines
3.4 KiB
Python
"""Optional historical report migration; production only reads existing journals."""
|
|
|
|
import hashlib
|
|
import json
|
|
import shutil
|
|
from pathlib import Path
|
|
from tools.rag.common import DATA_DIR
|
|
from tools.runtime.report_files import CORE_JSON, run_id
|
|
|
|
|
|
def migration_plan(data_dir: Path = DATA_DIR) -> list[dict]:
|
|
moves = []
|
|
for path in sorted(data_dir.iterdir()):
|
|
if not path.is_file() or path.name.startswith(".") or path.name in CORE_JSON:
|
|
continue
|
|
if path.suffix == ".json":
|
|
target = data_dir / "reports/archive/legacy" / path.name
|
|
elif path.suffix == ".log":
|
|
target = data_dir / "logs/archive/legacy" / path.name
|
|
else:
|
|
continue
|
|
moves.append(
|
|
{
|
|
"source": str(path.absolute()),
|
|
"destination": str(target.absolute()),
|
|
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
}
|
|
)
|
|
return moves
|
|
|
|
|
|
def retention_plan(data_dir: Path = DATA_DIR, *, keep: int = 5) -> list[dict]:
|
|
if keep < 1:
|
|
raise ValueError("Keep at least one verification")
|
|
completed = []
|
|
for path in (data_dir / "reports/verification").glob("*.json"):
|
|
try:
|
|
data = json.loads(path.read_text())
|
|
if isinstance(data, dict) and type(data.get("passed")) is bool:
|
|
completed.append(path)
|
|
except (ValueError, OSError):
|
|
continue
|
|
moves = []
|
|
for path in sorted(completed, key=lambda p: (p.stat().st_mtime_ns, p.name), reverse=True)[
|
|
keep:
|
|
]:
|
|
target = data_dir / "reports/archive/verification" / path.name
|
|
moves.append(
|
|
{
|
|
"source": str(path.absolute()),
|
|
"destination": str(target.absolute()),
|
|
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
}
|
|
)
|
|
log = data_dir / "logs/verification" / path.with_suffix(".pytest.log").name
|
|
if log.exists():
|
|
target = data_dir / "logs/archive/verification" / log.name
|
|
moves.append(
|
|
{
|
|
"source": str(log.absolute()),
|
|
"destination": str(target.absolute()),
|
|
"sha256": hashlib.sha256(log.read_bytes()).hexdigest(),
|
|
}
|
|
)
|
|
return moves
|
|
|
|
|
|
def apply_moves(moves: list[dict], *, data_dir: Path = DATA_DIR) -> Path:
|
|
# Validate the entire plan before moving anything. Never replace existing files.
|
|
for move in moves:
|
|
source, destination = Path(move["source"]), Path(move["destination"])
|
|
if (
|
|
destination.exists()
|
|
or hashlib.sha256(source.read_bytes()).hexdigest() != move["sha256"]
|
|
):
|
|
raise RuntimeError("Report migration conflict; no files moved")
|
|
journal = data_dir / "reports/migrations" / (run_id() + ".json")
|
|
journal.parent.mkdir(parents=True, exist_ok=True)
|
|
record = {
|
|
"method": "Move without rewriting content or deleting historical evidence",
|
|
"moves": [],
|
|
}
|
|
|
|
def write_journal():
|
|
temporary = journal.with_suffix(".tmp")
|
|
temporary.write_text(json.dumps(record, indent=2) + "\n")
|
|
temporary.replace(journal)
|
|
|
|
write_journal()
|
|
for move in moves:
|
|
destination = Path(move["destination"])
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.move(move["source"], destination)
|
|
record["moves"].append(move)
|
|
write_journal()
|
|
return journal
|