Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,534 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Resolve a 1C fact against an explicit current-configuration source.
|
||||
|
||||
This command is intentionally narrower than RAG: it verifies whether a named
|
||||
1C object member exists in the provided configuration evidence and returns the
|
||||
source/provenance. It does not search examples unless a snapshot path is passed
|
||||
explicitly by the caller.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from get_1c_object_brief_context import view_forms, view_modules # noqa: E402
|
||||
from get_1c_object_metadata import build_object_metadata # noqa: E402
|
||||
from resolve_1c_object import canonical_kind, load_json, normalize, parse_query, resolve_object # noqa: E402
|
||||
|
||||
|
||||
KIND_TO_RU = {
|
||||
"Catalog": "Справочник",
|
||||
"Document": "Документ",
|
||||
"Enum": "Перечисление",
|
||||
"InformationRegister": "РегистрСведений",
|
||||
"AccumulationRegister": "РегистрНакопления",
|
||||
"AccountingRegister": "РегистрБухгалтерии",
|
||||
"Report": "Отчет",
|
||||
"DataProcessor": "Обработка",
|
||||
"CommonModule": "ОбщийМодуль",
|
||||
"Form": "Форма",
|
||||
"Template": "Макет",
|
||||
"Command": "Команда",
|
||||
}
|
||||
|
||||
SNAPSHOT_KIND_ALIASES = {
|
||||
"Catalog": {"catalog", "справочник"},
|
||||
"Document": {"document", "документ"},
|
||||
"Enum": {"enum", "перечисление"},
|
||||
"InformationRegister": {"register", "information_register", "регистрсведений", "регистр сведений"},
|
||||
"AccumulationRegister": {"register", "accumulation_register", "регистрнакопления", "регистр накопления"},
|
||||
"Report": {"report", "отчет"},
|
||||
"DataProcessor": {"processing", "processor", "обработка"},
|
||||
"CommonModule": {"common_module", "общиймодуль", "общий модуль"},
|
||||
"Form": {"form", "форма"},
|
||||
}
|
||||
|
||||
MEMBER_AREAS = {
|
||||
"attribute": {"attribute", "attr", "реквизит", "реквизиты"},
|
||||
"tabular_section": {"tabular_section", "table", "табличнаячасть", "табличная часть", "тч"},
|
||||
"tabular_section_attribute": {"tabular_section_attribute", "table_attribute", "реквизиттч", "реквизит табличной части"},
|
||||
"form": {"form", "форма", "формы"},
|
||||
"command": {"command", "команда", "команды"},
|
||||
"module": {"module", "модуль", "модули"},
|
||||
"any": {"any", "любой", "все"},
|
||||
}
|
||||
|
||||
|
||||
def decode_arg(value: str | None, encoded: str | None) -> str | None:
|
||||
if encoded:
|
||||
return base64.b64decode(encoded).decode("utf-8")
|
||||
return value
|
||||
|
||||
|
||||
def compact_text(value: Any) -> str:
|
||||
return re.sub(r"[\s._-]+", "", str(value or "")).casefold()
|
||||
|
||||
|
||||
def member_area(value: str | None) -> str:
|
||||
if not value:
|
||||
return "any"
|
||||
wanted = compact_text(value)
|
||||
for area, aliases in MEMBER_AREAS.items():
|
||||
if wanted in {compact_text(alias) for alias in aliases}:
|
||||
return area
|
||||
return value
|
||||
|
||||
|
||||
def split_fact_path(raw: str) -> tuple[str | None, str, str | None, str | None]:
|
||||
"""Parse common 1C fact paths.
|
||||
|
||||
Supported examples:
|
||||
- Справочник.Номенклатура
|
||||
- Справочник.Номенклатура.Артикул
|
||||
- Справочник.Номенклатура.Цены.Цена
|
||||
"""
|
||||
|
||||
parts = [part.strip() for part in str(raw or "").split(".") if part.strip()]
|
||||
if len(parts) >= 2:
|
||||
kind = parts[0]
|
||||
object_name = parts[1]
|
||||
if len(parts) == 2:
|
||||
return kind, object_name, None, None
|
||||
if len(parts) == 3:
|
||||
return kind, object_name, None, parts[2]
|
||||
return kind, object_name, parts[2], parts[3]
|
||||
if len(parts) == 1:
|
||||
return None, parts[0], None, None
|
||||
raise ValueError("fact path is empty")
|
||||
|
||||
|
||||
def path_join(*parts: Any) -> str:
|
||||
return ".".join(str(part).strip() for part in parts if str(part or "").strip())
|
||||
|
||||
|
||||
def object_canonical_path(kind: Any, name: Any) -> str:
|
||||
normalized_kind = canonical_kind(str(kind or "")) or str(kind or "")
|
||||
kind_text = KIND_TO_RU.get(normalized_kind, normalized_kind)
|
||||
return path_join(kind_text, name)
|
||||
|
||||
|
||||
def member_canonical_path(object_path: str, *, table_section: Any = None, member: Any = None) -> str:
|
||||
return path_join(object_path, table_section, member)
|
||||
|
||||
|
||||
def same_name(left: Any, right: Any) -> bool:
|
||||
return normalize(str(left or "")) == normalize(str(right or ""))
|
||||
|
||||
|
||||
def matches_name(item: dict[str, Any], wanted: str) -> bool:
|
||||
return same_name(item.get("name"), wanted) or same_name(item.get("synonym"), wanted)
|
||||
|
||||
|
||||
def nearest(items: list[dict[str, Any]], wanted: str, limit: int = 8) -> list[dict[str, Any]]:
|
||||
wanted_norm = normalize(wanted)
|
||||
result = []
|
||||
for item in items:
|
||||
name = str(item.get("name") or "")
|
||||
synonym = str(item.get("synonym") or "")
|
||||
if wanted_norm and (wanted_norm in normalize(name) or wanted_norm in normalize(synonym) or normalize(name) in wanted_norm):
|
||||
result.append(compact_member(item))
|
||||
return result[:limit]
|
||||
|
||||
|
||||
def compact_member(item: dict[str, Any], *, area: str | None = None) -> dict[str, Any]:
|
||||
payload = {
|
||||
"area": area,
|
||||
"name": item.get("name"),
|
||||
"synonym": item.get("synonym"),
|
||||
"type": item.get("type"),
|
||||
"types": item.get("types"),
|
||||
"canonical_types": item.get("canonical_types"),
|
||||
"origin": item.get("origin"),
|
||||
"effective_action": item.get("effective_action"),
|
||||
"uuid": item.get("uuid"),
|
||||
}
|
||||
return {key: value for key, value in payload.items() if value not in (None, [], "")}
|
||||
|
||||
|
||||
def with_path_fields(
|
||||
member_payload: dict[str, Any],
|
||||
*,
|
||||
object_path: str,
|
||||
path_kind: str,
|
||||
table_section: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
payload = dict(member_payload)
|
||||
payload["canonical_path"] = member_canonical_path(
|
||||
object_path,
|
||||
table_section=table_section,
|
||||
member=payload.get("name"),
|
||||
)
|
||||
payload["path_kind"] = path_kind
|
||||
if table_section:
|
||||
payload["context_path"] = path_join(table_section, payload.get("name"))
|
||||
return payload
|
||||
|
||||
|
||||
def result_base(*, query: dict[str, Any], source: dict[str, Any], view: str, extension: str | None) -> dict[str, Any]:
|
||||
return {
|
||||
"schema": "onec_fact_resolution.v1",
|
||||
"query": query,
|
||||
"source": source,
|
||||
"view": view,
|
||||
"extension": extension if view == "extension" else None,
|
||||
}
|
||||
|
||||
|
||||
def resolve_from_route_index(
|
||||
index: dict[str, Any],
|
||||
*,
|
||||
index_path: Path,
|
||||
kind: str | None,
|
||||
object_name: str,
|
||||
member: str | None,
|
||||
area: str,
|
||||
table_section: str | None,
|
||||
view: str,
|
||||
extension: str | None,
|
||||
) -> dict[str, Any]:
|
||||
canonical_kind_value, parsed_name = parse_query(kind, object_name)
|
||||
query = {
|
||||
"kind": canonical_kind_value,
|
||||
"name": parsed_name,
|
||||
"member": member,
|
||||
"table_section": table_section,
|
||||
"area": area,
|
||||
}
|
||||
result = result_base(
|
||||
query=query,
|
||||
source={"kind": "route_index", "path": str(index_path)},
|
||||
view=view,
|
||||
extension=extension,
|
||||
)
|
||||
resolution = resolve_object(index, kind=canonical_kind_value, name=parsed_name, limit=50)
|
||||
canonical = resolution.get("canonical")
|
||||
if not canonical:
|
||||
result.update(
|
||||
{
|
||||
"exists": False,
|
||||
"confidence": "none",
|
||||
"reason": "object_not_found",
|
||||
"object": None,
|
||||
"nearest": resolution.get("matches") or [],
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
result["object"] = {
|
||||
"kind": canonical.get("kind"),
|
||||
"kind_ru": KIND_TO_RU.get(str(canonical.get("kind") or ""), canonical.get("kind")),
|
||||
"name": canonical.get("name"),
|
||||
"synonym": canonical.get("synonym"),
|
||||
"uuid": canonical.get("guid"),
|
||||
}
|
||||
object_path = object_canonical_path(result["object"].get("kind"), result["object"].get("name"))
|
||||
result["object"]["canonical_path"] = object_path
|
||||
result["object"]["path_kind"] = "metadata_object"
|
||||
result["active_extensions"] = sorted(
|
||||
{
|
||||
(overlay.get("origin") or {}).get("extension") or overlay.get("extension_name")
|
||||
for overlay in (resolution.get("extension_overlays") or [])
|
||||
if (overlay.get("origin") or {}).get("extension") or overlay.get("extension_name")
|
||||
}
|
||||
)
|
||||
if not member and not table_section:
|
||||
result.update({"exists": True, "confidence": "verified", "area": "object", "match": result["object"]})
|
||||
return result
|
||||
|
||||
metadata = build_object_metadata(index, kind=canonical_kind_value or "", name=parsed_name, view=view, extension=extension, include_storage=False)
|
||||
metadata_extensions = set(result.get("active_extensions") or [])
|
||||
for item in metadata.get("attributes") or []:
|
||||
origin = item.get("origin") or {}
|
||||
if origin.get("extension"):
|
||||
metadata_extensions.add(origin["extension"])
|
||||
for extension_name in origin.get("extensions") or []:
|
||||
if extension_name:
|
||||
metadata_extensions.add(extension_name)
|
||||
result["active_extensions"] = sorted(metadata_extensions)
|
||||
checks: list[tuple[str, list[dict[str, Any]]]] = []
|
||||
if area in {"any", "attribute"} and member:
|
||||
checks.append(("attribute", metadata.get("attributes") or []))
|
||||
if area in {"any", "tabular_section"} and not (area == "any" and table_section and member):
|
||||
wanted = table_section or member
|
||||
sections = metadata.get("tabular_sections") or []
|
||||
if wanted:
|
||||
match = next((item for item in sections if matches_name(item, wanted)), None)
|
||||
if match:
|
||||
result.update(
|
||||
{
|
||||
"exists": True,
|
||||
"confidence": "verified",
|
||||
"area": "tabular_section",
|
||||
"match": with_path_fields(
|
||||
compact_member(match, area="tabular_section"),
|
||||
object_path=object_path,
|
||||
path_kind="metadata_member",
|
||||
),
|
||||
}
|
||||
)
|
||||
return result
|
||||
checks.append(("tabular_section", sections))
|
||||
if area in {"any", "tabular_section_attribute"} and table_section and member:
|
||||
sections = metadata.get("tabular_sections") or []
|
||||
section = next((item for item in sections if matches_name(item, table_section)), None)
|
||||
attrs = section.get("attributes") if section else []
|
||||
if attrs:
|
||||
match = next((item for item in attrs if matches_name(item, member)), None)
|
||||
if match:
|
||||
result.update(
|
||||
{
|
||||
"exists": True,
|
||||
"confidence": "verified",
|
||||
"area": "tabular_section_attribute",
|
||||
"table_section": with_path_fields(
|
||||
compact_member(section, area="tabular_section"),
|
||||
object_path=object_path,
|
||||
path_kind="metadata_member",
|
||||
),
|
||||
"match": with_path_fields(
|
||||
compact_member(match, area="tabular_section_attribute"),
|
||||
object_path=object_path,
|
||||
path_kind="metadata_member",
|
||||
table_section=str(section.get("name") or table_section),
|
||||
),
|
||||
}
|
||||
)
|
||||
return result
|
||||
checks.append(("tabular_section_attribute", attrs))
|
||||
if area in {"any", "form"} and member:
|
||||
forms, _ = view_forms(canonical, resolution, view=view, extension=extension)
|
||||
checks.append(("form", forms))
|
||||
if area in {"any", "module"} and member:
|
||||
modules, _ = view_modules(canonical, resolution, view=view, extension=extension)
|
||||
checks.append(("module", modules))
|
||||
|
||||
for check_area, items in checks:
|
||||
if not member:
|
||||
continue
|
||||
match = next((item for item in items if matches_name(item, member)), None)
|
||||
if match:
|
||||
result.update(
|
||||
{
|
||||
"exists": True,
|
||||
"confidence": "verified",
|
||||
"area": check_area,
|
||||
"match": with_path_fields(
|
||||
compact_member(match, area=check_area),
|
||||
object_path=object_path,
|
||||
path_kind="metadata_member" if check_area in {"attribute", "module", "form", "command"} else check_area,
|
||||
),
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
near = []
|
||||
wanted = member or table_section or ""
|
||||
for check_area, items in checks:
|
||||
for item in nearest(items, wanted):
|
||||
item["area"] = check_area
|
||||
near.append(item)
|
||||
result.update(
|
||||
{
|
||||
"exists": False,
|
||||
"confidence": "verified_absent",
|
||||
"reason": "member_not_found_in_current_source",
|
||||
"nearest": near[:10],
|
||||
"counts": {
|
||||
"attributes": len(metadata.get("attributes") or []),
|
||||
"tabular_sections": len(metadata.get("tabular_sections") or []),
|
||||
},
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def snapshot_kind_matches(actual: str, wanted: str | None) -> bool:
|
||||
if not wanted:
|
||||
return True
|
||||
canonical = canonical_kind(wanted)
|
||||
aliases = SNAPSHOT_KIND_ALIASES.get(canonical or wanted, {wanted})
|
||||
return compact_text(actual) in {compact_text(alias) for alias in aliases}
|
||||
|
||||
|
||||
def resolve_from_snapshot(
|
||||
snapshot: dict[str, Any],
|
||||
*,
|
||||
snapshot_path: Path,
|
||||
kind: str | None,
|
||||
object_name: str,
|
||||
member: str | None,
|
||||
area: str,
|
||||
table_section: str | None,
|
||||
view: str,
|
||||
extension: str | None,
|
||||
) -> dict[str, Any]:
|
||||
query = {"kind": canonical_kind(kind), "name": object_name, "member": member, "table_section": table_section, "area": area}
|
||||
result = result_base(
|
||||
query=query,
|
||||
source={"kind": "metadata_snapshot", "path": str(snapshot_path), "name": (snapshot.get("source") or {}).get("name")},
|
||||
view=view,
|
||||
extension=extension,
|
||||
)
|
||||
objects = snapshot.get("objects") or []
|
||||
obj = next(
|
||||
(
|
||||
item
|
||||
for item in objects
|
||||
if snapshot_kind_matches(str(item.get("kind") or ""), kind)
|
||||
and (same_name(item.get("name"), object_name) or same_name(item.get("full_name"), object_name) or same_name(item.get("synonym"), object_name))
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not obj:
|
||||
result.update({"exists": False, "confidence": "none", "reason": "object_not_found", "nearest": []})
|
||||
return result
|
||||
result["object"] = {key: obj.get(key) for key in ("kind", "name", "full_name", "synonym") if obj.get(key)}
|
||||
object_path = str(obj.get("full_name") or object_canonical_path(obj.get("kind"), obj.get("name")))
|
||||
result["object"]["canonical_path"] = object_path
|
||||
result["object"]["path_kind"] = "metadata_object"
|
||||
if not member and not table_section:
|
||||
result.update({"exists": True, "confidence": "verified", "area": "object", "match": result["object"]})
|
||||
return result
|
||||
|
||||
checks: list[tuple[str, list[dict[str, Any]]]] = []
|
||||
if area in {"any", "attribute"} and member:
|
||||
checks.append(("attribute", obj.get("attributes") or []))
|
||||
if area in {"any", "tabular_section"} and not (area == "any" and table_section and member):
|
||||
wanted = table_section or member
|
||||
sections = obj.get("tabular_sections") or []
|
||||
if wanted:
|
||||
section = next((item for item in sections if matches_name(item, wanted)), None)
|
||||
if section:
|
||||
result.update(
|
||||
{
|
||||
"exists": True,
|
||||
"confidence": "verified",
|
||||
"area": "tabular_section",
|
||||
"match": with_path_fields(
|
||||
compact_member(section, area="tabular_section"),
|
||||
object_path=object_path,
|
||||
path_kind="metadata_member",
|
||||
),
|
||||
}
|
||||
)
|
||||
return result
|
||||
checks.append(("tabular_section", sections))
|
||||
if area in {"any", "tabular_section_attribute"} and table_section and member:
|
||||
section = next((item for item in obj.get("tabular_sections") or [] if matches_name(item, table_section)), None)
|
||||
attrs = section.get("attributes") if section else []
|
||||
checks.append(("tabular_section_attribute", attrs or []))
|
||||
if area in {"any", "form"} and member:
|
||||
checks.append(("form", obj.get("forms") or []))
|
||||
if area in {"any", "command"} and member:
|
||||
checks.append(("command", obj.get("commands") or []))
|
||||
if area in {"any", "module"} and member:
|
||||
checks.append(("module", obj.get("modules") or []))
|
||||
|
||||
for check_area, items in checks:
|
||||
if member:
|
||||
match = next((item for item in items if matches_name(item, member)), None)
|
||||
if match:
|
||||
context_section = None
|
||||
if check_area == "tabular_section_attribute" and table_section:
|
||||
context_section = table_section
|
||||
result.update(
|
||||
{
|
||||
"exists": True,
|
||||
"confidence": "verified",
|
||||
"area": check_area,
|
||||
"match": with_path_fields(
|
||||
compact_member(match, area=check_area),
|
||||
object_path=object_path,
|
||||
path_kind="metadata_member",
|
||||
table_section=context_section,
|
||||
),
|
||||
}
|
||||
)
|
||||
return result
|
||||
near = []
|
||||
wanted = member or table_section or ""
|
||||
for check_area, items in checks:
|
||||
for item in nearest(items, wanted):
|
||||
item["area"] = check_area
|
||||
near.append(item)
|
||||
result.update({"exists": False, "confidence": "verified_absent", "reason": "member_not_found_in_explicit_snapshot", "nearest": near[:10]})
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Resolve a concrete 1C fact against an explicit configuration source.")
|
||||
source = parser.add_mutually_exclusive_group(required=True)
|
||||
source.add_argument("--index", type=Path, help="Unified object route index for current configuration.")
|
||||
source.add_argument("--snapshot", type=Path, help="Explicit metadata snapshot. Use only when this is the intended source.")
|
||||
parser.add_argument("--path", help="Fact path, for example Справочник.Номенклатура.Артикул")
|
||||
parser.add_argument("--path-b64")
|
||||
parser.add_argument("--kind")
|
||||
parser.add_argument("--name")
|
||||
parser.add_argument("--member")
|
||||
parser.add_argument("--table-section")
|
||||
parser.add_argument("--area", default="any")
|
||||
parser.add_argument("--view", choices=["effective", "base", "extension"], default="effective")
|
||||
parser.add_argument("--extension")
|
||||
parser.add_argument("--output", type=Path)
|
||||
args = parser.parse_args()
|
||||
|
||||
fact_path = decode_arg(args.path, args.path_b64)
|
||||
if fact_path:
|
||||
path_kind, path_name, path_section, path_member = split_fact_path(fact_path)
|
||||
kind = args.kind or path_kind
|
||||
name = args.name or path_name
|
||||
table_section = args.table_section or path_section
|
||||
member = args.member or path_member
|
||||
else:
|
||||
kind = args.kind
|
||||
name = args.name
|
||||
table_section = args.table_section
|
||||
member = args.member
|
||||
if not name:
|
||||
raise SystemExit("Use --path or --kind/--name.")
|
||||
|
||||
area = member_area(args.area)
|
||||
if args.index:
|
||||
result = resolve_from_route_index(
|
||||
load_json(args.index),
|
||||
index_path=args.index,
|
||||
kind=kind,
|
||||
object_name=name,
|
||||
member=member,
|
||||
area=area,
|
||||
table_section=table_section,
|
||||
view=args.view,
|
||||
extension=args.extension,
|
||||
)
|
||||
else:
|
||||
result = resolve_from_snapshot(
|
||||
load_json(args.snapshot),
|
||||
snapshot_path=args.snapshot,
|
||||
kind=kind,
|
||||
object_name=name,
|
||||
member=member,
|
||||
area=area,
|
||||
table_section=table_section,
|
||||
view=args.view,
|
||||
extension=args.extension,
|
||||
)
|
||||
|
||||
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), "exists": result.get("exists"), "confidence": result.get("confidence")}, ensure_ascii=False))
|
||||
else:
|
||||
print(text)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user