Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create a review bundle from a ready 1C patch workspace without applying it."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import zipfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from check_1c_patch_preflight import build_preflight
|
||||
from check_1c_patch_bundle import check_bundle
|
||||
from diff_1c_patch_workspace import build_diff
|
||||
from render_1c_patch_preflight_markdown import render as render_preflight_markdown
|
||||
|
||||
|
||||
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 safe_relative(relative_path: str) -> Path:
|
||||
path = Path(relative_path.replace("\\", "/"))
|
||||
if path.is_absolute() or ".." in path.parts or not str(path):
|
||||
raise SystemExit(f"Unsafe relative path: {relative_path}")
|
||||
return path
|
||||
|
||||
|
||||
def slug_from_workspace(workspace: Path) -> str:
|
||||
return workspace.name or "onec-patch"
|
||||
|
||||
|
||||
def modified_records(workspace: Path, diff: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
manifest = load_json(workspace / "manifest.json")
|
||||
records_by_rel = {
|
||||
str(record.get("relative_path") or "").replace("\\", "/"): record
|
||||
for record in manifest.get("files") or []
|
||||
}
|
||||
result = []
|
||||
for item in diff.get("files") or []:
|
||||
if item.get("status") != "modified":
|
||||
continue
|
||||
relative = str(item.get("relative_path") or "").replace("\\", "/")
|
||||
record = dict(records_by_rel.get(relative) or {})
|
||||
record["relative_path"] = relative
|
||||
record["diff"] = {
|
||||
key: (item.get("diff") or {}).get(key)
|
||||
for key in ("added_lines", "removed_lines", "hunks", "patch_truncated", "patch_chars")
|
||||
}
|
||||
record["sha256"] = item.get("sha256")
|
||||
result.append(record)
|
||||
return result
|
||||
|
||||
|
||||
def render_readme(bundle: dict[str, Any]) -> str:
|
||||
lines = [
|
||||
"# 1C Patch Review Bundle",
|
||||
"",
|
||||
f"Workspace: `{bundle.get('workspace')}`",
|
||||
f"Created UTC: `{bundle.get('created_at_utc')}`",
|
||||
f"Preflight status: `{(bundle.get('preflight') or {}).get('status')}`",
|
||||
f"Preferred extension: `{bundle.get('preferred_extension')}`",
|
||||
"",
|
||||
"## Rules",
|
||||
"",
|
||||
"- This bundle is for review and disposable-base validation.",
|
||||
"- It does not apply changes to SQL, Config, ConfigSave, ConfigCAS, or source extension files.",
|
||||
"- Validate loading/packaging in a disposable 1C base before any production action.",
|
||||
"",
|
||||
"## Modified Files",
|
||||
"",
|
||||
]
|
||||
for item in bundle.get("files") or []:
|
||||
diff = item.get("diff") or {}
|
||||
lines.append(
|
||||
f"- `{item.get('relative_path')}` ({item.get('kind')}, +{diff.get('added_lines')}/-{diff.get('removed_lines')}, hunks={diff.get('hunks')})"
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Contents",
|
||||
"",
|
||||
"- `files/`: modified working files by extension-relative path.",
|
||||
"- `manifest.json`: machine-readable bundle manifest.",
|
||||
"- `preflight.json`: full preflight evidence.",
|
||||
"- `preflight.md`: human-readable preflight summary.",
|
||||
"- `diff.json`: workspace diff evidence.",
|
||||
]
|
||||
)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def create_bundle(workspace: Path, output_root: Path, slug: str | None, *, force: bool, max_patch_chars: int) -> dict[str, Any]:
|
||||
preflight = build_preflight(workspace, max_patch_chars=max_patch_chars)
|
||||
if preflight.get("status") != "ready_for_review":
|
||||
raise SystemExit(f"Workspace is not ready for review: {preflight.get('status')}")
|
||||
diff = build_diff(workspace, max_patch_chars=max_patch_chars)
|
||||
records = modified_records(workspace, diff)
|
||||
if not records:
|
||||
raise SystemExit("No modified files to bundle.")
|
||||
|
||||
manifest = load_json(workspace / "manifest.json")
|
||||
bundle_slug = slug or slug_from_workspace(workspace)
|
||||
bundle_dir = output_root / bundle_slug
|
||||
if bundle_dir.exists() and not force:
|
||||
raise SystemExit(f"Bundle already exists: {bundle_dir}. Use --force to replace.")
|
||||
if bundle_dir.exists():
|
||||
shutil.rmtree(bundle_dir)
|
||||
files_root = bundle_dir / "files"
|
||||
files_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
copied = []
|
||||
for record in records:
|
||||
rel = safe_relative(str(record.get("relative_path") or ""))
|
||||
src = workspace / "working" / rel
|
||||
dst = files_root / rel
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
copied.append({**record, "bundle_path": str(Path("files") / rel)})
|
||||
|
||||
bundle = {
|
||||
"schema": "onec_patch_bundle.v1",
|
||||
"created_at_utc": datetime.now(timezone.utc).isoformat(),
|
||||
"workspace": str(workspace),
|
||||
"source_manifest": str(workspace / "manifest.json"),
|
||||
"preferred_extension": manifest.get("preferred_extension"),
|
||||
"extension_root": manifest.get("extension_root"),
|
||||
"task": manifest.get("task"),
|
||||
"preflight": {
|
||||
"schema": preflight.get("schema"),
|
||||
"status": preflight.get("status"),
|
||||
"passed": preflight.get("passed"),
|
||||
"diff_summary": preflight.get("diff_summary"),
|
||||
"gates": preflight.get("gates"),
|
||||
},
|
||||
"files": copied,
|
||||
"counts": {
|
||||
"files": len(copied),
|
||||
"added_lines": sum(((item.get("diff") or {}).get("added_lines") or 0) for item in copied),
|
||||
"removed_lines": sum(((item.get("diff") or {}).get("removed_lines") or 0) for item in copied),
|
||||
"hunks": sum(((item.get("diff") or {}).get("hunks") or 0) for item in copied),
|
||||
},
|
||||
}
|
||||
write_json(bundle_dir / "manifest.json", bundle)
|
||||
write_json(bundle_dir / "preflight.json", preflight)
|
||||
(bundle_dir / "preflight.md").write_text(render_preflight_markdown(preflight), encoding="utf-8")
|
||||
write_json(bundle_dir / "diff.json", diff)
|
||||
(bundle_dir / "README.md").write_text(render_readme(bundle), encoding="utf-8")
|
||||
|
||||
zip_path = bundle_dir.with_suffix(".zip")
|
||||
if zip_path.exists():
|
||||
zip_path.unlink()
|
||||
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
||||
for path in sorted(bundle_dir.rglob("*")):
|
||||
if path.is_file():
|
||||
archive.write(path, path.relative_to(bundle_dir))
|
||||
bundle_check = check_bundle(bundle_dir, zip_path)
|
||||
if not bundle_check.get("passed"):
|
||||
raise SystemExit(f"Created bundle failed validation: {bundle_check.get('counts')}")
|
||||
|
||||
return {
|
||||
"schema": "onec_patch_bundle_creation.v1",
|
||||
"bundle_dir": str(bundle_dir),
|
||||
"zip_path": str(zip_path),
|
||||
"manifest": str(bundle_dir / "manifest.json"),
|
||||
"preflight_status": preflight.get("status"),
|
||||
"bundle_check": {
|
||||
"schema": bundle_check.get("schema"),
|
||||
"passed": bundle_check.get("passed"),
|
||||
"counts": bundle_check.get("counts"),
|
||||
},
|
||||
"counts": bundle["counts"],
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Create a review bundle from a ready 1C patch workspace.")
|
||||
parser.add_argument("--workspace", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, default=Path("reports/1c-sql/upo/patch-bundles"))
|
||||
parser.add_argument("--slug")
|
||||
parser.add_argument("--force", action="store_true")
|
||||
parser.add_argument("--max-patch-chars", type=int, default=200000)
|
||||
parser.add_argument("--output", type=Path)
|
||||
args = parser.parse_args()
|
||||
|
||||
result = create_bundle(args.workspace, args.output_root, args.slug, force=args.force, max_patch_chars=args.max_patch_chars)
|
||||
if args.output:
|
||||
write_json(args.output, result)
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user