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
+246
View File
@@ -0,0 +1,246 @@
#!/usr/bin/env python3
"""Safely append or replace one BSL routine inside a 1C patch workspace."""
from __future__ import annotations
import argparse
import base64
import json
import re
from pathlib import Path
from typing import Any
from validate_1c_patch_workspace_semantics import validate_workspace
WORD = r"А-Яа-яA-Za-z0-9_"
ROUTINE_RE = re.compile(rf"(?<![{WORD}])(Процедура|Функция)\s+([А-Яа-яA-Za-z_][{WORD}]*)", re.IGNORECASE)
END_RE = {
"процедура": re.compile(rf"(?<![{WORD}])КонецПроцедуры(?![{WORD}])", re.IGNORECASE),
"функция": re.compile(rf"(?<![{WORD}])КонецФункции(?![{WORD}])", re.IGNORECASE),
}
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 read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8-sig")
except UnicodeDecodeError:
return path.read_text(encoding="cp1251", errors="replace")
def normalize(value: str | None) -> str:
return re.sub(r"[\s._-]+", "", str(value or "")).casefold()
def line_starts(text: str) -> list[int]:
starts = [0]
for match in re.finditer(r"\n", text):
starts.append(match.end())
return starts
def offset_to_line(starts: list[int], offset: int) -> int:
line = 1
for index, start in enumerate(starts, start=1):
if start > offset:
break
line = index
return line
def routine_blocks(text: str) -> list[dict[str, Any]]:
starts = line_starts(text)
blocks = []
matches = list(ROUTINE_RE.finditer(text))
for match in matches:
kind = match.group(1)
name = match.group(2)
end_match = END_RE[kind.casefold()].search(text, match.end())
end = end_match.end() if end_match else len(text)
blocks.append(
{
"kind": kind,
"name": name,
"normalized_name": normalize(name),
"start": match.start(),
"declaration_start": match.start(),
"end": end,
"line_start": offset_to_line(starts, match.start()),
"line_end": offset_to_line(starts, end),
}
)
return blocks
def directive_start(text: str, declaration_start: int) -> int:
prefix = text[:declaration_start]
lines = prefix.splitlines(keepends=True)
start_offset = len(prefix)
index = len(lines) - 1
while index >= 0:
line = lines[index]
stripped = line.strip()
if stripped.startswith("&"):
start_offset -= len(line)
index -= 1
continue
if stripped == "":
candidate = index - 1
while candidate >= 0 and lines[candidate].strip() == "":
candidate -= 1
if candidate >= 0 and lines[candidate].strip().startswith("&"):
start_offset -= len(line)
index -= 1
continue
break
return start_offset
def one_routine_from_text(routine_text: str) -> dict[str, Any]:
blocks = routine_blocks(routine_text)
if len(blocks) != 1:
raise SystemExit(f"Routine text must contain exactly one procedure/function, found {len(blocks)}.")
block = blocks[0]
if block["end"] < len(routine_text.rstrip()):
suffix = routine_text[block["end"] :].strip()
if suffix:
raise SystemExit("Routine text must not contain extra code after the routine end.")
return block
def decode_routine_text(args: argparse.Namespace) -> str:
sources = [bool(args.routine_text), bool(args.routine_text_b64), bool(args.routine_file)]
if sum(sources) != 1:
raise SystemExit("Use exactly one of --routine-text, --routine-text-b64, or --routine-file.")
if args.routine_text is not None:
return args.routine_text
if args.routine_text_b64:
return base64.b64decode(args.routine_text_b64).decode("utf-8")
return Path(args.routine_file).read_text(encoding="utf-8-sig")
def safe_relative(relative_path: str) -> str:
normalized = relative_path.replace("\\", "/")
path = Path(normalized)
if path.is_absolute() or ".." in path.parts or not normalized:
raise SystemExit(f"Unsafe relative path: {relative_path}")
return normalized
def manifest_record(workspace: Path, relative_path: str) -> dict[str, Any]:
manifest = load_json(workspace / "manifest.json")
wanted = safe_relative(relative_path)
for record in manifest.get("files") or []:
record_relative = str(record.get("relative_path") or "").replace("\\", "/")
if record_relative == wanted:
if record.get("kind") != "bsl_module":
raise SystemExit(f"Manifest target is not a BSL module: {relative_path}")
return record
raise SystemExit(f"BSL module is not present in workspace manifest: {relative_path}")
def ensure_inside_working(workspace: Path, relative_path: str) -> Path:
working_root = (workspace / "working").resolve()
path = (working_root / Path(safe_relative(relative_path))).resolve()
if not str(path).casefold().startswith(str(working_root).casefold()):
raise SystemExit(f"Refusing path outside working/: {path}")
if not path.exists():
raise SystemExit(f"Working BSL module does not exist: {path}")
return path
def apply_edit(path: Path, routine_text: str, *, operation: str) -> dict[str, Any]:
new_block = one_routine_from_text(routine_text)
text = read_text(path)
blocks = routine_blocks(text)
matches = [block for block in blocks if block["normalized_name"] == new_block["normalized_name"]]
if len(matches) > 1:
raise SystemExit(f"Target module already has duplicate routine: {new_block['name']}")
exists = bool(matches)
if operation == "append" and exists:
raise SystemExit(f"Routine already exists, append refused: {new_block['name']}")
if operation == "replace" and not exists:
raise SystemExit(f"Routine does not exist, replace refused: {new_block['name']}")
replacement = routine_text.strip() + "\n"
if exists:
old = matches[0]
start = directive_start(text, int(old["declaration_start"]))
end = int(old["end"])
while end < len(text) and text[end : end + 1] in {"\r", "\n"}:
end += 1
updated = text[:start].rstrip() + "\n\n" + replacement + text[end:].lstrip("\r\n")
status = "replaced"
span = {"old_line_start": old["line_start"], "old_line_end": old["line_end"]}
else:
separator = "\n\n" if text.strip() else ""
updated = text.rstrip() + separator + replacement
status = "appended"
span = {}
path.write_text(updated, encoding="utf-8")
return {
"status": status,
"routine": {"kind": new_block["kind"], "name": new_block["name"]},
"path": str(path),
**span,
}
def edit_workspace(workspace: Path, relative_path: str, routine_text: str, *, operation: str, keep_on_failure: bool) -> dict[str, Any]:
record = manifest_record(workspace, relative_path)
path = ensure_inside_working(workspace, str(record.get("relative_path") or relative_path))
before = read_text(path)
edit = apply_edit(path, routine_text, operation=operation)
semantic = validate_workspace(workspace)
rolled_back = False
if not semantic.get("passed") and not keep_on_failure:
path.write_text(before, encoding="utf-8")
rolled_back = True
return {
"schema": "onec_bsl_routine_edit.v1",
"workspace": str(workspace),
"relative_path": str(record.get("relative_path")),
"operation": operation,
"edit": edit,
"rolled_back": rolled_back,
"semantic_validation": {
"schema": semantic.get("schema"),
"passed": semantic.get("passed"),
"counts": semantic.get("counts"),
"findings": semantic.get("findings"),
},
"passed": bool(semantic.get("passed")) and not rolled_back,
}
def main() -> int:
parser = argparse.ArgumentParser(description="Append/replace one BSL routine in a 1C patch workspace working file.")
parser.add_argument("--workspace", type=Path, required=True)
parser.add_argument("--relative-path", required=True)
parser.add_argument("--operation", choices=["append", "replace", "upsert"], default="upsert")
parser.add_argument("--routine-text")
parser.add_argument("--routine-text-b64")
parser.add_argument("--routine-file", type=Path)
parser.add_argument("--keep-on-failure", action="store_true", help="Keep the edit even when semantic validation fails.")
parser.add_argument("--output", type=Path)
args = parser.parse_args()
result = edit_workspace(args.workspace, args.relative_path, decode_routine_text(args), operation=args.operation, keep_on_failure=args.keep_on_failure)
if args.output:
write_json(args.output, result)
print(json.dumps({"output": str(args.output) if args.output else None, "passed": result["passed"], "rolled_back": result["rolled_back"], "edit": result["edit"], "semantic": result["semantic_validation"]["counts"]}, ensure_ascii=False))
return 0 if result["passed"] else 2
if __name__ == "__main__":
raise SystemExit(main())