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
@@ -0,0 +1,265 @@
#!/usr/bin/env python3
"""Compare two 1C saved-state object reports in 1C object terms."""
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
import sys
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parents[1]
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 object_map(report: dict[str, Any]) -> dict[str, dict[str, Any]]:
items = ((report.get("agent_summary") or {}).get("object_changes") or [])
result: dict[str, dict[str, Any]] = {}
for item in items:
if not isinstance(item, dict):
continue
full_name = str(item.get("full_name") or "")
if full_name:
result[full_name] = item
return result
def system_map(report: dict[str, Any]) -> dict[str, dict[str, Any]]:
items = ((report.get("agent_summary") or {}).get("system_changes") or [])
result: dict[str, dict[str, Any]] = {}
for item in items:
if not isinstance(item, dict):
continue
key = f"{item.get('layer')}::{item.get('extension')}::{item.get('name')}"
result[key] = item
return result
def stable_fingerprint(value: Any) -> str:
encoded = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
def comparable_object(item: dict[str, Any]) -> dict[str, Any]:
return {
"full_name": item.get("full_name"),
"layer": item.get("layer"),
"extension": item.get("extension"),
"kind": item.get("kind"),
"kind_ru": item.get("kind_ru"),
"name": item.get("name"),
"synonym": item.get("synonym"),
"parts_count": item.get("parts_count"),
"text_diff_parts": item.get("text_diff_parts"),
"active_missing_parts": item.get("active_missing_parts"),
"added_terms": item.get("added_terms") or [],
"removed_terms": item.get("removed_terms") or [],
"parts": [
{
"file_name": part.get("file_name"),
"payload_role": part.get("payload_role"),
"active_exists": part.get("active_exists"),
"summary": part.get("summary"),
"delta_chars": part.get("delta_chars"),
}
for part in item.get("parts") or []
if isinstance(part, dict)
],
}
def terms_delta(before: list[Any], after: list[Any]) -> dict[str, list[Any]]:
before_set = {str(value) for value in before}
after_set = {str(value) for value in after}
return {
"added": [value for value in after if str(value) not in before_set],
"removed": [value for value in before if str(value) not in after_set],
}
def parts_delta(before: list[dict[str, Any]], after: list[dict[str, Any]]) -> dict[str, Any]:
before_by_name = {str(part.get("file_name")): part for part in before if part.get("file_name")}
after_by_name = {str(part.get("file_name")): part for part in after if part.get("file_name")}
before_names = set(before_by_name)
after_names = set(after_by_name)
changed = []
for name in sorted(before_names & after_names):
before_part = before_by_name[name]
after_part = after_by_name[name]
if stable_fingerprint(before_part) != stable_fingerprint(after_part):
changed.append({
"file_name": name,
"before": before_part,
"after": after_part,
})
return {
"added": [after_by_name[name] for name in sorted(after_names - before_names)],
"removed": [before_by_name[name] for name in sorted(before_names - after_names)],
"changed": changed,
}
def object_delta(before: dict[str, Any], after: dict[str, Any]) -> dict[str, Any]:
before_cmp = comparable_object(before)
after_cmp = comparable_object(after)
return {
"full_name": after.get("full_name") or before.get("full_name"),
"before_fingerprint": stable_fingerprint(before_cmp),
"after_fingerprint": stable_fingerprint(after_cmp),
"before": before_cmp,
"after": after_cmp,
"term_delta": {
"added_terms": terms_delta(before_cmp.get("added_terms") or [], after_cmp.get("added_terms") or []),
"removed_terms": terms_delta(before_cmp.get("removed_terms") or [], after_cmp.get("removed_terms") or []),
},
"part_delta": parts_delta(before_cmp.get("parts") or [], after_cmp.get("parts") or []),
}
def compact_object(item: dict[str, Any]) -> dict[str, Any]:
comparable = comparable_object(item)
comparable["fingerprint"] = stable_fingerprint(comparable)
return comparable
def compare_reports(before_path: Path, after_path: Path) -> dict[str, Any]:
before_path = before_path.resolve()
after_path = after_path.resolve()
before = load_json(before_path)
after = load_json(after_path)
before_objects = object_map(before)
after_objects = object_map(after)
before_names = set(before_objects)
after_names = set(after_objects)
changed = []
unchanged = []
for name in sorted(before_names & after_names):
before_fp = stable_fingerprint(comparable_object(before_objects[name]))
after_fp = stable_fingerprint(comparable_object(after_objects[name]))
if before_fp == after_fp:
unchanged.append(name)
else:
changed.append(object_delta(before_objects[name], after_objects[name]))
before_system = system_map(before)
after_system = system_map(after)
system_added = sorted(set(after_system) - set(before_system))
system_removed = sorted(set(before_system) - set(after_system))
system_changed = [
key
for key in sorted(set(before_system) & set(after_system))
if stable_fingerprint(before_system[key]) != stable_fingerprint(after_system[key])
]
return {
"schema": "onec_saved_state_object_report_delta.v1",
"before_report": str(before_path),
"after_report": str(after_path),
"database": after.get("database") or before.get("database"),
"objects": {
"added": [compact_object(after_objects[name]) for name in sorted(after_names - before_names)],
"removed": [compact_object(before_objects[name]) for name in sorted(before_names - after_names)],
"changed": changed,
"unchanged": unchanged,
},
"system_changes": {
"added": [after_system[key] for key in system_added],
"removed": [before_system[key] for key in system_removed],
"changed": [{"key": key, "before": before_system[key], "after": after_system[key]} for key in system_changed],
},
"counts": {
"objects_added": len(after_names - before_names),
"objects_removed": len(before_names - after_names),
"objects_changed": len(changed),
"objects_unchanged": len(unchanged),
"system_added": len(system_added),
"system_removed": len(system_removed),
"system_changed": len(system_changed),
},
"safety": {
"read_only": True,
"sql_write_performed": False,
"public_terms_are_1c_objects": True,
},
}
def render_markdown(delta: dict[str, Any]) -> str:
module_path = REPO_ROOT / "scripts" / "render_1c_saved_state_object_report_delta_markdown.py"
spec = importlib.util.spec_from_file_location("saved_state_delta_markdown", module_path)
if spec is None or spec.loader is None:
raise RuntimeError(f"Cannot load Markdown renderer: {module_path}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module.render(delta)
def check_delta(delta_path: Path, check_output: Path) -> None:
module_path = REPO_ROOT / "scripts" / "check_1c_saved_state_object_report_delta.py"
spec = importlib.util.spec_from_file_location("saved_state_delta_check", module_path)
if spec is None or spec.loader is None:
raise RuntimeError(f"Cannot load delta checker: {module_path}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
result = module.check_delta(delta_path)
module.write_json(check_output, result)
if not result.get("passed"):
raise RuntimeError(f"Saved-state delta check failed: {check_output}")
def main() -> int:
parser = argparse.ArgumentParser(description="Compare two 1C saved-state object reports.")
parser.add_argument("--before", type=Path, required=True)
parser.add_argument("--after", type=Path, required=True)
parser.add_argument("--output", type=Path)
parser.add_argument("--markdown-output", type=Path)
parser.add_argument("--skip-markdown", action="store_true")
parser.add_argument("--check-output", type=Path)
parser.add_argument("--skip-check", action="store_true")
args = parser.parse_args()
result = compare_reports(args.before, args.after)
markdown_output = args.markdown_output
if markdown_output is None and args.output and not args.skip_markdown:
markdown_output = args.output.with_suffix(".md")
if markdown_output and not args.skip_markdown:
markdown_output = markdown_output.resolve()
result["markdown"] = str(markdown_output)
check_output = args.check_output
if check_output is None and args.output and not args.skip_check:
check_output = args.output.with_name(f"{args.output.stem}-check.json")
if check_output and not args.skip_check:
check_output = check_output.resolve()
result["check"] = str(check_output)
if args.output:
write_json(args.output, result)
if markdown_output and not args.skip_markdown:
markdown_output.parent.mkdir(parents=True, exist_ok=True)
markdown_output.write_text(render_markdown(result), encoding="utf-8")
if args.output:
write_json(args.output, result)
if check_output and not args.skip_check:
if not args.output:
raise SystemExit("Use --output when delta check output is enabled.")
check_delta(args.output, check_output)
print(json.dumps({"output": str(args.output) if args.output else None, "schema": result["schema"], "counts": result["counts"]}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())