Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user