161 lines
6.3 KiB
Python
161 lines
6.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Create manual evidence templates for a 1C extension validation plan."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
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 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 file_template(name: str, plan: dict[str, Any]) -> str:
|
|
if name.endswith(".json"):
|
|
payload: dict[str, Any] = {
|
|
"schema": "onec_extension_validation_manual_evidence.v1",
|
|
"status": "pending",
|
|
"created_at_utc": datetime.now(timezone.utc).isoformat(),
|
|
"validation_plan": plan.get("schema"),
|
|
"staging_dir": plan.get("staging_dir"),
|
|
"notes": "",
|
|
}
|
|
if name == "changed-objects-smoke.json":
|
|
payload["objects"] = [
|
|
{
|
|
**item,
|
|
"status": "pending",
|
|
"notes": "",
|
|
}
|
|
for item in ((plan.get("details") or {}).get("changed_objects") or [])
|
|
]
|
|
return json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
|
|
if name.endswith(".md"):
|
|
return "\n".join(
|
|
[
|
|
f"# {name}",
|
|
"",
|
|
"Status: pending",
|
|
"",
|
|
"Replace the status line with `Status: passed` only after the disposable-base check is complete.",
|
|
"Record what was checked in the disposable 1C base.",
|
|
"Do not include passwords, tokens, production connection strings, or personal data.",
|
|
"",
|
|
]
|
|
)
|
|
return "\n".join(
|
|
[
|
|
f"{name}",
|
|
f"Created UTC: {datetime.now(timezone.utc).isoformat()}",
|
|
"Status: pending",
|
|
"",
|
|
"Replace the status line with `Status: passed` only after the disposable-base check is complete.",
|
|
"Paste disposable 1C validation log here.",
|
|
"Do not include passwords, tokens, production connection strings, or personal data.",
|
|
"",
|
|
]
|
|
)
|
|
|
|
|
|
def create_evidence(plan_path: Path, output_root: Path | None, *, force: bool) -> dict[str, Any]:
|
|
plan = load_json(plan_path)
|
|
if plan.get("schema") != "onec_extension_validation_plan.v1":
|
|
raise SystemExit(f"Unsupported validation plan schema: {plan.get('schema')}")
|
|
if plan.get("status") != "ready_for_disposable_validation":
|
|
raise SystemExit(f"Validation plan is not ready for disposable validation: {plan.get('status')}")
|
|
|
|
root = evidence_root(plan, output_root)
|
|
root.mkdir(parents=True, exist_ok=True)
|
|
files = []
|
|
for name in (plan.get("evidence") or {}).get("required_files") or []:
|
|
relative = Path(str(name).replace("\\", "/"))
|
|
if relative.is_absolute() or ".." in relative.parts or not str(relative):
|
|
raise SystemExit(f"Unsafe evidence file name: {name}")
|
|
path = root / relative
|
|
existed = path.exists()
|
|
if existed and not force:
|
|
files.append({"path": str(path), "created": False, "skipped_existing": True})
|
|
continue
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(file_template(str(name), plan), encoding="utf-8")
|
|
files.append({"path": str(path), "created": True, "skipped_existing": False})
|
|
|
|
manifest = {
|
|
"schema": "onec_extension_validation_evidence_manifest.v1",
|
|
"created_at_utc": datetime.now(timezone.utc).isoformat(),
|
|
"status": "pending_manual_validation",
|
|
"plan_path": str(plan_path),
|
|
"plan_status": plan.get("status"),
|
|
"staging_dir": plan.get("staging_dir"),
|
|
"evidence_root": str(root),
|
|
"files": files,
|
|
"counts": {
|
|
"files": len(files),
|
|
"created": sum(1 for item in files if item.get("created")),
|
|
"skipped_existing": sum(1 for item in files if item.get("skipped_existing")),
|
|
},
|
|
}
|
|
write_json(root / "_codex_validation_evidence_manifest.json", manifest)
|
|
(root / "README.md").write_text(render_readme(manifest, plan), encoding="utf-8")
|
|
return manifest
|
|
|
|
|
|
def render_readme(manifest: dict[str, Any], plan: dict[str, Any]) -> str:
|
|
lines = [
|
|
"# 1C Extension Validation Evidence",
|
|
"",
|
|
f"Status: `{manifest.get('status')}`",
|
|
f"Validation plan: `{manifest.get('plan_path')}`",
|
|
f"Staging: `{manifest.get('staging_dir')}`",
|
|
"",
|
|
"## Rules",
|
|
"",
|
|
"- Fill these files only with evidence from a disposable 1C base.",
|
|
"- Do not include passwords, tokens, production connection strings, or personal data.",
|
|
"- This folder does not prove validation passed until the evidence checker passes.",
|
|
"",
|
|
"## Required Checks",
|
|
"",
|
|
]
|
|
for check in plan.get("checks") or []:
|
|
files = ", ".join(f"`{name}`" for name in check.get("expected_evidence") or [])
|
|
lines.append(f"- `{check.get('id')}`: {files}")
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Create manual evidence templates for a 1C extension validation plan.")
|
|
parser.add_argument("--plan", type=Path, required=True)
|
|
parser.add_argument("--output-root", type=Path)
|
|
parser.add_argument("--force", action="store_true")
|
|
parser.add_argument("--output", type=Path)
|
|
args = parser.parse_args()
|
|
|
|
result = create_evidence(args.plan, args.output_root, force=args.force)
|
|
if args.output:
|
|
write_json(args.output, result)
|
|
print(json.dumps({"output": str(args.output) if args.output else None, "status": result["status"], "counts": result["counts"]}, ensure_ascii=False))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|