Files
llm/scripts/create_1c_patch_workspace.py
T

317 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Create a safe local patch workspace from a passed 1C change proposal."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import shutil
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from check_1c_change_proposal_safety import check, load_json
def slugify(value: str) -> str:
translit = {
"а": "a", "б": "b", "в": "v", "г": "g", "д": "d", "е": "e", "ё": "e", "ж": "zh", "з": "z",
"и": "i", "й": "y", "к": "k", "л": "l", "м": "m", "н": "n", "о": "o", "п": "p", "р": "r",
"с": "s", "т": "t", "у": "u", "ф": "f", "х": "h", "ц": "c", "ч": "ch", "ш": "sh", "щ": "sch",
"ъ": "", "ы": "y", "ь": "", "э": "e", "ю": "yu", "я": "ya",
}
chars = []
for char in value.casefold():
chars.append(translit.get(char, char))
slug = re.sub(r"[^a-z0-9]+", "-", "".join(chars)).strip("-")
return slug[:80] or "onec-task"
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def unique_targets(proposal: dict[str, Any]) -> list[dict[str, Any]]:
targets = []
seen = set()
for proposal_item in proposal.get("proposals") or []:
for target in ((proposal_item.get("target_policy") or {}).get("write_candidates") or []):
path = str(target.get("path") or target.get("module_path") or "")
if not path or path in seen:
continue
seen.add(path)
targets.append(target)
return targets
def common_extension_root(paths: list[Path], preferred_extension: str | None) -> Path | None:
if not preferred_extension:
return None
for path in paths:
parts = list(path.parts)
lowered = [part.casefold() for part in parts]
if preferred_extension.casefold() not in lowered:
continue
index = lowered.index(preferred_extension.casefold())
return Path(*parts[: index + 1])
return None
def relative_target_path(path: Path, root: Path | None) -> Path:
if root:
try:
return path.relative_to(root)
except ValueError:
pass
return Path(path.name)
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 copy_targets(targets: list[dict[str, Any]], workspace: Path, root: Path | None) -> list[dict[str, Any]]:
records = []
for target in targets:
src = Path(str(target.get("path") or target.get("module_path")))
rel = relative_target_path(src, root)
original_dst = workspace / "original" / rel
working_dst = workspace / "working" / rel
original_dst.parent.mkdir(parents=True, exist_ok=True)
working_dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, original_dst)
shutil.copy2(src, working_dst)
records.append(
{
"source_path": str(src),
"relative_path": str(rel).replace("\\", "/"),
"original_path": str(original_dst),
"working_path": str(working_dst),
"kind": target.get("kind"),
"name": target.get("name"),
"origin": target.get("origin"),
"sha256": sha256_file(src),
"size": src.stat().st_size,
}
)
return records
def render_readme(proposal: dict[str, Any], safety: dict[str, Any], manifest: dict[str, Any]) -> str:
task = ((proposal.get("task") or {}).get("text") or "").strip()
lines = [
"# 1C Patch Workspace",
"",
f"Task: {task}",
f"Safety passed: `{safety.get('passed')}`",
"",
"## Rules",
"",
"- Edit files only under `working/`.",
"- Keep `original/` unchanged; it is used for diff generation.",
"- Do not write SQL, Config, ConfigSave, ConfigCAS, or production Designer state.",
"- Validate extension packaging/loading in a disposable base before any production action.",
"",
"## Files",
"",
]
for item in manifest.get("files") or []:
lines.append(f"- `{item.get('relative_path')}` ({item.get('kind')}, {item.get('origin')})")
lines.extend(
[
"",
"## Next Commands",
"",
"Check workspace integrity:",
"",
"```powershell",
"python scripts/check_1c_patch_workspace_integrity.py --workspace <this-workspace> --output <integrity-json>",
"```",
"",
"Check source freshness before packaging/apply:",
"",
"```powershell",
"python scripts/check_1c_patch_source_freshness.py --workspace <this-workspace> --output <freshness-json>",
"```",
"",
"Validate BSL/Form.xml semantics:",
"",
"```powershell",
"python scripts/validate_1c_patch_workspace_semantics.py --workspace <this-workspace> --output <semantic-validation-json>",
"```",
"",
"Append, replace, or upsert one BSL routine under `working/`:",
"",
"```powershell",
"python scripts/edit_1c_bsl_routine.py --workspace <this-workspace> --relative-path <manifest-bsl-module-relative-path> --operation upsert --routine-text-b64 <utf8-base64-bsl-routine> --output <edit-json>",
"```",
"",
"Append, replace, or upsert one Form.xml command under `working/`:",
"",
"```powershell",
"python scripts/edit_1c_form_command.py --workspace <this-workspace> --relative-path <manifest-form-xml-relative-path> --operation upsert --name <command-name> --title <russian-title> --action <bsl-handler-name> --output <edit-json>",
"```",
"",
"Append, replace, or upsert one visible Form.xml button under `working/`:",
"",
"```powershell",
"python scripts/edit_1c_form_button.py --workspace <this-workspace> --relative-path <manifest-form-xml-relative-path> --operation upsert --parent-name <parent-form-item-name> --name <button-name> --title <russian-title> --command-name <existing-command-name> --output <edit-json>",
"```",
"",
"Preferred atomic workflow for adding a visible form button:",
"",
"```powershell",
"python scripts/add_1c_form_button_workflow.py --workspace <this-workspace> --form-relative-path <manifest-form-xml-relative-path> --bsl-relative-path <manifest-form-module-relative-path> --operation upsert --routine-text-b64 <utf8-base64-bsl-routine> --command-name <command-name> --command-title <russian-command-title> --command-action <bsl-handler-name> --button-parent-name <parent-form-item-name> --button-name <button-name> --button-title <russian-button-title> --output <workflow-json>",
"```",
"",
"Generate diff after editing:",
"",
"```powershell",
"python scripts/diff_1c_patch_workspace.py --workspace <this-workspace> --output <diff-json>",
"```",
"",
"Create review bundle after preflight status is `ready_for_review`:",
"",
"```powershell",
"python scripts/create_1c_patch_bundle.py --workspace <this-workspace> --slug <bundle-slug> --output <bundle-json>",
"```",
"",
"Validate a created review bundle:",
"",
"```powershell",
"python scripts/check_1c_patch_bundle.py --bundle-dir <bundle-dir> --output <bundle-check-json>",
"```",
"",
"Create disposable extension XML staging copy from a valid bundle:",
"",
"```powershell",
"python scripts/create_1c_extension_staging_from_bundle.py --bundle-dir <bundle-dir> --slug <staging-slug> --output <staging-json>",
"```",
"",
"Validate a disposable extension XML staging copy:",
"",
"```powershell",
"python scripts/check_1c_extension_staging.py --staging-dir <staging-dir> --output <staging-check-json>",
"```",
"",
"Validate runner config for disposable 1C validation:",
"",
"```powershell",
"python scripts/check_1c_extension_runner_config.py --config <runner-config-json> --output <runner-config-check-json>",
"```",
"",
"Create a disposable-base validation plan for staging:",
"",
"```powershell",
"python scripts/create_1c_extension_validation_plan.py --staging-dir <staging-dir> --runner-config <runner-config-json> --output <validation-plan-json> --markdown-output <validation-plan-md>",
"```",
"",
"Create pending manual evidence templates from validation plan:",
"",
"```powershell",
"python scripts/create_1c_extension_validation_evidence.py --plan <validation-plan-json> --output <validation-evidence-manifest-json>",
"```",
"",
"Check filled validation evidence:",
"",
"```powershell",
"python scripts/check_1c_extension_validation_evidence.py --plan <validation-plan-json> --output <validation-evidence-check-json>",
"```",
"",
"Aggregate final validation gates for human review:",
"",
"```powershell",
"python scripts/check_1c_extension_validation_release.py --plan <validation-plan-json> --output <validation-release-check-json>",
"```",
"",
"Render final validation report for human review:",
"",
"```powershell",
"python scripts/render_1c_extension_validation_release_markdown.py --release-check <validation-release-check-json> --output <validation-release-md>",
"```",
"",
]
)
return "\n".join(lines)
def create_workspace(proposal_path: Path, output_root: Path, slug: str | None, force: bool) -> dict[str, Any]:
proposal = load_json(proposal_path)
safety = check(proposal)
if not safety.get("passed"):
raise SystemExit("Proposal safety check failed; workspace not created.")
task_text = ((proposal.get("task") or {}).get("text") or "").strip()
workspace_slug = slug or slugify(task_text)
workspace = output_root / workspace_slug
if workspace.exists() and not force:
raise SystemExit(f"Workspace already exists: {workspace}. Use --force to replace.")
if workspace.exists() and force:
shutil.rmtree(workspace)
workspace.mkdir(parents=True, exist_ok=True)
preferred_extension = None
for proposal_item in proposal.get("proposals") or []:
preferred_extension = (proposal_item.get("write_strategy") or {}).get("preferred_extension")
if preferred_extension:
break
targets = unique_targets(proposal)
source_paths = [Path(str(target.get("path") or target.get("module_path"))) for target in targets]
extension_root = common_extension_root(source_paths, preferred_extension)
files = copy_targets(targets, workspace, extension_root)
manifest = {
"schema": "onec_patch_workspace_manifest.v1",
"created_at_utc": datetime.now(timezone.utc).isoformat(),
"task": proposal.get("task"),
"source_proposal": str(proposal_path),
"preferred_extension": preferred_extension,
"extension_root": str(extension_root) if extension_root else None,
"workspace": str(workspace),
"files": files,
"safety": {
"schema": safety.get("schema"),
"passed": safety.get("passed"),
"counts": safety.get("counts"),
},
}
write_json(workspace / "proposal.json", proposal)
write_json(workspace / "safety.json", safety)
write_json(workspace / "manifest.json", manifest)
(workspace / "README.md").write_text(render_readme(proposal, safety, manifest), encoding="utf-8")
return {
"schema": "onec_patch_workspace_creation.v1",
"workspace": str(workspace),
"manifest": str(workspace / "manifest.json"),
"files": len(files),
"safety_passed": safety.get("passed"),
}
def main() -> int:
parser = argparse.ArgumentParser(description="Create safe 1C patch workspace.")
parser.add_argument("--proposal", type=Path, required=True)
parser.add_argument("--output-root", type=Path, default=Path("reports/1c-sql/upo/patch-workspaces"))
parser.add_argument("--slug")
parser.add_argument("--force", action="store_true")
parser.add_argument("--output", type=Path)
args = parser.parse_args()
result = create_workspace(args.proposal, args.output_root, args.slug, args.force)
output = json.dumps(result, ensure_ascii=False, indent=2) + "\n"
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(output, encoding="utf-8")
print(json.dumps(result, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())