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
+316
View File
@@ -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())