Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create a disposable-base validation plan for a staged 1C extension copy."""
|
||||
|
||||
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_runner_config import check_config
|
||||
|
||||
|
||||
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 changed_objects(manifest: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
result = []
|
||||
seen: set[tuple[str, str, str | None]] = set()
|
||||
for item in manifest.get("files") or []:
|
||||
parts = str(item.get("relative_path") or "").replace("\\", "/").split("/")
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
object_kind_folder, object_name = parts[0], parts[1]
|
||||
key = (object_kind_folder, object_name, item.get("name"))
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
result.append(
|
||||
{
|
||||
"object_kind_folder": object_kind_folder,
|
||||
"object_name": object_name,
|
||||
"artifact_kind": item.get("kind"),
|
||||
"artifact_name": item.get("name"),
|
||||
"relative_path": item.get("relative_path"),
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def build_required_checks(manifest: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
objects = changed_objects(manifest)
|
||||
checks = [
|
||||
{
|
||||
"id": "staging_integrity",
|
||||
"kind": "adapter",
|
||||
"required": True,
|
||||
"description": "Run check_1c_extension_staging and require passed=true.",
|
||||
"expected_evidence": ["staging-check.json"],
|
||||
},
|
||||
{
|
||||
"id": "load_extension_into_disposable_base",
|
||||
"kind": "1c_designer",
|
||||
"required": True,
|
||||
"description": "Load the staged extension source into a disposable 1C base. Production bases are forbidden.",
|
||||
"expected_evidence": ["designer-load-log.txt", "platform-version.txt"],
|
||||
},
|
||||
{
|
||||
"id": "configuration_syntax_check",
|
||||
"kind": "1c_designer",
|
||||
"required": True,
|
||||
"description": "Run 1C Designer syntax/configuration validation after loading the staged extension.",
|
||||
"expected_evidence": ["designer-syntax-check-log.txt"],
|
||||
},
|
||||
{
|
||||
"id": "extension_save_or_package_check",
|
||||
"kind": "1c_designer",
|
||||
"required": True,
|
||||
"description": "Save or package the extension from the disposable base and record that the platform accepted it.",
|
||||
"expected_evidence": ["extension-save-log.txt"],
|
||||
},
|
||||
]
|
||||
if objects:
|
||||
checks.append(
|
||||
{
|
||||
"id": "changed_objects_smoke",
|
||||
"kind": "1c_enterprise_or_manual",
|
||||
"required": True,
|
||||
"description": "Open or execute smoke scenarios for changed objects/forms/modules.",
|
||||
"objects": objects,
|
||||
"expected_evidence": ["changed-objects-smoke.json", "screenshots-or-manual-confirmation.md"],
|
||||
}
|
||||
)
|
||||
checks.append(
|
||||
{
|
||||
"id": "rollback_evidence",
|
||||
"kind": "operator",
|
||||
"required": True,
|
||||
"description": "Confirm the disposable base can be discarded or restored and no production state was modified.",
|
||||
"expected_evidence": ["rollback-confirmation.md"],
|
||||
}
|
||||
)
|
||||
return checks
|
||||
|
||||
|
||||
def build_plan(staging_dir: Path, runner_config_path: Path | None = None) -> dict[str, Any]:
|
||||
staging_check = check_staging(staging_dir)
|
||||
runner_config_check = check_config(runner_config_path) if runner_config_path else None
|
||||
if not staging_check.get("passed"):
|
||||
status = "blocked"
|
||||
runner_config = None
|
||||
elif runner_config_check and not runner_config_check.get("passed"):
|
||||
status = "blocked"
|
||||
runner_config = runner_config_check.get("sanitized_config")
|
||||
else:
|
||||
runner_config = runner_config_check.get("sanitized_config") if runner_config_check else None
|
||||
status = "ready_for_disposable_validation" if runner_config else "needs_runner_config"
|
||||
|
||||
manifest_path = staging_dir / "_codex_staging_manifest.json"
|
||||
manifest = load_json(manifest_path) if manifest_path.exists() else {}
|
||||
checks = build_required_checks(manifest)
|
||||
evidence_root = staging_dir / "_codex_validation_evidence"
|
||||
return {
|
||||
"schema": "onec_extension_validation_plan.v1",
|
||||
"created_at_utc": datetime.now(timezone.utc).isoformat(),
|
||||
"status": status,
|
||||
"passed": False,
|
||||
"staging_dir": str(staging_dir),
|
||||
"staging_manifest": str(manifest_path),
|
||||
"bundle_dir": manifest.get("bundle_dir"),
|
||||
"preferred_extension": manifest.get("preferred_extension"),
|
||||
"task": manifest.get("task"),
|
||||
"runner_config": runner_config,
|
||||
"runner_config_required": runner_config is None,
|
||||
"safety": {
|
||||
"production_base_allowed": False,
|
||||
"sql_write_allowed": False,
|
||||
"source_extension_write_allowed": False,
|
||||
"disposable_base_required": True,
|
||||
},
|
||||
"gates": [
|
||||
{
|
||||
"name": "staging_check",
|
||||
"schema": staging_check.get("schema"),
|
||||
"passed": staging_check.get("passed"),
|
||||
"counts": staging_check.get("counts"),
|
||||
},
|
||||
{
|
||||
"name": "runner_config_check",
|
||||
"schema": (runner_config_check or {}).get("schema"),
|
||||
"passed": (runner_config_check or {}).get("passed") if runner_config_check else None,
|
||||
"counts": (runner_config_check or {}).get("counts") if runner_config_check else None,
|
||||
},
|
||||
],
|
||||
"checks": checks,
|
||||
"evidence": {
|
||||
"root": str(evidence_root),
|
||||
"required_files": sorted({file_name for check in checks for file_name in check.get("expected_evidence", [])}),
|
||||
},
|
||||
"operator_steps": [
|
||||
"Use only a disposable 1C base copied from the target base or created for validation.",
|
||||
"Run the adapter staging integrity check and save its JSON evidence.",
|
||||
"Load the staged extension into the disposable base using the configured 1C runner or Designer procedure.",
|
||||
"Run syntax/configuration validation in 1C tooling and save logs.",
|
||||
"Run smoke checks for every changed object listed in this plan.",
|
||||
"Discard or restore the disposable base after validation.",
|
||||
],
|
||||
"details": {
|
||||
"staging_check": staging_check,
|
||||
"runner_config_check": runner_config_check,
|
||||
"changed_objects": changed_objects(manifest),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def render_markdown(plan: dict[str, Any]) -> str:
|
||||
lines = [
|
||||
"# 1C Extension Disposable Validation Plan",
|
||||
"",
|
||||
f"Status: `{plan.get('status')}`",
|
||||
f"Staging: `{plan.get('staging_dir')}`",
|
||||
f"Bundle: `{plan.get('bundle_dir')}`",
|
||||
f"Preferred extension: `{plan.get('preferred_extension')}`",
|
||||
"",
|
||||
"## Safety",
|
||||
"",
|
||||
"- Production base is not allowed.",
|
||||
"- SQL writes are not allowed.",
|
||||
"- Source extension writes are not allowed.",
|
||||
"- Disposable base validation is required.",
|
||||
"",
|
||||
"## Required Checks",
|
||||
"",
|
||||
]
|
||||
for check in plan.get("checks") or []:
|
||||
lines.append(f"- `{check.get('id')}` ({check.get('kind')}): {check.get('description')}")
|
||||
changed = (plan.get("details") or {}).get("changed_objects") or []
|
||||
if changed:
|
||||
lines.extend(["", "## Changed Objects", ""])
|
||||
for item in changed:
|
||||
lines.append(f"- `{item.get('object_kind_folder')}/{item.get('object_name')}`: `{item.get('artifact_name')}` ({item.get('artifact_kind')})")
|
||||
lines.extend(["", "## Evidence", ""])
|
||||
for name in (plan.get("evidence") or {}).get("required_files") or []:
|
||||
lines.append(f"- `{name}`")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Create a disposable-base validation plan for a staged 1C extension copy.")
|
||||
parser.add_argument("--staging-dir", type=Path, required=True)
|
||||
parser.add_argument("--runner-config", type=Path)
|
||||
parser.add_argument("--output", type=Path)
|
||||
parser.add_argument("--markdown-output", type=Path)
|
||||
args = parser.parse_args()
|
||||
|
||||
result = build_plan(args.staging_dir, args.runner_config)
|
||||
if args.output:
|
||||
write_json(args.output, result)
|
||||
if args.markdown_output:
|
||||
args.markdown_output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.markdown_output.write_text(render_markdown(result), encoding="utf-8")
|
||||
print(json.dumps({"output": str(args.output) if args.output else None, "status": result["status"], "checks": len(result["checks"])}, ensure_ascii=False))
|
||||
return 0 if result["status"] != "blocked" else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user