#!/usr/bin/env python3 """Validate a one-shot 1C saved-state watch manifest.""" from __future__ import annotations import argparse import json from pathlib import Path from typing import Any EXPECTED_SCHEMA = "onec_saved_state_watch_once.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) -> dict[str, Any]: result = {"severity": severity, "code": code, "message": message} if path is not None: result["path"] = str(path) return result def linked_path(manifest_path: Path, value: Any) -> Path | None: if not value: return None path = Path(str(value)) if path.is_absolute(): return path return (manifest_path.parent / path).resolve() def check_existing(manifest_path: Path, data: dict[str, Any], key: str, findings: list[dict[str, Any]], *, required: bool = True) -> Path | None: path = linked_path(manifest_path, data.get(key)) if path is None: if required: findings.append(issue("error", f"missing_{key}", f"Manifest field {key} is missing.")) return None if not path.exists(): findings.append(issue("error" if required else "warning", f"missing_{key}_file", f"Linked {key} file is missing.", path=path)) return path def check_manifest(manifest_path: Path) -> dict[str, Any]: findings: list[dict[str, Any]] = [] if not manifest_path.exists(): findings.append(issue("error", "missing_manifest", "Watch manifest is missing.", path=manifest_path)) return build_result(manifest_path, findings) data = load_json(manifest_path) if data.get("schema") != EXPECTED_SCHEMA: findings.append(issue("error", "invalid_schema", "Watch manifest schema is invalid.")) safety = data.get("safety") or {} if safety.get("read_only") is not True: findings.append(issue("error", "not_read_only", "Watch manifest safety.read_only must be true.")) if safety.get("sql_write_performed") is not False: findings.append(issue("error", "sql_write_flag", "Watch manifest safety.sql_write_performed must be false.")) if safety.get("secrets_in_report") is not False: findings.append(issue("error", "secrets_flag", "Watch manifest safety.secrets_in_report must be false.")) report_path = check_existing(manifest_path, data, "report", findings) check_existing(manifest_path, data, "markdown", findings, required=False) report_check_path = check_existing(manifest_path, data, "check", findings) check_existing(manifest_path, data, "manifest_markdown", findings, required=False) delta_path = check_existing(manifest_path, data, "delta", findings, required=False) check_existing(manifest_path, data, "delta_markdown", findings, required=False) delta_check_path = check_existing(manifest_path, data, "delta_check", findings, required=False) if report_path and report_path.exists(): report = load_json(report_path) counts = data.get("counts") or {} report_counts = report.get("counts") or {} if counts.get("object_changes") != report_counts.get("object_changes"): findings.append(issue("error", "object_count_mismatch", "Watch object_changes count differs from report.")) if counts.get("system_changes") != report_counts.get("system_changes"): findings.append(issue("error", "system_count_mismatch", "Watch system_changes count differs from report.")) if report_check_path and report_check_path.exists(): report_check = load_json(report_check_path) if report_check.get("passed") is not True: findings.append(issue("error", "report_check_failed", "Linked saved-state report check did not pass.", path=report_check_path)) if delta_path and delta_path.exists(): delta = load_json(delta_path) counts = data.get("counts") or {} delta_counts = delta.get("counts") or {} mapping = { "delta_objects_added": "objects_added", "delta_objects_removed": "objects_removed", "delta_objects_changed": "objects_changed", "delta_objects_unchanged": "objects_unchanged", } for watch_key, delta_key in mapping.items(): if counts.get(watch_key) != delta_counts.get(delta_key): findings.append(issue("error", "delta_count_mismatch", f"Watch {watch_key} differs from delta {delta_key}.")) if delta_check_path and delta_check_path.exists(): delta_check = load_json(delta_check_path) if delta_check.get("passed") is not True: findings.append(issue("error", "delta_check_failed", "Linked delta check did not pass.", path=delta_check_path)) return build_result(manifest_path, findings) def build_result(manifest_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_watch_once_check.v1", "manifest": str(manifest_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 a one-shot 1C saved-state watch manifest.") parser.add_argument("--manifest", type=Path, required=True) parser.add_argument("--output", type=Path) args = parser.parse_args() result = check_manifest(args.manifest) 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())