139 lines
6.0 KiB
Python
139 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate a latest 1C saved-state watch run lookup."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
EXPECTED_SCHEMA = "onec_saved_state_latest_watch_run.v1"
|
|
|
|
|
|
def load_json(path: Path) -> dict[str, Any]:
|
|
return json.loads(path.read_text(encoding="utf-8-sig"))
|
|
|
|
|
|
def write_json(path: Path, data: dict[str, Any]) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
|
|
|
|
def issue(severity: str, code: str, message: str, *, path: Path | str | None = None, detail: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
result: dict[str, Any] = {"severity": severity, "code": code, "message": message}
|
|
if path is not None:
|
|
result["path"] = str(path)
|
|
if detail:
|
|
result["detail"] = detail
|
|
return result
|
|
|
|
|
|
def linked_path(latest_path: Path, value: Any) -> Path | None:
|
|
if not value:
|
|
return None
|
|
path = Path(str(value))
|
|
if path.is_absolute():
|
|
return path
|
|
return (latest_path.parent / path).resolve()
|
|
|
|
|
|
def check_linked_check(latest_path: Path, run: dict[str, Any], key: str, passed_key: str, findings: list[dict[str, Any]]) -> None:
|
|
path = linked_path(latest_path, run.get(key))
|
|
if path is None:
|
|
return
|
|
if not path.exists():
|
|
findings.append(issue("error", f"missing_{key}", f"Linked {key} file is missing.", path=path))
|
|
return
|
|
linked = load_json(path)
|
|
if run.get(passed_key) != linked.get("passed"):
|
|
findings.append(issue("error", f"{passed_key}_mismatch", f"{passed_key} differs from linked check.", path=path))
|
|
|
|
|
|
def has_delta_changes(run: dict[str, Any]) -> bool:
|
|
counts = run.get("counts") or {}
|
|
return any(int(counts.get(key) or 0) > 0 for key in ("delta_objects_added", "delta_objects_removed", "delta_objects_changed"))
|
|
|
|
|
|
def check_latest(latest_path: Path) -> dict[str, Any]:
|
|
findings: list[dict[str, Any]] = []
|
|
if not latest_path.exists():
|
|
findings.append(issue("error", "missing_latest", "Latest watch lookup file is missing.", path=latest_path))
|
|
return build_result(latest_path, findings)
|
|
|
|
data = load_json(latest_path)
|
|
if data.get("schema") != EXPECTED_SCHEMA:
|
|
findings.append(issue("error", "invalid_schema", "Latest watch lookup schema is invalid.", detail={"schema": data.get("schema")}))
|
|
|
|
safety = data.get("safety") or {}
|
|
if safety.get("read_only") is not True:
|
|
findings.append(issue("error", "not_read_only", "Latest watch lookup safety.read_only must be true."))
|
|
if safety.get("sql_write_performed") is not False:
|
|
findings.append(issue("error", "sql_write_flag", "Latest watch lookup safety.sql_write_performed must be false."))
|
|
|
|
markdown = linked_path(latest_path, data.get("markdown"))
|
|
if data.get("markdown") and (markdown is None or not markdown.exists()):
|
|
findings.append(issue("error", "missing_markdown", "Linked latest Markdown file is missing.", path=markdown or "<null>"))
|
|
|
|
found = data.get("found")
|
|
latest = data.get("latest")
|
|
if found is True:
|
|
if not isinstance(latest, dict):
|
|
findings.append(issue("error", "missing_latest_run", "found=true requires latest object."))
|
|
return build_result(latest_path, findings)
|
|
run_dir = linked_path(latest_path, latest.get("run_dir"))
|
|
if run_dir is None or not run_dir.exists():
|
|
findings.append(issue("error", "missing_run_dir", "Latest run directory is missing.", path=run_dir or "<null>"))
|
|
for key in ("report", "manifest_check"):
|
|
path = linked_path(latest_path, latest.get(key))
|
|
if path is not None and not path.exists():
|
|
findings.append(issue("error", f"missing_{key}", f"Linked {key} is missing.", path=path))
|
|
check_linked_check(latest_path, latest, "manifest_check", "manifest_check_passed", findings)
|
|
check_linked_check(latest_path, latest, "delta_check", "delta_check_passed", findings)
|
|
if data.get("require_delta") is True and not latest.get("delta"):
|
|
findings.append(issue("error", "required_delta_missing", "require_delta=true but latest run has no delta."))
|
|
if data.get("require_changed") is True and not has_delta_changes(latest):
|
|
findings.append(issue("error", "required_changed_missing", "require_changed=true but latest run has no delta changes."))
|
|
elif found is False:
|
|
if latest is not None:
|
|
findings.append(issue("error", "unexpected_latest", "found=false requires latest=null."))
|
|
else:
|
|
findings.append(issue("error", "invalid_found", "found must be boolean."))
|
|
|
|
counts = data.get("counts")
|
|
if not isinstance(counts, dict):
|
|
findings.append(issue("error", "missing_counts", "Latest lookup must include counts."))
|
|
|
|
return build_result(latest_path, findings)
|
|
|
|
|
|
def build_result(latest_path: Path, findings: list[dict[str, Any]]) -> dict[str, Any]:
|
|
errors = [row for row in findings if row.get("severity") == "error"]
|
|
warnings = [row for row in findings if row.get("severity") == "warning"]
|
|
return {
|
|
"schema": "onec_saved_state_latest_watch_run_check.v1",
|
|
"latest": str(latest_path),
|
|
"passed": not errors,
|
|
"findings": findings,
|
|
"counts": {"errors": len(errors), "warnings": len(warnings)},
|
|
"safety": {"read_only": True, "sql_write_performed": False},
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Validate latest 1C saved-state watch run lookup.")
|
|
parser.add_argument("--latest", type=Path, required=True)
|
|
parser.add_argument("--output", type=Path)
|
|
args = parser.parse_args()
|
|
|
|
result = check_latest(args.latest)
|
|
if args.output:
|
|
write_json(args.output, result)
|
|
print(json.dumps({"output": str(args.output) if args.output else None, "passed": result["passed"], "counts": result["counts"]}, ensure_ascii=False))
|
|
return 0 if result["passed"] else 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|