#!/usr/bin/env python3 """Check manual evidence files for a 1C extension validation plan.""" from __future__ import annotations import argparse import json import re from pathlib import Path from typing import Any PENDING_MARKERS = ("status: pending", '"status": "pending"') PASSED_STATUSES = {"passed", "success", "ok"} STATUS_LINE_RE = re.compile(r"^\s*status\s*:\s*([A-Za-zА-Яа-я0-9_-]+)\s*$", re.IGNORECASE | re.MULTILINE) SECRET_TEXT_RE = re.compile(r"(password|passwd|pwd|secret|token|парол|секрет|usr|user)\s*[:=]", re.IGNORECASE) 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 evidence_root(plan: dict[str, Any], override: Path | None = None) -> Path: if override: return override configured = ((plan.get("runner_config") or {}).get("evidence_root")) or ((plan.get("evidence") or {}).get("root")) if not configured: raise SystemExit("Validation plan has no evidence root.") return Path(str(configured)) def text_status(text: str) -> str | None: match = STATUS_LINE_RE.search(text) return match.group(1).casefold() if match else None def check_json_evidence(path: Path, text: str, findings: list[dict[str, Any]]) -> tuple[bool, str | None]: try: payload = json.loads(text) except json.JSONDecodeError as exc: findings.append(issue("error", "invalid_json_evidence", f"Evidence JSON is invalid: {exc}", path=path)) return False, None status = str(payload.get("status") or "").casefold() if status not in PASSED_STATUSES: findings.append(issue("error", "evidence_status_not_passed", "Evidence JSON status must be passed/success/ok.", path=path, detail={"status": status or None})) return False, status or None if path.name == "changed-objects-smoke.json": objects = payload.get("objects") if not isinstance(objects, list) or not objects: findings.append(issue("error", "missing_smoke_objects", "changed-objects-smoke.json must contain a non-empty objects list.", path=path)) return False, status failed = [ {"object_name": item.get("object_name"), "status": item.get("status")} for item in objects if not isinstance(item, dict) or str(item.get("status") or "").casefold() not in PASSED_STATUSES ] if failed: findings.append(issue("error", "smoke_object_status_not_passed", "Every changed object smoke record must have status passed/success/ok.", path=path, detail={"failed": failed})) return False, status return True, status def check_text_evidence(path: Path, text: str, findings: list[dict[str, Any]]) -> tuple[bool, str | None]: status = text_status(text) if status not in PASSED_STATUSES: findings.append(issue("error", "evidence_status_not_passed", "Evidence text must contain a Status: passed/success/ok line.", path=path, detail={"status": status})) return False, status return True, status def check_evidence(plan_path: Path, output_root: Path | None = None) -> dict[str, Any]: findings: list[dict[str, Any]] = [] file_checks: list[dict[str, Any]] = [] plan = load_json(plan_path) if plan.get("schema") != "onec_extension_validation_plan.v1": findings.append(issue("error", "invalid_plan_schema", "Validation plan schema is not onec_extension_validation_plan.v1.", path=plan_path, detail={"schema": plan.get("schema")})) return build_result(plan_path, Path("."), findings, file_checks) root = evidence_root(plan, output_root) if not root.exists() or not root.is_dir(): findings.append(issue("error", "missing_evidence_root", "Evidence root is missing.", path=root)) return build_result(plan_path, root, findings, file_checks) for name in (plan.get("evidence") or {}).get("required_files") or []: relative = Path(str(name).replace("\\", "/")) path = root / relative check: dict[str, Any] = { "relative_path": str(relative).replace("\\", "/"), "path": str(path), "exists": path.exists(), "filled": False, } if not path.exists(): findings.append(issue("error", "missing_evidence_file", "Required evidence file is missing.", path=path)) else: text = path.read_text(encoding="utf-8-sig", errors="replace") stripped = text.strip() check["size"] = len(text.encode("utf-8")) if SECRET_TEXT_RE.search(text): findings.append(issue("error", "secret_like_text_in_evidence", "Evidence file contains secret-like key/value text.", path=path)) pending = any(marker in text.casefold() for marker in PENDING_MARKERS) if pending: findings.append(issue("error", "pending_evidence_file", "Evidence file still contains a pending template.", path=path)) if not stripped: findings.append(issue("error", "empty_evidence_file", "Evidence file is empty.", path=path)) elif path.suffix.casefold() == ".json": passed, status = check_json_evidence(path, text, findings) check["status"] = status check["filled"] = passed and not pending else: passed, status = check_text_evidence(path, text, findings) check["status"] = status check["filled"] = passed and not pending file_checks.append(check) manifest_path = root / "_codex_validation_evidence_manifest.json" if not manifest_path.exists(): findings.append(issue("warning", "missing_evidence_manifest", "Evidence manifest is missing.", path=manifest_path)) return build_result(plan_path, root, findings, file_checks) def build_result(plan_path: Path, root: Path, findings: list[dict[str, Any]], file_checks: 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_extension_validation_evidence_check.v1", "plan_path": str(plan_path), "evidence_root": str(root), "passed": not errors, "status": "validated" if not errors else "pending_or_blocked", "findings": findings, "file_checks": file_checks, "counts": { "files": len(file_checks), "filled": sum(1 for item in file_checks if item.get("filled")), "errors": len(errors), "warnings": len(warnings), }, } def main() -> int: parser = argparse.ArgumentParser(description="Check manual evidence files for a 1C extension validation plan.") parser.add_argument("--plan", type=Path, required=True) parser.add_argument("--evidence-root", type=Path) parser.add_argument("--output", type=Path) args = parser.parse_args() result = check_evidence(args.plan, args.evidence_root) if args.output: write_json(args.output, result) print(json.dumps({"output": str(args.output) if args.output else None, "passed": result["passed"], "status": result["status"], "counts": result["counts"]}, ensure_ascii=False)) return 0 if result["passed"] else 2 if __name__ == "__main__": raise SystemExit(main())