#!/usr/bin/env python3 """Resolve a 1C metadata object by configurator-visible kind/name.""" from __future__ import annotations import argparse import base64 import json import re from pathlib import Path from typing import Any KIND_ALIASES = { "документ": "Document", "documents": "Document", "document": "Document", "справочник": "Catalog", "catalogs": "Catalog", "catalog": "Catalog", "перечисление": "Enum", "enums": "Enum", "enum": "Enum", "регистрсведений": "InformationRegister", "регистр сведений": "InformationRegister", "informationregister": "InformationRegister", "informationregisters": "InformationRegister", "регистрнакопления": "AccumulationRegister", "регистр накопления": "AccumulationRegister", "accumulationregister": "AccumulationRegister", "accumulationregisters": "AccumulationRegister", "регистрбухгалтерии": "AccountingRegister", "регистр бухгалтерии": "AccountingRegister", "accountingregister": "AccountingRegister", "accountingregisters": "AccountingRegister", "отчет": "Report", "reports": "Report", "report": "Report", "обработка": "DataProcessor", "dataprocessor": "DataProcessor", "dataprocessors": "DataProcessor", "общиймодуль": "CommonModule", "общий модуль": "CommonModule", "commonmodule": "CommonModule", "commonmodules": "CommonModule", "форма": "Form", "forms": "Form", "form": "Form", "макет": "Template", "templates": "Template", "template": "Template", "команда": "Command", "commands": "Command", "command": "Command", } GENERATED_TYPE_KIND = { "DocumentObject": "Document", "DocumentRef": "Document", "DocumentManager": "Document", "DocumentSelection": "Document", "DocumentList": "Document", "CatalogObject": "Catalog", "CatalogRef": "Catalog", "CatalogManager": "Catalog", "CatalogSelection": "Catalog", "CatalogList": "Catalog", "EnumRef": "Enum", "EnumManager": "Enum", "InformationRegisterRecordSet": "InformationRegister", "InformationRegisterManager": "InformationRegister", "AccumulationRegisterRecordSet": "AccumulationRegister", "AccumulationRegisterManager": "AccumulationRegister", "AccountingRegisterRecordSet": "AccountingRegister", "AccountingRegisterManager": "AccountingRegister", "ReportManager": "Report", "DataProcessorManager": "DataProcessor", } TABLE_PREFIX_BY_ROLE = { "Document": "_Document", "DocumentChngR": "_DocumentChngR", "Catalog": "_Reference", "Reference": "_Reference", "Enum": "_Enum", "InfoRg": "_InfoRg", "InfoRgChngR": "_InfoRgChngR", "AccumRg": "_AccumRg", "AccumRgT": "_AccumRgT", "AccumRgOpt": "_AccumRgOpt", "AccRg": "_AccRg", "AccRgAT": "_AccRgAT", "AccRgCT": "_AccRgCT", "AccRgOpt": "_AccRgOpt", "BPr": "_BPr", "Task": "_Task", } def load_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8-sig")) def decode_arg(value: str | None, encoded: str | None) -> str | None: if encoded: return base64.b64decode(encoded).decode("utf-8") return value def normalize(value: str | None) -> str: return re.sub(r"[\s._-]+", "", str(value or "")).casefold() def canonical_kind(value: str | None) -> str | None: if not value: return None stripped = value.strip() return KIND_ALIASES.get(normalize(stripped), stripped) def parse_query(kind: str | None, name: str) -> tuple[str | None, str]: kind = canonical_kind(kind) query = name.strip() if "." not in query: return kind, query left, right = query.split(".", 1) if left.startswith("cfg:"): left = left[4:] generated_kind = GENERATED_TYPE_KIND.get(left) if generated_kind: return kind or generated_kind, right parsed_kind = canonical_kind(left) return kind or parsed_kind, right def physical_name(route: dict[str, Any]) -> str | None: role = str(route.get("storage_role") or "") number = route.get("sql_number") if number is None: return None prefix = TABLE_PREFIX_BY_ROLE.get(role) if prefix: return f"{prefix}{number}" if role == "VT": return None if role == "Fld": return f"_Fld{number}" if role == "LineNo": return f"_LineNo{number}" return None def is_extension_path(relative_path: str | None, path: str | None = None) -> bool: relative_parts = str(relative_path or "").replace("/", "\\").split("\\") if relative_parts and relative_parts[0].casefold() in {"расширения", "extensions"}: return True full_parts = [part.casefold() for part in str(path or "").replace("/", "\\").split("\\")] return "расширения" in full_parts or "extensions" in full_parts def top_objects(item: dict[str, Any]) -> list[dict[str, Any]]: return item.get("xml_top_objects") or [] def match_top(top: dict[str, Any], *, kind: str | None, wanted: str) -> tuple[float, str] | None: top_kind = canonical_kind(str(top.get("xml_kind") or "")) if kind and top_kind != kind: return None wanted_norm = normalize(wanted) name = str(top.get("name") or "") synonym = str(top.get("synonym") or "") relative_path = str(top.get("relative_path") or "") if normalize(name) == wanted_norm: return (1.0, "xml_top_object.name") if normalize(synonym) == wanted_norm: return (0.95, "xml_top_object.synonym") if wanted_norm and wanted_norm in normalize(name): return (0.82, "xml_top_object.name_contains") if wanted_norm and wanted_norm in normalize(synonym): return (0.78, "xml_top_object.synonym_contains") if wanted_norm and wanted_norm in normalize(relative_path): return (0.65, "xml_top_object.relative_path") return None def enrich_dbnames(routes: list[dict[str, Any]]) -> list[dict[str, Any]]: result = [] for route in routes: copy = dict(route) name = physical_name(copy) if name: copy["physical_name"] = name result.append(copy) return result def compact_match(guid: str, item: dict[str, Any], top: dict[str, Any], *, score: float, match_by: str) -> dict[str, Any]: return { "score": score, "match_by": match_by, "guid": guid, "kind": canonical_kind(str(top.get("xml_kind") or "")), "name": top.get("name"), "synonym": top.get("synonym"), "source": "extension" if is_extension_path(top.get("relative_path"), top.get("path")) else "base", "relative_path": top.get("relative_path"), "path": top.get("path"), "route_kind": item.get("route_kind") or [], "storage": { "dbnames": enrich_dbnames(item.get("dbnames") or []), "config_routes": item.get("config_routes") or [], "extension_routes": item.get("extension_routes") or [], }, "xml_occurrence_count": item.get("xml_occurrence_count"), } def find_matches(index: dict[str, Any], *, kind: str | None, name: str, limit: int) -> list[dict[str, Any]]: wanted_kind, wanted_name = parse_query(kind, name) matches = [] for guid, item in (index.get("objects") or {}).items(): for top in top_objects(item): match = match_top(top, kind=wanted_kind, wanted=wanted_name) if not match: continue score, match_by = match matches.append(compact_match(guid, item, top, score=score, match_by=match_by)) matches.sort( key=lambda row: ( -row["score"], row["source"] != "base", row.get("relative_path") or "", row.get("guid") or "", ) ) return matches[:limit] def resolve_object(index: dict[str, Any], *, kind: str | None, name: str, limit: int = 20) -> dict[str, Any]: wanted_kind, wanted_name = parse_query(kind, name) matches = find_matches(index, kind=wanted_kind, name=wanted_name, limit=limit) exact_base = [row for row in matches if row["score"] >= 0.95 and row["source"] == "base"] canonical = exact_base[0] if exact_base else (matches[0] if matches else None) extension_overlays = [ row for row in matches if canonical and row.get("source") == "extension" and normalize(row.get("name")) == normalize(canonical.get("name")) and row.get("kind") == canonical.get("kind") ] return { "schema": "onec_object_resolution.v1", "query": {"kind": wanted_kind, "name": wanted_name, "raw_kind": kind, "raw_name": name}, "canonical": canonical, "extension_overlays": extension_overlays, "matches": matches, "summary": { "match_count": len(matches), "has_canonical": canonical is not None, "extension_overlay_count": len(extension_overlays), }, } def main() -> int: parser = argparse.ArgumentParser(description="Resolve 1C metadata object by visible kind/name.") parser.add_argument("--index", type=Path, required=True) parser.add_argument("--kind") parser.add_argument("--name") parser.add_argument("--kind-b64") parser.add_argument("--name-b64") parser.add_argument("--limit", type=int, default=20) parser.add_argument("--output", type=Path) args = parser.parse_args() kind = decode_arg(args.kind, args.kind_b64) name = decode_arg(args.name, args.name_b64) if not name: raise SystemExit("Use --name or --name-b64.") result = resolve_object(load_json(args.index), kind=kind, name=name, limit=args.limit) text = 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(text, encoding="utf-8") print(json.dumps({"output": str(args.output), "summary": result["summary"]}, ensure_ascii=False)) else: print(text) return 0 if __name__ == "__main__": raise SystemExit(main())