Files
llm/scripts/check_1c_extension_validation_evidence.py

173 lines
7.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())