Initial SQL-only 1C adapter baseline

This commit is contained in:
2026-07-22 03:03:47 +03:00
commit e2503b77e7
545 changed files with 184711 additions and 0 deletions
@@ -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())