Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Aggregate final validation gates for a staged 1C extension."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from check_1c_extension_staging import check_staging
|
||||
from check_1c_extension_validation_evidence import check_evidence
|
||||
|
||||
|
||||
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 collect_gate(name: str, data: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"name": name,
|
||||
"schema": data.get("schema"),
|
||||
"passed": data.get("passed"),
|
||||
"status": data.get("status"),
|
||||
"counts": data.get("counts"),
|
||||
}
|
||||
|
||||
|
||||
def check_release(plan_path: Path, evidence_root: Path | None = None) -> dict[str, Any]:
|
||||
findings: list[dict[str, Any]] = []
|
||||
if not plan_path.exists():
|
||||
findings.append(issue("error", "missing_validation_plan", "Validation plan is missing.", path=plan_path))
|
||||
return build_result(plan_path, None, {}, {}, findings)
|
||||
|
||||
plan = load_json(plan_path)
|
||||
if plan.get("schema") != "onec_extension_validation_plan.v1":
|
||||
findings.append(issue("error", "invalid_validation_plan_schema", "Validation plan schema is not onec_extension_validation_plan.v1.", path=plan_path, detail={"schema": plan.get("schema")}))
|
||||
|
||||
if plan.get("status") != "ready_for_disposable_validation":
|
||||
findings.append(issue("error", "validation_plan_not_ready", "Validation plan must be ready_for_disposable_validation.", path=plan_path, detail={"status": plan.get("status")}))
|
||||
|
||||
staging_dir = Path(str(plan.get("staging_dir") or ""))
|
||||
staging_check = check_staging(staging_dir) if staging_dir else {"schema": "onec_extension_staging_check.v1", "passed": False, "counts": {"errors": 1}, "findings": []}
|
||||
if not staging_check.get("passed"):
|
||||
findings.append(issue("error", "staging_check_failed", "Staging check failed.", path=staging_dir, detail={"counts": staging_check.get("counts")}))
|
||||
|
||||
evidence_check = check_evidence(plan_path, evidence_root)
|
||||
if not evidence_check.get("passed"):
|
||||
findings.append(issue("error", "validation_evidence_check_failed", "Validation evidence check failed.", path=evidence_check.get("evidence_root"), detail={"counts": evidence_check.get("counts")}))
|
||||
|
||||
safety = plan.get("safety") if isinstance(plan.get("safety"), dict) else {}
|
||||
expected_safety = {
|
||||
"production_base_allowed": False,
|
||||
"sql_write_allowed": False,
|
||||
"source_extension_write_allowed": False,
|
||||
"disposable_base_required": True,
|
||||
}
|
||||
for key, expected in expected_safety.items():
|
||||
if safety.get(key) is not expected:
|
||||
findings.append(issue("error", "invalid_release_safety_flag", "Validation plan safety flag has an unexpected value.", path=plan_path, detail={"flag": key, "expected": expected, "actual": safety.get(key)}))
|
||||
|
||||
return build_result(plan_path, plan, staging_check, evidence_check, findings)
|
||||
|
||||
|
||||
def build_result(plan_path: Path, plan: dict[str, Any] | None, staging_check: dict[str, Any], evidence_check: dict[str, Any], 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_extension_validation_release_check.v1",
|
||||
"created_at_utc": datetime.now(timezone.utc).isoformat(),
|
||||
"plan_path": str(plan_path),
|
||||
"staging_dir": (plan or {}).get("staging_dir"),
|
||||
"bundle_dir": (plan or {}).get("bundle_dir"),
|
||||
"preferred_extension": (plan or {}).get("preferred_extension"),
|
||||
"passed": not errors,
|
||||
"status": "validated_for_human_review" if not errors else "blocked",
|
||||
"safety": {
|
||||
"production_apply_allowed": False,
|
||||
"automatic_apply_allowed": False,
|
||||
"human_approval_required": True,
|
||||
},
|
||||
"gates": [
|
||||
{
|
||||
"name": "validation_plan",
|
||||
"schema": (plan or {}).get("schema"),
|
||||
"passed": (plan or {}).get("status") == "ready_for_disposable_validation",
|
||||
"status": (plan or {}).get("status"),
|
||||
},
|
||||
collect_gate("staging_check", staging_check),
|
||||
collect_gate("validation_evidence_check", evidence_check),
|
||||
],
|
||||
"findings": findings,
|
||||
"counts": {
|
||||
"errors": len(errors),
|
||||
"warnings": len(warnings),
|
||||
},
|
||||
"next_actions": [
|
||||
"Review validation evidence and changed files manually.",
|
||||
"Do not apply to production automatically.",
|
||||
"If approved, perform production action through the approved human-controlled 1C release process.",
|
||||
] if not errors else [
|
||||
"Fix failed gates before review.",
|
||||
"Do not package, apply, or release this extension from the current evidence.",
|
||||
],
|
||||
"details": {
|
||||
"staging_check": staging_check,
|
||||
"validation_evidence_check": evidence_check,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Aggregate final validation gates for a staged 1C extension.")
|
||||
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_release(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())
|
||||
Reference in New Issue
Block a user