from __future__ import annotations import argparse import base64 import copy import hmac import concurrent.futures import csv import difflib import hashlib import io import json import math import multiprocessing import os import queue import re import sqlite3 import sys import threading import time import traceback import urllib.parse import urllib.error import urllib.request import uuid import xml.etree.ElementTree as ET import zlib from datetime import date, datetime, timezone from decimal import Decimal from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from typing import Any, Iterable CONNECTOR_ROOT = Path(__file__).resolve().parent if str(CONNECTOR_ROOT) not in sys.path: sys.path.insert(0, str(CONNECTOR_ROOT)) import repository_control SCHEMA = "onec_adapter_rest.v1" PARSER_ROOT = Path(__file__).resolve().parents[1] if len(Path(__file__).resolve().parents) >= 2 else Path(__file__).resolve().parent if PARSER_ROOT.exists() and str(PARSER_ROOT) not in sys.path: sys.path.insert(0, str(PARSER_ROOT)) from parser.scheduled_job import ( SCHEDULED_JOB_INTEGER_RANGES, SCHEDULED_JOB_WRITABLE_LIST_FIELDS, SCHEDULED_JOB_WRITABLE_SCALAR_FIELDS, config_schedule_datetime, scheduled_job_schedule_layout, scheduled_job_schedule_rebuild_tree, scheduled_job_schedule_write_edits, scheduled_job_sql_schedule, ) from parser.common_command import common_command_group_guid, index_common_command_groups STORAGE_TABLES = {"Config", "ConfigSave", "ConfigCAS", "ConfigCASSave", "Params"} SENSITIVE_RESULT_FIELD_RE = re.compile( r"(?:парол|password|телефон|phone|email|e-mail|почт|паспорт|инн|снилс|банк(?:овск)?(?:ий)?счет|card|карта)", re.IGNORECASE, ) ADAPTER_JOBS: dict[str, dict[str, Any]] = {} ADAPTER_JOB_LOCK = threading.Lock() ADAPTER_JOB_TTL_SECONDS = 1800 ADAPTER_JOB_STORE_LOADED = False ADAPTER_INSTANCE_ID = uuid.uuid4().hex ADAPTER_JOB_EVENT_SINK: Any = None BASE_ROOT_METADATA_CACHE: dict[tuple[str, str], dict[str, Any]] = {} BASE_ROOT_METADATA_CACHE_LOCK = threading.Lock() BASE_ROOT_METADATA_CACHE_TTL_SECONDS = 300 DATA_SCHEMA_CACHE: dict[str, dict[str, Any]] = {} DATA_SCHEMA_CACHE_LOCK = threading.Lock() DATA_SCHEMA_CACHE_TTL_SECONDS = 300 MOXEL_TEMPLATE_ARTIFACT_KIND = "template_part_moxel_v11" TEMPLATE_CONTENT_DEFAULT_MAX_BYTES = 262144 TEMPLATE_CONTENT_MAX_BYTES = 1048576 FULL_METHOD_SECTIONS = {"card", "semantic", "forms", "templates", "commands", "modules", "parts_summary"} FULL_METHOD_SECTION_ORDER = ["card", "semantic", "modules", "templates", "forms", "commands"] FULL_METHOD_ALL_KEY = "all" FULL_METHOD_DEFAULT_SECTIONS = ["card", "semantic", "modules", "templates", "forms", "commands"] ONEC_TEMPLATE_PLATFORM_TYPES = [ { "id": "tabular_document", "name": "Табличный документ", "xml_type": "SpreadsheetDocument", "decoder": "moxel", "status": "structure_and_safe_export_supported", "aliases": ["ТабличныйДокумент", "SpreadsheetDocument"], }, { "id": "text_document", "name": "Текстовый документ", "xml_type": "TextDocument", "decoder": "text", "status": "safe_export_supported", "aliases": ["ТекстовыйДокумент", "TextDocument"], }, { "id": "binary_data", "name": "Двоичные данные", "xml_type": "BinaryData", "decoder": "binary", "status": "safe_export_supported", "aliases": ["ДвоичныеДанные", "BinaryData"], }, { "id": "active_document", "name": "Active document", "xml_type": "ActiveDocument", "decoder": "active_document", "status": "not_implemented", "aliases": ["ActiveDocument", "Active document"], }, { "id": "html_document", "name": "HTML документ", "xml_type": "HTMLDocument", "decoder": "html", "status": "safe_export_supported", "aliases": ["HTMLДокумент", "HTMLDocument", "HtmlDocument"], }, { "id": "geographical_schema", "name": "Географическая схема", "xml_type": "GeographicalSchema", "decoder": "geographical_schema", "status": "not_implemented", "aliases": ["ГеографическаяСхема", "GeographicalSchema"], }, { "id": "graphical_schema", "name": "Графическая схема", "xml_type": "GraphicalSchema", "decoder": "graphical_schema", "status": "not_implemented", "aliases": ["ГрафическаяСхема", "GraphicalSchema"], }, { "id": "data_composition_schema", "name": "Схема компоновки данных", "xml_type": "DataCompositionSchema", "decoder": "data_composition_schema", "status": "not_implemented", "aliases": ["СхемаКомпоновкиДанных", "DataCompositionSchema"], }, { "id": "data_composition_appearance_template", "name": "Макет оформления компоновки данных", "xml_type": "DataCompositionAppearanceTemplate", "decoder": "data_composition_appearance", "status": "not_implemented", "aliases": ["МакетОформленияКомпоновкиДанных", "DataCompositionAppearanceTemplate"], }, { "id": "external_component", "name": "Внешняя компонента", "xml_type": "ExternalComponent", "decoder": "external_component", "status": "not_implemented", "aliases": ["ВнешняяКомпонента", "ExternalComponent"], }, ] KIND_ALIASES = { "конфигурация": "Configuration", "configuration": "Configuration", "документ": "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": "DataProcessor", "dataprocessors": "DataProcessor", "общиймодуль": "CommonModule", "общий модуль": "CommonModule", "commonmodule": "CommonModule", "commonmodules": "CommonModule", "общаяформа": "CommonForm", "общая форма": "CommonForm", "общиеформы": "CommonForm", "общие формы": "CommonForm", "commonform": "CommonForm", "commonforms": "CommonForm", "форма": "Form", "forms": "Form", "form": "Form", "макет": "Template", "templates": "Template", "template": "Template", "команда": "Command", "commands": "Command", "command": "Command", "константа": "Constant", "constants": "Constant", "constant": "Constant", "планвидовхарактеристик": "ChartOfCharacteristicTypes", "план видов характеристик": "ChartOfCharacteristicTypes", "chartofcharacteristictypes": "ChartOfCharacteristicTypes", "плансчетов": "ChartOfAccounts", "план счетов": "ChartOfAccounts", "chartofaccounts": "ChartOfAccounts", "планвидоврасчета": "ChartOfCalculationTypes", "план видов расчета": "ChartOfCalculationTypes", "chartofcalculationtypes": "ChartOfCalculationTypes", "планобмена": "ExchangePlan", "план обмена": "ExchangePlan", "exchangeplan": "ExchangePlan", "журналдокументов": "DocumentJournal", "журнал документов": "DocumentJournal", "documentjournal": "DocumentJournal", "нумератордокументов": "DocumentNumerator", "нумератор документов": "DocumentNumerator", "documentnumerator": "DocumentNumerator", "documentnumerators": "DocumentNumerator", "регламентноезадание": "ScheduledJob", "регламентное задание": "ScheduledJob", "scheduledjob": "ScheduledJob", "сервисинтеграции": "IntegrationService", "сервис интеграции": "IntegrationService", "integrationservice": "IntegrationService", "integrationservices": "IntegrationService", "регистррасчета": "CalculationRegister", "регистр расчета": "CalculationRegister", "calculationregister": "CalculationRegister", "последовательность": "Sequence", "sequence": "Sequence", "критерийотбора": "SelectionCriterion", "критерий отбора": "SelectionCriterion", "selectioncriterion": "SelectionCriterion", "filtercriterion": "SelectionCriterion", "filtercriteria": "SelectionCriterion", "подписканасобытие": "EventSubscription", "подписка на событие": "EventSubscription", "eventsubscription": "EventSubscription", "роль": "Role", "role": "Role", "определяемыйтип": "DefinedType", "определяемый тип": "DefinedType", "definedtype": "DefinedType", "общиймакет": "CommonTemplate", "общий макет": "CommonTemplate", "commontemplate": "CommonTemplate", "общаякартинка": "CommonPicture", "общая картинка": "CommonPicture", "commonpicture": "CommonPicture", "группакоманд": "CommandGroup", "группа команд": "CommandGroup", "commandgroup": "CommandGroup", "стиль": "Style", "style": "Style", "элементстиля": "StyleItem", "элемент стиля": "StyleItem", "styleitem": "StyleItem", "интерфейс": "Interface", "interface": "Interface", } PUBLIC_KIND = { "Configuration": "configuration", "Catalog": "catalog", "Document": "document", "Enum": "enum", "InformationRegister": "register", "AccumulationRegister": "register", "AccountingRegister": "register", "Report": "report", "DataProcessor": "processing", "CommonModule": "common_module", "CommonForm": "common_form", "BusinessProcess": "business_process", "Task": "task", "ChartOfAccounts": "chart", "ChartOfCalculationTypes": "chart", "ChartOfCharacteristicTypes": "chart", "Constant": "constant", "ExchangePlan": "exchange_plan", "DocumentJournal": "document_journal", "DocumentNumerator": "document_numerator", "ScheduledJob": "scheduled_job", "CalculationRegister": "register", "Sequence": "sequence", "SelectionCriterion": "selection_criterion", "EventSubscription": "event_subscription", "Role": "role", "DefinedType": "defined_type", "SessionParameter": "session_parameter", "FunctionalOption": "functional_option", "FunctionalOptionsParameter": "functional_options_parameter", "SettingsStorage": "settings_storage", "CommonAttribute": "common_attribute", "CommonCommand": "common_command", "Subsystem": "subsystem", "Language": "language", "WebService": "web_service", "HTTPService": "http_service", "WSReference": "ws_reference", "XDTOPackage": "xdto_package", "ExternalDataSource": "external_data_source", "IntegrationService": "integration_service", "CommonTemplate": "common_template", "CommonPicture": "common_picture", "CommandGroup": "command_group", "Style": "style", "StyleItem": "style_item", "Interface": "interface", } DBNAMES_ROLE_KIND = { "Document": "Document", "Reference": "Catalog", "Enum": "Enum", "Report": "Report", "DataProcessor": "DataProcessor", "InfoRg": "InformationRegister", "AccumRg": "AccumulationRegister", "AccRg": "AccountingRegister", "BPr": "BusinessProcess", "Task": "Task", "Const": "Constant", "Chrc": "ChartOfCharacteristicTypes", "CKinds": "ChartOfCalculationTypes", "Acc": "ChartOfAccounts", "Node": "ExchangePlan", "DocumentJournal": "DocumentJournal", "ScheduledJobs": "ScheduledJob", "CalcRg": "CalculationRegister", "Sequence": "Sequence", } # The base configuration root descriptor contains collections that do not have # their own DBNames data-table route. These platform collection identifiers are # read from Config/root and let discovery cover code/configuration-only objects # without inventing SQL storage identifiers for callers. ROOT_COLLECTION_KIND = { "09736b02-9cac-4e3f-b4f7-d3e9576ab948": "Role", "0c89c792-16c3-11d5-b96b-0050bae0a95d": "CommonTemplate", "0fe48980-252d-11d6-a3c7-0050bae0a776": "CommonModule", "0fffc09c-8f4c-47cc-b41c-8d5c5a221d79": "HTTPService", "11bdaf85-d5ad-4d91-bb24-aa0eee139052": "ScheduledJob", "15794563-ccec-41f6-a83c-ec5f7b9a5bc1": "CommonAttribute", "24c43748-c938-45d0-8d14-01424a72b11e": "SessionParameter", "30d554db-541e-4f62-8970-a1c6dcfeb2bc": "FunctionalOptionsParameter", "37f2fa9a-b276-11d4-9435-004095e12fc7": "Subsystem", "3e5404af-6ef8-4c73-ad11-91bd2dfac4c8": "Style", "3e7bfcc0-067d-11d6-a3c7-0050bae0a776": "SelectionCriterion", "46b4cd97-fd13-4eaa-aba2-3bddd7699218": "SettingsStorage", "4e828da6-0f44-4b5b-b1c0-a2b3cfe7bdcc": "EventSubscription", "58848766-36ea-4076-8800-e91eb49590d7": "StyleItem", "7dcd43d9-aca5-4926-b549-1842e6a4e8cf": "CommonPicture", "857c4a91-e5f4-4fac-86ec-787626f1c108": "ExchangePlan", "8657032e-7740-4e1d-a3ba-5dd6e8afb78f": "WebService", "9cd510ce-abfc-11d4-9434-004095e12fc7": "Language", "a7641777-7813-45c6-96ef-9d51587a6ac6": "Interface", "af547940-3268-434f-a3e7-e47d6d2638c3": "FunctionalOption", "c045099e-13b9-4fb6-9d50-fca00202971e": "DefinedType", "cc9df798-7c94-4616-97d2-7aa0b7bc515e": "XDTOPackage", "d26096fb-7a5d-4df9-af63-47d04771fa9b": "WSReference", "5274d9fc-9c3a-4a71-8f5e-a0db8ab23de5": "ExternalDataSource", "bf3420b0-f6f9-41a0-b83a-fe9d4ab0b65d": "IntegrationService", } # The application-object block in the same root descriptor is ordered by the # platform format. Each collection starts with its declared count followed by # public metadata object GUIDs. ROOT_APPLICATION_COLLECTION_KIND = { 0: "Constant", 1: "Document", 2: "CommonForm", 3: "InformationRegister", 4: "CommandGroup", 5: "CommonCommand", 6: "DocumentNumerator", 7: "DocumentJournal", 8: "Report", 9: "ChartOfCharacteristicTypes", 10: "AccumulationRegister", 11: "CalculationRegister", 12: "DataProcessor", 13: "Catalog", 14: "Enum", } ROOT_APPLICATION_CLASS_KIND = { "0195e80c-b157-11d4-9435-004095e12fc7": "Constant", "061d872a-5787-460e-95ac-ed74ea3a3e84": "Document", "07ee8426-87f1-11d5-b99c-0050bae0a95d": "CommonForm", "13134201-f60b-11d5-a3c7-0050bae0a776": "InformationRegister", "1c57eabe-7349-44b3-b1de-ebfeab67b47d": "CommandGroup", "2f1a5187-fb0e-4b05-9489-dc5dd6412348": "CommonCommand", "36a8e346-9aaa-4af9-bdbd-83be3c177977": "DocumentNumerator", "4612bd75-71b7-4a5c-8cc5-2b0b65f9fa0d": "DocumentJournal", "631b75a0-29e2-11d6-a3c7-0050bae0a776": "Report", "82a1b659-b220-4d94-a9bd-14d757b95a48": "ChartOfCharacteristicTypes", "b64d9a40-1642-11d6-a3c7-0050bae0a776": "AccumulationRegister", "bc587f20-35d9-11d6-a3c7-0050bae0a776": "CalculationRegister", "bf845118-327b-4682-b5c6-285d2a0eb296": "DataProcessor", "cf4abea6-37b2-11d4-940f-008048da11f9": "Catalog", "f6a80749-5ad7-400b-8519-39dc5dff2542": "Enum", } ROOT_DISCOVERY_KIND_SET = set(ROOT_COLLECTION_KIND.values()) | {"Configuration", "CommonForm", "CommonCommand", "CommandGroup", "DocumentNumerator", "Report", "DataProcessor"} GENERATED_TYPE_PREFIX = { "Catalog": "Catalog", "Document": "Document", "Enum": "Enum", "InformationRegister": "InformationRegister", "AccumulationRegister": "AccumulationRegister", "AccountingRegister": "AccountingRegister", "BusinessProcess": "BusinessProcess", "Task": "Task", "Constant": "Constant", "ChartOfCharacteristicTypes": "ChartOfCharacteristicTypes", "ChartOfAccounts": "ChartOfAccounts", "ChartOfCalculationTypes": "ChartOfCalculationTypes", "ExchangePlan": "ExchangePlan", "DocumentJournal": "DocumentJournal", "ScheduledJob": "ScheduledJob", "DefinedType": "DefinedType", } GENERATED_TYPE_CATEGORIES = { "Catalog": ["Object", "Ref", "Selection", "List", "Manager"], "Document": ["Object", "Ref", "Selection", "List", "Manager"], "Enum": ["Ref", "Manager", "List"], "BusinessProcess": ["Object", "Ref", "Selection", "List", "RoutePointRef", "RoutePoint", "Manager"], "Task": ["Object", "Ref", "Selection", "List", "Manager"], "InformationRegister": ["Record", "Manager", "Selection", "List", "RecordSet", "RecordKey", "RecordManager"], "AccumulationRegister": ["Record", "Manager", "Selection", "List", "RecordSet", "RecordKey", "RecordManager"], "AccountingRegister": ["Record", "ExtDimensions", "RecordSet", "RecordKey", "Selection", "List", "Manager"], "Constant": ["Manager", "ValueManager"], "ChartOfCharacteristicTypes": ["Object", "Ref", "Selection", "List", "Manager"], "ChartOfAccounts": ["Object", "Ref", "Selection", "List", "Manager"], "ChartOfCalculationTypes": ["Object", "Ref", "Selection", "List", "Manager"], "ExchangePlan": ["Object", "Ref", "Selection", "List", "Manager"], "DocumentJournal": ["Selection", "List", "Manager"], "ScheduledJob": ["Manager"], "DefinedType": ["DefinedType"], } BUILTIN_TYPE_GUIDS = { "e199ca70-93cf-46ce-a54b-6edc88c3a296": { "name": "ХранилищеЗначения", "presentation": "ХранилищеЗначения", "bsl_type": "ValueStorage", }, "220455ea-6c85-4513-996f-bbe79ed07774": { "name": "ФиксированноеСоответствие", "presentation": "ФиксированноеСоответствие", "bsl_type": "FixedMap", }, "3ee983d7-ace7-40f9-bb7e-2e916fcddd56": { "name": "ФиксированнаяСтруктура", "presentation": "ФиксированнаяСтруктура", "bsl_type": "FixedStructure", }, "4500381b-db30-4a10-9db4-990038032acf": { "name": "ФиксированныйМассив", "presentation": "ФиксированныйМассив", "bsl_type": "FixedArray", }, "fc01b5df-97fe-449b-83d4-218a090e681e": { "name": "УникальныйИдентификатор", "presentation": "УникальныйИдентификатор", "bsl_type": "UUID", }, } RU_KIND = { "Configuration": "Конфигурация", "Catalog": "Справочник", "Document": "Документ", "Enum": "Перечисление", "InformationRegister": "РегистрСведений", "AccumulationRegister": "РегистрНакопления", "AccountingRegister": "РегистрБухгалтерии", "Report": "Отчет", "DataProcessor": "Обработка", "CommonModule": "ОбщийМодуль", "CommonForm": "ОбщаяФорма", "Form": "Форма", "Template": "Макет", "Command": "Команда", "BusinessProcess": "БизнесПроцесс", "Task": "Задача", "Constant": "Константа", "ChartOfCharacteristicTypes": "ПланВидовХарактеристик", "ChartOfAccounts": "ПланСчетов", "ChartOfCalculationTypes": "ПланВидовРасчета", "ExchangePlan": "ПланОбмена", "DocumentJournal": "ЖурналДокументов", "DocumentNumerator": "НумераторДокументов", "ScheduledJob": "РегламентноеЗадание", "DefinedType": "ОпределяемыйТип", "CalculationRegister": "РегистрРасчета", "Sequence": "Последовательность", "SelectionCriterion": "КритерийОтбора", "EventSubscription": "ПодпискаНаСобытие", "Role": "Роль", "SessionParameter": "ПараметрСеанса", "FunctionalOption": "ФункциональнаяОпция", "FunctionalOptionsParameter": "ПараметрФункциональныхОпций", "SettingsStorage": "ХранилищеНастроек", "CommonAttribute": "ОбщийРеквизит", "CommonCommand": "ОбщаяКоманда", "Subsystem": "Подсистема", "Language": "Язык", "WebService": "WebСервис", "HTTPService": "HTTPСервис", "WSReference": "WSСсылка", "XDTOPackage": "ПакетXDTO", "ExternalDataSource": "ВнешнийИсточникДанных", "IntegrationService": "СервисИнтеграции", "CommonTemplate": "ОбщийМакет", "CommonPicture": "ОбщаяКартинка", "CommandGroup": "ГруппаКоманд", "Style": "Стиль", "StyleItem": "ЭлементСтиля", "Interface": "Интерфейс", } ONEC_CODE_ROOT_KIND = { "Конфигурация": "Configuration", "Справочники": "Catalog", "Документы": "Document", "Перечисления": "Enum", "РегистрыСведений": "InformationRegister", "РегистрыНакопления": "AccumulationRegister", "РегистрыБухгалтерии": "AccountingRegister", "Отчеты": "Report", "Обработки": "DataProcessor", "ОбщиеМодули": "CommonModule", "ОбщиеФормы": "CommonForm", "БизнесПроцессы": "BusinessProcess", "Задачи": "Task", "Константы": "Constant", "ПланыВидовХарактеристик": "ChartOfCharacteristicTypes", "ПланыСчетов": "ChartOfAccounts", "ПланыВидовРасчета": "ChartOfCalculationTypes", "ПланыОбмена": "ExchangePlan", "ЖурналыДокументов": "DocumentJournal", "НумераторыДокументов": "DocumentNumerator", "РегламентныеЗадания": "ScheduledJob", "ОпределяемыеТипы": "DefinedType", "РегистрыРасчета": "CalculationRegister", "Последовательности": "Sequence", "КритерииОтбора": "SelectionCriterion", "ПодпискиНаСобытия": "EventSubscription", "Роли": "Role", "ПараметрыСеанса": "SessionParameter", "ФункциональныеОпции": "FunctionalOption", "ПараметрыФункциональныхОпций": "FunctionalOptionsParameter", "ХранилищаНастроек": "SettingsStorage", "ОбщиеРеквизиты": "CommonAttribute", "ОбщиеКоманды": "CommonCommand", "Подсистемы": "Subsystem", "Языки": "Language", "WebСервисы": "WebService", "HTTPСервисы": "HTTPService", "WSСсылки": "WSReference", "ПакетыXDTO": "XDTOPackage", "ВнешниеИсточникиДанных": "ExternalDataSource", "СервисыИнтеграции": "IntegrationService", "ОбщиеМакеты": "CommonTemplate", "ОбщиеКартинки": "CommonPicture", "ГруппыКоманд": "CommandGroup", "Стили": "Style", "ЭлементыСтиля": "StyleItem", "Интерфейсы": "Interface", } ONEC_CODE_ROOT_BY_KIND = {kind: root for root, kind in ONEC_CODE_ROOT_KIND.items()} ONEC_TYPE_QUALIFIER_KIND = { "СправочникСсылка": "Catalog", "СправочникОбъект": "Catalog", "СправочникСписок": "Catalog", "СправочникВыборка": "Catalog", "СправочникМенеджер": "Catalog", "ДокументСсылка": "Document", "ДокументОбъект": "Document", "ДокументСписок": "Document", "ДокументВыборка": "Document", "ДокументМенеджер": "Document", "ПеречислениеСсылка": "Enum", "ПеречислениеСписок": "Enum", "ПеречислениеМенеджер": "Enum", "БизнесПроцессСсылка": "BusinessProcess", "БизнесПроцессОбъект": "BusinessProcess", "БизнесПроцессСписок": "BusinessProcess", "БизнесПроцессВыборка": "BusinessProcess", "БизнесПроцессМенеджер": "BusinessProcess", "ЗадачаСсылка": "Task", "ЗадачаОбъект": "Task", "ЗадачаСписок": "Task", "ЗадачаВыборка": "Task", "ЗадачаМенеджер": "Task", "ПланВидовХарактеристикСсылка": "ChartOfCharacteristicTypes", "ПланВидовХарактеристикОбъект": "ChartOfCharacteristicTypes", "ПланВидовХарактеристикСписок": "ChartOfCharacteristicTypes", "ПланВидовХарактеристикВыборка": "ChartOfCharacteristicTypes", "ПланВидовХарактеристикМенеджер": "ChartOfCharacteristicTypes", "ПланСчетовСсылка": "ChartOfAccounts", "ПланСчетовОбъект": "ChartOfAccounts", "ПланСчетовСписок": "ChartOfAccounts", "ПланСчетовВыборка": "ChartOfAccounts", "ПланСчетовМенеджер": "ChartOfAccounts", "ПланВидовРасчетаСсылка": "ChartOfCalculationTypes", "ПланВидовРасчетаОбъект": "ChartOfCalculationTypes", "ПланВидовРасчетаСписок": "ChartOfCalculationTypes", "ПланВидовРасчетаВыборка": "ChartOfCalculationTypes", "ПланВидовРасчетаМенеджер": "ChartOfCalculationTypes", "ПланОбменаСсылка": "ExchangePlan", "ПланОбменаОбъект": "ExchangePlan", "ПланОбменаСписок": "ExchangePlan", "ПланОбменаВыборка": "ExchangePlan", "ПланОбменаМенеджер": "ExchangePlan", "РегистрСведенийМенеджер": "InformationRegister", "РегистрСведенийМенеджерЗаписи": "InformationRegister", "РегистрСведенийНаборЗаписей": "InformationRegister", "РегистрСведенийКлючЗаписи": "InformationRegister", "РегистрСведенийСписок": "InformationRegister", "РегистрСведенийВыборка": "InformationRegister", "РегистрНакопленияМенеджер": "AccumulationRegister", "РегистрНакопленияНаборЗаписей": "AccumulationRegister", "РегистрНакопленияКлючЗаписи": "AccumulationRegister", "РегистрНакопленияСписок": "AccumulationRegister", "РегистрНакопленияВыборка": "AccumulationRegister", "РегистрБухгалтерииМенеджер": "AccountingRegister", "РегистрБухгалтерииНаборЗаписей": "AccountingRegister", "РегистрБухгалтерииКлючЗаписи": "AccountingRegister", "РегистрБухгалтерииСписок": "AccountingRegister", "РегистрБухгалтерииВыборка": "AccountingRegister", "РегистрРасчетаМенеджер": "CalculationRegister", "РегистрРасчетаНаборЗаписей": "CalculationRegister", "РегистрРасчетаКлючЗаписи": "CalculationRegister", "РегистрРасчетаСписок": "CalculationRegister", "РегистрРасчетаВыборка": "CalculationRegister", "КонстантаМенеджер": "Constant", "КонстантаМенеджерЗначения": "Constant", "ОтчетОбъект": "Report", "ОтчетМенеджер": "Report", "ОбработкаОбъект": "DataProcessor", "ОбработкаМенеджер": "DataProcessor", "ЖурналДокументовСписок": "DocumentJournal", "ЖурналДокументовМенеджер": "DocumentJournal", "ПоследовательностьМенеджер": "Sequence", } REF_TYPE_PRESENTATION_PREFIX = { "Catalog": "СправочникСсылка", "Document": "ДокументСсылка", "Enum": "ПеречислениеСсылка", "BusinessProcess": "БизнесПроцессСсылка", "Task": "ЗадачаСсылка", "ChartOfCharacteristicTypes": "ПланВидовХарактеристикСсылка", "ChartOfAccounts": "ПланСчетовСсылка", "ChartOfCalculationTypes": "ПланВидовРасчетаСсылка", "ExchangePlan": "ПланОбменаСсылка", } OBJECT_TYPE_PRESENTATION_PREFIX = { "Catalog": "СправочникОбъект", "Document": "ДокументОбъект", "BusinessProcess": "БизнесПроцессОбъект", "Task": "ЗадачаОбъект", "ChartOfCharacteristicTypes": "ПланВидовХарактеристикОбъект", "ChartOfAccounts": "ПланСчетовОбъект", "ChartOfCalculationTypes": "ПланВидовРасчетаОбъект", "ExchangePlan": "ПланОбменаОбъект", } LIST_TYPE_PRESENTATION_PREFIX = { "Catalog": "СправочникСписок", "Document": "ДокументСписок", "Enum": "ПеречислениеСписок", "BusinessProcess": "БизнесПроцессСписок", "Task": "ЗадачаСписок", "ChartOfCharacteristicTypes": "ПланВидовХарактеристикСписок", "ChartOfAccounts": "ПланСчетовСписок", "ChartOfCalculationTypes": "ПланВидовРасчетаСписок", "ExchangePlan": "ПланОбменаСписок", } METHODS = [ {"name": "health", "transport": "GET /health", "description": "Состояние REST-адаптера и live SQL-доступа."}, {"name": "help.methods", "transport": "GET /methods or POST /rpc", "description": "Список методов адаптера."}, {"name": "repository.status", "transport": "POST /rpc", "description": "Repository configuration and optional read-only availability probe for direct or configured TCP bridge access."}, {"name": "repository.layers.audit", "transport": "POST /rpc", "description": "Read-only audit of the base configuration and every discovered extension: repository connection state, support state, and the safe next write action."}, {"name": "repository.layer.connection.set", "transport": "POST /rpc", "description": "Persist the explicit repository connection state for base or one extension in the adapter configuration only. Requires confirm_repository_connection_change=true; never writes a 1C SQL database."}, {"name": "metadata.support.decode", "transport": "POST /rpc", "description": "Decode ParentConfigurations support rules from live SQL. The base source is resolved through Config/root and an extension source through its complete ConfigCAS manifest. Repository capture is evaluated separately."}, {"name": "repository.sql_state.snapshot", "transport": "POST /rpc", "description": "Read-only hashes of selected live SQL configuration-state payloads for controlled before/after repository-state experiments. Requires diagnostic=true and never infers a native lock."}, {"name": "repository.sql_state.diff", "transport": "POST /rpc", "description": "Compare two repository.sql_state.snapshot responses and report changed records and parts. Requires diagnostic=true and never infers a native lock."}, {"name": "repository.lock.plan", "transport": "POST /rpc", "description": "Resolve public 1C object references to repository development-object lock scope without changing repository state."}, {"name": "repository.lock.request", "transport": "POST /rpc", "description": "Create a persisted manual lock request for the exact resolved object scope; it does not modify SQL or claim a repository lock."}, {"name": "repository.lock.request.status", "transport": "POST /rpc", "description": "Read the adapter-side status of a manual repository lock request."}, {"name": "repository.lock.request.cancel", "transport": "POST /rpc", "description": "Cancel a pending manual repository lock request with explicit confirmation."}, {"name": "repository.lock", "transport": "POST /rpc", "description": "Lock a planned set of objects through the configured 1C Designer repository endpoint."}, {"name": "repository.lock.confirm", "transport": "POST /rpc", "description": "Record an explicit user confirmation for the exact planned object set when repository lock_mode=manual; this never claims automatic verification."}, {"name": "repository.lock.verify", "transport": "POST /rpc", "description": "Verify an adapter-owned repository lock session."}, {"name": "repository.lock.close", "transport": "POST /rpc", "description": "Close a manual confirmation after the user confirms that the objects were released in Configurator."}, {"name": "repository.unlock", "transport": "POST /rpc", "description": "Release only objects acquired by the specified adapter lock session."}, {"name": "repository.commit.plan", "transport": "POST /rpc", "description": "Validate an adapter-owned lock session, object set, and required repository version comment before commit."}, {"name": "repository.commit", "transport": "POST /rpc", "description": "Commit objects through 1C Designer using configured direct or TCP bridge repository access and explicit approval."}, {"name": "adapter.job.start", "transport": "POST /rpc", "description": "Start a long adapter-owned job. Use adapter.job.get/cancel to observe or cancel it."}, {"name": "adapter.job.get", "transport": "POST /rpc", "description": "Read status, progress, result, and partial_result for an adapter-owned job."}, {"name": "adapter.job.cancel", "transport": "POST /rpc", "description": "Request cancellation of an adapter-owned job."}, {"name": "metadata.kinds", "transport": "GET /metadata/kinds or POST /rpc", "description": "Live 1C metadata kinds. Requires base_id."}, {"name": "metadata.capabilities", "transport": "POST /rpc", "description": "Public adapter capabilities by 1C metadata kind."}, {"name": "metadata.adapter.audit", "transport": "POST /rpc", "description": "Public audit of recognized 1C metadata kinds, public kind counts, code carrier matrix, missing supported kinds, unmapped DBNames roles, child object support, and not-yet-decoded areas."}, {"name": "metadata.objects.list", "transport": "GET /metadata/objects or POST /rpc", "description": "1C base/effective metadata object list. Does not accept extension filters; use extension.objects.find for extension-scoped objects such as test2. Uses local metadata cache for the normal fast list; pass refresh_cache=true, include_missing=true, only_missing=true, or exact_counts=true for live verification. Requires limit >= 1 and offset >= 0. Missing/unreadable payloads are hidden by default; only_missing=true lists only them. SQL/storage traces are hidden unless include_storage=true."}, {"name": "metadata.object.get", "transport": "GET /metadata/object or POST /rpc", "description": "1C metadata object card. mode must be card or semantic; default card returns compact identity/matches/counts without semantic sections. include_semantic and include_storage must be JSON booleans true/false, string values are invalid. Pass mode=semantic or include_semantic=true only when decoded semantic sections are needed; for реквизиты/табличные части prefer metadata.object.attributes, for full profile prefer metadata.object.full/decode. SQL/storage traces are hidden unless include_storage=true."}, {"name": "metadata.object.properties", "transport": "POST /rpc", "description": "Unified SQL-only semantic property reader for every 1C metadata object. Uses a kind-specific SQL decoder when available and falls back to the generic live semantic profile for all other kinds. Accepts the standard public object selectors. XML exports are never read at runtime."}, {"name": "metadata.object.property.write", "transport": "POST /rpc", "description": "Name-first saved-state writer for safe scalar identity properties of objects and existing attributes, tabular sections, dimensions, and resources. Supports synonym and comment, never renames or changes collection structure, never writes active Config/ConfigCAS, and requires explicit prepare/apply/rollback gates."}, {"name": "metadata.object.member.add", "transport": "POST /rpc", "description": "Name-first saved-state structural writer that adds one object requisite or tabular-section column by cloning an existing Attribute template in the same collection. Generates a container-scoped GUID internally, preserves payload formatting, and supports plan/apply/verify/rollback only in ConfigSave/ConfigCASSave."}, {"name": "metadata.object.decode", "transport": "POST /rpc", "description": "1C-facing decoded object profile: identity and semantic sections. evidence_mode controls undecoded payload evidence: none, summary, full, raw. Raw storage offsets are exposed only with include_storage=true."}, {"name": "metadata.object.parts", "transport": "POST /rpc", "description": "1C-facing object part roles: metadata/form/module/template/help. evidence_mode controls undecoded payload evidence: none, summary, full, raw. Physical Config part keys and raw offsets are hidden unless include_storage=true."}, {"name": "metadata.object.modules", "transport": "POST /rpc", "description": "1C-facing BSL module list for a metadata object. include_storage must be a JSON boolean true/false, string values are invalid. Physical module ids are hidden unless include_storage=true."}, {"name": "metadata.object.related", "transport": "POST /rpc", "description": "1C-facing related metadata objects such as forms and templates. Physical record paths are hidden unless include_storage=true."}, {"name": "metadata.object.forms", "transport": "POST /rpc", "description": "1C-facing forms for a metadata object with form part roles. include_storage must be a JSON boolean true/false, string values are invalid. Physical Config part keys are hidden unless include_storage=true."}, {"name": "metadata.object.form.details", "transport": "POST /rpc", "description": "Decode object forms into public form properties: elements, attributes, commands, events, links, and decoded form parameters. Optional element/element_path/element_id focuses the returned elements list on one form element. include_parameters controls decoded parameter lists; max_parameters limits parameters per form node."}, {"name": "metadata.object.templates", "transport": "POST /rpc", "description": "1C-facing templates/makets for a metadata object with decoded content roles and public properties. evidence_mode controls undecoded payload evidence: none, summary, full, raw. include_storage must be a JSON boolean true/false."}, {"name": "metadata.object.template.details", "transport": "POST /rpc", "description": "Detailed public template/maket information: maket name, format, features, safe preview status, and undecoded evidence. evidence_mode=full/raw returns broader payload evidence; raw offsets require include_storage=true."}, {"name": "templates.read", "transport": "POST /rpc", "description": "Read one or more object templates by owner selector/template name or direct route and return public template structure. MOXCEL templates include decoded dimensions, named-area ranges, text/parameter cells, column widths, cell identifiers, coverage, and capability diagnostics. Pass include_content=true for a bounded read-only export (decoded container as base64 plus extracted HTML/text blocks); max_content_bytes is capped at 1 MiB per item. Use view=summary|structure|full and sections/max_* to keep responses compact."}, {"name": "templates.analyze", "transport": "POST /rpc", "description": "Analyze object templates for named areas, parameters, widths, overlaps, cell coverage, likely merge candidates, and MOXCEL decoding capability diagnostics. Use view=summary|structure|full and sections/max_* to keep responses compact."}, {"name": "templates.map", "transport": "POST /rpc", "description": "Compact agent-facing template map. Returns summary structure and analysis for MXL/MOXCEL templates without the full decoded payload lists unless sections/max_* request them."}, {"name": "templates.areas.find", "transport": "POST /rpc", "description": "Find a template by extension/object query or direct route and return decoded named areas with coordinates from current template payloads. Use area_query/area_name/area_occurrence for focused report/print-form maket area lookup; include_coverage=false returns a compact coordinate list."}, {"name": "metadata.object.commands", "transport": "POST /rpc", "description": "1C-facing commands for a metadata object. CommandGroup selectors resolve the reverse CommonCommand.Group relation by public names; callers do not pass GUIDs. Physical record paths are hidden unless include_storage=true."}, {"name": "metadata.definition.find", "transport": "POST /rpc", "description": "Find where a 1C name is defined: top-level metadata objects, object attributes, tabular-section fields, form attributes/elements/events/commands, templates, commands, BSL routines, and extension definitions from DBNames-Ext/ConfigCAS. Accepts public refs such as Обработка. or Document., plus ref, kind/name/guid, or object_type/object_name/object_guid selectors. Returns public 1C locations, origin as configuration/extension when known, read selectors, and related_selectors for next adapter calls. A single metadata object match is promoted to top-level object. areas=metadata/extensions can work without an object selector; object/form/module areas require an object selector. When an object selector or form is passed and areas is omitted, search is scoped to the selected object/form to avoid a full configuration scan. Default is live verification; pass use_cache=true only when a fast local index is acceptable, or refresh_cache=true to rebuild the index after configuration/extension updates. No SQL/storage details unless include_storage=true."}, {"name": "metadata.route.resolve", "transport": "POST /rpc", "description": "Resolve live ConfigCAS/DBNames routes for extension metadata objects or child objects by extension, query, kind, or GUID."}, {"name": "metadata.resolve_overrides", "transport": "POST /rpc", "description": "Build the discovered routine override chain for a target object/routine. Default configuration_view=effective_working means the logical Designer snapshot (base, saved changes, and extension layers); runtime_applied is executable now and compare returns both. The chain records extension action evidence (insert_before/insert_after/replace/replace_with_control); it never presents raw storage tables as the programming API."}, {"name": "metadata.object.special.details", "transport": "POST /rpc", "description": "Backward-compatible kind-specific SQL details for Configuration, Constant, CommonAttribute, SessionParameter, FunctionalOption, FunctionalOptionsParameter, DocumentNumerator, IntegrationService, CommandGroup, Role, ScheduledJob, EventSubscription, WebService, HTTPService, and DocumentJournal. Role details include object rights, RLS conditions, and restriction templates from its SQL .0 payload. Prefer metadata.object.properties. For DocumentJournal pass include_column_types=true to resolve column types."}, {"name": "metadata.form.decode", "transport": "POST /rpc", "description": "Decode one form into events, elements, commands, attributes, module summary, and decoded form parameters. evidence_mode controls undecoded payload evidence: none, summary, full, raw. Raw offsets require include_storage=true."}, {"name": "metadata.form.owner_index.build", "transport": "POST /rpc", "description": "Build/refresh the SQL-backed form owner index for CommonForm and object-owned forms from extension routes or direct form SQL payloads. XML remains analysis/learning only."}, {"name": "metadata.form.write_target.resolve", "transport": "POST /rpc", "description": "Resolve an agent-facing saved-state form write target into table, file_name, profile section, path, current value, candidates, and writable properties."}, {"name": "metadata.form.write_target.verify", "transport": "POST /rpc", "description": "Read-only agent check for a form write target. Verifies whether a saved-state form target is currently writable or whether the adapter would need to prepare ConfigSave/ConfigCASSave first."}, {"name": "metadata.saved_state.prepare", "transport": "POST /rpc", "description": "Prepare an empty saved-state working copy by public 1C object name/ref and semantic layer. Default mode is plan; SQL insert requires allow_sql_saved_state_prepare=true and blocks on target collisions. include_storage=true exposes storage diagnostics."}, {"name": "metadata.saved_state.status", "transport": "POST /rpc", "description": "Read-only name-first overview of a base_saved_state or extension_saved_state layer. Public mode reports semantic aggregate state; include_storage=true exposes SQL rows/files, hashes, and diff selectors."}, {"name": "metadata.saved_state.diff", "transport": "POST /rpc", "description": "Agent-facing read-only comparison of a saved-state module with its active source. Prefer a 1C object ref or kind/name plus module_ordinal; generated module_ref and table/file_name remain accepted for follow-up tooling. Reports changed/unchanged, needs_prepare, hashes, and compact payload diff."}, {"name": "metadata.saved_state.changes.list", "transport": "POST /rpc", "description": "Read-only name-first list of pending saved-state changes. Filter by semantic layer; public rows resolve 1C object/form/module context and hide SQL coordinates; include_storage=true exposes per-file diff/write diagnostics."}, {"name": "metadata.saved_state.forms.search", "transport": "POST /rpc", "description": "Fast name-first saved-state form search by public owner ref, form, element, command, attribute, or text. Public rows expose 1C names and semantic selectors; include_storage=true opts into SQL files, GUIDs, brace paths, markers, and write diagnostics."}, {"name": "metadata.saved_state.modules.search", "transport": "POST /rpc", "description": "Fast name-first saved-state BSL module search over ConfigSave/ConfigCASSave. Accepts public ref or kind/name selectors and resolves SQL routes internally. Public results expose 1C owner/form/module names, previews, and name-first metadata.write.plan targets; include_storage=true additionally exposes module_ref handles and physical write guards."}, {"name": "metadata.form.write_matrix.build", "transport": "POST /rpc", "description": "Build a source-aware matrix of decoded saved-state form scalar properties and safe write-smoke candidates."}, {"name": "metadata.form.write_matrix.smoke", "transport": "POST /rpc", "description": "Run apply_and_rollback smoke writes for safe entries from metadata.form.write_matrix.build and report verified write routes."}, {"name": "metadata.form.element.write", "transport": "POST /rpc", "description": "Saved-state form element write planner. Resolves a decoded form element and builds a reviewable changes.propose payload for ConfigSave/ConfigCASSave. Requires allow_saved_state_write=true and does not write SQL."}, {"name": "metadata.form.element.write_apply", "transport": "POST /rpc", "description": "Orchestrate saved-state form element write: plan only, apply, or apply_and_rollback smoke run with semantic verification. Requires explicit write/apply gates."}, {"name": "metadata.form.target.move", "transport": "POST /rpc", "description": "Saved-state form structural move planner. Currently supports preserve-format sibling slot swap with apply/apply_and_rollback gates."}, {"name": "metadata.form.command_button.write", "transport": "POST /rpc", "description": "Plan/apply a form command workflow: optional BSL handler routine upsert plus form command and visible command button append. Recognizes CommonForm/top-level common forms and object-owned forms; prepares saved-state when needed. XML is analysis/learning input only, not the live adapter write transport."}, {"name": "metadata.form.command_button.verify", "transport": "POST /rpc", "description": "Read-only verification for a saved-state form command workflow: command, visible button, embedded handler routine, command-handler link, and button-command link. Accepts the same public form selectors as metadata.form.command_button.write."}, {"name": "metadata.module.write_apply", "transport": "POST /rpc", "description": "Orchestrate saved-state BSL module stream writes from module_ref/table/file_name: plan, apply, or apply_and_rollback. Requires explicit saved-state write/apply gates."}, {"name": "metadata.write.plan", "transport": "POST /rpc", "description": "Read-only name-first metadata write planner. For forms pass target.kind=form with extension/ref/form and element, command, or attribute plus edits; for modules reuse the public write_plan_target from metadata.saved_state.modules.search. The adapter resolves saved-state SQL handles internally, reports layer/provenance requirements, and never applies changes."}, {"name": "metadata.write.preflight", "transport": "POST /rpc", "description": "Read-only preflight for high-level writes. Combines metadata.write.plan with live saved-state verification and reports ready, needs_prepare, needs_resolution, or blocked before any write."}, {"name": "metadata.write.capabilities", "transport": "POST /rpc", "description": "Agent-facing matrix of what the adapter can read, plan, and write to the saved-state layer. SQL/storage details are hidden unless include_storage=true."}, {"name": "metadata.write", "transport": "POST /rpc", "description": "High-level metadata write orchestrator. Routes saved-state form, module, and scheduled-job schedule targets by public 1C names, builds reviewable proposals, and can apply with explicit saved-state SQL gates. Module writes support text, old/new, and routine_name/routine_text edits with expected_sha1/expected_text_sha1 guards."}, {"name": "metadata.write.history", "transport": "POST /rpc", "description": "List recent adapter write operations or fetch one operation_id, including status, routed method, target summary, backup ids, and full result for a specific operation."}, {"name": "metadata.write.rollback", "transport": "POST /rpc", "description": "Rollback a saved-state write by operation_id or backup_id using write history evidence. Requires allow_sql_saved_state_rollback=true."}, {"name": "code.write", "transport": "POST /rpc", "description": "Agent-facing BSL code write facade. Works with 1C names and code text, defaults to saving into the working saved-state layer, and hides SQL/storage details unless include_storage=true. Supports full module text, routine_name/routine_text, and unique old/new fragment replacement."}, {"name": "metadata.write_learning.capture_before", "transport": "POST /rpc", "description": "Capture a saved-state form baseline for write-rule learning. Stores decoded writable targets and storage sha1 without payload hex."}, {"name": "metadata.write_learning.capture_after", "transport": "POST /rpc", "description": "Capture a saved-state form after a manual Designer edit for write-rule learning."}, {"name": "metadata.write_learning.diff", "transport": "POST /rpc", "description": "Compare before/after write-learning captures and return changed writable form properties."}, {"name": "metadata.write_learning.infer_rule", "transport": "POST /rpc", "description": "Infer a metadata.write payload from a write-learning diff."}, {"name": "metadata.object.attributes", "transport": "POST /metadata/object/attributes or POST /rpc", "description": "High-level 1C object attributes and tabular sections. only must be a string: all, attributes, tabular_sections, dimensions, resources, or register_fields. Default is live verification and then cache update; pass use_cache=true only when a fast local index is acceptable, or refresh_cache=true to force refresh. Use only=attributes or only=tabular_sections for a smaller public response. include_storage and use_cache must be JSON booleans true/false, string values are invalid. SQL/storage traces are hidden unless include_storage=true."}, {"name": "metadata.object.full", "transport": "POST /metadata/object/full or POST /rpc", "description": "Start a long job for a full high-level 1C object profile: card, semantic sections, decoded forms, templates, commands, module handles, and optional parts evidence. evidence_mode=full/raw automatically includes parts_summary; raw offsets require include_storage=true. Poll adapter.job.get/mcp.job.get."}, {"name": "metadata.snapshot", "transport": "POST /metadata/snapshot or POST /rpc", "description": "1C-facing live metadata summary for a concrete base_id."}, {"name": "metadata.cache.status", "transport": "POST /rpc", "description": "Internal metadata identity cache status for one explicit base_id. No default database is used."}, {"name": "metadata.cache.lookup", "transport": "POST /rpc", "description": "Internal metadata identity cache lookup by explicit base_id and 1C object name/guid."}, {"name": "metadata.cache.rebuild", "transport": "POST /rpc", "description": "Start a long job that rebuilds the internal metadata cache for one explicit base_id. Returns job_id; poll adapter.job.get and cancel with adapter.job.cancel."}, {"name": "metadata.cache.invalidate", "transport": "POST /rpc", "description": "Invalidate internal metadata identity cache for one explicit base_id. Supports dry_run JSON boolean."}, {"name": "infobase.users.search", "transport": "POST /rpc", "description": "Default user lookup for 1C infobase users shown in Configurator. Reads safe fields from dbo.v8users. This is authoritative for platform identity, authentication flags, platform administrator flag, and RolesID; exact role names require the 1C runtime API and are never inferred from BSP profiles."}, {"name": "infobase.user.get", "transport": "POST /rpc", "description": "Get one 1C infobase/Configurator user by exact name or platform user id. Returns safe dbo.v8users fields and explicit role-resolution limits. BSP users, groups, and profiles are a separate layer."}, {"name": "infobase.user.password.status", "transport": "POST /rpc", "description": "Read whether one exact infobase/Configurator user has an empty or non-empty password without exposing hashes or dbo.v8users.Data. Also reports whether standard authentication is enabled."}, {"name": "infobase.user.password.capabilities", "transport": "POST /rpc", "description": "Report whether the 1C runtime bridge for Configurator-user password operations is configured, including explicit unauthenticated test-mode status."}, {"name": "infobase.user.password.set", "transport": "POST /rpc", "description": "Set a new password for one exact infobase/Configurator user through a guarded SQL update of dbo.v8users.Data. Computes the case-sensitive and uppercase SHA-1/Base64 pair in memory, verifies transactional readback, and never persists or echoes the clear-text password."}, {"name": "infobase.user.password.clear", "transport": "POST /rpc", "description": "Clear the password of one exact infobase/Configurator user through a guarded SQL update of dbo.v8users.Data. Decodes the per-row container, replaces only the current password hash pair with empty-password hashes, verifies readback in one transaction, and requires exact user_id confirmation plus allow_password_clear=true."}, {"name": "access.snapshot.extract", "transport": "POST /rpc or POST /access/snapshot/extract", "description": "Discover or extract a normalized BSP access snapshot from live SQL: BSP catalog users, access groups, profiles, technical roles, memberships, role permissions, and data restrictions. This is not the Configurator user list and is not authoritative for platform authentication or direct platform role assignments."}, {"name": "access.graph.build", "transport": "POST /rpc or POST /access/graph", "description": "Build a normalized 1C access graph from an access snapshot: users, access groups, profiles, roles, data restrictions, and effective permissions with source chains."}, {"name": "access.user.explain", "transport": "POST /rpc or POST /access/user/explain", "description": "Explain BSP access for one BSP catalog user, including profile/group/role source chains and optional object/action filtering. For an ordinary request about users, start with infobase.users.search; BSP results do not replace Configurator role assignments."}, {"name": "access.users.search", "transport": "POST /rpc or POST /access/users/search", "description": "Search BSP catalog users by name, login, id/ref tail, or fuzzy fragment and return candidates for access.user.explain. Use only when the caller explicitly asks about BSP users, access groups, profiles, or RLS; ordinary 'users' means infobase/Configurator users."}, {"name": "access.keys.query", "transport": "POST /rpc or POST /access/keys/query", "description": "Page through BSP access key registers by group, user_set, object, access_set, or all. In object mode pass object_ref for a metadata object name and record_ref for a concrete application-data record; kind remains the query area. Legacy raw BSP object/object_id filters remain supported."}, {"name": "access.object_keys.resolve", "transport": "POST /rpc or POST /access/object-keys/resolve", "description": "Page through BSP object access keys. Pass object_ref for a public 1C metadata name and record_ref for a concrete application-data record; the adapter resolves internal object_sql_number/object_id values and readable presentations."}, {"name": "access.object.explain", "transport": "POST /rpc or POST /access/object/explain", "description": "Explain who can see a BSP-protected data record. Pass object_ref for its metadata object and record_ref for the concrete record; legacy raw BSP object/object_id/access_key filters remain supported."}, {"name": "access.object.roles", "transport": "POST /rpc or POST /access/object/roles", "description": "Find BSP roles that grant permissions for one metadata object and summarize read/insert/update/delete rights."}, {"name": "access.object.subjects", "transport": "POST /rpc or POST /access/object/subjects", "description": "Find roles, profiles, access groups, and users that receive permissions for one metadata object."}, {"name": "access.rls.discover", "transport": "POST /rpc or POST /access/rls/discover", "description": "Discover metadata candidates for BSP/RLS/data restriction extraction by names such as Огранич, Доступ, RLS, and Ключ."}, {"name": "access.role.profiles", "transport": "POST /rpc or POST /access/role/profiles", "description": "Find BSP access profiles that include a role, and access groups that use those profiles."}, {"name": "access.role.users", "transport": "POST /rpc or POST /access/role/users", "description": "Find users that receive a BSP role through access profiles and access groups, with fuzzy role matching."}, {"name": "access.role.audit_export", "transport": "POST /rpc or POST /access/role/audit-export", "description": "Export a flat audit report for role -> profile -> access group -> user, as JSON rows and optionally CSV text."}, {"name": "access.role.audit_analyze", "transport": "POST /rpc or POST /access/role/audit-analyze", "description": "Analyze role access audit rows and return risk findings for broad groups, external users, fuzzy matches, multiple paths, and high user counts."}, {"name": "semantic.cache.search", "transport": "POST /rpc", "description": "Search prepared semantic documents from decoded artifact cache. Results are candidate-only by default; pass validate_candidates=true to SHA-check top matches against current source bytes before using them."}, {"name": "semantic.cache.status", "transport": "POST /rpc", "description": "Report semantic document cache readiness by vector status, kind, and embedding model, including pending counts for embedding workers."}, {"name": "semantic.cache.validate", "transport": "POST /rpc", "description": "Validate one semantic cache candidate by document_id against current source bytes. Fresh matches can be used through read_selector; changed payloads are marked non-embedded and returned as stale."}, {"name": "semantic.cache.validate_batch", "transport": "POST /rpc", "description": "Validate semantic cache candidates in batches by document_ids or kind/vector_status filters. Each candidate is checked against current source bytes before use."}, {"name": "semantic.cache.refresh", "transport": "POST /rpc", "description": "Re-read and re-decode one semantic cache document from current source bytes, update semantic/artifact caches, and queue a fresh embedding when content changed."}, {"name": "semantic.cache.rebuild", "transport": "POST /rpc", "description": "Warm semantic/artifact caches from fresh extension route cache entries. Currently supports Template/MOXCEL routes and queues refreshed documents for embeddings."}, {"name": "semantic.cache.pending", "transport": "POST /rpc", "description": "List semantic cache documents that need embeddings. Returns text previews and content_sha1 preconditions for safe external embedding workers."}, {"name": "semantic.cache.embedding.upsert", "transport": "POST /rpc", "description": "Store an embedding for one semantic cache document only when document_id and content_sha1 still match the current source-derived document."}, {"name": "metadata.module_owner_cache.prune", "transport": "POST /rpc", "description": "Targeted local module-owner cache cleanup. Prefer a 1C owner ref or kind/name; owner GUID is resolved internally. Generated module_ref selectors remain accepted. Supports dry_run JSON boolean and never writes platform SQL."}, {"name": "extensions.list", "transport": "GET /extensions or POST /rpc", "description": "Публичный список расширений конфигурации: имя, порядок, дата обновления, активность и GUID. require base_id, limit >= 1 and offset >= 0 when provided. Технические поля скрыты, если явно не передан include_storage=true."}, {"name": "extension.cache.status", "transport": "POST /rpc", "description": "Report extension route cache freshness grouped by extension/kind, including stale counts and oldest validation timestamps."}, {"name": "extension.cache.rebuild", "transport": "POST /rpc", "description": "Warm the validated extension route cache from live extension manifests and descriptor payloads. Use extension/kind/max_items to scope the rebuild; no vector or semantic result is treated as authoritative."}, {"name": "extension.cache.validate", "transport": "POST /rpc", "description": "Validate cached extension routes against current live manifests and mark stale entries. Use before programming sessions or scheduled refreshes to keep source cache honest."}, {"name": "extension.objects.find", "transport": "POST /rpc", "description": "Fast search for extension metadata objects by extension name/GUID, object kind, GUID, or name fragment. Defaults to state=working: saved ConfigCASSave forms are returned over active/cache/manifest objects and marked saved_only or saved_override. Use state=active for activated metadata only, state=save for saved-state only, state=both to compare. Cached route candidates are live-validated before use; pass refresh_cache=true to skip cached candidates and rebuild from live sources. Pass full_scan=true only when a slower ConfigCAS payload scan is required. Returns routes and safe read selectors."}, {"name": "metadata.code_index.build", "transport": "POST /rpc", "description": "Build or warm a SQL-derived BSL module cache from live storage. SQL remains authoritative; cached rows store payload/text hashes and optional local vector chunks."}, {"name": "metadata.code_index.status", "transport": "POST /rpc", "description": "Report BSL code index and vector chunk cache counts. Status is informational; individual answers still require SQL verification."}, {"name": "metadata.code_index.search", "transport": "POST /rpc", "description": "Fast BSL lexical search over metadata_code_index_cache. Default mode verifies candidates against live SQL hashes before returning freshness."}, {"name": "metadata.code_index.verify", "transport": "POST /rpc", "description": "Verify one cached module_ref against current live SQL payload/text hashes and report cache_hit_verified or cache_hit_stale."}, {"name": "metadata.code_index.refresh_changed", "transport": "POST /rpc", "description": "Verify cached search candidates and rebuild changed modules from live SQL. Intended for small operational refreshes, not full rebuilds."}, {"name": "metadata.code_vector.search", "transport": "POST /rpc", "description": "Vector-like search over cached BSL chunks using local hashing embeddings or supplied query_embedding. Candidates are revalidated by default; vector cache is never authoritative."}, {"name": "schema.tables.list", "transport": "POST /rpc", "description": "Low-level diagnostic table list for developers. Requires diagnostic=true. Parameters: limit JSON integer, timeout_seconds JSON integer, like JSON string, include_columns JSON boolean."}, {"name": "storage.files.list", "transport": "POST /rpc", "description": "Low-level diagnostic list of storage payload records. Requires diagnostic=true."}, {"name": "storage.file.get", "transport": "POST /rpc", "description": "Low-level diagnostic read of one storage payload record. Requires diagnostic=true. Parameters: table, file_name, include_payload JSON boolean, timeout_seconds JSON integer."}, {"name": "storage.saved_state.apply_proposal", "transport": "POST /rpc", "description": "Apply a reviewed encoded proposal to ConfigSave/ConfigCASSave with backup, sha1 precondition, transaction, and readback verification. Requires allow_sql_saved_state_apply=true."}, {"name": "storage.saved_state.rollback", "transport": "POST /rpc", "description": "Rollback a saved-state apply by backup_id or backup_path. Requires allow_sql_saved_state_rollback=true."}, {"name": "storage.saved_state.backups.list", "transport": "POST /rpc", "description": "List local saved-state apply backups with optional base_id/table/file_name filters."}, {"name": "metadata.dbnames.summary", "transport": "POST /rpc", "description": "Low-level diagnostic DBNames summary for developers. Requires diagnostic=true."}, {"name": "code.search", "transport": "POST /rpc", "description": "Search decoded BSL in configuration_view=effective_working by default: the logical Designer snapshot, with saved development changes and extension layers preferred. Use runtime_applied only for code executable now, or compare for both. Object scope accepts ref, kind/name/guid, or object_type/object_name/object_guid. Every item has read_selector.method is code.read; reuse that selector directly. Results expose logical owners and read selectors, not Config/ConfigSave internals."}, {"name": "code.read", "transport": "POST /rpc", "description": "Read module or routine text in configuration_view=effective_working by default, by logical owner selector or a prior code.search read_selector. Pass module_ref from code.search read_selector unchanged when it is present. runtime_applied reads code executable now; compare returns both. The response identifies the view so saved Designer changes are never mistaken for already activated runtime code."}, {"name": "code.symbol.resolve", "transport": "POST /rpc", "description": "Conservatively resolve a BSL expression inside a concrete module/routine context. Full 1C paths and context-proven members are metadata; routine parameters, local variables, and short object names remain code symbols."}, {"name": "templates.bindings", "transport": "POST /rpc", "description": "Extract template dependencies, parameters/fields bindings and owner chain for a report/processing/form object."}, {"name": "diagnostics.call_chain", "transport": "POST /rpc", "description": "Build diagnostic call chain for a code entrypoint: entry method owner/module, possible overrides, and detected static usage links in the same object scope."}, {"name": "payload.diff", "transport": "POST /rpc", "description": "Low-level diagnostic comparison of two 1C payloads from live storage or inline bytes/text. Returns byte sha1/size changes, text unified diff, tree scalar changes, string changes, and compact undecoded evidence. Requires diagnostic=true."}, {"name": "codec.decode", "transport": "POST /rpc", "description": "Low-level diagnostic decode of a 1C payload record into text/tree. Requires diagnostic=true."}, {"name": "codec.encode", "transport": "POST /rpc", "description": "Low-level diagnostic encode of text/tree into a 1C payload envelope. Requires diagnostic=true."}, {"name": "modules.search", "transport": "POST /modules/search or POST /rpc", "description": "Live BSL text search. Defaults to state=working: saved tables ConfigCASSave/ConfigSave are preferred over active ConfigCAS/Config for programming-time code analysis; use state=active/save/both when comparing. Pass ref, kind/name/guid, object_type/object_name/object_guid, or module_ordinal to search inside one object's modules quickly. Supports `extension` for module owner scoping by extension, `routine_name` for narrowing to one procedure/function before text match, and owner_scan_limit for resolve_owners scans. Every public match includes read_selector.method=modules.read and either an object selector or opaque module_ref. Global search is partial by scan_limit unless increased (scope='all' checks ConfigCAS/ConfigCASSave + Config/ConfigSave). Counts and diagnostics.owner_resolution explain incomplete owner recovery. Physical module ids are hidden unless include_storage=true."}, {"name": "modules.read", "transport": "GET /modules/read or POST /rpc", "description": "Read a BSL module by object selector (ref, kind/name/guid, object_type/object_name/object_guid) and module_ordinal, or by module_ref from modules.search read_selector. Supports mode=summary, preview=true, max_chars, offset, routine_name, include_text=false. Set table=ConfigSave/ConfigCASSave to read saved data directly. Selected BSL fragment is returned in text; preview=true also returns preview as a compatibility alias. Internal module_id is accepted only for tooling; source/payload are hidden unless include_storage=true."}, {"name": "query.validate", "transport": "POST /query/validate or POST /rpc", "description": "Проверка read-only SQL-запроса."}, {"name": "query.run", "transport": "POST /query/run or POST /rpc", "description": "Low-level diagnostic execution of a validated read-only SQL query. Requires diagnostic=true."}, {"name": "data.schema", "transport": "POST /rpc", "description": "Resolve a 1C object by public name and return its logical application-data schema and type metadata."}, {"name": "data.list", "transport": "POST /rpc", "description": "Read application data by public 1C object selector with logical fields, exact filters, ordering, and pagination."}, {"name": "data.get", "transport": "POST /rpc", "description": "Read one application data record by public 1C object selector and 32-character reference id."}, {"name": "data.count", "transport": "POST /rpc", "description": "Count application data records by public 1C object selector and exact logical filters."}, {"name": "data.query", "transport": "POST /rpc", "description": "Universal logical application-data query facade over data.list/data.count."}, {"name": "data.present", "transport": "POST /rpc", "description": "Resolve a 1C application-data reference to a public presentation without exposing SQL identifiers."}, {"name": "data.movements", "transport": "POST /rpc", "description": "Read register movements for a recorder reference using a public register selector."}, {"name": "data.virtual", "transport": "POST /rpc", "description": "Read 1C-style register virtual views: slices for information registers and balances/turnovers for accumulation registers."}, {"name": "changes.propose", "transport": "POST /changes/propose or POST /rpc", "description": "Builds a reviewable encoded payload proposal from live source + path edits. Does not write to SQL."}, ] DATA_OBJECT_SELECTOR_METHODS = frozenset( { "data.schema", "data.list", "data.get", "data.count", "data.query", "data.present", "data.movements", "data.virtual", } ) METHOD_INPUT_SCHEMAS = { "repository.lock.confirm": { "type": "object", "required": ["base_id", "request_id", "user_confirmed_locked"], "properties": { "base_id": {"type": "string", "description": "Adapter base identifier."}, "request_id": {"type": "string", "description": "Exact id returned by repository.lock.request."}, "user_confirmed_locked": {"type": "boolean", "const": True, "description": "Explicit statement that the user has captured the exact request scope in Configurator."}, "confirmed_repository_user": {"type": "string", "description": "Optional only when repository_user is not saved in the resolved layer setting; otherwise, if supplied, it must match."}, }, "example": {"base_id": "neft", "request_id": "rreq-…", "user_confirmed_locked": True}, "notes": [ "Do not repeat objects, operation, or layer_id: they are taken from the persisted request.", "Use the next_call object returned by repository.lock.request whenever available.", "The successful response returns write_context with lock_session_id; copy it into write preflight and write operations.", ], }, } METHOD_INPUT_SCHEMAS.update( { method: { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": { "base_id": {"type": "string", "description": "Adapter base identifier."}, "object_ref": { "type": "string", "description": "Preferred public 1C object ref, for example Справочники.Номенклатура or РегистрСведений.КурсыВалют.", }, "ref": { "type": "string", "description": "Public object ref; data.get/data.present also accept a legacy 32-character record ref when object_ref identifies the object.", }, "kind": {"type": "string", "description": "1C metadata kind in English or Russian."}, "name": {"type": "string", "description": "Exact 1C metadata object name."}, "guid": {"type": "string", "description": "Metadata object GUID; callers normally should prefer a public name."}, "object_type": {"type": "string", "description": "MCP alias of kind."}, "object_name": {"type": "string", "description": "MCP alias of name."}, "object_guid": {"type": "string", "description": "MCP alias of guid."}, "record_ref": {"type": "string", "description": "32-character 1C application-data reference id."}, "recorder_ref": {"type": "string", "description": "32-character recorder reference for data.movements."}, }, } for method in DATA_OBJECT_SELECTOR_METHODS } ) METHOD_INPUT_SCHEMAS["metadata.support.decode"] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": { "base_id": {"type": "string", "description": "Adapter base identifier."}, "ref": { "type": "string", "description": "Optional public 1C object ref whose supplier support rule should be returned.", }, "kind": {"type": "string", "description": "Optional metadata kind in English or Russian."}, "name": {"type": "string", "description": "Optional exact 1C object name."}, "guid": {"type": "string", "description": "Optional metadata object GUID."}, "object_type": {"type": "string", "description": "MCP alias of kind."}, "object_name": {"type": "string", "description": "MCP alias of name."}, "object_guid": {"type": "string", "description": "MCP alias of guid."}, "member_ref": { "type": "string", "description": "Optional full public child ref, for example Catalog.Номенклатура.Attribute.Артикул.", }, "child_ref": {"type": "string", "description": "Alias of member_ref."}, "member_kind": { "type": "string", "description": "Optional child category: Attribute, TabularSection, Dimension, or Resource; Russian aliases are accepted.", }, "member_name": {"type": "string", "description": "Optional exact child metadata name."}, "canonical_path": {"type": "string", "description": "Alias of a full public object/member path."}, "layer_id": { "type": "string", "description": "base or extension:; defaults to base.", }, }, } METHOD_INPUT_SCHEMAS["metadata.object.property.write"] = { "type": "object", "required": ["base_id", "property", "value"], "additionalProperties": True, "properties": { "base_id": {"type": "string", "description": "Adapter base identifier."}, "ref": {"type": "string", "description": "Preferred public 1C object ref."}, "kind": {"type": "string", "description": "Optional metadata kind."}, "name": {"type": "string", "description": "Optional exact 1C object name."}, "guid": {"type": "string", "description": "Optional metadata GUID; names are preferred."}, "object_type": {"type": "string", "description": "MCP alias of kind."}, "object_name": {"type": "string", "description": "MCP alias of name."}, "object_guid": {"type": "string", "description": "MCP alias of guid."}, "extension": {"type": "string", "description": "Optional extension name or GUID."}, "layer": { "type": "string", "enum": ["base_saved_state", "extension_saved_state"], "description": "Semantic saved-state layer; inferred from extension when omitted.", }, "property": { "type": "string", "enum": ["synonym", "comment"], "description": "Safe scalar identity property. Object name/rename is intentionally unsupported.", }, "value": {"type": "string", "description": "Requested scalar value; an empty string is allowed."}, "language": {"type": "string", "description": "Existing synonym locale to edit; defaults to ru."}, "expected_old": {"type": "string", "description": "Optional semantic compare-and-set guard."}, "expected_sha1": {"type": "string", "description": "Optional saved-state payload compare-and-set guard."}, "execution_mode": {"type": "string", "enum": ["plan", "apply", "apply_and_verify", "apply_and_rollback"]}, "allow_saved_state_write": {"type": "boolean"}, "auto_prepare_saved_state": {"type": "boolean"}, "allow_sql_saved_state_prepare": {"type": "boolean"}, "allow_sql_saved_state_apply": {"type": "boolean"}, "allow_sql_saved_state_rollback": {"type": "boolean"}, "include_storage": {"type": "boolean"}, }, } METHOD_INPUT_SCHEMAS["metadata.object.member.add"] = { "type": "object", "required": ["base_id", "template_member_ref", "new_member_name"], "additionalProperties": True, "properties": { "base_id": {"type": "string"}, "ref": {"type": "string", "description": "Optional public parent object ref; inferred from template_member_ref."}, "kind": {"type": "string"}, "name": {"type": "string"}, "object_type": {"type": "string"}, "object_name": {"type": "string"}, "template_member_ref": { "type": "string", "description": "Full public ref of an existing Attribute to clone.", }, "new_member_name": {"type": "string", "description": "New 1C Attribute name."}, "new_member_synonym": {"type": "string", "description": "Optional ru synonym; defaults to the new name."}, "new_member_comment": {"type": "string", "description": "Optional comment; defaults to an empty string and is never inherited from the template."}, "extension": {"type": "string"}, "layer": {"type": "string", "enum": ["base_saved_state", "extension_saved_state"]}, "expected_sha1": {"type": "string"}, "execution_mode": {"type": "string", "enum": ["plan", "apply", "apply_and_verify", "apply_and_rollback"]}, "allow_saved_state_write": {"type": "boolean"}, "auto_prepare_saved_state": {"type": "boolean"}, "allow_sql_saved_state_prepare": {"type": "boolean"}, "allow_sql_saved_state_apply": {"type": "boolean"}, "allow_sql_saved_state_rollback": {"type": "boolean"}, "include_storage": {"type": "boolean"}, }, } METHOD_INPUT_SCHEMAS["metadata.write"] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": { "base_id": {"type": "string", "description": "Adapter base identifier."}, "target": { "type": "object", "additionalProperties": True, "description": "Name-first target. Use kind/area=object|form|module|schedule plus a public ref, kind/name, or canonical_path.", "properties": { "kind": {"type": "string", "enum": ["object", "form", "module", "schedule"]}, "area": {"type": "string", "enum": ["object", "form", "module", "schedule"]}, "ref": {"type": "string", "description": "Public 1C object/member ref."}, "canonical_path": {"type": "string", "description": "Public 1C code or metadata path."}, "extension": {"type": "string", "description": "Public extension name for an extension-owned form or module."}, "form": {"type": "string", "description": "Exact 1C form name."}, "element": {"type": "string", "description": "Exact 1C form element name."}, "command": {"type": "string", "description": "Exact 1C form command name."}, "attribute": {"type": "string", "description": "Exact 1C form attribute name."}, "module": {"type": "string", "description": "Public module role/name returned by saved-state module search."}, "qualified_name": {"type": "string", "description": "Public qualified form or module name returned by search."}, "stream_ordinal": {"type": "integer", "minimum": 1, "description": "Public 1-based stream ordinal returned by saved-state module search."}, "property": {"type": "string"}, "operation": { "type": "string", "description": "Operation such as add_attribute, property_change, replace, or upsert_routine.", }, "template_member_ref": {"type": "string"}, "new_member_name": {"type": "string"}, }, }, "target_kind": {"type": "string", "enum": ["object", "form", "module", "schedule"]}, "ref": {"type": "string", "description": "Optional public 1C object ref."}, "kind": {"type": "string", "description": "Optional metadata kind in English or Russian."}, "name": {"type": "string", "description": "Optional exact 1C object name."}, "object_type": {"type": "string", "description": "MCP alias of kind."}, "object_name": {"type": "string", "description": "MCP alias of name."}, "extension": {"type": "string"}, "property": {"type": "string"}, "value": {}, "edits": { "type": "array", "items": { "type": "object", "required": ["property", "value"], "properties": { "property": {"type": "string"}, "value": {}, }, }, }, "schedule": {"type": "object", "additionalProperties": True}, "template_member_ref": {"type": "string"}, "new_member_name": {"type": "string"}, "new_member_synonym": {"type": "string"}, "new_member_comment": {"type": "string"}, "execution_mode": { "type": "string", "enum": ["plan", "apply", "apply_and_verify", "apply_and_rollback"], }, "mode": { "type": "string", "enum": ["plan", "apply", "apply_and_verify", "apply_and_rollback"], }, "allow_saved_state_write": {"type": "boolean"}, "allow_sql_saved_state_prepare": {"type": "boolean"}, "allow_sql_saved_state_apply": {"type": "boolean"}, "allow_sql_saved_state_rollback": {"type": "boolean"}, }, "examples": [ { "base_id": "upo_test", "target": { "area": "object", "ref": "Catalog.Номенклатура", "property": "synonym", }, "value": "Номенклатура", "mode": "plan", }, { "base_id": "upo_test", "target": { "area": "object", "operation": "add_attribute", "template_member_ref": "Catalog.Номенклатура.Attribute.Артикул", "new_member_name": "КодПоставщика", }, "mode": "plan", }, { "base_id": "upo_test", "target": { "kind": "form", "extension": "test2", "ref": "Catalog.test2", "form": "t_Форма", "command": "ЗаменаДомена", }, "edits": [{"property": "Заголовок", "value": "Замена домена"}], "mode": "plan", }, { "base_id": "upo_test", "target": { "kind": "schedule", "ref": "ScheduledJob.ОбменДанными", }, "schedule": {"begin_time": "09:00:00"}, "mode": "plan", }, ], } METHOD_INPUT_SCHEMAS["metadata.write.plan"] = { "type": "object", "required": ["base_id", "target"], "additionalProperties": True, "properties": { "base_id": {"type": "string", "description": "Adapter base identifier."}, "target": { "type": "object", "required": ["kind"], "additionalProperties": True, "description": "Prefer a public name-first selector; storage handles are optional follow-up values resolved internally from names.", "properties": { "kind": {"type": "string", "enum": ["form", "module", "metadata"]}, "ref": {"type": "string", "description": "Preferred public owner ref, for example Catalog.Номенклатура."}, "extension": {"type": "string", "description": "Public extension name."}, "form": {"type": "string", "description": "Exact 1C form name."}, "element": {"type": "string", "description": "Exact form element name."}, "command": {"type": "string", "description": "Exact form command name."}, "attribute": {"type": "string", "description": "Exact form attribute name."}, "module": {"type": "string", "description": "Public module role/name returned by metadata.saved_state.modules.search."}, "qualified_name": {"type": "string", "description": "Public qualified name returned by saved-state search."}, "stream_ordinal": {"type": "integer", "minimum": 1}, "canonical_path": {"type": "string", "description": "Full public 1C metadata/code path when one is already known."}, }, }, "edits": { "type": "array", "description": "Form property edits. Planning resolves the target but does not apply them.", "items": { "type": "object", "required": ["property", "value"], "properties": { "property": {"type": "string", "description": "Public property presentation or canonical property name."}, "value": {}, }, }, }, "intent": {"type": "object", "additionalProperties": True, "description": "Module or metadata operation intent."}, "resolve_origin": {"type": "boolean", "description": "Resolve a canonical path origin when no saved-state selector is available."}, "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 300}, }, "examples": [ { "base_id": "upo_test", "target": { "kind": "form", "extension": "test2", "ref": "Catalog.test2", "form": "t_Форма", "command": "ЗаменаДомена", }, "edits": [{"property": "Заголовок", "value": "Замена домена"}], "resolve_origin": False, }, { "base_id": "upo_test", "target": { "kind": "module", "ref": "Catalog.Номенклатура", "module": "Модуль менеджера", "stream_ordinal": 1, }, "intent": { "operation": "replace_with_control", "control_fragment": "Процедура Обновить()", "new": "Процедура Обновить()\nКонецПроцедуры", }, "resolve_origin": False, }, ], } METHOD_INPUT_SCHEMAS["metadata.write.preflight"] = copy.deepcopy(METHOD_INPUT_SCHEMAS["metadata.write.plan"]) METHOD_INPUT_SCHEMAS["metadata.code_index.build"] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": { "base_id": {"type": "string", "description": "Adapter base identifier."}, "ref": { "type": "string", "description": "Optional public 1C object ref. Omit the object selector only for an intentional bounded global scan.", }, "kind": {"type": "string", "description": "Optional metadata kind in English or Russian."}, "name": {"type": "string", "description": "Optional exact 1C object name."}, "guid": {"type": "string", "description": "Optional metadata object GUID."}, "object_type": {"type": "string", "description": "MCP alias of kind."}, "object_name": {"type": "string", "description": "MCP alias of name."}, "object_guid": {"type": "string", "description": "MCP alias of guid."}, "extension_guid": {"type": "string", "description": "Optional extension layer GUID."}, "table": {"type": "string", "description": "Optional explicit storage state; normally resolved automatically."}, "max_items": {"type": "integer", "minimum": 1, "maximum": 20000}, "scan_limit": {"type": "integer", "minimum": 1, "maximum": 50000}, "include_vectors": {"type": "boolean"}, }, } METHOD_INPUT_SCHEMAS["metadata.form.owner_index.build"] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": { "base_id": {"type": "string", "description": "Adapter base identifier."}, "ref": { "type": "string", "description": "Optional public 1C owner ref, for example Обработки.ОбменДанными or ОбщиеФормы.Параметры.", }, "kind": {"type": "string", "description": "Optional owner metadata kind in English or Russian."}, "name": {"type": "string", "description": "Optional exact owner object name."}, "guid": {"type": "string", "description": "Optional owner metadata GUID."}, "object_type": {"type": "string", "description": "MCP alias of owner kind."}, "object_name": {"type": "string", "description": "MCP alias of owner name."}, "object_guid": {"type": "string", "description": "MCP alias of owner GUID."}, "form": {"type": "string", "description": "Optional exact form name inside the selected owner."}, "form_name": {"type": "string", "description": "Alias of form."}, "extension": {"type": "string", "description": "Optional extension name or GUID."}, "table": {"type": "string", "description": "Optional direct diagnostic storage route table."}, "file_name": {"type": "string", "description": "Optional direct diagnostic storage file."}, }, } METHOD_INPUT_SCHEMAS["metadata.form.command_button.verify"] = { "type": "object", "required": ["base_id", "command_name"], "additionalProperties": True, "properties": { "base_id": {"type": "string", "description": "Adapter base identifier."}, "ref": {"type": "string", "description": "Optional public 1C form-owner ref."}, "kind": {"type": "string", "description": "Optional owner metadata kind."}, "name": {"type": "string", "description": "Optional exact owner object name."}, "guid": {"type": "string", "description": "Optional owner metadata GUID."}, "object_type": {"type": "string", "description": "MCP alias of owner kind."}, "object_name": {"type": "string", "description": "MCP alias of owner name."}, "object_guid": {"type": "string", "description": "MCP alias of owner GUID."}, "form": {"type": "string", "description": "Exact nested form name."}, "form_name": {"type": "string", "description": "Alias of form."}, "command_name": {"type": "string", "description": "Exact form command name."}, "button_name": {"type": "string", "description": "Expected button name; defaults to command_name."}, "handler_name": {"type": "string", "description": "Expected handler routine; defaults to command_name."}, "extension": {"type": "string", "description": "Optional extension name or GUID."}, "include_storage": {"type": "boolean"}, }, } METHOD_INPUT_SCHEMAS["metadata.form.command_button.write"] = { "type": "object", "required": ["base_id", "command_name"], "additionalProperties": True, "properties": { "base_id": {"type": "string", "description": "Adapter base identifier."}, "ref": {"type": "string", "description": "Optional public 1C form-owner ref."}, "kind": {"type": "string", "description": "Optional owner metadata kind."}, "name": {"type": "string", "description": "Optional exact owner object name."}, "guid": {"type": "string", "description": "Optional owner metadata GUID."}, "object_type": {"type": "string", "description": "MCP alias of owner kind."}, "object_name": {"type": "string", "description": "MCP alias of owner name."}, "object_guid": {"type": "string", "description": "MCP alias of owner GUID."}, "form": {"type": "string", "description": "Exact nested form name."}, "form_name": {"type": "string", "description": "Alias of form."}, "form_guid": {"type": "string", "description": "Optional form GUID, distinct from the owner GUID."}, "command_name": {"type": "string", "description": "Exact command name to upsert."}, "button_name": {"type": "string", "description": "Button name; defaults to command_name."}, "handler_name": {"type": "string", "description": "Handler routine name; defaults to command_name."}, "extension": {"type": "string", "description": "Optional extension name or GUID."}, "execution_mode": {"type": "string", "enum": ["plan", "apply", "apply_and_verify", "apply_and_rollback"]}, "allow_saved_state_write": {"type": "boolean"}, "allow_sql_saved_state_apply": {"type": "boolean"}, "allow_sql_saved_state_rollback": {"type": "boolean"}, }, } _FORM_WRITE_TARGET_SELECTOR_PROPERTIES = { "base_id": {"type": "string", "description": "Adapter base identifier."}, "ref": {"type": "string", "description": "Optional public 1C form-owner ref."}, "kind": {"type": "string", "description": "Optional owner metadata kind."}, "name": {"type": "string", "description": "Optional exact owner object name."}, "guid": {"type": "string", "description": "Optional owner metadata GUID."}, "object_type": {"type": "string", "description": "MCP alias of owner kind."}, "object_name": {"type": "string", "description": "MCP alias of owner name."}, "object_guid": {"type": "string", "description": "MCP alias of owner GUID."}, "form": {"type": "string", "description": "Exact nested form name."}, "form_name": {"type": "string", "description": "Alias of form."}, "form_guid": {"type": "string", "description": "Optional form GUID, distinct from the owner GUID."}, "element": {"type": "string", "description": "Exact form element name."}, "element_name": {"type": "string", "description": "Alias of element."}, "command": {"type": "string", "description": "Exact form command name."}, "attribute": {"type": "string", "description": "Exact form attribute name."}, "property": {"type": "string", "description": "Scalar property to resolve."}, "extension": {"type": "string", "description": "Optional extension name or GUID."}, "table": {"type": "string", "description": "Saved-state table."}, "file_name": {"type": "string", "description": "Optional concrete saved-state file."}, "include_storage": {"type": "boolean"}, } for _form_write_target_method in ("metadata.form.write_target.resolve", "metadata.form.write_target.verify"): METHOD_INPUT_SCHEMAS[_form_write_target_method] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": dict(_FORM_WRITE_TARGET_SELECTOR_PROPERTIES), } for _form_element_write_method in ("metadata.form.element.write", "metadata.form.element.write_apply"): METHOD_INPUT_SCHEMAS[_form_element_write_method] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": { **_FORM_WRITE_TARGET_SELECTOR_PROPERTIES, "edits": {"type": "array", "description": "Non-empty scalar property edit list."}, "execution_mode": {"type": "string", "enum": ["plan", "apply", "apply_and_verify", "apply_and_rollback"]}, "allow_saved_state_write": {"type": "boolean"}, "allow_sql_saved_state_apply": {"type": "boolean"}, "allow_sql_saved_state_rollback": {"type": "boolean"}, }, } METHOD_INPUT_SCHEMAS["metadata.form.target.move"] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": { **_FORM_WRITE_TARGET_SELECTOR_PROPERTIES, "from_element": {"type": "string", "description": "Exact source sibling element name."}, "to_element": {"type": "string", "description": "Exact destination sibling element name."}, "with_element": {"type": "string", "description": "Alias of to_element for a sibling swap."}, "after_element": {"type": "string", "description": "Existing sibling used as the destination slot."}, "execution_mode": {"type": "string", "enum": ["plan", "apply", "apply_and_verify", "apply_and_rollback"]}, "allow_saved_state_write": {"type": "boolean"}, "allow_sql_saved_state_apply": {"type": "boolean"}, "allow_sql_saved_state_rollback": {"type": "boolean"}, }, } for _form_write_matrix_method in ("metadata.form.write_matrix.build", "metadata.form.write_matrix.smoke"): METHOD_INPUT_SCHEMAS[_form_write_matrix_method] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": { **_FORM_WRITE_TARGET_SELECTOR_PROPERTIES, "max_candidates": {"type": "integer", "minimum": 1, "maximum": 5000}, "allow_sql_saved_state_apply": {"type": "boolean"}, "allow_sql_saved_state_rollback": {"type": "boolean"}, }, } for _write_learning_capture_method in ("metadata.write_learning.capture_before", "metadata.write_learning.capture_after"): METHOD_INPUT_SCHEMAS[_write_learning_capture_method] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": { **_FORM_WRITE_TARGET_SELECTOR_PROPERTIES, "learning_id": {"type": "string", "description": "Local write-learning case identifier."}, }, } METHOD_INPUT_SCHEMAS["metadata.saved_state.forms.search"] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": { **_FORM_WRITE_TARGET_SELECTOR_PROPERTIES, "query": {"type": "string", "description": "Optional text search inside the decoded form."}, "limit": {"type": "integer", "minimum": 1, "maximum": 1000}, "scan_limit": {"type": "integer", "minimum": 1, "maximum": 5000}, "tables": {"type": "array", "items": {"type": "string"}}, }, } METHOD_INPUT_SCHEMAS["metadata.saved_state.status"] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": { "base_id": {"type": "string", "description": "Adapter base identifier."}, "layer": { "type": "string", "enum": ["base_saved_state", "extension_saved_state"], "description": "Preferred semantic saved-state layer selector.", }, "table": { "type": "string", "enum": ["ConfigSave", "ConfigCASSave"], "description": "Optional low-level storage selector; prefer layer.", }, "prefix": {"type": "string", "description": "Optional diagnostic storage file prefix."}, "limit": {"type": "integer", "minimum": 1, "maximum": 5000}, "include_files": {"type": "boolean"}, "include_unchanged": {"type": "boolean"}, "include_storage": { "type": "boolean", "description": "Expose SQL database/table/file coordinates, hashes, and concrete diff selectors.", }, }, } METHOD_INPUT_SCHEMAS["metadata.saved_state.prepare"] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": { "base_id": {"type": "string", "description": "Adapter base identifier."}, "layer": { "type": "string", "enum": ["base_saved_state", "extension_saved_state"], "description": "Preferred semantic target saved-state layer.", }, "ref": {"type": "string", "description": "Preferred public 1C object reference."}, "kind": {"type": "string", "description": "Optional metadata kind."}, "name": {"type": "string", "description": "Optional exact 1C object name."}, "object_type": {"type": "string", "description": "MCP alias of kind."}, "object_name": {"type": "string", "description": "MCP alias of name."}, "extension": {"type": "string", "description": "Optional extension name or GUID."}, "target_table": { "type": "string", "enum": ["ConfigSave", "ConfigCASSave"], "description": "Optional low-level storage selector; prefer layer.", }, "mode": {"type": "string", "enum": ["plan", "apply", "apply_and_verify"]}, "allow_sql_saved_state_prepare": {"type": "boolean"}, "include_storage": { "type": "boolean", "description": "Expose SQL tables, database, files, hashes, row details, and low-level apply diagnostics.", }, }, } METHOD_INPUT_SCHEMAS["metadata.saved_state.changes.list"] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": { "base_id": {"type": "string", "description": "Adapter base identifier."}, "layer": { "type": "string", "enum": ["base_saved_state", "extension_saved_state"], "description": "Preferred semantic saved-state layer filter.", }, "table": { "type": "string", "enum": ["ConfigSave", "ConfigCASSave"], "description": "Optional low-level saved-state layer filter.", }, "include_unchanged": {"type": "boolean"}, "include_context": { "type": "boolean", "description": "Request resolved object/form/module context explicitly; public mode enables it automatically.", }, "group_by_context": {"type": "boolean"}, "context_limit": {"type": "integer", "minimum": 0, "maximum": 500}, "limit": {"type": "integer", "minimum": 1, "maximum": 5000}, "include_storage": { "type": "boolean", "description": "Expose SQL tables/files, hashes, concrete diff selectors, module refs, and low-level actions.", }, }, } METHOD_INPUT_SCHEMAS["metadata.saved_state.modules.search"] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": { "base_id": {"type": "string", "description": "Adapter base identifier."}, "ref": {"type": "string", "description": "Optional public 1C module-owner ref."}, "kind": {"type": "string", "description": "Optional owner metadata kind."}, "name": {"type": "string", "description": "Optional exact owner object name."}, "guid": {"type": "string", "description": "Optional owner metadata GUID."}, "object_type": {"type": "string", "description": "MCP alias of owner kind."}, "object_name": {"type": "string", "description": "MCP alias of owner name."}, "object_guid": {"type": "string", "description": "MCP alias of owner GUID."}, "owner_guid": {"type": "string", "description": "Low-level exact owner GUID alias."}, "extension": {"type": "string", "description": "Optional extension name or GUID."}, "layer": { "type": "string", "enum": ["base_saved_state", "extension_saved_state"], "description": "Optional semantic saved-state layer; prefer this to SQL table names.", }, "query": {"type": "string", "description": "Optional BSL text query."}, "file_name": {"type": "string", "description": "Optional concrete saved-state file."}, "stream_index": {"type": "integer", "minimum": 0}, "limit": {"type": "integer", "minimum": 1, "maximum": 1000}, "scan_limit": {"type": "integer", "minimum": 1, "maximum": 5000}, "tables": {"type": "array", "items": {"type": "string"}}, "include_storage": { "type": "boolean", "description": "Expose SQL tables, file names, GUID identities, module_ref handles, hashes, and low-level write targets.", }, }, } METHOD_INPUT_SCHEMAS["code.write"] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": { "base_id": {"type": "string", "description": "Adapter base identifier."}, "ref": {"type": "string", "description": "Preferred public 1C module-owner ref."}, "kind": {"type": "string", "description": "Owner metadata kind."}, "name": {"type": "string", "description": "Exact owner object name."}, "guid": {"type": "string", "description": "Optional owner metadata GUID."}, "object_type": {"type": "string", "description": "MCP alias of owner kind."}, "object_name": {"type": "string", "description": "MCP alias of owner name."}, "object_guid": {"type": "string", "description": "MCP alias of owner GUID."}, "form": {"type": "string", "description": "Optional nested form name."}, "form_name": {"type": "string", "description": "Alias of form."}, "routine_name": {"type": "string", "description": "Exact BSL routine name."}, "routine_text": {"type": "string", "description": "Replacement BSL routine text."}, "module_text": {"type": "string", "description": "Replacement full module text."}, "old": {"type": "string", "description": "Exact old fragment."}, "new": {"type": "string", "description": "Replacement fragment."}, "extension": {"type": "string", "description": "Optional extension name or GUID."}, "execution_mode": {"type": "string", "enum": ["plan", "apply", "apply_and_verify", "apply_and_rollback"]}, "include_storage": {"type": "boolean"}, }, } METHOD_INPUT_SCHEMAS["templates.areas.find"] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": { "base_id": {"type": "string", "description": "Adapter base identifier."}, "ref": {"type": "string", "description": "Preferred public 1C template ref, for example Макеты.ПечатнаяФорма."}, "kind": {"type": "string", "description": "Template metadata kind."}, "name": {"type": "string", "description": "Exact template object name."}, "guid": {"type": "string", "description": "Optional template GUID."}, "object_type": {"type": "string", "description": "MCP alias of kind."}, "object_name": {"type": "string", "description": "MCP alias of template name."}, "object_guid": {"type": "string", "description": "MCP alias of template GUID."}, "extension": {"type": "string", "description": "Optional extension name or GUID."}, "area_name": {"type": "string", "description": "Exact named area."}, "area_query": {"type": "string", "description": "Named-area substring query."}, "area_occurrence": {"type": "integer", "minimum": 1}, "route_ref": {"type": "string", "description": "Optional low-level : route."}, "include_coverage": {"type": "boolean"}, }, } METHOD_INPUT_SCHEMAS["metadata.cache.lookup"] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": { "base_id": {"type": "string", "description": "Adapter base identifier."}, "ref": {"type": "string", "description": "Preferred public 1C object ref."}, "kind": {"type": "string", "description": "Metadata kind."}, "name": {"type": "string", "description": "Exact object name."}, "guid": {"type": "string", "description": "Optional metadata GUID."}, "object_type": {"type": "string", "description": "MCP alias of kind."}, "object_name": {"type": "string", "description": "MCP alias of name."}, "object_guid": {"type": "string", "description": "MCP alias of GUID."}, }, } _ACCESS_OBJECT_FILTER_PROPERTIES = { "base_id": {"type": "string", "description": "Adapter base identifier."}, "object_ref": { "type": "string", "description": "Public 1C metadata object ref, for example Справочники.Номенклатура. Resolved internally to the BSP object_sql_number.", }, "record_ref": { "type": "string", "description": "Concrete 1C application-data record reference (32 hex characters or UUID form). Resolved internally to the BSP object_id.", }, "object": { "type": "string", "description": "Legacy low-level exact BSP object field. Prefer object_ref plus record_ref.", }, "object_id": { "type": "string", "description": "Legacy low-level exact BSP record id. Prefer record_ref.", }, "object_sql_number": { "type": "integer", "description": "Legacy physical DBNames number. Prefer object_ref.", }, "access_key": {"type": "string", "description": "Optional exact BSP access-key id."}, "limit": {"type": "integer", "minimum": 1, "maximum": 20000}, "offset": {"type": "integer", "minimum": 0, "maximum": 10000000}, "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120}, "max_resolved_records": {"type": "integer", "minimum": 0, "maximum": 5000}, } METHOD_INPUT_SCHEMAS["access.keys.query"] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": { **_ACCESS_OBJECT_FILTER_PROPERTIES, "kind": { "type": "string", "enum": ["group", "user_set", "object", "set", "all"], "description": "Access-key query area, not a 1C metadata kind. Use object_ref to select a metadata object.", }, "area": { "type": "string", "enum": ["group", "user_set", "object", "set", "all"], "description": "Alias of kind.", }, "resolve_records": {"type": "boolean"}, }, } for _access_object_method in ("access.object_keys.resolve", "access.object.explain"): METHOD_INPUT_SCHEMAS[_access_object_method] = { "type": "object", "required": ["base_id"], "additionalProperties": True, "properties": dict(_ACCESS_OBJECT_FILTER_PROPERTIES), } RELATED_SECTION_RULES = { "Document": [ {"path": "4", "category": "Template"}, {"path": "6", "category": "Command"}, {"path": "7", "category": "Form"}, ], "Catalog": [ {"path": "3", "category": "Template"}, {"path": "4", "category": "Command"}, {"path": "7", "category": "Form"}, ], "Report": [ {"path": "3", "category": "Template"}, {"path": "5", "category": "Form"}, {"path": "7", "category": "Command"}, ], "DataProcessor": [ {"path": "4", "category": "Template"}, {"path": "5", "category": "Command"}, {"path": "6", "category": "Form"}, ], "Enum": [ {"path": "3", "category": "Form"}, {"path": "4", "category": "Template"}, ], "InformationRegister": [ {"path": "5", "category": "Form"}, {"path": "6", "category": "Template"}, {"path": "8", "category": "Command"}, ], "AccumulationRegister": [ {"path": "4", "category": "Command"}, {"path": "8", "category": "Form"}, ], "BusinessProcess": [{"path": "4", "category": "Form"}], "Task": [ {"path": "4", "category": "Form"}, {"path": "8", "category": "Command"}, ], "ChartOfCharacteristicTypes": [ {"path": "4", "category": "Template"}, {"path": "7", "category": "Form"}, ], "ChartOfAccounts": [{"path": "6", "category": "Form"}], "ChartOfCalculationTypes": [{"path": "7", "category": "Form"}], "CalculationRegister": [ {"path": "4", "category": "Recalculation"}, {"path": "5", "category": "Template"}, {"path": "7", "category": "Form"}, {"path": "8", "category": "Command"}, ], "DocumentJournal": [ {"path": "3", "category": "Template"}, {"path": "5", "category": "Command"}, {"path": "6", "category": "Form"}, ], "ExchangePlan": [ {"path": "4", "category": "Template"}, {"path": "6", "category": "Form"}, {"path": "7", "category": "Command"}, ], "SelectionCriterion": [{"path": "3", "category": "Form"}], "SettingsStorage": [{"path": "4", "category": "Form"}], } KIND_CAPABILITIES = { "Configuration": ["list", "get", "properties", "content", "modules", "special_details"], "Catalog": ["list", "get", "attributes", "tabular_sections", "forms", "templates", "commands", "modules"], "Document": ["list", "get", "attributes", "tabular_sections", "forms", "templates", "commands", "modules"], "Enum": ["list", "get", "enum_values", "values", "choice_settings", "forms", "templates", "properties", "special_details"], "InformationRegister": ["list", "get", "dimensions", "resources", "attributes", "forms", "templates", "commands", "modules"], "AccumulationRegister": ["list", "get", "dimensions", "resources", "attributes", "forms", "commands", "modules"], "AccountingRegister": ["list", "get", "dimensions", "resources", "attributes", "modules"], "BusinessProcess": ["list", "get", "attributes", "forms", "modules"], "Task": ["list", "get", "attributes", "addressing_attributes", "forms", "commands", "modules"], "Constant": ["list", "get", "value_type", "properties", "special_details"], "ChartOfCharacteristicTypes": ["list", "get", "attributes", "tabular_sections", "forms", "templates", "modules"], "ChartOfAccounts": ["list", "get", "attributes", "tabular_sections", "accounting_flags", "forms", "modules"], "ChartOfCalculationTypes": ["list", "get", "attributes", "tabular_sections", "forms", "modules", "properties", "special_details"], "ExchangePlan": ["list", "get", "attributes", "tabular_sections", "forms", "templates", "commands", "modules"], "DocumentJournal": ["list", "get", "columns", "forms", "templates", "commands", "properties", "special_details"], "DocumentNumerator": ["list", "get", "number_type", "number_length", "periodicity", "unique_check", "properties", "special_details"], "ScheduledJob": ["list", "get", "schedule", "method", "properties", "special_details"], "Report": ["list", "get", "attributes", "forms", "templates", "commands", "modules"], "DataProcessor": ["list", "get", "attributes", "forms", "templates", "commands", "modules"], "CommonModule": ["list", "get", "modules"], "CommonForm": ["get", "form_details", "modules"], "Form": ["get", "form_details"], "Template": ["get", "template_details"], "Command": ["get"], "CalculationRegister": ["list", "get", "dimensions", "resources", "attributes", "recalculations", "forms", "templates", "commands", "modules", "properties", "special_details"], "Sequence": ["list", "get", "dimensions"], "SelectionCriterion": ["list", "get", "type", "content", "forms", "properties", "special_details"], "EventSubscription": ["list", "get", "source", "handler", "properties", "special_details"], "Role": ["list", "get", "rights", "restrictions", "properties", "special_details"], "DefinedType": ["list", "get", "type", "types", "properties", "special_details"], "SessionParameter": ["list", "get", "type", "properties", "special_details"], "FunctionalOption": ["list", "get", "type", "privileged_get", "properties", "special_details"], "FunctionalOptionsParameter": ["list", "get", "use", "properties", "special_details"], "SettingsStorage": ["list", "get", "forms", "modules", "properties", "special_details"], "CommonAttribute": ["list", "get", "type", "data_separation", "properties", "special_details"], "CommonCommand": ["list", "get", "modules", "properties", "special_details"], "Subsystem": ["list", "get", "content", "command_interface", "properties", "special_details"], "Language": ["list", "get", "properties", "special_details"], "WebService": ["list", "get", "operations", "modules", "properties", "special_details"], "HTTPService": ["list", "get", "url_templates", "methods", "modules", "properties", "special_details"], "WSReference": ["list", "get", "operations", "schemas", "properties", "special_details"], "XDTOPackage": ["list", "get", "types", "properties", "special_details"], "ExternalDataSource": ["list", "get", "tables", "fields", "key_fields", "types", "cubes", "functions", "properties", "special_details"], "IntegrationService": ["list", "get", "channels", "modules", "properties", "special_details"], "CommonTemplate": ["list", "get", "templates", "template_details", "template_read", "template_analyze", "template_map"], "CommonPicture": ["list", "get", "binary_preview", "properties", "special_details"], "CommandGroup": ["list", "get", "commands", "properties", "special_details"], "Style": ["list", "get", "style_items", "properties", "special_details"], "StyleItem": ["list", "get", "value_type", "properties", "special_details"], "Interface": ["list", "get"], } CHILD_METADATA_KINDS = {"Form", "Template", "Command"} TOP_LEVEL_METADATA_KINDS = set(KIND_CAPABILITIES) - CHILD_METADATA_KINDS def normalize(value: Any) -> str: return re.sub(r"[\s._-]+", "", str(value or "")).casefold() for _kind, _kind_ru in RU_KIND.items(): KIND_ALIASES[normalize(_kind)] = _kind KIND_ALIASES[normalize(_kind_ru)] = _kind for _code_root, _kind in ONEC_CODE_ROOT_KIND.items(): KIND_ALIASES[normalize(_code_root)] = _kind for _type_qualifier, _kind in ONEC_TYPE_QUALIFIER_KIND.items(): KIND_ALIASES[normalize(_type_qualifier)] = _kind def normalize_exact(value: Any) -> str: return re.sub(r"\s+", " ", str(value or "").strip()).casefold() def text_quality_score(value: Any) -> int: text = str(value or "") if not text: return 0 cyrillic = sum(1 for char in text if "\u0400" <= char <= "\u04ff") latin = sum(1 for char in text if char.isascii() and char.isalpha()) digits = sum(1 for char in text if char.isdigit()) printable = sum(1 for char in text if char.isprintable() or char in "\r\n\t") cjk = sum(1 for char in text if "\u4e00" <= char <= "\u9fff") replacement = text.count("\ufffd") controls = sum(1 for char in text if ord(char) < 32 and char not in "\r\n\t") mojibake = sum(1 for char in text if char in "ÐÑÂÃÄÅÆÇÈÉÊËÌÍÎÏÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ") cyrillic_mojibake_pairs = len(re.findall(r"[РСГ][\u0400-\u04ff]", text)) return printable + cyrillic * 5 + latin + digits - cjk * 10 - replacement * 20 - controls * 8 - mojibake * 2 - cyrillic_mojibake_pairs * 12 def text_variants(value: Any) -> list[str]: text = str(value or "") if not text: return [] variants: list[str] = [] def add(candidate: str | None) -> None: if candidate and candidate not in variants: variants.append(candidate) add(text) transforms = ( ("latin1", "cp1251"), ("latin1", "utf-8"), ("cp1251", "utf-8"), ) for source, target in transforms: try: add(text.encode(source).decode(target)) except (UnicodeEncodeError, UnicodeDecodeError): continue return variants def best_text_variant(value: Any) -> str: variants = text_variants(value) if not variants: return str(value or "") return max(variants, key=text_quality_score) BSL_DECL_RE = re.compile(r"(?im)^\s*(?:Асинх\s+)?(?:Процедура|Функция)\s+[A-Za-zА-Яа-яЁё_][\wА-Яа-яЁё]*\s*\(") def cp1251_reverse_chars() -> dict[str, int]: result: dict[str, int] = {} for index in range(256): try: result[bytes([index]).decode("cp1251")] = index except UnicodeDecodeError: continue return result CP1251_REVERSE_CHARS = cp1251_reverse_chars() def bsl_text_signal_score(text: Any) -> int: value = str(text or "") if not value: return 0 score = 0 score += len(BSL_DECL_RE.findall(value)) * 20 score += len(re.findall(r"(?im)^\s*&На(?:Клиенте|Сервере|СервереБезКонтекста)\b", value)) * 8 score += len(re.findall(r"(?im)^\s*#(?:Если|Область|КонецОбласти)\b", value)) * 4 score += value.count("КонецПроцедуры") * 6 score += value.count("КонецФункции") * 6 score += sum(1 for char in value if "А" <= char <= "я" or char in "Ёё") score -= value.count("\ufffd") * 30 score -= len(re.findall(r"[РС][\u0400-\u04ff]", value)) * 3 return score def repair_bsl_mojibake_text(text: Any) -> str: source = str(text or "") if not source: return "" if is_bsl_like_text(source): return source candidates = [source, source.replace("п»ї", "\ufeff").replace("П»ї", "\ufeff")] cleaned = source.replace("п»ї", "").replace("П»ї", "").lstrip("\ufeff") try: repaired_bytes = bytes(CP1251_REVERSE_CHARS[char] for char in cleaned) repaired = repaired_bytes.decode("utf-8", errors="ignore") except Exception: repaired = "" if repaired and is_bsl_like_text(repaired): return ("\ufeff" + repaired) if source.startswith(("п»ї", "П»ї", "\ufeff")) else repaired for encoding in ("cp1251", "latin1"): try: repaired = cleaned.encode(encoding).decode("utf-8") except Exception: continue if is_bsl_like_text(repaired): return ("\ufeff" + repaired) if source.startswith(("п»ї", "П»ї", "\ufeff")) else repaired candidates.append(repaired) if source.startswith(("п»ї", "П»ї", "\ufeff")): candidates.append("\ufeff" + repaired) variants: list[str] = [] seen: set[str] = set() for candidate in candidates: if candidate in seen: continue seen.add(candidate) variants.append(candidate) return max(variants, key=lambda item: (bsl_text_signal_score(item), text_quality_score(item))) def is_bsl_like_text(text: Any) -> bool: value = str(text or "") return bool( BSL_DECL_RE.search(value) or "КонецПроцедуры" in value or "КонецФункции" in value or re.search(r"(?im)^\s*&На(?:Клиенте|Сервере|СервереБезКонтекста)\b", value) or re.search(r"(?im)^\s*#(?:Если|Область|КонецОбласти)\b", value) ) def normalized_variants(value: Any) -> set[str]: return {normalize(variant) for variant in text_variants(value) if normalize(variant)} def normalized_exact_variants(value: Any) -> set[str]: return {normalize_exact(variant) for variant in text_variants(value) if normalize_exact(variant)} def normalized_contains_any(needle: Any, haystack: Any) -> bool: needles = normalized_variants(needle) haystacks = normalized_variants(haystack) return any(needle_value in haystack_value for needle_value in needles for haystack_value in haystacks) def truthy(value: Any) -> bool: return str(value or "").strip().casefold() in {"1", "true", "yes", "on", "да"} def strict_bool_argument( payload: dict[str, Any], name: str, *, method: str, default: bool = False, ) -> tuple[bool | None, dict[str, Any] | None]: if name not in payload: return default, None value = payload.get(name) if isinstance(value, bool): return value, None return None, { "schema": "onec_adapter_request_error.v1", "method": method, "status": "invalid_argument", "error": "invalid_argument", "argument": name, "diagnostics": {"message": f"{name} must be a JSON boolean true/false, not a string or number."}, } ATTRIBUTE_ONLY_ALIASES = { "all": "all", "attributes": "attributes", "requisites": "attributes", "attrs": "attributes", "tabular_sections": "tabular_sections", "tabularsections": "tabular_sections", "table_parts": "tabular_sections", "tabs": "tabular_sections", "dimensions": "dimensions", "измерения": "dimensions", "resources": "resources", "ресурсы": "resources", "register_fields": "register_fields", "поля_регистра": "register_fields", } def invalid_argument(method: str, argument: str, message: str, *, allowed_values: list[str] | None = None) -> dict[str, Any]: result: dict[str, Any] = { "schema": "onec_adapter_request_error.v1", "method": method, "status": "invalid_argument", "error": "invalid_argument", "argument": argument, "diagnostics": {"message": message}, } if allowed_values is not None: result["allowed_values"] = allowed_values return result RUNTIME_XML_SOURCE_ARGUMENTS = { "xml_path", "xml_root", "meta_xml_path", "form_xml_path", "configuration_xml", "configuration_xml_path", "config_dump_info", "config_dump_info_path", } def runtime_xml_source_argument(value: Any, path: str = "payload") -> str | None: if isinstance(value, dict): for key, item in value.items(): current_path = f"{path}.{key}" if str(key).strip().casefold() in RUNTIME_XML_SOURCE_ARGUMENTS: return current_path nested = runtime_xml_source_argument(item, current_path) if nested: return nested elif isinstance(value, list): for index, item in enumerate(value): nested = runtime_xml_source_argument(item, f"{path}[{index}]") if nested: return nested return None def validate_sql_only_runtime_payload(method: str, payload: dict[str, Any]) -> dict[str, Any] | None: argument_path = runtime_xml_source_argument(payload) if not argument_path: return None return invalid_argument( method, argument_path, "The running adapter is SQL-only. XML exports and XML paths are accepted only by offline decoder-analysis scripts.", ) def child_not_found(method: str, child_kind_ru: str, child_name: Any, object_card: dict[str, Any], *, base_id: str) -> dict[str, Any]: not_found_phrase = "не найдена" if child_kind_ru.lower().endswith("а") else "не найден" return { "schema": "onec_adapter_request_error.v1", "method": method, "status": "not_found", "error": "not_found", "base_id": base_id, "object": object_card, "diagnostics": { "message": f"{child_kind_ru} `{child_name}` {not_found_phrase} у объекта `{object_card.get('name') or object_card.get('guid')}`.", }, } def optional_string_filter(payload: dict[str, Any], keys: list[str], *, method: str) -> tuple[Any, dict[str, Any] | None]: for key in keys: if key not in payload or payload.get(key) is None: continue value = payload.get(key) if not isinstance(value, str): return None, invalid_argument(method, key, f"{key} must be a JSON string.") if value != "": return value, None return None, None def validate_optional_string_arguments(payload: dict[str, Any], method: str, names: list[str]) -> dict[str, Any] | None: for name in names: if name in payload and payload.get(name) is not None and not isinstance(payload.get(name), str): return invalid_argument(method, name, f"{name} must be a JSON string.") return None def validate_optional_non_empty_string_arguments(payload: dict[str, Any], method: str, names: list[str]) -> dict[str, Any] | None: for name in names: if name not in payload: continue value = payload.get(name) if value is None or value == "": return invalid_argument(method, name, f"{name} must be a non-empty JSON string when provided.") if not isinstance(value, str): return invalid_argument(method, name, f"{name} must be a JSON string.") return None def child_identity_match_by(identity: dict[str, Any], filter_value: Any) -> str | None: wanted = normalize(filter_value or "") if not wanted: return None wanted_exact_variants = normalized_exact_variants(filter_value) wanted_variants = normalized_variants(filter_value) name_variants = normalized_variants(identity.get("name") or "") name_exact_variants = normalized_exact_variants(identity.get("name") or "") synonyms = identity.get("synonyms") or {} synonym_values = list(synonyms.values()) if isinstance(synonyms, dict) else [] if wanted_exact_variants & name_exact_variants: return "name_exact" if any(wanted_exact_variants & normalized_exact_variants(value or "") for value in synonym_values): return "synonym_exact" if any(wanted_value in name_value for wanted_value in wanted_variants for name_value in name_variants): return "name_contains" if any(normalized_contains_any(filter_value, value or "") for value in synonym_values): return "synonym_contains" return None def filter_related_children_by_identity(items: list[dict[str, Any]], category: str, filter_value: Any) -> list[tuple[dict[str, Any], str | None]]: wanted = normalize(filter_value or "") wanted_guid = str(filter_value or "").strip().lower() if is_guid_text(filter_value) else "" candidates: list[tuple[dict[str, Any], str | None]] = [] for item in items: if item.get("category") != category or item.get("status") != "ok": continue if not wanted: candidates.append((item, None)) continue if wanted_guid and str(item.get("guid") or "").strip().lower() == wanted_guid: candidates.append((item, "guid_exact")) continue match_by = child_identity_match_by(item.get("identity") or {}, filter_value) if match_by: candidates.append((item, match_by)) if wanted_guid and any(match_by == "guid_exact" for _, match_by in candidates): candidates = [(item, match_by) for item, match_by in candidates if match_by == "guid_exact"] elif wanted and any(match_by in {"name_exact", "synonym_exact"} for _, match_by in candidates): candidates = [(item, match_by) for item, match_by in candidates if match_by in {"name_exact", "synonym_exact"}] return candidates def limit_object_commands_result( object_commands: list[dict[str, Any]], form_commands: list[dict[str, Any]], max_commands: int, ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]], dict[str, Any]]: max_commands = max(1, int(max_commands or 1)) command_rows = [("object", item) for item in object_commands] + [("form", item) for item in form_commands] limited_rows = command_rows[:max_commands] limited_object_commands = [item for scope, item in limited_rows if scope == "object"] limited_form_commands = [item for scope, item in limited_rows if scope == "form"] limited_commands = [item for _, item in limited_rows] total_commands = len(command_rows) return ( limited_object_commands, limited_form_commands, limited_commands, { "commands_total": total_commands, "commands_truncated": total_commands > len(limited_commands), "max_commands": max_commands, }, ) def filter_public_rows_by_name(rows: list[dict[str, Any]], filter_value: Any) -> list[tuple[dict[str, Any], str | None]]: wanted = normalize(filter_value or "") candidates: list[tuple[dict[str, Any], str | None]] = [] for row in rows: if not wanted: candidates.append((row, None)) continue identity = {"name": row.get("name"), "synonyms": {"ru": row.get("synonym")} if row.get("synonym") else {}} match_by = child_identity_match_by(identity, filter_value) if match_by: candidates.append((row, match_by)) if wanted and any(match_by in {"name_exact", "synonym_exact"} for _, match_by in candidates): candidates = [(row, match_by) for row, match_by in candidates if match_by in {"name_exact", "synonym_exact"}] return candidates def command_match_by(item: dict[str, Any], filter_value: Any) -> str | None: if not normalize(filter_value or ""): return None identity = { "name": item.get("name"), "synonyms": {"ru": item.get("synonym") or item.get("title")} if (item.get("synonym") or item.get("title")) else {}, } return child_identity_match_by(identity, filter_value) def strict_include_storage(payload: dict[str, Any], method: str) -> tuple[bool | None, dict[str, Any] | None]: return strict_bool_argument(payload, "include_storage", method=method, default=False) def require_diagnostic_mode(payload: dict[str, Any], method: str) -> dict[str, Any] | None: if payload.get("_internal") is True: return None diagnostic, diagnostic_error = strict_bool_argument(payload, "diagnostic", method=method, default=False) if diagnostic_error: return diagnostic_error if diagnostic is True: return None return invalid_argument( method, "diagnostic", "Низкоуровневый диагностический метод доступен только при diagnostic=true.", allowed_values=["true"], ) def jsonable(value: Any) -> Any: if isinstance(value, Decimal): return int(value) if value == value.to_integral_value() else float(value) if isinstance(value, (datetime, date)): return value.isoformat() if isinstance(value, (bytes, bytearray)): return {"type": "binary", "bytes": len(value), "hex": bytes(value).hex()} return value def dbnames_ext_guid_from_idrref(data: bytes | bytearray | None) -> str | None: if not data or len(data) != 16: return None reordered = bytes(data[12:16] + data[10:12] + data[8:10] + data[0:2] + data[2:8]) return str(uuid.UUID(bytes=reordered)) def canonical_kind(value: str | None) -> str | None: if not value: return None stripped = value.strip() return KIND_ALIASES.get(normalize(stripped), stripped) def known_canonical_kind(value: str | None) -> str | None: """Resolve only known 1C metadata roots, including code and value-type roots.""" candidate = canonical_kind(value) return candidate if candidate in RU_KIND else None def parse_1c_object_path(value: Any, explicit_kind: str | None = None) -> dict[str, Any]: """Parse public refs and code-like 1C paths without exposing storage identifiers.""" raw = str(value or "").strip() parts = [part.strip() for part in raw.split(".") if part.strip()] result: dict[str, Any] = { "input": raw, "parts": parts, "kind": known_canonical_kind(explicit_kind), "name": "", "member_path": [], "recognized_root": False, } if not parts: return result first = parts[0] if first.casefold().startswith("cfg:"): first = first[4:].strip() parts[0] = first root_index = 0 if normalize(first) in {"metadata", "метаданные"}: root_index = 1 root = parts[root_index] if len(parts) > root_index else "" root_kind = known_canonical_kind(root) if root_kind: result["recognized_root"] = True result["input_root"] = root result["kind"] = result["kind"] or root_kind name_index = root_index + 1 if len(parts) > name_index: result["name"] = parts[name_index] result["member_path"] = parts[name_index + 1 :] elif result["kind"]: result["name"] = parts[0] result["member_path"] = parts[1:] else: result["name"] = raw kind = result.get("kind") name = str(result.get("name") or "") if kind and name: result["canonical_ref"] = f"{kind}.{name}" result["metadata_ref"] = f"{RU_KIND.get(kind, kind)}.{name}" result["code_ref"] = f"{ONEC_CODE_ROOT_BY_KIND.get(kind, RU_KIND.get(kind, kind))}.{name}" return result def parse_kind_request(kind: str | None) -> tuple[str | None, str | None]: raw = normalize(kind) public_values = set(PUBLIC_KIND.values()) internal_values = set(PUBLIC_KIND) | set(DBNAMES_ROLE_KIND.values()) | set(KIND_CAPABILITIES) wanted = canonical_kind(kind) if raw in public_values and (wanted not in internal_values): return None, raw if wanted in internal_values: return wanted, None return wanted, None def parse_object_query(kind: str | None, name: str) -> tuple[str | None, str]: query = str(name or "").strip() parsed = parse_1c_object_path(query, explicit_kind=kind) if parsed.get("kind") and parsed.get("name"): return str(parsed["kind"]), str(parsed["name"]) if "." not in query: return parse_kind_request(kind)[0], query left, right = query.split(".", 1) left = left[4:] if left.casefold().startswith("cfg:") else left return parse_kind_request(kind)[0] or parse_kind_request(left)[0], right def dbnames_record_storage_table(record: Any, default_table: str = "Config") -> str: source = str(getattr(record, "source", "") or "") if extension_guid_from_dbnames_source(source): return "ConfigCAS" return default_table def preferred_object_storage_table(row: dict[str, Any] | None, default_table: str = "Config") -> str: if not isinstance(row, dict): return default_table storage = row.get("storage") if isinstance(row.get("storage"), dict) else {} table = str(storage.get("table") or row.get("table") or default_table or "Config") return table if table in STORAGE_TABLES else "Config" def normalize_object_ref_payload(payload: dict[str, Any], method: str) -> dict[str, Any] | dict[str, Any]: """Fill kind/name/guid from public 1C refs such as Document.Поступление.""" if "ref" not in payload or payload.get("ref") in {None, ""}: return payload ref_value = payload.get("ref") if not isinstance(ref_value, str): return invalid_argument(method, "ref", "ref must be a JSON string.") ref = ref_value.strip() if not ref: return payload normalized = dict(payload) if is_guid_text(ref): if not normalized.get("guid"): normalized["guid"] = ref.lower() return normalized ref_kind, ref_name = parse_object_query(None, ref) if ref_kind and ref_name: if not normalized.get("kind"): normalized["kind"] = ref_kind if not normalized.get("name"): normalized["name"] = ref_name elif ref_name and not normalized.get("name"): normalized["name"] = ref_name return normalized def normalize_object_selector_aliases(payload: dict[str, Any], method: str) -> dict[str, Any]: """Fill kind/name/guid from MCP-friendly object_type/object_name/object_guid aliases.""" selector_error = validate_object_selector_arguments(payload, method, include_view=False) if selector_error: return selector_error normalized = normalize_object_ref_payload(payload, method) if isinstance(normalized, dict) and normalized.get("status") == "invalid_argument": return normalized result = dict(normalized) if not result.get("kind") and result.get("object_type") not in {None, ""}: result["kind"] = result.get("object_type") if not result.get("name") and result.get("object_name") not in {None, ""}: result["name"] = result.get("object_name") if not result.get("guid") and result.get("object_guid") not in {None, ""}: result["guid"] = result.get("object_guid") if result.get("kind"): result["kind"] = canonical_kind(str(result.get("kind") or "")) return result def normalize_data_object_selector_aliases(payload: dict[str, Any], method: str) -> dict[str, Any]: """Normalize data-method object names without consuming record references.""" object_ref = payload.get("object_ref") if object_ref is not None and not isinstance(object_ref, str): return invalid_argument(method, "object_ref", "object_ref must be a JSON string.") ordinal_value = first_non_empty_arg(payload, "ordinal", "index", "object_index") if ordinal_value not in {None, ""}: return invalid_argument( method, "ordinal", "Application-data methods require an object name, public ref, or GUID; ordinal selectors are not supported.", ) selector_payload = dict(payload) selector_payload.pop("record_ref", None) selector_payload.pop("recorder_ref", None) raw_ref = str(payload.get("ref") or "").replace("-", "").strip() if isinstance(object_ref, str) and object_ref.strip(): selector_payload["ref"] = object_ref.strip() elif re.fullmatch(r"[0-9a-fA-F]{32}", raw_ref): selector_payload.pop("ref", None) normalized_selector = normalize_object_selector_aliases(selector_payload, method) if isinstance(normalized_selector, dict) and normalized_selector.get("status") == "invalid_argument": return normalized_selector result = dict(payload) for key in ("kind", "name", "guid"): if normalized_selector.get(key) not in {None, ""}: result[key] = normalized_selector[key] return result def normalize_selector_payload_for_method(payload: dict[str, Any], method: str) -> dict[str, Any]: capabilities = OBJECT_SELECTOR_METHOD_CAPABILITIES.get(method) or {} ordinal_value = first_non_empty_arg(payload, "ordinal", "index", "object_index") if capabilities.get("accepts_ordinal") is False and ordinal_value not in {None, ""}: return invalid_argument( method, "ordinal", "This method requires an object name, public ref, or GUID; ordinal selectors are not supported.", ) if method in DATA_OBJECT_SELECTOR_METHODS: return normalize_data_object_selector_aliases(payload, method) return normalize_object_selector_aliases(payload, method) def has_object_selector(payload: dict[str, Any]) -> bool: ordinal_value = first_non_empty_arg(payload, "ordinal", "index", "object_index") return bool( payload.get("ref") or payload.get("guid") or payload.get("object_guid") or payload.get("name") or payload.get("object_name") or ordinal_value not in {None, ""} ) def object_selector_ref(kind: Any, name: Any) -> str | None: kind_text = str(kind or "").strip() name_text = str(name or "").strip() if not kind_text or not name_text: return None return f"{canonical_kind(kind_text) or kind_text}.{name_text}" def enrich_selector_with_object_ref(selector: dict[str, Any], object_info: dict[str, Any]) -> dict[str, Any]: enriched = dict(selector) if not enriched.get("name") and object_info.get("name") not in {None, ""}: enriched["name"] = object_info.get("name") public_ref = object_selector_ref(enriched.get("kind") or object_info.get("kind"), enriched.get("name") or object_info.get("name")) if public_ref: enriched["ref"] = public_ref return enriched ADAPTER_CONTRACT_VERSION = "onec-selector-contract.v1" OBJECT_SELECTOR_GUIDANCE = " Object selector accepts ref, kind/name/guid, or object_type/object_name/object_guid." OBJECT_SELECTOR_GUIDANCE_TERMS = ("ref", "kind/name/guid", "object_type/object_name/object_guid") OBJECT_SELECTOR_REQUIRED_MESSAGE = "Pass an object selector: ref, kind/name/guid, object_type/object_name/object_guid, or ordinal." OBJECT_SELECTOR_REQUIRED_MESSAGE_NO_ORDINAL = "Pass an object selector: ref, kind/name/guid, or object_type/object_name/object_guid." OBJECT_SELECTOR_GLOBAL_REQUIRED_MESSAGE = ( "Pass an object selector: ref, kind/name/guid, or object_type/object_name/object_guid; " "or use areas metadata/extensions for global lookup." ) OBJECT_SELECTOR_OR_MODULE_REQUIRED_MESSAGE = ( "Pass module_ref/module_id or object selector (ref, kind/name/guid, or object_type/object_name/object_guid)." ) MODULE_READ_SELECTOR_OR_MODULE_ID_MESSAGE = ( "Pass an object selector (ref, kind/name/guid, object_type/object_name/object_guid, or ordinal) " "or use module_id as
:[#stream:] where
is one of Config, ConfigSave, ConfigCAS, ConfigCASSave." ) OBJECT_SELECTOR_ARGUMENTS = ["ref", "object_type", "object_name", "object_guid", "kind", "name", "guid"] OBJECT_SELECTOR_DEFAULT_CAPABILITIES = { "accepts_ref": True, "accepts_kind_name_guid": True, "accepts_object_aliases": True, "accepts_ordinal": True, "accepts_module_ref": False, "accepts_extension": False, "allows_global_areas": False, "requires_object_selector": False, } OBJECT_SELECTOR_METHOD_CAPABILITIES = { method: dict(OBJECT_SELECTOR_DEFAULT_CAPABILITIES) for method in ( "metadata.objects.list", "metadata.object.get", "metadata.object.properties", "metadata.object.property.write", "metadata.object.member.add", "metadata.object.decode", "metadata.object.parts", "metadata.object.modules", "metadata.object.related", "metadata.object.forms", "metadata.object.form.details", "metadata.object.templates", "metadata.object.template.details", "templates.read", "templates.analyze", "templates.map", "metadata.object.commands", "metadata.object.special.details", "metadata.form.decode", "metadata.saved_state.prepare", "metadata.resolve_overrides", "templates.bindings", "diagnostics.call_chain", "access.object.roles", "access.object.subjects", ) } OBJECT_SELECTOR_METHOD_CAPABILITIES.update( { "metadata.object.property.write": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "requires_object_selector": True, }, "metadata.object.member.add": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "requires_object_selector": False, }, "metadata.objects.list": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_extension": False, "extension_alternative_method": "extension.objects.find", }, "metadata.definition.find": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_extension": True, "allows_global_areas": True, "requires_query": True, }, "metadata.route.resolve": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_extension": True, "allows_global_search": True, }, "extension.objects.find": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_extension": True, "allows_global_search": True, }, "metadata.object.attributes": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "requires_object_selector": True, }, "metadata.object.full": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "requires_object_selector": True, }, "metadata.saved_state.prepare": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_module_ref": True, "accepts_extension": True, }, "metadata.saved_state.diff": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_module_ref": True, "accepts_extension": True, "requires_object_selector": False, "module_selector_arguments": ["module_ordinal", "module_index", "module_number"], "read_only": True, }, "metadata.module_owner_cache.prune": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_module_ref": True, "requires_object_selector": False, "selector_purpose": "cache_owner_scope", "local_cache_mutation": True, }, "modules.search": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_extension": True, "allows_global_search": True, }, "modules.read": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_module_ref": True, "requires_object_selector": False, }, "code.search": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "allows_global_search": True, "requires_query": True, }, "code.read": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_module_ref": True, "requires_object_selector": False, }, "code.symbol.resolve": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_module_ref": True, "requires_object_selector": False, }, "metadata.support.decode": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "requires_object_selector": False, "selector_purpose": "optional_object_support_filter", }, "metadata.code_index.build": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "allows_global_search": True, "requires_object_selector": False, "selector_purpose": "optional_index_scope", }, "metadata.form.owner_index.build": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "requires_object_selector": False, "selector_purpose": "optional_form_owner_scope", "form_selector_arguments": ["form", "form_name"], }, "metadata.form.command_button.verify": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "requires_object_selector": False, "selector_purpose": "optional_form_owner_scope", "form_selector_arguments": ["form", "form_name"], "child_selector_arguments": ["command_name", "button_name", "handler_name"], "read_only": True, }, "metadata.form.command_button.write": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "requires_object_selector": False, "selector_purpose": "optional_form_owner_scope", "form_selector_arguments": ["form", "form_name", "form_guid"], "child_selector_arguments": ["command_name", "button_name", "handler_name"], "read_only": False, "write_guards": [ "allow_saved_state_write", "allow_sql_saved_state_apply", "repository_apply_gate", "verified_saved_state_target", ], }, "metadata.form.write_target.resolve": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "requires_object_selector": False, "selector_purpose": "optional_form_owner_scope", "form_selector_arguments": ["form", "form_name", "form_guid"], "child_selector_arguments": ["element", "element_name", "command", "attribute"], "read_only": True, }, "metadata.form.write_target.verify": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "requires_object_selector": False, "selector_purpose": "optional_form_owner_scope", "form_selector_arguments": ["form", "form_name", "form_guid"], "child_selector_arguments": ["element", "element_name", "command", "attribute"], "read_only": True, }, "metadata.form.element.write": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "requires_object_selector": False, "selector_purpose": "optional_form_owner_scope", "form_selector_arguments": ["form", "form_name", "form_guid"], "child_selector_arguments": ["element", "element_name", "command", "attribute"], "read_only": False, "write_guards": ["allow_saved_state_write", "proposal_only"], }, "metadata.form.element.write_apply": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "requires_object_selector": False, "selector_purpose": "optional_form_owner_scope", "form_selector_arguments": ["form", "form_name", "form_guid"], "child_selector_arguments": ["element", "element_name", "command", "attribute"], "read_only": False, "write_guards": [ "allow_saved_state_write", "allow_sql_saved_state_apply", "repository_apply_gate", "verified_saved_state_target", ], }, "metadata.form.target.move": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "requires_object_selector": False, "selector_purpose": "optional_form_owner_scope", "form_selector_arguments": ["form", "form_name", "form_guid"], "child_selector_arguments": ["from_element", "to_element", "with_element", "after_element"], "read_only": False, "write_guards": [ "allow_saved_state_write", "allow_sql_saved_state_apply", "repository_apply_gate", "verified_saved_state_target", ], }, "metadata.form.write_matrix.build": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "requires_object_selector": False, "selector_purpose": "optional_form_owner_scope", "form_selector_arguments": ["form", "form_name", "form_guid"], "child_selector_arguments": ["element", "element_name", "command", "attribute"], "read_only": True, }, "metadata.form.write_matrix.smoke": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "requires_object_selector": False, "selector_purpose": "optional_form_owner_scope", "form_selector_arguments": ["form", "form_name", "form_guid"], "child_selector_arguments": ["element", "element_name", "command", "attribute"], "read_only": False, "write_guards": [ "allow_sql_saved_state_apply", "allow_sql_saved_state_rollback", "apply_and_rollback_only", ], }, "metadata.write_learning.capture_before": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "requires_object_selector": False, "selector_purpose": "optional_form_owner_scope", "form_selector_arguments": ["form", "form_name", "form_guid"], "child_selector_arguments": ["element", "element_name", "command", "attribute"], "read_only": False, "write_guards": ["local_artifact_only", "no_sql_write"], }, "metadata.write_learning.capture_after": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "requires_object_selector": False, "selector_purpose": "optional_form_owner_scope", "form_selector_arguments": ["form", "form_name", "form_guid"], "child_selector_arguments": ["element", "element_name", "command", "attribute"], "read_only": False, "write_guards": ["local_artifact_only", "no_sql_write"], }, "metadata.saved_state.forms.search": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "requires_object_selector": False, "selector_purpose": "optional_form_owner_scope", "form_selector_arguments": ["form", "form_name", "form_guid"], "child_selector_arguments": ["element", "element_name", "command", "attribute"], "read_only": True, }, "metadata.saved_state.modules.search": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "allows_global_search": True, "requires_object_selector": False, "selector_purpose": "optional_module_owner_scope", "module_selector_arguments": ["file_name", "stream_index"], "read_only": True, }, "code.write": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "accepts_module_ref": True, "requires_object_selector": False, "selector_purpose": "module_owner_or_concrete_module", "form_selector_arguments": ["form", "form_name"], "child_selector_arguments": ["routine_name"], "read_only": False, "write_guards": [ "save_first", "allow_sql_saved_state_apply", "repository_apply_gate", "verified_saved_state_target", ], }, "templates.areas.find": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "accepts_extension": True, "requires_object_selector": False, "selector_purpose": "template_or_direct_route", "child_selector_arguments": ["area_name", "area_query", "area_occurrence"], "direct_route_arguments": ["route_ref", "table", "file_name"], "read_only": True, }, "metadata.cache.lookup": { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "requires_object_selector": True, "selector_purpose": "local_identity_cache_lookup", "read_only": True, "authoritative_source": "live_sql_not_cache", }, **{ method: { **OBJECT_SELECTOR_DEFAULT_CAPABILITIES, "accepts_ordinal": False, "requires_object_selector": True, "accepts_object_ref": True, "record_ref_argument": ( "recorder_ref" if method == "data.movements" else "record_ref" if method in {"data.get", "data.present"} else None ), } for method in DATA_OBJECT_SELECTOR_METHODS }, } ) OBJECT_SELECTOR_ALIAS_METHODS = frozenset(OBJECT_SELECTOR_METHOD_CAPABILITIES) DEFAULT_METHOD_INPUT_SCHEMA = { "type": "object", "additionalProperties": True, "description": "Method-specific JSON payload. See the method description and selector_capabilities.", } def object_selector_capabilities(method: str) -> dict[str, Any] | None: capabilities = OBJECT_SELECTOR_METHOD_CAPABILITIES.get(method) return dict(capabilities) if capabilities is not None else None def validate_object_selector_arguments(payload: dict[str, Any], method: str, *, include_view: bool = True) -> dict[str, Any] | None: arguments = [*OBJECT_SELECTOR_ARGUMENTS, *(["view"] if include_view else [])] return validate_optional_string_arguments(payload, method, arguments) def public_method_row(row: dict[str, Any]) -> dict[str, Any]: public = dict(row) name = str(public.get("name") or "") description = str(public.get("description") or "") if name in OBJECT_SELECTOR_ALIAS_METHODS and not all(term in description for term in OBJECT_SELECTOR_GUIDANCE_TERMS): public["description"] = f"{description}{OBJECT_SELECTOR_GUIDANCE}" selector_capabilities = object_selector_capabilities(name) if selector_capabilities is not None: public["selector_capabilities"] = selector_capabilities public["input_schema"] = copy.deepcopy(METHOD_INPUT_SCHEMAS.get(name) or DEFAULT_METHOD_INPUT_SCHEMA) specialized_routes = adapter_http_routes_for_method(name) if specialized_routes: public["http_routes"] = specialized_routes return public HTTP_GET_METHOD_ROUTES = { "/health": "health", "/methods": "help.methods", "/metadata/kinds": "metadata.kinds", "/metadata/objects": "metadata.objects.list", "/metadata/object": "metadata.object.get", "/extensions": "extensions.list", "/modules/read": "modules.read", } HTTP_POST_METHOD_ROUTES = { "/metadata/snapshot": "metadata.snapshot", "/metadata/object/decode": "metadata.object.decode", "/metadata/object/parts": "metadata.object.parts", "/metadata/object/modules": "metadata.object.modules", "/metadata/object/related": "metadata.object.related", "/metadata/object/forms": "metadata.object.forms", "/metadata/object/form-details": "metadata.object.form.details", "/metadata/object/templates": "metadata.object.templates", "/metadata/object/template-details": "metadata.object.template.details", "/templates/read": "templates.read", "/templates/analyze": "templates.analyze", "/templates/map": "templates.map", "/metadata/object/commands": "metadata.object.commands", "/metadata/route/resolve": "metadata.route.resolve", "/metadata/object/special-details": "metadata.object.special.details", "/metadata/form/decode": "metadata.form.decode", "/metadata/form/write-target/resolve": "metadata.form.write_target.resolve", "/metadata/saved-state/forms/search": "metadata.saved_state.forms.search", "/metadata/form/element-write": "metadata.form.element.write", "/metadata/form/element-write-apply": "metadata.form.element.write_apply", "/metadata/write-plan": "metadata.write.plan", "/metadata/write-capabilities": "metadata.write.capabilities", "/metadata/write": "metadata.write", "/metadata/write-learning/capture-before": "metadata.write_learning.capture_before", "/metadata/write-learning/capture-after": "metadata.write_learning.capture_after", "/metadata/write-learning/diff": "metadata.write_learning.diff", "/metadata/write-learning/infer-rule": "metadata.write_learning.infer_rule", "/code/write": "code.write", "/metadata/object/attributes": "metadata.object.attributes", "/metadata/object/full": "metadata.object.full", "/metadata/resolve-overrides": "metadata.resolve_overrides", "/modules/search": "modules.search", "/code/search": "code.search", "/code/read": "code.read", "/code/symbol/resolve": "code.symbol.resolve", "/templates/bindings": "templates.bindings", "/diagnostics/call-chain": "diagnostics.call_chain", "/metadata/module-owner-cache/prune": "metadata.module_owner_cache.prune", "/access/snapshot/extract": "access.snapshot.extract", "/access/graph": "access.graph.build", "/access/user/explain": "access.user.explain", "/access/users/search": "access.users.search", "/access/keys/query": "access.keys.query", "/access/object-keys/resolve": "access.object_keys.resolve", "/access/object/explain": "access.object.explain", "/access/object/roles": "access.object.roles", "/access/object/subjects": "access.object.subjects", "/access/rls/discover": "access.rls.discover", "/access/role/profiles": "access.role.profiles", "/access/role/users": "access.role.users", "/access/role/audit-export": "access.role.audit_export", "/access/role/audit-analyze": "access.role.audit_analyze", "/extension/objects/find": "extension.objects.find", "/query/validate": "query.validate", "/query/run": "query.run", "/codec/decode": "codec.decode", "/codec/encode": "codec.encode", "/changes/propose": "changes.propose", "/storage/saved-state/apply-proposal": "storage.saved_state.apply_proposal", "/storage/saved-state/rollback": "storage.saved_state.rollback", "/storage/saved-state/backups": "storage.saved_state.backups.list", } def adapter_http_routes_for_method(method: str) -> list[dict[str, str]]: routes = [ {"verb": "GET", "path": path} for path, route_method in HTTP_GET_METHOD_ROUTES.items() if route_method == method ] routes.extend( {"verb": "POST", "path": path} for path, route_method in HTTP_POST_METHOD_ROUTES.items() if route_method == method ) return routes def adapter_method_registry() -> dict[str, dict[str, Any]]: """Single runtime registry used by help, jobs, RPC validation, and MCP checks.""" return { str(row.get("name") or ""): public_method_row(row) for row in METHODS if str(row.get("name") or "") } def adapter_method_registry_diagnostics() -> dict[str, Any]: names = [str(row.get("name") or "") for row in METHODS] duplicates = sorted({name for name in names if name and names.count(name) > 1}) registered = set(adapter_method_registry()) route_methods = set(HTTP_GET_METHOD_ROUTES.values()) | set(HTTP_POST_METHOD_ROUTES.values()) return { "status": "ok" if not duplicates and set(METHOD_INPUT_SCHEMAS) <= registered and set(OBJECT_SELECTOR_METHOD_CAPABILITIES) <= registered and route_methods <= registered else "invalid", "counts": { "methods": len(registered), "methods_with_input_schema": sum(1 for row in adapter_method_registry().values() if isinstance(row.get("input_schema"), dict)), "explicit_input_schemas": len(METHOD_INPUT_SCHEMAS), "selector_contracts": len(OBJECT_SELECTOR_METHOD_CAPABILITIES), "specialized_http_routes": len(HTTP_GET_METHOD_ROUTES) + len(HTTP_POST_METHOD_ROUTES), }, "duplicates": duplicates, "input_schemas_without_method": sorted(set(METHOD_INPUT_SCHEMAS) - registered), "selector_contracts_without_method": sorted(set(OBJECT_SELECTOR_METHOD_CAPABILITIES) - registered), "http_routes_without_method": sorted(route_methods - registered), } def is_extension_path(path: str | None) -> bool: parts = [part.casefold() for part in str(path or "").replace("/", "\\").split("\\")] return "расширения" in parts or "extensions" in parts def is_extension_object(item: dict[str, Any], top: dict[str, Any]) -> bool: return bool(item.get("extension_routes")) or is_extension_path(top.get("relative_path") or top.get("path")) class AdapterState: def health(self, *, base_id: str | None = None) -> dict[str, Any]: if not base_id: return { "schema": "onec_adapter_health.v1", "status": "ok", "base_id": None, "contract_version": ADAPTER_CONTRACT_VERSION, "capabilities": ["live_sql", "read-only-query", "extensions"], "diagnostics": {"message": "Pass base_id to check a concrete 1C database source."}, } resolved_base_id = str(base_id) config, config_error = sql_config_for_base(resolved_base_id) result: dict[str, Any] = { "schema": "onec_adapter_health.v1", "status": "ok" if config else "degraded", "base_id": resolved_base_id, "contract_version": ADAPTER_CONTRACT_VERSION, "live_sql": { "configured": bool(config), "server": config.get("server") if config else None, "database": config.get("database") if config else None, "user": config.get("user") if config else None, }, "capabilities": ["live_sql", "read-only-query", "extensions"], } if config_error: result["diagnostics"] = config_error return result STATE: AdapterState def top_objects(index: dict[str, Any]): for guid, item in (index.get("objects") or {}).items(): for top in item.get("xml_top_objects") or []: yield str(guid), item, top def base_id_required(method: str) -> dict[str, Any]: return { "schema": "onec_adapter_request_error.v1", "method": method, "status": "error", "error": "base_id_required", "diagnostics": {"message": "Pass base_id explicitly. The adapter does not use a default database."}, } def live_source_unavailable(method: str, base_id: str, config_error: dict[str, Any] | None) -> dict[str, Any]: return { "schema": "onec_adapter_source_missing.v1", "method": method, "status": "source_missing", "base_id": base_id, "source": {"kind": "live_sql", "status": (config_error or {}).get("status", "not_configured")}, "diagnostics": config_error or {"message": "Live SQL connection is not configured for this base_id."}, } def compact_storage(item: dict[str, Any]) -> dict[str, Any]: return { "dbnames": item.get("dbnames") or [], "config_routes": item.get("config_routes") or [], "extension_routes": item.get("extension_routes") or [], "route_kind": item.get("route_kind") or [], "xml_occurrence_count": item.get("xml_occurrence_count"), } STORAGE_TRACE_KEYS = { "database", "depth", "file_name", "guids_sample", "id_path", "index", "marker", "marker_name", "module_id", "name_path", "object_storage_routes", "part_id", "path", "payload", "payload_bytes", "payload_role", "raw_bytes", "sha1", "source_file", "storage", "storage_routes", "strings_sample", "stream_index", "suffix", "table", "title_lang", "title_path", "type_code", } def strip_storage_traces(value: Any) -> Any: if isinstance(value, list): return [strip_storage_traces(item) for item in value] if not isinstance(value, dict): return value public: dict[str, Any] = {} for key, item in value.items(): if key in STORAGE_TRACE_KEYS: continue if key == "source" and isinstance(item, dict) and item.get("kind") == "live_sql": public[key] = {"kind": "live_metadata"} continue public[key] = strip_storage_traces(item) return public def public_error_result(result: dict[str, Any], *, include_storage: bool, method: str) -> dict[str, Any]: public = dict(result) public["method"] = method if include_storage: return public return strip_storage_traces(public) def public_metadata_row(row: dict[str, Any], *, include_storage: bool = False) -> dict[str, Any]: """Return a 1C-facing metadata row; physical storage is opt-in.""" public = dict(row) public_ref = object_selector_ref(public.get("kind"), public.get("name")) if public_ref: public["ref"] = public_ref storage = public.pop("storage", None) if include_storage and storage is not None: public["storage"] = storage return public def metadata_payload_missing_diagnostics() -> dict[str, str]: return {"message": "Описание объекта метаданных не найдено в хранилище конфигурации."} def public_semantic_profile( semantic: dict[str, Any] | None, *, include_storage: bool = False, resolved_types: dict[str, dict[str, Any]] | None = None, ) -> dict[str, Any] | None: if not isinstance(semantic, dict): return None resolved_types = resolved_types or {} public = dict(semantic) if not include_storage: public.pop("object_storage_routes", None) public.pop("section_rules", None) public.pop("generic_sections", None) sections = [] for section in public.get("sections") or []: if not isinstance(section, dict): sections.append(section) continue public_section = dict(section) if not include_storage: for key in STORAGE_TRACE_KEYS: public_section.pop(key, None) records = [] for record in public_section.get("records") or []: if not isinstance(record, dict): records.append(record) continue public_record = dict(record) if not include_storage: for key in STORAGE_TRACE_KEYS: public_record.pop(key, None) if "type" in public_record: public_record["type"] = public_type_info(public_record.get("type"), resolved_types, include_storage=include_storage) columns = [] for column in public_record.get("columns") or []: if not isinstance(column, dict): columns.append(column) continue public_column = dict(column) if not include_storage: for key in STORAGE_TRACE_KEYS: public_column.pop(key, None) if "type" in public_column: public_column["type"] = public_type_info(public_column.get("type"), resolved_types, include_storage=include_storage) columns.append(public_column) if "columns" in public_record: public_record["columns"] = columns records.append(public_record) public_section["records"] = records sections.append(public_section) public["sections"] = sections return public if include_storage else strip_storage_traces(public) OBJECT_MODULE_OWNER_KINDS = { "Catalog", "Document", "Report", "DataProcessor", "BusinessProcess", "Task", "ChartOfCharacteristicTypes", "ChartOfAccounts", "ChartOfCalculationTypes", "ExchangePlan", } REGISTER_MODULE_OWNER_KINDS = { "InformationRegister", "AccumulationRegister", "AccountingRegister", "CalculationRegister", } CODE_CARRIER_MATRIX = { "object_modules": { "status": "supported", "read_status": "supported", "write_status": "supported_saved_state", "module_roles": ["object_module", "manager_module", "command_module"], "owner_kinds": sorted(OBJECT_MODULE_OWNER_KINDS), "read_methods": ["modules.search", "modules.read", "code.search", "code.read"], "write_methods": ["code.write", "metadata.write", "storage.saved_state.apply_proposal"], "methods": ["modules.search", "modules.read", "code.search", "code.read", "code.write"], "default_state": "working", "notes": ["Reads prefer saved-state tables for programming-time analysis.", "Writes target ConfigSave/ConfigCASSave only."], }, "register_modules": { "status": "supported", "read_status": "supported", "write_status": "supported_saved_state", "module_roles": ["record_set_module", "register_module", "command_module"], "owner_kinds": sorted(REGISTER_MODULE_OWNER_KINDS), "read_methods": ["modules.search", "modules.read", "code.search", "code.read"], "write_methods": ["code.write", "metadata.write", "storage.saved_state.apply_proposal"], "methods": ["modules.search", "modules.read", "code.search", "code.read", "code.write"], "default_state": "working", "notes": ["Record-set and command/register streams are named at the public 1C level."], }, "common_modules": { "status": "supported", "read_status": "supported", "write_status": "supported_saved_state", "module_roles": ["common_module"], "owner_kinds": ["CommonModule"], "read_methods": ["modules.search", "modules.read", "code.search", "code.read"], "write_methods": ["code.write"], "methods": ["modules.search", "modules.read", "code.search", "code.read", "code.write"], "default_state": "working", "notes": ["Common modules are discovered by public 1C name and their canonical .0 Config stream is exposed as BSL."], }, "form_modules": { "status": "supported", "read_status": "supported", "write_status": "supported_saved_state", "module_roles": ["form_module"], "owner_kinds": ["CommonForm", "Form"], "read_methods": ["metadata.object.forms", "metadata.form.decode", "modules.search", "modules.read", "code.search", "code.read"], "write_methods": ["code.write", "metadata.write", "storage.saved_state.apply_proposal"], "methods": ["metadata.object.forms", "metadata.form.decode", "modules.search", "modules.read", "code.search", "code.read", "code.write"], "default_state": "working", "notes": ["Saved-state form modules are returned as public BSL text, not raw form containers."], }, "scheduled_jobs": { "status": "supported_reference", "read_status": "supported_reference", "write_status": "read_only", "module_roles": [], "owner_kinds": ["ScheduledJob"], "read_methods": ["metadata.object.properties", "metadata.object.special.details", "modules.read", "metadata.resolve_overrides", "code.search", "code.read"], "write_methods": [], "methods": ["metadata.object.properties", "metadata.object.special.details", "modules.read", "metadata.resolve_overrides", "code.search", "code.read"], "default_state": "working", "notes": ["SQL metadata resolves the common-module owner and returns a ready modules.read selector for the scheduled handler procedure."], }, "application_session_external_connection_modules": { "status": "supported_read", "read_status": "supported", "write_status": "read_only", "module_roles": ["ordinary_application_module", "managed_application_module", "session_module", "external_connection_module"], "owner_kinds": ["Configuration"], "read_methods": ["metadata.object.modules", "modules.search", "modules.read", "code.search", "code.read"], "write_methods": [], "methods": ["metadata.object.modules", "modules.search", "modules.read", "code.search", "code.read"], "default_state": "working", "notes": ["The SQL file prefix comes from the Configuration identity GUID; stable Config suffixes are mapped to public module roles."], }, "web_http_service_modules": { "status": "supported_read", "read_status": "supported", "write_status": "read_only", "module_roles": ["web_service_module", "http_service_module", "integration_service_module"], "owner_kinds": ["WebService", "HTTPService", "IntegrationService"], "read_methods": ["metadata.object.modules", "modules.search", "modules.read", "code.search", "code.read"], "write_methods": [], "methods": ["metadata.object.modules", "modules.search", "modules.read", "code.search", "code.read"], "default_state": "working", "notes": ["The canonical full BSL stream is selected from the service Config part; short duplicate container fragments are ignored."], }, "event_subscriptions": { "status": "supported_reference", "read_status": "supported_reference", "write_status": "read_only", "module_roles": [], "owner_kinds": ["EventSubscription"], "read_methods": ["metadata.object.properties", "metadata.object.special.details", "modules.read", "code.read"], "write_methods": [], "methods": ["metadata.object.properties", "metadata.object.special.details", "modules.read", "code.read"], "default_state": "working", "notes": ["SQL metadata resolves every event source plus the common-module handler and returns a ready modules.read selector for the handler procedure."], }, } METADATA_WRITE_CAPABILITIES = { "bsl_modules": { "status": "supported_saved_state", "targets": ["object_module", "manager_module", "command_module", "record_set_module", "register_module", "form_module", "common_module"], "operations": ["replace_module", "replace_routine", "replace_unique_fragment"], "agent_method": "code.write", "write_layer": "save", "apply_method": "storage.saved_state.apply_proposal", "guards": ["expected_sha1", "expected_text_sha1", "backup", "readback_verification"], }, "form_element_properties": { "status": "partial_saved_state", "targets": ["form_element", "form_command", "form_attribute"], "operations": [ "plan_property_write", "apply_property_write", "move_sibling_target", "upsert_command_button", "upsert_handler_routine", "verify_command_button_links", ], "agent_method": "metadata.write", "write_layer": "save", "guards": ["expected_sha1", "backup", "semantic_readback", "cache_invalidation"], "gaps": ["Cross-parent structural moves, arbitrary element deletion, complex command bindings, and inherited form properties are not fully writable yet."], }, "object_metadata": { "status": "partial_saved_state", "targets": ["object_identity", "attributes", "tabular_sections", "commands"], "operations": ["write_synonym", "write_comment", "write_member_synonym", "write_member_comment", "add_attribute_from_template"], "agent_method": "metadata.write", "write_layer": "save", "guards": ["expected_old", "expected_sha1", "backup", "semantic_readback", "settings_shape_verification", "cache_invalidation"], "gaps": ["Attribute add currently requires a safe existing Attribute template in the same collection. Object/member rename, arbitrary type construction, deletion, Dimension/Resource/TabularSection creation, and other structural collection writes remain disabled."], }, "templates": { "status": "read_only", "targets": ["template", "moxel", "html", "binary"], "operations": [], "write_layer": None, "gaps": ["Template rendering/round-trip encoding is not complete enough for safe writes."], }, "scheduled_jobs": { "status": "partial_saved_state", "targets": ["schedule", "method_reference"], "operations": ["plan_schedule_write", "apply_schedule_write", "verify_schedule_write", "rollback_schedule_write"], "agent_method": "metadata.write", "write_layer": "save", "guards": ["expected_sha1", "backup", "readback_verification"], "gaps": ["The method reference remains read-only here; edit executable code through the resolved module selector."], }, "web_http_services": { "status": "read_only", "targets": ["web_service_module", "http_service_module", "integration_service_module"], "operations": [], "write_layer": None, "gaps": ["Live SQL module discovery and reading are supported; saved-state write routing remains disabled until service-specific round-trip validation is complete."], }, "roles_rights_subscriptions": { "status": "read_only", "targets": ["rights", "roles", "event_subscriptions"], "operations": ["metadata.object.properties", "metadata.object.special.details", "modules.read"], "write_layer": None, "gaps": ["Role rights and event subscriptions are fully decoded for reads; saved-state mutation remains intentionally disabled."], }, } def module_suffix_from_file_name(file_name: Any) -> str | None: match = re.search(r"\.(\d+)$", str(file_name or "")) return match.group(1) if match else None def module_suffix_from_module_id(module_id: Any) -> str | None: table, file_name, _stream_index = parse_module_id(str(module_id or "")) if not table or not file_name: return None return module_suffix_from_file_name(file_name) def public_module_role( *, owner_kind: str | None = None, suffix: str | None = None, ordinal: int | None = None, current_name: str | None = None, ) -> dict[str, Any]: kind = canonical_kind(str(owner_kind or "")) suffix = str(suffix or "").strip().lstrip(".") if kind == "Configuration": configuration_role = { "0": ("ordinary_application_module", "Модуль обычного приложения"), "5": ("external_connection_module", "Модуль внешнего соединения"), "6": ("managed_application_module", "Модуль управляемого приложения"), "7": ("session_module", "Модуль сеанса"), }.get(suffix) if configuration_role: return {"kind": configuration_role[0], "name": configuration_role[1], "suffix": suffix} service_role = { "WebService": ("web_service_module", "Модуль Web-сервиса"), "HTTPService": ("http_service_module", "Модуль HTTP-сервиса"), "IntegrationService": ("integration_service_module", "Модуль сервиса интеграции"), }.get(kind) if service_role: return {"kind": service_role[0], "name": service_role[1], **({"suffix": suffix} if suffix else {})} if kind == "SettingsStorage": return {"kind": "manager_module", "name": "Модуль менеджера", **({"suffix": suffix} if suffix else {})} if suffix == "2": return {"kind": "command_module", "name": "Модуль команды", **({"suffix": suffix} if suffix else {})} if suffix == "3": return {"kind": "manager_module", "name": "Модуль менеджера", **({"suffix": suffix} if suffix else {})} if suffix == "0": if kind in REGISTER_MODULE_OWNER_KINDS: return {"kind": "record_set_module", "name": "Модуль набора записей", "suffix": suffix} if kind == "Constant": return {"kind": "value_manager_module", "name": "Модуль менеджера значения", "suffix": suffix} if kind == "DocumentJournal": return {"kind": "manager_module", "name": "Модуль менеджера", "suffix": suffix} if kind == "CommonForm": return {"kind": "form_module", "name": "Модуль формы", "suffix": suffix} if kind == "CommonModule": return {"kind": "common_module", "name": current_name or "Общий модуль", "suffix": suffix} if kind in OBJECT_MODULE_OWNER_KINDS or not kind: return {"kind": "object_module", "name": "Модуль объекта", "suffix": suffix} if kind == "CommonModule": return {"kind": "common_module", "name": current_name or "Общий модуль"} if kind == "CommonForm": return {"kind": "form_module", "name": "Модуль формы"} if kind in REGISTER_MODULE_OWNER_KINDS and ordinal in {None, 1}: return {"kind": "record_set_module", "name": "Модуль набора записей"} if kind in REGISTER_MODULE_OWNER_KINDS: display_name = "Модуль регистра" if ordinal in {None, 1} else f"Модуль регистра {ordinal}" return {"kind": "register_module", "name": display_name} if kind == "Constant" and ordinal in {None, 1}: return {"kind": "value_manager_module", "name": "Модуль менеджера значения"} if kind == "DocumentJournal" and ordinal in {None, 1}: return {"kind": "manager_module", "name": "Модуль менеджера"} display_name = "Модуль объекта" if ordinal in {None, 1} else f"Модуль объекта {ordinal}" return {"kind": "object_module", "name": display_name} def public_module_row( module: dict[str, Any], *, include_storage: bool = False, ordinal: int | None = None, owner_kind: str | None = None, owner_name: str | None = None, ) -> dict[str, Any]: public = dict(module) kind = canonical_kind(str(owner_kind or "")) role = public_module_role( owner_kind=kind, suffix=str(public.get("suffix") or module_suffix_from_module_id(public.get("module_id")) or ""), ordinal=ordinal, # A common module is addressed in 1C code by the metadata object name. # The raw row name is a Config/ConfigCAS stream coordinate and must # never become its public name. current_name=str(owner_name or "") if kind == "CommonModule" else None, ) original_name = public.get("name") public["name"] = role.get("name") public["kind"] = role.get("kind") if ordinal is not None: public["module_ordinal"] = ordinal if include_storage and original_name and original_name != public.get("name"): public["storage_name"] = original_name if not include_storage: for key in ( "module_id", "source", "table", "file_name", "suffix", "stream_index", "bytes", "sha1", "encoding", "payload_role", "storage_name", ): public.pop(key, None) return public def public_code_qualified_name( *, owner: dict[str, Any] | None = None, form: dict[str, Any] | None = None, module: dict[str, Any] | None = None, ) -> str | None: owner_name = str((owner or {}).get("name") or "").strip() form_name = str((form or {}).get("name") or "").strip() module_name = str((module or {}).get("name") or "").strip() if ( str((module or {}).get("kind") or "").strip() == "common_module" and owner_name and normalize(module_name) == normalize(owner_name) ): return owner_name parts = [part for part in (owner_name, form_name, module_name) if part] return ".".join(parts) if parts else None def public_module_with_qualified_name( module: dict[str, Any], *, owner: dict[str, Any] | None = None, include_storage: bool = False, ordinal: int | None = None, owner_kind: str | None = None, ) -> dict[str, Any]: public = public_module_row( module, include_storage=include_storage, ordinal=ordinal, owner_kind=owner_kind or ((owner or {}).get("kind") if isinstance(owner, dict) else None), owner_name=(owner or {}).get("name") if isinstance(owner, dict) else None, ) qualified_name = public_code_qualified_name(owner=owner, module=public) if qualified_name: public["qualified_name"] = qualified_name public["display_name"] = qualified_name return public def template_type_key(value: Any) -> str: return re.sub(r"[\s_\-]+", "", str(value or "").casefold()) def normalize_onec_template_platform_type(value: Any) -> dict[str, Any] | None: key = template_type_key(value) if not key: return None for item in ONEC_TEMPLATE_PLATFORM_TYPES: values = [item.get("id"), item.get("name"), item.get("xml_type"), *(item.get("aliases") or [])] if key in {template_type_key(candidate) for candidate in values}: return dict(item) return None def onec_template_type_by_id(type_id: str) -> dict[str, Any]: return normalize_onec_template_platform_type(type_id) or {"id": type_id, "name": type_id} def declared_template_platform_type_from_part(part: dict[str, Any]) -> dict[str, Any] | None: candidates: list[Any] = [ part.get("template_type_id"), part.get("template_type"), part.get("platform_type_id"), part.get("platform_type"), ] classification = part.get("classification") if isinstance(part.get("classification"), dict) else {} candidates.extend( [ classification.get("template_type_id"), classification.get("template_type"), classification.get("platform_type_id"), classification.get("platform_type"), ] ) candidates.extend(classification.get("strings_sample") or []) for candidate in candidates: template_type = normalize_onec_template_platform_type(candidate) if template_type: return template_type return None def template_type_candidates_from_features(features: dict[str, Any], content_parts: list[dict[str, Any]]) -> list[dict[str, Any]]: candidates: list[dict[str, Any]] = [] declared = normalize_onec_template_platform_type(features.get("declared_platform_type_id") or features.get("declared_platform_type")) if declared: declared.update({"confidence": "high", "evidence": ["TemplateType metadata"]}) candidates.append(declared) def append_candidate(candidate: dict[str, Any]) -> None: if any(item.get("id") == candidate.get("id") for item in candidates): return candidates.append(candidate) if features.get("tabular_document"): candidate = onec_template_type_by_id("tabular_document") candidate.update({"confidence": "high", "evidence": ["MOXCEL marker"]}) append_candidate(candidate) if features.get("html"): candidate = onec_template_type_by_id("html_document") candidate.update({"confidence": "medium", "evidence": ["HTML marker in payload preview"]}) append_candidate(candidate) if features.get("bsl"): candidate = onec_template_type_by_id("text_document") candidate.update({"confidence": "low", "evidence": ["text payload with BSL markers"]}) append_candidate(candidate) has_content = bool(content_parts) if has_content and not candidates: candidate = onec_template_type_by_id("binary_data") candidate.update({"confidence": "low", "evidence": ["content payload without known text/tabular markers"]}) append_candidate(candidate) return candidates def public_template_summary(parts: list[dict[str, Any]], *, include_storage: bool = False) -> dict[str, Any]: content_parts = [part for part in parts if part.get("role") != "metadata_payload"] declared_platform_type = next( (template_type for template_type in (declared_template_platform_type_from_part(part) for part in parts) if template_type), None, ) features = { "tabular_document": any(((part.get("features") or {}).get("tabular_document")) for part in content_parts), "html": any(((part.get("features") or {}).get("html")) for part in content_parts), "bsl": any(((part.get("features") or {}).get("bsl")) for part in content_parts), **( { "declared_platform_type": declared_platform_type.get("name"), "declared_platform_type_id": declared_platform_type.get("id"), "declared_platform_xml_type": declared_platform_type.get("xml_type"), } if declared_platform_type else {} ), } template_type_candidates = template_type_candidates_from_features(features, content_parts) summary: dict[str, Any] = { "features": features, "known_platform_types": ONEC_TEMPLATE_PLATFORM_TYPES, "template_type_candidates": template_type_candidates, "counts": {"parts": len(parts), "content_parts": len(content_parts)}, } if features.get("tabular_document"): summary["format"] = "ТабличныйДокумент" summary["platform_type"] = "Табличный документ" summary["preview"] = { "status": "preview_not_supported", "message": "Макет распознан как табличный документ; безопасный просмотр содержимого пока не поддержан.", } elif features.get("html"): summary["format"] = "HTML" summary["platform_type"] = "HTML документ" elif features.get("bsl"): summary["format"] = "ТекстМодуля" summary["platform_type"] = "Текстовый документ" else: summary["format"] = "НеОпределено" summary["platform_type"] = template_type_candidates[0].get("name") if template_type_candidates else "НеОпределено" if include_storage: summary["parts"] = parts summary["content"] = { "roles": sorted({str(part.get("role") or "") for part in content_parts if part.get("role")}), "kinds": sorted({str(part.get("content_kind") or "") for part in content_parts if part.get("content_kind")}), } return summary def module_profile( base_id: str, module: dict[str, Any], *, include_text: bool = False, include_storage: bool = False, display_name: str | None = None, timeout_seconds: int = 60, ) -> dict[str, Any]: module_id = str(module.get("module_id") or "") result = read_module({"base_id": base_id, "module_id": module_id, "include_text": True, "timeout_seconds": timeout_seconds}) if result.get("status") != "ok": profile = { "status": result.get("status"), "diagnostics": result.get("diagnostics"), } if include_storage: profile["module_id"] = module_id return profile text = str(result.get("text") or "") try: from parser.bsl_validation import routine_blocks, validate_bsl_text except Exception as exc: profile = { "status": "error", "diagnostics": {"message": f"BSL validator is unavailable: {exc}"}, } if include_storage: profile["module_id"] = module_id return profile routines = [ { "kind": block.get("kind"), "name": block.get("name"), "line_start": block.get("line_start"), "line_end": block.get("line_end"), } for block in routine_blocks(text) ] profile: dict[str, Any] = { "status": "ok", "kind": module.get("kind"), "name": display_name or module.get("name"), "validation": validate_bsl_text(text), "routines": routines, "counts": { "routines": len(routines), "procedures": sum(1 for item in routines if str(item.get("kind") or "").casefold() == "процедура"), "functions": sum(1 for item in routines if str(item.get("kind") or "").casefold() == "функция"), "lines": len(text.replace("\r\n", "\n").replace("\r", "\n").split("\n")) if text else 0, }, } if include_storage: profile.update( { "module_id": module_id, "bytes": module.get("bytes"), "sha1": module.get("sha1") or (((result.get("payload") or {}).get("stream") or {}).get("sha1")), "encoding": module.get("encoding"), } ) profile["completeness"] = "complete" if profile["validation"].get("status") == "ok" else "fragment_or_invalid" if profile["completeness"] != "complete": profile["diagnostics"] = { "message": "The stream contains BSL markers but does not pass full-module structural validation. Treat it as a fragment or invalid module text.", } if include_text: profile["text"] = text else: profile["text_preview"] = text[:1000] return profile def public_form_row(form: dict[str, Any], *, include_storage: bool = False) -> dict[str, Any]: public = dict(form) if not include_storage: public.pop("source", None) parts = [] for part in public.get("parts") or []: if not isinstance(part, dict): parts.append(part) continue public_part = dict(part) if not include_storage: for key in ("part_id", "source", "suffix", "sha1"): public_part.pop(key, None) parts.append(public_part) public["parts"] = parts return public def public_form_profile(profile: dict[str, Any], *, include_storage: bool = False) -> dict[str, Any]: if include_storage: return profile return strip_storage_traces(profile) def form_profile_capabilities(profile: dict[str, Any]) -> dict[str, Any]: counts = profile.get("counts") if isinstance(profile.get("counts"), dict) else {} module = profile.get("module") if isinstance(profile.get("module"), dict) else {} return { "elements": int(counts.get("items_total") or counts.get("items") or 0) > 0, "attributes": int(counts.get("attributes_total") or counts.get("attributes") or 0) > 0, "commands": int(counts.get("commands_total") or counts.get("commands") or 0) > 0, "tables": int(counts.get("tables_total") or counts.get("tables") or 0) > 0, "command_bars": int(counts.get("command_bars_total") or counts.get("command_bars") or 0) > 0, "events": int(counts.get("events") or 0) > 0, "handler_links": int(counts.get("handler_links") or 0) > 0, "button_command_links": int(counts.get("button_command_links") or 0) > 0, "module": int(module.get("routine_count") or counts.get("module_routines") or 0) > 0, } def form_profile_properties(profile: dict[str, Any]) -> dict[str, Any]: counts = profile.get("counts") if isinstance(profile.get("counts"), dict) else {} form_semantic = profile.get("form_semantic") if isinstance(profile.get("form_semantic"), dict) else {} return { "semantic": form_semantic.get("groups") or {}, "elements": counts.get("items"), "elements_total": counts.get("items_total"), "attributes": counts.get("attributes"), "attributes_total": counts.get("attributes_total"), "commands": counts.get("commands"), "commands_total": counts.get("commands_total"), "tables": counts.get("tables"), "tables_total": counts.get("tables_total"), "command_bars": counts.get("command_bars"), "events": counts.get("events"), "module_routines": counts.get("module_routines"), "handler_links": counts.get("handler_links"), "resolved_handlers": counts.get("resolved_handlers"), "missing_handlers": counts.get("missing_handlers"), "button_command_links": counts.get("button_command_links"), "truncated": { "elements": bool(counts.get("items_truncated")), "attributes": bool(counts.get("attributes_truncated")), "commands": bool(counts.get("commands_truncated")), "tables": bool(counts.get("tables_truncated")), "command_bars": bool(counts.get("command_bars_truncated")), }, } def form_public_commands(profile: dict[str, Any]) -> list[dict[str, Any]]: command_links_by_name = { normalize(str(link.get("command") or "")): str(link.get("handler") or "").strip() for link in (profile.get("command_links") or []) if isinstance(link, dict) and str(link.get("command") or "").strip() and str(link.get("handler") or "").strip() } commands: list[dict[str, Any]] = [] for command in profile.get("commands") or []: if not isinstance(command, dict): continue public = dict(command) command_name = str(public.get("name") or "").strip() action = str(public.get("action") or public.get("handler") or "").strip() if not action: action = command_links_by_name.get(normalize(command_name), "") if not action: semantic = public.get("semantic") if isinstance(public.get("semantic"), dict) else {} groups = semantic.get("groups") if isinstance(semantic.get("groups"), dict) else {} for values in groups.values(): if not isinstance(values, list): continue for item in values: if not isinstance(item, dict): continue if normalize(str(item.get("name") or "")) == "action" and str(item.get("value") or "").strip(): action = str(item.get("value") or "").strip() break if action: break if action: public["action"] = action public.setdefault("handler", action) commands.append(public) return commands def form_public_sections(profile: dict[str, Any]) -> dict[str, Any]: return { "elements": profile.get("items") or [], "attributes": profile.get("attributes") or [], "parameters": profile.get("parameters") or [], "commands": form_public_commands(profile), "events": profile.get("events") or [], "handler_links": profile.get("handler_links") or [], "command_links": profile.get("command_links") or [], "button_command_links": profile.get("button_command_links") or [], "module": profile.get("module") or {"status": "not_found"}, } def form_element_filter_from_payload(payload: dict[str, Any]) -> dict[str, Any]: return { "element": payload.get("element") or payload.get("element_name"), "element_path": payload.get("element_path") or payload.get("path"), "element_id": payload.get("element_id") or payload.get("id"), } def form_element_matches(item: dict[str, Any], selector: dict[str, Any]) -> tuple[bool, str | None]: element = selector.get("element") element_path = selector.get("element_path") element_id = selector.get("element_id") if element_path not in {None, ""} and str(item.get("path") or "") == str(element_path): return True, "path_exact" if element_id not in {None, ""} and normalize_exact(item.get("id")) == normalize_exact(element_id): return True, "id_exact" if element not in {None, ""}: if normalize_exact(item.get("name")) == normalize_exact(element): return True, "name_exact" if normalize_exact(item.get("title")) == normalize_exact(element): return True, "title_exact" if normalize(item.get("name")) == normalize(element): return True, "name_normalized" if normalize(item.get("title")) == normalize(element): return True, "title_normalized" return False, None def apply_form_element_filter(profile: dict[str, Any], selector: dict[str, Any]) -> dict[str, Any]: if not any(selector.get(key) not in {None, ""} for key in ("element", "element_path", "element_id")): return profile filtered = dict(profile) total_matches = 0 for section in ("items", "commands", "attributes", "tables", "command_bars"): rows = [item for item in profile.get(section) or [] if isinstance(item, dict)] matches: list[dict[str, Any]] = [] for item in rows: matched, match_by = form_element_matches(item, selector) if matched: row = dict(item) row["match_by"] = match_by matches.append(row) filtered[section] = matches total_matches += len(matches) counts = dict(profile.get("counts") or {}) counts["items"] = len(filtered.get("items") or []) counts["commands"] = len(filtered.get("commands") or []) counts["attributes"] = len(filtered.get("attributes") or []) counts["tables"] = len(filtered.get("tables") or []) counts["command_bars"] = len(filtered.get("command_bars") or []) counts["focused_elements"] = total_matches filtered["counts"] = counts filtered["element_filter"] = {key: value for key, value in selector.items() if value not in {None, ""}} if not total_matches: filtered["diagnostics"] = { "message": "Элемент/команда/атрибут формы по заданному имени, path или id не найден в декодированном профиле формы.", } return filtered def payload_public_properties(classification: dict[str, Any]) -> dict[str, Any]: role = str(classification.get("role") or "unknown") markers = [str(value) for value in classification.get("markers") or []] counts = classification.get("counts") or {} root = classification.get("root") or {} content_kind = { "metadata_payload": "metadata", "form_payload": "form", "template_payload": "template", "help_or_html_payload": "html_or_help", "bsl_module_payload": "bsl_module", "stream_container": "stream_container", "brace_payload": "brace_payload", "binary_or_unknown_payload": "binary_or_unknown", }.get(role, role) public: dict[str, Any] = { "role": role, "content_kind": content_kind, "root_marker": root.get("root_marker") if isinstance(root, dict) else None, "markers": markers, "counts": { "stream_blocks": counts.get("stream_blocks") or 0, "base64_blocks": counts.get("base64_blocks") or 0, }, "features": { "tabular_document": "MOXCEL" in markers, "html": any(block.get("has_html_marker") for block in classification.get("base64_blocks") or []), "bsl": any(block.get("has_bsl_marker") for block in classification.get("stream_blocks") or []), "streams": int(counts.get("stream_blocks") or 0) > 0, "base64": int(counts.get("base64_blocks") or 0) > 0, }, } return public def payload_public_preview(classification: dict[str, Any], *, include_text_preview: bool = True) -> dict[str, Any]: markers = [str(value) for value in classification.get("markers") or []] counts = classification.get("counts") or {} preview: dict[str, Any] = { "streams": [], "base64": [], } if include_text_preview: preview["streams"] = [ { "encoding": block.get("encoding"), "text_preview": block.get("text_preview"), "has_bsl_marker": bool(block.get("has_bsl_marker")), "has_html_marker": bool(block.get("has_html_marker")), } for block in classification.get("stream_blocks") or [] if block.get("text_preview") or block.get("has_bsl_marker") or block.get("has_html_marker") ][:20] preview["base64"] = [ { "encoding": block.get("encoding"), "text_preview": block.get("text_preview"), "has_bsl_marker": bool(block.get("has_bsl_marker")), "has_html_marker": bool(block.get("has_html_marker")), } for block in classification.get("base64_blocks") or [] if block.get("text_preview") or block.get("has_bsl_marker") or block.get("has_html_marker") ][:20] if "MOXCEL" in markers: preview["tabular_document"] = { "status": "preview_not_supported" if not preview["streams"] and not preview["base64"] else "partial", "format": "MOXCEL", "markers": markers, "structure": { "root_marker": (classification.get("root") or {}).get("root_marker") if isinstance(classification.get("root"), dict) else None, "stream_blocks": counts.get("stream_blocks") or 0, "base64_blocks": counts.get("base64_blocks") or 0, "strings_sample": (classification.get("strings_sample") or [])[:20], }, "diagnostics": { "message": "Макет распознан как табличный документ MOXCEL. Текст/области из бинарного табличного документа пока не извлекаются публичным preview." }, } return preview def payload_public_undecoded_evidence( classification: dict[str, Any], *, include_text_preview: bool = True, mode: str = "summary", allow_storage_details: bool = False, max_strings: int = 20, max_blocks: int = 12, max_excerpt_chars: int = 1200, ) -> dict[str, Any]: if not isinstance(classification, dict): return {} mode = str(mode or "summary").casefold() if mode == "none": return {} if mode == "full": max_strings = max(max_strings, 80) max_blocks = max(max_blocks, 50) max_excerpt_chars = max(max_excerpt_chars, 6000) include_text_preview = True elif mode == "raw": max_strings = max(max_strings, 200) max_blocks = max(max_blocks, 100) max_excerpt_chars = max(max_excerpt_chars, 20000) include_text_preview = True def public_block_samples(blocks: Any) -> list[dict[str, Any]]: items: list[dict[str, Any]] = [] for block in blocks or []: if not isinstance(block, dict): continue item = { "encoding": block.get("encoding"), "has_bsl_marker": bool(block.get("has_bsl_marker")), "has_html_marker": bool(block.get("has_html_marker")), } if mode == "raw" and allow_storage_details: for key in ("header_offset", "header_end", "data_offset", "data_end", "block_length", "declared_1", "declared_2", "bytes", "sha1"): if key in block: item[key] = block.get(key) if include_text_preview and block.get("text_preview"): text_preview = str(block.get("text_preview") or "") item["text_preview"] = text_preview[:max_excerpt_chars] if mode in {"full", "raw"} and include_text_preview and isinstance(block.get("text"), str): item["text_excerpt"] = str(block.get("text") or "")[:max_excerpt_chars] items.append(item) if len(items) >= max_blocks: break return items evidence: dict[str, Any] = { "status": classification.get("status"), "role": classification.get("role"), "payload": { "compression": classification.get("compression"), "encoding": classification.get("encoding"), "raw_bytes": classification.get("raw_bytes"), "payload_bytes": classification.get("payload_bytes"), "sha1": classification.get("sha1"), "payload_sha1": classification.get("payload_sha1"), }, "root": classification.get("root"), "markers": list(classification.get("markers") or [])[:max_strings], "strings_sample": list(classification.get("strings_sample") or [])[:max_strings], "counts": dict(classification.get("counts") or {}), "stream_blocks_sample": public_block_samples(classification.get("stream_blocks")), "base64_blocks_sample": public_block_samples(classification.get("base64_blocks")), } text = classification.get("text") if include_text_preview and isinstance(text, str) and text: evidence["text_excerpt"] = text[:max_excerpt_chars] if mode == "raw" and allow_storage_details and classification.get("tree") is not None: evidence["tree"] = classification.get("tree") return evidence def public_child_identity(item: dict[str, Any]) -> dict[str, Any]: identity = item.get("identity") if isinstance(item.get("identity"), dict) else {} if not identity and isinstance(item.get("record_identity"), dict): identity = item.get("record_identity") or {} synonyms = identity.get("synonyms") if isinstance(identity, dict) else None return { "guid": identity.get("guid") or item.get("guid"), "name": identity.get("name"), "synonym": next(iter(synonyms.values()), None) if isinstance(synonyms, dict) and synonyms else None, "status": item.get("status"), } PUBLIC_PAYLOAD_ROLE_NAMES = { "metadata_payload": "Метаданные объекта", "bsl_module_payload": "Модуль", "form_payload": "Форма", "template_payload": "Макет", "help_or_html_payload": "Справка/HTML", "command_payload": "Команда", "unknown": "Не определено", } def public_payload_role(role: Any) -> str: role_text = str(role or "unknown") return PUBLIC_PAYLOAD_ROLE_NAMES.get(role_text, role_text) PUBLIC_FORBIDDEN_KEYS = { "path", "evidence_path", "event_path", "command_path", "button_path", "strings_sample", "guids_sample", "type_guid", "resolved", "storage_routes", "file_name", "source_file", "module_id", "raw_bytes", "payload", "root", "root_marker", "markers", "command_guid", "_history_evidence", "text_preview", } PUBLIC_ALLOWED_PATH_KEYS = { "canonical_path", "context_path", "form_path", "input_path", "safe_as_metadata_path", } def sanitize_public_result(value: Any) -> Any: if isinstance(value, dict): sanitized = { key: sanitize_public_result(item) for key, item in value.items() if key not in PUBLIC_FORBIDDEN_KEYS and (not str(key).endswith("_path") or str(key) in PUBLIC_ALLOWED_PATH_KEYS) } if "kind" in sanitized and "presentation" in sanitized: sanitized.pop("code", None) if sanitized.get("source") == "platform_standard_field": sanitized.pop("source", None) return sanitized if isinstance(value, list): return [sanitize_public_result(item) for item in value] return value def tree_ordered_strings(tree: Any, *, limit: int = 500) -> list[str]: try: from parser.payload import collect_strings except Exception: return [] seen: set[str] = set() result = [] for value in collect_strings(tree, limit=limit): text = str(value or "") if not text or text in seen: continue seen.add(text) result.append(text) return result def tree_ordered_scalars(tree: Any, *, limit: int = 1000) -> list[str]: try: from parser.payload import scalar except Exception: return [] values: list[str] = [] def children(node: Any) -> list[Any]: if isinstance(node, dict) and node.get("type") in {"list", "sequence"}: return node.get("items") or [] return [] def walk(node: Any) -> None: if len(values) >= limit: return text = scalar(node) if text not in {None, ""}: values.append(str(text)) return for child in children(node): walk(child) walk(tree) return values def public_pattern_type_from_tree(tree: Any, resolved_types: dict[str, dict[str, Any]] | None = None, *, raw: bool = False) -> dict[str, Any] | None: values = tree_ordered_scalars(tree) try: index = values.index("Pattern") except ValueError: return None if index + 1 >= len(values): return None code = values[index + 1] result: dict[str, Any] = {"code": code} if code == "D": result.update({"kind": "date", "presentation": "Дата"}) elif code == "B": result.update({"kind": "boolean", "presentation": "Булево"}) elif code == "S": result.update({"kind": "string", "presentation": "Строка"}) if index + 2 < len(values): try: length = int(values[index + 2]) result["length"] = length if length > 0: result["presentation"] = f"Строка({length})" except ValueError: pass elif code == "N": result.update({"kind": "number", "presentation": "Число"}) if index + 2 < len(values): try: result["precision"] = int(values[index + 2]) except ValueError: pass if index + 3 < len(values): try: result["scale"] = int(values[index + 3]) except ValueError: pass if "precision" in result: scale = result.get("scale") result["presentation"] = f"Число({result['precision']}, {scale})" if scale is not None else f"Число({result['precision']})" elif code == "#": result.update({"kind": "reference", "presentation": "Ссылка"}) if index + 2 < len(values) and is_guid_text(values[index + 2]): result["type_guid"] = values[index + 2].lower() elif code == "R": result.update({"kind": "binary", "presentation": "ДвоичныеДанные"}) else: result.update({"kind": "unknown", "presentation": code}) if raw: return result return public_type_info(result, resolved_types or {}, include_storage=False) def scheduled_job_method_name(tree: Any, identity: dict[str, Any] | None) -> str | None: values = tree_ordered_scalars(tree) for index, value in enumerate(values[:-1]): if not is_guid_text(value) or value == "00000000-0000-0000-0000-000000000000": continue candidate = values[index + 1] next_values = values[index + 2 : index + 5] schedule_numbers = [item for item in next_values if re.fullmatch(r"-?\d+", item or "")] has_schedule_numbers = len(schedule_numbers) >= 2 if re.fullmatch(r"[A-Za-zА-Яа-яЁё_][\wА-Яа-яЁё]*", candidate or "") and len(candidate) > 3 and has_schedule_numbers: return candidate return None def scheduled_job_schedule_parameters(tree: Any, method_name: str | None) -> dict[str, Any]: values = tree_ordered_scalars(tree) if method_name: indexes = [index for index, value in enumerate(values) if value == method_name] else: indexes = [] for index in indexes: numeric = [] for value in values[index + 1 : index + 8]: if re.fullmatch(r"-?\d+", value or ""): numeric.append(int(value)) elif numeric: break if len(numeric) >= 2: result: dict[str, Any] = { "status": "partial", "interval_seconds": numeric[1], } if len(numeric) >= 3: result["offset_seconds"] = numeric[2] result["periodicity"] = { "presentation": { 0: "Не задана", 1: "Однократно", 2: "Ежедневно", 3: "Повторять с интервалом", 4: "Еженедельно", 5: "Ежемесячно", }.get(numeric[0], "Неизвестная периодичность"), } result["repeat"] = { "interval_seconds": numeric[1], "interval_presentation": format_seconds_ru(numeric[1]), **({"offset_seconds": numeric[2], "offset_presentation": format_seconds_ru(numeric[2])} if len(numeric) >= 3 else {}), } missing_note = "Поле не найдено в текущем декодированном payload расписания." result["activity"] = {"status": "not_found_in_decoded_metadata", "diagnostics": {"message": missing_note}} result["day_restrictions"] = {"status": "not_found_in_decoded_metadata", "days_of_week": [], "days_of_month": [], "diagnostics": {"message": missing_note}} result["time_window"] = {"status": "not_found_in_decoded_metadata", "start_time": None, "end_time": None, "diagnostics": {"message": missing_note}} result["date_window"] = {"status": "not_found_in_decoded_metadata", "start_date": None, "end_date": None, "diagnostics": {"message": missing_note}} result["kind"] = {"status": "not_found_in_decoded_metadata", "predefined": None, "user_defined": None, "diagnostics": {"message": missing_note}} return result return {"status": "not_decoded_yet"} def format_seconds_ru(seconds: int) -> str: if seconds == 0: return "0 секунд" parts = [] days, rem = divmod(abs(seconds), 86400) hours, rem = divmod(rem, 3600) minutes, secs = divmod(rem, 60) if days: parts.append(f"{days} дн.") if hours: parts.append(f"{hours} ч.") if minutes: parts.append(f"{minutes} мин.") if secs or not parts: parts.append(f"{secs} сек.") return ("-" if seconds < 0 else "") + " ".join(parts) def document_journal_document_types( base_id: str, tree: Any, *, dbnames_records: list[Any] | None = None, timeout_seconds: int = 60, table: str = "Config", ) -> list[dict[str, Any]]: values = [value.lower() for value in tree_ordered_scalars(tree, limit=2000) if is_guid_text(value)] records = dbnames_records if records is None: records, error = live_dbnames_records(base_id, timeout_seconds=timeout_seconds) if error: return [] document_guids = { str(getattr(record, "guid", "") or "").lower() for record in records or [] if DBNAMES_ROLE_KIND.get(str(getattr(record, "storage_role", "") or "")) == "Document" } result = [] seen: set[str] = set() for guid in values: if guid in seen or guid not in document_guids: continue identity, _ = live_config_identity(base_id, guid, timeout_seconds=timeout_seconds, table=table) result.append( { "guid": guid, "kind": "Document", "name": (identity or {}).get("name"), "synonym": next(iter(((identity or {}).get("synonyms") or {}).values()), None) if isinstance((identity or {}).get("synonyms"), dict) else None, } ) seen.add(guid) return result def document_journal_column_title(node: Any) -> tuple[str | None, str | None]: values = tree_ordered_scalars(node, limit=120) controls = {"#", "Pattern", "B", "U", "S", "N", "D", "ru", *[str(index) for index in range(20)]} name = next((value for value in values if value not in controls and not is_guid_text(value)), None) synonym = None for index, value in enumerate(values[:-1]): if value != "ru": continue candidate = values[index + 1] if candidate and candidate not in controls and not is_guid_text(candidate): synonym = candidate break return name, synonym def document_journal_record_containers(tree: Any) -> list[list[Any]]: try: from parser.child_records import declared_child_records from parser.payload import get_tree_path except Exception: return [] containers: list[list[Any]] = [] try: records = declared_child_records(get_tree_path(tree, "4"), "4") if records: containers.append(records) except Exception: pass def children(node: Any) -> list[Any]: if isinstance(node, dict) and node.get("type") in {"list", "sequence"}: return node.get("items") or [] return [] def walk(node: Any, path: str, depth: int) -> None: if depth > 4: return try: records = declared_child_records(node, path) except Exception: records = [] if records: containers.append(records) for index, child in enumerate(children(node)): walk(child, f"{path}.{index}" if path else str(index), depth + 1) walk(tree, "", 0) return containers def document_journal_field_type_map( base_id: str, document_types: list[dict[str, Any]], *, dbnames_records: list[Any] | None = None, timeout_seconds: int = 60, table: str = "Config", ) -> dict[str, dict[str, Any]]: result: dict[str, dict[str, Any]] = {} records = dbnames_records if records is None: records, error = live_dbnames_records(base_id, timeout_seconds=timeout_seconds) if error: records = [] for document in document_types: guid = str(document.get("guid") or "").lower() if not guid: continue data, _, read_error = read_storage_file_bytes(base_id, table, guid, timeout_seconds=timeout_seconds) if read_error: continue decoded = decode_config_object_full(data, kind="Document", dbnames_records=records, max_depth=3) semantic = decoded.get("semantic") if decoded.get("status") == "ok" else None sections = (semantic or {}).get("sections") or [] type_guids = collect_reference_type_guids_from_sections(sections) resolved_types = resolve_type_guids(base_id, type_guids, timeout_seconds=timeout_seconds, table=table) for section in sections: if section.get("category") != "Attribute": continue for attribute in section.get("records") or []: identity = attribute.get("identity") if isinstance(attribute.get("identity"), dict) else {} attribute_guid = str(identity.get("guid") or "").lower() if not attribute_guid or not attribute.get("type"): continue result[attribute_guid] = sanitize_public_result( public_type_info(attribute.get("type"), resolved_types, include_storage=False) ) return result def document_journal_column_type(record_node: Any, field_types: dict[str, dict[str, Any]]) -> dict[str, Any] | None: if not field_types: return None values = [value.lower() for value in tree_ordered_scalars(record_node, limit=500) if is_guid_text(value)] types = [] seen: set[str] = set() for guid in values: field_type = field_types.get(guid) if not field_type: continue key = json.dumps(field_type, ensure_ascii=False, sort_keys=True) if key in seen: continue seen.add(key) types.append(field_type) if not types: return None if len(types) == 1: return types[0] return { "kind": "composite", "presentation": "Составной тип", "types": types, } def document_journal_column_field_guids(record_node: Any) -> set[str]: ignored = {"00000000-0000-0000-0000-000000000000", "157fa490-4ce9-11d4-9415-008048da11f9"} return { value.lower() for value in tree_ordered_scalars(record_node, limit=800) if is_guid_text(value) and value.lower() not in ignored } def document_journal_all_column_field_guids(tree: Any) -> set[str]: result: set[str] = set() for records in document_journal_record_containers(tree): for record in records: node = getattr(record, "node", None) name, _ = document_journal_column_title(node) if name: result.update(document_journal_column_field_guids(node)) return result def document_journal_columns( base_id: str, tree: Any, *, document_types: list[dict[str, Any]] | None = None, dbnames_records: list[Any] | None = None, include_column_types: bool = False, timeout_seconds: int = 60, max_columns: int | None = None, table: str = "Config", ) -> list[dict[str, Any]]: field_types = ( document_journal_field_type_map( base_id, document_types or [], table=table, dbnames_records=dbnames_records, timeout_seconds=timeout_seconds, ) if include_column_types else {} ) return document_journal_columns_from_field_types( base_id, tree, field_types, table=table, timeout_seconds=timeout_seconds, max_columns=max_columns, ) def document_journal_columns_from_field_types( base_id: str, tree: Any, field_types: dict[str, dict[str, Any]], *, timeout_seconds: int = 60, max_columns: int | None = None, table: str = "Config", ) -> list[dict[str, Any]]: candidates = [] raw_type_guids: set[str] = set() for records in document_journal_record_containers(tree): columns = [] for record in records: node = getattr(record, "node", None) name, synonym = document_journal_column_title(node) raw_type = document_journal_column_type(node, field_types) or public_pattern_type_from_tree(node, {}) if isinstance(raw_type, dict) and raw_type.get("type_guid"): raw_type_guids.add(str(raw_type.get("type_guid")).lower()) if not name: continue columns.append({"name": name, "synonym": synonym, "_raw_type": raw_type}) if columns: typed = sum(1 for column in columns if column.get("_raw_type")) candidates.append((typed, len(columns), columns)) if not candidates: return [] _, _, columns = max(candidates, key=lambda item: (item[1] >= 2, item[1], item[0])) resolved_types = ( resolve_type_guids(base_id, raw_type_guids, timeout_seconds=timeout_seconds, table=table) if raw_type_guids else {} ) public_columns = [] seen: set[str] = set() for column in columns: if max_columns is not None and len(public_columns) >= max_columns: break name = str(column.get("name") or "") if not name or name in seen: continue seen.add(name) item: dict[str, Any] = {"name": name} if column.get("synonym"): item["synonym"] = column.get("synonym") raw_type = column.get("_raw_type") if raw_type: item["type"] = public_type_info(raw_type, resolved_types, include_storage=False) public_columns.append(item) return public_columns def object_row(guid: str, item: dict[str, Any], top: dict[str, Any], *, score: float | None = None, match_by: str | None = None) -> dict[str, Any]: internal_kind = canonical_kind(str(top.get("xml_kind") or "")) row = { "guid": guid, "kind": internal_kind, "kind_ru": RU_KIND.get(str(internal_kind or ""), internal_kind), "public_kind": PUBLIC_KIND.get(str(internal_kind or ""), "other"), "name": top.get("name"), "synonym": top.get("synonym"), "source": "extension" if is_extension_object(item, top) else "base", "relative_path": top.get("relative_path"), "path": top.get("path"), } if score is not None: row["score"] = score if match_by: row["match_by"] = match_by row["storage"] = compact_storage(item) return row 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, "name" if normalize(synonym) == wanted_norm: return 0.95, "synonym" if wanted_norm and wanted_norm in normalize(name): return 0.82, "name_contains" if wanted_norm and wanted_norm in normalize(synonym): return 0.78, "synonym_contains" if wanted_norm and wanted_norm in normalize(relative_path): return 0.65, "relative_path" return None def kind_matches_request(internal: str, wanted: str | None, requested_public: str | None) -> bool: if wanted: return internal == wanted if requested_public: return PUBLIC_KIND.get(internal, "other") == requested_public return True def parse_ordinal(value: Any, method: str, *, argument: str = "ordinal") -> tuple[int | None, dict[str, Any] | None]: if value is None or value == "": return None, None if isinstance(value, bool) or not isinstance(value, int): return None, invalid_argument(method, argument, f"{argument} must be a JSON integer.") ordinal = value if ordinal < 1: return None, invalid_argument(method, argument, f"{argument} is 1-based and must be >= 1.") return ordinal, None def parse_int_argument( payload: dict[str, Any], name: str, *, method: str, default: int, minimum: int = 0, maximum: int | None = None, ) -> tuple[int | None, dict[str, Any] | None]: if name not in payload: return default, None raw = payload.get(name) if isinstance(raw, bool) or not isinstance(raw, int): return None, { "schema": "onec_adapter_request_error.v1", "method": method, "status": "invalid_argument", "error": "invalid_argument", "argument": name, "diagnostics": {"message": f"{name} must be a JSON integer."}, } value = raw if value < minimum: return None, { "schema": "onec_adapter_request_error.v1", "method": method, "status": "invalid_argument", "error": "invalid_argument", "argument": name, "diagnostics": {"message": f"{name} must be >= {minimum}."}, } if maximum is not None and value > maximum: return None, { "schema": "onec_adapter_request_error.v1", "method": method, "status": "invalid_argument", "error": "invalid_argument", "argument": name, "diagnostics": {"message": f"{name} must be <= {maximum}."}, } return value, None def parse_int_alias_argument( payload: dict[str, Any], primary: str, alias: str, *, method: str, default: int, minimum: int = 0, maximum: int | None = None, ) -> tuple[int | None, dict[str, Any] | None]: if primary in payload and alias in payload: return None, invalid_argument(method, alias, f"Pass either {primary} or {alias}, not both.") key = primary if primary in payload else alias return parse_int_argument({key: payload.get(key)} if key in payload else {}, key, method=method, default=default, minimum=minimum, maximum=maximum) def first_non_empty_arg(payload: dict[str, Any], *names: str, default: Any = None) -> Any: for name in names: value = payload.get(name) if value is not None and value != "": return value return default def validate_explicit_ordinal_arguments(payload: dict[str, Any], method: str, names: tuple[str, ...] = ("ordinal", "index", "object_index")) -> dict[str, Any] | None: for name in names: value = payload.get(name) if name in payload and (value is None or value == ""): return invalid_argument(method, name, f"{name} must be a JSON integer when provided.") return None def validate_explicit_guid_argument(payload: dict[str, Any], method: str, argument: str = "guid") -> dict[str, Any] | None: if argument not in payload: return None value = payload.get(argument) if value is None or value == "": return invalid_argument(method, argument, f"{argument} must be a non-empty JSON string when provided.") if not isinstance(value, str): return invalid_argument(method, argument, f"{argument} must be a JSON string.") return None def parse_object_lookup_limit(payload: dict[str, Any], method: str, *, default: int = 20) -> tuple[int | None, dict[str, Any] | None]: return parse_int_argument(payload, "limit", method=method, default=default, minimum=1) OBJECT_VIEW_VALUES = ["effective", "base", "extension"] EVIDENCE_MODE_VALUES = ["none", "summary", "full", "raw"] def parse_view_argument(payload: dict[str, Any], method: str, *, default: str = "effective") -> tuple[str | None, dict[str, Any] | None]: if "view" not in payload: return default, None raw = payload.get("view") if raw is None or raw == "": return None, invalid_argument(method, "view", f"view must be one of: {', '.join(OBJECT_VIEW_VALUES)}.", allowed_values=OBJECT_VIEW_VALUES) if not isinstance(raw, str): return None, invalid_argument(method, "view", "view must be a JSON string.", allowed_values=OBJECT_VIEW_VALUES) value = raw.strip().casefold() if value not in OBJECT_VIEW_VALUES: return None, invalid_argument(method, "view", f"view must be one of: {', '.join(OBJECT_VIEW_VALUES)}.", allowed_values=OBJECT_VIEW_VALUES) return value, None def parse_evidence_mode_argument(payload: dict[str, Any], method: str, *, default: str = "summary") -> tuple[str | None, dict[str, Any] | None]: key = "evidence_mode" if "evidence_mode" in payload else "undecoded_evidence_mode" if "undecoded_evidence_mode" in payload else "" if not key: return default, None raw = payload.get(key) if raw is None or raw == "": return None, invalid_argument(method, key, f"{key} must be one of: {', '.join(EVIDENCE_MODE_VALUES)}.", allowed_values=EVIDENCE_MODE_VALUES) if not isinstance(raw, str): return None, invalid_argument(method, key, f"{key} must be a JSON string.", allowed_values=EVIDENCE_MODE_VALUES) value = raw.strip().casefold() if value not in EVIDENCE_MODE_VALUES: return None, invalid_argument(method, key, f"{key} must be one of: {', '.join(EVIDENCE_MODE_VALUES)}.", allowed_values=EVIDENCE_MODE_VALUES) return value, None def is_guid_text(value: Any) -> bool: return bool(re.fullmatch(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", str(value or "").strip())) def config_tree_scalar(node: Any) -> str: if not isinstance(node, dict): return str(node or "") if node.get("type") in {"atom", "string"}: return str(node.get("value") or "") return "" def config_tree_item_at_path(tree: Any, path: tuple[int, ...]) -> Any | None: node = tree for index in path: if not isinstance(node, dict) or not isinstance(node.get("items"), list): return None items = node.get("items") or [] if index < 0 or index >= len(items): return None node = items[index] return node def config_tree_scalar_at_path(tree: Any, path: tuple[int, ...]) -> str: return config_tree_scalar(config_tree_item_at_path(tree, path)) def config_tree_localized_text(node: Any) -> dict[str, str]: if not isinstance(node, dict) or not isinstance(node.get("items"), list): return {} items = node.get("items") or [] if len(items) >= 3 and config_tree_scalar(items[0]).isdigit(): language = config_tree_scalar(items[1]) content = config_tree_scalar(items[2]) return {language: content} if language and content else {} result: dict[str, str] = {} for item in items: result.update(config_tree_localized_text(item)) return result def sql_config_property(name: str, value: Any, *, raw: str, path: str, confidence: str, include_storage: bool) -> dict[str, Any]: return { "name": name, "value": value, "confidence": confidence, "evidence": "live_sql_config_decoder", **({"storage": {"config_path": path, "raw": raw}} if include_storage else {}), } def document_numerator_sql_details(tree: Any, *, include_storage: bool) -> dict[str, Any]: raw_values = { "NumberType": config_tree_scalar_at_path(tree, (1, 2)), "NumberLength": config_tree_scalar_at_path(tree, (1, 3)), "NumberAllowedLength": config_tree_scalar_at_path(tree, (1, 4)), "NumberPeriodicity": config_tree_scalar_at_path(tree, (1, 5)), "CheckUnique": config_tree_scalar_at_path(tree, (1, 6)), } values: dict[str, Any] = { "NumberType": {"1": "String"}.get(raw_values["NumberType"], {"status": "unknown_code", "code": raw_values["NumberType"]}), "NumberLength": int(raw_values["NumberLength"]) if raw_values["NumberLength"].isdigit() else None, "NumberAllowedLength": {"1": "Variable"}.get(raw_values["NumberAllowedLength"], {"status": "unknown_code", "code": raw_values["NumberAllowedLength"]}), "NumberPeriodicity": {"1": "Year"}.get(raw_values["NumberPeriodicity"], {"status": "unknown_code", "code": raw_values["NumberPeriodicity"]}), "CheckUnique": {"0": False, "1": True}.get(raw_values["CheckUnique"], None), } paths = {"NumberType": "1.2", "NumberLength": "1.3", "NumberAllowedLength": "1.4", "NumberPeriodicity": "1.5", "CheckUnique": "1.6"} properties = [ sql_config_property(name, values[name], raw=raw_values[name], path=paths[name], confidence="high", include_storage=include_storage) for name in paths ] return { "number_type": values["NumberType"], "number_length": values["NumberLength"], "number_allowed_length": values["NumberAllowedLength"], "number_periodicity": values["NumberPeriodicity"], "check_unique": values["CheckUnique"], "properties": properties, } def chart_of_calculation_types_sql_details(tree: Any, *, include_storage: bool) -> dict[str, Any]: """Decode stable scalar settings from a ChartOfCalculationTypes descriptor. The paths were verified against live Config payloads for Начисления and Удержания and their offline XML exports. XML is evidence only; runtime values always come from the SQL Config payload. """ paths = { "UseStandardCommands": "1.24", "CodeLength": "1.25", "CodeType": "1.26", "CodeAllowedLength": "1.27", "DescriptionLength": "1.30", "DefaultObjectForm": "1.32", "DefaultListForm": "1.33", "DefaultChoiceForm": "1.34", "AuxiliaryObjectForm": "1.45", "AuxiliaryListForm": "1.46", "AuxiliaryChoiceForm": "1.47", "ObjectPresentation": "1.48", "ExtendedObjectPresentation": "1.49", "ListPresentation": "1.50", "ExtendedListPresentation": "1.51", "Explanation": "1.52", "ActionPeriodUse": "1.57", } raw_values = { name: config_tree_scalar_at_path(tree, tuple(int(part) for part in path.split("."))) for name, path in paths.items() } def optional_guid(raw: str) -> dict[str, Any] | None: return {"guid": raw.lower(), "status": "unresolved"} if is_guid_text(raw) and raw != "00000000-0000-0000-0000-000000000000" else None values: dict[str, Any] = { "UseStandardCommands": {"0": False, "1": True}.get(raw_values["UseStandardCommands"]), "CodeLength": int(raw_values["CodeLength"]) if raw_values["CodeLength"].isdigit() else None, "CodeType": {"1": "String"}.get(raw_values["CodeType"], {"status": "unknown_code", "code": raw_values["CodeType"]}), "CodeAllowedLength": {"0": "Variable"}.get( raw_values["CodeAllowedLength"], {"status": "unknown_code", "code": raw_values["CodeAllowedLength"]}, ), "DescriptionLength": int(raw_values["DescriptionLength"]) if raw_values["DescriptionLength"].isdigit() else None, "DefaultObjectForm": optional_guid(raw_values["DefaultObjectForm"]), "DefaultListForm": optional_guid(raw_values["DefaultListForm"]), "DefaultChoiceForm": optional_guid(raw_values["DefaultChoiceForm"]), "AuxiliaryObjectForm": optional_guid(raw_values["AuxiliaryObjectForm"]), "AuxiliaryListForm": optional_guid(raw_values["AuxiliaryListForm"]), "AuxiliaryChoiceForm": optional_guid(raw_values["AuxiliaryChoiceForm"]), "ObjectPresentation": config_tree_localized_text(config_tree_item_at_path(tree, (1, 48))), "ExtendedObjectPresentation": config_tree_localized_text(config_tree_item_at_path(tree, (1, 49))), "ListPresentation": config_tree_localized_text(config_tree_item_at_path(tree, (1, 50))), "ExtendedListPresentation": config_tree_localized_text(config_tree_item_at_path(tree, (1, 51))), "Explanation": config_tree_localized_text(config_tree_item_at_path(tree, (1, 52))), "ActionPeriodUse": {"0": False, "1": True}.get(raw_values["ActionPeriodUse"]), } properties = [ sql_config_property(name, values[name], raw=raw_values[name], path=paths[name], confidence="high", include_storage=include_storage) for name in paths ] return { "use_standard_commands": values["UseStandardCommands"], "code_length": values["CodeLength"], "code_type": values["CodeType"], "code_allowed_length": values["CodeAllowedLength"], "description_length": values["DescriptionLength"], "default_object_form": values["DefaultObjectForm"], "default_list_form": values["DefaultListForm"], "default_choice_form": values["DefaultChoiceForm"], "auxiliary_object_form": values["AuxiliaryObjectForm"], "auxiliary_list_form": values["AuxiliaryListForm"], "auxiliary_choice_form": values["AuxiliaryChoiceForm"], "object_presentation": values["ObjectPresentation"], "extended_object_presentation": values["ExtendedObjectPresentation"], "list_presentation": values["ListPresentation"], "extended_list_presentation": values["ExtendedListPresentation"], "explanation": values["Explanation"], "action_period_use": values["ActionPeriodUse"], "properties": properties, } def calculation_register_sql_details(tree: Any, *, include_storage: bool) -> dict[str, Any]: """Decode the verified scalar header of a CalculationRegister descriptor.""" paths = { "Periodicity": "1.16", "ActionPeriod": "1.17", "BasePeriod": "1.18", "DefaultListForm": "1.19", "AuxiliaryListForm": "1.20", "ChartOfCalculationTypes": "1.22", "UseStandardCommands": "1.24", "IncludeHelpInContents": "1.25", "DataLockControlMode": "1.26", "FullTextSearch": "1.27", "ListPresentation": "1.28", "ExtendedListPresentation": "1.30", "Explanation": "1.31", } raw_values = { name: config_tree_scalar_at_path(tree, tuple(int(part) for part in path.split("."))) for name, path in paths.items() } def optional_guid(raw: str, *, kind: str | None = None) -> dict[str, Any] | None: if not is_guid_text(raw) or raw == "00000000-0000-0000-0000-000000000000": return None return {"guid": raw.lower(), **({"kind": kind} if kind else {}), "status": "unresolved"} values: dict[str, Any] = { "Periodicity": {"2": "Month"}.get(raw_values["Periodicity"], {"status": "unknown_code", "code": raw_values["Periodicity"]}), "ActionPeriod": {"0": False, "1": True}.get(raw_values["ActionPeriod"]), "BasePeriod": {"0": False, "1": True}.get(raw_values["BasePeriod"]), "DefaultListForm": optional_guid(raw_values["DefaultListForm"], kind="Form"), "AuxiliaryListForm": optional_guid(raw_values["AuxiliaryListForm"], kind="Form"), "ChartOfCalculationTypes": optional_guid(raw_values["ChartOfCalculationTypes"], kind="ChartOfCalculationTypes"), "UseStandardCommands": {"0": False, "1": True}.get(raw_values["UseStandardCommands"]), "IncludeHelpInContents": {"0": False, "1": True}.get(raw_values["IncludeHelpInContents"]), "DataLockControlMode": {"1": "Managed"}.get( raw_values["DataLockControlMode"], {"status": "unknown_code", "code": raw_values["DataLockControlMode"]}, ), "FullTextSearch": {"0": "DontUse"}.get(raw_values["FullTextSearch"], {"status": "unknown_code", "code": raw_values["FullTextSearch"]}), "ListPresentation": config_tree_localized_text(config_tree_item_at_path(tree, (1, 28))), "ExtendedListPresentation": config_tree_localized_text(config_tree_item_at_path(tree, (1, 30))), "Explanation": config_tree_localized_text(config_tree_item_at_path(tree, (1, 31))), } properties = [ sql_config_property(name, values[name], raw=raw_values[name], path=paths[name], confidence="high", include_storage=include_storage) for name in paths ] known_collection_roles = { 3: "Attribute", 4: "Recalculation", 5: "Template", 6: "Resource", 7: "Form", 8: "Command", 9: "Dimension", } child_collections = [] for path_index in range(3, 10): node = config_tree_item_at_path(tree, (path_index,)) items = node.get("items") if isinstance(node, dict) and isinstance(node.get("items"), list) else [] marker_guid = config_tree_scalar(items[0]) if items else "" declared_raw = config_tree_scalar(items[1]) if len(items) > 1 else "" declared_count = int(declared_raw) if declared_raw.isdigit() else 0 role = known_collection_roles.get(path_index) child_collections.append( { "path": str(path_index), "collection_ordinal": path_index - 2, "role": role or "UnclassifiedFieldCollection", "status": "classified" if role else "needs_non_empty_sample", "declared_count": declared_count, **({"possible_roles": ["Dimension", "Resource", "Attribute"]} if not role else {}), **({"storage": {"class_guid": marker_guid}} if include_storage and is_guid_text(marker_guid) else {}), } ) return { "periodicity": values["Periodicity"], "action_period": values["ActionPeriod"], "base_period": values["BasePeriod"], "default_list_form": values["DefaultListForm"], "auxiliary_list_form": values["AuxiliaryListForm"], "chart_of_calculation_types": values["ChartOfCalculationTypes"], "use_standard_commands": values["UseStandardCommands"], "include_help_in_contents": values["IncludeHelpInContents"], "data_lock_control_mode": values["DataLockControlMode"], "full_text_search": values["FullTextSearch"], "list_presentation": values["ListPresentation"], "extended_list_presentation": values["ExtendedListPresentation"], "explanation": values["Explanation"], "child_collections": child_collections, "properties": properties, } def configuration_sql_details(tree: Any, *, include_storage: bool) -> dict[str, Any]: paths: dict[str, tuple[int, ...]] = { "Name": (3, 1, 1, 1, 1, 2), "Synonym": (3, 1, 1, 1, 1, 3), "DetailedInformation": (3, 1, 1, 4), "BriefInformation": (3, 1, 1, 5), "Copyright": (3, 1, 1, 6), "VendorInformationAddress": (3, 1, 1, 7), "ConfigurationInformationAddress": (3, 1, 1, 8), "Vendor": (3, 1, 1, 14), "Version": (3, 1, 1, 15), "UpdateCatalogAddress": (3, 1, 1, 16), } localized = {"Synonym", "DetailedInformation", "BriefInformation", "Copyright", "VendorInformationAddress", "ConfigurationInformationAddress"} values: dict[str, Any] = {} properties: list[dict[str, Any]] = [] for name, path in paths.items(): node = config_tree_item_at_path(tree, path) value: Any = config_tree_localized_text(node) if name in localized else config_tree_scalar(node) values[name] = value properties.append( sql_config_property( name, value, raw=config_tree_scalar(node), path=".".join(str(index) for index in path), confidence="high", include_storage=include_storage, ) ) return { "name": values["Name"], "synonyms": values["Synonym"], "vendor": values["Vendor"], "version": values["Version"], "update_catalog_address": values["UpdateCatalogAddress"], "brief_information": values["BriefInformation"], "detailed_information": values["DetailedInformation"], "copyright": values["Copyright"], "vendor_information_address": values["VendorInformationAddress"], "configuration_information_address": values["ConfigurationInformationAddress"], "properties": properties, "counts": {"decoded_properties": sum(1 for value in values.values() if value not in (None, "", {})), "declared_properties": len(paths)}, } def integration_service_sql_details(tree: Any, *, include_storage: bool) -> dict[str, Any]: container = config_tree_item_at_path(tree, (3,)) items = container.get("items") if isinstance(container, dict) and isinstance(container.get("items"), list) else [] declared = int(config_tree_scalar(items[1])) if len(items) > 1 and config_tree_scalar(items[1]).isdigit() else 0 channels: list[dict[str, Any]] = [] for ordinal, wrapper in enumerate(items[2 : 2 + declared], start=1): wrapper_items = wrapper.get("items") if isinstance(wrapper, dict) and isinstance(wrapper.get("items"), list) else [] record = wrapper_items[0] if wrapper_items else None record_items = record.get("items") if isinstance(record, dict) and isinstance(record.get("items"), list) else [] if len(record_items) < 8: continue identity = record_items[1] identity_items = identity.get("items") if isinstance(identity, dict) and isinstance(identity.get("items"), list) else [] name = config_tree_scalar(identity_items[2]) if len(identity_items) > 2 else "" synonyms = config_tree_localized_text(identity_items[3]) if len(identity_items) > 3 else {} direction_raw = config_tree_scalar(record_items[6]) transaction_raw = config_tree_scalar(record_items[7]) channel = { "ordinal": ordinal, "name": name, "synonyms": synonyms, "external_channel_name": config_tree_scalar(record_items[4]), "message_direction": {"0": "Send", "1": "Receive"}.get(direction_raw, {"status": "unknown_code", "code": direction_raw}), "receive_message_processing": config_tree_scalar(record_items[5]) or None, "transactioned": {"0": False, "1": True}.get(transaction_raw), "confidence": "high", "evidence": "live_sql_config_decoder", } if include_storage: channel["storage"] = { "config_path": f"3.{ordinal + 1}.0", "generated_type_id": config_tree_scalar(record_items[2]), "generated_value_id": config_tree_scalar(record_items[3]), "direction_raw": direction_raw, "transactioned_raw": transaction_raw, } channels.append(channel) return { "external_integration_service_address": {"status": "not_decoded", "reason": "no_non_empty_sql_sample"}, "channels": channels, "counts": {"channels": len(channels), "declared_channels": declared}, } def public_pattern_value_type( base_id: str, type_node: Any, *, table: str = "Config", timeout_seconds: int = 60, ) -> dict[str, Any] | None: values = tree_ordered_scalars(type_node) type_guids: list[str] = [] for index, value in enumerate(values[:-1]): candidate = str(values[index + 1] or "").strip().lower() if value == "#" and is_guid_text(candidate) and candidate not in type_guids: type_guids.append(candidate) resolved_types = resolve_type_guids(base_id, set(type_guids), timeout_seconds=timeout_seconds, table=table) types: list[dict[str, Any]] = [] items = config_tree_list_items(type_node) for item in items[1:]: item_values = tree_ordered_scalars(item) if not item_values: continue code = item_values[0] if code == "#" and len(item_values) > 1 and is_guid_text(item_values[1]): resolved = resolved_types.get(item_values[1].lower()) or {} if resolved.get("guid_role") == "builtin_type": types.append( { "kind": "builtin", "name": resolved.get("name"), "presentation": resolved.get("presentation") or resolved.get("name"), "bsl_type": resolved.get("bsl_type"), } ) continue types.append( public_type_info( {"kind": "reference", "presentation": "Ссылка", "type_guid": item_values[1].lower()}, resolved_types, include_storage=False, ) ) continue if code in {"B", "D", "N", "R", "S"}: synthetic = {"type": "list", "items": [{"type": "string", "value": "Pattern"}, item]} primitive = public_pattern_type_from_tree(synthetic) if isinstance(primitive, dict): types.append(primitive) if not types: return public_pattern_type_from_tree(type_node, resolved_types) if len(types) == 1: return types[0] presentations = [str(item.get("presentation") or "Значение") for item in types if isinstance(item, dict)] return {"kind": "union", "presentation": " | ".join(presentations), "types": types, "count": len(types)} def public_tree_metadata_references( base_id: str, node: Any, *, table: str = "Config", timeout_seconds: int = 60, ) -> list[dict[str, Any]]: zero_guid = "00000000-0000-0000-0000-000000000000" guids: list[str] = [] for guid in config_tree_guids(node): normalized = str(guid or "").lower() if normalized != zero_guid and normalized not in guids: guids.append(normalized) resolved = public_metadata_guid_references(base_id, guids, table=table, timeout_seconds=timeout_seconds) result: list[dict[str, Any]] = [] seen_refs: set[str] = set() for guid in guids: item = resolved.get(guid) ref = str((item or {}).get("ref") or "") if isinstance(item, dict) else "" if not ref or ref in seen_refs or (item or {}).get("status") not in {"ok", "resolved"}: continue seen_refs.add(ref) result.append(item) return result def common_attribute_sql_details(base_id: str, tree: Any, *, table: str, timeout_seconds: int) -> dict[str, Any]: bool_use = {"0": "DontUse", "1": "Use"} separation = {"0": "DontUse", "1": "Separate"} return { "value_type": public_pattern_value_type(base_id, config_tree_item_at_path(tree, (1, 1, 1, 2)), table=table, timeout_seconds=timeout_seconds), "content": public_tree_metadata_references(base_id, config_tree_item_at_path(tree, (1, 2)), table=table, timeout_seconds=timeout_seconds), "indexing": {"0": "DontIndex", "1": "Index", "2": "IndexWithAdditionalOrder"}.get(config_tree_scalar_at_path(tree, (1, 3)), "Unknown"), "full_text_search": bool_use.get(config_tree_scalar_at_path(tree, (1, 4))), "data_separation": {"0": "Separate", "1": "DontUse"}.get(config_tree_scalar_at_path(tree, (1, 5))), "separated_data_use": {"0": "IndependentlyAndSimultaneously", "1": "Independently"}.get(config_tree_scalar_at_path(tree, (1, 6))), "data_separation_value": public_tree_metadata_references(base_id, config_tree_item_at_path(tree, (1, 7)), table=table, timeout_seconds=timeout_seconds), "data_separation_use": public_tree_metadata_references(base_id, config_tree_item_at_path(tree, (1, 8)), table=table, timeout_seconds=timeout_seconds), "conditional_separation": public_tree_metadata_references(base_id, config_tree_item_at_path(tree, (1, 9)), table=table, timeout_seconds=timeout_seconds), "users_separation": separation.get(config_tree_scalar_at_path(tree, (1, 10))), "authentication_separation": separation.get(config_tree_scalar_at_path(tree, (1, 11))), "configuration_extensions_separation": separation.get(config_tree_scalar_at_path(tree, (1, 13))), "data_history": bool_use.get(config_tree_scalar_at_path(tree, (1, 14))), } def session_parameter_sql_details(base_id: str, tree: Any, *, table: str, timeout_seconds: int) -> dict[str, Any]: return {"value_type": public_pattern_value_type(base_id, config_tree_item_at_path(tree, (1, 1, 2)), table=table, timeout_seconds=timeout_seconds)} def functional_option_sql_details(base_id: str, tree: Any, *, table: str, timeout_seconds: int) -> dict[str, Any]: location_guid = config_tree_scalar_at_path(tree, (1, 2)).strip().lower() location_map = ( public_metadata_guid_references(base_id, [location_guid], table=table, timeout_seconds=timeout_seconds) if is_guid_text(location_guid) else {} ) return { "location": location_map.get(location_guid), "privileged_get_mode": {"0": False, "1": True}.get(config_tree_scalar_at_path(tree, (1, 4))), "content": public_tree_metadata_references(base_id, config_tree_item_at_path(tree, (1, 3)), table=table, timeout_seconds=timeout_seconds), } def functional_options_parameter_sql_details(base_id: str, tree: Any, *, table: str, timeout_seconds: int) -> dict[str, Any]: return {"use": public_tree_metadata_references(base_id, config_tree_item_at_path(tree, (1, 2)), table=table, timeout_seconds=timeout_seconds)} STANDARD_COMMAND_GROUP_GUIDS = { "1af6d528-0b86-4fba-ab95-bd7475db03ba": "NavigationPanelImportant", "4f499c31-050b-47c5-aa84-d0366c0a0da8": "ActionsPanelCreate", "5b360bff-01a1-49b6-93d2-26e7e8e3a038": "ActionsPanelReports", "77ea1b8f-dd79-4717-9dba-5628e7f348cf": "NavigationPanelOrdinary", "8ab1540c-0bfa-4fa6-a1e1-5d5069efc7d8": "FormNavigationPanelSeeAlso", "aabb34e1-98c1-4bd0-bf7f-243f95437b44": "ActionsPanelTools", "bc80566a-86a5-4e87-acd4-872239385a2e": "NavigationPanelSeeAlso", "cb50f5c0-8013-4262-93a2-f0db379d6b6b": "FormCommandBarImportant", "dc11a6be-de1f-4b64-a7a5-9b17bf4ec9f2": "FormNavigationPanelImportant", "dc2ade0f-383e-4c78-85f2-c0dabc0e2dc0": "FormCommandBarCreateBasedOn", "eacad741-96b9-4b3a-bf79-dde9ecead1a1": "FormNavigationPanelGoTo", } STYLE_VALUE_TYPE_GUIDS = { "9cd510c7-abfc-11d4-9434-004095e12fc7": "Color", "9cd510c8-abfc-11d4-9434-004095e12fc7": "Font", "4d10ca00-111a-4d43-9c96-92cd773716de": "Border", } STYLE_VALUE_TYPE_CODES = {"0": "Color", "1": "Font", "2": "Border"} # The numeric values are stable platform web-color identifiers. The map is # intentionally additive: an unknown code remains visible instead of being # guessed or discarded. STYLE_WEB_COLOR_CODES = { "8": "Black", "10": "Blue", "20": "Cream", "21": "Crimson", "23": "DarkBlue", "26": "DarkGray", "33": "DarkRed", "37": "DarkSlateGray", "44": "FireBrick", "46": "ForestGreen", "48": "Gainsboro", "49": "GhostWhite", "52": "Gray", "53": "Green", "55": "HoneyDew", "64": "LemonChiffon", "67": "LightCyan", "71": "LightGray", "72": "LightPink", "79": "LightYellow", "82": "Linen", "84": "Maroon", "86": "MediumBlue", "87": "MediumGray", "98": "MistyRose", "105": "Orange", "115": "Pink", "119": "Red", "128": "Silver", "130": "SlateBlue", "134": "SteelBlue", "140": "Violet", "141": "VioletRed", "144": "WhiteSmoke", "145": "Yellow", } STANDARD_STYLE_CODES = { "-42": "NavigationColor", "-32": "LargeTextFont", "-31": "NormalTextFont", "-23": "ToolTipBackColor", "-21": "ButtonTextColor", "-16": "SpecialTextColor", "-3": "FormTextColor", "-1": "FormBackColor", } def style_color_sql_value(node: Any) -> dict[str, Any]: variant = config_tree_scalar_at_path(node, (1,)) code = config_tree_scalar_at_path(node, (2, 0)) if variant == "0" and re.fullmatch(r"\d+", code or ""): number = int(code) rgb = f"#{number & 255:02X}{(number >> 8) & 255:02X}{(number >> 16) & 255:02X}" return {"kind": "absolute", "value": rgb, "storage_bgr": number} if variant == "2": name = STYLE_WEB_COLOR_CODES.get(code) return {"kind": "web", "code": int(code) if re.fullmatch(r"\d+", code or "") else code, "value": f"web:{name}" if name else None, "status": "ok" if name else "unknown_code"} if variant == "3": name = STANDARD_STYLE_CODES.get(code) return {"kind": "standard_style", "code": int(code) if re.fullmatch(r"-?\d+", code or "") else code, "value": f"style:{name}" if name else None, "status": "ok" if name else "unknown_code"} return {"kind": "unknown", "variant": variant, "code": code, "status": "unknown_encoding"} def style_font_sql_value(node: Any) -> dict[str, Any]: values = [config_tree_scalar_at_path(node, (index,)) for index in range(19)] height_raw = values[3] weight_raw = values[7] scale_raw = values[18] return { "kind": "absolute" if values[17] == "1" else "platform", "face_name": values[16] or None, "height": (int(height_raw) / 10) if re.fullmatch(r"-?\d+", height_raw or "") else None, "bold": int(weight_raw) >= 700 if re.fullmatch(r"-?\d+", weight_raw or "") else None, "italic": values[8] == "1", "underline": values[9] == "1", "strikeout": values[10] == "1", "scale": int(scale_raw) if re.fullmatch(r"\d+", scale_raw or "") else None, } def style_border_sql_value(node: Any) -> dict[str, Any]: style_code = config_tree_scalar_at_path(node, (2, 0)) width = config_tree_scalar_at_path(node, (3,)) return { "style": {"0": "Single"}.get(style_code, {"status": "unknown_code", "code": style_code}), "width": int(width) if re.fullmatch(r"\d+", width or "") else None, } def style_typed_sql_value(node: Any, value_type: str) -> dict[str, Any]: if value_type == "Color": return style_color_sql_value(node) if value_type == "Font": return style_font_sql_value(node) if value_type == "Border": return style_border_sql_value(node) return {"status": "unknown_type", "type": value_type} def style_item_sql_details(tree: Any) -> dict[str, Any]: type_code = config_tree_scalar_at_path(tree, (1, 1)) wrapper = config_tree_item_at_path(tree, (1, 2)) type_guid = config_tree_scalar_at_path(wrapper, (1,)).lower() value_type = STYLE_VALUE_TYPE_CODES.get(type_code) or STYLE_VALUE_TYPE_GUIDS.get(type_guid) or "Unknown" value_node = config_tree_item_at_path(wrapper, (3,)) return { "value_type": value_type, "value": style_typed_sql_value(value_node, value_type), } def language_sql_details(tree: Any) -> dict[str, Any]: return {"language_code": config_tree_scalar_at_path(tree, (1, 2)) or None} def binary_part_summary(data: bytes) -> dict[str, Any]: tree = parse_config_tree_from_bytes(data or b"") encoded = "" for node in iter_config_tree_nodes(tree): items = node.get("items") if isinstance(node.get("items"), list) else [] if items and config_tree_scalar(items[0]) == "#base64": encoded = "".join(config_tree_scalar(item) for item in items[1:]) break try: content = base64.b64decode(encoded, validate=True) if encoded else b"" except Exception: content = b"" media_type = None if content.startswith(b"\xff\xd8\xff"): media_type = "image/jpeg" elif content.startswith(b"\x89PNG\r\n\x1a\n"): media_type = "image/png" elif content.startswith((b"GIF87a", b"GIF89a")): media_type = "image/gif" elif content.startswith(b"BM"): media_type = "image/bmp" elif content.startswith(b"PK\x03\x04"): media_type = "application/zip" elif content.startswith(b"\x00\x00\x01\x00"): media_type = "image/x-icon" elif b" dict[str, Any]: return { "availability_for_choice": {"0": False, "1": True}.get(config_tree_scalar_at_path(tree, (1, 2))), "availability_for_appearance": {"0": False, "1": True}.get(config_tree_scalar_at_path(tree, (1, 3))), "binary": binary_part_summary(binary_data or b"") if binary_data is not None else {"status": "source_missing"}, } def style_sql_details(tree: Any, values_tree: Any) -> dict[str, Any]: root_items = values_tree.get("items") if isinstance(values_tree, dict) and isinstance(values_tree.get("items"), list) else [] declared = int(config_tree_scalar(root_items[1])) if len(root_items) > 1 and re.fullmatch(r"\d+", config_tree_scalar(root_items[1])) else 0 items: list[dict[str, Any]] = [] for entry in root_items[2:]: type_code = config_tree_scalar_at_path(entry, (1,)) value_type = STYLE_VALUE_TYPE_CODES.get(type_code, "Unknown") standard_code = config_tree_scalar_at_path(entry, (0, 0)) standard_name = STANDARD_STYLE_CODES.get(standard_code) items.append({ "standard_code": int(standard_code) if re.fullmatch(r"-?\d+", standard_code or "") else standard_code, "standard_name": standard_name, "ref": f"style:{standard_name}" if standard_name else None, "value_type": value_type, "value": style_typed_sql_value(config_tree_item_at_path(entry, (2,)), value_type), }) return {"items": items, "declared_items": declared} def xml_local_name(value: str) -> str: return str(value or "").rsplit("}", 1)[-1] def public_xml_attributes(element: ET.Element) -> dict[str, str]: return {xml_local_name(key): value for key, value in element.attrib.items()} def xdto_property_xml_details(element: ET.Element) -> dict[str, Any]: result: dict[str, Any] = public_xml_attributes(element) inline_type = next((child for child in element if xml_local_name(child.tag) == "typeDef"), None) if inline_type is not None: result["type_definition"] = xdto_type_xml_details(inline_type) return result def xdto_type_xml_details(element: ET.Element) -> dict[str, Any]: result: dict[str, Any] = { "kind": xml_local_name(element.tag), **public_xml_attributes(element), } properties = [xdto_property_xml_details(child) for child in element if xml_local_name(child.tag) == "property"] enumerations = [ {"value": (child.text or "").strip(), **public_xml_attributes(child)} for child in element if xml_local_name(child.tag) == "enumeration" ] if properties: result["properties"] = properties if enumerations: result["enumerations"] = enumerations return result def xdto_package_xml_details(data: bytes) -> dict[str, Any]: try: from parser.payload import decode_payload_lossless decoded = decode_payload_lossless(data or b"") payload = bytes(decoded.get("payload") or b"") root = ET.fromstring(payload) except Exception as exc: return {"status": "invalid_xml", "diagnostics": {"message": str(exc)}} imports = [public_xml_attributes(child).get("namespace") for child in root if xml_local_name(child.tag) == "import"] types = [ xdto_type_xml_details(child) for child in root if xml_local_name(child.tag) in {"objectType", "valueType"} ] tag_counts: dict[str, int] = {} for element in root.iter(): tag = xml_local_name(element.tag) tag_counts[tag] = tag_counts.get(tag, 0) + 1 return { "status": "ok", "namespace": root.attrib.get("targetNamespace"), "element_form_qualified": root.attrib.get("elementFormQualified"), "attribute_form_qualified": root.attrib.get("attributeFormQualified"), "imports": [value for value in imports if value], "types": types, "counts": { "types": len(types), "object_types": tag_counts.get("objectType", 0), "value_types": tag_counts.get("valueType", 0), "properties": tag_counts.get("property", 0), "enumerations": tag_counts.get("enumeration", 0), "inline_type_definitions": tag_counts.get("typeDef", 0), "imports": tag_counts.get("import", 0), }, } def wsdl_operation_xml_details(element: ET.Element, actions: dict[str, str]) -> dict[str, Any]: name = element.attrib.get("name") result: dict[str, Any] = {"name": name} for child in element: tag = xml_local_name(child.tag) if tag in {"input", "output", "fault"}: result[tag] = public_xml_attributes(child) if name in actions: result["soap_action"] = actions[name] return result def wsdl_xml_details(root: ET.Element) -> dict[str, Any]: actions: dict[str, str] = {} bindings: list[dict[str, Any]] = [] for binding in [item for item in root if xml_local_name(item.tag) == "binding"]: operations: list[dict[str, Any]] = [] for operation in [item for item in binding if xml_local_name(item.tag) == "operation"]: action_node = next( ( child for child in operation if xml_local_name(child.tag) == "operation" and child.attrib.get("soapAction") is not None ), None, ) action = action_node.attrib.get("soapAction") if action_node is not None else None name = operation.attrib.get("name") if name and action: actions[name] = action operations.append({"name": name, **({"soap_action": action} if action else {})}) bindings.append({**public_xml_attributes(binding), "operations": operations}) messages = [] for message in [item for item in root if xml_local_name(item.tag) == "message"]: messages.append({**public_xml_attributes(message), "parts": [public_xml_attributes(child) for child in message if xml_local_name(child.tag) == "part"]}) port_types = [] for port_type in [item for item in root if xml_local_name(item.tag) == "portType"]: port_types.append( { **public_xml_attributes(port_type), "operations": [wsdl_operation_xml_details(child, actions) for child in port_type if xml_local_name(child.tag) == "operation"], } ) services = [] for service in [item for item in root if xml_local_name(item.tag) == "service"]: ports = [] for port in [item for item in service if xml_local_name(item.tag) == "port"]: address = next((child.attrib.get("location") for child in port if xml_local_name(child.tag) == "address"), None) ports.append({**public_xml_attributes(port), **({"address": address} if address else {})}) services.append({**public_xml_attributes(service), "ports": ports}) return { "name": root.attrib.get("name"), "target_namespace": root.attrib.get("targetNamespace"), "messages": messages, "port_types": port_types, "bindings": bindings, "services": services, "counts": { "messages": len(messages), "operations": sum(len(item.get("operations") or []) for item in port_types), "bindings": len(bindings), "services": len(services), "ports": sum(len(item.get("ports") or []) for item in services), }, } def xsd_xml_details(root: ET.Element) -> dict[str, Any]: imports = [public_xml_attributes(item) for item in root if xml_local_name(item.tag) in {"import", "include"}] elements = [public_xml_attributes(item) for item in root if xml_local_name(item.tag) == "element"] types: list[dict[str, Any]] = [] for type_node in [item for item in root if xml_local_name(item.tag) in {"complexType", "simpleType"}]: members = [public_xml_attributes(item) for item in type_node.iter() if item is not type_node and xml_local_name(item.tag) in {"element", "attribute"}] enumerations = [(item.attrib.get("value") or (item.text or "").strip()) for item in type_node.iter() if xml_local_name(item.tag) == "enumeration"] restriction = next((public_xml_attributes(item) for item in type_node.iter() if xml_local_name(item.tag) == "restriction"), None) types.append({"kind": xml_local_name(type_node.tag), **public_xml_attributes(type_node), "members": members, "enumerations": enumerations, **({"restriction": restriction} if restriction else {})}) return { "target_namespace": root.attrib.get("targetNamespace"), "element_form_default": root.attrib.get("elementFormDefault"), "imports": imports, "elements": elements, "types": types, "counts": {"imports": len(imports), "elements": len(elements), "types": len(types)}, } def ws_reference_sql_details(tree: Any, definition_data: bytes) -> dict[str, Any]: try: from parser.cas_payload import stream_blocks_with_data from parser.payload import decode_payload_lossless decoded = decode_payload_lossless(definition_data or b"") streams = stream_blocks_with_data(bytes(decoded.get("payload") or b""), limit=500) except Exception as exc: return {"location_url": config_tree_scalar_at_path(tree, (1, 1, 0)) or None, "status": "invalid_container", "diagnostics": {"message": str(exc)}} wsdls: list[dict[str, Any]] = [] schemas: list[dict[str, Any]] = [] xml_streams = 0 for stream in streams: text = stream.get("text") if not text or "<" not in text: continue try: root = ET.fromstring(text.lstrip("\ufeff")) except Exception: continue tag = xml_local_name(root.tag) xml_streams += 1 if tag == "definitions": wsdls.append(wsdl_xml_details(root)) elif tag == "schema": schemas.append(xsd_xml_details(root)) return { "status": "ok" if wsdls else "partial", "location_url": config_tree_scalar_at_path(tree, (1, 1, 0)) or None, "manager_type_guid": config_tree_scalar_at_path(tree, (1, 3)).lower() or None, "manager_value_guid": config_tree_scalar_at_path(tree, (1, 4)).lower() or None, "definitions": wsdls, "schemas": schemas, "counts": { "streams": len(streams), "xml_streams": xml_streams, "wsdl_definitions": len(wsdls), "schemas": len(schemas), "operations": sum(int((item.get("counts") or {}).get("operations") or 0) for item in wsdls), "services": sum(int((item.get("counts") or {}).get("services") or 0) for item in wsdls), "ports": sum(int((item.get("counts") or {}).get("ports") or 0) for item in wsdls), }, } def common_command_sql_details(base_id: str, tree: Any, identity: dict[str, Any], *, table: str, timeout_seconds: int) -> dict[str, Any]: body = config_tree_item_at_path(tree, (1, 1, 2)) group_guid = common_command_group_guid(tree) or "" group_map = public_metadata_guid_references(base_id, [group_guid], table=table, timeout_seconds=timeout_seconds) if is_guid_text(group_guid) else {} group = group_map.get(group_guid) if not isinstance(group, dict) or not group.get("ref"): standard_name = STANDARD_COMMAND_GROUP_GUIDS.get(group_guid) group = {"kind": "standard_command_group", "name": standard_name, "ref": standard_name, "status": "ok"} if standard_name else None name = str(identity.get("name") or "") ref = object_selector_ref("CommonCommand", name) if name else None return { "group": group, "command_parameter_type": public_pattern_value_type(base_id, config_tree_item_at_path(body, (8,)), table=table, timeout_seconds=timeout_seconds), "module": { "kind": "command_module", "name": "Модуль команды", "read_selector": {"method": "modules.read", "base_id": base_id, "ref": ref, "module_ordinal": 1, "state": "working"} if ref else None, }, } def settings_storage_sql_details(base_id: str, tree: Any, *, table: str, timeout_seconds: int) -> dict[str, Any]: forms = public_tree_metadata_references(base_id, config_tree_item_at_path(tree, (4,)), table=table, timeout_seconds=timeout_seconds) default_nodes = { "default_save_form": (1, 4), "default_load_form": (1, 5), "auxiliary_save_form": (1, 6), "auxiliary_load_form": (1, 7), } details: dict[str, Any] = {"forms": forms} by_guid = {str(item.get("guid") or "").lower(): item for item in forms if isinstance(item, dict)} for key, path in default_nodes.items(): guid = config_tree_scalar_at_path(tree, path).strip().lower() details[key] = by_guid.get(guid) if guid in by_guid else None return details def subsystem_sql_details( base_id: str, tree: Any, interface_tree: Any, *, table: str, timeout_seconds: int, ) -> dict[str, Any]: picture_items = public_tree_metadata_references(base_id, config_tree_item_at_path(tree, (1, 5)), table=table, timeout_seconds=timeout_seconds) interface_items = public_tree_metadata_references(base_id, interface_tree, table=table, timeout_seconds=timeout_seconds) if interface_tree else [] return { "include_help_in_contents": {"0": False, "1": True}.get(config_tree_scalar_at_path(tree, (1, 2))), "include_in_command_interface": {"0": False, "1": True}.get(config_tree_scalar_at_path(tree, (1, 4))), "use_one_command": {"0": False, "1": True}.get(config_tree_scalar_at_path(tree, (1, 6, 0))), "picture": picture_items[0] if picture_items else None, "content": public_tree_metadata_references(base_id, config_tree_item_at_path(tree, (1, 7)), table=table, timeout_seconds=timeout_seconds), "child_subsystems": public_tree_metadata_references(base_id, config_tree_item_at_path(tree, (3,)), table=table, timeout_seconds=timeout_seconds), "command_interface": { "status": "ok" if interface_tree else "not_configured", "items": interface_items, "counts": {"references": len(interface_items)}, }, } def command_group_sql_details(tree: Any, *, include_storage: bool) -> dict[str, Any]: category_raw = config_tree_scalar_at_path(tree, (1, 2)) representation_raw = config_tree_scalar_at_path(tree, (1, 3)) picture_flag = config_tree_scalar_at_path(tree, (1, 1, 1)) picture_node = config_tree_item_at_path(tree, (1, 1, 2)) picture_values = [config_tree_scalar(item) for item in (picture_node.get("items") or [])] if isinstance(picture_node, dict) else [] picture: dict[str, Any] | None = None if picture_flag == "1": picture_guid = next((value.lower() for value in picture_values if is_guid_text(value)), None) picture_code = next((value for value in picture_values if re.fullmatch(r"-?\d+", value) and value != "0"), None) if picture_guid: picture = {"kind": "metadata_or_standard_picture", "guid": picture_guid, "status": "requires_identity_resolution"} elif picture_code: picture = { "kind": "standard_picture", "code": int(picture_code), "ref": {"-13": "StdPicture.Print"}.get(picture_code), "status": "ok" if picture_code == "-13" else "unknown_code", } details = { "category": {"1": "NavigationPanel", "2": "FormNavigationPanel", "4": "ActionsPanel", "8": "FormCommandBar"}.get(category_raw, {"status": "unknown_code", "code": category_raw}), "representation": {"0": "Text", "1": "Picture", "2": "PictureAndText", "3": "Auto"}.get(representation_raw, {"status": "unknown_code", "code": representation_raw}), "tool_tip": config_tree_localized_text(config_tree_item_at_path(tree, (1, 4))), "picture": picture, "load_transparent": ({"0": False, "1": True}.get(config_tree_scalar_at_path(tree, (1, 1, 6))) if picture else None), "confidence": "high", "evidence": "live_sql_config_decoder", } if include_storage: details["storage"] = {"category_path": "1.2", "category_raw": category_raw, "representation_path": "1.3", "representation_raw": representation_raw, "picture_path": "1.1.2"} return details def iter_config_tree_nodes(tree: Any): stack = [tree] while stack: node = stack.pop() if not isinstance(node, dict): continue yield node items = node.get("items") if isinstance(items, list): stack.extend(reversed(items)) def live_base_root_metadata_index(base_id: str, *, table: str = "Config", timeout_seconds: int = 60) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: """Read top-level metadata collections from Config/root without scanning payload files.""" source_table = "Config" if table not in {"Config", "ConfigSave"} else table cache_key = (str(base_id), source_table) now = time.time() with BASE_ROOT_METADATA_CACHE_LOCK: cached = BASE_ROOT_METADATA_CACHE.get(cache_key) if cached and now - float(cached.get("cached_at") or 0) <= BASE_ROOT_METADATA_CACHE_TTL_SECONDS: return [dict(row) for row in cached.get("rows") or []], [dict(item) for item in cached.get("diagnostics") or []] diagnostics: list[dict[str, Any]] = [] pointer_data, _, pointer_error = read_storage_file_bytes(base_id, source_table, "root", timeout_seconds=timeout_seconds) if pointer_error and source_table == "ConfigSave": source_table = "Config" cache_key = (str(base_id), source_table) pointer_data, _, pointer_error = read_storage_file_bytes(base_id, source_table, "root", timeout_seconds=timeout_seconds) if pointer_error: diagnostics.append({"code": "configuration_root_unavailable", "table": source_table, "diagnostics": pointer_error.get("diagnostics")}) return [], diagnostics pointer_tree = parse_config_tree_from_bytes(pointer_data or b"") pointer_items = pointer_tree.get("items") if isinstance(pointer_tree, dict) else [] root_file = next((config_tree_scalar(item).lower() for item in (pointer_items or [])[1:] if is_guid_text(config_tree_scalar(item))), "") if not root_file: return [], [{"code": "configuration_root_pointer_invalid", "table": source_table}] root_data, _, root_error = read_storage_file_bytes(base_id, source_table, root_file, timeout_seconds=timeout_seconds) if root_error: return [], [{"code": "configuration_root_descriptor_unavailable", "table": source_table, "file_name": root_file, "diagnostics": root_error.get("diagnostics")}] root_tree = parse_config_tree_from_bytes(root_data or b"") if not isinstance(root_tree, dict): return [], [{"code": "configuration_root_descriptor_invalid", "table": source_table, "file_name": root_file}] rows_by_key: dict[tuple[str, str], dict[str, Any]] = { ("Configuration", root_file): { "guid": root_file, "kind": "Configuration", "kind_ru": RU_KIND["Configuration"], "public_kind": PUBLIC_KIND["Configuration"], "source": "base", "storage": { "table": source_table, "file_name": root_file, "discovery": "configuration_root_descriptor", "root_file": root_file, }, } } application_block_found = False for node in iter_config_tree_nodes(root_tree): items = node.get("items") if isinstance(node.get("items"), list) else [] if len(items) >= 2: collection_guid = config_tree_scalar(items[0]).lower() kind = ROOT_COLLECTION_KIND.get(collection_guid) declared_text = config_tree_scalar(items[1]) if kind and re.fullmatch(r"\d+", declared_text): object_guids = [config_tree_scalar(item).lower() for item in items[2:] if is_guid_text(config_tree_scalar(item))] if int(declared_text) == len(object_guids): for guid in object_guids: rows_by_key[(kind, guid)] = { "guid": guid, "kind": kind, "kind_ru": RU_KIND.get(kind, kind), "public_kind": PUBLIC_KIND.get(kind, "other"), "source": "base", "storage": { "table": source_table, "file_name": guid, "discovery": "configuration_root", "root_file": root_file, "collection_guid": collection_guid, }, } if application_block_found or len(items) != 18 or config_tree_scalar(items[2]) != "15": continue collections = items[3:] if len(collections) != 15 or not all(isinstance(item, dict) and isinstance(item.get("items"), list) for item in collections): continue application_block_found = True for ordinal, collection in enumerate(collections): collection_items = collection.get("items") or [] if len(collection_items) < 2: continue class_guid = config_tree_scalar(collection_items[0]).lower() kind = ROOT_APPLICATION_CLASS_KIND.get(class_guid) or ROOT_APPLICATION_COLLECTION_KIND.get(ordinal) if not kind: diagnostics.append({"code": "application_collection_kind_unknown", "ordinal": ordinal, "class_guid": class_guid}) continue declared_text = config_tree_scalar(collection_items[1]) object_guids = [config_tree_scalar(item).lower() for item in collection_items[2:] if is_guid_text(config_tree_scalar(item))] if not re.fullmatch(r"\d+", declared_text) or int(declared_text) != len(object_guids): diagnostics.append({"code": "application_collection_count_mismatch", "ordinal": ordinal, "kind": kind, "declared": declared_text, "actual": len(object_guids)}) for guid in object_guids: rows_by_key[(kind, guid)] = { "guid": guid, "kind": kind, "kind_ru": RU_KIND.get(kind, kind), "public_kind": PUBLIC_KIND.get(kind, "other"), "source": "base", "storage": { "table": source_table, "file_name": guid, "discovery": "configuration_root", "root_file": root_file, "collection_ordinal": ordinal, "collection_guid": class_guid, }, } if not application_block_found: diagnostics.append({"code": "application_collections_not_found", "table": source_table, "file_name": root_file}) rows = sorted(rows_by_key.values(), key=lambda row: (str(row.get("kind") or ""), str(row.get("guid") or ""))) with BASE_ROOT_METADATA_CACHE_LOCK: BASE_ROOT_METADATA_CACHE[cache_key] = {"cached_at": now, "rows": rows, "diagnostics": diagnostics} return [dict(row) for row in rows], diagnostics def merge_root_metadata_candidates( candidates: dict[str, dict[str, Any]], root_rows: list[dict[str, Any]], *, wanted_kind: str | None, requested_public: str | None, ) -> None: for root_row in root_rows: internal = str(root_row.get("kind") or "") if not kind_matches_request(internal, wanted_kind, requested_public): continue guid = str(root_row.get("guid") or "").lower() if not guid: continue existing = candidates.get(guid) if existing: existing_storage = existing.setdefault("storage", {}) root_storage = root_row.get("storage") if isinstance(root_row.get("storage"), dict) else {} existing_storage.setdefault("file_name", root_storage.get("file_name") or guid) existing_storage["root_discovery"] = {key: value for key, value in root_storage.items() if key not in {"table", "file_name"}} continue candidates[guid] = dict(root_row) def kind_request_needs_root_discovery(wanted_kind: str | None, requested_public: str | None) -> bool: if not wanted_kind and not requested_public: return True return any(kind_matches_request(kind, wanted_kind, requested_public) for kind in ROOT_DISCOVERY_KIND_SET) def get_kinds(base_id: str | None = None) -> dict[str, Any]: if not base_id: return base_id_required("metadata.kinds") records, error = live_dbnames_records(str(base_id)) if error: return error identities: dict[str, set[str]] = {} for record in records or []: role = getattr(record, "storage_role", "") kind = DBNAMES_ROLE_KIND.get(role) guid = str(getattr(record, "guid", "") or "").lower() if not kind or not guid: continue identities.setdefault(kind, set()).add(guid) root_rows, root_diagnostics = live_base_root_metadata_index(str(base_id), table="Config") for row in root_rows: kind = str(row.get("kind") or "") guid = str(row.get("guid") or "").lower() if kind and guid: identities.setdefault(kind, set()).add(guid) internal_counts = {kind: len(guids) for kind, guids in identities.items()} public_counts: dict[str, int] = {} for kind, count in internal_counts.items(): public = PUBLIC_KIND.get(kind, "other") public_counts[public] = public_counts.get(public, 0) + count kinds = [{"kind": key, "count": public_counts[key]} for key in sorted(public_counts)] return { "schema": "onec_metadata_kinds.v1", "status": "ok", "base_id": str(base_id), "source": {"kind": "live_metadata"}, "kinds": kinds, "internal_kind_counts": dict(sorted(internal_counts.items())), **({"diagnostics": root_diagnostics} if root_diagnostics else {}), } def metadata_capabilities(payload: dict[str, Any]) -> dict[str, Any]: base_id_or_error = require_base_id(payload, "metadata.capabilities") if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error kind_error = validate_optional_string_arguments(payload, "metadata.capabilities", ["kind"]) if kind_error: return kind_error include_missing, include_missing_error = strict_bool_argument(payload, "include_missing", method="metadata.capabilities", default=False) if include_missing_error: return include_missing_error wanted_kind, requested_public = parse_kind_request(payload.get("kind")) kinds_result = get_kinds(base_id) if kinds_result.get("status") != "ok": result = dict(kinds_result) result["method"] = "metadata.capabilities" return result internal_counts = kinds_result.get("internal_kind_counts") or {} capabilities = [] for kind in sorted(KIND_CAPABILITIES): if wanted_kind or requested_public: if not kind_matches_request(kind, wanted_kind, requested_public): continue count = int(internal_counts.get(kind) or 0) if count <= 0 and not include_missing: continue capabilities.append( { "kind": kind, "kind_ru": RU_KIND.get(kind, kind), "public_kind": PUBLIC_KIND.get(kind, "other"), "count": count, "capabilities": KIND_CAPABILITIES[kind], } ) if payload.get("kind") and not capabilities: return { "schema": "onec_metadata_capabilities.v1", "status": "not_found", "error": "not_found", "base_id": base_id, "source": {"kind": "live_metadata"}, "query": {"kind": payload.get("kind"), "include_missing": include_missing}, "capabilities": [], "counts": {"kinds": 0}, "diagnostics": {"message": "Вид метаданных не найден или не поддерживается адаптером."}, } return { "schema": "onec_metadata_capabilities.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_metadata"}, "query": {"kind": payload.get("kind"), "include_missing": include_missing}, **({"selected": capabilities[0]} if payload.get("kind") and len(capabilities) == 1 else {}), "capabilities": capabilities, "counts": {"kinds": len(capabilities)}, } def classify_unmapped_source_role(role: str) -> str: if re.search(r"(ChngR|Opt|SInf|BfK|DlK|Agg|Turnover|TurnoverCt|TurnoverDt)$", role): return "auxiliary_storage" if role.startswith(("AccumRg", "InfoRg", "AccRg", "Reference", "Document", "Task", "BPr", "CKinds", "Chrc", "Const")): return "auxiliary_storage" if role in {"Fld", "VT", "LineNo", "ByDims", "ByField", "ByParentField", "ByProperty", "ByResource", "FrmDtSettings", "DynListSettings", "RepSettings", "RepVarSettings", "BPrPoints"}: return "metadata_parts" if role.startswith(("DataHistory", "DbCopies", "DbSegments", "Extensions", "IntegService", "IntegChannel", "Users")): return "platform_system_storage" if role in { "CommonSettings", "ConfigChngR", "Consts", "DataSeparationUse", "DefaultInternalSettings", "DefaultSystemSettings", "Descr", "EDBT", "ErrorProcessingSettings", "ExtsChngR", "InternalSettings", "ODataSettings", "SystemSettings", }: return "platform_system_storage" # Live DBNames verification shows these as singleton platform tables # (normally with the zero GUID), not configuration metadata collections. # ExtDataSrcPrms is storage attached to an ExternalDataSource or its global # fallback row, so it is auxiliary rather than a standalone metadata kind. if role == "ExtDataSrcPrms": return "auxiliary_storage" if role.startswith(("STT",)) or role in { "Acoustic", "Bots", "Ecs", "LangModel", "MobileClientDataExchange", "URLExternalData", "WebSocketClients", }: return "platform_system_storage" return "unclassified" def metadata_adapter_audit(payload: dict[str, Any]) -> dict[str, Any]: base_id_or_error = require_base_id(payload, "metadata.adapter.audit") if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error if "include_details" in payload: return invalid_argument( "metadata.adapter.audit", "include_details", "metadata.adapter.audit does not support include_details; use include_unmapped=true for additional audit sections.", ) include_missing, include_missing_error = strict_bool_argument(payload, "include_missing", method="metadata.adapter.audit", default=False) if include_missing_error: return include_missing_error include_unmapped, include_unmapped_error = strict_bool_argument(payload, "include_unmapped", method="metadata.adapter.audit", default=False) if include_unmapped_error: return include_unmapped_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.adapter.audit", default=60, minimum=1) if timeout_error: return timeout_error records, error = live_dbnames_records(base_id, timeout_seconds=int(timeout_seconds or 60)) if error: result = dict(error) result["method"] = "metadata.adapter.audit" return result recognized: dict[str, set[str]] = {} recognized_source_roles: dict[str, set[str]] = {} source_role_counts: dict[str, int] = {} unknown_role_counts: dict[str, int] = {} unknown_role_categories: dict[str, int] = {} for record in records or []: role = str(getattr(record, "storage_role", "") or "") if not role: continue source_role_counts[role] = source_role_counts.get(role, 0) + 1 kind = DBNAMES_ROLE_KIND.get(role) guid = str(getattr(record, "guid", "") or "").lower() if kind and guid: recognized.setdefault(kind, set()).add(guid) recognized_source_roles.setdefault(kind, set()).add(role) elif role: unknown_role_counts[role] = unknown_role_counts.get(role, 0) + 1 category = classify_unmapped_source_role(role) unknown_role_categories[category] = unknown_role_categories.get(category, 0) + 1 root_rows, root_diagnostics = live_base_root_metadata_index(base_id, table="Config", timeout_seconds=int(timeout_seconds or 60)) for row in root_rows: kind = str(row.get("kind") or "") guid = str(row.get("guid") or "").lower() if not kind or not guid: continue recognized.setdefault(kind, set()).add(guid) storage = row.get("storage") if isinstance(row.get("storage"), dict) else {} source_role = "ConfigRoot" if storage.get("collection_guid"): source_role = f"ConfigRoot:{storage['collection_guid']}" elif storage.get("collection_ordinal") is not None: source_role = f"ConfigRoot:application:{storage['collection_ordinal']}" recognized_source_roles.setdefault(kind, set()).add(source_role) kind_support = [] recognized_kind_counts: dict[str, int] = {} public_kind_counts: dict[str, int] = {} missing_supported_kinds: list[dict[str, Any]] = [] for kind in sorted(TOP_LEVEL_METADATA_KINDS): count = len(recognized.get(kind, set())) recognized_kind_counts[kind] = count public_kind = PUBLIC_KIND.get(kind, "other") public_kind_counts[public_kind] = public_kind_counts.get(public_kind, 0) + count if count <= 0: missing_supported_kinds.append( { "kind": kind, "kind_ru": RU_KIND.get(kind, kind), "public_kind": public_kind, "presence_status": "supported_absent_in_selected_base", "capabilities": KIND_CAPABILITIES[kind], } ) if count <= 0 and not include_missing: continue kind_support.append( { "kind": kind, "kind_ru": RU_KIND.get(kind, kind), "public_kind": public_kind, "count": count, "source_roles": sorted(recognized_source_roles.get(kind, set())), "capabilities": KIND_CAPABILITIES[kind], } ) result = { "schema": "onec_adapter_audit.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_metadata"}, "metadata_kinds": kind_support, "recognized_kind_counts": {key: value for key, value in sorted(recognized_kind_counts.items()) if value > 0}, "public_kind_counts": {key: value for key, value in sorted(public_kind_counts.items()) if value > 0}, "child_objects": { "forms": { "status": "supported", "properties": ["name", "synonym", "elements", "element_types_xml_confirmed", "attributes", "commands", "tables", "command_bars", "events", "handler_links", "module_summary"], "methods": ["metadata.object.forms", "metadata.object.form.details", "metadata.form.decode"], }, "templates": { "status": "supported", "properties": ["name", "synonym", "format", "tabular_document", "html", "bsl", "safe_preview", "bounded_content_export"], "methods": ["metadata.object.templates", "metadata.object.template.details", "templates.read"], }, "commands": { "status": "supported", "properties": ["name", "synonym", "role", "object_commands", "form_commands"], "methods": ["metadata.object.commands", "metadata.object.form.details"], }, }, "code_carriers": CODE_CARRIER_MATRIX, "write_capabilities": METADATA_WRITE_CAPABILITIES, "special_objects": { "Configuration": ["vendor", "version", "information", "addresses"], "CommandGroup": ["category", "representation", "tool_tip", "picture"], "Constant": ["value_type"], "DocumentJournal": ["document_types", "column_names", "column_synonyms", "column_types_explicit"], "DocumentNumerator": ["number_type", "number_length", "number_allowed_length", "number_periodicity", "check_unique"], "IntegrationService": ["channels", "external_integration_service_address"], "ScheduledJob": [ "method", "use", "predefined", "restart_count_on_failure", "restart_interval_on_failure", "begin_date", "end_date", "begin_time", "end_time", "completion_time", "completion_interval", "repeat_period_in_day", "repeat_pause", "week_days", "week_day_in_month", "day_in_month", "months", "weeks_period", "days_repeat_period", ], }, "not_yet_decoded": [ "Редкие свойства оформления и поведения элементов управляемых форм, для которых ещё не подтверждены стабильные SQL-позиции; все типы элементов из эталонного UPO Form.xml уже распознаются", "Платформенный визуальный рендер макетов (пиксели/PDF); безопасный ограниченный экспорт исходного содержимого уже поддержан templates.read include_content=true", ], "optional_deep_reads": [ { "kind": "DocumentJournal", "property": "column_types", "flag": "include_column_types=true", "execution": "adapter.job.start", "reason": "Типы разрешаются по реквизитам всех документов журнала и могут требовать длительного чтения метаданных.", } ], "counts": { "metadata_kinds": len(kind_support), "recognized_kinds": len([count for count in recognized_kind_counts.values() if count > 0]), "supported_kinds": len(TOP_LEVEL_METADATA_KINDS), "missing_supported_kinds": len([count for count in recognized_kind_counts.values() if count <= 0]), }, **({"diagnostics": root_diagnostics} if root_diagnostics else {}), } if include_missing: result["missing_supported_kinds"] = missing_supported_kinds if include_unmapped: result["metadata_candidates"] = [ {"source_role": role, "category": classify_unmapped_source_role(role), "records": unknown_role_counts[role]} for role in sorted(unknown_role_counts) if classify_unmapped_source_role(role) in {"platform_feature_candidate", "unclassified"} ] result["unmapped_source_roles"] = [ { "source_role": role, "category": classify_unmapped_source_role(role), "records": unknown_role_counts[role], } for role in sorted(unknown_role_counts) ] result["counts"].update( { "source_records": len(records or []), "known_source_roles": len([role for role in source_role_counts if role in DBNAMES_ROLE_KIND]), "unmapped_source_roles": len(unknown_role_counts), "unmapped_categories": dict(sorted(unknown_role_categories.items())), } ) return result def metadata_write_capabilities(payload: dict[str, Any]) -> dict[str, Any]: base_id = payload.get("base_id") if isinstance(payload.get("base_id"), str) else None return { "schema": "onec_metadata_write_capabilities.v1", "status": "ok", "base_id": base_id, "default_write_layer": "save", "agent_rule": "Agent-facing writes must target the saved-state layer; active configuration writes are not exposed.", "code_carriers": CODE_CARRIER_MATRIX, "write_capabilities": METADATA_WRITE_CAPABILITIES, "safe_methods": ["code.write", "metadata.write", "metadata.write.plan"], "technical_apply_method": "storage.saved_state.apply_proposal", "unsupported_summary": [ key for key, value in METADATA_WRITE_CAPABILITIES.items() if str(value.get("status") or "").startswith(("not_supported", "read_only")) ], } def list_objects( kind: str | None, *, base_id: str | None = None, limit: Any = 200, offset: Any = 0, include_storage: bool = False, include_missing: bool = False, only_missing: bool = False, exact_counts: bool = False, refresh_cache: bool = False, table: str = "Config", name_filter: str | None = None, ) -> dict[str, Any]: if not base_id: return base_id_required("metadata.objects.list") resolved_base_id = str(base_id) parsed_limit, limit_error = parse_int_argument({"limit": limit}, "limit", method="metadata.objects.list", default=200, minimum=1) if limit_error: return limit_error parsed_offset, offset_error = parse_int_argument({"offset": offset}, "offset", method="metadata.objects.list", default=0, minimum=0) if offset_error: return offset_error limit = int(parsed_limit or 200) offset = int(parsed_offset or 0) storage_table = str(table or "Config") if storage_table not in STORAGE_TABLES: return invalid_argument("metadata.objects.list", "table", "Unsupported storage table.", allowed_values=sorted(STORAGE_TABLES)) normalized_name_filter = normalize(name_filter or "") wanted, requested_public = parse_kind_request(kind) if not include_storage and not include_missing and not only_missing and not exact_counts and not refresh_cache and not normalized_name_filter: cached = metadata_cache_list_rows(resolved_base_id, kind, limit=limit, offset=offset) if cached: rows, cached_total = cached root_cache_incomplete = False if wanted and kind_request_needs_root_discovery(wanted, requested_public): cached_root_rows, _cached_root_diagnostics = live_base_root_metadata_index(resolved_base_id, table=storage_table) root_candidate_count = sum( 1 for row in cached_root_rows if kind_matches_request(str(row.get("kind") or ""), wanted, requested_public) ) root_cache_incomplete = root_candidate_count > cached_total if not root_cache_incomplete: public_page = [metadata_cache_public_row(row) for row in rows] return { "schema": "onec_metadata_objects.v1", "status": "ok", "base_id": resolved_base_id, "source": {"kind": "metadata_cache"}, "query": { "kind": kind, "limit": limit, "offset": offset, "include_storage": include_storage, "include_missing": include_missing, "only_missing": only_missing, "exact_counts": exact_counts, "refresh_cache": refresh_cache, "table": storage_table, }, "objects": public_page, "counts": { "returned": len(public_page), "page": len(public_page), "total": cached_total, "scanned": 0, "counts_exact": False, "visible_counts_exact": True, "total_visible": cached_total, "missing": None, "hidden_missing": None, "page_missing": 0, "visible_missing": 0, }, "cache": {"status": "hit", "role": "metadata_identity_cache"}, "diagnostics": [ { "message": "Обычный список получен из локального кеша метаданных. Для проверки отсутствующих объектов передайте include_missing=true, only_missing=true или exact_counts=true.", } ], } records, error = live_dbnames_records(resolved_base_id) if error: return error candidates: dict[str, dict[str, Any]] = {} for record in records or []: role = getattr(record, "storage_role", "") internal = DBNAMES_ROLE_KIND.get(role) if not internal: continue public = PUBLIC_KIND.get(internal, "other") if not kind_matches_request(internal, wanted, requested_public): continue guid = str(getattr(record, "guid", "") or "").lower() if not guid: continue row = candidates.setdefault( guid, { "guid": guid, "kind": internal, "kind_ru": RU_KIND.get(internal, internal), "public_kind": public, "name": None, "synonym": None, "source": "extension" if dbnames_record_storage_table(record, storage_table) == "ConfigCAS" else "base", "storage": {"table": dbnames_record_storage_table(record, storage_table), "dbnames": []}, }, ) record_table = dbnames_record_storage_table(record, storage_table) if record_table == "ConfigCAS": row["source"] = "extension" row.setdefault("storage", {})["table"] = "ConfigCAS" row["storage"]["dbnames"].append( { "source_file": getattr(record, "source", None), "storage_role": role, "table": record_table, "sql_number": getattr(record, "sql_number", None), "index": getattr(record, "index", None), } ) root_diagnostics: list[dict[str, Any]] = [] if storage_table in {"Config", "ConfigSave"} and kind_request_needs_root_discovery(wanted, requested_public): root_rows, root_diagnostics = live_base_root_metadata_index(resolved_base_id, table=storage_table) merge_root_metadata_candidates(candidates, root_rows, wanted_kind=wanted, requested_public=requested_public) candidate_rows = list(candidates.values()) candidate_rows.sort(key=lambda row: (row["kind"] or "", row["guid"])) diagnostics = list(root_diagnostics) page: list[dict[str, Any]] = [] visible_rows: list[dict[str, Any]] = [] visible_seen = 0 hidden_missing = 0 missing = 0 total_visible = 0 scanned_candidates = 0 scanned_all = True chunk_size = 80 if exact_counts or only_missing else min(80, max(10, offset + limit)) for start in range(0, len(candidate_rows), chunk_size): chunk = [dict(row) for row in candidate_rows[start : start + chunk_size]] payloads: dict[str, bytes] = {} for chunk_table in sorted({preferred_object_storage_table(row, storage_table) for row in chunk}): table_rows = [row for row in chunk if preferred_object_storage_table(row, storage_table) == chunk_table] table_payloads, _, payload_error = read_storage_files_bytes(resolved_base_id, chunk_table, [row["guid"] for row in table_rows]) if payload_error: diagnostics.append({"message": (payload_error.get("diagnostics") or {}).get("message"), "table": chunk_table}) continue payloads.update(table_payloads or {}) for row in chunk: if not payloads or row["guid"] not in payloads: row["status"] = "source_missing" row["diagnostics"] = metadata_payload_missing_diagnostics() else: identity = config_identity_from_bytes(payloads[row["guid"]]) if identity: row["name"] = identity.get("name") synonyms = identity.get("synonyms") or {} row["synonym"] = next(iter(synonyms.values()), None) if isinstance(synonyms, dict) else None row["identity"] = identity row["status"] = "ok" else: row["status"] = "partial" row["diagnostics"] = {"message": "Не удалось прочитать имя объекта из описания метаданных."} scanned_candidates += 1 is_missing = not bool(row.get("name")) if normalized_name_filter and normalized_name_filter not in normalize(row.get("name") or "") and normalized_name_filter not in normalize(row.get("synonym") or "") and normalized_name_filter not in normalize(row.get("guid") or ""): continue if is_missing: missing += 1 if only_missing: visible = is_missing else: visible = include_missing or not is_missing if is_missing and not include_missing and not only_missing: hidden_missing += 1 if not visible: continue total_visible += 1 if exact_counts: visible_rows.append(row) elif visible_seen >= offset and len(page) < limit: page.append(row) visible_seen += 1 if not exact_counts and len(page) >= limit: scanned_all = start + len(chunk) >= len(candidate_rows) break if exact_counts: visible_rows.sort(key=lambda row: (row["kind"] or "", normalize(row.get("name") or row["guid"]))) page = visible_rows[offset : offset + limit] page.sort(key=lambda row: (row["kind"] or "", normalize(row.get("name") or row["guid"]))) public_page = [public_metadata_row(row, include_storage=include_storage) for row in page] counts_are_exact = bool(exact_counts or scanned_all) page_missing = sum(1 for row in page if not row.get("name")) if not counts_are_exact: diagnostics.append( { "message": "Счетчики total_visible, missing и hidden_missing не вычислялись полностью в быстром режиме. Передайте exact_counts=true, если нужны точные счетчики по всему виду.", } ) return { "schema": "onec_metadata_objects.v1", "status": "ok", "base_id": resolved_base_id, "source": {"kind": "live_metadata"}, "query": { "kind": kind, "name_filter": name_filter, "limit": limit, "offset": offset, "include_storage": include_storage, "include_missing": include_missing, "only_missing": only_missing, "exact_counts": exact_counts, "table": storage_table, }, "objects": public_page, "counts": { "returned": len(page), "page": len(page), "total": len(candidate_rows), "scanned": scanned_candidates, "counts_exact": counts_are_exact, "visible_counts_exact": counts_are_exact, "total_visible": total_visible if counts_are_exact else None, "missing": missing if counts_are_exact else None, "hidden_missing": hidden_missing if counts_are_exact else None, "page_missing": page_missing, "visible_missing": page_missing, }, "diagnostics": diagnostics, } def metadata_objects_list_extension_not_supported(payload: dict[str, Any]) -> dict[str, Any]: return { "schema": "onec_adapter_request_error.v1", "method": "metadata.objects.list", "status": "invalid_argument", "error": "invalid_argument", "argument": "extension", "base_id": payload.get("base_id"), "diagnostics": { "message": "metadata.objects.list не фильтрует объекты по расширению. Для объектов расширения используйте extension.objects.find или metadata.definition.find с extension.", "next_method": "extension.objects.find", "next_payload": { "base_id": payload.get("base_id"), "extension": payload.get("extension"), **({"kind": payload.get("kind")} if payload.get("kind") else {}), **({"query": payload.get("name_filter") or payload.get("name_contains") or payload.get("name")} if (payload.get("name_filter") or payload.get("name_contains") or payload.get("name")) else {}), **({"limit": payload.get("limit")} if payload.get("limit") else {}), }, }, } def get_object( kind: str | None, name: str, *, base_id: str | None = None, view: str = "effective", limit: int = 20, include_storage: bool = False, ordinal: Any = None, include_semantic: bool = True, timeout_seconds: int = 60, table: str = "Config", file_name: str | None = None, extension_guid: str | None = None, resolve_semantic_types: bool = True, semantic_include_generic: bool = True, semantic_categories: set[str] | list[str] | tuple[str, ...] | None = None, semantic_lightweight: bool = False, ) -> dict[str, Any]: if not base_id: return base_id_required("metadata.object.get") resolved_base_id = str(base_id) view_value, view_error = parse_view_argument({"view": view}, "metadata.object.get") if view_error: return view_error view = str(view_value or "effective") storage_table = str(table or "Config") if storage_table not in STORAGE_TABLES: return invalid_argument("metadata.object.get", "table", "Unsupported storage table.", allowed_values=sorted(STORAGE_TABLES)) wanted_kind, wanted_name = parse_object_query(kind, name) direct_file_name = str(file_name or "").strip() public_extension_guid = str(extension_guid or "").strip().lower() if ( not direct_file_name and storage_table == "ConfigCASSave" and is_guid_text(public_extension_guid) and wanted_name and not is_guid_text(wanted_name) ): saved_matches = extension_objects_find( { "base_id": resolved_base_id, "extension": public_extension_guid, "state": "save", "kind": wanted_kind, "query": wanted_name, "limit": max(20, int(limit or 20)), "include_storage": True, "timeout_seconds": timeout_seconds, } ) exact_saved = [ item for item in saved_matches.get("objects") or [] if isinstance(item, dict) and (not wanted_kind or canonical_kind(str(item.get("kind") or "")) == wanted_kind) and normalize_exact(item.get("name") or "") == normalize_exact(wanted_name) ] if len(exact_saved) == 1: saved_item = exact_saved[0] saved_route = saved_item.get("route") if isinstance(saved_item.get("route"), dict) else {} saved_file_name = str(saved_route.get("file_name") or saved_route.get("descriptor_file_name") or "").strip() if saved_file_name: return get_object( saved_item.get("kind") or wanted_kind, str(saved_item.get("guid") or wanted_name), base_id=resolved_base_id, view=view, limit=limit, include_storage=include_storage, include_semantic=include_semantic, timeout_seconds=timeout_seconds, table=storage_table, file_name=saved_file_name, extension_guid=public_extension_guid, resolve_semantic_types=resolve_semantic_types, semantic_include_generic=semantic_include_generic, semantic_categories=semantic_categories, semantic_lightweight=semantic_lightweight, ) if not direct_file_name and storage_table == "ConfigCASSave" and public_extension_guid and is_guid_text(wanted_name): direct_file_name = f"{public_extension_guid}__{wanted_name.lower()}" if direct_file_name: if Path(direct_file_name).name != direct_file_name: return invalid_argument("metadata.object.get", "file_name", "file_name must be a safe storage file name.") data, _config, read_error = read_storage_file_bytes( resolved_base_id, storage_table, direct_file_name, timeout_seconds=timeout_seconds, ) if read_error or data is None: result = dict( read_error or { "schema": "onec_adapter_source_error.v1", "status": "source_missing", "base_id": resolved_base_id, "source": {"kind": "live_sql", "table": storage_table}, "diagnostics": {"message": "The requested storage file was not found."}, } ) result["method"] = "metadata.object.get" return result identity = config_identity_from_bytes(data) or saved_state_descriptor_identity_from_bytes(data, direct_file_name) or {} try: from parser.cas_payload import classify_payload detected_kind = extension_metadata_payload_kind(data, identity, classify_payload(data, include_text=False)) except Exception: detected_kind = None object_kind = wanted_kind or detected_kind guid = str(identity.get("guid") or direct_file_name.split("__", 1)[-1]).strip().lower() object_row = { "guid": guid, "kind": object_kind, "kind_ru": RU_KIND.get(str(object_kind or ""), object_kind), "public_kind": PUBLIC_KIND.get(str(object_kind or ""), "other"), "name": identity.get("name") or wanted_name, "synonym": identity.get("synonym") or next(iter((identity.get("synonyms") or {}).values()), None), "source": "extension_saved_state" if storage_table == "ConfigCASSave" else "live_sql", "storage": {"table": storage_table, "file_name": direct_file_name}, "match_by": "direct_file_name", "score": 1.0, } semantic = None if include_semantic: records, records_error = live_dbnames_records(resolved_base_id, timeout_seconds=timeout_seconds) if records_error: records = [] decoded = decode_config_object_full( data, kind=str(object_kind or ""), dbnames_records=records or [], include_text=False, include_tree=False, max_depth=3, semantic_include_generic=semantic_include_generic, semantic_categories=semantic_categories, semantic_lightweight=semantic_lightweight, ) if decoded.get("status") == "ok": semantic = public_semantic_profile(decoded.get("semantic"), include_storage=include_storage, resolved_types={}) public_object = public_metadata_row(object_row, include_storage=include_storage) return { "schema": "onec_metadata_object.v1", "status": "ok", "base_id": resolved_base_id, "source": {"kind": "live_sql", "table": storage_table}, "view": view, "query": { "kind": wanted_kind, "name": wanted_name, "raw_kind": kind, "raw_name": name, "file_name": direct_file_name if include_storage else None, "include_storage": include_storage, }, "object": public_object, **({"semantic": semantic} if semantic else {}), "extension_overlays": [], "matches": [public_object], "counts": {"matches": 1, "extension_overlays": 0, "candidates": 1, "scanned": 1}, "diagnostics": {"note": "Object descriptor was read directly from the selected live SQL storage table."}, } ordinal_value, ordinal_error = parse_ordinal(ordinal, "metadata.object.get") if ordinal_error: return ordinal_error if ordinal_value is not None: if not wanted_kind: return { "schema": "onec_adapter_request_error.v1", "method": "metadata.object.get", "status": "error", "error": "kind_required", "diagnostics": {"message": "kind is required when selecting an object by ordinal."}, } ordinal_result = list_objects( wanted_kind, base_id=resolved_base_id, limit=1, offset=ordinal_value - 1, include_storage=False, table=storage_table, ) if ordinal_result.get("status") != "ok" or not ordinal_result.get("objects"): result = dict(ordinal_result) result["method"] = "metadata.object.get" result["status"] = "not_found" result["error"] = "not_found" result["diagnostics"] = {"message": f"Object ordinal {ordinal_value} was not found for kind {wanted_kind}."} return public_error_result(result, include_storage=include_storage, method="metadata.object.get") selected = dict((ordinal_result.get("objects") or [])[0]) return get_object( selected.get("kind") or wanted_kind, str(selected.get("guid") or ""), base_id=resolved_base_id, view=view, limit=limit, include_storage=include_storage, table=storage_table, include_semantic=include_semantic, timeout_seconds=timeout_seconds, resolve_semantic_types=resolve_semantic_types, semantic_include_generic=semantic_include_generic, semantic_categories=semantic_categories, semantic_lightweight=semantic_lightweight, ) cache_hit = metadata_cache_lookup_row(resolved_base_id, wanted_kind, wanted_name) if wanted_name and not is_guid_text(wanted_name) else None if wanted_kind in {"DataProcessor", "Report"}: cache_hit = None if cache_hit: direct = metadata_cache_public_row(cache_hit) semantic = None if include_semantic: records, error = live_dbnames_records(resolved_base_id, timeout_seconds=timeout_seconds) if error: result = dict(error) result["method"] = "metadata.object.get" return result data, _, read_error = read_storage_file_bytes(resolved_base_id, storage_table, str(direct["guid"]), timeout_seconds=timeout_seconds) if read_error: result = dict(read_error) result["method"] = "metadata.object.get" return result decoded = decode_config_object_full( data or b"", kind=str(direct.get("kind") or wanted_kind or ""), dbnames_records=records, include_text=False, include_tree=False, max_depth=3, semantic_include_generic=semantic_include_generic, semantic_categories=semantic_categories, semantic_lightweight=semantic_lightweight, ) if decoded.get("status") == "ok": semantic_raw = decoded.get("semantic") resolved_types = ( resolve_type_guids( resolved_base_id, collect_reference_type_guids_from_sections((semantic_raw or {}).get("sections") or []), timeout_seconds=timeout_seconds, table=storage_table, ) if resolve_semantic_types else {} ) semantic = public_semantic_profile(semantic_raw, include_storage=include_storage, resolved_types=resolved_types) return { "schema": "onec_metadata_object.v1", "status": "ok", "base_id": resolved_base_id, "source": {"kind": "live_metadata"}, "view": view, "query": {"kind": wanted_kind, "name": wanted_name, "raw_kind": kind, "raw_name": name, "include_storage": include_storage}, "object": direct, **({"semantic": semantic} if semantic else {}), "extension_overlays": [], "matches": [direct], "counts": {"matches": 1, "extension_overlays": 0, "cache_hit": 1, "scanned": 0}, "diagnostics": {"note": "Object identity was resolved from the local metadata cache."}, } records, error = live_dbnames_records(resolved_base_id, timeout_seconds=timeout_seconds) if error: result = dict(error) result["method"] = "metadata.object.get" return result _, requested_public = parse_kind_request(kind) candidates: dict[str, dict[str, Any]] = {} for record in records or []: role = getattr(record, "storage_role", "") internal = DBNAMES_ROLE_KIND.get(role) if not internal: continue public = PUBLIC_KIND.get(internal, "other") if not kind_matches_request(internal, wanted_kind, requested_public): continue guid = str(getattr(record, "guid", "") or "").lower() if not guid: continue row = candidates.setdefault( guid, { "guid": guid, "kind": internal, "kind_ru": RU_KIND.get(internal, internal), "public_kind": public, "name": None, "synonym": None, "source": "extension" if dbnames_record_storage_table(record, storage_table) == "ConfigCAS" else "base", "storage": {"table": dbnames_record_storage_table(record, storage_table), "dbnames": []}, }, ) record_table = dbnames_record_storage_table(record, storage_table) if record_table == "ConfigCAS": row["source"] = "extension" row.setdefault("storage", {})["table"] = "ConfigCAS" row["storage"]["dbnames"].append( { "source_file": getattr(record, "source", None), "storage_role": role, "table": record_table, "sql_number": getattr(record, "sql_number", None), "index": getattr(record, "index", None), } ) if storage_table in {"Config", "ConfigSave"} and kind_request_needs_root_discovery(wanted_kind, requested_public): root_rows, _root_diagnostics = live_base_root_metadata_index(resolved_base_id, table=storage_table, timeout_seconds=timeout_seconds) merge_root_metadata_candidates(candidates, root_rows, wanted_kind=wanted_kind, requested_public=requested_public) matches = [] candidate_rows = list(candidates.values()) candidate_rows.sort(key=lambda row: (row["kind"] or "", row["guid"])) if is_guid_text(wanted_name): direct = candidates.get(wanted_name.lower()) if direct: direct_table = preferred_object_storage_table(direct, storage_table) data, _, read_error = read_storage_file_bytes(resolved_base_id, direct_table, str(direct["guid"]), timeout_seconds=timeout_seconds) if read_error: result = dict(read_error) result["method"] = "metadata.object.get" return result identity = config_identity_from_bytes(data or b"") if identity: direct["name"] = identity.get("name") synonyms = identity.get("synonyms") or {} direct["synonym"] = next(iter(synonyms.values()), None) if isinstance(synonyms, dict) else None direct["identity"] = identity direct["score"] = 1.0 direct["match_by"] = "guid" if include_semantic: decoded = decode_config_object_full( data or b"", kind=str(direct.get("kind") or wanted_kind or ""), dbnames_records=records, include_text=False, include_tree=False, max_depth=3, semantic_include_generic=semantic_include_generic, semantic_categories=semantic_categories, semantic_lightweight=semantic_lightweight, ) if decoded.get("status") == "ok": semantic_raw = decoded.get("semantic") resolved_types = ( resolve_type_guids( resolved_base_id, collect_reference_type_guids_from_sections((semantic_raw or {}).get("sections") or []), timeout_seconds=timeout_seconds, table=direct_table, ) if resolve_semantic_types else {} ) semantic = public_semantic_profile(semantic_raw, include_storage=include_storage, resolved_types=resolved_types) else: semantic = None else: semantic = None return { "schema": "onec_metadata_object.v1", "status": "ok", "base_id": resolved_base_id, "source": {"kind": "live_metadata"}, "view": view, "query": {"kind": wanted_kind, "name": wanted_name, "raw_kind": kind, "raw_name": name, "include_storage": include_storage}, "object": public_metadata_row(direct, include_storage=include_storage), **({"semantic": semantic} if semantic else {}), "extension_overlays": [], "matches": [public_metadata_row(direct, include_storage=include_storage)], "counts": {"matches": 1, "extension_overlays": 0, "candidates": len(candidate_rows), "scanned": 1}, "diagnostics": { "note": "High-level metadata response. Physical storage routes are hidden unless include_storage=true.", }, } scanned = 0 for start in range(0, len(candidate_rows), 80): chunk = candidate_rows[start : start + 80] payloads: dict[str, bytes] = {} for chunk_table in sorted({preferred_object_storage_table(row, storage_table) for row in chunk}): table_rows = [row for row in chunk if preferred_object_storage_table(row, storage_table) == chunk_table] table_payloads, _, payload_error = read_storage_files_bytes( resolved_base_id, chunk_table, [row["guid"] for row in table_rows], timeout_seconds=timeout_seconds, ) if payload_error: result = dict(payload_error) result["method"] = "metadata.object.get" return result payloads.update(table_payloads or {}) for row in chunk: scanned += 1 identity = config_identity_from_bytes(payloads[row["guid"]]) if payloads and row["guid"] in payloads else None if identity: row["name"] = identity.get("name") synonyms = identity.get("synonyms") or {} row["synonym"] = next(iter(synonyms.values()), None) if isinstance(synonyms, dict) else None row["identity"] = identity match = match_top( { "xml_kind": row.get("kind"), "name": row.get("name"), "synonym": row.get("synonym"), "relative_path": row.get("guid"), }, kind=wanted_kind, wanted=wanted_name, ) if not match: continue score, match_by = match item = dict(row) item["score"] = score item["match_by"] = match_by matches.append(item) if any(float(row.get("score") or 0) >= 0.95 for row in matches): break matches.sort(key=lambda row: (-float(row["score"]), normalize(row.get("name") or ""), row.get("guid") or "")) canonical = next((row for row in matches if row["score"] >= 0.95), None) or (matches[0] if matches else None) if not canonical and wanted_kind in {"DataProcessor", "Report"} and wanted_name and not is_guid_text(wanted_name): extension_matches, _ = metadata_extension_definition_matches( base_id=resolved_base_id, query=wanted_name, max_files=5000, max_matches=max(limit, 20), timeout_seconds=timeout_seconds, include_storage=True, use_cache=True, ) for match in extension_matches: kind_ru = str(match.get("kind") or "") if kind_ru not in {RU_KIND.get(wanted_kind), wanted_kind, "Определение расширения"} and wanted_kind: continue identity_name = str(match.get("name") or "") if normalize(identity_name) != normalize(wanted_name): continue row = { "guid": str(match.get("guid") or "").lower(), "kind": wanted_kind, "kind_ru": RU_KIND.get(wanted_kind, wanted_kind), "public_kind": PUBLIC_KIND.get(wanted_kind, "other"), "name": identity_name, "synonym": match.get("synonym"), "source": "extension", "storage": { "table": "ConfigCAS", "file_name": ( ((match.get("source") or {}).get("file_name") if isinstance(match.get("source"), dict) else None) or match.get("source_file") ), }, "score": 1.0, "match_by": "extension_definition", } if row["guid"]: matches.append(row) canonical = row break semantic = None if canonical: config, _ = sql_config_for_base(resolved_base_id) if config: metadata_cache_upsert(config, canonical) if include_semantic: canonical_table = preferred_object_storage_table(canonical, storage_table) data, _, read_error = read_storage_file_bytes(resolved_base_id, canonical_table, str(canonical["guid"]), timeout_seconds=timeout_seconds) if read_error: result = dict(read_error) result["method"] = "metadata.object.get" return result decoded = decode_config_object_full( data or b"", kind=str(canonical.get("kind") or wanted_kind or ""), dbnames_records=records, include_text=False, include_tree=False, max_depth=3, semantic_include_generic=semantic_include_generic, semantic_categories=semantic_categories, semantic_lightweight=semantic_lightweight, ) if decoded.get("status") == "ok": semantic_raw = decoded.get("semantic") resolved_types = ( resolve_type_guids( resolved_base_id, collect_reference_type_guids_from_sections((semantic_raw or {}).get("sections") or []), timeout_seconds=timeout_seconds, table=canonical_table, ) if resolve_semantic_types else {} ) semantic = public_semantic_profile(semantic_raw, include_storage=include_storage, resolved_types=resolved_types) else: semantic = None return { "schema": "onec_metadata_object.v1", "status": "ok" if canonical else "not_found", **({"method": "metadata.object.get", "error": "not_found"} if not canonical else {}), "base_id": resolved_base_id, "source": {"kind": "live_metadata"}, "view": view, "query": {"kind": wanted_kind, "name": wanted_name, "raw_kind": kind, "raw_name": name, "include_storage": include_storage}, "object": public_metadata_row(canonical, include_storage=include_storage) if canonical else None, **({"semantic": semantic} if semantic else {}), "extension_overlays": [], "matches": [public_metadata_row(row, include_storage=include_storage) for row in matches[:limit]], "counts": {"matches": len(matches), "extension_overlays": 0, "candidates": len(candidate_rows), "scanned": scanned}, "diagnostics": { "note": "High-level metadata response. Physical storage routes are hidden unless include_storage=true.", **({"message": "Объект метаданных не найден по заданному виду и имени."} if not canonical else {}), }, } def metadata_snapshot(payload: dict[str, Any]) -> dict[str, Any]: base_id_or_error = require_base_id(payload, "metadata.snapshot") if isinstance(base_id_or_error, dict): return base_id_or_error include_modules, include_modules_error = strict_bool_argument(payload, "include_modules", method="metadata.snapshot", default=False) if include_modules_error: return include_modules_error if "limit" in payload: return invalid_argument("metadata.snapshot", "limit", "metadata.snapshot does not support limit; use metadata.objects.list for paged object lists.") base_id = base_id_or_error kinds = get_kinds(base_id) if kinds.get("status") != "ok": result = dict(kinds) result["method"] = "metadata.snapshot" return result return { "schema": "onec_metadata_snapshot.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_metadata"}, "include_modules": bool(include_modules), "kinds": kinds.get("kinds"), } def access_item_id(item: Any) -> str: if isinstance(item, dict): for key in ("id", "ref", "uuid", "guid", "name", "code"): value = item.get(key) if value not in {None, ""}: return str(value) return json.dumps(item, ensure_ascii=False, sort_keys=True) return str(item) def access_item_name(item: dict[str, Any], fallback: str) -> str: for key in ("name", "presentation", "synonym", "title", "full_name"): value = item.get(key) if value not in {None, ""}: return str(value) return fallback def access_name_is_placeholder(name: Any, item_id: Any) -> bool: name_text = str(name or "").strip() item_text = str(item_id or "").strip() if not name_text: return True return name_text.casefold() == item_text.casefold() or access_ref_tail(name_text).casefold() == access_ref_tail(item_text).casefold() def access_list(value: Any) -> list[Any]: if value is None: return [] if isinstance(value, list): return value if isinstance(value, tuple): return list(value) if isinstance(value, str): return [part.strip() for part in re.split(r"[,;]", value) if part.strip()] return [value] def access_pick_list(item: dict[str, Any], *keys: str) -> list[Any]: for key in keys: if key not in item: continue value = item.get(key) if value is not None and value != "": return access_list(value) return [] def access_bool(value: Any, default: bool = False) -> bool: if value is None: return default if isinstance(value, bool): return value return str(value).strip().casefold() in {"1", "true", "yes", "y", "да", "истина"} def access_subject_refs(raw: Any, default_type: str | None = None) -> list[dict[str, str]]: refs: list[dict[str, str]] = [] for item in access_list(raw): if isinstance(item, dict): subject_type = str(item.get("type") or item.get("subject_type") or default_type or "").strip() subject_id = access_item_id(item) else: subject_type = str(default_type or "").strip() subject_id = access_item_id(item) if subject_id: refs.append({"type": subject_type, "id": subject_id}) return refs def access_permission_key(permission: dict[str, Any], action: str) -> tuple[str, str, str]: object_ref = str(permission.get("object") or permission.get("object_ref") or permission.get("metadata") or "*") action_ref = str(action or permission.get("action") or "*") scope = str(permission.get("scope") or permission.get("mode") or "") return object_ref.casefold(), action_ref.casefold(), scope.casefold() def access_normalize_permission(raw: Any) -> list[dict[str, Any]]: if isinstance(raw, str): return [{"object": raw, "actions": ["*"]}] if not isinstance(raw, dict): return [] permission = dict(raw) object_ref = permission.get("object") or permission.get("object_ref") or permission.get("metadata") or permission.get("target") or "*" actions = access_pick_list(permission, "actions", "rights", "permissions") if not actions and permission.get("action") not in {None, ""}: actions = [permission.get("action")] if not actions: actions = ["*"] return [ { **{key: value for key, value in permission.items() if key not in {"action", "actions", "rights", "permissions", "metadata", "target"}}, "object": str(object_ref), "action": str(action), } for action in actions ] def access_snapshot_from_payload(payload: dict[str, Any], method: str) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: source = payload.get("access") if source is None and isinstance(payload.get("snapshot"), dict): source = payload["snapshot"].get("access") if source is None: source = payload.get("data") if source is None: return None, invalid_argument(method, "access", "Pass access snapshot data in access, snapshot.access, or data.") if not isinstance(source, dict): return None, invalid_argument(method, "access", "access must be a JSON object.") return source, None ACCESS_DISCOVERY_MARKERS = { "users": ["пользовател", "user"], "groups": ["групп", "group"], "profiles": ["профил", "profile"], "roles": ["роль", "роли", "role"], "permissions": ["прав", "доступ", "permission", "access"], "restrictions": ["огранич", "rls", "restriction"], } ACCESS_BSP_RIGHT_FIELD_LABELS = { 27940: "Добавление", 27941: "Изменение", 27942: "Чтение", 27943: "Изменение", 27944: "ДобавлениеБезОграничения", 27945: "ЧтениеБезОграничения", 27946: "Удаление", 27947: "ИзменениеБезОграничения", } def access_identifier_parts(value: Any) -> dict[str, str] | None: raw = str(value or "").strip() if not raw: return None type_code = "" ref = raw if ":" in raw: type_code, ref = [part.strip() for part in raw.split(":", 1)] normalized_ref = re.sub(r"[^0-9a-fA-F]", "", ref).upper() if len(normalized_ref) != 32: return None result = {"id": raw, "ref": normalized_ref} if type_code: result["type_code"] = type_code.upper() return result def access_ref_tail(value: Any) -> str: parts = access_identifier_parts(value) if parts: return parts["ref"] raw = str(value or "").strip() return (raw.split(":", 1)[1] if ":" in raw else raw).upper() def access_identifier_guid_variants(value: Any) -> list[str]: parts = access_identifier_parts(value) if not parts: return [] try: raw_bytes = bytes.fromhex(parts["ref"]) except ValueError: return [] variants: list[str] = [] for guid in ( str(uuid.UUID(bytes=raw_bytes)), str(uuid.UUID(bytes=bytes(raw_bytes[12:16] + raw_bytes[10:12] + raw_bytes[8:10] + raw_bytes[0:2] + raw_bytes[2:8]))), ): if guid not in variants: variants.append(guid) return variants def access_identifier_payload_from_index(config: dict[str, str] | None, value: Any) -> dict[str, Any] | None: if not config: return None for guid in access_identifier_guid_variants(value): with cache_connection() as conn: row = conn.execute( """ SELECT payload_json, guid_role, kind, kind_ru, public_kind, name, synonym, full_name, presentation FROM metadata_guid_index WHERE server_key = ? AND database_name = ? AND guid = ? ORDER BY CASE guid_role WHEN 'metadata_object' THEN 0 WHEN 'metadata_type' THEN 1 WHEN 'generated_type' THEN 2 ELSE 3 END LIMIT 1 """, (cache_server_key(config), cache_database_name(config), guid), ).fetchone() if not row: continue payload: dict[str, Any] = {} try: loaded = json.loads(row["payload_json"] or "{}") if isinstance(loaded, dict): payload.update(loaded) except Exception: pass for key in ("guid_role", "kind", "kind_ru", "public_kind", "name", "synonym", "full_name", "presentation"): if row[key] not in {None, ""}: payload.setdefault(key, row[key]) payload.setdefault("guid", guid) payload.setdefault("match_by", "metadata_guid_index") return payload return None def access_public_identifier_resolution(value: Any, payload: dict[str, Any]) -> dict[str, Any]: result = { "id": str(value or ""), "guid": payload.get("guid"), "match_by": payload.get("match_by") or "metadata_guid_index", } for key in ("kind", "kind_ru", "public_kind", "name", "synonym", "full_name", "presentation"): if payload.get(key) not in {None, ""}: result[key] = payload.get(key) return result def access_enrich_snapshot_identifiers(access: dict[str, Any], base_id: str) -> dict[str, Any]: config, _ = sql_config_for_base(base_id) role_ids = {str(role.get("id") or "") for role in access_list(access.get("roles")) if isinstance(role, dict)} object_ids: set[str] = set() for role in access_list(access.get("roles")): if not isinstance(role, dict): continue for permission in access_list(role.get("permissions")): if isinstance(permission, dict) and permission.get("object") not in {None, ""}: object_ids.add(str(permission.get("object"))) role_map = {value: access_identifier_payload_from_index(config, value) for value in sorted(role_ids) if value} object_map = {value: access_identifier_payload_from_index(config, value) for value in sorted(object_ids) if value} resolved_roles = {key: value for key, value in role_map.items() if isinstance(value, dict)} resolved_objects = {key: value for key, value in object_map.items() if isinstance(value, dict)} for role in access_list(access.get("roles")): if not isinstance(role, dict): continue role_id = str(role.get("id") or "") resolved_role = resolved_roles.get(role_id) if resolved_role: role.setdefault("name", resolved_role.get("name") or resolved_role.get("full_name") or role_id) role["resolution"] = access_public_identifier_resolution(role_id, resolved_role) for permission in access_list(role.get("permissions")): if not isinstance(permission, dict): continue object_id = str(permission.get("object") or "") resolved_object = resolved_objects.get(object_id) if not resolved_object: continue permission["object_resolution"] = access_public_identifier_resolution(object_id, resolved_object) for source_key, target_key in (("name", "object_name"), ("kind", "object_kind"), ("full_name", "object_full_name")): if resolved_object.get(source_key) not in {None, ""}: permission[target_key] = resolved_object.get(source_key) return { "status": "ok" if (resolved_roles or resolved_objects) else "unresolved", "source": "metadata_guid_index", "roles": { "total": len(role_ids), "resolved": len(resolved_roles), "unresolved": len(role_ids) - len(resolved_roles), }, "objects": { "total": len(object_ids), "resolved": len(resolved_objects), "unresolved": len(object_ids) - len(resolved_objects), }, } def access_schema_candidate_score(table_name: str, columns: list[str]) -> tuple[int, list[str]]: haystack = " ".join([table_name, *columns]).casefold() reasons: list[str] = [] score = 0 for area, markers in ACCESS_DISCOVERY_MARKERS.items(): matched = [marker for marker in markers if marker in haystack] if matched: score += 10 + len(matched) reasons.append(area) if any("состав" in column.casefold() or "member" in column.casefold() for column in columns): score += 5 reasons.append("membership") if any("владел" in column.casefold() or "owner" in column.casefold() for column in columns): score += 2 reasons.append("owner_column") return score, sorted(set(reasons)) def access_rows_from_query(base_id: str, query: str, *, limit: int, timeout_seconds: int) -> tuple[list[dict[str, Any]], dict[str, Any] | None, bool]: validation = validate_query({"base_id": base_id, "query": query, "timeout_seconds": timeout_seconds}) if not validation.get("valid"): return [], {"status": "rejected", "validation": validation}, False conn, config, error = connect_live_sql(base_id, "access.snapshot.extract", timeout_seconds=timeout_seconds) if error: return [], error, False rows: list[dict[str, Any]] = [] truncated = False try: with conn: with conn.cursor(as_dict=True) as cursor: cursor.execute(query) fetched = cursor.fetchmany(limit + 1) truncated = len(fetched) > limit rows = [{key: jsonable(value) for key, value in row.items()} for row in fetched[:limit]] except Exception as exc: return [], { "schema": "onec_access_snapshot_extract.v1", "status": "error", "base_id": base_id, "source": {"kind": "live_sql", "database": (config or {}).get("database")}, "diagnostics": {"message": str(exc)}, }, False return rows, None, truncated def access_map_row(row: dict[str, Any], mapping: dict[str, str] | None, defaults: dict[str, Any] | None = None) -> dict[str, Any]: if not mapping: return dict(row) result = dict(defaults or {}) for target, source in mapping.items(): if source in row: result[target] = row.get(source) return result def access_snapshot_from_extractor_rows(rows_by_area: dict[str, list[dict[str, Any]]], mappings: dict[str, Any]) -> dict[str, Any]: access: dict[str, Any] = {"users": [], "groups": [], "profiles": [], "roles": [], "data_restrictions": []} for area in ("users", "groups", "profiles", "roles", "data_restrictions"): area_mapping = mappings.get(area) if isinstance(mappings.get(area), dict) else None access[area] = [access_map_row(row, area_mapping) for row in rows_by_area.get(area) or []] access_keys: dict[str, list[dict[str, Any]]] = {} for area in ("access_group_keys", "access_user_keys", "access_object_keys", "access_set_keys"): area_mapping = mappings.get(area) if isinstance(mappings.get(area), dict) else None rows = [access_map_row(row, area_mapping) for row in rows_by_area.get(area) or []] if rows: access_keys[area] = rows if access_keys: access["access_keys"] = access_keys for row in rows_by_area.get("group_users") or []: mapped = access_map_row(row, mappings.get("group_users") if isinstance(mappings.get("group_users"), dict) else None) group_id = str(mapped.get("group") or mapped.get("group_id") or "") user_id = str(mapped.get("user") or mapped.get("user_id") or "") for group in access["groups"]: if str(group.get("id") or group.get("name") or "") == group_id: group.setdefault("users", []).append(user_id) user_group_members: dict[str, list[str]] = {} for row in rows_by_area.get("user_group_members") or []: mapped = access_map_row(row, mappings.get("user_group_members") if isinstance(mappings.get("user_group_members"), dict) else None) user_group_id = str(mapped.get("group") or mapped.get("group_id") or "") user_id = str(mapped.get("user") or mapped.get("user_id") or "") if user_group_id and user_id: user_group_members.setdefault(user_group_id, []).append(user_id) if user_group_members: for group in access["groups"]: expanded_users: list[str] = [] for user_id in list(group.get("users") or []): expanded_users.extend(user_group_members.get(str(user_id), [])) for user_id in expanded_users: if user_id not in group.setdefault("users", []): group["users"].append(user_id) for row in rows_by_area.get("group_profiles") or []: mapped = access_map_row(row, mappings.get("group_profiles") if isinstance(mappings.get("group_profiles"), dict) else None) group_id = str(mapped.get("group") or mapped.get("group_id") or "") profile_id = str(mapped.get("profile") or mapped.get("profile_id") or "") for group in access["groups"]: if str(group.get("id") or group.get("name") or "") == group_id: group.setdefault("profiles", []).append(profile_id) for row in rows_by_area.get("profile_roles") or []: mapped = access_map_row(row, mappings.get("profile_roles") if isinstance(mappings.get("profile_roles"), dict) else None) profile_id = str(mapped.get("profile") or mapped.get("profile_id") or "") role_id = str(mapped.get("role") or mapped.get("role_id") or "") for profile in access["profiles"]: if str(profile.get("id") or profile.get("name") or "") == profile_id: profile.setdefault("roles", []).append(role_id) for row in rows_by_area.get("role_permissions") or []: mapped = access_map_row(row, mappings.get("role_permissions") if isinstance(mappings.get("role_permissions"), dict) else None) role_id = str(mapped.get("role") or mapped.get("role_id") or "") permission = { "object": mapped.get("object") or mapped.get("object_ref") or "*", **({"object_name": mapped.get("object_name")} if mapped.get("object_name") not in {None, ""} else {}), **( {"actions": mapped.get("actions")} if mapped.get("actions") not in {None, ""} else {"action": mapped.get("action") or mapped.get("right") or "*"} ), **({"source_field": mapped.get("source_field")} if mapped.get("source_field") not in {None, ""} else {}), **({"source_fields": mapped.get("source_fields")} if mapped.get("source_fields") not in {None, ""} else {}), } for role in access["roles"]: if str(role.get("id") or role.get("name") or "") == role_id: if mapped.get("role_name") not in {None, ""} and role.get("name") in {None, "", role.get("id")}: role["name"] = mapped.get("role_name") role.setdefault("permissions", []).append(permission) return access def access_sql_hex(column: str) -> str: return f"CONVERT(varchar(64), {column}, 2)" def access_sql_ref_expr(prefix: str, field: str) -> str: rrref = f"{prefix}.{field}_RRRef" rtref = f"{prefix}.{field}_RTRef" return f"CASE WHEN {rrref} IS NULL THEN NULL ELSE CONCAT({access_sql_hex(rtref)}, ':', {access_sql_hex(rrref)}) END" def access_route_sql_number(item: dict[str, Any] | None) -> int | None: if not isinstance(item, dict): return None for route in item.get("storage_routes") or []: if isinstance(route, dict) and route.get("sql_number") is not None: try: return int(route.get("sql_number")) except (TypeError, ValueError): return None return None def access_object_sql_number(profile: dict[str, Any]) -> int | None: obj = profile.get("object") if isinstance(profile.get("object"), dict) else {} storage = obj.get("storage") if isinstance(obj.get("storage"), dict) else {} for route in storage.get("dbnames") or []: if isinstance(route, dict) and route.get("sql_number") is not None: try: return int(route.get("sql_number")) except (TypeError, ValueError): return None return None def access_tabular_column_sql_number(profile: dict[str, Any], tabular_section: str, column: str) -> int | None: for section in profile.get("tabular_sections") or []: if not isinstance(section, dict) or str(section.get("name") or "").casefold() != tabular_section.casefold(): continue for item in section.get("columns") or []: if isinstance(item, dict) and str(item.get("name") or "").casefold() == column.casefold(): return access_route_sql_number(item) return None def access_bsp_metadata_profile(base_id: str, kind: str, name: str) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: result = metadata_object_attributes( { "base_id": base_id, "kind": kind, "name": name, "include_storage": True, "only": "all", "limit": 500, "timeout_seconds": 60, } ) if result.get("status") != "ok": return None, result return result, None def access_sql_existing_tables(base_id: str, table_prefix: str, *, timeout_seconds: int = 30) -> list[str]: conn, _, error = connect_live_sql(base_id, "access.snapshot.extract", timeout_seconds=timeout_seconds) if error: return [] escaped = table_prefix.replace("[", "[[]").replace("%", "[%]").replace("_", "[_]") try: with conn: with conn.cursor(as_dict=True) as cursor: cursor.execute( """ SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'dbo' AND (TABLE_NAME = %s OR TABLE_NAME LIKE %s) ORDER BY TABLE_NAME """, (table_prefix, f"{escaped}X%"), ) rows = cursor.fetchall() except Exception: return [] tables: list[str] = [] for row in rows: name = str(row.get("TABLE_NAME") or "") if name == table_prefix or re.fullmatch(rf"{re.escape(table_prefix)}X\d+", name): tables.append(name) return tables def access_identifier_union_sql(tables: list[str]) -> str: selected = [table for table in tables if re.fullmatch(r"_Reference\d+(X\d+)?", table)] if not selected: return "SELECT CAST(NULL AS varbinary(16)) AS _IDRRef, CAST(NULL AS nvarchar(512)) AS _Description WHERE 1 = 0" return "\nUNION ALL\n".join( f"SELECT _IDRRef, _Description FROM dbo.[{table}] WHERE _Marked = 0x00" for table in selected ) def access_reference_description_union_sql(tables: list[str]) -> str: selected = [table for table in tables if re.fullmatch(r"_Reference\d+(X\d+)?", table)] if not selected: return "SELECT CAST(NULL AS varbinary(16)) AS _IDRRef, CAST(NULL AS nvarchar(512)) AS _Description, CAST(0 AS bit) AS _Marked WHERE 1 = 0" return "\nUNION ALL\n".join( f"SELECT _IDRRef, _Description, _Marked FROM dbo.[{table}]" for table in selected ) def access_resolve_user_names(base_id: str, refs: set[str], diagnostics: dict[str, Any] | None = None, *, timeout_seconds: int = 60, max_refs: int = 20000) -> dict[str, str]: normalized_refs = sorted({access_ref_tail(ref) for ref in refs if re.fullmatch(r"[0-9A-F]{32}", access_ref_tail(ref) or "")})[:max_refs] if not normalized_refs: return {} sql_numbers = (((diagnostics or {}).get("metadata") or {}).get("sql_numbers") or {}) if isinstance(diagnostics, dict) else {} table_numbers: list[int] = [] for key in ("users", "external_users", "user_groups"): try: number = int(sql_numbers.get(key)) except (TypeError, ValueError): continue if number not in table_numbers: table_numbers.append(number) if not table_numbers: return {} table_unions: list[str] = [] for number in table_numbers: table_prefix = f"_Reference{number}" tables = access_sql_existing_tables(base_id, table_prefix, timeout_seconds=timeout_seconds) or [table_prefix] table_unions.append(access_reference_description_union_sql(tables)) conn, _, error = connect_live_sql(base_id, "access.user_names.resolve", timeout_seconds=timeout_seconds) if error: return {} where = ",".join(f"0x{ref}" for ref in normalized_refs) names: dict[str, str] = {} try: with conn: with conn.cursor(as_dict=True) as cursor: for union_sql in table_unions: cursor.execute( f""" SELECT {access_sql_hex('u._IDRRef')} AS id, u._Description AS name FROM ({union_sql}) u WHERE u._IDRRef IN ({where}) AND u._Marked = 0x00 """ ) for row in cursor.fetchall(): ref = access_ref_tail(row.get("id")) name = str(row.get("name") or "").strip() if ref and name and name != ref: names.setdefault(ref, name) except Exception: return names return names ACCESS_BSP_EXTRACTOR_PLAN_CACHE: dict[str, tuple[dict[str, str], dict[str, dict[str, str]], dict[str, Any]]] = {} def access_bsp_extractor_plan(base_id: str) -> tuple[dict[str, str] | None, dict[str, dict[str, str]] | None, dict[str, Any] | None]: cache_key = str(base_id or "") cached = ACCESS_BSP_EXTRACTOR_PLAN_CACHE.get(cache_key) if cached: queries, mappings, diagnostics = cached return queries, mappings, {**diagnostics, "cache": {"hit": True, "key": cache_key}} required = { "users": ("Catalog", "Пользователи"), "external_users": ("Catalog", "ВнешниеПользователи"), "groups": ("Catalog", "ГруппыДоступа"), "user_groups": ("Catalog", "ГруппыПользователей"), "profiles": ("Catalog", "ПрофилиГруппДоступа"), "metadata_identifiers": ("Catalog", "ИдентификаторыОбъектовМетаданных"), "access_keys": ("Catalog", "КлючиДоступа"), "access_sets": ("Catalog", "НаборыГруппДоступа"), "role_rights": ("InformationRegister", "ПраваРолей"), "access_group_keys": ("InformationRegister", "КлючиДоступаГруппДоступа"), "access_user_keys": ("InformationRegister", "КлючиДоступаПользователей"), "access_object_keys": ("InformationRegister", "КлючиДоступаКОбъектам"), "access_set_keys": ("InformationRegister", "КлючиДоступаНаборовГруппДоступа"), } profiles: dict[str, dict[str, Any]] = {} errors: dict[str, Any] = {} for key, (kind, name) in required.items(): profile, error = access_bsp_metadata_profile(base_id, kind, name) if error: errors[key] = error elif profile: profiles[key] = profile if errors: return None, None, {"status": "error", "diagnostics": {"message": "Could not resolve required BSP access metadata objects.", "errors": errors}} numbers = {key: access_object_sql_number(profile) for key, profile in profiles.items()} group_user_field = access_tabular_column_sql_number(profiles["groups"], "Пользователи", "Пользователь") profile_role_field = access_tabular_column_sql_number(profiles["profiles"], "Роли", "Роль") user_group_member_field = access_tabular_column_sql_number(profiles["user_groups"], "Состав", "Пользователь") missing = [key for key, value in {**numbers, "groups.Пользователи.Пользователь": group_user_field, "profiles.Роли.Роль": profile_role_field, "user_groups.Состав.Пользователь": user_group_member_field}.items() if value is None] if missing: return None, None, {"status": "error", "diagnostics": {"message": "Could not resolve required SQL numbers for BSP access metadata.", "missing": missing}} group_table = f"_Reference{numbers['groups']}" profile_table = f"_Reference{numbers['profiles']}" users_table = f"_Reference{numbers['users']}" external_users_table = f"_Reference{numbers['external_users']}" user_groups_table = f"_Reference{numbers['user_groups']}" users_union = access_reference_description_union_sql(access_sql_existing_tables(base_id, users_table, timeout_seconds=30) or [users_table]) external_users_union = access_reference_description_union_sql(access_sql_existing_tables(base_id, external_users_table, timeout_seconds=30) or [external_users_table]) user_groups_union = access_reference_description_union_sql(access_sql_existing_tables(base_id, user_groups_table, timeout_seconds=30) or [user_groups_table]) group_users_table = f"{group_table}_VT{int(group_user_field) - 2}" profile_roles_table = f"{profile_table}_VT{int(profile_role_field) - 2}" user_group_members_table = f"{user_groups_table}_VT{int(user_group_member_field) - 2}" rights_table = f"_InfoRg{numbers['role_rights']}" access_keys_table = f"_Reference{numbers['access_keys']}" access_sets_table = f"_Reference{numbers['access_sets']}" access_group_keys_table = f"_InfoRg{numbers['access_group_keys']}" access_user_keys_table = f"_InfoRg{numbers['access_user_keys']}" access_object_keys_table = f"_InfoRg{numbers['access_object_keys']}" access_set_keys_table = f"_InfoRg{numbers['access_set_keys']}" identifier_table = f"_Reference{numbers['metadata_identifiers']}" identifier_tables = access_sql_existing_tables(base_id, identifier_table, timeout_seconds=30) or [identifier_table] identifier_union = access_identifier_union_sql(identifier_tables) rights_actions_expr = " + ".join( f"CASE WHEN rr._Fld{field_number} = 0x01 THEN N'{ACCESS_BSP_RIGHT_FIELD_LABELS[field_number]},' ELSE N'' END" for field_number in ACCESS_BSP_RIGHT_FIELD_LABELS ) rights_source_fields_expr = " + ".join( f"CASE WHEN rr._Fld{field_number} = 0x01 THEN '_Fld{field_number},' ELSE '' END" for field_number in ACCESS_BSP_RIGHT_FIELD_LABELS ) queries = { "users": f""" SELECT CONCAT('{int(numbers['users']):08X}', ':', {access_sql_hex('u._IDRRef')}) AS id, u._Description AS name, CAST(1 AS bit) AS active, CASE WHEN u._Marked = 0x01 THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END AS marked, N'user' AS user_type, CAST(0 AS bit) AS service, CAST(0 AS bit) AS administrator FROM ({users_union}) u WHERE u._Marked = 0x00 UNION ALL SELECT CONCAT('{int(numbers['external_users']):08X}', ':', {access_sql_hex('eu._IDRRef')}) AS id, eu._Description AS name, CAST(1 AS bit) AS active, CASE WHEN eu._Marked = 0x01 THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END AS marked, N'external_user' AS user_type, CAST(0 AS bit) AS service, CAST(0 AS bit) AS administrator FROM ({external_users_union}) eu WHERE eu._Marked = 0x00 UNION ALL SELECT CONCAT('{int(numbers['user_groups']):08X}', ':', {access_sql_hex('ug._IDRRef')}) AS id, ug._Description AS name, CAST(1 AS bit) AS active, CASE WHEN ug._Marked = 0x01 THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END AS marked, N'user_group' AS user_type, CAST(0 AS bit) AS service, CAST(0 AS bit) AS administrator FROM ({user_groups_union}) ug WHERE ug._Marked = 0x00 """, "groups": f""" SELECT {access_sql_hex('g._IDRRef')} AS id, g._Description AS name, {access_sql_hex('g._Fld14123RRef')} AS profile FROM dbo.[{group_table}] g WHERE g._Marked = 0x00 """, "profiles": f""" SELECT {access_sql_hex('p._IDRRef')} AS id, p._Description AS name FROM dbo.[{profile_table}] p WHERE p._Marked = 0x00 """, "group_users": f""" SELECT {access_sql_hex('gu._Reference' + str(numbers['groups']) + '_IDRRef')} AS [group], {access_sql_ref_expr('gu', '_Fld' + str(group_user_field))} AS [user] FROM dbo.[{group_users_table}] gu """, "profile_roles": f""" SELECT {access_sql_hex('pr._Reference' + str(numbers['profiles']) + '_IDRRef')} AS [profile], {access_sql_ref_expr('pr', '_Fld' + str(profile_role_field))} AS [role] FROM dbo.[{profile_roles_table}] pr """, "roles": f""" SELECT DISTINCT {access_sql_ref_expr('pr', '_Fld' + str(profile_role_field))} AS id, COALESCE(role_ident._Description, {access_sql_ref_expr('pr', '_Fld' + str(profile_role_field))}) AS name FROM dbo.[{profile_roles_table}] pr LEFT JOIN ({identifier_union}) role_ident ON role_ident._IDRRef = pr._Fld{profile_role_field}_RRRef """, "group_profiles": f""" SELECT {access_sql_hex('g._IDRRef')} AS [group], {access_sql_hex('g._Fld14123RRef')} AS [profile] FROM dbo.[{group_table}] g WHERE g._Marked = 0x00 AND g._Fld14123RRef <> 0x00000000000000000000000000000000 """, "user_group_members": f""" SELECT {access_sql_hex('ug._Reference' + str(numbers['user_groups']) + '_IDRRef')} AS [group], {access_sql_hex('ug._Fld' + str(user_group_member_field) + 'RRef')} AS [user] FROM dbo.[{user_group_members_table}] ug """, "role_permissions": f""" SELECT CONCAT('000000C4:', {access_sql_hex('rr._Fld27939RRef')}) AS [role], CONCAT('000000C4:', {access_sql_hex('rr._Fld27938RRef')}) AS [object], role_ident._Description AS role_name, object_ident._Description AS object_name, {rights_actions_expr} AS actions, {rights_source_fields_expr} AS source_fields FROM dbo.[{rights_table}] rr LEFT JOIN ({identifier_union}) role_ident ON role_ident._IDRRef = rr._Fld27939RRef LEFT JOIN ({identifier_union}) object_ident ON object_ident._IDRRef = rr._Fld27938RRef WHERE rr._Fld27940 = 0x01 OR rr._Fld27941 = 0x01 OR rr._Fld27942 = 0x01 OR rr._Fld27943 = 0x01 OR rr._Fld27944 = 0x01 OR rr._Fld27945 = 0x01 OR rr._Fld27946 = 0x01 OR rr._Fld27947 = 0x01 """, "access_group_keys": f""" SELECT {access_sql_hex('gk._Fld25997_RRRef')} AS [group], {access_sql_ref_expr('gk', '_Fld25997')} AS group_ref, g._Description AS group_name, {access_sql_hex('gk._Fld25998RRef')} AS access_key, k._Description AS access_key_name, k._Fld15806 AS access_key_hash, k._Fld15807 AS access_key_field_mask FROM dbo.[{access_group_keys_table}] gk LEFT JOIN dbo.[{group_table}] g ON g._IDRRef = gk._Fld25997_RRRef LEFT JOIN dbo.[{access_keys_table}] k ON k._IDRRef = gk._Fld25998RRef """, "access_user_keys": f""" SELECT {access_sql_hex('uk._Fld26026RRef')} AS user_set, ns._Description AS user_set_name, {access_sql_ref_expr('ns', '_Fld16819')} AS [user], {access_sql_hex('uk._Fld26027RRef')} AS access_key, k._Description AS access_key_name, k._Fld15806 AS access_key_hash, k._Fld15807 AS access_key_field_mask FROM dbo.[{access_user_keys_table}] uk LEFT JOIN dbo.[{access_sets_table}] ns ON ns._IDRRef = uk._Fld26026RRef LEFT JOIN dbo.[{access_keys_table}] k ON k._IDRRef = uk._Fld26027RRef """, "access_object_keys": f""" SELECT {access_sql_ref_expr('ok', '_Fld26004')} AS object, {access_sql_hex('ok._Fld26004_RTRef')} AS object_type_code, CONVERT(int, ok._Fld26004_RTRef) AS object_sql_number, {access_sql_hex('ok._Fld26004_RRRef')} AS object_id, {access_sql_hex('ok._Fld26005RRef')} AS access_key, {access_sql_hex('ok._Fld26006RRef')} AS access_key_value, k._Description AS access_key_name, k._Fld15806 AS access_key_hash, k._Fld15807 AS access_key_field_mask FROM dbo.[{access_object_keys_table}] ok LEFT JOIN dbo.[{access_keys_table}] k ON k._IDRRef = ok._Fld26005RRef """, "access_set_keys": f""" SELECT {access_sql_hex('sk._Fld26019RRef')} AS access_set, ns._Description AS access_set_name, {access_sql_hex('sk._Fld26020RRef')} AS access_key, k._Description AS access_key_name, k._Fld15806 AS access_key_hash, k._Fld15807 AS access_key_field_mask FROM dbo.[{access_set_keys_table}] sk LEFT JOIN dbo.[{access_sets_table}] ns ON ns._IDRRef = sk._Fld26019RRef LEFT JOIN dbo.[{access_keys_table}] k ON k._IDRRef = sk._Fld26020RRef """, } mappings = { "users": {"id": "id", "name": "name", "active": "active", "marked": "marked", "user_type": "user_type", "service": "service", "administrator": "administrator"}, "groups": {"id": "id", "name": "name"}, "profiles": {"id": "id", "name": "name"}, "roles": {"id": "id", "name": "name"}, "group_users": {"group": "group", "user": "user"}, "group_profiles": {"group": "group", "profile": "profile"}, "profile_roles": {"profile": "profile", "role": "role"}, "user_group_members": {"group": "group", "user": "user"}, "role_permissions": {"role": "role", "object": "object", "role_name": "role_name", "object_name": "object_name", "actions": "actions", "source_fields": "source_fields"}, "access_group_keys": {"group": "group", "group_ref": "group_ref", "group_name": "group_name", "access_key": "access_key", "access_key_name": "access_key_name", "access_key_hash": "access_key_hash", "access_key_field_mask": "access_key_field_mask"}, "access_user_keys": {"user_set": "user_set", "user_set_name": "user_set_name", "user": "user", "access_key": "access_key", "access_key_name": "access_key_name", "access_key_hash": "access_key_hash", "access_key_field_mask": "access_key_field_mask"}, "access_object_keys": {"object": "object", "object_type_code": "object_type_code", "object_sql_number": "object_sql_number", "object_id": "object_id", "access_key": "access_key", "access_key_value": "access_key_value", "access_key_name": "access_key_name", "access_key_hash": "access_key_hash", "access_key_field_mask": "access_key_field_mask"}, "access_set_keys": {"access_set": "access_set", "access_set_name": "access_set_name", "access_key": "access_key", "access_key_name": "access_key_name", "access_key_hash": "access_key_hash", "access_key_field_mask": "access_key_field_mask"}, } diagnostics = { "preset": "bsp", "metadata": { "sql_numbers": numbers, "tables": { "users": users_table, "external_users": external_users_table, "groups": group_table, "profiles": profile_table, "group_users": group_users_table, "profile_roles": profile_roles_table, "user_group_members": user_group_members_table, "role_permissions": rights_table, "metadata_identifiers": identifier_tables, "access_keys": access_keys_table, "access_sets": access_sets_table, "access_group_keys": access_group_keys_table, "access_user_keys": access_user_keys_table, "access_object_keys": access_object_keys_table, "access_set_keys": access_set_keys_table, }, "fields": { "groups.Пользователи.Пользователь": group_user_field, "profiles.Роли.Роль": profile_role_field, "user_groups.Состав.Пользователь": user_group_member_field, "role_permissions.flags": [f"_Fld{number}" for number in ACCESS_BSP_RIGHT_FIELD_LABELS], }, "permission_action_labels": { "status": "adapter_mapping", "mapping": {f"_Fld{number}": label for number, label in ACCESS_BSP_RIGHT_FIELD_LABELS.items()}, "message": "BSP ПраваРолей flag labels are decoded by the adapter mapping; source_fields are kept in permissions for live-base verification.", }, }, } ACCESS_BSP_EXTRACTOR_PLAN_CACHE[cache_key] = (queries, mappings, diagnostics) return queries, mappings, {**diagnostics, "cache": {"hit": False, "key": cache_key}} def access_snapshot_discover(base_id: str, *, limit: int, timeout_seconds: int) -> dict[str, Any]: conn, config, error = connect_live_sql(base_id, "access.snapshot.extract", timeout_seconds=timeout_seconds) if error: return error rows: list[dict[str, Any]] = [] try: with conn: with conn.cursor(as_dict=True) as cursor: cursor.execute( """ SELECT TOP (%d) c.TABLE_SCHEMA AS schema_name, c.TABLE_NAME AS table_name, c.COLUMN_NAME AS column_name FROM INFORMATION_SCHEMA.COLUMNS c WHERE c.TABLE_NAME LIKE N'%%Пользовател%%' OR c.TABLE_NAME LIKE N'%%Групп%%' OR c.TABLE_NAME LIKE N'%%Профил%%' OR c.TABLE_NAME LIKE N'%%Рол%%' OR c.TABLE_NAME LIKE N'%%Доступ%%' OR c.TABLE_NAME LIKE N'%%Прав%%' OR c.COLUMN_NAME LIKE N'%%Пользовател%%' OR c.COLUMN_NAME LIKE N'%%Групп%%' OR c.COLUMN_NAME LIKE N'%%Профил%%' OR c.COLUMN_NAME LIKE N'%%Рол%%' OR c.COLUMN_NAME LIKE N'%%Доступ%%' OR c.COLUMN_NAME LIKE N'%%Прав%%' ORDER BY c.TABLE_SCHEMA, c.TABLE_NAME, c.ORDINAL_POSITION """ % max(1, min(limit * 50, 5000)) ) rows = [{key: jsonable(value) for key, value in row.items()} for row in cursor.fetchall()] except Exception as exc: return { "schema": "onec_access_snapshot_extract.v1", "status": "error", "base_id": base_id, "source": {"kind": "live_sql", "database": (config or {}).get("database")}, "diagnostics": {"message": str(exc)}, } grouped: dict[str, list[str]] = {} for row in rows: key = f"{row.get('schema_name')}.{row.get('table_name')}" grouped.setdefault(key, []).append(str(row.get("column_name") or "")) candidates = [] for table_ref, columns in grouped.items(): score, reasons = access_schema_candidate_score(table_ref, columns) if score > 0: candidates.append({"table": table_ref, "score": score, "reasons": reasons, "columns": columns[:50]}) candidates.sort(key=lambda item: (-int(item["score"]), str(item["table"]))) return { "schema": "onec_access_snapshot_extract.v1", "status": "discovery", "base_id": base_id, "source": {"kind": "live_sql", "database": (config or {}).get("database")}, "access": {"users": [], "groups": [], "profiles": [], "roles": [], "data_restrictions": []}, "candidates": candidates[:limit], "counts": {"candidate_tables": len(candidates), "scanned_columns": len(rows)}, "diagnostics": { "message": "No extractor queries were provided. Review candidates and pass queries+mappings to build a normalized access snapshot.", "required_areas": ["users", "groups", "profiles", "roles", "group_users", "group_profiles", "profile_roles", "role_permissions", "data_restrictions"], }, } def access_snapshot_extract(payload: dict[str, Any]) -> dict[str, Any]: method = "access.snapshot.extract" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error limit, limit_error = parse_int_argument(payload, "limit", method=method, default=1000, minimum=1, maximum=20000) if limit_error: return limit_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1, maximum=120) if timeout_error: return timeout_error max_effective_permissions, max_effective_permissions_error = parse_int_argument( payload, "max_effective_permissions_per_user", method=method, default=5000, minimum=0, maximum=200000, ) if max_effective_permissions_error: return max_effective_permissions_error resolve_identifiers, resolve_identifiers_error = strict_bool_argument(payload, "resolve_identifiers", method=method, default=True) if resolve_identifiers_error: return resolve_identifiers_error preset = str(payload.get("preset") or payload.get("profile") or "").strip().casefold() if not preset and payload.get("queries") is None: preset = "bsp" if preset in {"bsp", "бсп"}: queries, mappings, preset_diagnostics = access_bsp_extractor_plan(base_id) if not queries or not mappings: result = access_snapshot_discover(base_id, limit=int(limit or 200), timeout_seconds=int(timeout_seconds or 30)) result["status"] = "partial" if result.get("status") == "discovery" else result.get("status") result["preset"] = "bsp" result["diagnostics"] = { **(result.get("diagnostics") if isinstance(result.get("diagnostics"), dict) else {}), "preset_error": (preset_diagnostics or {}).get("diagnostics") or preset_diagnostics, } return result payload = {**payload, "queries": queries, "mappings": mappings} payload_diagnostics = preset_diagnostics else: payload_diagnostics = None queries = payload.get("queries") if queries is None: return access_snapshot_discover(base_id, limit=int(limit or 200), timeout_seconds=int(timeout_seconds or 30)) if not isinstance(queries, dict): return invalid_argument(method, "queries", "queries must be a JSON object keyed by extractor area.") mappings = payload.get("mappings") if isinstance(payload.get("mappings"), dict) else {} rows_by_area: dict[str, list[dict[str, Any]]] = {} diagnostics: dict[str, Any] = {"extractors": {}} for area, query in queries.items(): if not isinstance(query, str) or not query.strip(): return invalid_argument(method, f"queries.{area}", "Each extractor query must be a non-empty JSON string.") rows, error, truncated = access_rows_from_query(base_id, query, limit=int(limit or 1000), timeout_seconds=int(timeout_seconds or 30)) if error: return error rows_by_area[str(area)] = rows diagnostics["extractors"][str(area)] = {"rows": len(rows), "limit": int(limit or 1000), "truncated": bool(truncated)} access = access_snapshot_from_extractor_rows(rows_by_area, mappings) graph = build_access_graph_from_snapshot( access, base_id=base_id, max_permissions_per_user=max_effective_permissions, resolve_identifiers=bool(resolve_identifiers), ) return { "schema": "onec_access_snapshot_extract.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "mode": "explicit_readonly_extractors"}, **({"preset": preset} if preset else {}), "access": access, "graph": graph, "counts": graph.get("counts"), "diagnostics": {**diagnostics, **(payload_diagnostics or {})}, } ACCESS_KEY_QUERY_AREAS = { "group": "access_group_keys", "groups": "access_group_keys", "access_group": "access_group_keys", "access_group_keys": "access_group_keys", "user": "access_user_keys", "users": "access_user_keys", "user_set": "access_user_keys", "access_user_keys": "access_user_keys", "object": "access_object_keys", "objects": "access_object_keys", "access_object_keys": "access_object_keys", "set": "access_set_keys", "access_set": "access_set_keys", "access_set_keys": "access_set_keys", } ACCESS_RECORD_TABLE_PREFIXES = [ "_Reference", "_Document", "_BPr", "_Task", "_Enum", "_Acc", "_CKinds", "_Chrc", "_Node", ] def access_sql_string_literal(value: Any) -> str: return "N'" + str(value or "").replace("'", "''") + "'" def access_record_table_candidates(sql_number: int) -> list[str]: return [f"{prefix}{sql_number}" for prefix in ACCESS_RECORD_TABLE_PREFIXES] def normalize_access_object_record_filters( payload: dict[str, Any], method: str, base_id: str, *, timeout_seconds: int, ) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any] | None]: """Resolve public metadata/record refs without overloading access.keys.query kind.""" argument_error = validate_optional_string_arguments(payload, method, ["object_ref", "record_ref"]) if argument_error: return payload, {}, argument_error normalized = dict(payload) resolution: dict[str, Any] = {} object_ref = str(payload.get("object_ref") or "").strip() if object_ref: schema_result = data_object_schema( { "base_id": base_id, "object_ref": object_ref, "timeout_seconds": timeout_seconds, } ) if schema_result.get("status") != "ok": error = dict(schema_result) error["method"] = method error.setdefault("argument", "object_ref") return payload, {}, error object_card = schema_result.get("object") if isinstance(schema_result.get("object"), dict) else {} object_kind = str(object_card.get("kind") or "") table = schema_result.get("table") if isinstance(schema_result.get("table"), dict) else {} table_name = str(table.get("name") or "") expected_prefix = DATA_TABLE_PREFIXES.get(object_kind) table_match = re.fullmatch(rf"{re.escape(expected_prefix or '')}(\d+)", table_name) if expected_prefix else None if not table_match: return payload, {}, { "schema": "onec_adapter_request_error.v1", "method": method, "status": "unsupported_kind", "error": "unsupported_kind", "argument": "object_ref", "base_id": base_id, "object": object_card, "diagnostics": { "message": "The selected metadata object has no BSP application-record route.", }, } object_sql_number = int(table_match.group(1)) explicit_sql_number = payload.get("object_sql_number") if explicit_sql_number not in {None, ""}: try: explicit_sql_number = int(explicit_sql_number) except (TypeError, ValueError): return payload, {}, invalid_argument(method, "object_sql_number", "object_sql_number must be an integer.") if explicit_sql_number != object_sql_number: return payload, {}, invalid_argument( method, "object_sql_number", "object_sql_number conflicts with the metadata object resolved from object_ref.", ) normalized["object_sql_number"] = object_sql_number resolution["metadata_object"] = { "input": object_ref, "ref": object_selector_ref(object_kind, object_card.get("name")) or object_ref, "kind": object_kind or None, "name": object_card.get("name"), "guid": object_card.get("guid"), "object_sql_number": object_sql_number, } record_ref = str(payload.get("record_ref") or "").strip() if record_ref: record_parts = access_identifier_parts(record_ref) if not record_parts: return payload, {}, invalid_argument( method, "record_ref", "record_ref must contain exactly one 32-hex-character 1C application-data reference.", ) record_id = record_parts["ref"] explicit_object_id = payload.get("object_id") if explicit_object_id not in {None, ""}: explicit_record_id = access_ref_tail(explicit_object_id) if explicit_record_id != record_id: return payload, {}, invalid_argument( method, "object_id", "object_id conflicts with record_ref.", ) normalized["object_id"] = record_id resolution["data_record"] = { "input": record_ref, "record_ref": record_id, **({"type_code": record_parts["type_code"]} if record_parts.get("type_code") else {}), } return normalized, resolution, None def access_resolve_object_key_records(base_id: str, rows: list[dict[str, Any]], *, timeout_seconds: int = 60, max_records: int = 200) -> dict[str, Any]: wanted: dict[int, set[str]] = {} for row in rows: try: sql_number = int(row.get("object_sql_number")) except (TypeError, ValueError): continue object_id = access_ref_tail(row.get("object_id") or row.get("object")) if re.fullmatch(r"[0-9A-F]{32}", object_id or ""): wanted.setdefault(sql_number, set()).add(object_id) if not wanted: return {"rows": rows, "diagnostics": {"resolved": 0, "requested": 0, "tables": {}}} conn, _, error = connect_live_sql(base_id, "access.object_keys.resolve", timeout_seconds=timeout_seconds) if error: return {"rows": rows, "diagnostics": {"resolved": 0, "requested": sum(len(values) for values in wanted.values()), "error": error}} table_info: dict[int, dict[str, Any]] = {} resolved: dict[tuple[int, str], dict[str, Any]] = {} try: with conn: with conn.cursor(as_dict=True) as cursor: all_candidates = [table for number in wanted for table in access_record_table_candidates(number)] if all_candidates: placeholders = ",".join(["%s"] * len(all_candidates)) cursor.execute( f""" SELECT TABLE_NAME, COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME IN ({placeholders}) ORDER BY TABLE_NAME, ORDINAL_POSITION """, tuple(all_candidates), ) columns_by_table: dict[str, set[str]] = {} for row in cursor.fetchall(): columns_by_table.setdefault(str(row.get("TABLE_NAME") or ""), set()).add(str(row.get("COLUMN_NAME") or "")) for number in wanted: for table in access_record_table_candidates(number): columns = columns_by_table.get(table) or set() if "_IDRRef" not in columns: continue presentation_columns = [column for column in ("_Description", "_Number", "_Date_Time") if column in columns] table_info[number] = {"table": table, "columns": sorted(columns), "presentation_columns": presentation_columns} break remaining_budget = max(0, max_records) for number, ids in wanted.items(): info = table_info.get(number) if not info or remaining_budget <= 0: continue selected_ids = sorted(ids)[:remaining_budget] remaining_budget -= len(selected_ids) table = str(info["table"]) select_parts = [f"CONVERT(varchar(64), _IDRRef, 2) AS object_id"] if "_Description" in info.get("presentation_columns", []): select_parts.append("_Description AS description") if "_Number" in info.get("presentation_columns", []): select_parts.append("_Number AS number") if "_Date_Time" in info.get("presentation_columns", []): select_parts.append("_Date_Time AS date") if "_Marked" in info.get("columns", []): select_parts.append("CASE WHEN _Marked = 0x01 THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END AS marked") where = ",".join(f"0x{object_id}" for object_id in selected_ids) cursor.execute(f"SELECT {', '.join(select_parts)} FROM dbo.[{table}] WHERE _IDRRef IN ({where})") for record in cursor.fetchall(): object_id = str(record.get("object_id") or "").upper() presentation = ( record.get("description") or " ".join(str(record.get(key) or "") for key in ("number", "date") if record.get(key) not in {None, ""}).strip() or object_id ) resolved[(number, object_id)] = { "table": table, "object_sql_number": number, "object_id": object_id, "presentation": presentation, **({"description": record.get("description")} if record.get("description") not in {None, ""} else {}), **({"number": record.get("number")} if record.get("number") not in {None, ""} else {}), **({"date": jsonable(record.get("date"))} if record.get("date") not in {None, ""} else {}), **({"marked": bool(record.get("marked"))} if record.get("marked") is not None else {}), } except Exception as exc: return {"rows": rows, "diagnostics": {"resolved": len(resolved), "requested": sum(len(values) for values in wanted.values()), "error": str(exc), "tables": table_info}} enriched: list[dict[str, Any]] = [] for row in rows: item = dict(row) try: sql_number = int(item.get("object_sql_number")) except (TypeError, ValueError): enriched.append(item) continue record = resolved.get((sql_number, access_ref_tail(item.get("object_id") or item.get("object")))) if record: item["object_record"] = record item["object_presentation"] = record.get("presentation") enriched.append(item) return { "rows": enriched, "diagnostics": { "requested": sum(len(values) for values in wanted.values()), "resolved": len(resolved), "max_records": max_records, "tables": {str(number): {"table": info.get("table"), "presentation_columns": info.get("presentation_columns")} for number, info in table_info.items()}, }, } def access_key_query_filters(payload: dict[str, Any], area: str) -> list[str]: filters: list[str] = [] area_columns = { "access_group_keys": ["group", "group_ref", "group_name", "access_key"], "access_user_keys": ["user_set", "user_set_name", "user", "access_key"], "access_object_keys": ["object", "object_type_code", "object_sql_number", "object_id", "access_key", "access_key_value"], "access_set_keys": ["access_set", "access_set_name", "access_key"], }.get(area, []) aliases = { "group_id": "group", "group": "group", "user": "user", "user_set": "user_set", "object": "object", "object_id": "object_id", "object_type_code": "object_type_code", "access_set": "access_set", "set": "access_set", "key": "access_key", "access_key": "access_key", } for argument, column in aliases.items(): if column not in area_columns or payload.get(argument) in {None, ""}: continue filters.append(f"q.[{column}] = {access_sql_string_literal(payload.get(argument))}") if "object_sql_number" in area_columns and payload.get("object_sql_number") not in {None, ""}: try: filters.append(f"q.[object_sql_number] = {int(payload.get('object_sql_number'))}") except (TypeError, ValueError): pass name_query = str(payload.get("query") or payload.get("name") or "").strip() if name_query: name_columns = [column for column in area_columns if column.endswith("_name")] if name_columns: like_value = access_sql_string_literal(f"%{name_query}%") filters.append("(" + " OR ".join(f"q.[{column}] LIKE {like_value}" for column in name_columns) + ")") return filters def access_key_query_run_area( base_id: str, area: str, query: str, mapping: dict[str, str], payload: dict[str, Any], *, limit: int, offset: int, timeout_seconds: int, ) -> dict[str, Any]: filters = access_key_query_filters(payload, area) where = (" WHERE " + " AND ".join(filters)) if filters else "" order_column = { "access_group_keys": "group", "access_user_keys": "user_set", "access_object_keys": "object", "access_set_keys": "access_set", }.get(area, "access_key") paged_query = f""" SELECT * FROM ({query}) q {where} ORDER BY q.[{order_column}], q.[access_key] OFFSET {offset} ROWS FETCH NEXT {limit + 1} ROWS ONLY """ rows, error, truncated_by_fetch = access_rows_from_query(base_id, paged_query, limit=limit + 1, timeout_seconds=timeout_seconds) if error: return error truncated = truncated_by_fetch or len(rows) > limit rows = rows[:limit] mapped = [access_map_row(row, mapping) for row in rows] return { "area": area, "rows": mapped, "counts": {"rows": len(mapped), "limit": limit, "offset": offset, "truncated": bool(truncated)}, "filters": { key: payload.get(key) for key in ( "group", "group_id", "user", "user_set", "object_ref", "record_ref", "object", "object_id", "object_sql_number", "access_set", "set", "key", "access_key", "query", "name", ) if payload.get(key) not in {None, ""} }, } def access_keys_query(payload: dict[str, Any]) -> dict[str, Any]: method = str(payload.get("_method") or "access.keys.query") base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error limit, limit_error = parse_int_argument(payload, "limit", method=method, default=1000, minimum=1, maximum=20000) if limit_error: return limit_error offset, offset_error = parse_int_argument(payload, "offset", method=method, default=0, minimum=0, maximum=10000000) if offset_error: return offset_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1, maximum=120) if timeout_error: return timeout_error resolve_records, resolve_records_error = strict_bool_argument(payload, "resolve_records", method=method, default=False) if resolve_records_error: return resolve_records_error max_resolved_records, max_resolved_records_error = parse_int_argument(payload, "max_resolved_records", method=method, default=200, minimum=0, maximum=5000) if max_resolved_records_error: return max_resolved_records_error raw_kind = str(payload.get("kind") or payload.get("area") or "all").strip().casefold() if method == "access.object_keys.resolve" and raw_kind in {"", "all", "*"}: raw_kind = "object" areas = list(ACCESS_KEY_QUERY_AREAS.values()) if raw_kind in {"", "all", "*"} else [ACCESS_KEY_QUERY_AREAS.get(raw_kind, "")] areas = [area for area in dict.fromkeys(areas) if area] if not areas: return invalid_argument(method, "kind", "kind must be one of group, user_set, object, set, or all.", allowed_values=["group", "user_set", "object", "set", "all"]) selector_resolution: dict[str, Any] = {} if "access_object_keys" in areas: normalized_payload, selector_resolution, selector_error = normalize_access_object_record_filters( payload, method, base_id, timeout_seconds=int(timeout_seconds), ) if selector_error: return selector_error payload = normalized_payload queries, mappings, diagnostics = access_bsp_extractor_plan(base_id) if not queries or not mappings: return {"schema": "onec_access_keys_query.v1", "status": "error", "base_id": base_id, "diagnostics": (diagnostics or {}).get("diagnostics") or diagnostics} results: dict[str, Any] = {} for area in areas: result = access_key_query_run_area( base_id, area, queries[area], mappings.get(area) if isinstance(mappings.get(area), dict) else {}, payload, limit=int(limit), offset=int(offset), timeout_seconds=int(timeout_seconds), ) if result.get("status") in {"error", "rejected", "invalid_argument"} or result.get("error"): return result if resolve_records and area == "access_object_keys": resolution = access_resolve_object_key_records( base_id, result.get("rows") if isinstance(result.get("rows"), list) else [], timeout_seconds=int(timeout_seconds), max_records=int(max_resolved_records), ) result["rows"] = resolution.get("rows") or [] result["record_resolution"] = resolution.get("diagnostics") results[area] = result return { "schema": "onec_access_object_keys_resolve.v1" if method == "access.object_keys.resolve" else "onec_access_keys_query.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "preset": "bsp"}, "areas": results, "counts": {area: result.get("counts") for area, result in results.items()}, **({"selector_resolution": selector_resolution} if selector_resolution else {}), "diagnostics": diagnostics, } def access_object_keys_resolve(payload: dict[str, Any]) -> dict[str, Any]: return access_keys_query({**payload, "_method": "access.object_keys.resolve", "kind": "object", "resolve_records": True}) def access_object_explain(payload: dict[str, Any]) -> dict[str, Any]: method = "access.object.explain" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error if not any(payload.get(key) not in {None, ""} for key in ("object_ref", "record_ref", "object", "object_id", "access_key")): return invalid_argument( method, "record_ref", "Pass object_ref and/or record_ref, or a legacy raw object, object_id, or access_key filter.", ) limit, limit_error = parse_int_argument(payload, "limit", method=method, default=1000, minimum=1, maximum=20000) if limit_error: return limit_error subject_limit, subject_limit_error = parse_int_argument(payload, "subject_limit", method=method, default=1000, minimum=1, maximum=20000) if subject_limit_error: return subject_limit_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=120, minimum=1, maximum=120) if timeout_error: return timeout_error max_resolved_records, max_resolved_records_error = parse_int_argument(payload, "max_resolved_records", method=method, default=200, minimum=0, maximum=5000) if max_resolved_records_error: return max_resolved_records_error normalized_payload, selector_resolution, selector_error = normalize_access_object_record_filters( payload, method, base_id, timeout_seconds=int(timeout_seconds), ) if selector_error: return selector_error payload = normalized_payload queries, mappings, diagnostics = access_bsp_extractor_plan(base_id) if not queries or not mappings: return {"schema": "onec_access_object_explain.v1", "status": "error", "base_id": base_id, "diagnostics": (diagnostics or {}).get("diagnostics") or diagnostics} object_result = access_key_query_run_area( base_id, "access_object_keys", queries["access_object_keys"], mappings.get("access_object_keys") if isinstance(mappings.get("access_object_keys"), dict) else {}, payload, limit=int(limit), offset=int(payload.get("offset") if isinstance(payload.get("offset"), int) else 0), timeout_seconds=int(timeout_seconds), ) if object_result.get("error") or object_result.get("status") in {"error", "rejected", "invalid_argument"}: return object_result resolution = access_resolve_object_key_records( base_id, object_result.get("rows") if isinstance(object_result.get("rows"), list) else [], timeout_seconds=int(timeout_seconds), max_records=int(max_resolved_records), ) object_rows = resolution.get("rows") if isinstance(resolution.get("rows"), list) else [] access_key_ids = sorted({access_ref_tail(row.get("access_key")) for row in object_rows if row.get("access_key") not in {None, ""}}) subject_rows: dict[str, list[dict[str, Any]]] = {"access_group_keys": [], "access_user_keys": [], "access_set_keys": []} for access_key in access_key_ids[:100]: key_payload = {**payload, "access_key": access_key} for area in ("access_group_keys", "access_user_keys", "access_set_keys"): result = access_key_query_run_area( base_id, area, queries[area], mappings.get(area) if isinstance(mappings.get(area), dict) else {}, key_payload, limit=int(subject_limit), offset=0, timeout_seconds=int(timeout_seconds), ) if result.get("error") or result.get("status") in {"error", "rejected", "invalid_argument"}: return result subject_rows[area].extend(result.get("rows") if isinstance(result.get("rows"), list) else []) users_by_ref: dict[str, dict[str, Any]] = {} user_names_by_ref: dict[str, str] = {} for row in subject_rows["access_user_keys"]: user_ref = str(row.get("user") or "").strip() user_name = str(row.get("user_set_name") or "").strip() if user_ref and user_name: user_names_by_ref[access_ref_tail(user_ref)] = user_name if user_ref: users_by_ref[access_ref_tail(user_ref)] = {"id": user_ref, "name": user_name or user_ref, "source": "access_user_key", "access_key": row.get("access_key")} group_ids = sorted({access_ref_tail(row.get("group") or row.get("group_ref")) for row in subject_rows["access_group_keys"] if row.get("group") or row.get("group_ref")}) if group_ids: rows_by_area: dict[str, list[dict[str, Any]]] = {"groups": [], "group_users": [], "user_group_members": [], "users": []} for area in ("users", "groups", "group_users", "user_group_members"): rows, error, _ = access_rows_from_query(base_id, queries[area], limit=20000, timeout_seconds=int(timeout_seconds)) if error: return error rows_by_area[area] = rows mini_access = access_snapshot_from_extractor_rows(rows_by_area, mappings) groups_by_id = {access_ref_tail(group.get("id")): group for group in mini_access.get("groups") or [] if isinstance(group, dict)} users_by_id = {access_ref_tail(user.get("id")): user for user in mini_access.get("users") or [] if isinstance(user, dict)} wanted_user_refs = {access_ref_tail(user_ref) for group_id in group_ids for user_ref in (groups_by_id.get(group_id) or {}).get("users") or []} user_names_by_ref.update(access_resolve_user_names(base_id, wanted_user_refs, diagnostics, timeout_seconds=int(timeout_seconds), max_refs=int(subject_limit))) if wanted_user_refs and "access_user_keys" in queries: name_rows, name_error, _ = access_rows_from_query(base_id, queries["access_user_keys"], limit=20000, timeout_seconds=int(timeout_seconds)) if name_error: return name_error name_mapping = mappings.get("access_user_keys") if isinstance(mappings.get("access_user_keys"), dict) else {} for name_row in name_rows: mapped_name_row = access_map_row(name_row, name_mapping) user_ref = access_ref_tail(mapped_name_row.get("user")) user_name = str(mapped_name_row.get("user_set_name") or "").strip() if user_ref in wanted_user_refs and user_name: user_names_by_ref.setdefault(user_ref, user_name) for group_id in group_ids: group = groups_by_id.get(group_id) or {} for user_ref in group.get("users") or []: user = users_by_id.get(access_ref_tail(user_ref)) or {"id": user_ref, "name": user_ref} user_ref_tail = access_ref_tail(user_ref) user_name = user_names_by_ref.get(user_ref_tail) or user.get("name") or user_ref users_by_ref.setdefault(user_ref_tail, {"id": user.get("id") or user_ref, "name": user_name, "source": "access_group_key", "group": group.get("id") or group_id}) groups = sorted(subject_rows["access_group_keys"], key=lambda item: (str(item.get("group_name") or ""), str(item.get("group") or "")))[:subject_limit] user_sets = sorted(subject_rows["access_user_keys"], key=lambda item: (str(item.get("user_set_name") or ""), str(item.get("user") or "")))[:subject_limit] users = sorted(users_by_ref.values(), key=lambda item: str(item.get("name") or item.get("id")))[:subject_limit] summary = { "text": ( f"Object access explanation: {len(object_rows)} object key rows, " f"{len(access_key_ids)} access keys, {len(groups)} group sources, " f"{len(user_sets)} user-set sources, {len(users)} users resolved." ) } return { "schema": "onec_access_object_explain.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "preset": "bsp"}, "query": { key: payload.get(key) for key in ("object_ref", "record_ref", "object", "object_id", "object_sql_number", "access_key") if payload.get(key) not in {None, ""} }, **({"selector_resolution": selector_resolution} if selector_resolution else {}), "summary": summary, "object_keys": object_rows, "access_keys": access_key_ids, "groups": groups, "user_sets": user_sets, "users": users, "counts": { "object_keys": len(object_rows), "access_keys": len(access_key_ids), "groups": len(groups), "user_sets": len(user_sets), "users": len(users), }, "diagnostics": {"object_key_counts": object_result.get("counts"), "record_resolution": resolution.get("diagnostics"), "bsp": diagnostics}, } ACCESS_PERMISSION_ACTION_KEYS = { "просмотр": "read", "read": "read", "чтение": "read", "чтениебезограничения": "read", "view": "read", "добавление": "insert", "добавлениебезограничения": "insert", "insert": "insert", "create": "insert", "изменение": "update", "изменениебезограничения": "update", "update": "update", "write": "update", "запись": "write", "записи": "write", "edit": "update", "удаление": "delete", "удалениебезограничения": "delete", "delete": "delete", "remove": "delete", } def access_permission_action_key(value: Any) -> str: compact = re.sub(r"[^0-9a-zа-яё]+", "", str(value or "").strip().casefold()) return ACCESS_PERMISSION_ACTION_KEYS.get(compact, compact) def access_permission_is_unrestricted(value: Any) -> bool: return "безогранич" in str(value or "").strip().casefold() def access_action_filter_keys(value: Any) -> set[str]: key = access_permission_action_key(value) if not key: return set() if key == "write": return {"insert", "update"} return {key} def access_permission_matches_action(permission: dict[str, Any], filter_keys: set[str]) -> bool: if not filter_keys: return True return bool(set(access_permission_action_keys(permission)) & filter_keys) def access_permission_action_keys(permission: dict[str, Any]) -> list[str]: keys: list[str] = [] raw_actions = access_list(permission.get("actions")) if permission.get("actions") not in {None, ""} else access_list(permission.get("action")) for action in raw_actions: key = access_permission_action_key(action) if key and key not in keys: keys.append(key) return keys def access_permission_rights(permissions: list[dict[str, Any]]) -> dict[str, bool]: rights = {"read": False, "insert": False, "update": False, "delete": False} for permission in permissions: for key in access_permission_action_keys(permission): if key in rights: rights[key] = True return rights def access_permission_rights_detail(permissions: list[dict[str, Any]]) -> dict[str, dict[str, bool]]: detail = { "read": {"allowed": False, "unrestricted": False}, "insert": {"allowed": False, "unrestricted": False}, "update": {"allowed": False, "unrestricted": False}, "delete": {"allowed": False, "unrestricted": False}, } for permission in permissions: raw_actions = access_list(permission.get("actions")) if permission.get("actions") not in {None, ""} else access_list(permission.get("action")) for action in raw_actions: key = access_permission_action_key(action) if key not in detail: continue detail[key]["allowed"] = True if access_permission_is_unrestricted(action): detail[key]["unrestricted"] = True return detail def access_object_selector_from_card(payload: dict[str, Any], object_kind: str | None, object_card: dict[str, Any] | None) -> tuple[str, dict[str, Any]]: object_name = str((object_card or {}).get("name") or payload.get("name") or payload.get("object_name") or "").strip() object_synonym = str((object_card or {}).get("synonym") or "").strip() object_kind_ru = str((object_card or {}).get("kind_ru") or RU_KIND.get(str(object_kind or ""), object_kind or "") or "").strip() object_selector = ( object_selector_ref(object_kind, object_name) or str(payload.get("ref") or "").strip() or str(payload.get("name") or payload.get("object_name") or "").strip() ) object_payload = { "guid": (object_card or {}).get("guid"), "kind": object_kind, "kind_ru": object_kind_ru or None, "name": object_name or None, "synonym": object_synonym or None, "ref": object_selector or None, } return object_selector, object_payload def access_object_permission_candidates(permission: dict[str, Any]) -> set[str]: candidates: set[str] = set() def add(value: Any) -> None: text = str(value or "").strip() if text: candidates.add(text.casefold()) add(permission.get("object")) add(access_ref_tail(permission.get("object"))) add(permission.get("object_name")) add(permission.get("object_full_name")) resolution = permission.get("object_resolution") if isinstance(permission.get("object_resolution"), dict) else {} for key in ("guid", "name", "synonym", "full_name", "presentation"): add(resolution.get(key)) object_name = str(permission.get("object_name") or resolution.get("name") or "").strip() for kind_key in ("kind", "kind_ru", "public_kind"): object_kind = str(permission.get("object_kind") or resolution.get(kind_key) or "").strip() if object_kind and object_name: add(f"{object_kind}.{object_name}") return candidates def access_object_match_candidates(selector: str, object_payload: dict[str, Any] | None = None) -> set[str]: candidates: set[str] = set() def add(value: Any) -> None: text = str(value or "").strip() if text: candidates.add(text.casefold()) object_payload = object_payload if isinstance(object_payload, dict) else {} add(selector) add(access_ref_tail(selector)) for key in ("guid", "name", "synonym", "ref", "full_name", "presentation"): add(object_payload.get(key)) object_name = str(object_payload.get("name") or "").strip() object_synonym = str(object_payload.get("synonym") or "").strip() for kind_key in ("kind", "kind_ru", "public_kind"): object_kind = str(object_payload.get(kind_key) or "").strip() for name in (object_name, object_synonym): if object_kind and name: add(f"{object_kind}.{name}") add(f"{name} ({object_kind})") return candidates def access_object_role_permission_search_terms(object_payload: dict[str, Any]) -> list[str]: terms: list[str] = [] for key in ("synonym", "name"): value = str(object_payload.get(key) or "").strip() if len(value) >= 3 and value not in terms: terms.append(value) return terms def access_object_roles_fast_permissions( base_id: str, *, object_payload: dict[str, Any], object_selector: str, object_guid: str | None, action_filter_keys: set[str], timeout_seconds: int, limit: int, ) -> dict[str, Any]: queries, mappings, diagnostics = access_bsp_extractor_plan(base_id) if not queries or "role_permissions" not in queries: return {"status": "error", "diagnostics": diagnostics or {"message": "BSP role_permissions extractor is unavailable."}} terms = access_object_role_permission_search_terms(object_payload) if not terms: return {"status": "not_found", "terms": [], "roles": [], "diagnostics": diagnostics} like_filters = " OR ".join(f"q.[object_name] LIKE {access_sql_string_literal('%' + term + '%')}" for term in terms) query = f"SELECT * FROM ({queries['role_permissions']}) q WHERE ({like_filters})" rows, error, truncated = access_rows_from_query(base_id, query, limit=max(limit, 20000), timeout_seconds=timeout_seconds) if error: return {"status": "error", "diagnostics": error.get("diagnostics") or error} role_mapping = mappings.get("role_permissions") if isinstance(mappings.get("role_permissions"), dict) else None matched_roles_by_tail: dict[str, dict[str, Any]] = {} for row in rows: mapped = access_map_row(row, role_mapping) permission = { "object": mapped.get("object") or mapped.get("object_ref") or "*", **({"object_name": mapped.get("object_name")} if mapped.get("object_name") not in {None, ""} else {}), **({"actions": mapped.get("actions")} if mapped.get("actions") not in {None, ""} else {"action": mapped.get("action") or mapped.get("right") or "*"}), **({"source_field": mapped.get("source_field")} if mapped.get("source_field") not in {None, ""} else {}), **({"source_fields": mapped.get("source_fields")} if mapped.get("source_fields") not in {None, ""} else {}), } if not access_permission_matches_object(permission, selector=object_selector, object_guid=object_guid, object_payload=object_payload): continue if not access_permission_matches_action(permission, action_filter_keys): continue role_id = str(mapped.get("role") or mapped.get("role_id") or "") role_tail = access_ref_tail(role_id) if not role_tail: continue role = matched_roles_by_tail.setdefault( role_tail, { "id": role_id, "name": mapped.get("role_name") or role_id, "permissions": [], }, ) role["permissions"].append(permission) for role in matched_roles_by_tail.values(): permissions = [item for item in role.get("permissions") or [] if isinstance(item, dict)] role["rights"] = access_permission_rights(permissions) role["rights_detail"] = access_permission_rights_detail(permissions) role["source"] = "role_permissions_filtered" roles = sorted(matched_roles_by_tail.values(), key=lambda item: (str(item.get("name") or item.get("id")), str(item.get("id") or ""))) return { "status": "ok" if roles else "not_found", "roles": roles, "terms": terms, "truncated": truncated, "counts": {"rows": len(rows), "roles": len(roles), "permissions": sum(len(role.get("permissions") or []) for role in roles)}, "diagnostics": diagnostics, } def access_object_subjects_fast_chain( base_id: str, *, matched_roles: list[dict[str, Any]], timeout_seconds: int, limit: int, scope_subject_limit: int | None = None, ) -> dict[str, Any]: queries, mappings, diagnostics = access_bsp_extractor_plan(base_id) if not queries or not mappings: return {"status": "error", "diagnostics": (diagnostics or {}).get("diagnostics") or diagnostics} if not matched_roles: return {"status": "not_found", "roles": [], "profiles": [], "groups": [], "users": [], "counts": {"roles": 0, "profiles": 0, "groups": 0, "users": 0}} rows_by_area: dict[str, list[dict[str, Any]]] = {} extractor_counts: dict[str, Any] = {} for area in ("profiles", "profile_roles", "group_profiles", "groups", "group_users", "user_group_members", "users"): query = queries.get(area) if not query: rows_by_area[area] = [] extractor_counts[area] = {"rows": 0, "truncated": False, "skipped": True} continue rows, error, truncated = access_rows_from_query(base_id, query, limit=20000, timeout_seconds=int(timeout_seconds)) if error: return {"status": "error", "diagnostics": error.get("diagnostics") or error} rows_by_area[area] = rows extractor_counts[area] = {"rows": len(rows), "truncated": bool(truncated)} access = access_snapshot_from_extractor_rows(rows_by_area, mappings) role_tails = {access_ref_tail(role.get("id")) for role in matched_roles if access_ref_tail(role.get("id"))} profiles_by_id = {access_ref_tail(profile.get("id")): profile for profile in access.get("profiles") or [] if isinstance(profile, dict)} groups_by_id = {access_ref_tail(group.get("id")): group for group in access.get("groups") or [] if isinstance(group, dict)} users_by_id = {access_ref_tail(user.get("id")): user for user in access.get("users") or [] if isinstance(user, dict)} matched_profile_ids = { access_ref_tail(profile.get("id")) for profile in profiles_by_id.values() if {access_ref_tail(role_id) for role_id in profile.get("roles") or []} & role_tails } matched_group_ids = { access_ref_tail(group.get("id")) for group in groups_by_id.values() if {access_ref_tail(profile_id) for profile_id in group.get("profiles") or []} & matched_profile_ids } matched_group_ids.update( access_ref_tail(group.get("id")) for group in groups_by_id.values() if {access_ref_tail(role_id) for role_id in group.get("roles") or []} & role_tails ) matched_user_ids: set[str] = set() matched_user_ids.update( access_ref_tail(user.get("id")) for user in users_by_id.values() if {access_ref_tail(role_id) for role_id in user.get("roles") or []} & role_tails ) for group_id in matched_group_ids: group = groups_by_id.get(group_id) or {} for user_id in group.get("users") or []: user_tail = access_ref_tail(user_id) if user_tail: matched_user_ids.add(user_tail) matched_profiles = sorted( [profiles_by_id[profile_id] for profile_id in matched_profile_ids if profile_id in profiles_by_id], key=lambda item: str(item.get("name") or item.get("id")), ) matched_groups = sorted( [groups_by_id[group_id] for group_id in matched_group_ids if group_id in groups_by_id], key=lambda item: str(item.get("name") or item.get("id")), ) matched_roles_by_tail = {access_ref_tail(role.get("id")): role for role in matched_roles if access_ref_tail(role.get("id"))} role_names_by_tail = {role_tail: role.get("name") or role.get("id") for role_tail, role in matched_roles_by_tail.items()} matched_users: list[dict[str, Any]] = [] for user_id in matched_user_ids: user = users_by_id.get(user_id) or {"id": user_id, "name": user_id} user_groups: list[dict[str, Any]] = [] role_sources: list[dict[str, Any]] = [] user_role_tails: set[str] = set() for role_id in user.get("roles") or []: role_tail = access_ref_tail(role_id) if role_tail not in role_tails: continue source = { "type": "direct_user_role", "user": user.get("id") or user_id, "role": role_id, "role_name": role_names_by_tail.get(role_tail) or role_id, } if source not in role_sources: role_sources.append(source) user_role_tails.add(role_tail) for group_id in sorted(matched_group_ids): group = groups_by_id.get(group_id) or {} if user_id not in {access_ref_tail(item) for item in group.get("users") or []}: continue group_item = {"id": group.get("id") or group_id, "name": group.get("name") or group_id} if group_item not in user_groups: user_groups.append(group_item) for role_id in group.get("roles") or []: role_tail = access_ref_tail(role_id) if role_tail not in role_tails: continue source = { "type": "group_role", "group": group.get("id") or group_id, "group_name": group.get("name") or group_id, "role": role_id, "role_name": role_names_by_tail.get(role_tail) or role_id, } if source not in role_sources: role_sources.append(source) user_role_tails.add(role_tail) for profile_id in group.get("profiles") or []: profile_tail = access_ref_tail(profile_id) if profile_tail not in matched_profile_ids: continue profile = profiles_by_id.get(profile_tail) or {"id": profile_id, "name": profile_id} for role_id in profile.get("roles") or []: role_tail = access_ref_tail(role_id) if role_tail not in role_tails: continue source = { "type": "group_profile_role", "group": group.get("id") or group_id, "group_name": group.get("name") or group_id, "profile": profile.get("id") or profile_id, "profile_name": profile.get("name") or profile_id, "role": role_id, "role_name": role_names_by_tail.get(role_tail) or role_id, } if source not in role_sources: role_sources.append(source) user_role_tails.add(role_tail) user_permissions: list[dict[str, Any]] = [] user_roles: list[dict[str, Any]] = [] for role_tail in sorted(user_role_tails): role = matched_roles_by_tail.get(role_tail) if not role: continue user_roles.append({"id": role.get("id"), "name": role.get("name")}) for permission in role.get("permissions") or []: if isinstance(permission, dict) and permission not in user_permissions: user_permissions.append(permission) matched_users.append( { "id": user.get("id") or user_id, "name": user.get("name") or user_id, "active": user.get("active"), "marked": user.get("marked"), "user_type": user.get("user_type"), "user": user, "groups": user_groups, "roles": user_roles, "rights": access_permission_rights(user_permissions), "rights_detail": access_permission_rights_detail(user_permissions), "permissions": user_permissions, "role_sources": role_sources, } ) matched_users = sorted(matched_users, key=lambda item: str(item.get("name") or item.get("id"))) scope_limit = int(scope_subject_limit) if scope_subject_limit is not None else max(len(matched_groups), len(matched_users)) return { "status": "ok" if matched_roles or matched_users else "not_found", "roles": matched_roles[: int(limit)], "profiles": matched_profiles[: int(limit)], "groups": matched_groups[: int(limit)], "users": matched_users[: int(limit)], "_scope_groups": matched_groups[:scope_limit], "_scope_users": matched_users[:scope_limit], "counts": {"roles": len(matched_roles), "profiles": len(matched_profiles), "groups": len(matched_groups), "users": len(matched_user_ids)}, "diagnostics": {"extractors": extractor_counts, "bsp": diagnostics}, } def access_subject_access_key_scope( base_id: str, *, groups: list[dict[str, Any]], users: list[dict[str, Any]], total_groups: int | None = None, total_users: int | None = None, timeout_seconds: int, limit: int = 20000, sample_limit: int = 20, ) -> dict[str, Any]: queries, mappings, diagnostics = access_bsp_extractor_plan(base_id) if not queries or not mappings: return {"status": "error", "diagnostics": (diagnostics or {}).get("diagnostics") or diagnostics} group_ids = sorted({access_ref_tail(group.get("id")) for group in groups if isinstance(group, dict) and access_ref_tail(group.get("id"))}) user_ids = sorted({access_ref_tail(user.get("id") or ((user.get("user") or {}).get("id") if isinstance(user.get("user"), dict) else None)) for user in users if isinstance(user, dict) and access_ref_tail(user.get("id") or ((user.get("user") or {}).get("id") if isinstance(user.get("user"), dict) else None))}) rows: dict[str, list[dict[str, Any]]] = {"access_group_keys": [], "access_user_keys": []} extractor_counts: dict[str, Any] = {} if group_ids and queries.get("access_group_keys"): group_literals = ", ".join(access_sql_string_literal(group_id) for group_id in group_ids) group_ref_filters = " OR ".join(f"q.[group_ref] LIKE {access_sql_string_literal('%' + group_id)}" for group_id in group_ids) group_query = ( f"SELECT * FROM ({queries['access_group_keys']}) q " f"WHERE q.[group] IN ({group_literals}) OR " + group_ref_filters ) group_rows, error, truncated = access_rows_from_query(base_id, group_query, limit=limit, timeout_seconds=timeout_seconds) if error: return {"status": "error", "diagnostics": error.get("diagnostics") or error} mapping = mappings.get("access_group_keys") if isinstance(mappings.get("access_group_keys"), dict) else None rows["access_group_keys"] = [access_map_row(row, mapping) for row in group_rows] extractor_counts["access_group_keys"] = {"rows": len(group_rows), "limit": limit, "truncated": bool(truncated)} if user_ids and queries.get("access_user_keys"): user_like = " OR ".join(f"q.[user] LIKE {access_sql_string_literal('%' + user_id)}" for user_id in user_ids) user_query = f"SELECT * FROM ({queries['access_user_keys']}) q WHERE {user_like}" user_rows, error, truncated = access_rows_from_query(base_id, user_query, limit=limit, timeout_seconds=timeout_seconds) if error: return {"status": "error", "diagnostics": error.get("diagnostics") or error} mapping = mappings.get("access_user_keys") if isinstance(mappings.get("access_user_keys"), dict) else None rows["access_user_keys"] = [access_map_row(row, mapping) for row in user_rows] extractor_counts["access_user_keys"] = {"rows": len(user_rows), "limit": limit, "truncated": bool(truncated)} access_key_ids = sorted( { access_ref_tail(item.get("access_key")) for item in [*rows["access_group_keys"], *rows["access_user_keys"]] if item.get("access_key") not in {None, ""} } ) total_groups = len(group_ids) if total_groups is None else int(total_groups) total_users = len(user_ids) if total_users is None else int(total_users) coverage = { "groups_checked": len(group_ids), "groups_total": total_groups, "users_checked": len(user_ids), "users_total": total_users, "complete": len(group_ids) >= total_groups and len(user_ids) >= total_users, } return { "status": "ok", "kind": "subject_access_keys", "note": "BSP access keys restrict data records/dimensions for subjects; they are not metadata-object role permissions.", "counts": { "groups_checked": len(group_ids), "users_checked": len(user_ids), "group_key_rows": len(rows["access_group_keys"]), "user_key_rows": len(rows["access_user_keys"]), "subject_access_keys": len(access_key_ids), }, "coverage": coverage, "samples": { "group_keys": rows["access_group_keys"][:sample_limit], "user_keys": rows["access_user_keys"][:sample_limit], "access_keys": access_key_ids[:sample_limit], }, "truncated": any(bool(item.get("truncated")) for item in extractor_counts.values() if isinstance(item, dict)), "diagnostics": {"extractors": extractor_counts, "bsp": diagnostics}, } def access_permission_matches_object( permission: dict[str, Any], *, selector: str, object_guid: str | None = None, object_payload: dict[str, Any] | None = None, ) -> bool: selector_text = str(selector or "").strip().casefold() if not selector_text and not object_guid: return False resolution = permission.get("object_resolution") if isinstance(permission.get("object_resolution"), dict) else {} if object_guid: if str(resolution.get("guid") or "").strip().lower() == object_guid: return True selector_tail = access_ref_tail(selector) object_match_candidates = access_object_match_candidates(selector, object_payload) object_match_normalized = {normalize(candidate) for candidate in object_match_candidates if normalize(candidate)} for candidate in access_object_permission_candidates(permission): candidate_normalized = normalize(candidate) if object_guid and candidate == object_guid: return True if selector_text and candidate == selector_text: return True if selector_text and "." in selector_text and candidate.endswith(f".{selector_text.split('.', 1)[1]}"): return True if selector_text and selector_text in candidate: return True if selector_tail and candidate == selector_tail.casefold(): return True for object_candidate in object_match_candidates: if object_candidate and (candidate == object_candidate or object_candidate in candidate or candidate in object_candidate): return True for object_candidate in object_match_normalized: if object_candidate and candidate_normalized and (candidate_normalized == object_candidate or object_candidate in candidate_normalized or candidate_normalized in object_candidate): return True return False def access_object_roles(payload: dict[str, Any]) -> dict[str, Any]: method = "access.object.roles" normalized_payload = normalize_object_selector_aliases(payload, method) if isinstance(normalized_payload, dict) and normalized_payload.get("status") == "invalid_argument": return normalized_payload base_id_or_error = require_base_id(normalized_payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error if not has_object_selector(normalized_payload): return invalid_argument(method, "object", OBJECT_SELECTOR_REQUIRED_MESSAGE_NO_ORDINAL) limit, limit_error = parse_int_argument(normalized_payload, "limit", method=method, default=200, minimum=1, maximum=20000) if limit_error: return limit_error timeout_seconds, timeout_error = parse_int_argument(normalized_payload, "timeout_seconds", method=method, default=120, minimum=1, maximum=120) if timeout_error: return timeout_error max_permissions, max_permissions_error = parse_int_argument(normalized_payload, "max_effective_permissions_per_user", method=method, default=0, minimum=0, maximum=200000) if max_permissions_error: return max_permissions_error action_filter = str(normalized_payload.get("action") or normalized_payload.get("right") or "").strip() action_filter_key = access_permission_action_key(action_filter) if action_filter else "" action_filter_keys = access_action_filter_keys(action_filter_key) object_guid, object_kind, object_card, object_error = resolve_object_guid( normalized_payload, base_id, timeout_seconds=int(timeout_seconds), method=method, ) if object_error: return object_error object_selector, object_payload = access_object_selector_from_card(normalized_payload, object_kind, object_card) object_payload["guid"] = object_guid fast_roles = access_object_roles_fast_permissions( base_id, object_payload=object_payload, object_selector=object_selector, object_guid=object_guid, action_filter_keys=action_filter_keys, timeout_seconds=int(timeout_seconds), limit=int(limit), ) if fast_roles.get("status") in {"ok", "not_found"} and int(max_permissions) <= 0: matched_roles = list(fast_roles.get("roles") or [])[: int(limit)] query_payload = {key: normalized_payload.get(key) for key in ("ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "action") if normalized_payload.get(key) not in {None, ""}} summary_text = ( f"Object roles: {len(matched_roles)} roles matched" f"{' for action ' + action_filter_key if action_filter_key else ''}." ) return { "schema": "onec_access_object_roles.v1", "status": "ok" if matched_roles else "not_found", "base_id": base_id, "source": {"kind": "live_sql", "preset": "bsp", "extraction": "role_permissions_filtered"}, "query": query_payload, "object": object_payload, "summary": {"text": summary_text}, "roles": matched_roles, "counts": { "roles": len(matched_roles), "permissions": sum(len(role.get("permissions") or []) for role in matched_roles), }, "extraction_counts": fast_roles.get("counts"), "diagnostics": {"fast_role_permissions": {key: fast_roles.get(key) for key in ("status", "terms", "truncated", "counts")}, "bsp": fast_roles.get("diagnostics")}, } extracted = access_snapshot_extract( { "base_id": base_id, "preset": "bsp", "limit": 20000, "timeout_seconds": int(timeout_seconds), "resolve_identifiers": True, "max_effective_permissions_per_user": int(max_permissions), } ) if extracted.get("status") != "ok": return {"schema": "onec_access_object_roles.v1", **extracted} access = extracted.get("access") if isinstance(extracted.get("access"), dict) else {} graph = extracted.get("graph") if isinstance(extracted.get("graph"), dict) else {} roles = [role for role in access.get("roles") or [] if isinstance(role, dict)] roles_by_tail: dict[str, dict[str, Any]] = { access_ref_tail(item.get("id")): item for item in roles if access_ref_tail(item.get("id")) } matched_roles_by_tail: dict[str, dict[str, Any]] = {} for role in roles: matched_permissions: list[dict[str, Any]] = [] for permission in access_list(role.get("permissions")): if not isinstance(permission, dict): continue if not access_permission_matches_object(permission, selector=object_selector, object_guid=object_guid, object_payload=object_payload): continue if not access_permission_matches_action(permission, action_filter_keys): continue matched_permissions.append(permission) if not matched_permissions: continue rights = access_permission_rights(matched_permissions) role_tail = access_ref_tail(role.get("id")) matched_roles_by_tail[role_tail] = { "id": role.get("id"), "name": role.get("name") or ((role.get("resolution") or {}).get("name") if isinstance(role.get("resolution"), dict) else None) or role.get("id"), **({"resolution": role.get("resolution")} if isinstance(role.get("resolution"), dict) else {}), "rights": rights, "rights_detail": access_permission_rights_detail(matched_permissions), "permissions": matched_permissions, } role_permissions_from_users: dict[str, list[dict[str, Any]]] = {} role_permission_keys_from_users: dict[str, set[str]] = {} for effective in graph.get("effective_users") or []: if not isinstance(effective, dict): continue for role in effective.get("roles") or []: if isinstance(role, dict) and access_ref_tail(role.get("id")): roles_by_tail.setdefault(access_ref_tail(role.get("id")), role) for permission in effective.get("permissions") or []: if not isinstance(permission, dict): continue if not access_permission_matches_object(permission, selector=object_selector, object_guid=object_guid, object_payload=object_payload): continue if not access_permission_matches_action(permission, action_filter_keys): continue for source in permission.get("sources") or []: if not isinstance(source, dict): continue role_tail = access_ref_tail(source.get("role")) if not role_tail: continue role_permissions = role_permissions_from_users.setdefault(role_tail, []) permission_key = json.dumps(permission, ensure_ascii=False, sort_keys=True, default=str) permission_keys = role_permission_keys_from_users.setdefault(role_tail, set()) if permission_key not in permission_keys: role_permissions.append(permission) permission_keys.add(permission_key) for role_tail, permissions in role_permissions_from_users.items(): if role_tail in matched_roles_by_tail or not permissions: continue role = roles_by_tail.get(role_tail) or {"id": role_tail, "name": role_tail} matched_roles_by_tail[role_tail] = { "id": role.get("id") or role_tail, "name": role.get("name") or ((role.get("resolution") or {}).get("name") if isinstance(role.get("resolution"), dict) else None) or role.get("id") or role_tail, **({"resolution": role.get("resolution")} if isinstance(role.get("resolution"), dict) else {}), "rights": access_permission_rights(permissions), "rights_detail": access_permission_rights_detail(permissions), "permissions": permissions, "source": "effective_permissions", } matched_roles = sorted(matched_roles_by_tail.values(), key=lambda item: (str(item.get("name") or item.get("id")), str(item.get("id") or "")))[: int(limit)] query_payload = {key: normalized_payload.get(key) for key in ("ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "action") if normalized_payload.get(key) not in {None, ""}} summary_text = ( f"Object roles: {len(matched_roles)} roles matched" f"{' for action ' + action_filter_key if action_filter_key else ''}." ) return { "schema": "onec_access_object_roles.v1", "status": "ok" if matched_roles else "not_found", "base_id": base_id, "source": {"kind": "live_sql", "preset": "bsp", "extraction": "access.snapshot.extract"}, "query": query_payload, "object": object_payload, "summary": {"text": summary_text}, "roles": matched_roles, "counts": { "roles": len(matched_roles), "permissions": sum(len(role.get("permissions") or []) for role in matched_roles), }, "extraction_counts": extracted.get("counts"), "diagnostics": extracted.get("diagnostics"), } def access_object_subjects(payload: dict[str, Any]) -> dict[str, Any]: method = "access.object.subjects" normalized_payload = normalize_object_selector_aliases(payload, method) if isinstance(normalized_payload, dict) and normalized_payload.get("status") == "invalid_argument": return normalized_payload base_id_or_error = require_base_id(normalized_payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error if not has_object_selector(normalized_payload): return invalid_argument(method, "object", OBJECT_SELECTOR_REQUIRED_MESSAGE_NO_ORDINAL) limit, limit_error = parse_int_argument(normalized_payload, "limit", method=method, default=1000, minimum=1, maximum=20000) if limit_error: return limit_error timeout_seconds, timeout_error = parse_int_argument(normalized_payload, "timeout_seconds", method=method, default=120, minimum=1, maximum=120) if timeout_error: return timeout_error max_permissions, max_permissions_error = parse_int_argument(normalized_payload, "max_effective_permissions_per_user", method=method, default=0, minimum=0, maximum=200000) if max_permissions_error: return max_permissions_error include_access_key_scope = bool(normalized_payload.get("include_access_key_scope")) access_key_scope_limit, access_key_scope_limit_error = parse_int_argument(normalized_payload, "access_key_scope_limit", method=method, default=20000, minimum=1, maximum=200000) if access_key_scope_limit_error: return access_key_scope_limit_error access_key_scope_subject_limit, access_key_scope_subject_limit_error = parse_int_argument( normalized_payload, "access_key_scope_subject_limit", method=method, default=20000, minimum=1, maximum=200000, ) if access_key_scope_subject_limit_error: return access_key_scope_subject_limit_error action_filter = str(normalized_payload.get("action") or normalized_payload.get("right") or "").strip() action_filter_keys = access_action_filter_keys(action_filter) object_guid, object_kind, object_card, object_error = resolve_object_guid( normalized_payload, base_id, timeout_seconds=int(timeout_seconds), method=method, ) if object_error: return object_error object_selector, object_payload = access_object_selector_from_card(normalized_payload, object_kind, object_card) object_payload["guid"] = object_guid fast_roles = access_object_roles_fast_permissions( base_id, object_payload=object_payload, object_selector=object_selector, object_guid=object_guid, action_filter_keys=action_filter_keys, timeout_seconds=int(timeout_seconds), limit=20000, ) if fast_roles.get("status") in {"ok", "not_found"} and int(max_permissions) <= 0: fast_subjects = access_object_subjects_fast_chain( base_id, matched_roles=[role for role in fast_roles.get("roles") or [] if isinstance(role, dict)], timeout_seconds=int(timeout_seconds), limit=int(limit), scope_subject_limit=int(access_key_scope_subject_limit), ) query_payload = {key: normalized_payload.get(key) for key in ("ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "action") if normalized_payload.get(key) not in {None, ""}} roles = [role for role in fast_subjects.get("roles") or [] if isinstance(role, dict)] profiles = [profile for profile in fast_subjects.get("profiles") or [] if isinstance(profile, dict)] groups = [group for group in fast_subjects.get("groups") or [] if isinstance(group, dict)] users = [user for user in fast_subjects.get("users") or [] if isinstance(user, dict)] scope_groups = [group for group in fast_subjects.get("_scope_groups") or groups if isinstance(group, dict)] scope_users = [user for user in fast_subjects.get("_scope_users") or users if isinstance(user, dict)] fast_subject_counts = fast_subjects.get("counts") if isinstance(fast_subjects.get("counts"), dict) else {} access_key_scope = ( access_subject_access_key_scope( base_id, groups=scope_groups, users=scope_users, total_groups=int(fast_subject_counts.get("groups") or len(groups)), total_users=int(fast_subject_counts.get("users") or len(users)), timeout_seconds=int(timeout_seconds), limit=int(access_key_scope_limit), ) if include_access_key_scope else None ) summary_text = ( f"Object subjects: {len(roles)} roles, {len(profiles)} profiles, " f"{len(groups)} groups, {len(users)} users" f"{' for action ' + access_permission_action_key(action_filter) if action_filter else ''}." ) return { "schema": "onec_access_object_subjects.v1", "status": "ok" if roles or users else "not_found", "base_id": base_id, "source": {"kind": "live_sql", "preset": "bsp", "extraction": "role_profile_group_user_chain"}, "query": query_payload, "object": object_payload, "summary": {"text": summary_text}, "roles": roles, "profiles": profiles, "groups": groups, "users": users, **({"access_key_scope": access_key_scope} if access_key_scope is not None else {}), "counts": fast_subjects.get("counts") or {"roles": len(roles), "profiles": len(profiles), "groups": len(groups), "users": len(users)}, "extraction_counts": {"role_permissions": fast_roles.get("counts"), "subjects": fast_subjects.get("counts")}, "diagnostics": {"fast_role_permissions": {key: fast_roles.get(key) for key in ("status", "terms", "truncated", "counts")}, "fast_subjects": fast_subjects.get("diagnostics")}, } extracted = access_snapshot_extract( { "base_id": base_id, "preset": "bsp", "limit": 20000, "timeout_seconds": int(timeout_seconds), "resolve_identifiers": True, "max_effective_permissions_per_user": int(max_permissions), } ) if extracted.get("status") != "ok": return {"schema": "onec_access_object_subjects.v1", **extracted} access = extracted.get("access") if isinstance(extracted.get("access"), dict) else {} graph = extracted.get("graph") if isinstance(extracted.get("graph"), dict) else {} matched_roles: dict[str, dict[str, Any]] = {} roles_by_tail: dict[str, dict[str, Any]] = { access_ref_tail(item.get("id")): item for item in access.get("roles") or [] if isinstance(item, dict) and access_ref_tail(item.get("id")) } for role in [item for item in access.get("roles") or [] if isinstance(item, dict)]: matched_permissions: list[dict[str, Any]] = [] for permission in access_list(role.get("permissions")): if not isinstance(permission, dict): continue if not access_permission_matches_object(permission, selector=object_selector, object_guid=object_guid, object_payload=object_payload): continue if not access_permission_matches_action(permission, action_filter_keys): continue matched_permissions.append(permission) if matched_permissions: role_id = str(role.get("id") or "") matched_roles[access_ref_tail(role_id)] = { "id": role.get("id"), "name": role.get("name") or ((role.get("resolution") or {}).get("name") if isinstance(role.get("resolution"), dict) else None) or role.get("id"), **({"resolution": role.get("resolution")} if isinstance(role.get("resolution"), dict) else {}), "rights": access_permission_rights(matched_permissions), "rights_detail": access_permission_rights_detail(matched_permissions), "permissions": matched_permissions, } users: list[dict[str, Any]] = [] profile_ids: set[str] = set() group_ids: set[str] = set() role_ids_from_users: set[str] = set() role_permissions_from_users: dict[str, list[dict[str, Any]]] = {} role_permission_keys_from_users: dict[str, set[str]] = {} for effective in graph.get("effective_users") or []: if not isinstance(effective, dict): continue matched_permissions = [] user_role_ids: set[str] = set() for role in effective.get("roles") or []: if isinstance(role, dict) and access_ref_tail(role.get("id")): roles_by_tail.setdefault(access_ref_tail(role.get("id")), role) for permission in effective.get("permissions") or []: if not isinstance(permission, dict): continue if not access_permission_matches_object(permission, selector=object_selector, object_guid=object_guid, object_payload=object_payload): continue if not access_permission_matches_action(permission, action_filter_keys): continue matched_permissions.append(permission) for source in permission.get("sources") or []: if not isinstance(source, dict): continue role_tail = access_ref_tail(source.get("role")) if role_tail: role_ids_from_users.add(role_tail) user_role_ids.add(role_tail) role_permissions = role_permissions_from_users.setdefault(role_tail, []) permission_key = json.dumps(permission, ensure_ascii=False, sort_keys=True, default=str) permission_keys = role_permission_keys_from_users.setdefault(role_tail, set()) if permission_key not in permission_keys: role_permissions.append(permission) permission_keys.add(permission_key) for chain in source.get("chains") or []: if not isinstance(chain, dict): continue if chain.get("profile") not in {None, ""}: profile_ids.add(access_ref_tail(chain.get("profile"))) if chain.get("group") not in {None, ""}: group_ids.add(access_ref_tail(chain.get("group"))) if not matched_permissions: continue user = effective.get("user") if isinstance(effective.get("user"), dict) else {} users.append( { "id": user.get("id"), "name": user.get("name"), "active": user.get("active"), "marked": user.get("marked"), "user_type": user.get("user_type"), "user": user, "groups": effective.get("groups") or [], "roles": [ role for role in effective.get("roles") or [] if isinstance(role, dict) and access_ref_tail(role.get("id")) in user_role_ids ], "rights": access_permission_rights(matched_permissions), "rights_detail": access_permission_rights_detail(matched_permissions), "permissions": matched_permissions, "data_restrictions": effective.get("data_restrictions") or [], } ) if len(users) >= int(limit): break for role_tail, permissions in role_permissions_from_users.items(): if role_tail in matched_roles or not permissions: continue role = roles_by_tail.get(role_tail) or {"id": role_tail, "name": role_tail} matched_roles[role_tail] = { "id": role.get("id") or role_tail, "name": role.get("name") or ((role.get("resolution") or {}).get("name") if isinstance(role.get("resolution"), dict) else None) or role.get("id") or role_tail, **({"resolution": role.get("resolution")} if isinstance(role.get("resolution"), dict) else {}), "rights": access_permission_rights(permissions), "rights_detail": access_permission_rights_detail(permissions), "permissions": permissions, "source": "effective_permissions", } matched_role_tails = set(matched_roles) | role_ids_from_users profiles = [ profile for profile in access.get("profiles") or [] if isinstance(profile, dict) and ( access_ref_tail(profile.get("id")) in profile_ids or ({access_ref_tail(role_id) for role_id in profile.get("roles") or []} & matched_role_tails) ) ] profile_tails = {access_ref_tail(profile.get("id")) for profile in profiles} groups = [ group for group in access.get("groups") or [] if isinstance(group, dict) and ( access_ref_tail(group.get("id")) in group_ids or ({access_ref_tail(profile_id) for profile_id in group.get("profiles") or []} & profile_tails) ) ] roles = sorted(matched_roles.values(), key=lambda item: (str(item.get("name") or item.get("id")), str(item.get("id") or ""))) profiles = sorted(profiles, key=lambda item: str(item.get("name") or item.get("id")))[: int(limit)] groups = sorted(groups, key=lambda item: str(item.get("name") or item.get("id")))[: int(limit)] summary_text = ( f"Object subjects: {len(roles)} roles, {len(profiles)} profiles, " f"{len(groups)} groups, {len(users)} users" f"{' for action ' + access_permission_action_key(action_filter) if action_filter else ''}." ) query_payload = {key: normalized_payload.get(key) for key in ("ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "action") if normalized_payload.get(key) not in {None, ""}} return { "schema": "onec_access_object_subjects.v1", "status": "ok" if roles or users else "not_found", "base_id": base_id, "source": {"kind": "live_sql", "preset": "bsp", "extraction": "access.snapshot.extract"}, "query": query_payload, "object": object_payload, "summary": {"text": summary_text}, "roles": roles[: int(limit)], "profiles": profiles, "groups": groups, "users": users, "counts": { "roles": len(roles), "profiles": len(profiles), "groups": len(groups), "users": len(users), "permissions": sum(len(user.get("permissions") or []) for user in users), }, "extraction_counts": extracted.get("counts"), "diagnostics": extracted.get("diagnostics"), } ACCESS_RLS_DISCOVERY_TERMS = ("Огранич", "Доступ", "RLS", "Ключ") ACCESS_RLS_DISCOVERY_KINDS = ("InformationRegister", "Catalog") def access_rls_candidate_score(item: dict[str, Any]) -> tuple[int, list[str]]: text = " ".join(str(item.get(key) or "") for key in ("name", "synonym", "full_name", "kind", "kind_ru")).casefold() score = 0 reasons: list[str] = [] for term in ACCESS_RLS_DISCOVERY_TERMS: if term.casefold() in text: score += 10 reasons.append(f"name_contains:{term}") if "ключидоступа" in re.sub(r"[^0-9a-zа-яё]+", "", text): score += 20 reasons.append("known_bsp_access_keys") if "праваролей" in re.sub(r"[^0-9a-zа-яё]+", "", text): score += 15 reasons.append("known_bsp_role_rights") if "огранич" in text: score += 15 reasons.append("restriction_name") return score, reasons def access_rls_public_fields(attributes_result: dict[str, Any]) -> dict[str, Any]: fields: dict[str, Any] = {"dimensions": [], "resources": [], "attributes": [], "tabular_sections": []} for area in ("dimensions", "resources", "attributes"): for item in attributes_result.get(area) or []: if not isinstance(item, dict): continue fields[area].append( { "name": item.get("name"), "synonym": item.get("synonym"), "type": item.get("type"), **({"storage_routes": item.get("storage_routes")} if item.get("storage_routes") else {}), } ) for section in attributes_result.get("tabular_sections") or []: if not isinstance(section, dict): continue fields["tabular_sections"].append( { "name": section.get("name"), "synonym": section.get("synonym"), "columns": [ { "name": column.get("name"), "synonym": column.get("synonym"), "type": column.get("type"), **({"storage_routes": column.get("storage_routes")} if column.get("storage_routes") else {}), } for column in section.get("columns") or [] if isinstance(column, dict) ], **({"storage_routes": section.get("storage_routes")} if section.get("storage_routes") else {}), } ) return fields def access_rls_discover(payload: dict[str, Any]) -> dict[str, Any]: method = "access.rls.discover" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error limit, limit_error = parse_int_argument(payload, "limit", method=method, default=50, minimum=1, maximum=500) if limit_error: return limit_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=120, minimum=1, maximum=120) if timeout_error: return timeout_error terms_raw = payload.get("terms") if terms_raw is None or terms_raw == "": terms = list(ACCESS_RLS_DISCOVERY_TERMS) else: terms = [str(item).strip() for item in access_list(terms_raw) if str(item).strip()] if not terms: return invalid_argument(method, "terms", "terms must contain at least one non-empty string.") candidates_by_guid: dict[str, dict[str, Any]] = {} diagnostics: list[dict[str, Any]] = [] for kind in ACCESS_RLS_DISCOVERY_KINDS: for term in terms: listed = list_objects( kind, base_id=base_id, limit=int(limit), offset=0, include_storage=True, table="Config", name_filter=term, ) if listed.get("status") != "ok": diagnostics.append({"kind": kind, "term": term, "status": listed.get("status"), "diagnostics": listed.get("diagnostics")}) continue for item in listed.get("objects") or []: if not isinstance(item, dict) or item.get("guid") in {None, ""}: continue score, reasons = access_rls_candidate_score(item) if score <= 0: continue candidate = candidates_by_guid.setdefault(str(item.get("guid")), {**item, "score": 0, "reasons": []}) candidate["score"] = max(int(candidate.get("score") or 0), score) for reason in reasons: if reason not in candidate["reasons"]: candidate["reasons"].append(reason) candidates = sorted(candidates_by_guid.values(), key=lambda item: (-int(item.get("score") or 0), str(item.get("kind") or ""), str(item.get("name") or item.get("guid") or "")))[: int(limit)] detailed_candidates: list[dict[str, Any]] = [] for candidate in candidates: attributes = metadata_object_attributes( { "base_id": base_id, "kind": candidate.get("kind"), "name": candidate.get("name") or candidate.get("guid"), "guid": candidate.get("guid"), "include_storage": True, "only": "all", "limit": 200, "timeout_seconds": int(timeout_seconds), } ) item = { "guid": candidate.get("guid"), "kind": candidate.get("kind"), "kind_ru": candidate.get("kind_ru"), "name": candidate.get("name"), "synonym": candidate.get("synonym"), "score": candidate.get("score"), "reasons": candidate.get("reasons") or [], **({"storage": candidate.get("storage")} if candidate.get("storage") else {}), } if attributes.get("status") == "ok": item["fields"] = access_rls_public_fields(attributes) item["field_counts"] = {key: len(value) for key, value in item["fields"].items()} else: item["field_status"] = attributes.get("status") item["field_diagnostics"] = attributes.get("diagnostics") detailed_candidates.append(item) return { "schema": "onec_access_rls_discover.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_metadata"}, "query": {"terms": terms, "limit": int(limit)}, "candidates": detailed_candidates, "counts": {"candidates": len(detailed_candidates), "diagnostics": len(diagnostics)}, "diagnostics": diagnostics, } ACCESS_ROLE_QUERY_SYNONYMS = { "запись": ["добавление", "изменение"], "записи": ["добавление", "изменение"], "редактирование": ["изменение"], "изменить": ["изменение"], "создание": ["добавление"], "создать": ["добавление"], } def access_role_search_tokens(value: Any) -> list[str]: raw_tokens = re.findall(r"[\wА-Яа-яЁё]+", str(value or "").casefold()) tokens: list[str] = [] for token in raw_tokens: if len(token) < 3: continue candidates = [token, *ACCESS_ROLE_QUERY_SYNONYMS.get(token, [])] for candidate in list(candidates): stem = re.sub(r"(иями|ями|ами|ого|ему|ыми|ими|ией|иям|иях|иях|ий|ый|ой|ая|ое|ые|ых|ам|ям|ах|ях|ов|ев|ей|ом|ем|ою|ею|ою|у|ю|а|я|ы|и|е|о)$", "", candidate) if len(stem) >= 5 and stem not in candidates: candidates.append(stem) for candidate in candidates: if candidate not in tokens: tokens.append(candidate) return tokens def access_role_match_score(role: dict[str, Any], selector: str) -> int: role_id = str(role.get("id") or "") role_name = str(role.get("name") or "") selector_tail = access_ref_tail(selector) selector_text = selector.casefold() role_name_text = role_name.casefold() role_id_text = role_id.casefold() if access_ref_tail(role_id) == selector_tail or role_id_text == selector_text: return 10000 if role_name_text == selector_text: return 9000 if selector_text and selector_text in role_name_text: return 8000 + len(selector_text) tokens = access_role_search_tokens(selector) if not tokens: return 0 matched = [token for token in tokens if token in role_name_text] if not matched: return 0 score = len(matched) * 100 if len(matched) == len(tokens): score += 1000 score += min(50, sum(len(token) for token in matched)) return score def access_role_chain(base_id: str, role_selector: str, *, timeout_seconds: int = 120) -> dict[str, Any]: queries, mappings, diagnostics = access_bsp_extractor_plan(base_id) if not queries or not mappings: return {"status": "error", "diagnostics": (diagnostics or {}).get("diagnostics") or diagnostics} rows_by_area: dict[str, list[dict[str, Any]]] = {} extractor_counts: dict[str, Any] = {} for area in ("roles", "profiles", "profile_roles", "group_profiles", "groups", "group_users", "user_group_members", "users"): if area not in queries: rows_by_area[area] = [] extractor_counts[area] = {"rows": 0, "truncated": False, "skipped": True} continue rows, error, truncated = access_rows_from_query(base_id, queries[area], limit=20000, timeout_seconds=int(timeout_seconds)) if error: return error rows_by_area[area] = rows extractor_counts[area] = {"rows": len(rows), "truncated": bool(truncated)} access = access_snapshot_from_extractor_rows(rows_by_area, mappings) roles = [role for role in access.get("roles") or [] if isinstance(role, dict)] profiles = [profile for profile in access.get("profiles") or [] if isinstance(profile, dict)] groups = [group for group in access.get("groups") or [] if isinstance(group, dict)] users = [user for user in access.get("users") or [] if isinstance(user, dict)] scored_roles = [ {**role, "_match_score": access_role_match_score(role, role_selector)} for role in roles ] alternatives = sorted( [role for role in scored_roles if int(role.get("_match_score") or 0) > 0], key=lambda item: (-int(item.get("_match_score") or 0), str(item.get("name") or "")), )[:10] best_score = int(alternatives[0].get("_match_score") or 0) if alternatives else 0 matched_roles = [{key: value for key, value in role.items() if key != "_match_score"} for role in alternatives if int(role.get("_match_score") or 0) == best_score] if not matched_roles: return { "status": "not_found", "base_id": base_id, "query": {"role": role_selector}, "roles": [], "profiles": [], "groups": [], "users": [], "alternatives": [], "counts": {"roles": 0, "profiles": 0, "groups": 0, "users": 0}, "diagnostics": {"extractors": extractor_counts, "bsp": diagnostics}, } matched_role_ids = {access_ref_tail(role.get("id")) for role in matched_roles} matched_profiles = [ profile for profile in profiles if {access_ref_tail(role_id) for role_id in profile.get("roles") or []} & matched_role_ids ] matched_profile_ids = {access_ref_tail(profile.get("id")) for profile in matched_profiles} matched_groups = [ group for group in groups if {access_ref_tail(profile_id) for profile_id in group.get("profiles") or []} & matched_profile_ids ] graph = build_access_graph_from_snapshot(access, base_id=base_id, resolve_identifiers=False) profiles_by_id = {access_ref_tail(profile.get("id")): profile for profile in profiles} groups_by_id = {access_ref_tail(group.get("id")): group for group in groups} matched_profile_ids = set(matched_profile_ids) matched_group_ids = {access_ref_tail(group.get("id")) for group in matched_groups} users_by_id = {access_ref_tail(user.get("id")): user for user in users} matched_users_by_ref: dict[str, dict[str, Any]] = {} wanted_user_refs: set[str] = set() for effective in graph.get("effective_users") or []: if not isinstance(effective, dict): continue effective_user = effective.get("user") if isinstance(effective.get("user"), dict) else {} effective_user_tail = access_ref_tail(effective_user.get("id")) matched_role_sources: list[dict[str, Any]] = [] for role in effective.get("roles") or []: if not isinstance(role, dict) or access_ref_tail(role.get("id")) not in matched_role_ids: continue matched_role_sources.extend( [ {**source, "role": role.get("id"), "role_name": role.get("name")} for source in role.get("sources") or [] if isinstance(source, dict) ] ) if not matched_role_sources: continue for source in matched_role_sources: if source.get("profile") not in {None, ""}: matched_profile_ids.add(access_ref_tail(source.get("profile"))) if source.get("group") not in {None, ""}: matched_group_ids.add(access_ref_tail(source.get("group"))) for group_id in effective.get("groups") or []: group_tail = access_ref_tail(group_id) if group_tail: matched_group_ids.add(group_tail) if effective_user_tail: wanted_user_refs.add(effective_user_tail) user = users_by_id.get(effective_user_tail) or effective_user or {"id": effective_user_tail, "name": effective_user_tail} entry = matched_users_by_ref.setdefault( effective_user_tail, { "id": user.get("id") or effective_user.get("id"), "name": user.get("name") or effective_user.get("name") or user.get("id") or effective_user_tail, "active": user.get("active") if user.get("active") is not None else effective_user.get("active"), "groups": [], "role_sources": [], }, ) for source_key in ("marked", "user_type", "service", "administrator"): value = user.get(source_key) if value is None and source_key == "user_type": value = effective_user.get(source_key) if value is None and source_key in {"marked", "service", "administrator"} and effective_user.get(source_key) is True: value = True if value is not None: entry[source_key] = value for source in matched_role_sources: if source not in entry["role_sources"]: entry["role_sources"].append(source) for group_id in effective.get("groups") or []: group = groups_by_id.get(access_ref_tail(group_id)) or {"id": group_id, "name": group_id} group_item = {"id": group.get("id"), "name": group.get("name")} if group_item not in entry["groups"]: entry["groups"].append(group_item) user_names_by_ref = access_resolve_user_names( base_id, wanted_user_refs, diagnostics, timeout_seconds=int(timeout_seconds), ) for user_tail, item in matched_users_by_ref.items(): item["name"] = user_names_by_ref.get(user_tail) or item.get("name") or item.get("id") matched_profiles = [profiles_by_id[profile_id] for profile_id in sorted(matched_profile_ids) if profile_id in profiles_by_id] matched_groups = [groups_by_id[group_id] for group_id in sorted(matched_group_ids) if group_id in groups_by_id] matched_users = sorted(matched_users_by_ref.values(), key=lambda item: str(item.get("name") or item.get("id"))) return { "status": "ok", "base_id": base_id, "query": {"role": role_selector}, "roles": matched_roles, "profiles": matched_profiles, "groups": matched_groups, "users": matched_users, "alternatives": [{key: value for key, value in role.items() if key != "_match_score"} for role in alternatives], "counts": {"roles": len(matched_roles), "profiles": len(matched_profiles), "groups": len(matched_groups), "users": len(matched_users)}, "diagnostics": {"extractors": extractor_counts, "bsp": diagnostics, "role_match": {"best_score": best_score, "tokens": access_role_search_tokens(role_selector)}}, } def access_role_profiles(payload: dict[str, Any]) -> dict[str, Any]: method = "access.role.profiles" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error role_selector = str(payload.get("role") or payload.get("role_id") or payload.get("role_name") or payload.get("query") or "").strip() if not role_selector: return invalid_argument(method, "role", "Pass role, role_id, role_name, or query.") limit, limit_error = parse_int_argument(payload, "limit", method=method, default=1000, minimum=1, maximum=20000) if limit_error: return limit_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=120, minimum=1, maximum=120) if timeout_error: return timeout_error chain = access_role_chain(base_id, role_selector, timeout_seconds=int(timeout_seconds)) if chain.get("status") == "error": return {"schema": "onec_access_role_profiles.v1", **chain} if chain.get("status") == "not_found": return {"schema": "onec_access_role_profiles.v1", **chain} matched_roles = chain.get("roles") if isinstance(chain.get("roles"), list) else [] matched_profiles = sorted(chain.get("profiles") or [], key=lambda item: str(item.get("name") or item.get("id")))[: int(limit)] matched_groups = sorted(chain.get("groups") or [], key=lambda item: str(item.get("name") or item.get("id")))[: int(limit)] matched_profiles = sorted(matched_profiles, key=lambda item: str(item.get("name") or item.get("id")))[: int(limit)] matched_groups = sorted(matched_groups, key=lambda item: str(item.get("name") or item.get("id")))[: int(limit)] summary = { "text": ( f"Role profiles: {len(matched_roles)} role matches, " f"{len(matched_profiles)} profiles, {len(matched_groups)} access groups." ) } return { "schema": "onec_access_role_profiles.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "preset": "bsp"}, "query": {"role": role_selector}, "summary": summary, "roles": matched_roles[: int(limit)], "profiles": matched_profiles, "groups": matched_groups, "alternatives": chain.get("alternatives") or [], "counts": {"roles": len(matched_roles), "profiles": len(matched_profiles), "groups": len(matched_groups)}, "diagnostics": chain.get("diagnostics") or {}, } def access_role_users(payload: dict[str, Any]) -> dict[str, Any]: method = "access.role.users" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error role_selector = str(payload.get("role") or payload.get("role_id") or payload.get("role_name") or payload.get("query") or "").strip() if not role_selector: return invalid_argument(method, "role", "Pass role, role_id, role_name, or query.") limit, limit_error = parse_int_argument(payload, "limit", method=method, default=20000, minimum=1, maximum=20000) if limit_error: return limit_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=120, minimum=1, maximum=120) if timeout_error: return timeout_error chain = access_role_chain(base_id, role_selector, timeout_seconds=int(timeout_seconds)) if chain.get("status") == "error": return {"schema": "onec_access_role_users.v1", **chain} if chain.get("status") == "not_found": return {"schema": "onec_access_role_users.v1", **chain} roles = chain.get("roles") if isinstance(chain.get("roles"), list) else [] profiles = chain.get("profiles") if isinstance(chain.get("profiles"), list) else [] groups = chain.get("groups") if isinstance(chain.get("groups"), list) else [] users = sorted(chain.get("users") or [], key=lambda item: str(item.get("name") or item.get("id")))[: int(limit)] summary = { "text": ( f"Role users: {len(roles)} role matches, {len(profiles)} profiles, " f"{len(groups)} access groups, {len(users)} users." ) } return { "schema": "onec_access_role_users.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "preset": "bsp"}, "query": {"role": role_selector}, "summary": summary, "roles": roles, "profiles": sorted(profiles, key=lambda item: str(item.get("name") or item.get("id"))), "groups": sorted(groups, key=lambda item: str(item.get("name") or item.get("id"))), "users": users, "alternatives": chain.get("alternatives") or [], "counts": {"roles": len(roles), "profiles": len(profiles), "groups": len(groups), "users": len(users)}, "diagnostics": chain.get("diagnostics") or {}, } ACCESS_ROLE_AUDIT_EXPORT_COLUMNS = [ "base_id", "generated_at", "query_role", "matched_role_id", "matched_role_name", "profile_id", "profile_name", "group_id", "group_name", "user_id", "user_name", "user_type", "user_active", "user_marked", "user_groups_count", "access_path", ] def infobase_user_public(row: dict[str, Any]) -> dict[str, Any]: """Return only safe, documented identity fields from dbo.v8users.""" role_set_id = str(row.get("role_set_id") or "").strip() or None administrator = bool(row.get("platform_administrator")) result = { "user_kind": "infobase_user", "visible_in": "Configurator > Administration > Users", "id": str(row.get("id") or "").upper() or None, "name": row.get("name"), "full_name": row.get("full_name"), "changed": jsonable(row.get("changed")), "visible_in_login_list": bool(row.get("visible_in_login_list")), "standard_authentication_enabled": bool(row.get("standard_authentication_enabled")), "os_authentication_configured": bool(row.get("os_authentication_configured")), "email_configured": bool(row.get("email_configured")), "platform_administrator": administrator, "role_set_id": role_set_id, "protected_data_bytes": int(row.get("protected_data_bytes") or 0), "role_assignment": { "source": "dbo.v8users", "authoritative": True, "role_set_id": role_set_id, "platform_administrator": administrator, "exact_role_names_status": "runtime_required", "exact_role_names": None, "message": ( "RolesID proves the platform role set assigned to this infobase user, but SQL does not expose " "a supported role-name mapping. Resolve exact role names through the 1C " "ПользователиИнформационнойБазы runtime API; never substitute BSP profiles or groups." ), }, } return {key: value for key, value in result.items() if value is not None} def infobase_user_match_score(user: dict[str, Any], selector: str) -> int: selector_text = str(selector or "").strip().casefold() selector_id = re.sub(r"[^0-9a-f]", "", selector_text) user_id = re.sub(r"[^0-9a-f]", "", str(user.get("id") or "").casefold()) name = str(user.get("name") or "").casefold() full_name = str(user.get("full_name") or "").casefold() if selector_id and selector_id == user_id: return 10000 if selector_text and selector_text in {name, full_name}: return 9000 if selector_text and (selector_text in name or selector_text in full_name or selector_text in user_id): return 8000 + len(selector_text) ratio = max( difflib.SequenceMatcher(None, selector_text, name).ratio() if selector_text and name else 0, difflib.SequenceMatcher(None, selector_text, full_name).ratio() if selector_text and full_name else 0, ) return int(ratio * 100) def infobase_users_read(base_id: str, *, scan_limit: int, timeout_seconds: int) -> tuple[list[dict[str, Any]], dict[str, Any] | None]: conn, config, error = connect_live_sql(base_id, "infobase.users.search", timeout_seconds=timeout_seconds) if error: return [], error rows: list[dict[str, Any]] = [] try: with conn: with conn.cursor(as_dict=True) as cursor: cursor.execute( """ SELECT TOP (%d) CONVERT(varchar(32), ID, 2) AS id, Name AS name, Descr AS full_name, Changed AS changed, CONVERT(varchar(40), RolesID) AS role_set_id, CONVERT(int, Show) AS visible_in_login_list, CONVERT(int, EAuth) AS standard_authentication_enabled, CASE WHEN OSName IS NULL OR OSName = N'' THEN 0 ELSE 1 END AS os_authentication_configured, CASE WHEN Email IS NULL OR Email = N'' THEN 0 ELSE 1 END AS email_configured, CONVERT(int, AdmRole) AS platform_administrator, DATALENGTH(Data) AS protected_data_bytes FROM dbo.v8users WITH (READCOMMITTED) ORDER BY Name, ID """ % int(scan_limit) ) rows = [infobase_user_public({key: jsonable(value) for key, value in row.items()}) for row in cursor.fetchall()] except Exception as exc: return [], { "schema": "onec_infobase_users.v1", "status": "error", "base_id": base_id, "source": {"kind": "live_sql", "database": (config or {}).get("database"), "table": "v8users"}, "diagnostics": {"message": str(exc)}, } finally: try: conn.close() except Exception: pass return rows, None def infobase_users_search(payload: dict[str, Any]) -> dict[str, Any]: method = "infobase.users.search" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error selector = str(payload.get("query") or payload.get("user") or payload.get("name") or "").strip() limit, limit_error = parse_int_argument(payload, "limit", method=method, default=20, minimum=1, maximum=500) if limit_error: return limit_error scan_limit, scan_limit_error = parse_int_argument(payload, "scan_limit", method=method, default=5000, minimum=1, maximum=50000) if scan_limit_error: return scan_limit_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1, maximum=120) if timeout_error: return timeout_error users, error = infobase_users_read(base_id, scan_limit=int(scan_limit), timeout_seconds=int(timeout_seconds)) if error: return error if selector: scored = [(infobase_user_match_score(user, selector), user) for user in users] scored.sort(key=lambda item: (-item[0], str(item[1].get("name") or item[1].get("id") or ""))) matches = [{**user, "match_score": score} for score, user in scored if score >= 100][: int(limit)] nearest = [{**user, "match_score": score} for score, user in scored if score > 0][: int(limit)] else: matches = users[: int(limit)] nearest = [] return { "schema": "onec_infobase_users.v1", "status": "ok" if matches else "not_found", "base_id": base_id, "terminology": { "default_user_meaning": "infobase_user", "bsp_user_is_separate": True, "message": "Unqualified 'user' means the infobase user visible in Configurator. Use access.users.search only for explicit BSP catalog/group/profile questions.", }, "source": {"kind": "live_sql", "table": "dbo.v8users", "authoritative_for_platform_identity": True}, "query": {"user": selector}, "users": matches, "nearest": nearest, "counts": {"users": len(matches), "nearest": len(nearest), "scanned": len(users), "truncated": len(users) >= int(scan_limit)}, "capabilities": { "can_read_identity": True, "can_read_authentication_flags": True, "can_read_platform_administrator": True, "can_read_role_set_id": True, "can_read_exact_role_names_from_sql": False, "exact_role_names_require": "1C runtime ПользователиИнформационнойБазы API", "password_hashes_exposed": False, "protected_data_exposed": False, }, } def infobase_user_get(payload: dict[str, Any]) -> dict[str, Any]: method = "infobase.user.get" selector = str(payload.get("user") or payload.get("name") or payload.get("id") or "").strip() if not selector: return invalid_argument(method, "user", "Pass user, name, or platform user id.") result = infobase_users_search({**payload, "query": selector, "limit": 20}) if result.get("status") == "error": return result exact = [user for user in result.get("users") or [] if int(user.get("match_score") or 0) >= 9000] if len(exact) == 1: return { "schema": "onec_infobase_user.v1", "status": "ok", "base_id": result.get("base_id"), "terminology": result.get("terminology"), "source": result.get("source"), "user": {key: value for key, value in exact[0].items() if key != "match_score"}, "capabilities": result.get("capabilities"), "bsp_correlation": { "status": "separate_layer_not_requested", "authoritative_for_platform_roles": False, "next_method": "access.users.search", }, } return { "schema": "onec_infobase_user.v1", "status": "ambiguous" if len(exact) > 1 else "not_found", "base_id": result.get("base_id"), "query": {"user": selector}, "candidates": exact or (result.get("nearest") or [])[:10], "terminology": result.get("terminology"), } def infobase_user_admin_config_for_base(base_id: str) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: raw_map = os.environ.get("ONEC_INFOBASE_USER_ADMIN_BASES_JSON") raw_map_file = os.environ.get("ONEC_INFOBASE_USER_ADMIN_BASES_JSON_FILE") if not raw_map and raw_map_file: try: raw_map = Path(raw_map_file).read_text(encoding="utf-8-sig") except FileNotFoundError: return None, { "status": "not_configured", "message": f"Runtime user-admin config file is not present for base_id '{base_id}'.", } except Exception as exc: return None, {"status": "invalid_config", "message": f"Cannot read runtime user-admin config: {exc}"} if not raw_map: return None, { "status": "not_configured", "message": "Configure ONEC_INFOBASE_USER_ADMIN_BASES_JSON or ONEC_INFOBASE_USER_ADMIN_BASES_JSON_FILE with a runtime bridge entry for this base_id.", } try: config_map = json.loads(raw_map) except json.JSONDecodeError as exc: return None, {"status": "invalid_config", "message": f"Runtime user-admin config is not valid JSON: {exc}"} if not isinstance(config_map, dict): return None, {"status": "invalid_config", "message": "Runtime user-admin config must be an object keyed by base_id."} item = config_map.get(base_id) if not isinstance(item, dict): return None, {"status": "not_configured", "message": f"No runtime user-admin bridge configured for base_id '{base_id}'."} url = str(item.get("url") or "").strip().rstrip("/") token_env = str(item.get("token_env") or "").strip() token = os.environ.get(token_env, "") if token_env else "" allow_insecure_http = item.get("allow_insecure_http") is True if not url: return None, {"status": "invalid_config", "message": f"Runtime user-admin bridge URL is empty for base_id '{base_id}'."} parsed = urllib.parse.urlparse(url) if parsed.scheme not in {"http", "https"} or not parsed.netloc: return None, {"status": "invalid_config", "message": "Runtime user-admin bridge URL must be an absolute HTTP(S) URL."} if parsed.scheme != "https" and not allow_insecure_http: return None, { "status": "blocked_insecure_transport", "message": "Password transport requires HTTPS. For an isolated test network only, set allow_insecure_http=true explicitly in the non-committed runtime config.", } unauthenticated_test_mode = truthy(os.environ.get("ONEC_INFOBASE_USER_ADMIN_ALLOW_UNAUTHENTICATED")) if (not token_env or not token) and not unauthenticated_test_mode: return None, { "status": "not_configured", "message": "Runtime user-admin bridge token must be supplied through the configured token_env; never store it in the JSON config.", "token_env": token_env or None, } return { "url": url, "token": token, "token_env": token_env, "allow_insecure_http": allow_insecure_http, "unauthenticated_test_mode": unauthenticated_test_mode, }, None def infobase_user_password_unauthenticated_test_mode() -> bool: return truthy(os.environ.get("ONEC_INFOBASE_USER_ADMIN_ALLOW_UNAUTHENTICATED")) def infobase_user_password_capabilities(payload: dict[str, Any]) -> dict[str, Any]: method = "infobase.user.password.capabilities" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error sql_config, sql_config_error = sql_config_for_base(base_id) service_auth_configured = bool(adapter_service_token()) unauthenticated_test_mode = infobase_user_password_unauthenticated_test_mode() access_ready = service_auth_configured or unauthenticated_test_mode set_ready = bool(sql_config and access_ready) clear_ready = bool(sql_config and access_ready) ready = set_ready or clear_ready return { "schema": "onec_infobase_user_password_capabilities.v1", "status": "ready" if ready else "blocked", "base_id": base_id, "operations": {"status": bool(sql_config), "set": set_ready, "clear": clear_ready}, "requirements": { "adapter_service_authentication": not unauthenticated_test_mode, "adapter_service_authentication_configured": service_auth_configured, "unauthenticated_test_mode": unauthenticated_test_mode, "set_transport": "sql_dbo_v8users_data", "set_sql_configured": bool(sql_config), "clear_transport": "sql_dbo_v8users_data", "clear_sql_configured": bool(sql_config), "exact_user_id_confirmation": True, "explicit_allow_flag": True, "administrator_extra_confirmation": True, }, "security": { "direct_sql_password_write": clear_ready, "sql_clear_transactional_readback": True, "sql_clear_changes_only_current_hash_pair": True, "password_persisted_by_adapter": False, "password_echoed": False, "password_hashes_exposed": False, "password_operation_history_payload": False, "password_operation_result_audited": True, }, "operation_blockers": { **({"set": sql_config_error} if sql_config_error else {}), **({"clear": sql_config_error} if sql_config_error else {}), }, **( {"blocker": {"status": "service_auth_required", "message": "Set ONEC_ADAPTER_SERVICE_TOKEN before enabling password mutations; anonymous password writes are always blocked."}} if not access_ready else {} ), } INFOBASE_EMPTY_PASSWORD_SHA1_BASE64 = base64.b64encode(hashlib.sha1(b"").digest()).decode("ascii") INFOBASE_SHA1_BASE64_RE = re.compile(r"^[A-Za-z0-9+/]{27}=$") def infobase_user_password_data_decode(data: bytes) -> dict[str, Any]: if not isinstance(data, bytes) or len(data) < 4: raise ValueError("v8users.Data is empty or too short") key_size = int(data[0]) if key_size < 1 or len(data) <= key_size + 1: raise ValueError("v8users.Data has an invalid XOR key header") key = data[1 : key_size + 1] encrypted_payload = data[key_size + 1 :] payload = bytes(value ^ key[index % key_size] for index, value in enumerate(encrypted_payload)) trailing_nuls = len(payload) - len(payload.rstrip(b"\x00")) core = payload[:-trailing_nuls] if trailing_nuls else payload bom = b"\xef\xbb\xbf" if core.startswith(b"\xef\xbb\xbf") else b"" text = core[len(bom) :].decode("utf-8") from parser.payload import parse_brace_text, scalar tree = parse_brace_text(text) if not isinstance(tree, dict) or tree.get("type") != "list" or not isinstance(tree.get("items"), list): raise ValueError("v8users.Data does not contain the expected brace-list root") items = tree["items"] values = [scalar(item) for item in items] if len(values) < 14 or str(values[7]) not in {"0", "1"}: raise ValueError("v8users.Data authentication layout is not recognized") password_pair = None for index in range(10, min(len(values) - 1, 20)): first = values[index] second = values[index + 1] if ( isinstance(first, str) and isinstance(second, str) and INFOBASE_SHA1_BASE64_RE.fullmatch(first) and INFOBASE_SHA1_BASE64_RE.fullmatch(second) ): password_pair = (index, index + 1) break if password_pair is None: raise ValueError("current password hash pair was not found in v8users.Data") return { "key_size": key_size, "key": key, "bom": bom, "trailing_nuls": trailing_nuls, "text": text, "items": items, "values": values, "password_pair": password_pair, } def infobase_user_password_status(payload: dict[str, Any]) -> dict[str, Any]: method = "infobase.user.password.status" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error selector = str(payload.get("user") or payload.get("name") or payload.get("id") or "").strip() if not selector: return invalid_argument(method, "user", "Pass the exact Configurator user name or platform id.") timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1, maximum=120) if timeout_error: return timeout_error selected = infobase_user_get( { "base_id": base_id, "user": selector, "scan_limit": payload.get("scan_limit", 5000), "timeout_seconds": int(timeout_seconds), } ) if selected.get("status") != "ok": return { "schema": "onec_infobase_user_password_status.v1", "status": selected.get("status") or "not_found", "base_id": base_id, "query": {"user": selector}, "candidates": selected.get("candidates") or [], } user = selected.get("user") if isinstance(selected.get("user"), dict) else {} user_id = re.sub(r"[^0-9a-f]", "", str(user.get("id") or "").casefold()) conn, config, error = connect_live_sql(base_id, method, timeout_seconds=int(timeout_seconds)) if error: return error try: with conn: with conn.cursor(as_dict=True) as cursor: cursor.execute( "SELECT Name, CONVERT(int,EAuth) AS standard_authentication_enabled, Data FROM dbo.v8users WITH (READCOMMITTED) WHERE ID=%s", (bytes.fromhex(user_id),), ) row = cursor.fetchone() if not row or str(row.get("Name") or "") != str(user.get("name") or ""): return { "schema": "onec_infobase_user_password_status.v1", "status": "not_found", "base_id": base_id, "query": {"user": selector}, } standard_authentication_enabled = bool(row.get("standard_authentication_enabled")) decoded = infobase_user_password_data_decode(bytes(row.get("Data") or b"")) pair = tuple(decoded["password_pair"]) empty = all(decoded["values"][index] == INFOBASE_EMPTY_PASSWORD_SHA1_BASE64 for index in pair) password_state = "standard_authentication_disabled" if not standard_authentication_enabled else ("empty" if empty else "set") return { "schema": "onec_infobase_user_password_status.v1", "status": "ok", "base_id": base_id, "target": {"id": user.get("id"), "name": user.get("name")}, "password_state": password_state, "standard_authentication_enabled": standard_authentication_enabled, "can_login_with_empty_password": standard_authentication_enabled and empty, "platform_administrator": bool(user.get("platform_administrator")), "source": {"kind": "live_sql", "database": (config or {}).get("database"), "table": "dbo.v8users", "column": "Data"}, "security": {"password_hashes_exposed": False, "protected_data_exposed": False}, } except Exception as exc: return { "schema": "onec_infobase_user_password_status.v1", "status": "error", "base_id": base_id, "target": {"id": user.get("id"), "name": user.get("name")}, "error": "sql_password_status_failed", "diagnostics": {"message": str(exc)}, } finally: try: conn.close() except Exception: pass def infobase_user_password_data_set(data: bytes, password: str) -> tuple[bytes, dict[str, Any]]: decoded = infobase_user_password_data_decode(data) pair = tuple(decoded["password_pair"]) values = decoded["values"] desired_hashes = infobase_user_password_hash_pair(password) already_applied = all(values[index] == desired_hashes[offset] for offset, index in enumerate(pair)) if already_applied: return data, {"already_applied": True, "layout_fields": len(values), "password_pair": list(pair)} text = str(decoded["text"]) for offset, index in sorted(enumerate(pair), key=lambda item: item[1], reverse=True): node = decoded["items"][index] if not isinstance(node, dict) or node.get("type") != "string": raise ValueError("password hash node is not a quoted string") start = int(node["pos"]) end = int(node["end"]) text = text[:start] + '"' + desired_hashes[offset] + '"' + text[end:] plain = decoded["bom"] + text.encode("utf-8") + (b"\x00" * int(decoded["trailing_nuls"])) key_size = int(decoded["key_size"]) key = bytes(decoded["key"]) new_data = bytes([key_size]) + key + bytes(value ^ key[index % key_size] for index, value in enumerate(plain)) verified = infobase_user_password_data_decode(new_data) changed = [index for index, (old, new) in enumerate(zip(values, verified["values"])) if old != new] if any(index not in pair for index in changed) or any( verified["values"][index] != desired_hashes[offset] for offset, index in enumerate(pair) ): raise ValueError("password update changed fields outside the current hash pair") if len(new_data) != len(data): raise ValueError("password update changed the v8users.Data byte length") return new_data, {"already_applied": False, "layout_fields": len(values), "password_pair": list(pair)} def infobase_user_password_hash_pair(password: str) -> tuple[str, str]: return ( base64.b64encode(hashlib.sha1(password.encode("utf-8")).digest()).decode("ascii"), base64.b64encode(hashlib.sha1(password.upper().encode("utf-8")).digest()).decode("ascii"), ) def infobase_user_password_data_clear(data: bytes) -> tuple[bytes, dict[str, Any]]: new_data, details = infobase_user_password_data_set(data, "") return new_data, {**details, "already_clear": details["already_applied"]} def infobase_user_password_write_sql( base_id: str, user: dict[str, Any], *, operation: str, new_password: str | None, request_id: str, timeout_seconds: int, ) -> dict[str, Any]: method = f"infobase.user.password.{operation}" user_id = re.sub(r"[^0-9a-f]", "", str(user.get("id") or "").casefold()) target = {"id": user.get("id"), "name": user.get("name")} conn, config, error = connect_live_sql(base_id, method, timeout_seconds=timeout_seconds) if error: return error try: cursor = conn.cursor(as_dict=True) cursor.execute( """ SELECT ID, Name, CONVERT(int, EAuth) AS standard_authentication_enabled, Data FROM dbo.v8users WITH (UPDLOCK, HOLDLOCK, ROWLOCK) WHERE ID = %s """, (bytes.fromhex(user_id),), ) row = cursor.fetchone() if not row or str(row.get("Name") or "") != str(user.get("name") or ""): conn.rollback() return { "schema": "onec_infobase_user_password_change.v1", "status": "blocked", "base_id": base_id, "operation": operation, "request_id": request_id, "target": target, "error": "target_changed_after_confirmation", } if not bool(row.get("standard_authentication_enabled")): conn.rollback() return { "schema": "onec_infobase_user_password_change.v1", "status": "blocked", "base_id": base_id, "operation": operation, "request_id": request_id, "target": target, "error": "standard_authentication_disabled", "diagnostics": {"message": "EAuth=0; changing stored hashes would not enable password login."}, } old_data = bytes(row.get("Data") or b"") new_data, details = infobase_user_password_data_set(old_data, new_password or "") if details["already_applied"]: conn.rollback() return { "schema": "onec_infobase_user_password_change.v1", "status": "ok", "base_id": base_id, "operation": operation, "request_id": request_id, "applied": False, "already_clear": operation == "clear", "already_set": operation == "set", "target": target, "source": {"kind": "live_sql", "table": "dbo.v8users", "column": "Data"}, } cursor.execute( "UPDATE dbo.v8users SET Data = %s WHERE ID = %s AND Data = %s", (new_data, bytes.fromhex(user_id), old_data), ) if cursor.rowcount != 1: conn.rollback() return { "schema": "onec_infobase_user_password_change.v1", "status": "conflict", "base_id": base_id, "operation": operation, "request_id": request_id, "target": target, "error": "concurrent_user_data_change", } cursor.execute("SELECT Data FROM dbo.v8users WITH (HOLDLOCK) WHERE ID = %s", (bytes.fromhex(user_id),)) readback = cursor.fetchone() readback_data = bytes((readback or {}).get("Data") or b"") verified = infobase_user_password_data_decode(readback_data) pair = tuple(verified["password_pair"]) desired_hashes = infobase_user_password_hash_pair(new_password or "") if readback_data != new_data or any( verified["values"][index] != desired_hashes[offset] for offset, index in enumerate(pair) ): conn.rollback() return { "schema": "onec_infobase_user_password_change.v1", "status": "verification_failed", "base_id": base_id, "operation": operation, "request_id": request_id, "target": target, "error": "sql_readback_mismatch", } conn.commit() return { "schema": "onec_infobase_user_password_change.v1", "status": "ok", "base_id": base_id, "operation": operation, "request_id": request_id, "applied": True, "target": target, "source": {"kind": "live_sql", "database": (config or {}).get("database"), "table": "dbo.v8users", "column": "Data"}, "verification": { "transaction_committed": True, "readback_matches": True, "only_current_password_hash_pair_changed": True, "data_length_unchanged": len(old_data) == len(new_data), "layout_fields": details["layout_fields"], }, } except Exception as exc: try: conn.rollback() except Exception: pass return { "schema": "onec_infobase_user_password_change.v1", "status": "error", "base_id": base_id, "operation": operation, "request_id": request_id, "target": target, "error": "sql_password_update_failed", "diagnostics": {"message": str(exc)}, } finally: try: conn.close() except Exception: pass def infobase_user_password_clear_sql( base_id: str, user: dict[str, Any], *, request_id: str, timeout_seconds: int, ) -> dict[str, Any]: return infobase_user_password_write_sql( base_id, user, operation="clear", new_password=None, request_id=request_id, timeout_seconds=timeout_seconds, ) def infobase_user_password_set_sql( base_id: str, user: dict[str, Any], *, new_password: str, request_id: str, timeout_seconds: int, ) -> dict[str, Any]: return infobase_user_password_write_sql( base_id, user, operation="set", new_password=new_password, request_id=request_id, timeout_seconds=timeout_seconds, ) def infobase_user_admin_runtime_call(config: dict[str, Any], request_payload: dict[str, Any], *, timeout_seconds: int) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: url = str(config["url"]).rstrip("/") + "/infobase-users/password" headers = { "Accept": "application/json", "Content-Type": "application/json; charset=utf-8", } if config.get("token"): headers["Authorization"] = f"Bearer {config['token']}" request = urllib.request.Request( url, data=json.dumps(request_payload, ensure_ascii=False).encode("utf-8"), headers=headers, method="POST", ) try: with urllib.request.urlopen(request, timeout=timeout_seconds) as response: raw = response.read().decode("utf-8-sig") decoded = json.loads(raw) if raw.strip() else {} except urllib.error.HTTPError as exc: return None, { "status": "runtime_error", "error": "runtime_http_error", "http_status": exc.code, "message": "The 1C runtime bridge rejected the password operation. Its response body is intentionally not returned.", } except (urllib.error.URLError, TimeoutError) as exc: return None, { "status": "runtime_result_unknown", "error": "runtime_unreachable_or_timeout", "message": f"The runtime acknowledgement was not received: {type(exc).__name__}. Check the operation by request_id before retrying.", } except (json.JSONDecodeError, ValueError): return None, {"status": "runtime_error", "error": "invalid_runtime_response", "message": "The runtime bridge returned a non-JSON response."} if not isinstance(decoded, dict): return None, {"status": "runtime_error", "error": "invalid_runtime_response", "message": "The runtime bridge response must be a JSON object."} return decoded, None def infobase_user_password_change(payload: dict[str, Any], *, operation: str) -> dict[str, Any]: method = f"infobase.user.password.{operation}" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error allow_argument = "allow_password_change" if operation == "set" else "allow_password_clear" allowed, allowed_error = strict_bool_argument(payload, allow_argument, method=method, default=False) if allowed_error: return allowed_error if not allowed: return invalid_argument(method, allow_argument, f"Pass {allow_argument}=true after reviewing the exact Configurator user target.") if not adapter_service_token() and not infobase_user_password_unauthenticated_test_mode(): return { "schema": "onec_infobase_user_password_change.v1", "status": "blocked", "base_id": base_id, "operation": operation, "error": "service_auth_required", "diagnostics": {"message": "Anonymous password mutations are forbidden. Configure ONEC_ADAPTER_SERVICE_TOKEN and the matching MCP backend token first."}, } selector = str(payload.get("user") or payload.get("name") or payload.get("id") or "").strip() confirm_user_id = re.sub(r"[^0-9a-f]", "", str(payload.get("confirm_user_id") or "").casefold()) if not selector: return invalid_argument(method, "user", "Pass the exact Configurator user name or platform id.") if len(confirm_user_id) != 32: return invalid_argument(method, "confirm_user_id", "Pass the exact 32-hex platform user id returned by infobase.user.get.") new_password: str | None = None if operation == "set": if not isinstance(payload.get("new_password"), str): return invalid_argument(method, "new_password", "new_password must be a JSON string.") new_password = str(payload.get("new_password")) if not new_password: return invalid_argument(method, "new_password", "Use infobase.user.password.clear for an empty password.") if len(new_password) > 1024 or "\x00" in new_password: return invalid_argument(method, "new_password", "new_password must contain 1..1024 characters and no NUL characters.") elif "new_password" in payload: return invalid_argument(method, "new_password", "Do not pass new_password to the clear operation.") selected = infobase_user_get({"base_id": base_id, "user": selector, "scan_limit": payload.get("scan_limit", 5000), "timeout_seconds": payload.get("timeout_seconds", 30)}) if selected.get("status") != "ok": return { "schema": "onec_infobase_user_password_change.v1", "status": "blocked", "base_id": base_id, "operation": operation, "error": "exact_user_required", "selection": {key: selected.get(key) for key in ("status", "query", "candidates") if selected.get(key) is not None}, } user = selected.get("user") if isinstance(selected.get("user"), dict) else {} actual_user_id = re.sub(r"[^0-9a-f]", "", str(user.get("id") or "").casefold()) if actual_user_id != confirm_user_id: return { "schema": "onec_infobase_user_password_change.v1", "status": "blocked", "base_id": base_id, "operation": operation, "error": "user_confirmation_mismatch", "target": {"id": user.get("id"), "name": user.get("name")}, } if user.get("platform_administrator") is True: allow_administrator, administrator_error = strict_bool_argument(payload, "allow_administrator_password_change", method=method, default=False) if administrator_error: return administrator_error if not allow_administrator: return invalid_argument( method, "allow_administrator_password_change", "The selected user is a platform administrator. Pass allow_administrator_password_change=true after confirming an alternate administrator remains available.", ) timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1, maximum=120) if timeout_error: return timeout_error request_id = str(payload.get("request_id") or uuid.uuid4()) if operation == "clear": return infobase_user_password_clear_sql( base_id, user, request_id=request_id, timeout_seconds=int(timeout_seconds), ) return infobase_user_password_set_sql( base_id, user, new_password=str(new_password), request_id=request_id, timeout_seconds=int(timeout_seconds), ) def access_user_type_label(user_id: Any, diagnostics: dict[str, Any] | None = None) -> str: parts = access_identifier_parts(user_id) if not parts or not parts.get("type_code"): return "unknown" type_code = str(parts.get("type_code") or "").upper() sql_numbers = (((diagnostics or {}).get("bsp") or {}).get("metadata") or {}).get("sql_numbers") if isinstance(diagnostics, dict) else None if not isinstance(sql_numbers, dict): return "unknown" labels = { "users": "user", "external_users": "external_user", "user_groups": "user_group", } for key, label in labels.items(): try: if type_code == f"{int(sql_numbers.get(key)):08X}": return label except (TypeError, ValueError): continue return "unknown" def access_user_match_score(user: dict[str, Any], selector: str) -> int: selector_text = str(selector or "").strip().casefold() selector_tail = access_ref_tail(selector).casefold() user_id = str(user.get("id") or "") user_name = str(user.get("name") or "") user_id_text = user_id.casefold() user_name_text = user_name.casefold() if selector_tail and selector_tail == access_ref_tail(user_id).casefold(): return 10000 if selector_text and selector_text in {user_id_text, user_name_text}: return 9000 if selector_text and (selector_text in user_name_text or selector_text in user_id_text): return 8000 + len(selector_text) tokens = access_role_search_tokens(selector) if tokens: haystack = f"{user_name_text} {user_id_text}" matched = [token for token in tokens if token in haystack] if matched: return len(matched) * 100 + (500 if len(matched) == len(tokens) else 0) ratio = difflib.SequenceMatcher(None, selector_text, user_name_text).ratio() if selector_text and user_name_text else 0 return int(ratio * 100) def access_compact_user_candidate(user: dict[str, Any], *, score: int | None = None) -> dict[str, Any]: result = { "id": user.get("id"), "name": user.get("name"), "active": user.get("active"), "marked": user.get("marked"), "user_type": user.get("user_type"), } if score is not None: result["match_score"] = score return {key: value for key, value in result.items() if value not in {None, ""}} def access_nearest_users_from_graph(graph: dict[str, Any], selector: str, *, limit: int = 10) -> list[dict[str, Any]]: candidates: list[tuple[int, dict[str, Any]]] = [] seen: set[str] = set() for item in graph.get("effective_users") or []: if not isinstance(item, dict): continue user = item.get("user") if isinstance(item.get("user"), dict) else {} key = access_ref_tail(user.get("id")) or str(user.get("name") or "") if not key or key in seen: continue seen.add(key) score = access_user_match_score(user, selector) candidates.append((score, user)) candidates.sort(key=lambda item: (-item[0], str(item[1].get("name") or item[1].get("id") or ""))) return [access_compact_user_candidate(user, score=score) for score, user in candidates[:limit] if score > 0] def access_users_search(payload: dict[str, Any]) -> dict[str, Any]: method = "access.users.search" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error selector = str(payload.get("query") or payload.get("user") or payload.get("name") or "").strip() if not selector: return invalid_argument(method, "query", "Pass query, user, or name.") limit, limit_error = parse_int_argument(payload, "limit", method=method, default=20, minimum=1, maximum=200) if limit_error: return limit_error scan_limit, scan_limit_error = parse_int_argument(payload, "scan_limit", method=method, default=20000, minimum=1, maximum=50000) if scan_limit_error: return scan_limit_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=120, minimum=1, maximum=120) if timeout_error: return timeout_error queries, mappings, diagnostics = access_bsp_extractor_plan(base_id) if not queries or not mappings or not queries.get("users"): return {"schema": "onec_access_users_search.v1", "status": "error", "base_id": base_id, "diagnostics": (diagnostics or {}).get("diagnostics") or diagnostics} rows, error, truncated = access_rows_from_query(base_id, queries["users"], limit=int(scan_limit), timeout_seconds=int(timeout_seconds)) if error: return {"schema": "onec_access_users_search.v1", **error} mapping = mappings.get("users") if isinstance(mappings.get("users"), dict) else None users = [access_map_row(row, mapping) for row in rows] scored = [(access_user_match_score(user, selector), user) for user in users if isinstance(user, dict)] scored.sort(key=lambda item: (-item[0], str(item[1].get("name") or item[1].get("id") or ""))) matches = [access_compact_user_candidate(user, score=score) for score, user in scored if score >= 100][: int(limit)] nearest = [access_compact_user_candidate(user, score=score) for score, user in scored if score > 0][: int(limit)] return { "schema": "onec_access_users_search.v1", "status": "ok" if matches else "not_found", "base_id": base_id, "user_kind": "bsp_catalog_user", "terminology": { "default_user_meaning": "infobase_user", "this_result_is": "bsp_catalog_user", "authoritative_for_platform_roles": False, "message": "This method searches BSP catalog users. It does not search dbo.v8users and must not be used as proof of Configurator authentication or direct platform role assignments.", "default_user_method": "infobase.users.search", }, "query": {"user": selector}, "users": matches, "nearest": nearest, "counts": {"users": len(matches), "nearest": len(nearest), "scanned": len(users), "truncated": bool(truncated)}, "diagnostics": {"bsp": diagnostics}, } def access_role_audit_csv(rows: list[dict[str, Any]]) -> str: output = io.StringIO() writer = csv.DictWriter(output, fieldnames=ACCESS_ROLE_AUDIT_EXPORT_COLUMNS, extrasaction="ignore", lineterminator="\n") writer.writeheader() for row in rows: writer.writerow(row) return output.getvalue() def access_role_audit_export(payload: dict[str, Any]) -> dict[str, Any]: method = "access.role.audit_export" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error role_selector = str(payload.get("role") or payload.get("role_id") or payload.get("role_name") or payload.get("query") or "").strip() if not role_selector: return invalid_argument(method, "role", "Pass role, role_id, role_name, or query.") export_format = str(payload.get("format") or "json").strip().casefold() if export_format not in {"json", "csv"}: return invalid_argument(method, "format", "format must be one of: json, csv.", allowed_values=["json", "csv"]) limit, limit_error = parse_int_argument(payload, "limit", method=method, default=20000, minimum=1, maximum=20000) if limit_error: return limit_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=120, minimum=1, maximum=120) if timeout_error: return timeout_error chain = access_role_chain(base_id, role_selector, timeout_seconds=int(timeout_seconds)) if chain.get("status") == "error": return {"schema": "onec_access_role_audit_export.v1", **chain} if chain.get("status") == "not_found": return {"schema": "onec_access_role_audit_export.v1", **chain, "format": export_format, "rows": []} generated_at = datetime.now(timezone.utc).isoformat() roles = chain.get("roles") if isinstance(chain.get("roles"), list) else [] profiles = chain.get("profiles") if isinstance(chain.get("profiles"), list) else [] groups = chain.get("groups") if isinstance(chain.get("groups"), list) else [] users = chain.get("users") if isinstance(chain.get("users"), list) else [] profiles_by_id = {access_ref_tail(profile.get("id")): profile for profile in profiles if isinstance(profile, dict)} groups_by_id = {access_ref_tail(group.get("id")): group for group in groups if isinstance(group, dict)} rows: list[dict[str, Any]] = [] for role in roles: role_id = role.get("id") role_tail = access_ref_tail(role_id) role_profiles = [ profile for profile in profiles_by_id.values() if role_tail in {access_ref_tail(item) for item in profile.get("roles") or []} ] or profiles for profile in role_profiles: profile_id = profile.get("id") profile_tail = access_ref_tail(profile_id) profile_groups = [ group for group in groups_by_id.values() if profile_tail in {access_ref_tail(item) for item in group.get("profiles") or []} ] or groups for group in profile_groups: group_tail = access_ref_tail(group.get("id")) group_users = [ user for user in users if any(access_ref_tail(user_group.get("id")) == group_tail for user_group in user.get("groups") or []) ] if not group_users: group_users = [{"id": "", "name": ""}] for user in group_users: access_path = " -> ".join( str(part or "") for part in (role.get("name"), profile.get("name"), group.get("name"), user.get("name")) if part not in {None, ""} ) rows.append( { "base_id": base_id, "generated_at": generated_at, "query_role": role_selector, "matched_role_id": role_id, "matched_role_name": role.get("name"), "profile_id": profile_id, "profile_name": profile.get("name"), "group_id": group.get("id"), "group_name": group.get("name"), "user_id": user.get("id"), "user_name": user.get("name"), "user_type": user.get("user_type") or access_user_type_label(user.get("id"), chain.get("diagnostics") if isinstance(chain.get("diagnostics"), dict) else None), "user_active": user.get("active") if user.get("active") is not None else True, "user_marked": bool(user.get("marked")) if user.get("marked") is not None else False, "user_groups_count": len(user.get("groups") or []), "access_path": access_path, } ) direct_source_types = {"direct_user_role", "group_role"} role_by_tail = {access_ref_tail(role.get("id")): role for role in roles if isinstance(role, dict)} for user in users: if not isinstance(user, dict): continue for source in user.get("role_sources") or []: if not isinstance(source, dict) or source.get("type") not in direct_source_types: continue role_tail = access_ref_tail(source.get("role")) role = role_by_tail.get(role_tail) or {"id": source.get("role"), "name": source.get("role_name")} group = groups_by_id.get(access_ref_tail(source.get("group"))) if source.get("group") not in {None, ""} else None access_path = " -> ".join( str(part or "") for part in (role.get("name"), (group or {}).get("name"), user.get("name")) if part not in {None, ""} ) rows.append( { "base_id": base_id, "generated_at": generated_at, "query_role": role_selector, "matched_role_id": role.get("id"), "matched_role_name": role.get("name"), "profile_id": "", "profile_name": "", "group_id": (group or {}).get("id") or source.get("group") or "", "group_name": (group or {}).get("name") or source.get("group") or "", "user_id": user.get("id"), "user_name": user.get("name"), "user_type": user.get("user_type") or access_user_type_label(user.get("id"), chain.get("diagnostics") if isinstance(chain.get("diagnostics"), dict) else None), "user_active": user.get("active") if user.get("active") is not None else True, "user_marked": bool(user.get("marked")) if user.get("marked") is not None else False, "user_groups_count": len(user.get("groups") or []), "access_path": access_path, } ) for row in rows: if row.get("access_path") in {None, ""}: row["access_path"] = " -> ".join( str(row.get(key) or "") for key in ("matched_role_name", "profile_name", "group_name", "user_name") if row.get(key) not in {None, ""} ) deduped_rows: list[dict[str, Any]] = [] seen_rows: set[tuple[str, str, str, str]] = set() for row in rows: key = ( str(row.get("matched_role_id") or ""), str(row.get("profile_id") or ""), str(row.get("group_id") or ""), str(row.get("user_id") or ""), ) if key in seen_rows: continue seen_rows.add(key) deduped_rows.append(row) rows = deduped_rows rows = sorted(rows, key=lambda row: (str(row.get("matched_role_name") or ""), str(row.get("profile_name") or ""), str(row.get("group_name") or ""), str(row.get("user_name") or "")))[: int(limit)] result = { "schema": "onec_access_role_audit_export.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "preset": "bsp"}, "format": export_format, "generated_at": generated_at, "query": {"role": role_selector}, "summary": { "text": ( f"Role audit export: {len(roles)} role matches, {len(profiles)} profiles, " f"{len(groups)} access groups, {len(users)} users, {len(rows)} rows." ) }, "columns": ACCESS_ROLE_AUDIT_EXPORT_COLUMNS, "rows": rows, "roles": roles, "profiles": sorted(profiles, key=lambda item: str(item.get("name") or item.get("id"))), "groups": sorted(groups, key=lambda item: str(item.get("name") or item.get("id"))), "users": sorted(users, key=lambda item: str(item.get("name") or item.get("id")))[: int(limit)], "alternatives": chain.get("alternatives") or [], "counts": {"roles": len(roles), "profiles": len(profiles), "groups": len(groups), "users": len(users), "rows": len(rows)}, "diagnostics": chain.get("diagnostics") or {}, } if export_format == "csv": result["content_type"] = "text/csv; charset=utf-8" result["csv"] = access_role_audit_csv(rows) return result def access_role_audit_risk_level(findings: list[dict[str, Any]]) -> str: severities = {str(item.get("severity") or "") for item in findings} if "high" in severities: return "high" if "medium" in severities: return "medium" return "low" def access_role_audit_analyze(payload: dict[str, Any]) -> dict[str, Any]: method = "access.role.audit_analyze" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error role_selector = str(payload.get("role") or payload.get("role_id") or payload.get("role_name") or payload.get("query") or "").strip() if not role_selector: return invalid_argument(method, "role", "Pass role, role_id, role_name, or query.") user_threshold, user_threshold_error = parse_int_argument(payload, "user_threshold", method=method, default=50, minimum=1, maximum=100000) if user_threshold_error: return user_threshold_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=120, minimum=1, maximum=120) if timeout_error: return timeout_error export = access_role_audit_export({**payload, "format": "json", "limit": 20000, "timeout_seconds": int(timeout_seconds)}) if export.get("status") != "ok": return {"schema": "onec_access_role_audit_analyze.v1", **export} rows = export.get("rows") if isinstance(export.get("rows"), list) else [] findings: list[dict[str, Any]] = [] users = export.get("users") if isinstance(export.get("users"), list) else [] groups = export.get("groups") if isinstance(export.get("groups"), list) else [] profiles = export.get("profiles") if isinstance(export.get("profiles"), list) else [] alternatives = export.get("alternatives") if isinstance(export.get("alternatives"), list) else [] if len(users) >= int(user_threshold): findings.append( { "severity": "medium", "code": "many_users", "message": f"Роль получают {len(users)} пользователей; порог аудита {int(user_threshold)}.", "details": {"users": len(users), "threshold": int(user_threshold)}, } ) if len(alternatives) > 1: findings.append( { "severity": "low", "code": "fuzzy_role_alternatives", "message": f"По фразе найдено {len(alternatives)} похожих ролей; проверьте выбранное совпадение.", "details": {"alternatives": alternatives[:5]}, } ) broad_group_terms = ("администратор", "полные права", "full", "admin") broad_groups = [group for group in groups if any(term in str(group.get("name") or "").casefold() for term in broad_group_terms)] if broad_groups: findings.append( { "severity": "high", "code": "broad_access_group", "message": "Роль назначена через широкую или административную группу доступа.", "details": {"groups": [{"id": group.get("id"), "name": group.get("name")} for group in broad_groups]}, } ) external_rows = [row for row in rows if row.get("user_type") == "external_user"] if external_rows: findings.append( { "severity": "medium", "code": "external_users", "message": f"Роль получают внешние пользователи: {len({row.get('user_id') for row in external_rows})}.", "details": {"users": sorted({str(row.get("user_name") or row.get("user_id")) for row in external_rows})[:20]}, } ) user_group_rows = [row for row in rows if row.get("user_type") == "user_group"] if user_group_rows: findings.append( { "severity": "medium", "code": "user_group_subjects", "message": f"В отчете есть группы пользователей как субъекты доступа: {len({row.get('user_id') for row in user_group_rows})}.", "details": {"subjects": sorted({str(row.get("user_name") or row.get("user_id")) for row in user_group_rows})[:20]}, } ) inactive_rows = [row for row in rows if row.get("user_active") is False or row.get("user_marked") is True] if inactive_rows: findings.append( { "severity": "high", "code": "inactive_or_marked_users", "message": "Неактивные или помеченные пользователи попали в цепочку роли.", "details": {"users": sorted({str(row.get("user_name") or row.get("user_id")) for row in inactive_rows})[:20]}, } ) paths_by_user: dict[str, set[str]] = {} for row in rows: user_id = str(row.get("user_id") or "") if not user_id: continue paths_by_user.setdefault(user_id, set()).add(str(row.get("access_path") or "")) multiple_path_users = [user_id for user_id, paths in paths_by_user.items() if len(paths) > 1] if multiple_path_users: findings.append( { "severity": "low", "code": "multiple_access_paths", "message": f"Некоторые пользователи получают роль несколькими путями: {len(multiple_path_users)}.", "details": {"user_ids": multiple_path_users[:20]}, } ) profile_group_counts: dict[str, set[str]] = {} for row in rows: profile_name = str(row.get("profile_name") or "") group_name = str(row.get("group_name") or "") if profile_name and group_name: profile_group_counts.setdefault(profile_name, set()).add(group_name) multi_group_profiles = {profile: sorted(groups_set) for profile, groups_set in profile_group_counts.items() if len(groups_set) > 1} if multi_group_profiles: findings.append( { "severity": "low", "code": "profile_used_by_multiple_groups", "message": "Один или несколько профилей используются несколькими группами доступа.", "details": {"profiles": multi_group_profiles}, } ) risk_level = access_role_audit_risk_level(findings) return { "schema": "onec_access_role_audit_analyze.v1", "status": "ok", "base_id": export.get("base_id"), "source": export.get("source"), "query": export.get("query"), "summary": { "text": ( f"Role audit analysis: risk={risk_level}, {len(findings)} findings, " f"{len(users)} users, {len(groups)} groups, {len(profiles)} profiles." ) }, "risk_level": risk_level, "findings": findings, "counts": {**(export.get("counts") if isinstance(export.get("counts"), dict) else {}), "findings": len(findings)}, "roles": export.get("roles") or [], "profiles": profiles, "groups": groups, "users": users, "alternatives": alternatives, "diagnostics": export.get("diagnostics") or {}, } def build_access_graph_from_snapshot( access: dict[str, Any], *, base_id: str | None = None, max_permissions_per_user: int | None = None, resolve_identifiers: bool = False, ) -> dict[str, Any]: users: dict[str, dict[str, Any]] = {} groups: dict[str, dict[str, Any]] = {} profiles: dict[str, dict[str, Any]] = {} roles: dict[str, dict[str, Any]] = {} restrictions: list[dict[str, Any]] = [] access_keys = access.get("access_keys") if isinstance(access.get("access_keys"), dict) else {} identifier_resolution: dict[str, Any] | None = None if resolve_identifiers and base_id: identifier_resolution = access_enrich_snapshot_identifiers(access, str(base_id)) access_group_keys = [item for item in access_list(access_keys.get("access_group_keys") if access_keys else None) if isinstance(item, dict)] access_user_keys = [item for item in access_list(access_keys.get("access_user_keys") if access_keys else None) if isinstance(item, dict)] access_object_keys = [item for item in access_list(access_keys.get("access_object_keys") if access_keys else None) if isinstance(item, dict)] access_object_keys_by_key: dict[str, list[dict[str, Any]]] = {} for item in access_object_keys: key = access_ref_tail(item.get("access_key")) if key: access_object_keys_by_key.setdefault(key, []).append(item) access_key_sample_limit = 20 def access_keys_for_user(user_id: str, seen_groups: set[str]) -> dict[str, Any]: if not access_keys: return {"counts": {"group_keys": 0, "user_keys": 0, "object_keys": 0, "total": 0}, "samples": {"group_keys": [], "user_keys": [], "object_keys": []}} seen_group_refs = {access_ref_tail(group_id) for group_id in seen_groups} user_ref = access_ref_tail(user_id) group_matches = [item for item in access_group_keys if access_ref_tail(item.get("group") or item.get("group_ref")) in seen_group_refs] user_matches = [item for item in access_user_keys if access_ref_tail(item.get("user")) == user_ref] subject_key_refs = {access_ref_tail(item.get("access_key")) for item in [*group_matches, *user_matches] if item.get("access_key") not in {None, ""}} object_matches: list[dict[str, Any]] = [] for key_ref in subject_key_refs: object_matches.extend(access_object_keys_by_key.get(key_ref, [])) object_type_codes = {str(item.get("object_type_code") or "").upper() for item in object_matches if item.get("object_type_code") not in {None, ""}} object_sql_numbers = {int(item.get("object_sql_number")) for item in object_matches if isinstance(item.get("object_sql_number"), int)} return { "counts": { "group_keys": len(group_matches), "user_keys": len(user_matches), "object_keys": len(object_matches), "object_key_types": len(object_type_codes or {str(number) for number in object_sql_numbers}), "total": len(group_matches) + len(user_matches), }, "samples": { "group_keys": group_matches[:access_key_sample_limit], "user_keys": user_matches[:access_key_sample_limit], "object_keys": object_matches[:access_key_sample_limit], }, } def ensure_user(raw: Any) -> dict[str, Any]: raw_item = raw if isinstance(raw, dict) else {"id": access_item_id(raw), "name": access_item_id(raw)} user_id = access_item_id(raw_item) item = users.setdefault( user_id, { "id": user_id, "name": access_item_name(raw_item, user_id), "active": access_bool(raw_item.get("active"), True) and not access_bool(raw_item.get("disabled") or raw_item.get("blocked"), False), "marked": access_bool(raw_item.get("marked"), False), "user_type": str(raw_item.get("user_type") or raw_item.get("type") or "").strip() or None, "service": access_bool(raw_item.get("service") or raw_item.get("is_service"), False), "administrator": access_bool(raw_item.get("administrator") or raw_item.get("admin") or raw_item.get("full_access"), False), "groups": [], "roles": [], }, ) candidate_name = access_item_name(raw_item, user_id) if candidate_name and access_name_is_placeholder(item.get("name"), user_id) and not access_name_is_placeholder(candidate_name, user_id): item["name"] = candidate_name if raw_item.get("marked") not in {None, ""}: item["marked"] = access_bool(raw_item.get("marked"), False) if raw_item.get("user_type") not in {None, ""}: item["user_type"] = str(raw_item.get("user_type") or "").strip() for source_key, target_key in (("service", "service"), ("is_service", "service"), ("administrator", "administrator"), ("admin", "administrator"), ("full_access", "administrator")): if raw_item.get(source_key) not in {None, ""}: item[target_key] = access_bool(raw_item.get(source_key), False) for group_ref in access_pick_list(raw_item, "groups", "access_groups", "group_refs"): group_id = access_item_id(group_ref) if group_id and group_id not in item["groups"]: item["groups"].append(group_id) for role_ref in access_pick_list(raw_item, "roles", "direct_roles"): role_id = access_item_id(role_ref) if role_id and role_id not in item["roles"]: item["roles"].append(role_id) return item def ensure_group(raw: Any) -> dict[str, Any]: raw_item = raw if isinstance(raw, dict) else {"id": access_item_id(raw), "name": access_item_id(raw)} group_id = access_item_id(raw_item) item = groups.setdefault( group_id, { "id": group_id, "name": access_item_name(raw_item, group_id), "active": access_bool(raw_item.get("active"), True) and not access_bool(raw_item.get("disabled") or raw_item.get("inactive"), False), "users": [], "profiles": [], "roles": [], "parent_groups": [], }, ) for user_ref in access_pick_list(raw_item, "users", "members", "user_refs"): user_id = access_item_id(user_ref) if user_id and user_id not in item["users"]: item["users"].append(user_id) ensure_user(user_ref if isinstance(user_ref, dict) else {"id": user_id, "name": user_id}) if group_id not in users[user_id]["groups"]: users[user_id]["groups"].append(group_id) for profile_ref in access_pick_list(raw_item, "profiles", "access_profiles", "profile_refs"): profile_id = access_item_id(profile_ref) if profile_id and profile_id not in item["profiles"]: item["profiles"].append(profile_id) for role_ref in access_pick_list(raw_item, "roles", "direct_roles"): role_id = access_item_id(role_ref) if role_id and role_id not in item["roles"]: item["roles"].append(role_id) for parent_ref in access_pick_list(raw_item, "parent_groups", "parents", "groups"): parent_id = access_item_id(parent_ref) if parent_id and parent_id != group_id and parent_id not in item["parent_groups"]: item["parent_groups"].append(parent_id) return item def ensure_profile(raw: Any) -> dict[str, Any]: raw_item = raw if isinstance(raw, dict) else {"id": access_item_id(raw), "name": access_item_id(raw)} profile_id = access_item_id(raw_item) item = profiles.setdefault(profile_id, {"id": profile_id, "name": access_item_name(raw_item, profile_id), "roles": []}) for role_ref in access_pick_list(raw_item, "roles", "role_refs"): role_id = access_item_id(role_ref) if role_id and role_id not in item["roles"]: item["roles"].append(role_id) return item def ensure_role(raw: Any) -> dict[str, Any]: raw_item = raw if isinstance(raw, dict) else {"id": access_item_id(raw), "name": access_item_id(raw)} role_id = access_item_id(raw_item) item = roles.setdefault( role_id, { "id": role_id, "name": access_item_name(raw_item, role_id), "elevated": access_bool(raw_item.get("elevated") or raw_item.get("administrator") or raw_item.get("full_access"), False), "permissions": [], }, ) for raw_permission in access_pick_list(raw_item, "permissions", "rights", "object_permissions"): for permission in access_normalize_permission(raw_permission): if permission not in item["permissions"]: item["permissions"].append(permission) return item for raw_user in access_list(access.get("users")): ensure_user(raw_user) for key_row in access_user_keys: user_ref = str(key_row.get("user") or "").strip() user_name = str(key_row.get("user_set_name") or "").strip() if user_ref and user_name: ensure_user({"id": user_ref, "name": user_name}) for raw_group in access_list(access.get("groups") or access.get("access_groups")): ensure_group(raw_group) for raw_profile in access_list(access.get("profiles") or access.get("access_profiles")): ensure_profile(raw_profile) for raw_role in access_list(access.get("roles")): ensure_role(raw_role) for assignment in access_list(access.get("assignments")): if not isinstance(assignment, dict): continue for subject in access_subject_refs(assignment.get("subjects") or assignment.get("subject"), assignment.get("subject_type")): target = users.get(subject["id"]) if subject["type"] in {"", "user", "User", "пользователь"} else groups.get(subject["id"]) if target is None and subject["type"] in {"", "user", "User", "пользователь"}: target = ensure_user({"id": subject["id"], "name": subject["id"]}) elif target is None: target = ensure_group({"id": subject["id"], "name": subject["id"]}) for group_ref in access_pick_list(assignment, "groups", "access_groups"): group_id = access_item_id(group_ref) if group_id and subject["type"] in {"", "user", "User", "пользователь"} and group_id not in target["groups"]: target["groups"].append(group_id) for profile_ref in access_pick_list(assignment, "profiles", "access_profiles"): profile_id = access_item_id(profile_ref) if profile_id and "profiles" in target and profile_id not in target["profiles"]: target["profiles"].append(profile_id) for role_ref in access_pick_list(assignment, "roles"): role_id = access_item_id(role_ref) if role_id and role_id not in target["roles"]: target["roles"].append(role_id) for raw_restriction in access_list(access.get("data_restrictions") or access.get("restrictions") or access.get("rls")): if not isinstance(raw_restriction, dict): continue subject_type = str(raw_restriction.get("subject_type") or raw_restriction.get("type") or "").strip() subject_id = str(raw_restriction.get("subject_id") or raw_restriction.get("id") or raw_restriction.get("subject") or "").strip() restrictions.append( { **raw_restriction, "subject_type": subject_type, "subject_id": subject_id, "dimension": str(raw_restriction.get("dimension") or raw_restriction.get("kind") or raw_restriction.get("object") or ""), "values": access_list(raw_restriction.get("values") or raw_restriction.get("value")), } ) effective_users: list[dict[str, Any]] = [] for user_id, user in users.items(): role_sources: dict[str, list[dict[str, Any]]] = {} def add_role(role_id: str, source: dict[str, Any]) -> None: if not role_id: return if role_id not in roles: ensure_role({"id": role_id, "name": role_id}) role_sources.setdefault(role_id, []).append(source) for role_id in user.get("roles") or []: add_role(str(role_id), {"type": "direct_user_role", "user": user_id}) group_queue = list(user.get("groups") or []) seen_groups: set[str] = set() while group_queue: group_id = str(group_queue.pop(0)) if group_id in seen_groups: continue seen_groups.add(group_id) group = groups.get(group_id) if not group: group = ensure_group({"id": group_id, "name": group_id}) for role_id in group.get("roles") or []: add_role(str(role_id), {"type": "group_role", "group": group_id}) for profile_id in group.get("profiles") or []: profile = profiles.get(str(profile_id)) or ensure_profile({"id": profile_id, "name": profile_id}) for role_id in profile.get("roles") or []: add_role(str(role_id), {"type": "group_profile_role", "group": group_id, "profile": str(profile_id)}) for parent_id in group.get("parent_groups") or []: if parent_id not in seen_groups: group_queue.append(str(parent_id)) permission_map: dict[tuple[str, str, str], dict[str, Any]] = {} for role_id, sources in role_sources.items(): role = roles.get(role_id) or {} for permission in role.get("permissions") or []: key = access_permission_key(permission, str(permission.get("action") or "*")) entry = permission_map.setdefault( key, { "object": permission.get("object") or "*", "action": permission.get("action") or "*", **({"scope": permission.get("scope")} if permission.get("scope") else {}), **({"object_name": permission.get("object_name")} if permission.get("object_name") else {}), **({"object_kind": permission.get("object_kind")} if permission.get("object_kind") else {}), **({"object_full_name": permission.get("object_full_name")} if permission.get("object_full_name") else {}), **({"object_resolution": permission.get("object_resolution")} if permission.get("object_resolution") else {}), "sources": [], }, ) entry["sources"].append({"role": role_id, "role_name": role.get("name"), "chains": sources}) user_restrictions = [ item for item in restrictions if (item.get("subject_type") in {"", "user", "User", "пользователь"} and item.get("subject_id") == user_id) or (item.get("subject_type") in {"group", "Group", "access_group", "группа"} and item.get("subject_id") in seen_groups) ] permissions = sorted(permission_map.values(), key=lambda item: (str(item.get("object")), str(item.get("action")))) permissions_total = len(permissions) permissions_truncated = False if max_permissions_per_user is not None and max_permissions_per_user >= 0 and len(permissions) > max_permissions_per_user: permissions = permissions[:max_permissions_per_user] permissions_truncated = True user_access_keys = access_keys_for_user(user_id, seen_groups) effective_users.append( { "user": {key: user.get(key) for key in ("id", "name", "active", "marked", "user_type", "service", "administrator")}, "groups": sorted(seen_groups), "roles": [{"id": role_id, "name": (roles.get(role_id) or {}).get("name"), "sources": sources} for role_id, sources in sorted(role_sources.items())], "permissions": permissions, "permission_counts": {"total": permissions_total, "returned": len(permissions), "truncated": permissions_truncated}, "access_keys": user_access_keys, "data_restrictions": user_restrictions, } ) return { "schema": "onec_access_graph.v1", "status": "ok", "base_id": base_id, "source": {"kind": "access_snapshot"}, "users": sorted(users.values(), key=lambda item: item.get("name") or item.get("id")), "groups": sorted(groups.values(), key=lambda item: item.get("name") or item.get("id")), "profiles": sorted(profiles.values(), key=lambda item: item.get("name") or item.get("id")), "roles": sorted(roles.values(), key=lambda item: item.get("name") or item.get("id")), **({"access_keys": access_keys} if access_keys else {}), "effective_users": sorted(effective_users, key=lambda item: (item.get("user") or {}).get("name") or (item.get("user") or {}).get("id")), "counts": { "users": len(users), "groups": len(groups), "profiles": len(profiles), "roles": len(roles), "data_restrictions": len(restrictions), **( { "access_group_keys": len(access_list(access_keys.get("access_group_keys"))), "access_user_keys": len(access_list(access_keys.get("access_user_keys"))), "access_object_keys": len(access_list(access_keys.get("access_object_keys"))), "access_set_keys": len(access_list(access_keys.get("access_set_keys"))), } if access_keys else {} ), "effective_users": len(effective_users), "effective_permissions_total": sum((item.get("permission_counts") or {}).get("total") or 0 for item in effective_users), "effective_permissions_returned": sum((item.get("permission_counts") or {}).get("returned") or 0 for item in effective_users), "effective_users_permissions_truncated": sum(1 for item in effective_users if (item.get("permission_counts") or {}).get("truncated")), }, "diagnostics": { "note": "Access graph is computed from provided normalized access snapshot data. Live extraction from configuration-specific registers can feed this same schema.", **({"identifier_resolution": identifier_resolution} if identifier_resolution else {}), }, } def access_graph_build(payload: dict[str, Any]) -> dict[str, Any]: access, error = access_snapshot_from_payload(payload, "access.graph.build") if error: return error max_permissions, max_permissions_error = parse_int_argument(payload, "max_effective_permissions_per_user", method="access.graph.build", default=None, minimum=0, maximum=200000) if max_permissions_error: return max_permissions_error resolve_identifiers, resolve_identifiers_error = strict_bool_argument(payload, "resolve_identifiers", method="access.graph.build", default=True) if resolve_identifiers_error: return resolve_identifiers_error return build_access_graph_from_snapshot( access or {}, base_id=payload.get("base_id"), max_permissions_per_user=max_permissions, resolve_identifiers=bool(resolve_identifiers), ) def access_user_explain_from_graph(payload: dict[str, Any], graph: dict[str, Any], user_selector: str) -> dict[str, Any]: method = "access.user.explain" object_filter = str(payload.get("object") or payload.get("object_ref") or "").strip() object_filter_cf = object_filter.casefold() object_filter_selector = str(payload.get("_object_filter_selector") or object_filter).strip() object_filter_guid = str(payload.get("_object_filter_guid") or "").strip().lower() or None object_filter_payload = payload.get("_object_filter_payload") if isinstance(payload.get("_object_filter_payload"), dict) else None action_filter_keys = access_action_filter_keys(payload.get("action") or payload.get("right")) selected = None selector_cf = user_selector.casefold() selector_tail = access_ref_tail(user_selector).casefold() for item in graph.get("effective_users") or []: user = item.get("user") or {} user_id = str(user.get("id") or "") user_name = str(user.get("name") or "") if selector_cf in {user_id.casefold(), user_name.casefold()} or selector_tail == access_ref_tail(user_id).casefold(): selected = item break if not selected: nearest_users = access_nearest_users_from_graph(graph, user_selector, limit=10) return { "schema": "onec_access_user_explain.v1", "status": "not_found", "error": "not_found", "base_id": payload.get("base_id"), "query": {"user": user_selector}, "nearest_users": nearest_users, "diagnostics": { "message": "User was not found in the provided access snapshot or live extraction.", "hint": "Use access.users.search/access_users_search to find the exact user name or ref.", }, } permissions = [] object_permissions_all_actions: list[dict[str, Any]] = [] for permission in selected.get("permissions") or []: if object_filter: if object_filter_payload: if not access_permission_matches_object(permission, selector=object_filter_selector, object_guid=object_filter_guid, object_payload=object_filter_payload): continue else: object_haystack = " ".join(str(permission.get(key) or "") for key in ("object", "object_name", "object_full_name")).casefold() if object_filter_cf not in object_haystack: continue object_permissions_all_actions.append(permission) if not access_permission_matches_action(permission, action_filter_keys): continue permissions.append(permission) access_keys = selected.get("access_keys") if isinstance(selected.get("access_keys"), dict) else {} resolve_records = payload.get("resolve_records") is True if resolve_records and payload.get("base_id") and access_keys: samples = access_keys.get("samples") if isinstance(access_keys.get("samples"), dict) else {} object_samples = samples.get("object_keys") if isinstance(samples.get("object_keys"), list) else [] resolution = access_resolve_object_key_records( str(payload.get("base_id")), object_samples, timeout_seconds=int(payload.get("timeout_seconds") if isinstance(payload.get("timeout_seconds"), int) else 60), max_records=int(payload.get("max_resolved_records") if isinstance(payload.get("max_resolved_records"), int) else 200), ) access_keys = { **access_keys, "samples": {**samples, "object_keys": resolution.get("rows") or object_samples}, "record_resolution": resolution.get("diagnostics"), } permission_object_names = sorted({str(item.get("object_name") or item.get("object") or "") for item in permissions if item.get("object_name") or item.get("object")}) summary = { "text": ( f"{(selected.get('user') or {}).get('name') or user_selector}: " f"{len(selected.get('groups') or [])} groups, {len(selected.get('roles') or [])} roles, " f"{len(permissions)} returned permissions" + (f", {(access_keys.get('counts') or {}).get('total', 0)} subject access keys" if access_keys else "") + (f", {(access_keys.get('counts') or {}).get('object_keys', 0)} matching object keys" if access_keys else "") + "." ), "permission_objects": permission_object_names[:20], } available_actions_for_object = access_permission_rights(object_permissions_all_actions) if object_filter else None if object_filter and action_filter_keys and not permissions and object_permissions_all_actions: summary["hint"] = "No permissions matched requested action, but the user has other rights for this object." return { "schema": "onec_access_user_explain.v1", "status": "ok", "base_id": payload.get("base_id"), "source": graph.get("source"), "query": {"user": user_selector, "object": payload.get("object") or payload.get("object_ref"), "action": payload.get("action") or payload.get("right")}, "user": selected.get("user"), "groups": selected.get("groups"), "roles": selected.get("roles"), "permissions": permissions, "access_keys": access_keys, "data_restrictions": selected.get("data_restrictions"), "summary": summary, **({"available_actions_for_object": available_actions_for_object} if available_actions_for_object is not None else {}), "counts": { "roles": len(selected.get("roles") or []), "permissions": len(permissions), "data_restrictions": len(selected.get("data_restrictions") or []), **({"access_keys": (access_keys.get("counts") or {}).get("total", 0)} if access_keys else {}), }, **({"diagnostics": graph.get("diagnostics")} if isinstance(graph.get("diagnostics"), dict) else {}), } def access_user_explain(payload: dict[str, Any]) -> dict[str, Any]: method = "access.user.explain" user_selector = payload.get("user") or payload.get("user_id") or payload.get("name") if user_selector in {None, ""}: return invalid_argument(method, "user", "Pass user, user_id, or name.") if not isinstance(user_selector, str): return invalid_argument(method, "user", "user must be a JSON string.") has_snapshot = payload.get("access") is not None or isinstance(payload.get("snapshot"), dict) or payload.get("data") is not None max_permissions, max_permissions_error = parse_int_argument(payload, "max_effective_permissions_per_user", method=method, default=None, minimum=0, maximum=200000) if max_permissions_error: return max_permissions_error resolve_identifiers, resolve_identifiers_error = strict_bool_argument(payload, "resolve_identifiers", method=method, default=True) if resolve_identifiers_error: return resolve_identifiers_error normalized_payload = dict(payload) has_object_filter = any(payload.get(key) not in {None, ""} for key in ("object", "object_ref", "ref", "kind", "guid", "object_type", "object_name", "object_guid")) if payload.get("base_id") not in {None, ""} and has_object_filter: object_selector_payload = dict(payload) if object_selector_payload.get("object") not in {None, ""} and not has_object_selector(object_selector_payload): object_selector_payload["ref"] = object_selector_payload.get("object") normalized_selector = normalize_object_selector_aliases(object_selector_payload, "access.object.roles") if not (isinstance(normalized_selector, dict) and normalized_selector.get("status") == "invalid_argument") and has_object_selector(normalized_selector): object_guid, object_kind, object_card, object_error = resolve_object_guid( normalized_selector, str(payload.get("base_id")), timeout_seconds=int(payload.get("timeout_seconds") if isinstance(payload.get("timeout_seconds"), int) else 120), method=method, ) if not object_error: object_selector, object_payload = access_object_selector_from_card(normalized_selector, object_kind, object_card) object_payload["guid"] = object_guid normalized_payload["_object_filter_selector"] = object_selector normalized_payload["_object_filter_guid"] = object_guid normalized_payload["_object_filter_payload"] = object_payload payload = normalized_payload if has_snapshot: access, error = access_snapshot_from_payload(payload, method) if error: return error graph = build_access_graph_from_snapshot( access or {}, base_id=payload.get("base_id"), max_permissions_per_user=max_permissions, resolve_identifiers=bool(resolve_identifiers), ) return access_user_explain_from_graph(payload, graph, user_selector) base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return invalid_argument(method, "access", "Pass access snapshot data, or pass base_id for live BSP explanation.") preset = str(payload.get("preset") or payload.get("profile") or "bsp").strip().casefold() if preset not in {"bsp", "бсп"}: return invalid_argument(method, "preset", "Only preset='bsp' is supported for live explanation.") live_payload = { **payload, "preset": "bsp", "limit": int(payload.get("limit") if isinstance(payload.get("limit"), int) else 20000), "timeout_seconds": int(payload.get("timeout_seconds") if isinstance(payload.get("timeout_seconds"), int) else 120), "max_effective_permissions_per_user": int(max_permissions if max_permissions is not None else (20000 if has_object_filter else 5000)), "resolve_identifiers": bool(resolve_identifiers), } extracted = access_snapshot_extract(live_payload) if extracted.get("status") != "ok": return extracted graph = extracted.get("graph") if isinstance(extracted.get("graph"), dict) else {} result = access_user_explain_from_graph(payload, graph, user_selector) if result.get("status") == "ok": result["source"] = {"kind": "live_sql", "preset": "bsp", "extraction": "access.snapshot.extract"} result["extraction_counts"] = extracted.get("counts") result["extraction_diagnostics"] = extracted.get("diagnostics") return result def manifest_name(path: Any) -> str | None: if not path: return None name = Path(str(path).replace("\\", "/")).name match = re.match(r"^\d+_(.+?)-[0-9a-f]{8}\.json$", name, re.IGNORECASE) return match.group(1) if match else None def sql_config_for_base(base_id: str) -> tuple[dict[str, str] | None, dict[str, Any] | None]: raw_map = os.environ.get("ONEC_SQL_BASES_JSON") raw_map_file = os.environ.get("ONEC_SQL_BASES_JSON_FILE") if not raw_map and raw_map_file: try: raw_map = Path(raw_map_file).read_text(encoding="utf-8-sig") except Exception as exc: return None, {"status": "invalid_config", "message": f"Cannot read ONEC_SQL_BASES_JSON_FILE: {exc}"} if raw_map: try: config_map = json.loads(raw_map) except json.JSONDecodeError as exc: return None, {"status": "invalid_config", "message": f"ONEC_SQL_BASES_JSON is not valid JSON: {exc}"} if not isinstance(config_map, dict): return None, {"status": "invalid_config", "message": "ONEC_SQL_BASES_JSON must be an object keyed by base_id."} item = config_map.get(base_id) if not item: return None, {"status": "not_configured", "message": f"No SQL connection configured for base_id '{base_id}'."} if not isinstance(item, dict): return None, {"status": "invalid_config", "message": f"SQL connection config for base_id '{base_id}' must be an object."} password = str(item.get("password") or "") password_env = str(item.get("password_env") or "") if password_env: password = os.environ.get(password_env, "") config = { "server": str(item.get("server") or ""), "database": str(item.get("database") or ""), "user": str(item.get("user") or ""), "password": password, } missing = [key for key, value in config.items() if not value] if missing: return None, { "status": "not_configured", "message": f"Missing SQL connection fields for base_id '{base_id}': {', '.join(missing)}.", "password_env": password_env or None, } return config, None return None, { "status": "not_configured", "message": "Set ONEC_SQL_BASES_JSON or ONEC_SQL_BASES_JSON_FILE with an explicit entry for this base_id.", } def sql_configured_base_ids() -> list[str]: raw_map = os.environ.get("ONEC_SQL_BASES_JSON") raw_map_file = os.environ.get("ONEC_SQL_BASES_JSON_FILE") if not raw_map and raw_map_file: try: raw_map = Path(raw_map_file).read_text(encoding="utf-8-sig") except Exception: raw_map = "" if not raw_map: return [] try: config_map = json.loads(raw_map) except Exception: return [] if not isinstance(config_map, dict): return [] return sorted(str(key) for key, value in config_map.items() if isinstance(value, dict)) def cache_db_path() -> Path: return Path(os.environ.get("ONEC_ADAPTER_CACHE_DB") or "/data/adapter-cache.sqlite") def cache_server_key(config: dict[str, str]) -> str: return str(config.get("server") or "").strip().casefold() def cache_database_name(config: dict[str, str]) -> str: return str(config.get("database") or "").strip() def cache_connection() -> sqlite3.Connection: path = cache_db_path() path.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(path) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA synchronous=NORMAL") conn.execute( """ CREATE TABLE IF NOT EXISTS metadata_identity_cache ( server_key TEXT NOT NULL, database_name TEXT NOT NULL, kind TEXT NOT NULL, kind_ru TEXT, public_kind TEXT, name TEXT, synonym TEXT, normalized_name TEXT, normalized_synonym TEXT, full_name TEXT, normalized_full_name TEXT, guid TEXT NOT NULL, source TEXT, updated_at REAL NOT NULL, last_seen_at REAL NOT NULL, PRIMARY KEY (server_key, database_name, kind, guid) ) """ ) conn.execute("CREATE INDEX IF NOT EXISTS idx_metadata_cache_name ON metadata_identity_cache(server_key, database_name, kind, normalized_name)") conn.execute("CREATE INDEX IF NOT EXISTS idx_metadata_cache_synonym ON metadata_identity_cache(server_key, database_name, kind, normalized_synonym)") conn.execute("CREATE INDEX IF NOT EXISTS idx_metadata_cache_full ON metadata_identity_cache(server_key, database_name, normalized_full_name)") conn.execute( """ CREATE TABLE IF NOT EXISTS metadata_type_cache ( server_key TEXT NOT NULL, database_name TEXT NOT NULL, type_guid TEXT NOT NULL, status TEXT NOT NULL, presentation TEXT, kind TEXT, kind_ru TEXT, name TEXT, owner_guid TEXT, generated_category TEXT, payload_json TEXT NOT NULL, updated_at REAL NOT NULL, last_seen_at REAL NOT NULL, PRIMARY KEY (server_key, database_name, type_guid) ) """ ) conn.execute("CREATE INDEX IF NOT EXISTS idx_metadata_type_cache_kind ON metadata_type_cache(server_key, database_name, kind, name)") conn.execute("CREATE INDEX IF NOT EXISTS idx_metadata_type_cache_presentation ON metadata_type_cache(server_key, database_name, presentation)") conn.execute( """ CREATE TABLE IF NOT EXISTS metadata_guid_index ( server_key TEXT NOT NULL, database_name TEXT NOT NULL, guid TEXT NOT NULL, guid_role TEXT NOT NULL, kind TEXT, kind_ru TEXT, public_kind TEXT, name TEXT, synonym TEXT, normalized_name TEXT, normalized_synonym TEXT, full_name TEXT, normalized_full_name TEXT, presentation TEXT, normalized_presentation TEXT, owner_guid TEXT, owner_kind TEXT, owner_name TEXT, type_guid TEXT, value_guid TEXT, value_type_guid TEXT, value_presentation TEXT, source TEXT, source_file TEXT, payload_json TEXT, updated_at REAL NOT NULL, last_seen_at REAL NOT NULL, PRIMARY KEY (server_key, database_name, guid, guid_role) ) """ ) conn.execute("CREATE INDEX IF NOT EXISTS idx_guid_index_guid ON metadata_guid_index(server_key, database_name, guid)") conn.execute("CREATE INDEX IF NOT EXISTS idx_guid_index_type_guid ON metadata_guid_index(server_key, database_name, type_guid)") conn.execute("CREATE INDEX IF NOT EXISTS idx_guid_index_name ON metadata_guid_index(server_key, database_name, kind, normalized_name)") conn.execute("CREATE INDEX IF NOT EXISTS idx_guid_index_owner ON metadata_guid_index(server_key, database_name, owner_guid)") conn.execute("CREATE INDEX IF NOT EXISTS idx_guid_index_presentation ON metadata_guid_index(server_key, database_name, normalized_presentation)") conn.execute("CREATE INDEX IF NOT EXISTS idx_guid_index_full_name ON metadata_guid_index(server_key, database_name, normalized_full_name)") conn.execute( """ CREATE TABLE IF NOT EXISTS metadata_module_owner_cache ( server_key TEXT NOT NULL, database_name TEXT NOT NULL, module_ref TEXT NOT NULL, module_table TEXT, file_name TEXT, stream_index INTEGER, owner_guid TEXT NOT NULL, owner_kind TEXT, owner_name TEXT, owner_synonym TEXT, module_payload_json TEXT, updated_at REAL NOT NULL, last_seen_at REAL NOT NULL, PRIMARY KEY (server_key, database_name, module_ref) ) """ ) conn.execute("CREATE INDEX IF NOT EXISTS idx_module_owner_cache_owner ON metadata_module_owner_cache(server_key, database_name, owner_guid)") conn.execute("CREATE INDEX IF NOT EXISTS idx_module_owner_cache_owner_ref ON metadata_module_owner_cache(server_key, database_name, owner_guid, module_ref)") conn.execute( """ CREATE TABLE IF NOT EXISTS metadata_form_owner_cache ( server_key TEXT NOT NULL, database_name TEXT NOT NULL, form_key TEXT NOT NULL, extension_guid TEXT, extension_name TEXT, owner_kind TEXT, owner_name TEXT, owner_guid TEXT, form_name TEXT, form_guid TEXT, table_name TEXT, file_name TEXT, module_ref TEXT, bsl_offset INTEGER, payload_json TEXT, updated_at REAL NOT NULL, last_seen_at REAL NOT NULL, PRIMARY KEY (server_key, database_name, form_key) ) """ ) conn.execute("CREATE INDEX IF NOT EXISTS idx_form_owner_cache_name ON metadata_form_owner_cache(server_key, database_name, owner_kind, form_name)") conn.execute("CREATE INDEX IF NOT EXISTS idx_form_owner_cache_module ON metadata_form_owner_cache(server_key, database_name, module_ref)") conn.execute("CREATE INDEX IF NOT EXISTS idx_form_owner_cache_file ON metadata_form_owner_cache(server_key, database_name, table_name, file_name)") conn.execute("CREATE INDEX IF NOT EXISTS idx_form_owner_cache_extension ON metadata_form_owner_cache(server_key, database_name, extension_guid, extension_name)") conn.execute( """ CREATE TABLE IF NOT EXISTS extension_route_cache ( server_key TEXT NOT NULL, database_name TEXT NOT NULL, extension_guid TEXT, extension_name TEXT, root_cas_key TEXT, object_key TEXT NOT NULL, object_base_id TEXT, descriptor_cas_key TEXT NOT NULL, object_kind TEXT, kind_ru TEXT, name TEXT, synonym TEXT, normalized_name TEXT, normalized_synonym TEXT, normalized_full_name TEXT, guid TEXT, route_json TEXT NOT NULL, manifest_entries_json TEXT NOT NULL, descriptor_payload_sha1 TEXT, freshness_status TEXT NOT NULL, updated_at REAL NOT NULL, validated_at REAL, last_seen_at REAL NOT NULL, stale_reason TEXT, PRIMARY KEY (server_key, database_name, descriptor_cas_key) ) """ ) conn.execute("CREATE INDEX IF NOT EXISTS idx_extension_route_cache_name ON extension_route_cache(server_key, database_name, object_kind, normalized_name)") conn.execute("CREATE INDEX IF NOT EXISTS idx_extension_route_cache_synonym ON extension_route_cache(server_key, database_name, object_kind, normalized_synonym)") conn.execute("CREATE INDEX IF NOT EXISTS idx_extension_route_cache_guid ON extension_route_cache(server_key, database_name, guid)") conn.execute("CREATE INDEX IF NOT EXISTS idx_extension_route_cache_extension ON extension_route_cache(server_key, database_name, extension_guid, object_kind)") conn.execute( """ CREATE TABLE IF NOT EXISTS semantic_document_cache ( server_key TEXT NOT NULL, database_name TEXT NOT NULL, document_id TEXT NOT NULL, object_kind TEXT, object_guid TEXT, object_name TEXT, extension_guid TEXT, source_route_json TEXT, content_sha1 TEXT NOT NULL, text_preview TEXT, embedding_model TEXT, embedding_json TEXT, vector_status TEXT NOT NULL, authoritative_source TEXT NOT NULL, updated_at REAL NOT NULL, last_seen_at REAL NOT NULL, PRIMARY KEY (server_key, database_name, document_id) ) """ ) conn.execute("CREATE INDEX IF NOT EXISTS idx_semantic_document_cache_object ON semantic_document_cache(server_key, database_name, object_kind, object_guid)") conn.execute("CREATE INDEX IF NOT EXISTS idx_semantic_document_cache_source ON semantic_document_cache(server_key, database_name, authoritative_source, vector_status)") conn.execute( """ CREATE TABLE IF NOT EXISTS decoded_artifact_cache ( server_key TEXT NOT NULL, database_name TEXT NOT NULL, artifact_kind TEXT NOT NULL, content_sha1 TEXT NOT NULL, source_table TEXT, source_file TEXT, payload_bytes INTEGER, artifact_json TEXT NOT NULL, semantic_text TEXT, updated_at REAL NOT NULL, last_seen_at REAL NOT NULL, PRIMARY KEY (server_key, database_name, artifact_kind, content_sha1) ) """ ) conn.execute("CREATE INDEX IF NOT EXISTS idx_decoded_artifact_source ON decoded_artifact_cache(server_key, database_name, source_table, source_file)") conn.execute( """ CREATE TABLE IF NOT EXISTS metadata_code_index_cache ( server_key TEXT NOT NULL, database_name TEXT NOT NULL, module_ref TEXT NOT NULL, source_table TEXT NOT NULL, file_name TEXT NOT NULL, owner_kind TEXT, owner_name TEXT, owner_guid TEXT, form_name TEXT, extension_guid TEXT, extension_name TEXT, bsl_offset INTEGER, stream_index INTEGER, payload_sha1 TEXT NOT NULL, text_sha1 TEXT NOT NULL, text TEXT NOT NULL, routines_json TEXT NOT NULL, routine_count INTEGER NOT NULL, source_bytes INTEGER, updated_at REAL NOT NULL, last_verified_at REAL, last_seen_at REAL NOT NULL, PRIMARY KEY (server_key, database_name, module_ref) ) """ ) conn.execute("CREATE INDEX IF NOT EXISTS idx_code_index_text ON metadata_code_index_cache(server_key, database_name, source_table, text_sha1)") conn.execute("CREATE INDEX IF NOT EXISTS idx_code_index_file ON metadata_code_index_cache(server_key, database_name, source_table, file_name)") conn.execute("CREATE INDEX IF NOT EXISTS idx_code_index_owner ON metadata_code_index_cache(server_key, database_name, owner_kind, owner_name)") conn.execute("CREATE INDEX IF NOT EXISTS idx_code_index_verified ON metadata_code_index_cache(server_key, database_name, last_verified_at)") conn.execute( """ CREATE TABLE IF NOT EXISTS metadata_code_vector_cache ( server_key TEXT NOT NULL, database_name TEXT NOT NULL, chunk_id TEXT NOT NULL, module_ref TEXT NOT NULL, routine_name TEXT, chunk_kind TEXT NOT NULL, chunk_index INTEGER NOT NULL, text_sha1 TEXT NOT NULL, payload_sha1 TEXT NOT NULL, embedding_model TEXT NOT NULL, embedding_json TEXT NOT NULL, text_preview TEXT, updated_at REAL NOT NULL, last_seen_at REAL NOT NULL, PRIMARY KEY (server_key, database_name, chunk_id) ) """ ) conn.execute("CREATE INDEX IF NOT EXISTS idx_code_vector_module ON metadata_code_vector_cache(server_key, database_name, module_ref)") conn.execute("CREATE INDEX IF NOT EXISTS idx_code_vector_model ON metadata_code_vector_cache(server_key, database_name, embedding_model)") conn.execute( """ CREATE TABLE IF NOT EXISTS metadata_write_history ( server_key TEXT NOT NULL, database_name TEXT NOT NULL, operation_id TEXT NOT NULL, method TEXT NOT NULL, routed_method TEXT, status TEXT, base_id TEXT, target_kind TEXT, target_summary_json TEXT, backup_ids_json TEXT, result_json TEXT NOT NULL, created_at REAL NOT NULL, PRIMARY KEY (server_key, database_name, operation_id) ) """ ) conn.execute("CREATE INDEX IF NOT EXISTS idx_write_history_created ON metadata_write_history(server_key, database_name, created_at)") conn.execute("CREATE INDEX IF NOT EXISTS idx_write_history_status ON metadata_write_history(server_key, database_name, status)") return conn def cache_identity_row(config: dict[str, str], row: dict[str, Any]) -> dict[str, Any]: kind = str(row.get("kind") or "") name = str(row.get("name") or "") synonym = str(row.get("synonym") or "") kind_ru = str(row.get("kind_ru") or RU_KIND.get(kind, kind)) full_name = ".".join(part for part in [kind_ru, name] if part) return { "server_key": cache_server_key(config), "database_name": cache_database_name(config), "kind": kind, "kind_ru": kind_ru, "public_kind": row.get("public_kind") or PUBLIC_KIND.get(kind, "other"), "name": name or None, "synonym": synonym or None, "normalized_name": normalize(name), "normalized_synonym": normalize(synonym), "full_name": full_name, "normalized_full_name": normalize(full_name), "guid": str(row.get("guid") or "").lower(), "source": row.get("source") or "base", } def metadata_cache_upsert(config: dict[str, str], row: dict[str, Any]) -> None: item = cache_identity_row(config, row) if not item["kind"] or not item["guid"]: return metadata_guid_index_upsert( config, { **item, "guid_role": "metadata_object", "payload": metadata_cache_public_row(item), "source_file": item.get("guid"), }, ) now = time.time() with cache_connection() as conn: conn.execute( """ INSERT INTO metadata_identity_cache ( server_key, database_name, kind, kind_ru, public_kind, name, synonym, normalized_name, normalized_synonym, full_name, normalized_full_name, guid, source, updated_at, last_seen_at ) VALUES ( :server_key, :database_name, :kind, :kind_ru, :public_kind, :name, :synonym, :normalized_name, :normalized_synonym, :full_name, :normalized_full_name, :guid, :source, :updated_at, :last_seen_at ) ON CONFLICT(server_key, database_name, kind, guid) DO UPDATE SET kind_ru=excluded.kind_ru, public_kind=excluded.public_kind, name=excluded.name, synonym=excluded.synonym, normalized_name=excluded.normalized_name, normalized_synonym=excluded.normalized_synonym, full_name=excluded.full_name, normalized_full_name=excluded.normalized_full_name, source=excluded.source, updated_at=excluded.updated_at, last_seen_at=excluded.last_seen_at """, {**item, "updated_at": now, "last_seen_at": now}, ) def metadata_cache_lookup_row(base_id: str, kind: str | None, name: str) -> dict[str, Any] | None: config, _ = sql_config_for_base(base_id) if not config: return None wanted_kind, wanted_name = parse_object_query(kind, name) normalized = normalize(wanted_name) if not wanted_kind or not normalized: return None with cache_connection() as conn: row = conn.execute( """ SELECT * FROM metadata_identity_cache WHERE server_key = ? AND database_name = ? AND kind = ? AND (normalized_name = ? OR normalized_synonym = ? OR normalized_full_name = ?) ORDER BY CASE WHEN normalized_name = ? THEN 0 WHEN normalized_full_name = ? THEN 1 ELSE 2 END LIMIT 1 """, (cache_server_key(config), cache_database_name(config), wanted_kind, normalized, normalized, normalized, normalized, normalized), ).fetchone() return dict(row) if row else None def metadata_cache_lookup_guid(base_id: str, guid: str) -> dict[str, Any] | None: config, _ = sql_config_for_base(base_id) normalized_guid = str(guid or "").strip().lower() if not config or not is_guid_text(normalized_guid): return None with cache_connection() as conn: row = conn.execute( """ SELECT * FROM metadata_identity_cache WHERE server_key = ? AND database_name = ? AND guid = ? LIMIT 1 """, (cache_server_key(config), cache_database_name(config), normalized_guid), ).fetchone() if row: return metadata_cache_public_row(dict(row)) payload = metadata_guid_index_lookup_payload(config, normalized_guid, "metadata_object") if isinstance(payload, dict) and payload.get("guid"): result = dict(payload) result.setdefault("status", "ok") result.setdefault("match_by", "cache_guid") return result return None def metadata_cache_list_rows( base_id: str, kind: str | None, *, limit: int, offset: int, ) -> tuple[list[dict[str, Any]], int] | None: config, _ = sql_config_for_base(base_id) if not config: return None wanted, requested_public = parse_kind_request(kind) where = ["server_key = ?", "database_name = ?"] params: list[Any] = [cache_server_key(config), cache_database_name(config)] if wanted: where.append("kind = ?") params.append(wanted) elif requested_public: where.append("public_kind = ?") params.append(requested_public) where.append("name IS NOT NULL") where.append("name <> ''") where_sql = " AND ".join(where) with cache_connection() as conn: count = int( conn.execute( f"SELECT COUNT(*) AS count FROM metadata_identity_cache WHERE {where_sql}", params, ).fetchone()["count"] ) if count <= 0: return None rows = conn.execute( f""" SELECT * FROM metadata_identity_cache WHERE {where_sql} ORDER BY normalized_name, guid LIMIT ? OFFSET ? """, [*params, limit, offset], ).fetchall() return [dict(row) for row in rows], count def metadata_cache_public_row(row: dict[str, Any]) -> dict[str, Any]: result = { "guid": row.get("guid"), "kind": row.get("kind"), "kind_ru": row.get("kind_ru"), "public_kind": row.get("public_kind"), "name": row.get("name"), "synonym": row.get("synonym"), "source": row.get("source") or "base", "status": "ok", "identity": { "guid": row.get("guid"), "name": row.get("name"), **({"synonyms": {"ru": row.get("synonym")}} if row.get("synonym") else {}), }, "score": 1.0, "match_by": "cache", } public_ref = object_selector_ref(result.get("kind"), result.get("name")) if public_ref: result["ref"] = public_ref return result def metadata_guid_index_upsert(config: dict[str, str], item: dict[str, Any]) -> None: guid = str(item.get("guid") or item.get("type_guid") or "").lower() guid_role = str(item.get("guid_role") or "").strip() if not is_guid_text(guid) or not guid_role: return kind = str(item.get("kind") or "") name = str(item.get("name") or "") synonym = str(item.get("synonym") or "") kind_ru = str(item.get("kind_ru") or RU_KIND.get(kind, kind)) presentation = str(item.get("presentation") or "") full_name = str(item.get("full_name") or ".".join(part for part in [kind_ru, name] if part)) payload = item.get("payload") payload_json = json.dumps(payload, ensure_ascii=False, sort_keys=True) if isinstance(payload, dict) else item.get("payload_json") now = time.time() with cache_connection() as conn: conn.execute( """ INSERT INTO metadata_guid_index ( server_key, database_name, guid, guid_role, kind, kind_ru, public_kind, name, synonym, normalized_name, normalized_synonym, full_name, normalized_full_name, presentation, normalized_presentation, owner_guid, owner_kind, owner_name, type_guid, value_guid, value_type_guid, value_presentation, source, source_file, payload_json, updated_at, last_seen_at ) VALUES ( :server_key, :database_name, :guid, :guid_role, :kind, :kind_ru, :public_kind, :name, :synonym, :normalized_name, :normalized_synonym, :full_name, :normalized_full_name, :presentation, :normalized_presentation, :owner_guid, :owner_kind, :owner_name, :type_guid, :value_guid, :value_type_guid, :value_presentation, :source, :source_file, :payload_json, :updated_at, :last_seen_at ) ON CONFLICT(server_key, database_name, guid, guid_role) DO UPDATE SET kind=excluded.kind, kind_ru=excluded.kind_ru, public_kind=excluded.public_kind, name=excluded.name, synonym=excluded.synonym, normalized_name=excluded.normalized_name, normalized_synonym=excluded.normalized_synonym, full_name=excluded.full_name, normalized_full_name=excluded.normalized_full_name, presentation=excluded.presentation, normalized_presentation=excluded.normalized_presentation, owner_guid=excluded.owner_guid, owner_kind=excluded.owner_kind, owner_name=excluded.owner_name, type_guid=excluded.type_guid, value_guid=excluded.value_guid, value_type_guid=excluded.value_type_guid, value_presentation=excluded.value_presentation, source=excluded.source, source_file=excluded.source_file, payload_json=excluded.payload_json, updated_at=excluded.updated_at, last_seen_at=excluded.last_seen_at """, { "server_key": cache_server_key(config), "database_name": cache_database_name(config), "guid": guid, "guid_role": guid_role, "kind": kind or None, "kind_ru": kind_ru or None, "public_kind": item.get("public_kind") or (PUBLIC_KIND.get(kind, "other") if kind else None), "name": name or None, "synonym": synonym or None, "normalized_name": normalize(name), "normalized_synonym": normalize(synonym), "full_name": full_name or None, "normalized_full_name": normalize(full_name), "presentation": presentation or None, "normalized_presentation": normalize(presentation), "owner_guid": str(item.get("owner_guid") or "").lower() or None, "owner_kind": item.get("owner_kind"), "owner_name": item.get("owner_name"), "type_guid": str(item.get("type_guid") or "").lower() or None, "value_guid": str(item.get("value_guid") or "").lower() or None, "value_type_guid": str(item.get("value_type_guid") or "").lower() or None, "value_presentation": item.get("value_presentation"), "source": item.get("source") or "base", "source_file": item.get("source_file"), "payload_json": payload_json, "updated_at": now, "last_seen_at": now, }, ) def metadata_guid_index_lookup_types(config: dict[str, str], type_guids: set[str]) -> dict[str, dict[str, Any]]: wanted = sorted({str(guid or "").lower() for guid in type_guids if is_guid_text(str(guid or ""))}) if not wanted: return {} result: dict[str, dict[str, Any]] = {} with cache_connection() as conn: for start in range(0, len(wanted), 500): chunk = wanted[start : start + 500] placeholders = ",".join(["?"] * len(chunk)) rows = conn.execute( f""" SELECT guid, payload_json FROM metadata_guid_index WHERE server_key = ? AND database_name = ? AND guid_role IN ('generated_type', 'builtin_type', 'metadata_type', 'metadata_object') AND guid IN ({placeholders}) """, (cache_server_key(config), cache_database_name(config), *chunk), ).fetchall() for row in rows: try: payload = json.loads(row["payload_json"] or "{}") except Exception: continue if isinstance(payload, dict): result[str(row["guid"]).lower()] = payload return result def metadata_guid_index_lookup_payload(config: dict[str, str], guid: str, guid_role: str) -> dict[str, Any] | None: normalized_guid = str(guid or "").lower() normalized_role = str(guid_role or "").strip() if not is_guid_text(normalized_guid) or not normalized_role: return None with cache_connection() as conn: row = conn.execute( """ SELECT payload_json FROM metadata_guid_index WHERE server_key = ? AND database_name = ? AND guid = ? AND guid_role = ? LIMIT 1 """, (cache_server_key(config), cache_database_name(config), normalized_guid, normalized_role), ).fetchone() if not row: return None try: payload = json.loads(row["payload_json"] or "{}") except Exception: return None return payload if isinstance(payload, dict) else None EXTENSION_DEFINITION_CACHE_ROLE = "extension_definition_v1" EXTENSION_DEFINITION_CACHE_MARKER_ROLE = "extension_definition_cache_status_v1" EXTENSION_DEFINITION_CACHE_MARKER_GUID = "00000000-0000-0000-0000-000000000001" def metadata_guid_index_lookup_by_name( config: dict[str, str] | None, *, guid_role: str, query: str, limit: int, ) -> list[dict[str, Any]]: if not config or not str(guid_role or "").strip() or limit <= 0: return [] normalized = normalize(query) normalized_exact = normalize_exact(query) if not normalized and not normalized_exact: return [] like_value = f"%{normalized}%" result: list[dict[str, Any]] = [] with cache_connection() as conn: rows = conn.execute( """ SELECT payload_json FROM metadata_guid_index WHERE server_key = ? AND database_name = ? AND guid_role = ? AND ( normalized_name = ? OR normalized_synonym = ? OR normalized_full_name = ? OR normalized_presentation = ? OR normalized_name LIKE ? OR normalized_synonym LIKE ? OR normalized_full_name LIKE ? OR normalized_presentation LIKE ? ) ORDER BY CASE WHEN normalized_name = ? THEN 0 WHEN normalized_synonym = ? THEN 1 WHEN normalized_full_name = ? THEN 2 WHEN normalized_presentation = ? THEN 3 ELSE 4 END, normalized_name, guid LIMIT ? """, ( cache_server_key(config), cache_database_name(config), guid_role, normalized, normalized, normalized, normalized, like_value, like_value, like_value, like_value, normalized, normalized, normalized, normalized, int(limit), ), ).fetchall() for row in rows: try: payload = json.loads(row["payload_json"] or "{}") except Exception: continue if isinstance(payload, dict): result.append(payload) return result def extension_route_cache_upsert(config: dict[str, str] | None, match: dict[str, Any], *, descriptor_payload_sha1: str | None = None, freshness_status: str = "fresh") -> None: if not config: return route = match.get("route") if isinstance(match.get("route"), dict) else {} descriptor_cas_key = str(route.get("file_name") or match.get("guid") or "").strip().lower() if not descriptor_cas_key: return manifest_entry = route.get("manifest_entry") if isinstance(route.get("manifest_entry"), dict) else {} manifest_entries = match.get("manifest_entries") if isinstance(match.get("manifest_entries"), list) else [] extension = (match.get("origin") or {}).get("extension") if isinstance(match.get("origin"), dict) else None if not isinstance(extension, dict): extension = manifest_entry.get("extension") if isinstance(manifest_entry.get("extension"), dict) else {} kind = str(match.get("kind") or "") kind_ru = str(match.get("kind_ru") or RU_KIND.get(kind, kind)) name = str(match.get("name") or "") synonym = str(match.get("synonym") or "") full_name = ".".join(part for part in [kind_ru, name] if part) now = time.time() with cache_connection() as conn: conn.execute( """ INSERT INTO extension_route_cache ( server_key, database_name, extension_guid, extension_name, root_cas_key, object_key, object_base_id, descriptor_cas_key, object_kind, kind_ru, name, synonym, normalized_name, normalized_synonym, normalized_full_name, guid, route_json, manifest_entries_json, descriptor_payload_sha1, freshness_status, updated_at, validated_at, last_seen_at, stale_reason ) VALUES ( :server_key, :database_name, :extension_guid, :extension_name, :root_cas_key, :object_key, :object_base_id, :descriptor_cas_key, :object_kind, :kind_ru, :name, :synonym, :normalized_name, :normalized_synonym, :normalized_full_name, :guid, :route_json, :manifest_entries_json, :descriptor_payload_sha1, :freshness_status, :updated_at, :validated_at, :last_seen_at, NULL ) ON CONFLICT(server_key, database_name, descriptor_cas_key) DO UPDATE SET extension_guid=excluded.extension_guid, extension_name=excluded.extension_name, root_cas_key=excluded.root_cas_key, object_key=excluded.object_key, object_base_id=excluded.object_base_id, object_kind=excluded.object_kind, kind_ru=excluded.kind_ru, name=excluded.name, synonym=excluded.synonym, normalized_name=excluded.normalized_name, normalized_synonym=excluded.normalized_synonym, normalized_full_name=excluded.normalized_full_name, guid=excluded.guid, route_json=excluded.route_json, manifest_entries_json=excluded.manifest_entries_json, descriptor_payload_sha1=excluded.descriptor_payload_sha1, freshness_status=excluded.freshness_status, updated_at=excluded.updated_at, validated_at=excluded.validated_at, last_seen_at=excluded.last_seen_at, stale_reason=NULL """, { "server_key": cache_server_key(config), "database_name": cache_database_name(config), "extension_guid": str((extension or {}).get("guid") or "").lower() or None, "extension_name": (extension or {}).get("name"), "root_cas_key": str(route.get("root_cas_key") or manifest_entry.get("root_cas_key") or "").lower() or None, "object_key": str(manifest_entry.get("object_id") or descriptor_cas_key), "object_base_id": str(manifest_entry.get("object_base_id") or "").lower() or None, "descriptor_cas_key": descriptor_cas_key, "object_kind": kind or None, "kind_ru": kind_ru or None, "name": name or None, "synonym": synonym or None, "normalized_name": normalize(name), "normalized_synonym": normalize(synonym), "normalized_full_name": normalize(full_name), "guid": str(match.get("guid") or "").lower() or None, "route_json": json.dumps(route, ensure_ascii=False, sort_keys=True), "manifest_entries_json": json.dumps(manifest_entries, ensure_ascii=False, sort_keys=True), "descriptor_payload_sha1": descriptor_payload_sha1, "freshness_status": freshness_status, "updated_at": now, "validated_at": now if freshness_status == "fresh" else None, "last_seen_at": now, }, ) def extension_route_cache_mark_stale(config: dict[str, str] | None, descriptor_cas_key: str, reason: str) -> None: if not config: return key = str(descriptor_cas_key or "").strip().lower() if not key: return now = time.time() with cache_connection() as conn: conn.execute( """ UPDATE extension_route_cache SET freshness_status='stale', stale_reason=?, validated_at=?, last_seen_at=? WHERE server_key=? AND database_name=? AND descriptor_cas_key=? """, (reason, now, now, cache_server_key(config), cache_database_name(config), key), ) def extension_route_cache_lookup( config: dict[str, str] | None, *, query: str, kind_filter: str | None, guid_filter: str, extension_guid: str | None, limit: int, ) -> list[dict[str, Any]]: if not config or limit <= 0: return [] clauses = ["server_key=?", "database_name=?", "freshness_status!='stale'"] params: list[Any] = [cache_server_key(config), cache_database_name(config)] if kind_filter: clauses.append("object_kind=?") params.append(kind_filter) if extension_guid: clauses.append("extension_guid=?") params.append(extension_guid.lower()) normalized_query = normalize(query) if guid_filter: clauses.append("(guid=? OR descriptor_cas_key=? OR object_base_id=?)") params.extend([guid_filter, guid_filter, guid_filter]) elif normalized_query: like_value = f"%{normalized_query}%" clauses.append("(normalized_name=? OR normalized_synonym=? OR normalized_full_name=? OR normalized_name LIKE ? OR normalized_synonym LIKE ? OR normalized_full_name LIKE ?)") params.extend([normalized_query, normalized_query, normalized_query, like_value, like_value, like_value]) with cache_connection() as conn: rows = conn.execute( f""" SELECT * FROM extension_route_cache WHERE {' AND '.join(clauses)} ORDER BY CASE WHEN normalized_name = ? THEN 0 WHEN normalized_synonym = ? THEN 1 WHEN normalized_full_name = ? THEN 2 ELSE 3 END, normalized_name, descriptor_cas_key LIMIT ? """, (*params, normalized_query, normalized_query, normalized_query, limit), ).fetchall() return [dict(row) for row in rows] def extension_route_cache_row_to_match(base_id: str, row: dict[str, Any], *, include_storage: bool, freshness: dict[str, Any]) -> dict[str, Any]: try: route = json.loads(row.get("route_json") or "{}") except Exception: route = {} try: manifest_entries = json.loads(row.get("manifest_entries_json") or "[]") except Exception: manifest_entries = [] if not isinstance(route, dict): route = {} if not isinstance(manifest_entries, list): manifest_entries = [] extension = {"guid": row.get("extension_guid"), "name": row.get("extension_name")} if row.get("extension_guid") or row.get("extension_name") else None kind = row.get("object_kind") identity = {"name": row.get("name"), "guid": row.get("guid") or row.get("descriptor_cas_key")} match = { "kind": kind, "kind_ru": row.get("kind_ru") or RU_KIND.get(str(kind or ""), kind), "name": row.get("name"), "synonym": row.get("synonym"), "guid": row.get("guid") or row.get("descriptor_cas_key"), "match_by": "source_cache", "origin": { "source": "extension", "presentation": "Расширение", "extension": extension, "status": "ok" if extension else "extension_unresolved", }, "route": route if include_storage else {key: route.get(key) for key in ("route_type", "table", "file_name", "manifest_entries")}, "read_selectors": extension_object_read_selectors(base_id, str(kind or ""), identity, route), "freshness": freshness, } if include_storage: match["manifest_entries"] = manifest_entries match["cache"] = { "source": "extension_route_cache", "descriptor_payload_sha1": row.get("descriptor_payload_sha1"), "updated_at": row.get("updated_at"), "validated_at": row.get("validated_at"), } return match def extension_route_cache_recent_freshness(row: dict[str, Any], *, ttl_seconds: int) -> dict[str, Any] | None: if ttl_seconds <= 0: return None if str(row.get("freshness_status") or "").strip() == "stale": return None try: validated_at = float(row.get("validated_at") or 0) except Exception: validated_at = 0 if validated_at <= 0: return None age_seconds = max(0.0, time.time() - validated_at) if age_seconds > ttl_seconds: return None return { "status": "fresh", "validated_by": "recent_source_cache", "validation_required_after_seconds": ttl_seconds, "age_seconds": round(age_seconds, 3), "validated_at": validated_at, } def validate_extension_route_cache_row(base_id: str, config: dict[str, str] | None, row: dict[str, Any], *, timeout_seconds: int) -> tuple[dict[str, Any] | None, dict[str, Any]]: descriptor_key = str(row.get("descriptor_cas_key") or "").strip().lower() extension_guid = str(row.get("extension_guid") or "").strip().lower() or None manifests, diagnostics = live_extension_manifests(base_id, extension_guid=extension_guid, timeout_seconds=timeout_seconds) for manifest in manifests: entries = [entry for entry in manifest.get("entries") or [] if isinstance(entry, dict)] current = next((entry for entry in entries if str(entry.get("cas_key") or "").lower() == descriptor_key), None) if not current: continue descriptor_data, _descriptor_config, descriptor_error = read_storage_file_bytes( base_id, "ConfigCAS", descriptor_key, timeout_seconds=timeout_seconds, ) if descriptor_data is not None and not descriptor_error: try: from parser.cas_payload import classify_payload descriptor_identity = config_identity_from_bytes(descriptor_data) or {} detected_kind = extension_metadata_payload_kind( descriptor_data, descriptor_identity, classify_payload(descriptor_data, include_text=False), ) except Exception: detected_kind = None cached_kind = str(row.get("object_kind") or "").strip() if detected_kind and cached_kind and detected_kind != cached_kind: reason = f"semantic_kind_mismatch:{cached_kind}->{detected_kind}" extension_route_cache_mark_stale(config, descriptor_key, reason) return None, { "status": "stale", "validated_by": "live_manifest_and_descriptor", "reason": reason, } root_cas_key = str(manifest.get("root_cas_key") or "").lower() now = time.time() if config: with cache_connection() as conn: conn.execute( """ UPDATE extension_route_cache SET freshness_status='fresh', validated_at=?, last_seen_at=?, root_cas_key=?, stale_reason=NULL WHERE server_key=? AND database_name=? AND descriptor_cas_key=? """, (now, now, root_cas_key or row.get("root_cas_key"), cache_server_key(config), cache_database_name(config), descriptor_key), ) return dict(row), { "status": "fresh", "validated_by": "live_manifest", "root_cas_key": root_cas_key, "root_changed": bool(row.get("root_cas_key") and root_cas_key and str(row.get("root_cas_key")).lower() != root_cas_key), } reason = "descriptor_not_present_in_current_manifest" extension_route_cache_mark_stale(config, descriptor_key, reason) return None, {"status": "stale", "validated_by": "live_manifest", "reason": reason, "diagnostics": diagnostics} def decoded_artifact_cache_lookup(config: dict[str, str] | None, *, artifact_kind: str, content_sha1: str) -> dict[str, Any] | None: if not config: return None kind = str(artifact_kind or "").strip() sha1 = str(content_sha1 or "").strip().lower() if not kind or not re.fullmatch(r"[0-9a-f]{40}", sha1): return None with cache_connection() as conn: row = conn.execute( """ SELECT artifact_json, semantic_text FROM decoded_artifact_cache WHERE server_key=? AND database_name=? AND artifact_kind=? AND content_sha1=? LIMIT 1 """, (cache_server_key(config), cache_database_name(config), kind, sha1), ).fetchone() if not row: return None conn.execute( """ UPDATE decoded_artifact_cache SET last_seen_at=? WHERE server_key=? AND database_name=? AND artifact_kind=? AND content_sha1=? """, (time.time(), cache_server_key(config), cache_database_name(config), kind, sha1), ) try: artifact = json.loads(row["artifact_json"] or "{}") except Exception: return None if isinstance(artifact, dict): artifact.setdefault("artifact_cache", {"status": "hit", "content_sha1": sha1}) if row["semantic_text"]: artifact.setdefault("semantic_text", row["semantic_text"]) return artifact return None def decoded_artifact_cache_upsert( config: dict[str, str] | None, *, artifact_kind: str, content_sha1: str, source_table: str, source_file: str, payload_bytes: int, artifact: dict[str, Any], semantic_text: str | None = None, ) -> None: if not config or not isinstance(artifact, dict): return kind = str(artifact_kind or "").strip() sha1 = str(content_sha1 or "").strip().lower() if not kind or not re.fullmatch(r"[0-9a-f]{40}", sha1): return now = time.time() with cache_connection() as conn: conn.execute( """ INSERT INTO decoded_artifact_cache ( server_key, database_name, artifact_kind, content_sha1, source_table, source_file, payload_bytes, artifact_json, semantic_text, updated_at, last_seen_at ) VALUES ( :server_key, :database_name, :artifact_kind, :content_sha1, :source_table, :source_file, :payload_bytes, :artifact_json, :semantic_text, :updated_at, :last_seen_at ) ON CONFLICT(server_key, database_name, artifact_kind, content_sha1) DO UPDATE SET source_table=excluded.source_table, source_file=excluded.source_file, payload_bytes=excluded.payload_bytes, artifact_json=excluded.artifact_json, semantic_text=excluded.semantic_text, updated_at=excluded.updated_at, last_seen_at=excluded.last_seen_at """, { "server_key": cache_server_key(config), "database_name": cache_database_name(config), "artifact_kind": kind, "content_sha1": sha1, "source_table": source_table, "source_file": source_file, "payload_bytes": int(payload_bytes or 0), "artifact_json": json.dumps(artifact, ensure_ascii=False, sort_keys=True), "semantic_text": semantic_text, "updated_at": now, "last_seen_at": now, }, ) def template_structure_semantic_text(structure: dict[str, Any]) -> str: if not isinstance(structure, dict): return "" lines: list[str] = [] if structure.get("format"): lines.append(f"format: {structure.get('format')}") dimensions = structure.get("dimensions") if isinstance(structure.get("dimensions"), dict) else {} if dimensions: lines.append(f"dimensions: rows={dimensions.get('rows')} columns={dimensions.get('columns')}") capacity_dimensions = structure.get("capacity_dimensions") if isinstance(structure.get("capacity_dimensions"), dict) else {} if capacity_dimensions and capacity_dimensions != dimensions: lines.append(f"capacity_dimensions: rows={capacity_dimensions.get('rows')} columns={capacity_dimensions.get('columns')}") used_dimensions = structure.get("used_dimensions") if isinstance(structure.get("used_dimensions"), dict) else {} if used_dimensions: lines.append(f"used_dimensions: rows={used_dimensions.get('rows')} columns={used_dimensions.get('columns')}") format_dimensions = structure.get("format_dimensions") if isinstance(structure.get("format_dimensions"), dict) else {} if format_dimensions: lines.append(f"format_dimensions: rows={format_dimensions.get('rows')} columns={format_dimensions.get('columns')}") for area in (structure.get("named_areas") or [])[:200]: if isinstance(area, dict) and area.get("name"): range_info = area.get("range") if isinstance(area.get("range"), dict) else {} one_based = range_info.get("one_based") if isinstance(range_info.get("one_based"), dict) else {} lines.append( "area: " + str(area.get("name")) + ( f" R{one_based.get('top')}C{one_based.get('left')}:R{one_based.get('bottom')}C{one_based.get('right')}" if one_based else "" ) ) for parameter in (structure.get("parameters") or [])[:200]: if isinstance(parameter, dict) and parameter.get("name"): lines.append(f"parameter: {parameter.get('name')}") for cell in (structure.get("cells") or [])[:200]: if isinstance(cell, dict): text = str(cell.get("text") or cell.get("parameter") or "").strip() if text: lines.append(f"cell R{cell.get('row')}C{cell.get('column')}: {text[:200]}") for hint in (structure.get("cell_coordinate_hints") or [])[:200]: if isinstance(hint, dict): text = str(hint.get("text") or "").strip() one_based = hint.get("one_based") if isinstance(hint.get("one_based"), dict) else {} row = one_based.get("row") column = one_based.get("column") if text and (row is not None or column is not None): lines.append(f"cell_hint R{row}C{column}: {text[:200]}") return "\n".join(lines)[:20000] def semantic_document_cache_upsert( config: dict[str, str] | None, *, document_id: str, object_kind: str | None, object_guid: str | None, object_name: str | None, extension_guid: str | None, source_route: dict[str, Any], content_sha1: str, text: str, authoritative_source: str = "decoded_artifact_cache", ) -> None: if not config or not document_id or not text: return now = time.time() with cache_connection() as conn: conn.execute( """ INSERT INTO semantic_document_cache ( server_key, database_name, document_id, object_kind, object_guid, object_name, extension_guid, source_route_json, content_sha1, text_preview, embedding_model, embedding_json, vector_status, authoritative_source, updated_at, last_seen_at ) VALUES ( :server_key, :database_name, :document_id, :object_kind, :object_guid, :object_name, :extension_guid, :source_route_json, :content_sha1, :text_preview, NULL, NULL, 'pending_embedding', :authoritative_source, :updated_at, :last_seen_at ) ON CONFLICT(server_key, database_name, document_id) DO UPDATE SET object_kind=excluded.object_kind, object_guid=excluded.object_guid, object_name=excluded.object_name, extension_guid=excluded.extension_guid, source_route_json=excluded.source_route_json, content_sha1=excluded.content_sha1, text_preview=excluded.text_preview, vector_status=CASE WHEN semantic_document_cache.content_sha1 = excluded.content_sha1 THEN semantic_document_cache.vector_status ELSE 'pending_embedding' END, authoritative_source=excluded.authoritative_source, updated_at=excluded.updated_at, last_seen_at=excluded.last_seen_at """, { "server_key": cache_server_key(config), "database_name": cache_database_name(config), "document_id": document_id, "object_kind": object_kind, "object_guid": str(object_guid or "").lower() or None, "object_name": object_name, "extension_guid": str(extension_guid or "").lower() or None, "source_route_json": json.dumps(source_route, ensure_ascii=False, sort_keys=True), "content_sha1": content_sha1, "text_preview": text[:4000], "authoritative_source": authoritative_source, "updated_at": now, "last_seen_at": now, }, ) def semantic_document_cache_lookup(config: dict[str, str] | None, document_id: str) -> dict[str, Any] | None: if not config or not document_id: return None with cache_connection() as conn: row = conn.execute( """ SELECT document_id, object_kind, object_guid, object_name, extension_guid, source_route_json, content_sha1, text_preview, embedding_model, embedding_json, vector_status, authoritative_source, updated_at, last_seen_at FROM semantic_document_cache WHERE server_key=? AND database_name=? AND document_id=? LIMIT 1 """, (cache_server_key(config), cache_database_name(config), str(document_id or "")), ).fetchone() return dict(row) if row else None def semantic_document_cache_mark_seen(config: dict[str, str] | None, document_id: str) -> None: if not config or not document_id: return with cache_connection() as conn: conn.execute( """ UPDATE semantic_document_cache SET last_seen_at=? WHERE server_key=? AND database_name=? AND document_id=? """, (time.time(), cache_server_key(config), cache_database_name(config), str(document_id or "")), ) def semantic_document_cache_mark_changed(config: dict[str, str] | None, document_id: str) -> None: if not config or not document_id: return now = time.time() with cache_connection() as conn: conn.execute( """ UPDATE semantic_document_cache SET vector_status='error', embedding_model=NULL, embedding_json=NULL, updated_at=?, last_seen_at=? WHERE server_key=? AND database_name=? AND document_id=? """, (now, now, cache_server_key(config), cache_database_name(config), str(document_id or "")), ) def semantic_document_cache_delete(config: dict[str, str] | None, document_id: str) -> None: if not config or not document_id: return with cache_connection() as conn: conn.execute( """ DELETE FROM semantic_document_cache WHERE server_key=? AND database_name=? AND document_id=? """, (cache_server_key(config), cache_database_name(config), str(document_id or "")), ) def refreshed_semantic_document_id(old_document_id: str, source_route: dict[str, Any], content_sha1: str) -> str: old_text = str(old_document_id or "").strip() table = str(source_route.get("table") or "ConfigCAS").strip() or "ConfigCAS" part_id = str(source_route.get("part_id") or source_route.get("file_name") or "").strip() if old_text.startswith("template_part:") and part_id and re.fullmatch(r"[0-9a-f]{40}", str(content_sha1 or "")): return f"template_part:{table}:{part_id}:{str(content_sha1).lower()}" return old_text def numeric_vector(value: Any) -> list[float] | None: if not isinstance(value, list) or not value: return None vector: list[float] = [] for item in value: if not isinstance(item, (int, float)) or isinstance(item, bool): return None vector.append(float(item)) return vector def cosine_similarity(left: list[float], right: list[float]) -> float | None: if not left or len(left) != len(right): return None dot = sum(a * b for a, b in zip(left, right)) left_norm = math.sqrt(sum(a * a for a in left)) right_norm = math.sqrt(sum(b * b for b in right)) if not left_norm or not right_norm: return None return dot / (left_norm * right_norm) CODE_INDEX_VECTOR_MODEL = "local-code-hashing-v1" CODE_INDEX_VECTOR_DIMENSIONS = 64 CODE_INDEX_METHODS = { "metadata.code_index.build", "metadata.code_index.status", "metadata.code_index.search", "metadata.code_index.verify", "metadata.code_index.refresh_changed", "metadata.code_vector.search", } def code_text_sha1(text: str) -> str: return hashlib.sha1(str(text or "").replace("\r\n", "\n").replace("\r", "\n").encode("utf-8")).hexdigest() def code_hashing_embedding(text: str, *, dimensions: int = CODE_INDEX_VECTOR_DIMENSIONS) -> list[float]: vector = [0.0] * dimensions for token in re.findall(r"[\wА-Яа-яЁё]+", str(text or "").casefold()): digest = hashlib.sha1(token.encode("utf-8")).digest() index = int.from_bytes(digest[:4], "little") % dimensions sign = 1.0 if digest[4] % 2 == 0 else -1.0 vector[index] += sign norm = math.sqrt(sum(value * value for value in vector)) return [value / norm for value in vector] if norm else vector def public_routine_blocks(text: str) -> list[dict[str, Any]]: try: from parser.bsl_validation import routine_blocks routines = list(routine_blocks(str(text or ""))) except Exception: routines = [] return [ { "kind": routine.get("kind"), "name": routine.get("name"), "line_start": routine.get("line_start"), "line_end": routine.get("line_end"), } for routine in routines ] def code_module_owner_from_cache(config: dict[str, str] | None, module_ref: str) -> dict[str, Any]: form_owner = metadata_form_owner_cache_lookup(config, module_ref=module_ref) if form_owner: owner = form_owner.get("owner") if isinstance(form_owner.get("owner"), dict) else {} form = form_owner.get("form") if isinstance(form_owner.get("form"), dict) else {} extension = form_owner.get("extension") if isinstance(form_owner.get("extension"), dict) else {} return { "owner_kind": owner.get("kind") or form.get("kind"), "owner_name": owner.get("name") or form.get("name"), "owner_guid": owner.get("guid") or form.get("guid"), "form_name": form.get("name"), "extension_guid": extension.get("guid"), "extension_name": extension.get("name"), "bsl_offset": form_owner.get("bsl_offset"), } module_owner = metadata_module_owner_cache_lookup(config or {}, module_ref) if config else None if not module_owner and config: base_module_ref = normalize_module_ref_for_form_owner(module_ref) if base_module_ref != module_ref: module_owner = metadata_module_owner_cache_lookup(config, base_module_ref) if module_owner: owner = module_owner.get("owner") or {} module_table, module_file_name, _ = parse_module_id(module_ref) saved_extension_guid = "" if module_table == "ConfigCASSave" and "__" in str(module_file_name or ""): saved_extension_guid = str(module_file_name or "").split("__", 1)[0].strip().lower() return { "owner_kind": owner.get("kind"), "owner_name": owner.get("name"), "owner_guid": owner.get("guid"), "form_name": None, "extension_guid": saved_extension_guid if is_guid_text(saved_extension_guid) else None, "extension_name": None, "bsl_offset": None, } return {} def extract_code_index_text_from_payload(data: bytes, *, module_ref: str, bsl_offset: int | None = None) -> tuple[str, dict[str, Any]]: table, file_name, stream_index = parse_module_id(module_ref) if stream_index is not None: try: from parser.cas_payload import classify_payload classified = classify_payload(data, include_text=True) except Exception as exc: return "", {"status": "error", "message": str(exc)} streams = classified.get("stream_blocks") or [] if stream_index < 0 or stream_index >= len(streams): return "", {"status": "not_found", "message": "Stream index not found."} return str((streams[stream_index] or {}).get("text") or ""), {"status": "ok", "source": "stream", "stream_index": stream_index} decoded = payload_text_from_bytes(data) container_text = str(decoded.get("text") or "") text, extraction = extract_bsl_text_from_container(container_text, bsl_offset=bsl_offset) return str(text or ""), {"status": extraction.get("status"), "source": "bsl_container", **extraction} def code_index_row_payload(row: sqlite3.Row | dict[str, Any]) -> dict[str, Any]: item = dict(row) try: routines = json.loads(item.get("routines_json") or "[]") except Exception: routines = [] return { "module_ref": item.get("module_ref"), "table": item.get("source_table"), "file_name": item.get("file_name"), "owner": { "kind": item.get("owner_kind"), "name": item.get("owner_name"), "guid": item.get("owner_guid"), "form": item.get("form_name"), "extension": {"guid": item.get("extension_guid"), "name": item.get("extension_name")}, }, "bsl_offset": item.get("bsl_offset"), "stream_index": item.get("stream_index"), "payload_sha1": item.get("payload_sha1"), "text_sha1": item.get("text_sha1"), "text": item.get("text"), "routines": routines if isinstance(routines, list) else [], "routine_count": item.get("routine_count"), "source_bytes": item.get("source_bytes"), "updated_at": item.get("updated_at"), "last_verified_at": item.get("last_verified_at"), "last_seen_at": item.get("last_seen_at"), } def collect_backup_ids(value: Any) -> list[str]: found: list[str] = [] def walk(item: Any) -> None: if isinstance(item, dict): backup_id = item.get("backup_id") if isinstance(backup_id, str) and re.fullmatch(r"[0-9a-f]{32}", backup_id) and backup_id not in found: found.append(backup_id) for child in item.values(): walk(child) elif isinstance(item, list): for child in item: walk(child) elif isinstance(item, str) and re.fullmatch(r"[0-9a-f]{32}", item) and item not in found: found.append(item) walk(value) return found def write_history_target_summary(result: dict[str, Any]) -> dict[str, Any]: summary: dict[str, Any] = {} for key in ("target_kind", "routed_method", "execution_mode"): if result.get(key) not in {None, ""}: summary[key] = result.get(key) routed_method = write_history_routed_method(result) if routed_method: summary["routed_method"] = routed_method target = result.get("target") if isinstance(result.get("target"), dict) else None if target: target_keys = ("id", "name") if result.get("schema") == "onec_infobase_user_password_change.v1" else ("kind", "form", "object", "extension", "table") summary["target"] = {key: target.get(key) for key in target_keys if target.get(key) not in {None, ""}} if result.get("schema") == "onec_infobase_user_password_change.v1": summary["operation"] = result.get("operation") summary["transport"] = "sql_dbo_v8users_data" path_resolution = result.get("path_resolution") if isinstance(result.get("path_resolution"), dict) else None if path_resolution: summary["canonical_path"] = path_resolution.get("canonical_path") summary["path_kind"] = path_resolution.get("path_kind") return summary def write_history_routed_method(result: dict[str, Any]) -> str: routed = str(result.get("routed_method") or "").strip() if routed: return routed route = result.get("route") if isinstance(result.get("route"), dict) else {} return str(route.get("method") or "").strip() def metadata_write_history_record(base_id: str, method: str, result: dict[str, Any]) -> str | None: config, _error = sql_config_for_base(base_id) if not config: return None operation_id = uuid.uuid4().hex now = time.time() target_summary = write_history_target_summary(result) backup_ids = collect_backup_ids(result) routed_method = write_history_routed_method(result) try: with cache_connection() as conn: conn.execute( """ INSERT INTO metadata_write_history ( server_key, database_name, operation_id, method, routed_method, status, base_id, target_kind, target_summary_json, backup_ids_json, result_json, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( cache_server_key(config), cache_database_name(config), operation_id, method, routed_method, str(result.get("status") or ""), base_id, str(result.get("target_kind") or ""), json.dumps(target_summary, ensure_ascii=False, sort_keys=True), json.dumps(backup_ids, ensure_ascii=False), json.dumps(result, ensure_ascii=False, sort_keys=True, default=str), now, ), ) except Exception: return None return operation_id def attach_write_history_operation(payload: dict[str, Any], method: str, result: Any) -> Any: if method not in WRITE_HISTORY_RECORDED_METHODS or not isinstance(result, dict): return result operation_id = metadata_write_history_record(str(payload.get("base_id") or result.get("base_id") or ""), method, result) if operation_id: result.setdefault("operation_id", operation_id) return result def metadata_write_history(payload: dict[str, Any]) -> dict[str, Any]: method = "metadata.write.history" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error limit, limit_error = parse_int_argument(payload, "limit", method=method, default=20, minimum=1, maximum=200) if limit_error: return limit_error include_summary, include_summary_error = strict_bool_argument(payload, "include_summary", method=method, default=False) if include_summary_error: return include_summary_error operation_id = str(payload.get("operation_id") or "").strip() operation_method = str(payload.get("operation_method") or payload.get("write_method") or "").strip() status_filter = str(payload.get("status") or "").strip() routed_method_filter = str(payload.get("routed_method") or "").strip() backup_id = str(payload.get("backup_id") or "").strip() config, config_error = sql_config_for_base(base_id) if not config: return public_error_result({"status": "source_missing", "diagnostics": config_error}, include_storage=False, method=method) with cache_connection() as conn: if operation_id: rows = conn.execute( """ SELECT * FROM metadata_write_history WHERE server_key=? AND database_name=? AND operation_id=? """, (cache_server_key(config), cache_database_name(config), operation_id), ).fetchall() else: conditions = ["server_key=?", "database_name=?"] parameters: list[Any] = [cache_server_key(config), cache_database_name(config)] if operation_method: conditions.append("method=?") parameters.append(operation_method) if status_filter: conditions.append("status=?") parameters.append(status_filter) if routed_method_filter: conditions.append("routed_method=?") parameters.append(routed_method_filter) if backup_id: conditions.append("backup_ids_json LIKE ?") parameters.append(f"%{backup_id}%") parameters.append(int(limit or 20)) rows = conn.execute( f""" SELECT * FROM metadata_write_history WHERE {" AND ".join(conditions)} ORDER BY created_at DESC LIMIT ? """, tuple(parameters), ).fetchall() operations = [] summary = { "by_method": {}, "by_status": {}, "by_routed_method": {}, "with_backups": 0, } for row in rows: data = dict(row) row_backup_ids = json.loads(data.get("backup_ids_json") or "[]") if backup_id and backup_id not in row_backup_ids: continue method_key = str(data.get("method") or "") status_key = str(data.get("status") or "") routed_key = str(data.get("routed_method") or "") if method_key: summary["by_method"][method_key] = int(summary["by_method"].get(method_key) or 0) + 1 if status_key: summary["by_status"][status_key] = int(summary["by_status"].get(status_key) or 0) + 1 if routed_key: summary["by_routed_method"][routed_key] = int(summary["by_routed_method"].get(routed_key) or 0) + 1 if row_backup_ids: summary["with_backups"] = int(summary["with_backups"] or 0) + 1 operations.append( { "operation_id": data.get("operation_id"), "method": data.get("method"), "routed_method": data.get("routed_method") or None, "status": data.get("status"), "base_id": data.get("base_id"), "target_kind": data.get("target_kind") or None, "target_summary": json.loads(data.get("target_summary_json") or "{}"), "backup_ids": row_backup_ids, "created_at": datetime.fromtimestamp(float(data.get("created_at") or 0), tz=timezone.utc).isoformat(), **({"result": json.loads(data.get("result_json") or "{}")} if operation_id else {}), } ) return { "schema": "onec_metadata_write_history.v1", "method": method, "status": "ok", "base_id": base_id, "query": { "operation_id": operation_id or None, "operation_method": operation_method or None, "status": status_filter or None, "routed_method": routed_method_filter or None, "backup_id": backup_id or None, "limit": int(limit or 20), "include_summary": bool(include_summary), }, "operations": operations, "counts": {"operations": len(operations)}, **({"summary": summary} if include_summary else {}), } def metadata_write_rollback(payload: dict[str, Any]) -> dict[str, Any]: method = METADATA_WRITE_ROLLBACK_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error allow_rollback, allow_error = strict_bool_argument(payload, "allow_sql_saved_state_rollback", method=method, default=False) if allow_error: return allow_error if not allow_rollback: return invalid_argument(method, "allow_sql_saved_state_rollback", "Saved-state rollback by operation is opt-in; pass allow_sql_saved_state_rollback=true.") repository_error = repository_apply_gate(payload, method, "apply") if repository_error: return repository_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error operation_id = str(payload.get("operation_id") or "").strip() explicit_backup_id = str(payload.get("backup_id") or "").strip() if not operation_id and not explicit_backup_id: return invalid_argument(method, "operation_id", "Pass operation_id or backup_id.") history_payload = {"base_id": base_id, "limit": 20} if operation_id: history_payload["operation_id"] = operation_id elif explicit_backup_id: history_payload["backup_id"] = explicit_backup_id history = metadata_write_history(history_payload) operations = [item for item in history.get("operations") or [] if isinstance(item, dict)] if not operations: return { "schema": "onec_metadata_write_rollback.v1", "method": method, "status": "not_found", "base_id": base_id, "query": {"operation_id": operation_id or None, "backup_id": explicit_backup_id or None}, "diagnostics": {"message": "No write history operation with rollback backup evidence was found."}, } operation = operations[0] backup_ids = [str(item) for item in operation.get("backup_ids") or [] if item] if explicit_backup_id: if explicit_backup_id not in backup_ids and operation_id: return invalid_argument(method, "backup_id", "backup_id does not belong to the selected operation.") backup_id = explicit_backup_id elif len(backup_ids) == 1: backup_id = backup_ids[0] elif not backup_ids: return { "schema": "onec_metadata_write_rollback.v1", "method": method, "status": "not_found", "base_id": base_id, "operation": {key: operation.get(key) for key in ("operation_id", "method", "status", "routed_method")}, "diagnostics": {"message": "Selected operation does not contain backup ids."}, } else: return { "schema": "onec_metadata_write_rollback.v1", "method": method, "status": "ambiguous", "base_id": base_id, "operation": {key: operation.get(key) for key in ("operation_id", "method", "status", "routed_method")}, "backup_ids": backup_ids, "diagnostics": {"message": "Selected operation contains multiple backups. Pass backup_id explicitly."}, } rollback_result = storage_saved_state_rollback( { **repository_write_context(payload), "base_id": base_id, "backup_id": backup_id, "allow_sql_saved_state_rollback": True, "timeout_seconds": int(timeout_seconds or 30), } ) return { "schema": "onec_metadata_write_rollback.v1", "method": method, "status": rollback_result.get("status"), "applied": bool(rollback_result.get("applied")), "base_id": base_id, "operation": {key: operation.get(key) for key in ("operation_id", "method", "status", "routed_method")}, "backup_id": backup_id, "rollback_result": rollback_result, } def code_index_prune_file_modules( config: dict[str, str], *, table: str, file_name: str, keep_module_refs: list[str], ) -> int: keep = {str(item or "").strip() for item in keep_module_refs if str(item or "").strip()} with cache_connection() as conn: rows = conn.execute( """ SELECT module_ref FROM metadata_code_index_cache WHERE server_key=? AND database_name=? AND source_table=? AND file_name=? """, (cache_server_key(config), cache_database_name(config), table, file_name), ).fetchall() stale = [str(row["module_ref"] or "") for row in rows if str(row["module_ref"] or "") not in keep] for module_ref in stale: conn.execute( "DELETE FROM metadata_code_vector_cache WHERE server_key=? AND database_name=? AND module_ref=?", (cache_server_key(config), cache_database_name(config), module_ref), ) conn.execute( "DELETE FROM metadata_code_index_cache WHERE server_key=? AND database_name=? AND module_ref=?", (cache_server_key(config), cache_database_name(config), module_ref), ) return len(stale) def code_index_upsert( config: dict[str, str], *, base_id: str, table: str, file_name: str, module_ref: str, data: bytes, text: str, owner: dict[str, Any] | None = None, bsl_offset: int | None = None, stream_index: int | None = None, verified: bool = True, ) -> dict[str, Any]: now = time.time() payload_sha1 = hashlib.sha1(data).hexdigest() text_sha1 = code_text_sha1(text) routines = public_routine_blocks(text) owner = owner or {} with cache_connection() as conn: conn.execute( """ INSERT INTO metadata_code_index_cache ( server_key, database_name, module_ref, source_table, file_name, owner_kind, owner_name, owner_guid, form_name, extension_guid, extension_name, bsl_offset, stream_index, payload_sha1, text_sha1, text, routines_json, routine_count, source_bytes, updated_at, last_verified_at, last_seen_at ) VALUES ( :server_key, :database_name, :module_ref, :source_table, :file_name, :owner_kind, :owner_name, :owner_guid, :form_name, :extension_guid, :extension_name, :bsl_offset, :stream_index, :payload_sha1, :text_sha1, :text, :routines_json, :routine_count, :source_bytes, :updated_at, :last_verified_at, :last_seen_at ) ON CONFLICT(server_key, database_name, module_ref) DO UPDATE SET source_table=excluded.source_table, file_name=excluded.file_name, owner_kind=excluded.owner_kind, owner_name=excluded.owner_name, owner_guid=excluded.owner_guid, form_name=excluded.form_name, extension_guid=excluded.extension_guid, extension_name=excluded.extension_name, bsl_offset=excluded.bsl_offset, stream_index=excluded.stream_index, payload_sha1=excluded.payload_sha1, text_sha1=excluded.text_sha1, text=excluded.text, routines_json=excluded.routines_json, routine_count=excluded.routine_count, source_bytes=excluded.source_bytes, updated_at=excluded.updated_at, last_verified_at=excluded.last_verified_at, last_seen_at=excluded.last_seen_at """, { "server_key": cache_server_key(config), "database_name": cache_database_name(config), "module_ref": module_ref, "source_table": table, "file_name": file_name, "owner_kind": owner.get("owner_kind"), "owner_name": owner.get("owner_name"), "owner_guid": owner.get("owner_guid"), "form_name": owner.get("form_name"), "extension_guid": owner.get("extension_guid"), "extension_name": owner.get("extension_name"), "bsl_offset": bsl_offset, "stream_index": stream_index, "payload_sha1": payload_sha1, "text_sha1": text_sha1, "text": text, "routines_json": json.dumps(routines, ensure_ascii=False, sort_keys=True), "routine_count": len(routines), "source_bytes": len(data), "updated_at": now, "last_verified_at": now if verified else None, "last_seen_at": now, }, ) return { "module_ref": module_ref, "payload_sha1": payload_sha1, "text_sha1": text_sha1, "routines": routines, "routine_count": len(routines), } def code_index_chunk_texts(text: str, routines: list[dict[str, Any]]) -> list[dict[str, Any]]: normalized = str(text or "").replace("\r\n", "\n").replace("\r", "\n") lines = normalized.split("\n") chunks = [{"chunk_kind": "module", "routine_name": None, "chunk_index": 0, "text": normalized[:8000]}] for index, routine in enumerate(routines, start=1): try: start = int(routine.get("line_start") or 0) end = int(routine.get("line_end") or 0) except Exception: continue if start < 1 or end < start or start > len(lines): continue routine_text = "\n".join(lines[start - 1 : min(end, len(lines))]) chunks.append({"chunk_kind": "routine", "routine_name": routine.get("name"), "chunk_index": index, "text": routine_text}) return chunks def code_vector_upsert_chunks(config: dict[str, str], row: dict[str, Any]) -> int: module_ref = str(row.get("module_ref") or "") if not module_ref: return 0 chunks = code_index_chunk_texts(str(row.get("text") or ""), row.get("routines") or []) now = time.time() count = 0 with cache_connection() as conn: for chunk in chunks: chunk_text = str(chunk.get("text") or "") if not chunk_text.strip(): continue chunk_id = hashlib.sha1( f"{module_ref}|{chunk.get('chunk_kind')}|{chunk.get('routine_name') or ''}|{chunk.get('chunk_index')}|{row.get('text_sha1')}".encode("utf-8") ).hexdigest() embedding = code_hashing_embedding(chunk_text) conn.execute( """ INSERT INTO metadata_code_vector_cache ( server_key, database_name, chunk_id, module_ref, routine_name, chunk_kind, chunk_index, text_sha1, payload_sha1, embedding_model, embedding_json, text_preview, updated_at, last_seen_at ) VALUES ( :server_key, :database_name, :chunk_id, :module_ref, :routine_name, :chunk_kind, :chunk_index, :text_sha1, :payload_sha1, :embedding_model, :embedding_json, :text_preview, :updated_at, :last_seen_at ) ON CONFLICT(server_key, database_name, chunk_id) DO UPDATE SET module_ref=excluded.module_ref, routine_name=excluded.routine_name, chunk_kind=excluded.chunk_kind, chunk_index=excluded.chunk_index, text_sha1=excluded.text_sha1, payload_sha1=excluded.payload_sha1, embedding_model=excluded.embedding_model, embedding_json=excluded.embedding_json, text_preview=excluded.text_preview, updated_at=excluded.updated_at, last_seen_at=excluded.last_seen_at """, { "server_key": cache_server_key(config), "database_name": cache_database_name(config), "chunk_id": chunk_id, "module_ref": module_ref, "routine_name": chunk.get("routine_name"), "chunk_kind": chunk.get("chunk_kind") or "module", "chunk_index": int(chunk.get("chunk_index") or 0), "text_sha1": row.get("text_sha1"), "payload_sha1": row.get("payload_sha1"), "embedding_model": CODE_INDEX_VECTOR_MODEL, "embedding_json": json.dumps(embedding, ensure_ascii=False, separators=(",", ":")), "text_preview": chunk_text[:1000], "updated_at": now, "last_seen_at": now, }, ) count += 1 return count def semantic_lexical_score(query: str, text: str, object_name: str | None = None) -> float: normalized_query = normalize(query) if not normalized_query: return 0.0 haystack = normalize("\n".join(part for part in [object_name or "", text or ""] if part)) if not haystack: return 0.0 score = 0.0 if normalized_query == haystack: score += 10.0 elif normalized_query in haystack: score += 5.0 terms = [normalize(term) for term in re.split(r"\s+", query) if normalize(term)] if terms: score += sum(1.0 for term in terms if term in haystack) / max(1, len(terms)) return score def semantic_cache_read_selector(base_id: str, object_kind: str | None, source_route: dict[str, Any]) -> dict[str, Any]: table = source_route.get("table") file_name = source_route.get("file_name") or source_route.get("part_id") if object_kind == "Template" or table or file_name: return { "method": "templates.read", "base_id": base_id, "kind": "Template", "table": table or "ConfigCAS", "file_name": file_name, } return {"method": "metadata.route.resolve", "base_id": base_id, **({"guid": source_route.get("guid")} if source_route.get("guid") else {})} def semantic_cache_search(payload: dict[str, Any]) -> dict[str, Any]: method = "semantic.cache.search" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error config, config_error = sql_config_for_base(base_id) if not config: return public_error_result({"status": "source_missing", "diagnostics": config_error}, include_storage=False, method=method) query = str(payload.get("query") or "").strip() query_embedding = numeric_vector(payload.get("query_embedding")) if payload.get("query_embedding") is not None and query_embedding is None: return invalid_argument(method, "query_embedding", "query_embedding must be a non-empty JSON array of numbers.") if not query and not query_embedding: return invalid_argument(method, "query", "Pass query text or query_embedding.") object_kind = canonical_kind(str(payload.get("kind") or payload.get("object_kind") or "")) if (payload.get("kind") or payload.get("object_kind")) else None limit, limit_error = parse_int_alias_argument(payload, "limit", "max_matches", method=method, default=20, minimum=1, maximum=200) if limit_error: return limit_error scan_limit, scan_limit_error = parse_int_argument(payload, "scan_limit", method=method, default=1000, minimum=1, maximum=10000) if scan_limit_error: return scan_limit_error include_vectors, include_vectors_error = strict_bool_argument(payload, "include_vectors", method=method, default=False) if include_vectors_error: return include_vectors_error validate_candidates, validate_candidates_error = strict_bool_argument(payload, "validate_candidates", method=method, default=False) if validate_candidates_error: return validate_candidates_error validation_limit, validation_limit_error = parse_int_argument(payload, "validation_limit", method=method, default=limit, minimum=1, maximum=200) if validation_limit_error: return validation_limit_error validation_timeout_seconds, validation_timeout_error = parse_int_argument(payload, "validation_timeout_seconds", method=method, default=30, minimum=1, maximum=300) if validation_timeout_error: return validation_timeout_error clauses = ["server_key=?", "database_name=?"] params: list[Any] = [cache_server_key(config), cache_database_name(config)] if object_kind: clauses.append("object_kind=?") params.append(object_kind) with cache_connection() as conn: rows = conn.execute( f""" SELECT document_id, object_kind, object_guid, object_name, extension_guid, source_route_json, content_sha1, text_preview, embedding_model, embedding_json, vector_status, authoritative_source, updated_at, last_seen_at FROM semantic_document_cache WHERE {' AND '.join(clauses)} ORDER BY updated_at DESC LIMIT ? """, (*params, int(scan_limit or 1000)), ).fetchall() candidates: list[dict[str, Any]] = [] for row in rows: text_preview = str(row["text_preview"] or "") lexical = semantic_lexical_score(query, text_preview, row["object_name"]) if query else 0.0 vector_score = None if query_embedding and row["embedding_json"]: try: stored_embedding = numeric_vector(json.loads(row["embedding_json"] or "[]")) except Exception: stored_embedding = None if stored_embedding: vector_score = cosine_similarity(query_embedding, stored_embedding) score = float(vector_score if vector_score is not None else lexical) if score <= 0: continue try: source_route = json.loads(row["source_route_json"] or "{}") except Exception: source_route = {} if not isinstance(source_route, dict): source_route = {} match_by = "vector_embedding" if vector_score is not None else "lexical_cache" item = { "document_id": row["document_id"], "object": { "kind": row["object_kind"], "name": row["object_name"], "guid": row["object_guid"], "extension_guid": row["extension_guid"], }, "score": score, "match_by": match_by, "text_preview": text_preview, "source_route": source_route, "read_selector": semantic_cache_read_selector(base_id, row["object_kind"], source_route), "freshness": { "status": "candidate_only", "validation_required": True, "message": "Semantic cache results are retrieval candidates. Read through read_selector/source route before using for programming changes.", }, "cache": { "content_sha1": row["content_sha1"], "vector_status": row["vector_status"], "embedding_model": row["embedding_model"], "authoritative_source": row["authoritative_source"], "updated_at": row["updated_at"], "last_seen_at": row["last_seen_at"], }, } if include_vectors and row["embedding_json"]: item["embedding"] = json.loads(row["embedding_json"] or "[]") candidates.append(item) candidates.sort(key=lambda item: (-float(item.get("score") or 0), str(item.get("document_id") or ""))) matches = candidates[: int(limit or 20)] validation_counts = {"checked": 0, "fresh": 0, "stale": 0, "source_missing": 0, "not_found": 0, "other": 0} if validate_candidates and matches: for item in matches[: int(validation_limit or limit or 20)]: validation = semantic_cache_validate( { "base_id": base_id, "document_id": item.get("document_id"), "timeout_seconds": int(validation_timeout_seconds or 30), } ) item["validation"] = { "status": validation.get("status"), **({"error": validation.get("error")} if validation.get("error") else {}), **({"cache": validation.get("cache")} if validation.get("cache") else {}), **({"source": validation.get("source")} if validation.get("source") else {}), } validation_counts["checked"] += 1 status = str(validation.get("status") or "") if status == "ok": validation_counts["fresh"] += 1 item["freshness"] = validation.get("freshness") or item["freshness"] if validation.get("read_selector"): item["read_selector"] = validation["read_selector"] elif status == "stale": validation_counts["stale"] += 1 item["freshness"] = validation.get("freshness") or { "status": "stale", "validation_required": True, } elif status == "source_missing": validation_counts["source_missing"] += 1 item["freshness"] = validation.get("freshness") or { "status": "stale", "validation_required": True, } elif status == "not_found": validation_counts["not_found"] += 1 else: validation_counts["other"] += 1 return { "schema": "onec_semantic_cache_search.v1", "status": "ok" if matches else "not_found", **({"error": "not_found"} if not matches else {}), "base_id": base_id, "source": {"kind": "semantic_cache", "authoritative": False}, "query": { "query": query or None, "query_embedding": {"dimensions": len(query_embedding)} if query_embedding else None, "kind": object_kind, "limit": int(limit or 20), "scan_limit": int(scan_limit or 1000), "validate_candidates": bool(validate_candidates), "validation_limit": int(validation_limit or limit or 20), }, "matches": matches, "counts": { "matches": len(matches), "scanned_documents": len(rows), "candidate_matches": len(candidates), **({"validation": validation_counts} if validate_candidates else {}), }, "diagnostics": [ { "message": "Semantic cache search is not a source of truth. Use returned read_selector/source_route for live/source-cache validation before programming changes.", } ], } def metadata_code_index_status(payload: dict[str, Any]) -> dict[str, Any]: method = "metadata.code_index.status" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error config, config_error = sql_config_for_base(base_id_or_error) if not config: return public_error_result({"status": "source_missing", "diagnostics": config_error}, include_storage=False, method=method) with cache_connection() as conn: total = conn.execute( "SELECT COUNT(*) AS count FROM metadata_code_index_cache WHERE server_key=? AND database_name=?", (cache_server_key(config), cache_database_name(config)), ).fetchone() vectors = conn.execute( "SELECT COUNT(*) AS count FROM metadata_code_vector_cache WHERE server_key=? AND database_name=?", (cache_server_key(config), cache_database_name(config)), ).fetchone() by_table = conn.execute( """ SELECT source_table, COUNT(*) AS count, MIN(last_verified_at) AS oldest_verified_at, MAX(last_verified_at) AS newest_verified_at FROM metadata_code_index_cache WHERE server_key=? AND database_name=? GROUP BY source_table ORDER BY source_table """, (cache_server_key(config), cache_database_name(config)), ).fetchall() return { "schema": "onec_code_index_status.v1", "method": method, "status": "ok", "base_id": base_id_or_error, "source": {"kind": "code_index_cache", "authoritative": False}, "counts": {"modules": int((total or {})["count"] or 0), "vector_chunks": int((vectors or {})["count"] or 0)}, "tables": [ { "table": row["source_table"], "modules": int(row["count"] or 0), "oldest_verified_at": row["oldest_verified_at"], "newest_verified_at": row["newest_verified_at"], } for row in by_table ], "freshness": { "status": "cache_status_only", "validation_required": True, "message": "SQL remains authoritative; cache status does not prove individual modules are current.", }, } def current_code_index_text_from_sql( *, base_id: str, table: str, file_name: str, module_ref: str, data: bytes, bsl_offset: int | None = None, timeout_seconds: int = 30, ) -> tuple[str, dict[str, Any]]: _parsed_table, _parsed_file_name, stream_index = parse_module_id(module_ref) if stream_index is None and table in {"ConfigCAS", "ConfigCASSave", "Config", "ConfigSave"}: try: decoded = metadata_form_decode( { "base_id": base_id, "table": table, "file_name": file_name, "include_module": True, "include_module_text": True, "include_storage": False, "evidence_mode": "none", "max_items": 1, "timeout_seconds": int(timeout_seconds or 30), } ) profile = decoded.get("profile") if isinstance(decoded.get("profile"), dict) else {} module = profile.get("module") if isinstance(profile.get("module"), dict) else {} module_text = str(module.get("text") or "") if decoded.get("status") == "ok" and module_text.strip(): return module_text, {"status": "ok", "source": "form_embedded_module", "module_path": module.get("path")} except Exception: pass return extract_code_index_text_from_payload(data, module_ref=module_ref, bsl_offset=bsl_offset) def metadata_code_index_build(payload: dict[str, Any]) -> dict[str, Any]: method = "metadata.code_index.build" normalized_payload = normalize_object_selector_aliases(payload, method) if isinstance(normalized_payload, dict) and normalized_payload.get("status") == "invalid_argument": return normalized_payload payload = normalized_payload base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error config, config_error = sql_config_for_base(base_id) if not config: return public_error_result({"status": "source_missing", "diagnostics": config_error}, include_storage=False, method=method) extension_guid = str(payload.get("extension_guid") or "").strip().lower() if "extension_guid" in payload and not is_guid_text(extension_guid): return invalid_argument(method, "extension_guid", "extension_guid must be a GUID string.") has_object_selector = bool(payload.get("guid") or payload.get("name")) table = str( payload.get("table") or ( "ConfigCASSave" if extension_guid else "Config" if has_object_selector else "ConfigCAS" ) ) if table not in STORAGE_TABLES: return invalid_argument(method, "table", "Unsupported storage table.", allowed_values=sorted(STORAGE_TABLES)) prefix = str(payload.get("prefix") or "") max_items, max_items_error = parse_int_argument(payload, "max_items", method=method, default=500, minimum=1, maximum=20000) if max_items_error: return max_items_error scan_limit, scan_limit_error = parse_int_argument(payload, "scan_limit", method=method, default=int(max_items or 500), minimum=1, maximum=50000) if scan_limit_error: return scan_limit_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=120, minimum=1) if timeout_error: return timeout_error include_vectors, include_vectors_error = strict_bool_argument(payload, "include_vectors", method=method, default=True) if include_vectors_error: return include_vectors_error object_card: dict[str, Any] = {} discovered_module_refs: list[str] = [] object_form_module_jobs: dict[str, list[tuple[str, str, dict[str, Any], int | None, int | None]]] = {} forms_scanned = 0 form_modules_discovered = 0 empty_form_modules = 0 form_module_errors = 0 if has_object_selector: object_result = get_object( payload.get("kind"), str(payload.get("name") or payload.get("guid") or ""), base_id=base_id, table=table, extension_guid=extension_guid or None, include_storage=False, include_semantic=False, timeout_seconds=int(timeout_seconds or 120), ) if object_result.get("status") != "ok": result = dict(object_result) result["method"] = method return result object_card = object_result.get("object") or {} object_selector = { **payload, "base_id": base_id, "kind": object_card.get("kind") or payload.get("kind"), "guid": object_card.get("guid") or payload.get("guid"), "name": None, "table": table, **({"extension_guid": extension_guid} if extension_guid else {}), "timeout_seconds": int(timeout_seconds or 120), } modules_result = metadata_object_modules({**object_selector, "include_storage": True}) if modules_result.get("status") == "ok": discovered_module_refs.extend( str(module.get("module_id") or "") for module in modules_result.get("modules") or [] if isinstance(module, dict) and str(module.get("module_id") or "") ) commands_result = metadata_object_commands({**object_selector, "include_form_commands": False, "include_storage": False}) if commands_result.get("status") == "ok": discovered_module_refs.extend( str((command.get("read_selector") or {}).get("module_ref") or "") for command in commands_result.get("object_commands") or [] if isinstance(command, dict) and isinstance(command.get("read_selector"), dict) ) forms_result = metadata_object_forms({**object_selector, "include_storage": True}) if forms_result.get("status") == "ok": for form in forms_result.get("forms") or []: forms_scanned += 1 form_source = form.get("source") if isinstance(form, dict) and isinstance(form.get("source"), dict) else {} form_table = str(form_source.get("table") or table) form_file_name = str(form_source.get("file_name") or "") if form_table != table or not form_file_name: continue decoded_form = metadata_form_decode( { "base_id": base_id, "table": form_table, "file_name": form_file_name, "include_module": True, "include_module_text": True, "include_storage": True, "evidence_mode": "none", "max_items": 1, "timeout_seconds": int(timeout_seconds or 120), } ) profile = decoded_form.get("profile") if isinstance(decoded_form.get("profile"), dict) else {} form_module = profile.get("module") if isinstance(profile.get("module"), dict) else {} form_module_text = str(form_module.get("text") or "") if decoded_form.get("status") != "ok": form_module_errors += 1 continue if not form_module_text.strip() or not is_bsl_like_text(form_module_text): empty_form_modules += 1 continue form_module_ref = str((form_module.get("read_selector") or {}).get("module_ref") or f"{form_table}:{form_file_name}#form_module") bsl_offset_value = form_module.get("bsl_offset") bsl_offset_int = int(bsl_offset_value) if bsl_offset_value not in {None, ""} else None object_form_module_jobs.setdefault(form_file_name, []).append( ( form_module_ref, form_module_text, {"status": "ok", "source": "form_embedded_module", "bsl_offset": bsl_offset_int}, bsl_offset_int, None, ) ) form_modules_discovered += 1 discovered_module_refs.append(form_module_ref) file_names = [] for module_ref in discovered_module_refs: module_table, module_file_name, _ = parse_module_id(module_ref) if module_table == table and module_file_name and module_file_name not in file_names: file_names.append(module_file_name) else: files = storage_files_list( { "base_id": base_id, "table": table, "prefix": prefix, "limit": int(scan_limit or max_items or 500), "timeout_seconds": int(timeout_seconds or 120), "_internal": True, } ) if files.get("status") != "ok": return public_error_result(files, include_storage=False, method=method) file_names = [str(row.get("FileName") or "") for row in files.get("files") or [] if str(row.get("FileName") or "")] indexed = 0 skipped = 0 vectors = 0 pruned = 0 errors: list[dict[str, Any]] = [] for chunk_start in range(0, min(len(file_names), int(max_items or 500)), 100): chunk = file_names[chunk_start : chunk_start + 100] payloads, _read_config, read_error = read_storage_files_bytes(base_id, table, chunk, timeout_seconds=min(int(timeout_seconds or 120), 60)) if read_error: errors.append(read_error) continue for file_name in chunk: data = (payloads or {}).get(file_name) if not data: skipped += 1 continue base_module_ref = f"{table}:{file_name}" indexed_this_file = 0 indexed_module_refs: list[str] = [] try: from parser.cas_payload import classify_payload classified = classify_payload(data, include_text=True) except Exception: classified = {} streams = classified.get("stream_blocks") if isinstance(classified, dict) else [] module_jobs: list[tuple[str, str, dict[str, Any], int | None, int | None]] = [] module_jobs.extend(object_form_module_jobs.get(file_name) or []) if streams: for stream_index, stream in enumerate(streams): text = repair_bsl_mojibake_text(str((stream or {}).get("text") or "")) if text.strip() and (bool((stream or {}).get("has_bsl_marker")) or is_bsl_like_text(text)): module_jobs.append((f"{base_module_ref}#stream:{stream_index}", text, {"status": "ok", "source": "stream", "stream_index": stream_index}, None, stream_index)) if not module_jobs: owner = code_module_owner_from_cache(config, base_module_ref) bsl_offset = owner.get("bsl_offset") text, extraction = extract_code_index_text_from_payload(data, module_ref=base_module_ref, bsl_offset=int(bsl_offset) if bsl_offset not in {None, ""} else None) if text.strip() and extraction.get("status") == "ok": module_jobs.append((base_module_ref, text, extraction, int(bsl_offset) if bsl_offset not in {None, ""} else extraction.get("bsl_offset"), None)) for module_ref, text, extraction, bsl_offset_value, stream_index in module_jobs: owner = code_module_owner_from_cache(config, module_ref) upserted = code_index_upsert( config, base_id=base_id, table=table, file_name=file_name, module_ref=module_ref, data=data, text=text, owner=owner, bsl_offset=bsl_offset_value, stream_index=stream_index, verified=True, ) row = { **upserted, "module_ref": module_ref, "text": text, "routines": upserted.get("routines") or [], } if include_vectors: vectors += code_vector_upsert_chunks(config, row) indexed += 1 indexed_this_file += 1 indexed_module_refs.append(module_ref) pruned += code_index_prune_file_modules(config, table=table, file_name=file_name, keep_module_refs=indexed_module_refs) if not indexed_this_file: skipped += 1 return { "schema": "onec_code_index_build.v1", "method": method, "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "table": table}, "query": {"table": table, "prefix": prefix or None, "kind": payload.get("kind"), "name": payload.get("name"), "guid": payload.get("guid"), "extension_guid": extension_guid or None, "max_items": int(max_items or 500), "scan_limit": int(scan_limit or 500), "include_vectors": bool(include_vectors)}, **({"object": object_card} if object_card else {}), "counts": {"indexed": indexed, "skipped": skipped, "pruned": pruned, "vector_chunks": vectors, "errors": len(errors), "scanned_files": min(len(file_names), int(max_items or 500)), "discovered_module_refs": len(set(discovered_module_refs)), "forms_scanned": forms_scanned, "form_modules_discovered": form_modules_discovered, "empty_form_modules": empty_form_modules, "form_module_errors": form_module_errors}, "freshness": {"status": "live_sql_verified", "verified_against_sql": True}, "diagnostics": errors[:10], } def metadata_code_index_verify(payload: dict[str, Any]) -> dict[str, Any]: method = "metadata.code_index.verify" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error config, config_error = sql_config_for_base(base_id) if not config: return public_error_result({"status": "source_missing", "diagnostics": config_error}, include_storage=False, method=method) module_ref = str(payload.get("module_ref") or payload.get("module_id") or "").strip() if not module_ref: return invalid_argument(method, "module_ref", "module_ref is required.") table, file_name, stream_index = parse_module_id(module_ref) if not table or not file_name: return invalid_argument(method, "module_ref", MODULE_READ_SELECTOR_OR_MODULE_ID_MESSAGE) with cache_connection() as conn: row = conn.execute( """ SELECT * FROM metadata_code_index_cache WHERE server_key=? AND database_name=? AND module_ref=? LIMIT 1 """, (cache_server_key(config), cache_database_name(config), module_ref), ).fetchone() if not row: row = conn.execute( """ SELECT * FROM metadata_code_index_cache WHERE server_key=? AND database_name=? AND module_ref=? LIMIT 1 """, (cache_server_key(config), cache_database_name(config), normalize_module_ref_for_form_owner(module_ref)), ).fetchone() if not row: return {"schema": "onec_code_index_verify.v1", "method": method, "status": "not_found", "error": "not_found", "base_id": base_id, "module_ref": module_ref} cached = code_index_row_payload(row) data, _read_config, error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=int(payload.get("timeout_seconds") or 30)) if error: return {"schema": "onec_code_index_verify.v1", "method": method, "status": "source_missing", "error": "source_missing", "base_id": base_id, "module_ref": module_ref, "diagnostics": error.get("diagnostics")} current_payload_sha1 = hashlib.sha1(data).hexdigest() text, extraction = current_code_index_text_from_sql( base_id=base_id, table=table, file_name=file_name, module_ref=cached["module_ref"], data=data, bsl_offset=cached.get("bsl_offset"), timeout_seconds=int(payload.get("timeout_seconds") or 30), ) current_text_sha1 = code_text_sha1(text) fresh = current_payload_sha1 == cached.get("payload_sha1") and current_text_sha1 == cached.get("text_sha1") now = time.time() with cache_connection() as conn: if fresh: conn.execute( "UPDATE metadata_code_index_cache SET last_verified_at=?, last_seen_at=? WHERE server_key=? AND database_name=? AND module_ref=?", (now, now, cache_server_key(config), cache_database_name(config), cached["module_ref"]), ) return { "schema": "onec_code_index_verify.v1", "method": method, "status": "ok" if fresh else "stale", **({"error": "stale"} if not fresh else {}), "base_id": base_id, "module_ref": cached["module_ref"], "freshness": { "source": "code_index_cache", "verified_against_sql": True, "payload_sha1": cached.get("payload_sha1"), "text_sha1": cached.get("text_sha1"), "current_payload_sha1": current_payload_sha1, "current_text_sha1": current_text_sha1, "status": "cache_hit_verified" if fresh else "cache_hit_stale", }, "cache": {key: cached.get(key) for key in ("table", "file_name", "owner", "bsl_offset", "routine_count", "updated_at", "last_verified_at")}, "extraction": extraction, } def metadata_code_index_search(payload: dict[str, Any]) -> dict[str, Any]: method = "metadata.code_index.search" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error config, config_error = sql_config_for_base(base_id) if not config: return public_error_result({"status": "source_missing", "diagnostics": config_error}, include_storage=False, method=method) query = str(payload.get("query") or payload.get("pattern") or "").strip() if not query: return invalid_argument(method, "query", "Передайте непустой query.") mode = str(payload.get("mode") or "fast").strip().casefold() if mode not in {"fast", "live", "background_refresh"}: return invalid_argument(method, "mode", "mode must be one of: fast, live, background_refresh.", allowed_values=["fast", "live", "background_refresh"]) if mode == "live": result = search_modules({**payload, "method": None, "query": query, "resolve_owners": True}) result["method"] = method result["freshness"] = {"status": "live_sql_verified", "verified_against_sql": True} return result limit, limit_error = parse_int_alias_argument(payload, "limit", "max_matches", method=method, default=20, minimum=1, maximum=200) if limit_error: return limit_error scan_limit, scan_limit_error = parse_int_argument(payload, "scan_limit", method=method, default=1000, minimum=1, maximum=10000) if scan_limit_error: return scan_limit_error verify, verify_error = strict_bool_argument(payload, "verify", method=method, default=True) if verify_error: return verify_error like = f"%{query}%" with cache_connection() as conn: rows = conn.execute( """ SELECT * FROM metadata_code_index_cache WHERE server_key=? AND database_name=? AND text LIKE ? ORDER BY last_verified_at DESC, updated_at DESC LIMIT ? """, (cache_server_key(config), cache_database_name(config), like, int(scan_limit or 1000)), ).fetchall() matches: list[dict[str, Any]] = [] for row in rows[: int(limit or 20)]: cached = code_index_row_payload(row) text = str(cached.get("text") or "") offset = text.casefold().find(query.casefold()) snippet = text_snippet(text, query) if offset >= 0 else {"text": text[:300], "offset": None} freshness = { "source": "code_index_cache", "verified_against_sql": False, "payload_sha1": cached.get("payload_sha1"), "text_sha1": cached.get("text_sha1"), "status": "cache_hit_unverified", } if verify: verification = metadata_code_index_verify({"base_id": base_id, "module_ref": cached["module_ref"], "timeout_seconds": payload.get("timeout_seconds", 30)}) freshness = verification.get("freshness") or freshness read_selector = { "method": "modules.read", "base_id": base_id, "module_ref": cached["module_ref"], "preview": True, "max_chars": int(payload.get("read_max_chars") or 4000), **({"bsl_offset": cached.get("bsl_offset")} if cached.get("bsl_offset") is not None else {}), } matches.append( { "score": 1.0, "snippet": snippet, "owner": cached.get("owner"), "module": {"name": "Модуль БСЛ", "routine_count": cached.get("routine_count"), "form": (cached.get("owner") or {}).get("form")}, "read_selector": read_selector, "origin": {"source": "code_index_cache", "status": "verified" if freshness.get("status") == "cache_hit_verified" else "candidate"}, "freshness": freshness, } ) return { "schema": "onec_code_index_search.v1", "method": method, "status": "ok" if matches else "not_found", **({"error": "not_found"} if not matches else {}), "base_id": base_id, "source": {"kind": "code_index_cache", "authoritative": False}, "query": {"query": query, "mode": mode, "limit": int(limit or 20), "scan_limit": int(scan_limit or 1000), "verify": bool(verify)}, "matches": matches, "counts": {"matches": len(matches), "candidates": len(rows)}, } def metadata_code_index_refresh_changed(payload: dict[str, Any]) -> dict[str, Any]: method = "metadata.code_index.refresh_changed" search = metadata_code_index_search({**payload, "method": None, "query": str(payload.get("query") or ""), "verify": True}) stale = [match for match in search.get("matches") or [] if (match.get("freshness") or {}).get("status") == "cache_hit_stale"] refreshed = 0 for match in stale: selector = match.get("read_selector") or {} module_ref = str(selector.get("module_ref") or "") table, file_name, _ = parse_module_id(module_ref) if not table or not file_name: continue build = metadata_code_index_build({"base_id": payload.get("base_id"), "table": table, "prefix": file_name, "max_items": 1, "scan_limit": 1, "include_vectors": True}) refreshed += int((build.get("counts") or {}).get("indexed") or 0) return {"schema": "onec_code_index_refresh_changed.v1", "method": method, "status": "ok", "base_id": payload.get("base_id"), "counts": {"stale": len(stale), "refreshed": refreshed}, "search": search} def metadata_code_vector_search(payload: dict[str, Any]) -> dict[str, Any]: method = "metadata.code_vector.search" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error config, config_error = sql_config_for_base(base_id) if not config: return public_error_result({"status": "source_missing", "diagnostics": config_error}, include_storage=False, method=method) query = str(payload.get("query") or "").strip() query_embedding = numeric_vector(payload.get("query_embedding")) or (code_hashing_embedding(query) if query else None) if not query_embedding: return invalid_argument(method, "query", "Pass query or query_embedding.") limit, limit_error = parse_int_alias_argument(payload, "limit", "max_matches", method=method, default=10, minimum=1, maximum=100) if limit_error: return limit_error scan_limit, scan_limit_error = parse_int_argument(payload, "scan_limit", method=method, default=2000, minimum=1, maximum=20000) if scan_limit_error: return scan_limit_error verify, verify_error = strict_bool_argument(payload, "verify", method=method, default=True) if verify_error: return verify_error with cache_connection() as conn: rows = conn.execute( """ SELECT v.*, c.owner_kind, c.owner_name, c.owner_guid, c.form_name, c.bsl_offset, c.source_table, c.file_name FROM metadata_code_vector_cache v JOIN metadata_code_index_cache c ON c.server_key=v.server_key AND c.database_name=v.database_name AND c.module_ref=v.module_ref WHERE v.server_key=? AND v.database_name=? AND v.embedding_model=? ORDER BY v.updated_at DESC LIMIT ? """, (cache_server_key(config), cache_database_name(config), CODE_INDEX_VECTOR_MODEL, int(scan_limit or 2000)), ).fetchall() candidates: list[dict[str, Any]] = [] for row in rows: try: embedding = numeric_vector(json.loads(row["embedding_json"] or "[]")) except Exception: embedding = None score = cosine_similarity(query_embedding, embedding or []) if score is None: continue candidates.append((float(score), row)) candidates.sort(key=lambda pair: -pair[0]) matches: list[dict[str, Any]] = [] for score, row in candidates[: int(limit or 10)]: freshness = { "source": "code_vector_cache", "verified_against_sql": False, "payload_sha1": row["payload_sha1"], "text_sha1": row["text_sha1"], "status": "vector_candidate_unverified", } if verify: verification = metadata_code_index_verify({"base_id": base_id, "module_ref": row["module_ref"], "timeout_seconds": payload.get("timeout_seconds", 30)}) freshness = verification.get("freshness") or freshness matches.append( { "score": score, "match_by": "vector_embedding", "chunk": {"id": row["chunk_id"], "kind": row["chunk_kind"], "routine_name": row["routine_name"], "index": row["chunk_index"]}, "text_preview": row["text_preview"], "owner": {"kind": row["owner_kind"], "name": row["owner_name"], "guid": row["owner_guid"], "form": row["form_name"]}, "read_selector": { "method": "modules.read", "base_id": base_id, "module_ref": row["module_ref"], "preview": True, "max_chars": int(payload.get("read_max_chars") or 4000), **({"routine_name": row["routine_name"]} if row["routine_name"] else {}), **({"bsl_offset": row["bsl_offset"]} if row["bsl_offset"] is not None else {}), }, "freshness": freshness, } ) return { "schema": "onec_code_vector_search.v1", "method": method, "status": "ok" if matches else "not_found", **({"error": "not_found"} if not matches else {}), "base_id": base_id, "source": {"kind": "code_vector_cache", "authoritative": False, "embedding_model": CODE_INDEX_VECTOR_MODEL}, "query": {"query": query or None, "query_embedding": {"dimensions": len(query_embedding)}, "verify": bool(verify), "limit": int(limit or 10), "scan_limit": int(scan_limit or 2000)}, "matches": matches, "counts": {"matches": len(matches), "candidates": len(candidates)}, } def semantic_cache_pending(payload: dict[str, Any]) -> dict[str, Any]: method = "semantic.cache.pending" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error config, config_error = sql_config_for_base(base_id) if not config: return public_error_result({"status": "source_missing", "diagnostics": config_error}, include_storage=False, method=method) limit, limit_error = parse_int_argument(payload, "limit", method=method, default=100, minimum=1, maximum=1000) if limit_error: return limit_error object_kind = canonical_kind(str(payload.get("kind") or payload.get("object_kind") or "")) if (payload.get("kind") or payload.get("object_kind")) else None status_filter = str(payload.get("vector_status") or "pending_embedding").strip() if status_filter not in {"pending_embedding", "embedded", "error", "all"}: return invalid_argument(method, "vector_status", "vector_status must be one of: pending_embedding, embedded, error, all.") clauses = ["server_key=?", "database_name=?"] params: list[Any] = [cache_server_key(config), cache_database_name(config)] if object_kind: clauses.append("object_kind=?") params.append(object_kind) if status_filter != "all": clauses.append("vector_status=?") params.append(status_filter) with cache_connection() as conn: rows = conn.execute( f""" SELECT document_id, object_kind, object_guid, object_name, extension_guid, source_route_json, content_sha1, text_preview, vector_status, authoritative_source, updated_at, last_seen_at FROM semantic_document_cache WHERE {' AND '.join(clauses)} ORDER BY updated_at ASC LIMIT ? """, (*params, int(limit or 100)), ).fetchall() documents: list[dict[str, Any]] = [] for row in rows: try: source_route = json.loads(row["source_route_json"] or "{}") except Exception: source_route = {} if not isinstance(source_route, dict): source_route = {} documents.append( { "document_id": row["document_id"], "object": { "kind": row["object_kind"], "name": row["object_name"], "guid": row["object_guid"], "extension_guid": row["extension_guid"], }, "content_sha1": row["content_sha1"], "text": row["text_preview"], "source_route": source_route, "read_selector": semantic_cache_read_selector(base_id, row["object_kind"], source_route), "vector_status": row["vector_status"], "authoritative_source": row["authoritative_source"], "updated_at": row["updated_at"], "last_seen_at": row["last_seen_at"], "precondition": { "document_id": row["document_id"], "content_sha1": row["content_sha1"], "message": "Pass both values to semantic.cache.embedding.upsert. If content_sha1 changed, the embedding will be rejected.", }, } ) return { "schema": "onec_semantic_cache_pending.v1", "status": "ok", "base_id": base_id, "source": {"kind": "semantic_cache", "authoritative": False}, "query": {"kind": object_kind, "vector_status": status_filter, "limit": int(limit or 100)}, "documents": documents, "counts": {"documents": len(documents)}, } def semantic_cache_status(payload: dict[str, Any]) -> dict[str, Any]: method = "semantic.cache.status" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error config, config_error = sql_config_for_base(base_id) if not config: return public_error_result({"status": "source_missing", "diagnostics": config_error}, include_storage=False, method=method) object_kind = canonical_kind(str(payload.get("kind") or payload.get("object_kind") or "")) if (payload.get("kind") or payload.get("object_kind")) else None include_entries, include_entries_error = strict_bool_argument(payload, "include_entries", method=method, default=False) if include_entries_error: return include_entries_error limit, limit_error = parse_int_argument(payload, "limit", method=method, default=50, minimum=1, maximum=1000) if limit_error: return limit_error clauses = ["server_key=?", "database_name=?"] params: list[Any] = [cache_server_key(config), cache_database_name(config)] if object_kind: clauses.append("object_kind=?") params.append(object_kind) where_sql = " AND ".join(clauses) with cache_connection() as conn: total_row = conn.execute( f""" SELECT COUNT(*) AS total, SUM(CASE WHEN vector_status='pending_embedding' THEN 1 ELSE 0 END) AS pending, SUM(CASE WHEN vector_status='embedded' THEN 1 ELSE 0 END) AS embedded, SUM(CASE WHEN vector_status='error' THEN 1 ELSE 0 END) AS error, MIN(updated_at) AS oldest_updated_at, MAX(updated_at) AS newest_updated_at FROM semantic_document_cache WHERE {where_sql} """, params, ).fetchone() group_rows = conn.execute( f""" SELECT COALESCE(object_kind, '') AS object_kind, COALESCE(vector_status, '') AS vector_status, COALESCE(embedding_model, '') AS embedding_model, COUNT(*) AS count, MIN(updated_at) AS oldest_updated_at, MAX(updated_at) AS newest_updated_at FROM semantic_document_cache WHERE {where_sql} GROUP BY object_kind, vector_status, embedding_model ORDER BY object_kind, vector_status, embedding_model """, params, ).fetchall() entry_rows = [] if include_entries: entry_rows = conn.execute( f""" SELECT document_id, object_kind, object_guid, object_name, extension_guid, content_sha1, vector_status, embedding_model, authoritative_source, updated_at, last_seen_at FROM semantic_document_cache WHERE {where_sql} ORDER BY CASE WHEN vector_status='pending_embedding' THEN 0 WHEN vector_status='error' THEN 1 ELSE 2 END, updated_at, document_id LIMIT ? """, (*params, int(limit or 50)), ).fetchall() groups = [ { "kind": row["object_kind"] or None, "vector_status": row["vector_status"] or None, "embedding_model": row["embedding_model"] or None, "count": int(row["count"] or 0), "oldest_updated_at": row["oldest_updated_at"], "newest_updated_at": row["newest_updated_at"], } for row in group_rows ] entries = [ { "document_id": row["document_id"], "object": { "kind": row["object_kind"], "name": row["object_name"], "guid": row["object_guid"], "extension_guid": row["extension_guid"], }, "content_sha1": row["content_sha1"], "vector_status": row["vector_status"], "embedding_model": row["embedding_model"], "authoritative_source": row["authoritative_source"], "updated_at": row["updated_at"], "last_seen_at": row["last_seen_at"], } for row in entry_rows ] return { "schema": "onec_semantic_cache_status.v1", "status": "ok", "base_id": base_id, "source": {"kind": "semantic_document_cache", "authoritative": False}, "query": {"kind": object_kind, "include_entries": bool(include_entries), "limit": int(limit or 50)}, "counts": { "total": int((total_row or {})["total"] or 0) if total_row else 0, "pending_embedding": int((total_row or {})["pending"] or 0) if total_row else 0, "embedded": int((total_row or {})["embedded"] or 0) if total_row else 0, "error": int((total_row or {})["error"] or 0) if total_row else 0, "groups": len(groups), }, "oldest_updated_at": (total_row or {})["oldest_updated_at"] if total_row else None, "newest_updated_at": (total_row or {})["newest_updated_at"] if total_row else None, "groups": groups, **({"entries": entries} if include_entries else {}), "diagnostics": [ { "message": "Semantic cache status is readiness telemetry. Semantic documents are not authoritative source objects; validate via source route/read_selector before programming changes.", } ], } def semantic_cache_validate(payload: dict[str, Any]) -> dict[str, Any]: method = "semantic.cache.validate" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error config, config_error = sql_config_for_base(base_id) if not config: return public_error_result({"status": "source_missing", "diagnostics": config_error}, include_storage=False, method=method) document_id = str(payload.get("document_id") or "").strip() row = semantic_document_cache_lookup(config, document_id) if not row: return { "schema": "onec_semantic_cache_validate.v1", "status": "not_found", "error": "not_found", "base_id": base_id, "document_id": document_id, "diagnostics": [{"message": "Semantic cache document was not found for this base_id."}], } try: source_route = json.loads(row.get("source_route_json") or "{}") except Exception: source_route = {} if not isinstance(source_route, dict): source_route = {} table = str(source_route.get("table") or "ConfigCAS").strip() or "ConfigCAS" file_name = str(source_route.get("file_name") or source_route.get("part_id") or "").strip() if not file_name: semantic_document_cache_mark_changed(config, document_id) return { "schema": "onec_semantic_cache_validate.v1", "status": "stale", "error": "route_missing", "base_id": base_id, "document_id": document_id, "object": { "kind": row.get("object_kind"), "name": row.get("object_name"), "guid": row.get("object_guid"), "extension_guid": row.get("extension_guid"), }, "freshness": { "status": "stale", "validation_required": True, "reason": "semantic_source_route_missing_file_name", }, "cache": {"content_sha1": row.get("content_sha1"), "vector_status": "error"}, } timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1, maximum=300) if timeout_error: return timeout_error data, _, error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=int(timeout_seconds or 30)) if error or data is None: semantic_document_cache_mark_changed(config, document_id) return { "schema": "onec_semantic_cache_validate.v1", "status": "source_missing", "error": "source_missing", "base_id": base_id, "document_id": document_id, "object": { "kind": row.get("object_kind"), "name": row.get("object_name"), "guid": row.get("object_guid"), "extension_guid": row.get("extension_guid"), }, "source_route": source_route, "freshness": { "status": "stale", "validation_required": True, "reason": "semantic_source_payload_unreadable", }, "cache": {"content_sha1": row.get("content_sha1"), "vector_status": "error"}, "diagnostics": [{"message": "Failed to read current source bytes for semantic cache validation.", "error": error}], } current_sha1 = hashlib.sha1(data).hexdigest() cached_sha1 = str(row.get("content_sha1") or "").strip().lower() if current_sha1 != cached_sha1: semantic_document_cache_mark_changed(config, document_id) return { "schema": "onec_semantic_cache_validate.v1", "status": "stale", "error": "content_changed", "base_id": base_id, "document_id": document_id, "object": { "kind": row.get("object_kind"), "name": row.get("object_name"), "guid": row.get("object_guid"), "extension_guid": row.get("extension_guid"), }, "source_route": source_route, "read_selector": semantic_cache_read_selector(base_id, row.get("object_kind"), source_route), "freshness": { "status": "stale", "validation_required": True, "reason": "semantic_source_payload_changed", }, "cache": { "cached_content_sha1": cached_sha1, "current_content_sha1": current_sha1, "vector_status": "error", }, "diagnostics": [ { "message": "Semantic cache document no longer matches current source bytes. Re-decode the object and recompute embedding before using this result for programming changes.", } ], } semantic_document_cache_mark_seen(config, document_id) return { "schema": "onec_semantic_cache_validate.v1", "status": "ok", "base_id": base_id, "document_id": document_id, "object": { "kind": row.get("object_kind"), "name": row.get("object_name"), "guid": row.get("object_guid"), "extension_guid": row.get("extension_guid"), }, "source_route": source_route, "read_selector": semantic_cache_read_selector(base_id, row.get("object_kind"), source_route), "freshness": { "status": "fresh", "validated_by": "current_source_sha1", "validation_required": False, }, "cache": { "content_sha1": cached_sha1, "vector_status": row.get("vector_status"), "embedding_model": row.get("embedding_model"), "authoritative_source": row.get("authoritative_source"), }, "source": { "authoritative": True, "kind": "live_sql_payload_sha1", "message": "The semantic cache candidate matched current source bytes. Use read_selector for the actual source object payload.", }, } def semantic_cache_validate_batch(payload: dict[str, Any]) -> dict[str, Any]: method = "semantic.cache.validate_batch" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error config, config_error = sql_config_for_base(base_id) if not config: return public_error_result({"status": "source_missing", "diagnostics": config_error}, include_storage=False, method=method) limit, limit_error = parse_int_argument(payload, "limit", method=method, default=100, minimum=1, maximum=1000) if limit_error: return limit_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1, maximum=300) if timeout_error: return timeout_error ids_value = payload.get("document_ids") document_ids: list[str] = [] if ids_value is not None: if not isinstance(ids_value, list): return invalid_argument(method, "document_ids", "document_ids must be a JSON array of strings.") for item in ids_value: if not isinstance(item, str): return invalid_argument(method, "document_ids", "document_ids must contain only strings.") text = item.strip() if text and text not in document_ids: document_ids.append(text) document_ids = document_ids[: int(limit or 100)] else: object_kind = canonical_kind(str(payload.get("kind") or payload.get("object_kind") or "")) if (payload.get("kind") or payload.get("object_kind")) else None status_filter = str(payload.get("vector_status") or "all").strip() if status_filter not in {"pending_embedding", "embedded", "error", "all"}: return invalid_argument(method, "vector_status", "vector_status must be one of: pending_embedding, embedded, error, all.") clauses = ["server_key=?", "database_name=?"] params: list[Any] = [cache_server_key(config), cache_database_name(config)] if object_kind: clauses.append("object_kind=?") params.append(object_kind) if status_filter != "all": clauses.append("vector_status=?") params.append(status_filter) with cache_connection() as conn: rows = conn.execute( f""" SELECT document_id FROM semantic_document_cache WHERE {' AND '.join(clauses)} ORDER BY updated_at ASC, document_id LIMIT ? """, (*params, int(limit or 100)), ).fetchall() document_ids = [str(row["document_id"] or "") for row in rows if str(row["document_id"] or "")] results: list[dict[str, Any]] = [] counts = {"checked": 0, "fresh": 0, "stale": 0, "source_missing": 0, "not_found": 0, "other": 0} for document_id in document_ids: item = semantic_cache_validate( { "base_id": base_id, "document_id": document_id, "timeout_seconds": int(timeout_seconds or 30), } ) compact = { "document_id": document_id, "status": item.get("status"), **({"error": item.get("error")} if item.get("error") else {}), **({"object": item.get("object")} if item.get("object") else {}), **({"freshness": item.get("freshness")} if item.get("freshness") else {}), **({"cache": item.get("cache")} if item.get("cache") else {}), **({"read_selector": item.get("read_selector")} if item.get("read_selector") else {}), } results.append(compact) counts["checked"] += 1 status = str(item.get("status") or "") if status == "ok": counts["fresh"] += 1 elif status == "stale": counts["stale"] += 1 elif status == "source_missing": counts["source_missing"] += 1 elif status == "not_found": counts["not_found"] += 1 else: counts["other"] += 1 return { "schema": "onec_semantic_cache_validate_batch.v1", "status": "ok", "base_id": base_id, "source": { "kind": "semantic_cache_validation_batch", "authoritative": False, "message": "Batch validation reports freshness only. Use each fresh read_selector for the actual source object.", }, "query": { "document_ids": document_ids if ids_value is not None else None, "kind": canonical_kind(str(payload.get("kind") or payload.get("object_kind") or "")) if (payload.get("kind") or payload.get("object_kind")) else None, "vector_status": str(payload.get("vector_status") or "all").strip() if ids_value is None else None, "limit": int(limit or 100), }, "counts": counts, "results": results, "diagnostics": [ { "message": "Fresh batch items are verified by current source bytes. Stale/source_missing items must be re-read and re-embedded before programming changes.", } ], } def semantic_cache_refresh(payload: dict[str, Any]) -> dict[str, Any]: method = "semantic.cache.refresh" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error config, config_error = sql_config_for_base(base_id) if not config: return public_error_result({"status": "source_missing", "diagnostics": config_error}, include_storage=False, method=method) document_id = str(payload.get("document_id") or "").strip() row = semantic_document_cache_lookup(config, document_id) if not row: return { "schema": "onec_semantic_cache_refresh.v1", "status": "not_found", "error": "not_found", "base_id": base_id, "document_id": document_id, "diagnostics": [{"message": "Semantic cache document was not found for this base_id."}], } object_kind = str(row.get("object_kind") or "").strip() if object_kind and object_kind != "Template": return { "schema": "onec_semantic_cache_refresh.v1", "status": "unsupported", "error": "unsupported_kind", "base_id": base_id, "document_id": document_id, "object": { "kind": row.get("object_kind"), "name": row.get("object_name"), "guid": row.get("object_guid"), "extension_guid": row.get("extension_guid"), }, "diagnostics": [{"message": "semantic.cache.refresh currently supports Template semantic documents produced from decoded template artifacts."}], } try: source_route = json.loads(row.get("source_route_json") or "{}") except Exception: source_route = {} if not isinstance(source_route, dict): source_route = {} table = str(source_route.get("table") or "ConfigCAS").strip() or "ConfigCAS" file_name = str(source_route.get("file_name") or source_route.get("part_id") or "").strip() if not file_name: return { "schema": "onec_semantic_cache_refresh.v1", "status": "stale", "error": "route_missing", "base_id": base_id, "document_id": document_id, "freshness": {"status": "stale", "validation_required": True, "reason": "semantic_source_route_missing_file_name"}, } timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1, maximum=300) if timeout_error: return timeout_error data, _, error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=int(timeout_seconds or 60)) if error or data is None: semantic_document_cache_mark_changed(config, document_id) return { "schema": "onec_semantic_cache_refresh.v1", "status": "source_missing", "error": "source_missing", "base_id": base_id, "document_id": document_id, "source_route": source_route, "freshness": {"status": "stale", "validation_required": True, "reason": "semantic_source_payload_unreadable"}, "diagnostics": [{"message": "Failed to read current source bytes for semantic cache refresh.", "error": error}], } current_sha1 = hashlib.sha1(data).hexdigest() cached_sha1 = str(row.get("content_sha1") or "").strip().lower() structure = extract_moxel_public_structure(data) semantic_text = template_structure_semantic_text(structure) if not semantic_text.strip(): semantic_document_cache_mark_changed(config, document_id) return { "schema": "onec_semantic_cache_refresh.v1", "status": "unsupported", "error": "empty_semantic_text", "base_id": base_id, "document_id": document_id, "source_route": source_route, "cache": {"cached_content_sha1": cached_sha1, "current_content_sha1": current_sha1, "vector_status": "error"}, "diagnostics": [{"message": "Current payload decoded, but no semantic text could be produced for embedding."}], } artifact = { "part_id": file_name, "table": table, "content_kind": "MOXCEL", "structure": structure, "artifact_cache": {"status": "refresh_stored", "content_sha1": current_sha1}, } decoded_artifact_cache_upsert( config, artifact_kind=MOXEL_TEMPLATE_ARTIFACT_KIND, content_sha1=current_sha1, source_table=table, source_file=file_name, payload_bytes=len(data), artifact=artifact, semantic_text=semantic_text, ) new_document_id = refreshed_semantic_document_id(document_id, {**source_route, "table": table, "file_name": file_name, "part_id": source_route.get("part_id") or file_name}, current_sha1) refreshed_route = {**source_route, "table": table, "file_name": file_name, "part_id": source_route.get("part_id") or file_name} semantic_document_cache_upsert( config, document_id=new_document_id, object_kind=row.get("object_kind") or "Template", object_guid=row.get("object_guid") or file_name, object_name=row.get("object_name"), extension_guid=row.get("extension_guid"), source_route=refreshed_route, content_sha1=current_sha1, text=semantic_text, authoritative_source=row.get("authoritative_source") or "decoded_artifact_cache", ) replaced_document = new_document_id != document_id if replaced_document: semantic_document_cache_delete(config, document_id) return { "schema": "onec_semantic_cache_refresh.v1", "status": "ok", "base_id": base_id, "document_id": new_document_id, "previous_document_id": document_id if replaced_document else None, "object": { "kind": row.get("object_kind") or "Template", "name": row.get("object_name"), "guid": row.get("object_guid") or file_name, "extension_guid": row.get("extension_guid"), }, "source_route": refreshed_route, "read_selector": semantic_cache_read_selector(base_id, row.get("object_kind") or "Template", refreshed_route), "cache": { "previous_content_sha1": cached_sha1 or None, "content_sha1": current_sha1, "content_changed": current_sha1 != cached_sha1, "vector_status": "pending_embedding", "artifact_cache": "stored", "semantic_text_chars": len(semantic_text), }, "freshness": { "status": "fresh", "validated_by": "refresh_current_source_decode", "validation_required": False, }, "precondition": { "document_id": new_document_id, "content_sha1": current_sha1, "message": "Pass both values to semantic.cache.embedding.upsert after computing a fresh embedding.", }, "diagnostics": [ { "message": "Semantic document was refreshed from current source bytes and queued for embedding.", } ], } def semantic_cache_rebuild(payload: dict[str, Any]) -> dict[str, Any]: method = "semantic.cache.rebuild" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error extension_guid, extension_error = extension_filter_to_guid(base_id, str(payload.get("extension") or ""), method=method) if extension_error: return extension_error object_kind = canonical_kind(str(payload.get("kind") or payload.get("object_kind") or payload.get("object_type") or "Template")) if object_kind != "Template": return invalid_argument(method, "kind", "semantic.cache.rebuild currently supports kind=Template only.") limit, limit_error = parse_int_argument(payload, "limit", method=method, default=100, minimum=1, maximum=5000) if limit_error: return limit_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=120, minimum=1, maximum=600) if timeout_error: return timeout_error refresh_routes, refresh_routes_error = strict_bool_argument(payload, "refresh_routes", method=method, default=False) if refresh_routes_error: return refresh_routes_error include_entries, include_entries_error = strict_bool_argument(payload, "include_entries", method=method, default=False) if include_entries_error: return include_entries_error cache_config, config_error = sql_config_for_base(base_id) if not cache_config: return public_error_result({"status": "source_missing", "diagnostics": config_error}, include_storage=False, method=method) route_rebuild = None if refresh_routes: route_rebuild = extension_cache_rebuild( { "base_id": base_id, "extension": payload.get("extension"), "kind": "Template", "max_items": int(limit or 100), "timeout_seconds": int(timeout_seconds or 120), "include_matches": False, } ) if route_rebuild.get("status") not in {"ok", "not_found"}: return route_rebuild clauses = ["server_key=?", "database_name=?", "freshness_status!='stale'", "object_kind='Template'"] params: list[Any] = [cache_server_key(cache_config), cache_database_name(cache_config)] if extension_guid: clauses.append("extension_guid=?") params.append(extension_guid.lower()) with cache_connection() as conn: before_rows = conn.execute( """ SELECT document_id, content_sha1 FROM semantic_document_cache WHERE server_key=? AND database_name=? """, (cache_server_key(cache_config), cache_database_name(cache_config)), ).fetchall() before_docs = {str(row["document_id"] or ""): str(row["content_sha1"] or "") for row in before_rows} route_rows = conn.execute( f""" SELECT descriptor_cas_key, extension_guid, extension_name, object_kind, name, guid, route_json FROM extension_route_cache WHERE {' AND '.join(clauses)} ORDER BY updated_at DESC, descriptor_cas_key LIMIT ? """, (*params, int(limit or 100)), ).fetchall() entries: list[dict[str, Any]] = [] errors: list[dict[str, Any]] = [] templates_read = 0 parts_seen = 0 for row in route_rows: try: route = json.loads(row["route_json"] or "{}") except Exception: route = {} if not isinstance(route, dict): route = {} table = str(route.get("table") or "ConfigCAS").strip() or "ConfigCAS" file_name = str(route.get("file_name") or row["descriptor_cas_key"] or "").strip() if not file_name: errors.append({"descriptor_cas_key": row["descriptor_cas_key"], "error": "route_missing_file_name"}) continue result = read_template_by_route( { "base_id": base_id, "kind": "Template", "table": table, "file_name": file_name, "timeout_seconds": int(timeout_seconds or 120), "view": "summary", } ) if result.get("status") != "ok": errors.append( { "descriptor_cas_key": row["descriptor_cas_key"], "name": row["name"], "status": result.get("status"), "error": result.get("error"), "diagnostics": result.get("diagnostics"), } ) continue templates_read += int((result.get("counts") or {}).get("templates") or 0) parts_seen += int((result.get("counts") or {}).get("parts") or 0) if include_entries: entries.append( { "descriptor_cas_key": row["descriptor_cas_key"], "object": { "kind": row["object_kind"], "name": row["name"], "guid": row["guid"], "extension_guid": row["extension_guid"], }, "route": {"table": table, "file_name": file_name}, "counts": result.get("counts"), } ) with cache_connection() as conn: after_rows = conn.execute( """ SELECT document_id, content_sha1, vector_status FROM semantic_document_cache WHERE server_key=? AND database_name=? """, (cache_server_key(cache_config), cache_database_name(cache_config)), ).fetchall() after_docs = {str(row["document_id"] or ""): {"content_sha1": str(row["content_sha1"] or ""), "vector_status": row["vector_status"]} for row in after_rows} created = [document_id for document_id in after_docs if document_id not in before_docs] changed = [ document_id for document_id, item in after_docs.items() if document_id in before_docs and before_docs[document_id] != item["content_sha1"] ] pending = [document_id for document_id, item in after_docs.items() if item.get("vector_status") == "pending_embedding"] return { "schema": "onec_semantic_cache_rebuild.v1", "status": "ok", "base_id": base_id, "source": { "kind": "extension_route_cache", "authoritative": False, "message": "Semantic rebuild warms local search/index caches. Validate candidates against current source bytes before programming changes.", }, "query": { "extension": payload.get("extension"), "extension_guid": extension_guid, "kind": object_kind, "limit": int(limit or 100), "refresh_routes": bool(refresh_routes), }, "counts": { "routes_scanned": len(route_rows), "templates_read": templates_read, "parts_seen": parts_seen, "semantic_documents_created": len(created), "semantic_documents_changed": len(changed), "pending_embedding_total": len(pending), "errors": len(errors), **({"route_rebuild_cached_routes": (route_rebuild.get("counts") or {}).get("cached_routes")} if route_rebuild else {}), }, **({"route_rebuild": route_rebuild} if route_rebuild else {}), **({"entries": entries[:200]} if include_entries else {}), **({"errors": errors[:200]} if errors else {}), "diagnostics": [ { "message": "Only Template routes are rebuilt at this stage; semantic/vector results remain cache candidates until validated.", } ], } def semantic_cache_embedding_upsert(payload: dict[str, Any]) -> dict[str, Any]: method = "semantic.cache.embedding.upsert" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error config, config_error = sql_config_for_base(base_id) if not config: return public_error_result({"status": "source_missing", "diagnostics": config_error}, include_storage=False, method=method) document_id = str(payload.get("document_id") or "").strip() content_sha1 = str(payload.get("content_sha1") or "").strip().lower() embedding_model = str(payload.get("embedding_model") or "").strip() embedding = numeric_vector(payload.get("embedding")) if not document_id: return invalid_argument(method, "document_id", "document_id is required.") if not re.fullmatch(r"[0-9a-f]{40}", content_sha1): return invalid_argument(method, "content_sha1", "content_sha1 must be a 40-character lowercase SHA1 hex string from semantic.cache.pending.") if not embedding_model: return invalid_argument(method, "embedding_model", "embedding_model is required.") if embedding is None: return invalid_argument(method, "embedding", "embedding must be a non-empty JSON array of numbers.") now = time.time() with cache_connection() as conn: row = conn.execute( """ SELECT content_sha1, vector_status FROM semantic_document_cache WHERE server_key=? AND database_name=? AND document_id=? LIMIT 1 """, (cache_server_key(config), cache_database_name(config), document_id), ).fetchone() if not row: return { "schema": "onec_semantic_cache_embedding_upsert.v1", "status": "not_found", "error": "document_not_found", "base_id": base_id, "document_id": document_id, "diagnostics": {"message": "Semantic document was not found. Rebuild/read the source artifact first."}, } current_sha1 = str(row["content_sha1"] or "").lower() if current_sha1 != content_sha1: return { "schema": "onec_semantic_cache_embedding_upsert.v1", "status": "conflict", "error": "content_sha1_mismatch", "base_id": base_id, "document_id": document_id, "expected_content_sha1": current_sha1, "provided_content_sha1": content_sha1, "diagnostics": { "message": "Embedding was rejected because the semantic document changed after it was queued. Fetch semantic.cache.pending again and recompute embedding.", }, } conn.execute( """ UPDATE semantic_document_cache SET embedding_model=?, embedding_json=?, vector_status='embedded', updated_at=?, last_seen_at=? WHERE server_key=? AND database_name=? AND document_id=? AND content_sha1=? """, ( embedding_model, json.dumps(embedding, ensure_ascii=False, separators=(",", ":")), now, now, cache_server_key(config), cache_database_name(config), document_id, content_sha1, ), ) return { "schema": "onec_semantic_cache_embedding_upsert.v1", "status": "ok", "base_id": base_id, "document_id": document_id, "content_sha1": content_sha1, "embedding_model": embedding_model, "dimensions": len(embedding), "vector_status": "embedded", "freshness": { "status": "stored_with_content_precondition", "message": "Embedding is tied to this content_sha1 and will not be reused for changed source content.", }, } FIELD_TYPE_CACHE_ROLE = "metadata_field_type_v1" def metadata_field_type_cache_lookup(config: dict[str, str] | None, field_guids: set[str]) -> dict[str, dict[str, Any]]: wanted = sorted({str(guid or "").lower() for guid in field_guids if is_guid_text(str(guid or ""))}) if not config or not wanted: return {} result: dict[str, dict[str, Any]] = {} with cache_connection() as conn: for start in range(0, len(wanted), 500): chunk = wanted[start : start + 500] placeholders = ",".join(["?"] * len(chunk)) rows = conn.execute( f""" SELECT guid, payload_json FROM metadata_guid_index WHERE server_key = ? AND database_name = ? AND guid_role = ? AND guid IN ({placeholders}) """, (cache_server_key(config), cache_database_name(config), FIELD_TYPE_CACHE_ROLE, *chunk), ).fetchall() for row in rows: try: payload = json.loads(row["payload_json"] or "{}") except Exception: continue if isinstance(payload, dict): result[str(row["guid"]).lower()] = payload return result def metadata_field_type_cache_upsert(config: dict[str, str] | None, field_guid: str, type_info: Any, *, owner: dict[str, Any] | None = None, field_name: str | None = None) -> None: guid = str(field_guid or "").lower() if not config or not is_guid_text(guid) or not isinstance(type_info, dict): return owner = owner or {} metadata_guid_index_upsert( config, { "guid": guid, "guid_role": FIELD_TYPE_CACHE_ROLE, "kind": "Attribute", "kind_ru": "Реквизит", "name": field_name, "presentation": str(type_info.get("presentation") or ""), "owner_guid": owner.get("guid"), "owner_kind": owner.get("kind"), "owner_name": owner.get("name"), "payload": sanitize_public_result(type_info), "source_file": owner.get("guid"), }, ) def metadata_attributes_cache_role(scope: str) -> str: normalized_scope = normalize(scope or "all") or "all" if normalized_scope in {"attrs", "requisites"}: normalized_scope = "attributes" if normalized_scope in {"tabs", "tabularsections", "tableparts"}: normalized_scope = "tabular_sections" return f"object_attributes_v2_{normalized_scope}" def metadata_commands_cache_role(include_form_commands: bool) -> str: return "object_commands_v2_with_forms" if include_form_commands else "object_commands_v2_object_only" def public_visible_command(item: dict[str, Any], *, include_storage: bool = False) -> bool: if include_storage: return True if item.get("status") == "source_missing" and not item.get("name") and not item.get("synonym") and not item.get("title"): return False return True def metadata_modules_cache_role() -> str: return "object_modules_v3" def metadata_module_owner_cache_role() -> str: return "module_owner_v1" def metadata_module_owner_cache_upsert( config: dict[str, str], module_ref: str, owner: dict[str, Any], module: dict[str, Any] | None = None, ) -> None: normalized_module_ref = str(module_ref or "").strip() if not config or not normalized_module_ref: return owner_guid = str((owner or {}).get("guid") or "").lower() if not is_guid_text(owner_guid): return now = time.time() module_payload = { "module_name": (module or {}).get("name"), "module_ordinal": (module or {}).get("module_ordinal"), "suffix": (module or {}).get("suffix"), "stream_index": (module or {}).get("stream_index"), "file_name": (module or {}).get("file_name"), "payload": (module or {}).get("payload"), } payload_json = json.dumps(module_payload, ensure_ascii=False, sort_keys=True) if module_payload else None module_table, module_file_name, module_stream_index = parse_module_id(normalized_module_ref) with cache_connection() as conn: conn.execute( """ INSERT INTO metadata_module_owner_cache ( server_key, database_name, module_ref, module_table, file_name, stream_index, owner_guid, owner_kind, owner_name, owner_synonym, module_payload_json, updated_at, last_seen_at ) VALUES ( :server_key, :database_name, :module_ref, :module_table, :file_name, :stream_index, :owner_guid, :owner_kind, :owner_name, :owner_synonym, :payload_json, :updated_at, :last_seen_at ) ON CONFLICT(server_key, database_name, module_ref) DO UPDATE SET module_table=excluded.module_table, file_name=excluded.file_name, stream_index=excluded.stream_index, owner_guid=excluded.owner_guid, owner_kind=excluded.owner_kind, owner_name=excluded.owner_name, owner_synonym=excluded.owner_synonym, module_payload_json=excluded.module_payload_json, updated_at=excluded.updated_at, last_seen_at=excluded.last_seen_at """, { "server_key": cache_server_key(config), "database_name": cache_database_name(config), "module_ref": normalized_module_ref, "module_table": module_table, "file_name": module_file_name, "stream_index": module_stream_index, "owner_guid": owner_guid or None, "owner_kind": owner.get("kind"), "owner_name": owner.get("name"), "owner_synonym": owner.get("synonym"), "payload_json": payload_json, "updated_at": now, "last_seen_at": now, }, ) def metadata_module_owner_cache_lookup(config: dict[str, str], module_ref: str) -> dict[str, Any] | None: normalized_module_ref = str(module_ref or "").strip() if not config or not normalized_module_ref: return None row = None with cache_connection() as conn: row = conn.execute( """ SELECT owner_guid, owner_kind, owner_name, owner_synonym, module_payload_json FROM metadata_module_owner_cache WHERE server_key = ? AND database_name = ? AND module_ref = ? LIMIT 1 """, (cache_server_key(config), cache_database_name(config), normalized_module_ref), ).fetchone() if not row: return None payload = {} if row["module_payload_json"]: try: loaded = json.loads(row["module_payload_json"]) if isinstance(loaded, dict): payload = loaded except Exception: payload = {} return { "owner_guid": row["owner_guid"] or None, "owner_kind": row["owner_kind"] or None, "owner_name": row["owner_name"] or None, "owner_synonym": row["owner_synonym"] or None, "module_payload": payload, "owner": {"kind": row["owner_kind"] or None, "name": row["owner_name"] or None, "synonym": row["owner_synonym"] or None, "guid": row["owner_guid"] or None}, } def form_owner_module_refs(table: str, file_name: str) -> list[str]: if table not in STORAGE_TABLES or not file_name or Path(file_name).name != file_name: return [] base_ref = f"{table}:{file_name}" return [base_ref, f"{base_ref}#form_module"] def normalize_module_ref_for_form_owner(module_ref: str) -> str: table, file_name, _stream_index = parse_module_id(str(module_ref or "").strip()) if not table or not file_name: return str(module_ref or "").strip() return f"{table}:{file_name}" def metadata_form_owner_cache_upsert( config: dict[str, str] | None, *, base_id: str, owner_kind: str | None, form_name: str | None, table: str, file_name: str, extension: dict[str, Any] | None = None, owner_name: str | None = None, owner_guid: str | None = None, form_guid: str | None = None, bsl_offset: int | None = None, payload: dict[str, Any] | None = None, ) -> dict[str, Any] | None: if not config or table not in STORAGE_TABLES or not file_name or Path(file_name).name != file_name: return None kind = canonical_kind(str(owner_kind or "")) or str(owner_kind or "") or None name = str(form_name or owner_name or "").strip() if not name and not form_guid: return None extension = extension if isinstance(extension, dict) else {} normalized_extension_guid = str(extension.get("guid") or "").strip().lower() or None extension_name = str(extension.get("name") or "").strip() or None normalized_form_guid = str(form_guid or "").strip().lower() or None normalized_owner_guid = str(owner_guid or normalized_form_guid or "").strip().lower() or None primary_module_ref = f"{table}:{file_name}" form_key_parts = [normalized_extension_guid or normalize(extension_name or ""), kind or "", normalize(name) or normalized_form_guid or primary_module_ref] form_key = "|".join(form_key_parts) now = time.time() cache_payload = { "base_id": base_id, "extension": extension or None, "owner": {"kind": kind, "name": owner_name or name or None, "guid": normalized_owner_guid}, "form": {"name": name or None, "guid": normalized_form_guid, "kind": kind}, "source": {"table": table, "file_name": file_name}, "module_ref": primary_module_ref, "bsl_offset": bsl_offset, **(payload or {}), } with cache_connection() as conn: conn.execute( """ INSERT INTO metadata_form_owner_cache ( server_key, database_name, form_key, extension_guid, extension_name, owner_kind, owner_name, owner_guid, form_name, form_guid, table_name, file_name, module_ref, bsl_offset, payload_json, updated_at, last_seen_at ) VALUES ( :server_key, :database_name, :form_key, :extension_guid, :extension_name, :owner_kind, :owner_name, :owner_guid, :form_name, :form_guid, :table_name, :file_name, :module_ref, :bsl_offset, :payload_json, :updated_at, :last_seen_at ) ON CONFLICT(server_key, database_name, form_key) DO UPDATE SET extension_guid=excluded.extension_guid, extension_name=excluded.extension_name, owner_kind=excluded.owner_kind, owner_name=excluded.owner_name, owner_guid=excluded.owner_guid, form_name=excluded.form_name, form_guid=excluded.form_guid, table_name=excluded.table_name, file_name=excluded.file_name, module_ref=excluded.module_ref, bsl_offset=excluded.bsl_offset, payload_json=excluded.payload_json, updated_at=excluded.updated_at, last_seen_at=excluded.last_seen_at """, { "server_key": cache_server_key(config), "database_name": cache_database_name(config), "form_key": form_key, "extension_guid": normalized_extension_guid, "extension_name": extension_name, "owner_kind": kind, "owner_name": owner_name or name or None, "owner_guid": normalized_owner_guid, "form_name": name or None, "form_guid": normalized_form_guid, "table_name": table, "file_name": file_name, "module_ref": primary_module_ref, "bsl_offset": bsl_offset, "payload_json": json.dumps(cache_payload, ensure_ascii=False, sort_keys=True), "updated_at": now, "last_seen_at": now, }, ) return cache_payload def metadata_form_owner_cache_row_payload(row: dict[str, Any]) -> dict[str, Any]: payload: dict[str, Any] = {} try: loaded = json.loads(row.get("payload_json") or "{}") if isinstance(loaded, dict): payload = loaded except Exception: payload = {} table = row.get("table_name") file_name = row.get("file_name") module_ref = row.get("module_ref") or (f"{table}:{file_name}" if table and file_name else None) return { **payload, "extension": payload.get("extension") or {"guid": row.get("extension_guid"), "name": row.get("extension_name")}, "owner": payload.get("owner") or {"kind": row.get("owner_kind"), "name": row.get("owner_name"), "guid": row.get("owner_guid")}, "form": payload.get("form") or {"name": row.get("form_name"), "guid": row.get("form_guid"), "kind": row.get("owner_kind")}, "source": {"table": table, "file_name": file_name}, "module_ref": module_ref, "bsl_offset": row.get("bsl_offset"), } def metadata_form_owner_cache_lookup( config: dict[str, str] | None, *, owner_kind: str | None = None, form_name: str | None = None, extension: str | None = None, table: str | None = None, file_name: str | None = None, form_guid: str | None = None, module_ref: str | None = None, ) -> dict[str, Any] | None: if not config: return None clauses = ["server_key=?", "database_name=?"] params: list[Any] = [cache_server_key(config), cache_database_name(config)] if form_guid: clauses.append("form_guid=?") params.append(str(form_guid).strip().lower()) elif module_ref: normalized_ref = normalize_module_ref_for_form_owner(module_ref) clauses.append("module_ref=?") params.append(normalized_ref) elif table and file_name: clauses.extend(["table_name=?", "file_name=?"]) params.extend([table, file_name]) else: kind = canonical_kind(str(owner_kind or "")) or str(owner_kind or "") or None if kind: clauses.append("owner_kind=?") params.append(kind) normalized_form_name = normalize(form_name or "") if normalized_form_name: clauses.append("(form_name=? OR replace(form_name, ' ', '')=? OR lower(replace(form_name, ' ', ''))=?)") params.extend([str(form_name or ""), str(form_name or "").replace(" ", ""), normalized_form_name]) if extension: normalized_extension = normalize(extension) clauses.append("(extension_guid=? OR lower(extension_name)=? OR replace(lower(extension_name), ' ', '')=?)") params.extend([str(extension).lower(), str(extension).lower(), normalized_extension]) with cache_connection() as conn: try: row = conn.execute( f""" SELECT * FROM metadata_form_owner_cache WHERE {' AND '.join(clauses)} ORDER BY updated_at DESC LIMIT 1 """, params, ).fetchone() except sqlite3.OperationalError: return None if not row: return None return metadata_form_owner_cache_row_payload(dict(row)) def metadata_module_owner_cache_prune_for_owner(config: dict[str, str], owner_guid: str, module_refs: list[str]) -> None: normalized_owner_guid = str(owner_guid or "").lower() if not is_guid_text(normalized_owner_guid): return normalized_refs = sorted({str(module_ref or "").strip() for module_ref in module_refs if str(module_ref or "").strip()}) with cache_connection() as conn: if normalized_refs: placeholders = ",".join(["?"] * len(normalized_refs)) conn.execute( f""" DELETE FROM metadata_module_owner_cache WHERE server_key=? AND database_name=? AND owner_guid=? AND module_ref NOT IN ({placeholders}) """, ( cache_server_key(config), cache_database_name(config), normalized_owner_guid, *normalized_refs, ), ) else: conn.execute( """ DELETE FROM metadata_module_owner_cache WHERE server_key=:server_key AND database_name=:database_name AND owner_guid=:owner_guid """, { "server_key": cache_server_key(config), "database_name": cache_database_name(config), "owner_guid": normalized_owner_guid, }, ) def metadata_type_cache_lookup_many(config: dict[str, str], type_guids: set[str]) -> dict[str, dict[str, Any]]: wanted = sorted({str(guid or "").lower() for guid in type_guids if is_guid_text(str(guid or ""))}) if not wanted: return {} result: dict[str, dict[str, Any]] = {} with cache_connection() as conn: for start in range(0, len(wanted), 500): chunk = wanted[start : start + 500] placeholders = ",".join(["?"] * len(chunk)) rows = conn.execute( f""" SELECT type_guid, payload_json FROM metadata_type_cache WHERE server_key = ? AND database_name = ? AND type_guid IN ({placeholders}) """, (cache_server_key(config), cache_database_name(config), *chunk), ).fetchall() for row in rows: try: payload = json.loads(row["payload_json"]) except Exception: continue if isinstance(payload, dict): result[str(row["type_guid"]).lower()] = payload return result def metadata_type_cache_upsert(config: dict[str, str], type_guid: str, resolved: dict[str, Any]) -> None: guid = str(type_guid or "").lower() if not is_guid_text(guid) or not isinstance(resolved, dict): return value_type = resolved.get("value_type") if isinstance(resolved.get("value_type"), dict) else {} presentation = resolved_type_presentation(resolved) if resolved.get("status") == "ok" else str(resolved.get("presentation") or "") metadata_guid_index_upsert( config, { "guid": guid, "guid_role": resolved.get("guid_role") or "metadata_type", "kind": resolved.get("kind"), "kind_ru": resolved.get("kind_ru"), "name": resolved.get("name"), "presentation": presentation, "owner_guid": resolved.get("owner_guid"), "type_guid": guid, "value_guid": resolved.get("value_guid"), "value_type_guid": value_type.get("type_guid"), "value_presentation": value_type.get("presentation"), "payload": resolved, "source_file": resolved.get("owner_guid"), }, ) now = time.time() payload_json = json.dumps(resolved, ensure_ascii=False, sort_keys=True) with cache_connection() as conn: conn.execute( """ INSERT INTO metadata_type_cache ( server_key, database_name, type_guid, status, presentation, kind, kind_ru, name, owner_guid, generated_category, payload_json, updated_at, last_seen_at ) VALUES ( :server_key, :database_name, :type_guid, :status, :presentation, :kind, :kind_ru, :name, :owner_guid, :generated_category, :payload_json, :updated_at, :last_seen_at ) ON CONFLICT(server_key, database_name, type_guid) DO UPDATE SET status=excluded.status, presentation=excluded.presentation, kind=excluded.kind, kind_ru=excluded.kind_ru, name=excluded.name, owner_guid=excluded.owner_guid, generated_category=excluded.generated_category, payload_json=excluded.payload_json, updated_at=excluded.updated_at, last_seen_at=excluded.last_seen_at """, { "server_key": cache_server_key(config), "database_name": cache_database_name(config), "type_guid": guid, "status": str(resolved.get("status") or ""), "presentation": resolved_type_presentation(resolved) if resolved.get("status") == "ok" else str(resolved.get("presentation") or ""), "kind": resolved.get("kind"), "kind_ru": resolved.get("kind_ru"), "name": resolved.get("name"), "owner_guid": resolved.get("owner_guid"), "generated_category": resolved.get("generated_category"), "payload_json": payload_json, "updated_at": now, "last_seen_at": now, }, ) def metadata_guid_index_migrate_legacy_types(config: dict[str, str]) -> int: migrated = 0 with cache_connection() as conn: rows = conn.execute( """ SELECT type_guid, payload_json FROM metadata_type_cache WHERE server_key = ? AND database_name = ? """, (cache_server_key(config), cache_database_name(config)), ).fetchall() for row in rows: try: payload = json.loads(row["payload_json"] or "{}") except Exception: continue if not isinstance(payload, dict): continue metadata_type_cache_upsert(config, str(row["type_guid"] or ""), payload) migrated += 1 return migrated def metadata_cache_status(payload: dict[str, Any]) -> dict[str, Any]: base_id_or_error = require_base_id(payload, "metadata.cache.status") if isinstance(base_id_or_error, dict): return base_id_or_error _, include_samples_error = strict_bool_argument(payload, "include_samples", method="metadata.cache.status", default=False) if include_samples_error: return include_samples_error base_ids = [base_id_or_error] with cache_connection() as conn: rows = [] for base_id in base_ids: config, config_error = sql_config_for_base(base_id) if not config: rows.append({"base_id": base_id, "status": "source_missing", "diagnostics": config_error}) continue count = conn.execute( "SELECT COUNT(*) AS count FROM metadata_identity_cache WHERE server_key=? AND database_name=?", (cache_server_key(config), cache_database_name(config)), ).fetchone()["count"] type_count = conn.execute( "SELECT COUNT(*) AS count FROM metadata_type_cache WHERE server_key=? AND database_name=?", (cache_server_key(config), cache_database_name(config)), ).fetchone()["count"] guid_count = conn.execute( "SELECT COUNT(*) AS count FROM metadata_guid_index WHERE server_key=? AND database_name=?", (cache_server_key(config), cache_database_name(config)), ).fetchone()["count"] owner_map_count = conn.execute( "SELECT COUNT(*) AS count FROM metadata_module_owner_cache WHERE server_key=? AND database_name=?", (cache_server_key(config), cache_database_name(config)), ).fetchone()["count"] extension_route_count = conn.execute( "SELECT COUNT(*) AS count FROM extension_route_cache WHERE server_key=? AND database_name=?", (cache_server_key(config), cache_database_name(config)), ).fetchone()["count"] extension_route_stale_count = conn.execute( "SELECT COUNT(*) AS count FROM extension_route_cache WHERE server_key=? AND database_name=? AND freshness_status='stale'", (cache_server_key(config), cache_database_name(config)), ).fetchone()["count"] semantic_document_count = conn.execute( "SELECT COUNT(*) AS count FROM semantic_document_cache WHERE server_key=? AND database_name=?", (cache_server_key(config), cache_database_name(config)), ).fetchone()["count"] decoded_artifact_count = conn.execute( "SELECT COUNT(*) AS count FROM decoded_artifact_cache WHERE server_key=? AND database_name=?", (cache_server_key(config), cache_database_name(config)), ).fetchone()["count"] rows.append( { "base_id": base_id, "server": config["server"], "database": config["database"], "cached_objects": int(count), "cached_types": int(type_count), "guid_index_entries": int(guid_count), "module_owner_cache_entries": int(owner_map_count), "extension_route_cache_entries": int(extension_route_count), "extension_route_cache_stale_entries": int(extension_route_stale_count), "semantic_document_cache_entries": int(semantic_document_count), "decoded_artifact_cache_entries": int(decoded_artifact_count), } ) return {"schema": "onec_metadata_cache_status.v1", "status": "ok", "caches": rows, "counts": {"bases": len(rows)}} def metadata_cache_lookup(payload: dict[str, Any]) -> dict[str, Any]: method = "metadata.cache.lookup" payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error for argument in ("guid", "kind", "name"): if argument not in payload: continue value = payload.get(argument) if value is None or value == "": return invalid_argument("metadata.cache.lookup", argument, f"{argument} must be a non-empty JSON string when provided.") if not isinstance(value, str): return invalid_argument("metadata.cache.lookup", argument, f"{argument} must be a JSON string.") guid = str(payload.get("guid") or "").strip() if guid: obj = metadata_cache_lookup_guid(base_id_or_error, guid) else: row = metadata_cache_lookup_row(base_id_or_error, str(payload.get("kind") or ""), str(payload.get("name") or "")) obj = metadata_cache_public_row(row) if row else None return { "schema": "onec_metadata_cache_lookup.v1", "status": "ok" if obj else "not_found", **({"error": "not_found"} if not obj else {}), "base_id": base_id_or_error, "object": obj, } def metadata_cache_invalidate(payload: dict[str, Any]) -> dict[str, Any]: base_id_or_error = require_base_id(payload, "metadata.cache.invalidate") if isinstance(base_id_or_error, dict): return base_id_or_error dry_run, dry_run_error = strict_bool_argument(payload, "dry_run", method="metadata.cache.invalidate", default=False) if dry_run_error: return dry_run_error base_ids = [base_id_or_error] deleted = 0 with cache_connection() as conn: for base_id in base_ids: config, _ = sql_config_for_base(base_id) if not config: continue cache_tables = ( "metadata_identity_cache", "metadata_type_cache", "metadata_guid_index", "metadata_module_owner_cache", "metadata_form_owner_cache", "extension_route_cache", "semantic_document_cache", "decoded_artifact_cache", "metadata_code_index_cache", "metadata_code_vector_cache", ) if dry_run: for table_name in cache_tables: deleted += int( conn.execute( f"SELECT COUNT(*) AS count FROM {table_name} WHERE server_key=? AND database_name=?", (cache_server_key(config), cache_database_name(config)), ).fetchone()["count"] ) continue for table_name in cache_tables: cursor = conn.execute( f"DELETE FROM {table_name} WHERE server_key=? AND database_name=?", (cache_server_key(config), cache_database_name(config)), ) deleted += int(cursor.rowcount or 0) return {"schema": "onec_metadata_cache_invalidate.v1", "status": "ok", "dry_run": bool(dry_run), "counts": {"bases": len(base_ids), "deleted": deleted}} def invalidate_adapter_caches_after_saved_state_change(base_id: str, *, reason: str) -> dict[str, Any]: """Invalidate persistent and process-local views after a committed saved-state mutation.""" normalized_base_id = str(base_id or "").strip() persistent: dict[str, Any] try: persistent = metadata_cache_invalidate({"base_id": normalized_base_id, "dry_run": False}) except Exception as exc: persistent = {"status": "error", "diagnostics": {"message": str(exc)}} runtime_counts = { "base_root_metadata": 0, "data_schema": 0, "extension_manifests": 0, } with BASE_ROOT_METADATA_CACHE_LOCK: root_keys = [ key for key in BASE_ROOT_METADATA_CACHE if isinstance(key, tuple) and key and str(key[0]).strip() == normalized_base_id ] for key in root_keys: BASE_ROOT_METADATA_CACHE.pop(key, None) runtime_counts["base_root_metadata"] = len(root_keys) normalized_casefold = normalized_base_id.casefold() with DATA_SCHEMA_CACHE_LOCK: schema_keys = [] for key in DATA_SCHEMA_CACHE: try: cached_selector = json.loads(key) except (TypeError, ValueError): continue if str(cached_selector.get("base_id") or "").casefold() == normalized_casefold: schema_keys.append(key) for key in schema_keys: DATA_SCHEMA_CACHE.pop(key, None) runtime_counts["data_schema"] = len(schema_keys) manifest_cache = globals().get("EXTENSION_MANIFEST_CACHE") manifest_lock = globals().get("EXTENSION_MANIFEST_CACHE_LOCK") if isinstance(manifest_cache, dict): def clear_manifest_entries() -> int: manifest_keys = [ key for key in manifest_cache if isinstance(key, tuple) and key and str(key[0]).strip() == normalized_base_id ] for key in manifest_keys: manifest_cache.pop(key, None) return len(manifest_keys) if manifest_lock is not None: with manifest_lock: runtime_counts["extension_manifests"] = clear_manifest_entries() else: runtime_counts["extension_manifests"] = clear_manifest_entries() persistent_status = str(persistent.get("status") or "error") return { "schema": "onec_saved_state_cache_invalidation.v1", "status": "ok" if persistent_status == "ok" else "partial", "base_id": normalized_base_id, "reason": reason, "persistent": { "status": persistent_status, "deleted": int(((persistent.get("counts") or {}).get("deleted") or 0)), }, "runtime": runtime_counts, **( {"diagnostics": persistent.get("diagnostics")} if persistent_status != "ok" and persistent.get("diagnostics") else {} ), } def metadata_module_owner_cache_prune(payload: dict[str, Any]) -> dict[str, Any]: method = "metadata.module_owner_cache.prune" payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload validation_error = validate_metadata_module_owner_cache_prune_payload(payload) if validation_error: return validation_error base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error timeout_seconds, _ = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1, maximum=300) include_storage, _ = strict_include_storage(payload, method) owner_guid = str(payload.get("owner_guid") or "").strip().lower() public_selector_used = not owner_guid and has_object_selector(payload) owner_resolution: dict[str, Any] | None = None if public_selector_used: resolved_owner_guid, owner_resolution = metadata_saved_state_modules_owner_guid_from_selector( base_id, payload, timeout_seconds=int(timeout_seconds or 30), ) owner_guid = str(resolved_owner_guid or "").strip().lower() if not is_guid_text(owner_guid): status = str((owner_resolution or {}).get("status") or "not_found") return { "schema": "onec_module_owner_cache_prune.v1", "method": method, "status": status if status in {"not_found", "ambiguous"} else "not_found", "error": "owner_not_resolved", "base_id": base_id, "selector": { **({"ref": payload.get("ref")} if payload.get("ref") else {}), **({"kind": payload.get("kind")} if payload.get("kind") else {}), **({"name": payload.get("name")} if payload.get("name") else {}), }, "owner_resolution": owner_resolution, "diagnostics": (owner_resolution or {}).get("diagnostics") or {"message": "The named 1C module owner was not resolved."}, } module_ref = str(payload.get("module_ref") or "").strip() module_refs_raw = payload.get("module_refs") module_refs = [] if isinstance(module_refs_raw, (list, tuple)): for index, item in enumerate(module_refs_raw): if not isinstance(item, str): return invalid_argument( "metadata.module_owner_cache.prune", "module_refs", f"module_refs[{index}] must be a JSON string.", ) item = item.strip() if item: module_refs.append(item) module_refs = sorted({ref for ref in module_refs if ref}) elif module_refs_raw is not None: return invalid_argument("metadata.module_owner_cache.prune", "module_refs", "module_refs must be a JSON array of strings.") dry_run, dry_run_error = strict_bool_argument(payload, "dry_run", method="metadata.module_owner_cache.prune", default=False) if dry_run_error: return dry_run_error config, config_error = sql_config_for_base(base_id) if not config: return { "schema": "onec_module_owner_cache_prune.v1", "status": "error", "base_id": base_id, "error": "source_missing", "diagnostics": config_error or {"message": "SQL source is not configured for this base_id."}, } where_parts = ["server_key = ?", "database_name = ?"] params: list[Any] = [cache_server_key(config), cache_database_name(config)] if owner_guid: where_parts.append("owner_guid = ?") params.append(owner_guid) if module_ref: where_parts.append("module_ref = ?") params.append(module_ref) if module_refs: placeholders = ",".join(["?"] * len(module_refs)) where_parts.append(f"module_ref IN ({placeholders})") params.extend(module_refs) where = " AND ".join(where_parts) query = f"SELECT COUNT(*) AS count FROM metadata_module_owner_cache WHERE {where}" public_selector = { **({"ref": payload.get("ref")} if payload.get("ref") else {}), **({"kind": payload.get("kind")} if payload.get("kind") else {}), **({"name": payload.get("name")} if payload.get("name") else {}), } resolved_owner = (owner_resolution or {}).get("object") if isinstance((owner_resolution or {}).get("object"), dict) else {} public_owner = { **({"kind": resolved_owner.get("kind")} if resolved_owner.get("kind") else {}), **({"name": resolved_owner.get("name")} if resolved_owner.get("name") else {}), **({"synonym": resolved_owner.get("synonym")} if resolved_owner.get("synonym") else {}), **( { "ref": resolved_owner.get("ref") or object_selector_ref(resolved_owner.get("kind"), resolved_owner.get("name")) } if resolved_owner.get("ref") or (resolved_owner.get("kind") and resolved_owner.get("name")) else {} ), } public_query = ( { "owner": public_owner or public_selector, **({"module_ref": module_ref} if module_ref else {}), **({"module_refs": module_refs} if module_refs else {}), **({"storage": {"owner_guid": owner_guid}} if include_storage else {}), } if public_selector_used else {"owner_guid": owner_guid or None, "module_ref": module_ref or None, "module_refs": module_refs or None} ) with cache_connection() as conn: matched = int(conn.execute(query, params).fetchone()["count"]) if bool(dry_run): return { "schema": "onec_module_owner_cache_prune.v1", "status": "ok", "base_id": base_id, "dry_run": True, "counts": {"matched": matched}, "query": public_query, **({"resolved_by": "public_object_selector"} if public_selector_used else {}), } cursor = conn.execute(f"DELETE FROM metadata_module_owner_cache WHERE {where}", params) deleted = int(cursor.rowcount or 0) return { "schema": "onec_module_owner_cache_prune.v1", "status": "ok", "base_id": base_id, "dry_run": False, "counts": {"matched": matched, "deleted": deleted}, "query": public_query, **({"resolved_by": "public_object_selector"} if public_selector_used else {}), } def metadata_cache_rebuild_base(base_id: str, *, table: str = "Config", timeout_seconds: int = 240, batch_size: int = 100) -> dict[str, Any]: storage_table = str(table or "Config") if storage_table not in STORAGE_TABLES: return { "base_id": base_id, "status": "error", "counts": {"candidates": 0, "updated": 0, "failed": 0, "type_updated": 0, "migrated_types": 0, "defined_type_files": 0}, "diagnostics": {"message": f"Unsupported storage table '{storage_table}'."}, } config, config_error = sql_config_for_base(base_id) if not config: return {"base_id": base_id, "status": "source_missing", "diagnostics": config_error} records, error = live_dbnames_records(base_id, timeout_seconds=timeout_seconds) if error: return {"base_id": base_id, "status": "error", "diagnostics": error.get("diagnostics") or error} migrated_types = metadata_guid_index_migrate_legacy_types(config) candidates: dict[str, dict[str, Any]] = {} for record in records or []: kind = DBNAMES_ROLE_KIND.get(getattr(record, "storage_role", "")) guid = str(getattr(record, "guid", "") or "").lower() if not kind or not guid: continue candidates.setdefault( guid, { "guid": guid, "kind": kind, "kind_ru": RU_KIND.get(kind, kind), "public_kind": PUBLIC_KIND.get(kind, "other"), "name": None, "synonym": None, "source": "base", }, ) # DBNames contains storage-backed objects, but several first-class metadata # kinds (for example SettingsStorage and Subsystem) are fully enumerated only # by the configuration root descriptor. Cache rebuilding must use the same # merged discovery set as metadata.objects.list; otherwise a valid but partial # cache shadows objects that are visible through live root discovery. dbnames_guids = set(candidates) root_priority_guids: set[str] = set() if storage_table in {"Config", "ConfigSave"}: root_rows, _root_diagnostics = live_base_root_metadata_index(base_id, table=storage_table, timeout_seconds=timeout_seconds) merge_root_metadata_candidates(candidates, root_rows, wanted_kind=None, requested_public=None) root_priority_guids = { str(row.get("guid") or "").lower() for row in root_rows if row.get("guid") and str(row.get("guid") or "").lower() not in dbnames_guids } # Root-only kinds must become usable even when a very large DBNames/type # refresh reaches its job deadline later in the rebuild. items = sorted( candidates.values(), key=lambda row: ( 0 if str(row.get("guid") or "").lower() in root_priority_guids else 1, str(row.get("kind") or ""), str(row.get("guid") or ""), ), ) updated = 0 type_updated = 0 failed = 0 started = time.time() for start in range(0, len(items), max(1, batch_size)): chunk = items[start : start + batch_size] payloads, _, read_error = read_storage_files_bytes(base_id, storage_table, [row["guid"] for row in chunk], timeout_seconds=timeout_seconds) if read_error: failed += len(chunk) continue for row in chunk: identity = config_identity_from_bytes(payloads[row["guid"]]) if payloads and row["guid"] in payloads else None if not identity: failed += 1 continue row["name"] = identity.get("name") synonyms = identity.get("synonyms") or {} row["synonym"] = next(iter(synonyms.values()), None) if isinstance(synonyms, dict) else None row["identity"] = identity metadata_cache_upsert(config, row) for generated in generated_type_records_from_bytes(payloads[row["guid"]], kind=str(row.get("kind") or ""), guid=str(row.get("guid") or "")): type_guid = str(generated.get("type_guid") or "").lower() if not type_guid: continue resolved = resolved_type_from_generated(base_id, type_guid, generated, timeout_seconds=timeout_seconds) metadata_type_cache_upsert(config, type_guid, resolved) type_updated += 1 updated += 1 last_file_name = "" seen_defined_files = 0 while True: page = live_config_file_name_page_after( base_id, last_file_name, page_size=max(1, min(batch_size * 10, 5000)), timeout_seconds=timeout_seconds, table=storage_table, ) if not page: break last_file_name = page[-1] payloads, _, read_error = read_storage_files_bytes(base_id, storage_table, page, timeout_seconds=timeout_seconds) if read_error: continue for guid, data in (payloads or {}).items(): generated_records = generated_type_records_from_bytes(data, kind="DefinedType", guid=guid) if not generated_records: continue seen_defined_files += 1 for generated in generated_records: type_guid = str(generated.get("type_guid") or "").lower() if not type_guid: continue resolved = resolved_type_from_generated(base_id, type_guid, generated, timeout_seconds=timeout_seconds) metadata_type_cache_upsert(config, type_guid, resolved) type_updated += 1 return { "base_id": base_id, "status": "ok", "server": config["server"], "database": config["database"], "counts": {"candidates": len(items), "updated": updated, "failed": failed, "type_updated": type_updated, "migrated_types": migrated_types, "defined_type_files": seen_defined_files}, "duration_ms": int((time.time() - started) * 1000), } def metadata_cache_rebuild(payload: dict[str, Any]) -> dict[str, Any]: validation_error = validate_metadata_cache_rebuild_payload(payload) if validation_error: return validation_error return adapter_start_job({"method": "metadata.cache.rebuild", "payload": payload}) def validate_metadata_cache_rebuild_payload(payload: dict[str, Any]) -> dict[str, Any] | None: if "_run_sync" in payload: return invalid_argument("metadata.cache.rebuild", "_run_sync", "_run_sync is an internal adapter flag and is not accepted by the public API.") base_id_or_error = require_base_id(payload, "metadata.cache.rebuild") if isinstance(base_id_or_error, dict): return base_id_or_error _, refresh_error = strict_bool_argument(payload, "refresh", method="metadata.cache.rebuild", default=True) if refresh_error: return refresh_error _, timeout_seconds_error = parse_int_argument(payload, "timeout_seconds", method="metadata.cache.rebuild", default=240, minimum=1) if timeout_seconds_error: return timeout_seconds_error _, batch_size_error = parse_int_argument(payload, "batch_size", method="metadata.cache.rebuild", default=100, minimum=1, maximum=5000) if batch_size_error: return batch_size_error table_error = metadata_storage_table(payload, "metadata.cache.rebuild") if isinstance(table_error, dict): return table_error return None def adapter_cache_rebuild_partial_result(payload: dict[str, Any]) -> dict[str, Any]: return { "schema": "onec_metadata_cache_rebuild.v1", "status": "partial", "base_id": payload.get("base_id"), "query": { "base_id": payload.get("base_id"), "batch_size": payload.get("batch_size") or 100, "timeout_seconds": payload.get("timeout_seconds") or 240, "table": payload.get("table") or "Config", }, "sections": { "dbnames": "pending", "objects": "pending", "defined_types": "pending", }, "results": [], "counts": { "bases": 1, "ok_bases": 0, "failed_bases": 0, "candidate_objects": 0, "updated": 0, "type_updated": 0, "failed_objects": 0, "failed": 0, "defined_type_files": 0, }, "failed_sections": [], "diagnostics": [], } def adapter_update_cache_rebuild_job( job_id: str, partial: dict[str, Any], current_step: str, completed: int, total: int | None, *, started_at: float, running_steps: list[str] | None = None, queued_steps: list[str] | None = None, done_steps: list[str] | None = None, failed_steps: list[str] | None = None, ) -> None: now = adapter_now() elapsed = round(max(0.0, now - started_at), 3) partial["elapsed_seconds"] = elapsed progress: dict[str, Any] = { "current_step": current_step, "completed_steps": completed, "total_steps": total, "percent": int((completed / total) * 100) if total else 0, "elapsed_seconds": elapsed, } if running_steps is not None: progress["running_steps"] = running_steps if queued_steps is not None: progress["queued_steps"] = queued_steps if done_steps is not None: progress["done_steps"] = done_steps if failed_steps is not None: progress["failed_steps"] = failed_steps adapter_job_set(job_id, partial_result=partial, current_step=current_step, progress=progress) def adapter_run_metadata_cache_rebuild_job(job_id: str, payload: dict[str, Any], timeout_seconds: float) -> None: partial = adapter_cache_rebuild_partial_result(payload) started_at = adapter_now() base_id = str(payload.get("base_id") or "") table_or_error = metadata_storage_table(payload, "metadata.cache.rebuild") if isinstance(table_or_error, dict): adapter_job_finish(job_id, "error", result=adapter_public_error("metadata.cache.rebuild", "invalid_argument", table_or_error)) return table = table_or_error partial["query"]["table"] = table batch_size = int(payload.get("batch_size") or 100) timeout_value = adapter_timeout_payload_value(float(payload.get("timeout_seconds") or timeout_seconds or 240)) queued_steps = ["dbnames", "objects", "defined_types"] done_steps: list[str] = [] failed_steps: list[str] = [] adapter_update_cache_rebuild_job(job_id, partial, "starting", 0, None, started_at=started_at, running_steps=[], queued_steps=queued_steps, done_steps=done_steps, failed_steps=failed_steps) try: config, config_error = sql_config_for_base(base_id) if not config: partial["status"] = "error" partial["failed_sections"].append({"section": "dbnames", "status": "source_missing", "diagnostics": config_error}) partial["counts"]["failed_bases"] = 1 partial["counts"]["failed"] = 1 adapter_job_finish(job_id, "done", result=partial, progress={"current_step": "done", "completed_steps": 1, "total_steps": 1, "percent": 100}) return queued_steps.remove("dbnames") partial["sections"]["dbnames"] = "running" adapter_update_cache_rebuild_job(job_id, partial, "dbnames", 0, None, started_at=started_at, running_steps=["dbnames"], queued_steps=queued_steps, done_steps=done_steps, failed_steps=failed_steps) records, error = live_dbnames_records(base_id, timeout_seconds=timeout_value) if error: partial["status"] = "error" partial["sections"]["dbnames"] = "failed" partial["failed_sections"].append({"section": "dbnames", "status": "error", "diagnostics": error.get("diagnostics") or error}) partial["counts"]["failed_bases"] = 1 partial["counts"]["failed"] = 1 adapter_job_finish(job_id, "done", result=partial, progress={"current_step": "done", "completed_steps": 1, "total_steps": 1, "percent": 100}) return migrated_types = metadata_guid_index_migrate_legacy_types(config) partial["sections"]["dbnames"] = "ok" done_steps.append("dbnames") candidates: dict[str, dict[str, Any]] = {} for record in records or []: kind = DBNAMES_ROLE_KIND.get(getattr(record, "storage_role", "")) guid = str(getattr(record, "guid", "") or "").lower() if not kind or not guid: continue candidates.setdefault( guid, { "guid": guid, "kind": kind, "kind_ru": RU_KIND.get(kind, kind), "public_kind": PUBLIC_KIND.get(kind, "other"), "name": None, "synonym": None, "source": "base", }, ) items = list(candidates.values()) partial["counts"]["candidate_objects"] = len(items) object_chunks = max(1, math.ceil(len(items) / max(1, batch_size))) total_steps = 2 + object_chunks queued_steps.remove("objects") partial["sections"]["objects"] = "running" completed = 1 updated = 0 failed = 0 type_updated = 0 adapter_update_cache_rebuild_job(job_id, partial, "objects", completed, total_steps, started_at=started_at, running_steps=["objects"], queued_steps=queued_steps, done_steps=done_steps, failed_steps=failed_steps) for start in range(0, len(items), max(1, batch_size)): if adapter_job_cancel_requested(job_id): partial["status"] = "cancelled" adapter_job_finish(job_id, "cancelled", partial_result=partial, result=partial) return chunk = items[start : start + batch_size] payloads, _, read_error = read_storage_files_bytes(base_id, table, [row["guid"] for row in chunk], timeout_seconds=timeout_value) if read_error: failed += len(chunk) else: for row in chunk: data = payloads[row["guid"]] if payloads and row["guid"] in payloads else None identity = config_identity_from_bytes(data) if data else None if not identity: failed += 1 continue row["name"] = identity.get("name") synonyms = identity.get("synonyms") or {} row["synonym"] = next(iter(synonyms.values()), None) if isinstance(synonyms, dict) else None row["identity"] = identity metadata_cache_upsert(config, row) for generated in generated_type_records_from_bytes(data, kind=str(row.get("kind") or ""), guid=str(row.get("guid") or "")): type_guid = str(generated.get("type_guid") or "").lower() if not type_guid: continue resolved = resolved_type_from_generated(base_id, type_guid, generated, timeout_seconds=timeout_value) metadata_type_cache_upsert(config, type_guid, resolved) type_updated += 1 updated += 1 completed += 1 partial["counts"].update({"updated": updated, "failed_objects": failed, "type_updated": type_updated}) adapter_update_cache_rebuild_job(job_id, partial, f"objects:{min(start + len(chunk), len(items))}/{len(items)}", completed, total_steps, started_at=started_at, running_steps=["objects"], queued_steps=queued_steps, done_steps=done_steps, failed_steps=failed_steps) partial["sections"]["objects"] = "ok" done_steps.append("objects") queued_steps.remove("defined_types") partial["sections"]["defined_types"] = "running" adapter_update_cache_rebuild_job(job_id, partial, "defined_types", completed, total_steps, started_at=started_at, running_steps=["defined_types"], queued_steps=queued_steps, done_steps=done_steps, failed_steps=failed_steps) last_file_name = "" seen_defined_files = 0 while True: if adapter_job_cancel_requested(job_id): partial["status"] = "cancelled" adapter_job_finish(job_id, "cancelled", partial_result=partial, result=partial) return page = live_config_file_name_page_after( base_id, last_file_name, page_size=max(1, min(batch_size * 10, 5000)), timeout_seconds=timeout_value, table=table, ) if not page: break last_file_name = page[-1] payloads, _, read_error = read_storage_files_bytes(base_id, table, page, timeout_seconds=timeout_value) if read_error: continue for guid, data in (payloads or {}).items(): generated_records = generated_type_records_from_bytes(data, kind="DefinedType", guid=guid) if not generated_records: continue seen_defined_files += 1 for generated in generated_records: type_guid = str(generated.get("type_guid") or "").lower() if not type_guid: continue resolved = resolved_type_from_generated(base_id, type_guid, generated, timeout_seconds=timeout_value) metadata_type_cache_upsert(config, type_guid, resolved) type_updated += 1 partial["counts"].update({"type_updated": type_updated, "defined_type_files": seen_defined_files}) adapter_update_cache_rebuild_job(job_id, partial, f"defined_types:{last_file_name}", completed, total_steps, started_at=started_at, running_steps=["defined_types"], queued_steps=queued_steps, done_steps=done_steps, failed_steps=failed_steps) partial["sections"]["defined_types"] = "ok" done_steps.append("defined_types") completed = total_steps partial["status"] = "ok" if failed == 0 else "partial" partial["counts"].update( { "ok_bases": 1, "failed_bases": 0, "candidate_objects": len(items), "updated": updated, "type_updated": type_updated, "failed_objects": failed, "failed": failed, "defined_type_files": seen_defined_files, } ) partial["results"] = [ { "base_id": base_id, "status": partial["status"], "server": config["server"], "database": config["database"], "counts": dict(partial["counts"]), "duration_ms": int((adapter_now() - started_at) * 1000), } ] adapter_update_cache_rebuild_job(job_id, partial, "done", completed, total_steps, started_at=started_at, running_steps=[], queued_steps=[], done_steps=done_steps, failed_steps=failed_steps) adapter_job_finish(job_id, "done", result=partial, partial_result=partial, progress={"current_step": "done", "completed_steps": completed, "total_steps": total_steps, "percent": 100, "running_steps": [], "queued_steps": [], "done_steps": done_steps, "failed_steps": failed_steps, "elapsed_seconds": partial.get("elapsed_seconds")}) except Exception as exc: adapter_job_finish(job_id, "error", **adapter_public_error("metadata.cache.rebuild", "adapter_job_exception", {"message": str(exc), "traceback": traceback.format_exc(limit=8)})) def import_pymssql(): try: import pymssql # type: ignore except Exception as exc: return None, { "schema": "onec_adapter_error.v1", "status": "error", "source": {"kind": "live_sql", "status": "driver_unavailable"}, "diagnostics": {"message": str(exc)}, } return pymssql, None def connect_live_sql(base_id: str, method: str, *, timeout_seconds: int = 30): config, config_error = sql_config_for_base(base_id) if not config: return None, None, live_source_unavailable(method, base_id, config_error) pymssql, import_error = import_pymssql() if not pymssql: error = dict(import_error or {}) error.update({"method": method, "base_id": base_id}) return None, None, error try: conn = pymssql.connect( server=config["server"], user=config["user"], password=config["password"], database=config["database"], login_timeout=min(timeout_seconds, 15), timeout=timeout_seconds, ) except Exception as exc: return None, config, { "schema": "onec_adapter_source_error.v1", "method": method, "status": "error", "base_id": base_id, "source": {"kind": "live_sql", "database": config["database"]}, "diagnostics": {"message": str(exc)}, } return conn, config, None def require_base_id(payload: dict[str, Any], method: str) -> str | dict[str, Any]: if not payload.get("base_id"): return base_id_required(method) if not isinstance(payload.get("base_id"), str): return invalid_argument(method, "base_id", "base_id must be a JSON string.") return str(payload.get("base_id")) def schema_tables_list(payload: dict[str, Any]) -> dict[str, Any]: base_id_or_error = require_base_id(payload, "schema.tables.list") if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error include_columns, include_columns_error = strict_bool_argument(payload, "include_columns", method="schema.tables.list", default=False) if include_columns_error: return include_columns_error limit, limit_error = parse_int_argument(payload, "limit", method="schema.tables.list", default=500, minimum=1, maximum=5000) if limit_error: return limit_error like_error = validate_optional_non_empty_string_arguments(payload, "schema.tables.list", ["like"]) if like_error: return like_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method="schema.tables.list", default=30, minimum=1) if timeout_error: return timeout_error diagnostic_error = require_diagnostic_mode(payload, "schema.tables.list") if diagnostic_error: return diagnostic_error like = str(payload.get("like") or "%") conn, config, error = connect_live_sql(base_id, "schema.tables.list", timeout_seconds=int(timeout_seconds or 30)) if error: return error rows = [] started = time.time() try: with conn: with conn.cursor(as_dict=True) as cursor: cursor.execute( """ SELECT TOP (%d) TABLE_SCHEMA AS [schema_name], TABLE_NAME AS [table_name], TABLE_TYPE AS [table_type] FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE %%s ORDER BY TABLE_SCHEMA, TABLE_NAME """ % limit, (like,), ) rows = [{key: jsonable(value) for key, value in row.items()} for row in cursor.fetchall()] except Exception as exc: return { "schema": "onec_schema_tables.v1", "status": "error", "base_id": base_id, "source": {"kind": "live_sql", "database": (config or {}).get("database")}, "diagnostics": {"message": str(exc)}, "tables": [], "counts": {"tables": 0}, } return { "schema": "onec_schema_tables.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "database": config["database"]}, "query": {"like": like, "limit": limit, "include_columns": bool(include_columns)}, "tables": rows, "counts": {"tables": len(rows)}, "duration_ms": int((time.time() - started) * 1000), } def storage_table(payload: dict[str, Any], method: str) -> str | dict[str, Any]: if "table" in payload and payload.get("table") is not None and not isinstance(payload.get("table"), str): return invalid_argument(method, "table", "table must be a JSON string.") table = str(payload.get("table") or "") if table not in STORAGE_TABLES: return { "schema": "onec_adapter_request_error.v1", "method": method, "status": "invalid_argument", "error": "invalid_argument", "argument": "table", "supported_tables": sorted(STORAGE_TABLES), } return table def metadata_storage_table(payload: dict[str, Any], method: str, default_table: str = "Config") -> str | dict[str, Any]: normalized_payload = dict(payload) if normalized_payload.get("table") is None: normalized_payload["table"] = default_table return storage_table(normalized_payload, method) def storage_files_list(payload: dict[str, Any]) -> dict[str, Any]: base_id_or_error = require_base_id(payload, "storage.files.list") if isinstance(base_id_or_error, dict): return base_id_or_error table_or_error = storage_table(payload, "storage.files.list") if isinstance(table_or_error, dict): return table_or_error base_id = base_id_or_error table = table_or_error limit, limit_error = parse_int_argument(payload, "limit", method="storage.files.list", default=200, minimum=1, maximum=5000) if limit_error: return limit_error prefix_error = validate_optional_non_empty_string_arguments(payload, "storage.files.list", ["prefix"]) if prefix_error: return prefix_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method="storage.files.list", default=30, minimum=1) if timeout_error: return timeout_error diagnostic_error = require_diagnostic_mode(payload, "storage.files.list") if diagnostic_error: return diagnostic_error prefix = str(payload.get("prefix") or "") like = f"{prefix}%" if prefix else "%" conn, config, error = connect_live_sql(base_id, "storage.files.list", timeout_seconds=int(timeout_seconds or 30)) if error: return error rows = [] started = time.time() try: with conn: with conn.cursor(as_dict=True) as cursor: cursor.execute( f""" SELECT TOP ({limit}) FileName, COUNT(*) AS PartCount, SUM(DATALENGTH(BinaryData)) AS Bytes FROM dbo.[{table}] WHERE FileName LIKE %s GROUP BY FileName ORDER BY FileName """, (like,), ) rows = [{key: jsonable(value) for key, value in row.items()} for row in cursor.fetchall()] except Exception as exc: return { "schema": "onec_storage_files.v1", "status": "error", "base_id": base_id, "source": {"kind": "live_sql", "database": (config or {}).get("database"), "table": table}, "diagnostics": {"message": str(exc)}, "files": [], "counts": {"files": 0}, } return { "schema": "onec_storage_files.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "database": config["database"], "table": table}, "query": {"prefix": prefix, "limit": limit}, "files": rows, "counts": {"files": len(rows)}, "duration_ms": int((time.time() - started) * 1000), } def read_storage_file_bytes(base_id: str, table: str, file_name: str, *, timeout_seconds: int = 30) -> tuple[bytes | None, dict[str, str] | None, dict[str, Any] | None]: conn, config, error = connect_live_sql(base_id, "storage.file.get", timeout_seconds=timeout_seconds) if error: return None, config, error parts: list[bytes] = [] try: with conn: with conn.cursor(as_dict=True) as cursor: cursor.execute(f"SELECT BinaryData FROM dbo.[{table}] WHERE FileName = %s ORDER BY PartNo", (file_name,)) for row in cursor.fetchall(): value = row.get("BinaryData") if isinstance(value, (bytes, bytearray)): parts.append(bytes(value)) except Exception as exc: return None, config, { "schema": "onec_adapter_source_error.v1", "method": "storage.file.get", "status": "error", "base_id": base_id, "source": {"kind": "live_sql", "database": (config or {}).get("database"), "table": table}, "diagnostics": {"message": str(exc)}, } if not parts: return None, config, { "schema": "onec_adapter_source_missing.v1", "method": "storage.file.get", "status": "source_missing", "base_id": base_id, "source": {"kind": "live_sql", "database": (config or {}).get("database"), "table": table, "file_name": file_name}, "diagnostics": {"message": "FileName was not found in the requested live SQL storage table."}, } return b"".join(parts), config, None BASE_SUPPORT_CONFIGURATION_ID_PATH = (3, 1, 1, 1, 1, 1, 2) def discover_base_support_source( base_id: str, *, timeout_seconds: int = 30, ) -> tuple[dict[str, str] | None, dict[str, Any] | None]: """Resolve the base configuration ParentConfigurations payload from live SQL.""" pointer_data, config, pointer_error = read_storage_file_bytes( base_id, "Config", "root", timeout_seconds=timeout_seconds ) if pointer_error: return None, public_error_result(pointer_error, method="metadata.support.decode") pointer_tree = parse_config_tree_from_bytes(pointer_data or b"") pointer_items = pointer_tree.get("items") if isinstance(pointer_tree, dict) else [] root_file = next( ( config_tree_scalar(item).lower() for item in (pointer_items or [])[1:] if is_guid_text(config_tree_scalar(item)) ), "", ) if not root_file: return None, { "schema": "onec_support_rules.v1", "method": "metadata.support.decode", "status": "source_unknown", "base_id": base_id, "diagnostics": { "message": "Config/root does not contain a valid configuration root descriptor GUID.", "permission_inference": "forbidden", }, } root_data, _, root_error = read_storage_file_bytes( base_id, "Config", root_file, timeout_seconds=timeout_seconds ) if root_error: return None, public_error_result(root_error, method="metadata.support.decode") root_tree = parse_config_tree_from_bytes(root_data or b"") configuration_id = config_tree_scalar_at_path(root_tree, BASE_SUPPORT_CONFIGURATION_ID_PATH).lower() if not is_guid_text(configuration_id): return None, { "schema": "onec_support_rules.v1", "method": "metadata.support.decode", "status": "source_unknown", "base_id": base_id, "diagnostics": { "message": "The base configuration descriptor does not contain the expected internal configuration GUID.", "root_file": root_file, "configuration_id_path": ".".join(map(str, BASE_SUPPORT_CONFIGURATION_ID_PATH)), "permission_inference": "forbidden", }, } return { "kind": "live_sql", "database": str((config or {}).get("database") or ""), "table": "Config", "file_name": f"{configuration_id}.4", "root_file": root_file, "configuration_id": configuration_id, "discovery": "base_root_descriptor", }, None def discover_extension_support_source( base_id: str, extension_guid: str, *, timeout_seconds: int = 30, ) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: """Resolve an extension ParentConfigurations payload from its complete CAS manifest.""" manifests, diagnostics = live_extension_manifests( base_id, extension_guid=extension_guid, timeout_seconds=timeout_seconds, ) manifest = next( ( item for item in manifests if str((item.get("extension") or {}).get("guid") or "").lower() == extension_guid.lower() ), None, ) if not manifest: return None, { "schema": "onec_support_rules.v1", "method": "metadata.support.decode", "status": "source_unknown", "base_id": base_id, "layer_id": f"extension:{extension_guid}", "diagnostics": { "message": "The extension CAS manifest could not be resolved from live SQL.", "manifest_diagnostics": diagnostics, "permission_inference": "forbidden", }, } configuration_id = str(manifest.get("extension_configuration_guid") or "").lower() if not is_guid_text(configuration_id): return None, { "schema": "onec_support_rules.v1", "method": "metadata.support.decode", "status": "source_unknown", "base_id": base_id, "layer_id": f"extension:{extension_guid}", "diagnostics": { "message": "The extension manifest does not contain a valid configuration GUID.", "permission_inference": "forbidden", }, } support_object_id = f"{configuration_id}.4" entry = next( ( item for item in (manifest.get("entries") or []) if str(item.get("object_id") or "").lower() == support_object_id ), None, ) source = { "kind": "live_sql", "database": "", "table": "ConfigCAS", "root_cas_key": str(manifest.get("root_cas_key") or ""), "configuration_id": configuration_id, "extension": manifest.get("extension"), "manifest_declared_count": manifest.get("declared_count"), "manifest_entry_count": manifest.get("entry_count"), "discovery": "extension_cas_manifest", } if not entry: return {**source, "support_payload_present": False}, None return { **source, "file_name": str(entry.get("cas_key") or "").lower(), "support_object_id": support_object_id, "support_payload_present": True, }, None def metadata_support_decode(payload: dict[str, Any]) -> dict[str, Any]: method = "metadata.support.decode" normalized = normalize_object_selector_aliases(payload, method) if isinstance(normalized, dict) and normalized.get("status") == "invalid_argument": return normalized payload = normalized base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error argument_error = validate_optional_non_empty_string_arguments( payload, method, ["file_name", "object_guid", "guid", "layer_id"], ) if argument_error: return argument_error layer_id = str(payload.get("layer_id") or "base").strip().lower() if layer_id != "base" and not layer_id.startswith("extension:"): return invalid_argument(method, "layer_id", "layer_id must be base or extension:.") if layer_id.startswith("extension:") and not is_guid_text(layer_id.split(":", 1)[1]): return invalid_argument(method, "layer_id", "The extension layer_id suffix must be a GUID.") object_guid = str(payload.get("object_guid") or payload.get("guid") or "").strip().lower() if object_guid and not is_guid_text(object_guid): return invalid_argument(method, "object_guid", "object_guid must be a GUID string.") file_name = str(payload.get("file_name") or "").strip() source_discovery = "caller_verified" discovered_source: dict[str, Any] | None = None timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error resolved_object: dict[str, Any] | None = None if not object_guid and payload.get("name"): if layer_id == "base": resolved = get_object( payload.get("kind"), str(payload.get("name") or ""), base_id=base_id_or_error, include_semantic=False, timeout_seconds=int(timeout_seconds or 30), ) if resolved.get("status") != "ok": result = dict(resolved) result["method"] = method return result object_card = resolved.get("object") if isinstance(resolved.get("object"), dict) else resolved else: found = extension_objects_find( { "base_id": base_id_or_error, "extension_guid": layer_id.split(":", 1)[1], "query": payload.get("name"), "kind": payload.get("kind"), "limit": 20, "scan_limit": 5000, } ) if found.get("status") not in {"ok", "partial"}: result = dict(found) result["method"] = method return result wanted_name = normalize_exact(payload.get("name")) object_card = next( ( item for item in found.get("objects") or [] if isinstance(item, dict) and normalize_exact(item.get("name")) == wanted_name and ( not payload.get("kind") or canonical_kind(str(item.get("kind") or "")) == canonical_kind(str(payload.get("kind") or "")) ) ), None, ) if not object_card: return { "schema": "onec_support_rules.v1", "method": method, "status": "not_found", "error": "object_not_found", "base_id": base_id_or_error, "query": { "kind": payload.get("kind"), "name": payload.get("name"), "layer_id": layer_id, }, "diagnostics": {"message": "The selected extension object was not found by its 1C name."}, } object_guid = str((object_card or {}).get("guid") or "").strip().lower() if not is_guid_text(object_guid): return { "schema": "onec_support_rules.v1", "method": method, "status": "source_missing", "error": "object_guid_unresolved", "base_id": base_id_or_error, "query": {"kind": payload.get("kind"), "name": payload.get("name"), "layer_id": layer_id}, "diagnostics": {"message": "The selected object was resolved by name, but its metadata GUID is unavailable."}, } resolved_object = public_metadata_row(object_card) if not file_name: if layer_id == "base": discovered_source, discovery_error = discover_base_support_source( base_id_or_error, timeout_seconds=int(timeout_seconds or 30) ) else: discovered_source, discovery_error = discover_extension_support_source( base_id_or_error, layer_id.split(":", 1)[1], timeout_seconds=int(timeout_seconds or 30), ) if discovery_error: return discovery_error if discovered_source and discovered_source.get("support_payload_present") is False: return { "schema": "onec_support_rules.v1", "method": method, "status": "ok", "base_id": base_id_or_error, "layer_id": layer_id, "source": discovered_source, "format_marker": None, "supplier_count": 0, "suppliers": [], "query": { "object_guid": object_guid or None, **({"object": resolved_object} if resolved_object else {}), }, "object_support": { "object_guid": object_guid or None, "status": "not_supported", "edit_allowed_by_support": True, "evidence": "support_payload_absent_from_complete_extension_manifest", }, "matches": [], "counts": {"suppliers": 0, "matches": 0}, "diagnostics": { "source_discovery": "extension_cas_manifest", "repository_lock_evaluated": False, }, } file_name = str((discovered_source or {}).get("file_name") or "") source_discovery = str((discovered_source or {}).get("discovery") or "") if Path(file_name).name != file_name: return invalid_argument(method, "file_name", "file_name must be a safe storage file name.") table_payload = dict(payload) if discovered_source: table_payload["table"] = discovered_source["table"] table_or_error = storage_table(table_payload, method) if isinstance(table_or_error, dict): return table_or_error data, config, source_error = read_storage_file_bytes( base_id_or_error, table_or_error, file_name, timeout_seconds=int(timeout_seconds or 30), ) if source_error: return public_error_result(source_error, method=method) try: from parser.support_rules import parse_parent_configurations_bytes decoded = parse_parent_configurations_bytes(data or b"", source=file_name) except ValueError as exc: return { "schema": "onec_support_rules.v1", "method": method, "status": "invalid_format", "base_id": base_id_or_error, "diagnostics": {"message": str(exc), "permission_inference": "forbidden"}, } suppliers = [] matches = [] for supplier in decoded["suppliers"]: rules = list(supplier.rules) suppliers.append({ "configuration_guid": supplier.configuration_guid, "general_mode_code": supplier.general_mode_code, "general_mode": supplier.general_mode, "version": supplier.version, "producer": supplier.producer, "name": supplier.name, "declared_object_count": supplier.declared_object_count, "parsed_object_count": len(rules), }) if object_guid: matches.extend({**rule.to_dict(), "configuration_guid": supplier.configuration_guid} for rule in rules if rule.object_guid == object_guid) object_support = None if object_guid: if not matches: object_support = { "object_guid": object_guid, "status": "not_supported", "edit_allowed_by_support": True, "evidence": "absent_from_complete_parent_configurations", } elif any(int(match.get("rule_code", -1)) == 0 for match in matches): object_support = { "object_guid": object_guid, "status": "not_editable", "edit_allowed_by_support": False, "evidence": "effective_supplier_rule", } elif any(int(match.get("rule_code", -1)) == 1 for match in matches): object_support = { "object_guid": object_guid, "status": "editable_support_preserved", "edit_allowed_by_support": True, "evidence": "effective_supplier_rule", } else: object_support = { "object_guid": object_guid, "status": "not_supported", "edit_allowed_by_support": True, "evidence": "effective_supplier_rule", } return { "schema": "onec_support_rules.v1", "method": method, "status": "ok", "base_id": base_id_or_error, "layer_id": layer_id, "source": discovered_source or {"kind": "live_sql", "database": (config or {}).get("database"), "table": table_or_error, "file_name": file_name}, "format_marker": decoded["format_marker"], "supplier_count": decoded["supplier_count"], "suppliers": suppliers, "query": { "object_guid": object_guid or None, **({"object": resolved_object} if resolved_object else {}), }, "object_support": object_support, "matches": matches, "counts": {"suppliers": len(suppliers), "matches": len(matches)}, "diagnostics": { "source_discovery": source_discovery, "missing_match_semantics": "not_supported" if object_guid and not matches else None, "repository_lock_evaluated": False, }, } REPOSITORY_SQL_STATE_TABLES = ("Params", "Config", "ConfigSave", "ConfigCAS", "ConfigCASSave") def repository_sql_state_snapshot(payload: dict[str, Any]) -> dict[str, Any]: method = "repository.sql_state.snapshot" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error diagnostic_error = require_diagnostic_mode(payload, method) if diagnostic_error: return diagnostic_error raw_tables = payload.get("tables", ["Params"]) if not isinstance(raw_tables, list) or not raw_tables or any(not isinstance(item, str) for item in raw_tables): return invalid_argument(method, "tables", "tables must be a non-empty JSON array of storage table names.") tables = list(dict.fromkeys(str(item).strip() for item in raw_tables)) unsupported = [item for item in tables if item not in REPOSITORY_SQL_STATE_TABLES] if unsupported: return invalid_argument( method, "tables", "Unsupported SQL state table.", allowed_values=list(REPOSITORY_SQL_STATE_TABLES), ) include_extensions, include_extensions_error = strict_bool_argument( payload, "include_extensions", method=method, default=True ) if include_extensions_error: return include_extensions_error timeout_seconds, timeout_error = parse_int_argument( payload, "timeout_seconds", method=method, default=120, minimum=1, maximum=600 ) if timeout_error: return timeout_error conn, config, connection_error = connect_live_sql( base_id_or_error, method, timeout_seconds=int(timeout_seconds or 120) ) if connection_error: return connection_error records: list[dict[str, Any]] = [] extensions: list[dict[str, Any]] = [] started = time.time() try: with conn: with conn.cursor(as_dict=True) as cursor: for table in tables: cursor.execute( f"SELECT FileName, PartNo, BinaryData FROM dbo.[{table}] ORDER BY FileName, PartNo" ) current_name: str | None = None current_parts: list[dict[str, Any]] = [] current_hash = hashlib.sha1() current_bytes = 0 def finish_record() -> None: nonlocal current_name, current_parts, current_hash, current_bytes if current_name is None: return records.append( { "area": "storage", "table": table, "file_name": current_name, "part_count": len(current_parts), "bytes": current_bytes, "sha1": current_hash.hexdigest(), "parts": current_parts, } ) for row in cursor: file_name = str(row.get("FileName") or "") if current_name is not None and file_name != current_name: finish_record() current_parts = [] current_hash = hashlib.sha1() current_bytes = 0 current_name = file_name value = row.get("BinaryData") data = bytes(value) if isinstance(value, (bytes, bytearray, memoryview)) else b"" part_no = jsonable(row.get("PartNo")) current_hash.update(data) current_bytes += len(data) current_parts.append( {"part_no": part_no, "bytes": len(data), "sha1": hashlib.sha1(data).hexdigest()} ) finish_record() if include_extensions: cursor.execute( "SELECT [_IDRRef], [_ExtName], [_ExtensionOrder], [_ExtensionZippedInfo] " "FROM dbo.[_ExtensionsInfo] ORDER BY [_ExtensionOrder], [_ExtName]" ) for row in cursor.fetchall(): zipped = row.get("_ExtensionZippedInfo") zipped_data = bytes(zipped) if isinstance(zipped, (bytes, bytearray, memoryview)) else b"" extensions.append( { "area": "extensions_info", "guid": dbnames_ext_guid_from_idrref(row.get("_IDRRef")), "name": jsonable(row.get("_ExtName")), "order": jsonable(row.get("_ExtensionOrder")), "bytes": len(zipped_data), "sha1": hashlib.sha1(zipped_data).hexdigest(), "root_cas_key": extension_root_key_from_zipped_info(zipped_data), } ) except Exception as exc: return { "schema": "onec_repository_sql_state_snapshot.v1", "method": method, "status": "error", "base_id": base_id_or_error, "diagnostics": {"message": str(exc), "native_lock_inference": "forbidden"}, } portable_snapshot = { "schema": "onec_repository_sql_state_snapshot.v1", "base_id": base_id_or_error, "scope": {"tables": tables, "include_extensions": bool(include_extensions)}, "records": records, "extensions": extensions, } identity = json.dumps( portable_snapshot, ensure_ascii=False, sort_keys=True, separators=(",", ":"), ).encode("utf-8") snapshot_id = hashlib.sha256(identity).hexdigest() comparison_token = base64.urlsafe_b64encode(zlib.compress(identity, level=9)).decode("ascii") return { "schema": "onec_repository_sql_state_snapshot.v1", "method": method, "status": "ok", "base_id": base_id_or_error, "snapshot_id": snapshot_id, "comparison_token": comparison_token, "source": {"kind": "live_sql", "database": (config or {}).get("database")}, "scope": {"tables": tables, "include_extensions": bool(include_extensions)}, "records": records, "extensions": extensions, "counts": {"records": len(records), "extensions": len(extensions)}, "duration_ms": int((time.time() - started) * 1000), "diagnostics": { "payload_returned": False, "native_lock_inference": "forbidden_until_repeatable_differential_validation", }, } def repository_sql_state_diff(payload: dict[str, Any]) -> dict[str, Any]: method = "repository.sql_state.diff" diagnostic_error = require_diagnostic_mode(payload, method) if diagnostic_error: return diagnostic_error def token_snapshot(value: Any, argument: str) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: if value is None or value == "": return None, None if not isinstance(value, str): return None, invalid_argument(method, argument, f"{argument} must be a comparison_token string.") try: decoded = zlib.decompress(base64.urlsafe_b64decode(value.encode("ascii"))) snapshot = json.loads(decoded.decode("utf-8")) except Exception: return None, invalid_argument(method, argument, f"{argument} is not a valid comparison_token.") if not isinstance(snapshot, dict): return None, invalid_argument(method, argument, f"{argument} did not decode to a snapshot object.") snapshot["snapshot_id"] = hashlib.sha256(decoded).hexdigest() return snapshot, None before = payload.get("before") after = payload.get("after") if before is None and payload.get("before_token") is not None: before, token_error = token_snapshot(payload.get("before_token"), "before_token") if token_error: return token_error if after is None and payload.get("after_token") is not None: after, token_error = token_snapshot(payload.get("after_token"), "after_token") if token_error: return token_error if not isinstance(before, dict) or not isinstance(after, dict): return invalid_argument(method, "before/after", "before and after must be snapshot JSON objects.") if before.get("schema") != "onec_repository_sql_state_snapshot.v1" or after.get("schema") != before.get("schema"): return invalid_argument(method, "before/after", "Both inputs must be repository.sql_state.snapshot responses.") if str(before.get("base_id") or "") != str(after.get("base_id") or ""): return invalid_argument(method, "before/after", "Snapshots must belong to the same base_id.") def keyed(snapshot: dict[str, Any]) -> dict[str, dict[str, Any]]: result: dict[str, dict[str, Any]] = {} for item in snapshot.get("records") or []: if isinstance(item, dict): result[f"storage:{item.get('table')}:{item.get('file_name')}"] = item for item in snapshot.get("extensions") or []: if isinstance(item, dict): result[f"extensions_info:{item.get('guid') or item.get('name')}"] = item return result before_items = keyed(before) after_items = keyed(after) added = [] removed = [] changed = [] for key in sorted(set(before_items) | set(after_items)): old = before_items.get(key) new = after_items.get(key) if old is None: added.append({"key": key, "after": new}) elif new is None: removed.append({"key": key, "before": old}) elif old.get("sha1") != new.get("sha1") or old.get("bytes") != new.get("bytes"): old_parts = {str(item.get("part_no")): item for item in old.get("parts") or [] if isinstance(item, dict)} new_parts = {str(item.get("part_no")): item for item in new.get("parts") or [] if isinstance(item, dict)} changed_parts = [ part for part in sorted(set(old_parts) | set(new_parts)) if old_parts.get(part) != new_parts.get(part) ] changed.append( { "key": key, "before": {key: old.get(key) for key in ("bytes", "sha1", "part_count") if key in old}, "after": {key: new.get(key) for key in ("bytes", "sha1", "part_count") if key in new}, "changed_parts": changed_parts, } ) return { "schema": "onec_repository_sql_state_diff.v1", "method": method, "status": "ok", "base_id": before.get("base_id"), "before_snapshot_id": before.get("snapshot_id"), "after_snapshot_id": after.get("snapshot_id"), "changed": bool(added or removed or changed), "added": added, "removed": removed, "modified": changed, "counts": {"added": len(added), "removed": len(removed), "modified": len(changed)}, "diagnostics": {"native_lock_inference": "forbidden_until_repeatable_differential_validation"}, } SAVED_STATE_SOURCE_BY_TARGET = {"ConfigSave": "Config", "ConfigCASSave": "ConfigCAS"} SAVED_STATE_TARGET_BY_SOURCE = {"Config": "ConfigSave", "ConfigCAS": "ConfigCASSave"} SAVED_STATE_TABLE_BY_LAYER = {"base_saved_state": "ConfigSave", "extension_saved_state": "ConfigCASSave"} SAVED_STATE_LAYER_BY_TABLE = {table: layer for layer, table in SAVED_STATE_TABLE_BY_LAYER.items()} SAVED_STATE_ACTIVE_LAYER_BY_TABLE = {"ConfigSave": "base_configuration", "ConfigCASSave": "extension_configuration"} SAVED_STATE_COPY_COLUMNS = ("FileName", "Creation", "Modified", "Attributes", "DataSize", "BinaryData", "PartNo") def saved_state_row_public(row: dict[str, Any]) -> dict[str, Any]: def value(*names: str) -> Any: for name in names: if name in row: return row.get(name) lowered = {str(key).casefold(): key for key in row} for name in names: key = lowered.get(name.casefold()) if key is not None: return row.get(key) return None return { "file_name": value("FileName", "file_name"), "part_no": value("PartNo", "part_no"), "data_size": value("DataSize", "data_size"), "binary_bytes": value("BinaryBytes", "binary_bytes"), "binary_sha1": str(value("BinarySHA1", "binary_sha1") or "").lower(), } def saved_state_copy_row_details(base_id: str, table: str, file_names: list[str], *, timeout_seconds: int = 30) -> tuple[list[dict[str, Any]] | None, dict[str, str] | None, dict[str, Any] | None]: if table not in STORAGE_TABLES: return None, None, invalid_argument("metadata.saved_state.prepare", "table", "Unsupported storage table.", allowed_values=sorted(STORAGE_TABLES)) names = sorted({name for name in file_names if name and Path(name).name == name}) if not names: return [], None, None conn, config, error = connect_live_sql(base_id, "metadata.saved_state.prepare", timeout_seconds=timeout_seconds) if error: return None, config, error rows: list[dict[str, Any]] = [] try: with conn: with conn.cursor(as_dict=True) as cursor: for start in range(0, len(names), 500): chunk = names[start : start + 500] placeholders = ",".join(["%s"] * len(chunk)) cursor.execute( f""" SELECT FileName, PartNo, DataSize, DATALENGTH(BinaryData) AS BinaryBytes, CONVERT(varchar(40), HASHBYTES('SHA1', BinaryData), 2) AS BinarySHA1 FROM dbo.[{table}] WHERE FileName IN ({placeholders}) ORDER BY FileName, PartNo """, tuple(chunk), ) rows.extend({key: jsonable(value) for key, value in row.items()} for row in cursor.fetchall()) except Exception as exc: return None, config, { "schema": "onec_adapter_source_error.v1", "method": "metadata.saved_state.prepare", "status": "error", "base_id": base_id, "source": {"kind": "live_sql", "database": (config or {}).get("database"), "table": table}, "diagnostics": {"message": str(exc)}, } finally: try: conn.close() except Exception: pass return rows, config, None def saved_state_prepare_file_names(payload: dict[str, Any], base_id: str, source_table: str, timeout_seconds: int) -> tuple[list[str], dict[str, Any] | None, dict[str, Any] | None]: method = "metadata.saved_state.prepare" raw_file_names = payload.get("file_names") if raw_file_names is not None: if not isinstance(raw_file_names, list) or not all(isinstance(item, str) and item and Path(item).name == item for item in raw_file_names): return [], None, invalid_argument(method, "file_names", "file_names must be an array of safe FileName strings.") return sorted(set(raw_file_names)), {"mode": "explicit_file_names"}, None file_name = str(payload.get("file_name") or "").strip() if file_name: if Path(file_name).name != file_name: return [], None, invalid_argument(method, "file_name", "file_name must be a safe FileName value.") return [file_name], {"mode": "explicit_file_name"}, None module_ref = str(payload.get("module_ref") or payload.get("module_id") or "").strip() if module_ref: module_table, module_file_name, _stream_index = parse_module_id(module_ref) if not module_table or not module_file_name: return [], None, invalid_argument(method, "module_ref", "Use module_ref in the form
:[#stream:].") if module_table in SAVED_STATE_SOURCE_BY_TARGET: module_table = SAVED_STATE_SOURCE_BY_TARGET[module_table] if module_table != source_table: return [], None, invalid_argument(method, "module_ref", f"module_ref table must match source family {source_table}.") return [module_file_name], {"mode": "module_ref", "module_ref": module_ref}, None extension_name = str(payload.get("extension") or "").strip() if extension_name: if source_table != "ConfigCAS": return [], None, invalid_argument(method, "target_table", "Extension saved-state preparation must target ConfigCASSave.", allowed_values=["ConfigCASSave"]) config, _ = sql_config_for_base(base_id) query = str(payload.get("query") or payload.get("name") or payload.get("object_name") or payload.get("guid") or payload.get("object_guid") or "").strip() kind_filter = canonical_kind(str(payload.get("kind") or payload.get("object_type") or "")) guid_filter = str(payload.get("guid") or payload.get("object_guid") or "").strip().lower() rows = extension_route_cache_lookup( config, query=query, kind_filter=kind_filter if kind_filter else None, guid_filter=guid_filter, extension_guid=None, limit=20, ) matches = [] requested_name = str(payload.get("name") or payload.get("object_name") or "").strip() for row in rows: if extension_name and normalize(row.get("extension_name")) != normalize(extension_name) and str(row.get("extension_guid") or "").lower() != extension_name.lower(): continue if requested_name and normalize(row.get("name")) != normalize(requested_name): continue matches.append(extension_route_cache_row_to_match(base_id, row, include_storage=True, freshness={"status": "cache_hit_verified"})) if len(matches) != 1: return [], {"mode": "extension_route_cache", "matches": len(matches), "extension": extension_name}, { "schema": "onec_saved_state_prepare.v1", "method": method, "status": "not_found" if not matches else "ambiguous", "base_id": base_id, "source": {"kind": "extension_route_cache", "table": source_table}, "diagnostics": { "message": "Extension object route was not resolved to exactly one cached route. Run extension.cache.rebuild/validate or pass file_names/module_ref.", "matches": len(matches), }, } match = matches[0] route = match.get("route") if isinstance(match.get("route"), dict) else {} file_names = [] route_file = str(route.get("file_name") or "").strip().lower() if route_file and Path(route_file).name == route_file: file_names.append(route_file) for entry in match.get("manifest_entries") or []: if not isinstance(entry, dict): continue cas_key = str(entry.get("cas_key") or "").strip().lower() if cas_key and Path(cas_key).name == cas_key: file_names.append(cas_key) return sorted(set(file_names)), { "mode": "extension_route_cache", "kind": match.get("kind"), "guid": match.get("guid"), "name": match.get("name"), "extension": (match.get("origin") or {}).get("extension") if isinstance(match.get("origin"), dict) else None, }, None guid, kind, object_card, error = resolve_object_guid( {**payload, "table": source_table}, base_id, timeout_seconds=timeout_seconds, method=method, table=source_table, ) if error: return [], None, error limit_value, limit_error = parse_int_argument(payload, "part_limit", method=method, default=5000, minimum=1, maximum=20000) if limit_error: return [], None, limit_error files = storage_files_list({"base_id": base_id, "table": source_table, "prefix": guid, "limit": int(limit_value or 5000), "timeout_seconds": timeout_seconds, "_internal": True}) if files.get("status") != "ok": return [], object_card, files names = [ str(row.get("FileName") or "") for row in files.get("files") or [] if str(row.get("FileName") or "") == guid or str(row.get("FileName") or "").startswith(f"{guid}.") or str(row.get("FileName") or "").startswith(f"{guid}__") ] return sorted(set(names)), object_card or {"guid": guid, "kind": kind}, None def apply_saved_state_prepare_copy( base_id: str, source_table: str, target_table: str, file_names: list[str], *, expected_source_rows: int, prepared_rows: list[dict[str, Any]], timeout_seconds: int, ) -> dict[str, Any]: method = "metadata.saved_state.prepare" conn, config, error = connect_live_sql(base_id, method, timeout_seconds=timeout_seconds) if error: return error started = time.time() names = sorted(set(file_names)) try: cursor = conn.cursor(as_dict=True) placeholders = ",".join(["%s"] * len(names)) cursor.execute( f"SELECT FileName, PartNo FROM dbo.[{target_table}] WITH (UPDLOCK, HOLDLOCK) WHERE FileName IN ({placeholders})", tuple(names), ) collisions = [{key: jsonable(value) for key, value in row.items()} for row in cursor.fetchall()] if collisions: conn.rollback() return { "schema": "onec_saved_state_prepare.v1", "status": "blocked_target_collision", "applied": False, "base_id": base_id, "source": {"kind": "live_sql", "database": config["database"], "table": source_table}, "target": {"table": target_table}, "collisions": [saved_state_row_public(row) for row in collisions], "diagnostics": {"message": "Target saved-state table already contains planned FileName values. Do not overwrite human or pending Configurator changes."}, } column_list = ", ".join(f"[{column}]" for column in SAVED_STATE_COPY_COLUMNS) source_columns = ", ".join(f"s.[{column}]" for column in SAVED_STATE_COPY_COLUMNS) cursor.execute( f""" INSERT INTO dbo.[{target_table}] ({column_list}) SELECT {source_columns} FROM dbo.[{source_table}] AS s WHERE s.FileName IN ({placeholders}) """, tuple(names), ) inserted = int(cursor.rowcount or 0) if inserted != expected_source_rows: conn.rollback() return { "schema": "onec_saved_state_prepare.v1", "status": "precondition_failed", "applied": False, "base_id": base_id, "source": {"kind": "live_sql", "database": config["database"], "table": source_table}, "target": {"table": target_table}, "counts": {"expected_insert_rows": expected_source_rows, "inserted_rows": inserted}, "diagnostics": {"message": "Copied row count did not match current source row count."}, } receipt = write_saved_state_prepare_receipt( base_id=base_id, config=config, source_table=source_table, target_table=target_table, rows=prepared_rows, ) conn.commit() except Exception as exc: try: conn.rollback() except Exception: pass return { "schema": "onec_saved_state_prepare.v1", "status": "error", "applied": False, "base_id": base_id, "source": {"kind": "live_sql", "database": (config or {}).get("database"), "table": source_table}, "target": {"table": target_table}, "diagnostics": {"message": str(exc)}, } finally: try: conn.close() except Exception: pass return { "schema": "onec_saved_state_prepare.v1", "status": "applied", "applied": True, "base_id": base_id, "source": {"kind": "live_sql", "database": config["database"], "table": source_table}, "target": {"table": target_table}, "counts": {"inserted_rows": inserted, "file_names": len(names)}, "prepare_receipt": receipt, "cache_invalidation": invalidate_adapter_caches_after_saved_state_change( base_id, reason="saved_state_prepare", ), "duration_ms": int((time.time() - started) * 1000), } def saved_state_prepare_receipt_dir() -> Path: return storage_apply_backup_dir() / "prepare-receipts" def write_saved_state_prepare_receipt( *, base_id: str, config: dict[str, str], source_table: str, target_table: str, rows: list[dict[str, Any]], ) -> dict[str, Any]: receipt_id = uuid.uuid4().hex created_at = datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z") root = saved_state_prepare_receipt_dir() root.mkdir(parents=True, exist_ok=True) path = root / f"{created_at.replace(':', '').replace('-', '')}-{receipt_id}.json" normalized_rows = [ { key: row.get(key) for key in ("file_name", "part_no", "data_size", "binary_bytes", "binary_sha1") } for row in rows ] evidence = { "schema": "onec_saved_state_prepare_receipt.v1", "receipt_id": receipt_id, "created_at_utc": created_at, "base_id": base_id, "source": { "server": config.get("server"), "database": config.get("database"), "table": source_table, }, "target": {"table": target_table}, "rows": normalized_rows, "status": "prepared", } path.write_text(json.dumps(evidence, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") return { "receipt_id": receipt_id, "path": str(path), "rows": len(normalized_rows), } def resolve_saved_state_prepare_receipt(receipt_id: str) -> Path | None: normalized = str(receipt_id or "").strip().lower() if not re.fullmatch(r"[0-9a-f]{32}", normalized): return None root = saved_state_prepare_receipt_dir() matches = sorted(root.glob(f"*{normalized}.json")) if root.is_dir() else [] return matches[0] if len(matches) == 1 else None def rollback_saved_state_prepare_receipt( base_id: str, receipt_id: str, *, timeout_seconds: int, ) -> dict[str, Any]: path = resolve_saved_state_prepare_receipt(receipt_id) if path is None: return { "schema": "onec_saved_state_prepare_rollback.v1", "status": "not_found", "applied": False, "receipt_id": receipt_id, } try: evidence = json.loads(path.read_text(encoding="utf-8-sig")) except Exception as exc: return { "schema": "onec_saved_state_prepare_rollback.v1", "status": "invalid_receipt", "applied": False, "receipt_id": receipt_id, "diagnostics": {"message": str(exc)}, } if str(evidence.get("base_id") or "") != str(base_id): return { "schema": "onec_saved_state_prepare_rollback.v1", "status": "blocked", "applied": False, "error": "receipt_base_mismatch", "receipt_id": receipt_id, } if evidence.get("status") == "rolled_back": return { "schema": "onec_saved_state_prepare_rollback.v1", "status": "already_rolled_back", "applied": True, "receipt_id": receipt_id, } target = evidence.get("target") if isinstance(evidence.get("target"), dict) else {} target_table = str(target.get("table") or "") if target_table not in SAVED_STATE_SOURCE_BY_TARGET: return { "schema": "onec_saved_state_prepare_rollback.v1", "status": "invalid_receipt", "applied": False, "error": "receipt_target_invalid", "receipt_id": receipt_id, } expected_rows = [ row for row in (evidence.get("rows") or []) if isinstance(row, dict) and row.get("file_name") and Path(str(row["file_name"])).name == str(row["file_name"]) ] file_names = sorted({str(row["file_name"]) for row in expected_rows}) if not expected_rows or not file_names: return { "schema": "onec_saved_state_prepare_rollback.v1", "status": "invalid_receipt", "applied": False, "error": "receipt_rows_missing", "receipt_id": receipt_id, } conn, config, error = connect_live_sql(base_id, "metadata.saved_state.prepare.rollback", timeout_seconds=timeout_seconds) if error: return error started = time.time() try: cursor = conn.cursor(as_dict=True) placeholders = ",".join(["%s"] * len(file_names)) cursor.execute( f""" SELECT FileName, PartNo, DataSize, DATALENGTH(BinaryData) AS BinaryBytes, CONVERT(varchar(40), HASHBYTES('SHA1', BinaryData), 2) AS BinarySHA1 FROM dbo.[{target_table}] WITH (UPDLOCK, HOLDLOCK) WHERE FileName IN ({placeholders}) ORDER BY FileName, PartNo """, tuple(file_names), ) current_rows = [ saved_state_row_public({key: jsonable(value) for key, value in row.items()}) for row in cursor.fetchall() ] comparable_fields = ("file_name", "part_no", "data_size", "binary_bytes", "binary_sha1") expected_comparable = sorted( tuple(row.get(field) for field in comparable_fields) for row in expected_rows ) current_comparable = sorted( tuple(row.get(field) for field in comparable_fields) for row in current_rows ) if current_comparable != expected_comparable: conn.rollback() return { "schema": "onec_saved_state_prepare_rollback.v1", "status": "precondition_failed", "applied": False, "error": "prepared_rows_changed", "receipt_id": receipt_id, "counts": { "expected_rows": len(expected_comparable), "current_rows": len(current_comparable), }, } cursor.execute( f"DELETE FROM dbo.[{target_table}] WHERE FileName IN ({placeholders})", tuple(file_names), ) deleted = int(cursor.rowcount or 0) if deleted != len(expected_rows): conn.rollback() return { "schema": "onec_saved_state_prepare_rollback.v1", "status": "precondition_failed", "applied": False, "error": "prepared_row_delete_count_mismatch", "receipt_id": receipt_id, "counts": {"expected_rows": len(expected_rows), "deleted_rows": deleted}, } conn.commit() except Exception as exc: try: conn.rollback() except Exception: pass return { "schema": "onec_saved_state_prepare_rollback.v1", "status": "error", "applied": False, "receipt_id": receipt_id, "diagnostics": {"message": str(exc)}, } finally: try: conn.close() except Exception: pass evidence["status"] = "rolled_back" evidence["rolled_back_at_utc"] = datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z") path.write_text(json.dumps(evidence, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") return { "schema": "onec_saved_state_prepare_rollback.v1", "status": "rolled_back", "applied": True, "base_id": base_id, "receipt_id": receipt_id, "target": {"table": target_table}, "counts": {"deleted_rows": deleted, "file_names": len(file_names)}, "cache_invalidation": invalidate_adapter_caches_after_saved_state_change( base_id, reason="saved_state_prepare_rollback", ), "duration_ms": int((time.time() - started) * 1000), } def public_saved_state_prepare_object(object_card: dict[str, Any] | None) -> dict[str, Any] | None: if not isinstance(object_card, dict): return None kind = object_card.get("kind") or object_card.get("object_type") name = object_card.get("name") or object_card.get("object_name") extension = object_card.get("extension") result = { **({"kind": kind} if kind else {}), **({"name": name} if name else {}), **({"synonym": object_card.get("synonym")} if object_card.get("synonym") else {}), **({"extension": extension} if isinstance(extension, str) and extension else {}), } ref = object_selector_ref(str(kind or ""), str(name or "")) if ref: result["ref"] = ref return result or None def public_saved_state_prepare_call_payload(payload: dict[str, Any]) -> dict[str, Any]: target_table = str(payload.get("target_table") or payload.get("table") or "").strip() source_table = str(payload.get("source_table") or "").strip() layer = str(payload.get("layer") or "").strip() if layer not in SAVED_STATE_TABLE_BY_LAYER: if target_table in SAVED_STATE_LAYER_BY_TABLE: layer = SAVED_STATE_LAYER_BY_TABLE[target_table] elif source_table in SAVED_STATE_TARGET_BY_SOURCE: layer = SAVED_STATE_LAYER_BY_TABLE[SAVED_STATE_TARGET_BY_SOURCE[source_table]] elif str(payload.get("extension") or "").strip(): layer = "extension_saved_state" else: layer = "base_saved_state" ref = str(payload.get("ref") or "").strip() kind = str(payload.get("kind") or payload.get("object_type") or "").strip() name = str(payload.get("name") or payload.get("object_name") or "").strip() if ref and not is_guid_text(ref): ref_kind, ref_name = parse_object_query(None, ref) kind = kind or str(ref_kind or "") name = name or str(ref_name or "") if not ref or is_guid_text(ref): ref = object_selector_ref(kind, name) or "" result: dict[str, Any] = { "base_id": payload.get("base_id"), "layer": layer, **({"ref": ref} if ref else {}), **({"kind": canonical_kind(kind)} if kind else {}), **({"name": name} if name else {}), **({"extension": payload.get("extension")} if isinstance(payload.get("extension"), str) and payload.get("extension") else {}), **({"query": payload.get("query")} if isinstance(payload.get("query"), str) and payload.get("query") else {}), "mode": "plan", } if not any(result.get(key) for key in ("ref", "name", "query")): result["selector_required"] = "Pass a public 1C ref or kind/name selector." return result def public_saved_state_prepare_embedded_result(result: dict[str, Any], payload: dict[str, Any]) -> dict[str, Any]: target_table = str(payload.get("target_table") or payload.get("table") or "").strip() source_table = str(payload.get("source_table") or "").strip() layer = str(payload.get("layer") or "").strip() if target_table not in SAVED_STATE_SOURCE_BY_TARGET: if layer in SAVED_STATE_TABLE_BY_LAYER: target_table = SAVED_STATE_TABLE_BY_LAYER[layer] elif source_table in SAVED_STATE_TARGET_BY_SOURCE: target_table = SAVED_STATE_TARGET_BY_SOURCE[source_table] elif str(payload.get("extension") or "").strip(): target_table = "ConfigCASSave" else: target_table = "ConfigSave" object_card = result.get("object") if isinstance(result.get("object"), dict) else None return public_saved_state_prepare_result( result, base_id=str(payload.get("base_id") or result.get("base_id") or ""), target_table=target_table, object_card=object_card, ) def public_saved_state_prepare_result( result: dict[str, Any], *, base_id: str, target_table: str, object_card: dict[str, Any] | None = None, ) -> dict[str, Any]: layer = SAVED_STATE_LAYER_BY_TABLE[target_table] raw_counts = result.get("counts") if isinstance(result.get("counts"), dict) else {} apply_result = result.get("apply_result") if isinstance(result.get("apply_result"), dict) else {} apply_counts = apply_result.get("counts") if isinstance(apply_result.get("counts"), dict) else {} result_object = result.get("object") if isinstance(result.get("object"), dict) else None object_public = public_saved_state_prepare_object(object_card or result_object) public: dict[str, Any] = { "schema": "onec_saved_state_prepare.v1", "method": "metadata.saved_state.prepare", "status": result.get("status") or "error", "applied": bool(result.get("applied")), "ready_to_copy": bool(result.get("ready_to_copy")), "base_id": base_id, "source": { "kind": "active_configuration", "layer": SAVED_STATE_ACTIVE_LAYER_BY_TABLE[target_table], }, "target": {"layer": layer}, **({"object": object_public} if object_public else {}), "counts": { "selected_objects": 1 if object_public else 0, "source_records": int(raw_counts.get("source_rows") or 0), "existing_saved_records": int(raw_counts.get("target_rows") or 0), "inserted_records": int(apply_counts.get("inserted_rows") or 0), }, "write_mode": { "target": "saved_state", "sql_write_performed": bool(result.get("applied")), "requires_allow_flag": True, }, "freshness": { "source": "saved_state", "status": "live_verified", "verified": True, }, "diagnostics": { "note": "Storage coordinates, files, hashes, and row details are hidden unless include_storage=true.", }, } if result.get("error"): public["error"] = result.get("error") verification = result.get("verification") if isinstance(result.get("verification"), dict) else None if verification: mismatched = verification.get("mismatched") if isinstance(verification.get("mismatched"), list) else [] public["verification"] = { "status": verification.get("status"), "expected_records": verification.get("expected_rows"), "actual_records": verification.get("actual_rows"), "mismatched_records": len(mismatched), } return public def metadata_saved_state_prepare(payload: dict[str, Any]) -> dict[str, Any]: method = "metadata.saved_state.prepare" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error normalized_payload = normalize_object_selector_aliases(payload, method) if normalized_payload.get("status") == "invalid_argument": return normalized_payload payload = normalized_payload timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1) if timeout_error: return timeout_error timeout_seconds = int(timeout_seconds or 60) mode = str(payload.get("mode") or payload.get("execution_mode") or "plan").strip().casefold() if mode not in {"plan", "apply", "apply_and_verify"}: return invalid_argument(method, "mode", "Unsupported mode.", allowed_values=["plan", "apply", "apply_and_verify"]) repository_error = repository_apply_gate(payload, method, mode) if repository_error: return repository_error include_storage, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error include_storage = bool(include_storage) layer = str(payload.get("layer") or "").strip() if layer and layer not in SAVED_STATE_TABLE_BY_LAYER: return invalid_argument(method, "layer", "layer must be base_saved_state or extension_saved_state.", allowed_values=sorted(SAVED_STATE_TABLE_BY_LAYER)) requested_target_table = str(payload.get("target_table") or payload.get("table") or "").strip() if layer and requested_target_table and requested_target_table != SAVED_STATE_TABLE_BY_LAYER[layer]: return invalid_argument(method, "layer", "layer conflicts with target_table.") target_table = requested_target_table or SAVED_STATE_TABLE_BY_LAYER.get(layer, "") source_table = str(payload.get("source_table") or "").strip() if not target_table: if source_table in SAVED_STATE_TARGET_BY_SOURCE: target_table = SAVED_STATE_TARGET_BY_SOURCE[source_table] elif str(payload.get("extension") or "").strip(): target_table = "ConfigCASSave" else: target_table = "ConfigSave" if target_table not in SAVED_STATE_SOURCE_BY_TARGET: return invalid_argument(method, "target_table", "target_table must be ConfigSave or ConfigCASSave.", allowed_values=sorted(SAVED_STATE_SOURCE_BY_TARGET)) if str(payload.get("extension") or "").strip() and target_table != "ConfigCASSave": return invalid_argument(method, "layer", "Extension object preparation requires extension_saved_state.", allowed_values=["extension_saved_state"]) expected_source_table = SAVED_STATE_SOURCE_BY_TARGET[target_table] if source_table and source_table != expected_source_table: return invalid_argument(method, "source_table", f"source_table must be {expected_source_table} for {target_table}.", allowed_values=[expected_source_table]) source_table = expected_source_table file_names, object_card, file_error = saved_state_prepare_file_names(payload, base_id, source_table, timeout_seconds) if file_error: return file_error if include_storage else public_saved_state_prepare_result( file_error, base_id=base_id, target_table=target_table, object_card=object_card, ) if not file_names: no_rows_result = { "schema": "onec_saved_state_prepare.v1", "method": method, "status": "blocked_no_active_source_rows", "applied": False, "base_id": base_id, "source": {"kind": "live_sql", "table": source_table}, "target": {"table": target_table}, "object": object_card, "counts": {"file_names": 0}, } return no_rows_result if include_storage else public_saved_state_prepare_result( no_rows_result, base_id=base_id, target_table=target_table, object_card=object_card, ) source_rows, config, source_error = saved_state_copy_row_details(base_id, source_table, file_names, timeout_seconds=timeout_seconds) if source_error: return source_error if include_storage else public_saved_state_prepare_result( source_error, base_id=base_id, target_table=target_table, object_card=object_card, ) target_rows, _target_config, target_error = saved_state_copy_row_details(base_id, target_table, file_names, timeout_seconds=timeout_seconds) if target_error: return target_error if include_storage else public_saved_state_prepare_result( target_error, base_id=base_id, target_table=target_table, object_card=object_card, ) source_rows = source_rows or [] target_rows = target_rows or [] public_source_rows = [saved_state_row_public(row) for row in source_rows] if len(public_source_rows) == len(file_names): for index, row in enumerate(public_source_rows): if not row.get("file_name"): row["file_name"] = file_names[index] public_target_rows = [saved_state_row_public(row) for row in target_rows] status = "plan_ready" if source_rows and not target_rows else ("blocked_target_collision" if target_rows else "blocked_no_active_source_rows") result: dict[str, Any] = { "schema": "onec_saved_state_prepare.v1", "status": status, "applied": False, "ready_to_copy": status == "plan_ready", "base_id": base_id, "source": {"kind": "live_sql", "database": (config or {}).get("database"), "table": source_table}, "target": {"table": target_table}, "object": object_card, "file_names": file_names, "source_rows": public_source_rows, "target_collisions": public_target_rows, "counts": {"file_names": len(file_names), "source_rows": len(source_rows), "target_rows": len(target_rows)}, "write_mode": {"sql_write_performed": False, "requires_allow_flag": True}, } if mode == "plan" or status != "plan_ready": return result if include_storage else public_saved_state_prepare_result( result, base_id=base_id, target_table=target_table, object_card=object_card, ) allow_prepare, allow_error = strict_bool_argument(payload, "allow_sql_saved_state_prepare", method=method, default=False) if allow_error: return allow_error if not allow_prepare: return invalid_argument(method, "allow_sql_saved_state_prepare", "Saved-state preparation writes SQL inserts; pass allow_sql_saved_state_prepare=true after reviewing the plan.") apply_result = apply_saved_state_prepare_copy( base_id, source_table, target_table, file_names, expected_source_rows=len(source_rows), prepared_rows=public_source_rows, timeout_seconds=timeout_seconds, ) result["apply_result"] = apply_result result["applied"] = bool(apply_result.get("applied")) result["write_mode"]["sql_write_performed"] = bool(apply_result.get("applied")) result["status"] = apply_result.get("status") or "error" if mode == "apply" or not result["applied"]: return result if include_storage else public_saved_state_prepare_result( result, base_id=base_id, target_table=target_table, object_card=object_card, ) verify_rows, _verify_config, verify_error = saved_state_copy_row_details(base_id, target_table, file_names, timeout_seconds=timeout_seconds) if verify_error: result["status"] = "verify_error" result["verify_error"] = verify_error return result if include_storage else public_saved_state_prepare_result( result, base_id=base_id, target_table=target_table, object_card=object_card, ) expected = {(row.get("FileName"), row.get("PartNo")): saved_state_row_public(row) for row in source_rows} actual = {(row.get("FileName"), row.get("PartNo")): saved_state_row_public(row) for row in (verify_rows or [])} mismatched = [ {"expected": expected[key], "actual": actual.get(key)} for key in sorted(expected) if actual.get(key) != expected[key] ] result["verification"] = { "status": "ok" if not mismatched and len(actual) == len(expected) else "mismatch", "expected_rows": len(expected), "actual_rows": len(actual), "mismatched": mismatched, } result["status"] = "verified" if result["verification"]["status"] == "ok" else "verification_failed" return result if include_storage else public_saved_state_prepare_result( result, base_id=base_id, target_table=target_table, object_card=object_card, ) def saved_state_diff_public_module_target( payload: dict[str, Any], base_id: str, target_table: str, timeout_seconds: int, ) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: source_table = SAVED_STATE_SOURCE_BY_TARGET[target_table] last_result: dict[str, Any] | None = None for table in (target_table, source_table): for refresh_cache in (False, True): module_result = read_module( { **payload, "base_id": base_id, "table": table, "include_storage": True, "include_text": False, "mode": "summary", "max_chars": 1, "refresh_cache": refresh_cache, "timeout_seconds": timeout_seconds, } ) last_result = module_result if module_result.get("status") != "ok": continue source = module_result.get("source") if isinstance(module_result.get("source"), dict) else {} module_id = str(module_result.get("module_id") or "").strip() module_table, module_file_name, stream_index = parse_module_id(module_id) file_name = str(source.get("file_name") or module_file_name or "").strip() if not file_name or Path(file_name).name != file_name: continue owner = module_result.get("owner") if isinstance(module_result.get("owner"), dict) else {} module = module_result.get("module") if isinstance(module_result.get("module"), dict) else {} module_ordinal = first_non_empty_arg( payload, "module_ordinal", "module_index", "module_number", default=module.get("module_ordinal") or 1, ) public_ref = str(payload.get("ref") or object_selector_ref(owner.get("kind"), owner.get("name")) or "").strip() selector = { **({"ref": public_ref} if public_ref else {}), **({"kind": owner.get("kind")} if owner.get("kind") else {}), **({"name": owner.get("name")} if owner.get("name") else {}), **({"module_ordinal": module_ordinal} if module else {}), **({"extension": payload.get("extension")} if payload.get("extension") else {}), } public_role = public_module_role( owner_kind=str(owner.get("kind") or ""), suffix=module_suffix_from_module_id(module_id), ordinal=int(module_ordinal or 1), current_name=str(owner.get("name") or "") if canonical_kind(str(owner.get("kind") or "")) == "CommonModule" else None, ) target_module_ref = f"{target_table}:{file_name}" if stream_index is not None: target_module_ref += f"#stream:{stream_index}" return { "file_name": file_name, "module_ref": target_module_ref, "stream_index": stream_index, "resolved_from_table": module_table or table, "cache_refresh_used": refresh_cache, "selector": selector, "object": {key: owner.get(key) for key in ("kind", "name", "synonym", "guid") if owner.get(key) is not None}, "module": { **{key: public_role.get(key) for key in ("kind", "name") if public_role.get(key) is not None}, **({"module_ordinal": module_ordinal} if module else {}), }, }, None return None, { "schema": "onec_saved_state_diff.v1", "method": SAVED_STATE_DIFF_METHOD, "status": str((last_result or {}).get("status") or "not_found"), "error": str((last_result or {}).get("error") or "module_not_found"), "base_id": base_id, "diagnostics": (last_result or {}).get("diagnostics") or {"message": "The named 1C object module was not resolved in saved or active metadata."}, } def metadata_saved_state_diff(payload: dict[str, Any]) -> dict[str, Any]: method = SAVED_STATE_DIFF_METHOD payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error max_changes, max_changes_error = parse_int_argument(payload, "max_changes", method=method, default=200, minimum=1, maximum=5000) if max_changes_error: return max_changes_error max_text_diff_lines, max_text_diff_lines_error = parse_int_argument(payload, "max_text_diff_lines", method=method, default=200, minimum=0, maximum=5000) if max_text_diff_lines_error: return max_text_diff_lines_error include_payload_diff, include_payload_diff_error = strict_bool_argument(payload, "include_payload_diff", method=method, default=False) if include_payload_diff_error: return include_payload_diff_error include_storage, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error for argument, default in (("include_text_diff", True), ("include_tree_diff", True), ("include_evidence", False)): _, bool_error = strict_bool_argument(payload, argument, method=method, default=default) if bool_error: return bool_error module_ref = str(payload.get("module_ref") or payload.get("module_id") or "").strip() module_table = module_file_name = None if module_ref: module_table, module_file_name, _stream_index = parse_module_id(module_ref) if not module_table or not module_file_name: return invalid_argument(method, "module_ref", "Use module_ref in the form
:[#stream:].") table = str(payload.get("table") or payload.get("target_table") or module_table or "").strip() public_selector_used = not module_ref and not str(payload.get("file_name") or "").strip() and has_object_selector(payload) if not table and public_selector_used: table = "ConfigCASSave" if str(payload.get("extension") or "").strip() else "ConfigSave" file_name = str(payload.get("file_name") or module_file_name or "").strip() if table not in SAVED_STATE_SOURCE_BY_TARGET: return invalid_argument(method, "table", "table must be ConfigSave or ConfigCASSave.", allowed_values=sorted(SAVED_STATE_SOURCE_BY_TARGET)) resolved_target: dict[str, Any] | None = None if public_selector_used: resolved_target, resolution_error = saved_state_diff_public_module_target( payload, base_id, table, int(timeout_seconds or 30), ) if resolution_error: return resolution_error file_name = str((resolved_target or {}).get("file_name") or "").strip() module_ref = str((resolved_target or {}).get("module_ref") or "").strip() if not file_name or Path(file_name).name != file_name: return invalid_argument( method, "selector", "Pass a 1C object selector plus module_ordinal, or a generated module_ref/saved-state file_name.", ) source_table = str(payload.get("source_table") or SAVED_STATE_SOURCE_BY_TARGET[table]).strip() if source_table != SAVED_STATE_SOURCE_BY_TARGET[table]: return invalid_argument(method, "source_table", f"source_table must be {SAVED_STATE_SOURCE_BY_TARGET[table]} for {table}.") storage_target = {"table": table, "file_name": file_name, **({"module_ref": module_ref} if module_ref else {})} target = storage_target if public_selector_used: target = { "selector": (resolved_target or {}).get("selector") or {}, "object": (resolved_target or {}).get("object") or {}, "module": (resolved_target or {}).get("module") or {}, **({"storage": storage_target} if include_storage else {}), } active_source = {"table": source_table, "file_name": file_name} saved_bytes, config, saved_error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=int(timeout_seconds or 30)) if saved_error: status = "not_found" if saved_error.get("status") == "source_missing" else saved_error.get("status") or "error" return { "schema": "onec_saved_state_diff.v1", "method": method, "status": status, "error": "saved_state_not_found" if status == "not_found" else saved_error.get("error", "source_error"), "base_id": base_id, "target": target, "source": ( {"kind": "live_metadata", "active": {"state": "active"}} if public_selector_used and not include_storage else active_source ), "current_state": {"source": "active", "activation_state": "active"}, "needs_prepare": status == "not_found", "prepare_payload": ( { "method": "metadata.saved_state.prepare", "base_id": base_id, "layer": SAVED_STATE_LAYER_BY_TABLE[table], **((resolved_target or {}).get("selector") or {}), "mode": "plan", } if public_selector_used else { "method": "metadata.saved_state.prepare", "base_id": base_id, "target_table": table, "file_name": file_name, "mode": "plan", } ), "diagnostics": saved_error.get("diagnostics") or {"message": "Saved-state target was not found."}, } active_bytes, active_config, active_error = read_storage_file_bytes(base_id, source_table, file_name, timeout_seconds=int(timeout_seconds or 30)) if active_error: status = "not_found" if active_error.get("status") == "source_missing" else active_error.get("status") or "error" return { "schema": "onec_saved_state_diff.v1", "method": method, "status": status, "error": "active_source_not_found" if status == "not_found" else active_error.get("error", "source_error"), "base_id": base_id, "target": target, "source": ( {"kind": "live_metadata", "active": {"state": "active"}} if public_selector_used and not include_storage else active_source ), "current_state": {"source": "saved_state", "activation_state": "not_activated"}, "needs_prepare": False, "diagnostics": active_error.get("diagnostics") or {"message": "Active source payload was not found."}, } diff = payload_diff( { "base_id": base_id, "diagnostic": True, "before": {"payload_base64": base64.b64encode(active_bytes or b"").decode("ascii")}, "after": {"payload_base64": base64.b64encode(saved_bytes or b"").decode("ascii")}, "max_changes": int(max_changes or 200), "max_text_diff_lines": int(max_text_diff_lines or 200), "include_text_diff": bool(payload.get("include_text_diff", True)), "include_tree_diff": bool(payload.get("include_tree_diff", True)), "include_evidence": bool(payload.get("include_evidence", False)), "timeout_seconds": int(timeout_seconds or 30), } ) if diff.get("status") not in {"changed", "unchanged"}: return { "schema": "onec_saved_state_diff.v1", "method": method, "status": diff.get("status") or "error", "error": diff.get("error", "payload_diff_failed"), "base_id": base_id, "target": target, "source": ( {"kind": "live_metadata", "active": {"state": "active"}, "saved": {"state": "saved"}} if public_selector_used and not include_storage else active_source ), "needs_prepare": False, "diagnostics": diff.get("diagnostics") or diff, } result: dict[str, Any] = { "schema": "onec_saved_state_diff.v1", "method": method, "status": diff.get("status"), "base_id": base_id, "source": ( { "kind": "live_metadata", "active": {"state": "active"}, "saved": {"state": "saved"}, **( { "storage": { "database": (config or active_config or {}).get("database"), "active": active_source, "saved": {"table": table, "file_name": file_name}, } } if include_storage else {} ), } if public_selector_used else { "kind": "live_sql", "database": (config or active_config or {}).get("database"), "active": active_source, "saved": {"table": table, "file_name": file_name}, } ), "target": target, **({"resolved_by": "public_object_module_selector"} if public_selector_used else {}), "current_state": { "source": "saved_state" if diff.get("status") == "changed" else "both", "activation_state": "not_activated" if diff.get("status") == "changed" else "same_as_active", }, "needs_prepare": False, "bytes": diff.get("bytes"), "sha1": diff.get("sha1"), "comparison": { "differs": diff.get("status") == "changed", "text_same": (diff.get("text") or {}).get("same") if isinstance(diff.get("text"), dict) else None, "tree_same": (diff.get("tree") or {}).get("same") if isinstance(diff.get("tree"), dict) else None, "strings_same": (diff.get("strings") or {}).get("same") if isinstance(diff.get("strings"), dict) else None, }, "text": diff.get("text"), "tree": diff.get("tree"), "strings": diff.get("strings"), "counts": diff.get("counts"), "freshness": { "source": "live_sql", "status": "live_sql_verified", "verified_against_sql": True, "active_payload_sha1": hashlib.sha1(active_bytes or b"").hexdigest(), "saved_payload_sha1": hashlib.sha1(saved_bytes or b"").hexdigest(), }, } if include_payload_diff: result["payload_diff"] = diff return result def saved_state_status_rows(base_id: str, table: str, *, prefix: str = "", limit: int = 500, timeout_seconds: int = 30) -> tuple[list[dict[str, Any]] | None, dict[str, str] | None, dict[str, Any] | None]: method = SAVED_STATE_STATUS_METHOD if table not in SAVED_STATE_SOURCE_BY_TARGET: return None, None, invalid_argument(method, "table", "table must be ConfigSave or ConfigCASSave.", allowed_values=sorted(SAVED_STATE_SOURCE_BY_TARGET)) conn, config, error = connect_live_sql(base_id, method, timeout_seconds=timeout_seconds) if error: return None, config, error rows: list[dict[str, Any]] = [] try: with conn: with conn.cursor(as_dict=True) as cursor: if prefix: cursor.execute( f""" SELECT TOP ({int(limit)}) FileName, PartNo, DataSize, DATALENGTH(BinaryData) AS BinaryBytes, CONVERT(varchar(40), HASHBYTES('SHA1', BinaryData), 2) AS BinarySHA1 FROM dbo.[{table}] WHERE FileName LIKE %s ORDER BY FileName, PartNo """, (f"{prefix}%",), ) else: cursor.execute( f""" SELECT TOP ({int(limit)}) FileName, PartNo, DataSize, DATALENGTH(BinaryData) AS BinaryBytes, CONVERT(varchar(40), HASHBYTES('SHA1', BinaryData), 2) AS BinarySHA1 FROM dbo.[{table}] ORDER BY FileName, PartNo """ ) rows.extend({key: jsonable(value) for key, value in row.items()} for row in cursor.fetchall()) except Exception as exc: return None, config, { "schema": "onec_adapter_source_error.v1", "method": method, "status": "error", "base_id": base_id, "source": {"kind": "live_sql", "database": (config or {}).get("database"), "table": table}, "diagnostics": {"message": str(exc)}, } finally: try: conn.close() except Exception: pass return rows, config, None def metadata_saved_state_status(payload: dict[str, Any]) -> dict[str, Any]: method = SAVED_STATE_STATUS_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error limit, limit_error = parse_int_argument(payload, "limit", method=method, default=500, minimum=1, maximum=5000) if limit_error: return limit_error include_files, include_files_error = strict_bool_argument(payload, "include_files", method=method, default=True) if include_files_error: return include_files_error include_unchanged, include_unchanged_error = strict_bool_argument(payload, "include_unchanged", method=method, default=True) if include_unchanged_error: return include_unchanged_error include_storage, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error include_storage = bool(include_storage) layer = str(payload.get("layer") or "").strip() if layer and layer not in SAVED_STATE_TABLE_BY_LAYER: return invalid_argument(method, "layer", "layer must be base_saved_state or extension_saved_state.", allowed_values=sorted(SAVED_STATE_TABLE_BY_LAYER)) requested_table = str(payload.get("table") or payload.get("target_table") or "").strip() if layer and requested_table and requested_table != SAVED_STATE_TABLE_BY_LAYER[layer]: return invalid_argument(method, "layer", "layer conflicts with table.") table = requested_table or SAVED_STATE_TABLE_BY_LAYER.get(layer) or "ConfigSave" if table not in SAVED_STATE_SOURCE_BY_TARGET: return invalid_argument(method, "table", "table must be ConfigSave or ConfigCASSave.", allowed_values=sorted(SAVED_STATE_SOURCE_BY_TARGET)) layer = SAVED_STATE_LAYER_BY_TABLE[table] prefix = str(payload.get("prefix") or "").strip() if prefix and Path(prefix).name != prefix: return invalid_argument(method, "prefix", "prefix must be a safe FileName prefix.") source_table = SAVED_STATE_SOURCE_BY_TARGET[table] saved_rows, config, rows_error = saved_state_status_rows(base_id, table, prefix=prefix, limit=int(limit or 500), timeout_seconds=int(timeout_seconds or 30)) if rows_error: return rows_error saved_rows = saved_rows or [] saved_public = [saved_state_row_public(row) for row in saved_rows] file_names = sorted({str(row.get("file_name") or "") for row in saved_public if row.get("file_name")}) active_rows, _active_config, active_error = saved_state_copy_row_details(base_id, source_table, file_names, timeout_seconds=int(timeout_seconds or 30)) if active_error: return active_error active_public = [saved_state_row_public(row) for row in (active_rows or [])] active_by_file: dict[str, list[dict[str, Any]]] = {} saved_by_file: dict[str, list[dict[str, Any]]] = {} for row in active_public: active_by_file.setdefault(str(row.get("file_name") or ""), []).append(row) for row in saved_public: saved_by_file.setdefault(str(row.get("file_name") or ""), []).append(row) files: list[dict[str, Any]] = [] counts = { "saved_rows": len(saved_public), "saved_files": len(saved_by_file), "active_rows": len(active_public), "changed_files": 0, "unchanged_files": 0, "saved_only_files": 0, "changed_rows": 0, } for file_name in sorted(saved_by_file): saved_file_rows = sorted(saved_by_file[file_name], key=lambda row: int(row.get("part_no") or 0)) active_file_rows = sorted(active_by_file.get(file_name, []), key=lambda row: int(row.get("part_no") or 0)) saved_map = {(row.get("part_no"), row.get("binary_sha1"), row.get("binary_bytes"), row.get("data_size")) for row in saved_file_rows} active_map = {(row.get("part_no"), row.get("binary_sha1"), row.get("binary_bytes"), row.get("data_size")) for row in active_file_rows} if not active_file_rows: status = "saved_only" counts["saved_only_files"] += 1 changed_parts = len(saved_file_rows) elif saved_map == active_map: status = "unchanged" counts["unchanged_files"] += 1 changed_parts = 0 else: status = "changed" counts["changed_files"] += 1 changed_parts = len(saved_map.symmetric_difference(active_map)) counts["changed_rows"] += changed_parts if include_files and (include_unchanged or status != "unchanged"): files.append( { "file_name": file_name, "status": status, "saved_rows": len(saved_file_rows), "active_rows": len(active_file_rows), "changed_parts": changed_parts, "saved_sha1": [row.get("binary_sha1") for row in saved_file_rows], "active_sha1": [row.get("binary_sha1") for row in active_file_rows], "diff_selector": { "method": SAVED_STATE_DIFF_METHOD, "base_id": base_id, "table": table, "file_name": file_name, }, } ) status = "empty" if not saved_public else ("changed" if counts["changed_files"] or counts["saved_only_files"] else "unchanged") storage_result = { "schema": "onec_saved_state_status.v1", "method": method, "status": status, "base_id": base_id, "source": { "kind": "live_sql", "database": (config or {}).get("database"), "saved": {"table": table}, "active": {"table": source_table}, }, "query": {"table": table, "source_table": source_table, "prefix": prefix or None, "limit": int(limit or 500)}, "current_state": {"source": "saved_state" if saved_public else "active", "activation_state": "not_activated" if saved_public else "active"}, "counts": counts, "files": files, "freshness": { "source": "live_sql", "status": "live_sql_verified", "verified_against_sql": True, }, } if include_storage: return storage_result return { "schema": "onec_saved_state_status.v1", "method": method, "status": status, "base_id": base_id, "source": {"kind": "saved_state"}, "query": { "layer": layer, "limit": int(limit or 500), "include_storage": False, }, "layer": { "name": layer, "active_source": SAVED_STATE_ACTIVE_LAYER_BY_TABLE[table], }, "current_state": storage_result["current_state"], "counts": { "saved_records": counts["saved_rows"], "saved_objects": counts["saved_files"], "active_records": counts["active_rows"], "changed_objects": counts["changed_files"], "unchanged_objects": counts["unchanged_files"], "saved_only_objects": counts["saved_only_files"], "changed_parts": counts["changed_rows"], }, "freshness": { "source": "saved_state", "status": "live_verified", "verified": True, }, "diagnostics": { "note": "Storage coordinates, file names, hashes, and diff selectors are hidden unless include_storage=true.", "changes_method": SAVED_STATE_CHANGES_LIST_METHOD, }, } def metadata_saved_state_change_context( *, base_id: str, table: str, file_name: str, timeout_seconds: int, ) -> dict[str, Any] | None: try: form_row = saved_state_form_search_row( base_id=base_id, table=table, file_name=file_name, payload={"max_targets": 0}, timeout_seconds=timeout_seconds, ) except Exception: form_row = None if isinstance(form_row, dict): form = form_row.get("form") if isinstance(form_row.get("form"), dict) else {} owner = form_row.get("owner") if isinstance(form_row.get("owner"), dict) else {} identity = form.get("identity") if isinstance(form.get("identity"), dict) else {} name = form_row.get("name") or form.get("name") or identity.get("name") synonym = form_row.get("synonym") or form.get("synonym") or identity.get("synonym") if not owner.get("name") and name: related_owner = saved_state_related_descriptor_identity( base_id=base_id, table=table, file_name=file_name, timeout_seconds=timeout_seconds, ) if related_owner and related_owner.get("name"): owner = { "status": "resolved", "kind": related_owner.get("kind") or "Catalog", "name": related_owner.get("name"), "synonym": related_owner.get("synonym"), "guid": related_owner.get("guid"), "source": "saved_state_descriptor", } owner_name = str(owner.get("name") or "").strip() if name or owner_name: return { "kind": "form", "presentation": ".".join(part for part in [owner_name, str(name or ""), "Форма"] if part), **({"owner": owner} if owner else {}), "form": { "name": name, "synonym": synonym, "guid": form.get("guid") or identity.get("guid"), }, "source": {"method": SAVED_STATE_FORMS_SEARCH_METHOD, "status": "resolved_by_file"}, } try: module_row = saved_state_module_search_row( base_id=base_id, table=table, file_name=file_name, file_row={"FileName": file_name}, payload={"preview_chars": 0}, timeout_seconds=timeout_seconds, ) except Exception: module_row = None if isinstance(module_row, dict): owner = module_row.get("owner") if isinstance(module_row.get("owner"), dict) else None form = module_row.get("form") if isinstance(module_row.get("form"), dict) else None module = module_row.get("module") if isinstance(module_row.get("module"), dict) else {} streams = module_row.get("streams") if isinstance(module_row.get("streams"), list) else [] if not str((owner or {}).get("name") or "").strip() and not str((form or {}).get("name") or "").strip(): return None return { "kind": "module", "presentation": module_row.get("display_name") or module_row.get("qualified_name") or module.get("name"), **({"owner": owner} if owner else {}), **({"form": form} if form else {}), "module": module, "streams": [ { "stream_index": stream.get("stream_index"), "module_ref": stream.get("module_ref"), "text_sha1": stream.get("text_sha1"), "write_plan_target": stream.get("write_plan_target"), } for stream in streams[:5] if isinstance(stream, dict) ], "source": {"method": SAVED_STATE_MODULES_SEARCH_METHOD, "status": "resolved_by_file"}, } return None def public_saved_state_change_context(context: dict[str, Any] | None) -> dict[str, Any] | None: if not isinstance(context, dict): return None owner = context.get("owner") if isinstance(context.get("owner"), dict) else {} form = context.get("form") if isinstance(context.get("form"), dict) else {} module = context.get("module") if isinstance(context.get("module"), dict) else {} owner_public = { key: value for key, value in owner.items() if key in {"status", "kind", "name", "synonym", "source"} and value is not None } owner_ref = object_selector_ref(owner_public.get("kind"), owner_public.get("name")) if owner_ref: owner_public["ref"] = owner_ref form_public = { key: value for key, value in form.items() if key in {"name", "synonym", "source"} and value is not None } module_public = { key: value for key, value in module.items() if key in {"kind", "name"} and value is not None } presentation = str(context.get("presentation") or "").strip() selector = { **({"ref": owner_ref} if owner_ref else {}), **({"form": form_public.get("name")} if form_public.get("name") else {}), **({"module": module_public.get("name")} if module_public.get("name") else {}), **({"qualified_name": presentation} if presentation else {}), } source = context.get("source") if isinstance(context.get("source"), dict) else {} return { "kind": context.get("kind") or ("module" if module_public else ("form" if form_public else "object")), **({"presentation": presentation} if presentation else {}), **({"owner": owner_public} if owner_public else {}), **({"form": form_public} if form_public else {}), **({"module": module_public} if module_public else {}), **({"selector": selector} if selector else {}), **( { "resolution": { key: value for key, value in source.items() if key in {"method", "status"} and value is not None } } if source else {} ), } def public_saved_state_freshness(freshness: dict[str, Any] | None) -> dict[str, Any]: raw = freshness if isinstance(freshness, dict) else {} raw_status = str(raw.get("status") or "").strip() status = raw_status.replace("live_sql", "live") if raw_status else "unknown" verified = bool(raw.get("verified") or raw.get("verified_against_sql") or raw_status == "live_sql_verified") return { "source": "saved_state", "status": status, "verified": verified, } def public_saved_state_layer_counts(counts: dict[str, Any] | None) -> dict[str, Any]: raw = counts if isinstance(counts, dict) else {} return { "saved_records": raw.get("saved_rows", 0), "saved_objects": raw.get("saved_files", 0), "active_records": raw.get("active_rows", 0), "changed": raw.get("changed_files", 0), "unchanged": raw.get("unchanged_files", 0), "saved_only": raw.get("saved_only_files", 0), "changed_parts": raw.get("changed_rows", 0), } def public_saved_state_change_item(item: dict[str, Any]) -> dict[str, Any]: table = str(item.get("table") or "") layer = "base_saved_state" if table == "ConfigSave" else "extension_saved_state" context = public_saved_state_change_context(item.get("context")) return { "layer": layer, "status": item.get("status"), "changed_parts": item.get("changed_parts"), **({"context": context} if context else {}), **({"presentation": context.get("presentation")} if context and context.get("presentation") else {}), **({"selector": context.get("selector")} if context and context.get("selector") else {}), **( { "diagnostics": { "status": "context_unresolved", "message": "Saved-state change is counted, but its 1C object/form/module name was not resolved within context_limit.", } } if not context else {} ), } def public_saved_state_change_group(group: dict[str, Any], index: int) -> dict[str, Any]: unresolved = str(group.get("kind") or "") == "file" context = None if unresolved else public_saved_state_change_context(group) presentation = str((context or {}).get("presentation") or "").strip() public_files = [ { "layer": "base_saved_state" if str(item.get("table") or "") == "ConfigSave" else "extension_saved_state", "status": item.get("status"), "changed_parts": item.get("changed_parts"), } for item in group.get("files") or [] if isinstance(item, dict) ] raw_counts = group.get("counts") if isinstance(group.get("counts"), dict) else {} public_counts = { "changes": raw_counts.get("files", 0), "changed": raw_counts.get("changed_files", 0), "saved_only": raw_counts.get("saved_only_files", 0), "unchanged": raw_counts.get("unchanged_files", 0), } return { "key": f"unresolved_change:{index}" if unresolved else f"{str(group.get('kind') or 'change')}:{presentation or index}", "kind": "unresolved" if unresolved else group.get("kind"), **({"presentation": presentation} if presentation else {}), **({"owner": context.get("owner")} if context and context.get("owner") else {}), **({"form": context.get("form")} if context and context.get("form") else {}), **({"module": context.get("module")} if context and context.get("module") else {}), **({"selector": context.get("selector")} if context and context.get("selector") else {}), "counts": public_counts, "changes": public_files, **( { "diagnostics": { "status": "context_unresolved", "message": "The saved-state group has no resolved 1C object/form/module name.", } } if unresolved else {} ), } def saved_state_change_family_key(file_name: Any) -> str: return re.sub(r"\.\d+$", "", str(file_name or "").strip()) def enrich_saved_state_change_context_families(files: list[dict[str, Any]]) -> None: named_forms: dict[tuple[str, str], dict[str, Any]] = {} named_owners: dict[tuple[str, str], dict[str, Any]] = {} for item in files: context = item.get("context") if isinstance(item.get("context"), dict) else {} key = (str(item.get("table") or ""), saved_state_change_family_key(item.get("file_name"))) form = context.get("form") if isinstance(context.get("form"), dict) else {} owner = context.get("owner") if isinstance(context.get("owner"), dict) else {} if key[1] and form.get("name"): named_forms[key] = form if key[1] and owner.get("name"): named_owners[key] = owner for item in files: context = item.get("context") if isinstance(item.get("context"), dict) else None if not context: continue key = (str(item.get("table") or ""), saved_state_change_family_key(item.get("file_name"))) form = context.get("form") if isinstance(context.get("form"), dict) else {} owner = context.get("owner") if isinstance(context.get("owner"), dict) else {} module = context.get("module") if isinstance(context.get("module"), dict) else {} context_changed = False if not form.get("name") and key in named_forms: form = named_forms[key] context["form"] = form context_changed = True if not owner.get("name") and key in named_owners: owner = named_owners[key] context["owner"] = owner context_changed = True if context_changed and module and (owner.get("name") or form.get("name")): qualified_name = public_code_qualified_name(owner=owner, form=form, module=module) if qualified_name: context["presentation"] = qualified_name elif context_changed and context.get("kind") == "form" and (owner.get("name") or form.get("name")): context["presentation"] = ".".join( part for part in [str(owner.get("name") or ""), str(form.get("name") or ""), "Форма"] if part ) def saved_state_change_group_identity(item: dict[str, Any]) -> tuple[str, dict[str, Any]]: context = item.get("context") if isinstance(item.get("context"), dict) else {} table = str(item.get("table") or "") file_name = str(item.get("file_name") or "") kind = str(context.get("kind") or "file") presentation = str(context.get("presentation") or file_name or table) owner = context.get("owner") if isinstance(context.get("owner"), dict) else None form = context.get("form") if isinstance(context.get("form"), dict) else None module = context.get("module") if isinstance(context.get("module"), dict) else None identity_parts = [kind] if owner: identity_parts.append(str(owner.get("guid") or owner.get("name") or owner.get("type") or "")) if form: identity_parts.append(str(form.get("guid") or form.get("name") or "")) if module: identity_parts.append(str(module.get("kind") or module.get("name") or "")) identity_parts.append(presentation) if kind == "file": identity_parts.extend([table, file_name]) key = "|".join(part for part in identity_parts if part) group = { "key": key, "kind": kind, "presentation": presentation, **({"owner": owner} if owner else {}), **({"form": form} if form else {}), **({"module": module} if module else {}), "counts": {"files": 0, "changed_files": 0, "saved_only_files": 0, "unchanged_files": 0}, "files": [], } return key, group def append_unique_selector(target: list[dict[str, Any]], selector: Any) -> None: if not isinstance(selector, dict) or not selector: return if selector not in target: target.append(selector) def saved_state_change_item_action_selectors(item: dict[str, Any]) -> dict[str, Any]: selectors: dict[str, Any] = {"diff": []} append_unique_selector(selectors["diff"], item.get("diff_selector")) context = item.get("context") if isinstance(item.get("context"), dict) else {} streams = context.get("streams") if isinstance(context.get("streams"), list) else [] module_refs: list[str] = [] write_plan_targets: list[dict[str, Any]] = [] for stream in streams: if not isinstance(stream, dict): continue module_ref = str(stream.get("module_ref") or "").strip() if module_ref and module_ref not in module_refs: module_refs.append(module_ref) append_unique_selector(write_plan_targets, stream.get("write_plan_target")) if module_refs: selectors["module_refs"] = module_refs if write_plan_targets: selectors["write_plan_targets"] = write_plan_targets return selectors def saved_state_change_group_next_actions(base_id: str, group: dict[str, Any]) -> list[dict[str, Any]]: selectors = group.get("selectors") if isinstance(group.get("selectors"), dict) else {} actions: list[dict[str, Any]] = [] for selector in (selectors.get("diff") or [])[:10]: if isinstance(selector, dict): actions.append({"kind": "inspect_diff", "method": SAVED_STATE_DIFF_METHOD, "payload": selector}) for module_ref in (selectors.get("module_refs") or [])[:10]: module_ref_value = str(module_ref or "").strip() if module_ref_value: actions.append( { "kind": "read_module", "method": "code.read", "payload": {"base_id": base_id, "module_ref": module_ref_value, "state": "working"}, } ) for target in (selectors.get("write_plan_targets") or [])[:10]: if isinstance(target, dict) and target: actions.append( { "kind": "preflight_write", "method": METADATA_WRITE_PREFLIGHT_METHOD, "payload": {"base_id": base_id, "target": target, "resolve_origin": False}, } ) return actions def saved_state_change_group_action_summary(actions: list[dict[str, Any]]) -> dict[str, Any]: by_kind: dict[str, int] = {} methods: dict[str, int] = {} for action in actions: if not isinstance(action, dict): continue kind = str(action.get("kind") or "") method = str(action.get("method") or "") if kind: by_kind[kind] = by_kind.get(kind, 0) + 1 if method: methods[method] = methods.get(method, 0) + 1 return { "total": len([action for action in actions if isinstance(action, dict)]), "by_kind": by_kind, "methods": methods, } def saved_state_change_group_recommended_action(actions: list[dict[str, Any]]) -> dict[str, Any] | None: priority = {"inspect_diff": 0, "read_module": 1, "preflight_write": 2} candidates = [action for action in actions if isinstance(action, dict) and str(action.get("kind") or "") in priority] if not candidates: return None return sorted(candidates, key=lambda action: priority[str(action.get("kind") or "")])[0] def saved_state_changes_action_summary(groups: list[dict[str, Any]]) -> dict[str, Any]: actions: list[dict[str, Any]] = [] for group in groups: if isinstance(group, dict) and isinstance(group.get("next_actions"), list): actions.extend(action for action in group.get("next_actions") or [] if isinstance(action, dict)) return saved_state_change_group_action_summary(actions) def saved_state_changes_recommended_action(groups: list[dict[str, Any]]) -> dict[str, Any] | None: group_priority = {"changed": 0, "saved_only": 1, "unchanged": 2} candidates: list[tuple[int, str, dict[str, Any], dict[str, Any]]] = [] for group in groups: if not isinstance(group, dict) or not isinstance(group.get("recommended_next_action"), dict): continue counts = group.get("counts") if isinstance(group.get("counts"), dict) else {} if int(counts.get("changed_files") or 0) > 0: status = "changed" elif int(counts.get("saved_only_files") or 0) > 0: status = "saved_only" else: status = "unchanged" candidates.append((group_priority.get(status, 99), str(group.get("presentation") or ""), group, group["recommended_next_action"])) if not candidates: return None _priority, _presentation, group, action = sorted(candidates, key=lambda item: (item[0], item[1]))[0] return { "group_key": group.get("key"), "group_kind": group.get("kind"), "group_presentation": group.get("presentation"), "action": action, } def build_saved_state_change_groups(base_id: str, files: list[dict[str, Any]]) -> list[dict[str, Any]]: groups_by_key: dict[str, dict[str, Any]] = {} for item in files: key, group_template = saved_state_change_group_identity(item) group = groups_by_key.setdefault(key, group_template) group_selectors = group.setdefault("selectors", {"diff": []}) status = str(item.get("status") or "") group["counts"]["files"] += 1 if status == "changed": group["counts"]["changed_files"] += 1 elif status == "saved_only": group["counts"]["saved_only_files"] += 1 elif status == "unchanged": group["counts"]["unchanged_files"] += 1 group["files"].append( { "table": item.get("table"), "file_name": item.get("file_name"), "status": item.get("status"), "changed_parts": item.get("changed_parts"), "diff_selector": item.get("diff_selector"), } ) item_selectors = saved_state_change_item_action_selectors(item) for selector in item_selectors.get("diff") or []: append_unique_selector(group_selectors.setdefault("diff", []), selector) for module_ref in item_selectors.get("module_refs") or []: module_refs = group_selectors.setdefault("module_refs", []) if module_ref not in module_refs: module_refs.append(module_ref) for write_plan_target in item_selectors.get("write_plan_targets") or []: append_unique_selector(group_selectors.setdefault("write_plan_targets", []), write_plan_target) groups = sorted(groups_by_key.values(), key=lambda group: (str(group.get("kind") or ""), str(group.get("presentation") or ""), str(group.get("key") or ""))) for group in groups: next_actions = saved_state_change_group_next_actions(base_id, group) group["next_actions"] = next_actions group["action_summary"] = saved_state_change_group_action_summary(next_actions) recommended_action = saved_state_change_group_recommended_action(next_actions) if recommended_action: group["recommended_next_action"] = recommended_action return groups def metadata_saved_state_changes_list(payload: dict[str, Any]) -> dict[str, Any]: method = SAVED_STATE_CHANGES_LIST_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error limit, limit_error = parse_int_argument(payload, "limit", method=method, default=500, minimum=1, maximum=5000) if limit_error: return limit_error include_unchanged, include_unchanged_error = strict_bool_argument(payload, "include_unchanged", method=method, default=False) if include_unchanged_error: return include_unchanged_error include_context, include_context_error = strict_bool_argument(payload, "include_context", method=method, default=False) if include_context_error: return include_context_error group_by_context, group_by_context_error = strict_bool_argument(payload, "group_by_context", method=method, default=False) if group_by_context_error: return group_by_context_error include_storage, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error include_storage = bool(include_storage) context_limit, context_limit_error = parse_int_argument(payload, "context_limit", method=method, default=50, minimum=0, maximum=500) if context_limit_error: return context_limit_error layer = str(payload.get("layer") or "").strip() if layer and layer not in SAVED_STATE_TABLE_BY_LAYER: return invalid_argument(method, "layer", "layer must be base_saved_state or extension_saved_state.", allowed_values=sorted(SAVED_STATE_TABLE_BY_LAYER)) requested_table = str(payload.get("table") or payload.get("target_table") or "").strip() if layer and requested_table and requested_table != SAVED_STATE_TABLE_BY_LAYER[layer]: return invalid_argument(method, "layer", "layer conflicts with table.") table = requested_table or SAVED_STATE_TABLE_BY_LAYER.get(layer, "") tables = [table] if table else ["ConfigSave", "ConfigCASSave"] if any(item not in SAVED_STATE_SOURCE_BY_TARGET for item in tables): return invalid_argument(method, "table", "table must be ConfigSave or ConfigCASSave.", allowed_values=sorted(SAVED_STATE_SOURCE_BY_TARGET)) prefix = str(payload.get("prefix") or "").strip() if prefix and Path(prefix).name != prefix: return invalid_argument(method, "prefix", "prefix must be a safe FileName prefix.") files: list[dict[str, Any]] = [] table_summaries: list[dict[str, Any]] = [] errors: list[dict[str, Any]] = [] counts = { "tables": 0, "files": 0, "changed_files": 0, "saved_only_files": 0, "unchanged_files": 0, "error_tables": 0, } for current_table in tables: status_result = metadata_saved_state_status( { "base_id": base_id, "table": current_table, "prefix": prefix, "limit": int(limit or 500), "include_files": True, "include_unchanged": bool(include_unchanged), "include_storage": True, "timeout_seconds": int(timeout_seconds or 30), } ) if status_result.get("schema") != "onec_saved_state_status.v1": counts["error_tables"] += 1 errors.append({"table": current_table, "status": status_result.get("status"), "error": status_result.get("error"), "diagnostics": status_result.get("diagnostics")}) continue counts["tables"] += 1 table_counts = status_result.get("counts") if isinstance(status_result.get("counts"), dict) else {} table_summaries.append( { "table": current_table, "status": status_result.get("status"), "counts": table_counts, "freshness": status_result.get("freshness"), } ) for item in status_result.get("files") or []: if not isinstance(item, dict): continue item_status = str(item.get("status") or "") if item_status == "unchanged" and not include_unchanged: continue counts["files"] += 1 if item_status == "changed": counts["changed_files"] += 1 elif item_status == "saved_only": counts["saved_only_files"] += 1 elif item_status == "unchanged": counts["unchanged_files"] += 1 files.append({"table": current_table, **item}) context_enrichment = bool(include_context or group_by_context or not include_storage) if context_enrichment and files: enriched = 0 for item in files: if enriched >= int(context_limit or 0): break table_name = str(item.get("table") or "") file_name = str(item.get("file_name") or "") if table_name not in SAVED_STATE_SOURCE_BY_TARGET or not file_name: continue context = metadata_saved_state_change_context( base_id=base_id, table=table_name, file_name=file_name, timeout_seconds=int(timeout_seconds or 30), ) if context: item["context"] = context enriched += 1 enrich_saved_state_change_context_families(files) files.sort(key=lambda item: (str(item.get("table") or ""), str(item.get("status") or ""), str(item.get("file_name") or ""))) groups = build_saved_state_change_groups(base_id, files) if group_by_context else [] if group_by_context: counts["groups"] = len(groups) status = "error" if counts["error_tables"] and not counts["tables"] else ("changed" if counts["changed_files"] or counts["saved_only_files"] else ("unchanged" if counts["unchanged_files"] else "empty")) result = { "schema": "onec_saved_state_changes_list.v1", "method": method, "status": status, "base_id": base_id, "query": { "tables": tables, "prefix": prefix or None, "limit": int(limit or 500), "include_unchanged": bool(include_unchanged), "include_context": bool(include_context), "group_by_context": bool(group_by_context), "context_enrichment": context_enrichment, "context_limit": int(context_limit or 0), "include_storage": include_storage, }, "tables": table_summaries, "counts": counts, "files": files, "errors": errors, "freshness": { "source": "live_sql", "status": "live_sql_verified" if not errors else "partial_live_sql_verified", "verified_against_sql": True, }, } if group_by_context: result["groups"] = groups result["action_summary"] = saved_state_changes_action_summary(groups) recommended_action = saved_state_changes_recommended_action(groups) if recommended_action: result["recommended_next_action"] = recommended_action if include_storage: return result public_layers = [ { "layer": "base_saved_state" if str(summary.get("table") or "") == "ConfigSave" else "extension_saved_state", "status": summary.get("status"), "counts": public_saved_state_layer_counts(summary.get("counts")), "freshness": public_saved_state_freshness(summary.get("freshness")), } for summary in table_summaries ] public_files = [public_saved_state_change_item(item) for item in files] public_errors = [ { "layer": "base_saved_state" if str(error.get("table") or "") == "ConfigSave" else "extension_saved_state", "status": error.get("status"), "error": error.get("error"), "diagnostics": { "status": "layer_unavailable", "message": "Saved-state layer could not be inspected; storage diagnostics are hidden unless include_storage=true.", }, } for error in errors ] public_counts = { "layers": counts.get("tables", 0), "changes": counts.get("files", 0), "changed": counts.get("changed_files", 0), "saved_only": counts.get("saved_only_files", 0), "unchanged": counts.get("unchanged_files", 0), "error_layers": counts.get("error_tables", 0), **({"groups": counts.get("groups")} if counts.get("groups") is not None else {}), } public_result = { "schema": "onec_saved_state_changes_list.v1", "method": method, "status": status, "base_id": base_id, "source": {"kind": "saved_state"}, "query": { "layers": ["base_saved_state" if item == "ConfigSave" else "extension_saved_state" for item in tables], "limit": int(limit or 500), "include_unchanged": bool(include_unchanged), "include_context": True, "group_by_context": bool(group_by_context), "context_enrichment": True, "context_limit": int(context_limit or 0), "include_storage": False, }, "layers": public_layers, "counts": public_counts, "changes": public_files, "errors": public_errors, "freshness": public_saved_state_freshness(result["freshness"]), "diagnostics": { "note": "SQL coordinates, hashes, and low-level actions are hidden unless include_storage=true.", "resolved_contexts": len([item for item in public_files if item.get("context")]), "unresolved_contexts": len([item for item in public_files if not item.get("context")]), }, } if group_by_context: public_groups = [public_saved_state_change_group(group, index) for index, group in enumerate(groups, start=1)] public_result["groups"] = public_groups public_result["counts"] = {**public_counts, "groups": len(public_groups)} return public_result def storage_apply_backup_dir() -> Path: return Path(os.environ.get("ONEC_ADAPTER_BACKUP_DIR") or "/data/adapter-apply-backups") def resolve_storage_apply_backup_path(backup_id: str | None = None, backup_path: str | None = None) -> Path | dict[str, Any]: method = "storage.saved_state.rollback" root = storage_apply_backup_dir().resolve() if backup_path: path = Path(backup_path).resolve() try: path.relative_to(root) except ValueError: return invalid_argument(method, "backup_path", "backup_path must be inside the adapter backup directory.") if not path.is_file(): return invalid_argument(method, "backup_path", "backup_path was not found.") return path if not backup_id: return invalid_argument(method, "backup_id", "Pass backup_id or backup_path.") if not re.fullmatch(r"[0-9a-f]{32}", backup_id): return invalid_argument(method, "backup_id", "backup_id must be a 32-character lowercase hex id.") matches = sorted(root.glob(f"*{backup_id}.json")) if root.is_dir() else [] if not matches: return invalid_argument(method, "backup_id", "Backup id was not found.") if len(matches) > 1: return invalid_argument(method, "backup_id", "Backup id is ambiguous; pass backup_path.") return matches[0] def write_storage_apply_backup( *, base_id: str, config: dict[str, str], table: str, file_name: str, original: bytes, replacement: bytes, proposal: dict[str, Any], ) -> dict[str, Any]: backup_id = uuid.uuid4().hex created_at = datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z") backup_root = storage_apply_backup_dir() backup_root.mkdir(parents=True, exist_ok=True) path = backup_root / f"{created_at.replace(':', '').replace('-', '')}-{backup_id}.json" evidence = { "schema": "onec_storage_apply_backup.v1", "backup_id": backup_id, "created_at_utc": created_at, "base_id": base_id, "source": { "kind": "live_sql", "server": config.get("server"), "database": config.get("database"), "table": table, "file_name": file_name, }, "original": {"sha1": hashlib.sha1(original).hexdigest(), "bytes": len(original), "payload_hex": original.hex()}, "replacement": {"sha1": hashlib.sha1(replacement).hexdigest(), "bytes": len(replacement)}, "proposal": { "schema": proposal.get("schema"), "method": proposal.get("method"), "status": proposal.get("status"), "source": proposal.get("source"), "original": proposal.get("original"), "encoded": {key: value for key, value in (proposal.get("encoded") or {}).items() if key != "payload_hex"}, "counts": proposal.get("counts"), "edits": proposal.get("edits") or [], }, "rollback": { "method": "storage.saved_state.apply_proposal", "payload": { "base_id": base_id, "allow_sql_saved_state_apply": True, "proposal": { "schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": {"table": table, "file_name": file_name}, "original": {"sha1": hashlib.sha1(replacement).hexdigest(), "bytes": len(replacement)}, "encoded": {"sha1": hashlib.sha1(original).hexdigest(), "bytes": len(original), "payload_hex": original.hex()}, }, }, }, } path.write_text(json.dumps(evidence, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") return {"backup_id": backup_id, "path": str(path), "sha1": evidence["original"]["sha1"], "bytes": len(original)} def semantic_verify_saved_state_apply( *, base_id: str, table: str, file_name: str, proposal: dict[str, Any], timeout_seconds: int, ) -> dict[str, Any] | None: if proposal.get("method") != FORM_ELEMENT_WRITE_METHOD or not isinstance(proposal.get("element"), dict): return None element = proposal.get("element") or {} selector: dict[str, Any] = {} if element.get("path"): selector["element_path"] = str(element.get("path")) elif element.get("name"): selector["element"] = str(element.get("name")) elif element.get("id"): selector["element_id"] = str(element.get("id")) else: return {"status": "skipped", "reason": "element_selector_missing"} decoded = metadata_form_decode( { "base_id": base_id, "table": table, "file_name": file_name, "include_storage": True, "include_parameters": True, "max_items": 5000, "timeout_seconds": timeout_seconds, } ) result: dict[str, Any] = { "schema": "onec_saved_state_semantic_verification.v1", "method": "metadata.form.decode", "status": "ok" if decoded.get("status") == "ok" else "error", "selector": selector, "source": decoded.get("source"), "checks": [], } if decoded.get("status") != "ok": result["diagnostics"] = decoded.get("diagnostics") return result profile = decoded.get("profile") if isinstance(decoded.get("profile"), dict) else {} items = filter_form_profile_write_targets(form_profile_write_targets(profile), selector) expected_section = str(element.get("section") or "") if expected_section: items = [item for item in items if str(item.get("_profile_section") or "") == expected_section] if len(items) != 1: result["status"] = "not_found" if not items else "ambiguous" result["counts"] = {"matches": len(items)} return result item = items[0] result["element"] = {key: item.get(key) for key in ("name", "id", "title", "path", "marker", "type_name", "_profile_section")} if "_profile_section" in result["element"]: result["element"]["section"] = result["element"].pop("_profile_section") semantic_edits = proposal.get("form_element_edits") or proposal.get("edits") or [] for edit in semantic_edits: if not isinstance(edit, dict): continue expected = edit.get("value") if "value" in edit else edit.get("new") actual = form_property_current_value(item, edit.get("property")) result["checks"].append( { "property": edit.get("property"), "expected": expected, "actual": actual, "ok": str(actual) == str(expected), } ) if result["checks"]: result["status"] = "ok" if all(check.get("ok") for check in result["checks"]) else "mismatch" else: result["status"] = "skipped" result["reason"] = "no_form_element_edits" return result def apply_storage_file_bytes_single_part( base_id: str, table: str, file_name: str, replacement: bytes, *, expected_sha1: str, proposal: dict[str, Any], timeout_seconds: int = 30, ) -> dict[str, Any]: conn, config, error = connect_live_sql(base_id, "storage.saved_state.apply_proposal", timeout_seconds=timeout_seconds) if error: return error started = time.time() try: cursor = conn.cursor(as_dict=True) cursor.execute( f"SELECT PartNo, BinaryData FROM dbo.[{table}] WITH (UPDLOCK, HOLDLOCK) WHERE FileName = %s ORDER BY PartNo", (file_name,), ) rows = cursor.fetchall() parts = [bytes(row.get("BinaryData")) for row in rows if isinstance(row.get("BinaryData"), (bytes, bytearray))] current = b"".join(parts) current_sha1 = hashlib.sha1(current).hexdigest() if current else "" if not rows or not current: conn.rollback() return { "schema": "onec_storage_saved_state_apply.v1", "status": "source_missing", "applied": False, "base_id": base_id, "source": {"kind": "live_sql", "database": config["database"], "table": table, "file_name": file_name}, "diagnostics": {"message": "FileName was not found in the requested saved-state storage table."}, } if expected_sha1 and current_sha1 != expected_sha1: conn.rollback() return { "schema": "onec_storage_saved_state_apply.v1", "status": "precondition_failed", "applied": False, "base_id": base_id, "source": {"kind": "live_sql", "database": config["database"], "table": table, "file_name": file_name}, "original": {"expected_sha1": expected_sha1, "actual_sha1": current_sha1, "bytes": len(current)}, "diagnostics": {"message": "Current saved-state payload SHA1 differs from proposal original.sha1."}, } if len(rows) != 1: conn.rollback() return { "schema": "onec_storage_saved_state_apply.v1", "status": "unsupported_part_layout", "applied": False, "base_id": base_id, "source": {"kind": "live_sql", "database": config["database"], "table": table, "file_name": file_name}, "original": {"sha1": current_sha1, "bytes": len(current), "parts": len(rows)}, "diagnostics": {"message": "Apply v1 only updates saved-state payloads stored as one SQL part. Multi-part replace needs a table-schema-aware writer."}, } backup = write_storage_apply_backup( base_id=base_id, config=config, table=table, file_name=file_name, original=current, replacement=replacement, proposal=proposal, ) part_no = rows[0].get("PartNo") cursor.execute( f"UPDATE dbo.[{table}] SET BinaryData = %s WHERE FileName = %s AND PartNo = %s", (replacement, file_name, part_no), ) if cursor.rowcount != 1: conn.rollback() return { "schema": "onec_storage_saved_state_apply.v1", "status": "error", "applied": False, "base_id": base_id, "source": {"kind": "live_sql", "database": config["database"], "table": table, "file_name": file_name}, "backup": backup, "diagnostics": {"message": f"Expected to update exactly one row, updated {cursor.rowcount}."}, } conn.commit() except Exception as exc: try: conn.rollback() except Exception: pass return { "schema": "onec_storage_saved_state_apply.v1", "status": "error", "applied": False, "base_id": base_id, "source": {"kind": "live_sql", "database": (config or {}).get("database"), "table": table, "file_name": file_name}, "diagnostics": {"message": str(exc)}, } finally: try: conn.close() except Exception: pass readback, _read_config, read_error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=timeout_seconds) readback_sha1 = hashlib.sha1(readback).hexdigest() if readback else None encoded_sha1 = hashlib.sha1(replacement).hexdigest() verified = bool(readback_sha1 == encoded_sha1) result = { "schema": "onec_storage_saved_state_apply.v1", "status": "applied" if verified else "readback_mismatch", "applied": verified, "base_id": base_id, "source": {"kind": "live_sql", "database": config["database"], "table": table, "file_name": file_name}, "backup": backup, "original": {"sha1": expected_sha1, "bytes": len(current), "parts": 1}, "encoded": {"sha1": encoded_sha1, "bytes": len(replacement)}, "readback": {"sha1": readback_sha1, "bytes": len(readback) if readback else None, "verified": verified, "error": read_error}, "duration_ms": int((time.time() - started) * 1000), } semantic = semantic_verify_saved_state_apply( base_id=base_id, table=table, file_name=file_name, proposal=proposal, timeout_seconds=timeout_seconds, ) if semantic is not None: result["semantic_verification"] = semantic if verified and semantic.get("status") not in {"ok", "skipped"}: result["status"] = "semantic_verification_failed" result["applied"] = False result["cache_invalidation"] = invalidate_adapter_caches_after_saved_state_change( base_id, reason="saved_state_payload_apply", ) return result def read_storage_files_bytes( base_id: str, table: str, file_names: list[str], *, timeout_seconds: int = 30, ) -> tuple[dict[str, bytes] | None, dict[str, str] | None, dict[str, Any] | None]: safe_names = [name for name in file_names if name and Path(name).name == name] if not safe_names: return {}, None, None conn, config, error = connect_live_sql(base_id, "storage.files.get", timeout_seconds=timeout_seconds) if error: return None, config, error grouped: dict[str, list[bytes]] = {name: [] for name in safe_names} try: with conn: with conn.cursor(as_dict=True) as cursor: for start in range(0, len(safe_names), 500): chunk = safe_names[start : start + 500] placeholders = ",".join(["%s"] * len(chunk)) cursor.execute( f"SELECT FileName, BinaryData FROM dbo.[{table}] WHERE FileName IN ({placeholders}) ORDER BY FileName, PartNo", tuple(chunk), ) for row in cursor.fetchall(): file_name = str(row.get("FileName") or "") value = row.get("BinaryData") if file_name in grouped and isinstance(value, (bytes, bytearray)): grouped[file_name].append(bytes(value)) except Exception as exc: return None, config, { "schema": "onec_adapter_source_error.v1", "method": "storage.files.get", "status": "error", "base_id": base_id, "source": {"kind": "live_sql", "database": (config or {}).get("database"), "table": table}, "diagnostics": {"message": str(exc)}, } return {name: b"".join(parts) for name, parts in grouped.items() if parts}, config, None def live_config_file_name_page_after( base_id: str, last_file_name: str = "", *, table: str = "Config", page_size: int = 2000, timeout_seconds: int = 60, ) -> list[str]: conn, _, error = connect_live_sql(base_id, "storage.files.page", timeout_seconds=timeout_seconds) if error: return [] rows: list[str] = [] try: with conn: with conn.cursor(as_dict=True) as cursor: storage_table = str(table or "Config") if storage_table not in STORAGE_TABLES: return [] cursor.execute( f""" SELECT TOP ({max(1, min(int(page_size), 10000))}) FileName FROM dbo.[{storage_table}] WHERE FileName > %s GROUP BY FileName ORDER BY FileName """, (last_file_name,), ) rows = [ str(row.get("FileName") or "").lower() for row in cursor.fetchall() if is_guid_text(str(row.get("FileName") or "")) ] except Exception: return [] return rows def storage_file_get(payload: dict[str, Any]) -> dict[str, Any]: base_id_or_error = require_base_id(payload, "storage.file.get") if isinstance(base_id_or_error, dict): return base_id_or_error table_or_error = storage_table(payload, "storage.file.get") if isinstance(table_or_error, dict): return table_or_error include_payload, include_payload_error = strict_bool_argument(payload, "include_payload", method="storage.file.get", default=False) if include_payload_error: return include_payload_error if "file_name" in payload and not isinstance(payload.get("file_name"), str): return invalid_argument("storage.file.get", "file_name", "file_name must be a JSON string.") file_name = str(payload.get("file_name") or "") if not file_name or Path(file_name).name != file_name: return { "schema": "onec_adapter_request_error.v1", "method": "storage.file.get", "status": "invalid_argument", "error": "invalid_argument", "argument": "file_name", "diagnostics": {"message": "Pass a single safe FileName value from the live SQL storage table."}, } timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method="storage.file.get", default=30, minimum=1) if timeout_error: return timeout_error diagnostic_error = require_diagnostic_mode(payload, "storage.file.get") if diagnostic_error: return diagnostic_error base_id = base_id_or_error table = table_or_error data, config, error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=int(timeout_seconds or 30)) if error: return error result: dict[str, Any] = { "schema": "onec_storage_file.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "database": config["database"], "table": table, "file_name": file_name}, "file": {"file_name": file_name, "bytes": len(data), "sha1": hashlib.sha1(data).hexdigest()}, } if include_payload: result["file"]["payload_hex"] = data.hex() return result def storage_saved_state_apply_proposal(payload: dict[str, Any]) -> dict[str, Any]: method = "storage.saved_state.apply_proposal" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error repository_error = repository_apply_gate(payload, method, "apply") if repository_error: return repository_error allow_apply, allow_apply_error = strict_bool_argument(payload, "allow_sql_saved_state_apply", method=method, default=False) if allow_apply_error: return allow_apply_error if not allow_apply: return invalid_argument( method, "allow_sql_saved_state_apply", "Saved-state SQL apply is opt-in; pass allow_sql_saved_state_apply=true after reviewing the proposal and backup policy.", ) timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error proposal = payload.get("proposal") if not isinstance(proposal, dict): return invalid_argument(method, "proposal", "proposal must be a JSON object returned by changes.propose or metadata.form.element.write.") source = proposal.get("source") if isinstance(proposal.get("source"), dict) else {} encoded = proposal.get("encoded") if isinstance(proposal.get("encoded"), dict) else {} original = proposal.get("original") if isinstance(proposal.get("original"), dict) else {} table = str(source.get("table") or payload.get("table") or "") file_name = str(source.get("file_name") or payload.get("file_name") or "") if table not in FORM_ELEMENT_SAVED_STATE_TABLES: return invalid_argument(method, "proposal.source.table", "Only saved-state tables may be applied.", allowed_values=sorted(FORM_ELEMENT_SAVED_STATE_TABLES)) if not file_name or Path(file_name).name != file_name: return invalid_argument(method, "proposal.source.file_name", "Proposal source.file_name must be a safe storage FileName.") for edit in proposal.get("edits") or []: if not isinstance(edit, dict): continue if str(edit.get("path") or "") == "2" and str(edit.get("mode") or "") != "path_preserve_format": return { "schema": "onec_storage_saved_state_apply.v1", "status": "blocked", "applied": False, "base_id": base_id_or_error, "error": "unsafe_form_module_payload_write", "source": {"table": table, "file_name": file_name}, "diagnostics": { "message": "Saved-state form module payload edits at path 2 must use path_preserve_format; canonical form payload serialization is blocked.", }, } payload_hex = encoded.get("payload_hex") if not isinstance(payload_hex, str) or not payload_hex: return invalid_argument(method, "proposal.encoded.payload_hex", "Proposal must include encoded.payload_hex. Re-run changes.propose with include_payload=true.") try: replacement = bytes.fromhex(payload_hex) except ValueError: return invalid_argument(method, "proposal.encoded.payload_hex", "encoded.payload_hex is not valid hex.") expected_encoded_sha1 = str(encoded.get("sha1") or "").lower() actual_encoded_sha1 = hashlib.sha1(replacement).hexdigest() if expected_encoded_sha1 and expected_encoded_sha1 != actual_encoded_sha1: return { "schema": "onec_storage_saved_state_apply.v1", "status": "precondition_failed", "applied": False, "base_id": base_id_or_error, "source": {"kind": "live_sql", "table": table, "file_name": file_name}, "encoded": {"expected_sha1": expected_encoded_sha1, "actual_sha1": actual_encoded_sha1, "bytes": len(replacement)}, "diagnostics": {"message": "encoded.payload_hex SHA1 differs from proposal encoded.sha1."}, } allow_unsafe_form_payload_apply, unsafe_apply_error = strict_bool_argument(payload, "allow_unsafe_form_payload_apply", method=method, default=False) if unsafe_apply_error: return unsafe_apply_error if proposal.get("method") == FORM_ELEMENT_WRITE_METHOD and not allow_unsafe_form_payload_apply: original_bytes = original.get("bytes") encoded_bytes = encoded.get("bytes") encoded_validation = proposal.get("validation") if isinstance(proposal.get("validation"), dict) else {} if encoded_validation.get("mode") == "path" and original_bytes != encoded_bytes: return { "schema": "onec_storage_saved_state_apply.v1", "status": "unsafe_form_payload_rewrite", "applied": False, "base_id": base_id_or_error, "source": {"kind": "live_sql", "table": table, "file_name": file_name}, "original": {"sha1": original.get("sha1"), "bytes": original_bytes}, "encoded": {"sha1": actual_encoded_sha1, "bytes": encoded_bytes}, "diagnostics": { "message": "Saved-state form write proposal rewrites the serialized form payload length. This can corrupt 1C form streams; apply is blocked until the codec preserves the original byte layout.", "override": "Pass allow_unsafe_form_payload_apply=true only for disposable test bases.", }, } expected_original_sha1 = str(original.get("sha1") or payload.get("expected_sha1") or "").lower() if not expected_original_sha1: return invalid_argument(method, "proposal.original.sha1", "Proposal must include original.sha1 for the write precondition.") return apply_storage_file_bytes_single_part( base_id_or_error, table, file_name, replacement, expected_sha1=expected_original_sha1, proposal=proposal, timeout_seconds=int(timeout_seconds or 30), ) def repository_write_context(payload: dict[str, Any]) -> dict[str, Any]: """Carry resolved repository/support coordination through nested writes.""" return { key: payload[key] for key in ("lock_session_id", "repository_object", "support_object_guid", "extension_guid", "owner_resolution") if payload.get(key) is not None } REPOSITORY_WRITE_CONTEXT_KEYS = ("lock_session_id", "repository_object", "layer_id") def normalize_repository_write_context(payload: dict[str, Any], method: str) -> tuple[dict[str, Any], dict[str, Any] | None]: """Accept the confirmation write_context directly or under repository_lock. Top-level fields remain the canonical transport. Nested forms exist so a client can forward repository.lock.confirm.write_context verbatim. """ normalized = dict(payload) for container_name in ("write_context", "repository_lock"): context = payload.get(container_name) if context is None: continue if not isinstance(context, dict): return payload, { "status": "blocked", "error": "invalid_repository_lock_context", "argument": container_name, "message": f"{container_name} must be an object returned by repository.lock.confirm.write_context.", } for key in REPOSITORY_WRITE_CONTEXT_KEYS: nested_value = context.get(key) if nested_value is None: continue current_value = normalized.get(key) if current_value is not None and str(current_value).strip() != str(nested_value).strip(): return payload, { "status": "blocked", "error": "repository_lock_context_conflict", "argument": key, "message": f"Top-level {key} conflicts with {container_name}.{key}.", "top_level": current_value, "nested": nested_value, } normalized[key] = nested_value return normalized, None def storage_saved_state_rollback(payload: dict[str, Any]) -> dict[str, Any]: method = "storage.saved_state.rollback" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error repository_error = repository_apply_gate(payload, method, "apply_and_rollback") if repository_error: return repository_error allow_rollback, allow_error = strict_bool_argument(payload, "allow_sql_saved_state_rollback", method=method, default=False) if allow_error: return allow_error if not allow_rollback: return invalid_argument(method, "allow_sql_saved_state_rollback", "Saved-state rollback is opt-in; pass allow_sql_saved_state_rollback=true.") timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error backup_id = str(payload.get("backup_id") or "").strip() backup_path = str(payload.get("backup_path") or "").strip() resolved = resolve_storage_apply_backup_path(backup_id or None, backup_path or None) if isinstance(resolved, dict): return resolved try: evidence = json.loads(resolved.read_text(encoding="utf-8-sig")) except Exception as exc: return { "schema": "onec_storage_saved_state_rollback.v1", "status": "error", "applied": False, "base_id": base_id_or_error, "backup": {"path": str(resolved)}, "diagnostics": {"message": f"Could not read backup evidence: {exc}"}, } rollback = evidence.get("rollback") if isinstance(evidence.get("rollback"), dict) else {} rollback_payload = rollback.get("payload") if isinstance(rollback.get("payload"), dict) else None if not rollback_payload: return { "schema": "onec_storage_saved_state_rollback.v1", "status": "invalid_backup", "applied": False, "base_id": base_id_or_error, "backup": {"path": str(resolved), "backup_id": evidence.get("backup_id")}, "diagnostics": {"message": "Backup evidence does not contain rollback.payload."}, } rollback_payload = dict(rollback_payload) rollback_payload["base_id"] = base_id_or_error rollback_payload["allow_sql_saved_state_apply"] = True rollback_payload["timeout_seconds"] = int(timeout_seconds or 30) rollback_payload.update(repository_write_context(payload)) apply_result = storage_saved_state_apply_proposal(rollback_payload) return { "schema": "onec_storage_saved_state_rollback.v1", "status": apply_result.get("status"), "applied": bool(apply_result.get("applied")), "base_id": base_id_or_error, "backup": { "backup_id": evidence.get("backup_id"), "path": str(resolved), "source": evidence.get("source"), "original": {key: value for key, value in (evidence.get("original") or {}).items() if key != "payload_hex"}, "replacement": evidence.get("replacement"), }, "apply_result": apply_result, } def storage_saved_state_backups_list(payload: dict[str, Any]) -> dict[str, Any]: method = "storage.saved_state.backups.list" base_id = str(payload.get("base_id") or "").strip() table_filter = str(payload.get("table") or "").strip() file_filter = str(payload.get("file_name") or "").strip() limit, limit_error = parse_int_argument(payload, "limit", method=method, default=50, minimum=1, maximum=500) if limit_error: return limit_error root = storage_apply_backup_dir() backups: list[dict[str, Any]] = [] if root.exists(): for path in sorted(root.glob("*.json"), key=lambda item: item.stat().st_mtime, reverse=True): if len(backups) >= int(limit or 50): break try: data = json.loads(path.read_text(encoding="utf-8")) except Exception: continue source = data.get("source") if isinstance(data.get("source"), dict) else {} if base_id and str(data.get("base_id") or "") != base_id: continue if table_filter and str(source.get("table") or "") != table_filter: continue if file_filter and str(source.get("file_name") or "") != file_filter: continue backups.append( { "backup_id": data.get("backup_id"), "created_at_utc": data.get("created_at_utc"), "base_id": data.get("base_id"), "source": {key: source.get(key) for key in ("database", "table", "file_name") if source.get(key) is not None}, "original": {key: (data.get("original") or {}).get(key) for key in ("sha1", "bytes")}, "replacement": {key: (data.get("replacement") or {}).get(key) for key in ("sha1", "bytes")}, "path": str(path), } ) return { "schema": "onec_saved_state_backups.v1", "method": method, "status": "ok", "backup_dir": str(root), "backups": backups, "counts": {"returned": len(backups)}, } def metadata_dbnames_summary(payload: dict[str, Any]) -> dict[str, Any]: base_id_or_error = require_base_id(payload, "metadata.dbnames.summary") if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error prefix_error = validate_optional_string_arguments(payload, "metadata.dbnames.summary", ["prefix"]) if prefix_error: return prefix_error limit, limit_error = parse_int_argument(payload, "limit", method="metadata.dbnames.summary", default=50, minimum=1, maximum=5000) if limit_error: return limit_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.dbnames.summary", default=30, minimum=1) if timeout_error: return timeout_error diagnostic_error = require_diagnostic_mode(payload, "metadata.dbnames.summary") if diagnostic_error: return diagnostic_error prefix = str(payload.get("prefix") or "DBNames") files = storage_files_list({"base_id": base_id, "table": "Params", "prefix": prefix, "limit": limit, "timeout_seconds": timeout_seconds, "_internal": True}) if files.get("status") != "ok": result = dict(files) result["method"] = "metadata.dbnames.summary" return result try: from parser.dbnames import parse_dbnames_bytes, parse_dbnames_version_bytes except Exception as exc: return { "schema": "onec_metadata_dbnames_summary.v1", "status": "error", "base_id": base_id, "source": {"kind": "live_sql", "table": "Params"}, "diagnostics": {"message": f"DBNames parser is unavailable: {exc}"}, "dbnames": [], "counts": {"files": 0, "records": 0}, } summaries = [] total_records = 0 role_counts: dict[str, int] = {} main_dbnames_ok = False version_seen = False version_token: str | None = None fallback_tokens: list[str] = [] for file_row in files.get("files") or []: file_name = str(file_row.get("FileName") or "") if not file_name.startswith("DBNames") or file_name.startswith("DBNamesVersion-"): continue data, config, error = read_storage_file_bytes(base_id, "Params", file_name, timeout_seconds=int(timeout_seconds or 30)) if error: summaries.append({"file_name": file_name, "status": "error", "diagnostics": error.get("diagnostics")}) continue try: parsed = ( parse_dbnames_version_bytes(data, source=file_name) if file_name == "DBNamesVersion" else parse_dbnames_bytes(data, source=file_name) ) except Exception as exc: summaries.append({"file_name": file_name, "status": "error", "diagnostics": {"message": str(exc)}}) continue if file_name == "DBNamesVersion": version_seen = True version_token = str(parsed.get("version") or "") or None summaries.append( { "file_name": file_name, "status": "ok", "bytes": len(data), "sha1": hashlib.sha1(data).hexdigest(), "compression": parsed.get("compression"), "encoding": parsed.get("encoding"), "marker": parsed.get("marker"), "version": parsed.get("version"), } ) continue records = parsed.get("records") or [] main_dbnames_ok = True fallback_tokens.append(hashlib.sha1(data).hexdigest()) total_records += len(records) local_roles: dict[str, int] = {} for record in records: role = getattr(record, "storage_role", "") local_roles[role] = local_roles.get(role, 0) + 1 role_counts[role] = role_counts.get(role, 0) + 1 summaries.append( { "file_name": file_name, "status": "ok", "bytes": len(data), "sha1": hashlib.sha1(data).hexdigest(), "compression": parsed.get("compression"), "encoding": parsed.get("encoding"), "declared_count": parsed.get("declared_count"), "records": len(records), "role_counts": dict(sorted(local_roles.items())), } ) has_errors = any(item.get("status") == "error" for item in summaries) status = "ok" if main_dbnames_ok and version_seen and not has_errors else ("degraded" if main_dbnames_ok else "error") fallback_token = None if len(fallback_tokens) == 1: fallback_token = fallback_tokens[0] elif fallback_tokens: fallback_token = hashlib.sha1("|".join(sorted(fallback_tokens)).encode("ascii")).hexdigest() freshness_token = version_token or fallback_token return { "schema": "onec_metadata_dbnames_summary.v1", "status": status, "base_id": base_id, "source": {"kind": "live_sql", "table": "Params"}, "dbnames": summaries, "counts": {"files": len(summaries), "records": total_records}, "role_counts": dict(sorted(role_counts.items())), "freshness": { "token": freshness_token, "source": "DBNamesVersion" if version_token else "dbnames_sha1", "degraded": status == "degraded", }, } def live_dbnames_records(base_id: str, *, limit_files: int = 200, timeout_seconds: int = 30) -> tuple[list[Any] | None, dict[str, Any] | None]: files = storage_files_list({"base_id": base_id, "table": "Params", "prefix": "DBNames", "limit": limit_files, "timeout_seconds": timeout_seconds, "_internal": True}) if files.get("status") != "ok": error = dict(files) error["method"] = "metadata.dbnames.records" return None, error try: from parser.dbnames import parse_dbnames_bytes except Exception as exc: return None, { "schema": "onec_adapter_error.v1", "status": "error", "base_id": base_id, "diagnostics": {"message": f"DBNames parser is unavailable: {exc}"}, } records = [] for file_row in files.get("files") or []: file_name = str(file_row.get("FileName") or "") if ( not file_name.startswith("DBNames") or file_name == "DBNamesVersion" or file_name.startswith("DBNamesVersion-") ): continue data, _, error = read_storage_file_bytes(base_id, "Params", file_name, timeout_seconds=timeout_seconds) if error: return None, error try: parsed = parse_dbnames_bytes(data, source=file_name) except Exception as exc: return None, { "schema": "onec_adapter_source_error.v1", "method": "metadata.dbnames.records", "status": "error", "base_id": base_id, "source": {"kind": "live_sql", "table": "Params", "file_name": file_name}, "diagnostics": {"message": str(exc)}, } records.extend(parsed.get("records") or []) return records, None def config_identity_from_bytes(data: bytes) -> dict[str, Any] | None: try: from parser.config_object import find_identity from parser.payload import parse_brace_text, payload_to_text except Exception: return None decoded = payload_to_text(data) text = decoded.get("text") if not text or "{" not in text: return None try: identity = find_identity(parse_brace_text(text)) except Exception: return None if not identity: return None result = identity.to_dict() if result.get("name"): variants = text_variants(result.get("name")) result["name"] = best_text_variant(result.get("name")) if len(variants) > 1: result["name_variants"] = variants synonyms = result.get("synonyms") if isinstance(synonyms, dict): synonym_variants: dict[str, list[str]] = {} for key, value in list(synonyms.items()): if not value: continue variants = text_variants(value) synonyms[key] = best_text_variant(value) if len(variants) > 1: synonym_variants[key] = variants if synonym_variants: result["synonym_variants"] = synonym_variants return result def parse_config_tree_from_bytes(data: bytes) -> Any | None: try: from parser.payload import decode_payload_lossless, parse_brace_text except Exception: return None decoded = decode_payload_lossless(data) text = decoded.get("text") if not text or "{" not in text: return None try: return parse_brace_text(text) except Exception: return None def payload_text_from_bytes(data: bytes) -> dict[str, Any]: try: from parser.payload import payload_to_text except Exception as exc: return {"status": "error", "diagnostics": {"message": f"Payload parser is unavailable: {exc}"}} decoded = payload_to_text(data) return { "status": "ok" if decoded.get("text") is not None else "undecodable", "compression": decoded.get("compression"), "encoding": decoded.get("encoding"), "raw_bytes": decoded.get("raw_bytes"), "payload_bytes": decoded.get("payload_bytes"), "text": decoded.get("text"), } def decode_payload_full(data: bytes, *, include_text: bool = True, include_tree: bool = False) -> dict[str, Any]: try: from parser.payload import decode_payload_lossless, parse_brace_text, root_signature except Exception as exc: return {"status": "error", "diagnostics": {"message": f"Payload parser is unavailable: {exc}"}} decoded = decode_payload_lossless(data) text = decoded.get("text") result: dict[str, Any] = { "status": "ok" if text is not None else "undecodable", "compression": decoded.get("compression"), "encoding": decoded.get("encoding"), "raw_bytes": decoded.get("raw_bytes"), "payload_bytes": decoded.get("payload_bytes"), "sha1": hashlib.sha1(data).hexdigest(), } tree = None if text and "{" in text: try: tree = parse_brace_text(text) result["root"] = root_signature(tree) except Exception as exc: result["tree_error"] = str(exc) if include_text: result["text"] = text if include_tree: result["tree"] = tree return result def payload_source_bytes(source: dict[str, Any], *, default_base_id: str | None = None, timeout_seconds: int = 30) -> tuple[bytes | None, dict[str, Any], dict[str, Any] | None]: if not isinstance(source, dict): return None, {}, invalid_argument("payload.diff", "source", "source must be a JSON object.") if source.get("payload_base64") not in {None, ""}: try: data = base64.b64decode(str(source.get("payload_base64") or ""), validate=True) except Exception as exc: return None, {}, invalid_argument("payload.diff", "payload_base64", f"payload_base64 is not valid base64: {exc}") return data, {"kind": "inline", "encoding": "base64", "bytes": len(data)}, None if source.get("payload_hex") not in {None, ""}: try: data = bytes.fromhex(str(source.get("payload_hex") or "")) except Exception as exc: return None, {}, invalid_argument("payload.diff", "payload_hex", f"payload_hex is not valid hex: {exc}") return data, {"kind": "inline", "encoding": "hex", "bytes": len(data)}, None if source.get("text") is not None: if not isinstance(source.get("text"), str): return None, {}, invalid_argument("payload.diff", "text", "text must be a JSON string.") encoding = str(source.get("encoding") or "utf-8-sig") try: data = str(source.get("text") or "").encode(encoding) except Exception as exc: return None, {}, invalid_argument("payload.diff", "encoding", f"text cannot be encoded with {encoding}: {exc}") return data, {"kind": "inline", "encoding": encoding, "bytes": len(data)}, None base_id = str(source.get("base_id") or default_base_id or "").strip() table = str(source.get("table") or "").strip() file_name = str(source.get("file_name") or "").strip() if not base_id: return None, {}, base_id_required("payload.diff") if table not in STORAGE_TABLES: return None, {}, invalid_argument("payload.diff", "table", "Unsupported storage table.", allowed_values=sorted(STORAGE_TABLES)) if not file_name or Path(file_name).name != file_name: return None, {}, invalid_argument("payload.diff", "file_name", "Pass a single safe FileName value from the live SQL storage table.") data, config, error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=timeout_seconds) if error: error["method"] = "payload.diff" return None, {}, error return data, {"kind": "live_sql", "database": (config or {}).get("database"), "table": table, "file_name": file_name}, None def payload_diff_node_summary(value: Any) -> dict[str, Any]: if isinstance(value, dict): if value.get("type") == "list": items = value.get("items") if isinstance(value.get("items"), list) else [] head = items[0].get("value") if items and isinstance(items[0], dict) else None return {"type": "list", "items": len(items), "head": head} return {"type": "dict", "keys": len(value)} if isinstance(value, list): return {"type": "list", "items": len(value)} return {"type": type(value).__name__, "value": value} def payload_diff_scalar(value: Any) -> Any: if isinstance(value, dict) and value.get("type") in {"string", "atom"}: return value.get("value") if isinstance(value, (str, int, float, bool)) or value is None: return value return None def payload_tree_scalar_changes(before: Any, after: Any, *, max_changes: int = 200, path: str = "$") -> list[dict[str, Any]]: changes: list[dict[str, Any]] = [] def walk(left: Any, right: Any, current_path: str) -> None: if len(changes) >= max_changes: return left_scalar = payload_diff_scalar(left) right_scalar = payload_diff_scalar(right) if left_scalar is not None or right_scalar is not None: if left_scalar != right_scalar: changes.append({"path": current_path, "old": left_scalar, "new": right_scalar}) return if isinstance(left, dict) and left.get("type") == "list": left_items = left.get("items") if isinstance(left.get("items"), list) else [] right_items = right.get("items") if isinstance(right, dict) and right.get("type") == "list" and isinstance(right.get("items"), list) else [] max_len = max(len(left_items), len(right_items)) for index in range(max_len): next_path = f"{current_path}.{index}" if index >= len(left_items): changes.append({"path": next_path, "old": None, "new": payload_diff_node_summary(right_items[index]), "kind": "added_node"}) elif index >= len(right_items): changes.append({"path": next_path, "old": payload_diff_node_summary(left_items[index]), "new": None, "kind": "removed_node"}) else: walk(left_items[index], right_items[index], next_path) if len(changes) >= max_changes: break return if isinstance(left, dict) and isinstance(right, dict): keys = sorted(set(left) | set(right)) for key in keys: walk(left.get(key), right.get(key), f"{current_path}.{key}") if len(changes) >= max_changes: break return if payload_diff_node_summary(left) != payload_diff_node_summary(right): changes.append({"path": current_path, "old": payload_diff_node_summary(left), "new": payload_diff_node_summary(right), "kind": "changed_node"}) walk(before, after, path) return changes def payload_strings(value: Any, *, limit: int = 10000) -> list[str]: strings: list[str] = [] def walk(node: Any) -> None: if len(strings) >= limit: return if isinstance(node, dict): if node.get("type") == "string": strings.append(str(node.get("value") or "")) return for child in node.values(): walk(child) elif isinstance(node, list): for child in node: walk(child) walk(value) return strings def payload_diff(payload: dict[str, Any]) -> dict[str, Any]: method = "payload.diff" base_id = str(payload.get("base_id") or "").strip() or None timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error max_changes, max_changes_error = parse_int_argument(payload, "max_changes", method=method, default=200, minimum=1, maximum=5000) if max_changes_error: return max_changes_error max_text_diff_lines, max_text_diff_lines_error = parse_int_argument(payload, "max_text_diff_lines", method=method, default=200, minimum=0, maximum=5000) if max_text_diff_lines_error: return max_text_diff_lines_error for argument, default in (("include_text_diff", True), ("include_tree_diff", True), ("include_evidence", True)): _, bool_error = strict_bool_argument(payload, argument, method=method, default=default) if bool_error: return bool_error diagnostic_error = require_diagnostic_mode(payload, method) if diagnostic_error: return diagnostic_error before_source = payload.get("before") after_source = payload.get("after") if not isinstance(before_source, dict): return invalid_argument(method, "before", "before must be a JSON object source.") if not isinstance(after_source, dict): return invalid_argument(method, "after", "after must be a JSON object source.") before_bytes, before_public, before_error = payload_source_bytes(before_source, default_base_id=base_id, timeout_seconds=int(timeout_seconds or 30)) if before_error: return before_error after_bytes, after_public, after_error = payload_source_bytes(after_source, default_base_id=base_id, timeout_seconds=int(timeout_seconds or 30)) if after_error: return after_error before_bytes = before_bytes or b"" after_bytes = after_bytes or b"" before_decoded = decode_payload_full(before_bytes, include_text=True, include_tree=bool(payload.get("include_tree_diff", True))) after_decoded = decode_payload_full(after_bytes, include_text=True, include_tree=bool(payload.get("include_tree_diff", True))) before_text = before_decoded.get("text") if isinstance(before_decoded.get("text"), str) else "" after_text = after_decoded.get("text") if isinstance(after_decoded.get("text"), str) else "" text_diff_lines: list[str] = [] if bool(payload.get("include_text_diff", True)) and before_text != after_text and int(max_text_diff_lines or 0) > 0: text_diff_lines = list( difflib.unified_diff( before_text.splitlines(), after_text.splitlines(), fromfile="before", tofile="after", lineterm="", n=3, ) )[: int(max_text_diff_lines or 200)] tree_changes: list[dict[str, Any]] = [] string_changes: list[dict[str, Any]] = [] if bool(payload.get("include_tree_diff", True)): before_tree = before_decoded.get("tree") after_tree = after_decoded.get("tree") if before_tree is not None and after_tree is not None: tree_changes = payload_tree_scalar_changes(before_tree, after_tree, max_changes=int(max_changes or 200)) before_strings = payload_strings(before_tree) after_strings = payload_strings(after_tree) for index in range(max(len(before_strings), len(after_strings))): if len(string_changes) >= int(max_changes or 200): break old = before_strings[index] if index < len(before_strings) else None new = after_strings[index] if index < len(after_strings) else None if old != new: string_changes.append({"index": index, "old": old, "new": new}) result: dict[str, Any] = { "schema": "onec_payload_diff.v1", "method": method, "status": "unchanged" if hashlib.sha1(before_bytes).hexdigest() == hashlib.sha1(after_bytes).hexdigest() else "changed", "source": {"before": before_public, "after": after_public}, "bytes": { "before": len(before_bytes), "after": len(after_bytes), "delta": len(after_bytes) - len(before_bytes), "same": before_bytes == after_bytes, }, "sha1": { "before": hashlib.sha1(before_bytes).hexdigest(), "after": hashlib.sha1(after_bytes).hexdigest(), "same": hashlib.sha1(before_bytes).hexdigest() == hashlib.sha1(after_bytes).hexdigest(), }, "decoded": { "before": {key: before_decoded.get(key) for key in ("status", "compression", "encoding", "raw_bytes", "payload_bytes", "root", "tree_error") if key in before_decoded}, "after": {key: after_decoded.get(key) for key in ("status", "compression", "encoding", "raw_bytes", "payload_bytes", "root", "tree_error") if key in after_decoded}, }, "text": { "same": before_text == after_text, "before_chars": len(before_text), "after_chars": len(after_text), "diff_lines": text_diff_lines, "diff_truncated": bool(text_diff_lines) and len(text_diff_lines) >= int(max_text_diff_lines or 0), }, "tree": { "same": not tree_changes, "changes": tree_changes, "changes_truncated": len(tree_changes) >= int(max_changes or 200), }, "strings": { "same": not string_changes, "changes": string_changes, "changes_truncated": len(string_changes) >= int(max_changes or 200), }, "counts": { "tree_changes": len(tree_changes), "string_changes": len(string_changes), "text_diff_lines": len(text_diff_lines), }, } if bool(payload.get("include_evidence", True)): try: from parser.cas_payload import classify_payload before_classified = classify_payload(before_bytes, include_text=False, include_tree=False) after_classified = classify_payload(after_bytes, include_text=False, include_tree=False) result["evidence"] = { "before": payload_public_undecoded_evidence(before_classified, mode="summary"), "after": payload_public_undecoded_evidence(after_classified, mode="summary"), } except Exception as exc: result["evidence"] = {"status": "error", "diagnostics": {"message": str(exc)}} return result def decode_config_object_full( data: bytes, *, kind: str | None = None, dbnames_records: list[Any] | None = None, include_text: bool = False, include_tree: bool = False, max_depth: int = 3, semantic_include_generic: bool = True, semantic_categories: set[str] | list[str] | tuple[str, ...] | None = None, semantic_lightweight: bool = False, ) -> dict[str, Any]: try: from parser.config_semantic import decode_config_semantic from parser.payload import decode_payload_lossless, parse_brace_text, root_signature except Exception as exc: return {"status": "error", "diagnostics": {"message": f"Config semantic decoder is unavailable: {exc}"}} decoded = decode_payload_lossless(data) text = decoded.get("text") result: dict[str, Any] = { "status": "ok" if text is not None else "undecodable", "compression": decoded.get("compression"), "encoding": decoded.get("encoding"), "raw_bytes": decoded.get("raw_bytes"), "payload_bytes": decoded.get("payload_bytes"), "sha1": hashlib.sha1(data).hexdigest(), } if not text or "{" not in text: return result try: tree = parse_brace_text(text) result["root"] = root_signature(tree) result["semantic"] = decode_config_semantic( tree, kind=kind, dbnames_records=dbnames_records, max_depth=max_depth, include_generic=semantic_include_generic, categories=semantic_categories, lightweight=semantic_lightweight, ) if include_tree: result["tree"] = tree if include_text: result["text"] = text except Exception as exc: result["status"] = "error" result["diagnostics"] = {"message": str(exc)} return result def text_snippet(text: str, query: str, *, radius: int = 160) -> dict[str, Any]: lower = text.casefold() wanted = query.casefold() index = lower.find(wanted) if index < 0: return {"offset": None, "text": text[: radius * 2]} start = max(0, index - radius) end = min(len(text), index + len(query) + radius) return {"offset": index, "text": text[start:end]} def extract_bsl_text_from_container(text: str, *, bsl_offset: int | None = None) -> tuple[str, dict[str, Any]]: if not text: return "", {"status": "empty"} candidates = [] if bsl_offset is not None and bsl_offset >= 0: candidates.append(bsl_offset) patterns = [ r"(?m)^[ \t]*&[А-Яа-яA-Za-z]", r"(?im)^[ \t]*(Процедура|Функция)\s+[A-Za-zА-Яа-яЁё_]", ] for pattern in patterns: match = re.search(pattern, text) if match: candidates.append(match.start()) if not candidates: return text, {"status": "not_found", "diagnostics": {"message": "BSL start marker was not found; returning container text."}} start = max(0, min(candidates)) return text[start:], {"status": "ok", "bsl_offset": start, "container_chars": len(text)} def live_config_identity( base_id: str, guid: str, *, timeout_seconds: int = 30, table: str = "Config", ) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: data, _, error = read_storage_file_bytes(base_id, table, guid.lower(), timeout_seconds=timeout_seconds) if error: if error.get("status") == "source_missing": return None, None return None, error return config_identity_from_bytes(data), None def dbnames_kind_counts(records: list[Any]) -> dict[str, int]: seen: set[tuple[str, str]] = set() counts: dict[str, int] = {} for record in records: role = getattr(record, "storage_role", "") kind = DBNAMES_ROLE_KIND.get(role) guid = str(getattr(record, "guid", "") or "").lower() if not kind or not guid: continue key = (kind, guid) if key in seen: continue seen.add(key) public = PUBLIC_KIND.get(kind, "other") counts[public] = counts.get(public, 0) + 1 return counts def generated_type_records_from_tree(tree: Any, *, kind: str | None, name: str | None, guid: str | None) -> list[dict[str, Any]]: try: from parser.payload import GUID_RE as PAYLOAD_GUID_RE, scalar except Exception: return [] if not isinstance(tree, dict): return [] root_items = tree.get("items") or [] if len(root_items) < 2 or not isinstance(root_items[1], dict): return [] items = root_items[1].get("items") or [] if len(items) < 4: return [] categories = GENERATED_TYPE_CATEGORIES.get(str(kind or ""), []) prefix = GENERATED_TYPE_PREFIX.get(str(kind or ""), str(kind or "")) result = [] zero_guid = "00000000-0000-0000-0000-000000000000" # Current 8.3 payloads commonly place an identity list before the generated # type/value GUID pairs. Find the longest direct-child GUID run instead of # assuming that pairs always start at body index 1. runs: list[tuple[int, int]] = [] run_start: int | None = None for item_index, item in enumerate(items): value = str(scalar(item) or "").lower() is_generated_guid = bool(PAYLOAD_GUID_RE.fullmatch(value)) and value != zero_guid if is_generated_guid and run_start is None: run_start = item_index elif not is_generated_guid and run_start is not None: runs.append((run_start, item_index)) run_start = None if run_start is not None: runs.append((run_start, len(items))) even_runs = [(start, end - ((end - start) % 2)) for start, end in runs if end - start >= 2] start_index, end_index = max(even_runs, key=lambda run: (run[1] - run[0], -run[0]), default=(1, 1)) pair_index = 0 index = start_index while index + 1 < end_index: type_id = str(scalar(items[index]) or "") value_id = str(scalar(items[index + 1]) or "") category = categories[pair_index] if pair_index < len(categories) else f"Generated{pair_index + 1}" if str(kind or "") == "DefinedType" and name: generated_name = f"DefinedType.{name}" else: generated_name = f"{prefix}{category}.{name}" if prefix and category and name else None result.append( { "type_guid": type_id.lower(), "value_guid": value_id.lower(), "category": category, "name": generated_name, "owner": { "guid": guid, "kind": kind, "kind_ru": RU_KIND.get(str(kind or ""), kind), "name": name, }, "presentation": f"cfg:{generated_name}" if generated_name else "", } ) pair_index += 1 index += 2 # Object payloads on current 8.3 builds keep the Manager type/value pair # later in the header, after kind-specific scalar properties. It is not # necessarily contiguous with Object/Ref/Selection/List pairs above. if "Manager" in categories and not any(record.get("category") == "Manager" for record in result): late_pairs: list[tuple[str, str]] = [] for late_index in range(max(end_index, 9), len(items) - 1): type_id = scalar(items[late_index]).lower() value_id = scalar(items[late_index + 1]).lower() if ( PAYLOAD_GUID_RE.fullmatch(type_id) and PAYLOAD_GUID_RE.fullmatch(value_id) and type_id != zero_guid and value_id != zero_guid ): late_pairs.append((type_id, value_id)) if late_pairs: type_id, value_id = late_pairs[-1] generated_name = f"{prefix}Manager.{name}" if prefix and name else None result.append( { "type_guid": type_id, "value_guid": value_id, "category": "Manager", "name": generated_name, "owner": { "guid": guid, "kind": kind, "kind_ru": RU_KIND.get(str(kind or ""), kind), "name": name, }, "presentation": f"cfg:{generated_name}" if generated_name else "", } ) return result def looks_like_defined_type_tree(tree: Any) -> bool: values = tree_ordered_scalars(tree, limit=40) return ( len(values) > 17 and values[0] == "1" and values[1] == "0" and is_guid_text(values[2]) and is_guid_text(values[3]) and values[4] == "3" and is_guid_text(values[7]) and values[16] == "Pattern" ) def generated_type_records_from_bytes(data: bytes, *, kind: str | None, guid: str | None) -> list[dict[str, Any]]: tree = parse_config_tree_from_bytes(data) if tree is None: return [] if str(kind or "") == "DefinedType" and not looks_like_defined_type_tree(tree): return [] identity = config_identity_from_bytes(data) or {} records = generated_type_records_from_tree(tree, kind=kind, name=identity.get("name"), guid=guid) if str(kind or "") == "DefinedType": raw_type = public_pattern_type_from_tree(tree, {}, raw=True) if raw_type: for record in records: record["value_type"] = raw_type return records def live_generated_type_map( base_id: str, type_guids: set[str], *, dbnames_records: list[Any] | None = None, timeout_seconds: int = 60, table: str = "Config", ) -> dict[str, dict[str, Any]]: if not type_guids: return {} wanted = {guid.lower() for guid in type_guids} candidates: dict[str, str] = {} for record in dbnames_records or []: role = getattr(record, "storage_role", "") kind = DBNAMES_ROLE_KIND.get(role) guid = str(getattr(record, "guid", "") or "").lower() if not kind or not guid or kind not in GENERATED_TYPE_CATEGORIES: continue candidates.setdefault(guid, kind) resolved: dict[str, dict[str, Any]] = {} items = list(candidates.items()) for start in range(0, len(items), 300): if wanted.issubset(resolved): break chunk = items[start : start + 300] payloads, _, error = read_storage_files_bytes( base_id, table, [guid for guid, _ in chunk], timeout_seconds=timeout_seconds, ) if error: break for guid, kind in chunk: data = (payloads or {}).get(guid) if not data: continue for generated in generated_type_records_from_bytes(data, kind=kind, guid=guid): type_guid = str(generated.get("type_guid") or "").lower() if type_guid in wanted: resolved[type_guid] = generated if wanted.issubset(resolved): break return resolved def live_defined_type_map( base_id: str, type_guids: set[str], *, known_config_guids: set[str] | None = None, timeout_seconds: int = 60, table: str = "Config", ) -> dict[str, dict[str, Any]]: if not type_guids: return {} wanted = {guid.lower() for guid in type_guids} known = known_config_guids or set() resolved: dict[str, dict[str, Any]] = {} last_file_name = "" while True: if wanted.issubset(resolved): break page = live_config_file_name_page_after( base_id, last_file_name, table=table, page_size=2000, timeout_seconds=timeout_seconds, ) if not page: break last_file_name = page[-1] candidates = [file_name for file_name in page if file_name not in known] if not candidates: continue payloads, _, error = read_storage_files_bytes(base_id, table, candidates, timeout_seconds=timeout_seconds) if error: continue for guid in candidates: data = (payloads or {}).get(guid) if not data: continue for generated in generated_type_records_from_bytes(data, kind="DefinedType", guid=guid): type_guid = str(generated.get("type_guid") or "").lower() if type_guid in wanted: resolved[type_guid] = generated if wanted.issubset(resolved): break return resolved def resolved_type_from_generated( base_id: str, type_guid: str, generated: dict[str, Any], *, timeout_seconds: int = 60, depth: int = 0, table: str = "Config", ) -> dict[str, Any]: owner = generated.get("owner") or {} raw_value_type = generated.get("value_type") if isinstance(generated.get("value_type"), dict) else None public_value_type = None if raw_value_type and raw_value_type.get("type_guid") and depth < 2: nested = resolve_type_guids( base_id, {str(raw_value_type.get("type_guid")).lower()}, timeout_seconds=timeout_seconds, _depth=depth + 1, table=table, ) public_value_type = public_type_info(raw_value_type, nested, include_storage=False) return { "guid": str(type_guid or "").lower(), "status": "ok", "guid_role": "generated_type", "generated_category": generated.get("category"), "generated_name": generated.get("name"), "value_guid": generated.get("value_guid"), "kind": owner.get("kind"), "kind_ru": owner.get("kind_ru"), "name": owner.get("name"), "owner_guid": owner.get("guid"), "presentation": generated.get("presentation") or "", **({"value_type": public_value_type} if public_value_type else {}), } def resolve_type_guids( base_id: str, type_guids: set[str], *, timeout_seconds: int = 60, _depth: int = 0, resolve_generated_live: bool = True, table: str = "Config", ) -> dict[str, dict[str, Any]]: if not type_guids: return {} requested = {str(guid or "").lower() for guid in type_guids if is_guid_text(str(guid or ""))} config, _ = sql_config_for_base(base_id) cached_types = metadata_guid_index_lookup_types(config, requested) if config else {} if config: legacy_cached = metadata_type_cache_lookup_many(config, requested - set(cached_types)) cached_types.update(legacy_cached) for legacy_guid, legacy_payload in legacy_cached.items(): metadata_type_cache_upsert(config, legacy_guid, legacy_payload) result: dict[str, dict[str, Any]] = dict(cached_types) for guid in sorted(requested): if guid in BUILTIN_TYPE_GUIDS: builtin = BUILTIN_TYPE_GUIDS[guid] resolved_builtin = { "guid": guid, "status": "ok", "guid_role": "builtin_type", "kind": "Builtin", "kind_ru": "ВстроенныйТип", "name": builtin.get("name"), "bsl_type": builtin.get("bsl_type"), "presentation": builtin.get("presentation") or "", } result[guid] = resolved_builtin unresolved_request = {guid for guid in requested if guid not in result or result[guid].get("status") != "ok"} if not unresolved_request: return result records, error = live_dbnames_records(base_id, timeout_seconds=timeout_seconds) if error: return result kind_by_guid: dict[str, str] = {} for record in records or []: role = getattr(record, "storage_role", "") kind = DBNAMES_ROLE_KIND.get(role) guid = str(getattr(record, "guid", "") or "").lower() if kind and guid in unresolved_request and guid not in kind_by_guid: kind_by_guid[guid] = kind generated_candidates = { guid.lower() for guid in unresolved_request if guid.lower() not in kind_by_guid and guid.lower() not in BUILTIN_TYPE_GUIDS } generated_map = ( live_generated_type_map( base_id, generated_candidates, dbnames_records=records, timeout_seconds=timeout_seconds, table=table, ) if resolve_generated_live else {} ) identities_by_guid: dict[str, dict[str, Any]] = {} direct_identity_guids = sorted(guid for guid in unresolved_request if guid in kind_by_guid and guid not in generated_map) if direct_identity_guids: payloads, _, _identity_batch_error = read_storage_files_bytes( base_id, table, direct_identity_guids, timeout_seconds=timeout_seconds, ) for guid in direct_identity_guids: data = (payloads or {}).get(guid) if not data: continue identity = config_identity_from_bytes(data) if identity: identities_by_guid[guid] = identity unresolved_generated = {guid.lower() for guid in generated_candidates if guid.lower() not in generated_map} if unresolved_generated: known_config_guids = {str(getattr(record, "guid", "") or "").lower() for record in records or []} if resolve_generated_live: generated_map.update( live_defined_type_map( base_id, unresolved_generated, known_config_guids=known_config_guids, timeout_seconds=timeout_seconds, table=table, ) ) for guid in sorted(unresolved_request): if guid in generated_map: result[guid] = resolved_type_from_generated( base_id, guid, generated_map[guid], timeout_seconds=timeout_seconds, depth=_depth, table=table, ) if config: metadata_type_cache_upsert(config, guid, result[guid]) continue identity_error = None identity = identities_by_guid.get(guid) if identity is None and guid not in identities_by_guid: identity, identity_error = live_config_identity(base_id, guid, timeout_seconds=timeout_seconds, table=table) kind = kind_by_guid.get(guid) synonyms = (identity or {}).get("synonyms") or {} synonym = next(iter(synonyms.values()), None) if isinstance(synonyms, dict) else None name = (identity or {}).get("name") status = "ok" if identity or kind else "generated_type_unresolved" result[guid] = { "guid": guid, "status": status, "guid_role": "metadata_object" if status == "ok" else "generated_type", "kind": kind, "kind_ru": RU_KIND.get(kind or "", kind), "name": name, "synonym": synonym, "presentation": ".".join(part for part in [RU_KIND.get(kind or "", kind), name] if part), **( { "diagnostics": { "message": ( "Pattern reference points to a generated 1C type GUID, not directly to a metadata object GUID. " "Live generated-type mapping is not decoded yet." ) } } if status != "ok" else {} ), **({"diagnostics": identity_error.get("diagnostics")} if identity_error else {}), } if config and result[guid].get("status") == "ok": metadata_type_cache_upsert(config, guid, result[guid]) return result def collect_reference_type_guids_from_sections(sections: list[Any]) -> set[str]: type_guids: set[str] = set() for section in sections: if not isinstance(section, dict): continue for record in section.get("records") or []: if not isinstance(record, dict): continue record_type = record.get("type") or {} if isinstance(record_type, dict) and record_type.get("kind") == "reference" and record_type.get("type_guid"): type_guids.add(str(record_type.get("type_guid")).lower()) for column in record.get("columns") or []: if not isinstance(column, dict): continue column_type = column.get("type") or {} if isinstance(column_type, dict) and column_type.get("kind") == "reference" and column_type.get("type_guid"): type_guids.add(str(column_type.get("type_guid")).lower()) return type_guids def with_resolved_type(type_info: Any, resolved_types: dict[str, dict[str, Any]]) -> Any: if not isinstance(type_info, dict): return type_info result = dict(type_info) type_guid = str(result.get("type_guid") or "").lower() if type_guid and type_guid in resolved_types: result["resolved"] = resolved_types[type_guid] return result def resolved_type_presentation(resolved: dict[str, Any]) -> str: category = str(resolved.get("generated_category") or "") kind = str(resolved.get("kind") or "") name = str(resolved.get("name") or "") if not name: generated_name = str(resolved.get("generated_name") or "") if "." in generated_name: name = generated_name.rsplit(".", 1)[-1] if not name: return str(resolved.get("presentation") or "").removeprefix("cfg:") if category == "DefinedType": return f"ОпределяемыйТип.{name}" if category == "Ref": prefix = REF_TYPE_PRESENTATION_PREFIX.get(kind) elif category == "Object": prefix = OBJECT_TYPE_PRESENTATION_PREFIX.get(kind) elif category == "List": prefix = LIST_TYPE_PRESENTATION_PREFIX.get(kind) else: prefix = None if prefix: return f"{prefix}.{name}" if kind in RU_KIND: return f"{RU_KIND[kind]}.{name}" return str(resolved.get("presentation") or "").removeprefix("cfg:") or name def public_type_info(type_info: Any, resolved_types: dict[str, dict[str, Any]], *, include_storage: bool = False) -> Any: enriched = with_resolved_type(type_info, resolved_types) if not isinstance(enriched, dict) or include_storage: return enriched public = {key: value for key, value in enriched.items() if key not in {"type_guid", "resolved", "code"}} resolved = enriched.get("resolved") if isinstance(resolved, dict) and resolved.get("status") == "ok": presentation = resolved_type_presentation(resolved) if presentation: public["presentation"] = presentation if isinstance(resolved.get("value_type"), dict): public["value_type"] = resolved.get("value_type") elif enriched.get("kind") == "reference" and str(public.get("presentation") or "").strip() in {"", "Ссылка", "Reference"}: public["presentation"] = "Ссылка(тип ссылки не определен)" public["diagnostics"] = { "message": "Не удалось определить конкретный объект метаданных для ссылочного типа. Для служебной диагностики используйте include_storage=true.", } return public def platform_reference_type_fallback(owner_kind: str | None, field_name: str | None, public_type: Any) -> dict[str, Any] | None: if not isinstance(public_type, dict) or public_type.get("kind") != "reference": return None presentation = str(public_type.get("presentation") or "").strip() if presentation not in {"", "Ссылка", "Reference", "Ссылка(тип ссылки не определен)"}: return None normalized_name = normalize(field_name or "") if owner_kind == "Task" and normalized_name == normalize("Предмет"): return { "kind": "reference", "presentation": "ЛюбаяСсылка", "allowed_types": ["СправочникСсылка", "ДокументСсылка", "БизнесПроцессСсылка", "ЗадачаСсылка"], } if owner_kind == "BusinessProcess" and normalized_name == normalize("ЗадачаИсточник"): return { "kind": "reference", "presentation": "ЗадачаСсылка", } return None def public_metadata_item( record: dict[str, Any], resolved_types: dict[str, dict[str, Any]], *, include_storage: bool = False, owner_kind: str | None = None, extensions_by_guid: dict[str, dict[str, Any]] | None = None, ) -> dict[str, Any]: name = record.get("likely_name") public_type = public_type_info(record.get("type"), resolved_types, include_storage=include_storage) if not include_storage: fallback_type = platform_reference_type_fallback(owner_kind, str(name or ""), public_type) if fallback_type: public_type = fallback_type item = { "name": name, "type": public_type, } identity = record.get("identity") if isinstance(record.get("identity"), dict) else {} synonyms = identity.get("synonyms") if isinstance(identity, dict) else None if isinstance(synonyms, dict) and synonyms: item["synonym"] = next(iter(synonyms.values())) origin = public_origin_from_storage_routes(record.get("storage_routes"), extensions_by_guid) if origin: item["origin"] = origin if include_storage: item.update( { "identity": record.get("identity"), "path": record.get("path"), "index": record.get("index"), "strings_sample": record.get("strings_sample"), "guids_sample": record.get("guids_sample"), } ) if "storage_routes" in record: item["storage_routes"] = record.get("storage_routes") return item def public_reference_type_counts(*groups: list[dict[str, Any]]) -> dict[str, int]: resolved = 0 unresolved = 0 def inspect_type(type_info: Any) -> None: nonlocal resolved, unresolved if not isinstance(type_info, dict) or type_info.get("kind") != "reference": return presentation = str(type_info.get("presentation") or "").strip() resolved_info = type_info.get("resolved") if isinstance(type_info.get("resolved"), dict) else None if ( "тип ссылки не определен" in presentation or presentation in {"", "Ссылка", "Reference"} or (resolved_info is not None and resolved_info.get("status") != "ok") ): unresolved += 1 else: resolved += 1 for group in groups: for item in group or []: inspect_type((item or {}).get("type")) for column in (item or {}).get("columns") or []: inspect_type((column or {}).get("type")) return {"resolved_reference_types": resolved, "unresolved_reference_types": unresolved} def enrich_semantic_types(semantic: dict[str, Any] | None, resolved_types: dict[str, dict[str, Any]]) -> dict[str, Any] | None: if not isinstance(semantic, dict) or not resolved_types: return semantic enriched = dict(semantic) sections = [] for section in enriched.get("sections") or []: if not isinstance(section, dict): sections.append(section) continue enriched_section = dict(section) records = [] for record in enriched_section.get("records") or []: if not isinstance(record, dict): records.append(record) continue enriched_record = dict(record) enriched_record["type"] = with_resolved_type(enriched_record.get("type"), resolved_types) columns = [] for column in enriched_record.get("columns") or []: if not isinstance(column, dict): columns.append(column) continue enriched_column = dict(column) enriched_column["type"] = with_resolved_type(enriched_column.get("type"), resolved_types) columns.append(enriched_column) if "columns" in enriched_record: enriched_record["columns"] = columns records.append(enriched_record) enriched_section["records"] = records sections.append(enriched_section) enriched["sections"] = sections return enriched def live_extensions_from_sql(base_id: str, *, include_storage: bool = False) -> dict[str, Any] | None: config, config_error = sql_config_for_base(base_id) if not config: return { "schema": "onec_extensions_list.v1", "status": "source_missing", "base_id": base_id, "source": {"kind": "live_metadata", "status": (config_error or {}).get("status", "not_configured")}, "extensions": [], "counts": {"extensions": 0}, "diagnostics": config_error or {}, } try: import pymssql # type: ignore except Exception as exc: return { "schema": "onec_extensions_list.v1", "status": "error", "base_id": base_id, "source": {"kind": "live_metadata", "status": "driver_unavailable"}, "extensions": [], "counts": {"extensions": 0}, "diagnostics": {"message": str(exc)}, } database = config["database"] rows = [] try: with pymssql.connect( server=config["server"], user=config["user"], password=config["password"], database=database, login_timeout=5, timeout=20, ) as conn: with conn.cursor(as_dict=True) as cursor: cursor.execute( """ SELECT [_IDRRef], [_ExtensionOrder], [_ExtName], [_UpdateTime], [_ExtensionUsePurpose], [_ExtensionScope], DATALENGTH([_ExtensionZippedInfo]) AS [_ExtensionZippedInfoBytes] FROM dbo.[_ExtensionsInfo] ORDER BY [_ExtensionOrder], [_ExtName] """ ) for index, row in enumerate(cursor.fetchall(), start=1): idrref = row.get("_IDRRef") extension = { "name": jsonable(row.get("_ExtName")), "order": jsonable(row.get("_ExtensionOrder")), "update_time": jsonable(row.get("_UpdateTime")), "guid": dbnames_ext_guid_from_idrref(idrref), "active": True, } if include_storage: extension.update( { "row_index": index, "extension_order": jsonable(row.get("_ExtensionOrder")), "use_purpose": jsonable(row.get("_ExtensionUsePurpose")), "scope": jsonable(row.get("_ExtensionScope")), "dbnames_ext_guid": dbnames_ext_guid_from_idrref(idrref), "dbnames_ext_file": None, "dbnames_ext_file_bytes": None, "extension_zipped_info": {"type": "binary", "bytes": jsonable(row.get("_ExtensionZippedInfoBytes"))}, "active_inference": "present_in_live_sql_extensions_info", } ) rows.append(extension) except Exception as exc: return { "schema": "onec_extensions_list.v1", "status": "error", "base_id": base_id, "source": {"kind": "live_metadata", "status": "error"}, "extensions": [], "counts": {"extensions": 0}, "diagnostics": {"message": str(exc)}, } return { "schema": "onec_extensions_list.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "database": database, "table": "_ExtensionsInfo"} if include_storage else {"kind": "live_metadata"}, "extensions": rows, "counts": {"extensions": len(rows), "extension_row_count": len(rows)}, } def extension_guid_from_dbnames_source(source: Any) -> str | None: match = re.fullmatch(r"DBNames-Ext-([0-9a-fA-F-]{36})", str(source or "").strip()) return match.group(1).lower() if match else None def extension_map_by_guid(base_id: str) -> dict[str, dict[str, Any]]: result = live_extensions_from_sql(base_id, include_storage=False) or {} mapping: dict[str, dict[str, Any]] = {} for extension in result.get("extensions") or []: if not isinstance(extension, dict): continue guid = str(extension.get("guid") or "").strip().lower() if guid: mapping[guid] = { "name": extension.get("name"), "guid": guid, "order": extension.get("order"), "active": extension.get("active"), } return mapping def public_origin_from_storage_routes(routes: Any, extensions_by_guid: dict[str, dict[str, Any]] | None = None) -> dict[str, Any] | None: if not isinstance(routes, list) or not routes: return None extensions_by_guid = extensions_by_guid or {} extension_guids: list[str] = [] has_base_route = False for route in routes: if not isinstance(route, dict): continue source = str(route.get("source") or "") extension_guid = extension_guid_from_dbnames_source(source) if extension_guid and extension_guid not in extension_guids: extension_guids.append(extension_guid) elif source == "DBNames": has_base_route = True if extension_guids: extensions = [extensions_by_guid.get(guid) or {"guid": guid, "name": None, "active": None} for guid in extension_guids] return { "source": "extension", "presentation": "Расширение", "extension": extensions[0] if len(extensions) == 1 else None, **({"extensions": extensions} if len(extensions) > 1 else {}), "status": "ok" if all(item.get("name") for item in extensions) else "extension_unresolved", **( { "diagnostics": { "message": "Определение связано с DBNames расширения, но имя расширения не найдено в _ExtensionsInfo.", } } if any(not item.get("name") for item in extensions) else {} ), } if has_base_route: return {"source": "configuration", "presentation": "Конфигурация", "extension": None, "status": "ok"} return None def list_extensions(payload: dict[str, Any] | None = None) -> dict[str, Any]: payload = payload or {} base_id_or_error = require_base_id(payload, "extensions.list") if isinstance(base_id_or_error, dict): return base_id_or_error include_storage, include_storage_error = strict_include_storage(payload, "extensions.list") if include_storage_error: return include_storage_error if "limit" in payload: parsed_limit, limit_error = parse_int_argument(payload, "limit", method="extensions.list", default=200, minimum=1) if limit_error: return limit_error else: parsed_limit = None if "offset" in payload: parsed_offset, offset_error = parse_int_argument(payload, "offset", method="extensions.list", default=0, minimum=0) if offset_error: return offset_error else: parsed_offset = 0 timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method="extensions.list", default=30, minimum=1) if timeout_error: return timeout_error base_id = base_id_or_error raw_extensions_result = live_extensions_from_sql(base_id, include_storage=bool(include_storage)) if not raw_extensions_result: return live_source_unavailable("extensions.list", base_id, None) extensions = list(raw_extensions_result.get("extensions") or []) for extension in extensions: if not isinstance(extension, dict): continue extension["load_order"] = extension.get("order") extension["depends_on"] = extension.get("depends_on") or [] if "is_forbid_conflict" not in extension: extension["is_forbid_conflict"] = False if extension.get("name"): extension["presentation"] = str(extension.get("name") or extension.get("guid") or "") extensions = sorted( extensions, key=lambda item: item.get("load_order") if isinstance(item, dict) and item.get("load_order") is not None else 10**9, ) total_extensions = len(extensions) page = extensions[parsed_offset:] if parsed_limit is None else extensions[parsed_offset : parsed_offset + parsed_limit] return { **raw_extensions_result, "extensions": page, "counts": {"extensions": len(page), "extension_row_count": total_extensions}, "query": {"limit": parsed_limit, "offset": parsed_offset, "include_storage": bool(include_storage)}, } def strip_sql_comments(query: str) -> str: query = re.sub(r"/\*.*?\*/", " ", query, flags=re.DOTALL) query = re.sub(r"--[^\r\n]*", " ", query) return query def sql_without_literals(query: str) -> str: """Return SQL with quoted literals removed for conservative token checks.""" return re.sub(r"N?'(?:''|[^'])*'", "''", query, flags=re.IGNORECASE) def mask_sensitive_query_rows(rows: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[str]]: masked_fields = sorted({str(key) for row in rows for key in row if SENSITIVE_RESULT_FIELD_RE.search(str(key))}) if not masked_fields: return rows, [] masked = [] for row in rows: masked.append({key: ("***" if str(key) in masked_fields and value is not None else value) for key, value in row.items()}) return masked, masked_fields def validate_query(payload: dict[str, Any]) -> dict[str, Any]: base_id_or_error = require_base_id(payload, "query.validate") if isinstance(base_id_or_error, dict): return base_id_or_error if "query" not in payload: return invalid_argument("query.validate", "query", "query is required and must be a non-empty JSON string.") if not isinstance(payload.get("query"), str): return invalid_argument("query.validate", "query", "query must be a JSON string.") if not str(payload.get("query") or "").strip(): return invalid_argument("query.validate", "query", "query is required and must be a non-empty JSON string.") timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method="query.validate", default=30, minimum=1) if timeout_error: return timeout_error query = str(payload.get("query") or "") cleaned = strip_sql_comments(query).strip() checked_sql = sql_without_literals(cleaned) forbidden = re.compile( r"\b(insert|update|delete|drop|alter|truncate|merge|exec|execute|create|grant|revoke|deny|backup|restore|dbcc|use|set)\b", re.IGNORECASE, ) unsafe_read_features = re.compile( r"\b(openrowset|opendatasource|openquery|next\s+value\s+for|xp_[a-z0-9_]+|sp_[a-z0-9_]+)\b", re.IGNORECASE, ) starts_readonly = bool(re.match(r"^\s*(select|with)\b", cleaned, flags=re.IGNORECASE)) select_into = bool(re.search(r"\bselect\b.+\binto\b", checked_sql, flags=re.IGNORECASE | re.DOTALL)) statements = [part.strip() for part in cleaned.split(";") if part.strip()] ok = bool(cleaned) and starts_readonly and not forbidden.search(checked_sql) and not unsafe_read_features.search(checked_sql) and not select_into and len(statements) <= 1 reason = "ok" if not cleaned: reason = "empty" elif not starts_readonly: reason = "only_select_or_with_allowed" elif forbidden.search(checked_sql): reason = "forbidden_keyword" elif unsafe_read_features.search(checked_sql): reason = "unsafe_read_feature" elif select_into: reason = "select_into_forbidden" elif len(statements) > 1: reason = "multiple_statements_forbidden" return { "schema": "onec_query_validation.v1", "status": "ok", "valid": ok, "read_only": ok, "reason": reason, } def run_readonly_query(payload: dict[str, Any]) -> dict[str, Any]: base_id_or_error = require_base_id(payload, "query.run") if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error if "query" not in payload: return invalid_argument("query.run", "query", "query is required and must be a non-empty JSON string.") if not isinstance(payload.get("query"), str): return invalid_argument("query.run", "query", "query must be a JSON string.") if not str(payload.get("query") or "").strip(): return invalid_argument("query.run", "query", "query is required and must be a non-empty JSON string.") limit, limit_error = parse_int_argument(payload, "limit", method="query.run", default=100, minimum=1, maximum=1000) if limit_error: return limit_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method="query.run", default=30, minimum=1) if timeout_error: return timeout_error validation = validate_query(payload) if validation.get("status") == "invalid_argument": validation = dict(validation) validation["method"] = "query.run" return validation diagnostic_error = require_diagnostic_mode(payload, "query.run") if diagnostic_error: return diagnostic_error if not validation.get("valid"): return { "schema": "onec_query_result.v1", "status": "rejected", "base_id": base_id, "validation": validation, "rows": [], "counts": {"rows": 0}, } config, config_error = sql_config_for_base(base_id) if not config: return live_source_unavailable("query.run", base_id, config_error) try: import pymssql # type: ignore except Exception as exc: return { "schema": "onec_query_result.v1", "status": "error", "base_id": base_id, "source": {"kind": "live_sql", "status": "driver_unavailable"}, "diagnostics": {"message": str(exc)}, "rows": [], "counts": {"rows": 0}, } params = payload.get("params") or {} started = time.time() try: with pymssql.connect( server=config["server"], user=config["user"], password=config["password"], database=config["database"], login_timeout=min(timeout_seconds, 15), timeout=timeout_seconds, ) as conn: with conn.cursor(as_dict=True) as cursor: cursor.execute(str(payload.get("query") or ""), params) rows = cursor.fetchmany(limit + 1) truncated = len(rows) > limit rows = rows[:limit] columns = [column[0] for column in (cursor.description or [])] except Exception as exc: return { "schema": "onec_query_result.v1", "status": "error", "base_id": base_id, "source": {"kind": "live_sql", "database": config["database"]}, "validation": validation, "diagnostics": {"message": str(exc)}, "rows": [], "counts": {"rows": 0}, } public_rows = [{key: jsonable(value) for key, value in row.items()} for row in rows] public_rows, masked_fields = mask_sensitive_query_rows(public_rows) return { "schema": "onec_query_result.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "database": config["database"]}, "validation": validation, "columns": columns, "rows": public_rows, "counts": {"rows": len(rows), "limit": limit, "truncated": truncated}, "masking": {"enabled": True, "masked_fields": masked_fields}, "duration_ms": int((time.time() - started) * 1000), } DATA_TABLE_PREFIXES = { "Catalog": "_Reference", "Document": "_Document", "InformationRegister": "_InfoRg", "AccumulationRegister": "_AccumRg", "AccountingRegister": "_AccRg", "CalculationRegister": "_CalcRg", "BusinessProcess": "_BPr", "Task": "_Task", "ChartOfAccounts": "_Acc", "ChartOfCalculationTypes": "_CKinds", "ChartOfCharacteristicTypes": "_Chrc", "ExchangePlan": "_Node", "Sequence": "_Sequence", "Constant": "_Const", "Enum": "_Enum", } DATA_SYSTEM_COLUMNS = { "_IDRRef": "ref", "_Version": "version", "_Marked": "marked_for_deletion", "_Code": "code", "_Description": "description", "_Date_Time": "date", "_Number": "number", "_Posted": "posted", "_Period": "period", "_Active": "active", "_LineNo": "line_no", "_RecorderTRef": "recorder_type", "_RecorderRRef": "recorder_ref", "_PredefinedID": "predefined_ref", "_Folder": "is_folder", "_ParentIDRRef": "parent_ref", "_OwnerIDRRef": "owner_ref", "_EnumOrder": "enum_order", "_DescriptionHash": "description_hash", "_RecordKey": "record_key", "_Completed": "completed", "_Started": "started", "_HeadTaskRRef": "head_task_ref", "_BusinessProcess_TYPE": "business_process", "_BusinessProcess_RTRef": "business_process", "_BusinessProcess_RRRef": "business_process", "_Point_TYPE": "route_point", "_Point_RTRef": "route_point", "_Point_RRRef": "route_point", "_Name": "name", "_Executed": "executed", } def data_sql_rows(base_id: str, query: str, params: Any = None, *, timeout_seconds: int = 30) -> tuple[list[dict[str, Any]], dict[str, Any] | None]: config, config_error = sql_config_for_base(base_id) if not config: return [], live_source_unavailable("data.read", base_id, config_error) try: import pymssql # type: ignore with pymssql.connect( server=config["server"], user=config["user"], password=config["password"], database=config["database"], login_timeout=min(timeout_seconds, 15), timeout=timeout_seconds, ) as conn: with conn.cursor(as_dict=True) as cursor: cursor.execute(query, params or ()) return [dict(row) for row in cursor.fetchall()], None except Exception as exc: return [], { "schema": "onec_data_error.v1", "status": "error", "base_id": base_id, "error": "data_sql_error", "diagnostics": {"message": str(exc)}, } def data_fallback_field_routes(base_id: str, physical_names: list[str], *, timeout_seconds: int = 60) -> dict[str, dict[str, Any]]: """Resolve remaining _Fld/_Dim/_Resource columns through DBNames and descriptor identities.""" requested: dict[tuple[str, int], list[str]] = {} role_by_prefix = {"Fld": "attributes", "Dim": "dimensions", "Resource": "resources"} for physical in physical_names: match = re.match(r"^_(Fld|Dim|Resource)(\d+)(?:$|[A-Za-z_])", physical) if match: requested.setdefault((match.group(1), int(match.group(2))), []).append(physical) if not requested: return {} records, error = live_dbnames_records(base_id, timeout_seconds=timeout_seconds) if error: return {} selected: dict[tuple[str, int], Any] = {} for record in records or []: key = (str(getattr(record, "storage_role", "") or ""), int(getattr(record, "sql_number", 0) or 0)) if key not in requested: continue current = selected.get(key) if current is None or str(getattr(current, "source", "") or "") != "DBNames": selected[key] = record guids = sorted({str(getattr(record, "guid", "") or "").lower() for record in selected.values() if is_guid_text(str(getattr(record, "guid", "") or ""))}) payloads, _, payload_error = read_storage_files_bytes(base_id, "Config", guids, timeout_seconds=timeout_seconds) if payload_error: payloads = {} root_rows, _ = live_base_root_metadata_index(base_id, table="Config", timeout_seconds=timeout_seconds) common_attribute_guids = {str(row.get("guid") or "").lower() for row in root_rows if row.get("kind") == "CommonAttribute"} result: dict[str, dict[str, Any]] = {} for key, record in selected.items(): guid = str(getattr(record, "guid", "") or "").lower() identity = config_identity_from_bytes((payloads or {}).get(guid) or b"") or {} name = str(identity.get("name") or "").strip() if not name: continue section = "common_attributes" if guid in common_attribute_guids else role_by_prefix.get(key[0], "attributes") for physical in requested.get(key) or []: result[physical] = {"name": name, "section": section, "guid": guid} return result def data_object_schema_uncached(payload: dict[str, Any]) -> dict[str, Any]: method = "data.schema" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error normalized = normalize_object_selector_aliases(payload, method) if isinstance(normalized, dict) and normalized.get("status") == "invalid_argument": return normalized if not has_object_selector(normalized): return invalid_argument(method, "selector", OBJECT_SELECTOR_REQUIRED_MESSAGE) card_result = get_object( normalized.get("kind"), str(normalized.get("name") or normalized.get("guid") or ""), base_id=base_id_or_error, include_storage=True, limit=20, timeout_seconds=int(normalized.get("timeout_seconds") or 60), ) if card_result.get("status") != "ok": card_result["method"] = method return card_result object_card = card_result.get("object") if isinstance(card_result.get("object"), dict) else card_result kind = str(object_card.get("kind") or "") storage = object_card.get("storage") if isinstance(object_card.get("storage"), dict) else {} dbname = next((row for row in storage.get("dbnames") or [] if isinstance(row, dict) and row.get("sql_number") is not None), None) if not dbname and kind: live_rows = list_objects( kind, base_id=base_id_or_error, limit=100, offset=0, include_storage=True, refresh_cache=True, name_filter=str(object_card.get("name") or normalized.get("name") or ""), ) wanted_guid = str(object_card.get("guid") or "").casefold() live_card = next( ( row for row in live_rows.get("objects") or [] if isinstance(row, dict) and ( (wanted_guid and str(row.get("guid") or "").casefold() == wanted_guid) or normalize(str(row.get("name") or "")) == normalize(str(object_card.get("name") or "")) ) ), None, ) if live_card: object_card = {**object_card, **live_card} storage = object_card.get("storage") if isinstance(object_card.get("storage"), dict) else {} dbname = next((row for row in storage.get("dbnames") or [] if isinstance(row, dict) and row.get("sql_number") is not None), None) prefix = DATA_TABLE_PREFIXES.get(kind) if not prefix or not dbname: return { "schema": "onec_data_schema.v1", "status": "unsupported_kind", "base_id": base_id_or_error, "object": public_metadata_row(object_card), "diagnostics": {"message": "This metadata kind has no universal SQL data-table route yet."}, } table = f"{prefix}{int(dbname['sql_number'])}" rows, error = data_sql_rows( base_id_or_error, "SELECT c.name, t.name AS type_name, c.max_length, c.precision, c.scale, c.is_nullable " "FROM sys.columns c JOIN sys.types t ON t.user_type_id=c.user_type_id " "JOIN sys.tables b ON b.object_id=c.object_id WHERE b.name=%s ORDER BY c.column_id", (table,), timeout_seconds=int(normalized.get("timeout_seconds") or 60), ) if error: error["method"] = method return error if not rows: return {"schema": "onec_data_schema.v1", "status": "source_missing", "base_id": base_id_or_error, "object": public_metadata_row(object_card), "diagnostics": {"message": "Physical data table was not found."}} attributes_result = metadata_object_attributes({**normalized, "base_id": base_id_or_error, "include_storage": True, "only": "all"}) logical_by_physical: dict[str, dict[str, Any]] = {} for section in ("dimensions", "resources", "attributes"): for item in attributes_result.get(section) or []: if not isinstance(item, dict): continue for route in item.get("storage_routes") or []: physical = str((route or {}).get("physical_name_candidate") or "") if physical: logical_by_physical[physical] = {"name": item.get("name"), "section": section, "type": item.get("type")} physical_names = [str(row.get("name") or "") for row in rows] unresolved_physical = [ physical for physical in physical_names if not DATA_SYSTEM_COLUMNS.get(physical) and not any(re.match(rf"^{re.escape(candidate)}(?:$|[A-Za-z_])", physical) for candidate in logical_by_physical) ] fallback_by_physical = data_fallback_field_routes( base_id_or_error, unresolved_physical, timeout_seconds=int(normalized.get("timeout_seconds") or 60), ) constant_value_type: dict[str, Any] | None = None if kind == "Constant": special = metadata_object_special_details( { "base_id": base_id_or_error, "kind": kind, "name": object_card.get("name"), "guid": object_card.get("guid"), "timeout_seconds": int(normalized.get("timeout_seconds") or 60), } ) details = special.get("details") if isinstance(special.get("details"), dict) else {} value_type = details.get("value_type") if isinstance(value_type, dict) and value_type.get("kind"): constant_value_type = value_type fields = [] constant_value_prefix = f"_Fld{int(dbname['sql_number']) + 1}" if kind == "Constant" else None for row in rows: physical = str(row.get("name") or "") logical = DATA_SYSTEM_COLUMNS.get(physical) descriptor = logical_by_physical.get(physical) if constant_value_prefix and re.match(rf"^{re.escape(constant_value_prefix)}(?:$|[A-Za-z_])", physical): logical = "value" descriptor = {"name": "value", "section": "value", "type": constant_value_type} if descriptor: logical = str(descriptor.get("name") or physical) if not logical: base = next( ( candidate for candidate in logical_by_physical if re.match(rf"^{re.escape(candidate)}(?:$|[A-Za-z_])", physical) ), None, ) if base: descriptor = logical_by_physical[base] logical = str(descriptor.get("name") or base) if not logical and physical in fallback_by_physical: descriptor = fallback_by_physical[physical] logical = str(descriptor.get("name") or physical) fields.append( { "name": logical or physical, "physical_name": physical, "section": (descriptor or {}).get("section") or "system", "type": (descriptor or {}).get("type") or {"kind": str(row.get("type_name") or "sql")}, "storage": {key: jsonable(row.get(key)) for key in ("type_name", "max_length", "precision", "scale", "is_nullable")}, } ) return { "schema": "onec_data_schema.v1", "status": "ok", "base_id": base_id_or_error, "object": public_metadata_row(object_card), "table": {"name": table, "row_kind": kind}, "fields": fields, "counts": {"fields": len(fields), "physical_columns": len(physical_names)}, } def data_schema_cache_key(payload: dict[str, Any]) -> str: """Use one cache entry for equivalent public selectors of the same object.""" base_id = str(payload.get("base_id") or "").strip().casefold() public_ref = str(payload.get("object_ref") or payload.get("ref") or "").strip() if public_ref and not re.fullmatch(r"[0-9a-fA-F-]{32,36}", public_ref): head, separator, tail = public_ref.partition(".") canonical_ref = f"{canonical_kind(head) or head}.{tail}" if separator else public_ref identity = {"ref": canonical_ref.casefold()} else: kind = canonical_kind(str(payload.get("kind") or payload.get("object_type") or "")) name = str(payload.get("name") or payload.get("object_name") or "").strip() guid = str(payload.get("guid") or payload.get("object_guid") or "").strip().casefold() if kind and name: identity = {"ref": f"{kind}.{name}".casefold()} elif guid: identity = {"guid": guid} else: identity = {"selector": "unresolved"} return json.dumps({"base_id": base_id, **identity}, ensure_ascii=False, sort_keys=True) def data_object_schema(payload: dict[str, Any]) -> dict[str, Any]: cache_key = data_schema_cache_key(payload) if not truthy(payload.get("refresh_cache")): with DATA_SCHEMA_CACHE_LOCK: cached = DATA_SCHEMA_CACHE.get(cache_key) if cached and time.time() - float(cached.get("cached_at") or 0) <= DATA_SCHEMA_CACHE_TTL_SECONDS: result = copy.deepcopy(cached.get("result") or {}) result["cache"] = {"status": "hit", "ttl_seconds": DATA_SCHEMA_CACHE_TTL_SECONDS} return result result = data_object_schema_uncached(payload) if result.get("status") == "ok": with DATA_SCHEMA_CACHE_LOCK: DATA_SCHEMA_CACHE[cache_key] = {"cached_at": time.time(), "result": copy.deepcopy(result)} result["cache"] = {"status": "miss", "ttl_seconds": DATA_SCHEMA_CACHE_TTL_SECONDS} return result def onec_data_value(value: Any, *, logical_name: str = "") -> Any: if isinstance(value, Decimal): return int(value) if value == value.to_integral_value() else float(value) if isinstance(value, datetime): if value.year >= 4000: try: value = value.replace(year=value.year - 2000) except ValueError: pass return value.isoformat() if isinstance(value, (bytes, bytearray)): raw = bytes(value) if len(raw) == 1 and logical_name in {"marked_for_deletion", "posted", "active", "completed", "started", "executed"}: return raw != b"\x00" if len(raw) == 16: return {"type": "reference", "hex": raw.hex().upper(), "guid_variants": access_identifier_guid_variants(f"00000000:{raw.hex()}")} return {"type": "binary", "bytes": len(raw), "hex": raw.hex() if len(raw) <= 64 else None} return value def onec_data_filter_value(value: Any, physical_name: str) -> Any: if isinstance(value, str) and (physical_name.endswith("RRef") or physical_name == "_IDRRef"): compact = value.replace("-", "").strip() if re.fullmatch(r"[0-9a-fA-F]{32}", compact): return bytes.fromhex(compact) if isinstance(value, bool): return b"\x01" if value else b"\x00" return value def data_record_ref(payload: dict[str, Any]) -> str: explicit = str(payload.get("record_ref") or "").replace("-", "").strip() if explicit: return explicit legacy = str(payload.get("ref") or "").replace("-", "").strip() return legacy if re.fullmatch(r"[0-9a-fA-F]{32}", legacy) else "" def data_schema_selector_payload(payload: dict[str, Any]) -> dict[str, Any]: selector = dict(payload) object_ref = selector.pop("object_ref", None) selector.pop("record_ref", None) selector.pop("recorder_ref", None) if object_ref not in {None, ""}: selector["ref"] = object_ref elif re.fullmatch(r"[0-9a-fA-F]{32}", str(payload.get("ref") or "").replace("-", "").strip()): selector.pop("ref", None) return selector def enum_value_public_map(base_id: str, selector: dict[str, Any], *, timeout_seconds: int) -> dict[str, dict[str, Any]]: properties = metadata_object_properties({**selector, "base_id": base_id, "timeout_seconds": timeout_seconds}) semantic = properties.get("properties") if isinstance(properties.get("properties"), dict) else {} result: dict[str, dict[str, Any]] = {} for section in semantic.get("sections") or []: if not isinstance(section, dict) or section.get("category") != "EnumValue": continue for record in section.get("records") or []: identity = record.get("identity") if isinstance(record, dict) and isinstance(record.get("identity"), dict) else {} guid = str(identity.get("guid") or "").lower() name = str(identity.get("name") or record.get("likely_name") or "") synonyms = identity.get("synonyms") if isinstance(identity.get("synonyms"), dict) else {} if guid and name: result[guid] = { "name": name, "synonym": next(iter(synonyms.values()), None), "value_ref": f"Enum.{(properties.get('object') or {}).get('name')}.EnumValue.{name}", } return result def enrich_enum_data_rows(rows: list[dict[str, Any]], values: dict[str, dict[str, Any]]) -> list[dict[str, Any]]: for row in rows: reference = row.get("ref") if isinstance(row, dict) and isinstance(row.get("ref"), dict) else {} identity = next( ( values.get(str(candidate or "").lower()) for candidate in reference.get("guid_variants") or [] if values.get(str(candidate or "").lower()) ), None, ) if identity: row.update({key: value for key, value in identity.items() if value is not None}) return rows def data_read(payload: dict[str, Any], *, count_only: bool = False, method: str | None = None) -> dict[str, Any]: method = method or ("data.count" if count_only else "data.list") schema = data_object_schema(data_schema_selector_payload(payload)) if schema.get("status") != "ok": schema["method"] = method return schema base_id = str(schema.get("base_id") or "") table = str((schema.get("table") or {}).get("name") or "") all_fields = schema.get("fields") or [] by_logical: dict[str, list[dict[str, Any]]] = {} for field in all_fields: by_logical.setdefault(str(field.get("name") or ""), []).append(field) requested_fields = payload.get("fields") if requested_fields is None: requested_names = list(by_logical) elif isinstance(requested_fields, list) and all(isinstance(item, str) for item in requested_fields): requested_names = list(dict.fromkeys(requested_fields)) else: return invalid_argument(method, "fields", "fields must be an array of logical field names.") unknown = [name for name in requested_names if name not in by_logical] if unknown: return invalid_argument(method, "fields", f"Unknown logical fields: {', '.join(unknown)}.", allowed_values=sorted(by_logical)) filters = payload.get("filters") or {} if not isinstance(filters, dict): return invalid_argument(method, "filters", "filters must be a JSON object with exact-match logical field values.") where = [] params: list[Any] = [] record_ref = data_record_ref(payload) if record_ref: ref = record_ref if not re.fullmatch(r"[0-9a-fA-F]{32}", ref): return invalid_argument(method, "record_ref", "record_ref must be a 32-character hexadecimal 1C reference id.") where.append("[_IDRRef]=%s") params.append(bytes.fromhex(ref)) for name, value in filters.items(): candidates = by_logical.get(str(name)) or [] if len(candidates) != 1: return invalid_argument(method, "filters", f"Field `{name}` is unknown or composite; exact scalar filtering is not available.") physical = str(candidates[0].get("physical_name") or "") if not re.fullmatch(r"_[A-Za-z0-9_]+", physical): return invalid_argument(method, "filters", f"Unsafe physical route for `{name}`.") where.append(f"[{physical}]=%s") params.append(onec_data_filter_value(value, physical)) if not payload.get("include_deleted") and "marked_for_deletion" in by_logical: where.append("[_Marked]=0x00") where_sql = " WHERE " + " AND ".join(where) if where else "" timeout = int(payload.get("timeout_seconds") or 30) if count_only: rows, error = data_sql_rows(base_id, f"SELECT COUNT_BIG(*) AS row_count FROM dbo.[{table}]{where_sql}", tuple(params), timeout_seconds=timeout) if error: return error return {"schema": "onec_data_count.v1", "status": "ok", "base_id": base_id, "object": schema.get("object"), "count": int((rows[0] or {}).get("row_count") or 0)} limit, limit_error = parse_int_argument(payload, "limit", method=method, default=100, minimum=1, maximum=1000) if limit_error: return limit_error offset, offset_error = parse_int_argument(payload, "offset", method=method, default=0, minimum=0, maximum=1000000) if offset_error: return offset_error select_parts = [] alias_map: dict[str, str] = {} alias_types: dict[str, dict[str, Any]] = {} for logical in requested_names: for index, field in enumerate(by_logical[logical]): physical = str(field.get("physical_name") or "") if not re.fullmatch(r"_[A-Za-z0-9_]+", physical): continue alias = logical if len(by_logical[logical]) == 1 else f"{logical}__{physical.rsplit('_', 1)[-1]}" select_parts.append(f"[{physical}] AS [{alias}]") alias_map[alias] = logical if isinstance(field.get("type"), dict): alias_types[alias] = field["type"] order_field = str(payload.get("order_by") or ("date" if "date" in by_logical else "ref" if "ref" in by_logical else requested_names[0])) order_candidates = by_logical.get(order_field) or [] order_physical = str((order_candidates[0] or {}).get("physical_name") or "") if len(order_candidates) == 1 else "" if not re.fullmatch(r"_[A-Za-z0-9_]+", order_physical): return invalid_argument(method, "order_by", "order_by must name one scalar logical field.", allowed_values=sorted(name for name, rows in by_logical.items() if len(rows) == 1)) direction = str(payload.get("order") or "asc").strip().casefold() if direction not in {"asc", "desc"}: return invalid_argument(method, "order", "order must be asc or desc.", allowed_values=["asc", "desc"]) sql = f"SELECT {', '.join(select_parts)} FROM dbo.[{table}]{where_sql} ORDER BY [{order_physical}] {direction.upper()} OFFSET %s ROWS FETCH NEXT %s ROWS ONLY" rows, error = data_sql_rows(base_id, sql, tuple([*params, int(offset or 0), int(limit or 100)]), timeout_seconds=timeout) if error: return error decoded = decode_data_rows(rows, alias_map, alias_types) if str((schema.get("object") or {}).get("kind") or "") == "Enum": decoded = enrich_enum_data_rows( decoded, enum_value_public_map( base_id, data_schema_selector_payload(payload), timeout_seconds=timeout, ), ) return { "schema": "onec_data_result.v1", "status": "ok", "base_id": base_id, "object": schema.get("object"), "rows": decoded, "counts": {"rows": len(decoded), "limit": int(limit or 100), "offset": int(offset or 0)}, "query": {"fields": requested_names, "filters": filters, "order_by": order_field, "order": direction}, } def decode_data_rows( rows: list[dict[str, Any]], alias_map: dict[str, str], alias_types: dict[str, dict[str, Any]] | None = None, ) -> list[dict[str, Any]]: alias_types = alias_types or {} decoded = [] for row in rows: item: dict[str, Any] = {} composite: dict[str, dict[str, Any]] = {} for alias, value in row.items(): logical = alias_map.get(alias, alias) type_info = alias_types.get(alias) or {} if type_info.get("kind") == "boolean" and isinstance(value, (bytes, bytearray)) and len(value) == 1: value = bytes(value) != b"\x00" if alias != logical and "__" in alias: composite.setdefault(logical, {})[alias.split("__", 1)[1]] = onec_data_value(value, logical_name=logical) else: item[logical] = onec_data_value(value, logical_name=logical) item.update({name: {"type": "composite", "parts": parts} for name, parts in composite.items()}) decoded.append(item) return decoded DATA_VIRTUAL_ALIASES = { "slice_last": "slice_last", "slicelast": "slice_last", "срезпоследних": "slice_last", "slice_first": "slice_first", "slicefirst": "slice_first", "срезпервых": "slice_first", "balances": "balances", "balance": "balances", "остатки": "balances", "turnovers": "turnovers", "turnover": "turnovers", "обороты": "turnovers", "balances_and_turnovers": "balances_and_turnovers", "balancesandturnovers": "balances_and_turnovers", "остаткииобороты": "balances_and_turnovers", } def data_virtual_datetime(value: Any, argument: str, method: str) -> tuple[datetime | None, dict[str, Any] | None]: if value in {None, ""}: return None, None if isinstance(value, datetime): parsed = value elif isinstance(value, date): parsed = datetime.combine(value, datetime.min.time()) elif isinstance(value, str): try: parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) except ValueError: return None, invalid_argument(method, argument, f"{argument} must be an ISO date or datetime.") else: return None, invalid_argument(method, argument, f"{argument} must be an ISO date or datetime.") if parsed.tzinfo is not None: parsed = parsed.astimezone(timezone.utc).replace(tzinfo=None) if parsed.year < 3000: try: parsed = parsed.replace(year=parsed.year + 2000) except ValueError: return None, invalid_argument(method, argument, f"{argument} is outside the supported 1C SQL date range.") return parsed, None def data_virtual(payload: dict[str, Any]) -> dict[str, Any]: method = "data.virtual" raw_virtual = str(payload.get("virtual_table") or payload.get("view") or "").strip() virtual = DATA_VIRTUAL_ALIASES.get(normalize(raw_virtual)) or DATA_VIRTUAL_ALIASES.get(raw_virtual.casefold()) if not virtual: return invalid_argument(method, "virtual_table", "Unsupported virtual table.", allowed_values=sorted(set(DATA_VIRTUAL_ALIASES.values()))) schema = data_object_schema(data_schema_selector_payload(payload)) if schema.get("status") != "ok": schema["method"] = method return schema base_id = str(schema.get("base_id") or "") kind = str((schema.get("object") or {}).get("kind") or "") if virtual.startswith("slice_") and kind != "InformationRegister": return invalid_argument(method, "virtual_table", "Slice views are available only for information registers.") if virtual in {"balances", "turnovers", "balances_and_turnovers"} and kind != "AccumulationRegister": return { "schema": "onec_data_virtual.v1", "status": "unsupported_register", "base_id": base_id, "object": schema.get("object"), "virtual_table": virtual, "diagnostics": {"message": "Universal SQL aggregation is enabled only for accumulation registers; accounting-register totals require register-specific account/subconto semantics."}, } table = str((schema.get("table") or {}).get("name") or "") fields = [field for field in schema.get("fields") or [] if isinstance(field, dict)] dimensions = [field for field in fields if field.get("section") in {"dimensions", "common_attributes"}] resources = [field for field in fields if field.get("section") == "resources"] by_name: dict[str, list[dict[str, Any]]] = {} for field in fields: by_name.setdefault(str(field.get("name") or ""), []).append(field) filters = payload.get("filters") or {} if not isinstance(filters, dict): return invalid_argument(method, "filters", "filters must be a JSON object with exact-match logical field values.") where: list[str] = [] params: list[Any] = [] for name, value in filters.items(): candidates = by_name.get(str(name)) or [] if len(candidates) != 1: return invalid_argument(method, "filters", f"Field `{name}` is unknown or composite; exact scalar filtering is not available.") physical = str(candidates[0].get("physical_name") or "") if not re.fullmatch(r"_[A-Za-z0-9_]+", physical): return invalid_argument(method, "filters", f"Unsafe physical route for `{name}`.") where.append(f"[{physical}]=%s") params.append(onec_data_filter_value(value, physical)) if "active" in by_name and "active" not in filters: where.append("[_Active]=0x01") start, start_error = data_virtual_datetime(payload.get("start"), "start", method) if start_error: return start_error end, end_error = data_virtual_datetime(payload.get("end") or payload.get("period"), "end", method) if end_error: return end_error limit, limit_error = parse_int_argument(payload, "limit", method=method, default=100, minimum=1, maximum=1000) if limit_error: return limit_error allow_full_scan, allow_full_scan_error = strict_bool_argument(payload, "allow_full_scan", method=method, default=False) if allow_full_scan_error: return allow_full_scan_error timeout = int(payload.get("timeout_seconds") or 60) def safe_field(field: dict[str, Any]) -> tuple[str, str] | None: physical = str(field.get("physical_name") or "") logical = str(field.get("name") or "") return (physical, logical) if re.fullmatch(r"_[A-Za-z0-9_]+", physical) and logical else None dimension_routes = [route for field in dimensions if (route := safe_field(field))] resource_routes = [route for field in resources if (route := safe_field(field))] if dimension_routes and not filters and not allow_full_scan: return invalid_argument( method, "filters", "At least one exact dimension filter is required for a virtual-table query unless allow_full_scan=true.", allowed_values=sorted({logical for _, logical in dimension_routes}), ) if virtual.startswith("slice_"): if "period" not in by_name: return {"schema": "onec_data_virtual.v1", "status": "unsupported_register", "base_id": base_id, "object": schema.get("object"), "virtual_table": virtual, "diagnostics": {"message": "This information register is not periodic and has no slice view."}} if end is not None: where.append("[_Period]<=%s") params.append(end) selected_fields = [field for field in fields if field.get("section") != "system" or field.get("name") in {"period", "active", "recorder_ref", "line_no"}] select_parts: list[str] = [] alias_map: dict[str, str] = {} logical_seen: dict[str, int] = {} for field in selected_fields: route = safe_field(field) if not route: continue physical, logical = route index = logical_seen.get(logical, 0) logical_seen[logical] = index + 1 alias = logical if index == 0 else f"{logical}__{physical.rsplit('_', 1)[-1]}" select_parts.append(f"[{physical}] AS [{alias}]") alias_map[alias] = logical partition = ", ".join(f"[{physical}]" for physical, _ in dimension_routes) order = "DESC" if virtual == "slice_last" else "ASC" where_sql = " WHERE " + " AND ".join(where) if where else "" partition_sql = f"PARTITION BY {partition} " if partition else "" sql = f"WITH ranked AS (SELECT {', '.join(select_parts)}, ROW_NUMBER() OVER ({partition_sql}ORDER BY [_Period] {order}) AS [_rn] FROM dbo.[{table}]{where_sql}) SELECT TOP {int(limit or 100)} {', '.join(f'[{alias}]' for alias in alias_map)} FROM ranked WHERE [_rn]=1" rows, error = data_sql_rows(base_id, sql, tuple(params), timeout_seconds=timeout) if error: return error decoded = decode_data_rows(rows, alias_map) else: if not resource_routes: return {"schema": "onec_data_virtual.v1", "status": "unsupported_register", "base_id": base_id, "object": schema.get("object"), "virtual_table": virtual, "diagnostics": {"message": "No numeric resources were resolved for this accumulation register."}} if "_RecordKind" not in {str(field.get("physical_name") or "") for field in fields}: return {"schema": "onec_data_virtual.v1", "status": "unsupported_register", "base_id": base_id, "object": schema.get("object"), "virtual_table": virtual, "diagnostics": {"message": "The accumulation register has no movement direction column."}} dim_select = [f"[{physical}] AS [{logical}]" for physical, logical in dimension_routes] group_sql = ", ".join(f"[{physical}]" for physical, _ in dimension_routes) alias_map = {logical: logical for _, logical in dimension_routes} aggregates: list[str] = [] select_params: list[Any] = [] if virtual == "balances": if end is None: return invalid_argument(method, "end", "end is required for balances.") where.append("[_Period]<=%s") params.append(end) for physical, logical in resource_routes: aggregates.append(f"SUM(CASE WHEN [_RecordKind]=0 THEN [{physical}] ELSE -[{physical}] END) AS [{logical}]") alias_map[logical] = logical elif virtual == "turnovers": if start is None or end is None: return invalid_argument(method, "start/end", "start and end are required for turnovers.") where.extend(["[_Period]>=%s", "[_Period]<=%s"]) params.extend([start, end]) for physical, logical in resource_routes: aggregates.append(f"SUM(CASE WHEN [_RecordKind]=0 THEN [{physical}] ELSE -[{physical}] END) AS [{logical}]") alias_map[logical] = logical else: if start is None or end is None: return invalid_argument(method, "start/end", "start and end are required for balances_and_turnovers.") where.append("[_Period]<=%s") params.append(end) for physical, logical in resource_routes: aggregates.extend( [ f"SUM(CASE WHEN [_Period]<%s THEN CASE WHEN [_RecordKind]=0 THEN [{physical}] ELSE -[{physical}] END ELSE 0 END) AS [{logical}__opening]", f"SUM(CASE WHEN [_Period]>=%s AND [_Period]<=%s THEN CASE WHEN [_RecordKind]=0 THEN [{physical}] ELSE -[{physical}] END ELSE 0 END) AS [{logical}__turnover]", f"SUM(CASE WHEN [_RecordKind]=0 THEN [{physical}] ELSE -[{physical}] END) AS [{logical}__closing]", ] ) select_params.extend([start, start, end]) alias_map.update({f"{logical}__opening": logical, f"{logical}__turnover": logical, f"{logical}__closing": logical}) where_sql = " WHERE " + " AND ".join(where) if where else "" select_sql = ", ".join([*dim_select, *aggregates]) sql = f"SELECT TOP {int(limit or 100)} {select_sql} FROM dbo.[{table}]{where_sql}{f' GROUP BY {group_sql}' if group_sql else ''}" rows, error = data_sql_rows(base_id, sql, tuple([*select_params, *params]), timeout_seconds=timeout) if error: return error decoded = decode_data_rows(rows, alias_map) return { "schema": "onec_data_virtual.v1", "status": "ok", "base_id": base_id, "object": schema.get("object"), "virtual_table": virtual, "rows": decoded, "counts": {"rows": len(decoded), "limit": int(limit or 100)}, "query": {"start": payload.get("start"), "end": payload.get("end") or payload.get("period"), "filters": filters}, } def data_present(payload: dict[str, Any]) -> dict[str, Any]: method = "data.present" record_ref = data_record_ref(payload) if not record_ref: return invalid_argument(method, "record_ref", "record_ref is required for data.present.") schema = data_object_schema(data_schema_selector_payload(payload)) if schema.get("status") != "ok": return schema names = {str(field.get("name") or "") for field in schema.get("fields") or []} fields = [name for name in ("ref", "description", "code", "number", "date") if name in names] result = data_read({**payload, "record_ref": record_ref, "fields": fields, "limit": 1}, method=method) if result.get("status") != "ok": return result row = (result.get("rows") or [None])[0] if not isinstance(row, dict): return {"schema": "onec_data_presentation.v1", "status": "not_found", "base_id": result.get("base_id"), "object": result.get("object")} presentation = row.get("description") or row.get("number") or row.get("code") or record_ref return {"schema": "onec_data_presentation.v1", "status": "ok", "base_id": result.get("base_id"), "object": result.get("object"), "ref": row.get("ref"), "presentation": presentation, "record": row} def data_movements(payload: dict[str, Any]) -> dict[str, Any]: method = "data.movements" recorder_ref = payload.get("recorder_ref") or data_record_ref(payload) if not recorder_ref: return invalid_argument(method, "recorder_ref", "recorder_ref is required.") schema = data_object_schema(data_schema_selector_payload(payload)) if schema.get("status") != "ok": return schema kind = str(((schema.get("object") or {}).get("kind") or "")) if kind not in {"InformationRegister", "AccumulationRegister", "AccountingRegister"}: return invalid_argument(method, "selector", "data.movements requires a register object selector.") names = {str(field.get("name") or "") for field in schema.get("fields") or []} if "recorder_ref" not in names: return {"schema": "onec_data_movements.v1", "status": "unsupported_register", "base_id": schema.get("base_id"), "object": schema.get("object"), "diagnostics": {"message": "This register has no recorder field and is not subordinate to a recorder."}} filters = dict(payload.get("filters") or {}) filters["recorder_ref"] = recorder_ref result = data_read({**payload, "record_ref": None, "filters": filters}, method=method) if result.get("status") == "ok": result["schema"] = "onec_data_movements.v1" return result def parse_module_id(module_id: str) -> tuple[str | None, str | None, int | None]: if ":" not in module_id: return None, None, None table, rest = module_id.split(":", 1) file_name, fragment = (rest.split("#", 1) + [""])[:2] if "#" in rest else (rest, "") if table not in STORAGE_TABLES or Path(file_name).name != file_name: return None, None, None stream_index = None if fragment: if fragment in {"form_module", "bsl", "bsl_container"}: return table, file_name, None match = re.fullmatch(r"stream[:=](\d+)", fragment) if not match: return None, None, None stream_index = int(match.group(1)) return table, file_name, stream_index MODULE_READ_MODES = ["text", "summary", "routines", "routines_only"] def validate_modules_read_arguments(payload: dict[str, Any]) -> dict[str, Any] | None: for name in ["include_storage", "include_text", "summary", "routines_only", "include_container_preview"]: _, bool_error = strict_bool_argument(payload, name, method="modules.read", default=False) if bool_error: return bool_error mode = payload.get("mode") if mode not in {None, ""}: if not isinstance(mode, str): return invalid_argument("modules.read", "mode", "mode must be a JSON string.", allowed_values=MODULE_READ_MODES) if mode.strip().casefold() not in MODULE_READ_MODES: return invalid_argument("modules.read", "mode", f"Unsupported mode `{mode}`.", allowed_values=MODULE_READ_MODES) _, preview_error = strict_bool_argument(payload, "preview", method="modules.read", default=False) if preview_error: return preview_error if "routine_name" in payload and payload.get("routine_name") is not None and not isinstance(payload.get("routine_name"), str): return invalid_argument("modules.read", "routine_name", "routine_name must be a JSON string.") string_error = validate_optional_non_empty_string_arguments(payload, "modules.read", ["module_id", "module_ref"]) if string_error: return string_error table_error = metadata_storage_table(payload, "modules.read") if isinstance(table_error, dict): return table_error _, bsl_offset_error = parse_int_argument(payload, "bsl_offset", method="modules.read", default=0, minimum=0) if bsl_offset_error: return bsl_offset_error _, offset_error = parse_int_argument(payload, "offset", method="modules.read", default=0, minimum=0) if offset_error: return offset_error _, max_chars_error = parse_int_argument(payload, "max_chars", method="modules.read", default=1, minimum=1) if max_chars_error: return max_chars_error _, container_preview_chars_error = parse_int_argument(payload, "container_preview_chars", method="modules.read", default=1000, minimum=1) if container_preview_chars_error: return container_preview_chars_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="modules.read", default=30, minimum=1) if timeout_error: return timeout_error for name in ("module_ordinal", "module_index", "module_number"): if name in payload and (payload.get(name) is None or payload.get(name) == ""): return invalid_argument("modules.read", name, f"{name} must be a JSON integer when provided.") module_ordinal_value = first_non_empty_arg(payload, "module_ordinal", "module_index", "module_number") if module_ordinal_value is not None: _, ordinal_error = parse_ordinal(module_ordinal_value, "modules.read", argument="module_ordinal") if ordinal_error: return ordinal_error return None def module_text_response( text: str, payload: dict[str, Any], *, include_text_default: bool = True, ) -> dict[str, Any]: normalized = text.replace("\r\n", "\n").replace("\r", "\n") lines = normalized.split("\n") if normalized else [] try: from parser.bsl_validation import routine_blocks except Exception: routine_blocks = None routines = list(routine_blocks(normalized)) if routine_blocks else [] public_routines = [ { "kind": routine.get("kind"), "name": routine.get("name"), "line_start": routine.get("line_start"), "line_end": routine.get("line_end"), } for routine in routines ] routine_name = str(payload.get("routine_name") or "").strip() selected_text = normalized selected_range: dict[str, Any] | None = None if routine_name: wanted = normalize(routine_name) routine = next((item for item in routines if normalize(str(item.get("name") or "")) == wanted), None) if routine: start = max(1, int(routine.get("line_start") or 1)) end = max(start, int(routine.get("line_end") or start)) selected_text = "\n".join(lines[start - 1 : end]) canonical_routine_name = str(routine.get("name") or "") if routine_name == canonical_routine_name: match_by = "routine_exact" elif routine_name.casefold() == canonical_routine_name.casefold(): match_by = "routine_case_insensitive" else: match_by = "routine_normalized" selected_range = {"routine_name": canonical_routine_name, "line_start": start, "line_end": end, "match_by": match_by} else: selected_text = "" selected_range = {"routine_name": routine_name, "status": "not_found"} offset, offset_error = parse_int_argument(payload, "offset", method="modules.read", default=0, minimum=0) default_max_chars = 4000 if truthy(payload.get("preview")) else len(selected_text) max_chars, max_chars_error = parse_int_argument(payload, "max_chars", method="modules.read", default=default_max_chars, minimum=1) mode = str(payload.get("mode") or "").strip().casefold() include_text = truthy(payload.get("include_text", "1" if include_text_default else "0")) summary_requested = mode in {"summary", "routines", "routines_only"} or truthy(payload.get("summary")) or truthy(payload.get("routines_only")) or not include_text if mode in {"summary", "routines", "routines_only"}: include_text = False result: dict[str, Any] = { "summary": { "chars": len(normalized), "lines": len(lines), "routines": len(public_routines), "procedures": sum(1 for item in public_routines if str(item.get("kind") or "").casefold() == "процедура"), "functions": sum(1 for item in public_routines if str(item.get("kind") or "").casefold() == "функция"), }, "routines": public_routines, } if selected_range: result["selection"] = selected_range if offset_error or max_chars_error: argument_error = offset_error or max_chars_error or {} result["status"] = "invalid_argument" result["error"] = argument_error.get("error", "invalid_argument") result["argument"] = argument_error.get("argument") result["diagnostics"] = argument_error.get("diagnostics") return result fragment = selected_text[int(offset or 0) : int(offset or 0) + int(max_chars or 0)] if selected_range and selected_range.get("status") == "not_found": result["status"] = "not_found" result["method"] = "modules.read" result["error"] = "routine_not_found" result["diagnostics"] = {"message": f"Процедура или функция `{routine_name}` не найдена в модуле."} elif int(offset or 0) > len(selected_text): result["status"] = "range_not_satisfiable" result["error"] = "offset_out_of_range" result["diagnostics"] = {"message": f"offset {offset} больше размера выбранного текста {len(selected_text)}."} preview_requested = truthy(payload.get("preview")) if include_text and not summary_requested: result["text"] = fragment result["text_range"] = { "offset": offset, "chars": len(fragment), "total_chars": len(selected_text), "truncated": offset + len(fragment) < len(selected_text), "mode": "preview" if preview_requested else "text", } if preview_requested and include_text and not summary_requested: result["preview"] = fragment return result def cached_module_owner_payload(base_id: str, module_id: str) -> dict[str, Any] | None: config, _ = sql_config_for_base(base_id) if not config: return None cached = metadata_module_owner_cache_lookup(config, module_id) if not cached: return None owner_payload = cached.get("owner") if isinstance(cached.get("owner"), dict) else {} module_payload = cached.get("module_payload") if isinstance(cached.get("module_payload"), dict) else {} if not owner_payload: return None return { "owner": { "status": "resolved" if owner_payload.get("guid") else "partial", "kind": owner_payload.get("kind"), "name": owner_payload.get("name"), "synonym": owner_payload.get("synonym"), "guid": owner_payload.get("guid"), "source": "metadata.module_owner_cache", }, "module": module_payload, } def module_ref_matches(candidate: str, wanted: str) -> bool: if candidate == wanted: return True candidate_table, candidate_file_name, candidate_stream_index = parse_module_id(candidate) wanted_table, wanted_file_name, wanted_stream_index = parse_module_id(wanted) if not candidate_table or not candidate_file_name or not wanted_table or not wanted_file_name: return False if candidate_table != wanted_table or candidate_file_name != wanted_file_name: return False if wanted_stream_index is None: return True return candidate_stream_index == wanted_stream_index def extension_module_owner_payload( base_id: str, module_id: str, *, table: str, timeout_seconds: int, ) -> dict[str, Any] | None: if table not in {"ConfigCAS", "ConfigCASSave"}: return None if table in FORM_ELEMENT_SAVED_STATE_TABLES: _, _, stream_index = parse_module_id(module_id) return { "owner": { "status": "partial", "kind": "saved_state", "name": None, "synonym": None, "guid": None, "source": "direct_saved_state_module_ref", }, "module": { "module_name": "Saved-state module", "stream_index": stream_index, }, "origin": { "source": "saved_state", "presentation": "Saved-state Configurator layer", "status": "ok", }, } guid_sources, source_error = extension_definition_guid_sources(base_id, timeout_seconds=timeout_seconds) if source_error or not guid_sources: return None owner_table = "ConfigCASSave" if table == "ConfigCASSave" else "ConfigCAS" for owner_guid, sources in sorted(guid_sources.items()): if not is_guid_text(owner_guid): continue modules_result = metadata_object_modules( { "base_id": base_id, "guid": owner_guid, "table": owner_table, "include_storage": True, "timeout_seconds": timeout_seconds, } ) if modules_result.get("status") != "ok": continue modules = [module for module in modules_result.get("modules") or [] if isinstance(module, dict)] for ordinal, module in enumerate(modules, start=1): candidate_module_id = str(module.get("module_id") or "").strip() if not candidate_module_id or not module_ref_matches(candidate_module_id, module_id): continue object_info = modules_result.get("object") if isinstance(modules_result.get("object"), dict) else {} source_item = next((source for source in sources or [] if isinstance(source, dict)), {}) extension = source_item.get("extension") if isinstance(source_item.get("extension"), dict) else {} _, _, candidate_stream_index = parse_module_id(candidate_module_id) public_module = public_module_row( module, include_storage=False, ordinal=ordinal, owner_kind=object_info.get("kind"), owner_name=object_info.get("name"), ) return { "owner": { "status": "resolved" if object_info.get("guid") else "partial", "kind": object_info.get("kind"), "name": object_info.get("name"), "synonym": object_info.get("synonym"), "guid": object_info.get("guid") or owner_guid, "source": "extension_definition_guid_sources", }, "module": { "module_name": public_module.get("name"), "module_ordinal": ordinal, "stream_index": candidate_stream_index, }, "origin": { "source": "extension", "presentation": "Расширение", "extension": extension or {"guid": None, "name": None, "active": None}, "status": "ok" if extension.get("name") else "extension_unresolved", }, } return None def module_origin_from_storage_table(table: str) -> dict[str, Any]: table_name = str(table or "").strip() if table_name == "Config": return { "source": "configuration", "presentation": "Конфигурация", "status": "ok", "storage_table": table_name, "write_surface": "base_saved_state", } if table_name == "ConfigSave": return { "source": "saved_state", "presentation": "Saved-state Configurator layer", "status": "ok", "storage_table": table_name, "write_surface": "base_saved_state", } if table_name == "ConfigCASSave": return { "source": "saved_state", "presentation": "Saved-state Configurator layer", "status": "ok", "storage_table": table_name, "write_surface": "saved_state", "diagnostics": {"message": "Owner extension/base layer requires resolved module owner evidence."}, } if table_name == "ConfigCAS": return { "source": "cas_reference", "presentation": "CAS module reference", "status": "owner_unresolved", "storage_table": table_name, "write_surface": "requires_owner_resolution", "diagnostics": {"message": "ConfigCAS may contain base or extension payloads; resolve owner before write planning."}, } return { "source": "unknown", "presentation": "Unknown module source", "status": "unknown", "storage_table": table_name or None, "write_surface": "requires_owner_resolution", } def merge_module_owner_context(result: dict[str, Any], owner_context: dict[str, Any] | None) -> dict[str, Any]: if not owner_context: return result owner = owner_context.get("owner") if isinstance(owner_context.get("owner"), dict) else None module_payload = owner_context.get("module") if isinstance(owner_context.get("module"), dict) else {} origin = owner_context.get("origin") if isinstance(owner_context.get("origin"), dict) else None if owner and not result.get("owner"): result["owner"] = owner if origin and not result.get("origin"): result["origin"] = origin form_from_context = owner_context.get("form") if isinstance(owner_context.get("form"), dict) else None if form_from_context and not result.get("form"): result["form"] = form_from_context module = result.get("module") if isinstance(result.get("module"), dict) else {} if module_payload: context_module_name = module_payload.get("module_name") or module_payload.get("name") if context_module_name and module.get("name") in {None, "", "BSL module", "Модуль БСЛ"}: module["name"] = context_module_name if context_module_name == "Модуль формы" and module.get("kind") in {None, "", "bsl_container_module", "container_payload"}: module["kind"] = "form_module" if module_payload.get("module_ordinal") is not None and module.get("module_ordinal") is None: module["module_ordinal"] = module_payload.get("module_ordinal") if module_payload.get("stream_index") is not None and module.get("stream_index") is None: module["stream_index"] = module_payload.get("stream_index") if module_payload.get("form") and not module.get("form"): module["form"] = module_payload.get("form") if module: result["module"] = module form_payload = result.get("form") if isinstance(result.get("form"), dict) else form_from_context qualified_name = public_code_qualified_name(owner=owner, form=form_payload, module=module) if qualified_name: result["qualified_name"] = qualified_name result["display_name"] = qualified_name return result def read_module(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, "modules.read") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, "modules.read") if isinstance(base_id_or_error, dict): return base_id_or_error argument_error = validate_modules_read_arguments(payload) if argument_error: return argument_error table_for_read = metadata_storage_table(payload, "modules.read") if isinstance(table_for_read, dict): return table_for_read include_storage = bool(payload.get("include_storage", False)) module_id = str(payload.get("module_id") or payload.get("module_ref") or "") selected_module: dict[str, Any] | None = None owner_context: dict[str, Any] | None = None if not module_id: module_ordinal_value = first_non_empty_arg(payload, "module_ordinal", "module_index", "module_number", default=1) module_ordinal, ordinal_error = parse_ordinal(module_ordinal_value, "modules.read", argument="module_ordinal") if ordinal_error: return ordinal_error modules_result = metadata_object_modules({**payload, "include_storage": True, "table": table_for_read}) if modules_result.get("status") != "ok": return public_error_result(modules_result, include_storage=include_storage, method="modules.read") object_info = modules_result.get("object") if isinstance(modules_result.get("object"), dict) else {} owner_context = { "owner": { "status": "resolved" if object_info.get("guid") else "partial", "kind": object_info.get("kind") or canonical_kind(str(payload.get("kind") or "")), "name": object_info.get("name") or payload.get("name"), "synonym": object_info.get("synonym"), "guid": object_info.get("guid") or payload.get("guid"), "source": "live_metadata", }, "module": {"module_ordinal": module_ordinal}, } modules = [module for module in modules_result.get("modules") or [] if isinstance(module, dict)] index = int(module_ordinal or 1) - 1 if index < 0 or index >= len(modules): return { "schema": "onec_module_read.v1", "method": "modules.read", "status": "not_found", "error": "not_found", "base_id": base_id_or_error, "source": {"kind": "live_metadata"}, "query": { "guid": payload.get("guid"), "kind": payload.get("kind"), "name": payload.get("name"), "ordinal": first_non_empty_arg(payload, "ordinal", "index", "object_index"), "module_ordinal": module_ordinal, }, "diagnostics": {"message": f"Module ordinal {module_ordinal} was not found for the selected object."}, } selected_module = modules[index] module_id = str(selected_module.get("module_id") or "") table, file_name, stream_index = parse_module_id(module_id) if not table or not file_name: return { "schema": "onec_adapter_request_error.v1", "method": "modules.read", "status": "error", "error": "invalid_module_id", "diagnostics": {"message": MODULE_READ_SELECTOR_OR_MODULE_ID_MESSAGE}, } if module_id and owner_context is None: cache_config, _ = sql_config_for_base(base_id_or_error) cached_form_owner = metadata_form_owner_cache_lookup(cache_config, module_ref=module_id) if cached_form_owner: cached_owner = cached_form_owner.get("owner") if isinstance(cached_form_owner.get("owner"), dict) else {} cached_form = cached_form_owner.get("form") if isinstance(cached_form_owner.get("form"), dict) else {} owner_context = { "owner": { "status": "resolved", "kind": cached_owner.get("kind") or cached_form.get("kind"), "name": cached_owner.get("name") or cached_form.get("name"), "synonym": cached_owner.get("synonym"), "guid": cached_owner.get("guid") or cached_form.get("guid"), "source": "metadata_form_owner_cache", }, "module": { "module_name": "Модуль формы", "module_ordinal": None, "stream_index": None, "form": cached_form.get("name"), }, "origin": { "source": "extension" if cached_form_owner.get("extension") else "metadata_form_owner_cache", "presentation": "Расширение" if cached_form_owner.get("extension") else "Индекс форм", "extension": cached_form_owner.get("extension"), "status": "ok", }, } else: owner_context = cached_module_owner_payload(base_id_or_error, module_id) direct_container_module_ref = stream_index is None and ( "bsl_offset" in payload or "#form_module" in module_id or "#bsl" in module_id or "#bsl_container" in module_id ) if owner_context is None and direct_container_module_ref: owner_context = { "owner": { "status": "partial", "kind": None, "name": None, "synonym": None, "guid": None, "source": "direct_container_module_ref", "diagnostics": { "message": "Контейнерный BSL формы прочитан напрямую по SQL module_ref/bsl_offset; точный владелец формы требует отдельной индексации metadata.", }, }, "module": { "module_name": "Модуль формы", "stream_index": None, }, "origin": module_origin_from_storage_table(table), } if owner_context is None: owner_context = extension_module_owner_payload( base_id_or_error, module_id, table=table, timeout_seconds=int(payload.get("timeout_seconds") or 30), ) if table in FORM_ELEMENT_SAVED_STATE_TABLES and ( owner_context is None or not isinstance(owner_context.get("owner"), dict) or not str((owner_context.get("owner") or {}).get("name") or "").strip() ): saved_context = saved_state_public_module_context( base_id=base_id_or_error, table=table, file_name=file_name, object_kind=canonical_kind(str(payload.get("object_type") or payload.get("kind") or "")) or None, timeout_seconds=int(payload.get("timeout_seconds") or 30), prefer_form_module=stream_index is None, ) if saved_context: existing_context = owner_context or {} owner_context = {**existing_context, **saved_context} if isinstance(existing_context.get("module"), dict) or isinstance(saved_context.get("module"), dict): owner_context["module"] = {**(existing_context.get("module") or {}), **(saved_context.get("module") or {})} if isinstance(existing_context.get("owner"), dict) or isinstance(saved_context.get("owner"), dict): owner_context["owner"] = {**(existing_context.get("owner") or {}), **(saved_context.get("owner") or {})} if isinstance(existing_context.get("origin"), dict) or isinstance(saved_context.get("origin"), dict): owner_context["origin"] = {**(existing_context.get("origin") or {}), **(saved_context.get("origin") or {})} if owner_context is None: owner_context = {} if not isinstance(owner_context.get("origin"), dict): owner_context["origin"] = module_origin_from_storage_table(table) data, config, error = read_storage_file_bytes(base_id_or_error, table, file_name, timeout_seconds=int(payload.get("timeout_seconds") or 30)) if error: error["method"] = "modules.read" return error if stream_index is not None: try: from parser.cas_payload import classify_payload except Exception as exc: return { "schema": "onec_module_read.v1", "status": "error", "base_id": base_id_or_error, "module_id": module_id, "diagnostics": {"message": f"Payload classifier is unavailable: {exc}"}, } classified = classify_payload(data, include_text=True) streams = classified.get("stream_blocks") or [] if stream_index < 0 or stream_index >= len(streams): result = { "schema": "onec_module_read.v1", "status": "not_found", "base_id": base_id_or_error, "diagnostics": {"message": "Stream index was not found in the requested payload."}, } if include_storage: result["module_id"] = module_id return result stream = streams[stream_index] stream_text = repair_bsl_mojibake_text(str(stream.get("text") or "")) text_info = module_text_response(stream_text, payload) descriptor_identity = saved_state_module_owner_identity( base_id=base_id_or_error, table=table, file_name=file_name, timeout_seconds=int(payload.get("timeout_seconds") or 30), ) if table in FORM_ELEMENT_SAVED_STATE_TABLES else None effective_owner_kind = canonical_kind(str(payload.get("object_type") or payload.get("kind") or "")) or ( descriptor_identity.get("kind") if isinstance(descriptor_identity, dict) else None ) saved_state_module_role = saved_state_bsl_module_role( file_name, owner_kind=effective_owner_kind, ) if table in FORM_ELEMENT_SAVED_STATE_TABLES else {} result = { "schema": "onec_module_read.v1", "status": text_info.pop("status", "ok"), "base_id": base_id_or_error, "source": {"kind": "live_metadata"}, **( {"module": public_module_row( selected_module, include_storage=False, ordinal=int(payload.get("module_ordinal") or payload.get("module_index") or payload.get("module_number") or 1), owner_kind=owner_context.get("owner", {}).get("kind") if isinstance(owner_context.get("owner"), dict) else None, owner_name=owner_context.get("owner", {}).get("name") if isinstance(owner_context.get("owner"), dict) else None, )} if selected_module else ({"module": saved_state_module_role} if saved_state_module_role else {}) ), **text_info, } if descriptor_identity and descriptor_identity.get("name"): result["owner"] = { "status": "resolved", "kind": effective_owner_kind or "Catalog", "name": descriptor_identity.get("name"), "synonym": descriptor_identity.get("synonym"), "guid": descriptor_identity.get("guid"), "source": "saved_state_descriptor", } if include_storage: result["module_id"] = module_id result["source"] = {"kind": "live_sql", "database": config["database"], "table": table, "file_name": file_name, "stream_index": stream_index} result["payload"] = { "role": classified.get("role"), "compression": classified.get("compression"), "raw_bytes": classified.get("raw_bytes"), "payload_bytes": classified.get("payload_bytes"), "stream": {key: value for key, value in stream.items() if key != "text"}, } return merge_module_owner_context(result, owner_context) decoded = payload_text_from_bytes(data) if decoded.get("status") != "ok": try: from parser.cas_payload import classify_payload classified = classify_payload(data, include_text=True) bsl_stream_indexes = [ index for index, stream in enumerate(classified.get("stream_blocks") or []) if stream.get("has_bsl_marker") and str(stream.get("text") or "").strip() ] except Exception: bsl_stream_indexes = [] if len(bsl_stream_indexes) == 1: fallback_payload = dict(payload) fallback_payload.pop("module_id", None) fallback_payload["module_ref"] = f"{table}:{file_name}#stream:{bsl_stream_indexes[0]}" return read_module(fallback_payload) container_text = str(decoded.pop("text", None) or "") bsl_offset = int(payload["bsl_offset"]) if "bsl_offset" in payload else None text, extraction = extract_bsl_text_from_container(container_text, bsl_offset=bsl_offset) if extraction.get("status") == "ok" and ( bsl_offset is not None or "#form_module" in module_id or "#bsl" in module_id or "#bsl_container" in module_id ): text = form_embedded_module_public_text(str(text or "")) text_info = module_text_response(str(text or ""), payload) result = { "schema": "onec_module_read.v1", "status": text_info.pop("status", decoded.get("status")), "base_id": base_id_or_error, "source": {"kind": "live_metadata"}, "module": ( public_module_row( selected_module, include_storage=False, ordinal=int(payload.get("module_ordinal") or payload.get("module_index") or payload.get("module_number") or 1), owner_kind=owner_context.get("owner", {}).get("kind") if isinstance(owner_context.get("owner"), dict) else None, owner_name=owner_context.get("owner", {}).get("name") if isinstance(owner_context.get("owner"), dict) else None, ) if selected_module else {"kind": "bsl_container_module" if extraction.get("status") == "ok" else "container_payload", "name": "BSL module"} ), "extraction": extraction, **text_info, } if bool(payload.get("include_container_preview", False)): container_preview_chars, _ = parse_int_argument(payload, "container_preview_chars", method="modules.read", default=1000, minimum=1) result["container_preview"] = container_text[: int(container_preview_chars or 1000)] if include_storage: result["module_id"] = module_id result["source"] = {"kind": "live_sql", "database": config["database"], "table": table, "file_name": file_name} result["payload"] = decoded return merge_module_owner_context(result, owner_context) STANDARD_OBJECT_MEMBERS = { "Catalog": {"Наименование", "Код", "ПометкаУдаления", "Ссылка"}, "Document": {"Дата", "Номер", "ПометкаУдаления", "Ссылка", "Проведен"}, } def bsl_symbol_expression_parts(expression: str) -> list[str]: return [part.strip() for part in str(expression or "").split(".") if part.strip()] def bsl_declared_symbols(text: str) -> set[str]: symbols: set[str] = set() for match in re.finditer(r"(?im)^\s*Перем\s+([^;\n]+)", text or ""): for part in re.split(r",", match.group(1)): name = re.sub(r"\s+Экспорт\b", "", part, flags=re.IGNORECASE).strip() if name: symbols.add(name) for match in re.finditer(r"(?im)^\s*(?:Для\s+Каждого|Для каждого)\s+([A-Za-zА-Яа-я_][\wА-Яа-я]*)\s+Из\b", text or ""): symbols.add(match.group(1)) for match in re.finditer(r"(?m)^\s*([A-Za-zА-Яа-я_][\wА-Яа-я]*)\s*=", text or ""): symbols.add(match.group(1)) return symbols def bsl_routine_params(text: str, routine_name: str | None) -> set[str]: if not routine_name: return set() wanted = normalize(routine_name) pattern = re.compile(r"(?im)^\s*(?:Асинх\s+)?(?:Процедура|Функция)\s+([A-Za-zА-Яа-я_][\wА-Яа-я]*)\s*\(([^)]*)\)") params: set[str] = set() for match in pattern.finditer(text or ""): if normalize(match.group(1)) != wanted: continue for raw in match.group(2).split(","): cleaned = re.sub(r"(?i)\b(Знач|Val)\b", "", raw).strip() cleaned = cleaned.split("=")[0].strip() if cleaned: params.add(cleaned) return params def bsl_symbol_is_full_metadata_path(parts: list[str]) -> bool: return len(parts) >= 2 and canonical_kind(parts[0]) in set(KIND_CAPABILITIES) def code_symbol_resolve(payload: dict[str, Any]) -> dict[str, Any]: method = "code.symbol.resolve" payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error expression = str(first_non_empty_arg(payload, "expression", "symbol", "path", default="") or "").strip() parts = bsl_symbol_expression_parts(expression) result: dict[str, Any] = { "schema": "onec_bsl_symbol_resolution.v1", "method": method, "status": "unresolved", "base_id": base_id_or_error, "path_kind": "code_symbol", "query": { "expression": expression, "routine_name": payload.get("routine_name"), "module_ref": payload.get("module_ref"), "module_id": payload.get("module_id"), "kind": payload.get("kind"), "name": payload.get("name"), "ref": payload.get("ref"), }, "segments": parts, } if not parts: return invalid_argument(method, "expression", "expression must be a non-empty BSL expression.") if bsl_symbol_is_full_metadata_path(parts): origin = metadata_definition_find( { "base_id": base_id_or_error, "query": parts[-1], "kind": parts[0], "name": parts[1], "areas": ["metadata", "object", "extensions"], "use_cache": payload.get("use_cache", False), "timeout_seconds": payload.get("timeout_seconds", 30), } ) compact = metadata_write_plan_compact_origin_lookup(origin) if origin.get("status") == "ok" else origin canonical_path = ".".join(parts) return { **result, "status": "resolved" if origin.get("status") == "ok" and (origin.get("matches") or origin.get("object")) else "unresolved", "resolution_kind": "metadata_path", "path_kind": "metadata_path", "canonical_path": canonical_path, "safe_as_metadata_path": bool(origin.get("status") == "ok" and (origin.get("matches") or origin.get("object"))), "origin_lookup": compact, } module_result = read_module( { **payload, "base_id": base_id_or_error, "include_text": True, "max_chars": payload.get("max_chars", 200000), } ) result["module_read"] = { "status": module_result.get("status"), "schema": module_result.get("schema"), "owner": module_result.get("owner"), "module": module_result.get("module"), } text = str(module_result.get("text") or module_result.get("preview") or "") if module_result.get("status") not in {"ok", "partial"} or not text: result.update( { "reason": "module_context_not_read", "safe_as_metadata_path": False, "diagnostics": {"message": "Module text is required to distinguish local BSL symbols from metadata paths."}, } ) return result first = parts[0] params = bsl_routine_params(text, str(payload.get("routine_name") or "")) param = next((item for item in params if normalize(item) == normalize(first)), None) if param: result.update( { "status": "resolved", "resolution_kind": "parameter", "symbol": param, "context_path": ".".join([param, *parts[1:]]), "safe_as_metadata_path": False, } ) return result local = next((item for item in bsl_declared_symbols(text) if normalize(item) == normalize(first)), None) if local: result.update( { "status": "resolved", "resolution_kind": "local_variable", "symbol": local, "context_path": ".".join([local, *parts[1:]]), "safe_as_metadata_path": False, } ) return result owner = module_result.get("owner") if isinstance(module_result.get("owner"), dict) else {} owner_kind = payload.get("kind") or owner.get("kind") owner_name = payload.get("name") or owner.get("name") if owner_kind and owner_name: attrs = metadata_object_attributes( { "base_id": base_id_or_error, "kind": owner_kind, "name": owner_name, "view": payload.get("view", "effective"), "limit": 5000, } ) members: list[dict[str, Any]] = [] for area_name in ("attributes", "dimensions", "resources", "tabular_sections", "forms", "commands", "modules"): for item in attrs.get(area_name) or []: if isinstance(item, dict): members.append({**item, "area": area_name}) standard = next((item for item in STANDARD_OBJECT_MEMBERS.get(str(canonical_kind(str(owner_kind or "")) or ""), set()) if normalize(item) == normalize(first)), None) if standard: object_path = metadata_write_plan_path_parts(f"{owner_kind}.{owner_name}").get("canonical_path") or f"{owner_kind}.{owner_name}" result.update( { "status": "resolved", "resolution_kind": "context_metadata_member", "path_kind": "metadata_member", "area": "standard_attribute", "canonical_path": ".".join([object_path, standard, *parts[1:]]), "context_path": ".".join([standard, *parts[1:]]), "match": {"area": "standard_attribute", "name": standard, "standard": True}, "safe_as_metadata_path": True, } ) return result member = next((item for item in members if normalize(item.get("name")) == normalize(first) or normalize(item.get("synonym")) == normalize(first)), None) if member: object_path = metadata_write_plan_path_parts(f"{owner_kind}.{owner_name}").get("canonical_path") or f"{owner_kind}.{owner_name}" result.update( { "status": "resolved", "resolution_kind": "context_metadata_member", "path_kind": "metadata_member", "area": member.get("area"), "canonical_path": ".".join([object_path, str(member.get("name") or first), *parts[1:]]), "context_path": ".".join([str(member.get("name") or first), *parts[1:]]), "match": {key: member.get(key) for key in ("area", "name", "synonym", "type", "types") if member.get(key) not in (None, "", [])}, "safe_as_metadata_path": True, } ) return result candidates = metadata_definition_find( { "base_id": base_id_or_error, "query": first, "areas": ["metadata", "extensions"], "use_cache": payload.get("use_cache", False), "timeout_seconds": payload.get("timeout_seconds", 30), } ) result.update( { "reason": "not_a_confirmed_metadata_path_or_local_symbol", "safe_as_metadata_path": False, "candidates": [ { "canonical_path": item.get("canonical_path"), "kind": item.get("kind"), "name": item.get("name"), "reason": "short_object_name_requires_kind", } for item in (candidates.get("matches") or []) if isinstance(item, dict) ][:10], } ) return result def search_modules(payload: dict[str, Any]) -> dict[str, Any]: normalized_payload = normalize_object_selector_aliases(payload, "modules.search") if isinstance(normalized_payload, dict) and normalized_payload.get("status") == "invalid_argument": return normalized_payload payload = normalized_payload base_id_or_error = require_base_id(payload, "modules.search") if isinstance(base_id_or_error, dict): return base_id_or_error query_value = payload.get("query") if query_value is not None and not isinstance(query_value, str): return invalid_argument("modules.search", "query", "query must be a JSON string.") query = str(query_value or "").strip() include_storage, include_storage_error = strict_include_storage(payload, "modules.search") if include_storage_error: return include_storage_error include_storage = bool(include_storage) resolve_owners, resolve_owners_error = strict_bool_argument(payload, "resolve_owners", method="modules.search", default=False) if resolve_owners_error: return resolve_owners_error resolve_owners = bool(resolve_owners) full_scan_value, full_scan_error = strict_bool_argument(payload, "full_scan", method="modules.search", default=False) if full_scan_error: return full_scan_error full_scan = bool(full_scan_value) if not query: return invalid_argument("modules.search", "query", "Передайте непустой query.") query_cf = query.casefold() table = str(payload.get("table") or "auto") prefix = str(payload.get("prefix") or "") state = str(payload.get("state") or "working").strip().lower() if state not in EXTENSION_OBJECTS_FIND_STATES: return invalid_argument("modules.search", "state", "Unsupported saved-state mode.", allowed_values=sorted(EXTENSION_OBJECTS_FIND_STATES)) string_error = validate_optional_string_arguments( payload, "modules.search", ["ref", "object_type", "object_name", "object_guid", "table", "prefix", "scope", "extension", "extension_guid", "routine_name", "state"], ) if string_error: return string_error saved_extension_guid = str(payload.get("extension_guid") or "").strip().lower() if "extension_guid" in payload and not is_guid_text(saved_extension_guid): return invalid_argument("modules.search", "extension_guid", "extension_guid must be a GUID string.") if table != "auto" and table not in STORAGE_TABLES: return invalid_argument("modules.search", "table", "Unsupported storage table.", allowed_values=["auto", *sorted(STORAGE_TABLES)]) table_for_read = table if table in STORAGE_TABLES else "Config" scope = str(payload.get("scope") or "auto").strip().casefold() scan_limit, scan_limit_error = parse_int_argument(payload, "scan_limit", method="modules.search", default=300, minimum=1, maximum=5000) if scan_limit_error: return scan_limit_error limit, limit_error = parse_int_alias_argument(payload, "limit", "max_matches", method="modules.search", default=20, minimum=1, maximum=100) if limit_error: return limit_error owner_scan_limit, owner_scan_limit_error = parse_int_argument(payload, "owner_scan_limit", method="modules.search", default=40, minimum=1, maximum=200) if owner_scan_limit_error: return owner_scan_limit_error read_max_chars, read_max_chars_error = parse_int_argument(payload, "read_max_chars", method="modules.search", default=4000, minimum=1, maximum=100000) if read_max_chars_error: return read_max_chars_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method="modules.search", default=60, minimum=1) if timeout_error: return timeout_error extension_filter = str(payload.get("extension") or "").strip() extension_guid: str | None = None extension_owner_objects: list[tuple[dict[str, Any], list[dict[str, Any]]]] = [] extension_target_module_refs: set[str] = set() extension_target_files_by_table: dict[str, set[str]] = {} extension_owner_guids: list[str] = [] extension_active_object_guids: set[str] = set() extension_route_fallback_scan = False extension_route_fallback_diagnostics: list[dict[str, Any]] = [] routine_name = str(payload.get("routine_name") or "").strip() routine_name_cf = normalize(routine_name) if extension_filter: if is_guid_text(extension_filter): extension_guid = extension_filter.strip().lower() else: extensions_result = extension_map_by_guid(base_id_or_error) wanted_extension = normalize(extension_filter) for extension in extensions_result.values(): if normalize(str(extension.get("name") or "")) == wanted_extension: extension_guid = str(extension.get("guid") or "").strip().lower() break if not extension_guid: return { "schema": "onec_modules_search.v1", "status": "not_found", "base_id": base_id_or_error, "source": {"kind": "live_metadata"}, "query": { "query": query, "limit": limit, "max_matches": limit, "scan_limit": scan_limit, "scope": scope, "table": table, "prefix": prefix, "extension": extension_filter, "routine_name": routine_name or None, "include_storage": include_storage, }, "matches": [], "counts": {"matches": 0, "scanned_files": 0, "scan_limit": scan_limit, "truncated": False, "tables_scanned": []}, "diagnostics": {"message": f"Расширение `{extension_filter}` не найдено."}, } guid_sources, guid_error = extension_definition_guid_sources(base_id_or_error, timeout_seconds=int(timeout_seconds or 60)) if guid_error: guid_error["method"] = "modules.search" return guid_error for definition_guid, sources in guid_sources.items(): for source in sources or []: source_extension = source.get("extension") or {} source_guid = str(source_extension.get("guid") or "").strip().lower() source_role = str(source.get("storage_role") or "") if source_guid == extension_guid and source_role in DBNAMES_ROLE_KIND: extension_owner_guids.append(definition_guid) break extension_owner_guids = sorted(set(guid for guid in extension_owner_guids if is_guid_text(guid))) active_objects_result = extension_objects_find( { "base_id": base_id_or_error, "extension": extension_guid, "state": "active", "limit": 500, "include_storage": False, "use_cache": True, "timeout_seconds": int(timeout_seconds or 60), } ) if active_objects_result.get("status") == "ok": extension_active_object_guids = { str(item.get("guid") or "").strip().lower() for item in active_objects_result.get("objects") or [] if isinstance(item, dict) and str(item.get("guid") or "").strip() } for owner_guid in extension_owner_guids: if table_for_read in {"ConfigSave", "ConfigCASSave"}: extension_owner_table = "ConfigCASSave" elif state in {"working", "save"}: extension_owner_table = "ConfigCASSave" else: extension_owner_table = "ConfigCAS" modules_result = metadata_object_modules( { "base_id": base_id_or_error, "guid": owner_guid, "table": extension_owner_table, "include_storage": True, "timeout_seconds": int(timeout_seconds or 60), } ) if modules_result.get("status") != "ok": continue modules = [module for module in modules_result.get("modules") or [] if isinstance(module, dict)] if not modules: continue extension_owner_objects.append((modules_result.get("object") or {}, modules)) for module in modules: module_id = str(module.get("module_id") or "").strip() if not module_id: continue extension_target_module_refs.add(module_id) table_name, file_name, _ = parse_module_id(module_id) if table_name and file_name: extension_target_files_by_table.setdefault(table_name, set()).add(file_name) if not extension_target_module_refs: extension_route_fallback_scan = True manifests, manifest_diagnostics = live_extension_manifests( base_id_or_error, extension_guid=extension_guid, timeout_seconds=int(timeout_seconds or 60), ) manifest_keys = { str(entry.get("cas_key") or "").strip().lower() for manifest in manifests for entry in (manifest.get("entries") or []) if isinstance(entry, dict) and str(entry.get("cas_key") or "").strip() } if manifest_keys: extension_target_files_by_table["ConfigCAS"] = manifest_keys extension_route_fallback_diagnostics.append( { "code": "extension_module_owner_routes_not_found", "message": f"В расширении `{extension_filter}` модульные записи не найдены через DBNames owner routes; выполняется fallback scan по manifest ConfigCAS.", "manifest_entries": len(manifest_keys), "manifest_diagnostics": manifest_diagnostics, } ) cache_config, _ = sql_config_for_base(base_id_or_error) def extract_routine_text(text_value: str, wanted_routine_cf: str) -> tuple[str, int]: source_text = str(text_value or "") if not wanted_routine_cf: return source_text, 0 try: from parser.bsl_validation import routine_blocks routines = list(routine_blocks(source_text)) except Exception: return "", 0 lines = source_text.split("\n") for routine in routines: routine_name_from_source = str(routine.get("name") or "") if normalize(routine_name_from_source) != wanted_routine_cf: continue start_line = int(routine.get("line_start") or 0) end_line = int(routine.get("line_end") or 0) if not start_line or not end_line: continue if start_line < 1: start_line = 1 if end_line < start_line: continue if start_line > len(lines): continue end_line = min(end_line, len(lines)) prefix_len = sum(len(line) + 1 for line in lines[: start_line - 1]) return "\n".join(lines[start_line - 1 : end_line]), prefix_len return "", 0 object_ordinal_selector = first_non_empty_arg(payload, "ordinal", "index", "object_index") has_object_selector = bool(payload.get("guid") or payload.get("name") or (object_ordinal_selector is not None)) if has_object_selector: modules_result = metadata_object_modules({**payload, "include_storage": True, "table": table_for_read}) if modules_result.get("status") != "ok": return public_error_result(modules_result, include_storage=include_storage, method="modules.search") object_info = modules_result.get("object") or {} public_owner = { "status": "resolved", "kind": object_info.get("kind") or canonical_kind(str(payload.get("kind") or "")), "name": object_info.get("name") or payload.get("name"), "synonym": object_info.get("synonym"), "guid": object_info.get("guid") or payload.get("guid"), } modules = [module for module in modules_result.get("modules") or [] if isinstance(module, dict)] requested_module_ordinal = first_non_empty_arg(payload, "module_ordinal", "module_index", "module_number") module_rows: list[tuple[int, dict[str, Any]]] = list(enumerate(modules, start=1)) command_module_rows: list[dict[str, Any]] = [] if requested_module_ordinal in {None, ""}: commands_result = metadata_object_commands( { **payload, "base_id": base_id_or_error, "guid": public_owner.get("guid"), "kind": public_owner.get("kind"), "table": table_for_read, "include_form_commands": False, "include_storage": False, "timeout_seconds": int(timeout_seconds or 60), } ) if commands_result.get("status") == "ok": command_module_rows = [ command for command in commands_result.get("object_commands") or [] if isinstance(command, dict) and isinstance(command.get("read_selector"), dict) ] if requested_module_ordinal not in {None, ""}: module_ordinal, ordinal_error = parse_ordinal(requested_module_ordinal, "modules.search", argument="module_ordinal") if ordinal_error: return ordinal_error requested_index = int(module_ordinal or 1) - 1 if not (0 <= requested_index < len(modules)): return { "schema": "onec_modules_search.v1", "status": "not_found", "error": "module_not_found", "base_id": base_id_or_error, "source": {"kind": "live_metadata"}, "owner": public_owner, "query": { "query": query, "limit": limit, "max_matches": limit, "scope": "object_modules", "kind": payload.get("kind"), "name": payload.get("name"), "guid": payload.get("guid"), "module_ordinal": requested_module_ordinal, "extension": extension_filter or None, "routine_name": routine_name or None, "include_storage": include_storage, }, "matches": [], "counts": {"matches": 0, "available_modules": len(modules)}, "diagnostics": {"message": f"Module ordinal {module_ordinal} was not found for the selected object."}, } module_rows = [(int(module_ordinal or 1), modules[requested_index])] matches = [] for ordinal, module in module_rows: module_id = str(module.get("module_id") or "") table_name, file_name, stream_index = parse_module_id(module_id) if not table_name or not file_name: continue data, config, error = read_storage_file_bytes(base_id_or_error, table_name, file_name, timeout_seconds=int(timeout_seconds or 60)) if error: continue if stream_index is not None: try: from parser.cas_payload import classify_payload classified = classify_payload(data, include_text=True) except Exception: classified = {} streams = classified.get("stream_blocks") or [] text = repair_bsl_mojibake_text(str((streams[stream_index] if 0 <= stream_index < len(streams) else {}).get("text") or "")) text = form_embedded_module_public_text(text) else: decoded = payload_text_from_bytes(data) container_text = str(decoded.get("text") or "") bsl_offset = module.get("bsl_offset") text, _ = extract_bsl_text_from_container(container_text, bsl_offset=int(bsl_offset) if bsl_offset not in {None, ""} else None) text = form_embedded_module_public_text(str(text or "")) routine_text, routine_offset = ("", 0) if routine_name_cf: routine_text, routine_offset = extract_routine_text(text, routine_name_cf) if not routine_text: routine_text = str(text or "") if routine_name_cf and not routine_text: continue if query_cf not in routine_text.casefold(): continue public_module = public_module_with_qualified_name(module, owner=public_owner, include_storage=False, ordinal=ordinal, owner_kind=public_owner.get("kind")) snippet = text_snippet(routine_text, query) if routine_name_cf and routine_offset: snippet["offset"] = (snippet.get("offset") or 0) + routine_offset if snippet.get("offset") is not None else snippet["offset"] match = { "score": 1.0, "snippet": snippet, "owner": public_owner, "module": { "name": public_module.get("name"), "module_ordinal": ordinal, "form": None, }, **({"qualified_name": public_module.get("qualified_name")} if public_module.get("qualified_name") else {}), **({"display_name": public_module.get("display_name")} if public_module.get("display_name") else {}), "read_selector": enrich_selector_with_object_ref( { "base_id": base_id_or_error, "method": "modules.read", "kind": public_owner.get("kind"), "guid": public_owner.get("guid"), "module_ordinal": ordinal, "preview": True, "max_chars": int(read_max_chars or 4000), }, public_owner, ), "origin": module_origin_from_storage_table(table_name), } if include_storage: match.update({"module_id": module_id, "table": table_name, "file_name": file_name, **({"stream_index": stream_index} if stream_index is not None else {})}) if routine_name_cf: if include_storage and routine_offset: match["read_selector"]["bsl_offset"] = routine_offset match["module"]["routine_name"] = routine_name match["query"] = {"routine_name": routine_name} matches.append(match) if len(matches) >= limit: break if len(matches) < limit: for command in command_module_rows: command_selector = dict(command.get("read_selector") or {}) command_selector.pop("method", None) module_result = read_module( { **command_selector, "include_storage": False, "max_chars": int(read_max_chars or 4000), "timeout_seconds": int(timeout_seconds or 60), } ) if module_result.get("status") != "ok": continue text = str(module_result.get("text") or "") routine_text, routine_offset = ("", 0) if routine_name_cf: routine_text, routine_offset = extract_routine_text(text, routine_name_cf) if not routine_text: routine_text = text if routine_name_cf and not routine_text: continue if query_cf not in routine_text.casefold(): continue snippet = text_snippet(routine_text, query) if routine_name_cf and routine_offset and snippet.get("offset") is not None: snippet["offset"] = int(snippet.get("offset") or 0) + routine_offset command_name = str(command.get("name") or command.get("synonym") or "") qualified_name = ".".join( part for part in [public_owner.get("name"), "Команда", command_name, "Модуль команды"] if part ) read_selector = { **(command.get("read_selector") or {}), "preview": True, "max_chars": int(read_max_chars or 4000), } module_ref = str(read_selector.get("module_ref") or "") command_table, _, _ = parse_module_id(module_ref) match = { "score": 1.0, "snippet": snippet, "owner": public_owner, "module": { "kind": "command_module", "name": "Модуль команды", "command": command_name, }, "qualified_name": qualified_name, "display_name": qualified_name, "read_selector": read_selector, "origin": module_origin_from_storage_table(command_table or table_for_read), } if command_table in {"ConfigSave", "ConfigCASSave"}: match["activation_state"] = "saved_state" if routine_name_cf: match["module"]["routine_name"] = routine_name match["query"] = {"routine_name": routine_name} matches.append(match) if len(matches) >= limit: break return { "schema": "onec_modules_search.v1", "status": "ok", "base_id": base_id_or_error, "source": {"kind": "live_metadata"}, "query": { "query": query, "limit": limit, "max_matches": limit, "scope": "object_modules", "kind": payload.get("kind"), "name": payload.get("name"), "guid": payload.get("guid"), "module_ordinal": requested_module_ordinal, "extension": extension_filter or None, "routine_name": routine_name or None, "include_storage": include_storage, }, "matches": matches, "counts": { "matches": len(matches), "scanned_modules": len(module_rows) + len(command_module_rows), "available_modules": len(modules) + len(command_module_rows), "owner_modules": len(modules), "command_modules": len(command_module_rows), "complete": True, "scan_limit_hit": False, "owner_resolved": len(matches), "owner_unresolved": 0, "owner_scan_limit_hit": False, "owner_indexed_module_refs": len(matches), }, } scan_budget = int(scan_limit or 300) if table == "auto": if scope == "all": tables_to_scan = ["ConfigCASSave", "ConfigSave", "ConfigCAS", "Config"] if state in {"working", "save", "both"} else ["ConfigCAS", "Config", "ConfigCASSave", "ConfigSave"] elif scope == "config": tables_to_scan = ["ConfigSave", "Config"] if state in {"working", "save", "both"} else ["Config", "ConfigSave"] elif scope in {"configcas", "modules"}: tables_to_scan = ["ConfigCASSave", "ConfigCAS"] if state in {"working", "save", "both"} else ["ConfigCAS", "ConfigCASSave"] else: tables_to_scan = ["ConfigCASSave", "ConfigSave", "ConfigCAS", "Config"] if state in {"working", "save", "both"} else ["ConfigCAS", "Config", "ConfigCASSave", "ConfigSave"] if state == "save": tables_to_scan = [item for item in tables_to_scan if item in {"ConfigCASSave", "ConfigSave"}] elif state == "active": tables_to_scan = [item for item in tables_to_scan if item in {"ConfigCAS", "Config"}] else: tables_to_scan = [table] if extension_route_fallback_scan: tables_to_scan = [item for item in tables_to_scan if item in {"ConfigCAS", "ConfigCASSave"}] or ["ConfigCAS", "ConfigCASSave"] if extension_target_files_by_table: tables_to_scan = [item for item in tables_to_scan if item in extension_target_files_by_table] if not tables_to_scan: return { "schema": "onec_modules_search.v1", "status": "not_found", "base_id": base_id_or_error, "source": {"kind": "live_metadata"}, "query": { "query": query, "limit": limit, "max_matches": limit, "scan_limit": scan_limit, "scope": scope, "table": table, "prefix": prefix, "extension": extension_filter or None, "routine_name": routine_name or None, "state": state, "full_scan": full_scan, "include_storage": include_storage, }, "matches": [], "counts": {"matches": 0, "scanned_files": 0, "scan_limit": scan_limit, "truncated": False}, "diagnostics": {"message": f"По расширению `{extension_filter}` не найдено целевых модульных файлов в выбранных таблицах."}, } scanned_files_total = 0 scanned_tables: list[str] = [] all_payloads: dict[str, tuple[dict[str, bytes], dict[str, Any] | None]] = {} matches = [] skipped_active_extension_fallback = False for candidate_table in tables_to_scan: remaining_budget = max(0, scan_budget - scanned_files_total) if remaining_budget <= 0: break files_payload = {"base_id": base_id_or_error, "table": candidate_table, "limit": remaining_budget, "_internal": True} effective_prefix = prefix if not effective_prefix and extension_filter and extension_guid and candidate_table in {"ConfigCAS", "ConfigCASSave"}: effective_prefix = f"{extension_guid}__" if effective_prefix: files_payload["prefix"] = effective_prefix manifest_direct_files = ( extension_route_fallback_scan and candidate_table == "ConfigCAS" and bool(extension_target_files_by_table.get("ConfigCAS")) ) if manifest_direct_files: file_names = sorted(extension_target_files_by_table.get("ConfigCAS") or set())[:remaining_budget] candidate_files = {"status": "ok", "files": [{"FileName": name} for name in file_names], "truncated": False} else: candidate_files = storage_files_list(files_payload) if candidate_files.get("status") != "ok": result = dict(candidate_files) result["method"] = "modules.search" return result file_names = [str(row.get("FileName") or "") for row in candidate_files.get("files") or []] if extension_target_files_by_table: file_names = [name for name in file_names if name in extension_target_files_by_table.get(candidate_table, set())] if not file_names: continue if not file_names: if table == "auto" and scope in {"auto", "modules", "configcas"} and candidate_table in {"ConfigCAS", "ConfigCASSave"}: continue if table != "auto" or scope in {"config", "configcas"} or scope == "all": continue scanned_files_total += len(file_names) scanned_tables.append(candidate_table) payloads, config, error = read_storage_files_bytes(base_id_or_error, candidate_table, file_names, timeout_seconds=int(timeout_seconds or 60)) if error: error["method"] = "modules.search" return error all_payloads[candidate_table] = (payloads, config) if table == "auto" and scope in {"auto", "modules", "configcas"} and state != "both": break if table == "auto" and scope == "all" and candidate_files.get("truncated"): break if not scanned_tables: return { "schema": "onec_modules_search.v1", "status": "ok", "base_id": base_id_or_error, "source": {"kind": "live_sql", "database": None, "tables": scanned_tables or ([table] if table != "auto" else [])}, "query": { "query": query, "limit": limit, "max_matches": limit, "scan_limit": scan_limit, "scope": scope, "table": table, "prefix": prefix, "extension": extension_filter or None, "routine_name": routine_name or None, "state": state, "full_scan": full_scan, "include_storage": include_storage, }, "matches": [], "counts": {"matches": 0, "scanned_files": 0, "scan_limit": scan_limit, "truncated": False}, "diagnostics": {"message": "Файлы для поиска не найдены в выбранных таблицах."}, } owner_index: dict[str, dict[str, Any]] = {} cache_owner_enabled = False if resolve_owners and cache_config: with cache_connection() as conn: cache_owner_enabled = bool(conn.execute("SELECT 1 FROM metadata_module_owner_cache WHERE server_key=? AND database_name=? LIMIT 1", (cache_server_key(cache_config), cache_database_name(cache_config))).fetchone()) owner_resolution = { "requested": bool(resolve_owners), "strategy": "disabled", "cache_available": bool(cache_owner_enabled), "owner_scan_limit": int(owner_scan_limit or 40), "owner_objects_scanned": 0, "owner_objects_remaining": int(owner_scan_limit or 40), "owner_scan_limit_hit": False, "indexed_module_refs": 0, "source": None, } def metadata_owner_cache_lookup(module_id: str) -> dict[str, Any] | None: if not cache_owner_enabled: return None cached = metadata_module_owner_cache_lookup(cache_config, module_id) if not cached: return None owner_payload = cached.get("owner") or {} module_payload = cached.get("module_payload") or {} if not isinstance(owner_payload, dict) or not owner_payload.get("kind") or not is_guid_text(owner_payload.get("guid") or ""): return None return { "owner": { "status": "resolved", "kind": owner_payload.get("kind"), "name": owner_payload.get("name"), "synonym": owner_payload.get("synonym"), "guid": owner_payload.get("guid"), }, "module": { "name": module_payload.get("module_name"), "module_ordinal": module_payload.get("module_ordinal"), "form": None, }, "module_payload": module_payload, "read_selector": { "base_id": base_id_or_error, "method": "modules.read", "kind": owner_payload.get("kind"), "guid": owner_payload.get("guid"), "preview": True, "max_chars": int(read_max_chars or 4000), }, } def build_owner_index() -> None: if not resolve_owners or cache_owner_enabled: if cache_owner_enabled: owner_resolution["strategy"] = "metadata_module_owner_cache" owner_resolution["source"] = "metadata.module_owner_cache" return if extension_owner_objects: owner_resolution["strategy"] = "extension_owner_objects" owner_resolution["source"] = "extension_filter" owners = extension_owner_objects for owner_info, modules in owners: owner_resolution["owner_objects_scanned"] = int(owner_resolution["owner_objects_scanned"] or 0) + 1 owner_payload = { "kind": owner_info.get("kind"), "name": owner_info.get("name"), "synonym": owner_info.get("synonym"), "guid": owner_info.get("guid"), } for ordinal, module in enumerate(modules, start=1): module_id = str(module.get("module_id") or "") if not module_id: continue owner_resolution["indexed_module_refs"] = int(owner_resolution["indexed_module_refs"] or 0) + 1 owner_index[module_id] = { "owner": { "status": "resolved", **owner_payload, }, "module": { "name": public_module_row( module, include_storage=False, ordinal=ordinal, owner_kind=owner_payload.get("kind"), owner_name=owner_payload.get("name"), ).get("name"), "module_ordinal": ordinal, "form": None, }, "read_selector": enrich_selector_with_object_ref( { "base_id": base_id_or_error, "method": "modules.read", "kind": owner_payload.get("kind"), "guid": owner_payload.get("guid"), "module_ordinal": ordinal, "preview": True, "max_chars": int(read_max_chars or 4000), }, owner_payload, ), } return owner_resolution["strategy"] = "live_metadata_scan" owner_resolution["source"] = "metadata.objects.list+metadata.object.modules" kinds = ( [canonical_kind(str(payload.get("kind")))] if payload.get("kind") else [kind for kind in sorted(KIND_CAPABILITIES) if "modules" in KIND_CAPABILITIES.get(kind, [])] ) remaining_owner_objects = int(owner_scan_limit or 40) for owner_kind in [kind for kind in kinds if kind]: if remaining_owner_objects <= 0: break listed = list_objects( owner_kind, base_id=base_id_or_error, limit=remaining_owner_objects, offset=0, include_storage=False, table=table_for_read, ) if listed.get("status") != "ok": continue objects = listed.get("objects") or [] remaining_owner_objects -= len(objects) owner_resolution["owner_objects_scanned"] = int(owner_resolution["owner_objects_scanned"] or 0) + len(objects) owner_resolution["owner_objects_remaining"] = max(0, remaining_owner_objects) for obj in objects: selector = {"base_id": base_id_or_error, "kind": obj.get("kind"), "guid": obj.get("guid"), "timeout_seconds": int(timeout_seconds or 60)} modules_result = metadata_object_modules({**selector, "include_storage": True, "table": table_for_read}) if modules_result.get("status") != "ok": continue for ordinal, module in enumerate(modules_result.get("modules") or [], start=1): module_id = str(module.get("module_id") or "") if not module_id: continue owner_resolution["indexed_module_refs"] = int(owner_resolution["indexed_module_refs"] or 0) + 1 owner_index[module_id] = { "owner": { "status": "resolved", "kind": obj.get("kind"), "name": obj.get("name"), "synonym": obj.get("synonym"), "guid": obj.get("guid"), }, "module": { "name": public_module_row( module, include_storage=False, ordinal=ordinal, owner_kind=obj.get("kind"), owner_name=obj.get("name"), ).get("name"), "module_ordinal": ordinal, "form": None, }, "read_selector": enrich_selector_with_object_ref( { "base_id": base_id_or_error, "method": "modules.read", "kind": obj.get("kind"), "guid": obj.get("guid"), "module_ordinal": ordinal, "preview": True, "max_chars": int(read_max_chars or 4000), }, obj, ), } owner_resolution["owner_scan_limit_hit"] = remaining_owner_objects <= 0 owner_resolution["owner_objects_remaining"] = max(0, remaining_owner_objects) def enrich_match(match: dict[str, Any], module_id: str) -> dict[str, Any]: enrichment = owner_index.get(module_id) if not enrichment: return match enriched = dict(match) enriched["owner"] = enrichment["owner"] enriched["module"] = {**(match.get("module") or {}), **enrichment["module"]} enriched["read_selector"] = enrichment["read_selector"] return enriched build_owner_index() def public_module_match( module_id: str, snippet: dict[str, Any], *, stream_index: int | None = None, bsl_offset: int | None = None, ) -> dict[str, Any]: cached_owner = metadata_owner_cache_lookup(module_id) cached_form_owner = metadata_form_owner_cache_lookup(cache_config, module_ref=module_id) if not cached_owner else None module_table, module_file_name, _ = parse_module_id(module_id) read_selector = { "base_id": base_id_or_error, "method": "modules.read", "module_ref": module_id, "preview": True, "max_chars": int(read_max_chars or 4000), } if cached_form_owner and cached_form_owner.get("bsl_offset") is not None and stream_index is None: read_selector["bsl_offset"] = int(cached_form_owner.get("bsl_offset") or 0) if cached_owner and cached_owner.get("module", {}).get("module_ordinal") and cached_owner.get("owner", {}).get("guid"): read_selector["kind"] = cached_owner.get("owner", {}).get("kind") read_selector["guid"] = cached_owner.get("owner", {}).get("guid") read_selector["module_ordinal"] = cached_owner.get("module", {}).get("module_ordinal") read_selector = enrich_selector_with_object_ref(read_selector, cached_owner.get("owner") or {}) if stream_index is None and bsl_offset is not None: read_selector["bsl_offset"] = int(bsl_offset) match = { "score": 1.0, "snippet": snippet, "owner": cached_owner["owner"] if cached_owner else { "status": "resolved", "kind": ((cached_form_owner.get("owner") or {}).get("kind") if isinstance(cached_form_owner, dict) else None) or ((cached_form_owner.get("form") or {}).get("kind") if isinstance(cached_form_owner, dict) else None), "name": ((cached_form_owner.get("owner") or {}).get("name") if isinstance(cached_form_owner, dict) else None) or ((cached_form_owner.get("form") or {}).get("name") if isinstance(cached_form_owner, dict) else None), "synonym": None, "guid": ((cached_form_owner.get("owner") or {}).get("guid") if isinstance(cached_form_owner, dict) else None) or ((cached_form_owner.get("form") or {}).get("guid") if isinstance(cached_form_owner, dict) else None), "source": "metadata_form_owner_cache", } if cached_form_owner else { "status": "unresolved", "kind": None, "name": None, "synonym": None, "diagnostics": { "message": "Владелец модуля не восстановлен. Для чтения используйте read_selector; для точного владельца ограничьте поиск kind/name/guid, включите resolve_owners или увеличьте owner_scan_limit.", }, }, "module": { "name": "Модуль БСЛ", "module_ordinal": None, "form": None, }, "read_selector": read_selector, "origin": module_origin_from_storage_table(module_table or ""), } if cached_owner: cached_module = cached_owner.get("module") or {} if cached_module.get("name"): match["module"]["name"] = cached_module.get("name") if cached_module.get("module_ordinal"): match["module"]["module_ordinal"] = cached_module.get("module_ordinal") if cached_owner.get("module_payload", {}).get("stream_index") is not None: match["module"]["stream_index"] = cached_owner.get("module_payload", {}).get("stream_index") if cached_form_owner: cached_form = cached_form_owner.get("form") if isinstance(cached_form_owner.get("form"), dict) else {} match["module"]["name"] = "Модуль формы" match["module"]["form"] = cached_form.get("name") match["origin"] = { "source": "extension" if cached_form_owner.get("extension") else "metadata_form_owner_cache", "presentation": "Расширение" if cached_form_owner.get("extension") else "Индекс форм", "extension": cached_form_owner.get("extension"), "status": "ok", } if stream_index is not None: match["module"]["stream_index"] = stream_index if module_table in {"ConfigSave", "ConfigCASSave"}: saved_context = saved_state_public_module_context( base_id=base_id_or_error, table=module_table, file_name=module_file_name or "", object_kind=canonical_kind(str(payload.get("object_type") or payload.get("kind") or "")) or None, timeout_seconds=int(timeout_seconds or 60), prefer_form_module=stream_index is None and bsl_offset is not None, ) saved_owner = saved_context.get("owner") if isinstance(saved_context.get("owner"), dict) else None saved_form = saved_context.get("form") if isinstance(saved_context.get("form"), dict) else None saved_module = saved_context.get("module") if isinstance(saved_context.get("module"), dict) else None saved_qualified_name = str(saved_context.get("qualified_name") or "").strip() if saved_owner: match["owner"] = saved_owner if saved_form: match["form"] = saved_form match["module"]["form"] = saved_form.get("name") if saved_module: match["module"].update(saved_module) if saved_qualified_name: match["qualified_name"] = saved_qualified_name match["display_name"] = saved_qualified_name saved_identity = saved_state_module_file_identity(module_file_name or "") saved_owner_guid = str(saved_identity.get("module_guid") or saved_identity.get("owner_guid") or "").strip().lower() active_owner_guid_set = {str(guid or "").strip().lower() for guid in extension_owner_guids} | extension_active_object_guids if extension_filter and saved_owner_guid: match["activation_state"] = "saved_override" if saved_owner_guid in active_owner_guid_set else "saved_only" else: match["activation_state"] = "saved_state" else: match["activation_state"] = "active" return enrich_match(match, module_id) for selected_table in scanned_tables: payloads, table_config = all_payloads.get(selected_table, ({}, None)) config = table_config or config for file_name, data in (payloads or {}).items(): if extension_target_files_by_table and file_name not in extension_target_files_by_table.get(selected_table, set()): continue decoded = payload_text_from_bytes(data) container_text = str(decoded.get("text") or "") bsl_text, extraction = extract_bsl_text_from_container(container_text) bsl_search_text = str(bsl_text or "") if extraction.get("status") == "ok": bsl_search_text = form_embedded_module_public_text(bsl_search_text) bsl_search_offset = int(extraction.get("bsl_offset") or 0) if routine_name_cf: routine_text, routine_offset = extract_routine_text(bsl_search_text, routine_name_cf) if routine_text: bsl_search_text = routine_text bsl_search_offset += int(routine_offset or 0) else: bsl_search_text = "" if extraction.get("status") == "ok" and query_cf in bsl_search_text.casefold(): module_id = f"{selected_table}:{file_name}" if extension_target_module_refs and module_id not in extension_target_module_refs: continue snippet = text_snippet(bsl_search_text, query) match = public_module_match(module_id, snippet, bsl_offset=int(extraction.get("bsl_offset") or 0)) match["extraction"] = { "status": "ok", "source": "bsl_text", "container_offset": extraction.get("bsl_offset"), **({"routine_offset": routine_offset} if routine_name_cf and routine_offset else {}), } if routine_name_cf and bsl_search_offset: snippet["offset"] = snippet["offset"] + (routine_offset or 0) if snippet.get("offset") is not None else snippet["offset"] if include_storage: match.update( { "module_id": module_id, "table": selected_table, "file_name": file_name, "payload": { "compression": decoded.get("compression"), "encoding": decoded.get("encoding"), "raw_bytes": decoded.get("raw_bytes"), "payload_bytes": decoded.get("payload_bytes"), }, } ) if routine_name_cf: match["module"]["routine_name"] = routine_name matches.append(match) if len(matches) >= limit: break continue try: from parser.cas_payload import classify_payload classified = classify_payload(data, include_text=True) except Exception: classified = {} for index, stream in enumerate(classified.get("stream_blocks") or []): stream_text = repair_bsl_mojibake_text(str(stream.get("text") or "")) stream_text = form_embedded_module_public_text(stream_text) stream_search_text = stream_text if routine_name_cf: routine_text, routine_offset = extract_routine_text(stream_text, routine_name_cf) if not routine_text: continue stream_search_text = routine_text if query_cf not in stream_search_text.casefold(): continue module_id = f"{selected_table}:{file_name}#stream:{index}" if extension_target_module_refs and module_id not in extension_target_module_refs: continue snippet = text_snippet(stream_search_text, query) if routine_name_cf and routine_offset: snippet["offset"] = (snippet.get("offset") or 0) + routine_offset if snippet.get("offset") is not None else snippet["offset"] match = public_module_match(module_id, snippet, stream_index=index) if include_storage: if routine_name_cf: match["module"]["routine_name"] = routine_name match.update( { "module_id": module_id, "table": selected_table, "file_name": file_name, "stream_index": index, "payload": { "role": classified.get("role"), "compression": classified.get("compression"), "raw_bytes": classified.get("raw_bytes"), "payload_bytes": classified.get("payload_bytes"), "stream": {key: value for key, value in stream.items() if key != "text"}, }, } ) matches.append(match) if len(matches) >= limit: break if len(matches) >= limit: break if len(matches) >= limit: break truncated = scanned_files_total >= scan_budget status = "partial" if truncated else "ok" resolved_owner_count = sum(1 for match in matches if (match.get("owner") or {}).get("status") == "resolved") unresolved_owner_count = sum(1 for match in matches if (match.get("owner") or {}).get("status") != "resolved") diagnostics = { "note": "Это поиск по текстам модулей. Каждый результат содержит read_selector для следующего публичного чтения модуля; owner.status показывает, удалось ли восстановить владельца.", "owner_resolution": owner_resolution, } if extension_route_fallback_scan: diagnostics["extension_route_fallback"] = extension_route_fallback_diagnostics if skipped_active_extension_fallback: diagnostics["active_fallback_scan"] = { "status": "skipped", "reason": "full_scan_disabled_for_extension", "message": "Skipped broad active ConfigCAS scan for an extension-scoped module query. Pass full_scan=true to force deep active discovery.", } if truncated: diagnostics["message"] = "Глобальный поиск ограничен scan_limit; результат может быть неполным. Увеличьте scan_limit или передайте kind/name/guid для поиска по конкретному объекту." return { "schema": "onec_modules_search.v1", "status": status, "base_id": base_id_or_error, "source": ( {"kind": "live_sql", "database": (config or {}).get("database"), "tables": scanned_tables} if include_storage else {"kind": "live_metadata"} ), "query": ( {"query": query, "limit": limit, "max_matches": limit, "scan_limit": scan_limit, "scope": scope, "table": table, "prefix": prefix, "extension": extension_filter or None, "routine_name": routine_name or None, "state": state, "full_scan": full_scan, "include_storage": include_storage} if include_storage else {"query": query, "limit": limit, "max_matches": limit, "scan_limit": scan_limit, "scope": scope, "table": table, "prefix": prefix, "extension": extension_filter or None, "routine_name": routine_name or None, "state": state, "full_scan": full_scan, "include_storage": include_storage} ), "matches": matches, "counts": { "matches": len(matches), "scanned_files": scanned_files_total, "scan_limit": scan_limit, "truncated": truncated, "complete": not truncated, "scan_limit_hit": truncated, "tables_scanned": scanned_tables, "activation_state": { activation_state: sum(1 for match in matches if str(match.get("activation_state") or "active") == activation_state) for activation_state in sorted({str(match.get("activation_state") or "active") for match in matches}) }, "owner_resolved": resolved_owner_count, "owner_unresolved": unresolved_owner_count, "owner_scan_limit_hit": bool(owner_resolution.get("owner_scan_limit_hit")), "owner_indexed_module_refs": int(owner_resolution.get("indexed_module_refs") or 0), }, "diagnostics": diagnostics, } def _code_query_object_selector(payload: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any] | None]: kind = canonical_kind(str(payload.get("object_type") or payload.get("kind") or "")) name = payload.get("object_name") or payload.get("name") guid = payload.get("object_guid") or payload.get("guid") selector = { **({"kind": kind} if kind else {}), **({"name": name} if name is not None else {}), **({"guid": guid} if guid is not None else {}), } resolved = { "kind": selector.get("kind"), "name": selector.get("name"), "guid": selector.get("guid"), } return selector, resolved def _snippet_to_line_column(text: str, offset: int | None) -> tuple[int | None, int | None]: if offset is None: return None, None safe_offset = max(0, int(offset)) normalized = str(text or "").replace("\r\n", "\n").replace("\r", "\n") if safe_offset >= len(normalized): safe_offset = max(0, len(normalized) - 1) before = normalized[:safe_offset] line = before.count("\n") + 1 if normalized else 1 last_nl = before.rfind("\n") column = len(before) - (last_nl + 1) + 1 return line, column def _extract_bsl_routine_text_for_code_read(text: str, routine_name: str) -> tuple[str, dict[str, Any] | None]: wanted = normalize(str(routine_name or "")) if not wanted: return str(text or ""), None try: from parser.bsl_validation import routine_blocks routines = list(routine_blocks(str(text or ""))) except Exception: return "", None lines = str(text or "").split("\n") for routine in routines: routine_source_name = str(routine.get("name") or "") if normalize(routine_source_name) != wanted: continue start_line = int(routine.get("line_start") or 0) end_line = int(routine.get("line_end") or 0) if not start_line or not end_line or end_line < start_line: continue selected = "\n".join(lines[start_line - 1 : min(end_line, len(lines))]) return selected, {"routine_name": routine_source_name, "line_start": start_line, "line_end": min(end_line, len(lines))} return "", None FORM_EMBEDDED_MODULE_TRAILING_MARKER_RE = re.compile(r"(?P.*?)(?P(?:\r?\n){1,2}///----.*)$", re.DOTALL) def split_form_embedded_module_public_text(text: str) -> tuple[str, str]: match = FORM_EMBEDDED_MODULE_TRAILING_MARKER_RE.match(str(text or "")) if not match: return str(text or ""), "" return match.group("body"), match.group("suffix") def form_embedded_module_public_text(text: str) -> str: public_text, _suffix = split_form_embedded_module_public_text(text) return public_text def preserve_form_embedded_module_suffix(current_text: str, new_text: str) -> str: _current_public, suffix = split_form_embedded_module_public_text(current_text) if not suffix: return new_text _new_public, new_suffix = split_form_embedded_module_public_text(new_text) if new_suffix: return new_text return f"{new_text.rstrip()}{suffix}" def code_saved_state_common_form_module_search(payload: dict[str, Any], *, query: str, limit: int, offset: int, scan_limit: int, timeout_seconds: int, include_line_numbers: bool, include_context: bool, include_storage: bool, state: str) -> dict[str, Any] | None: object_type = str(payload.get("object_type") or payload.get("kind") or "").strip() object_name = str(payload.get("object_name") or payload.get("name") or "").strip() object_guid = str(payload.get("object_guid") or payload.get("guid") or "").strip() if canonical_kind(object_type) != "CommonForm" or not (object_name or object_guid): return None if state not in {"working", "save", "both"}: return None search = metadata_saved_state_modules_search( { "base_id": payload.get("base_id"), "object_type": "CommonForm", **({"object_name": object_name} if object_name else {}), **({"object_guid": object_guid} if object_guid else {}), "query": query, "routine_name": payload.get("routine_name"), "limit": int(limit or 25) + int(offset or 0), "scan_limit": int(scan_limit or 300), "preview_chars": int(payload.get("max_chars") or 100000), "timeout_seconds": int(timeout_seconds or 60), "include_storage": True, } ) if search.get("status") != "ok" or not search.get("modules"): return None raw_items: list[dict[str, Any]] = [] for module in search.get("modules") or []: if not isinstance(module, dict): continue for stream in module.get("streams") or []: if not isinstance(stream, dict): continue text = form_embedded_module_public_text(str(stream.get("preview") or "")) search_text = text selection = None routine_name = str(payload.get("routine_name") or "") if routine_name: routine_text, routine_selection = _extract_bsl_routine_text_for_code_read(text, routine_name) if routine_text: search_text = routine_text selection = routine_selection offset_value = search_text.casefold().find(str(query or "").casefold()) if query and offset_value < 0: continue snippet = text_snippet(search_text, query) if query else {"text": search_text, "offset": None} line = column = None if include_line_numbers and offset_value >= 0: line, column = _snippet_to_line_column(search_text, offset_value) if selection and selection.get("line_start"): line = int(selection["line_start"]) + int(line or 1) - 1 read_selector = { "method": "code.read", "base_id": payload.get("base_id"), "object_type": "CommonForm", **({"object_name": object_name} if object_name else {}), **({"object_guid": object_guid} if object_guid else {}), **({"routine_name": routine_name} if routine_name else {}), "include_text": True, "max_chars": int(payload.get("max_chars") or 100000), } item = { "match": str(snippet.get("text") or ""), "line": line, "column": column, "context": str(snippet.get("text") or ""), "resolved_owner": {"status": "resolved", "kind": "CommonForm", "name": object_name or None, "guid": object_guid or None, "source": "saved_state"}, "origin": {"source": "saved_state", "presentation": "Saved state", "status": "ok"}, "module": {"name": "Модуль формы", "form": object_name or None, **({"routine_name": routine_name} if routine_name else {})}, "query": query, "read_selector": read_selector, "source": read_selector, "activation_state": "saved_state", "current_state": {"source": "saved_state", "activation_state": "not_activated"}, } if not include_context: item.pop("context", None) item["match"] = query if include_storage: item["storage"] = {"table": module.get("table"), "file_name": module.get("file_name"), "module_path": stream.get("module_path")} raw_items.append(item) sliced = raw_items[int(offset or 0) : int(offset or 0) + int(limit or 25)] return { "schema": "onec_code_search.v1", "status": "ok" if sliced else "not_found", "base_id": payload.get("base_id"), "source": ( {"kind": "saved_state", "tables": search.get("source", {}).get("tables")} if include_storage else {"kind": "saved_state"} ), "current_state": {"source": "saved_state", "activation_state": "not_activated"}, "object": {"kind": "CommonForm", "name": object_name or None, "guid": object_guid or None}, "query": { "query": query, "kind": "CommonForm", "name": object_name or None, "guid": object_guid or None, "limit": int(limit or 25), "offset": int(offset or 0), "scan_limit": int(scan_limit or 300), "state": state, "include_storage": bool(include_storage), "include_line_numbers": bool(include_line_numbers), "include_context": bool(include_context), }, "items": sliced, "counts": {"matches": len(sliced), "returned": len(sliced), "offset": int(offset or 0), "scan_limit": int(scan_limit or 300), "truncated": False, "complete": True, "scan_limit_hit": False, "owner_resolved": len(sliced), "owner_unresolved": 0, "owner_scan_limit_hit": False, "owner_indexed_module_refs": 0}, "diagnostics": {"note": "Saved-state CommonForm module search; physical storage details are hidden unless include_storage=true."}, } def code_saved_state_common_form_read(payload: dict[str, Any], *, include_text: bool, include_line_numbers: bool, max_chars: int, timeout_seconds: int) -> dict[str, Any] | None: object_type = str(payload.get("object_type") or payload.get("kind") or "").strip() object_name = str(payload.get("object_name") or payload.get("name") or "").strip() object_guid = str(payload.get("object_guid") or payload.get("guid") or "").strip() if canonical_kind(object_type) != "CommonForm" or not (object_name or object_guid): return None routine_name = str(payload.get("routine_name") or "") search = metadata_saved_state_modules_search( { "base_id": payload.get("base_id"), "object_type": "CommonForm", **({"object_name": object_name} if object_name else {}), **({"object_guid": object_guid} if object_guid else {}), **({"query": routine_name} if routine_name else {}), "limit": 2, "scan_limit": 1000, "preview_chars": int(max_chars or 100000), "timeout_seconds": int(timeout_seconds or 60), "include_storage": True, } ) streams: list[dict[str, Any]] = [] for module in search.get("modules") or []: if not isinstance(module, dict): continue streams.extend([stream for stream in module.get("streams") or [] if isinstance(stream, dict)]) if search.get("status") != "ok" or len(streams) != 1: return None module_text = form_embedded_module_public_text(str(streams[0].get("preview") or "")) text = module_text selection = None if routine_name: text, selection = _extract_bsl_routine_text_for_code_read(module_text, routine_name) if not text: return { "schema": "onec_code_read.v1", "method": "code.read", "status": "not_found", "error": "routine_not_found", "base_id": payload.get("base_id"), "diagnostics": {"message": f"Routine `{routine_name}` was not found in the saved-state form module."}, } result = { "schema": "onec_code_read.v1", "method": "code.read", "status": "ok", "base_id": payload.get("base_id"), "source": {"kind": "saved_state", "target": "common_form_module", "include_line_numbers": bool(include_line_numbers)}, "current_state": {"source": "saved_state", "activation_state": "not_activated"}, "resolved_owner": {"kind": "CommonForm", "name": object_name or None, "guid": object_guid or None}, "module": {"name": "Модуль формы", "form": object_name or None, **({"routine_name": routine_name} if routine_name else {})}, "selection": selection, "text": text if include_text else "", } if include_line_numbers and selection and selection.get("line_start") is not None: result["line"] = int(selection.get("line_start") or 1) result["column"] = 1 return result def code_read_layer_item(result: dict[str, Any] | None, *, source: str, include_text: bool) -> dict[str, Any]: current_state = ( {"source": "saved_state", "activation_state": "not_activated"} if source == "saved_state" else {"source": "active", "activation_state": "active"} ) if not isinstance(result, dict): return {"source": source, "status": "not_found", "current_state": current_state} item = { "source": source, "status": result.get("status") or "unknown", "current_state": result.get("current_state") if isinstance(result.get("current_state"), dict) else current_state, } if result.get("error"): item["error"] = result.get("error") if include_text and result.get("text") is not None: item["text"] = result.get("text") if isinstance(result.get("selection"), dict): item["selection"] = result.get("selection") if isinstance(result.get("diagnostics"), dict): item["diagnostics"] = result.get("diagnostics") return item def code_read_both_response(payload: dict[str, Any], *, base_id: str, saved_result: dict[str, Any] | None, active_result: dict[str, Any] | None, include_text: bool) -> dict[str, Any]: saved_layer = code_read_layer_item(saved_result, source="saved_state", include_text=include_text) active_layer = code_read_layer_item(active_result, source="active", include_text=include_text) ok_sources = [layer["source"] for layer in (saved_layer, active_layer) if layer.get("status") in {"ok", "summary", "text"}] saved_text = saved_layer.get("text") if isinstance(saved_layer.get("text"), str) else None active_text = active_layer.get("text") if isinstance(active_layer.get("text"), str) else None comparison = { "saved_status": saved_layer.get("status"), "active_status": active_layer.get("status"), "both_present": bool(saved_text is not None and active_text is not None), "differs": bool(saved_text is not None and active_text is not None and saved_text != active_text), } result = { "schema": "onec_code_read.v1", "method": "code.read", "status": "ok" if ok_sources else "not_found", "base_id": base_id, "current_state": {"source": "both", "activation_state": "mixed"}, "query": { "kind": payload.get("object_type") or payload.get("kind"), "name": payload.get("object_name") or payload.get("name"), "guid": payload.get("object_guid") or payload.get("guid"), "routine_name": payload.get("routine_name"), "state": "both", }, "layers": [saved_layer, active_layer], "comparison": comparison, } if include_text: if saved_text is not None: result["text"] = saved_text result["text_source"] = "saved_state" elif active_text is not None: result["text"] = active_text result["text_source"] = "active" return result def code_search_both_response(base_id: str, saved_result: dict[str, Any], active_result: dict[str, Any]) -> dict[str, Any]: saved_items = [dict(item) for item in saved_result.get("items") or [] if isinstance(item, dict)] active_items = [dict(item) for item in active_result.get("items") or [] if isinstance(item, dict)] for item in saved_items: item.setdefault("current_state", {"source": "saved_state", "activation_state": "not_activated"}) for item in active_items: item.setdefault("current_state", {"source": "active", "activation_state": "active"}) status = "ok" if saved_items or active_items else "not_found" saved_counts = saved_result.get("counts") if isinstance(saved_result.get("counts"), dict) else {} active_counts = active_result.get("counts") if isinstance(active_result.get("counts"), dict) else {} return { "schema": "onec_code_search.v1", "status": status, "base_id": base_id, "source": {"kind": "both", "layers": ["saved_state", "active"]}, "current_state": {"source": "both", "activation_state": "mixed"}, "query": {**(saved_result.get("query") if isinstance(saved_result.get("query"), dict) else {}), "state": "both"}, "items": saved_items + active_items, "layers": [ { "source": "saved_state", "status": saved_result.get("status") or ("ok" if saved_items else "not_found"), "current_state": {"source": "saved_state", "activation_state": "not_activated"}, "counts": saved_counts, }, { "source": "active", "status": active_result.get("status") or ("ok" if active_items else "not_found"), "current_state": {"source": "active", "activation_state": "active"}, "counts": active_counts, }, ], "counts": { "matches": len(saved_items) + len(active_items), "returned": len(saved_items) + len(active_items), "saved_matches": len(saved_items), "active_matches": len(active_items), "complete": bool(saved_counts.get("complete", True)) and bool(active_counts.get("complete", True)), "scan_limit_hit": bool(saved_counts.get("scan_limit_hit", False)) or bool(active_counts.get("scan_limit_hit", False)), }, "comparison": { "saved_status": saved_result.get("status"), "active_status": active_result.get("status"), "saved_matches": len(saved_items), "active_matches": len(active_items), "both_present": bool(saved_items and active_items), }, } def public_code_origin(origin: Any, *, include_storage: bool) -> Any: """Expose source semantics without making SQL storage a programming selector.""" if not isinstance(origin, dict) or include_storage: return origin return { key: value for key, value in origin.items() if key not in {"storage_table", "table", "file_name", "module_id"} } def code_search(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, "code.search") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload payload = normalize_configuration_view(payload, "code.search") if payload.get("status") == "invalid_argument": return payload configuration_view = str(payload.get("configuration_view") or "effective_working") base_id_or_error = require_base_id(payload, "code.search") if isinstance(base_id_or_error, dict): return base_id_or_error query_value = payload.get("query") or payload.get("pattern") if query_value is not None and not isinstance(query_value, str): return invalid_argument("code.search", "query", "query must be a JSON string.") query = str(query_value or "").strip() if not query: return invalid_argument("code.search", "query", "Передайте непустой query.") include_line_numbers, include_line_numbers_error = strict_bool_argument(payload, "include_line_numbers", method="code.search", default=False) if include_line_numbers_error: return include_line_numbers_error include_context, include_context_error = strict_bool_argument(payload, "include_context", method="code.search", default=True) if include_context_error: return include_context_error include_storage, include_storage_error = strict_include_storage(payload, "code.search") if include_storage_error: return include_storage_error limit, limit_error = parse_int_alias_argument(payload, "limit", "max_matches", method="code.search", default=25, minimum=1, maximum=500) if limit_error: return limit_error offset, offset_error = parse_int_argument(payload, "offset", method="code.search", default=0, minimum=0) if offset_error: return offset_error scan_limit, scan_limit_error = parse_int_argument(payload, "scan_limit", method="code.search", default=300, minimum=1, maximum=5000) if scan_limit_error: return scan_limit_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method="code.search", default=60, minimum=1) if timeout_error: return timeout_error state = str(payload.get("state") or "working").strip().lower() if state not in EXTENSION_OBJECTS_FIND_STATES: return invalid_argument("code.search", "state", "Unsupported saved-state mode.", allowed_values=sorted(EXTENSION_OBJECTS_FIND_STATES)) saved_extension_guid = str(payload.get("extension_guid") or "").strip().lower() if "extension_guid" in payload and not is_guid_text(saved_extension_guid): return invalid_argument("code.search", "extension_guid", "extension_guid must be a GUID string.") module_ordinal = payload.get("module_ordinal") if module_ordinal is not None: _, module_ordinal_error = parse_ordinal(module_ordinal, "code.search", argument="module_ordinal") if module_ordinal_error: return module_ordinal_error object_selector, resolved_object = _code_query_object_selector(payload) saved_state_result = code_saved_state_common_form_module_search( {**payload, "base_id": base_id_or_error}, query=query, limit=int(limit or 25), offset=int(offset or 0), scan_limit=int(scan_limit or 300), timeout_seconds=int(timeout_seconds or 60), include_line_numbers=bool(include_line_numbers), include_context=bool(include_context), include_storage=bool(include_storage), state=state, ) if saved_state_result is not None: if state == "both": active_result = code_search( { **payload, "base_id": base_id_or_error, "state": "active", "configuration_view": "runtime_applied", } ) return annotate_configuration_view(code_search_both_response(base_id_or_error, saved_state_result, active_result), configuration_view) return annotate_configuration_view(saved_state_result, configuration_view) search_payload = { "base_id": base_id_or_error, "query": query, "include_storage": bool(include_storage), "scan_limit": int(scan_limit or 300), "limit": int(limit or 25) + int(offset or 0), "routine_name": payload.get("routine_name"), "scope": payload.get("scope", "auto"), "table": payload.get("table", "auto"), "prefix": payload.get("prefix", ""), "state": state, "max_chars": payload.get("max_chars"), "resolve_owners": True, "timeout_seconds": int(timeout_seconds or 60), **({"kind": object_selector.get("kind")} if object_selector.get("kind") else {}), **({"name": object_selector.get("name")} if object_selector.get("name") is not None else {}), **({"guid": object_selector.get("guid")} if object_selector.get("guid") is not None else {}), **({"extension": payload.get("extension")} if payload.get("extension") not in {None, ""} else {}), **({"extension_guid": payload.get("extension_guid")} if payload.get("extension_guid") not in {None, ""} else {}), **( {"module_ordinal": module_ordinal} if module_ordinal is not None and payload.get("module_ordinal") is not None else {} ), } modules_result = search_modules(search_payload) if modules_result.get("status") == "error": return modules_result raw_matches = modules_result.get("matches") or [] sliced = raw_matches[int(offset or 0) : int(offset or 0) + int(limit or 25)] items: list[dict[str, Any]] = [] for match in sliced: snippet = match.get("snippet") or {} context_text = str(snippet.get("text") or "") offset_value = snippet.get("offset") item_read_selector = dict(match.get("read_selector") or {}) if item_read_selector: item_read_selector["method"] = "code.read" item_line = None item_column = None if truthy(include_line_numbers): text_for_line = "" routine_name = str(match.get("module", {}).get("routine_name") or "") if routine_name: owner_ref = match.get("read_selector") or {} read_payload = { **({"kind": owner_ref.get("kind")} if owner_ref.get("kind") else {}), **({"guid": owner_ref.get("guid")} if owner_ref.get("guid") else {}), **({"name": owner_ref.get("name")} if owner_ref.get("name") else {}), **({"module_ordinal": owner_ref.get("module_ordinal")} if owner_ref.get("module_ordinal") else {}), **({"module_id": owner_ref.get("module_id")} if owner_ref.get("module_id") else {}), **({"module_ref": owner_ref.get("module_ref")} if owner_ref.get("module_ref") else {}), "base_id": base_id_or_error, "routine_name": routine_name, "preview": True, "max_chars": int(payload.get("max_chars") or 4000), } read_payload["include_storage"] = False read_payload["max_chars"] = int(payload.get("max_chars") or 4000) read_result = read_module(read_payload) if read_result.get("status") == "ok": selection = read_result.get("selection") or {} if selection.get("line_start") is not None: item_line = int(selection.get("line_start")) item_column = 1 if offset_value is not None: item_line, item_column = _snippet_to_line_column(str(read_result.get("text") or ""), int(offset_value)) if context_text and not include_context: context_text = "" elif offset_value is not None and context_text: item_line, item_column = _snippet_to_line_column(context_text, int(offset_value)) items.append( { "match": str(context_text if context_text else ""), "line": item_line, "column": item_column, "context": context_text, "resolved_owner": match.get("owner"), "origin": public_code_origin(match.get("origin"), include_storage=bool(include_storage)), "module": match.get("module"), **({"qualified_name": match.get("qualified_name")} if match.get("qualified_name") else {}), **({"display_name": match.get("display_name")} if match.get("display_name") else {}), **({"activation_state": match.get("activation_state")} if match.get("activation_state") else {}), **({"current_state": {"source": "saved_state", "activation_state": "not_activated"}} if str(match.get("activation_state") or "").startswith("saved") else {}), "query": query_value, "read_selector": item_read_selector, "source": item_read_selector, } ) if not truthy(include_context): item = items[-1] item.pop("context", None) item["match"] = match.get("match") or query public_status = modules_result.get("status") or ("ok" if items else "not_found") if not items and public_status == "ok": public_status = "not_found" return annotate_configuration_view({ "schema": "onec_code_search.v1", "status": public_status, "base_id": base_id_or_error, "source": modules_result.get("source"), "object": resolved_object, "query": { "query": query, "kind": resolved_object.get("kind"), "name": resolved_object.get("name"), "guid": resolved_object.get("guid"), "limit": int(limit or 25), "offset": int(offset or 0), "scan_limit": int(scan_limit or 300), "state": state, "include_storage": bool(include_storage), "include_line_numbers": bool(include_line_numbers), "include_context": bool(include_context), }, "items": items, "counts": { "matches": len(items), "returned": len(items), "offset": int(offset or 0), "scan_limit": int(scan_limit or 300), "truncated": bool(modules_result.get("counts", {}).get("truncated")), "complete": bool(modules_result.get("counts", {}).get("complete", not modules_result.get("counts", {}).get("truncated"))), "scan_limit_hit": bool(modules_result.get("counts", {}).get("scan_limit_hit", modules_result.get("counts", {}).get("truncated"))), "owner_resolved": int(modules_result.get("counts", {}).get("owner_resolved") or 0), "owner_unresolved": int(modules_result.get("counts", {}).get("owner_unresolved") or 0), "owner_scan_limit_hit": bool(modules_result.get("counts", {}).get("owner_scan_limit_hit", False)), "owner_indexed_module_refs": int(modules_result.get("counts", {}).get("owner_indexed_module_refs") or 0), }, "diagnostics": modules_result.get("diagnostics"), }, configuration_view) def code_read(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, "code.read") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload payload = normalize_configuration_view(payload, "code.read") if payload.get("status") == "invalid_argument": return payload configuration_view = str(payload.get("configuration_view") or "effective_working") base_id_or_error = require_base_id(payload, "code.read") if isinstance(base_id_or_error, dict): return base_id_or_error include_line_numbers, include_line_numbers_error = strict_bool_argument(payload, "include_line_numbers", method="code.read", default=False) if include_line_numbers_error: return include_line_numbers_error _, include_storage_error = strict_include_storage(payload, "code.read") if include_storage_error: return include_storage_error include_text, include_text_error = strict_bool_argument(payload, "include_text", method="code.read", default=True) if include_text_error: return include_text_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method="code.read", default=60, minimum=1) if timeout_error: return timeout_error max_chars = int(payload.get("max_chars") or 0) if payload.get("max_chars") not in {None, ""} else 100000 state = str(payload.get("state") or "working").strip().lower() if state not in EXTENSION_OBJECTS_FIND_STATES: return invalid_argument("code.read", "state", "Unsupported saved-state mode.", allowed_values=sorted(EXTENSION_OBJECTS_FIND_STATES)) object_selector, _ = _code_query_object_selector(payload) module_ref_value = str(first_non_empty_arg(payload, "module_ref", "module_id") or "").strip() module_ref_table = "" if module_ref_value: parsed_table, _parsed_file_name, _parsed_stream = parse_module_id(module_ref_value) module_ref_table = parsed_table saved_state_module_ref = bool(module_ref_table in FORM_ELEMENT_SAVED_STATE_TABLES) is_saved_state_common_form_request = canonical_kind(str(payload.get("object_type") or payload.get("kind") or "")) == "CommonForm" and bool( payload.get("object_name") or payload.get("name") or payload.get("object_guid") or payload.get("guid") ) saved_state_result = None if state in {"working", "save", "both"}: if saved_state_module_ref: saved_read_payload = { **payload, "base_id": base_id_or_error, "include_storage": False, "include_text": bool(include_text), "max_chars": max_chars, "state": "save", } saved_read_payload.pop("include_line_numbers", None) saved_state_result = read_module(saved_read_payload) if isinstance(saved_state_result, dict) and saved_state_result.get("status") == "source_missing": saved_table, saved_file_name, saved_stream = parse_module_id(module_ref_value) active_table = {"ConfigSave": "Config", "ConfigCASSave": "ConfigCAS"}.get(saved_table, "") if active_table and saved_file_name: active_module_ref = f"{active_table}:{saved_file_name}" + (f"#stream:{saved_stream}" if saved_stream is not None else "") active_fallback = read_module({ **saved_read_payload, "module_ref": active_module_ref, "module_id": active_module_ref, "state": "active", }) if isinstance(active_fallback, dict) and active_fallback.get("status") in {"ok", "summary", "text"}: saved_state_result = dict(active_fallback) saved_state_result["current_state"] = {"source": "active_fallback", "activation_state": "active"} saved_state_result["diagnostics"] = { **(active_fallback.get("diagnostics") if isinstance(active_fallback.get("diagnostics"), dict) else {}), "message": "Saved working copy was absent; the effective view fell back to the corresponding active module.", } if isinstance(saved_state_result, dict): saved_state_result = dict(saved_state_result) saved_state_result["schema"] = "onec_code_read.v1" saved_state_result["method"] = "code.read" saved_state_result["source"] = { "kind": "code_read", "target": "module_or_routine", "include_line_numbers": bool(include_line_numbers), } saved_state_result["current_state"] = {"source": "saved_state", "activation_state": "not_activated"} else: saved_state_result = code_saved_state_common_form_read( {**payload, "base_id": base_id_or_error}, include_text=bool(include_text), include_line_numbers=bool(include_line_numbers), max_chars=int(max_chars or 100000), timeout_seconds=int(timeout_seconds or 60), ) if saved_state_result is not None and state != "both": saved_state_result = enrich_code_read_logical_owner(saved_state_result, payload, base_id=base_id_or_error) return attach_effective_routine_chain( annotate_configuration_view(saved_state_result, configuration_view), payload, view=configuration_view, ) if state == "save" and (is_saved_state_common_form_request or saved_state_module_ref): return annotate_configuration_view({ "schema": "onec_code_read.v1", "method": "code.read", "status": "not_found", "error": "saved_state_code_not_found", "base_id": base_id_or_error, "current_state": {"source": "saved_state", "activation_state": "not_activated"}, "diagnostics": {"message": "Saved-state code was not found for the requested CommonForm selector."}, }, configuration_view) read_payload = { "base_id": base_id_or_error, "include_storage": False, "include_text": bool(include_text), "preview": truthy(payload.get("preview")), } read_payload.update(payload) if state == "both": read_payload["state"] = "active" if saved_state_module_ref: read_payload.pop("module_ref", None) read_payload.pop("module_id", None) read_payload.pop("include_line_numbers", None) if "query" in read_payload: read_payload.pop("query") read_payload.update(object_selector) if state == "both" and saved_state_module_ref and not any( [object_selector.get("kind"), object_selector.get("guid"), object_selector.get("name"), first_non_empty_arg(payload, "module_ordinal", "module_index", "module_number")] ): return annotate_configuration_view(code_read_both_response( payload, base_id=base_id_or_error, saved_result=saved_state_result, active_result={ "schema": "onec_code_read.v1", "method": "code.read", "status": "not_found", "error": "active_selector_required", "current_state": {"source": "active", "activation_state": "active"}, "diagnostics": {"message": "Active-layer comparison for a saved-state module_ref requires an owner selector or module ordinal; the saved module_ref itself is not an active-layer handle."}, }, include_text=bool(include_text), ), configuration_view) read_payload["max_chars"] = max_chars if "mode" in read_payload and read_payload["mode"] is not None and str(read_payload["mode"]).strip() == "summary": read_payload["mode"] = "summary" result = read_module(read_payload) if result.get("status") not in {"ok", "summary", "text"}: result = dict(result) result["method"] = "code.read" result["current_state"] = {"source": "active", "activation_state": "active"} if state == "both": return annotate_configuration_view(code_read_both_response( payload, base_id=base_id_or_error, saved_result=saved_state_result, active_result=result, include_text=bool(include_text), ), configuration_view) return annotate_configuration_view(result, configuration_view) result["schema"] = "onec_code_read.v1" result["method"] = "code.read" result["source"] = { "kind": "code_read", "target": "module_or_routine", "include_line_numbers": bool(include_line_numbers), } result["current_state"] = {"source": "active", "activation_state": "active"} if state == "both": return annotate_configuration_view(code_read_both_response( payload, base_id=base_id_or_error, saved_result=saved_state_result, active_result=result, include_text=bool(include_text), ), configuration_view) if truthy(include_line_numbers): selection = result.get("selection") or {} if selection.get("line_start") is not None: result["line"] = int(selection.get("line_start")) result["column"] = 1 if result.get("text_range", {}).get("offset") is not None: line, column = _snippet_to_line_column(str(result.get("text") or ""), int(result.get("text_range", {}).get("offset"))) result["line"], result["column"] = line, column elif result.get("text_range", {}).get("offset") is not None and result.get("text"): line, column = _snippet_to_line_column(str(result.get("text") or ""), int(result.get("text_range", {}).get("offset"))) result["line"] = line result["column"] = column result["resolved_owner"] = { "kind": object_selector.get("kind"), "guid": object_selector.get("guid"), "name": object_selector.get("name"), } result = enrich_code_read_logical_owner(result, payload, base_id=base_id_or_error) result["resolved_selector"] = { "base_id": base_id_or_error, **({ "kind": object_selector.get("kind")} if object_selector.get("kind") else {}), **({"guid": object_selector.get("guid")} if object_selector.get("guid") is not None else {}), **({"name": object_selector.get("name")} if object_selector.get("name") is not None else {}), **({"module_id": result.get("module_id")} if result.get("module_id") else {}), **({"module_ordinal": first_non_empty_arg(payload, "module_ordinal", "module_index", "module_number")} if first_non_empty_arg(payload, "module_ordinal", "module_index", "module_number") is not None else {}), } return attach_effective_routine_chain( annotate_configuration_view(result, configuration_view), payload, view=configuration_view, ) def _code_binding_extract_placeholders(text: str) -> list[str]: if not isinstance(text, str): return [] pattern = re.compile(r"\{([^\{\}]+)\}") matches = pattern.findall(text.replace("\r\n", "\n")) return [value.strip() for value in matches if str(value or "").strip()] def templates_bindings(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, "templates.bindings") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, "templates.bindings") if isinstance(base_id_or_error, dict): return base_id_or_error object_selector, _ = _code_query_object_selector(payload) include_storage, include_storage_error = strict_include_storage(payload, "templates.bindings") if include_storage_error: return include_storage_error template_name_filter = payload.get("template") template_query = { **payload, "base_id": base_id_or_error, **({"name_filter": template_name_filter} if template_name_filter else {}), "include_text": True, "include_tree": False, "include_storage": bool(include_storage), } template_query.update(object_selector) templates_result = metadata_object_templates(template_query) if templates_result.get("status") != "ok": return { "schema": "onec_templates_bindings.v1", "status": "error", "base_id": base_id_or_error, "method": "templates.bindings", "source": templates_result.get("source"), "object": templates_result.get("object"), "diagnostics": templates_result.get("diagnostics", {"message": "Failed to read templates."}), "bindings": [], } bindings: list[dict[str, Any]] = [] for template in templates_result.get("templates") or []: if not isinstance(template, dict): continue source_text = template.get("text") or template.get("text_preview") or "" placeholders = _code_binding_extract_placeholders(str(source_text)) binding_item = { "template": { "name": template.get("name"), "presentation": template.get("presentation") or template.get("name"), "file_name": template.get("file_name"), }, "bindings": sorted(set(placeholders)), "binding_count": len(set(placeholders)), "format": template.get("format") or template.get("kind"), } if bool(include_storage): binding_item["source"] = { "kind": "live_sql", "table": template.get("table"), "part_id": template.get("part_id"), "encoding": template.get("encoding"), } bindings.append(binding_item) return { "schema": "onec_templates_bindings.v1", "status": "ok" if bindings else "not_found", **({"error": "not_found"} if not bindings else {}), "base_id": base_id_or_error, "method": "templates.bindings", "object": templates_result.get("object"), "source": templates_result.get("source"), "query": { "kind": object_selector.get("kind"), "name": object_selector.get("name"), "guid": object_selector.get("guid"), "template": template_name_filter, }, "bindings": bindings, "counts": {"templates": len(bindings), "total_bindings": sum((len(item.get("bindings") or []) for item in bindings))}, "diagnostics": templates_result.get("diagnostics", {}), } def metadata_extension_action_from_evidence( *, source: str, method_name: str, read_result: dict[str, Any] | None = None, module: dict[str, Any] | None = None, ) -> dict[str, Any]: origin = (read_result or {}).get("origin") if isinstance((read_result or {}).get("origin"), dict) else {} selection = (read_result or {}).get("selection") if isinstance((read_result or {}).get("selection"), dict) else {} evidence_sources = [origin, selection, module or {}] raw_action = "" for evidence in evidence_sources: for key in ("operation_class", "action_class", "extension_action", "operation", "action", "change_kind"): value = str(evidence.get(key) or "").strip() if value: raw_action = value break if raw_action: break if not raw_action and source == "extension": source_text = str((read_result or {}).get("text") or (read_result or {}).get("preview") or "") annotation = re.search( r"&\s*(Перед|После|Вместо|ИзменениеИКонтроль)\s*\(\s*[\"']?([^\"'\)\s]+)", source_text, re.IGNORECASE, ) if annotation and normalize(annotation.group(2)) == normalize(method_name): raw_action = { "перед": "insert_before", "после": "insert_after", "вместо": "replace", "изменениеиконтроль": "replace_with_control", }.get(normalize(annotation.group(1)), "") if raw_action: operation_class = metadata_write_plan_operation_class(raw_action) else: operation_class = "" if source != "extension": return { "status": "ok", "source": "configuration", "routine": method_name, "operation_class": operation_class or "base_definition", "requires_control_fragment": False, } known_extension_operations = {"insert_before", "insert_after", "replace", "replace_with_control"} if operation_class in known_extension_operations: return { "status": "ok", "source": "extension", "routine": method_name, "operation_class": operation_class, "raw_action": raw_action, "requires_control_fragment": operation_class == "replace_with_control", } return { "status": "unknown", "source": "extension", "routine": method_name, "operation_class": "unknown_extension_action", "requires": [ "extension routine action evidence: insert_before, insert_after, replace, or replace_with_control", "controlled base fragment when operation is replace_with_control", ], "diagnostics": { "message": "Routine text was found in an extension layer, but the adapter has not resolved the extension action type yet. Do not treat this as a plain replace without action metadata." }, } def metadata_resolve_overrides_write_plan_evidence( *, base_id: str, method_name: str, object_payload: dict[str, Any], chain: list[dict[str, Any]], ) -> dict[str, Any]: object_kind = object_payload.get("kind") or object_payload.get("type") if object_payload else None object_name = object_payload.get("name") if object_payload else None object_guid = object_payload.get("guid") if object_payload else None object_ref = object_selector_ref(str(object_kind or ""), str(object_name or "")) evidence: dict[str, Any] = { "method": METADATA_WRITE_PLAN_METHOD, "base_id": base_id, "target": { "kind": "module", "routine_name": method_name, }, "next_resolution": { "method": SAVED_STATE_MODULES_SEARCH_METHOD, "params": { "base_id": base_id, "query": method_name, "limit": 10, **({"ref": object_ref} if object_ref else {}), }, }, "diagnostics": { "message": "Pass target.extension_action into metadata.write.plan together with a concrete saved-state module route before apply." }, } if object_payload: if object_kind: evidence["target"]["object_type"] = object_kind evidence["next_resolution"]["params"]["object_type"] = object_kind if object_name: evidence["target"]["object_name"] = object_name evidence["next_resolution"]["params"]["object_name"] = object_name if object_guid: evidence["target"]["object_guid"] = object_guid if object_kind and object_name: canonical = metadata_write_plan_path_parts(f"{object_kind}.{object_name}.{method_name}") if canonical.get("is_full_path") and canonical.get("path_kind") == "module_routine": evidence["target"]["canonical_path"] = canonical.get("canonical_path") extension_actions = [ item.get("extension_action") for item in chain if item.get("source") == "extension" and isinstance(item.get("extension_action"), dict) ] if len(extension_actions) == 1: evidence["target"]["extension_action"] = extension_actions[0] evidence["next_resolution"]["params"]["layer"] = "extension_saved_state" action_class = metadata_write_plan_operation_class(str(extension_actions[0].get("operation_class") or "")) if action_class not in {"", "unknown_extension_action", "base_definition"}: evidence["intent"] = {"operation": action_class} elif extension_actions: evidence["next_resolution"]["params"]["layer"] = "extension_saved_state" evidence["extension_actions"] = extension_actions evidence["diagnostics"]["message"] = "Multiple extension actions were found; narrow the extension/module before building metadata.write.plan." else: base_actions = [ item.get("extension_action") for item in chain if item.get("source") == "configuration" and isinstance(item.get("extension_action"), dict) and item.get("extension_action", {}).get("operation_class") == "base_definition" ] if base_actions: evidence["next_resolution"]["params"]["layer"] = "base_saved_state" return evidence def metadata_resolve_overrides(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, "metadata.resolve_overrides") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, "metadata.resolve_overrides") if isinstance(base_id_or_error, dict): return base_id_or_error method_name = str(payload.get("method_name") or "").strip() if not method_name: return invalid_argument("metadata.resolve_overrides", "method_name", "method_name is required.") state = str(payload.get("state") or "working").strip().lower() if state not in EXTENSION_OBJECTS_FIND_STATES: return invalid_argument("metadata.resolve_overrides", "state", "Unsupported saved-state mode.", allowed_values=sorted(EXTENSION_OBJECTS_FIND_STATES)) object_selector, _ = _code_query_object_selector(payload) if not object_selector.get("kind") and not object_selector.get("guid") and not object_selector.get("name"): return invalid_argument( "metadata.resolve_overrides", "selector", OBJECT_SELECTOR_REQUIRED_MESSAGE_NO_ORDINAL, ) object_ref = { **({"kind": object_selector.get("kind")} if object_selector.get("kind") else {}), **({"name": object_selector.get("name")} if object_selector.get("name") is not None else {}), **({"guid": object_selector.get("guid")} if object_selector.get("guid") is not None else {}), "base_id": base_id_or_error, } extension_filter = str(payload.get("extension") or "").strip() modules_result = metadata_object_modules(object_ref) if modules_result.get("status") != "ok" and not (extension_filter and state in {"working", "save", "both"}): return public_error_result(modules_result, include_storage=False, method="metadata.resolve_overrides") modules = [module for module in modules_result.get("modules") or [] if isinstance(module, dict)] if modules_result.get("status") == "ok" else [] chain: list[dict[str, Any]] = [] if extension_filter and state in {"working", "save", "both"}: extension_guid, extension_error = extension_filter_to_guid(base_id_or_error, extension_filter, method="metadata.resolve_overrides") if not extension_error and extension_guid: saved_modules_result = search_modules( { "base_id": base_id_or_error, "extension": extension_filter, "query": method_name, "routine_name": method_name, "state": "save", "limit": int(payload.get("saved_state_limit") or 50), "scan_limit": int(payload.get("scan_limit") or 1000), "include_storage": False, "timeout_seconds": int(payload.get("timeout_seconds") or 60), } ) for module_match in saved_modules_result.get("matches") or []: if not isinstance(module_match, dict): continue read_selector = module_match.get("read_selector") if isinstance(module_match.get("read_selector"), dict) else {} module_ref = str(read_selector.get("module_ref") or "") if not module_ref: continue read_result = read_module( { "base_id": base_id_or_error, "module_ref": module_ref, "routine_name": method_name, "include_storage": False, "max_chars": 100000, "include_text": True, "preview": True, } ) if read_result.get("status") not in {"ok", "summary"}: continue selection = read_result.get("selection") or {} if selection.get("status") == "not_found": continue module_info = module_match.get("module") if isinstance(module_match.get("module"), dict) else {} chain.append( { "order": len(chain) + 1, "mechanism": "routine", "method": method_name, "source": "saved_state", "activation_state": module_match.get("activation_state") or "saved_state", "extension_action": metadata_extension_action_from_evidence( source="extension", method_name=method_name, read_result=read_result, module={"module_id": module_ref}, ), "line_start": selection.get("line_start"), "line_end": selection.get("line_end"), "match_by": selection.get("match_by"), "module": { "module_ref": module_ref, "name": module_info.get("name") or "Saved-state module", "form": module_info.get("form"), "stream_index": module_info.get("stream_index"), }, "read_selector": { "base_id": base_id_or_error, "module_ref": module_ref, "routine_name": method_name, "preview": True, "max_chars": 100000, }, } ) elif extension_error: return extension_error if state == "save": modules = [] for ordinal, module in enumerate(modules, start=1): module_id = str(module.get("module_id") or "") if not module_id: continue module_read_payload = { "base_id": base_id_or_error, "module_id": module_id, "routine_name": method_name, "include_storage": False, "max_chars": 100000, "include_text": True, "preview": True, } read_result = read_module(module_read_payload) if read_result.get("status") not in {"ok", "summary"}: continue selection = read_result.get("selection") or {} if selection.get("status") == "not_found": continue source = "configuration" if module_id.startswith("ConfigCAS:"): source = "extension" extension_action = metadata_extension_action_from_evidence( source=source, method_name=method_name, read_result=read_result, module=module, ) chain.append( { "order": len(chain) + 1, "mechanism": "routine", "method": method_name, "source": source, "extension_action": extension_action, "line_start": selection.get("line_start"), "line_end": selection.get("line_end"), "match_by": selection.get("match_by"), "module": { "module_ordinal": ordinal, "name": public_module_row( module, include_storage=False, ordinal=ordinal, owner_kind=object_ref.get("kind"), owner_name=object_ref.get("name"), ).get("name"), }, "read_selector": { "base_id": base_id_or_error, "module_id": module_id, "routine_name": method_name, "preview": True, "max_chars": 100000, }, } ) return { "schema": "onec_metadata_resolve_overrides.v1", "status": "ok" if chain else "not_found", **({"error": "not_found"} if not chain else {}), "base_id": base_id_or_error, "method": "metadata.resolve_overrides", "object": (modules_result.get("object") or {}), "state": state, "target_method": method_name, "chain": chain, "extension_actions": [ item.get("extension_action") for item in chain if item.get("source") == "extension" and isinstance(item.get("extension_action"), dict) ], "write_plan_evidence": metadata_resolve_overrides_write_plan_evidence( base_id=base_id_or_error, method_name=method_name, object_payload=modules_result.get("object") if isinstance(modules_result.get("object"), dict) else {}, chain=chain, ), "counts": {"steps": len(chain), "resolved": len(chain), "not_found": int(len(modules) - len(chain)) if modules else 0}, "diagnostics": { "message": "Chain is built from module text scan of discovered object modules. Extension names are inferred from module storage prefix; точное определение расширения требует metadata.module_owner_cache." } if chain else {"message": "Переопределения не найдены в доступных модулях объекта."}, } def diagnostics_call_chain(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, "diagnostics.call_chain") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, "diagnostics.call_chain") if isinstance(base_id_or_error, dict): return base_id_or_error method_name = str(payload.get("entry_method") or payload.get("method_name") or "").strip() if not method_name: return invalid_argument("diagnostics.call_chain", "entry_method", "entry_method is required.") object_selector, _ = _code_query_object_selector(payload) if not object_selector.get("kind") and not object_selector.get("guid") and not object_selector.get("name"): return invalid_argument("diagnostics.call_chain", "selector", OBJECT_SELECTOR_REQUIRED_MESSAGE_NO_ORDINAL) override = metadata_resolve_overrides( { "base_id": base_id_or_error, "method_name": method_name, "object_type": object_selector.get("kind"), "object_name": object_selector.get("name"), "object_guid": object_selector.get("guid"), } ) if override.get("status") not in {"ok", "not_found"}: return override usage = code_search( { "base_id": base_id_or_error, "query": method_name, "object_type": object_selector.get("kind"), "object_name": object_selector.get("name"), "object_guid": object_selector.get("guid"), "limit": 200, "include_context": False, "include_storage": False, } ) call_nodes = override.get("chain") if isinstance(override.get("chain"), list) else [] return { "schema": "onec_diagnostics_call_chain.v1", "status": "ok", "base_id": base_id_or_error, "entry": { "method": method_name, "object": { "kind": object_selector.get("kind"), "name": object_selector.get("name"), "guid": object_selector.get("guid"), }, }, "chain": call_nodes, "usage": { "status": usage.get("status"), "matches": len(usage.get("items") or []), "items": usage.get("items") or [], }, "risks": [ "Диагностика формирует приблизительный статический граф (без исполнения, без runtime данных).", ], "counts": {"chain_nodes": len(call_nodes), "usage_matches": len(usage.get("items") or [])}, } def codec_decode(payload: dict[str, Any]) -> dict[str, Any]: base_id_or_error = require_base_id(payload, "codec.decode") if isinstance(base_id_or_error, dict): return base_id_or_error table_or_error = storage_table(payload, "codec.decode") if isinstance(table_or_error, dict): return table_or_error include_text, include_text_error = strict_bool_argument(payload, "include_text", method="codec.decode", default=True) if include_text_error: return include_text_error include_tree, include_tree_error = strict_bool_argument(payload, "include_tree", method="codec.decode", default=False) if include_tree_error: return include_tree_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method="codec.decode", default=30, minimum=1) if timeout_error: return timeout_error diagnostic_error = require_diagnostic_mode(payload, "codec.decode") if diagnostic_error: return diagnostic_error if "file_name" in payload and not isinstance(payload.get("file_name"), str): return invalid_argument("codec.decode", "file_name", "file_name must be a JSON string.") file_name = str(payload.get("file_name") or "") if not file_name or Path(file_name).name != file_name: return { "schema": "onec_adapter_request_error.v1", "method": "codec.decode", "status": "invalid_argument", "error": "invalid_argument", "argument": "file_name", } data, config, error = read_storage_file_bytes(base_id_or_error, table_or_error, file_name, timeout_seconds=int(timeout_seconds or 30)) if error: error["method"] = "codec.decode" return error decoded = decode_payload_full(data, include_text=bool(include_text), include_tree=bool(include_tree)) return { "schema": "onec_codec_decode.v1", "status": decoded.get("status"), "base_id": base_id_or_error, "source": {"kind": "live_sql", "database": config["database"], "table": table_or_error, "file_name": file_name}, "decoded": decoded, } def codec_encode(payload: dict[str, Any]) -> dict[str, Any]: include_payload, include_payload_error = strict_bool_argument(payload, "include_payload", method="codec.encode", default=False) if include_payload_error: return include_payload_error if "text" in payload and not isinstance(payload.get("text"), str): return invalid_argument("codec.encode", "text", "text must be a JSON string.") if "source" in payload and payload.get("source") is not None and not isinstance(payload.get("source"), dict): return invalid_argument("codec.encode", "source", "source must be a JSON object.") timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method="codec.encode", default=30, minimum=1) if timeout_error: return timeout_error diagnostic_error = require_diagnostic_mode(payload, "codec.encode") if diagnostic_error: return diagnostic_error try: from parser.payload import decode_payload_lossless, encode_brace_tree, encode_payload_lossless except Exception as exc: return { "schema": "onec_codec_encode.v1", "status": "error", "diagnostics": {"message": f"Payload codec is unavailable: {exc}"}, } decoded_meta = payload.get("decoded") if isinstance(payload.get("decoded"), dict) else None source = payload.get("source") if isinstance(payload.get("source"), dict) else None original_bytes = None config = None if source: base_id = str(source.get("base_id") or payload.get("base_id") or "") table = str(source.get("table") or payload.get("table") or "") file_name = str(source.get("file_name") or payload.get("file_name") or "") if not base_id: return base_id_required("codec.encode") if table not in STORAGE_TABLES or not file_name: return { "schema": "onec_adapter_request_error.v1", "method": "codec.encode", "status": "error", "error": "source_required", } original_bytes, config, error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=int(timeout_seconds or 30)) if error: error["method"] = "codec.encode" return error decoded_meta = decode_payload_lossless(original_bytes) if not decoded_meta: return { "schema": "onec_adapter_request_error.v1", "method": "codec.encode", "status": "error", "error": "decoded_or_source_required", } try: if "tree" in payload: encoded = encode_brace_tree(payload["tree"], decoded_meta) elif "text" in payload: encoded = encode_payload_lossless(decoded_meta, text=str(payload.get("text") or "")) elif original_bytes is not None: encoded = original_bytes else: encoded = encode_payload_lossless(decoded_meta) except Exception as exc: return { "schema": "onec_codec_encode.v1", "status": "error", "diagnostics": {"message": str(exc)}, } result = { "schema": "onec_codec_encode.v1", "status": "ok", "source": source, "encoded": { "bytes": len(encoded), "sha1": hashlib.sha1(encoded).hexdigest(), "compression": decoded_meta.get("compression"), "encoding": decoded_meta.get("encoding"), "matches_original": bool(original_bytes is not None and encoded == original_bytes), }, } if include_payload: result["encoded"]["payload_hex"] = encoded.hex() if config: result["source"]["database"] = config["database"] return result def changes_propose(payload: dict[str, Any]) -> dict[str, Any]: string_error = validate_optional_string_arguments(payload, "changes.propose", ["summary", "description", "module_id", "table", "file_name"]) if string_error: return string_error include_text, include_text_error = strict_bool_argument(payload, "include_text", method="changes.propose", default=False) if include_text_error: return include_text_error include_payload, include_payload_error = strict_bool_argument(payload, "include_payload", method="changes.propose", default=False) if include_payload_error: return include_payload_error preserve_format, preserve_format_error = strict_bool_argument(payload, "preserve_format", method="changes.propose", default=False) if preserve_format_error: return preserve_format_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method="changes.propose", default=30, minimum=1) if timeout_error: return timeout_error try: from parser.payload import ( decode_payload_lossless, encode_brace_tree, encode_payload_lossless, append_brace_text_child, get_tree_path, patch_brace_text_path, parse_brace_text, scalar, serialize_brace_tree, set_tree_path, swap_brace_text_paths, ) except Exception as exc: return { "schema": "onec_change_proposal.v1", "status": "error", "applied": False, "diagnostics": {"message": f"Payload codec is unavailable: {exc}"}, } if "source" in payload and payload.get("source") is not None and not isinstance(payload.get("source"), dict): return invalid_argument("changes.propose", "source", "source must be a JSON object.") source = payload.get("source") if isinstance(payload.get("source"), dict) else {} for argument in ("base_id", "module_id", "table", "file_name"): if argument in source: value = source.get(argument) if value is None or value == "": return invalid_argument("changes.propose", f"source.{argument}", f"source.{argument} must be a non-empty JSON string when provided.") if not isinstance(value, str): return invalid_argument("changes.propose", f"source.{argument}", f"source.{argument} must be a JSON string.") for argument in ("module_id", "table", "file_name"): if argument in payload: value = payload.get(argument) if value is None or value == "": return invalid_argument("changes.propose", argument, f"{argument} must be a non-empty JSON string when provided.") if not isinstance(value, str): return invalid_argument("changes.propose", argument, f"{argument} must be a JSON string.") base_id = str(source.get("base_id") or payload.get("base_id") or "") if not base_id: return base_id_required("changes.propose") module_id = str(source.get("module_id") or payload.get("module_id") or "") module_table = module_file_name = None module_stream_index = None if module_id: module_table, module_file_name, module_stream_index = parse_module_id(module_id) if not module_table or not module_file_name: return { "schema": "onec_adapter_request_error.v1", "method": "changes.propose", "status": "error", "error": "invalid_module_id", "diagnostics": {"message": "Use module_id in the form
:#stream: where
is Config, ConfigSave, ConfigCAS, or ConfigCASSave."}, } table = str(source.get("table") or payload.get("table") or module_table or "Config") file_name = str(source.get("file_name") or payload.get("file_name") or module_file_name or "") if table not in STORAGE_TABLES or not file_name or Path(file_name).name != file_name: return { "schema": "onec_adapter_request_error.v1", "method": "changes.propose", "status": "error", "error": "source_required", "diagnostics": {"message": "Pass source/base_id with source/module_id, or source/table and safe source/file_name."}, } edits = payload.get("edits") if edits is None and payload.get("path"): edits = [{"path": payload.get("path"), "value": payload.get("value"), "node_type": payload.get("node_type", "auto")}] if edits is not None and not isinstance(edits, list): return invalid_argument("changes.propose", "edits", "edits must be a non-empty JSON array of {path, value, node_type?}.") if not edits: return invalid_argument("changes.propose", "edits", "Pass edits as a non-empty list of {path, value, node_type?}.") normalized_edits: list[Any] = [] for edit in edits: if isinstance(edit, dict) and module_stream_index is not None and "path" not in edit and "stream_index" not in edit: normalized_edits.append({**edit, "stream_index": module_stream_index}) else: normalized_edits.append(edit) edits = normalized_edits data, config, error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=int(timeout_seconds or 30)) if error: error["method"] = "changes.propose" return error response_source = { "kind": "live_sql", "database": config["database"], "table": table, "file_name": file_name, **({"module_id": module_id} if module_id else {}), **({"stream_index": module_stream_index} if module_stream_index is not None else {}), } original_sha1 = hashlib.sha1(data).hexdigest() expected_sha1 = str(source.get("expected_sha1") or payload.get("expected_sha1") or "").lower() if expected_sha1 and expected_sha1 != original_sha1: return { "schema": "onec_change_proposal.v1", "status": "precondition_failed", "applied": False, "base_id": base_id, "source": response_source, "original": {"sha1": original_sha1, "bytes": len(data)}, "diagnostics": {"message": "Source payload SHA1 differs from expected_sha1."}, } stream_mode = any(isinstance(edit, dict) and "stream_index" in edit for edit in edits) path_mode = any(isinstance(edit, dict) and "path" in edit for edit in edits) structural_mode = any(isinstance(edit, dict) and ("swap_paths" in edit or "append_child" in edit or "replace_root" in edit) for edit in edits) if sum(1 for value in (stream_mode, path_mode, structural_mode) if value) > 1: return { "schema": "onec_adapter_request_error.v1", "method": "changes.propose", "status": "error", "error": "mixed_edit_modes", "diagnostics": {"message": "Do not mix stream_index, path, and structural edits in one proposal."}, } try: decoded = decode_payload_lossless(data) applied_edits = [] proposal_text = None if stream_mode: from parser.cas_payload import replace_stream_block, stream_blocks_with_data current_payload = decoded.get("payload") if not isinstance(current_payload, (bytes, bytearray)): raise ValueError("source payload bytes are unavailable") modified_payload = bytes(current_payload) for index, edit in enumerate(edits): if not isinstance(edit, dict): raise ValueError(f"edit {index} is not an object") replace = edit.get("replace") if isinstance(edit.get("replace"), dict) else None routine = edit.get("routine") if isinstance(edit.get("routine"), dict) else None stream_index = int(edit.get("stream_index")) blocks = stream_blocks_with_data(modified_payload) block = blocks[stream_index] if 0 <= stream_index < len(blocks) else None raw_stream_text = str((block or {}).get("text") or "") repaired_stream_text = repair_bsl_mojibake_text(raw_stream_text) if raw_stream_text and repaired_stream_text != raw_stream_text and (replace or routine or "expected_contains" in edit or "expected_text_sha1" in edit): expected_contains = str(edit.get("expected_contains") or "") if "expected_contains" in edit else "" if expected_contains and expected_contains not in repaired_stream_text: raise ValueError("expected_contains was not found in repaired stream text") expected_text_sha1 = str(edit.get("expected_text_sha1") or "") if "expected_text_sha1" in edit else "" if expected_text_sha1 and expected_text_sha1.lower() != code_text_sha1(repaired_stream_text): raise ValueError("expected_text_sha1 does not match repaired stream text") repaired_new_text: str | None = None routine_edit = None if replace is not None: old = str(replace.get("old") or "") new = str(replace.get("new") or "") if not old: raise ValueError("replace.old is required") if old not in repaired_stream_text: raise ValueError("replace.old was not found in repaired stream text") count = int(replace.get("count") or 1) repaired_new_text = repaired_stream_text.replace(old, new, count) if routine is not None: from parser.bsl_validation import replace_routine_text repaired_new_text, routine_edit = replace_routine_text( repaired_stream_text, str(routine.get("text") or ""), operation=str(routine.get("operation") or "replace"), name=str(routine.get("name")) if routine.get("name") else None, expected_old_sha1=str(routine.get("expected_old_sha1")) if routine.get("expected_old_sha1") else None, expected_old_contains=str(routine.get("expected_old_contains")) if routine.get("expected_old_contains") else None, ) if repaired_new_text is not None: repaired_new_text = repaired_new_text.lstrip("\ufeff") modified_payload, stream_edit = replace_stream_block(modified_payload, stream_index, text=repaired_new_text) stream_edit["index"] = index stream_edit["mode"] = "stream" stream_edit["encoding_repaired"] = True stream_edit["old_text_sha1"] = code_text_sha1(repaired_stream_text) stream_edit["new_text_sha1"] = code_text_sha1(repaired_new_text) stream_edit["old_text_preview"] = repaired_stream_text[:500] stream_edit["new_text_preview"] = repaired_new_text[:500] if routine_edit: stream_edit["routine"] = routine_edit applied_edits.append(stream_edit) continue modified_payload, stream_edit = replace_stream_block( modified_payload, stream_index, text=str(edit["text"]) if "text" in edit else None, replace=replace, routine=routine, expected_contains=str(edit.get("expected_contains")) if "expected_contains" in edit else None, expected_text_sha1=str(edit.get("expected_text_sha1")) if "expected_text_sha1" in edit else None, ) stream_edit["index"] = index stream_edit["mode"] = "stream" applied_edits.append(stream_edit) encoded = encode_payload_lossless(decoded, payload=modified_payload) else: text = decoded.get("text") if not text or "{" not in text: raise ValueError("source payload is not a brace-tree text payload") if preserve_format: proposal_text = str(text) for index, edit in enumerate(edits): if not isinstance(edit, dict): raise ValueError(f"edit {index} is not an object") if "replace_root" in edit: replacement_root = edit.get("replace_root") if not isinstance(replacement_root, dict) or replacement_root.get("type") not in {"list", "sequence"}: raise ValueError(f"edit {index} replace_root must be a parsed list/sequence node") brace_offset = proposal_text.find("{") if brace_offset < 0: raise ValueError("replace_root requires a brace-tree text payload") old_root_text = proposal_text[brace_offset:] new_root_text = serialize_brace_tree(replacement_root) proposal_text = proposal_text[:brace_offset] + new_root_text applied_edits.append( { "index": index, "mode": "structural_replace_root_preserve_prefix", "old_tree_sha1": hashlib.sha1(old_root_text.encode("utf-8")).hexdigest(), "new_tree_sha1": hashlib.sha1(new_root_text.encode("utf-8")).hexdigest(), "old_tree_chars": len(old_root_text), "new_tree_chars": len(new_root_text), "fields": list(edit.get("fields") or []), "resized_collections": list(edit.get("resized_collections") or []), "old_counts": dict(edit.get("old_counts") or {}), "new_counts": dict(edit.get("new_counts") or {}), } ) continue if "swap_paths" in edit: swap_paths = edit.get("swap_paths") if not isinstance(swap_paths, list) or len(swap_paths) != 2: raise ValueError(f"edit {index} swap_paths must contain exactly two paths") path_a = str(swap_paths[0] or "") path_b = str(swap_paths[1] or "") proposal_text, patch_info = swap_brace_text_paths(proposal_text, path_a, path_b) applied_edits.append({"index": index, "mode": "structural_swap_preserve_format", **patch_info}) continue if "append_child" in edit: append_child = edit.get("append_child") if not isinstance(append_child, dict): raise ValueError(f"edit {index} append_child must be an object") parent_path = str(append_child.get("parent_path") or append_child.get("path") or "") node_text = str(append_child.get("node_text") or "") child_node = append_child.get("node") if node_text: child_node = parse_brace_text(node_text) if not parent_path or child_node is None: raise ValueError(f"edit {index} append_child requires parent_path and node/node_text") proposal_text, patch_info = append_brace_text_child(proposal_text, parent_path, child_node) applied_edits.append({"index": index, "mode": "structural_append_child_preserve_format", **patch_info}) continue path = str(edit.get("path") or "") tree = parse_brace_text(proposal_text) old_node = get_tree_path(tree, path) old_value = scalar(old_node) if "expected_old" in edit and str(edit.get("expected_old")) != old_value: return { "schema": "onec_change_proposal.v1", "status": "precondition_failed", "applied": False, "base_id": base_id, "source": response_source, "original": {"sha1": original_sha1, "bytes": len(data)}, "edit": {"index": index, "path": path, "expected_old": edit.get("expected_old"), "actual_old": old_value}, "diagnostics": {"message": "Edit expected_old does not match current value."}, } proposal_text, patch_info = patch_brace_text_path(proposal_text, path, edit.get("value"), node_type=str(edit.get("node_type") or "auto")) applied_edits.append({"index": index, "mode": "path_preserve_format", **patch_info}) encoded = encode_payload_lossless(decoded, text=proposal_text) else: tree = parse_brace_text(text) for index, edit in enumerate(edits): if not isinstance(edit, dict): raise ValueError(f"edit {index} is not an object") path = str(edit.get("path") or "") old_node = get_tree_path(tree, path) old_value = scalar(old_node) if "expected_old" in edit and str(edit.get("expected_old")) != old_value: return { "schema": "onec_change_proposal.v1", "status": "precondition_failed", "applied": False, "base_id": base_id, "source": response_source, "original": {"sha1": original_sha1, "bytes": len(data)}, "edit": {"index": index, "path": path, "expected_old": edit.get("expected_old"), "actual_old": old_value}, "diagnostics": {"message": "Edit expected_old does not match current value."}, } tree = set_tree_path(tree, path, edit.get("value"), node_type=str(edit.get("node_type") or "auto")) new_node = get_tree_path(tree, path) applied_edits.append( { "index": index, "mode": "path", "path": path, "old": old_value, "new": scalar(new_node), "old_node_type": old_node.get("type") if isinstance(old_node, dict) else None, "new_node_type": new_node.get("type") if isinstance(new_node, dict) else None, } ) proposal_text = serialize_brace_tree(tree) encoded = encode_brace_tree(tree, decoded) except Exception as exc: return { "schema": "onec_change_proposal.v1", "status": "error", "applied": False, "base_id": base_id, "source": response_source, "diagnostics": {"message": str(exc)}, } encoded_sha1 = hashlib.sha1(encoded).hexdigest() validation: dict[str, Any] try: if stream_mode: from parser.cas_payload import classify_payload classified = classify_payload(encoded, include_text=True) stream_validations = [] try: from parser.bsl_validation import validate_bsl_text except Exception: validate_bsl_text = None streams = classified.get("stream_blocks") or [] for edit in applied_edits: stream_index = edit.get("stream_index") stream = streams[stream_index] if isinstance(stream_index, int) and 0 <= stream_index < len(streams) else None text = stream.get("text") if isinstance(stream, dict) else None item = { "stream_index": stream_index, "has_text": bool(text), "has_bsl_marker": bool(stream and stream.get("has_bsl_marker")), } if text and validate_bsl_text: item["bsl"] = validate_bsl_text(text) stream_validations.append(item) validation = { "status": "ok" if classified.get("status") == "ok" else "error", "mode": "stream", "role": classified.get("role"), "compression": classified.get("compression"), "payload_bytes": classified.get("payload_bytes"), "counts": classified.get("counts"), "stream_indexes": [edit.get("stream_index") for edit in applied_edits], "streams": stream_validations, } if any(((item.get("bsl") or {}).get("status") == "error") for item in stream_validations): validation["status"] = "error" else: roundtrip_decoded = decode_payload_lossless(encoded) roundtrip_text = roundtrip_decoded.get("text") if not roundtrip_text: raise ValueError("encoded payload text is not decodable") roundtrip_tree = parse_brace_text(roundtrip_text) checks = [] for edit in applied_edits: if edit.get("mode") in {"path", "path_preserve_format"}: checks.append( { "path": edit.get("path"), "value": scalar(get_tree_path(roundtrip_tree, str(edit.get("path") or ""))), "expected": edit.get("new"), "ok": scalar(get_tree_path(roundtrip_tree, str(edit.get("path") or ""))) == edit.get("new"), } ) elif edit.get("mode") == "structural_swap_preserve_format": path_a = str(edit.get("path_a") or "") path_b = str(edit.get("path_b") or "") checks.append( { "path_a": path_a, "path_b": path_b, "ok": bool(get_tree_path(roundtrip_tree, path_a) and get_tree_path(roundtrip_tree, path_b)), } ) elif edit.get("mode") == "structural_append_child_preserve_format": parent_path = str(edit.get("parent_path") or "") inserted_index = edit.get("inserted_index") appended_path = f"{parent_path}.{inserted_index}" if parent_path and isinstance(inserted_index, int) else "" appended_node = get_tree_path(roundtrip_tree, appended_path) if appended_path else None parent_node = get_tree_path(roundtrip_tree, parent_path) if parent_path else None parent_items = config_tree_list_items(parent_node) declared_count = config_tree_scalar(parent_items[1]) if len(parent_items) >= 2 else "" count_ok = ( not edit.get("count_updated") or (declared_count.isdigit() and int(declared_count) == len(parent_items) - 2) ) checks.append( { "operation": "append_child", "parent_path": parent_path, "inserted_index": inserted_index, "appended_path": appended_path, "node_present": appended_node is not None, "count_ok": count_ok, "ok": appended_node is not None and count_ok, } ) elif edit.get("mode") == "structural_replace_root_preserve_prefix": roundtrip_serialized = serialize_brace_tree(roundtrip_tree) checks.append( { "operation": "replace_root", "expected_tree_sha1": edit.get("new_tree_sha1"), "actual_tree_sha1": hashlib.sha1(roundtrip_serialized.encode("utf-8")).hexdigest(), "ok": hashlib.sha1(roundtrip_serialized.encode("utf-8")).hexdigest() == edit.get("new_tree_sha1"), } ) validation = { "status": "ok", "mode": "path_preserve_format" if preserve_format else "path", "compression": roundtrip_decoded.get("compression"), "encoding": roundtrip_decoded.get("encoding"), "checks": checks, } validation["status"] = "ok" if all(check.get("ok") for check in validation["checks"]) else "error" except Exception as exc: validation = {"status": "error", "diagnostics": {"message": str(exc)}} result: dict[str, Any] = { "schema": "onec_change_proposal.v1", "status": "accepted_for_review", "applied": False, "base_id": base_id, "source": response_source, "original": {"sha1": original_sha1, "bytes": len(data)}, "encoded": { "sha1": encoded_sha1, "bytes": len(encoded), "compression": decoded.get("compression"), "encoding": decoded.get("encoding"), "matches_original": encoded == data, }, "edits": applied_edits, "validation": validation, "counts": {"edits": len(applied_edits)}, "diagnostics": {"note": "Proposal only. The adapter did not write to SQL."}, } if include_text: result["text"] = proposal_text if include_payload: result["encoded"]["payload_hex"] = encoded.hex() return result def validate_changes_propose_payload(payload: dict[str, Any]) -> dict[str, Any] | None: string_error = validate_optional_string_arguments(payload, "changes.propose", ["summary", "description", "module_id", "table", "file_name"]) if string_error: return string_error _, include_text_error = strict_bool_argument(payload, "include_text", method="changes.propose", default=False) if include_text_error: return include_text_error _, include_payload_error = strict_bool_argument(payload, "include_payload", method="changes.propose", default=False) if include_payload_error: return include_payload_error _, preserve_format_error = strict_bool_argument(payload, "preserve_format", method="changes.propose", default=False) if preserve_format_error: return preserve_format_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="changes.propose", default=30, minimum=1) if timeout_error: return timeout_error if "source" in payload and payload.get("source") is not None and not isinstance(payload.get("source"), dict): return invalid_argument("changes.propose", "source", "source must be a JSON object.") source = payload.get("source") if isinstance(payload.get("source"), dict) else {} for argument in ("base_id", "module_id", "table", "file_name"): if argument in source: value = source.get(argument) if value is None or value == "": return invalid_argument("changes.propose", f"source.{argument}", f"source.{argument} must be a non-empty JSON string when provided.") if not isinstance(value, str): return invalid_argument("changes.propose", f"source.{argument}", f"source.{argument} must be a JSON string.") for argument in ("module_id", "table", "file_name"): if argument in payload: value = payload.get(argument) if value is None or value == "": return invalid_argument("changes.propose", argument, f"{argument} must be a non-empty JSON string when provided.") if not isinstance(value, str): return invalid_argument("changes.propose", argument, f"{argument} must be a JSON string.") if not str(source.get("base_id") or payload.get("base_id") or ""): return base_id_required("changes.propose") edits = payload.get("edits") if edits is None and payload.get("path"): edits = [{"path": payload.get("path"), "value": payload.get("value"), "node_type": payload.get("node_type", "auto")}] if edits is not None and not isinstance(edits, list): return invalid_argument("changes.propose", "edits", "edits must be a non-empty JSON array of {path, value, node_type?}.") if not edits: return invalid_argument("changes.propose", "edits", "Pass edits as a non-empty list of {path, value, node_type?}.") return None def validate_metadata_object_decode_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "metadata.object.decode") if isinstance(base_id_or_error, dict): return base_id_or_error extension_guid = str(payload.get("extension_guid") or "").strip().lower() if "extension_guid" in payload and not is_guid_text(extension_guid): return invalid_argument("metadata.object.decode", "extension_guid", "extension_guid must be a GUID string.") selector_error = validate_object_selector_arguments(payload, "metadata.object.decode") if selector_error: return selector_error guid_error = validate_explicit_guid_argument(payload, "metadata.object.decode") if guid_error: return guid_error ordinal_error = validate_explicit_ordinal_arguments(payload, "metadata.object.decode") if ordinal_error: return ordinal_error _, lookup_limit_error = parse_object_lookup_limit(payload, "metadata.object.decode") if lookup_limit_error: return lookup_limit_error _, view_error = parse_view_argument(payload, "metadata.object.decode") if view_error: return view_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.decode", default=60, minimum=1) if timeout_error: return timeout_error _, include_storage_error = strict_include_storage(payload, "metadata.object.decode") if include_storage_error: return include_storage_error _, include_text_error = strict_bool_argument(payload, "include_text", method="metadata.object.decode", default=False) if include_text_error: return include_text_error _, include_tree_error = strict_bool_argument(payload, "include_tree", method="metadata.object.decode", default=False) if include_tree_error: return include_tree_error _, evidence_mode_error = parse_evidence_mode_argument(payload, "metadata.object.decode") if evidence_mode_error: return evidence_mode_error table_or_error = metadata_storage_table(payload, "metadata.object.decode") if isinstance(table_or_error, dict): return table_or_error _, max_depth_error = parse_int_argument(payload, "max_depth", method="metadata.object.decode", default=3, minimum=1, maximum=8) if max_depth_error: return max_depth_error return None def decode_metadata_object(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, "metadata.object.decode") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, "metadata.object.decode") if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error extension_guid = str(payload.get("extension_guid") or "").strip().lower() if "extension_guid" in payload and not is_guid_text(extension_guid): return invalid_argument("metadata.object.decode", "extension_guid", "extension_guid must be a GUID string.") guid_error = validate_explicit_guid_argument(payload, "metadata.object.decode") if guid_error: return guid_error ordinal_error = validate_explicit_ordinal_arguments(payload, "metadata.object.decode") if ordinal_error: return ordinal_error lookup_limit, lookup_limit_error = parse_object_lookup_limit(payload, "metadata.object.decode") if lookup_limit_error: return lookup_limit_error view, view_error = parse_view_argument(payload, "metadata.object.decode") if view_error: return view_error parsed_timeout, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.decode", default=60, minimum=1) if timeout_error: return timeout_error timeout_seconds = int(parsed_timeout or 60) table_or_error = metadata_storage_table(payload, "metadata.object.decode") if isinstance(table_or_error, dict): return table_or_error table = table_or_error include_storage, include_storage_error = strict_include_storage(payload, "metadata.object.decode") if include_storage_error: return include_storage_error include_storage = bool(include_storage) include_text, include_text_error = strict_bool_argument(payload, "include_text", method="metadata.object.decode", default=False) if include_text_error: return include_text_error include_tree, include_tree_error = strict_bool_argument(payload, "include_tree", method="metadata.object.decode", default=False) if include_tree_error: return include_tree_error evidence_mode, evidence_mode_error = parse_evidence_mode_argument(payload, "metadata.object.decode") if evidence_mode_error: return evidence_mode_error max_depth, max_depth_error = parse_int_argument(payload, "max_depth", method="metadata.object.decode", default=3, minimum=1, maximum=8) if max_depth_error: return max_depth_error records, error = live_dbnames_records(base_id, timeout_seconds=timeout_seconds) if error: error["method"] = "metadata.object.decode" return error object_card: dict[str, Any] | None = None guid = str(payload.get("guid") or "").strip().lower() kind = canonical_kind(str(payload.get("kind") or "")) if payload.get("kind") else None if not guid: ordinal_value = first_non_empty_arg(payload, "ordinal", "index", "object_index") if ordinal_value is not None and ordinal_value != "": ordinal, ordinal_error = parse_ordinal(ordinal_value, "metadata.object.decode") if ordinal_error: return ordinal_error if not kind: return { "schema": "onec_adapter_request_error.v1", "method": "metadata.object.decode", "status": "error", "error": "kind_required", "diagnostics": {"message": "kind is required when selecting an object by ordinal."}, } page = list_objects(kind=kind, base_id=base_id, limit=1, offset=int(ordinal or 1) - 1, include_storage=False, table=table) if page.get("status") != "ok" or not page.get("objects"): result = dict(page) result["method"] = "metadata.object.decode" result["status"] = "not_found" result["error"] = "not_found" result["diagnostics"] = {"message": f"Object ordinal {ordinal} was not found for kind {kind}."} return result object_card = (page.get("objects") or [])[0] guid = str((object_card or {}).get("guid") or "").lower() kind = canonical_kind(str((object_card or {}).get("kind") or kind or "")) if (object_card or kind) else None if not guid: object_result = get_object( kind, str(payload.get("name") or ""), base_id=base_id, view=str(view or "effective"), limit=int(lookup_limit or 20), table=table, extension_guid=extension_guid or None, timeout_seconds=timeout_seconds, ) if object_result.get("status") != "ok": result = dict(object_result) result["method"] = "metadata.object.decode" return result object_card = object_result.get("object") guid = str((object_card or {}).get("guid") or "").lower() kind = canonical_kind(str((object_card or {}).get("kind") or kind or "")) if (object_card or kind) else None if not guid: return { "schema": "onec_metadata_object_decode.v1", "status": "not_found", "error": "not_found", "base_id": base_id, "query": {"guid": payload.get("guid"), "kind": payload.get("kind"), "name": payload.get("name")}, "diagnostics": {"message": "Object was not found. Pass guid or kind/name."}, } if table == "ConfigCASSave" and extension_guid and (not object_card or not object_card.get("name")): saved_object_result = get_object( kind, guid, base_id=base_id, view=str(view or "effective"), limit=int(lookup_limit or 20), table=table, extension_guid=extension_guid, include_storage=include_storage, include_semantic=False, timeout_seconds=timeout_seconds, ) if saved_object_result.get("status") == "ok": object_card = saved_object_result.get("object") or object_card kind = canonical_kind(str((object_card or {}).get("kind") or kind or "")) if (object_card or kind) else None storage_file_name = f"{extension_guid}__{guid}" if table == "ConfigCASSave" and extension_guid else guid data, config, error = read_storage_file_bytes(base_id, table, storage_file_name, timeout_seconds=timeout_seconds) if error: error["method"] = "metadata.object.decode" return error try: from parser.cas_payload import classify_payload except Exception: classify_payload = None decoded = decode_config_object_full( data, kind=kind, dbnames_records=records, include_text=bool(include_text), include_tree=bool(include_tree), max_depth=int(max_depth or 3), ) classified_payload = ( classify_payload( data, include_text=bool(include_text), include_tree=bool(include_tree), ) if classify_payload else {} ) semantic_raw = decoded.get("semantic") if decoded.get("status") == "ok" else None resolved_types = resolve_type_guids( base_id, collect_reference_type_guids_from_sections((semantic_raw or {}).get("sections") or []), timeout_seconds=timeout_seconds, table=table, ) public_decoded = dict(decoded) semantic_public = public_semantic_profile(semantic_raw, include_storage=False, resolved_types=resolved_types) if not include_storage: public_decoded = { "status": decoded.get("status"), "root": decoded.get("root"), "semantic": semantic_public, "undecoded_evidence": payload_public_undecoded_evidence( classified_payload, include_text_preview=bool(include_text), mode=str(evidence_mode or "summary"), allow_storage_details=bool(include_storage), ), } if "diagnostics" in decoded: public_decoded["diagnostics"] = decoded.get("diagnostics") if include_text and "text" in decoded: public_decoded["text"] = decoded.get("text") if include_tree and "tree" in decoded: public_decoded["tree"] = decoded.get("tree") else: public_decoded["undecoded_evidence"] = payload_public_undecoded_evidence( classified_payload, include_text_preview=bool(include_text), mode=str(evidence_mode or "summary"), allow_storage_details=bool(include_storage), ) semantic_sections = (semantic_public or {}).get("sections") or [] semantic_counts = { "semantic_sections": len(semantic_sections), "attributes": sum(int((section or {}).get("declared_record_count") or 0) for section in semantic_sections if (section or {}).get("category") == "Attribute"), "tabular_sections": sum(int((section or {}).get("declared_record_count") or 0) for section in semantic_sections if (section or {}).get("category") == "TabularSection"), "dimensions": sum(int((section or {}).get("declared_record_count") or 0) for section in semantic_sections if (section or {}).get("category") == "Dimension"), "resources": sum(int((section or {}).get("declared_record_count") or 0) for section in semantic_sections if (section or {}).get("category") == "Resource"), } return { "schema": "onec_metadata_object_decode.v1", "status": decoded.get("status"), "base_id": base_id, "source": {"kind": "live_sql", "database": config["database"], "table": table, "file_name": storage_file_name} if include_storage else {"kind": "live_metadata"}, "query": { "guid": payload.get("guid"), "kind": payload.get("kind"), "name": payload.get("name"), "table": table, "include_storage": include_storage, }, "object": object_card or {"guid": guid, "kind": kind}, "decoded": public_decoded, "counts": semantic_counts, } def resolve_object_guid( payload: dict[str, Any], base_id: str, *, timeout_seconds: int = 60, method: str = "metadata.object.selector", table: str = "Config", ) -> tuple[str | None, str | None, dict[str, Any] | None, dict[str, Any] | None]: guid_error = validate_explicit_guid_argument(payload, method) if guid_error: return None, None, None, guid_error ordinal_argument_error = validate_explicit_ordinal_arguments(payload, method) if ordinal_argument_error: return None, None, None, ordinal_argument_error lookup_limit, lookup_limit_error = parse_object_lookup_limit(payload, method) if lookup_limit_error: return None, None, None, lookup_limit_error view, view_error = parse_view_argument(payload, method) if view_error: return None, None, None, view_error guid = str(payload.get("guid") or "").strip().lower() kind = canonical_kind(str(payload.get("kind") or "")) if payload.get("kind") else None object_card = None if guid: return guid, kind, {"guid": guid, "kind": kind}, None ordinal_value = first_non_empty_arg(payload, "ordinal", "index", "object_index") if ordinal_value not in {None, ""}: ordinal, ordinal_error = parse_ordinal(ordinal_value, method) if ordinal_error: return None, None, None, ordinal_error if not kind: return None, None, None, { "schema": "onec_adapter_request_error.v1", "method": method, "status": "error", "error": "kind_required", "diagnostics": {"message": "kind is required when selecting an object by ordinal."}, } page = list_objects( kind=kind, base_id=base_id, limit=1, offset=int(ordinal or 1) - 1, include_storage=False, table=table, ) if page.get("status") != "ok": result = dict(page) result["method"] = method return None, None, None, result objects = page.get("objects") or [] if objects: object_card = objects[0] guid = str((object_card or {}).get("guid") or "").lower() kind = canonical_kind(str((object_card or {}).get("kind") or kind or "")) if (object_card or kind) else None return guid, kind, object_card, None return None, None, None, { "schema": "onec_adapter_request_error.v1", "method": method, "status": "not_found", "error": "not_found", "base_id": base_id, "diagnostics": {"message": f"Object ordinal {ordinal} was not found for kind {kind}."}, } object_result = get_object( kind, str(payload.get("name") or ""), base_id=base_id, view=str(view or "effective"), limit=int(lookup_limit or 20), timeout_seconds=timeout_seconds, table=table, include_semantic=False, ) if object_result.get("status") != "ok": result = dict(object_result) result["method"] = method return None, None, None, result object_card = object_result.get("object") guid = str((object_card or {}).get("guid") or "").lower() kind = canonical_kind(str((object_card or {}).get("kind") or kind or "")) if (object_card or kind) else None if not guid: return None, kind, object_card, { "schema": "onec_metadata_object_parts.v1", "status": "not_found", "error": "not_found", "base_id": base_id, "query": {"guid": payload.get("guid"), "kind": payload.get("kind"), "name": payload.get("name")}, "diagnostics": {"message": "Object was not found. Pass guid or kind/name."}, } return guid, kind, object_card, None def metadata_object_parts(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, "metadata.object.parts") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, "metadata.object.parts") if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error extension_guid = str(payload.get("extension_guid") or "").strip().lower() if "extension_guid" in payload and not is_guid_text(extension_guid): return invalid_argument("metadata.object.parts", "extension_guid", "extension_guid must be a GUID string.") timeout_value, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.parts", default=60, minimum=1) if timeout_error: return timeout_error timeout_seconds = int(timeout_value or 60) refresh_cache = truthy(payload.get("refresh_cache")) include_storage, include_storage_error = strict_include_storage(payload, "metadata.object.parts") if include_storage_error: return include_storage_error include_storage = bool(include_storage) include_text, include_text_error = strict_bool_argument(payload, "include_text", method="metadata.object.parts", default=False) if include_text_error: return include_text_error include_tree, include_tree_error = strict_bool_argument(payload, "include_tree", method="metadata.object.parts", default=False) if include_tree_error: return include_tree_error evidence_mode, evidence_mode_error = parse_evidence_mode_argument(payload, "metadata.object.parts") if evidence_mode_error: return evidence_mode_error table_or_error = metadata_storage_table(payload, "metadata.object.parts") if isinstance(table_or_error, dict): return table_or_error table = table_or_error part_limit, part_limit_error = parse_int_argument(payload, "part_limit", method="metadata.object.parts", default=200, minimum=1, maximum=5000) if part_limit_error: return part_limit_error guid, kind, object_card, error = resolve_object_guid( payload, base_id, timeout_seconds=timeout_seconds, method="metadata.object.parts", table=table, ) if error: return error if table == "ConfigCASSave" and extension_guid and (not object_card or not object_card.get("name")): saved_object_result = get_object( kind, str(guid or ""), base_id=base_id, table=table, extension_guid=extension_guid, include_storage=include_storage, include_semantic=False, timeout_seconds=timeout_seconds, ) if saved_object_result.get("status") == "ok": object_card = saved_object_result.get("object") or object_card storage_prefix_guid = str(guid or "").lower() if table == "ConfigCASSave" and extension_guid and storage_prefix_guid: storage_prefix_guid = f"{extension_guid}__{storage_prefix_guid}" if kind == "Configuration" and isinstance(object_card, dict): identity = object_card.get("identity") if isinstance(object_card.get("identity"), dict) else {} # The cached public Configuration identity may contain the root # descriptor GUID. Re-read the current SQL descriptor because its # embedded identity GUID is the prefix of configuration-level parts. if guid: descriptor_data, _, descriptor_error = read_storage_file_bytes( base_id, table, str(guid), timeout_seconds=timeout_seconds, ) if not descriptor_error: descriptor_identity = config_identity_from_bytes(descriptor_data or b"") or {} descriptor_identity_guid = str(descriptor_identity.get("guid") or "").strip().lower() if is_guid_text(descriptor_identity_guid): identity = descriptor_identity object_card = {**object_card, "identity": descriptor_identity} identity_guid = str(identity.get("guid") or "").strip().lower() if is_guid_text(identity_guid): storage_prefix_guid = identity_guid try: from parser.cas_payload import classify_payload except Exception as exc: return { "schema": "onec_metadata_object_parts.v1", "status": "error", "base_id": base_id, "diagnostics": {"message": f"Payload classifier is unavailable: {exc}"}, } files = storage_files_list({"base_id": base_id, "table": table, "prefix": storage_prefix_guid, "limit": part_limit, "timeout_seconds": timeout_seconds, "_internal": True}) if files.get("status") != "ok": return public_error_result(files, include_storage=include_storage, method="metadata.object.parts") file_names = [ str(row.get("FileName") or "") for row in files.get("files") or [] if str(row.get("FileName") or "") == storage_prefix_guid or str(row.get("FileName") or "").startswith(f"{storage_prefix_guid}.") ] if kind == "Configuration" and guid and guid != storage_prefix_guid and guid not in file_names: file_names.insert(0, str(guid)) payloads, config, read_error = read_storage_files_bytes(base_id, table, file_names, timeout_seconds=timeout_seconds) if read_error: return public_error_result(read_error, include_storage=include_storage, method="metadata.object.parts") parts = [] for file_name in sorted(payloads or {}, key=lambda value: (value != guid, value != storage_prefix_guid, value)): data = (payloads or {})[file_name] classified = classify_payload( data, include_text=bool(include_text), include_tree=bool(include_tree), ) suffix = "" if file_name == guid else file_name[len(storage_prefix_guid) :] part = { "part_id": file_name, "suffix": suffix, "table": table, "classification": classified, } parts.append(part) role_counts: dict[str, int] = {} for part in parts: role = str(((part.get("classification") or {}).get("role")) or "unknown") public_role = public_payload_role(role) if not include_storage else role role_counts[public_role] = role_counts.get(public_role, 0) + 1 public_parts = [] for part in parts: classification = part.get("classification") or {} if include_storage: public_part = dict(part) public_part["undecoded_evidence"] = payload_public_undecoded_evidence( classification, include_text_preview=bool(include_text), mode=str(evidence_mode or "summary"), allow_storage_details=True, ) public_parts.append(public_part) else: public_parts.append( { **payload_public_properties(classification), "preview": payload_public_preview(classification, include_text_preview=bool(include_text)), "undecoded_evidence": payload_public_undecoded_evidence( classification, include_text_preview=bool(include_text), mode=str(evidence_mode or "summary"), allow_storage_details=False, ), } ) return { "schema": "onec_metadata_object_parts.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "database": (config or {}).get("database"), "table": table} if include_storage else {"kind": "live_metadata"}, "query": {"guid": payload.get("guid"), "kind": payload.get("kind"), "name": payload.get("name"), "include_storage": include_storage}, "object": object_card or {"guid": guid, "kind": kind}, "parts": public_parts, "counts": {"parts": len(parts), "roles": dict(sorted(role_counts.items()))}, "diagnostics": { "note": "Диагностические координаты частей скрыты. Для служебной диагностики используйте include_storage=true.", }, } def metadata_object_modules(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, "metadata.object.modules") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, "metadata.object.modules") if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error extension_guid = str(payload.get("extension_guid") or "").strip().lower() if "extension_guid" in payload and not is_guid_text(extension_guid): return invalid_argument("metadata.object.modules", "extension_guid", "extension_guid must be a GUID string.") timeout_value, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.modules", default=60, minimum=1) if timeout_error: return timeout_error timeout_seconds = int(timeout_value or 60) ordinal_argument_error = validate_explicit_ordinal_arguments(payload, "metadata.object.modules") if ordinal_argument_error: return ordinal_argument_error lookup_limit, lookup_limit_error = parse_object_lookup_limit(payload, "metadata.object.modules") if lookup_limit_error: return lookup_limit_error view, view_error = parse_view_argument(payload, "metadata.object.modules") if view_error: return view_error requested_module, requested_module_error = optional_string_filter(payload, ["module", "name_filter"], method="metadata.object.modules") if requested_module_error: return requested_module_error wanted_module = normalize(requested_module or "") include_storage, include_storage_error = strict_include_storage(payload, "metadata.object.modules") if include_storage_error: return include_storage_error include_storage = bool(include_storage) table_or_error = metadata_storage_table(payload, "metadata.object.modules") if isinstance(table_or_error, dict): return table_or_error table = table_or_error object_probe = get_object( payload.get("kind"), str(payload.get("name") or payload.get("guid") or ""), base_id=base_id, view=str(view or "effective"), limit=int(lookup_limit or 20), table=table, include_storage=True, ordinal=first_non_empty_arg(payload, "ordinal", "index", "object_index"), include_semantic=False, timeout_seconds=timeout_seconds, extension_guid=extension_guid or None, ) if object_probe.get("status") != "ok": result = dict(object_probe) result["method"] = "metadata.object.modules" return result object_card = object_probe.get("object") or {} object_guid = str(object_card.get("guid") or payload.get("guid") or "").lower() config, _ = sql_config_for_base(base_id) cache_role = metadata_modules_cache_role() if config and object_guid and not truthy(payload.get("refresh_cache")): cached_result = metadata_guid_index_lookup_payload(config, object_guid, cache_role) if cached_result: cached_object = cached_result.get("object") if isinstance(cached_result.get("object"), dict) else {} merged_object = { **cached_object, **{key: value for key, value in object_card.items() if value is not None and value != ""}, } modules = [module for module in cached_result.get("modules") or [] if isinstance(module, dict)] owner_public = { "kind": merged_object.get("kind"), "kind_ru": merged_object.get("kind_ru"), "public_kind": merged_object.get("public_kind"), "guid": object_guid, "name": merged_object.get("name"), "synonym": merged_object.get("synonym"), } public_modules = [ public_module_with_qualified_name( module, owner=owner_public, include_storage=include_storage, ordinal=index + 1, owner_kind=merged_object.get("kind") or object_card.get("kind"), ) for index, module in enumerate(modules) ] cached_module_refs = [str(module.get("module_id") or "") for module in modules if str(module.get("module_id") or "").strip()] if config and object_guid: metadata_module_owner_cache_prune_for_owner(config, object_guid, cached_module_refs) for module in modules: module_id = str(module.get("module_id") or "") if not module_id: continue metadata_module_owner_cache_upsert(config, module_id, owner_public, module=module) matched_modules = [] for module, match_by in filter_public_rows_by_name(public_modules, requested_module): public_module = dict(module) if wanted_module: public_module["match_by"] = match_by matched_modules.append(public_module) if wanted_module and not matched_modules: result = child_not_found("metadata.object.modules", "Модуль", requested_module, merged_object or object_card, base_id=base_id) result.update( { "schema": "onec_object_modules.v1", "source": {"kind": "live_metadata"} if not include_storage else cached_result.get("source", {"kind": "live_metadata"}), "query": {"module": requested_module, "include_storage": include_storage}, "modules": [], "counts": {"modules": 0, "available_modules": len(public_modules)}, "cache": {"status": "hit", "role": cache_role}, } ) return result return { "schema": "onec_object_modules.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_metadata"} if not include_storage else cached_result.get("source", {"kind": "live_metadata"}), "object": merged_object or object_card, "query": {"module": requested_module, "include_storage": include_storage}, "modules": matched_modules, "counts": {**(cached_result.get("counts") or {}), "modules": len(matched_modules), "available_modules": len(public_modules)}, "cache": {"status": "hit", "role": cache_role}, "diagnostics": { "note": "Storage coordinates are hidden unless include_storage=true.", }, } parts_payload = { **payload, "guid": object_guid, "kind": object_card.get("kind") or payload.get("kind"), "table": table, "include_text": False, "include_tree": False, "include_storage": True, "timeout_seconds": timeout_seconds, } if canonical_kind(str(object_card.get("kind") or payload.get("kind") or "")) == "Configuration" and object_card.get("name"): # Resolve by public name so metadata.object.parts can retain the # configuration identity GUID used as the SQL file prefix. parts_payload.pop("guid", None) parts_payload["name"] = object_card.get("name") parts_result = metadata_object_parts(parts_payload) if parts_result.get("status") != "ok": return public_error_result(parts_result, include_storage=include_storage, method="metadata.object.modules") modules = [] for part in parts_result.get("parts") or []: classification = part.get("classification") or {} owner_kind = canonical_kind(str(object_card.get("kind") or payload.get("kind") or "")) configuration_suffix = str(part.get("suffix") or "").lstrip(".") if owner_kind == "Configuration" else "" if classification.get("role") != "bsl_module_payload" and configuration_suffix not in {"0", "5", "6", "7"}: continue candidate_streams = [ (index, stream) for index, stream in enumerate(classification.get("stream_blocks") or []) if stream.get("has_bsl_marker") ] # A Config service part contains one logical BSL module. Some platform # versions also repeat a short tail/head fragment as another marked # stream. Keep the full stream so callers do not see phantom modules. if owner_kind == "Configuration": if configuration_suffix in {"0", "5", "6", "7"}: streams = list(classification.get("stream_blocks") or []) # Configuration modules use the fourth zero-based stream in # the SQL container. The external-connection module may be # intentionally empty, so it has no BSL marker but must still # remain visible as a real configuration module. if len(streams) > 4: candidate_streams = [(4, streams[4])] if owner_kind in {"CommonModule", "WebService", "HTTPService", "IntegrationService"} and len(candidate_streams) > 1: candidate_streams = [ max( candidate_streams, key=lambda pair: ( int((pair[1] or {}).get("bytes") or 0), len(str((pair[1] or {}).get("text_preview") or "")), ), ) ] for index, stream in candidate_streams: modules.append( { "module_id": f"{part.get('table')}:{part.get('part_id')}#stream:{index}", "table": part.get("table"), "file_name": part.get("part_id"), "suffix": part.get("suffix"), "stream_index": index, "kind": "bsl_stream_module", "name": f"{part.get('part_id')}#stream:{index}", "bytes": stream.get("bytes"), "sha1": stream.get("sha1"), "encoding": stream.get("encoding"), "text_preview": stream.get("text_preview"), "payload_role": classification.get("role"), } ) parts_object = parts_result.get("object") if isinstance(parts_result.get("object"), dict) else {} owner_public = { "kind": parts_object.get("kind") or object_card.get("kind"), "kind_ru": parts_object.get("kind_ru") or object_card.get("kind_ru"), "public_kind": parts_object.get("public_kind") or object_card.get("public_kind"), "guid": object_guid, "name": parts_object.get("name") or object_card.get("name"), "synonym": parts_object.get("synonym") or object_card.get("synonym"), } public_modules = [ public_module_with_qualified_name( module, owner=owner_public, include_storage=include_storage, ordinal=index + 1, owner_kind=owner_public.get("kind"), ) for index, module in enumerate(modules) ] module_refs = [str(module.get("module_id") or "") for module in modules if str(module.get("module_id") or "").strip()] if config and object_guid: metadata_module_owner_cache_prune_for_owner(config, object_guid, module_refs) for module in modules: module_id = str(module.get("module_id") or "") if not module_id: continue metadata_module_owner_cache_upsert(config, module_id, owner_public, module=module) matched_modules = [] for module, match_by in filter_public_rows_by_name(public_modules, requested_module): public_module = dict(module) if wanted_module: public_module["match_by"] = match_by matched_modules.append(public_module) if wanted_module and not matched_modules: result = child_not_found("metadata.object.modules", "Модуль", requested_module, parts_result.get("object") or object_card, base_id=base_id) result.update( { "schema": "onec_object_modules.v1", "source": parts_result.get("source") if include_storage else {"kind": "live_metadata"}, "query": {"module": requested_module, "include_storage": include_storage}, "modules": [], "counts": {"modules": 0, "available_modules": len(public_modules), "parts": (parts_result.get("counts") or {}).get("parts")}, } ) return result result = { "schema": "onec_object_modules.v1", "status": "ok", "base_id": base_id, "source": parts_result.get("source") if include_storage else {"kind": "live_metadata"}, "object": parts_result.get("object") or object_card, "query": {"module": requested_module, "include_storage": include_storage}, "modules": matched_modules, "counts": {"modules": len(matched_modules), "available_modules": len(public_modules), "parts": (parts_result.get("counts") or {}).get("parts")}, "diagnostics": { "note": "Storage coordinates are hidden unless include_storage=true.", }, } if config and object_guid: cache_payload = { "object": parts_result.get("object") or object_card, "modules": modules, "counts": {"modules": len(modules), "parts": (parts_result.get("counts") or {}).get("parts")}, "source": parts_result.get("source"), } metadata_guid_index_upsert( config, { "guid": object_guid, "guid_role": cache_role, "kind": object_card.get("kind"), "kind_ru": object_card.get("kind_ru"), "public_kind": object_card.get("public_kind"), "name": object_card.get("name"), "synonym": object_card.get("synonym"), "presentation": ".".join(part for part in [object_card.get("kind_ru"), object_card.get("name")] if part), "payload": cache_payload, "source_file": object_guid, }, ) result["cache"] = {"status": "stored", "role": cache_role} return result def metadata_object_related(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, "metadata.object.related") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, "metadata.object.related") if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error guid_error = validate_explicit_guid_argument(payload, "metadata.object.related") if guid_error: return guid_error extension_guid = str(payload.get("extension_guid") or "").strip().lower() if "extension_guid" in payload and not is_guid_text(extension_guid): return invalid_argument( "metadata.object.related", "extension_guid", "extension_guid must be a GUID string.", ) timeout_value, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.related", default=60, minimum=1) if timeout_error: return timeout_error timeout_seconds = int(timeout_value or 60) ordinal_argument_error = validate_explicit_ordinal_arguments(payload, "metadata.object.related") if ordinal_argument_error: return ordinal_argument_error lookup_limit, lookup_limit_error = parse_object_lookup_limit(payload, "metadata.object.related") if lookup_limit_error: return lookup_limit_error view, view_error = parse_view_argument(payload, "metadata.object.related") if view_error: return view_error include_storage, include_storage_error = strict_include_storage(payload, "metadata.object.related") if include_storage_error: return include_storage_error include_storage = bool(include_storage) include_text, include_text_error = strict_bool_argument(payload, "include_text", method="metadata.object.related", default=False) if include_text_error: return include_text_error guids_per_record, guids_per_record_error = parse_int_argument(payload, "guids_per_record", method="metadata.object.related", default=5, minimum=1, maximum=50) if guids_per_record_error: return guids_per_record_error table_or_error = metadata_storage_table(payload, "metadata.object.related") if isinstance(table_or_error, dict): return table_or_error table = table_or_error object_probe = get_object( payload.get("kind"), str(payload.get("name") or payload.get("guid") or ""), base_id=base_id, view=str(view or "effective"), limit=int(lookup_limit or 20), table=table, extension_guid=extension_guid or None, include_storage=True, ordinal=first_non_empty_arg(payload, "ordinal", "index", "object_index"), include_semantic=False, timeout_seconds=timeout_seconds, ) if object_probe.get("status") != "ok": result = dict(object_probe) result["method"] = "metadata.object.related" return result object_card = object_probe.get("object") or {} guid = str(object_card.get("guid") or payload.get("guid") or "").lower() kind = str(object_card.get("kind") or payload.get("kind") or "") object_table = preferred_object_storage_table(object_card, table) object_file_name = str(((object_card.get("storage") or {}).get("file_name") if isinstance(object_card.get("storage"), dict) else "") or guid) rules = RELATED_SECTION_RULES.get(str(kind or ""), []) if not rules: return { "schema": "onec_metadata_object_related.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_metadata"}, "object": public_metadata_row(object_card or {"guid": guid, "kind": kind}, include_storage=include_storage), "rules": [], "related": [], "counts": {"related": 0, "by_category": {}, "by_status": {}}, "capabilities": { "related": False, "reason": "У этого вида объекта адаптер не знает связанных разделов.", }, } data, config, read_error = read_storage_file_bytes(base_id, object_table, object_file_name, timeout_seconds=timeout_seconds) if read_error: return public_error_result(read_error, include_storage=include_storage, method="metadata.object.related") tree = parse_config_tree_from_bytes(data) if tree is None: return { "schema": "onec_metadata_object_related.v1", "status": "undecodable", "base_id": base_id, "source": {"kind": "live_sql", "database": config["database"], "table": object_table, "file_name": object_file_name} if include_storage else {"kind": "live_metadata"}, "object": public_metadata_row(object_card or {"guid": guid, "kind": kind}, include_storage=include_storage), "related": [], "counts": {"related": 0}, } try: from parser.cas_payload import classify_payload from parser.child_records import declared_child_records from parser.config_object import find_identity from parser.structured_metadata import get_by_path except Exception as exc: return { "schema": "onec_metadata_object_related.v1", "status": "error", "base_id": base_id, "diagnostics": {"message": f"Related object decoder is unavailable: {exc}"}, } related = [] for rule in rules: section = get_by_path(tree, str(rule["path"])) if section is None: continue for record in declared_child_records(section, str(rule["path"])): record_identity = find_identity(record.node) candidate_guids = [] if record_identity: candidate_guids.append(record_identity.guid) else: for candidate in sorted(record.evidence.get("guids") or []): if candidate == "00000000-0000-0000-0000-000000000000": continue if candidate not in candidate_guids: candidate_guids.append(candidate) if not candidate_guids: item = {"category": rule["category"], "status": "no_guid_evidence"} if include_storage: item.update({"section_path": rule["path"], "record_path": record.path, "record_index": record.index}) related.append(item) continue for related_guid in candidate_guids[:guids_per_record]: item: dict[str, Any] = { "category": rule["category"], "guid": related_guid, "status": "source_missing", } if record_identity: item["record_identity"] = record_identity.to_dict() if include_storage: item.update( { "section_path": rule["path"], "record_path": record.path, "record_index": record.index, "source": {"kind": "live_sql", "table": object_table, "file_name": related_guid}, } ) related_file_name = ( f"{extension_guid}__{related_guid}" if object_table == "ConfigCASSave" and extension_guid else related_guid ) if include_storage: item["source"]["file_name"] = related_file_name related_data, _, related_error = read_storage_file_bytes(base_id, object_table, related_file_name, timeout_seconds=timeout_seconds) if related_error and object_table == "ConfigCASSave" and rule["category"] == "Command": command_module_file_name = f"{related_file_name}.2" related_data, _, related_error = read_storage_file_bytes( base_id, object_table, command_module_file_name, timeout_seconds=timeout_seconds, ) if not related_error: related_file_name = command_module_file_name if include_storage: item["source"]["file_name"] = related_file_name if related_error: if include_storage: item["diagnostics"] = related_error.get("diagnostics") else: item["diagnostics"] = {"message": "Описание связанного объекта метаданных не найдено или недоступно."} related.append(item) continue classified = classify_payload( related_data, include_text=bool(include_text), include_tree=False, ) item["status"] = "ok" item["identity"] = config_identity_from_bytes(related_data) or (record_identity.to_dict() if record_identity else None) if include_storage: item["classification"] = {key: value for key, value in classified.items() if key not in {"text", "tree"}} related.append(item) counts_by_category: dict[str, int] = {} counts_by_status: dict[str, int] = {} for item in related: category = str(item.get("category") or "") status = str(item.get("status") or "") counts_by_category[category] = counts_by_category.get(category, 0) + 1 counts_by_status[status] = counts_by_status.get(status, 0) + 1 return { "schema": "onec_metadata_object_related.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "database": config["database"], "table": object_table, "file_name": object_file_name} if include_storage else {"kind": "live_metadata"}, "object": public_metadata_row(object_card or {"guid": guid, "kind": kind}, include_storage=include_storage), "rules": rules if include_storage else [{"category": rule.get("category")} for rule in rules], "related": related, "counts": { "related": len(related), "by_category": dict(sorted(counts_by_category.items())), "by_status": dict(sorted(counts_by_status.items())), }, "diagnostics": { "note": "Storage coordinates are hidden unless include_storage=true.", }, } def metadata_object_forms(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, "metadata.object.forms") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, "metadata.object.forms") if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error extension_guid = str(payload.get("extension_guid") or "").strip().lower() if "extension_guid" in payload and not is_guid_text(extension_guid): return invalid_argument("metadata.object.forms", "extension_guid", "extension_guid must be a GUID string.") timeout_value, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.forms", default=60, minimum=1) if timeout_error: return timeout_error timeout_seconds = int(timeout_value or 60) ordinal_argument_error = validate_explicit_ordinal_arguments(payload, "metadata.object.forms") if ordinal_argument_error: return ordinal_argument_error lookup_limit, lookup_limit_error = parse_object_lookup_limit(payload, "metadata.object.forms") if lookup_limit_error: return lookup_limit_error view, view_error = parse_view_argument(payload, "metadata.object.forms") if view_error: return view_error include_text, include_text_error = strict_bool_argument(payload, "include_text", method="metadata.object.forms", default=False) if include_text_error: return include_text_error include_tree, include_tree_error = strict_bool_argument(payload, "include_tree", method="metadata.object.forms", default=False) if include_tree_error: return include_tree_error table_or_error = metadata_storage_table(payload, "metadata.object.forms") if isinstance(table_or_error, dict): return table_or_error table = table_or_error requested_form, requested_form_error = optional_string_filter(payload, ["form", "name_filter"], method="metadata.object.forms") if requested_form_error: return requested_form_error include_storage, include_storage_error = strict_include_storage(payload, "metadata.object.forms") if include_storage_error: return include_storage_error include_storage = bool(include_storage) object_probe = get_object( payload.get("kind"), str(payload.get("name") or payload.get("guid") or ""), base_id=base_id, view=str(view or "effective"), limit=int(lookup_limit or 20), table=table, extension_guid=extension_guid or None, include_storage=True, ordinal=first_non_empty_arg(payload, "ordinal", "index", "object_index"), include_semantic=False, timeout_seconds=timeout_seconds, ) if object_probe.get("status") != "ok": result = dict(object_probe) result["method"] = "metadata.object.forms" return result object_card = object_probe.get("object") or {} object_guid = str(object_card.get("guid") or "").lower() object_kind = str(object_card.get("kind") or payload.get("kind") or "") object_table = preferred_object_storage_table(object_card, table) has_form_rules = any(rule.get("category") == "Form" for rule in RELATED_SECTION_RULES.get(object_kind, [])) if not has_form_rules: return { "schema": "onec_object_forms.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_metadata"}, "object": public_metadata_row(object_card, include_storage=include_storage), "query": {"form": requested_form, "include_storage": include_storage}, "forms": [], "counts": {"forms": 0, "related": 0}, "capabilities": { "forms": False, "reason": "У этого вида объекта адаптер не знает разделов форм.", }, } related_payload = { **payload, "guid": object_guid, "kind": object_card.get("kind") or payload.get("kind"), "table": object_table, "include_text": False, "include_storage": include_storage, } related_result = metadata_object_related(related_payload) if related_result.get("status") != "ok": result = dict(related_result) result["method"] = "metadata.object.forms" return result wanted = normalize(requested_form or "") form_items = filter_related_children_by_identity(related_result.get("related") or [], "Form", requested_form) manifest_form_diagnostics: list[dict[str, Any]] = [] if object_table == "ConfigCAS" and not form_items: object_storage = object_card.get("storage") if isinstance(object_card.get("storage"), dict) else {} object_file_name = str(object_storage.get("file_name") or (related_result.get("source") or {}).get("file_name") or "").strip() origin = object_card.get("origin") if isinstance(object_card.get("origin"), dict) else {} extension = origin.get("extension") if isinstance(origin.get("extension"), dict) else {} manifest_form_items, manifest_form_diagnostics = extension_manifest_form_items_for_object( base_id, object_guid=object_guid, object_file_name=object_file_name, extension_guid=str(extension.get("guid") or "") or None, timeout_seconds=timeout_seconds, ) if not manifest_form_items: refreshed_matches, refreshed_diagnostics, _refreshed_stats = extension_manifest_object_matches( base_id=base_id, query="", kind_filter=object_kind or None, guid_filter=object_guid, extension_guid=str(extension.get("guid") or "") or None, limit=1, timeout_seconds=timeout_seconds, include_storage=True, ) manifest_form_diagnostics.extend(refreshed_diagnostics) refreshed_route = (refreshed_matches[0].get("route") if refreshed_matches else {}) if refreshed_matches else {} refreshed_file_name = str(refreshed_route.get("file_name") or "").strip() if refreshed_file_name and refreshed_file_name != object_file_name: manifest_form_items, refreshed_form_diagnostics = extension_manifest_form_items_for_object( base_id, object_guid=object_guid, object_file_name=refreshed_file_name, extension_guid=str(extension.get("guid") or "") or None, timeout_seconds=timeout_seconds, ) manifest_form_diagnostics.extend(refreshed_form_diagnostics) form_items = filter_related_children_by_identity( [{"identity": item.get("identity"), **item} for item, _match_by in manifest_form_items], "Form", requested_form, ) if not form_items: form_items = manifest_form_items if not wanted else [ (item, match_by) for item, match_by in manifest_form_items if normalize((item.get("identity") or {}).get("name")) == wanted ] if object_table == "ConfigCAS" and wanted and not form_items: form_matches, _ = metadata_extension_definition_matches( base_id=base_id, query=str(requested_form or ""), max_files=5000, max_matches=5, timeout_seconds=timeout_seconds, include_storage=True, use_cache=True, ) for form_match in form_matches: if normalize(form_match.get("name")) != wanted: continue form_match_source = form_match.get("source") if isinstance(form_match.get("source"), dict) else {} form_source = { "kind": "live_sql", "table": str(form_match_source.get("table") or "ConfigCAS"), "file_name": str(form_match_source.get("file_name") or form_match.get("source_file") or ""), } form_items.append( ( { "category": "Form", "guid": form_match.get("guid"), "status": "ok", "identity": { "guid": form_match.get("guid"), "name": form_match.get("name"), "synonyms": {"ru": form_match.get("synonym")} if form_match.get("synonym") else {}, }, "source": form_source, }, str(form_match.get("match_by") or "extension_definition"), ) ) break forms = [] for item, match_by in form_items: identity = item.get("identity") or {} synonyms = identity.get("synonyms") or {} form_source = item.get("source") if isinstance(item.get("source"), dict) else {} form_file_name = str(form_source.get("file_name") or item.get("guid") or "") if object_table == "ConfigCASSave" and extension_guid and item.get("guid"): form_file_name = f"{extension_guid}__{str(item.get('guid')).lower()}.0" form_source = {"kind": "live_sql", "table": object_table, "file_name": form_file_name} manifest_entries = item.get("manifest_entries") if isinstance(item.get("manifest_entries"), list) else [] payload_entry = next((entry for entry in manifest_entries if isinstance(entry, dict) and str(entry.get("suffix") or "") == ".0" and entry.get("cas_key")), None) if object_table == "ConfigCAS" and payload_entry: form_file_name = str(payload_entry.get("cas_key") or form_file_name) form_source = { "kind": "live_sql", "table": "ConfigCAS", "file_name": form_file_name, } elif object_table == "ConfigCAS" and identity.get("name"): form_matches, _ = metadata_extension_definition_matches( base_id=base_id, query=str(identity.get("name") or ""), max_files=5000, max_matches=1, timeout_seconds=timeout_seconds, include_storage=True, use_cache=True, ) form_match = next((match for match in form_matches if normalize(match.get("name")) == normalize(identity.get("name"))), None) form_match_source = form_match.get("source") if isinstance((form_match or {}).get("source"), dict) else {} form_match_file_name = form_match_source.get("file_name") or ((form_match or {}).get("source_file") if isinstance(form_match, dict) else None) if form_match_file_name: form_file_name = str(form_match_file_name) form_source = { "kind": "live_sql", "table": "ConfigCAS", "file_name": form_file_name, } form_parts = [] if object_table in {"ConfigCAS", "ConfigCASSave"} and form_file_name and form_file_name != str(item.get("guid") or ""): try: from parser.cas_payload import classify_payload except Exception: classify_payload = None form_data, _, form_read_error = read_storage_file_bytes(base_id, object_table, form_file_name, timeout_seconds=timeout_seconds) if form_data and classify_payload: classification = classify_payload(form_data, include_text=bool(include_text), include_tree=bool(include_tree)) public_part = { "role": classification.get("role") or "unclassified_related_payload", "root": classification.get("root"), } if include_storage: public_part.update( { "part_id": form_file_name, "suffix": "", "raw_bytes": classification.get("raw_bytes"), "payload_bytes": classification.get("payload_bytes"), "sha1": classification.get("sha1"), "strings_sample": classification.get("strings_sample"), "base64_blocks": classification.get("base64_blocks"), "stream_blocks": classification.get("stream_blocks"), } ) form_parts.append(public_part) elif form_read_error and include_storage: form_parts.append({"role": "source_missing", "root": None, "diagnostics": form_read_error.get("diagnostics")}) else: parts_result = metadata_object_parts( { "base_id": base_id, "guid": item.get("guid"), "kind": "Form", "table": object_table, "include_text": bool(include_text), "include_tree": bool(include_tree), "timeout_seconds": timeout_seconds, } ) for part in parts_result.get("parts") or []: classification = part.get("classification") or {} public_part = { "role": classification.get("role") or "unclassified_related_payload", "root": classification.get("root"), } if include_storage: public_part.update( { "part_id": part.get("part_id"), "suffix": part.get("suffix"), "raw_bytes": classification.get("raw_bytes"), "payload_bytes": classification.get("payload_bytes"), "sha1": classification.get("sha1"), "strings_sample": classification.get("strings_sample"), "base64_blocks": classification.get("base64_blocks"), "stream_blocks": classification.get("stream_blocks"), } ) form_parts.append(public_part) form_row = { "guid": item.get("guid"), "name": identity.get("name"), "synonyms": synonyms, "counts": { "parts": len(form_parts), "form_payload_parts": sum(1 for part in form_parts if part.get("role") == "form_payload"), }, } if include_storage: form_row["parts"] = form_parts if wanted: form_row["match_by"] = match_by if include_storage: form_row["source"] = form_source or item.get("source") form_row["related_record"] = { "section_path": item.get("section_path"), "record_path": item.get("record_path"), "record_index": item.get("record_index"), } forms.append(form_row) if wanted and not forms: result = child_not_found("metadata.object.forms", "Форма", requested_form, related_result.get("object") or object_card, base_id=base_id) result.update( { "schema": "onec_object_forms.v1", "source": related_result.get("source") if include_storage else {"kind": "live_metadata"}, "query": {"form": requested_form, "include_storage": include_storage}, "forms": [], "counts": {"forms": 0, "related": (related_result.get("counts") or {}).get("related")}, } ) return result return { "schema": "onec_object_forms.v1", "status": "ok", "base_id": base_id, "source": related_result.get("source") if include_storage else {"kind": "live_metadata"}, "object": public_metadata_row(object_card, include_storage=include_storage), "query": {"form": requested_form, "include_storage": include_storage}, "forms": forms, "counts": {"forms": len(forms), "related": (related_result.get("counts") or {}).get("related")}, "diagnostics": { "note": "Form rows are resolved from object metadata; form profile/details are decoded separately by form GUID from live storage. form_payload_parts may be 0 when related payload classifier cannot tag the same storage row, and does not mean profile decoding failed.", **({"extension_manifest_forms": manifest_form_diagnostics} if manifest_form_diagnostics else {}), }, } def enrich_form_profile_object_data_paths(profile: dict[str, Any], object_fields: dict[str, Any]) -> int: """Replace form-name fallbacks with public object field names resolved by GUID.""" top_fields: dict[str, str] = {} table_fields: dict[tuple[str, str], str] = {} def identity_guid(row: dict[str, Any]) -> str: identity = row.get("identity") if isinstance(row.get("identity"), dict) else {} return str(identity.get("guid") or "").strip().lower() for section in ("attributes", "dimensions", "resources"): for field in object_fields.get(section) or []: if not isinstance(field, dict): continue guid = identity_guid(field) name = str(field.get("name") or "").strip() if guid and name: top_fields[guid] = name for table in object_fields.get("tabular_sections") or []: if not isinstance(table, dict): continue table_name = str(table.get("name") or "").strip() for field in table.get("columns") or []: if not isinstance(field, dict): continue guid = identity_guid(field) name = str(field.get("name") or "").strip() if table_name and guid and name: table_fields[(normalize_exact(table_name), guid)] = name resolved = 0 for item in profile.get("items") or []: if not isinstance(item, dict): continue old_path = str(item.get("path_to_data") or "").strip() parts = old_path.split(".") if len(parts) < 2 or normalize_exact(parts[0]) not in {"объект", "object"}: continue item_guids = {str(guid or "").strip().lower() for guid in item.get("guids_sample") or []} candidates: set[str] = set() if len(parts) == 2: candidates = {name for guid, name in top_fields.items() if guid in item_guids} new_path = f"{parts[0]}.{next(iter(candidates))}" if len(candidates) == 1 else "" else: table_key = normalize_exact(parts[1]) candidates = { name for (candidate_table, guid), name in table_fields.items() if candidate_table == table_key and guid in item_guids } if len(candidates) == 1: field_name = next(iter(candidates)) if parts[-1].casefold().startswith("total") and not field_name.casefold().startswith("total"): field_name = f"Total{field_name}" new_path = f"{parts[0]}.{parts[1]}.{field_name}" else: new_path = "" if not new_path or new_path == old_path: continue item["path_to_data"] = new_path item["data_path_resolution"] = { "source": "object_metadata_identity_guid", "previous_path": old_path, "path_to_data": new_path, } semantic = item.get("semantic") if isinstance(item.get("semantic"), dict) else {} for properties in (semantic.get("groups") or {}).values(): for prop in properties or []: if isinstance(prop, dict) and prop.get("name") == "ПутьКДанным": prop["value"] = new_path prop["source"] = "object_metadata_identity_guid" resolved += 1 return resolved def metadata_object_form_details(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, "metadata.object.form.details") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, "metadata.object.form.details") if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error include_storage, include_storage_error = strict_include_storage(payload, "metadata.object.form.details") if include_storage_error: return include_storage_error include_storage = bool(include_storage) table_or_error = metadata_storage_table(payload, "metadata.object.form.details") if isinstance(table_or_error, dict): return table_or_error table = table_or_error include_module_text, include_module_text_error = strict_bool_argument(payload, "include_module_text", method="metadata.object.form.details", default=False) if include_module_text_error: return include_module_text_error include_parameters, include_parameters_error = strict_bool_argument(payload, "include_parameters", method="metadata.object.form.details", default=True) if include_parameters_error: return include_parameters_error max_forms, max_forms_error = parse_int_argument(payload, "max_forms", method="metadata.object.form.details", default=20, minimum=1, maximum=100) if max_forms_error: return max_forms_error max_items, max_items_error = parse_int_argument(payload, "max_items", method="metadata.object.form.details", default=1000, minimum=1, maximum=5000) if max_items_error: return max_items_error max_attributes, max_attributes_error = parse_int_argument(payload, "max_attributes", method="metadata.object.form.details", default=1000, minimum=1, maximum=5000) if max_attributes_error: return max_attributes_error max_commands, max_commands_error = parse_int_argument(payload, "max_commands", method="metadata.object.form.details", default=1000, minimum=1, maximum=5000) if max_commands_error: return max_commands_error max_parameters, max_parameters_error = parse_int_argument(payload, "max_parameters", method="metadata.object.form.details", default=80, minimum=1, maximum=500) if max_parameters_error: return max_parameters_error timeout_value, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.form.details", default=60, minimum=1) if timeout_error: return timeout_error element_error = validate_optional_string_arguments(payload, "metadata.object.form.details", ["element", "element_name", "element_path", "path", "element_id", "id"]) if element_error: return element_error timeout_seconds = int(timeout_value or 60) requested_kind = canonical_kind(str(payload.get("object_type") or payload.get("kind") or "")) requested_name = str(payload.get("object_name") or payload.get("name") or payload.get("form") or payload.get("form_name") or "").strip() if requested_kind == "CommonForm": decoded = metadata_form_decode( { **payload, "base_id": base_id, "object_type": "CommonForm", "object_name": requested_name, "form": payload.get("form") or requested_name, "name": payload.get("form") or requested_name, "max_items": max_items, "include_module_text": bool(include_module_text), "include_parameters": bool(include_parameters), "max_parameters": int(max_parameters or 80), "include_storage": include_storage, "table": table, "timeout_seconds": timeout_seconds, **({key: value for key, value in form_element_filter_from_payload(payload).items() if value not in {None, ""}}), } ) if decoded.get("status") != "ok": result = dict(decoded) result["method"] = "metadata.object.form.details" return result profile = decoded.get("profile") if isinstance(decoded.get("profile"), dict) else {} form_info = decoded.get("form") if isinstance(decoded.get("form"), dict) else {} detail = { **form_info, "profile": profile, **form_public_sections(profile), "properties": form_profile_properties(profile), "capabilities": form_profile_capabilities(profile), } if include_storage and isinstance(decoded.get("source"), dict): detail["source"] = decoded.get("source") return { "schema": "onec_object_form_details.v1", "status": "ok", "base_id": base_id, "source": decoded.get("source") if include_storage else {"kind": "live_metadata"}, "object": { "kind": "CommonForm", "name": form_info.get("name") or requested_name or None, "guid": form_info.get("guid"), }, "query": { "form": payload.get("form") or requested_name or None, **({key: value for key, value in form_element_filter_from_payload(payload).items() if value not in {None, ""}}), "max_forms": 1, "include_parameters": bool(include_parameters), "max_parameters": int(max_parameters or 80), "include_storage": include_storage, }, "forms": [detail], "counts": {"forms": 1, "available_forms": 1}, } forms_result = metadata_object_forms({**payload, "include_storage": True, "table": table}) if forms_result.get("status") != "ok": result = dict(forms_result) result["method"] = "metadata.object.form.details" return result object_card = forms_result.get("object") if isinstance(forms_result.get("object"), dict) else {} object_fields = metadata_object_attributes( { "base_id": base_id, "kind": object_card.get("kind") or requested_kind, "guid": object_card.get("guid"), "only": "all", "include_storage": True, "limit": 1000, "table": table, "timeout_seconds": timeout_seconds, } ) details = [] for form in (forms_result.get("forms") or [])[:max_forms]: form_source = form.get("source") if isinstance(form.get("source"), dict) else {} form_source_file_name = str(form_source.get("file_name") or "").strip() decoded = metadata_form_decode( { "base_id": base_id, "form_guid": form.get("guid"), **({"file_name": form_source_file_name} if form_source_file_name else {}), "max_items": max_items, "include_module_text": bool(include_module_text), "include_parameters": bool(include_parameters), "max_parameters": int(max_parameters or 80), "include_storage": include_storage, "table": form_source.get("table") or table, "timeout_seconds": timeout_seconds, **({key: value for key, value in form_element_filter_from_payload(payload).items() if value not in {None, ""}}), } ) detail = dict(form) if not include_storage: for key in ("source", "parts", "related_record"): detail.pop(key, None) if decoded.get("status") == "ok": profile = decoded.get("profile") or {} if object_fields.get("status") == "ok": enrich_form_profile_object_data_paths(profile, object_fields) detail["profile"] = profile detail.update(form_public_sections(profile)) detail["properties"] = form_profile_properties(profile) detail["capabilities"] = form_profile_capabilities(profile) else: detail["profile"] = {"status": decoded.get("status"), "diagnostics": decoded.get("diagnostics")} detail["errors"] = [{"section": "form.decode", "status": decoded.get("status"), "diagnostics": decoded.get("diagnostics")}] details.append(detail) return { "schema": "onec_object_form_details.v1", "status": "ok", "base_id": base_id, "source": forms_result.get("source") if include_storage else {"kind": "live_metadata"}, "object": public_metadata_row(forms_result.get("object") or {}, include_storage=include_storage), "query": { "form": payload.get("form") or payload.get("name_filter"), **({key: value for key, value in form_element_filter_from_payload(payload).items() if value not in {None, ""}}), "max_forms": max_forms, "include_parameters": bool(include_parameters), "max_parameters": int(max_parameters or 80), "include_storage": include_storage, }, "forms": details, "counts": {"forms": len(details), "available_forms": (forms_result.get("counts") or {}).get("forms")}, } DEFINITION_FIND_AREAS = {"metadata", "object", "form", "commands", "templates", "modules", "extensions"} DEFINITION_FIND_DEFAULT_AREAS = ["metadata", "object", "form", "commands", "templates", "modules", "extensions"] def definition_match_by(item: dict[str, Any], query: str) -> str | None: wanted = normalize(query) wanted_exact = normalize_exact(query) candidates = [ ("name", item.get("name")), ("synonym", item.get("synonym")), ("title", item.get("title")), ("handler", item.get("handler")), ("event_name", item.get("event_name")), ("command", item.get("command")), ] sample_candidates: list[tuple[str, Any]] = [] for sample_key in ("strings_sample", "properties_sample", "property_names", "guids_sample"): sample_values = item.get(sample_key) if isinstance(sample_values, list): sample_candidates.extend((sample_key, value) for value in sample_values) for key, value in candidates: if value is not None and normalize_exact(value) == wanted_exact: return f"{key}_exact" for key, value in sample_candidates: if value is not None and normalize_exact(value) == wanted_exact: return f"{key}_exact" for key, value in candidates: if value is not None and normalize(value) == wanted: return f"{key}_normalized" for key, value in sample_candidates: if value is not None and normalize(value) == wanted: return f"{key}_normalized" for key, value in candidates: normalized = normalize(value) if wanted and normalized and wanted in normalized: return f"{key}_contains" for key, value in sample_candidates: normalized = normalize(value) if wanted and normalized and wanted in normalized: return f"{key}_contains" return None def definition_origin(item: dict[str, Any] | None, object_card: dict[str, Any] | None) -> dict[str, Any]: item = item or {} object_card = object_card or {} if isinstance(item.get("origin"), dict): return dict(item.get("origin") or {}) extension = item.get("extension") if isinstance(item.get("extension"), dict) else None source = str(item.get("source") or object_card.get("source") or "").strip().casefold() if extension: return { "source": "extension", "presentation": "Расширение", "extension": { "name": extension.get("name"), "synonym": extension.get("synonym"), "guid": extension.get("guid"), }, "status": "ok" if extension.get("name") or extension.get("guid") else "extension_unresolved", } if source == "extension": return { "source": "extension", "presentation": "Расширение", "extension": None, "status": "extension_unresolved", "diagnostics": { "message": "Определение относится к расширению, но текущий декодированный payload не содержит имя расширения-владельца.", }, } if source == "base": return {"source": "configuration", "presentation": "Конфигурация", "extension": None, "status": "ok"} return { "source": "unknown", "presentation": "Источник не определен", "extension": None, "status": "not_resolved", "diagnostics": { "message": "В текущем публичном payload нет признака, где именно определен этот элемент: в конфигурации или расширении.", }, } def definition_type_public(item: dict[str, Any]) -> Any: if "type" in item: return item.get("type") if "value_type" in item: return item.get("value_type") return None def definition_match( *, query: str, area: str, kind_ru: str, location: dict[str, Any], item: dict[str, Any], object_card: dict[str, Any], read_selector: dict[str, Any], form_card: dict[str, Any] | None = None, ) -> dict[str, Any] | None: match_by = definition_match_by(item, query) if not match_by: return None name = item.get("name") or item.get("handler") or item.get("command") or item.get("event_name") result: dict[str, Any] = { "area": area, "kind": kind_ru, "name": name, "synonym": item.get("synonym") or item.get("title"), "match_by": match_by, "object": { "kind": object_card.get("kind"), "kind_ru": object_card.get("kind_ru"), "name": object_card.get("name"), "synonym": object_card.get("synonym"), "guid": object_card.get("guid"), }, "location": location, "origin": definition_origin(item, object_card), "read_selector": read_selector, } if form_card: result["form"] = { "name": form_card.get("name"), "synonym": (form_card.get("synonym") or next(iter((form_card.get("synonyms") or {}).values()), None) if isinstance(form_card.get("synonyms"), dict) else None), "guid": form_card.get("guid"), } type_info = definition_type_public(item) if type_info: result["type"] = type_info if str(match_by or "").startswith(("strings_sample", "properties_sample", "property_names", "guids_sample")): result["evidence"] = { "strings_sample": item.get("strings_sample") or [], "guids_sample": item.get("guids_sample") or [], } return result def definition_read_selector(base_id: str, object_card: dict[str, Any], **extra: Any) -> dict[str, Any]: kind = object_card.get("kind") name = object_card.get("name") selector = { "base_id": base_id, "kind": kind, "name": name, "guid": object_card.get("guid"), } public_ref = object_selector_ref(kind, name) if public_ref: selector["ref"] = public_ref selector.update({key: value for key, value in extra.items() if value is not None}) return selector def object_related_selectors(base_id: str, object_card: dict[str, Any]) -> dict[str, dict[str, Any]]: kind = str(object_card.get("kind") or "") capabilities = set(KIND_CAPABILITIES.get(kind, [])) selectors: dict[str, dict[str, Any]] = {} selectors["card"] = definition_read_selector(base_id, object_card, method="metadata.object.get") selectors["full"] = definition_read_selector(base_id, object_card, method="metadata.object.full") if "attributes" in capabilities or "tabular_sections" in capabilities or "dimensions" in capabilities or "resources" in capabilities: selectors["attributes"] = definition_read_selector(base_id, object_card, method="metadata.object.attributes", only="all") if "forms" in capabilities: selectors["forms"] = definition_read_selector(base_id, object_card, method="metadata.object.forms") selectors["form_details"] = definition_read_selector(base_id, object_card, method="metadata.object.form.details") if "templates" in capabilities: selectors["templates"] = definition_read_selector(base_id, object_card, method="metadata.object.templates") if "commands" in capabilities: selectors["commands"] = definition_read_selector(base_id, object_card, method="metadata.object.commands") if "modules" in capabilities: selectors["modules"] = definition_read_selector(base_id, object_card, method="metadata.object.modules") selectors["code_search"] = definition_read_selector(base_id, object_card, method="code.search") selectors["modules_search"] = definition_read_selector(base_id, object_card, method="modules.search") return selectors def metadata_definition_match_by_name(name: Any, synonym: Any, query: str) -> str | None: query_norm = normalize(query) query_exact = normalize_exact(query) if normalize_exact(name) == query_exact: return "name_exact" if normalize_exact(synonym) == query_exact: return "synonym_exact" if normalize(name) == query_norm: return "name_normalized" if normalize(synonym) == query_norm: return "synonym_normalized" if query_norm and normalize(name) and query_norm in normalize(name): return "name_contains" if query_norm and normalize(synonym) and query_norm in normalize(synonym): return "synonym_contains" return None def metadata_configuration_definition_matches( *, base_id: str, query: str, max_matches: int, use_cache: bool = False, table: str = "Config", ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: query_kind, query_name = parse_object_query(None, query) query_for_match = query_name if query_kind and query_name else query if use_cache: config, config_error = sql_config_for_base(base_id) if not config: return [], [{"area": "metadata", "status": (config_error or {}).get("status"), "diagnostics": config_error}] rows = metadata_guid_index_lookup_by_name( config, guid_role="metadata_object", query=query_for_match, limit=max_matches, ) source = "metadata_cache" else: rows = [] page_size = 5000 kinds_to_scan = [query_kind] if query_kind else sorted(KIND_CAPABILITIES) for kind in [kind for kind in kinds_to_scan if kind]: offset = 0 while len(rows) < max_matches: page = list_objects( kind, base_id=base_id, limit=page_size, offset=offset, include_storage=False, include_missing=False, only_missing=False, exact_counts=True, refresh_cache=True, table=table, ) if page.get("status") != "ok": return rows, [{"area": "metadata", "status": page.get("status"), "diagnostics": page.get("diagnostics"), "kind": kind}] objects = page.get("objects") or [] for item in objects: if query_kind and not kind_matches_request(str(item.get("kind") or ""), query_kind, None): continue if metadata_definition_match_by_name(item.get("name"), item.get("synonym"), query_for_match): rows.append(item) if len(rows) >= max_matches: break if len(objects) < page_size: break offset += page_size source = "live_metadata" matches: list[dict[str, Any]] = [] for row in rows: if query_kind and not kind_matches_request(str(row.get("kind") or ""), query_kind, None): continue match_by = metadata_definition_match_by_name(row.get("name"), row.get("synonym"), query_for_match) if not match_by: continue kind_ru = row.get("kind_ru") or RU_KIND.get(str(row.get("kind") or ""), row.get("kind") or "ОбъектМетаданных") origin_source = str(row.get("source") or "").strip().casefold() origin = { "source": "extension" if origin_source == "extension" else "configuration", "presentation": "Расширение" if origin_source == "extension" else "Конфигурация", "extension": row.get("extension") if isinstance(row.get("extension"), dict) else None, "status": "ok", } matches.append( { "area": "metadata", "kind": kind_ru, "name": row.get("name"), "synonym": row.get("synonym"), "guid": row.get("guid"), "match_by": match_by, "location": { "presentation": ".".join(part for part in [kind_ru, row.get("name")] if part), "section": "Объекты метаданных", }, "origin": origin, "read_selector": definition_read_selector(base_id, row, method="metadata.object.get"), "object": { "kind": row.get("kind"), "kind_ru": kind_ru, "name": row.get("name"), "synonym": row.get("synonym"), "guid": row.get("guid"), }, "related_selectors": object_related_selectors( base_id, { "kind": row.get("kind"), "kind_ru": kind_ru, "name": row.get("name"), "synonym": row.get("synonym"), "guid": row.get("guid"), }, ), } ) diagnostics = [{"area": "metadata", "source": source, "cache": "hit" if use_cache and matches else ("miss" if use_cache else "not_used"), "matches": len(matches)}] return matches[:max_matches], diagnostics def extension_definition_guid_sources(base_id: str, *, timeout_seconds: int = 60) -> tuple[dict[str, list[dict[str, Any]]], dict[str, Any] | None]: records, error = live_dbnames_records(base_id, timeout_seconds=timeout_seconds) if error: return {}, error extensions_by_guid = extension_map_by_guid(base_id) result: dict[str, list[dict[str, Any]]] = {} for record in records or []: source = str(getattr(record, "source", "") or "") extension_guid = extension_guid_from_dbnames_source(source) if not extension_guid: continue definition_guid = str(getattr(record, "guid", "") or "").lower() if not definition_guid: continue extension = extensions_by_guid.get(extension_guid) or {"guid": extension_guid, "name": None, "active": None} result.setdefault(definition_guid, []).append( { "extension": extension, "storage_role": str(getattr(record, "storage_role", "") or ""), "sql_number": int(getattr(record, "sql_number", 0) or 0), } ) return result, None def extension_definition_kind_ru(role: str) -> str: kind = DBNAMES_ROLE_KIND.get(role) if kind: return RU_KIND.get(kind, kind) if role == "Fld": return "Реквизит/поле расширения" if role == "VT": return "Табличная часть расширения" if role == "LineNo": return "Номер строки табличной части" if role.endswith("ChngR"): return "Изменение объекта расширением" return "Определение расширения" def extension_definition_match_from_identity( *, base_id: str, identity: dict[str, Any], source_item: dict[str, Any], query: str, match_by: str, include_storage: bool = False, file_name: str | None = None, extension_sources: list[dict[str, Any]] | None = None, ) -> dict[str, Any]: extension = source_item.get("extension") or {} role = str(source_item.get("storage_role") or "") kind_ru = extension_definition_kind_ru(role) item_name = str(identity.get("name") or query or "") synonym = next(iter((identity.get("synonyms") or {}).values()), None) if isinstance(identity.get("synonyms"), dict) else None match: dict[str, Any] = { "area": "extensions", "kind": kind_ru, "name": item_name, "synonym": synonym, "match_by": match_by, "location": { "presentation": ".".join(part for part in ["Расширение", extension.get("name"), kind_ru, item_name] if part), "section": "Расширения", }, "origin": { "source": "extension", "presentation": "Расширение", "extension": extension, "status": "ok" if extension.get("name") else "extension_unresolved", }, "read_selector": { "base_id": base_id, "method": "metadata.definition.find", "query": item_name, "areas": ["extensions"], }, } if identity.get("guid"): match["guid"] = identity.get("guid") if include_storage: match["source"] = {"kind": "live_metadata", "table": "ConfigCAS", "file_name": file_name} match["extension_sources"] = extension_sources or [source_item] return match def extension_definition_identity_match_by(identity: dict[str, Any], query: str) -> str | None: name = identity.get("name") synonyms = list((identity.get("synonyms") or {}).values()) if isinstance(identity.get("synonyms"), dict) else [] query_norms = normalized_variants(query) query_exacts = normalized_exact_variants(query) name_norms = normalized_variants(name) name_exacts = normalized_exact_variants(name) if query_exacts & name_exacts: return "name_exact" if any(query_exacts & normalized_exact_variants(value) for value in synonyms if value): return "synonym_exact" if query_norms & name_norms: return "name_normalized" if any(query_norms & normalized_variants(value) for value in synonyms if value): return "synonym_normalized" if any(query_norm and name_norm and query_norm in name_norm for query_norm in query_norms for name_norm in name_norms): return "name_contains" if any(normalized_contains_any(query, value) for value in synonyms if value): return "synonym_contains" return None def metadata_extension_definition_cache_marker(config: dict[str, str] | None) -> dict[str, Any] | None: if not config: return None marker = metadata_guid_index_lookup_payload(config, EXTENSION_DEFINITION_CACHE_MARKER_GUID, EXTENSION_DEFINITION_CACHE_MARKER_ROLE) return marker if isinstance(marker, dict) and marker.get("status") == "complete" else None def metadata_extension_definition_cache_lookup( base_id: str, *, query: str, max_matches: int, ) -> tuple[list[dict[str, Any]], dict[str, Any] | None]: config, _ = sql_config_for_base(base_id) if not config: return [], None matches = metadata_guid_index_lookup_by_name( config, guid_role=EXTENSION_DEFINITION_CACHE_ROLE, query=query, limit=max_matches, ) filtered: list[dict[str, Any]] = [] for match in matches: recalculated = extension_definition_identity_match_by( { "name": match.get("name"), "synonyms": {"ru": match.get("synonym")} if match.get("synonym") else {}, }, query, ) if recalculated: match["match_by"] = recalculated filtered.append(match) marker = metadata_extension_definition_cache_marker(config) return filtered, marker def metadata_extension_definition_cache_upsert( config: dict[str, str] | None, *, base_id: str, identity: dict[str, Any], source_item: dict[str, Any], file_name: str, ) -> dict[str, Any] | None: guid = str(identity.get("guid") or "").lower() if not config or not is_guid_text(guid): return None role = str(source_item.get("storage_role") or "") extension = source_item.get("extension") or {} match = extension_definition_match_from_identity( base_id=base_id, identity=identity, source_item=source_item, query=str(identity.get("name") or ""), match_by="cache", include_storage=True, file_name=file_name, ) match["source_file"] = file_name metadata_guid_index_upsert( config, { "guid": guid, "guid_role": EXTENSION_DEFINITION_CACHE_ROLE, "kind": DBNAMES_ROLE_KIND.get(role) or role or "ExtensionDefinition", "kind_ru": extension_definition_kind_ru(role), "public_kind": PUBLIC_KIND.get(DBNAMES_ROLE_KIND.get(role) or "", "extension_definition"), "name": identity.get("name"), "synonym": match.get("synonym"), "presentation": (match.get("location") or {}).get("presentation"), "owner_guid": (extension or {}).get("guid"), "owner_kind": "Extension", "owner_name": (extension or {}).get("name"), "source": "extension", "source_file": file_name, "payload": match, }, ) return match def metadata_extension_definition_matches( *, base_id: str, query: str, max_files: int, max_matches: int, timeout_seconds: int, include_storage: bool = False, refresh_cache: bool = False, use_cache: bool = False, ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: diagnostics: list[dict[str, Any]] = [] config, _ = sql_config_for_base(base_id) if use_cache and not refresh_cache: cached_matches, cache_marker = metadata_extension_definition_cache_lookup(base_id, query=query, max_matches=max_matches) if cached_matches: if not include_storage or any( (isinstance(match.get("source"), dict) and match["source"].get("file_name")) or match.get("source_file") for match in cached_matches ): diagnostics.append({"area": "extensions", "cache": "hit", "matches": len(cached_matches)}) return cached_matches[:max_matches], diagnostics diagnostics.append({"area": "extensions", "cache": "stale_missing_storage", "matches": len(cached_matches)}) if cache_marker and not include_storage: diagnostics.append({"area": "extensions", "cache": "hit_empty", "indexed_definitions": cache_marker.get("indexed_definitions")}) return [], diagnostics if cache_marker and include_storage: diagnostics.append({"area": "extensions", "cache": "hit_empty_storage_refresh", "indexed_definitions": cache_marker.get("indexed_definitions")}) guid_sources, source_error = extension_definition_guid_sources(base_id, timeout_seconds=timeout_seconds) if source_error: diagnostics.append({"area": "extensions", "status": source_error.get("status"), "diagnostics": source_error.get("diagnostics")}) return [], diagnostics if not guid_sources: diagnostics.append({"area": "extensions", "message": "DBNames расширений не содержит индексируемых определений; выполняется CAS-only scan."}) files = storage_files_list({"base_id": base_id, "table": "ConfigCAS", "limit": max_files, "_internal": True, "timeout_seconds": timeout_seconds}) if files.get("status") != "ok": diagnostics.append({"area": "extensions", "status": files.get("status"), "diagnostics": files.get("diagnostics")}) return [], diagnostics guid_pattern = re.compile(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}") matches: list[dict[str, Any]] = [] seen_matches: set[tuple[str, str, str]] = set() scanned = 0 indexed = 0 completed_scan = len(files.get("files") or []) < max_files file_rows = files.get("files") or [] for file_start in range(0, len(file_rows), 120): file_chunk = file_rows[file_start : file_start + 120] file_names = [str(row.get("FileName") or "") for row in file_chunk if str(row.get("FileName") or "")] payloads, _, read_error = read_storage_files_bytes(base_id, "ConfigCAS", file_names, timeout_seconds=min(timeout_seconds, 60)) if read_error: diagnostics.append({"area": "extensions", "status": read_error.get("status"), "diagnostics": read_error.get("diagnostics")}) continue for file_name in file_names: data = (payloads or {}).get(file_name) if not data: continue text = payload_text_from_bytes(data).get("text") or "" scanned += 1 identity = config_identity_from_bytes(data) or {} payload_guids = {value.lower() for value in guid_pattern.findall(text)} identity_guid = str(identity.get("guid") or "").lower() source_items = [] if identity_guid: source_items.extend(guid_sources.get(identity_guid) or []) if source_items: cached = metadata_extension_definition_cache_upsert( config, base_id=base_id, identity=identity, source_item=source_items[0], file_name=file_name, ) if cached: indexed += 1 match_by = extension_definition_identity_match_by(identity, query) if not match_by: continue for guid in sorted(payload_guids): if guid == identity_guid: continue source_items.extend(guid_sources.get(guid) or []) primary_source = source_items[0] if source_items else { "extension": {}, "storage_role": "", "sql_number": None, "source": "ConfigCAS", } extension = primary_source.get("extension") or {} match = extension_definition_match_from_identity( base_id=base_id, identity=identity, source_item=primary_source, query=query, match_by=match_by, include_storage=include_storage, file_name=file_name, extension_sources=source_items, ) cached = metadata_extension_definition_cache_upsert( config, base_id=base_id, identity=identity, source_item=primary_source, file_name=file_name, ) if cached: indexed += 1 dedupe_key = (str(match.get("guid") or match.get("name") or ""), str((extension or {}).get("guid") or ""), str(match.get("kind") or "")) if dedupe_key in seen_matches: continue seen_matches.add(dedupe_key) if len(matches) < max_matches: matches.append(match) if len(matches) >= max_matches: break if len(matches) >= max_matches: break if completed_scan and config: metadata_guid_index_upsert( config, { "guid": EXTENSION_DEFINITION_CACHE_MARKER_GUID, "guid_role": EXTENSION_DEFINITION_CACHE_MARKER_ROLE, "kind": "ExtensionDefinitionCache", "kind_ru": "Индекс определений расширений", "name": "Индекс определений расширений", "presentation": "Индекс определений расширений", "source": "extension", "payload": { "status": "complete", "base_id": base_id, "scanned_payloads": scanned, "indexed_definitions": indexed, "max_files": max_files, "updated_at": time.time(), }, }, ) diagnostics.append( { "area": "extensions", "cache": "not_used", "index": "rebuilt" if refresh_cache and completed_scan else ("updated" if completed_scan else "partial_scan"), "scanned_payloads": scanned, "indexed_definitions": indexed, "indexed_extension_guids": len(guid_sources), } ) return matches, diagnostics def extension_filter_to_guid(base_id: str, extension: str, *, method: str) -> tuple[str | None, dict[str, Any] | None]: extension_filter = str(extension or "").strip() if not extension_filter: return None, None if is_guid_text(extension_filter): return extension_filter.lower(), None wanted_variants = normalized_variants(extension_filter) for item in extension_map_by_guid(base_id).values(): if normalized_variants(str(item.get("name") or "")) & wanted_variants: return str(item.get("guid") or "").lower(), None return None, { "schema": f"onec_{method.replace('.', '_')}.v1", "status": "not_found", "error": "extension_not_found", "base_id": base_id, "query": {"extension": extension_filter}, "diagnostics": {"message": f"Расширение `{extension_filter}` не найдено."}, } def extension_source_matches(source: dict[str, Any], extension_guid: str | None) -> bool: if not extension_guid: return True extension = source.get("extension") if isinstance(source.get("extension"), dict) else {} return str(extension.get("guid") or "").strip().lower() == extension_guid EXTENSION_MANIFEST_CACHE: dict[tuple[str, str], dict[str, Any]] = {} EXTENSION_MANIFEST_CACHE_LOCK = threading.Lock() def extension_root_key_from_zipped_info(data: bytes) -> str: return data[4:24].hex() if len(data) >= 24 else "" def extension_zipped_info_rows(base_id: str, *, timeout_seconds: int = 30) -> tuple[list[dict[str, Any]], dict[str, Any] | None]: config, config_error = sql_config_for_base(base_id) if not config: return [], config_error try: import pymssql # type: ignore except Exception as exc: return [], {"status": "error", "diagnostics": {"message": str(exc)}} rows: list[dict[str, Any]] = [] try: with pymssql.connect( server=config["server"], user=config["user"], password=config["password"], database=config["database"], login_timeout=5, timeout=timeout_seconds, ) as conn: with conn.cursor(as_dict=True) as cursor: cursor.execute( """ SELECT [_IDRRef], [_ExtName], [_ExtensionOrder], [_ExtensionZippedInfo] FROM dbo.[_ExtensionsInfo] ORDER BY [_ExtensionOrder], [_ExtName] """ ) for row in cursor.fetchall(): zipped = row.get("_ExtensionZippedInfo") or b"" if isinstance(zipped, memoryview): zipped = zipped.tobytes() elif not isinstance(zipped, (bytes, bytearray)): zipped = bytes(zipped or b"") guid = dbnames_ext_guid_from_idrref(row.get("_IDRRef")) root_key = extension_root_key_from_zipped_info(bytes(zipped)) rows.append( { "name": jsonable(row.get("_ExtName")), "guid": guid, "order": jsonable(row.get("_ExtensionOrder")), "root_cas_key": root_key, "zipped_info_bytes": len(zipped), } ) except Exception as exc: return [], {"status": "error", "diagnostics": {"message": str(exc)}} return rows, None def manifest_scalar(node: Any) -> str: if isinstance(node, dict) and node.get("type") in {"atom", "string"}: return str(node.get("value") or "") return "" def manifest_base64_to_sha1(value: str) -> str | None: if not re.fullmatch(r"[A-Za-z0-9+/]+={0,2}", str(value or "")): return None try: data = base64.b64decode(value, validate=True) except Exception: return None return data.hex() if len(data) == 20 else None def extract_extension_manifest_from_root_payload(data: bytes, *, root_key: str, extension: dict[str, Any]) -> dict[str, Any]: from parser.payload import decode_payload_lossless, parse_brace_text decoded = decode_payload_lossless(data) text = str(decoded.get("text") or "") tree = parse_brace_text(text) if not (isinstance(tree, dict) and tree.get("type") == "sequence"): return { "status": "error", "root_cas_key": root_key, "extension": extension, "entries": [], "diagnostics": {"message": "Extension root CAS payload did not decode to a sequence manifest."}, } items = tree.get("items") or [] if items and isinstance(items[0], dict) and items[0].get("type") == "atom" and manifest_scalar(items[0]).strip("ï»¿п»ї") == "": items = items[1:] if len(items) == 4 and manifest_scalar(items[0]) in {"", "п»ї"}: items = items[1:] if len(items) < 3: return { "status": "error", "root_cas_key": root_key, "extension": extension, "entries": [], "diagnostics": {"message": "Extension root CAS manifest has fewer than three top-level items."}, } payload_block = items[1] manifest_block = items[2] extension_configuration_guid = "" if isinstance(payload_block, dict) and payload_block.get("type") == "list": block_items = payload_block.get("items") or [] if len(block_items) > 1: extension_configuration_guid = manifest_scalar(block_items[1]).lower() manifest_items = manifest_block.get("items") if isinstance(manifest_block, dict) else [] declared_count = int(manifest_scalar(manifest_items[0]) or "0") if manifest_items else 0 entries: list[dict[str, Any]] = [] for index in range(1, len(manifest_items or []), 2): object_id = manifest_scalar(manifest_items[index]) value = manifest_scalar(manifest_items[index + 1]) if index + 1 < len(manifest_items) else "" cas_key = manifest_base64_to_sha1(value) if not object_id or not cas_key: continue entries.append( { "object_id": object_id, "object_base_id": object_id.split(".", 1)[0].lower(), "suffix": "" if "." not in object_id else "." + object_id.split(".", 1)[1], "cas_key": cas_key, } ) return { "status": "ok", "root_cas_key": root_key, "extension": extension, "extension_configuration_guid": extension_configuration_guid, "declared_count": declared_count, "entry_count": len(entries), "entries": entries, } def live_extension_manifests(base_id: str, *, extension_guid: str | None = None, timeout_seconds: int = 60) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: rows, row_error = extension_zipped_info_rows(base_id, timeout_seconds=timeout_seconds) diagnostics: list[dict[str, Any]] = [] if row_error: return [], [{"status": row_error.get("status"), "diagnostics": row_error.get("diagnostics") or row_error}] manifests: list[dict[str, Any]] = [] for row in rows: if extension_guid and str(row.get("guid") or "").lower() != extension_guid.lower(): continue root_key = str(row.get("root_cas_key") or "").strip().lower() if not root_key: diagnostics.append({"extension": row.get("name"), "status": "missing_root_cas_key"}) continue cache_key = (base_id, root_key) with EXTENSION_MANIFEST_CACHE_LOCK: cached = EXTENSION_MANIFEST_CACHE.get(cache_key) if cached: manifests.append(cached) continue data, _, error = read_storage_file_bytes(base_id, "ConfigCAS", root_key, timeout_seconds=timeout_seconds) if error or data is None: diagnostics.append({"extension": row.get("name"), "root_cas_key": root_key, "status": "root_cas_missing", "diagnostics": (error or {}).get("diagnostics")}) continue try: manifest = extract_extension_manifest_from_root_payload(data, root_key=root_key, extension={key: row.get(key) for key in ("name", "guid", "order")}) except Exception as exc: diagnostics.append({"extension": row.get("name"), "root_cas_key": root_key, "status": "parse_error", "diagnostics": {"message": str(exc)}}) continue with EXTENSION_MANIFEST_CACHE_LOCK: EXTENSION_MANIFEST_CACHE[cache_key] = manifest manifests.append(manifest) return manifests, diagnostics def manifest_related_entries_for_cas_key( base_id: str, cas_key: str, *, extension_guid: str | None = None, timeout_seconds: int = 60, ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: manifests, diagnostics = live_extension_manifests(base_id, extension_guid=extension_guid, timeout_seconds=timeout_seconds) related: list[dict[str, Any]] = [] wanted = str(cas_key or "").strip().lower() for manifest in manifests: entries = [entry for entry in manifest.get("entries") or [] if isinstance(entry, dict)] bases = {str(entry.get("object_base_id") or "").lower() for entry in entries if str(entry.get("cas_key") or "").lower() == wanted} if not bases: continue for entry in entries: if str(entry.get("object_base_id") or "").lower() in bases: related.append( { **entry, "extension": manifest.get("extension"), "root_cas_key": manifest.get("root_cas_key"), } ) return related, diagnostics def extension_manifest_form_items_for_object( base_id: str, *, object_guid: str, object_file_name: str, extension_guid: str | None, timeout_seconds: int, ) -> tuple[list[tuple[dict[str, Any], str]], list[dict[str, Any]]]: diagnostics: list[dict[str, Any]] = [] data, _, read_error = read_storage_file_bytes(base_id, "ConfigCAS", object_file_name, timeout_seconds=timeout_seconds) if read_error or data is None: diagnostics.append({"status": "object_metadata_missing", "diagnostics": (read_error or {}).get("diagnostics")}) return [], diagnostics try: from parser.payload import decode_payload_lossless except Exception as exc: diagnostics.append({"status": "payload_decoder_unavailable", "diagnostics": {"message": str(exc)}}) return [], diagnostics object_text = str(decode_payload_lossless(data).get("text") or "") object_guids = {value.lower() for value in re.findall(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", object_text)} if not object_guids: return [], diagnostics manifests, manifest_diagnostics = live_extension_manifests(base_id, extension_guid=extension_guid, timeout_seconds=timeout_seconds) diagnostics.extend(manifest_diagnostics) form_items: list[tuple[dict[str, Any], str]] = [] seen: set[str] = set() for manifest in manifests: entries = [entry for entry in manifest.get("entries") or [] if isinstance(entry, dict)] if not any(str(entry.get("cas_key") or "").lower() == object_file_name.lower() for entry in entries): continue by_base: dict[str, list[dict[str, Any]]] = {} for entry in entries: by_base.setdefault(str(entry.get("object_base_id") or "").lower(), []).append(entry) descriptor_entries = [ entry for entry in entries if str(entry.get("suffix") or "") == "" and str(entry.get("object_base_id") or "").lower() in object_guids and str(entry.get("object_base_id") or "").lower() != str(object_guid or "").lower() ] for entry in descriptor_entries: form_guid = str(entry.get("object_base_id") or "").lower() if form_guid in seen: continue descriptor_key = str(entry.get("cas_key") or "").lower() descriptor_data, _, descriptor_error = read_storage_file_bytes(base_id, "ConfigCAS", descriptor_key, timeout_seconds=timeout_seconds) if descriptor_error or descriptor_data is None: diagnostics.append({"status": "form_descriptor_missing", "guid": form_guid, "diagnostics": (descriptor_error or {}).get("diagnostics")}) continue identity = config_identity_from_bytes(descriptor_data) or {} if not identity.get("name"): continue related_entries = [ { **related, "extension": manifest.get("extension"), "root_cas_key": manifest.get("root_cas_key"), } for related in by_base.get(form_guid, []) ] payload_entry = next((related for related in related_entries if str(related.get("suffix") or "") == ".0"), None) source_entry = payload_entry or {**entry, "extension": manifest.get("extension"), "root_cas_key": manifest.get("root_cas_key")} form_items.append( ( { "category": "Form", "guid": form_guid, "status": "ok", "identity": { "guid": form_guid, "name": identity.get("name"), "synonyms": identity.get("synonyms") or {}, }, "source": { "kind": "live_sql", "table": "ConfigCAS", "file_name": source_entry.get("cas_key"), }, "manifest_entries": related_entries, }, "extension_manifest_child_guid", ) ) seen.add(form_guid) return form_items, diagnostics def extension_manifest_object_matches( *, base_id: str, cache_config: dict[str, str] | None = None, query: str, kind_filter: str | None, guid_filter: str, extension_guid: str | None, limit: int, timeout_seconds: int, include_storage: bool, ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], dict[str, Any]]: manifests, diagnostics = live_extension_manifests(base_id, extension_guid=extension_guid, timeout_seconds=timeout_seconds) stats = {"manifest_count": len(manifests), "descriptor_entries": 0, "descriptor_payloads_read": 0} if not manifests or limit <= 0: return [], diagnostics, stats try: from parser.cas_payload import classify_payload except Exception as exc: diagnostics.append({"status": "classifier_unavailable", "diagnostics": {"message": str(exc)}}) return [], diagnostics, stats descriptor_rows: list[dict[str, Any]] = [] related_by_descriptor: dict[str, list[dict[str, Any]]] = {} for manifest in manifests: entries = [entry for entry in manifest.get("entries") or [] if isinstance(entry, dict)] by_base: dict[str, list[dict[str, Any]]] = {} for entry in entries: by_base.setdefault(str(entry.get("object_base_id") or "").lower(), []).append(entry) for entry in entries: if str(entry.get("suffix") or "") != "": continue descriptor = { **entry, "extension": manifest.get("extension"), "root_cas_key": manifest.get("root_cas_key"), } descriptor_rows.append(descriptor) related_by_descriptor[str(entry.get("cas_key") or "").lower()] = [ { **related, "extension": manifest.get("extension"), "root_cas_key": manifest.get("root_cas_key"), } for related in by_base.get(str(entry.get("object_base_id") or "").lower(), []) ] stats["descriptor_entries"] = len(descriptor_rows) if not descriptor_rows: return [], diagnostics, stats matches: list[dict[str, Any]] = [] seen: set[str] = set() for start in range(0, len(descriptor_rows), 120): if len(matches) >= limit: break chunk = descriptor_rows[start : start + 120] file_names = [str(row.get("cas_key") or "").lower() for row in chunk if str(row.get("cas_key") or "")] payloads, _, read_error = read_storage_files_bytes(base_id, "ConfigCAS", file_names, timeout_seconds=min(timeout_seconds, 60)) if read_error: diagnostics.append({"status": read_error.get("status"), "diagnostics": read_error.get("diagnostics")}) continue stats["descriptor_payloads_read"] += len(payloads or {}) for row in chunk: file_name = str(row.get("cas_key") or "").lower() if not file_name or file_name in seen: continue data = (payloads or {}).get(file_name) if not data: continue descriptor_payload_sha1 = hashlib.sha1(data).hexdigest() classification = classify_payload(data, include_text=False) identity = config_identity_from_bytes(data) or {} identity_guid = str(identity.get("guid") or file_name).strip().lower() object_base_id = str(row.get("object_base_id") or "").lower() if guid_filter and guid_filter not in {identity_guid, file_name, object_base_id}: continue related_entries = related_by_descriptor.get(file_name) or [row] object_kind = extension_metadata_payload_kind(data, identity, classification) if not object_kind and kind_filter and query and extension_object_match_by(identity, query, identity_guid): object_kind = kind_filter if kind_filter and object_kind != kind_filter: continue string_values = [str(value or "") for value in classification.get("strings_sample") or []] string_identity = dict(identity) if not string_identity.get("name"): for value in string_values: if query and normalized_contains_any(query, value): string_identity["name"] = best_text_variant(value) break match_by = extension_object_match_by(string_identity, query, identity_guid) content_match_by = None if query and not match_by: for value in string_values: if normalized_contains_any(query, value): content_match_by = "payload_string_contains" break match_by = match_by or content_match_by or ("guid_exact" if guid_filter else ("scan" if not query else None)) if query and not match_by: continue seen.add(file_name) extension = row.get("extension") if isinstance(row.get("extension"), dict) else {} route = { "route_type": "extension_manifest_cas", "table": "ConfigCAS", "file_name": file_name, "manifest_entry": { "object_id": row.get("object_id"), "object_base_id": row.get("object_base_id"), "suffix": row.get("suffix"), "cas_key": row.get("cas_key"), "extension": extension or None, "root_cas_key": row.get("root_cas_key"), }, "manifest_entries": len(related_entries), "payload_signature": { "role": classification.get("role"), "root": classification.get("root"), "markers": classification.get("markers") or [], "payload_bytes": classification.get("payload_bytes"), }, } match = { "kind": object_kind, "kind_ru": RU_KIND.get(object_kind or "", object_kind), "name": string_identity.get("name"), "synonym": next(iter((string_identity.get("synonyms") or {}).values()), None) if isinstance(string_identity.get("synonyms"), dict) else None, "guid": identity_guid, "match_by": match_by, "origin": { "source": "extension", "presentation": "Расширение", "extension": extension or None, "status": "ok" if extension.get("name") or not extension_guid else "extension_unresolved", }, "route": route if include_storage else {key: route.get(key) for key in ("route_type", "table", "file_name", "manifest_entries")}, "read_selectors": extension_object_read_selectors(base_id, object_kind, {**string_identity, "guid": identity_guid}, route), } if include_storage: match["manifest_entries"] = related_entries match["strings_sample"] = [best_text_variant(value) for value in string_values] extension_route_cache_upsert(cache_config, match, descriptor_payload_sha1=descriptor_payload_sha1, freshness_status="fresh") matches.append(match) if len(matches) >= limit: break return matches, diagnostics, stats def extension_object_kind_from_evidence(identity: dict[str, Any], classification: dict[str, Any], sources: list[dict[str, Any]]) -> str | None: for source in sources: role_kind = DBNAMES_ROLE_KIND.get(str(source.get("storage_role") or "")) if role_kind: return role_kind role = str(classification.get("role") or "") root = classification.get("root") if isinstance(classification.get("root"), dict) else {} root_marker = str(root.get("root_marker") or "") if role == "template_payload" or root_marker == "8": return "Template" if role == "form_payload" or root_marker == "4": return "Form" name = str(identity.get("name") or "") if name.casefold().startswith(("form.", "форма.")): return "Form" if name.casefold().startswith(("template.", "макет.")): return "Template" return None def extension_metadata_payload_kind(data: bytes, identity: dict[str, Any], classification: dict[str, Any]) -> str | None: """Classify SQL metadata descriptors by their stable brace-tree envelope. The signatures are learned from ConfigCASSave payloads and do not depend on XML at runtime. In particular, a calculation-register descriptor is a marker-1 root with ten items whose metadata block starts with 21. """ tree = parse_config_tree_from_bytes(data) if not isinstance(tree, dict) or tree.get("type") != "list": return extension_object_kind_from_evidence(identity, classification, []) items = tree.get("items") or [] root_marker = manifest_scalar(items[0]) if items else "" metadata_block = items[1] if len(items) > 1 and isinstance(items[1], dict) else {} metadata_items = metadata_block.get("items") or [] if isinstance(metadata_block, dict) else [] metadata_marker = manifest_scalar(metadata_items[0]) if metadata_items else "" signatures = { ("1", 2, "3"): "Template", ("1", 10, "21"): "CalculationRegister", ("1", 8, "35"): "ChartOfCalculationTypes", ("1", 8, "57"): "Catalog", ("1", 3, "4"): "CommonForm", ("1", 3, "0"): "Form", } return signatures.get((root_marker, len(items), metadata_marker)) or extension_object_kind_from_evidence(identity, classification, []) def extension_object_match_by(identity: dict[str, Any], query: str, guid: str) -> str | None: if not query: return "scan" if guid and normalize_exact(query) == normalize_exact(guid): return "guid_exact" return extension_definition_identity_match_by(identity, query) def extension_object_read_selectors(base_id: str, kind: str | None, identity: dict[str, Any], route: dict[str, Any]) -> dict[str, dict[str, Any]]: guid = str(identity.get("guid") or route.get("file_name") or "").strip().lower() name = identity.get("name") selectors: dict[str, dict[str, Any]] = { "route": { "method": "metadata.route.resolve", "base_id": base_id, "guid": guid, "table": route.get("table"), "file_name": route.get("file_name"), } } if kind: selectors["card"] = { "method": "metadata.object.get", "base_id": base_id, "kind": kind, "guid": guid, **({"name": name} if name else {}), "table": route.get("table"), **({"file_name": route.get("file_name")} if route.get("file_name") else {}), **({"extension_guid": route.get("extension_guid")} if route.get("extension_guid") else {}), } if kind == "Template": selectors["template_read"] = { "method": "templates.read", "base_id": base_id, "kind": "Template", "guid": guid, "file_name": route.get("file_name"), "table": route.get("table"), } elif kind in {"CommonForm", "Form"}: selectors["form_decode"] = { "method": "metadata.form.decode", "base_id": base_id, "kind": kind, "guid": guid, **({"name": name} if name else {}), "table": route.get("table"), "file_name": route.get("file_name"), } elif kind in KIND_CAPABILITIES: if "forms" in KIND_CAPABILITIES.get(kind, []): selectors["forms"] = { "method": "metadata.object.forms", "base_id": base_id, "kind": kind, "guid": guid, **({"name": name} if name else {}), "table": route.get("table"), } selectors["form_details"] = { "method": "metadata.object.form.details", "base_id": base_id, "kind": kind, "guid": guid, **({"name": name} if name else {}), "table": route.get("table"), } if "templates" in KIND_CAPABILITIES.get(kind, []): selectors["templates"] = { "method": "metadata.object.templates", "base_id": base_id, "kind": kind, "guid": guid, **({"name": name} if name else {}), "table": route.get("table"), } if "modules" in KIND_CAPABILITIES.get(kind, []): selectors["modules_search"] = { "method": "modules.search", "base_id": base_id, "kind": kind, "guid": guid, **({"name": name} if name else {}), "table": route.get("table"), } return selectors EXTENSION_OBJECTS_FIND_STATES = {"working", "active", "save", "both"} # Public configuration views deliberately describe 1C semantics, rather than # the SQL tables used to recover them. ``working`` has historically been the # adapter default, but callers should not have to know that this means a # Designer snapshot with saved changes preferred over the applied snapshot. CONFIGURATION_VIEW_TO_STATE = { "effective": "working", "effective_working": "working", "designer": "working", "working": "working", "runtime": "active", "runtime_applied": "active", "applied": "active", "compare": "both", "comparison": "both", "both": "both", "saved": "save", "save": "save", } CONFIGURATION_VIEW_BY_STATE = { "working": "effective_working", "active": "runtime_applied", "both": "compare", "save": "saved_only", } def normalize_configuration_view(payload: dict[str, Any], method: str) -> dict[str, Any]: """Normalize the agent-facing configuration view into the storage-neutral state. The result intentionally retains ``configuration_view`` for response annotation, but all lower-level readers receive only the existing state contract. Supplying contradictory ``state`` and ``configuration_view`` is rejected instead of silently choosing one physical layer. """ normalized = dict(payload) raw_view = normalized.get("configuration_view") view = str(raw_view or "").strip().casefold() explicit_state = str(normalized.get("state") or "").strip().casefold() if view: mapped_state = CONFIGURATION_VIEW_TO_STATE.get(view) if not mapped_state: return invalid_argument( method, "configuration_view", "Unsupported configuration view.", allowed_values=sorted(CONFIGURATION_VIEW_TO_STATE), ) if explicit_state and explicit_state != mapped_state: return invalid_argument( method, "configuration_view", "configuration_view conflicts with state.", ) normalized["state"] = mapped_state normalized["configuration_view"] = CONFIGURATION_VIEW_BY_STATE[mapped_state] else: state = explicit_state or "working" normalized["state"] = state normalized["configuration_view"] = CONFIGURATION_VIEW_BY_STATE.get(state, "effective_working") return normalized def configuration_view_details(view: str) -> dict[str, Any]: """Return the execution meaning of an agent-visible configuration view.""" if view == "runtime_applied": return { "name": view, "meaning": "Configuration currently applied to the infobase and executable now.", "extension_resolution": "applied_extension_composition", } if view == "compare": return { "name": view, "meaning": "Comparison of the effective working configuration and the currently applied configuration.", "extension_resolution": "both_compositions", } if view == "saved_only": return { "name": view, "meaning": "Only explicitly saved Designer changes; this is a diagnostic view, not a complete configuration.", "extension_resolution": "saved_changes_only", } return { "name": "effective_working", "meaning": "Effective Designer configuration: base configuration plus saved development changes and discovered extension layers. It becomes executable after configuration update; it is not claimed to be the currently applied runtime.", "extension_resolution": "working_extension_composition", } def annotate_configuration_view(result: dict[str, Any], view: str) -> dict[str, Any]: """Keep SQL implementation details out of ordinary agent-facing results.""" annotated = dict(result) annotated["configuration_view"] = configuration_view_details(view) return annotated def effective_extension_routine_scan(base_id: str, routine_name: str, *, canonical_object: str) -> list[dict[str, Any]]: """Inspect extensions in platform load order without claiming unproven ownership.""" extensions = sorted( (item for item in extension_map_by_guid(base_id).values() if item.get("active") is not False), key=lambda item: (int(item.get("order") or 0), str(item.get("guid") or "")), ) layers: list[dict[str, Any]] = [] for extension in extensions: found = search_modules({ "base_id": base_id, "extension": extension.get("guid"), "query": routine_name, "routine_name": routine_name, "state": "working", "limit": 20, "scan_limit": 1000, "include_storage": False, "timeout_seconds": 30, }) matches = [item for item in found.get("matches") or [] if isinstance(item, dict)] if isinstance(found, dict) else [] proven = 0 actions: list[dict[str, Any]] = [] for match in matches: selector = match.get("read_selector") if isinstance(match.get("read_selector"), dict) else {} _table, file_name, _stream = parse_module_id(str(selector.get("module_ref") or selector.get("module_id") or "")) form_match = _FORM_MODULE_FILE_RE.fullmatch(file_name or "") if not form_match: continue resolved, _error = repository_resolve_form_guid_sql( method="effective.execution_chain", base_id=base_id, form_guid=str(form_match.group("form") or "").lower(), timeout_seconds=30, ) if resolved and str(resolved.get("resolved_object") or "") == canonical_object: proven += 1 read_result = read_module({ "base_id": base_id, "module_ref": selector.get("module_ref") or selector.get("module_id"), "routine_name": routine_name, "include_storage": False, "include_text": True, "max_chars": 100000, }) action = metadata_extension_action_from_evidence( source="extension", method_name=routine_name, read_result=read_result, module={"module_ref": selector.get("module_ref") or selector.get("module_id")}, ) action["extension"] = {key: extension.get(key) for key in ("name", "guid", "order")} actions.append(action) layers.append({ "extension": {key: extension.get(key) for key in ("name", "guid", "order", "active")}, "status": found.get("status") if isinstance(found, dict) else "error", "routine_candidates": len(matches), "proven_candidates": proven, "applicability": "proven" if proven else "not_proven" if matches else "not_found", "extension_actions": actions, }) return layers def attach_effective_routine_chain(result: dict[str, Any], payload: dict[str, Any], *, view: str) -> dict[str, Any]: """Attach the discovered extension/action chain to an ordinary routine read. This keeps the follow-up override analysis inside the adapter contract: an agent reading a named routine must not infer extension replacement from SQL-origin rows by itself. The chain remains evidence-based; an unknown extension action is stated as unknown rather than guessed. """ routine_name = str(payload.get("routine_name") or "").strip() if view != "effective_working" or not routine_name or result.get("status") not in {"ok", "summary", "text"}: return result selector, _ = _code_query_object_selector(payload) if not selector.get("kind") and not selector.get("name") and not selector.get("guid"): owner = result.get("resolved_owner") if isinstance(result.get("resolved_owner"), dict) else {} canonical_object = str(owner.get("canonical_object") or "").strip() if canonical_object: extension_layers = effective_extension_routine_scan(str(payload.get("base_id") or ""), routine_name, canonical_object=canonical_object) extension_actions = [ action for layer in extension_layers for action in (layer.get("extension_actions") or []) if isinstance(action, dict) ] execution_steps = [{ "order": 1, "source": "effective_working", "target": canonical_object, "role": "resolved_form_module", }] for index, action in enumerate(sorted(extension_actions, key=lambda item: int((item.get("extension") or {}).get("order") or 0)), start=2): execution_steps.append({ "order": index, "source": "extension", "extension": action.get("extension"), "operation_class": action.get("operation_class"), "role": "routine_interceptor", }) enriched = dict(result) enriched["execution_chain"] = { "status": "resolved_entry", "routine": routine_name, "steps": execution_steps, "extension_actions": extension_actions, "extension_layers": extension_layers, "diagnostics": { "message": "The selected form module was resolved to its logical owner. Active extensions were scanned in load order; only SQL-proven form matches contribute routine actions." }, } return enriched return result try: chain = metadata_resolve_overrides( { "base_id": payload.get("base_id"), "object_type": selector.get("kind"), "object_name": selector.get("name"), "object_guid": selector.get("guid"), "method_name": routine_name, "state": "working", **({"extension": payload.get("extension")} if payload.get("extension") else {}), } ) except Exception: return result if not isinstance(chain, dict): return result enriched = dict(result) enriched["execution_chain"] = { "status": chain.get("status"), "routine": routine_name, "steps": chain.get("chain") or [], "extension_actions": chain.get("extension_actions") or [], "diagnostics": chain.get("diagnostics"), } return enriched def enrich_code_read_logical_owner(result: dict[str, Any], payload: dict[str, Any], *, base_id: str) -> dict[str, Any]: """Resolve an opaque form-module selector to a public owner for every read state.""" context = resolve_write_gate_context({**payload, "base_id": base_id}) resolution = context.get("owner_resolution") if isinstance(context.get("owner_resolution"), dict) else {} if resolution.get("status") != "resolved": return result enriched = dict(result) enriched["resolved_owner"] = { "canonical_object": resolution.get("repository_object"), "owner": resolution.get("owner"), "form_guid": resolution.get("form_guid"), "layer_id": resolution.get("layer_id"), } return enriched def extension_saved_state_metadata_matches( *, base_id: str, payload: dict[str, Any], query: str, kind_filter: str | None, guid_filter: str, extension_guid: str, active_guid_keys: set[str], limit: int, scan_limit: int, timeout_seconds: int, include_storage: bool, ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], dict[str, Any]]: stats = {"saved_state_scanned": 0, "saved_state_rows": 0} prefix = f"{extension_guid}__" files = storage_files_list( { "base_id": base_id, "table": "ConfigCASSave", "prefix": prefix, "limit": min(max(scan_limit, limit), 5000), "diagnostic": True, "timeout_seconds": timeout_seconds, "_internal": True, } ) if files.get("status") != "ok": return [], [{"area": "saved_state", "status": files.get("status"), "diagnostics": files.get("diagnostics") or files}], stats file_rows = [row for row in files.get("files") or [] if isinstance(row, dict)] stats["saved_state_rows"] = len(file_rows) descriptor_names: list[str] = [] for row in file_rows: file_name = str(row.get("FileName") or row.get("file_name") or "") suffix = file_name[len(prefix) :] if file_name.startswith(prefix) else file_name if not suffix or suffix == "configinfo" or "." in suffix: continue descriptor_names.append(file_name) extension = {"guid": extension_guid, **({"name": payload.get("extension")} if payload.get("extension") else {})} matches: list[dict[str, Any]] = [] for start in range(0, len(descriptor_names), 120): if len(matches) >= limit: break chunk = descriptor_names[start : start + 120] payloads, _config, read_error = read_storage_files_bytes(base_id, "ConfigCASSave", chunk, timeout_seconds=min(timeout_seconds, 60)) if read_error: return matches, [{"area": "saved_state", "status": read_error.get("status"), "diagnostics": read_error.get("diagnostics") or read_error}], stats for file_name in chunk: data = (payloads or {}).get(file_name) if not data: continue stats["saved_state_scanned"] += 1 identity = saved_state_descriptor_identity_from_bytes(data, file_name) or {} try: from parser.cas_payload import classify_payload classification = classify_payload(data, include_text=False) except Exception: classification = {} object_kind = extension_metadata_payload_kind(data, identity, classification) if kind_filter and object_kind != kind_filter: continue guid = str(identity.get("guid") or file_name.removeprefix(prefix)).strip().lower() if guid_filter and guid_filter not in {guid, file_name.lower()}: continue name = identity.get("name") synonym = identity.get("synonym") match_by = extension_object_match_by(identity, query, guid) if query and not match_by and not any(normalized_contains_any(query, value) for value in (name, synonym, guid, file_name)): continue if not object_kind: continue activation_state = "saved_override" if guid in active_guid_keys or file_name.lower() in active_guid_keys else "saved_only" route = { "route_type": "saved_state_metadata", "table": "ConfigCASSave", "file_name": file_name, "descriptor_file_name": file_name, "extension_guid": extension_guid, } match = { "kind": object_kind, "kind_ru": RU_KIND.get(object_kind, object_kind), "name": name, "synonym": synonym, "guid": guid, "match_by": match_by or "saved_state_descriptor", "activation_state": activation_state, "current_state": {"source": "saved_state", "activation_state": "not_activated"}, "origin": { "source": "extension_saved_state", "presentation": "Расширение (save)", "extension": extension, "status": activation_state, }, "route": route if include_storage else {key: route.get(key) for key in ("route_type", "table", "file_name")}, "read_selectors": extension_object_read_selectors(base_id, object_kind, {"guid": guid, "name": name}, route), "saved_state": { "table": "ConfigCASSave", "file_name": file_name, "descriptor_file_name": file_name, "activation_state": activation_state, }, } if include_storage: match["payload_signature"] = { "role": classification.get("role"), "root": classification.get("root"), "markers": classification.get("markers") or [], } matches.append(match) if len(matches) >= limit: break return matches, [], stats def extension_saved_state_object_matches( *, base_id: str, payload: dict[str, Any], query: str, kind_filter: str | None, guid_filter: str, extension_guid: str | None, active_guid_keys: set[str], limit: int, scan_limit: int, timeout_seconds: int, include_storage: bool, ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], dict[str, Any]]: stats = {"saved_state_scanned": 0, "saved_state_rows": 0} if not extension_guid or limit <= 0: return [], [], stats if kind_filter and kind_filter != "CommonForm": return extension_saved_state_metadata_matches( base_id=base_id, payload=payload, query=query, kind_filter=kind_filter, guid_filter=guid_filter, extension_guid=extension_guid, active_guid_keys=active_guid_keys, limit=limit, scan_limit=scan_limit, timeout_seconds=timeout_seconds, include_storage=include_storage, ) search_payload: dict[str, Any] = { "base_id": base_id, "tables": ["ConfigCASSave"], "prefix": f"{extension_guid}__", "limit": max(limit, 1), "scan_limit": min(max(scan_limit, limit), 5000), "timeout_seconds": timeout_seconds, "max_targets": 1, "include_storage": True, } if query: search_payload["form"] = query result = metadata_saved_state_forms_search(search_payload) if result.get("status") not in {"ok", "not_found"}: return [], [{"area": "saved_state", "status": result.get("status"), "diagnostics": result.get("diagnostics") or result}], stats counts = result.get("counts") if isinstance(result.get("counts"), dict) else {} stats["saved_state_scanned"] = int(counts.get("scanned") or 0) rows = [row for row in (result.get("forms") or []) if isinstance(row, dict)] stats["saved_state_rows"] = len(rows) extension = {"guid": extension_guid, **({"name": payload.get("extension")} if payload.get("extension") else {})} matches: list[dict[str, Any]] = [] seen: set[str] = set() for row in rows: file_name = str(row.get("file_name") or "") form = row.get("form") if isinstance(row.get("form"), dict) else {} identity = form.get("identity") if isinstance(form.get("identity"), dict) else {} guid = str(identity.get("guid") or form.get("guid") or "").strip().lower() if not guid and "__" in file_name: guid = file_name.split("__", 1)[1].removesuffix(".0").lower() if not guid: guid = file_name.lower() if guid_filter and guid_filter not in {guid, file_name.lower()}: continue name = first_non_empty_arg(identity, "name") or form.get("name") or row.get("name") synonym = first_non_empty_arg(identity, "synonym") or form.get("synonym") or row.get("synonym") if query and not any(normalized_contains_any(query, value) for value in (name, synonym, guid, file_name)): continue key = guid or file_name.lower() if key in seen: continue seen.add(key) route = { "route_type": "saved_state_form", "table": row.get("table") or "ConfigCASSave", "file_name": file_name, "descriptor_file_name": identity.get("descriptor_file_name"), } activation_state = "saved_override" if key in active_guid_keys or file_name.lower() in active_guid_keys else "saved_only" match = { "kind": "CommonForm", "kind_ru": RU_KIND.get("CommonForm", "ОбщаяФорма"), "name": name, "synonym": synonym, "guid": guid, "qualified_name": public_code_qualified_name(owner={"name": extension.get("name")}, form={"name": name}) or name, "display_name": public_code_qualified_name(owner={"name": extension.get("name")}, form={"name": name}) or name, "match_by": identity.get("source") or "saved_state_form", "activation_state": activation_state, "current_state": {"source": "saved_state", "activation_state": "not_activated"}, "origin": { "source": "extension_saved_state", "presentation": "Расширение (save)", "extension": extension, "status": activation_state, }, "route": route if include_storage else {key_name: route.get(key_name) for key_name in ("route_type", "table", "file_name")}, "read_selectors": extension_object_read_selectors(base_id, "CommonForm", {"guid": guid, "name": name}, route), "saved_state": { "table": row.get("table") or "ConfigCASSave", "file_name": file_name, "descriptor_file_name": identity.get("descriptor_file_name"), "activation_state": activation_state, }, } if include_storage: match["saved_state_row"] = row matches.append(match) if len(matches) >= limit: break return matches, [], stats def extension_objects_find(payload: dict[str, Any]) -> dict[str, Any]: method = "extension.objects.find" payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error query = str(first_non_empty_arg(payload, "query", "name_filter", "name", "object_name") or "").strip() extension_guid, extension_error = extension_filter_to_guid(base_id, str(payload.get("extension") or ""), method=method) if extension_error: return extension_error kind_filter = canonical_kind(str(payload.get("kind") or payload.get("object_type") or "")) guid_filter = str(payload.get("guid") or payload.get("object_guid") or "").strip().lower() limit, limit_error = parse_int_alias_argument(payload, "limit", "max_matches", method=method, default=50, minimum=1, maximum=500) if limit_error: return limit_error scan_limit, scan_limit_error = parse_int_argument(payload, "scan_limit", method=method, default=5000, minimum=1, maximum=20000) if scan_limit_error: return scan_limit_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=90, minimum=1) if timeout_error: return timeout_error include_storage, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error include_storage = bool(include_storage) use_cache_value, use_cache_error = strict_bool_argument(payload, "use_cache", method=method, default=True) if use_cache_error: return use_cache_error refresh_cache_value, refresh_cache_error = strict_bool_argument(payload, "refresh_cache", method=method, default=False) if refresh_cache_error: return refresh_cache_error full_scan_value, full_scan_error = strict_bool_argument(payload, "full_scan", method=method, default=False) if full_scan_error: return full_scan_error state = str(payload.get("state") or "working").strip().lower() if state not in EXTENSION_OBJECTS_FIND_STATES: return invalid_argument(method, "state", "Unsupported saved-state mode.", allowed_values=sorted(EXTENSION_OBJECTS_FIND_STATES)) use_cache = bool(use_cache_value) refresh_cache = bool(refresh_cache_value) full_scan = bool(full_scan_value) cache_ttl_seconds, cache_ttl_error = parse_int_argument(payload, "cache_ttl_seconds", method=method, default=300, minimum=0, maximum=86400) if cache_ttl_error: return cache_ttl_error cache_config, _ = sql_config_for_base(base_id) matches: list[dict[str, Any]] = [] diagnostics: list[dict[str, Any]] = [] scanned = 0 cache_rows = [] cache_stale = 0 if use_cache and not refresh_cache: cache_rows = extension_route_cache_lookup( cache_config, query=query, kind_filter=kind_filter, guid_filter=guid_filter, extension_guid=extension_guid, limit=int(limit or 50), ) for row in cache_rows: recent_freshness = extension_route_cache_recent_freshness(row, ttl_seconds=int(cache_ttl_seconds or 0)) if recent_freshness: matches.append(extension_route_cache_row_to_match(base_id, row, include_storage=include_storage, freshness=recent_freshness)) if len(matches) >= int(limit or 50): break continue validated_row, freshness = validate_extension_route_cache_row(base_id, cache_config, row, timeout_seconds=min(int(timeout_seconds or 90), 30)) if validated_row: matches.append(extension_route_cache_row_to_match(base_id, validated_row, include_storage=include_storage, freshness=freshness)) else: cache_stale += 1 if len(matches) >= int(limit or 50): break if cache_rows: diagnostics.append( { "area": "extension_route_cache", "cache": "hit" if matches else "stale_or_miss", "candidates": len(cache_rows), "validated": len(matches), "stale": cache_stale, } ) manifest_matches, manifest_diagnostics, manifest_stats = extension_manifest_object_matches( base_id=base_id, cache_config=cache_config, query=query, kind_filter=kind_filter, guid_filter=guid_filter, extension_guid=extension_guid, limit=max(0, int(limit or 50) - len(matches)), timeout_seconds=int(timeout_seconds or 90), include_storage=include_storage, ) if len(matches) < int(limit or 50) else ([], [], {"manifest_count": 0, "descriptor_entries": 0, "descriptor_payloads_read": 0}) matches.extend(manifest_matches) diagnostics.extend(manifest_diagnostics) file_names: list[str] = [] try: from parser.cas_payload import classify_payload except Exception as exc: return adapter_public_error(method, "classifier_unavailable", {"message": str(exc)}) should_scan_configcas = full_scan or refresh_cache or not extension_guid if len(matches) < int(limit or 50) and should_scan_configcas: guid_sources, source_error = extension_definition_guid_sources(base_id, timeout_seconds=int(timeout_seconds or 90)) if source_error: if not matches: return public_error_result(source_error, include_storage=include_storage, method=method) diagnostics.append({"area": "dbnames_ext", "status": source_error.get("status"), "diagnostics": source_error.get("diagnostics") or source_error}) guid_sources = {} files = storage_files_list({"base_id": base_id, "table": "ConfigCAS", "limit": int(scan_limit or 5000), "_internal": True, "timeout_seconds": int(timeout_seconds or 90)}) if files.get("status") != "ok": return public_error_result(files, include_storage=include_storage, method=method) file_rows = files.get("files") or [] file_names = [str(row.get("FileName") or "") for row in file_rows if str(row.get("FileName") or "")] seen_file_names = {str((match.get("route") or {}).get("file_name") or "").lower() for match in matches if isinstance(match.get("route"), dict)} for chunk_start in range(0, len(file_names), 120): if len(matches) >= int(limit or 50): break chunk = file_names[chunk_start : chunk_start + 120] payloads, config, read_error = read_storage_files_bytes(base_id, "ConfigCAS", chunk, timeout_seconds=min(int(timeout_seconds or 90), 60)) if read_error: diagnostics.append({"status": read_error.get("status"), "diagnostics": read_error.get("diagnostics")}) continue for file_name in chunk: if file_name.lower() in seen_file_names: continue data = (payloads or {}).get(file_name) if not data: continue scanned += 1 classification = classify_payload(data, include_text=False) identity = config_identity_from_bytes(data) or {} identity_guid = str(identity.get("guid") or file_name).strip().lower() if guid_filter and identity_guid != guid_filter and file_name.lower() != guid_filter: continue sources = [source for source in (guid_sources.get(identity_guid) or []) if extension_source_matches(source, extension_guid)] string_values = [str(value or "") for value in classification.get("strings_sample") or []] string_identity = dict(identity) if not string_identity.get("name"): for value in string_values: if query and normalized_contains_any(query, value): string_identity["name"] = best_text_variant(value) break content_match_by = None if query: for value in string_values: if normalized_contains_any(query, value): content_match_by = "payload_string_contains" break if extension_guid and not sources and not content_match_by and not guid_filter: continue object_kind = extension_object_kind_from_evidence(identity, classification, sources) or extension_metadata_payload_kind(data, identity, classification) if not object_kind and kind_filter and query and content_match_by: object_kind = kind_filter if kind_filter and object_kind != kind_filter: continue match_by = extension_object_match_by(string_identity, query, identity_guid) or content_match_by if query and not match_by: continue primary_source = sources[0] if sources else {"extension": {}, "storage_role": None} route = { "route_type": "configcas_payload", "table": "ConfigCAS", "file_name": file_name, "payload_signature": { "role": classification.get("role"), "root": classification.get("root"), "markers": classification.get("markers") or [], "payload_bytes": classification.get("payload_bytes"), "stream_blocks": (classification.get("counts") or {}).get("stream_blocks"), "base64_blocks": (classification.get("counts") or {}).get("base64_blocks"), }, } extension = primary_source.get("extension") if isinstance(primary_source.get("extension"), dict) else {} match = { "kind": object_kind, "kind_ru": RU_KIND.get(object_kind or "", object_kind), "name": string_identity.get("name"), "synonym": next(iter((string_identity.get("synonyms") or {}).values()), None) if isinstance(string_identity.get("synonyms"), dict) else None, "guid": identity_guid, "match_by": match_by, "origin": { "source": "extension", "presentation": "Расширение", "extension": extension or None, "status": "ok" if extension.get("name") or not extension_guid else "extension_unresolved", }, "route": route if include_storage else {key: route.get(key) for key in ("route_type", "table", "file_name")}, "read_selectors": extension_object_read_selectors(base_id, object_kind, {**string_identity, "guid": identity_guid}, route), } if include_storage: match["extension_sources"] = sources match["strings_sample"] = [best_text_variant(value) for value in classification.get("strings_sample") or []] matches.append(match) seen_file_names.add(file_name.lower()) if len(matches) >= int(limit or 50): break elif len(matches) < int(limit or 50) and extension_guid: diagnostics.append( { "area": "configcas_scan", "status": "skipped", "reason": "full_scan_disabled_for_extension", "message": "Skipped slow ConfigCAS payload scan for an extension-scoped query. Pass full_scan=true or refresh_cache=true to force deep discovery.", } ) deduped_active_matches: list[dict[str, Any]] = [] seen_active_keys: set[str] = set() for match in matches: route = match.get("route") if isinstance(match.get("route"), dict) else {} key = str(match.get("guid") or route.get("file_name") or f"{match.get('kind')}:{match.get('name')}").strip().lower() if key and key in seen_active_keys: continue if key: seen_active_keys.add(key) deduped_active_matches.append(match) matches = deduped_active_matches active_matches = list(matches) active_guid_keys = { str(value).strip().lower() for match in active_matches for value in ( match.get("guid"), (match.get("route") or {}).get("file_name") if isinstance(match.get("route"), dict) else None, ) if value } saved_state_matches: list[dict[str, Any]] = [] saved_state_stats: dict[str, Any] = {"saved_state_scanned": 0, "saved_state_rows": 0} if state in {"working", "save", "both"}: saved_state_matches, saved_state_diagnostics, saved_state_stats = extension_saved_state_object_matches( base_id=base_id, payload=payload, query=query, kind_filter=kind_filter, guid_filter=guid_filter, extension_guid=extension_guid, active_guid_keys=active_guid_keys, limit=int(limit or 50), scan_limit=int(scan_limit or 5000), timeout_seconds=int(timeout_seconds or 90), include_storage=include_storage, ) diagnostics.extend(saved_state_diagnostics) saved_guid_keys = { str(value).strip().lower() for match in saved_state_matches for value in ( match.get("guid"), (match.get("route") or {}).get("file_name") if isinstance(match.get("route"), dict) else None, ) if value } for match in active_matches: match.setdefault("activation_state", "active") if state == "save": matches = saved_state_matches[: int(limit or 50)] elif state == "working": matches = (saved_state_matches + [match for match in active_matches if str(match.get("guid") or "").strip().lower() not in saved_guid_keys])[: int(limit or 50)] elif state == "both": matches = (saved_state_matches + active_matches)[: int(limit or 50)] else: matches = active_matches[: int(limit or 50)] activation_state_counts: dict[str, int] = {} for match in matches: activation_state = str(match.get("activation_state") or "active") activation_state_counts[activation_state] = activation_state_counts.get(activation_state, 0) + 1 truncated = len(file_names) >= int(scan_limit or 5000) and len(active_matches) >= int(limit or 50) return { "schema": "onec_extension_objects_find.v1", "status": "ok" if matches else "not_found", **({"error": "not_found"} if not matches else {}), "base_id": base_id, "source": {"kind": "live_metadata"} if not include_storage else {"kind": "live_sql", "table": "ConfigCAS"}, "query": { "extension": payload.get("extension"), "extension_guid": extension_guid, "query": query or None, "kind": kind_filter or None, "guid": guid_filter or None, "limit": int(limit or 50), "scan_limit": int(scan_limit or 5000), "include_storage": include_storage, "use_cache": use_cache, "refresh_cache": refresh_cache, "full_scan": full_scan, "state": state, "cache_ttl_seconds": int(cache_ttl_seconds or 0), }, "objects": matches, "counts": { "matches": len(matches), "scanned_payloads": scanned, "manifest_matches": len(manifest_matches), "manifest_count": manifest_stats.get("manifest_count"), "manifest_descriptor_entries": manifest_stats.get("descriptor_entries"), "manifest_descriptor_payloads_read": manifest_stats.get("descriptor_payloads_read"), "saved_state_matches": len(saved_state_matches), "saved_state_scanned": saved_state_stats.get("saved_state_scanned"), "saved_state_rows": saved_state_stats.get("saved_state_rows"), "activation_state": activation_state_counts, "scan_limit": int(scan_limit or 5000), "truncated": truncated, "source_cache_candidates": len(cache_rows), "source_cache_validated": len([match for match in matches if match.get("match_by") == "source_cache"]), "source_cache_stale": cache_stale, "configcas_scan_skipped": bool(extension_guid and not should_scan_configcas and len(matches) < int(limit or 50)), }, "diagnostics": diagnostics or ( [ { "message": "Совпадений в ConfigCAS не найдено. Если объект является дочерним элементом без DBNames-записи, нужен manifest/CAS route index или экспорт XML расширения.", } ] if not matches else [] ), } def extension_cache_rebuild(payload: dict[str, Any]) -> dict[str, Any]: method = "extension.cache.rebuild" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error extension_guid, extension_error = extension_filter_to_guid(base_id, str(payload.get("extension") or ""), method=method) if extension_error: return extension_error kind_filter = canonical_kind(str(payload.get("kind") or payload.get("object_type") or "")) if (payload.get("kind") or payload.get("object_type")) else None max_items, max_items_error = parse_int_argument(payload, "max_items", method=method, default=5000, minimum=1, maximum=50000) if max_items_error: return max_items_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=180, minimum=1) if timeout_error: return timeout_error include_matches, include_matches_error = strict_bool_argument(payload, "include_matches", method=method, default=False) if include_matches_error: return include_matches_error cache_config, config_error = sql_config_for_base(base_id) if not cache_config: return public_error_result({"status": "source_missing", "diagnostics": config_error}, include_storage=False, method=method) matches, diagnostics, stats = extension_manifest_object_matches( base_id=base_id, cache_config=cache_config, query="", kind_filter=kind_filter, guid_filter="", extension_guid=extension_guid, limit=int(max_items or 5000), timeout_seconds=int(timeout_seconds or 180), include_storage=True, ) cached = 0 descriptor_keys = set() for match in matches: route = match.get("route") if isinstance(match.get("route"), dict) else {} descriptor_key = str(route.get("file_name") or "").lower() if descriptor_key and descriptor_key not in descriptor_keys: descriptor_keys.add(descriptor_key) cached += 1 result = { "schema": "onec_extension_cache_rebuild.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_manifest", "table": "ConfigCAS"}, "query": { "extension": payload.get("extension"), "extension_guid": extension_guid, "kind": kind_filter, "max_items": int(max_items or 5000), }, "counts": { "cached_routes": cached, "matches": len(matches), "manifest_count": stats.get("manifest_count"), "manifest_descriptor_entries": stats.get("descriptor_entries"), "manifest_descriptor_payloads_read": stats.get("descriptor_payloads_read"), }, "diagnostics": diagnostics, } if include_matches: result["objects"] = [ { "kind": match.get("kind"), "name": match.get("name"), "guid": match.get("guid"), "origin": match.get("origin"), "route": { key: (match.get("route") or {}).get(key) for key in ("route_type", "table", "file_name", "manifest_entries") }, "read_selectors": match.get("read_selectors"), } for match in matches[:200] ] return result def extension_cache_status(payload: dict[str, Any]) -> dict[str, Any]: method = "extension.cache.status" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error extension_guid, extension_error = extension_filter_to_guid(base_id, str(payload.get("extension") or ""), method=method) if extension_error: return extension_error kind_filter = canonical_kind(str(payload.get("kind") or payload.get("object_type") or "")) if (payload.get("kind") or payload.get("object_type")) else None include_entries, include_entries_error = strict_bool_argument(payload, "include_entries", method=method, default=False) if include_entries_error: return include_entries_error limit, limit_error = parse_int_argument(payload, "limit", method=method, default=50, minimum=1, maximum=1000) if limit_error: return limit_error cache_config, config_error = sql_config_for_base(base_id) if not cache_config: return public_error_result({"status": "source_missing", "diagnostics": config_error}, include_storage=False, method=method) clauses = ["server_key=?", "database_name=?"] params: list[Any] = [cache_server_key(cache_config), cache_database_name(cache_config)] if extension_guid: clauses.append("extension_guid=?") params.append(extension_guid) if kind_filter: clauses.append("object_kind=?") params.append(kind_filter) where_sql = " AND ".join(clauses) with cache_connection() as conn: summary_rows = conn.execute( f""" SELECT COALESCE(extension_guid, '') AS extension_guid, COALESCE(extension_name, '') AS extension_name, COALESCE(object_kind, '') AS object_kind, COUNT(*) AS total, SUM(CASE WHEN freshness_status='stale' THEN 1 ELSE 0 END) AS stale, SUM(CASE WHEN freshness_status!='stale' THEN 1 ELSE 0 END) AS usable, MIN(validated_at) AS oldest_validated_at, MAX(validated_at) AS newest_validated_at, MIN(updated_at) AS oldest_updated_at, MAX(updated_at) AS newest_updated_at FROM extension_route_cache WHERE {where_sql} GROUP BY extension_guid, extension_name, object_kind ORDER BY extension_name, object_kind """, params, ).fetchall() total_row = conn.execute( f""" SELECT COUNT(*) AS total, SUM(CASE WHEN freshness_status='stale' THEN 1 ELSE 0 END) AS stale, SUM(CASE WHEN freshness_status!='stale' THEN 1 ELSE 0 END) AS usable, MIN(validated_at) AS oldest_validated_at, MAX(validated_at) AS newest_validated_at FROM extension_route_cache WHERE {where_sql} """, params, ).fetchone() entry_rows = [] if include_entries: entry_rows = conn.execute( f""" SELECT descriptor_cas_key, object_kind, name, extension_guid, extension_name, freshness_status, stale_reason, validated_at, updated_at FROM extension_route_cache WHERE {where_sql} ORDER BY CASE WHEN freshness_status='stale' THEN 0 ELSE 1 END, COALESCE(validated_at, 0), extension_name, object_kind, normalized_name LIMIT ? """, (*params, int(limit or 50)), ).fetchall() groups = [ { "extension": {"guid": row["extension_guid"] or None, "name": row["extension_name"] or None}, "kind": row["object_kind"] or None, "counts": { "total": int(row["total"] or 0), "usable": int(row["usable"] or 0), "stale": int(row["stale"] or 0), }, "oldest_validated_at": row["oldest_validated_at"], "newest_validated_at": row["newest_validated_at"], "oldest_updated_at": row["oldest_updated_at"], "newest_updated_at": row["newest_updated_at"], } for row in summary_rows ] entries = [ { "descriptor_cas_key": row["descriptor_cas_key"], "kind": row["object_kind"], "name": row["name"], "extension": {"guid": row["extension_guid"], "name": row["extension_name"]}, "freshness_status": row["freshness_status"], "stale_reason": row["stale_reason"], "validated_at": row["validated_at"], "updated_at": row["updated_at"], } for row in entry_rows ] return { "schema": "onec_extension_cache_status.v1", "status": "ok", "base_id": base_id, "source": {"kind": "extension_route_cache"}, "query": { "extension": payload.get("extension"), "extension_guid": extension_guid, "kind": kind_filter, "include_entries": bool(include_entries), "limit": int(limit or 50), }, "counts": { "total": int((total_row or {})["total"] or 0) if total_row else 0, "usable": int((total_row or {})["usable"] or 0) if total_row else 0, "stale": int((total_row or {})["stale"] or 0) if total_row else 0, "groups": len(groups), }, "oldest_validated_at": (total_row or {})["oldest_validated_at"] if total_row else None, "newest_validated_at": (total_row or {})["newest_validated_at"] if total_row else None, "groups": groups, **({"entries": entries} if include_entries else {}), } def extension_cache_validate(payload: dict[str, Any]) -> dict[str, Any]: method = "extension.cache.validate" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error extension_guid, extension_error = extension_filter_to_guid(base_id, str(payload.get("extension") or ""), method=method) if extension_error: return extension_error kind_filter = canonical_kind(str(payload.get("kind") or payload.get("object_type") or "")) if (payload.get("kind") or payload.get("object_type")) else None limit, limit_error = parse_int_argument(payload, "limit", method=method, default=1000, minimum=1, maximum=50000) if limit_error: return limit_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=120, minimum=1) if timeout_error: return timeout_error include_entries, include_entries_error = strict_bool_argument(payload, "include_entries", method=method, default=False) if include_entries_error: return include_entries_error cache_config, config_error = sql_config_for_base(base_id) if not cache_config: return public_error_result({"status": "source_missing", "diagnostics": config_error}, include_storage=False, method=method) clauses = ["server_key=?", "database_name=?"] params: list[Any] = [cache_server_key(cache_config), cache_database_name(cache_config)] if extension_guid: clauses.append("extension_guid=?") params.append(extension_guid) if kind_filter: clauses.append("object_kind=?") params.append(kind_filter) with cache_connection() as conn: rows = conn.execute( f""" SELECT * FROM extension_route_cache WHERE {' AND '.join(clauses)} ORDER BY extension_name, object_kind, normalized_name, descriptor_cas_key LIMIT ? """, (*params, int(limit or 1000)), ).fetchall() fresh = 0 stale = 0 checked_entries: list[dict[str, Any]] = [] diagnostics: list[dict[str, Any]] = [] manifests, manifest_diagnostics = live_extension_manifests(base_id, extension_guid=extension_guid, timeout_seconds=int(timeout_seconds or 120)) current_by_key: dict[str, dict[str, Any]] = {} current_by_extension_key: dict[tuple[str, str], dict[str, Any]] = {} for manifest in manifests: manifest_extension = manifest.get("extension") if isinstance(manifest.get("extension"), dict) else {} manifest_extension_guid = str((manifest_extension or {}).get("guid") or "").lower() root_cas_key = str(manifest.get("root_cas_key") or "").lower() for entry in manifest.get("entries") or []: if not isinstance(entry, dict): continue key = str(entry.get("cas_key") or "").lower() if not key: continue freshness_entry = { "status": "fresh", "validated_by": "live_manifest_batch", "root_cas_key": root_cas_key, "extension_guid": manifest_extension_guid or None, } current_by_key.setdefault(key, freshness_entry) if manifest_extension_guid: current_by_extension_key[(manifest_extension_guid, key)] = freshness_entry now = time.time() stale_updates: list[tuple[str, str]] = [] fresh_updates: list[tuple[float, str, str, str, str]] = [] for row_obj in rows: row = dict(row_obj) descriptor_key = str(row.get("descriptor_cas_key") or "").lower() row_extension_guid = str(row.get("extension_guid") or "").lower() freshness = current_by_extension_key.get((row_extension_guid, descriptor_key)) if row_extension_guid else None if not freshness: freshness = current_by_key.get(descriptor_key) if freshness: freshness = { **freshness, "root_changed": bool(row.get("root_cas_key") and freshness.get("root_cas_key") and str(row.get("root_cas_key")).lower() != str(freshness.get("root_cas_key")).lower()), } fresh_updates.append((now, str(freshness.get("root_cas_key") or row.get("root_cas_key") or ""), cache_server_key(cache_config), cache_database_name(cache_config), descriptor_key)) fresh += 1 else: stale += 1 reason = "descriptor_not_present_in_current_manifest" freshness = { "status": "stale", "validated_by": "live_manifest_batch", "reason": reason, } stale_updates.append((reason, descriptor_key)) if include_entries: checked_entries.append( { "descriptor_cas_key": row.get("descriptor_cas_key"), "kind": row.get("object_kind"), "name": row.get("name"), "extension": {"guid": row.get("extension_guid"), "name": row.get("extension_name")}, "freshness": freshness, } ) with cache_connection() as conn: for validated_at, root_cas_key, server_key, database_name, descriptor_key in fresh_updates: conn.execute( """ UPDATE extension_route_cache SET freshness_status='fresh', validated_at=?, last_seen_at=?, root_cas_key=?, stale_reason=NULL WHERE server_key=? AND database_name=? AND descriptor_cas_key=? """, (validated_at, validated_at, root_cas_key, server_key, database_name, descriptor_key), ) for reason, descriptor_key in stale_updates: conn.execute( """ UPDATE extension_route_cache SET freshness_status='stale', stale_reason=?, validated_at=?, last_seen_at=? WHERE server_key=? AND database_name=? AND descriptor_cas_key=? """, (reason, now, now, cache_server_key(cache_config), cache_database_name(cache_config), descriptor_key), ) diagnostics.extend(manifest_diagnostics) return { "schema": "onec_extension_cache_validate.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_manifest", "cache": "extension_route_cache"}, "query": { "extension": payload.get("extension"), "extension_guid": extension_guid, "kind": kind_filter, "limit": int(limit or 1000), }, "counts": { "checked": len(rows), "fresh": fresh, "stale": stale, }, **({"entries": checked_entries} if include_entries else {}), "diagnostics": diagnostics, } def metadata_route_resolve(payload: dict[str, Any]) -> dict[str, Any]: method = "metadata.route.resolve" find_payload = dict(payload) if not first_non_empty_arg(find_payload, "query", "name_filter", "name", "object_name") and first_non_empty_arg(find_payload, "guid", "object_guid"): find_payload["query"] = str(first_non_empty_arg(find_payload, "guid", "object_guid") or "") result = extension_objects_find({**find_payload, "include_storage": True}) if result.get("status") not in {"ok", "not_found"}: result["method"] = method return result routes = [] for item in result.get("objects") or []: routes.append( { "kind": item.get("kind"), "name": item.get("name"), "guid": item.get("guid"), "origin": item.get("origin"), "route": item.get("route"), "read_selectors": item.get("read_selectors"), } ) return { "schema": "onec_metadata_route_resolve.v1", "status": "ok" if routes else "not_found", **({"error": "not_found"} if not routes else {}), "base_id": result.get("base_id"), "source": result.get("source"), "query": result.get("query"), "routes": routes, "counts": {"routes": len(routes), **(result.get("counts") or {})}, "diagnostics": result.get("diagnostics") or [], } def parse_definition_find_areas(payload: dict[str, Any]) -> tuple[list[str], dict[str, Any] | None]: raw = payload.get("areas", payload.get("scope")) if raw is None: return list(DEFINITION_FIND_DEFAULT_AREAS), None if isinstance(raw, str): if not raw.strip(): return [], invalid_argument("metadata.definition.find", "areas", "areas must not be empty.", allowed_values=sorted(DEFINITION_FIND_AREAS | {"all"})) values = [part.strip() for part in raw.split(",") if part.strip()] elif isinstance(raw, list): values = raw else: return [], invalid_argument("metadata.definition.find", "areas", "areas must be a JSON string or array of strings.", allowed_values=sorted(DEFINITION_FIND_AREAS | {"all"})) areas: list[str] = [] for value in values: if not isinstance(value, str): return [], invalid_argument("metadata.definition.find", "areas", "areas items must be JSON strings.", allowed_values=sorted(DEFINITION_FIND_AREAS | {"all"})) normalized = value.strip().casefold() aliases = { "all": "all", "metadata": "metadata", "configuration": "metadata", "config": "metadata", "objects": "metadata", "метаданные": "metadata", "конфигурация": "metadata", "объекты": "metadata", "object": "object", "attributes": "object", "requisites": "object", "form": "form", "forms": "form", "commands": "commands", "command": "commands", "templates": "templates", "template": "templates", "makets": "templates", "макеты": "templates", "modules": "modules", "module": "modules", "bsl": "modules", "extensions": "extensions", "extension": "extensions", "расширения": "extensions", "расширение": "extensions", } area = aliases.get(normalized) if not area or (area != "all" and area not in DEFINITION_FIND_AREAS): return [], invalid_argument("metadata.definition.find", "areas", f"Unsupported area `{value}`.", allowed_values=sorted(DEFINITION_FIND_AREAS | {"all"})) if area == "all": return list(DEFINITION_FIND_DEFAULT_AREAS), None if area not in areas: areas.append(area) if not areas: return [], invalid_argument("metadata.definition.find", "areas", "areas must not be empty.", allowed_values=sorted(DEFINITION_FIND_AREAS | {"all"})) return areas, None def metadata_definition_find(payload: dict[str, Any]) -> dict[str, Any]: method = "metadata.definition.find" normalized_payload = normalize_object_selector_aliases(payload, method) if isinstance(normalized_payload, dict) and normalized_payload.get("status") == "invalid_argument": return normalized_payload payload = normalized_payload base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error extension_guid = str(payload.get("extension_guid") or "").strip().lower() if "extension_guid" in payload and not is_guid_text(extension_guid): return invalid_argument(method, "extension_guid", "extension_guid must be a GUID string.") query_value, query_error = optional_string_filter(payload, ["query", "definition", "identifier", "field", "requisite", "name_filter"], method=method) if query_error: return query_error if not query_value or not str(query_value).strip(): return invalid_argument(method, "query", "query must be a non-empty JSON string.") areas_explicit = "areas" in payload or "scope" in payload definition_path = parse_1c_object_path(query_value) inferred_path_area: str | None = None if definition_path.get("recognized_root") and definition_path.get("kind") and definition_path.get("name"): member_path = list(definition_path.get("member_path") or []) if member_path: payload = dict(payload) payload.setdefault("kind", definition_path["kind"]) payload.setdefault("name", definition_path["name"]) query_value = member_path[-1] section = normalize(member_path[0]) if member_path else "" if section in { normalize("Реквизиты"), normalize("Измерения"), normalize("Ресурсы"), normalize("ТабличныеЧасти"), "attributes", "dimensions", "resources", "tabularsections", }: inferred_path_area = "object" elif section in {normalize("Формы"), "forms"}: inferred_path_area = "form" elif section in {normalize("Команды"), "commands"}: inferred_path_area = "commands" elif section in {normalize("Макеты"), "templates"}: inferred_path_area = "templates" elif section in { normalize("Модули"), normalize("МодульОбъекта"), normalize("МодульМенеджера"), normalize("МодульНабораЗаписей"), "modules", "objectmodule", "managermodule", "recordsetmodule", } or definition_path.get("kind") in {"CommonModule", "CommonForm"}: inferred_path_area = "modules" include_storage, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error include_storage = bool(include_storage) areas, areas_error = parse_definition_find_areas(payload) if areas_error: return areas_error has_context_selector = bool(payload.get("guid") or payload.get("name") or payload.get("ref") or first_non_empty_arg(payload, "ordinal", "index", "object_index") not in {None, ""}) if not areas_explicit and has_context_selector: if inferred_path_area: areas = [inferred_path_area] elif payload.get("form") or payload.get("form_guid") or payload.get("file_name"): areas = ["form"] else: areas = ["object", "form", "commands", "templates", "modules", "extensions"] exact_only, exact_only_error = strict_bool_argument(payload, "exact_only", method=method, default=False) if exact_only_error: return exact_only_error refresh_cache, refresh_cache_error = strict_bool_argument(payload, "refresh_cache", method=method, default=False) if refresh_cache_error: return refresh_cache_error use_cache, use_cache_error = strict_bool_argument(payload, "use_cache", method=method, default=False) if use_cache_error: return use_cache_error timeout_value, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=90, minimum=1) if timeout_error: return timeout_error max_items, max_items_error = parse_int_argument(payload, "max_items", method=method, default=5000, minimum=1, maximum=5000) if max_items_error: return max_items_error max_matches, max_matches_error = parse_int_argument(payload, "max_matches", method=method, default=50, minimum=1, maximum=500) if max_matches_error: return max_matches_error table_or_error = metadata_storage_table(payload, method) if isinstance(table_or_error, dict): return table_or_error table = str(payload.get("table") or "Config") if not payload.get("guid") and not payload.get("name") and not (set(areas) & {"metadata", "extensions"}): return invalid_argument(method, "name", OBJECT_SELECTOR_GLOBAL_REQUIRED_MESSAGE) object_result: dict[str, Any] = {"source": {"kind": "live_metadata"}} object_card: dict[str, Any] = {} if payload.get("guid") or payload.get("name"): object_result = get_object( payload.get("kind"), str(payload.get("name") or payload.get("guid") or ""), base_id=base_id, view=str(payload.get("view") or "effective"), limit=int(payload.get("limit") or 20), include_storage=include_storage, include_semantic=False, table=table, extension_guid=extension_guid or None, timeout_seconds=int(timeout_value or 90), ) if object_result.get("status") != "ok": result = dict(object_result) result["method"] = method return result object_card = object_result.get("object") or {} matches: list[dict[str, Any]] = [] diagnostics: list[dict[str, Any]] = [] def append_match(item: dict[str, Any] | None) -> None: if not item: return if exact_only and not str(item.get("match_by") or "").endswith("_exact"): return if len(matches) < int(max_matches or 50): matches.append(item) def object_card_identity_match_by() -> str | None: if not object_card or "metadata" not in areas: return None query_text = str(query_value or "").strip() query_exact = normalize_exact(query_text) object_guid = str(object_card.get("guid") or "").strip().casefold() object_name = str(object_card.get("name") or "").strip() object_kind = str(object_card.get("kind") or "").strip() object_kind_ru = str(object_card.get("kind_ru") or RU_KIND.get(object_kind, object_kind) or "").strip() public_ref = object_selector_ref(object_kind, object_name) ru_ref = ".".join(part for part in [object_kind_ru, object_name] if part) if object_guid and query_text.casefold() == object_guid: return "guid_exact" if object_name and query_exact == normalize_exact(object_name): return "name_exact" if public_ref and query_exact == normalize_exact(public_ref): return "ref_exact" if ru_ref and query_exact == normalize_exact(ru_ref): return "ref_exact" return None def append_object_card_metadata_match(match_by: str | None) -> None: if not object_card or not match_by or any(item.get("area") == "metadata" for item in matches): return kind = str(object_card.get("kind") or "") kind_ru = object_card.get("kind_ru") or RU_KIND.get(kind, kind or "ОбъектМетаданных") append_match( { "area": "metadata", "kind": kind_ru, "name": object_card.get("name"), "synonym": object_card.get("synonym"), "guid": object_card.get("guid"), "match_by": match_by, "location": { "presentation": ".".join(part for part in [kind_ru, object_card.get("name")] if part), "section": "Объекты метаданных", }, "origin": {"source": "configuration", "presentation": "Конфигурация", "extension": None, "status": "ok"}, "read_selector": definition_read_selector(base_id, object_card, method="metadata.object.get"), "object": { "kind": object_card.get("kind"), "kind_ru": kind_ru, "name": object_card.get("name"), "synonym": object_card.get("synonym"), "guid": object_card.get("guid"), }, "related_selectors": object_related_selectors(base_id, object_card), } ) identity_match_by = object_card_identity_match_by() append_object_card_metadata_match(identity_match_by) skip_global_metadata_scan = bool(identity_match_by and object_card and (exact_only or set(areas) == {"metadata"})) if "metadata" in areas and not skip_global_metadata_scan: metadata_matches, metadata_diagnostics = metadata_configuration_definition_matches( base_id=base_id, query=str(query_value), max_matches=max(0, int(max_matches or 50) - len(matches)), use_cache=bool(use_cache), table=table, ) for item in metadata_matches: append_match(item) diagnostics.extend(metadata_diagnostics) if object_card and "object" in areas: attributes_result = metadata_object_attributes( { **payload, "base_id": base_id, "guid": object_card.get("guid"), "kind": object_card.get("kind"), "name": object_card.get("name"), "only": "all", "include_storage": include_storage, "timeout_seconds": int(timeout_value or 90), } ) if attributes_result.get("status") == "ok": for item in attributes_result.get("dimensions") or []: append_match( definition_match( query=str(query_value), area="object", kind_ru="Измерение", location={"presentation": f"{object_card.get('kind_ru')}.{object_card.get('name')}.Измерения.{item.get('name')}", "section": "Измерения"}, item=item, object_card=object_card, read_selector=definition_read_selector(base_id, object_card, method="metadata.object.attributes", only="dimensions"), ) ) for item in attributes_result.get("resources") or []: append_match( definition_match( query=str(query_value), area="object", kind_ru="Ресурс", location={"presentation": f"{object_card.get('kind_ru')}.{object_card.get('name')}.Ресурсы.{item.get('name')}", "section": "Ресурсы"}, item=item, object_card=object_card, read_selector=definition_read_selector(base_id, object_card, method="metadata.object.attributes", only="resources"), ) ) for item in attributes_result.get("attributes") or []: append_match( definition_match( query=str(query_value), area="object", kind_ru="Реквизит объекта", location={"presentation": f"{object_card.get('kind_ru')}.{object_card.get('name')}.Реквизиты.{item.get('name')}", "section": "Реквизиты"}, item=item, object_card=object_card, read_selector=definition_read_selector(base_id, object_card, method="metadata.object.attributes", only="attributes"), ) ) for tabular_section in attributes_result.get("tabular_sections") or []: append_match( definition_match( query=str(query_value), area="object", kind_ru="Табличная часть", location={"presentation": f"{object_card.get('kind_ru')}.{object_card.get('name')}.ТабличныеЧасти.{tabular_section.get('name')}", "section": "Табличные части"}, item=tabular_section, object_card=object_card, read_selector=definition_read_selector(base_id, object_card, method="metadata.object.attributes", only="tabular_sections"), ) ) for column in tabular_section.get("columns") or []: append_match( definition_match( query=str(query_value), area="object", kind_ru="Реквизит табличной части", location={ "presentation": f"{object_card.get('kind_ru')}.{object_card.get('name')}.ТабличныеЧасти.{tabular_section.get('name')}.{column.get('name')}", "section": "Табличные части", "tabular_section": tabular_section.get("name"), }, item=column, object_card=object_card, read_selector=definition_read_selector(base_id, object_card, method="metadata.object.attributes", only="tabular_sections"), ) ) else: diagnostics.append({"area": "object", "status": attributes_result.get("status"), "diagnostics": attributes_result.get("diagnostics")}) if object_card and "form" in areas: form_payload = { **payload, "base_id": base_id, "guid": object_card.get("guid"), "kind": object_card.get("kind"), "name": object_card.get("name"), "include_storage": include_storage, "max_forms": 20, "max_items": int(max_items or 5000), "max_attributes": int(max_items or 5000), "max_commands": int(max_items or 5000), "include_module_text": False, "timeout_seconds": int(timeout_value or 90), } forms_result = metadata_object_form_details(form_payload) if forms_result.get("status") == "ok": for form in forms_result.get("forms") or []: form_name = form.get("name") form_selector = definition_read_selector(base_id, object_card, method="metadata.object.form.details", form=form_name) for item in form.get("attributes") or []: append_match( definition_match( query=str(query_value), area="form", kind_ru="Реквизит формы", location={"presentation": f"{object_card.get('kind_ru')}.{object_card.get('name')}.Формы.{form_name}.Реквизиты.{item.get('name')}", "section": "Реквизиты формы"}, item=item, object_card=object_card, form_card=form, read_selector=form_selector, ) ) for item in form.get("elements") or []: append_match( definition_match( query=str(query_value), area="form", kind_ru="Элемент формы", location={"presentation": f"{object_card.get('kind_ru')}.{object_card.get('name')}.Формы.{form_name}.Элементы.{item.get('name')}", "section": "Элементы формы"}, item=item, object_card=object_card, form_card=form, read_selector=form_selector, ) ) for item in form.get("commands") or []: append_match( definition_match( query=str(query_value), area="form", kind_ru="Команда формы", location={"presentation": f"{object_card.get('kind_ru')}.{object_card.get('name')}.Формы.{form_name}.Команды.{item.get('name')}", "section": "Команды формы"}, item=item, object_card=object_card, form_card=form, read_selector=form_selector, ) ) for item in form.get("events") or []: append_match( definition_match( query=str(query_value), area="form", kind_ru="Событие формы", location={"presentation": f"{object_card.get('kind_ru')}.{object_card.get('name')}.Формы.{form_name}.События.{item.get('event_name')}", "section": "События формы"}, item=item, object_card=object_card, form_card=form, read_selector=form_selector, ) ) module = form.get("module") if isinstance(form.get("module"), dict) else {} for routine in module.get("routines_sample") or []: routine_kind = str(routine.get("kind") or "Процедура/Функция") append_match( definition_match( query=str(query_value), area="form", kind_ru=f"{routine_kind} формы", location={ "presentation": f"{object_card.get('kind_ru')}.{object_card.get('name')}.Формы.{form_name}.Модуль.{routine.get('name')}", "section": "Модуль формы", "module": "Модуль формы", }, item={"name": routine.get("name"), "title": routine_kind}, object_card=object_card, form_card=form, read_selector=form_selector, ) ) elif forms_result.get("status") != "not_found": diagnostics.append({"area": "form", "status": forms_result.get("status"), "diagnostics": forms_result.get("diagnostics")}) if object_card and "commands" in areas: commands_result = metadata_object_commands( { **payload, "base_id": base_id, "guid": object_card.get("guid"), "kind": object_card.get("kind"), "name": object_card.get("name"), "include_storage": include_storage, "max_commands": int(max_items or 5000), "timeout_seconds": int(timeout_value or 90), } ) if commands_result.get("status") == "ok": for item in commands_result.get("commands") or []: append_match( definition_match( query=str(query_value), area="commands", kind_ru="Команда объекта", location={"presentation": f"{object_card.get('kind_ru')}.{object_card.get('name')}.Команды.{item.get('name')}", "section": "Команды"}, item=item, object_card=object_card, read_selector=definition_read_selector(base_id, object_card, method="metadata.object.commands", command=item.get("name")), ) ) else: diagnostics.append({"area": "commands", "status": commands_result.get("status"), "diagnostics": commands_result.get("diagnostics")}) if object_card and "templates" in areas: templates_result = metadata_object_templates( { **payload, "base_id": base_id, "guid": object_card.get("guid"), "kind": object_card.get("kind"), "include_storage": include_storage, "timeout_seconds": int(timeout_value or 90), } ) if templates_result.get("status") == "ok": for item in templates_result.get("templates") or []: append_match( definition_match( query=str(query_value), area="templates", kind_ru="Макет", location={"presentation": f"{object_card.get('kind_ru')}.{object_card.get('name')}.Макеты.{item.get('name')}", "section": "Макеты"}, item=item, object_card=object_card, read_selector=definition_read_selector(base_id, object_card, method="metadata.object.template.details", template=item.get("name")), ) ) else: diagnostics.append({"area": "templates", "status": templates_result.get("status"), "diagnostics": templates_result.get("diagnostics")}) if object_card and "modules" in areas: modules_result = metadata_object_modules( { **payload, "base_id": base_id, "guid": object_card.get("guid"), "kind": object_card.get("kind"), "include_storage": include_storage, "timeout_seconds": int(timeout_value or 90), } ) if modules_result.get("status") == "ok": for module in modules_result.get("modules") or []: module_ordinal = module.get("module_ordinal") read_result = read_module( { "base_id": base_id, "guid": object_card.get("guid"), "kind": object_card.get("kind"), "name": object_card.get("name"), "module_ordinal": module_ordinal, "mode": "summary", "include_storage": include_storage, "timeout_seconds": int(timeout_value or 90), } ) if read_result.get("status") != "ok": diagnostics.append({"area": "modules", "module": module.get("name"), "status": read_result.get("status"), "diagnostics": read_result.get("diagnostics")}) continue for routine in read_result.get("routines") or []: item = {"name": routine.get("name"), "title": routine.get("kind")} append_match( definition_match( query=str(query_value), area="modules", kind_ru=str(routine.get("kind") or "Процедура/Функция"), location={"presentation": f"{object_card.get('kind_ru')}.{object_card.get('name')}.Модули.{module.get('name')}.{routine.get('name')}", "section": "Модули", "module": module.get("name")}, item=item, object_card=object_card, read_selector=definition_read_selector(base_id, object_card, method="modules.read", module_ordinal=module_ordinal, routine_name=routine.get("name")), ) ) command_modules_result = metadata_object_commands( { **payload, "base_id": base_id, "guid": object_card.get("guid"), "kind": object_card.get("kind"), "include_form_commands": False, "include_storage": False, "timeout_seconds": int(timeout_value or 90), } ) if command_modules_result.get("status") == "ok": for command in command_modules_result.get("object_commands") or []: command_selector = dict(command.get("read_selector") or {}) if not command_selector: continue command_selector.pop("method", None) read_result = read_module( { **command_selector, "mode": "summary", "include_storage": False, "timeout_seconds": int(timeout_value or 90), } ) if read_result.get("status") != "ok": diagnostics.append( { "area": "modules", "command": command.get("name"), "status": read_result.get("status"), "diagnostics": read_result.get("diagnostics"), } ) continue for routine in read_result.get("routines") or []: routine_item = { "name": routine.get("name"), "title": routine.get("kind"), "origin": { "source": "extension", "presentation": "Расширение", "extension": {"guid": extension_guid} if extension_guid else None, "status": "ok" if extension_guid else "extension_unresolved", }, } routine_selector = { **(command.get("read_selector") or {}), "routine_name": routine.get("name"), } append_match( definition_match( query=str(query_value), area="modules", kind_ru=str(routine.get("kind") or "Процедура/Функция"), location={ "presentation": f"{object_card.get('kind_ru')}.{object_card.get('name')}.Команды.{command.get('name')}.Модуль.{routine.get('name')}", "section": "Модули команд", "module": "Модуль команды", "command": command.get("name"), }, item=routine_item, object_card=object_card, read_selector=routine_selector, ) ) elif command_modules_result.get("status") != "not_found": diagnostics.append( { "area": "modules", "section": "command_modules", "status": command_modules_result.get("status"), "diagnostics": command_modules_result.get("diagnostics"), } ) else: diagnostics.append({"area": "modules", "status": modules_result.get("status"), "diagnostics": modules_result.get("diagnostics")}) if "extensions" in areas: extension_matches, extension_diagnostics = metadata_extension_definition_matches( base_id=base_id, query=str(query_value), max_files=min(int(max_items or 5000), 5000), max_matches=max(0, int(max_matches or 50) - len(matches)), timeout_seconds=int(timeout_value or 90), include_storage=include_storage, refresh_cache=bool(refresh_cache), use_cache=bool(use_cache), ) for item in extension_matches: append_match(item) diagnostics.extend(extension_diagnostics) # Extension route cache also contains forms and other CAS descriptors # that are not present in the older DBNames-derived definition index. route_result = extension_objects_find({ "base_id": base_id, "query": str(query_value), "state": "working", "limit": max(1, int(max_matches or 50) - len(matches)), "scan_limit": min(int(max_items or 5000), 5000), "include_storage": include_storage, "use_cache": bool(use_cache), "refresh_cache": bool(refresh_cache), "full_scan": bool(refresh_cache), "timeout_seconds": int(timeout_value or 90), }) if route_result.get("status") == "ok": route_matches: list[dict[str, Any]] = [] for item in route_result.get("objects") or []: if not isinstance(item, dict): continue selectors = item.get("read_selectors") if isinstance(item.get("read_selectors"), dict) else {} selector = selectors.get("form_decode") or selectors.get("card") or selectors.get("route") or {} route_match = definition_match( query=str(query_value), area="extensions", kind_ru=str(item.get("kind_ru") or "Определение расширения"), location={"presentation": str(item.get("name") or ""), "section": "Расширения"}, item=item, object_card=item, read_selector=selector, ) if route_match: route_matches.append(route_match) if route_matches: # Validated form/object routes are directly readable and rank # above broad DBNames definition candidates. route_guids = { str(((match.get("object") or {}).get("guid") or "")).lower() for match in route_matches if str(((match.get("object") or {}).get("guid") or "")).strip() } matches[:] = [ match for match in matches if str(match.get("guid") or ((match.get("object") or {}).get("guid") or "")).lower() not in route_guids ] matches[0:0] = route_matches del matches[int(max_matches or 50):] else: diagnostics.append({"area": "extensions", "route_cache_status": route_result.get("status"), "diagnostics": route_result.get("diagnostics")}) append_object_card_metadata_match(identity_match_by) counts_by_area: dict[str, int] = {} unresolved_origin = 0 extension_origin = 0 for item in matches: counts_by_area[str(item.get("area") or "")] = counts_by_area.get(str(item.get("area") or ""), 0) + 1 origin = item.get("origin") if isinstance(item.get("origin"), dict) else {} if origin.get("source") == "extension": extension_origin += 1 if origin.get("status") not in {"ok"}: unresolved_origin += 1 usage_matches: list[dict[str, Any]] = [] usage_status: str | None = None if object_card and not matches and "modules" in areas: usage_result = search_modules( { "base_id": base_id, "query": str(query_value), "include_storage": include_storage, "scan_limit": min(int(max_items or 5000), 5000), "limit": min(int(max_matches or 50), 100), "timeout_seconds": int(timeout_value or 90), } ) usage_status = str(usage_result.get("status") or "") if usage_status in {"ok", "partial"}: usage_matches = usage_result.get("matches") or [] if usage_matches: diagnostics.append( { "message": "Определение внутри выбранного объекта не найдено, но найдены использования имени в модулях. Это подсказка для поиска, а не место определения.", } ) elif usage_status: diagnostics.append({"area": "usage_modules", "status": usage_status, "diagnostics": usage_result.get("diagnostics")}) metadata_object_matches = [ item for item in matches if item.get("area") == "metadata" and isinstance(item.get("object"), dict) and item.get("object", {}).get("guid") ] resolved_object_card = object_card resolved_related_selectors: dict[str, dict[str, Any]] = {} if not resolved_object_card and len(metadata_object_matches) == 1: resolved_object_card = dict(metadata_object_matches[0].get("object") or {}) resolved_related_selectors = dict(metadata_object_matches[0].get("related_selectors") or {}) elif object_card: resolved_related_selectors = object_related_selectors(base_id, object_card) return { "schema": "onec_metadata_definition_find.v1", "status": "ok" if matches else "not_found", **({"error": "not_found"} if not matches else {}), "base_id": base_id, "source": {"kind": "live_metadata"} if not include_storage else object_result.get("source", {"kind": "live_metadata"}), "query": { "query": query_value, "kind": payload.get("kind"), "name": payload.get("name"), "guid": payload.get("guid"), "form": payload.get("form"), "areas": areas, "exact_only": bool(exact_only), "refresh_cache": bool(refresh_cache), "use_cache": bool(use_cache), "include_storage": include_storage, }, "object": resolved_object_card, **({"related_selectors": resolved_related_selectors} if resolved_related_selectors else {}), "matches": matches, **( { "usage_matches": usage_matches, "usage": { "status": usage_status, "meaning": "Использования имени в коде, не место определения.", }, } if usage_matches or usage_status else {} ), "counts": { "matches": len(matches), "by_area": counts_by_area, "usage_matches": len(usage_matches), "origin_extension": extension_origin, "origin_unresolved": unresolved_origin, "truncated": len(matches) >= int(max_matches or 50), "max_matches": int(max_matches or 50), }, "diagnostics": diagnostics or ( [ { "message": "Совпадений не найдено. Для проверки кода модуля используйте areas=['modules'] или modules.search с тем же selector.", } ] if not matches else [] ), } def metadata_object_templates(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, "metadata.object.templates") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, "metadata.object.templates") if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error timeout_value, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.templates", default=60, minimum=1) if timeout_error: return timeout_error timeout_seconds = int(timeout_value or 60) include_text, include_text_error = strict_bool_argument(payload, "include_text", method="metadata.object.templates", default=False) if include_text_error: return include_text_error include_tree, include_tree_error = strict_bool_argument(payload, "include_tree", method="metadata.object.templates", default=False) if include_tree_error: return include_tree_error table_or_error = metadata_storage_table(payload, "metadata.object.templates") if isinstance(table_or_error, dict): return table_or_error table = table_or_error requested_template, requested_template_error = optional_string_filter(payload, ["template", "name_filter"], method="metadata.object.templates") if requested_template_error: return requested_template_error include_storage, include_storage_error = strict_include_storage(payload, "metadata.object.templates") if include_storage_error: return include_storage_error include_storage = bool(include_storage) evidence_mode, evidence_mode_error = parse_evidence_mode_argument(payload, "metadata.object.templates") if evidence_mode_error: return evidence_mode_error if canonical_kind(str(payload.get("kind") or payload.get("object_type") or "")) == "CommonTemplate": guid, _, object_card, resolve_error = resolve_object_guid( payload, base_id, timeout_seconds=timeout_seconds, method="metadata.object.templates", table=table, ) if resolve_error: return resolve_error direct = read_template_by_guid( { "base_id": base_id, "guid": guid, "kind": "Template", "table": table, "view": "summary", "include_content": bool(include_text), "timeout_seconds": timeout_seconds, } ) if direct.get("status") != "ok": return direct templates = [] for template_item in direct.get("templates") or []: item = dict(template_item) item["name"] = item.get("name") or (object_card or {}).get("name") item["kind"] = "CommonTemplate" item["ref"] = (object_card or {}).get("ref") or object_selector_ref("CommonTemplate", str(item.get("name") or "")) templates.append(item) return { "schema": "onec_object_templates.v1", "status": "ok", "base_id": base_id, "source": direct.get("source") if include_storage else {"kind": "live_metadata"}, "object": object_card, "query": {"template": requested_template, "include_storage": include_storage}, "templates": templates, "counts": {"templates": len(templates), "related": 0, "top_level_common_template": 1}, "capabilities": {"templates": True, "top_level": True}, } related_result = metadata_object_related( { **payload, "include_text": False, "include_storage": include_storage, "table": table, } ) if related_result.get("status") != "ok": result = dict(related_result) result["method"] = "metadata.object.templates" return result related_capabilities = related_result.get("capabilities") if isinstance(related_result.get("capabilities"), dict) else {} if related_capabilities.get("related") is False: return { "schema": "onec_object_templates.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_metadata"}, "object": related_result.get("object"), "query": {"template": requested_template, "include_storage": include_storage}, "templates": [], "counts": {"templates": 0, "related": 0}, "capabilities": { "templates": False, "reason": "У этого вида объекта адаптер не знает разделов макетов.", }, } wanted = normalize(requested_template or "") template_items = filter_related_children_by_identity(related_result.get("related") or [], "Template", requested_template) templates = [] for item, match_by in template_items: identity = item.get("identity") or {} synonyms = identity.get("synonyms") or {} synonym = next(iter(synonyms.values()), None) if isinstance(synonyms, dict) else None parts_result = metadata_object_parts( { "base_id": base_id, "guid": item.get("guid"), "kind": "Template", "table": table, "include_text": bool(include_text), "include_tree": bool(include_tree), "include_storage": True, "evidence_mode": evidence_mode, "timeout_seconds": timeout_seconds, } ) parts = [] for part in parts_result.get("parts") or []: classification = part.get("classification") or {} public_part = payload_public_properties(classification) public_part["undecoded_evidence"] = payload_public_undecoded_evidence( classification, include_text_preview=bool(include_text), mode=str(evidence_mode or "summary"), allow_storage_details=bool(include_storage), ) if include_storage: public_part.update( { "part_id": part.get("part_id"), "suffix": part.get("suffix"), "raw_bytes": classification.get("raw_bytes"), "payload_bytes": classification.get("payload_bytes"), "sha1": classification.get("sha1"), } ) parts.append(public_part) template = { **public_child_identity(item), **public_template_summary(parts, include_storage=include_storage), } if wanted: template["match_by"] = match_by if include_storage: template["related"] = item templates.append(template) if wanted and not templates: result = child_not_found("metadata.object.templates", "Макет", requested_template, related_result.get("object") or {}, base_id=base_id) result.update( { "schema": "onec_object_templates.v1", "source": related_result.get("source") if include_storage else {"kind": "live_metadata"}, "query": {"template": requested_template, "include_storage": include_storage}, "templates": [], "counts": {"templates": 0, "related": (related_result.get("counts") or {}).get("related")}, } ) return result return { "schema": "onec_object_templates.v1", "status": "ok", "base_id": base_id, "source": related_result.get("source") if include_storage else {"kind": "live_metadata"}, "object": related_result.get("object"), "query": {"template": requested_template, "include_storage": include_storage}, "templates": templates, "counts": {"templates": len(templates), "related": (related_result.get("counts") or {}).get("related")}, } def metadata_object_template_details(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, "metadata.object.template.details") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, "metadata.object.template.details") if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error include_storage, include_storage_error = strict_include_storage(payload, "metadata.object.template.details") if include_storage_error: return include_storage_error include_storage = bool(include_storage) include_preview, include_preview_error = strict_bool_argument(payload, "include_preview", method="metadata.object.template.details", default=True) if include_preview_error: return include_preview_error evidence_mode, evidence_mode_error = parse_evidence_mode_argument(payload, "metadata.object.template.details") if evidence_mode_error: return evidence_mode_error table_or_error = metadata_storage_table(payload, "metadata.object.template.details") if isinstance(table_or_error, dict): return table_or_error table = table_or_error timeout_value, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.template.details", default=60, minimum=1) if timeout_error: return timeout_error timeout_seconds = int(timeout_value or 60) templates_result = metadata_object_templates({**payload, "include_storage": include_storage, "table": table}) if templates_result.get("status") != "ok": result = dict(templates_result) result["method"] = "metadata.object.template.details" return result details = [] for template in templates_result.get("templates") or []: parts_result = metadata_object_parts( { "base_id": base_id, "guid": template.get("guid"), "kind": "Template", "table": table, "include_text": False, "include_tree": False, "include_storage": True, "evidence_mode": evidence_mode, "timeout_seconds": timeout_seconds, } ) detailed_parts = [] for part in parts_result.get("parts") or []: classification = part.get("classification") or {} public_part = payload_public_properties(classification) public_part["preview"] = payload_public_preview( classification, include_text_preview=bool(include_preview), ) public_part["undecoded_evidence"] = payload_public_undecoded_evidence( classification, include_text_preview=bool(include_preview), mode=str(evidence_mode or "summary"), allow_storage_details=bool(include_storage), ) if include_storage: public_part.update( { "part_id": part.get("part_id"), "suffix": part.get("suffix"), "raw_bytes": classification.get("raw_bytes"), "payload_bytes": classification.get("payload_bytes"), "sha1": classification.get("sha1"), } ) detailed_parts.append(public_part) detail = dict(template) detail["counts"] = { **(detail.get("counts") or {}), "preview_streams": sum(len((part.get("preview") or {}).get("streams") or []) for part in detailed_parts), "preview_base64": sum(len((part.get("preview") or {}).get("base64") or []) for part in detailed_parts), } if include_storage: detail["parts"] = detailed_parts details.append(detail) return { "schema": "onec_object_template_details.v1", "status": "ok", "base_id": base_id, "source": templates_result.get("source") if include_storage else {"kind": "live_metadata"}, "object": templates_result.get("object"), "query": { "template": payload.get("template") or payload.get("name_filter"), "include_preview": bool(include_preview), "include_storage": include_storage, }, "templates": details, "counts": {"templates": len(details), "available_templates": (templates_result.get("counts") or {}).get("templates")}, } MOXEL_NAMED_AREA_RE = re.compile( r'"(?PОбласть[^"]+)"\s*,\s*\{1\s*,\s*\{3\s*,\s*' r"(?P\d+)\s*,\s*(?P\d+)\s*,\s*(?P\d+)\s*,\s*(?P\d+)\s*,\s*" r"(?P[0-9a-fA-F-]{36})\}\s*,\s*0\}", re.S, ) def decode_moxel_text_payload(payload_bytes: bytes) -> tuple[str | None, dict[str, Any]]: payload = bytes(payload_bytes or b"") marker = payload.find(b"\xef\xbb\xbf") if marker >= 0: try: return payload[marker + 3 :].decode("utf-8-sig"), {"encoding": "utf-8-sig", "bom_offset": marker} except Exception: pass if payload.startswith(b"MOXCEL"): for encoding in ("utf-8", "cp1251"): try: text = payload.decode(encoding) except Exception: continue if "{8," in text or "Область" in text: return text, {"encoding": encoding, "bom_offset": None} return None, {"encoding": None, "bom_offset": marker if marker >= 0 else None} def moxel_range(row1: int, col1: int, row2: int, col2: int) -> dict[str, Any]: top = min(row1, row2) left = min(col1, col2) bottom = max(row1, row2) right = max(col1, col2) return { "zero_based": { "top": top, "left": left, "bottom": bottom, "right": right, "row_start": top, "column_start": left, "row_end": bottom, "column_end": right, }, "one_based": { "top": top + 1, "left": left + 1, "bottom": bottom + 1, "right": right + 1, "row_start": top + 1, "column_start": left + 1, "row_end": bottom + 1, "column_end": right + 1, }, "height": bottom - top + 1, "width": right - left + 1, } def extract_moxel_dimensions(text: str | None) -> dict[str, int] | None: if not text: return None match = re.search(r"\}\s*,\s*\{(?P\d+)\s*,\s*(?P\d+)\}\s*,\s*\{3\s*,", text[:2000], re.S) if not match: match = re.search(r"\}\s*,\s*\{(?P\d{1,5})\s*,\s*(?P\d{1,5})\}\s*,", text[:2000], re.S) if not match: return None return {"rows": int(match.group("rows")), "columns": int(match.group("columns"))} def extract_moxel_named_areas(text: str | None) -> list[dict[str, Any]]: if not text: return [] areas: list[dict[str, Any]] = [] occurrences: dict[str, int] = {} for match in MOXEL_NAMED_AREA_RE.finditer(text): name = match.group("name") occurrences[name.casefold()] = occurrences.get(name.casefold(), 0) + 1 row1 = int(match.group("row1")) col1 = int(match.group("col1")) row2 = int(match.group("row2")) col2 = int(match.group("col2")) areas.append( { "name": name, "source": "moxel_text", "occurrence": occurrences[name.casefold()], "range": moxel_range(row1, col1, row2, col2), "guid": match.group("guid"), "offset": match.start(), } ) return areas def extract_moxel_named_area_candidates_from_tree(tree: dict[str, Any] | None, *, max_areas: int = 200) -> list[dict[str, Any]]: if not isinstance(tree, dict) or tree.get("type") != "list": return [] areas: list[dict[str, Any]] = [] occurrences: dict[str, int] = {} def raw_scalars(node: Any, *, limit: int = 20) -> list[str]: values: list[str] = [] def walk(current: Any) -> None: if len(values) >= limit or not isinstance(current, dict): return if current.get("type") in {"atom", "string"}: values.append(moxel_scalar(current)) return if current.get("type") == "list": for child in current.get("items") or []: walk(child) if len(values) >= limit: break walk(node) return values def walk(node: Any, path: str) -> None: if len(areas) >= max_areas or not isinstance(node, dict) or node.get("type") != "list": return items = node.get("items") or [] if len(items) >= 3 and moxel_int(items[0]) in {1, 2}: name = moxel_scalar(items[1]) range_node = items[2] if ( name and "Област" in name and isinstance(range_node, dict) and range_node.get("type") == "list" ): key = name.casefold() occurrences[key] = occurrences.get(key, 0) + 1 scalars = raw_scalars(range_node) areas.append( { "name": name, "source": "moxel_tree_named_area_candidate", "occurrence": occurrences[key], "range": None, "tree_position": path, "range_candidate": { "tree_position": f"{path}.2", "raw_scalars": scalars, "raw_scalar_count": len(scalars), }, "diagnostics": { "message": "Named area was found in the MOXCEL tree, but exact coordinate semantics for this area encoding are not decoded yet." }, } ) for index, child in enumerate(items): walk(child, f"{path}.{index}") walk(tree, "$") return areas def extract_moxel_named_range_candidates_from_tree(tree: dict[str, Any] | None, *, max_ranges: int = 300) -> list[dict[str, Any]]: if not isinstance(tree, dict) or tree.get("type") != "list": return [] ranges: list[dict[str, Any]] = [] def raw_scalars(node: Any, *, limit: int = 20) -> list[str]: values: list[str] = [] def walk(current: Any) -> None: if len(values) >= limit or not isinstance(current, dict): return if current.get("type") in {"atom", "string"}: values.append(moxel_scalar(current)) return if current.get("type") == "list": for child in current.get("items") or []: walk(child) if len(values) >= limit: break walk(node) return values def walk(node: Any, path: str) -> None: if len(ranges) >= max_ranges or not isinstance(node, dict) or node.get("type") != "list": return items = node.get("items") or [] if len(items) >= 3 and moxel_int(items[0]) in {1, 2}: cursor = 1 while cursor + 1 < len(items): name = moxel_scalar(items[cursor]) range_node = items[cursor + 1] if ( name and isinstance(range_node, dict) and range_node.get("type") == "list" and re.fullmatch(r"[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{1,100}", name) ): scalars = raw_scalars(range_node) if scalars: decoded_range = None if len(scalars) >= 8 and scalars[1] == "3" and all(re.fullmatch(r"-?\d+", value or "") for value in scalars[2:6]): left = int(scalars[2]) top = int(scalars[3]) right = int(scalars[4]) bottom = int(scalars[5]) if min(left, top, right, bottom) >= 0 and left <= right and top <= bottom: decoded_range = moxel_range(top, left, bottom, right) ranges.append( { "name": name, "kind": "named_area" if "Област" in name else "named_cell_or_range", "source": "moxel_tree_named_range_candidate", "range": decoded_range, "tree_position": path, "range_candidate": { "tree_position": f"{path}.{cursor + 1}", "raw_scalars": scalars, "raw_scalar_count": len(scalars), **({"coordinate_order": "left,top,right,bottom"} if decoded_range else {}), }, "diagnostics": { "message": "Named cell/range was found in the MOXCEL tree, but exact coordinate semantics for this encoding are not decoded yet." }, } ) cursor += 2 continue cursor += 1 for index, child in enumerate(items): walk(child, f"{path}.{index}") walk(tree, "$") seen: set[tuple[str, tuple[str, ...]]] = set() unique: list[dict[str, Any]] = [] for item in ranges: key = (str(item.get("name") or "").casefold(), tuple(((item.get("range_candidate") or {}).get("raw_scalars") or []))) if key in seen: continue seen.add(key) unique.append(item) return unique def moxel_scalar(node: Any) -> str: if isinstance(node, dict) and node.get("type") in {"atom", "string"}: return str(node.get("value") or "") return "" def moxel_int(node: Any) -> int | None: value = moxel_scalar(node) if not re.fullmatch(r"-?\d+", value or ""): return None try: return int(value) except Exception: return None def moxel_text_values_from_node(node: Any) -> list[str]: values: list[str] = [] def repair_text(value: str) -> str: if not value: return value try: repaired = value.encode("cp1251").decode("utf-8") except Exception: return value cyrillic_original = len(re.findall(r"[А-Яа-яЁё]", value)) cyrillic_repaired = len(re.findall(r"[А-Яа-яЁё]", repaired)) return repaired if cyrillic_repaired > cyrillic_original else value def walk(current: Any) -> None: if not isinstance(current, dict): return items = current.get("items") if current.get("type") == "list" else None if isinstance(items, list) and len(items) == 2 and all(isinstance(item, dict) and item.get("type") == "string" for item in items): language = moxel_scalar(items[0]) value = moxel_scalar(items[1]) if language in {"", "ru"} and value: values.append(repair_text(value)) return if isinstance(items, list): for child in items: walk(child) walk(node) return values def moxel_node_summary(node: Any, *, max_items: int = 12) -> dict[str, Any]: if not isinstance(node, dict): return {"type": "unknown"} if node.get("type") != "list": return {"type": node.get("type"), "value": moxel_scalar(node)} items = node.get("items") or [] values = [moxel_scalar(item) if isinstance(item, dict) and item.get("type") != "list" else None for item in items[:max_items]] return { "type": "list", "head": moxel_scalar(items[0]) if items else None, "list_length": len(items), "scalar_prefix": values, "truncated": len(items) > max_items, } def extract_moxel_cell_style_candidates_from_tree(tree: dict[str, Any] | None, *, max_candidates: int = 200) -> list[dict[str, Any]]: if not isinstance(tree, dict) or tree.get("type") != "list": return [] candidates: list[dict[str, Any]] = [] def coordinate_hints(preceding_scalars: list[dict[str, Any]]) -> dict[str, Any] | None: if not preceding_scalars: return None for item in reversed(preceding_scalars): try: column = int(str(item.get("value") or "").strip()) except (TypeError, ValueError): column = None if column is None or column < 0: continue return { "one_based": {"column": column + 1}, "zero_based": {"column": column}, "confidence": "high", "source": "moxel_schema_rule:inline_text_column_from_last_preceding_scalar_plus_one", } return None def walk(parent: dict[str, Any], path: str) -> bool: items = parent.get("items") if isinstance(parent.get("items"), list) else [] for index, node in enumerate(items): node_path = f"{path}.{index}" if not isinstance(node, dict) or node.get("type") != "list": continue child_items = node.get("items") or [] is_candidate = bool(child_items and moxel_int(child_items[0]) in {16, 24}) texts = moxel_text_values_from_node(node) if is_candidate else [] if is_candidate and texts: preceding_scalars: list[dict[str, Any]] = [] for sibling_index in range(max(0, index - 12), index): sibling = items[sibling_index] if isinstance(sibling, dict) and sibling.get("type") in {"atom", "string"}: preceding_scalars.append({"index": sibling_index, "value": moxel_scalar(sibling)}) immediate_preceding_scalars: list[dict[str, Any]] = [] sibling_index = index - 1 while sibling_index >= 0: sibling = items[sibling_index] if not isinstance(sibling, dict) or sibling.get("type") not in {"atom", "string"}: break immediate_preceding_scalars.append({"index": sibling_index, "value": moxel_scalar(sibling)}) sibling_index -= 1 immediate_preceding_scalars.reverse() next_moxel_record = items[index + 1] if index + 1 < len(items) else None candidate = { "tree_position": node_path, "type_code": moxel_int(child_items[0]), "cell_id": moxel_int(child_items[1]) if len(child_items) > 1 else None, "text": next((text for text in texts if text), None), "texts": texts, "source": "moxel_inline_text_cell", "confidence": "low", "style_evidence": { "preceding_scalars": preceding_scalars, "last_7_preceding_values": [item.get("value") for item in preceding_scalars[-7:]], "immediate_preceding_scalars": immediate_preceding_scalars, "immediate_preceding_values": [item.get("value") for item in immediate_preceding_scalars], "diagnostics": { "message": "Inline MOXCEL text cell with nearby scalar style fields. Exact border/style semantics require controlled before/after diffs." }, }, } hints = coordinate_hints(preceding_scalars) if hints: candidate["coordinate_hints"] = hints if isinstance(next_moxel_record, dict): candidate["next_moxel_record"] = { "tree_position": f"{path}.{index + 1}", **moxel_node_summary(next_moxel_record), } candidates.append(candidate) if len(candidates) >= max_candidates: return True if walk(node, node_path): return True return False walk(tree, "$") return candidates def moxel_cell_definition_from_node(node: Any, definitions: dict[int, dict[str, Any]]) -> dict[str, Any] | None: if not isinstance(node, dict) or node.get("type") != "list": return None items = node.get("items") or [] if not items: return None type_code = moxel_int(items[0]) if type_code == 0 and len(items) >= 2: referenced_id = moxel_int(items[1]) if referenced_id is None: return None resolved = dict(definitions.get(referenced_id) or {}) if not resolved: return {"type_code": 0, "cell_id": referenced_id, "reference": referenced_id, "source": "moxel_reference"} resolved["reference"] = referenced_id resolved["source"] = "moxel_reference" return resolved if type_code not in {16, 24} or len(items) < 2: return None cell_id = moxel_int(items[1]) texts = moxel_text_values_from_node(node) parameter = None if type_code == 24 and len(items) >= 3: parameter = moxel_scalar(items[2]) or None value = next((text for text in texts if text), None) definition = { "type_code": type_code, "cell_id": cell_id, "text": value, "texts": texts, **({"parameter": parameter} if parameter else {}), "source": "moxel_cell", } if cell_id is not None: definitions[cell_id] = definition return definition def extract_moxel_cells_from_tree(tree: dict[str, Any] | None, *, max_cells: int = 1000) -> list[dict[str, Any]]: if not isinstance(tree, dict) or tree.get("type") != "list": return [] definitions: dict[int, dict[str, Any]] = {} cells: list[dict[str, Any]] = [] seen_cells: set[tuple[Any, ...]] = set() def looks_like_row_header(items: list[Any], start: int) -> bool: if start + 3 >= len(items): return False next_row = moxel_int(items[start]) next_zero_marker = moxel_int(items[start + 1]) next_count = moxel_int(items[start + 2]) next_flag = moxel_int(items[start + 3]) return ( next_row is not None and next_zero_marker == 0 and next_count is not None and 0 < next_count <= 512 and next_flag is not None and next_flag >= 0 ) def append_cell(row: int, column: int, cell_def: dict[str, Any]) -> bool: text = cell_def.get("text") parameter = cell_def.get("parameter") if not text and not parameter: return False key = (row, column, cell_def.get("cell_id"), text, parameter) if key in seen_cells: return False seen_cells.add(key) cells.append( { "row": row + 1, "column": column + 1, "zero_based": {"row": row, "column": column}, "one_based": {"row": row + 1, "column": column + 1}, "type_code": cell_def.get("type_code"), "cell_id": cell_def.get("cell_id"), "text": text, **({"texts": cell_def.get("texts")} if cell_def.get("texts") else {}), **({"parameter": parameter} if parameter else {}), **({"reference": cell_def.get("reference")} if cell_def.get("reference") is not None else {}), "source": cell_def.get("source") or "moxel_cell", } ) return len(cells) >= max_cells def parse_row_runs(items: list[Any]) -> bool: index = 0 while index + 3 < len(items): row = moxel_int(items[index]) zero_marker = moxel_int(items[index + 1]) count = moxel_int(items[index + 2]) flag = moxel_int(items[index + 3]) if row is None or zero_marker != 0 or count is None or count <= 0 or count > 512 or flag is None or flag < 0: index += 1 continue # Observed MOXCEL row runs use the fourth scalar as the first cell # column (zero-based). Each subsequent scalar between cell nodes is # the next cell column. The last cell in the run is not followed by # its own column scalar. parsed_pairs: list[tuple[dict[str, Any] | None, int]] = [] cursor = index + 4 current_column = flag valid_run = True for cell_index in range(count): if cursor >= len(items): valid_run = False break cell_node = items[cursor] if not isinstance(cell_node, dict) or cell_node.get("type") != "list": valid_run = False break parsed_pairs.append((moxel_cell_definition_from_node(cell_node, definitions), current_column)) cursor += 1 if cell_index >= count - 1: continue next_column = moxel_int(items[cursor]) if cursor < len(items) else None if next_column is None or next_column < 0: valid_run = False break current_column = next_column cursor += 1 if valid_run and cursor < len(items) and moxel_int(items[cursor]) is not None and not looks_like_row_header(items, cursor): valid_run = False if not valid_run: parsed_pairs = [] cursor = index + 4 for _ in range(count): if cursor + 1 >= len(items): parsed_pairs = [] break cell_node = items[cursor] column = moxel_int(items[cursor + 1]) if column is None or not isinstance(cell_node, dict) or cell_node.get("type") != "list": parsed_pairs = [] break parsed_pairs.append((moxel_cell_definition_from_node(cell_node, definitions), column)) cursor += 2 if not parsed_pairs: index += 1 continue for cell_def, column in parsed_pairs: if cell_def and append_cell(row, column, cell_def): return True index += 1 return False def walk(node: Any) -> bool: if not isinstance(node, dict) or node.get("type") != "list": return False items = node.get("items") if isinstance(node.get("items"), list) else [] if parse_row_runs(items): return True for child in items: if walk(child): return True return False walk(tree) return cells def extract_moxel_column_widths_from_tree(tree: dict[str, Any] | None) -> list[dict[str, Any]]: if not isinstance(tree, dict): return [] widths: list[dict[str, Any]] = [] seen: set[int] = set() def walk(node: Any) -> None: if not isinstance(node, dict) or node.get("type") != "list": return items = node.get("items") or [] if len(items) >= 4 and moxel_int(items[0]) == 0: cursor = 2 local: list[dict[str, Any]] = [] while cursor + 1 < len(items): column = moxel_int(items[cursor]) value_node = items[cursor + 1] value_items = value_node.get("items") if isinstance(value_node, dict) and value_node.get("type") == "list" else None if column is None or not isinstance(value_items, list) or len(value_items) < 2 or moxel_scalar(value_items[0]) != "N": local = [] break width = moxel_int(value_items[1]) if width is None: local = [] break local.append( { "column": column + 1, "zero_based": {"column": column}, "one_based": {"column": column + 1}, "width": width, "source": "moxel_width_block", } ) cursor += 2 for item in local: column = int(item["zero_based"]["column"]) if column not in seen: seen.add(column) widths.append(item) for child in items: walk(child) walk(tree) return widths def moxel_format_record_payload( *, head: int, font_index: int, width: int, horizontal_code: int | None, vertical_code: int | None, extra_flag: int | None, border_values: dict[str, int] | None = None, text_color_index: int | None = None, back_color_index: int | None = None, fill_type_code: int | None = None, ) -> dict[str, Any]: horizontal_alignment = { 0: "Left", 2: "Right", 4: "Justify", 6: "Center", } vertical_alignment = { 0: "Top", 8: "Bottom", 24: "Center", } text_placement = { 0: "Auto", 1: "Cut", 2: "Block", } fill_type = { 0: "None", 1: "Parameter", 2: "Template", } format_flags: list[str] = [] if border_values is not None: format_flags.append("borders") if text_color_index is not None: format_flags.append("text_color") if back_color_index is not None: format_flags.append("back_color") if fill_type_code is not None: format_flags.append("fill_type") return { "record_type": head, "record_type_hex": f"0x{head:X}", "font_index": font_index, "width": width, **({"format_flags": format_flags} if format_flags else {}), **( { "horizontal_alignment": { "code": horizontal_code, "value": horizontal_alignment.get(horizontal_code, "Unknown"), } } if horizontal_code is not None else {} ), **( { "vertical_alignment": { "code": vertical_code, "value": vertical_alignment.get(vertical_code, "Unknown"), } } if vertical_code is not None else {} ), **( { "text_placement": { "code": extra_flag, "value": text_placement.get(extra_flag, "Unknown"), } } if extra_flag is not None else {} ), **({"extra_flag": extra_flag} if extra_flag is not None else {}), **({"borders": border_values} if border_values is not None else {}), **( { "text_color": { "style_index": text_color_index, "source": "moxel_format_record_flag_0x0400", } } if text_color_index is not None else {} ), **( { "back_color": { "style_index": back_color_index, "source": "moxel_format_record_flag_0x0800", } } if back_color_index is not None else {} ), **( { "fill_type": { "code": fill_type_code, "value": fill_type.get(fill_type_code, "Unknown"), "source": "moxel_format_record_flag_0x8000", } } if fill_type_code is not None else {} ), "source": "moxel_format_table_record", "confidence": "medium", } def decode_moxel_format_record_numbers(numbers: list[int]) -> dict[str, Any] | None: if not numbers: return None head = numbers[0] if head == 17281 and len(numbers) >= 6: font_index = numbers[1] width = numbers[2] return moxel_format_record_payload( head=head, font_index=font_index, width=width, horizontal_code=numbers[3], vertical_code=numbers[4], extra_flag=numbers[5], ) if head & 0x0081 != 0x0081 or len(numbers) < 3: return None cursor = 1 font_index = numbers[cursor] cursor += 1 border_values: dict[str, int] | None = None border_bits = [ ("left", 0x0002), ("top", 0x0004), ("right", 0x0008), ("bottom", 0x0010), ] active_border_bits = [(name, bit) for name, bit in border_bits if head & bit] has_border_color = bool(head & 0x0020) if active_border_bits or has_border_color: needed = len(active_border_bits) + (1 if has_border_color else 0) if len(numbers) < cursor + needed + 1: return None border_values = { "source": "moxel_format_record_border_flags", "flags": [f"0x{bit:04X}" for _, bit in active_border_bits] + (["0x0020"] if has_border_color else []), } for name, _bit in active_border_bits: border_values[name] = numbers[cursor] cursor += 1 if has_border_color: border_values["color_style_index"] = numbers[cursor] cursor += 1 if len(numbers) <= cursor: return None width = numbers[cursor] cursor += 1 text_color_index = None if head & 0x0400: if len(numbers) <= cursor: return None text_color_index = numbers[cursor] cursor += 1 back_color_index = None if head & 0x0800: if len(numbers) <= cursor: return None back_color_index = numbers[cursor] cursor += 1 fill_type_code = None if head & 0x8000: if len(numbers) <= cursor: return None fill_type_code = numbers[cursor] cursor += 1 return moxel_format_record_payload( head=head, font_index=font_index, width=width, horizontal_code=None, vertical_code=None, extra_flag=None, border_values=border_values, text_color_index=text_color_index, back_color_index=back_color_index, fill_type_code=fill_type_code, ) def extract_moxel_format_table_from_tree(tree: dict[str, Any] | None) -> list[dict[str, Any]]: if not isinstance(tree, dict) or tree.get("type") != "list": return [] def decode_record(node: Any, path: str) -> dict[str, Any] | None: if not isinstance(node, dict) or node.get("type") != "list": return None items = node.get("items") if isinstance(node.get("items"), list) else [] if not items: return None numbers: list[int] = [] for item in items: value = moxel_int(item) if value is None: return None numbers.append(value) payload = decode_moxel_format_record_numbers(numbers) if payload is None: return None return { "tree_position": path, **payload, } best_run: list[dict[str, Any]] = [] def inspect_siblings(items: list[Any], path: str) -> None: nonlocal best_run current: list[dict[str, Any]] = [] for index, child in enumerate(items): decoded = decode_record(child, f"{path}.{index}") if decoded: current.append(decoded) continue if len(current) > len(best_run) or (len(current) == len(best_run) and len(current) > 1): best_run = current current = [] if len(current) > len(best_run) or (len(current) == len(best_run) and len(current) > 1): best_run = current def walk(node: Any, path: str) -> None: if not isinstance(node, dict) or node.get("type") != "list": return items = node.get("items") if isinstance(node.get("items"), list) else [] inspect_siblings(items, path) for index, child in enumerate(items): walk(child, f"{path}.{index}") walk(tree, "$") if not best_run: return [] result: list[dict[str, Any]] = [] for index, item in enumerate(best_run, start=1): result.append( { "format_index": index, "zero_based": {"format_index": index - 1}, **item, } ) return result def extract_moxel_format_table_from_diagnostics(diagnostics: dict[str, Any] | None) -> list[dict[str, Any]]: if not isinstance(diagnostics, dict): return [] records = diagnostics.get("top_level_records") if isinstance(diagnostics.get("top_level_records"), list) else [] if not records: return [] def decode_record(record: Any) -> dict[str, Any] | None: if not isinstance(record, dict): return None values = record.get("numeric_items") if isinstance(record.get("numeric_items"), list) else [] if not values: return None try: numbers = [int(value) for value in values] except (TypeError, ValueError): return None payload = decode_moxel_format_record_numbers(numbers) if payload is None: return None return { "tree_position": record.get("tree_position"), **payload, } best_run: list[dict[str, Any]] = [] current: list[dict[str, Any]] = [] previous_index: int | None = None for record in records: decoded = decode_record(record) position = str((record or {}).get("tree_position") or "") match = re.fullmatch(r"\$\.(\d+)", position) index = int(match.group(1)) if match else None contiguous = previous_index is None or index is None or index == previous_index + 1 if decoded and contiguous: current.append(decoded) previous_index = index continue if len(current) > len(best_run) or (len(current) == len(best_run) and len(current) > 1): best_run = current current = [decoded] if decoded else [] previous_index = index if decoded else None if len(current) > len(best_run) or (len(current) == len(best_run) and len(current) > 1): best_run = current result: list[dict[str, Any]] = [] for index, item in enumerate(best_run, start=1): result.append( { "format_index": index, "zero_based": {"format_index": index - 1}, **item, } ) return result def extract_moxel_font_table_from_diagnostics(diagnostics: dict[str, Any] | None) -> list[dict[str, Any]]: if not isinstance(diagnostics, dict): return [] records = diagnostics.get("top_level_records") if isinstance(diagnostics.get("top_level_records"), list) else [] fonts: list[dict[str, Any]] = [] for record in records: if not isinstance(record, dict): continue values = record.get("numeric_items") if isinstance(record.get("numeric_items"), list) else [] strings = record.get("strings") if isinstance(record.get("strings"), list) else [] if not values or not strings: continue try: numbers = [int(value) for value in values] except (TypeError, ValueError): continue if len(numbers) < 8 or numbers[0] != 8: continue face_name = next((str(value) for value in strings if str(value or "")), "") if not face_name: continue height_raw = numbers[3] weight = numbers[7] fonts.append( { "font_index": len(fonts), "tree_position": record.get("tree_position"), "face_name": face_name, "height": height_raw / 10 if height_raw % 10 == 0 else height_raw, "height_raw": height_raw, "weight": weight, "bold": weight >= 600, "italic": bool(numbers[8]) if len(numbers) > 8 else False, "underline": bool(numbers[9]) if len(numbers) > 9 else False, "strikeout": bool(numbers[10]) if len(numbers) > 10 else False, "scale": numbers[17] if len(numbers) > 17 else None, "source": "moxel_font_table_record", "confidence": "medium", } ) return fonts def enrich_moxel_format_table_with_fonts( format_table: list[dict[str, Any]], font_table: list[dict[str, Any]], ) -> list[dict[str, Any]]: if not format_table or not font_table: return format_table fonts_by_index: dict[int, dict[str, Any]] = {} for item in font_table: if not isinstance(item, dict): continue try: font_index = int(item.get("font_index")) except (TypeError, ValueError): continue fonts_by_index[font_index] = item enriched: list[dict[str, Any]] = [] for item in format_table: if not isinstance(item, dict): continue result = dict(item) try: font_index = int(item.get("font_index")) except (TypeError, ValueError): font_index = None font = fonts_by_index.get(font_index) if font_index is not None else None if font: result["font"] = { "font_index": font.get("font_index"), "face_name": font.get("face_name"), "height": font.get("height"), "weight": font.get("weight"), "bold": font.get("bold"), "italic": font.get("italic"), "underline": font.get("underline"), "strikeout": font.get("strikeout"), } enriched.append(result) return enriched def extract_moxel_cell_format_links( cells: list[dict[str, Any]], format_table: list[dict[str, Any]], *, limit: int = 1000, ) -> list[dict[str, Any]]: if not cells or not format_table: return [] formats_by_index: dict[int, dict[str, Any]] = {} for item in format_table: if not isinstance(item, dict): continue try: format_index = int(item.get("format_index")) except (TypeError, ValueError): continue if format_index > 0: formats_by_index[format_index] = item if not formats_by_index: return [] links: list[dict[str, Any]] = [] seen: set[tuple[int, int, int]] = set() for cell in cells: if not isinstance(cell, dict): continue try: format_index = int(cell.get("cell_id")) row = int(cell.get("row")) column = int(cell.get("column")) except (TypeError, ValueError): continue fmt = formats_by_index.get(format_index) if not fmt: continue key = (row, column, format_index) if key in seen: continue seen.add(key) links.append( { "row": row, "column": column, "one_based": {"row": row, "column": column}, "zero_based": {"row": row - 1, "column": column - 1}, "format_index": format_index, "format": { "format_index": fmt.get("format_index"), "font_index": fmt.get("font_index"), "width": fmt.get("width"), **({"font": fmt.get("font")} if fmt.get("font") else {}), **({"horizontal_alignment": fmt.get("horizontal_alignment")} if fmt.get("horizontal_alignment") else {}), **({"vertical_alignment": fmt.get("vertical_alignment")} if fmt.get("vertical_alignment") else {}), **({"text_placement": fmt.get("text_placement")} if fmt.get("text_placement") else {}), **({"text_color": fmt.get("text_color")} if fmt.get("text_color") else {}), **({"back_color": fmt.get("back_color")} if fmt.get("back_color") else {}), **({"fill_type": fmt.get("fill_type")} if fmt.get("fill_type") else {}), **({"borders": fmt.get("borders")} if fmt.get("borders") else {}), }, **({"text": cell.get("text")} if cell.get("text") else {}), "source": "moxel_cell_id_as_format_index", "confidence": "medium", "diagnostics": { "message": "In controlled MOXCEL fixtures this cell scalar matches XML /formatIndex. Validate on more one-property probes before treating it as an authoritative style binding." }, } ) if len(links) >= limit: break return links def summarize_moxel_cell_format_links( cells: list[dict[str, Any]], format_table: list[dict[str, Any]], cell_format_links: list[dict[str, Any]], ) -> dict[str, Any]: total_cells = len(cells or []) linked_cells = len(cell_format_links or []) format_count = len(format_table or []) used_indexes = sorted( { int(item.get("format_index")) for item in cell_format_links or [] if isinstance(item, dict) and str(item.get("format_index") or "").isdigit() } ) coverage = round(linked_cells * 100.0 / total_cells, 2) if total_cells else 0.0 confidence = "none" if linked_cells: if coverage >= 80 and format_count > 1 and len(used_indexes) > 1: confidence = "high" elif coverage >= 10 or len(used_indexes) > 1: confidence = "medium" else: confidence = "low" return { "cells": total_cells, "format_table": format_count, "cell_format_links": linked_cells, "linked_cells_ratio_percent": coverage, "distinct_format_indexes": len(used_indexes), "format_indexes_sample": used_indexes[:20], "confidence": confidence, "source": "moxel_cell_id_as_format_index", } def extract_moxel_format_style_index_table( format_table: list[dict[str, Any]], diagnostics: dict[str, Any] | None, ) -> dict[str, Any]: def nested_negative_style_code(record: Any) -> int | None: if not isinstance(record, dict): return None values = record.get("numeric_items") if isinstance(record.get("numeric_items"), list) else [] if len(values) == 1: try: value = int(values[0]) except (TypeError, ValueError): value = None if value is not None and value < 0: return value for child in record.get("child_records") or []: value = nested_negative_style_code(child) if value is not None: return value return None def nested_single_numeric(record: Any) -> int | None: if not isinstance(record, dict): return None values = record.get("numeric_items") if isinstance(record.get("numeric_items"), list) else [] if len(values) == 1: try: return int(values[0]) except (TypeError, ValueError): return None for child in record.get("child_records") or []: value = nested_single_numeric(child) if value is not None: return value return None def packed_color_payload(value: int) -> dict[str, Any]: return { "decimal": value, "hex": f"0x{value:06X}", "rgb_big_endian": { "red": (value >> 16) & 0xFF, "green": (value >> 8) & 0xFF, "blue": value & 0xFF, "hex": f"#{value:06X}", }, "rgb_little_endian": { "red": value & 0xFF, "green": (value >> 8) & 0xFF, "blue": (value >> 16) & 0xFF, "hex": f"#{value & 0xFF:02X}{(value >> 8) & 0xFF:02X}{(value >> 16) & 0xFF:02X}", }, } references_by_index: dict[int, dict[str, Any]] = {} for fmt in format_table or []: if not isinstance(fmt, dict): continue format_index = fmt.get("format_index") for role, payload in ( ("text_color", fmt.get("text_color")), ("back_color", fmt.get("back_color")), ): if not isinstance(payload, dict): continue try: style_index = int(payload.get("style_index")) except (TypeError, ValueError): continue ref = references_by_index.setdefault( style_index, { "style_index": style_index, "roles": [], "format_indexes": [], "source": "moxel_format_record_style_index", "confidence": "medium", }, ) if role not in ref["roles"]: ref["roles"].append(role) if format_index not in ref["format_indexes"]: ref["format_indexes"].append(format_index) borders = fmt.get("borders") if isinstance(borders, dict): try: style_index = int(borders.get("color_style_index")) except (TypeError, ValueError): style_index = None if style_index is not None: ref = references_by_index.setdefault( style_index, { "style_index": style_index, "roles": [], "format_indexes": [], "source": "moxel_format_record_style_index", "confidence": "medium", }, ) if "border_color" not in ref["roles"]: ref["roles"].append("border_color") if format_index not in ref["format_indexes"]: ref["format_indexes"].append(format_index) references = sorted(references_by_index.values(), key=lambda item: int(item.get("style_index") or 0)) used_indexes = {int(item["style_index"]) for item in references} format_positions = {str(item.get("tree_position") or "") for item in format_table or [] if isinstance(item, dict)} candidate_indexes = {value for value in used_indexes if value != 0} candidate_records: list[dict[str, Any]] = [] records = diagnostics.get("top_level_records") if isinstance(diagnostics, dict) and isinstance(diagnostics.get("top_level_records"), list) else [] style_object_candidates: list[dict[str, Any]] = [] seen_style_object_positions: set[str] = set() def collect_style_object_candidates(record: Any) -> None: if not isinstance(record, dict): return values_for_style_object = record.get("numeric_items") if isinstance(record.get("numeric_items"), list) else [] try: style_object_numbers = [int(value) for value in values_for_style_object] except (TypeError, ValueError): style_object_numbers = [] position = str(record.get("tree_position") or "") if ( position and position not in seen_style_object_positions and len(style_object_numbers) == 3 and style_object_numbers[0] == 4 and tuple(style_object_numbers[1:]) in {(3, 3), (0, 0)} ): nested_value = nested_single_numeric(record) if nested_value is not None: seen_style_object_positions.add(position) style_object: dict[str, Any] = { "tree_position": record.get("tree_position"), "record_type": style_object_numbers, "source": "moxel_style_object_candidate", "confidence": "low", } if nested_value < 0: style_object.update({"kind": "style_code", "style_code": nested_value}) elif 0 <= nested_value <= 0xFFFFFF: style_object.update({"kind": "packed_color", "color": packed_color_payload(nested_value)}) else: style_object.update({"kind": "numeric_value", "value": nested_value}) style_object_candidates.append(style_object) for child in record.get("child_records") or []: collect_style_object_candidates(child) for record in records: if not isinstance(record, dict): continue collect_style_object_candidates(record) if str(record.get("tree_position") or "") in format_positions: continue values = record.get("numeric_items") if isinstance(record.get("numeric_items"), list) else [] if len(values) < 2 or len(values) > 16: continue try: numbers = [int(value) for value in values] except (TypeError, ValueError): continue payload_values = numbers[1:] if not payload_values: continue overlap = sorted({value for value in payload_values if value in candidate_indexes}) if not overlap: continue if len(overlap) < min(len(candidate_indexes), 2) and len(candidate_indexes) > 1: continue candidate_records.append( { "tree_position": record.get("tree_position"), "head": numbers[0], "numeric_items": numbers, **({"child_records": record.get("child_records")} if record.get("child_records") else {}), "referenced_style_indexes": overlap, "coverage_percent": round(len(overlap) * 100 / len(candidate_indexes), 2) if candidate_indexes else 0.0, "source": "moxel_top_level_record_style_index_candidate", "confidence": "low", "diagnostics": { "message": "Candidate record overlaps with style indexes used by format records. It is not an authoritative color dictionary yet.", }, } ) candidate_records.sort( key=lambda item: ( -float(item.get("coverage_percent") or 0), len(item.get("numeric_items") or []), str(item.get("tree_position") or ""), ) ) code_candidates_by_index: dict[int, list[dict[str, Any]]] = {} for candidate in candidate_records: numeric_items = candidate.get("numeric_items") if isinstance(candidate.get("numeric_items"), list) else [] child_records = candidate.get("child_records") if isinstance(candidate.get("child_records"), list) else [] if len(numeric_items) < 2 or len(child_records) != len(numeric_items) - 1: continue for style_index, child in zip(numeric_items[1:], child_records, strict=False): try: style_index_value = int(style_index) except (TypeError, ValueError): continue if style_index_value not in used_indexes: continue style_code = nested_negative_style_code(child) if style_code is None: continue bucket = code_candidates_by_index.setdefault(style_index_value, []) candidate_item = { "style_code": style_code, "tree_position": child.get("tree_position") if isinstance(child, dict) else None, "source_record": candidate.get("tree_position"), "source": "moxel_style_index_candidate_child_negative_code", "confidence": "low", } if candidate_item not in bucket: bucket.append(candidate_item) for reference in references: style_index = int(reference.get("style_index") or 0) if 0 <= style_index < len(style_object_candidates): style_object = style_object_candidates[style_index] reference["style_object"] = { key: value for key, value in style_object.items() if key in {"tree_position", "record_type", "kind", "style_code", "color", "source", "confidence"} } reference["style_object_source"] = "moxel_style_index_as_style_object_ordinal" reference["style_object_confidence"] = "low" code_candidates = code_candidates_by_index.get(style_index) or [] if not code_candidates: continue reference["style_code_candidates"] = code_candidates[:8] distinct_codes = sorted({int(item["style_code"]) for item in code_candidates if item.get("style_code") is not None}) if len(distinct_codes) == 1: reference["style_code"] = distinct_codes[0] reference["style_code_confidence"] = "low" return { "schema": "moxel_format_style_index_table.v1", "style_references": references, "candidate_records": candidate_records[:20], "style_object_candidates": style_object_candidates[:100], "counts": { "style_references": len(references), "candidate_records": len(candidate_records), "style_object_candidates": len(style_object_candidates), }, "source": "moxel_format_records_and_top_level_diagnostics", "confidence": "medium" if references else "none", "diagnostics": [ { "code": "local_style_indexes_not_global_colors", "message": "MOXCEL format records expose local style indexes. Candidate records may help reverse engineer local dictionaries, but indexes must not be mapped to global color names without more evidence.", } ], } def enrich_moxel_format_table_with_style_references( format_table: list[dict[str, Any]], format_style_index_table: dict[str, Any], ) -> list[dict[str, Any]]: if not format_table or not isinstance(format_style_index_table, dict): return format_table refs_by_index: dict[int, dict[str, Any]] = {} def remember_ref(ref: Any) -> None: if not isinstance(ref, dict): return try: style_index = int(ref.get("style_index")) except (TypeError, ValueError): return refs_by_index[style_index] = ref for ref in format_style_index_table.get("style_references") or []: remember_ref(ref) for fmt in format_table: if not isinstance(fmt, dict): continue for key in ("text_color", "back_color"): payload = fmt.get(key) if isinstance(payload, dict): remember_ref(payload.get("style_reference")) borders = fmt.get("borders") if isinstance(borders, dict): remember_ref(borders.get("color_style_reference")) if not refs_by_index: return format_table def public_ref(ref: dict[str, Any]) -> dict[str, Any]: return { key: value for key, value in ref.items() if key in { "style_index", "roles", "style_code", "style_code_confidence", "style_code_candidates", "style_object", "style_object_source", "style_object_confidence", "source", "confidence", } } def resolved_ref(ref: dict[str, Any]) -> dict[str, Any] | None: style_object = ref.get("style_object") if isinstance(ref.get("style_object"), dict) else {} result: dict[str, Any] = {} if style_object.get("kind"): result["kind"] = style_object.get("kind") result["source"] = ref.get("style_object_source") or style_object.get("source") result["confidence"] = ref.get("style_object_confidence") or style_object.get("confidence") if style_object.get("style_code") is not None: result["style_code"] = style_object.get("style_code") if isinstance(style_object.get("color"), dict): result["color"] = style_object.get("color") elif ref.get("style_code") is not None: result = { "kind": "style_code", "style_code": ref.get("style_code"), "source": "moxel_style_code_candidate", "confidence": ref.get("style_code_confidence") or "low", } if ref.get("style_code") is not None and "style_code" not in result: result["style_code_candidate"] = ref.get("style_code") result["style_code_candidate_confidence"] = ref.get("style_code_confidence") or "low" return result or None enriched: list[dict[str, Any]] = [] for fmt in format_table: if not isinstance(fmt, dict): continue result = dict(fmt) for key in ("text_color", "back_color"): payload = result.get(key) if not isinstance(payload, dict): continue try: style_index = int(payload.get("style_index")) except (TypeError, ValueError): continue ref = refs_by_index.get(style_index) if ref: resolved = resolved_ref(ref) result[key] = { **payload, "style_reference": public_ref(ref), **({"resolved": resolved, "resolved_style": resolved} if resolved else {}), } borders = result.get("borders") if isinstance(borders, dict): try: style_index = int(borders.get("color_style_index")) except (TypeError, ValueError): style_index = None ref = refs_by_index.get(style_index) if style_index is not None else None if ref: resolved = resolved_ref(ref) result["borders"] = { **borders, "color_style_reference": public_ref(ref), **({"color_resolved": resolved} if resolved else {}), } enriched.append(result) return enriched def extract_moxel_record_diagnostics( tree: dict[str, Any] | None, *, dimensions: dict[str, Any] | None = None, max_samples: int = 80, max_nodes: int = 50000, ) -> dict[str, Any] | None: if not isinstance(tree, dict) or tree.get("type") != "list": return None rows = int((dimensions or {}).get("rows") or 0) columns = int((dimensions or {}).get("columns") or 0) head_counts: dict[int, int] = {} head_samples: dict[int, list[dict[str, Any]]] = {} samples: list[dict[str, Any]] = [] coordinate_like_samples: list[dict[str, Any]] = [] visited = 0 truncated = False def numeric_atoms(items: list[Any]) -> list[int]: values: list[int] = [] for item in items: value = moxel_int(item) if value is not None: values.append(value) return values def string_atoms(items: list[Any]) -> list[str]: values: list[str] = [] for item in items: if isinstance(item, dict) and item.get("type") == "string": value = moxel_scalar(item) if value: values.append(value) return values def looks_coordinate_like(numbers: list[int]) -> bool: if len(numbers) < 4: return False row_limit = rows if rows > 0 else 10000 column_limit = columns if columns > 0 else 10000 small = [value for value in numbers if 0 <= value <= max(row_limit, column_limit)] if len(small) < 4: return False for index in range(0, len(numbers) - 3): row1, col1, row2, col2 = numbers[index : index + 4] if 0 <= row1 <= row_limit and 0 <= row2 <= row_limit and 0 <= col1 <= column_limit and 0 <= col2 <= column_limit: if row1 != row2 or col1 != col2: return True return False def sample_record(path: str, items: list[Any], numbers: list[int], strings: list[str]) -> dict[str, Any]: return { "tree_position": path, "head": numbers[0] if numbers else None, "list_length": len(items), "numeric_items": numbers[:24], "numeric_items_truncated": len(numbers) > 24, "strings": strings[:8], "strings_truncated": len(strings) > 8, } def child_record_samples(items: list[Any], path: str, *, limit: int = 12, depth: int = 1) -> list[dict[str, Any]]: children: list[dict[str, Any]] = [] for index, item in enumerate(items): if len(children) >= limit: break if not isinstance(item, dict) or item.get("type") != "list": continue child_items = item.get("items") if isinstance(item.get("items"), list) else [] if not child_items: continue numbers = numeric_atoms(child_items) strings = string_atoms(child_items) child_path = f"{path}.{index}" child_record = { "tree_position": child_path, "head": numbers[0] if numbers else None, "list_length": len(child_items), "numeric_items": numbers[:24], "numeric_items_truncated": len(numbers) > 24, "strings": strings[:8], "strings_truncated": len(strings) > 8, } if depth > 1: nested = child_record_samples(child_items, child_path, limit=limit, depth=depth - 1) if nested: child_record["child_records"] = nested children.append(child_record) return children def walk(node: Any, path: str, depth: int) -> None: nonlocal visited, truncated if truncated or not isinstance(node, dict) or node.get("type") != "list": return visited += 1 if visited > max_nodes: truncated = True return items = node.get("items") or [] if isinstance(items, list) and items: head = moxel_int(items[0]) if head is not None: numbers = numeric_atoms(items) strings = string_atoms(items) head_counts[head] = head_counts.get(head, 0) + 1 record_sample = sample_record(path, items, numbers, strings) if len(head_samples.setdefault(head, [])) < 3: head_samples[head].append(record_sample) if len(samples) < max_samples: samples.append(record_sample) if len(coordinate_like_samples) < max_samples and looks_coordinate_like(numbers): coordinate_like_samples.append(record_sample) if depth >= 48: return for index, child in enumerate(items if isinstance(items, list) else []): walk(child, f"{path}.{index}", depth + 1) walk(tree, "$", 0) top_level_records: list[dict[str, Any]] = [] shape_map: dict[tuple[int, int, int, int], dict[str, Any]] = {} root_items = tree.get("items") if isinstance(tree.get("items"), list) else [] for index, child in enumerate(root_items): if not isinstance(child, dict) or child.get("type") != "list": continue child_items = child.get("items") if isinstance(child.get("items"), list) else [] if not child_items: continue head = moxel_int(child_items[0]) if head is None: continue numbers = numeric_atoms(child_items) strings = string_atoms(child_items) record_sample = sample_record(f"$.{index}", child_items, numbers, strings) children = child_record_samples(child_items, f"$.{index}", depth=2) if children: record_sample["child_records"] = children top_level_records.append(record_sample) shape_key = (head, len(child_items), len(numbers), len(strings)) shape = shape_map.setdefault( shape_key, { "head": head, "list_length": len(child_items), "numeric_count": len(numbers), "string_count": len(strings), "count": 0, "positions": [], "numeric_prefixes": [], }, ) shape["count"] += 1 if len(shape["positions"]) < 12: shape["positions"].append(record_sample.get("tree_position")) prefix = numbers[: min(len(numbers), 10)] if prefix and len(shape["numeric_prefixes"]) < 12 and prefix not in shape["numeric_prefixes"]: shape["numeric_prefixes"].append(prefix) sorted_heads = sorted(head_counts.items(), key=lambda item: (-item[1], item[0])) top_level_shapes = sorted(shape_map.values(), key=lambda item: (-int(item.get("count") or 0), int(item.get("head") or 0), int(item.get("list_length") or 0))) top_level_shape_candidates: list[dict[str, Any]] = [] for shape in shape_map.values(): count = int(shape.get("count") or 0) list_length = int(shape.get("list_length") or 0) numeric_count = int(shape.get("numeric_count") or 0) head = int(shape.get("head") or 0) coordinate_like = any(looks_coordinate_like(prefix) for prefix in shape.get("numeric_prefixes") or []) score = 0 reasons: list[str] = [] if count <= 3: score += 5 reasons.append("rare_shape") if list_length >= 10: score += 4 reasons.append("long_record") if numeric_count >= 10: score += 3 reasons.append("many_numeric_fields") if abs(head) >= 100000: score += 2 reasons.append("large_head_code") if coordinate_like: score += 2 reasons.append("coordinate_like_prefix") if score <= 0: continue candidate = dict(shape) suggested_windows = [] for position in (shape.get("positions") or [])[:3]: if not isinstance(position, str): continue match = re.fullmatch(r"\$\.(\d+)", position) if not match: continue center = int(match.group(1)) start = max(0, center - 2) end = center + 2 suggested_windows.append( { "center": center, "start": start, "end": end, "tree_position": position, "request_hint": { "sections": "moxel_records", "moxel_record_start": start, "moxel_record_end": end, "moxel_record_heads": str(head), "moxel_record_context": 2, }, } ) candidate.update( { "score": score, "reasons": reasons, "coordinate_like": coordinate_like, "suggested_windows": suggested_windows, "confidence": "low", "source": "heuristic_top_level_shape", } ) top_level_shape_candidates.append(candidate) top_level_shape_candidates.sort(key=lambda item: (-int(item.get("score") or 0), int(item.get("count") or 0), -int(item.get("list_length") or 0), int(item.get("head") or 0))) for rank, candidate in enumerate(top_level_shape_candidates, start=1): candidate["rank"] = rank for window in candidate.get("suggested_windows") or []: if isinstance(window, dict) and isinstance(window.get("request_hint"), dict): window["request_hint"]["moxel_candidate_rank"] = rank candidate_reason_counts: dict[str, int] = {} candidate_score_counts: dict[int, int] = {} for candidate in top_level_shape_candidates: score = int(candidate.get("score") or 0) candidate_score_counts[score] = candidate_score_counts.get(score, 0) + 1 for reason in candidate.get("reasons") or []: reason_text = str(reason or "") if reason_text: candidate_reason_counts[reason_text] = candidate_reason_counts.get(reason_text, 0) + 1 candidate_scores = sorted(candidate_score_counts) top_level_candidate_summary = { "total": len(top_level_shape_candidates), "score_min": candidate_scores[0] if candidate_scores else None, "score_max": candidate_scores[-1] if candidate_scores else None, "score_counts": [{"score": score, "count": candidate_score_counts[score]} for score in sorted(candidate_score_counts, reverse=True)], "reason_counts": [ {"reason": reason, "count": count} for reason, count in sorted(candidate_reason_counts.items(), key=lambda item: (-item[1], item[0])) ], } return { "schema": "moxel_record_diagnostics.v1", "status": "ok", "authoritative_merge_decoder": False, "records_scanned": visited, "truncated": truncated, "head_counts": [{"head": head, "count": count} for head, count in sorted_heads[:200]], "head_samples": [ {"head": head, "count": count, "samples": head_samples.get(head) or []} for head, count in sorted_heads[:80] ], "top_level_records": top_level_records[:2000], "top_level_shapes": top_level_shapes[:500], "top_level_candidate_summary": top_level_candidate_summary, "top_level_shape_candidates": top_level_shape_candidates[:100], "samples": samples, "coordinate_like_samples": coordinate_like_samples, "notes": [ "These records are parser-level MOXCEL list diagnostics for reverse engineering.", "coordinate_like_samples are heuristics and must not be treated as merged-cell records.", ], } def moxel_index_runs(indexes: list[int]) -> list[dict[str, int]]: if not indexes: return [] ordered = sorted(set(indexes)) runs: list[dict[str, int]] = [] start = previous = ordered[0] for value in ordered[1:]: if value == previous + 1: previous = value continue runs.append({"start": start, "end": previous, "length": previous - start + 1}) start = previous = value runs.append({"start": start, "end": previous, "length": previous - start + 1}) return runs def summarize_moxel_numeric_block_records(records: list[dict[str, Any]], *, limit: int = 120) -> dict[str, Any]: record_summaries: list[dict[str, Any]] = [] slot_values: dict[int, dict[int, int]] = {} packed_indexes: dict[int, list[int]] = {} small_indexes: dict[int, list[int]] = {} shape_counts: dict[str, int] = {} for record_index, record in enumerate(records, start=1): numbers = [value for value in (record.get("numeric_items") or []) if isinstance(value, int)] if not numbers: continue shape = f"{numbers[0]}:{len(numbers)}" shape_counts[shape] = shape_counts.get(shape, 0) + 1 packed_columns = sorted({value // 32 for value in numbers if value > 0 and value <= 4096 and value % 32 == 0}) small_scalars = sorted({value for value in numbers if 2 <= value <= 512}) for value in packed_columns: packed_indexes.setdefault(value, []).append(record_index) for value in small_scalars: small_indexes.setdefault(value, []).append(record_index) for slot, value in enumerate(numbers): counts = slot_values.setdefault(slot, {}) counts[value] = counts.get(value, 0) + 1 if len(record_summaries) < limit: record_summaries.append( { "record_index": record_index, "tree_position": record.get("tree_position"), "shape": shape, "head": numbers[0], "numeric_count": len(numbers), "packed_div32_values": packed_columns, "small_scalars": small_scalars[:40], } ) slot_summary = [] for slot, counts in sorted(slot_values.items())[:40]: values = sorted(counts) div32_values = sorted({value // 32 for value in values if value > 0 and value <= 4096 and value % 32 == 0}) slot_summary.append( { "slot": slot, "distinct_values": len(values), "min": values[0] if values else None, "max": values[-1] if values else None, "top_values": [ {"value": value, "count": count} for value, count in sorted(counts.items(), key=lambda item: (-item[1], item[0]))[:12] ], "packed_div32_values": div32_values[:40], } ) return { "schema": "moxel_numeric_block_records.v1", "records_analyzed": len(records), "records_returned": len(record_summaries), "shape_summary": [ {"shape": shape, "count": count} for shape, count in sorted(shape_counts.items(), key=lambda item: (-item[1], item[0]))[:30] ], "numeric_slot_summary": slot_summary, "packed_div32_by_value": [ {"value": value, "count": len(indexes), "record_indexes": indexes[:40], "runs": moxel_index_runs(indexes)[:12]} for value, indexes in sorted(packed_indexes.items(), key=lambda item: item[0])[:80] ], "small_scalar_by_value": [ {"value": value, "count": len(indexes), "record_indexes": indexes[:40], "runs": moxel_index_runs(indexes)[:12]} for value, indexes in sorted(small_indexes.items(), key=lambda item: (-len(item[1]), item[0]))[:80] ], "records": record_summaries, } def extract_moxel_merge_record_block_candidates(diagnostics: dict[str, Any] | None) -> list[dict[str, Any]]: if not isinstance(diagnostics, dict): return [] records = diagnostics.get("top_level_records") if isinstance(diagnostics.get("top_level_records"), list) else [] candidates: list[dict[str, Any]] = [] for index, record in enumerate(records): if not isinstance(record, dict): continue numbers = record.get("numeric_items") if not isinstance(numbers, list) or len(numbers) != 1: continue try: declared_count = int(numbers[0]) except (TypeError, ValueError): continue if declared_count < 2: continue following = [item for item in records[index + 1 : index + 1 + declared_count] if isinstance(item, dict)] if len(following) < min(declared_count, 5): continue coordinate_like = [ item for item in following if any("coordinate_like" in str(reason) for reason in (item.get("reasons") or [])) or len([value for value in (item.get("numeric_items") or []) if isinstance(value, int) and value >= 0]) >= 4 ] long_numeric = [item for item in following if len(item.get("numeric_items") or []) >= 4] if len(long_numeric) < max(2, min(declared_count, 8)): continue string_records = [item for item in following if item.get("strings")] if len(string_records) >= max(1, min(declared_count, 3)): continue shape_counts: dict[str, int] = {} for item in following: item_numbers = item.get("numeric_items") if isinstance(item.get("numeric_items"), list) else [] if not item_numbers: continue head = item_numbers[0] shape = f"{head}:{len(item_numbers)}" shape_counts[shape] = shape_counts.get(shape, 0) + 1 shape_summary = [ {"shape": shape, "count": count} for shape, count in sorted(shape_counts.items(), key=lambda item: (-item[1], item[0]))[:20] ] div32_values = sorted( { int(value) // 32 for item in following for value in (item.get("numeric_items") or []) if isinstance(value, int) and value > 0 and value <= 4096 and value % 32 == 0 } ) column_edge_hints = [ { "column_or_edge": value, "source": "moxel_merge_block_scalar_div32", "confidence": "medium" if value > 1 else "low", } for value in div32_values ] small_scalar_positions: dict[int, list[str]] = {} small_scalar_counts: dict[int, int] = {} for item in following: position = str(item.get("tree_position") or "") for value in item.get("numeric_items") or []: if not isinstance(value, int) or value < 2 or value > 512: continue small_scalar_counts[value] = small_scalar_counts.get(value, 0) + 1 positions = small_scalar_positions.setdefault(value, []) if position and len(positions) < 8 and position not in positions: positions.append(position) row_or_size_hints = [ { "value": value, "count": small_scalar_counts[value], "positions": small_scalar_positions.get(value) or [], "source": "moxel_merge_block_small_scalar", "confidence": "low", } for value in sorted(small_scalar_counts, key=lambda item: (-small_scalar_counts[item], item))[:40] ] record_analysis = summarize_moxel_numeric_block_records(following) sample_records = [ { "record_index": record_index, "tree_position": item.get("tree_position"), "numeric_items": item.get("numeric_items"), "shape": ( f"{(item.get('numeric_items') or [None])[0]}:{len(item.get('numeric_items') or [])}" if item.get("numeric_items") else None ), "packed_div32_values": [ int(value) // 32 for value in (item.get("numeric_items") or []) if isinstance(value, int) and value > 0 and value <= 4096 and value % 32 == 0 ], "small_scalars": [ int(value) for value in (item.get("numeric_items") or []) if isinstance(value, int) and 2 <= value <= 512 ][:40], } for record_index, item in enumerate(following[:20], start=1) ] first_position = following[0].get("tree_position") if following else None last_position = following[-1].get("tree_position") if following else None confidence = "medium" if len(long_numeric) >= min(declared_count, 20) else "low" candidates.append( { "count": declared_count, "tree_position": record.get("tree_position"), "record_window": { "start": first_position, "end": last_position, "inspected": len(following), }, "evidence": { "singleton_count_record": record.get("numeric_items"), "following_long_numeric_records": len(long_numeric), "following_coordinate_like_records": len(coordinate_like), "shape_summary": shape_summary, "column_edge_hints": column_edge_hints, "row_or_size_hints": row_or_size_hints, "record_analysis": record_analysis, }, "sample_records": sample_records, "source": "moxel_top_level_count_before_coordinate_block", "confidence": confidence, "diagnostics": { "message": "MOXCEL top-level singleton count followed by numeric coordinate-like records. This is a merge-record block candidate, not an authoritative merged range decoder." }, } ) return candidates[:20] def extract_moxel_merge_count_hints(diagnostics: dict[str, Any] | None) -> list[dict[str, Any]]: if not isinstance(diagnostics, dict): return [] records = diagnostics.get("top_level_records") if isinstance(diagnostics.get("top_level_records"), list) else [] hints: list[dict[str, Any]] = [] for index, record in enumerate(records): if not isinstance(record, dict): continue numbers = record.get("numeric_items") if not isinstance(numbers, list) or len(numbers) != 1: continue try: count = int(numbers[0]) except (TypeError, ValueError): continue if count < 0 or count > 1000: continue previous = records[index - 1] if index > 0 and isinstance(records[index - 1], dict) else None next_records = [item for item in records[index + 1 : index + 4] if isinstance(item, dict)] zero_followers = [ item for item in next_records[:2] if isinstance(item.get("numeric_items"), list) and len(item.get("numeric_items") or []) == 1 and int((item.get("numeric_items") or [None])[0] or 0) == 0 ] previous_numbers = previous.get("numeric_items") if isinstance(previous, dict) and isinstance(previous.get("numeric_items"), list) else [] following_named_count = next_records[2] if len(next_records) >= 3 and isinstance(next_records[2], dict) else None following_named_numbers = ( following_named_count.get("numeric_items") if isinstance(following_named_count, dict) and isinstance(following_named_count.get("numeric_items"), list) else [] ) if len(previous_numbers) < 8 or len(zero_followers) < 2: continue named_count = None if len(following_named_numbers) == 1: try: named_count = int(following_named_numbers[0]) except (TypeError, ValueError): named_count = None confidence = "high" if named_count is None or named_count >= count else "medium" hints.append( { "count": count, "tree_position": record.get("tree_position"), "source": "moxel_top_level_merge_count_hint", "confidence": confidence, "evidence": { "singleton_count_record": record.get("numeric_items"), "previous_record": { "tree_position": previous.get("tree_position") if isinstance(previous, dict) else None, "numeric_count": len(previous_numbers), "numeric_items": previous_numbers[:32], }, "zero_followers": [ {"tree_position": item.get("tree_position"), "numeric_items": item.get("numeric_items")} for item in zero_followers ], "following_named_item_count": named_count, "following_named_item_count_position": ( following_named_count.get("tree_position") if isinstance(following_named_count, dict) else None ), }, "diagnostics": { "message": "MOXCEL singleton count in the observed merge-count slot. This confirms merge count, not merged range coordinates." }, } ) return hints[:20] def filter_moxel_merge_record_block_candidates_by_count_hints( candidates: list[dict[str, Any]], count_hints: list[dict[str, Any]], ) -> list[dict[str, Any]]: if not count_hints: return candidates hinted_counts: set[int] = set() for hint in count_hints: if not isinstance(hint, dict): continue try: count = int(hint.get("count")) except (TypeError, ValueError): continue if count > 0: hinted_counts.add(count) if not hinted_counts: return [] return [ candidate for candidate in candidates if isinstance(candidate, dict) and isinstance(candidate.get("count"), int) and int(candidate.get("count") or 0) in hinted_counts ] def moxel_exclusive_edge_to_inclusive(start: int, end: int) -> int: if end > start: return end - 1 return end def moxel_top_level_node_at_position(tree: dict[str, Any] | None, position: str) -> dict[str, Any] | None: if not isinstance(tree, dict) or tree.get("type") != "list": return None match = re.fullmatch(r"\$\.(\d+)", str(position or "")) if not match: return None root_items = tree.get("items") if isinstance(tree.get("items"), list) else [] index = int(match.group(1)) if index < 0 or index >= len(root_items): return None node = root_items[index] return node if isinstance(node, dict) and node.get("type") == "list" else None def moxel_merge_record_child_count(node: dict[str, Any] | None) -> int: if not isinstance(node, dict) or node.get("type") != "list": return 0 items = node.get("items") if isinstance(node.get("items"), list) else [] count = 0 for child in items[1:]: if not isinstance(child, dict) or child.get("type") != "list": continue child_items = child.get("items") if isinstance(child.get("items"), list) else [] numbers = [moxel_int(value) for value in child_items[:5]] if len(numbers) >= 5 and all(value is not None for value in numbers): count += 1 return count def filter_moxel_merge_count_hints_by_tree( tree: dict[str, Any] | None, count_hints: list[dict[str, Any]], ) -> list[dict[str, Any]]: if not count_hints: return [] filtered: list[dict[str, Any]] = [] for hint in count_hints: if not isinstance(hint, dict): continue try: count = int(hint.get("count")) except (TypeError, ValueError): continue if count <= 0: filtered.append(hint) continue node = moxel_top_level_node_at_position(tree, str(hint.get("tree_position") or "")) child_count = moxel_merge_record_child_count(node) if child_count >= count: filtered.append( { **hint, "evidence": { **(hint.get("evidence") if isinstance(hint.get("evidence"), dict) else {}), "merge_record_children": child_count, }, } ) return filtered def filter_moxel_merge_count_hints_by_ranges( count_hints: list[dict[str, Any]], merged_ranges: list[dict[str, Any]], ) -> list[dict[str, Any]]: if not count_hints: return [] has_ranges = bool(merged_ranges) filtered: list[dict[str, Any]] = [] for hint in count_hints: if not isinstance(hint, dict): continue try: count = int(hint.get("count")) except (TypeError, ValueError): continue if count <= 0 or has_ranges: filtered.append(hint) return filtered def extract_moxel_merged_ranges_from_tree( tree: dict[str, Any] | None, count_hints: list[dict[str, Any]], *, limit: int = 200, ) -> list[dict[str, Any]]: if not isinstance(tree, dict) or tree.get("type") != "list": return [] hint_by_position: dict[str, dict[str, Any]] = { str(hint.get("tree_position") or ""): hint for hint in count_hints if isinstance(hint, dict) and str(hint.get("tree_position") or "") } if not hint_by_position: return [] root_items = tree.get("items") if isinstance(tree.get("items"), list) else [] ranges: list[dict[str, Any]] = [] for index, node in enumerate(root_items): position = f"$.{index}" hint = hint_by_position.get(position) if not hint or not isinstance(node, dict) or node.get("type") != "list": continue items = node.get("items") if isinstance(node.get("items"), list) else [] if not items: continue declared_count = moxel_int(items[0]) try: hinted_count = int(hint.get("count")) except (TypeError, ValueError): hinted_count = None if declared_count is None or declared_count < 1 or hinted_count != declared_count: continue decoded_for_block = 0 for offset, child in enumerate(items[1:], start=1): if len(ranges) >= limit: break if not isinstance(child, dict) or child.get("type") != "list": continue child_items = child.get("items") if isinstance(child.get("items"), list) else [] numbers = [moxel_int(value) for value in child_items] if len(numbers) < 5 or any(value is None for value in numbers[:5]): continue left, top, right_edge, bottom_edge, flag = [int(value) for value in numbers[:5] if value is not None] if min(left, top, right_edge, bottom_edge) < 0: continue right = moxel_exclusive_edge_to_inclusive(left, right_edge) bottom = moxel_exclusive_edge_to_inclusive(top, bottom_edge) if right < left or bottom < top: continue range_info = moxel_range(top, left, bottom, right) if int(range_info.get("width") or 0) <= 1 and int(range_info.get("height") or 0) <= 1: continue decoded_for_block += 1 ranges.append( { "range": range_info, "source": "moxel_tree_merge_block", "confidence": "high", "tree_position": f"{position}.{offset}", "record_index": offset, "raw": { "left": left, "top": top, "right_exclusive": right_edge, "bottom_exclusive": bottom_edge, "flag": flag, }, "evidence": { "count_record": position, "count": declared_count, "coordinate_order": "left,top,rightExclusive,bottomExclusive,flag", }, } ) if decoded_for_block and decoded_for_block == declared_count: break return ranges[:limit] def moxel_range_contains_cell(range_info: dict[str, Any], cell: dict[str, Any]) -> bool: zero = range_info.get("zero_based") if isinstance(range_info.get("zero_based"), dict) else {} cell_zero = cell.get("zero_based") if isinstance(cell.get("zero_based"), dict) else {} try: row = int(cell_zero.get("row")) column = int(cell_zero.get("column")) return int(zero.get("top")) <= row <= int(zero.get("bottom")) and int(zero.get("left")) <= column <= int(zero.get("right")) except Exception: return False def infer_moxel_merged_range_candidates(named_areas: list[dict[str, Any]], cells: list[dict[str, Any]], *, limit: int = 200) -> list[dict[str, Any]]: candidates: list[dict[str, Any]] = [] seen: set[tuple[int, int, int, int, str]] = set() for area in named_areas: if not isinstance(area, dict) or not isinstance(area.get("range"), dict): continue range_info = area.get("range") or {} width = int(range_info.get("width") or 0) height = int(range_info.get("height") or 0) if width <= 1 and height <= 1: continue zero = range_info.get("zero_based") if isinstance(range_info.get("zero_based"), dict) else {} try: key = (int(zero.get("top")), int(zero.get("left")), int(zero.get("bottom")), int(zero.get("right")), str(area.get("name") or "")) except Exception: continue if key in seen: continue seen.add(key) contained_cells = [cell for cell in cells if isinstance(cell, dict) and moxel_range_contains_cell(range_info, cell)] if len(contained_cells) > 1: continue candidate = { "name": area.get("name"), "occurrence": area.get("occurrence"), "range": range_info, "source": "heuristic_named_area_range", "confidence": "low", "diagnostics": { "message": "Named area spans multiple rows or columns and contains at most one decoded text/parameter cell. This is a merge candidate, not an authoritative MOXCEL merged-cell record." }, } if contained_cells: candidate["cell"] = { "row": contained_cells[0].get("row"), "column": contained_cells[0].get("column"), "text": contained_cells[0].get("text"), **({"parameter": contained_cells[0].get("parameter")} if contained_cells[0].get("parameter") else {}), } candidates.append(candidate) if len(candidates) >= limit: break return candidates MOXEL_PLACEHOLDER_RE = re.compile(r"\[([A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]*)\]") MOXEL_IDENTIFIER_RE = re.compile(r"^[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{2,80}$") def extract_moxel_cell_parameters(cells: list[dict[str, Any]]) -> list[dict[str, Any]]: parameters: list[dict[str, Any]] = [] seen: set[tuple[str, int, int, str]] = set() for cell in cells: if not isinstance(cell, dict): continue row = int(cell.get("row") or 0) column = int(cell.get("column") or 0) direct_parameter = str(cell.get("parameter") or "").strip() if direct_parameter: key = (direct_parameter.casefold(), row, column, "cell_parameter") if key not in seen: seen.add(key) parameters.append( { "name": direct_parameter, "row": row, "column": column, "one_based": cell.get("one_based"), "source": "cell_parameter", "cell_text": cell.get("text"), } ) for placeholder in MOXEL_PLACEHOLDER_RE.findall(str(cell.get("text") or "")): key = (placeholder.casefold(), row, column, "placeholder") if key in seen: continue seen.add(key) parameters.append( { "name": placeholder, "row": row, "column": column, "one_based": cell.get("one_based"), "source": "placeholder", "cell_text": cell.get("text"), } ) return parameters def extract_moxel_cell_text_identifiers(cells: list[dict[str, Any]]) -> list[dict[str, Any]]: identifiers: list[dict[str, Any]] = [] seen: set[tuple[str, int, int]] = set() for cell in cells: if not isinstance(cell, dict): continue text = str(cell.get("text") or "").strip() if not MOXEL_IDENTIFIER_RE.fullmatch(text): continue row = int(cell.get("row") or 0) column = int(cell.get("column") or 0) key = (text.casefold(), row, column) if key in seen: continue seen.add(key) identifiers.append( { "name": text, "row": row, "column": column, "one_based": cell.get("one_based"), "source": "cell_text_identifier", **({"parameter": cell.get("parameter")} if cell.get("parameter") else {}), } ) return identifiers def moxel_area_cell_coverage(named_areas: list[dict[str, Any]], cells: list[dict[str, Any]], cell_parameters: list[dict[str, Any]], *, limit: int = 300) -> list[dict[str, Any]]: coverage: list[dict[str, Any]] = [] for area in named_areas[:limit]: if not isinstance(area, dict) or not isinstance(area.get("range"), dict): continue range_info = area.get("range") or {} area_cells = [cell for cell in cells if isinstance(cell, dict) and moxel_range_contains_cell(range_info, cell)] area_parameters = [ parameter for parameter in cell_parameters if isinstance(parameter, dict) and moxel_range_contains_cell( range_info, {"zero_based": {"row": int(parameter.get("row") or 1) - 1, "column": int(parameter.get("column") or 1) - 1}}, ) ] coverage.append( { "name": area.get("name"), "occurrence": area.get("occurrence"), "range": range_info, "cell_count": len(area_cells), "parameter_count": len(area_parameters), "cells": [ { "row": cell.get("row"), "column": cell.get("column"), "text": cell.get("text"), **({"parameter": cell.get("parameter")} if cell.get("parameter") else {}), } for cell in area_cells[:20] ], "parameters": [ { "name": parameter.get("name"), "row": parameter.get("row"), "column": parameter.get("column"), "source": parameter.get("source"), } for parameter in area_parameters[:20] ], } ) return coverage def infer_moxel_used_dimensions( *, capacity_dimensions: dict[str, Any] | None, cells: list[dict[str, Any]], named_areas: list[dict[str, Any]], named_range_candidates: list[dict[str, Any]], merged_ranges: list[dict[str, Any]], cell_coordinate_hints: list[dict[str, Any]] | None = None, ) -> dict[str, Any] | None: max_row = 0 max_column = 0 row_by_source: dict[str, int] = {} column_by_source: dict[str, int] = {} evidence: list[str] = [] def update_cell(row: Any, column: Any, source: str) -> None: nonlocal max_row, max_column try: row_value = int(row or 0) column_value = int(column or 0) except Exception: return if row_value > 0: max_row = max(max_row, row_value) row_by_source[source] = max(row_by_source.get(source, 0), row_value) if column_value > 0: max_column = max(max_column, column_value) column_by_source[source] = max(column_by_source.get(source, 0), column_value) if (row_value > 0 or column_value > 0) and source not in evidence: evidence.append(source) def update_range(range_info: Any, source: str) -> None: if not isinstance(range_info, dict): return one_based = range_info.get("one_based") if isinstance(range_info.get("one_based"), dict) else {} update_cell(one_based.get("bottom") or one_based.get("row_end"), one_based.get("right") or one_based.get("column_end"), source) for cell in cells: if isinstance(cell, dict): update_cell(cell.get("row"), cell.get("column"), "cells") for hint in cell_coordinate_hints or []: if isinstance(hint, dict): one_based = hint.get("one_based") if isinstance(hint.get("one_based"), dict) else {} update_cell(one_based.get("row"), one_based.get("column"), "cell_coordinate_hints") for area in named_areas: if isinstance(area, dict): update_range(area.get("range"), "named_areas") for candidate in named_range_candidates: if isinstance(candidate, dict): update_range(candidate.get("range"), "named_ranges") for merged_range in merged_ranges: if isinstance(merged_range, dict): update_range(merged_range.get("range"), "merged_ranges") if not max_row and not max_column: return None if column_by_source.get("cell_coordinate_hints"): preferred_column_sources = [ value for source, value in column_by_source.items() if source != "cells" and value > 0 ] if preferred_column_sources: max_column = max(preferred_column_sources) capacity_rows = int((capacity_dimensions or {}).get("rows") or 0) capacity_columns = int((capacity_dimensions or {}).get("columns") or 0) return { "rows": max_row or None, "columns": max_column or None, "evidence": evidence, "bounded_by_capacity": { "rows": bool(capacity_rows and max_row <= capacity_rows), "columns": bool(capacity_columns and max_column <= capacity_columns), }, } def infer_moxel_format_dimensions( *, capacity_dimensions: dict[str, Any] | None, column_widths: list[dict[str, Any]], row_heights: list[dict[str, Any]], ) -> dict[str, Any] | None: max_row = 0 max_column = 0 evidence: list[str] = [] for width in column_widths: if not isinstance(width, dict): continue try: column = int(width.get("column") or 0) except Exception: continue if column > 0: max_column = max(max_column, column) if "column_widths" not in evidence: evidence.append("column_widths") for height in row_heights: if not isinstance(height, dict): continue try: row = int(height.get("row") or 0) except Exception: continue if row > 0: max_row = max(max_row, row) if "row_heights" not in evidence: evidence.append("row_heights") if not max_row and not max_column: return None capacity_rows = int((capacity_dimensions or {}).get("rows") or 0) capacity_columns = int((capacity_dimensions or {}).get("columns") or 0) return { "rows": max_row or None, "columns": max_column or None, "evidence": evidence, "bounded_by_capacity": { "rows": bool(not max_row or capacity_rows and max_row <= capacity_rows), "columns": bool(not max_column or capacity_columns and max_column <= capacity_columns), }, } def moxel_structure_counts(structure: dict[str, Any]) -> dict[str, int]: cell_style_candidates = structure.get("cell_style_candidates") or [] return { "named_areas": len(structure.get("named_areas") or []), "named_range_candidates": len(structure.get("named_range_candidates") or []), "parameters": len(structure.get("parameters") or []), "cell_parameters": len(structure.get("cell_parameters") or []), "cell_text_identifiers": len(structure.get("cell_text_identifiers") or []), "cell_style_candidates": len(cell_style_candidates), "cell_style_coordinate_hints": len( [ item for item in cell_style_candidates if isinstance(item, dict) and isinstance(item.get("coordinate_hints"), dict) ] ), "cell_coordinate_hints": len(structure.get("cell_coordinate_hints") or []), "cells": len(structure.get("cells") or []), "area_cell_coverage": len(structure.get("area_cell_coverage") or []), "column_widths": len(structure.get("column_widths") or []), "format_table": len(structure.get("format_table") or []), "font_table": len(structure.get("font_table") or []), "format_style_index_table": len((structure.get("format_style_index_table") or {}).get("style_references") or []), "cell_format_links": len(structure.get("cell_format_links") or []), "merged_ranges": len(structure.get("merged_ranges") or []), "merged_range_candidates": len(structure.get("merged_range_candidates") or []), "merge_record_block_candidates": len(structure.get("merge_record_block_candidates") or []), "merge_count_hints": len(structure.get("merge_count_hints") or []), "row_heights": len(structure.get("row_heights") or []), } def extract_moxel_cell_coordinate_hints( cells: list[dict[str, Any]], cell_style_candidates: list[dict[str, Any]], *, limit: int = 1000, ) -> list[dict[str, Any]]: hints: list[dict[str, Any]] = [] seen: set[tuple[Any, ...]] = set() cells_by_text: dict[str, list[dict[str, Any]]] = {} cells_by_id: dict[int, list[dict[str, Any]]] = {} for cell in cells: if not isinstance(cell, dict): continue text = str(cell.get("text") or "") if text: cells_by_text.setdefault(text, []).append(cell) try: cell_id = int(cell.get("cell_id")) except Exception: cell_id = None if cell_id is not None: cells_by_id.setdefault(cell_id, []).append(cell) for style in cell_style_candidates: if not isinstance(style, dict) or not isinstance(style.get("coordinate_hints"), dict): continue coordinate_hint = style.get("coordinate_hints") or {} one_based_hint = coordinate_hint.get("one_based") if isinstance(coordinate_hint.get("one_based"), dict) else {} zero_based_hint = coordinate_hint.get("zero_based") if isinstance(coordinate_hint.get("zero_based"), dict) else {} text = str(style.get("text") or "") matched_cells: list[dict[str, Any]] = [] try: style_cell_id = int(style.get("cell_id")) except Exception: style_cell_id = None if style_cell_id is not None: matched_cells.extend(cells_by_id.get(style_cell_id) or []) if text: for cell in cells_by_text.get(text) or []: if cell not in matched_cells: matched_cells.append(cell) matched_cell = matched_cells[0] if matched_cells else {} matched_one_based = matched_cell.get("one_based") if isinstance(matched_cell.get("one_based"), dict) else {} matched_zero_based = matched_cell.get("zero_based") if isinstance(matched_cell.get("zero_based"), dict) else {} one_based = { **({"row": matched_one_based.get("row")} if matched_one_based.get("row") is not None else {}), **({"column": one_based_hint.get("column")} if one_based_hint.get("column") is not None else {}), } zero_based = { **({"row": matched_zero_based.get("row")} if matched_zero_based.get("row") is not None else {}), **({"column": zero_based_hint.get("column")} if zero_based_hint.get("column") is not None else {}), } key = (text, style.get("tree_position"), one_based.get("row"), one_based.get("column"), style.get("cell_id")) if key in seen: continue seen.add(key) hints.append( { "text": text or None, "cell_id": style.get("cell_id"), "tree_position": style.get("tree_position"), "one_based": one_based, "zero_based": zero_based, "confidence": coordinate_hint.get("confidence") or "medium", "source": "moxel_inline_text_coordinate_hint", "evidence": { "column": coordinate_hint.get("source"), **( { "row": "matched_decoded_cell_row", "matched_cell": { "row": matched_one_based.get("row"), "column": matched_one_based.get("column"), "cell_id": matched_cell.get("cell_id"), "source": matched_cell.get("source"), }, } if matched_cell else {} ), }, } ) if len(hints) >= limit: break return hints def extract_moxel_public_structure(data: bytes, *, max_strings: int = 300) -> dict[str, Any]: try: from parser.payload import decode_payload_lossless decoded = decode_payload_lossless(data) payload_bytes = bytes(decoded.get("payload") or b"") except Exception: payload_bytes = bytes(data or b"") decoded = {} moxel_text, moxel_text_info = decode_moxel_text_payload(payload_bytes) moxel_tree = None if moxel_text: try: from parser.payload import parse_brace_text moxel_tree = parse_brace_text(moxel_text) except Exception: moxel_tree = None tree_root_summary = None if isinstance(moxel_tree, dict): root_items = moxel_tree.get("items") if isinstance(moxel_tree.get("items"), list) else [] tree_root_summary = { "type": moxel_tree.get("type"), "items_count": len(root_items), "head": moxel_scalar(root_items[0]) if root_items else None, } text_candidates: list[str] = [] if moxel_text: text_candidates.append(moxel_text) for encoding in ("utf-16-le", "utf-8-sig", "utf-8", "cp1251"): try: text = payload_bytes.decode(encoding, errors="ignore") if text not in text_candidates: text_candidates.append(text) except Exception: continue strings: list[str] = [] seen: set[str] = set() text_re = re.compile(r"[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_ .:/\\-]{1,120}") for text in text_candidates: cleaned = text.replace("\x00", " ") for match in text_re.finditer(cleaned): value = " ".join(match.group(0).split()).strip(" .:/\\-") if len(value) < 2 or value.casefold() in seen: continue seen.add(value.casefold()) strings.append(value) if len(strings) >= max_strings: break if len(strings) >= max_strings: break parameter_names: list[str] = [] parameter_seen: set[str] = set() parameter_patterns = [ re.compile(r"&([A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]*)&"), re.compile(r"\{([A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]*)\}"), ] joined = "\n".join(text_candidates) for pattern in parameter_patterns: for match in pattern.finditer(joined): value = match.group(1) if value.casefold() not in parameter_seen: parameter_seen.add(value.casefold()) parameter_names.append(value) for value in strings: if len(parameter_names) >= 200: break if re.fullmatch(r"[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{2,60}", value) and value.casefold() not in parameter_seen: if any(marker in value.casefold() for marker in ("код", "дата", "сумма", "количество", "номенклатура", "период", "строка", "итог")): parameter_seen.add(value.casefold()) parameter_names.append(value) named_range_candidates = extract_moxel_named_range_candidates_from_tree(moxel_tree) named_areas = extract_moxel_named_areas(moxel_text) if moxel_tree: seen_named_areas = {(str(item.get("name") or "").casefold(), item.get("occurrence") or 1) for item in named_areas} for item in extract_moxel_named_area_candidates_from_tree(moxel_tree): key = (str(item.get("name") or "").casefold(), item.get("occurrence") or 1) if key not in seen_named_areas: seen_named_areas.add(key) named_areas.append(item) if not named_areas: named_area_names: list[str] = [] named_area_seen: set[str] = set() for value in strings: if re.fullmatch(r"[A-Za-zА-Яа-яЁё0-9_]*Област[A-Za-zА-Яа-яЁё0-9_]{0,80}", value) and value.casefold() not in named_area_seen: named_area_seen.add(value.casefold()) named_area_names.append(value) named_areas = [ { "name": value, "source": "best_effort_text", "range": None, "diagnostics": {"message": "Named area text was found in MOXCEL payload, but exact coordinates are not decoded yet."}, } for value in named_area_names ] capacity_dimensions = extract_moxel_dimensions(moxel_text) cells = extract_moxel_cells_from_tree(moxel_tree) cell_style_candidates = extract_moxel_cell_style_candidates_from_tree(moxel_tree) cell_coordinate_hints = extract_moxel_cell_coordinate_hints(cells, cell_style_candidates) cell_parameters = extract_moxel_cell_parameters(cells) cell_text_identifiers = extract_moxel_cell_text_identifiers(cells) column_widths = extract_moxel_column_widths_from_tree(moxel_tree) format_table = extract_moxel_format_table_from_tree(moxel_tree) row_heights: list[dict[str, Any]] = [] moxel_record_diagnostics = extract_moxel_record_diagnostics(moxel_tree, dimensions=capacity_dimensions) font_table = extract_moxel_font_table_from_diagnostics(moxel_record_diagnostics) if not format_table: format_table = extract_moxel_format_table_from_diagnostics(moxel_record_diagnostics) format_table = enrich_moxel_format_table_with_fonts(format_table, font_table) format_style_index_table = extract_moxel_format_style_index_table(format_table, moxel_record_diagnostics) format_table = enrich_moxel_format_table_with_style_references(format_table, format_style_index_table) cell_format_links = extract_moxel_cell_format_links(cells, format_table) cell_format_link_stats = summarize_moxel_cell_format_links(cells, format_table, cell_format_links) merged_range_candidates = infer_moxel_merged_range_candidates(named_areas, cells) merge_record_block_candidates = extract_moxel_merge_record_block_candidates(moxel_record_diagnostics) merge_count_hints = extract_moxel_merge_count_hints(moxel_record_diagnostics) merge_count_hints = filter_moxel_merge_count_hints_by_tree(moxel_tree, merge_count_hints) merged_ranges = extract_moxel_merged_ranges_from_tree(moxel_tree, merge_count_hints) merge_count_hints = filter_moxel_merge_count_hints_by_ranges(merge_count_hints, merged_ranges) merge_record_block_candidates = filter_moxel_merge_record_block_candidates_by_count_hints( merge_record_block_candidates, merge_count_hints, ) area_cell_coverage = moxel_area_cell_coverage(named_areas, cells, cell_parameters) used_dimensions = infer_moxel_used_dimensions( capacity_dimensions=capacity_dimensions, cells=cells, named_areas=named_areas, named_range_candidates=named_range_candidates, merged_ranges=merged_ranges, cell_coordinate_hints=cell_coordinate_hints, ) format_dimensions = infer_moxel_format_dimensions( capacity_dimensions=capacity_dimensions, column_widths=column_widths, row_heights=row_heights, ) has_named_area_coordinates = any(isinstance(item.get("range"), dict) for item in named_areas) has_cell_coordinates = bool(cells) diagnostics = [] if has_named_area_coordinates: diagnostics.append( { "code": "moxel_named_areas_decoded", "message": "MOXCEL named areas and their row/column ranges were decoded from the textual MOXCEL payload.", } ) else: diagnostics.append( { "code": "moxel_binary_decoder_incomplete", "message": "MOXCEL payload is available, but exact named-area coordinates, cells, merges, and widths require a dedicated binary decoder. Best-effort text/parameter extraction was returned.", } ) structure = { "format": "MOXCEL", "capabilities": { "decoded_binary": False, "decoded_text": bool(moxel_text), "cell_coordinates": has_cell_coordinates, "named_area_coordinates": has_named_area_coordinates, "named_areas": bool(named_areas), "merged_cells": False, "merged_cell_candidates": bool(merged_range_candidates), "merge_record_block_candidates": bool(merge_record_block_candidates), "merge_count_hints": bool(merge_count_hints), "column_widths": bool(column_widths), "format_table": bool(format_table), "font_table": bool(font_table), "format_style_index_table": bool(format_style_index_table.get("style_references")), "cell_format_links": bool(cell_format_links), "best_effort_strings": bool(strings), "best_effort_parameters": bool(parameter_names), "cell_parameters": bool(cell_parameters), "cell_text_identifiers": bool(cell_text_identifiers), "cell_style_candidates": bool(cell_style_candidates), "cell_coordinate_hints": bool(cell_coordinate_hints), "named_range_candidates": bool(named_range_candidates), }, "dimensions": capacity_dimensions, "capacity_dimensions": capacity_dimensions, "used_dimensions": used_dimensions, "format_dimensions": format_dimensions, "named_areas": named_areas, "named_range_candidates": named_range_candidates, "parameters": [{"name": value, "source": "best_effort_text"} for value in parameter_names], "cell_parameters": cell_parameters, "cell_text_identifiers": cell_text_identifiers, "area_cell_coverage": area_cell_coverage, "cells": cells, "cell_style_candidates": cell_style_candidates, "cell_coordinate_hints": cell_coordinate_hints, "format_table": format_table, "font_table": font_table, "format_style_index_table": format_style_index_table, "cell_format_links": cell_format_links, "cell_format_link_stats": cell_format_link_stats, "merged_ranges": merged_ranges, "merged_range_candidates": merged_range_candidates, "merge_record_block_candidates": merge_record_block_candidates, "merge_count_hints": merge_count_hints, "column_widths": column_widths, "row_heights": row_heights, "moxel_record_diagnostics": moxel_record_diagnostics, "strings_sample": strings[:120], "moxel_text_excerpt": moxel_text[:4000] if isinstance(moxel_text, str) else None, "tree_root_summary": tree_root_summary, "payload": { "compression": decoded.get("compression"), "encoding": decoded.get("encoding"), "raw_bytes": decoded.get("raw_bytes"), "payload_bytes": decoded.get("payload_bytes"), "text_encoding": moxel_text_info.get("encoding"), "text_bom_offset": moxel_text_info.get("bom_offset"), }, "diagnostics": diagnostics, } structure["counts"] = moxel_structure_counts(structure) return structure def template_content_media_type(classification: dict[str, Any], *, encoding: str | None = None) -> str: markers = {str(value) for value in classification.get("markers") or []} role = str(classification.get("role") or "") if "MOXCEL" in markers: return "application/vnd.1c.moxel" if role == "help_or_html_payload" or any(block.get("has_html_marker") for block in classification.get("base64_blocks") or []): return f"text/html; charset={encoding}" if encoding else "text/html" if encoding: return f"text/plain; charset={encoding}" return "application/octet-stream" def bounded_template_content_export(data: bytes, *, max_content_bytes: int) -> dict[str, Any]: """Return a bounded, read-only export of a decoded 1C template part.""" from parser.cas_payload import classify_payload from parser.payload import decode_payload_lossless decoded = decode_payload_lossless(data) payload_bytes = bytes(decoded.get("payload") or b"") classification = classify_payload(data, include_text=True, include_tree=False) returned = payload_bytes[:max_content_bytes] container: dict[str, Any] = { "encoding": "base64", "media_type": template_content_media_type(classification, encoding=classification.get("encoding")), "bytes": len(payload_bytes), "returned_bytes": len(returned), "sha1": hashlib.sha1(payload_bytes).hexdigest(), "data_base64": base64.b64encode(returned).decode("ascii"), "truncated": len(returned) < len(payload_bytes), } extracted: list[dict[str, Any]] = [] for block_kind, blocks in ( ("stream", classification.get("stream_blocks") or []), ("base64", classification.get("base64_blocks") or []), ): for index, block in enumerate(blocks): text_value = block.get("text") if not isinstance(text_value, str): continue encoded = text_value.encode("utf-8") returned_text_bytes = encoded[:max_content_bytes] while returned_text_bytes: try: returned_text = returned_text_bytes.decode("utf-8") break except UnicodeDecodeError: returned_text_bytes = returned_text_bytes[:-1] else: returned_text = "" extracted.append( { "source": f"{block_kind}_block", "index": index, "encoding": block.get("encoding"), "media_type": "text/html" if block.get("has_html_marker") else "text/plain", "bytes": len(encoded), "returned_bytes": len(returned_text_bytes), "sha1": block.get("sha1") or hashlib.sha1(encoded).hexdigest(), "text": returned_text, "truncated": len(returned_text_bytes) < len(encoded), } ) return { "status": "truncated" if container["truncated"] or any(item["truncated"] for item in extracted) else "complete", "max_content_bytes": max_content_bytes, "container": container, "extracted_text": extracted, "counts": {"extracted_text": len(extracted)}, } def template_part_structure( base_id: str, part: dict[str, Any], *, timeout_seconds: int, refresh_cache: bool = False, include_content: bool = False, max_content_bytes: int = TEMPLATE_CONTENT_DEFAULT_MAX_BYTES, ) -> dict[str, Any]: classification = part.get("classification") if isinstance(part.get("classification"), dict) else {} markers = [str(value) for value in classification.get("markers") or []] table = str(part.get("table") or "Config") part_id = str(part.get("part_id") or "") public = payload_public_properties(classification) public["part_id"] = part_id public["table"] = table if "MOXCEL" not in markers: public["structure"] = { "format": public.get("content_kind") or public.get("role"), "capabilities": { "decoded_binary": False, "cell_coordinates": False, "named_areas": False, }, "named_areas": [], "parameters": [], "diagnostics": [{"message": "Part is not a MOXCEL tabular document."}], } if include_content: data, _, error = read_storage_file_bytes(base_id, table, part_id, timeout_seconds=timeout_seconds) if error or data is None: public["content_export"] = {"status": "error", "diagnostics": {"message": "Failed to read template payload bytes.", "error": error}} else: public["content_export"] = bounded_template_content_export(data, max_content_bytes=max_content_bytes) return public data, _, error = read_storage_file_bytes(base_id, table, part_id, timeout_seconds=timeout_seconds) if error or data is None: public["structure"] = { "format": "MOXCEL", "capabilities": {"decoded_binary": False}, "diagnostics": [{"message": "Failed to read MOXCEL payload bytes.", "error": error}], } return public payload_sha1 = hashlib.sha1(data).hexdigest() cache_config, _ = sql_config_for_base(base_id) cached = None if refresh_cache else decoded_artifact_cache_lookup(cache_config, artifact_kind=MOXEL_TEMPLATE_ARTIFACT_KIND, content_sha1=payload_sha1) if cached: cached_public = dict(cached) cached_public["part_id"] = part_id cached_public["table"] = table cached_public["artifact_cache"] = {"status": "hit", "content_sha1": payload_sha1} if include_content: cached_public["content_export"] = bounded_template_content_export(data, max_content_bytes=max_content_bytes) return cached_public public["structure"] = extract_moxel_public_structure(data) if include_content: public["content_export"] = bounded_template_content_export(data, max_content_bytes=max_content_bytes) public["artifact_cache"] = {"status": "refresh_stored" if refresh_cache else "miss_stored", "content_sha1": payload_sha1} semantic_text = template_structure_semantic_text(public.get("structure") or {}) decoded_artifact_cache_upsert( cache_config, artifact_kind=MOXEL_TEMPLATE_ARTIFACT_KIND, content_sha1=payload_sha1, source_table=table, source_file=part_id, payload_bytes=len(data), artifact=public, semantic_text=semantic_text, ) semantic_document_cache_upsert( cache_config, document_id=f"template_part:{table}:{part_id}:{payload_sha1}", object_kind="Template", object_guid=part_id, object_name=None, extension_guid=None, source_route={"table": table, "file_name": part_id, "part_id": part_id}, content_sha1=payload_sha1, text=semantic_text, ) return public def read_template_by_guid(payload: dict[str, Any], *, analyze: bool = False) -> dict[str, Any]: method = "templates.analyze" if analyze else "templates.read" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error timeout_value, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1) if timeout_error: return timeout_error timeout_seconds = int(timeout_value or 60) refresh_cache = truthy(payload.get("refresh_cache")) include_content = truthy(payload.get("include_content")) max_content_bytes = int(payload.get("max_content_bytes") or TEMPLATE_CONTENT_DEFAULT_MAX_BYTES) table_or_error = metadata_storage_table(payload, method) if isinstance(table_or_error, dict): return table_or_error metadata_payload = dict(payload) metadata_payload.pop("view", None) parts_result = metadata_object_parts( { **metadata_payload, "base_id": base_id, "kind": "Template", "table": table_or_error, "include_storage": True, "include_text": False, "include_tree": False, "timeout_seconds": timeout_seconds, } ) if parts_result.get("status") != "ok": result = dict(parts_result) result["method"] = method return result parts = [ template_part_structure( base_id, part, timeout_seconds=timeout_seconds, refresh_cache=refresh_cache, include_content=include_content, max_content_bytes=max_content_bytes, ) for part in parts_result.get("parts") or [] if isinstance(part, dict) ] template = { "name": (parts_result.get("object") or {}).get("name"), "guid": (parts_result.get("object") or {}).get("guid") or payload.get("guid"), "kind": "Template", "parts": parts, "structure": merge_template_structures(parts), } if analyze: template["analysis"] = analyze_template_structure(template.get("structure") or {}) return apply_template_response_view({ "schema": "onec_templates_analyze.v1" if analyze else "onec_templates_read.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "table": table_or_error}, "object": parts_result.get("object"), "query": {"guid": payload.get("guid"), "template": payload.get("template") or payload.get("name_filter"), "table": table_or_error}, "templates": [template], "counts": {"templates": 1, "parts": len(parts)}, }, payload) def read_template_by_route(payload: dict[str, Any], *, analyze: bool = False) -> dict[str, Any]: method = "templates.analyze" if analyze else "templates.read" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error timeout_value, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1) if timeout_error: return timeout_error timeout_seconds = int(timeout_value or 60) refresh_cache = truthy(payload.get("refresh_cache")) include_content = truthy(payload.get("include_content")) max_content_bytes = int(payload.get("max_content_bytes") or TEMPLATE_CONTENT_DEFAULT_MAX_BYTES) table_or_error = metadata_storage_table(payload, method) if isinstance(table_or_error, dict): return table_or_error table = str(table_or_error or payload.get("table") or "ConfigCAS") file_name = str(payload.get("file_name") or payload.get("part_id") or payload.get("guid") or "").strip() if not file_name: return invalid_argument(method, "file_name", "file_name or route guid is required for route-based template reads.") files = storage_files_list({"base_id": base_id, "table": table, "prefix": file_name, "limit": 200, "timeout_seconds": timeout_seconds, "_internal": True}) if files.get("status") == "ok": file_names = [ str(row.get("FileName") or "") for row in files.get("files") or [] if str(row.get("FileName") or "") == file_name or str(row.get("FileName") or "").startswith(f"{file_name}.") ] else: file_names = [file_name] if not file_names: file_names = [file_name] payloads, _, error = read_storage_files_bytes(base_id, table, file_names, timeout_seconds=timeout_seconds) if error or not payloads: return public_error_result(error or {"status": "not_found", "diagnostics": {"message": "Template route payload was not found."}}, include_storage=True, method=method) extension_guid = None if payload.get("extension"): extension_guid, _ = extension_filter_to_guid(base_id, str(payload.get("extension") or ""), method=method) related_entries, manifest_diagnostics = manifest_related_entries_for_cas_key( base_id, file_name, extension_guid=extension_guid, timeout_seconds=timeout_seconds, ) if extension_guid and not related_entries: retry_entries, retry_diagnostics = manifest_related_entries_for_cas_key( base_id, file_name, extension_guid=None, timeout_seconds=timeout_seconds, ) if retry_entries: related_entries = retry_entries owner_extensions = sorted({str((entry.get("extension") or {}).get("name") or "") for entry in retry_entries if isinstance(entry.get("extension"), dict)}) manifest_diagnostics.append( { "code": "extension_manifest_owner_mismatch", "message": "Template route was not present in the requested extension manifest; it was found in another extension manifest.", "requested_extension": payload.get("extension"), "found_extensions": [value for value in owner_extensions if value], } ) manifest_diagnostics.extend(retry_diagnostics) manifest_keys = [str(entry.get("cas_key") or "").strip().lower() for entry in related_entries if str(entry.get("cas_key") or "").strip()] missing_manifest_keys = [key for key in manifest_keys if key not in (payloads or {})] if missing_manifest_keys: manifest_payloads, _, manifest_read_error = read_storage_files_bytes(base_id, "ConfigCAS", missing_manifest_keys, timeout_seconds=timeout_seconds) if manifest_read_error: manifest_diagnostics.append({"status": manifest_read_error.get("status"), "diagnostics": manifest_read_error.get("diagnostics")}) else: payloads.update(manifest_payloads or {}) try: from parser.cas_payload import classify_payload except Exception as exc: return adapter_public_error(method, "classifier_unavailable", {"message": str(exc)}) identity = config_identity_from_bytes((payloads or {}).get(file_name) or next(iter((payloads or {}).values()))) or {} manifest_entry_by_key = {str(entry.get("cas_key") or "").lower(): entry for entry in related_entries} public_parts = [] for part_file_name in sorted(payloads or {}, key=lambda value: (value != file_name, value)): classification = classify_payload((payloads or {})[part_file_name], include_text=False, include_tree=False) manifest_entry = manifest_entry_by_key.get(str(part_file_name).lower()) or {} part = { "part_id": part_file_name, "table": table, "suffix": manifest_entry.get("suffix") if manifest_entry else (part_file_name[len(file_name) :] if part_file_name.startswith(file_name) else ""), "classification": classification, } public_part = template_part_structure( base_id, part, timeout_seconds=timeout_seconds, refresh_cache=refresh_cache, include_content=include_content, max_content_bytes=max_content_bytes, ) if manifest_entry: public_part["manifest_route"] = { "object_id": manifest_entry.get("object_id"), "suffix": manifest_entry.get("suffix"), "cas_key": manifest_entry.get("cas_key"), "extension": manifest_entry.get("extension"), "root_cas_key": manifest_entry.get("root_cas_key"), } public_parts.append(public_part) template = { "name": identity.get("name") or payload.get("template") or payload.get("name"), "guid": identity.get("guid") or payload.get("guid") or file_name, "kind": "Template", "route": { "route_type": "extension_manifest_cas" if related_entries else ("configcas_payload" if table.startswith("ConfigCAS") else "storage_payload"), "table": table, "file_name": file_name, **({"manifest_entries": len(related_entries)} if related_entries else {}), }, "parts": public_parts, "structure": merge_template_structures(public_parts), } if analyze: template["analysis"] = analyze_template_structure(template.get("structure") or {}) return apply_template_response_view({ "schema": "onec_templates_analyze.v1" if analyze else "onec_templates_read.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "table": table}, "object": {"kind": "Template", "name": template.get("name"), "guid": template.get("guid")}, "query": {"table": table, "file_name": file_name, "template": payload.get("template") or payload.get("name_filter")}, "templates": [template], "counts": {"templates": 1, "parts": len(public_parts), "manifest_entries": len(related_entries)}, **({"diagnostics": manifest_diagnostics} if manifest_diagnostics else {}), }, payload) def parse_storage_route_ref(value: Any) -> dict[str, str] | None: text = str(value or "").strip() if not text or ":" not in text: return None table, file_name = text.split(":", 1) table = table.strip() file_name = file_name.strip() if table not in STORAGE_TABLES or not file_name: return None return {"table": table, "file_name": file_name} def normalize_template_route_ref_payload(payload: dict[str, Any]) -> dict[str, Any]: route_ref = parse_storage_route_ref(payload.get("route_ref")) if not route_ref: return payload normalized = dict(payload) normalized.setdefault("table", route_ref["table"]) normalized.setdefault("file_name", route_ref["file_name"]) return normalized def template_area_name_matches(area: dict[str, Any], area_query: str, *, match_mode: str = "contains") -> bool: query = str(area_query or "").strip() if not query: return True name = str((area or {}).get("name") or "") if match_mode == "exact": return any(normalize_exact(query) == normalize_exact(variant) for variant in text_variants(name)) return bool(normalized_contains_any(query, name) or normalized_contains_any(name, query)) def template_area_items_from_structure( structure: dict[str, Any], *, max_areas: int, area_query: str = "", area_match: str = "contains", area_occurrence: int | None = None, include_coverage: bool = True, ) -> tuple[list[dict[str, Any]], int, int]: if not isinstance(structure, dict): return [], 0, 0 coverage_by_key: dict[tuple[str, Any], dict[str, Any]] = {} if include_coverage: for item in structure.get("area_cell_coverage") or []: if isinstance(item, dict): coverage_by_key[(str(item.get("name") or "").casefold(), item.get("occurrence") or 1)] = item areas: list[dict[str, Any]] = [] total_named_areas = 0 matching_areas = 0 for item in structure.get("named_areas") or []: if not isinstance(item, dict): continue total_named_areas += 1 name = str(item.get("name") or "").strip() occurrence = item.get("occurrence") or 1 range_info = item.get("range") if isinstance(item.get("range"), dict) else None area = { "name": name, "occurrence": occurrence, "range": range_info, "coordinates_available": bool(range_info), "source": item.get("source"), } if item.get("diagnostics"): area["diagnostics"] = item.get("diagnostics") coverage = coverage_by_key.get((name.casefold(), occurrence)) if coverage: area["coverage"] = { "cell_count": coverage.get("cell_count"), "parameter_count": coverage.get("parameter_count"), "cells": coverage.get("cells") or [], "parameters": coverage.get("parameters") or [], } if not template_area_name_matches(area, area_query, match_mode=area_match): continue if area_occurrence is not None and int(occurrence or 1) != area_occurrence: continue matching_areas += 1 if len(areas) < max_areas: areas.append(area) return areas, total_named_areas, matching_areas def template_query_variants(query: str) -> list[str]: text = str(query or "").strip() variants: list[str] = [] if text: variants.append(text) if "_" in text: prefix, rest = text.split("_", 1) if 1 <= len(prefix) <= 8 and rest and rest not in variants: variants.append(rest) for variant in normalized_variants(text): if variant and variant not in variants: variants.append(variant) return variants def templates_areas_find(payload: dict[str, Any]) -> dict[str, Any]: method = "templates.areas.find" payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload payload = normalize_template_route_ref_payload(payload) base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error query = str(first_non_empty_arg(payload, "query", "template", "name_filter", "name", "object_name") or "").strip() area_value_source = "area_query" if payload.get("area_query") not in {None, ""} else ("area_name" if payload.get("area_name") not in {None, ""} else ("area" if payload.get("area") not in {None, ""} else None)) area_query = str(first_non_empty_arg(payload, "area_query", "area", "area_name") or "").strip() area_match = str(payload.get("area_match") or ("contains" if area_value_source == "area_query" else "exact")).strip().lower() if area_match not in {"contains", "exact"}: return invalid_argument(method, "area_match", "area_match must be one of: contains, exact.") area_occurrence, area_occurrence_error = parse_int_alias_argument(payload, "area_occurrence", "occurrence", method=method, default=0, minimum=0, maximum=100000) if area_occurrence_error: return area_occurrence_error if ("area_occurrence" in payload or "occurrence" in payload) and int(area_occurrence or 0) < 1: return invalid_argument(method, "area_occurrence", "area_occurrence/occurrence is 1-based and must be >= 1.") area_occurrence_filter = int(area_occurrence or 0) or None limit, limit_error = parse_int_argument(payload, "limit", method=method, default=5, minimum=1, maximum=50) if limit_error: return limit_error max_areas, max_areas_error = parse_int_argument(payload, "max_areas", method=method, default=500, minimum=0, maximum=5000) if max_areas_error: return max_areas_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=90, minimum=1, maximum=600) if timeout_error: return timeout_error cache_ttl_seconds, cache_ttl_error = parse_int_argument(payload, "cache_ttl_seconds", method=method, default=300, minimum=0, maximum=86400) if cache_ttl_error: return cache_ttl_error refresh_cache, refresh_cache_error = strict_bool_argument(payload, "refresh_cache", method=method, default=False) if refresh_cache_error: return refresh_cache_error include_empty, include_empty_error = strict_bool_argument(payload, "include_empty", method=method, default=True) if include_empty_error: return include_empty_error include_coverage, include_coverage_error = strict_bool_argument(payload, "include_coverage", method=method, default=True) if include_coverage_error: return include_coverage_error routes: list[dict[str, Any]] = [] direct_file_name = str(payload.get("file_name") or payload.get("part_id") or "").strip() direct_table = str(payload.get("table") or "ConfigCAS").strip() or "ConfigCAS" if direct_file_name: routes.append( { "object": {"kind": "Template", "name": query or payload.get("name"), "guid": direct_file_name}, "route": {"table": direct_table, "file_name": direct_file_name, "route_type": "direct_route"}, "freshness": {"status": "direct_route", "validation_required": False}, "match_by": "direct_route", } ) else: if not query: return invalid_argument(method, "query", "Pass query/template/name or file_name.") find_result = {"status": "not_found", "diagnostics": []} find_attempts: list[dict[str, Any]] = [] for query_variant in template_query_variants(query): for kind_value in ("Template", None): find_payload = { "base_id": base_id, "query": query_variant, "extension": payload.get("extension"), "limit": int(limit or 5), "include_storage": True, "refresh_cache": bool(refresh_cache), "cache_ttl_seconds": int(cache_ttl_seconds or 0), "timeout_seconds": int(timeout_seconds or 90), } if kind_value: find_payload["kind"] = kind_value attempt = extension_objects_find(find_payload) find_attempts.append( { "query": query_variant, "kind": kind_value, "status": attempt.get("status"), "matches": len(attempt.get("objects") or []), } ) if attempt.get("status") == "ok": find_result = attempt break if find_result.get("status") == "ok": break if find_result.get("status") != "ok": return { "schema": "onec_templates_areas_find.v1", "status": "not_found", "error": "not_found", "base_id": base_id, "query": {"query": query, "extension": payload.get("extension"), "limit": int(limit or 5)}, "templates": [], "areas": [], "counts": {"templates": 0, "areas": 0}, "diagnostics": (find_result.get("diagnostics") or [{"message": "Template route was not found."}]) + [{"area": "route_lookup", "attempts": find_attempts}], } for item in find_result.get("objects") or []: if isinstance(item, dict): routes.append(item) templates: list[dict[str, Any]] = [] all_areas: list[dict[str, Any]] = [] errors: list[dict[str, Any]] = [] for route_item in routes[: int(limit or 5)]: route = route_item.get("route") if isinstance(route_item.get("route"), dict) else {} table = str(route.get("table") or "ConfigCAS").strip() or "ConfigCAS" file_name = str(route.get("file_name") or route_item.get("guid") or "").strip() if not file_name: errors.append({"object": route_item.get("object") or route_item, "error": "route_missing_file_name"}) continue read_area_limit = 5000 if area_query or area_occurrence_filter else int(max_areas or 0) read_result = read_template_by_route( { "base_id": base_id, "kind": "Template", "table": table, "file_name": file_name, "view": "structure", "sections": "named_areas,coverage,diagnostics" if include_coverage else "named_areas,diagnostics", "max_areas": read_area_limit, "max_coverage": read_area_limit if include_coverage else 0, "timeout_seconds": int(timeout_seconds or 90), } ) if read_result.get("status") != "ok": errors.append({"route": {"table": table, "file_name": file_name}, "status": read_result.get("status"), "error": read_result.get("error")}) continue for template in read_result.get("templates") or []: if not isinstance(template, dict): continue structure = template.get("structure") if isinstance(template.get("structure"), dict) else {} areas, total_named_areas, matching_areas = template_area_items_from_structure( structure, max_areas=int(max_areas or 0), area_query=area_query, area_match=area_match, area_occurrence=area_occurrence_filter, include_coverage=bool(include_coverage), ) if not areas and not include_empty: continue route_object = route_item.get("object") if isinstance(route_item.get("object"), dict) else {} template_object = { "kind": "Template", "name": template.get("name") or route_object.get("name"), "guid": template.get("guid") or route_object.get("guid"), } template_entry = { "object": template_object, "route": {"table": table, "file_name": file_name, "route_ref": f"{table}:{file_name}", "route_type": route.get("route_type")}, "match_by": route_item.get("match_by"), "freshness": route_item.get("freshness") or {"status": "read_current_payload", "validation_required": False}, "read_selector": { **semantic_cache_read_selector(base_id, "Template", {"table": table, "file_name": file_name}), "route_ref": f"{table}:{file_name}", }, "structure": { "format": structure.get("format"), "dimensions": structure.get("dimensions"), "capabilities": structure.get("capabilities") or {}, "counts": structure.get("counts") or template_structure_counts(structure), }, "areas": areas, "counts": { "areas": len(areas), "areas_returned": len(areas), "matching_areas": matching_areas, "areas_limited": matching_areas > len(areas), "areas_with_coordinates": len([area for area in areas if area.get("coordinates_available")]), "decoded_named_areas": total_named_areas, }, } templates.append(template_entry) for area in areas: all_areas.append({"template": template_object, "route": template_entry["route"], **area}) return { "schema": "onec_templates_areas_find.v1", "status": "ok" if templates or all_areas else "not_found", **({"error": "not_found"} if not templates and not all_areas else {}), "base_id": base_id, "source": { "kind": "live_template_payload", "authoritative": True, "message": "Areas are decoded from current template payload reads. Coordinate availability depends on MOXCEL decoder capabilities.", }, "query": { "query": query or None, "extension": payload.get("extension"), "file_name": direct_file_name or None, "limit": int(limit or 5), "max_areas": int(max_areas or 0), "area_query": area_query or None, "area_match": area_match if area_query or area_occurrence_filter else None, "area_occurrence": area_occurrence_filter, "include_coverage": bool(include_coverage), "refresh_cache": bool(refresh_cache), "cache_ttl_seconds": int(cache_ttl_seconds or 0), }, "templates": templates, "areas": all_areas, "counts": { "templates": len(templates), "areas": len(all_areas), "areas_returned": len(all_areas), "matching_areas": sum(int((template.get("counts") or {}).get("matching_areas") or 0) for template in templates), "areas_limited": any(bool((template.get("counts") or {}).get("areas_limited")) for template in templates), "areas_with_coordinates": len([area for area in all_areas if area.get("coordinates_available")]), "decoded_named_areas": sum(int((template.get("counts") or {}).get("decoded_named_areas") or 0) for template in templates), "area_filter_applied": bool(area_query or area_occurrence_filter), "routes_considered": len(routes), "errors": len(errors), }, **({"errors": errors[:50]} if errors else {}), } def merge_template_structures(parts: list[dict[str, Any]]) -> dict[str, Any]: structures = [(part.get("structure") or {}) for part in parts if isinstance(part.get("structure"), dict)] parameters: list[dict[str, Any]] = [] parameter_seen: set[str] = set() named_areas: list[dict[str, Any]] = [] named_range_candidates: list[dict[str, Any]] = [] diagnostics: list[dict[str, Any]] = [] strings: list[str] = [] cells: list[dict[str, Any]] = [] column_widths: list[dict[str, Any]] = [] format_table: list[dict[str, Any]] = [] font_table: list[dict[str, Any]] = [] cell_format_links: list[dict[str, Any]] = [] cell_format_link_stats: dict[str, Any] | None = None row_heights: list[dict[str, Any]] = [] merged_ranges: list[dict[str, Any]] = [] merged_range_candidates: list[dict[str, Any]] = [] merge_record_block_candidates: list[dict[str, Any]] = [] merge_count_hints: list[dict[str, Any]] = [] cell_parameters: list[dict[str, Any]] = [] cell_text_identifiers: list[dict[str, Any]] = [] cell_style_candidates: list[dict[str, Any]] = [] cell_coordinate_hints: list[dict[str, Any]] = [] area_cell_coverage: list[dict[str, Any]] = [] moxel_record_diagnostics: list[dict[str, Any]] = [] capabilities = { "decoded_binary": False, "decoded_text": False, "cell_coordinates": False, "named_area_coordinates": False, "named_areas": False, "merged_cells": False, "merged_cell_candidates": False, "merge_record_block_candidates": False, "merge_count_hints": False, "column_widths": False, "format_table": False, "font_table": False, "cell_format_links": False, "best_effort_strings": False, "best_effort_parameters": False, "cell_parameters": False, "cell_text_identifiers": False, "cell_style_candidates": False, "cell_coordinate_hints": False, "named_range_candidates": False, } for structure in structures: for key in list(capabilities): capabilities[key] = bool(capabilities[key] or (structure.get("capabilities") or {}).get(key)) for item in structure.get("parameters") or []: name = str((item or {}).get("name") or "") if name and name.casefold() not in parameter_seen: parameter_seen.add(name.casefold()) parameters.append(item) for item in structure.get("named_areas") or []: if isinstance(item, dict): named_areas.append(item) for item in structure.get("named_range_candidates") or []: if isinstance(item, dict): named_range_candidates.append(item) for item in structure.get("cells") or []: if isinstance(item, dict): cells.append(item) for item in structure.get("cell_parameters") or []: if isinstance(item, dict): cell_parameters.append(item) for item in structure.get("cell_text_identifiers") or []: if isinstance(item, dict): cell_text_identifiers.append(item) for item in structure.get("cell_style_candidates") or []: if isinstance(item, dict): cell_style_candidates.append(item) for item in structure.get("cell_coordinate_hints") or []: if isinstance(item, dict): cell_coordinate_hints.append(item) for item in structure.get("area_cell_coverage") or []: if isinstance(item, dict): area_cell_coverage.append(item) for item in structure.get("column_widths") or []: if isinstance(item, dict): column_widths.append(item) for item in structure.get("format_table") or []: if isinstance(item, dict): format_table.append(item) for item in structure.get("font_table") or []: if isinstance(item, dict): font_table.append(item) for item in structure.get("cell_format_links") or []: if isinstance(item, dict): cell_format_links.append(item) if cell_format_link_stats is None and isinstance(structure.get("cell_format_link_stats"), dict): cell_format_link_stats = structure.get("cell_format_link_stats") or {} for item in structure.get("row_heights") or []: if isinstance(item, dict): row_heights.append(item) for item in structure.get("merged_ranges") or []: if isinstance(item, dict): merged_ranges.append(item) for item in structure.get("merged_range_candidates") or []: if isinstance(item, dict): merged_range_candidates.append(item) for item in structure.get("merge_record_block_candidates") or []: if isinstance(item, dict): merge_record_block_candidates.append(item) for item in structure.get("merge_count_hints") or []: if isinstance(item, dict): merge_count_hints.append(item) if isinstance(structure.get("moxel_record_diagnostics"), dict): moxel_record_diagnostics.append(structure.get("moxel_record_diagnostics") or {}) for item in structure.get("diagnostics") or []: if isinstance(item, dict): diagnostics.append(item) for value in structure.get("strings_sample") or []: if isinstance(value, str) and value not in strings: strings.append(value) dimensions = next((structure.get("dimensions") for structure in structures if isinstance(structure.get("dimensions"), dict)), None) capacity_dimensions = next( ( structure.get("capacity_dimensions") for structure in structures if isinstance(structure.get("capacity_dimensions"), dict) ), dimensions, ) used_dimensions = infer_moxel_used_dimensions( capacity_dimensions=capacity_dimensions, cells=cells, named_areas=named_areas, named_range_candidates=named_range_candidates, merged_ranges=merged_ranges, cell_coordinate_hints=cell_coordinate_hints, ) if used_dimensions is None: used_dimensions = next( ( structure.get("used_dimensions") for structure in structures if isinstance(structure.get("used_dimensions"), dict) ), None, ) format_dimensions = infer_moxel_format_dimensions( capacity_dimensions=capacity_dimensions, column_widths=column_widths, row_heights=row_heights, ) if format_dimensions is None: format_dimensions = next( ( structure.get("format_dimensions") for structure in structures if isinstance(structure.get("format_dimensions"), dict) ), None, ) return { "format": "MOXCEL" if any((part.get("features") or {}).get("tabular_document") for part in parts) else None, "capabilities": capabilities, "dimensions": dimensions, "capacity_dimensions": capacity_dimensions, "used_dimensions": used_dimensions, "format_dimensions": format_dimensions, "named_areas": named_areas, "named_range_candidates": named_range_candidates, "parameters": parameters, "cell_parameters": cell_parameters, "cell_text_identifiers": cell_text_identifiers, "cell_style_candidates": cell_style_candidates, "cell_coordinate_hints": cell_coordinate_hints, "area_cell_coverage": area_cell_coverage, "format_table": format_table, "font_table": font_table, "cell_format_links": cell_format_links, "cell_format_link_stats": cell_format_link_stats or summarize_moxel_cell_format_links(cells, format_table, cell_format_links), "cells": cells, "merged_ranges": merged_ranges, "merged_range_candidates": merged_range_candidates, "merge_record_block_candidates": merge_record_block_candidates, "merge_count_hints": merge_count_hints, "column_widths": column_widths, "row_heights": row_heights, "moxel_record_diagnostics": moxel_record_diagnostics, "strings_sample": strings[:120], "diagnostics": diagnostics, } def analyze_template_structure(structure: dict[str, Any]) -> dict[str, Any]: named_areas = structure.get("named_areas") if isinstance(structure.get("named_areas"), list) else [] parameters = structure.get("parameters") if isinstance(structure.get("parameters"), list) else [] cells = structure.get("cells") if isinstance(structure.get("cells"), list) else [] cell_parameters = structure.get("cell_parameters") if isinstance(structure.get("cell_parameters"), list) else [] cell_text_identifiers = structure.get("cell_text_identifiers") if isinstance(structure.get("cell_text_identifiers"), list) else [] cell_style_candidates = structure.get("cell_style_candidates") if isinstance(structure.get("cell_style_candidates"), list) else [] cell_coordinate_hints = structure.get("cell_coordinate_hints") if isinstance(structure.get("cell_coordinate_hints"), list) else [] area_cell_coverage = structure.get("area_cell_coverage") if isinstance(structure.get("area_cell_coverage"), list) else [] column_widths = structure.get("column_widths") if isinstance(structure.get("column_widths"), list) else [] merged_ranges = structure.get("merged_ranges") if isinstance(structure.get("merged_ranges"), list) else [] merged_range_candidates = structure.get("merged_range_candidates") if isinstance(structure.get("merged_range_candidates"), list) else [] merge_record_block_candidates = structure.get("merge_record_block_candidates") if isinstance(structure.get("merge_record_block_candidates"), list) else [] merge_count_hints = structure.get("merge_count_hints") if isinstance(structure.get("merge_count_hints"), list) else [] capabilities = structure.get("capabilities") if isinstance(structure.get("capabilities"), dict) else {} has_area_coordinates = bool(capabilities.get("named_area_coordinates")) issues: list[dict[str, Any]] = [] area_widths: list[dict[str, Any]] = [] width_variants: list[dict[str, Any]] = [] intersections: list[dict[str, Any]] = [] style_coordinate_hints = [ { "text": item.get("text"), "tree_position": item.get("tree_position"), "coordinate_hints": item.get("coordinate_hints"), "source": (item.get("coordinate_hints") or {}).get("source") if isinstance(item.get("coordinate_hints"), dict) else None, } for item in cell_style_candidates if isinstance(item, dict) and isinstance(item.get("coordinate_hints"), dict) ] cell_parameter_names = {str((item or {}).get("name") or "").casefold() for item in cell_parameters if str((item or {}).get("name") or "")} cell_text_identifier_names = {str((item or {}).get("name") or "").casefold() for item in cell_text_identifiers if str((item or {}).get("name") or "")} parameters_without_cells = [ item for item in parameters if str((item or {}).get("name") or "") and str((item or {}).get("name") or "").casefold() not in cell_parameter_names and str((item or {}).get("name") or "").casefold() not in cell_text_identifier_names and not str((item or {}).get("name") or "").casefold().startswith("область") ] if not named_areas: issues.append( { "code": "named_areas_not_decoded", "severity": "warning", "message": "Именованные области не декодированы; проверка ширины, пересечений и сдвигов невозможна без координат MOXCEL.", } ) if named_areas and not has_area_coordinates: issues.append( { "code": "named_area_coordinates_not_decoded", "severity": "warning", "message": "Имена областей найдены, но координаты областей недоступны; проверка ширин и пересечений ограничена.", } ) if not capabilities.get("cell_coordinates"): issues.append( { "code": "cell_coordinates_not_decoded", "severity": "warning", "message": "Координаты ячеек недоступны; анализ ширин колонок и объединений возвращен как not_available.", } ) if capabilities.get("cell_coordinates") and not capabilities.get("merged_cells"): issues.append( { "code": "merged_cells_not_decoded", "severity": "warning", "message": "Координаты текстовых ячеек декодированы, но объединения ячеек пока не извлекаются из MOXCEL.", } ) if named_areas and has_area_coordinates: grouped_ranges: dict[str, list[dict[str, Any]]] = {} ranged_areas: list[dict[str, Any]] = [] for item in named_areas: if not isinstance(item, dict) or not isinstance(item.get("range"), dict): continue range_info = item.get("range") or {} zero = range_info.get("zero_based") if isinstance(range_info.get("zero_based"), dict) else {} one = range_info.get("one_based") if isinstance(range_info.get("one_based"), dict) else {} try: top = int(zero.get("top")) left = int(zero.get("left")) bottom = int(zero.get("bottom")) right = int(zero.get("right")) except Exception: continue width = int(range_info.get("width") or (right - left + 1)) height = int(range_info.get("height") or (bottom - top + 1)) area_summary = { "name": item.get("name"), "occurrence": item.get("occurrence"), "width": width, "height": height, "range": {"zero_based": zero, "one_based": one}, } area_widths.append(area_summary) ranged_item = {**area_summary, "top": top, "left": left, "bottom": bottom, "right": right} ranged_areas.append(ranged_item) grouped_ranges.setdefault(str(item.get("name") or "").casefold(), []).append(ranged_item) for grouped in grouped_ranges.values(): shapes = sorted({(int(item.get("width") or 0), int(item.get("height") or 0)) for item in grouped}) if len(shapes) > 1: width_variants.append( { "name": grouped[0].get("name"), "occurrences": len(grouped), "shapes": [{"width": width, "height": height} for width, height in shapes], "ranges": [ { "occurrence": item.get("occurrence"), "width": item.get("width"), "height": item.get("height"), "range": item.get("range"), } for item in grouped[:20] ], } ) for index, left_area in enumerate(ranged_areas): for right_area in ranged_areas[index + 1 :]: top = max(int(left_area["top"]), int(right_area["top"])) left = max(int(left_area["left"]), int(right_area["left"])) bottom = min(int(left_area["bottom"]), int(right_area["bottom"])) right = min(int(left_area["right"]), int(right_area["right"])) if top > bottom or left > right: continue intersections.append( { "left": {"name": left_area.get("name"), "occurrence": left_area.get("occurrence")}, "right": {"name": right_area.get("name"), "occurrence": right_area.get("occurrence")}, "range": moxel_range(top, left, bottom, right), } ) if len(intersections) >= 100: break if len(intersections) >= 100: break if width_variants: issues.append( { "code": "named_area_shape_variants", "severity": "info", "message": "У части именованных областей есть несколько диапазонов с разной шириной или высотой.", "count": len(width_variants), } ) return { "status": "partial" if issues else "ok", "named_area_count": len(named_areas), "parameter_count": len(parameters), "checks": { "named_areas": "ok" if named_areas else "not_available", "area_widths": "ok" if named_areas and has_area_coordinates else "not_available", "area_intersections": "ok" if named_areas and has_area_coordinates else "not_available", "cells": "ok" if cells else "not_available", "column_widths": "ok" if column_widths else "not_available", "merged_cells": "ok" if merged_ranges else "not_available", "merged_cell_candidates": "ok" if merged_range_candidates else "not_available", "merge_record_block_candidates": "ok" if merge_record_block_candidates else "not_available", "merge_count_hints": "ok" if merge_count_hints else "not_available", "cell_parameters": "ok" if cell_parameters else ("best_effort_only" if parameters else "not_found"), "cell_text_identifiers": "ok" if cell_text_identifiers else "not_found", "cell_coordinate_hints": "ok" if cell_coordinate_hints else "not_available", "cell_style_coordinate_hints": "ok" if style_coordinate_hints else "not_available", "parameters_without_cells": "ok" if not parameters_without_cells else "found", }, "area_widths": area_widths[:200], "width_variants": width_variants[:100], "intersections": intersections, "cell_count": len(cells), "cells_sample": cells[:120], "cell_parameters": cell_parameters[:200], "cell_text_identifiers": cell_text_identifiers[:200], "cell_coordinate_hints": cell_coordinate_hints[:200], "cell_style_coordinate_hints": style_coordinate_hints[:200], "parameters_without_cells": parameters_without_cells[:200], "area_cell_coverage": area_cell_coverage[:200], "column_widths": column_widths[:200], "merged_ranges": merged_ranges[:200], "merged_range_candidates": merged_range_candidates[:200], "merge_record_block_candidates": merge_record_block_candidates[:200], "merge_count_hints": merge_count_hints[:200], "counts": { "area_widths": len(area_widths), "width_variants": len(width_variants), "intersections_returned": len(intersections), "cells": len(cells), "cell_parameters": len(cell_parameters), "cell_text_identifiers": len(cell_text_identifiers), "cell_coordinate_hints": len(cell_coordinate_hints), "cell_style_coordinate_hints": len(style_coordinate_hints), "parameters_without_cells": len(parameters_without_cells), "area_cell_coverage": len(area_cell_coverage), "column_widths": len(column_widths), "merged_ranges": len(merged_ranges), "merged_range_candidates": len(merged_range_candidates), "merge_record_block_candidates": len(merge_record_block_candidates), "merge_count_hints": len(merge_count_hints), }, "issues": issues, } TEMPLATE_RESPONSE_SECTIONS = { "named_areas", "named_range_candidates", "named_ranges", "parameters", "cell_parameters", "cell_text_identifiers", "cell_style_candidates", "cell_coordinate_hints", "coordinate_hints", "cells", "formats", "format_table", "font_table", "fonts", "format_style_index_table", "style_index_table", "style_references", "cell_format_links", "cell_format_link_stats", "format_links", "styles", "coverage", "area_cell_coverage", "column_widths", "widths", "row_heights", "heights", "merged_ranges", "merged_range_candidates", "merge_record_block_candidates", "merge_count_hints", "merges", "intersections", "width_variants", "issues", "diagnostics", "strings", "moxel_records", "moxel_record_diagnostics", "undecoded", "undecoded_evidence", "payload", "tree_root", "parts", } def parse_template_sections(payload: dict[str, Any]) -> set[str] | None: raw = payload.get("sections") if raw in {None, ""}: return None values: list[str] = [] if isinstance(raw, str): values = [item.strip() for item in raw.split(",")] elif isinstance(raw, list): values = [str(item or "").strip() for item in raw] return {value for value in values if value in TEMPLATE_RESPONSE_SECTIONS} or None def template_limit(payload: dict[str, Any], key: str, default: int, maximum: int = 5000) -> int: value = payload.get(key) if value in {None, ""}: return default try: parsed = int(value) except Exception: return default return max(0, min(parsed, maximum)) def limited_list(value: Any, limit: int) -> list[Any]: if not isinstance(value, list): return [] return value[: max(0, limit)] def compact_moxel_undecoded_evidence(structure: dict[str, Any], payload: dict[str, Any], *, view: str) -> dict[str, Any]: strings_limit = template_limit(payload, "max_strings", 20 if view == "summary" else 120) records_limit = template_limit(payload, "max_moxel_records", 20 if view == "summary" else 80) excerpt_limit = template_limit(payload, "max_excerpt_chars", 1200 if view == "summary" else 6000, maximum=20000) capabilities = structure.get("capabilities") if isinstance(structure.get("capabilities"), dict) else {} unresolved_capabilities = sorted([key for key, value in capabilities.items() if value is False]) diagnostics_items = structure.get("moxel_record_diagnostics") if isinstance(structure.get("moxel_record_diagnostics"), list) else [] first_diagnostics = diagnostics_items[0] if diagnostics_items and isinstance(diagnostics_items[0], dict) else {} evidence: dict[str, Any] = { "payload": structure.get("payload") or {}, "tree_root_summary": structure.get("tree_root_summary"), "unresolved_capabilities": unresolved_capabilities, "strings_sample": limited_list(structure.get("strings_sample"), strings_limit), "named_areas_without_range": [ { "name": item.get("name"), "occurrence": item.get("occurrence"), "source": item.get("source"), } for item in (structure.get("named_areas") or []) if isinstance(item, dict) and not isinstance(item.get("range"), dict) ][:strings_limit], "named_ranges_without_range": [ { "name": item.get("name"), "kind": item.get("kind"), "source": item.get("source"), } for item in (structure.get("named_range_candidates") or []) if isinstance(item, dict) and not isinstance(item.get("range"), dict) ][:strings_limit], "merged_range_candidates": limited_list(structure.get("merged_range_candidates"), min(20, records_limit)), "coordinate_like_samples": limited_list(first_diagnostics.get("coordinate_like_samples"), records_limit), "top_level_shapes": limited_list(first_diagnostics.get("top_level_shapes"), records_limit), "top_level_shape_candidates": limited_list(first_diagnostics.get("top_level_shape_candidates"), records_limit), "head_samples": limited_list(first_diagnostics.get("head_samples"), min(12, records_limit)), } excerpt = structure.get("moxel_text_excerpt") if isinstance(excerpt, str) and excerpt: evidence["moxel_text_excerpt"] = excerpt[:excerpt_limit] return evidence def moxel_record_top_level_index(record: dict[str, Any]) -> int | None: position = str(record.get("tree_position") or "") match = re.fullmatch(r"\$\.(\d+)", position) if not match: return None try: return int(match.group(1)) except Exception: return None def moxel_record_window(payload: dict[str, Any]) -> tuple[int | None, int | None]: start = payload.get("moxel_record_start") end = payload.get("moxel_record_end") start_value = int(start) if start not in {None, ""} else None end_value = int(end) if end not in {None, ""} else None if start_value is not None and end_value is not None and end_value < start_value: start_value, end_value = end_value, start_value return start_value, end_value def moxel_record_head_filter(payload: dict[str, Any]) -> set[int] | None: raw = payload.get("moxel_record_heads") if raw is None or raw == "": return None values = raw if isinstance(raw, list) else str(raw).split(",") heads: set[int] = set() for value in values: text = str(value or "").strip() if not text: continue try: heads.add(int(text)) except Exception: continue return heads or None def moxel_record_context_radius(payload: dict[str, Any]) -> int: value = payload.get("moxel_record_context") if value in {None, ""}: return 0 try: return max(0, int(value)) except Exception: return 0 def moxel_candidate_rank(payload: dict[str, Any]) -> int | None: value = payload.get("moxel_candidate_rank") if value in {None, ""}: return None try: parsed = int(value) except Exception: return None return parsed if parsed > 0 else None def moxel_candidate_window_index(payload: dict[str, Any]) -> int: value = payload.get("moxel_candidate_window_index") if value in {None, ""}: return 1 try: parsed = int(value) except Exception: return 1 return parsed if parsed > 0 else 1 def moxel_candidate_reason_filter(payload: dict[str, Any]) -> set[str] | None: raw = payload.get("moxel_candidate_reasons") if raw is None or raw == "": return None values = raw if isinstance(raw, list) else str(raw).split(",") reasons = {str(value or "").strip() for value in values if str(value or "").strip()} return reasons or None def moxel_candidate_head_filter(payload: dict[str, Any]) -> set[int] | None: raw = payload.get("moxel_candidate_heads") if raw is None or raw == "": return None values = raw if isinstance(raw, list) else str(raw).split(",") heads: set[int] = set() for value in values: text = str(value or "").strip() if not text: continue try: heads.add(int(text)) except Exception: continue return heads or None def moxel_candidate_window(payload: dict[str, Any]) -> tuple[int | None, int | None]: start = payload.get("moxel_candidate_start") end = payload.get("moxel_candidate_end") start_value = int(start) if start not in {None, ""} else None end_value = int(end) if end not in {None, ""} else None if start_value is not None and end_value is not None and end_value < start_value: start_value, end_value = end_value, start_value return start_value, end_value def moxel_candidate_positions(candidate: dict[str, Any]) -> list[int]: positions: list[int] = [] for position in candidate.get("positions") or []: if not isinstance(position, str): continue match = re.fullmatch(r"\$\.(\d+)", position) if match: positions.append(int(match.group(1))) return positions def moxel_candidate_min_score(payload: dict[str, Any]) -> int | None: value = payload.get("moxel_candidate_min_score") if value in {None, ""}: return None try: return max(0, int(value)) except Exception: return None def filter_moxel_shape_candidates(candidates: Any, payload: dict[str, Any], limit: int) -> list[Any]: if not isinstance(candidates, list): return [] reason_filter = moxel_candidate_reason_filter(payload) head_filter = moxel_candidate_head_filter(payload) candidate_start, candidate_end = moxel_candidate_window(payload) min_score = moxel_candidate_min_score(payload) if not reason_filter and not head_filter and candidate_start is None and candidate_end is None and min_score is None: return limited_list(candidates, limit) filtered = [] for candidate in candidates: if not isinstance(candidate, dict): continue if candidate_start is not None or candidate_end is not None: positions = moxel_candidate_positions(candidate) if not positions: continue if not any( (candidate_start is None or position >= candidate_start) and (candidate_end is None or position <= candidate_end) for position in positions ): continue if head_filter is not None: try: head = int(candidate.get("head")) except Exception: continue if head not in head_filter: continue reasons = {str(reason or "") for reason in candidate.get("reasons") or []} if reason_filter and not reason_filter.issubset(reasons): continue try: score = int(candidate.get("score") or 0) except Exception: score = 0 if min_score is not None and score < min_score: continue filtered.append(candidate) return limited_list(filtered, limit) def moxel_effective_record_payload(payload: dict[str, Any], diagnostics: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any] | None]: rank = moxel_candidate_rank(payload) if rank is None: return payload, None window_index = moxel_candidate_window_index(payload) candidates = diagnostics.get("top_level_shape_candidates") if isinstance(diagnostics.get("top_level_shape_candidates"), list) else [] if rank > len(candidates): return payload, {"rank": rank, "status": "not_found", "message": "Requested MOXCEL candidate rank is outside top_level_shape_candidates."} candidate = candidates[rank - 1] if isinstance(candidates[rank - 1], dict) else {} windows = candidate.get("suggested_windows") if isinstance(candidate.get("suggested_windows"), list) else [] if window_index > len(windows) or not isinstance(windows[window_index - 1], dict): return payload, { "rank": rank, "window_index": window_index, "status": "window_not_found", "available_windows": len(windows), "message": "Requested MOXCEL candidate window index is outside suggested_windows.", } window = windows[window_index - 1] request_hint = window.get("request_hint") if isinstance(window.get("request_hint"), dict) else {} effective = dict(payload) for key in ("moxel_record_start", "moxel_record_end", "moxel_record_heads", "moxel_record_context"): if request_hint.get(key) not in {None, ""} and effective.get(key) in {None, ""}: effective[key] = request_hint.get(key) return effective, { "rank": rank, "window_index": window_index, "status": "ok", "head": candidate.get("head"), "score": candidate.get("score"), "reasons": candidate.get("reasons"), "window": {key: window.get(key) for key in ("center", "start", "end", "tree_position") if key in window}, "source": "top_level_shape_candidates", } def filter_moxel_top_level_records(records: Any, payload: dict[str, Any], limit: int) -> list[Any]: if not isinstance(records, list): return [] start, end = moxel_record_window(payload) heads = moxel_record_head_filter(payload) context_radius = moxel_record_context_radius(payload) indexed_records: list[tuple[int, dict[str, Any]]] = [] matched_positions: set[int] = set() for ordinal, record in enumerate(records): if not isinstance(record, dict): continue index = moxel_record_top_level_index(record) if start is not None and (index is None or index < start): continue if end is not None and (index is None or index > end): continue if heads is not None: try: head = int(record.get("head")) except Exception: continue if head not in heads: continue indexed_records.append((ordinal, record)) matched_positions.add(ordinal) if context_radius <= 0 or not matched_positions: return limited_list([record for _, record in indexed_records], limit) context_positions: set[int] = set() for ordinal in matched_positions: for candidate in range(max(0, ordinal - context_radius), min(len(records), ordinal + context_radius + 1)): context_positions.add(candidate) with_context: list[dict[str, Any]] = [] for ordinal, record in enumerate(records): if ordinal not in context_positions or not isinstance(record, dict): continue item = dict(record) item["match"] = ordinal in matched_positions with_context.append(item) return limited_list(with_context, limit) def summarize_moxel_top_level_records(records: Any) -> dict[str, Any]: if not isinstance(records, list): return {"total": 0} positions: list[int] = [] head_counts: dict[int, int] = {} match_present = False matched_count = 0 numeric_by_head: dict[int, list[list[int | float]]] = {} record_rows_by_head: dict[int, list[dict[str, Any]]] = {} for record in records: if not isinstance(record, dict): continue index = moxel_record_top_level_index(record) if index is not None: positions.append(index) try: head = int(record.get("head")) except Exception: head = None if head is not None: head_counts[head] = head_counts.get(head, 0) + 1 if "match" in record: match_present = True if record.get("match") is True: matched_count += 1 numeric_items = record.get("numeric_items") if head is not None and isinstance(numeric_items, list): numeric_values = [value for value in numeric_items if isinstance(value, (int, float)) and not isinstance(value, bool)] if numeric_values: numeric_by_head.setdefault(head, []).append(numeric_values) record_rows_by_head.setdefault(head, []).append( { "tree_position": record.get("tree_position"), "match": record.get("match") if "match" in record else None, "numeric_items": numeric_values, } ) summary: dict[str, Any] = { "total": len([record for record in records if isinstance(record, dict)]), "head_counts": [{"head": head, "count": count} for head, count in sorted(head_counts.items(), key=lambda item: (-item[1], item[0]))], } if positions: summary["position_range"] = {"start": min(positions), "end": max(positions)} if match_present: summary["matched_count"] = matched_count numeric_groups: list[dict[str, Any]] = [] for head, rows in sorted(numeric_by_head.items(), key=lambda item: (-len(item[1]), item[0])): max_len = max((len(row) for row in rows), default=0) varying_fields: list[dict[str, Any]] = [] constant_fields: list[dict[str, Any]] = [] field_hints: list[dict[str, Any]] = [] for field_index in range(max_len): values = [row[field_index] for row in rows if field_index < len(row)] if not values: continue distinct = sorted(set(values)) item = { "index": field_index, "values": distinct[:8], "distinct_count": len(distinct), } if len(distinct) == 1: if len(constant_fields) < 12: constant_fields.append(item) elif len(varying_fields) < 12: item["min"] = min(distinct) item["max"] = max(distinct) varying_fields.append(item) if len(field_hints) < 12: field_hints.extend(moxel_numeric_field_hints(field_index, values, len(rows))) field_hints = field_hints[:12] group = { "head": head, "records": len(rows), "numeric_length_min": min((len(row) for row in rows), default=0), "numeric_length_max": max_len, "varying_fields": varying_fields, "constant_fields": constant_fields, } if field_hints: group["field_hints"] = field_hints matrix_fields: list[int] = [] for hint in field_hints: try: field_index = int(hint.get("index")) except Exception: continue if field_index not in matrix_fields: matrix_fields.append(field_index) for varying_field in varying_fields: try: field_index = int(varying_field.get("index")) except Exception: continue if field_index not in matrix_fields: matrix_fields.append(field_index) if len(matrix_fields) >= 8: break if matrix_fields: matrix_rows: list[dict[str, Any]] = [] for row in record_rows_by_head.get(head, [])[:12]: numeric_items = row.get("numeric_items") if isinstance(row.get("numeric_items"), list) else [] values = {str(index): numeric_items[index] for index in matrix_fields if index < len(numeric_items)} matrix_row = { "tree_position": row.get("tree_position"), "values": values, } if row.get("match") is not None: matrix_row["match"] = row.get("match") matrix_rows.append(matrix_row) field_runs = moxel_numeric_field_runs(matrix_rows, matrix_fields) group["numeric_field_matrix"] = { "fields": matrix_fields, "rows": matrix_rows, "field_runs": field_runs, "field_transitions": moxel_numeric_field_transitions(field_runs), } numeric_groups.append(group) if numeric_groups: summary["numeric_field_summary"] = numeric_groups[:8] return summary def moxel_numeric_field_runs(matrix_rows: list[dict[str, Any]], fields: list[int]) -> list[dict[str, Any]]: field_runs: list[dict[str, Any]] = [] for field in fields: key = str(field) runs: list[dict[str, Any]] = [] current: dict[str, Any] | None = None for row in matrix_rows: values = row.get("values") if isinstance(row.get("values"), dict) else {} if key not in values: continue value = values.get(key) position = row.get("tree_position") matched = row.get("match") is True if current is None or current.get("value") != value: if current is not None: runs.append(current) current = { "value": value, "start": position, "end": position, "rows": 1, "matched_count": 1 if matched else 0, } else: current["end"] = position current["rows"] = int(current.get("rows") or 0) + 1 if matched: current["matched_count"] = int(current.get("matched_count") or 0) + 1 if current is not None: runs.append(current) if runs: field_runs.append({"field": field, "runs": runs[:12]}) return field_runs[:8] def moxel_numeric_field_transitions(field_runs: list[dict[str, Any]]) -> list[dict[str, Any]]: transitions_by_field: list[dict[str, Any]] = [] for field_group in field_runs: if not isinstance(field_group, dict): continue runs = field_group.get("runs") if isinstance(field_group.get("runs"), list) else [] transitions: list[dict[str, Any]] = [] for previous, current in zip(runs, runs[1:]): if not isinstance(previous, dict) or not isinstance(current, dict): continue transitions.append( { "from": previous.get("value"), "to": current.get("value"), "before": previous.get("end"), "after": current.get("start"), "before_rows": previous.get("rows"), "after_rows": current.get("rows"), "before_matched_count": previous.get("matched_count"), "after_matched_count": current.get("matched_count"), } ) if transitions: transitions_by_field.append({"field": field_group.get("field"), "transitions": transitions[:12]}) return transitions_by_field[:8] def moxel_numeric_field_hints(field_index: int, values: list[int | float], record_count: int) -> list[dict[str, Any]]: distinct = sorted(set(values)) if len(distinct) <= 1: return [] all_ints = all(isinstance(value, int) and not isinstance(value, bool) for value in distinct) hints: list[dict[str, Any]] = [] if all_ints and set(distinct).issubset({0, 1}): hints.append( { "index": field_index, "kind": "flag_like", "confidence": "low", "reason": "field varies only between 0 and 1 in the returned records", "values": distinct, } ) if all_ints and len(distinct) <= 6 and min(distinct) >= 0 and max(distinct) <= 32 and not set(distinct).issubset({0, 1}): hints.append( { "index": field_index, "kind": "small_enum_like", "confidence": "low", "reason": "field has a small non-negative integer domain in the returned records", "values": distinct, } ) if all_ints and min(distinct) >= 0 and len(distinct) >= 2: span = max(distinct) - min(distinct) if field_index > 0 and (max(distinct) > 32 or span > max(2, record_count)): hints.append( { "index": field_index, "kind": "coordinate_or_offset_like", "confidence": "low", "reason": "field is non-negative, varies across records, and has a wider numeric span", "min": min(distinct), "max": max(distinct), "values": distinct[:8], } ) return hints[:3] def template_structure_counts(structure: dict[str, Any]) -> dict[str, int]: cell_style_candidates = structure.get("cell_style_candidates") or [] return { "named_areas": len(structure.get("named_areas") or []), "named_range_candidates": len(structure.get("named_range_candidates") or []), "parameters": len(structure.get("parameters") or []), "cell_parameters": len(structure.get("cell_parameters") or []), "cell_text_identifiers": len(structure.get("cell_text_identifiers") or []), "cell_style_candidates": len(cell_style_candidates), "cell_style_coordinate_hints": len( [ item for item in cell_style_candidates if isinstance(item, dict) and isinstance(item.get("coordinate_hints"), dict) ] ), "cell_coordinate_hints": len(structure.get("cell_coordinate_hints") or []), "cells": len(structure.get("cells") or []), "area_cell_coverage": len(structure.get("area_cell_coverage") or []), "column_widths": len(structure.get("column_widths") or []), "format_table": len(structure.get("format_table") or []), "merged_ranges": len(structure.get("merged_ranges") or []), "merged_range_candidates": len(structure.get("merged_range_candidates") or []), "merge_record_block_candidates": len(structure.get("merge_record_block_candidates") or []), "merge_count_hints": len(structure.get("merge_count_hints") or []), "row_heights": len(structure.get("row_heights") or []), "moxel_record_diagnostics": len(structure.get("moxel_record_diagnostics") or []), } def compact_template_structure(structure: dict[str, Any], payload: dict[str, Any], *, view: str, sections: set[str] | None) -> dict[str, Any]: if not isinstance(structure, dict): return {} format_table = structure.get("format_table") if isinstance(structure.get("format_table"), list) else [] font_table = structure.get("font_table") if isinstance(structure.get("font_table"), list) else [] diagnostics_items = structure.get("moxel_record_diagnostics") if isinstance(structure.get("moxel_record_diagnostics"), list) else [] if not font_table: for diagnostics in diagnostics_items: font_table = extract_moxel_font_table_from_diagnostics(diagnostics if isinstance(diagnostics, dict) else None) if font_table: break if not format_table: for diagnostics in diagnostics_items: format_table = extract_moxel_format_table_from_diagnostics(diagnostics if isinstance(diagnostics, dict) else None) if format_table: break format_table = enrich_moxel_format_table_with_fonts(format_table, font_table) format_style_index_table = ( structure.get("format_style_index_table") if isinstance(structure.get("format_style_index_table"), dict) else {} ) if not format_style_index_table or not format_style_index_table.get("style_references"): for diagnostics in diagnostics_items: format_style_index_table = extract_moxel_format_style_index_table( format_table, diagnostics if isinstance(diagnostics, dict) else None, ) if format_style_index_table.get("style_references"): break format_table = enrich_moxel_format_table_with_style_references(format_table, format_style_index_table) cell_format_links = structure.get("cell_format_links") if isinstance(structure.get("cell_format_links"), list) else [] if not cell_format_links: cells = structure.get("cells") if isinstance(structure.get("cells"), list) else [] cell_format_links = extract_moxel_cell_format_links(cells, format_table) else: cells = structure.get("cells") if isinstance(structure.get("cells"), list) else [] cell_format_link_stats = ( structure.get("cell_format_link_stats") if isinstance(structure.get("cell_format_link_stats"), dict) else summarize_moxel_cell_format_links(cells, format_table, cell_format_links) ) limits = { "named_areas": template_limit(payload, "max_areas", 20 if view == "summary" else 500), "parameters": template_limit(payload, "max_parameters", 50 if view == "summary" else 500), "cells": template_limit(payload, "max_cells", 20 if view == "summary" else 1000), "coverage": template_limit(payload, "max_coverage", 20 if view == "summary" else 500), "widths": template_limit(payload, "max_widths", 50 if view == "summary" else 500), "merged": template_limit(payload, "max_merged", 20 if view == "summary" else 500), "strings": template_limit(payload, "max_strings", 20 if view == "summary" else 120), "moxel_records": template_limit(payload, "max_moxel_records", 20 if view == "summary" else 80), } result: dict[str, Any] = { "format": structure.get("format"), "capabilities": structure.get("capabilities") or {}, "dimensions": structure.get("dimensions"), "capacity_dimensions": structure.get("capacity_dimensions"), "used_dimensions": structure.get("used_dimensions"), "format_dimensions": structure.get("format_dimensions"), "counts": { **template_structure_counts(structure), "format_table": len(format_table), "font_table": len(font_table), "format_style_index_table": len(format_style_index_table.get("style_references") or []), "cell_format_links": len(cell_format_links), }, } include_all = view == "full" and sections is None def wants(*names: str) -> bool: return include_all or sections is None and view == "structure" or bool(sections and any(name in sections for name in names)) if wants("named_areas"): result["named_areas"] = limited_list(structure.get("named_areas"), limits["named_areas"]) if wants("named_range_candidates", "named_ranges"): result["named_range_candidates"] = limited_list(structure.get("named_range_candidates"), limits["named_areas"]) if wants("parameters"): result["parameters"] = limited_list(structure.get("parameters"), limits["parameters"]) if wants("cell_parameters"): result["cell_parameters"] = limited_list(structure.get("cell_parameters"), limits["parameters"]) if wants("cell_text_identifiers"): result["cell_text_identifiers"] = limited_list(structure.get("cell_text_identifiers"), limits["parameters"]) if wants("cell_style_candidates", "styles", "formats"): result["cell_style_candidates"] = limited_list(structure.get("cell_style_candidates"), limits["cells"]) if wants("cell_coordinate_hints", "coordinate_hints"): result["cell_coordinate_hints"] = limited_list(structure.get("cell_coordinate_hints"), limits["cells"]) if wants("cells"): result["cells"] = limited_list(structure.get("cells"), limits["cells"]) if wants("coverage", "area_cell_coverage"): result["area_cell_coverage"] = limited_list(structure.get("area_cell_coverage"), limits["coverage"]) if wants("column_widths", "widths", "formats"): result["column_widths"] = limited_list(structure.get("column_widths"), limits["widths"]) if wants("format_table", "formats"): result["format_table"] = limited_list(format_table, limits["widths"]) if wants("font_table", "fonts", "formats"): result["font_table"] = limited_list(font_table, limits["widths"]) if wants("format_style_index_table", "style_index_table", "style_references", "formats", "styles"): result["format_style_index_table"] = { **format_style_index_table, "style_references": limited_list(format_style_index_table.get("style_references"), limits["widths"]), "candidate_records": limited_list(format_style_index_table.get("candidate_records"), limits["moxel_records"]), } if wants("cell_format_links", "format_links", "formats"): result["cell_format_links"] = limited_list(cell_format_links, limits["cells"]) if wants("cell_format_link_stats", "format_links", "formats"): result["cell_format_link_stats"] = cell_format_link_stats if wants("row_heights", "heights", "formats"): result["row_heights"] = limited_list(structure.get("row_heights"), limits["widths"]) if wants("merged_ranges"): result["merged_ranges"] = limited_list(structure.get("merged_ranges"), limits["merged"]) if wants("merged_range_candidates", "merges"): result["merged_range_candidates"] = limited_list(structure.get("merged_range_candidates"), limits["merged"]) if wants("merge_record_block_candidates", "merges"): result["merge_record_block_candidates"] = limited_list(structure.get("merge_record_block_candidates"), limits["merged"]) if wants("merge_count_hints", "merges"): result["merge_count_hints"] = limited_list(structure.get("merge_count_hints"), limits["merged"]) if wants("diagnostics"): result["diagnostics"] = limited_list(structure.get("diagnostics"), 50) if wants("payload"): result["payload"] = structure.get("payload") or {} if wants("tree_root"): result["tree_root_summary"] = structure.get("tree_root_summary") if wants("undecoded", "undecoded_evidence"): result["undecoded_evidence"] = compact_moxel_undecoded_evidence(structure, payload, view=view) if wants("moxel_records", "moxel_record_diagnostics"): result["moxel_record_diagnostics"] = [] for diagnostics in limited_list(structure.get("moxel_record_diagnostics"), 20): if not isinstance(diagnostics, dict): continue item = dict(diagnostics) item["head_counts"] = limited_list(item.get("head_counts"), limits["moxel_records"]) original_head_samples = diagnostics.get("head_samples") item["head_samples"] = [] for head_sample in limited_list(original_head_samples, limits["moxel_records"]): if not isinstance(head_sample, dict): continue compact_head_sample = dict(head_sample) compact_head_sample["samples"] = limited_list(compact_head_sample.get("samples"), 3) item["head_samples"].append(compact_head_sample) effective_payload, candidate_focus = moxel_effective_record_payload(payload, diagnostics) if candidate_focus is not None: item["top_level_candidate_focus"] = candidate_focus window_start, window_end = moxel_record_window(effective_payload) head_filter = moxel_record_head_filter(effective_payload) if window_start is not None or window_end is not None: item["top_level_window"] = { "start": window_start, "end": window_end, "source": "moxel_record_start/moxel_record_end", } if head_filter is not None: item["top_level_head_filter"] = sorted(head_filter) context_radius = moxel_record_context_radius(effective_payload) if context_radius > 0: item["top_level_context"] = { "radius": context_radius, "source": "moxel_record_context", "match_field": "match", } filtered_records = filter_moxel_top_level_records(item.get("top_level_records"), effective_payload, limits["moxel_records"]) item["top_level_records"] = filtered_records item["top_level_record_summary"] = summarize_moxel_top_level_records(filtered_records) item["top_level_shapes"] = limited_list(item.get("top_level_shapes"), limits["moxel_records"]) reason_filter = moxel_candidate_reason_filter(payload) if reason_filter is not None: item["top_level_candidate_reason_filter"] = sorted(reason_filter) candidate_head_filter = moxel_candidate_head_filter(payload) if candidate_head_filter is not None: item["top_level_candidate_head_filter"] = sorted(candidate_head_filter) candidate_start, candidate_end = moxel_candidate_window(payload) if candidate_start is not None or candidate_end is not None: item["top_level_candidate_window_filter"] = { "start": candidate_start, "end": candidate_end, "source": "moxel_candidate_start/moxel_candidate_end", } min_score = moxel_candidate_min_score(payload) if min_score is not None: item["top_level_candidate_min_score"] = min_score filtered_candidates = filter_moxel_shape_candidates(item.get("top_level_shape_candidates"), payload, limits["moxel_records"]) if isinstance(item.get("top_level_candidate_summary"), dict): summary = dict(item.get("top_level_candidate_summary") or {}) summary["returned_count"] = len(filtered_candidates) item["top_level_candidate_summary"] = summary item["top_level_shape_candidates"] = filtered_candidates item["samples"] = limited_list(item.get("samples"), limits["moxel_records"]) item["coordinate_like_samples"] = limited_list(item.get("coordinate_like_samples"), limits["moxel_records"]) result["moxel_record_diagnostics"].append(item) if wants("strings"): result["strings_sample"] = limited_list(structure.get("strings_sample"), limits["strings"]) if view == "summary" and sections is None: result["samples"] = { "named_areas": limited_list(structure.get("named_areas"), min(5, limits["named_areas"])), "cells": limited_list(structure.get("cells"), min(8, limits["cells"])), "cell_parameters": limited_list(structure.get("cell_parameters"), min(8, limits["parameters"])), "cell_style_candidates": limited_list(structure.get("cell_style_candidates"), min(8, limits["cells"])), "cell_coordinate_hints": limited_list(structure.get("cell_coordinate_hints"), min(8, limits["cells"])), "column_widths": limited_list(structure.get("column_widths"), min(8, limits["widths"])), "merged_range_candidates": limited_list(structure.get("merged_range_candidates"), min(8, limits["merged"])), "merge_record_block_candidates": limited_list(structure.get("merge_record_block_candidates"), min(8, limits["merged"])), "merge_count_hints": limited_list(structure.get("merge_count_hints"), min(8, limits["merged"])), } return result def compact_template_analysis(analysis: dict[str, Any], payload: dict[str, Any], *, view: str, sections: set[str] | None) -> dict[str, Any]: if not isinstance(analysis, dict): return {} result: dict[str, Any] = { "status": analysis.get("status"), "named_area_count": analysis.get("named_area_count"), "parameter_count": analysis.get("parameter_count"), "checks": analysis.get("checks") or {}, "counts": analysis.get("counts") or {}, "issues": limited_list(analysis.get("issues"), template_limit(payload, "max_issues", 50)), } include_all = view == "full" and sections is None def wants(*names: str) -> bool: return include_all or sections is None and view == "structure" or bool(sections and any(name in sections for name in names)) if wants("width_variants"): result["width_variants"] = limited_list(analysis.get("width_variants"), template_limit(payload, "max_width_variants", 100)) if wants("intersections"): result["intersections"] = limited_list(analysis.get("intersections"), template_limit(payload, "max_intersections", 20 if view == "summary" else 200)) if wants("cells"): result["cells_sample"] = limited_list(analysis.get("cells_sample"), template_limit(payload, "max_cells", 20 if view == "summary" else 500)) if wants("cell_parameters"): result["cell_parameters"] = limited_list(analysis.get("cell_parameters"), template_limit(payload, "max_parameters", 50)) if wants("cell_text_identifiers"): result["cell_text_identifiers"] = limited_list(analysis.get("cell_text_identifiers"), template_limit(payload, "max_parameters", 50)) if wants("cell_coordinate_hints", "coordinate_hints"): result["cell_coordinate_hints"] = limited_list(analysis.get("cell_coordinate_hints"), template_limit(payload, "max_cells", 20 if view == "summary" else 500)) if wants("cell_style_coordinate_hints", "coordinate_hints"): result["cell_style_coordinate_hints"] = limited_list(analysis.get("cell_style_coordinate_hints"), template_limit(payload, "max_cells", 20 if view == "summary" else 500)) if wants("parameters"): result["parameters_without_cells"] = limited_list(analysis.get("parameters_without_cells"), template_limit(payload, "max_parameters", 50)) if wants("coverage", "area_cell_coverage"): result["area_cell_coverage"] = limited_list(analysis.get("area_cell_coverage"), template_limit(payload, "max_coverage", 20 if view == "summary" else 500)) if wants("column_widths", "widths"): result["column_widths"] = limited_list(analysis.get("column_widths"), template_limit(payload, "max_widths", 50)) if wants("merged_ranges"): result["merged_ranges"] = limited_list(analysis.get("merged_ranges"), template_limit(payload, "max_merged", 50)) if wants("merged_range_candidates", "merges"): result["merged_range_candidates"] = limited_list(analysis.get("merged_range_candidates"), template_limit(payload, "max_merged", 50)) if wants("merge_record_block_candidates", "merges"): result["merge_record_block_candidates"] = limited_list(analysis.get("merge_record_block_candidates"), template_limit(payload, "max_merged", 50)) if wants("merge_count_hints", "merges"): result["merge_count_hints"] = limited_list(analysis.get("merge_count_hints"), template_limit(payload, "max_merged", 50)) if wants("area_widths"): result["area_widths"] = limited_list(analysis.get("area_widths"), template_limit(payload, "max_areas", 200)) return result def apply_template_response_view(result: dict[str, Any], payload: dict[str, Any], *, default_view: str = "full", map_mode: bool = False) -> dict[str, Any]: view = str(payload.get("view") or default_view or "full").strip().lower() if view not in {"summary", "structure", "full"}: view = default_view if default_view in {"summary", "structure", "full"} else "full" sections = parse_template_sections(payload) response = dict(result) response["view"] = view if sections: response["sections"] = sorted(sections) templates = [] for template in response.get("templates") or []: if not isinstance(template, dict): continue item = dict(template) if "structure" in item: item["structure"] = compact_template_structure(item.get("structure") or {}, payload, view=view, sections=sections) if "analysis" in item: item["analysis"] = compact_template_analysis(item.get("analysis") or {}, payload, view=view, sections=sections) if view in {"summary", "structure"} and not (sections and "parts" in sections): item.pop("parts", None) templates.append(item) response["templates"] = templates if map_mode: response["schema"] = "onec_templates_map.v1" return response def templates_read(payload: dict[str, Any], *, analyze: bool = False) -> dict[str, Any]: method = "templates.analyze" if analyze else "templates.read" payload = normalize_template_route_ref_payload(payload) payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload if canonical_kind(str(payload.get("kind") or payload.get("object_type") or "")) == "CommonTemplate": base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error timeout_value, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1) if timeout_error: return timeout_error table_or_error = metadata_storage_table(payload, method) if isinstance(table_or_error, dict): return table_or_error selector_payload = dict(payload) selector_payload.pop("view", None) guid, _, object_card, resolve_error = resolve_object_guid( selector_payload, base_id_or_error, timeout_seconds=int(timeout_value or 60), method=method, table=table_or_error, ) if resolve_error: return resolve_error direct = read_template_by_guid( { **payload, "guid": guid, "kind": "Template", "table": table_or_error, "timeout_seconds": int(timeout_value or 60), }, analyze=analyze, ) if direct.get("status") == "ok": direct = dict(direct) direct["object"] = object_card templates = [] for template_item in direct.get("templates") or []: item = dict(template_item) item["name"] = item.get("name") or (object_card or {}).get("name") item["kind"] = "CommonTemplate" item["ref"] = (object_card or {}).get("ref") or object_selector_ref("CommonTemplate", str(item.get("name") or "")) templates.append(item) direct["templates"] = templates return direct if payload.get("file_name") or (payload.get("table") in {"ConfigCAS", "ConfigCASSave"} and payload.get("guid") and not is_guid_text(str(payload.get("guid") or ""))): return read_template_by_route(payload, analyze=analyze) if canonical_kind(str(payload.get("kind") or payload.get("object_type") or "")) == "Template" or ( payload.get("guid") and not payload.get("name") and not payload.get("object_name") and not payload.get("owner_ref") ): return read_template_by_guid(payload, analyze=analyze) base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error timeout_value, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1) if timeout_error: return timeout_error timeout_seconds = int(timeout_value or 60) include_content = truthy(payload.get("include_content")) max_content_bytes = int(payload.get("max_content_bytes") or TEMPLATE_CONTENT_DEFAULT_MAX_BYTES) metadata_payload = dict(payload) metadata_payload.pop("view", None) details = metadata_object_template_details({**metadata_payload, "include_storage": True, "include_preview": True, "timeout_seconds": timeout_seconds}) if details.get("status") != "ok": result = dict(details) result["method"] = method return result templates = [] for template in details.get("templates") or []: template_row = dict(template) detailed_parts = [] parts_result = metadata_object_parts( { "base_id": base_id, "guid": template.get("guid"), "kind": "Template", "table": payload.get("table") or "Config", "include_storage": True, "include_text": False, "include_tree": False, "timeout_seconds": timeout_seconds, } ) if parts_result.get("status") == "ok": detailed_parts = [ template_part_structure( base_id, part, timeout_seconds=timeout_seconds, refresh_cache=truthy(payload.get("refresh_cache")), include_content=include_content, max_content_bytes=max_content_bytes, ) for part in parts_result.get("parts") or [] if isinstance(part, dict) ] template_row["parts"] = detailed_parts template_row["structure"] = merge_template_structures(detailed_parts) if analyze: template_row["analysis"] = analyze_template_structure(template_row["structure"]) templates.append(template_row) return apply_template_response_view({ "schema": "onec_templates_analyze.v1" if analyze else "onec_templates_read.v1", "status": "ok", "base_id": base_id, "source": details.get("source"), "object": details.get("object"), "query": details.get("query"), "templates": templates, "counts": {"templates": len(templates)}, }, payload) def templates_map(payload: dict[str, Any]) -> dict[str, Any]: map_payload = dict(payload) map_payload.setdefault("view", "summary") result = templates_read(map_payload, analyze=True) if isinstance(result, dict) and result.get("status") == "ok": mapped = dict(result) mapped["schema"] = "onec_templates_map.v1" mapped.setdefault("view", "summary") return mapped return result def metadata_command_group_commands( *, base_id: str, object_card: dict[str, Any], requested_command: str | None, include_storage: bool, table: str, max_commands: int, timeout_seconds: int, ) -> dict[str, Any]: object_guid = str(object_card.get("guid") or "").strip().lower() if not is_guid_text(object_guid): return { "schema": "onec_object_commands.v1", "status": "blocked", "base_id": base_id, "object": public_metadata_row(object_card, include_storage=include_storage), "commands": [], "object_commands": [], "form_commands": [], "counts": {"commands": 0, "object_commands": 0, "form_commands": 0, "scanned_common_commands": 0}, "capabilities": {"object_commands": False, "form_commands": False, "reason": "CommandGroup GUID was not resolved."}, } if table not in {"Config", "ConfigSave"}: return { "schema": "onec_object_commands.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_metadata"}, "object": public_metadata_row(object_card, include_storage=include_storage), "query": {"command": requested_command, "include_storage": include_storage}, "commands": [], "object_commands": [], "form_commands": [], "counts": {"commands": 0, "object_commands": 0, "form_commands": 0, "scanned_common_commands": 0}, "capabilities": { "object_commands": False, "form_commands": False, "reason": "CommandGroup reverse lookup currently supports base Config/ConfigSave only.", }, } listed = list_objects( "CommonCommand", base_id=base_id, limit=5000, offset=0, include_storage=False, include_missing=False, only_missing=False, exact_counts=True, refresh_cache=False, table="Config", ) if listed.get("status") != "ok": result = dict(listed) result["method"] = "metadata.object.commands" return result common_commands = [item for item in listed.get("objects") or [] if isinstance(item, dict) and is_guid_text(item.get("guid"))] file_names = [str(item["guid"]).lower() for item in common_commands] active_files, _, active_error = read_storage_files_bytes( base_id, "Config", file_names, timeout_seconds=timeout_seconds, ) if active_error: result = dict(active_error) result["method"] = "metadata.object.commands" return result effective_files = dict(active_files or {}) saved_overrides = 0 saved_files: dict[str, bytes] = {} if table == "ConfigSave": saved_files, _, saved_error = read_storage_files_bytes( base_id, "ConfigSave", file_names, timeout_seconds=timeout_seconds, ) if saved_error: result = dict(saved_error) result["method"] = "metadata.object.commands" return result for file_name, data in (saved_files or {}).items(): effective_files[file_name] = data saved_overrides += 1 reverse_index = index_common_command_groups(common_commands, effective_files) wanted = normalize(requested_command or "") matches: list[dict[str, Any]] = [] undecodable = int(reverse_index["undecodable"]) source_missing = int(reverse_index["source_missing"]) for item in reverse_index["groups"].get(object_guid, []): guid = str(item.get("guid") or "").lower() command = { "scope": "common", "kind": "CommonCommand", "guid": guid, "name": item.get("name"), "synonym": item.get("synonym"), "ref": item.get("ref") or object_selector_ref("CommonCommand", str(item.get("name") or "")), "module": { "kind": "command_module", "name": "Модуль команды", "read_selector": { "method": "modules.read", "base_id": base_id, "ref": item.get("ref") or object_selector_ref("CommonCommand", str(item.get("name") or "")), "module_ordinal": 1, "state": "working", }, }, } if wanted: match_by = command_match_by(command, requested_command) if not match_by: continue command["match_by"] = match_by if include_storage: command["storage"] = { "table": "ConfigSave" if guid in (saved_files or {}) else "Config", "file_name": guid, "relation": "common_command.group_guid", } matches.append(command) total_matches = len(matches) commands = matches[:max_commands] if wanted and not commands: result = child_not_found("metadata.object.commands", "Команда", requested_command, object_card, base_id=base_id) result.update( { "schema": "onec_object_commands.v1", "source": {"kind": "live_metadata"}, "query": {"command": requested_command, "include_storage": include_storage, "max_commands": max_commands}, "commands": [], "object_commands": [], "form_commands": [], "counts": { "commands": 0, "object_commands": 0, "form_commands": 0, "matched_common_commands": 0, "scanned_common_commands": len(common_commands), "source_missing": source_missing, "undecodable": undecodable, }, } ) return result return { "schema": "onec_object_commands.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_metadata"}, "object": public_metadata_row(object_card, include_storage=include_storage), "query": {"command": requested_command, "include_storage": include_storage, "max_commands": max_commands}, "commands": commands, "object_commands": commands, "form_commands": [], "counts": { "commands": len(commands), "object_commands": len(commands), "form_commands": 0, "matched_common_commands": total_matches, "scanned_common_commands": len(common_commands), "saved_overrides": saved_overrides, "source_missing": source_missing, "undecodable": undecodable, "truncated": max(0, total_matches - len(commands)), }, "capabilities": { "object_commands": True, "form_commands": False, "reverse_relation": "CommonCommand.group", "coverage": "common_commands", }, "diagnostics": { "note": "CommandGroup membership is a reverse relation from CommonCommand.Group; the group payload has no child command collection.", }, } def metadata_object_commands(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, "metadata.object.commands") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, "metadata.object.commands") if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error extension_guid = str(payload.get("extension_guid") or "").strip().lower() if "extension_guid" in payload and not is_guid_text(extension_guid): return invalid_argument("metadata.object.commands", "extension_guid", "extension_guid must be a GUID string.") requested_command, requested_command_error = optional_string_filter(payload, ["command", "name_filter"], method="metadata.object.commands") if requested_command_error: return requested_command_error include_storage, include_storage_error = strict_include_storage(payload, "metadata.object.commands") if include_storage_error: return include_storage_error include_storage = bool(include_storage) table_or_error = metadata_storage_table(payload, "metadata.object.commands") if isinstance(table_or_error, dict): return table_or_error table = table_or_error include_form_commands, include_form_commands_error = strict_bool_argument(payload, "include_form_commands", method="metadata.object.commands", default=True) if include_form_commands_error: return include_form_commands_error include_form_commands = bool(include_form_commands) refresh_cache, refresh_cache_error = strict_bool_argument(payload, "refresh_cache", method="metadata.object.commands", default=False) if refresh_cache_error: return refresh_cache_error max_forms, max_forms_error = parse_int_argument(payload, "max_forms", method="metadata.object.commands", default=20, minimum=1, maximum=100) if max_forms_error: return max_forms_error max_items, max_items_error = parse_int_argument(payload, "max_items", method="metadata.object.commands", default=200, minimum=1, maximum=5000) if max_items_error: return max_items_error max_form_items, max_form_items_error = parse_int_argument(payload, "max_form_items", method="metadata.object.commands", default=int(max_items or 200), minimum=1, maximum=5000) if max_form_items_error: return max_form_items_error max_attributes, max_attributes_error = parse_int_argument(payload, "max_attributes", method="metadata.object.commands", default=100, minimum=1, maximum=5000) if max_attributes_error: return max_attributes_error max_commands, max_commands_error = parse_int_argument(payload, "max_commands", method="metadata.object.commands", default=200, minimum=1, maximum=5000) if max_commands_error: return max_commands_error limit, limit_error = parse_int_argument(payload, "limit", method="metadata.object.commands", default=20, minimum=1, maximum=5000) if limit_error: return limit_error ordinal_argument_error = validate_explicit_ordinal_arguments(payload, "metadata.object.commands") if ordinal_argument_error: return ordinal_argument_error view, view_error = parse_view_argument(payload, "metadata.object.commands") if view_error: return view_error timeout_value, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.commands", default=60, minimum=1) if timeout_error: return timeout_error timeout_seconds = int(timeout_value or 60) wanted = normalize(requested_command or "") selector_name = str(payload.get("name") or payload.get("guid") or "") object_probe = get_object( payload.get("kind"), selector_name, base_id=base_id, view=str(view or "effective"), limit=int(limit or 20), include_storage=False, ordinal=first_non_empty_arg(payload, "ordinal", "index", "object_index"), include_semantic=False, timeout_seconds=timeout_seconds, table=table, extension_guid=extension_guid or None, ) if object_probe.get("status") != "ok": result = dict(object_probe) result["method"] = "metadata.object.commands" return result object_card = object_probe.get("object") or {} object_guid = str(object_card.get("guid") or "").lower() def with_saved_command_module_selector(item: dict[str, Any]) -> dict[str, Any]: command = dict(item) command_guid = str(command.get("guid") or "").strip().lower() if table == "ConfigCASSave" and extension_guid and is_guid_text(command_guid) and command.get("status") == "ok": command["module"] = {"kind": "command_module", "name": "Модуль команды"} command["read_selector"] = { "method": "modules.read", "base_id": base_id, "kind": object_card.get("kind") or payload.get("kind"), "guid": object_guid, "module_ref": f"{table}:{extension_guid}__{command_guid}.2", } if config: metadata_module_owner_cache_upsert( config, command["read_selector"]["module_ref"], { "kind": object_card.get("kind") or payload.get("kind"), "name": object_card.get("name") or payload.get("name"), "synonym": object_card.get("synonym"), "guid": object_guid, }, module={"kind": "command_module", "name": "Модуль команды", "suffix": "2"}, ) return command config, _ = sql_config_for_base(base_id) cache_role = metadata_commands_cache_role(include_form_commands) if config and object_guid and not include_storage and not refresh_cache: cached_result = metadata_guid_index_lookup_payload(config, object_guid, cache_role) if cached_result: cached_public = dict(cached_result) object_commands_cached_all = cached_public.get("object_commands") or [] object_commands_cached = [ with_saved_command_module_selector(item) for item in object_commands_cached_all if public_visible_command(item, include_storage=include_storage) ] form_commands_cached = cached_public.get("form_commands") or [] hidden_missing_object_commands = max(0, len(object_commands_cached_all) - len(object_commands_cached)) def command_matches(item: dict[str, Any]) -> bool: if not wanted: return True return ( wanted in normalize(item.get("name") or "") or wanted in normalize(item.get("title") or "") or wanted in normalize(item.get("synonym") or "") ) object_commands_filtered = [item for item in object_commands_cached if command_matches(item)] form_commands_filtered = [item for item in form_commands_cached if command_matches(item)] if wanted: for item in [*object_commands_filtered, *form_commands_filtered]: match_by = command_match_by(item, requested_command) if match_by: item["match_by"] = match_by object_commands_limited, form_commands_limited, commands_filtered, limit_counts = limit_object_commands_result( object_commands_filtered, form_commands_filtered, int(max_commands or 200), ) if wanted and not commands_filtered: result = child_not_found("metadata.object.commands", "Команда", requested_command, merged_object if (merged_object := (cached_public.get("object") or object_card)) else object_card, base_id=base_id) result.update( { "schema": "onec_object_commands.v1", "source": {"kind": "live_metadata"}, "query": {"command": requested_command, "include_storage": include_storage}, "commands": [], "object_commands": [], "form_commands": [], "counts": { **(cached_public.get("counts") or {}), "commands": 0, "object_commands": 0, "form_commands": 0, **limit_counts, "hidden_missing_object_commands": hidden_missing_object_commands, }, "cache": {"status": "hit", "role": cache_role}, } ) return result cached_public.update( { "query": {"command": requested_command, "include_storage": include_storage, "max_commands": int(max_commands or 200)}, "commands": commands_filtered, "object_commands": object_commands_limited, "form_commands": form_commands_limited, "counts": { **(cached_public.get("counts") or {}), "commands": len(commands_filtered), "object_commands": len(object_commands_limited), "form_commands": len(form_commands_limited), **limit_counts, "hidden_missing_object_commands": hidden_missing_object_commands, }, "cache": {"status": "hit", "role": cache_role}, } ) return cached_public object_kind = str(object_card.get("kind") or payload.get("kind") or "") if object_kind == "CommandGroup": command_group_result = metadata_command_group_commands( base_id=base_id, object_card=object_card, requested_command=requested_command, include_storage=include_storage, table=table, max_commands=int(max_commands or 200), timeout_seconds=timeout_seconds, ) command_group_counts = command_group_result.get("counts") if isinstance(command_group_result.get("counts"), dict) else {} if ( config and object_guid and not include_storage and not wanted and command_group_result.get("status") == "ok" and int(command_group_counts.get("truncated") or 0) == 0 ): metadata_guid_index_upsert( config, { "guid": object_guid, "guid_role": cache_role, "kind": object_card.get("kind"), "kind_ru": object_card.get("kind_ru"), "public_kind": object_card.get("public_kind"), "name": object_card.get("name"), "synonym": object_card.get("synonym"), "presentation": ".".join(part for part in [object_card.get("kind_ru"), object_card.get("name")] if part), "payload": command_group_result, "source_file": object_guid, }, ) command_group_result["cache"] = {"status": "stored", "role": cache_role} return command_group_result if object_kind not in RELATED_SECTION_RULES: result = { "schema": "onec_object_commands.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_metadata"}, "object": object_card, "query": {"command": requested_command, "include_storage": include_storage}, "commands": [], "object_commands": [], "form_commands": [], "counts": {"commands": 0, "object_commands": 0, "form_commands": 0, "related": 0}, "capabilities": { "object_commands": False, "form_commands": False, "reason": "У этого вида объекта адаптер не знает разделов команд или форм.", }, } if config and object_guid and not include_storage and not wanted: metadata_guid_index_upsert( config, { "guid": object_guid, "guid_role": cache_role, "kind": object_card.get("kind"), "kind_ru": object_card.get("kind_ru"), "public_kind": object_card.get("public_kind"), "name": object_card.get("name"), "synonym": object_card.get("synonym"), "presentation": ".".join(part for part in [object_card.get("kind_ru"), object_card.get("name")] if part), "payload": result, "source_file": object_guid, }, ) result["cache"] = {"status": "stored", "role": cache_role} return result related_result = metadata_object_related( { **payload, "guid": object_guid, "kind": object_card.get("kind") or payload.get("kind"), "include_text": False, "table": table, } ) if related_result.get("status") != "ok": result = dict(related_result) result["method"] = "metadata.object.commands" return result object_commands = [] seen_object_commands: set[tuple[str, str]] = set() hidden_missing_object_commands = 0 for item in related_result.get("related") or []: if item.get("category") != "Command": continue identity = item.get("identity") or item.get("record_identity") or {} synonyms = identity.get("synonyms") or {} synonym = next(iter(synonyms.values()), None) if isinstance(synonyms, dict) else None if not include_storage and item.get("status") == "source_missing" and not identity.get("name") and not synonym: hidden_missing_object_commands += 1 continue if wanted and wanted not in normalize(identity.get("name") or "") and wanted not in normalize(synonym or ""): continue command = public_child_identity(item) command["role"] = item.get("role") command["root"] = item.get("root") command["scope"] = "object" command = with_saved_command_module_selector(command) command_key = (str(command.get("guid") or "").casefold(), normalize(command.get("name") or command.get("synonym") or "")) if command_key in seen_object_commands: continue seen_object_commands.add(command_key) if wanted: match_by = command_match_by({"name": command.get("name"), "synonym": command.get("synonym")}, requested_command) if match_by: command["match_by"] = match_by if include_storage: command["related"] = item if not public_visible_command(command, include_storage=include_storage): hidden_missing_object_commands += 1 continue object_commands.append(command) form_commands = [] related_counts = related_result.get("counts") or {} related_by_category = related_counts.get("by_category") if isinstance(related_counts.get("by_category"), dict) else {} has_related_forms = int((related_by_category or {}).get("Form") or 0) > 0 if include_form_commands and has_related_forms: forms_result = metadata_object_form_details( { **payload, "guid": object_guid, "kind": object_card.get("kind") or payload.get("kind"), "include_storage": include_storage, "max_forms": max_forms, "max_items": max_form_items, "max_attributes": max_attributes, "max_commands": max_commands, "include_module_text": False, } ) if forms_result.get("status") == "ok": seen: set[tuple[str, str]] = set() for form in forms_result.get("forms") or []: form_name = form.get("name") for item in form.get("commands") or []: name = str(item.get("name") or "") if wanted and wanted not in normalize(name) and wanted not in normalize(item.get("title") or ""): continue key = (str(form_name or ""), name) if key in seen: continue seen.add(key) command = { "scope": "form", "form": form_name, "name": name, "title": item.get("title"), "id": item.get("id"), } if wanted: match_by = command_match_by(command, requested_command) if match_by: command["match_by"] = match_by form_commands.append(command) else: form_commands = [] full_object_commands = list(object_commands) full_form_commands = list(form_commands) object_commands, form_commands, commands, limit_counts = limit_object_commands_result( full_object_commands, full_form_commands, int(max_commands or 200), ) if wanted and not commands: result = child_not_found("metadata.object.commands", "Команда", requested_command, related_result.get("object") or object_card, base_id=base_id) result.update( { "schema": "onec_object_commands.v1", "source": related_result.get("source") if include_storage else {"kind": "live_metadata"}, "query": {"command": requested_command, "include_storage": include_storage, "max_commands": int(max_commands or 200)}, "commands": [], "object_commands": [], "form_commands": [], "counts": { "commands": 0, "object_commands": 0, "form_commands": 0, **limit_counts, "related": (related_result.get("counts") or {}).get("related"), }, } ) return result result = { "schema": "onec_object_commands.v1", "status": "ok", "base_id": base_id, "source": related_result.get("source") if include_storage else {"kind": "live_metadata"}, "object": related_result.get("object") or object_card, "query": {"command": requested_command, "include_storage": include_storage, "max_commands": int(max_commands or 200)}, "commands": commands, "object_commands": object_commands, "form_commands": form_commands, "counts": { "commands": len(commands), "object_commands": len(object_commands), "form_commands": len(form_commands), **limit_counts, "hidden_missing_object_commands": hidden_missing_object_commands, "related": (related_result.get("counts") or {}).get("related"), }, "capabilities": { "object_commands": True, "form_commands": include_form_commands and has_related_forms, }, } if config and object_guid and not include_storage and not wanted: metadata_guid_index_upsert( config, { "guid": object_guid, "guid_role": cache_role, "kind": object_card.get("kind"), "kind_ru": object_card.get("kind_ru"), "public_kind": object_card.get("public_kind"), "name": object_card.get("name"), "synonym": object_card.get("synonym"), "presentation": ".".join(part for part in [object_card.get("kind_ru"), object_card.get("name")] if part), "payload": { **result, "query": {"command": requested_command, "include_storage": include_storage}, "commands": [*full_object_commands, *full_form_commands], "object_commands": full_object_commands, "form_commands": full_form_commands, "counts": { **(result.get("counts") or {}), "commands": len(full_object_commands) + len(full_form_commands), "object_commands": len(full_object_commands), "form_commands": len(full_form_commands), "commands_total": len(full_object_commands) + len(full_form_commands), "commands_truncated": False, }, }, "source_file": object_guid, }, ) result["cache"] = {"status": "stored", "role": cache_role} return result def config_tree_identity(node: Any) -> dict[str, Any] | None: """Decode the standard 1C identity block from an arbitrary Config subtree.""" try: from parser.config_object import find_identity except Exception: return None identity = find_identity(node) return identity.to_dict() if identity else None def config_tree_list_items(node: Any) -> list[Any]: if isinstance(node, dict) and node.get("type") in {"list", "sequence"}: return list(node.get("items") or []) return [] def config_tree_guids(node: Any) -> list[str]: result: list[str] = [] def walk(value: Any) -> None: scalar_value = config_tree_scalar(value).strip().lower() if is_guid_text(scalar_value): result.append(scalar_value) return for child in config_tree_list_items(value): walk(child) walk(node) return result def public_metadata_guid_reference(base_id: str, guid: str) -> dict[str, Any]: normalized_guid = str(guid or "").strip().lower() cached = metadata_cache_lookup_guid(base_id, normalized_guid) if isinstance(cached, dict) and cached.get("name"): kind = canonical_kind(str(cached.get("kind") or "")) name = str(cached.get("name") or "") return { "guid": normalized_guid, "kind": kind or cached.get("kind"), "name": name, "ref": object_selector_ref(kind, name), "status": "ok", } config, _ = sql_config_for_base(base_id) if config: for cache_role in ("nested_metadata_reference_v1", "integration_channel_reference_v1"): nested_cached = metadata_guid_index_lookup_payload(config, normalized_guid, cache_role) if isinstance(nested_cached, dict) and nested_cached.get("name"): return {**nested_cached, "status": "ok"} return {"guid": normalized_guid, "status": "unresolved"} def config_tree_identity_records(tree: Any) -> dict[str, dict[str, Any]]: """Collect identities declared directly in a Config tree, including nested metadata.""" result: dict[str, dict[str, Any]] = {} def walk(node: Any, path: list[int]) -> None: items = config_tree_list_items(node) for index in range(max(0, len(items) - 2)): marker = config_tree_list_items(items[index]) if ( len(marker) != 3 or config_tree_scalar(marker[0]) != "1" or config_tree_scalar(marker[1]) != "0" ): continue guid = config_tree_scalar(marker[2]).strip().lower() name = config_tree_scalar(items[index + 1]).strip() synonyms_node = config_tree_list_items(items[index + 2]) if not is_guid_text(guid) or not name or not synonyms_node or not config_tree_scalar(synonyms_node[0]).isdigit(): continue synonyms: dict[str, str] = {} for synonym_index in range(1, len(synonyms_node) - 1, 2): language = config_tree_scalar(synonyms_node[synonym_index]) value = config_tree_scalar(synonyms_node[synonym_index + 1]) if language and value: synonyms[language] = value result.setdefault( guid, { "guid": guid, "name": name, "synonyms": synonyms, "evidence_path": ".".join(str(part) for part in [*path, index]), }, ) for child_index, child in enumerate(items): walk(child, [*path, child_index]) walk(tree, []) return result OBJECT_IDENTITY_PROPERTY_ALIASES = { "synonym": "synonym", "synonyms": "synonym", "синоним": "synonym", "синонимы": "synonym", "comment": "comment", "комментарий": "comment", } def normalize_object_identity_property(value: Any) -> str | None: return OBJECT_IDENTITY_PROPERTY_ALIASES.get(str(value or "").strip().casefold()) def config_tree_identity_property_target( tree: Any, object_guid: str, property_name: str, *, language: str = "ru", ) -> dict[str, Any]: """Resolve a supported identity scalar to an exact preserve-format tree path.""" normalized_guid = str(object_guid or "").strip().lower() record = config_tree_identity_records(tree).get(normalized_guid) if not record: return {"status": "not_found", "error": "object_identity_not_found"} try: marker_path = tuple(int(part) for part in str(record.get("evidence_path") or "").split(".") if part != "") except ValueError: return {"status": "unsupported", "error": "invalid_identity_evidence_path", "object": record} if not marker_path: return {"status": "unsupported", "error": "invalid_identity_evidence_path", "object": record} parent_path = marker_path[:-1] marker_index = marker_path[-1] parent = config_tree_item_at_path(tree, parent_path) siblings = config_tree_list_items(parent) normalized_property = normalize_object_identity_property(property_name) if normalized_property == "synonym": synonyms_index = marker_index + 2 if synonyms_index >= len(siblings): return {"status": "unsupported", "error": "synonym_container_missing", "object": record} synonym_items = config_tree_list_items(siblings[synonyms_index]) requested_language = str(language or "ru").strip() for index in range(1, len(synonym_items) - 1, 2): if config_tree_scalar(synonym_items[index]) != requested_language: continue value_node = synonym_items[index + 1] node_type = str(value_node.get("type") or "") if isinstance(value_node, dict) else "" if node_type not in {"atom", "string"}: return {"status": "unsupported", "error": "synonym_value_is_not_scalar", "object": record} path = (*parent_path, synonyms_index, index + 1) return { "status": "ok", "property": "synonym", "language": requested_language, "path": ".".join(str(part) for part in path), "path_tuple": path, "current": config_tree_scalar(value_node), "node_type": node_type, "object": record, } return { "status": "unsupported", "error": "synonym_language_missing", "language": requested_language, "available_languages": [ config_tree_scalar(synonym_items[index]) for index in range(1, len(synonym_items) - 1, 2) if config_tree_scalar(synonym_items[index]) ], "object": record, } if normalized_property == "comment": comment_index = marker_index + 3 if comment_index >= len(siblings): return {"status": "unsupported", "error": "comment_scalar_missing", "object": record} value_node = siblings[comment_index] node_type = str(value_node.get("type") or "") if isinstance(value_node, dict) else "" if node_type not in {"atom", "string"}: return {"status": "unsupported", "error": "comment_value_is_not_scalar", "object": record} path = (*parent_path, comment_index) return { "status": "ok", "property": "comment", "path": ".".join(str(part) for part in path), "path_tuple": path, "current": config_tree_scalar(value_node), "node_type": node_type, "object": record, } return { "status": "unsupported", "error": "unsupported_object_identity_property", "allowed_properties": sorted(set(OBJECT_IDENTITY_PROPERTY_ALIASES.values())), "object": record, } def nested_metadata_semantic_index( tree: Any, *, parent_kind: str, parent_ref: str, dbnames_records: list[Any], ) -> dict[str, dict[str, Any]]: """Name nested identities and, where known, their public 1C category and path.""" result: dict[str, dict[str, Any]] = {} try: from parser.config_semantic import decode_config_semantic profile = decode_config_semantic( tree, kind=parent_kind, dbnames_records=dbnames_records, include_generic=False, lightweight=True, ) except Exception: profile = {} for section in profile.get("sections") or []: category = str(section.get("category") or "") for record in section.get("records") or []: identity = record.get("identity") if isinstance(record.get("identity"), dict) else {} guid = str(identity.get("guid") or "").lower() name = str(identity.get("name") or record.get("likely_name") or "") if is_guid_text(guid) and name: result[guid] = { "category": category or None, "ref": ".".join(part for part in [parent_ref, category, name] if part), } for column in record.get("columns") or []: column_identity = column.get("identity") if isinstance(column.get("identity"), dict) else {} column_guid = str(column_identity.get("guid") or "").lower() column_name = str(column_identity.get("name") or column.get("likely_name") or "") if not is_guid_text(column_guid) or not column_name: continue result[column_guid] = { "category": "Attribute", "ref": ".".join( part for part in [parent_ref, category, name, "Attribute", column_name] if part ), } return result NESTED_MEMBER_KIND_ALIASES = { "attribute": "Attribute", "attributes": "Attribute", "атрибут": "Attribute", "атрибуты": "Attribute", "реквизит": "Attribute", "реквизиты": "Attribute", "column": "Attribute", "колонка": "Attribute", "tabularsection": "TabularSection", "tabularsections": "TabularSection", "табличнаячасть": "TabularSection", "табличныечасти": "TabularSection", "dimension": "Dimension", "dimensions": "Dimension", "измерение": "Dimension", "измерения": "Dimension", "resource": "Resource", "resources": "Resource", "ресурс": "Resource", "ресурсы": "Resource", } def canonical_nested_member_kind(value: Any) -> str | None: return NESTED_MEMBER_KIND_ALIASES.get(normalize(str(value or ""))) def normalized_nested_member_path(parts: Iterable[Any]) -> list[str]: result: list[str] = [] for part in parts: text = str(part or "").strip() if not text: continue result.append(canonical_nested_member_kind(text) or text.casefold()) return result def config_tree_member_identity_resolve( tree: Any, *, parent_guid: str, parent_kind: str, parent_name: str, member_path: list[str] | None = None, member_kind: str | None = None, member_name: str | None = None, ) -> dict[str, Any]: identities = config_tree_identity_records(tree) normalized_parent_guid = str(parent_guid or "").strip().lower() parent_ref = object_selector_ref(parent_kind, parent_name) or f"{parent_kind}.{parent_name}" semantic = nested_metadata_semantic_index( tree, parent_kind=parent_kind, parent_ref=parent_ref, dbnames_records=[], ) requested_path = normalized_nested_member_path(member_path or []) requested_kind = canonical_nested_member_kind(member_kind) requested_name = str(member_name or (member_path or [""])[-1]).strip() candidates: list[dict[str, Any]] = [] for guid, identity in identities.items(): if guid == normalized_parent_guid or str(identity.get("name") or "").casefold() != requested_name.casefold(): continue detail = semantic.get(guid) or {} category = canonical_nested_member_kind(detail.get("category")) or detail.get("category") if requested_kind and category != requested_kind: continue ref = str(detail.get("ref") or ".".join(part for part in [parent_ref, category, identity.get("name")] if part)) ref_parts = [part for part in ref.split(".") if part][2:] if requested_path and normalized_nested_member_path(ref_parts) != requested_path: continue candidates.append( { "guid": guid, "kind": category, "name": identity.get("name"), "synonyms": identity.get("synonyms") or {}, "ref": ref, } ) if len(candidates) == 1: return {"status": "ok", "member": candidates[0]} return { "status": "ambiguous" if len(candidates) > 1 else "not_found", "error": "member_identity_ambiguous" if len(candidates) > 1 else "member_identity_not_found", "selector": { **({"member_path": member_path} if member_path else {}), **({"member_kind": requested_kind} if requested_kind else {}), "member_name": requested_name, }, "candidates": candidates, } def config_tree_declared_record_for_guid(tree: Any, guid: str) -> dict[str, Any]: """Find the deepest declared child record that owns an exact identity GUID.""" try: from parser.child_records import declared_child_records except Exception as exc: return {"status": "error", "error": "child_record_decoder_unavailable", "diagnostics": {"message": str(exc)}} requested = str(guid or "").strip().lower() candidates: list[dict[str, Any]] = [] def walk(node: Any, path: str, depth: int) -> None: if depth > 12: return try: records = declared_child_records(node, path) except Exception: records = [] for record in records: identities = config_tree_identity_records(record.node) if requested in identities: candidates.append( { "record_path": record.path, "parent_path": ".".join(record.path.split(".")[:-1]), "node": record.node, "identity": identities[requested], } ) for index, child in enumerate(config_tree_list_items(node)): walk(child, f"{path}.{index}" if path else str(index), depth + 1) walk(tree, "", 0) if not candidates: return {"status": "not_found", "error": "declared_member_record_not_found"} max_depth = max(len(item["record_path"].split(".")) for item in candidates) deepest = [item for item in candidates if len(item["record_path"].split(".")) == max_depth] if len(deepest) != 1: return { "status": "ambiguous", "error": "declared_member_record_ambiguous", "candidates": [{key: item.get(key) for key in ("record_path", "parent_path")} for item in deepest], } return {"status": "ok", **deepest[0]} def deterministic_member_guid(parent_guid: str, member_kind: str, member_name: str, *, scope: str | None = None) -> str: seed = f"{member_kind}:{scope}:{member_name}" if scope else f"{member_kind}:{member_name}" return str(uuid.uuid5(uuid.UUID(str(parent_guid)), seed)).lower() def config_tree_scalar_occurrences(tree: Any) -> list[dict[str, str]]: occurrences: list[dict[str, str]] = [] def walk(node: Any, path: tuple[int, ...]) -> None: if isinstance(node, dict) and node.get("type") in {"atom", "string"}: occurrences.append( { "path": ".".join(str(part) for part in path), "value": str(node.get("value") or ""), } ) return for index, child in enumerate(config_tree_list_items(node)): walk(child, (*path, index)) walk(tree, ()) return occurrences def config_tree_set_scalar(tree: Any, path: tuple[int, ...], value: str) -> bool: target = config_tree_item_at_path(tree, path) if not isinstance(target, dict) or target.get("type") not in {"atom", "string"}: return False target["value"] = str(value) return True def metadata_member_record_identity_layout(tree: Any, guid: str) -> dict[str, Any]: identity = config_tree_identity_records(tree).get(str(guid or "").strip().lower()) if not identity: return {"status": "not_found", "error": "member_identity_not_found"} try: marker_path = tuple( int(part) for part in str(identity.get("evidence_path") or "").split(".") if part != "" ) except ValueError: return {"status": "unsupported", "error": "invalid_identity_evidence_path"} if not marker_path: return {"status": "unsupported", "error": "invalid_identity_evidence_path"} parent_path = marker_path[:-1] marker_index = marker_path[-1] marker = config_tree_list_items(config_tree_item_at_path(tree, marker_path)) siblings = config_tree_list_items(config_tree_item_at_path(tree, parent_path)) if len(marker) != 3 or marker_index + 3 >= len(siblings): return {"status": "unsupported", "error": "member_identity_layout_unsupported"} guid_path = (*marker_path, 2) name_path = (*parent_path, marker_index + 1) synonym_container_path = (*parent_path, marker_index + 2) comment_path = (*parent_path, marker_index + 3) synonym_items = config_tree_list_items(config_tree_item_at_path(tree, synonym_container_path)) synonym_paths: dict[str, tuple[int, ...]] = {} for index in range(1, len(synonym_items) - 1, 2): language = config_tree_scalar(synonym_items[index]) value_node = synonym_items[index + 1] if language and isinstance(value_node, dict) and value_node.get("type") in {"atom", "string"}: synonym_paths[language] = (*synonym_container_path, index + 1) return { "status": "ok", "identity": identity, "guid_path": guid_path, "name_path": name_path, "synonym_paths": synonym_paths, "comment_path": comment_path, "identity_paths": { ".".join(str(part) for part in guid_path), ".".join(str(part) for part in name_path), ".".join(str(part) for part in comment_path), *( ".".join(str(part) for part in path) for path in synonym_paths.values() ), }, } def metadata_member_record_shape_sha1(tree: Any, guid: str) -> str | None: from parser.payload import serialize_brace_tree normalized = clone_form_structural_node(tree, {}) layout = metadata_member_record_identity_layout(normalized, guid) if layout.get("status") != "ok": return None if not config_tree_set_scalar(normalized, layout["guid_path"], ""): return None if not config_tree_set_scalar(normalized, layout["name_path"], ""): return None if not config_tree_set_scalar(normalized, layout["comment_path"], ""): return None for language, path in (layout.get("synonym_paths") or {}).items(): if not config_tree_set_scalar(normalized, path, f""): return None return hashlib.sha1(serialize_brace_tree(normalized).encode("utf-8")).hexdigest() def clone_metadata_member_record( tree: Any, *, template_guid: str, new_guid: str, new_name: str, new_synonym: str, new_comment: str, ) -> tuple[Any | None, dict[str, Any]]: """Clone a declared member while changing only its explicit identity scalars.""" cloned = clone_form_structural_node(tree, {}) layout = metadata_member_record_identity_layout(cloned, template_guid) if layout.get("status") != "ok": return None, { "status": "blocked", "error": layout.get("error") or "template_identity_layout_unsupported", } identity = layout.get("identity") if isinstance(layout.get("identity"), dict) else {} template_name = str(identity.get("name") or "") identity_paths = set(layout.get("identity_paths") or set()) stale_candidates = { str(template_guid or "").strip().lower(), template_name, } stale_references = [ occurrence for occurrence in config_tree_scalar_occurrences(cloned) if occurrence["path"] not in identity_paths and occurrence["value"] in stale_candidates ] if stale_references: return None, { "status": "blocked", "error": "template_identity_referenced_outside_identity_fields", "stale_references": stale_references[:20], } changes_ok = [ config_tree_set_scalar(cloned, layout["guid_path"], new_guid), config_tree_set_scalar(cloned, layout["name_path"], new_name), config_tree_set_scalar(cloned, layout["comment_path"], new_comment), ] synonym_values: dict[str, str] = {} for language, path in (layout.get("synonym_paths") or {}).items(): value = new_synonym if language == "ru" else new_name synonym_values[language] = value changes_ok.append(config_tree_set_scalar(cloned, path, value)) if not all(changes_ok): return None, { "status": "blocked", "error": "template_identity_scalar_update_failed", } cloned_identities = config_tree_identity_records(cloned) new_identity = cloned_identities.get(new_guid) if not new_identity or template_guid in cloned_identities: return None, { "status": "blocked", "error": "cloned_identity_verification_failed", "identities": sorted(cloned_identities), } template_shape_sha1 = metadata_member_record_shape_sha1(tree, template_guid) cloned_shape_sha1 = metadata_member_record_shape_sha1(cloned, new_guid) shape_preserved = bool( template_shape_sha1 and cloned_shape_sha1 and template_shape_sha1 == cloned_shape_sha1 ) return ( cloned if shape_preserved else None, { "status": "ok" if shape_preserved else "blocked", "error": None if shape_preserved else "member_settings_shape_changed", "identity_fields_changed": ["guid", "name", "synonyms", "comment"], "synonyms": synonym_values, "template_shape_sha1": template_shape_sha1, "cloned_shape_sha1": cloned_shape_sha1, "settings_preserved": shape_preserved, "stale_references": [], }, ) def nested_metadata_guid_references( base_id: str, guids: Iterable[str], *, dbnames_records: list[Any], table: str = "Config", timeout_seconds: int = 60, ) -> dict[str, dict[str, Any]]: """Resolve fields/tabular sections through SQL schema -> parent Config relationships.""" requested = {str(guid or "").strip().lower() for guid in guids if is_guid_text(guid)} if not requested: return {} target_records = [ record for record in dbnames_records if str(getattr(record, "guid", "") or "").lower() in requested and str(getattr(record, "storage_role", "") or "") in {"Fld", "VT", "LineNo"} ] if not target_records: return {} field_numbers = { int(getattr(record, "sql_number", 0) or 0) for record in target_records if str(getattr(record, "storage_role", "") or "") == "Fld" } table_part_numbers = { int(getattr(record, "sql_number", 0) or 0) for record in target_records if str(getattr(record, "storage_role", "") or "") in {"VT", "LineNo"} } conn, _, error = connect_live_sql(base_id, "metadata.nested.resolve", timeout_seconds=timeout_seconds) if error: return {} physical_tables_by_route: dict[tuple[str, int], set[str]] = {} try: with conn: with conn.cursor(as_dict=True) as cursor: if field_numbers: cursor.execute( "SELECT t.name AS table_name, c.name AS column_name " "FROM sys.tables t JOIN sys.columns c ON c.object_id=t.object_id " "WHERE c.name LIKE '[_]Fld%'" ) for row in cursor.fetchall(): match = re.match(r"^_Fld(\d+)", str(row.get("column_name") or "")) if match and int(match.group(1)) in field_numbers: physical_tables_by_route.setdefault(("Fld", int(match.group(1))), set()).add(str(row.get("table_name") or "")) if table_part_numbers: cursor.execute("SELECT name AS table_name FROM sys.tables WHERE name LIKE '%[_]VT%'") for row in cursor.fetchall(): table_name = str(row.get("table_name") or "") match = re.search(r"_VT(\d+)$", table_name) if match and int(match.group(1)) in table_part_numbers: number = int(match.group(1)) physical_tables_by_route.setdefault(("VT", number), set()).add(table_name) physical_tables_by_route.setdefault(("LineNo", number), set()).add(table_name) except Exception: return {} object_by_route = { (str(getattr(record, "storage_role", "") or ""), int(getattr(record, "sql_number", 0) or 0)): record for record in dbnames_records if str(getattr(record, "storage_role", "") or "") in DBNAMES_ROLE_KIND } parent_by_guid: dict[str, Any] = {} target_roles: dict[str, set[str]] = {} for record in target_records: guid = str(getattr(record, "guid", "") or "").lower() role = str(getattr(record, "storage_role", "") or "") number = int(getattr(record, "sql_number", 0) or 0) target_roles.setdefault(guid, set()).add(role) table_names = set(physical_tables_by_route.get((role, number), set())) if role == "LineNo": table_names.update(physical_tables_by_route.get(("VT", number - 1), set())) for table_name in table_names: parent_match = re.match(r"^_([A-Za-z]+)(\d+)", table_name) if not parent_match: continue parent = object_by_route.get((parent_match.group(1), int(parent_match.group(2)))) if parent is not None: parent_by_guid.setdefault(guid, parent) break parent_guids = sorted( { str(getattr(parent, "guid", "") or "").lower() for parent in parent_by_guid.values() if is_guid_text(str(getattr(parent, "guid", "") or "")) } ) payloads, _, payload_error = read_storage_files_bytes(base_id, table, parent_guids, timeout_seconds=timeout_seconds) if payload_error: return {} result: dict[str, dict[str, Any]] = {} for parent_guid in parent_guids: data = (payloads or {}).get(parent_guid, b"") tree = parse_config_tree_from_bytes(data) identities = config_tree_identity_records(tree) parent_identity = identities.get(parent_guid) or config_identity_from_bytes(data) or {} parent_record = next( (parent for parent in parent_by_guid.values() if str(getattr(parent, "guid", "") or "").lower() == parent_guid), None, ) parent_kind = DBNAMES_ROLE_KIND.get(str(getattr(parent_record, "storage_role", "") or ""), "") parent_name = str(parent_identity.get("name") or "") parent_ref = object_selector_ref(parent_kind, parent_name) if parent_kind and parent_name else parent_name semantic = nested_metadata_semantic_index( tree, parent_kind=parent_kind, parent_ref=parent_ref, dbnames_records=dbnames_records, ) owner = { "guid": parent_guid, "kind": parent_kind or None, "name": parent_name or None, "ref": parent_ref or None, "status": "ok" if parent_name else "unresolved", } for guid, parent in parent_by_guid.items(): if str(getattr(parent, "guid", "") or "").lower() != parent_guid: continue identity = identities.get(guid) if not identity or not identity.get("name"): continue role_set = target_roles.get(guid) or set() default_category = "TabularSection" if role_set & {"VT", "LineNo"} else "Attribute" detail = semantic.get(guid) or {} category = str(detail.get("category") or default_category) name = str(identity.get("name") or "") result[guid] = { "guid": guid, "kind": category, "category": category, "name": name, "ref": detail.get("ref") or ".".join(part for part in [parent_ref, category, name] if part), "owner": owner, "scope": "nested_metadata", "status": "ok", } return result NESTED_METADATA_CLASS_KIND = { # Stable platform class discriminator observed in Config command collections. "078a6af8-d22c-4248-9c33-7e90075a3d2c": "Command", } def config_tree_nested_categories(tree: Any, requested: set[str]) -> dict[str, str]: result: dict[str, str] = {} for node in iter_config_tree_nodes(tree): items = config_tree_list_items(node) if len(items) < 3 or config_tree_scalar(items[0]) != "2": continue guid = config_tree_scalar(items[1]).strip().lower() class_guid = config_tree_scalar(items[2]).strip().lower() if guid in requested: result[guid] = NESTED_METADATA_CLASS_KIND.get(class_guid, "NestedObject") return result def cache_nested_metadata_reference(base_id: str, item: dict[str, Any], *, cache_role: str, source_file: str) -> None: config, _ = sql_config_for_base(base_id) if not config: return owner = item.get("owner") if isinstance(item.get("owner"), dict) else {} metadata_guid_index_upsert( config, { "guid": item.get("guid"), "guid_role": cache_role, "kind": item.get("kind"), "name": item.get("name"), "synonym": item.get("synonym"), "owner_guid": owner.get("guid"), "owner_kind": owner.get("kind"), "owner_name": owner.get("name"), "source": "base", "source_file": source_file, "payload": item, }, ) def integration_channel_guid_references( base_id: str, guids: Iterable[str], *, dbnames_records: list[Any], table: str = "Config", timeout_seconds: int = 60, ) -> dict[str, dict[str, Any]]: requested = {str(guid or "").strip().lower() for guid in guids if is_guid_text(guid)} if not requested: return {} ordered = sorted(dbnames_records, key=lambda record: (str(getattr(record, "source", "") or ""), int(getattr(record, "index", 0) or 0))) parent_guid = "" parent_by_channel: dict[str, str] = {} channel_records: dict[str, Any] = {} for record in ordered: role = str(getattr(record, "storage_role", "") or "") guid = str(getattr(record, "guid", "") or "").lower() if role.startswith("IntegService"): parent_guid = guid elif role.startswith("IntegChannel") and guid in requested and parent_guid: parent_by_channel[guid] = parent_guid channel_records[guid] = record if not parent_by_channel: return {} parent_guids = sorted(set(parent_by_channel.values())) payloads, _, error = read_storage_files_bytes(base_id, table, parent_guids, timeout_seconds=timeout_seconds) if error: return {} result: dict[str, dict[str, Any]] = {} for service_guid in parent_guids: data = (payloads or {}).get(service_guid, b"") tree = parse_config_tree_from_bytes(data) identity = config_identity_from_bytes(data) or {} service_name = str(identity.get("name") or "") service_ref = object_selector_ref("IntegrationService", service_name) if service_name else "" details = integration_service_sql_details(tree, include_storage=False) channels = details.get("channels") or [] for role_prefix, direction in (("IntegChannelInQueue", "Receive"), ("IntegChannelOutQueue", "Send")): candidates = sorted( ( (guid, record) for guid, record in channel_records.items() if parent_by_channel.get(guid) == service_guid and str(getattr(record, "storage_role", "") or "") == role_prefix ), key=lambda pair: int(getattr(pair[1], "sql_number", 0) or 0), ) named_channels = [channel for channel in channels if str(channel.get("message_direction") or "") == direction] for (guid, _), channel in zip(candidates, named_channels): name = str(channel.get("name") or "") if not name: continue item = { "guid": guid, "kind": "IntegrationChannel", "category": "Channel", "name": name, "ref": ".".join(part for part in [service_ref, "Channel", name] if part), "owner": { "guid": service_guid, "kind": "IntegrationService", "name": service_name or None, "ref": service_ref or None, "status": "ok" if service_name else "unresolved", }, "scope": "nested_metadata", "status": "ok", } result[guid] = item cache_nested_metadata_reference( base_id, item, cache_role="integration_channel_reference_v1", source_file=service_guid, ) return result def scan_nested_config_guid_references( base_id: str, guids: Iterable[str], *, table: str = "Config", timeout_seconds: int = 60, ) -> dict[str, dict[str, Any]]: """Last-resort SQL Config scan for nested objects that have no DBNames route.""" requested = {str(guid or "").strip().lower() for guid in guids if is_guid_text(guid)} if not requested: return {} result: dict[str, dict[str, Any]] = {} config, _ = sql_config_for_base(base_id) if config: for guid in list(requested): cached = metadata_guid_index_lookup_payload(config, guid, "nested_metadata_reference_v1") if isinstance(cached, dict) and cached.get("name"): result[guid] = {**cached, "status": "ok"} requested.discard(guid) if not requested: return result root_rows, _ = live_base_root_metadata_index(base_id, table=table, timeout_seconds=timeout_seconds) rows_by_guid = {str(row.get("guid") or "").lower(): row for row in root_rows if is_guid_text(str(row.get("guid") or ""))} file_names = list(rows_by_guid) try: from parser.payload import payload_to_text except Exception: return result for start in range(0, len(file_names), 500): if not requested: break payloads, _, error = read_storage_files_bytes( base_id, table, file_names[start : start + 500], timeout_seconds=timeout_seconds, ) if error: continue for parent_guid, data in (payloads or {}).items(): try: text = str(payload_to_text(data).get("text") or "").lower() except Exception: continue hits = {guid for guid in requested if guid in text} if not hits: continue tree = parse_config_tree_from_bytes(data) identities = config_tree_identity_records(tree) parent_identity = identities.get(parent_guid) or config_identity_from_bytes(data) or {} parent_row = rows_by_guid.get(parent_guid) or {} parent_kind = canonical_kind(str(parent_row.get("kind") or "")) parent_name = str(parent_identity.get("name") or "") parent_ref = object_selector_ref(parent_kind, parent_name) if parent_kind and parent_name else parent_name categories = config_tree_nested_categories(tree, hits) owner = { "guid": parent_guid, "kind": parent_kind or None, "name": parent_name or None, "ref": parent_ref or None, "status": "ok" if parent_name else "unresolved", } for guid in hits: identity = identities.get(guid) if not identity or not identity.get("name"): continue category = categories.get(guid, "NestedObject") name = str(identity.get("name") or "") synonyms = identity.get("synonyms") if isinstance(identity.get("synonyms"), dict) else {} item = { "guid": guid, "kind": category, "category": category, "name": name, "synonym": next(iter(synonyms.values()), None), "ref": ".".join(part for part in [parent_ref, category, name] if part), "owner": owner, "scope": "nested_metadata", "status": "ok", } result[guid] = item requested.discard(guid) cache_nested_metadata_reference( base_id, item, cache_role="nested_metadata_reference_v1", source_file=parent_guid, ) return result def public_metadata_guid_references( base_id: str, guids: Iterable[str], *, table: str = "Config", timeout_seconds: int = 60, ) -> dict[str, dict[str, Any]]: """Resolve many internal metadata GUIDs without an N+1 SQL scan.""" normalized = list(dict.fromkeys(str(guid or "").strip().lower() for guid in guids if is_guid_text(guid))) result = {guid: public_metadata_guid_reference(base_id, guid) for guid in normalized} unresolved = [guid for guid, item in result.items() if item.get("status") != "ok"] if not unresolved: return result records, records_error = live_dbnames_records(base_id, timeout_seconds=timeout_seconds) if records_error: return result kinds_by_guid: dict[str, str] = {} for record in records or []: guid = str(getattr(record, "guid", "") or "").strip().lower() kind = DBNAMES_ROLE_KIND.get(str(getattr(record, "storage_role", "") or "")) if guid in unresolved and kind: kinds_by_guid.setdefault(guid, kind) root_rows, _ = live_base_root_metadata_index(base_id, table=table, timeout_seconds=timeout_seconds) for row in root_rows: guid = str(row.get("guid") or "").strip().lower() kind = canonical_kind(str(row.get("kind") or "")) if guid in unresolved and kind: kinds_by_guid.setdefault(guid, kind) payloads, _, read_error = read_storage_files_bytes(base_id, table, unresolved, timeout_seconds=timeout_seconds) if read_error: return result for guid in unresolved: identity = config_identity_from_bytes((payloads or {}).get(guid, b"")) if not identity or not identity.get("name"): continue kind = kinds_by_guid.get(guid, "") name = str(identity.get("name") or "") result[guid] = { "guid": guid, "kind": kind or None, "name": name, "ref": object_selector_ref(kind, name) if kind else name, "status": "ok", } nested_unresolved = [guid for guid, item in result.items() if item.get("status") != "ok"] if nested_unresolved: result.update( nested_metadata_guid_references( base_id, nested_unresolved, dbnames_records=list(records or []), table=table, timeout_seconds=timeout_seconds, ) ) nested_unresolved = [guid for guid, item in result.items() if item.get("status") != "ok"] if nested_unresolved: result.update( integration_channel_guid_references( base_id, nested_unresolved, dbnames_records=list(records or []), table=table, timeout_seconds=timeout_seconds, ) ) nested_unresolved = [guid for guid, item in result.items() if item.get("status") != "ok"] if nested_unresolved: result.update( scan_nested_config_guid_references( base_id, nested_unresolved, table=table, timeout_seconds=timeout_seconds, ) ) return result def config_tree_comment(identity_node: Any) -> str | None: identity_items = config_tree_list_items(identity_node) comment = config_tree_scalar(identity_items[4]) if len(identity_items) > 4 else "" return comment or None def public_module_method_handler(base_id: str, owner: dict[str, Any] | None, method_name: str | None) -> dict[str, Any]: owner_card = dict(owner) if isinstance(owner, dict) else None method = str(method_name or "") or None owner_ref = str((owner_card or {}).get("ref") or "") handler: dict[str, Any] = { "owner": owner_card, "method": method, "ref": ".".join(part for part in [owner_ref, str(method or "")] if part) or None, } if owner_ref and method: handler["read_selector"] = { "method": "modules.read", "base_id": base_id, "ref": owner_ref, "routine_name": method, "state": "working", } return handler def external_data_source_identity(node: Any) -> dict[str, Any] | None: """Decode identity variants used by external data-source children.""" items = config_tree_list_items(node) if len(items) == 2 and config_tree_scalar(items[0]) == "0" and config_tree_list_items(items[1]): items = config_tree_list_items(items[1]) if len(items) < 3: return None selector = config_tree_list_items(items[1]) guid = next((value for value in reversed([config_tree_scalar(item).strip().lower() for item in selector]) if is_guid_text(value)), "") name = config_tree_scalar(items[2]) if not guid or not name: return None synonyms: dict[str, str] = {} synonym_items = config_tree_list_items(items[3]) if len(items) > 3 else [] if synonym_items and config_tree_scalar(synonym_items[0]).isdigit(): pairs = synonym_items[1:] for index in range(0, len(pairs) - 1, 2): language = config_tree_scalar(pairs[index]) value = config_tree_scalar(pairs[index + 1]) if language and value: synonyms[language] = value return { "guid": guid, "name": name, "synonyms": synonyms, **({"comment": config_tree_scalar(items[4])} if len(items) > 4 and config_tree_scalar(items[4]) else {}), } def external_data_source_child_guids(tree: Any, root_index: int) -> tuple[list[str], int]: collection = config_tree_list_items(config_tree_item_at_path(tree, (root_index,))) declared = int(config_tree_scalar(collection[1])) if len(collection) > 1 and config_tree_scalar(collection[1]).isdigit() else 0 guids = [ config_tree_scalar(item).strip().lower() for item in collection[2 : 2 + declared] if is_guid_text(config_tree_scalar(item).strip().lower()) ] return guids, declared def external_data_source_field_sql_details( base_id: str, wrapper: Any, *, owner_ref: str, table_ref: str, table: str = "Config", timeout_seconds: int = 60, ) -> dict[str, Any] | None: wrapper_items = config_tree_list_items(wrapper) record = config_tree_list_items(wrapper_items[0]) if wrapper_items else [] properties = config_tree_list_items(record[1]) if len(record) > 1 else [] definition = config_tree_list_items(properties[1]) if len(properties) > 1 else [] identity = external_data_source_identity(definition[1]) if len(definition) > 1 else None if not identity: return None name = str(identity.get("name") or "") field_ref = ".".join(part for part in [table_ref, "Field", name] if part) return { "identity": identity, "name_in_data_source": config_tree_scalar(record[2]) or None if len(record) > 2 else None, "value_type": public_pattern_value_type(base_id, definition[2], table=table, timeout_seconds=timeout_seconds) if len(definition) > 2 else None, "read_only": {"0": False, "1": True}.get(config_tree_scalar(record[3])) if len(record) > 3 else None, "allow_null": {"0": False, "1": True}.get(config_tree_scalar(record[4])) if len(record) > 4 else None, "ref": field_ref, "owner": {"ref": table_ref, "external_data_source_ref": owner_ref}, } def external_data_source_table_sql_details( base_id: str, data: bytes, *, owner_ref: str, table: str = "Config", timeout_seconds: int = 60, ) -> dict[str, Any] | None: tree = parse_config_tree_from_bytes(data) root = config_tree_list_items(tree) header = config_tree_list_items(root[1]) if len(root) > 1 else [] identity = external_data_source_identity(header[1]) if len(header) > 1 else None if not identity: return None name = str(identity.get("name") or "") table_ref = ".".join(part for part in [owner_ref, "Table", name] if part) field_collection = config_tree_list_items(root[6]) if len(root) > 6 else [] declared_fields = int(config_tree_scalar(field_collection[1])) if len(field_collection) > 1 and config_tree_scalar(field_collection[1]).isdigit() else 0 fields = [ field for wrapper in field_collection[2 : 2 + declared_fields] if ( field := external_data_source_field_sql_details( base_id, wrapper, owner_ref=owner_ref, table_ref=table_ref, table=table, timeout_seconds=timeout_seconds, ) ) ] field_by_guid = {str((field.get("identity") or {}).get("guid") or "").lower(): field for field in fields} key_guids = [guid for guid in config_tree_guids(header[18]) if guid in field_by_guid] if len(header) > 18 else [] return { "identity": identity, "ref": table_ref, "name_in_data_source": config_tree_scalar(header[17]) or None if len(header) > 17 else None, "table_type": {"0": "Table", "1": "View"}.get(config_tree_scalar(header[16]), {"status": "unknown_code", "code": config_tree_scalar(header[16])}) if len(header) > 16 else None, "key_fields": [field_by_guid[guid] for guid in key_guids], "fields": fields, "counts": { "fields": len(fields), "declared_fields": declared_fields, "key_fields": len(key_guids), "typed_fields": sum(1 for field in fields if field.get("value_type")), }, } def external_data_source_generic_children( payloads: dict[str, bytes], guids: list[str], *, owner_ref: str, category: str, ) -> list[dict[str, Any]]: result: list[dict[str, Any]] = [] for guid in guids: identity = config_identity_from_bytes(payloads.get(guid, b"")) if not identity: continue name = str(identity.get("name") or "") result.append({ "identity": identity, "ref": ".".join(part for part in [owner_ref, category, name] if part), "status": "generic_identity", }) return result def external_data_source_sql_details( base_id: str, tree: Any, *, table: str = "Config", timeout_seconds: int = 60, ) -> dict[str, Any]: root = config_tree_list_items(tree) header = config_tree_list_items(root[1]) if len(root) > 1 else [] identity = external_data_source_identity(header[1]) if len(header) > 1 else None source_name = str((identity or {}).get("name") or "") owner_ref = object_selector_ref("ExternalDataSource", source_name) if source_name else "ExternalDataSource" # Config serialization stores the three child collections as cubes, functions, tables. cube_guids, declared_cubes = external_data_source_child_guids(tree, 3) function_guids, declared_functions = external_data_source_child_guids(tree, 4) table_guids, declared_tables = external_data_source_child_guids(tree, 5) all_guids = [*cube_guids, *function_guids, *table_guids] payloads: dict[str, bytes] = {} read_error = None if all_guids: payloads, _, read_error = read_storage_files_bytes(base_id, table, all_guids, timeout_seconds=timeout_seconds) payloads = payloads or {} tables = [ decoded for guid in table_guids if (decoded := external_data_source_table_sql_details(base_id, payloads.get(guid, b""), owner_ref=owner_ref, table=table, timeout_seconds=timeout_seconds)) ] cubes = external_data_source_generic_children(payloads, cube_guids, owner_ref=owner_ref, category="Cube") functions = external_data_source_generic_children(payloads, function_guids, owner_ref=owner_ref, category="Function") return { "identity": identity, "data_lock_control_mode": DATA_LOCK_CONTROL_CODES.get(config_tree_scalar(header[8]), {"status": "unknown_code", "code": config_tree_scalar(header[8])}) if len(header) > 8 else None, "tables": tables, "cubes": cubes, "functions": functions, "counts": { "tables": len(tables), "declared_tables": declared_tables, "fields": sum(int((item.get("counts") or {}).get("fields") or 0) for item in tables), "typed_fields": sum(int((item.get("counts") or {}).get("typed_fields") or 0) for item in tables), "cubes": len(cubes), "declared_cubes": declared_cubes, "functions": len(functions), "declared_functions": declared_functions, "missing_child_payloads": sum(1 for guid in all_guids if guid not in payloads), }, **({"diagnostics": {"child_payload_read": read_error.get("status")}} if read_error else {}), } def defined_type_sql_details( base_id: str, tree: Any, *, table: str = "Config", timeout_seconds: int = 60, ) -> dict[str, Any]: body = config_tree_list_items(config_tree_item_at_path(tree, (1,))) identity = config_tree_identity(body[3]) if len(body) > 3 else None value_type = public_pattern_value_type(base_id, body[4], table=table, timeout_seconds=timeout_seconds) if len(body) > 4 else None type_items = list((value_type or {}).get("types") or []) if isinstance(value_type, dict) and value_type.get("kind") == "union" else ([value_type] if value_type else []) return { "identity": identity, "comment": config_tree_comment(body[3]) if len(body) > 3 else None, "value_type": value_type, "types": type_items, "counts": { "types": len(type_items), "resolved_types": sum(1 for item in type_items if isinstance(item, dict) and item.get("kind") != "unknown"), }, } def localized_config_text(node: Any) -> dict[str, str]: items = config_tree_list_items(node) if not items or not config_tree_scalar(items[0]).isdigit(): return {} result: dict[str, str] = {} for index in range(1, len(items) - 1, 2): language = config_tree_scalar(items[index]) value = config_tree_scalar(items[index + 1]) if language and value: result[language] = value return result def selection_criterion_sql_details( base_id: str, tree: Any, *, table: str = "Config", timeout_seconds: int = 60, ) -> dict[str, Any]: body = config_tree_list_items(config_tree_item_at_path(tree, (1,))) definition = config_tree_list_items(body[5]) if len(body) > 5 else [] identity = config_tree_identity(definition[1]) if len(definition) > 1 else None value_type = public_pattern_value_type(base_id, definition[2], table=table, timeout_seconds=timeout_seconds) if len(definition) > 2 else None content_node = body[6] if len(body) > 6 else None content_items = config_tree_list_items(content_node) declared_content = int(config_tree_scalar(content_items[1])) if len(content_items) > 1 and config_tree_scalar(content_items[1]).isdigit() else 0 content_guids: list[str] = [] for item in content_items[2 : 2 + declared_content]: guids = [guid for guid in config_tree_guids(item) if guid != "00000000-0000-0000-0000-000000000000"] if guids and guids[-1] not in content_guids: content_guids.append(guids[-1]) references = public_metadata_guid_references(base_id, content_guids, table=table, timeout_seconds=timeout_seconds) content = [references.get(guid, {"guid": guid, "status": "unresolved"}) for guid in content_guids] return { "identity": identity, "comment": config_tree_comment(definition[1]) if len(definition) > 1 else None, "value_type": value_type, "use_standard_commands": {"0": False, "1": True}.get(config_tree_scalar(body[7])) if len(body) > 7 else None, "default_list_form": public_metadata_guid_reference(base_id, config_tree_scalar(body[8])) if len(body) > 8 and is_guid_text(config_tree_scalar(body[8])) and config_tree_scalar(body[8]) != "00000000-0000-0000-0000-000000000000" else None, "default_choice_form": public_metadata_guid_reference(base_id, config_tree_scalar(body[9])) if len(body) > 9 and is_guid_text(config_tree_scalar(body[9])) and config_tree_scalar(body[9]) != "00000000-0000-0000-0000-000000000000" else None, "list_presentation": localized_config_text(body[12]) if len(body) > 12 else {}, "content": content, "counts": { "content": len(content), "declared_content": declared_content, "resolved_content": sum(1 for item in content if item.get("status") == "ok"), "unresolved_content": sum(1 for item in content if item.get("status") != "ok"), }, } ENUM_CHOICE_MODE_CODES = {"0": "FromValue", "1": "FromList", "2": "BothWays"} def enum_sql_details(tree: Any, *, owner_name: str | None = None) -> dict[str, Any]: root = config_tree_list_items(tree) body = config_tree_list_items(root[1]) if len(root) > 1 else [] identity = config_tree_identity(body[5]) if len(body) > 5 else None enum_name = str(owner_name or (identity or {}).get("name") or "") values_node = root[6] if len(root) > 6 else None value_items = config_tree_list_items(values_node) declared_values = int(config_tree_scalar(value_items[1])) if len(value_items) > 1 and config_tree_scalar(value_items[1]).isdigit() else 0 values: list[dict[str, Any]] = [] for index, wrapper in enumerate(value_items[2 : 2 + declared_values]): value_identity = config_tree_identity(wrapper) if not value_identity: identity_records = config_tree_identity_records(wrapper) value_identity = next(iter(identity_records.values()), None) if not value_identity: continue name = str(value_identity.get("name") or "") values.append( { "identity": value_identity, "ordinal": index + 1, "ref": ".".join(part for part in [object_selector_ref("Enum", enum_name), "Value", name] if part), } ) choice_code = config_tree_scalar(body[11]) if len(body) > 11 else "" return { "identity": identity, "comment": config_tree_comment(body[5]) if len(body) > 5 else None, "use_standard_commands": {"0": False, "1": True}.get(config_tree_scalar(body[6])) if len(body) > 6 else None, "quick_choice": {"0": False, "1": True}.get(config_tree_scalar(body[12])) if len(body) > 12 else None, "choice_mode": ENUM_CHOICE_MODE_CODES.get(choice_code, {"status": "unknown_code", "code": choice_code}), "values": values, "counts": {"values": len(values), "declared_values": declared_values}, } def event_subscription_sql_details(base_id: str, tree: Any) -> dict[str, Any]: body = config_tree_item_at_path(tree, (1,)) body_items = config_tree_list_items(body) identity_node = body_items[1] if len(body_items) > 1 else None source_node = body_items[2] if len(body_items) > 2 else None source_guids: list[str] = [] for item in config_tree_list_items(source_node)[1:]: item_values = config_tree_list_items(item) if len(item_values) >= 2 and config_tree_scalar(item_values[0]) == "#": guid = config_tree_scalar(item_values[1]).strip().lower() if is_guid_text(guid) and guid not in source_guids: source_guids.append(guid) event_code = config_tree_scalar(body_items[3]) if len(body_items) > 3 else "" event = event_code.split("_", 1)[0] if event_code else None owner_guid = config_tree_scalar(body_items[4]).strip().lower() if len(body_items) > 4 else "" method_name = config_tree_scalar(body_items[5]) if len(body_items) > 5 else "" owner = public_metadata_guid_reference(base_id, owner_guid) if is_guid_text(owner_guid) else None handler = public_module_method_handler(base_id, owner, method_name) return { "comment": config_tree_comment(identity_node), "sources": [public_metadata_guid_reference(base_id, guid) for guid in source_guids], "event": event, "event_code": event_code or None, "handler": handler, } SESSION_REUSE_CODES = {"0": "DontUse", "1": "Use", "2": "AutoUse"} DATA_LOCK_CONTROL_CODES = {"0": "Automatic", "1": "Managed"} HTTP_METHOD_CODES = {"3": "GET", "11": "POST"} WEB_PARAMETER_DIRECTION_CODES = {"0": "In", "1": "Out", "2": "InOut"} def config_tree_xdto_type(node: Any) -> dict[str, Any] | None: items = config_tree_list_items(node) if len(items) < 3: return None namespace = config_tree_scalar(items[1]) name = config_tree_scalar(items[2]) if not namespace and not name: return None return {"namespace": namespace or None, "name": name or None} def web_service_sql_details(base_id: str, tree: Any) -> dict[str, Any]: header = config_tree_list_items(config_tree_item_at_path(tree, (1,))) identity_node = header[2] if len(header) > 2 else None package_guids = config_tree_guids(header[3]) if len(header) > 3 else [] # The first GUID in the XDTO selector is a platform type discriminator. package_guid = package_guids[-1] if len(package_guids) > 1 else (package_guids[0] if package_guids else "") operations: list[dict[str, Any]] = [] collection = config_tree_list_items(config_tree_item_at_path(tree, (3,))) for container in collection[2:]: container_items = config_tree_list_items(container) operation_node = container_items[0] if container_items else None operation_items = config_tree_list_items(operation_node) operation_identity = config_tree_identity(operation_node) if not operation_identity: continue parameter_collection = config_tree_list_items(container_items[2]) if len(container_items) > 2 else [] parameter_records_node = parameter_collection[2] if len(parameter_collection) > 2 else None parameters: list[dict[str, Any]] = [] for parameter_node in config_tree_list_items(parameter_records_node): parameter_items = config_tree_list_items(parameter_node) parameter_identity = config_tree_identity(parameter_node) if not parameter_identity: continue direction_code = config_tree_scalar(parameter_items[0]) if parameter_items else "" parameters.append( { "identity": parameter_identity, "value_type": config_tree_xdto_type(parameter_items[2]) if len(parameter_items) > 2 else None, "nillable": {"0": False, "1": True}.get(config_tree_scalar(parameter_items[3])) if len(parameter_items) > 3 else None, "transfer_direction": WEB_PARAMETER_DIRECTION_CODES.get(direction_code, {"status": "unknown_code", "code": direction_code}), } ) lock_code = config_tree_scalar(operation_items[6]) if len(operation_items) > 6 else "" operations.append( { "identity": operation_identity, "returning_value_type": config_tree_xdto_type(operation_items[2]) if len(operation_items) > 2 else None, "nillable": {"0": False, "1": True}.get(config_tree_scalar(operation_items[3])) if len(operation_items) > 3 else None, "transactioned": {"0": False, "1": True}.get(config_tree_scalar(operation_items[4])) if len(operation_items) > 4 else None, "procedure_name": config_tree_scalar(operation_items[5]) or None if len(operation_items) > 5 else None, "data_lock_control_mode": DATA_LOCK_CONTROL_CODES.get(lock_code, {"status": "unknown_code", "code": lock_code}), "parameters": parameters, } ) reuse_code = config_tree_scalar(header[6]) if len(header) > 6 else "" max_age = config_tree_scalar(header[7]) if len(header) > 7 else "" return { "comment": config_tree_comment(identity_node), "namespace": config_tree_scalar(header[1]) or None if len(header) > 1 else None, "xdto_packages": [public_metadata_guid_reference(base_id, package_guid)] if is_guid_text(package_guid) else [], "descriptor_file_name": config_tree_scalar(header[4]) or None if len(header) > 4 else None, "reuse_sessions": SESSION_REUSE_CODES.get(reuse_code, {"status": "unknown_code", "code": reuse_code}), "session_max_age": int(max_age) if max_age.isdigit() else None, "operations": operations, } def http_service_sql_details(tree: Any) -> dict[str, Any]: header = config_tree_list_items(config_tree_item_at_path(tree, (1,))) identity_node = header[2] if len(header) > 2 else None url_templates: list[dict[str, Any]] = [] collection = config_tree_list_items(config_tree_item_at_path(tree, (3,))) for container in collection[2:]: container_items = config_tree_list_items(container) template_node = container_items[0] if container_items else None template_items = config_tree_list_items(template_node) template_identity = config_tree_identity(template_node) if not template_identity: continue method_collection = config_tree_list_items(container_items[2]) if len(container_items) > 2 else [] method_records_node = method_collection[2] if len(method_collection) > 2 else None methods: list[dict[str, Any]] = [] for method_node in config_tree_list_items(method_records_node): method_items = config_tree_list_items(method_node) method_identity = config_tree_identity(method_node) if not method_identity: continue method_code = config_tree_scalar(method_items[2]) if len(method_items) > 2 else "" methods.append( { "identity": method_identity, "http_method": HTTP_METHOD_CODES.get(method_code, str(method_identity.get("name") or "") or {"status": "unknown_code", "code": method_code}), "handler": config_tree_scalar(method_items[1]) or None if len(method_items) > 1 else None, } ) url_templates.append( { "identity": template_identity, "template": config_tree_scalar(template_items[1]) or None if len(template_items) > 1 else None, "methods": methods, } ) reuse_code = config_tree_scalar(header[3]) if len(header) > 3 else "" max_age = config_tree_scalar(header[4]) if len(header) > 4 else "" return { "comment": config_tree_comment(identity_node), "root_url": config_tree_scalar(header[1]) or None if len(header) > 1 else None, "reuse_sessions": SESSION_REUSE_CODES.get(reuse_code, {"status": "unknown_code", "code": reuse_code}), "session_max_age": int(max_age) if max_age.isdigit() else None, "url_templates": url_templates, } ROLE_RIGHT_NAMES = { "fd05f656-7a23-43a4-8996-f480a806fb97": "ActiveUsers", "900e3c92-6e18-4874-846a-b28780b5b54c": "Administration", "f7c6a0bb-bca6-4cd3-9146-832971cd7073": "AnalyticsSystemClient", "07ef4641-f7da-417a-bd75-35c40a17c2f7": "Automation", "3762abec-3836-446a-83ce-3e05001bca8b": "CollaborationSystemInfoBaseRegistration", "399d7390-8d83-4a57-b4d7-c902c15b701f": "ConfigurationExtensionsAdministration", "10b8ce49-ae3d-4a2e-afe7-1e3648bd59f7": "DataAdministration", "c0028105-4cc1-41ca-aef1-bfbd8fc8f8c4": "Delete", "b7bab52d-c1b1-4bd8-8276-02db08d42352": "Edit", "8497054a-ffd1-4ca7-bdfe-340b9ddc050a": "EditDataHistoryVersionComment", "1c799cf9-342d-4bf7-9b6f-951a009228ce": "EventLog", "8fb221e3-0d4f-43f2-ad71-1984cad63375": "ExclusiveMode", "4df6d046-3bf8-4dda-991c-53ba664296a5": "ExclusiveModeTerminationAtSessionStart", "02119c69-f08a-4142-9426-3725d74b7719": "ExternalConnection", "499e8968-ca89-43f0-9955-8756058b1b53": "Get", "74fd69fa-368e-4292-956a-65eb2f9877bd": "Execute", "b5f861d3-d9c5-45ec-98bf-0ed4d489a351": "InputByString", "33200740-82b0-4de7-8556-d3fb25ca4328": "Insert", "798cf688-ad74-44fe-a464-236b49e910e0": "InteractiveClearDeletionMark", "e7f9daf9-eac2-4ada-9c26-c380858f3589": "InteractiveClearDeletionMarkPredefinedData", "b53db6ed-6e5b-4035-8d24-f10083d646ed": "InteractiveDelete", "013a262e-165f-4815-bdae-7a1bed6a68e4": "InteractiveDeletePredefinedData", "fa6dbe86-856a-4ac4-b8ac-bce99f8b8b22": "InteractiveDeleteMarked", "65e5f92c-40ff-4130-9652-c0e7612d0609": "InteractiveDeleteMarkedPredefinedData", "5e664189-f0ee-439c-bdc5-eb81cca41ddf": "InteractiveExecute", "fb88c756-91c9-4351-9cdf-e027879886c6": "InteractiveInsert", "3b869658-ebc9-49ff-9bb3-e7c59686f538": "InteractiveActivate", "7b8359dd-7d4e-4bcd-a61c-b4b26eae19c6": "InteractiveOpenExtDataProcessors", "eb29e198-c338-4a20-a253-be6fc3dd44d9": "InteractiveOpenExtReports", "d76b72ba-5388-4b7f-af64-1b351f63a1e1": "InteractiveSetDeletionMark", "408c56c0-e210-4e2e-8e82-610050a08a39": "InteractiveSetDeletionMarkPredefinedData", "5d167fcc-b11f-403a-9a37-1eda64c19df1": "InteractivePosting", "21b4742a-d335-4234-bf0f-a3074a0e31ac": "InteractivePostingRegular", "4d0d77ec-8511-430d-bd77-8407f27bc8f4": "InteractiveUndoPosting", "b0c0cbfc-f2cc-4b80-8460-5d5d7a599d9d": "InteractiveChangeOfPosted", "84487e82-eb6c-4c51-ae16-3a6db17e886d": "InteractiveStart", "b9b44b51-3ac9-47cd-8b5a-df51afdcceb0": "MainWindowModeEmbeddedWorkplace", "818fc6c3-4691-44e3-a80c-e8d424730ead": "MainWindowModeFullscreenWorkplace", "155a0b35-4343-4047-989b-d385373b063e": "MainWindowModeKiosk", "d066966a-ff6a-4a41-bd68-6191cab083bc": "MainWindowModeNormal", "f6168734-8b8d-4a88-ab39-ef6b51758e83": "MainWindowModeWorkplace", "1e50809b-73ed-4935-bb77-2616c4cabdf5": "MobileClient", "31c3d4f6-7d02-4654-a14e-06aacafcb4fa": "Output", "e060de25-bffd-42fd-bb09-f3a788d65760": "Posting", "1c87578f-9e09-4ec0-a991-5629c87b1588": "Read", "64319ca1-f3d8-472e-82ce-5da233e6daaa": "ReadDataHistory", "1b762bf9-df7f-4255-bbe6-f7578f41368d": "ReadDataHistoryOfMissingData", "d8682bbb-7800-4aa0-8590-d3cb11fe2a29": "SaveUserData", "963624dc-9b02-4c20-a3f0-015ac64c6d81": "SessionOSAuthenticationChange", "669fef9d-9d4c-4333-8237-66351429d935": "SessionStandardAuthenticationChange", "1d306db2-d97e-4b57-9b28-5d21e838cd9e": "Set", "aad14f33-8a70-48dd-acd1-a661fe5b4263": "StandardAuthenticationChange", "65b6855f-85d5-4d33-ab75-be4485326dd5": "Start", "479a42c0-c3e9-4ae7-bf4a-75cebc14fec4": "SwitchToDataHistoryVersion", "265eec41-3ce1-4a07-bc3b-253d44c9a4f4": "TechnicalSpecialistMode", "29da0973-3b85-40e5-89da-bce02dbab08e": "ThickClient", "3c00c6ee-844e-4620-85e4-671e72f114d9": "ThinClient", "24abfe06-289a-48c5-8bb4-032c733e45c5": "TotalsControl", "f55a8f7f-2c65-404f-b530-093d9006adba": "UndoPosting", "287b74b8-3a66-4a76-ba27-4f1f6a93770e": "Update", "4d87a22d-ca7f-40ba-a367-a4eae62f4a7f": "UpdateDataBaseConfiguration", "b162ff57-0296-483e-9af8-dc37576802cb": "UpdateDataHistory", "c4ab1331-e58d-4a46-ad2e-fe6d80b72aa4": "UpdateDataHistoryOfMissingData", "a679c969-8ea1-4b8b-9e61-8a414ba448f4": "UpdateDataHistorySettings", "5b3ea0e2-fdb9-41f6-bf6c-25747906b4cb": "UpdateDataHistoryVersionComment", "c6de80da-a4f7-4ce9-bbeb-0b00ea564ec1": "Use", "aa6448f2-be0f-42ea-ba26-1af7f52b5b65": "View", "9342b152-a7ae-4c79-9b7b-f4f028a36479": "ViewDataHistory", "bd33c881-192c-4ef7-a51d-b146e38c5078": "WebClient", } ROLE_STANDARD_ATTRIBUTE_CODES = { "-5": "Active", "-4": "LineNumber", "-3": "Recorder", "-2": "Period", } def config_tree_strings(node: Any) -> list[str]: result: list[str] = [] def walk(value: Any) -> None: if isinstance(value, dict) and value.get("type") == "string": result.append(str(value.get("value") or "")) return for child in config_tree_list_items(value): walk(child) walk(node) return result def role_right_value(raw: str) -> dict[str, Any]: if raw == "1": return {"value": True, "state": "allowed"} if raw == "0": return {"value": False, "state": "denied"} if raw in {"-1", "4294967295"}: return {"value": False, "state": "denied"} return {"value": None, "state": "unknown", "raw": raw} def role_rights_and_restrictions(node: Any) -> tuple[list[dict[str, Any]], int]: values = config_tree_list_items(node) if not values: return [], 0 marker = config_tree_scalar(values[0]) if marker == "1" and len(values) > 1 and config_tree_scalar(values[1]).isdigit(): declared = int(config_tree_scalar(values[1])) cursor = 2 else: declared = max(0, (len(values) - 1) // 2) cursor = 1 rights: list[dict[str, Any]] = [] by_guid: dict[str, dict[str, Any]] = {} for _ in range(declared): if cursor + 1 >= len(values): break right_guid = config_tree_scalar(values[cursor]).strip().lower() raw_value = config_tree_scalar(values[cursor + 1]) cursor += 2 if not is_guid_text(right_guid): continue item = { "name": ROLE_RIGHT_NAMES.get(right_guid), "guid": right_guid, **role_right_value(raw_value), } if not item.get("name"): item["status"] = "unknown_right_guid" rights.append(item) by_guid[right_guid] = item restriction_count = 0 if cursor < len(values) and config_tree_scalar(values[cursor]).isdigit(): restriction_count = int(config_tree_scalar(values[cursor])) cursor += 1 for restriction_node in values[cursor : cursor + restriction_count]: restriction_items = config_tree_list_items(restriction_node) if not restriction_items: continue right_guid = config_tree_scalar(restriction_items[0]).strip().lower() conditions = [text for text in config_tree_strings(restriction_node) if text] target = by_guid.get(right_guid) if target is None: target = { "name": ROLE_RIGHT_NAMES.get(right_guid), "guid": right_guid, "value": None, "state": "restriction_only", } rights.append(target) by_guid[right_guid] = target target["restrictions"] = [ {"kind": "condition", "condition": condition, "status": "ok"} for condition in conditions ] return rights, restriction_count def role_rights_sql_details( base_id: str, tree: Any, *, table: str = "Config", timeout_seconds: int = 60, ) -> dict[str, Any]: root = config_tree_list_items(tree) object_container = config_tree_list_items(root[1]) if len(root) > 1 else [] declared_objects = int(config_tree_scalar(object_container[0])) if object_container and config_tree_scalar(object_container[0]).isdigit() else 0 records = object_container[1 : 1 + declared_objects] owner_guids: list[str] = [] for record in records: record_items = config_tree_list_items(record) selector = config_tree_list_items(record_items[0]) if record_items else [] owner_guid = config_tree_scalar(selector[1]).strip().lower() if len(selector) > 1 else "" if is_guid_text(owner_guid): owner_guids.append(owner_guid) references = public_metadata_guid_references( base_id, owner_guids, table=table, timeout_seconds=timeout_seconds, ) objects: list[dict[str, Any]] = [] restriction_total = 0 unknown_right_guids: set[str] = set() unresolved_objects = 0 for index, record in enumerate(records): record_items = config_tree_list_items(record) if len(record_items) < 2: continue selector = config_tree_list_items(record_items[0]) owner_guid = config_tree_scalar(selector[1]).strip().lower() if len(selector) > 1 else "" owner = references.get(owner_guid, {"guid": owner_guid, "status": "unresolved"}) if owner.get("status") != "ok": unresolved_objects += 1 child_selector = None if len(selector) > 2 and config_tree_scalar(selector[2]) == "1": child_node = selector[3] if len(selector) > 3 else None child_items = config_tree_list_items(child_node) child_selector = { "category_code": config_tree_scalar(child_items[0]) or None if child_items else None, "guid": config_tree_scalar(child_items[1]).strip().lower() or None if len(child_items) > 1 else None, } standard_name = ROLE_STANDARD_ATTRIBUTE_CODES.get(str(child_selector.get("category_code") or "")) if standard_name: child_selector.update( { "category": "StandardAttribute", "name": standard_name, "ref": ".".join( part for part in [str(owner.get("ref") or ""), "StandardAttribute", standard_name] if part ) or None, } ) rights, restriction_count = role_rights_and_restrictions(record_items[1]) restriction_total += restriction_count unknown_right_guids.update(str(right.get("guid") or "") for right in rights if not right.get("name")) objects.append( { "index": index, "object": owner, **({"child_selector": child_selector} if child_selector else {}), "rights": rights, "counts": { "rights": len(rights), "allowed": sum(1 for right in rights if right.get("state") == "allowed"), "denied": sum(1 for right in rights if right.get("state") == "denied"), "restrictions": restriction_count, }, } ) template_container = config_tree_list_items(root[2]) if len(root) > 2 else [] declared_templates = int(config_tree_scalar(template_container[0])) if template_container and config_tree_scalar(template_container[0]).isdigit() else 0 templates = [] for template_node in template_container[1 : 1 + declared_templates]: template_items = config_tree_list_items(template_node) if len(template_items) < 2: continue templates.append( { "name": config_tree_scalar(template_items[0]) or None, "condition": config_tree_scalar(template_items[1]) or None, "status": "ok", } ) boolean = lambda raw, false_codes={"0", "4294967295", "-1"}: True if raw == "1" else False if raw in false_codes else None return { "set_for_new_objects": boolean(config_tree_scalar(root[3])) if len(root) > 3 else None, "set_for_attributes_by_default": boolean(config_tree_scalar(root[4])) if len(root) > 4 else None, "independent_rights_of_child_objects": boolean(config_tree_scalar(root[5])) if len(root) > 5 else None, "objects": objects, "restriction_templates": templates, "counts": { "objects": len(objects), "declared_objects": declared_objects, "unresolved_objects": unresolved_objects, "rights": sum(len(item.get("rights") or []) for item in objects), "allowed_rights": sum((item.get("counts") or {}).get("allowed", 0) for item in objects), "denied_rights": sum((item.get("counts") or {}).get("denied", 0) for item in objects), "restrictions": restriction_total, "restriction_templates": len(templates), "unknown_right_guids": len(unknown_right_guids), }, **({"unknown_right_guids": sorted(unknown_right_guids)} if unknown_right_guids else {}), } def metadata_object_special_details(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, "metadata.object.special.details") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, "metadata.object.special.details") if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error validation_error = validate_metadata_object_special_details_payload(payload) if validation_error: return validation_error include_column_types, include_column_types_error = strict_bool_argument(payload, "include_column_types", method="metadata.object.special.details", default=False) if include_column_types_error: return include_column_types_error include_storage, include_storage_error = strict_include_storage(payload, "metadata.object.special.details") if include_storage_error: return include_storage_error parsed_timeout, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.special.details", default=60, minimum=1) if timeout_error: return timeout_error timeout_seconds = int(parsed_timeout or 60) table_or_error = metadata_storage_table(payload, "metadata.object.special.details") if isinstance(table_or_error, dict): return table_or_error table = table_or_error _, column_timeout_error = parse_int_argument(payload, "column_type_timeout_seconds", method="metadata.object.special.details", default=0, minimum=1) if column_timeout_error: return column_timeout_error _, max_columns_error = parse_int_argument(payload, "max_columns", method="metadata.object.special.details", default=0, minimum=1, maximum=5000) if max_columns_error: return max_columns_error if canonical_kind(str(payload.get("kind") or "")) == "DocumentJournal" and include_column_types: return adapter_start_job( {"method": "metadata.object.special.details", "payload": {**payload, "include_storage": bool(include_storage), "table": table}} ) extension_guid = str(payload.get("extension_guid") or "").strip().lower() selected_object_guid = str(payload.get("guid") or "").strip().lower() direct_saved_descriptor = table == "ConfigCASSave" and is_guid_text(extension_guid) and is_guid_text(selected_object_guid) if direct_saved_descriptor: kind = canonical_kind(str(payload.get("kind") or "")) if not kind: return invalid_argument("metadata.object.special.details", "kind", "kind is required for a saved extension descriptor selector.") guid = f"{extension_guid}__{selected_object_guid}" object_card = { "guid": selected_object_guid, "kind": kind, "name": payload.get("name"), "source": "extension_saved_state", } error = None else: guid, kind, object_card, error = resolve_object_guid( payload, base_id, timeout_seconds=timeout_seconds, method="metadata.object.special.details", table=table, ) if error: return error data, config, read_error = read_storage_file_bytes(base_id, table, guid, timeout_seconds=timeout_seconds) if read_error: return public_error_result(read_error, include_storage=include_storage, method="metadata.object.special.details") tree = parse_config_tree_from_bytes(data) identity = config_identity_from_bytes(data) or saved_state_descriptor_identity_from_bytes(data, guid) or (object_card or {}).get("identity") or {} if direct_saved_descriptor: object_card = { **(object_card or {}), "name": identity.get("name") or (object_card or {}).get("name"), "identity": identity, } strings = tree_ordered_strings(tree) details: dict[str, Any] = {} counts: dict[str, Any] = {} status = "ok" if kind == "Configuration": details = configuration_sql_details(tree, include_storage=bool(include_storage)) counts = dict(details.pop("counts", {})) elif kind == "Constant": resolved_types = {} values = tree_ordered_scalars(tree) try: pattern_index = values.index("Pattern") if pattern_index + 2 < len(values) and is_guid_text(values[pattern_index + 2]): type_guid = values[pattern_index + 2].lower() resolved_types = resolve_type_guids(base_id, {type_guid}, timeout_seconds=timeout_seconds, table=table) except Exception: resolved_types = {} raw_type = public_pattern_type_from_tree(tree, resolved_types) details = { "value_type": raw_type or {"status": "not_decoded_yet"}, "description": next((value for value in strings if value not in {identity.get("name"), *(((identity.get("synonyms") or {}).values()) if isinstance(identity.get("synonyms"), dict) else [])} and " " in value), None), } counts = {"value_type": 1 if raw_type else 0} elif kind == "DocumentNumerator": details = document_numerator_sql_details(tree, include_storage=bool(include_storage)) counts = {"properties": len(details.get("properties") or [])} elif kind == "ChartOfCalculationTypes": details = chart_of_calculation_types_sql_details(tree, include_storage=bool(include_storage)) counts = {"properties": len(details.get("properties") or []), "decoded_properties": len(details.get("properties") or [])} elif kind == "CalculationRegister": details = calculation_register_sql_details(tree, include_storage=bool(include_storage)) chart_reference = details.get("chart_of_calculation_types") if isinstance(details.get("chart_of_calculation_types"), dict) else None chart_guid = str((chart_reference or {}).get("guid") or "").lower() if direct_saved_descriptor and is_guid_text(chart_guid): chart_file_name = f"{extension_guid}__{chart_guid}" chart_data, _, chart_error = read_storage_file_bytes(base_id, table, chart_file_name, timeout_seconds=timeout_seconds) chart_identity = ( config_identity_from_bytes(chart_data or b"") or saved_state_descriptor_identity_from_bytes(chart_data or b"", chart_file_name) or {} ) if not chart_error else {} chart_name = str(chart_identity.get("name") or "") if chart_name: details["chart_of_calculation_types"] = { "guid": chart_guid, "kind": "ChartOfCalculationTypes", "name": chart_name, "ref": object_selector_ref("ChartOfCalculationTypes", chart_name), "status": "ok", } for property_row in details.get("properties") or []: if property_row.get("name") == "ChartOfCalculationTypes": property_row["value"] = details["chart_of_calculation_types"] break counts = { "properties": len(details.get("properties") or []), "decoded_properties": len(details.get("properties") or []), "child_collections": len(details.get("child_collections") or []), "classified_child_collections": sum( 1 for collection in details.get("child_collections") or [] if collection.get("status") == "classified" ), } elif kind == "IntegrationService": details = integration_service_sql_details(tree, include_storage=bool(include_storage)) counts = dict(details.pop("counts", {})) status = "ok" if counts.get("channels") == counts.get("declared_channels") else "partial" elif kind == "CommonAttribute": details = common_attribute_sql_details(base_id, tree, table=table, timeout_seconds=timeout_seconds) counts = { "content": len(details.get("content") or []), "value_types": 1 if details.get("value_type") else 0, "separation_references": sum( len(details.get(key) or []) for key in ("data_separation_value", "data_separation_use", "conditional_separation") ), } elif kind == "SessionParameter": details = session_parameter_sql_details(base_id, tree, table=table, timeout_seconds=timeout_seconds) value_type = details.get("value_type") if isinstance(details.get("value_type"), dict) else {} counts = {"value_types": int(value_type.get("count") or (1 if value_type else 0))} elif kind == "FunctionalOption": details = functional_option_sql_details(base_id, tree, table=table, timeout_seconds=timeout_seconds) counts = { "location": 1 if (details.get("location") or {}).get("ref") else 0, "content": len(details.get("content") or []), } if not counts["location"]: status = "partial" elif kind == "FunctionalOptionsParameter": details = functional_options_parameter_sql_details(base_id, tree, table=table, timeout_seconds=timeout_seconds) counts = {"use": len(details.get("use") or [])} if not counts["use"]: status = "partial" elif kind == "CommonCommand": details = common_command_sql_details(base_id, tree, identity, table=table, timeout_seconds=timeout_seconds) counts = { "group": 1 if (details.get("group") or {}).get("ref") else 0, "module": 1 if ((details.get("module") or {}).get("read_selector")) else 0, "parameter_type": 1 if details.get("command_parameter_type") else 0, } if not counts["group"] or not counts["module"]: status = "partial" elif kind == "SettingsStorage": details = settings_storage_sql_details(base_id, tree, table=table, timeout_seconds=timeout_seconds) counts = {"forms": len(details.get("forms") or [])} elif kind == "Subsystem": interface_data, _, interface_error = read_storage_file_bytes(base_id, table, f"{guid}.1", timeout_seconds=timeout_seconds) interface_tree = parse_config_tree_from_bytes(interface_data or b"") if not interface_error else None details = subsystem_sql_details(base_id, tree, interface_tree, table=table, timeout_seconds=timeout_seconds) counts = { "content": len(details.get("content") or []), "child_subsystems": len(details.get("child_subsystems") or []), "command_interface_references": len(((details.get("command_interface") or {}).get("items") or [])), } elif kind == "Language": details = language_sql_details(tree) counts = {"language_code": 1 if details.get("language_code") else 0} if not counts["language_code"]: status = "partial" elif kind == "CommonPicture": binary_data, _, binary_error = read_storage_file_bytes(base_id, table, f"{guid}.0", timeout_seconds=timeout_seconds) details = common_picture_sql_details(tree, None if binary_error else binary_data) counts = {"binary_parts": 1 if (details.get("binary") or {}).get("status") == "ok" else 0} if not counts["binary_parts"]: status = "partial" elif kind == "StyleItem": details = style_item_sql_details(tree) counts = {"typed_values": 1 if details.get("value_type") != "Unknown" else 0} if not counts["typed_values"]: status = "partial" elif kind == "Style": values_data, _, values_error = read_storage_file_bytes(base_id, table, f"{guid}.0", timeout_seconds=timeout_seconds) values_tree = parse_config_tree_from_bytes(values_data or b"") if not values_error else None details = style_sql_details(tree, values_tree) counts = {"items": len(details.get("items") or []), "declared_items": int(details.get("declared_items") or 0)} if counts["items"] != counts["declared_items"]: status = "partial" elif kind == "XDTOPackage": package_data, _, package_error = read_storage_file_bytes(base_id, table, f"{guid}.0", timeout_seconds=timeout_seconds) package = xdto_package_xml_details(package_data or b"") if not package_error else {"status": "source_missing"} details = { "namespace": config_tree_scalar_at_path(tree, (1, 2)) or package.get("namespace"), "package": package, } counts = dict(package.get("counts") or {}) if package.get("status") != "ok": status = "partial" elif kind == "WSReference": definition_data, _, definition_error = read_storage_file_bytes(base_id, table, f"{guid}.0", timeout_seconds=timeout_seconds) details = ws_reference_sql_details(tree, definition_data or b"") if not definition_error else {"status": "source_missing", "location_url": config_tree_scalar_at_path(tree, (1, 1, 0)) or None} counts = dict(details.pop("counts", {})) if details.get("status") != "ok": status = "partial" elif kind == "ExternalDataSource": details = external_data_source_sql_details(base_id, tree, table=table, timeout_seconds=timeout_seconds) counts = dict(details.pop("counts", {})) if ( counts.get("tables") != counts.get("declared_tables") or counts.get("cubes") != counts.get("declared_cubes") or counts.get("functions") != counts.get("declared_functions") or counts.get("missing_child_payloads") ): status = "partial" elif kind == "DefinedType": details = defined_type_sql_details(base_id, tree, table=table, timeout_seconds=timeout_seconds) counts = dict(details.pop("counts", {})) if counts.get("types") != counts.get("resolved_types"): status = "partial" elif kind == "SelectionCriterion": details = selection_criterion_sql_details(base_id, tree, table=table, timeout_seconds=timeout_seconds) counts = dict(details.pop("counts", {})) if counts.get("content") != counts.get("declared_content") or counts.get("unresolved_content"): status = "partial" elif kind == "Enum": details = enum_sql_details(tree, owner_name=str(identity.get("name") or "")) counts = dict(details.pop("counts", {})) if counts.get("values") != counts.get("declared_values"): status = "partial" elif kind == "CommandGroup": details = command_group_sql_details(tree, include_storage=bool(include_storage)) picture = details.get("picture") if isinstance(details.get("picture"), dict) else None picture_guid = str((picture or {}).get("guid") or "") if is_guid_text(picture_guid): picture_data, _, picture_error = read_storage_file_bytes(base_id, table, picture_guid, timeout_seconds=timeout_seconds) picture_identity = config_identity_from_bytes(picture_data or b"") if not picture_error else None if picture_identity and picture_identity.get("name"): details["picture"] = { "kind": "metadata_picture", "guid": picture_guid, "name": picture_identity["name"], "ref": object_selector_ref("CommonPicture", picture_identity["name"]), "status": "ok", } counts = {"properties": 4, "decoded_properties": 4} elif kind == "Role": rights_file = f"{guid}.0" rights_data, _, rights_error = read_storage_file_bytes(base_id, table, rights_file, timeout_seconds=timeout_seconds) if rights_error: details = { "comment": config_tree_comment(config_tree_item_at_path(tree, (1, 1))), "rights": {"status": "source_missing"}, } counts = {"objects": 0, "rights": 0, "restrictions": 0, "restriction_templates": 0} status = "partial" else: rights_details = role_rights_sql_details( base_id, parse_config_tree_from_bytes(rights_data or b""), table=table, timeout_seconds=timeout_seconds, ) counts = dict(rights_details.pop("counts", {})) details = { "comment": config_tree_comment(config_tree_item_at_path(tree, (1, 1))), **rights_details, } status = "ok" if counts.get("objects") == counts.get("declared_objects") else "partial" elif kind == "ScheduledJob": method = scheduled_job_method_name(tree, identity) method_owner_guid = config_tree_scalar_at_path(tree, (1, 6)) or None method_owner = public_metadata_guid_reference(base_id, method_owner_guid) if is_guid_text(method_owner_guid) else None handler = public_module_method_handler(base_id, method_owner, method) schedule_file = f"{guid}.0" schedule_data, _, schedule_error = read_storage_file_bytes(base_id, table, schedule_file, timeout_seconds=timeout_seconds) if schedule_error: schedule = { "status": "not_configured", "diagnostics": {"message": "No separate SQL saved schedule payload exists for this scheduled job."}, } else: schedule = scheduled_job_sql_schedule(parse_config_tree_from_bytes(schedule_data or b""), include_storage=bool(include_storage)) if include_storage: schedule.setdefault("storage", {})["file_name"] = schedule_file use_raw = config_tree_scalar_at_path(tree, (1, 4)) predefined_raw = config_tree_scalar_at_path(tree, (1, 5)) restart_count_raw = config_tree_scalar_at_path(tree, (1, 8)) restart_interval_raw = config_tree_scalar_at_path(tree, (1, 9)) details = { "method": method, "method_owner_guid": method_owner_guid, "method_owner": method_owner, "handler": handler, "description": next((value for value in strings if " " in value and value not in set((identity.get("synonyms") or {}).values() if isinstance(identity.get("synonyms"), dict) else [])), None), "use": {"0": False, "1": True}.get(use_raw), "predefined": {"0": False, "1": True}.get(predefined_raw), "restart_count_on_failure": int(restart_count_raw) if restart_count_raw.isdigit() else None, "restart_interval_on_failure": int(restart_interval_raw) if restart_interval_raw.isdigit() else None, "schedule": schedule, } counts = { "method": 1 if method else 0, "handler": 1 if handler.get("read_selector") else 0, "schedule": 1 if schedule.get("status") == "ok" else 0, } if not details["method"] or not handler.get("read_selector"): status = "partial" elif kind == "EventSubscription": details = event_subscription_sql_details(base_id, tree) source_type_guids = {str(source.get("guid") or "").lower() for source in details.get("sources") or [] if isinstance(source, dict) and is_guid_text(source.get("guid"))} resolved_source_types = resolve_type_guids( base_id, source_type_guids, table=table, timeout_seconds=timeout_seconds, ) resolved_sources: list[dict[str, Any]] = [] for source in details.get("sources") or []: source_guid = str(source.get("guid") or "").lower() if isinstance(source, dict) else "" resolved = resolved_source_types.get(source_guid) if not isinstance(resolved, dict) or resolved.get("status") != "ok": resolved_sources.append(source) continue source_kind = canonical_kind(str(resolved.get("kind") or "")) source_name = str(resolved.get("name") or "") resolved_sources.append( { "type_guid": source_guid, "kind": source_kind or resolved.get("kind"), "name": source_name or None, "ref": object_selector_ref(source_kind, source_name), "type_ref": resolved_type_presentation(resolved), "status": "ok", } ) details["sources"] = resolved_sources reference_guids: list[str] = [] handler = details.get("handler") if isinstance(details.get("handler"), dict) else {} owner = handler.get("owner") if isinstance(handler.get("owner"), dict) else {} if owner.get("guid"): reference_guids.append(str(owner.get("guid") or "")) resolved_references = public_metadata_guid_references( base_id, reference_guids, table=table, timeout_seconds=timeout_seconds, ) owner_guid = str(owner.get("guid") or "") if owner_guid: handler["owner"] = resolved_references.get(owner_guid, owner) if (handler.get("owner") or {}).get("name") and not (handler.get("owner") or {}).get("kind"): handler["owner"].update( { "kind": "CommonModule", "ref": object_selector_ref("CommonModule", str(handler["owner"].get("name") or "")), } ) handler = public_module_method_handler(base_id, handler.get("owner"), handler.get("method")) details["handler"] = handler counts = { "sources": len(details.get("sources") or []), "handler": 1 if (details.get("handler") or {}).get("method") else 0, } status = "ok" if counts["handler"] else "partial" elif kind == "WebService": details = web_service_sql_details(base_id, tree) package_guids = [str(package.get("guid") or "") for package in details.get("xdto_packages") or [] if isinstance(package, dict)] resolved_packages = public_metadata_guid_references( base_id, package_guids, table=table, timeout_seconds=timeout_seconds, ) details["xdto_packages"] = [resolved_packages.get(str(package.get("guid") or ""), package) for package in details.get("xdto_packages") or []] for package in details["xdto_packages"]: if isinstance(package, dict) and package.get("name") and not package.get("kind"): package.update( { "kind": "XDTOPackage", "ref": object_selector_ref("XDTOPackage", str(package.get("name") or "")), } ) operations = details.get("operations") or [] counts = { "operations": len(operations), "parameters": sum(len(operation.get("parameters") or []) for operation in operations if isinstance(operation, dict)), "xdto_packages": len(details.get("xdto_packages") or []), } status = "ok" if operations else "partial" elif kind == "HTTPService": details = http_service_sql_details(tree) url_templates = details.get("url_templates") or [] counts = { "url_templates": len(url_templates), "methods": sum(len(template.get("methods") or []) for template in url_templates if isinstance(template, dict)), } status = "ok" if url_templates else "partial" elif kind == "DocumentJournal": dbnames_records, _ = live_dbnames_records(base_id, timeout_seconds=timeout_seconds) document_types = document_journal_document_types( base_id, tree, dbnames_records=dbnames_records, table=table, timeout_seconds=timeout_seconds, ) columns = document_journal_columns( base_id, tree, document_types=document_types, dbnames_records=dbnames_records, table=table, include_column_types=bool(include_column_types), timeout_seconds=timeout_seconds, ) details = { "columns": columns if columns else {"status": "not_decoded_yet"}, "document_types": document_types if document_types else {"status": "not_decoded_yet"}, "description": next((value for value in strings if " " in value), None), } counts = { "document_types": len(document_types), "columns": len(columns), "typed_columns": sum(1 for column in columns if isinstance(column, dict) and column.get("type")), } status = "ok" if document_types and columns else "partial" else: details = {"status": "unsupported_kind", "supported_kinds": sorted(SPECIAL_PROPERTY_KINDS)} counts = {} status = "unsupported" return { "schema": "onec_object_special_details.v1", "status": status, "base_id": base_id, "source": {"kind": "live_sql", "database": (config or {}).get("database"), "table": table, "file_name": guid} if include_storage else {"kind": "live_metadata"}, "object": object_card or {"guid": guid, "kind": kind, "identity": identity}, "details": details, "counts": counts, } SPECIAL_PROPERTY_KINDS = frozenset( { "CommandGroup", "CommonAttribute", "CommonCommand", "Configuration", "Constant", "ChartOfCalculationTypes", "CalculationRegister", "DocumentJournal", "DocumentNumerator", "EventSubscription", "FunctionalOption", "FunctionalOptionsParameter", "HTTPService", "IntegrationService", "Role", "ScheduledJob", "SessionParameter", "SettingsStorage", "Language", "CommonPicture", "Style", "StyleItem", "XDTOPackage", "WSReference", "ExternalDataSource", "DefinedType", "SelectionCriterion", "Enum", "Subsystem", "WebService", } ) def metadata_object_properties(payload: dict[str, Any]) -> dict[str, Any]: """Return one stable public property envelope, backed only by live SQL metadata.""" payload = normalize_object_selector_aliases(payload, "metadata.object.properties") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload if not has_object_selector(payload): return invalid_argument("metadata.object.properties", "selector", OBJECT_SELECTOR_REQUIRED_MESSAGE) # The special reader resolves aliases and the actual object kind from live metadata. # It is retained as an implementation detail and as a backward-compatible endpoint. special = metadata_object_special_details(payload) if special.get("status") in {"ok", "partial"}: result = dict(special) result.update( { "schema": "onec_metadata_object_properties.v1", "method": "metadata.object.properties", "decoder": "kind_specific_sql", "properties": result.pop("details", {}), } ) return result if special.get("status") != "unsupported": result = dict(special) result["method"] = "metadata.object.properties" result.setdefault("requested_method", "metadata.object.properties") return result generic_payload = { key: value for key, value in payload.items() if key not in {"include_column_types", "column_type_timeout_seconds", "max_columns", "mode", "include_semantic"} } generic = call_method_impl("metadata.object.get", {**generic_payload, "mode": "semantic", "include_semantic": True}) if generic.get("status") != "ok": result = dict(generic) result["method"] = "metadata.object.properties" return result return { "schema": "onec_metadata_object_properties.v1", "method": "metadata.object.properties", "status": "ok", "base_id": generic.get("base_id"), "source": generic.get("source") or {"kind": "live_metadata"}, "decoder": "generic_semantic_sql", "object": generic.get("object"), "properties": generic.get("semantic") or {}, "counts": generic.get("counts") or {}, "diagnostics": { "note": "No kind-specific decoder is registered; properties use the generic live SQL semantic profile.", "special_property_kinds": sorted(SPECIAL_PROPERTY_KINDS), }, } def validate_metadata_object_special_details_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "metadata.object.special.details") if isinstance(base_id_or_error, dict): return base_id_or_error selector_error = validate_object_selector_arguments(payload, "metadata.object.special.details") if selector_error: return selector_error _, include_column_types_error = strict_bool_argument(payload, "include_column_types", method="metadata.object.special.details", default=False) if include_column_types_error: return include_column_types_error _, include_storage_error = strict_include_storage(payload, "metadata.object.special.details") if include_storage_error: return include_storage_error table_or_error = metadata_storage_table(payload, "metadata.object.special.details") if isinstance(table_or_error, dict): return table_or_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.special.details", default=60, minimum=1) if timeout_error: return timeout_error _, column_timeout_error = parse_int_argument(payload, "column_type_timeout_seconds", method="metadata.object.special.details", default=0, minimum=1) if column_timeout_error: return column_timeout_error _, max_columns_error = parse_int_argument(payload, "max_columns", method="metadata.object.special.details", default=0, minimum=1, maximum=5000) if max_columns_error: return max_columns_error guid_error = validate_explicit_guid_argument(payload, "metadata.object.special.details") if guid_error: return guid_error ordinal_error = validate_explicit_ordinal_arguments(payload, "metadata.object.special.details") if ordinal_error: return ordinal_error _, lookup_limit_error = parse_object_lookup_limit(payload, "metadata.object.special.details") if lookup_limit_error: return lookup_limit_error _, view_error = parse_view_argument(payload, "metadata.object.special.details") if view_error: return view_error return None def preferred_form_payload_file_name(form_guid: str, candidates: Iterable[Any]) -> str: """Pick the form body part, never the GUID descriptor, from SQL Config files.""" normalized_guid = str(form_guid or "").strip().casefold() if not normalized_guid: return "" safe_names = [ str(name or "").strip() for name in candidates if str(name or "").strip() and Path(str(name or "").strip()).name == str(name or "").strip() ] exact_payload = next((name for name in safe_names if name.casefold() == f"{normalized_guid}.0"), "") if exact_payload: return exact_payload return next((name for name in safe_names if name.casefold().startswith(f"{normalized_guid}.")), "") def enrich_form_command_references(base_id: str, profile: dict[str, Any]) -> None: """Resolve internal form command GUIDs to public 1C object references from cache.""" for row in profile.get("items") or []: if not isinstance(row, dict): continue reference = row.get("command_reference") if isinstance(row.get("command_reference"), dict) else None command_guid = str((reference or {}).get("guid") or "").strip().lower() if not reference or not is_guid_text(command_guid): continue known_command_name = str(reference.get("command_name") or "") if known_command_name: row["command_name"] = known_command_name semantic = row.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) main = groups.setdefault("Основные", []) if not any(isinstance(prop, dict) and prop.get("name") == "ИмяКоманды" for prop in main): main.append({"name": "ИмяКоманды", "value": known_command_name, "source": "sql_standard_command_guid", "status": "ok"}) continue identity = metadata_cache_lookup_guid(base_id, command_guid) if not isinstance(identity, dict) or not identity.get("name"): command_data, _, command_error = read_storage_file_bytes(base_id, "Config", command_guid, timeout_seconds=30) direct_identity = config_identity_from_bytes(command_data or b"") if not command_error else None if isinstance(direct_identity, dict) and direct_identity.get("name"): identity = { "kind": "CommonCommand", "guid": command_guid, "name": direct_identity.get("name"), "synonyms": direct_identity.get("synonyms") or {}, "status": "ok", "match_by": "direct_sql_guid", } if not isinstance(identity, dict) or not identity.get("name"): continue kind = canonical_kind(str(identity.get("kind") or "")) name = str(identity.get("name") or "") public_ref = object_selector_ref(kind, name) reference.update( { "kind": kind or identity.get("kind"), "name": name, "ref": public_ref, "status": "ok", "resolved_by": identity.get("match_by") or "metadata_identity", } ) if kind != "CommonCommand" or not public_ref: continue row["command_name"] = public_ref semantic = row.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) main = groups.setdefault("Основные", []) if not any(isinstance(prop, dict) and prop.get("name") == "ИмяКоманды" for prop in main): main.append( { "name": "ИмяКоманды", "value": public_ref, "source": "sql_metadata_command_guid", "status": "ok", } ) def enrich_form_style_references(base_id: str, profile: dict[str, Any]) -> None: """Resolve form StyleItem GUID values to public style names from live SQL metadata.""" for row in profile.get("items") or []: if not isinstance(row, dict): continue semantic = row.get("semantic") if isinstance(row.get("semantic"), dict) else {} for properties in (semantic.get("groups") or {}).values(): for prop in properties or []: if not isinstance(prop, dict): continue reference = prop.get("value") if isinstance(prop.get("value"), dict) else None guid = str((reference or {}).get("guid") or "").strip().lower() if not reference or canonical_kind(str(reference.get("kind") or "")) != "StyleItem" or not is_guid_text(guid): continue identity = metadata_cache_lookup_guid(base_id, guid) if not isinstance(identity, dict) or not identity.get("name"): style_data, _, style_error = read_storage_file_bytes(base_id, "Config", guid, timeout_seconds=30) direct_identity = config_identity_from_bytes(style_data or b"") if not style_error else None if isinstance(direct_identity, dict) and direct_identity.get("name"): identity = { "kind": "StyleItem", "guid": guid, "name": direct_identity.get("name"), "synonyms": direct_identity.get("synonyms") or {}, "status": "ok", "match_by": "direct_sql_guid", } if not isinstance(identity, dict) or not identity.get("name"): continue name = str(identity.get("name") or "") reference.update( { "kind": "StyleItem", "name": name, "ref": object_selector_ref("StyleItem", name), "status": "ok", "resolved_by": identity.get("match_by") or "metadata_identity", } ) prop["style_reference"] = reference prop["value"] = f"style:{name}" def enrich_form_choice_list_references(base_id: str, profile: dict[str, Any]) -> None: """Resolve ChoiceList enum GUID pairs to public Enum object/value names.""" pending: list[tuple[dict[str, Any], dict[str, Any]]] = [] type_guids: set[str] = set() for row in profile.get("items") or []: if not isinstance(row, dict): continue semantic = row.get("semantic") if isinstance(row.get("semantic"), dict) else {} for properties in (semantic.get("groups") or {}).values(): for prop in properties or []: if not isinstance(prop, dict) or prop.get("name") != "ChoiceList": continue choice_list = prop.get("value") if isinstance(prop.get("value"), dict) else {} for item in choice_list.get("items") or []: reference = item.get("value") if isinstance(item, dict) and isinstance(item.get("value"), dict) else None type_guid = str((reference or {}).get("type_guid") or "").strip().lower() value_guid = str((reference or {}).get("value_guid") or "").strip().lower() if not reference or reference.get("kind") != "EnumValue" or not is_guid_text(type_guid) or not is_guid_text(value_guid): continue pending.append((item, reference)) type_guids.add(type_guid) if not pending: return resolved_types = resolve_type_guids(base_id, type_guids, timeout_seconds=30, table="Config") enum_values_by_owner: dict[str, dict[str, dict[str, Any]]] = {} for resolved in resolved_types.values(): if not isinstance(resolved, dict) or canonical_kind(str(resolved.get("kind") or "")) != "Enum": continue owner_guid = str(resolved.get("owner_guid") or "").strip().lower() if not is_guid_text(owner_guid) or owner_guid in enum_values_by_owner: continue owner_data, _, owner_error = read_storage_file_bytes(base_id, "Config", owner_guid, timeout_seconds=30) values: dict[str, dict[str, Any]] = {} if owner_data and not owner_error: decoded = decode_config_object_full( owner_data, kind="Enum", semantic_include_generic=False, semantic_categories={"EnumValue"}, semantic_lightweight=True, ) semantic = decoded.get("semantic") if isinstance(decoded.get("semantic"), dict) else {} for section in semantic.get("sections") or []: if not isinstance(section, dict) or section.get("category") != "EnumValue": continue for record in section.get("records") or []: identity = record.get("identity") if isinstance(record, dict) and isinstance(record.get("identity"), dict) else {} guid = str(identity.get("guid") or "").strip().lower() if is_guid_text(guid) and identity.get("name"): values[guid] = identity enum_values_by_owner[owner_guid] = values for item, reference in pending: type_guid = str(reference.get("type_guid") or "").lower() value_guid = str(reference.get("value_guid") or "").lower() resolved = resolved_types.get(type_guid) if not isinstance(resolved, dict): continue enum_name = str(resolved.get("name") or "") owner_guid = str(resolved.get("owner_guid") or "").lower() value_identity = enum_values_by_owner.get(owner_guid, {}).get(value_guid) value_name = str((value_identity or {}).get("name") or "") if not enum_name or not value_name: continue ref = f"Enum.{enum_name}.EnumValue.{value_name}" item["value"] = {"kind": "EnumValue", "ref": ref, "name": value_name, "status": "ok"} if not str(item.get("presentation") or ""): synonyms = (value_identity or {}).get("synonyms") if isinstance((value_identity or {}).get("synonyms"), dict) else {} item["presentation"] = str(synonyms.get("ru") or value_name) def metadata_form_decode(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, "metadata.form.decode") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, "metadata.form.decode") if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error timeout_seconds_value, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.form.decode", default=60, minimum=1) if timeout_error: return timeout_error timeout_seconds = int(timeout_seconds_value or 60) include_storage, include_storage_error = strict_include_storage(payload, "metadata.form.decode") if include_storage_error: return include_storage_error include_storage = bool(include_storage) include_module_text, include_module_text_error = strict_bool_argument(payload, "include_module_text", method="metadata.form.decode", default=False) if include_module_text_error: return include_module_text_error include_module, include_module_error = strict_bool_argument(payload, "include_module", method="metadata.form.decode", default=False) if include_module_error: return include_module_error include_module_text = bool(include_module_text or include_module) evidence_mode, evidence_mode_error = parse_evidence_mode_argument(payload, "metadata.form.decode") if evidence_mode_error: return evidence_mode_error include_parameters, include_parameters_error = strict_bool_argument(payload, "include_parameters", method="metadata.form.decode", default=True) if include_parameters_error: return include_parameters_error max_items, max_items_error = parse_int_argument(payload, "max_items", method="metadata.form.decode", default=500, minimum=1, maximum=5000) if max_items_error: return max_items_error max_parameters, max_parameters_error = parse_int_argument(payload, "max_parameters", method="metadata.form.decode", default=80, minimum=1, maximum=500) if max_parameters_error: return max_parameters_error for argument in ("table", "file_name", "form_guid", "guid"): if argument not in payload: continue value = payload.get(argument) if value is None or value == "": return invalid_argument("metadata.form.decode", argument, f"{argument} must be a non-empty JSON string when provided.") if not isinstance(value, str): return invalid_argument("metadata.form.decode", argument, f"{argument} must be a JSON string.") table_error = validate_optional_string_arguments(payload, "metadata.form.decode", ["table", "file_name", "form_guid"]) if table_error: return table_error element_error = validate_optional_string_arguments(payload, "metadata.form.decode", ["element", "element_name", "element_path", "path", "element_id", "id"]) if element_error: return element_error table = str(payload.get("table") or "Config") if table not in STORAGE_TABLES: return invalid_argument("metadata.form.decode", "table", "Unsupported storage table.", allowed_values=sorted(STORAGE_TABLES)) file_name = str(payload.get("file_name") or "") form_guid = str(payload.get("form_guid") or payload.get("guid") or "").strip().lower() cached_form_info: dict[str, Any] | None = None source_state = str(payload.get("source_state") or payload.get("state") or "").strip().casefold() wants_working_state = source_state in {"working", "save", "saved", "designer"} requested_form_name = str(payload.get("form") or payload.get("form_name") or payload.get("name_filter") or payload.get("name") or "").strip() if wants_working_state and not file_name and not form_guid and requested_form_name: saved_state_lookup = metadata_saved_state_forms_search( { "base_id": base_id, "form": requested_form_name, "query": requested_form_name, "extension": str(payload.get("extension") or "").strip(), "tables": ["ConfigCASSave"] if payload.get("extension") else ["ConfigCASSave", "ConfigSave"], "limit": 20, "scan_limit": int(payload.get("scan_limit") or 5000), "timeout_seconds": timeout_seconds, "include_storage": True, } ) if saved_state_lookup.get("status") == "ok": for item in saved_state_lookup.get("forms") or []: if not isinstance(item, dict): continue form_info = item.get("form") if isinstance(item.get("form"), dict) else {} item_name = str(item.get("name") or form_info.get("name") or "").strip() if item_name and normalize(item_name) != normalize(requested_form_name): continue source = item.get("source") if isinstance(item.get("source"), dict) else {} file_info = item.get("file") if isinstance(item.get("file"), dict) else {} candidate_table = str(source.get("table") or file_info.get("table") or item.get("table") or "") candidate_file_name = str( source.get("file_name") or file_info.get("file_name") or file_info.get("FileName") or item.get("file_name") or "" ) if not candidate_table and candidate_file_name: candidate_table = "ConfigCASSave" if payload.get("extension") else "ConfigSave" if candidate_table in FORM_ELEMENT_SAVED_STATE_TABLES and candidate_file_name and Path(candidate_file_name).name == candidate_file_name: table = candidate_table file_name = candidate_file_name cached_form_info = item form_guid = str(form_info.get("guid") or form_guid or "").strip().lower() break if not file_name and not form_guid: cache_config, _ = sql_config_for_base(base_id) cached_form = metadata_form_owner_cache_lookup( cache_config, owner_kind=str(payload.get("kind") or payload.get("object_type") or "CommonForm"), form_name=str(payload.get("form") or payload.get("form_name") or payload.get("name") or payload.get("name_filter") or ""), extension=str(payload.get("extension") or ""), ) if not cached_form and payload.get("extension"): cached_form = metadata_form_owner_cache_lookup( cache_config, owner_kind=str(payload.get("kind") or payload.get("object_type") or "CommonForm"), form_name=str(payload.get("form") or payload.get("form_name") or payload.get("name") or payload.get("name_filter") or ""), ) cached_source = cached_form.get("source") if isinstance(cached_form, dict) and isinstance(cached_form.get("source"), dict) else {} cached_table = str(cached_source.get("table") or "") cached_file_name = str(cached_source.get("file_name") or "") if cached_table in STORAGE_TABLES and cached_file_name and Path(cached_file_name).name == cached_file_name: table = cached_table file_name = cached_file_name cached_form_info = cached_form cached_form_payload = cached_form.get("form") if isinstance(cached_form.get("form"), dict) else {} form_guid = str(cached_form_payload.get("guid") or form_guid or "").strip().lower() if not file_name and not form_guid and (payload.get("form") or payload.get("name_filter")) and (payload.get("kind") or payload.get("name") or payload.get("ordinal")): forms_result = metadata_object_forms( {**payload, "include_storage": True, "table": table} ) if forms_result.get("status") != "ok": result = dict(forms_result) result["method"] = "metadata.form.decode" return public_error_result(result, include_storage=include_storage, method="metadata.form.decode") forms = forms_result.get("forms") or [] if not forms: return { "schema": "onec_form_decode.v1", "status": "not_found", "base_id": base_id, "source": {"kind": "live_metadata"}, "diagnostics": {"message": "Form was not found by object selector and form/name_filter."}, } form_guid = str((forms[0] or {}).get("guid") or "").strip().lower() form_source = (forms[0] or {}).get("source") if isinstance((forms[0] or {}).get("source"), dict) else {} source_table = str(form_source.get("table") or "") if source_table in STORAGE_TABLES: table = source_table source_file_name = str(form_source.get("file_name") or "") if source_file_name and Path(source_file_name).name == source_file_name: file_name = source_file_name if not file_name and form_guid: parts = storage_files_list({"base_id": base_id, "table": table, "prefix": form_guid, "limit": 50, "timeout_seconds": timeout_seconds, "_internal": True}) if parts.get("status") != "ok": result = dict(parts) result["method"] = "metadata.form.decode" return result candidates = [str(row.get("FileName") or "") for row in parts.get("files") or []] file_name = preferred_form_payload_file_name(form_guid, candidates) if not file_name: file_name = form_guid elif file_name and form_guid and file_name.casefold() == form_guid.casefold(): # metadata.object.forms exposes the descriptor GUID as its public source. # The managed-form body in base Config is the sibling `.0` payload. parts = storage_files_list({"base_id": base_id, "table": table, "prefix": form_guid, "limit": 50, "timeout_seconds": timeout_seconds, "_internal": True}) if parts.get("status") == "ok": candidates = [str(row.get("FileName") or "") for row in parts.get("files") or []] file_name = preferred_form_payload_file_name(form_guid, candidates) or file_name if not file_name or Path(file_name).name != file_name: return { "schema": "onec_adapter_request_error.v1", "method": "metadata.form.decode", "status": "error", "error": "file_name_or_form_guid_required", } data, config, error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=timeout_seconds) if error: error["method"] = "metadata.form.decode" return error tree = parse_config_tree_from_bytes(data) classified_payload: dict[str, Any] | None = None if tree is None: try: from parser.cas_payload import classify_payload except Exception: classify_payload = None if classify_payload: try: classified = classify_payload(data, include_tree=True) classified_payload = classified tree = classified.get("tree") except Exception: tree = None elif include_module_text or include_parameters: try: from parser.cas_payload import classify_payload except Exception: classify_payload = None if classify_payload: try: classified_payload = classify_payload(data, include_text=bool(include_module_text), include_tree=True) except Exception: classified_payload = None if tree is None: result = { "schema": "onec_form_decode.v1", "status": "undecodable", "base_id": base_id, "source": {"kind": "live_metadata"}, } if classified_payload is not None: result["undecoded_evidence"] = payload_public_undecoded_evidence( classified_payload, include_text_preview=bool(include_module_text), mode=str(evidence_mode or "summary"), allow_storage_details=bool(include_storage), ) if include_storage: result["source"] = {"kind": "live_sql", "database": config["database"], "table": table, "file_name": file_name} return result try: from parser.form_payload import decode_form_payload except Exception as exc: return { "schema": "onec_form_decode.v1", "status": "error", "base_id": base_id, "diagnostics": {"message": f"Form payload parser is unavailable: {exc}"}, } element_selector = form_element_filter_from_payload(payload) has_element_selector = any(value not in {None, ""} for value in element_selector.values()) decode_max_items = 5000 if has_element_selector else int(max_items or 500) profile = decode_form_payload( tree, max_items=decode_max_items, include_module_text=bool(include_module_text), include_parameters=bool(include_parameters), max_parameters=int(max_parameters or 80), ) enrich_form_command_references(base_id, profile) enrich_form_style_references(base_id, profile) enrich_form_choice_list_references(base_id, profile) profile = apply_form_element_filter(profile, element_selector) if has_element_selector and len(profile.get("items") or []) > int(max_items or 500): limited_items = (profile.get("items") or [])[: int(max_items or 500)] profile["items"] = limited_items counts = dict(profile.get("counts") or {}) counts["items"] = len(limited_items) counts["items_truncated"] = True profile["counts"] = counts public_profile = public_form_profile(profile, include_storage=include_storage) profile_counts = public_profile.get("counts") if isinstance(public_profile.get("counts"), dict) else {} result = { "schema": "onec_form_decode.v1", "status": profile.get("status"), "base_id": base_id, "source": {"kind": "live_metadata"}, "form": {"guid": form_guid or file_name.split(".", 1)[0]}, "profile": public_profile, "query": { **({key: value for key, value in form_element_filter_from_payload(payload).items() if value not in {None, ""}}), "include_parameters": bool(include_parameters), "max_parameters": int(max_parameters or 80), }, "counts": { "items": profile_counts.get("items"), "items_total": profile_counts.get("items_total"), "focused_elements": profile_counts.get("focused_elements"), "attributes": profile_counts.get("attributes"), "attributes_total": profile_counts.get("attributes_total"), "commands": profile_counts.get("commands"), "commands_total": profile_counts.get("commands_total"), "events": profile_counts.get("events"), "handler_links": profile_counts.get("handler_links"), "resolved_handlers": profile_counts.get("resolved_handlers"), "missing_handlers": profile_counts.get("missing_handlers"), "button_command_links": profile_counts.get("button_command_links"), }, } if include_storage: result["source"] = { "kind": "live_sql", "database": config["database"], "table": table, "file_name": file_name, } result["form"]["file_name"] = file_name if classified_payload is None: try: from parser.cas_payload import classify_payload except Exception: classify_payload = None if classify_payload: try: classified_payload = classify_payload(data, include_text=bool(include_module_text), include_tree=False) except Exception: classified_payload = None if classified_payload is not None: result["undecoded_evidence"] = payload_public_undecoded_evidence( classified_payload, include_text_preview=bool(include_module_text), mode=str(evidence_mode or "summary"), allow_storage_details=bool(include_storage), ) if cached_form_info: result["owner"] = cached_form_info.get("owner") result["origin"] = { "source": "metadata_form_owner_cache", "extension": cached_form_info.get("extension"), "status": "resolved", } result["form"] = {**result.get("form", {}), **(cached_form_info.get("form") if isinstance(cached_form_info.get("form"), dict) else {})} return result def form_owner_index_entry_from_sql_payload( *, base_id: str, config: dict[str, str] | None, table: str, file_name: str, owner_kind: str | None, form_name: str | None, extension: dict[str, Any] | None = None, owner_name: str | None = None, owner_guid: str | None = None, form_guid: str | None = None, timeout_seconds: int = 60, ) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: data, _read_config, error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=timeout_seconds) if error: return None, error decoded = payload_text_from_bytes(data) container_text = str(decoded.get("text") or "") _bsl_text, extraction = extract_bsl_text_from_container(container_text) bsl_offset = extraction.get("bsl_offset") if extraction.get("status") == "ok" else None identity = config_identity_from_bytes(data) or {} effective_form_name = form_name or identity.get("name") or owner_name effective_form_guid = form_guid or identity.get("guid") or (file_name.split(".", 1)[0] if "." in file_name else None) entry = metadata_form_owner_cache_upsert( config, base_id=base_id, owner_kind=owner_kind, form_name=effective_form_name, table=table, file_name=file_name, extension=extension, owner_name=owner_name or effective_form_name, owner_guid=owner_guid or identity.get("guid"), form_guid=effective_form_guid, bsl_offset=int(bsl_offset) if bsl_offset is not None else None, payload={ "identity": identity or None, "extraction": extraction, "diagnostics": { "source_boundary": "Live adapter indexes forms from SQL payloads. XML may be used for analysis/learning only." }, }, ) return entry, None def metadata_form_owner_index_build(payload: dict[str, Any]) -> dict[str, Any]: method = FORM_OWNER_INDEX_BUILD_METHOD payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=90, minimum=1) if timeout_error: return timeout_error limit, limit_error = parse_int_argument(payload, "limit", method=method, default=10, minimum=1, maximum=100) if limit_error: return limit_error scan_limit, scan_limit_error = parse_int_argument(payload, "scan_limit", method=method, default=5000, minimum=1, maximum=20000) if scan_limit_error: return scan_limit_error refresh_cache, refresh_cache_error = strict_bool_argument(payload, "refresh_cache", method=method, default=False) if refresh_cache_error: return refresh_cache_error table_error = validate_optional_string_arguments(payload, method, ["table", "file_name", "form", "form_name", "name", "kind", "extension"]) if table_error: return table_error table = str(payload.get("table") or "").strip() file_name = str(payload.get("file_name") or "").strip() if table and table not in STORAGE_TABLES: return invalid_argument(method, "table", "Unsupported storage table.", allowed_values=sorted(STORAGE_TABLES)) if file_name and Path(file_name).name != file_name: return invalid_argument(method, "file_name", "file_name must be a storage file name, not a path.") config, config_error = sql_config_for_base(base_id) if config_error: return public_error_result(config_error, include_storage=False, method=method) requested_kind = canonical_kind(str(payload.get("kind") or payload.get("object_type") or "")) or str(payload.get("kind") or payload.get("object_type") or "") or None requested_owner_name = str(payload.get("name") or payload.get("object_name") or "").strip() explicit_form_name = str(payload.get("form") or payload.get("form_name") or "").strip() requested_form = ( explicit_form_name or requested_owner_name if requested_kind == "CommonForm" else explicit_form_name ) indexed: list[dict[str, Any]] = [] errors: list[dict[str, Any]] = [] if table and file_name: entry, error = form_owner_index_entry_from_sql_payload( base_id=base_id, config=config, table=table, file_name=file_name, owner_kind=requested_kind or "CommonForm", form_name=requested_form or None, extension={"name": payload.get("extension")} if payload.get("extension") else None, owner_name=requested_owner_name or None, owner_guid=str(payload.get("guid") or payload.get("object_guid") or "") or None, timeout_seconds=int(timeout_seconds or 90), ) if entry: indexed.append(entry) if error: errors.append(error) else: find_payload = { "base_id": base_id, "extension": payload.get("extension"), "kind": requested_kind or "CommonForm", "query": requested_owner_name or requested_form, "include_storage": True, "limit": int(limit or 10), "scan_limit": int(scan_limit or 5000), "timeout_seconds": int(timeout_seconds or 90), "refresh_cache": bool(refresh_cache), } objects_result = extension_objects_find(find_payload) objects = [item for item in objects_result.get("objects") or [] if isinstance(item, dict)] if objects_result.get("status") == "ok" else [] if objects_result.get("status") != "ok": errors.append({"area": "extension.objects.find", "status": objects_result.get("status"), "diagnostics": objects_result.get("diagnostics")}) for item in objects[: int(limit or 10)]: route = item.get("route") if isinstance(item.get("route"), dict) else {} route_table = str(route.get("table") or "ConfigCAS") route_file = str(route.get("file_name") or "") if route_table not in STORAGE_TABLES or not route_file or Path(route_file).name != route_file: continue origin = item.get("origin") if isinstance(item.get("origin"), dict) else {} extension = origin.get("extension") if isinstance(origin.get("extension"), dict) else None if not extension and payload.get("extension"): extension = {"name": str(payload.get("extension") or "")} extension_guid = str((extension or {}).get("guid") or "").strip().lower() or None candidate_files: list[tuple[str, str, str]] = [] related_entries, related_diagnostics = manifest_related_entries_for_cas_key( base_id, route_file, extension_guid=extension_guid, timeout_seconds=min(int(timeout_seconds or 90), 60), ) if related_diagnostics: errors.extend({"area": "manifest_related_entries", **diag} for diag in related_diagnostics if isinstance(diag, dict)) for related in sorted( [entry for entry in related_entries if isinstance(entry, dict)], key=lambda entry: (0 if str(entry.get("suffix") or "") == ".0" else 1, str(entry.get("suffix") or "")), ): cas_key = str(related.get("cas_key") or "").strip().lower() if cas_key and Path(cas_key).name == cas_key: candidate_files.append(("ConfigCAS", cas_key, str(related.get("suffix") or ""))) candidate_files.append((route_table, route_file, "descriptor")) seen_candidates: set[tuple[str, str]] = set() for candidate_table, candidate_file, candidate_suffix in candidate_files: dedupe = (candidate_table, candidate_file) if dedupe in seen_candidates: continue seen_candidates.add(dedupe) entry, error = form_owner_index_entry_from_sql_payload( base_id=base_id, config=config, table=candidate_table, file_name=candidate_file, owner_kind=str(item.get("kind") or requested_kind or "CommonForm"), form_name=requested_form or (str(item.get("name") or "") if str(item.get("kind") or requested_kind) == "CommonForm" else None), extension=extension, owner_name=str(item.get("name") or requested_form or ""), owner_guid=str(item.get("guid") or ""), form_guid=str(item.get("guid") or route_file.split(".", 1)[0]), timeout_seconds=int(timeout_seconds or 90), ) if entry and (entry.get("bsl_offset") is not None or candidate_suffix == "descriptor" or not related_entries): entry["manifest_suffix"] = candidate_suffix indexed.append(entry) if entry.get("bsl_offset") is not None: break if error: errors.append(error) return { "schema": "onec_form_owner_index_build.v1", "method": method, "status": "ok" if indexed else "not_found", **({"error": "not_found"} if not indexed else {}), "base_id": base_id, "source": {"kind": "live_sql", "cache": "metadata_form_owner_cache"}, "query": { "extension": payload.get("extension"), "kind": requested_kind or "CommonForm", "owner": requested_owner_name or None, "form": requested_form or None, "table": table or None, "file_name": file_name or None, "limit": int(limit or 10), "scan_limit": int(scan_limit or 5000), }, "forms": indexed, "counts": {"indexed": len(indexed), "errors": len(errors)}, "diagnostics": errors or [ { "message": "Form owner index built from SQL evidence. XML is reserved for analysis/learning and is not used as the live write transport." } ], } FORM_ELEMENT_WRITE_METHOD = "metadata.form.element.write" FORM_ELEMENT_WRITE_APPLY_METHOD = "metadata.form.element.write_apply" OBJECT_PROPERTY_WRITE_METHOD = "metadata.object.property.write" OBJECT_MEMBER_ADD_METHOD = "metadata.object.member.add" FORM_TARGET_MOVE_METHOD = "metadata.form.target.move" FORM_COMMAND_BUTTON_WRITE_METHOD = "metadata.form.command_button.write" FORM_COMMAND_BUTTON_VERIFY_METHOD = "metadata.form.command_button.verify" FORM_OWNER_INDEX_BUILD_METHOD = "metadata.form.owner_index.build" MODULE_WRITE_APPLY_METHOD = "metadata.module.write_apply" METADATA_WRITE_PLAN_METHOD = "metadata.write.plan" METADATA_WRITE_PREFLIGHT_METHOD = "metadata.write.preflight" METADATA_WRITE_METHOD = "metadata.write" METADATA_WRITE_ROLLBACK_METHOD = "metadata.write.rollback" CODE_WRITE_METHOD = "code.write" FORM_WRITE_TARGET_RESOLVE_METHOD = "metadata.form.write_target.resolve" FORM_WRITE_TARGET_VERIFY_METHOD = "metadata.form.write_target.verify" FORM_WRITE_MATRIX_BUILD_METHOD = "metadata.form.write_matrix.build" FORM_WRITE_MATRIX_SMOKE_METHOD = "metadata.form.write_matrix.smoke" SAVED_STATE_FORMS_SEARCH_METHOD = "metadata.saved_state.forms.search" SAVED_STATE_MODULES_SEARCH_METHOD = "metadata.saved_state.modules.search" SAVED_STATE_STATUS_METHOD = "metadata.saved_state.status" SAVED_STATE_DIFF_METHOD = "metadata.saved_state.diff" SAVED_STATE_CHANGES_LIST_METHOD = "metadata.saved_state.changes.list" FORM_ELEMENT_WRITE_APPLY_MODES = {"plan", "apply", "apply_and_verify", "apply_and_rollback"} FORM_DECODE_SELECTOR_KEYS = {"element", "element_name", "element_path", "path", "element_id", "id", "command", "attribute"} TECHNICAL_WRITE_METHODS = { "changes.propose", FORM_WRITE_TARGET_RESOLVE_METHOD, FORM_WRITE_MATRIX_BUILD_METHOD, FORM_WRITE_MATRIX_SMOKE_METHOD, SAVED_STATE_FORMS_SEARCH_METHOD, SAVED_STATE_MODULES_SEARCH_METHOD, SAVED_STATE_STATUS_METHOD, SAVED_STATE_DIFF_METHOD, SAVED_STATE_CHANGES_LIST_METHOD, FORM_ELEMENT_WRITE_METHOD, FORM_ELEMENT_WRITE_APPLY_METHOD, FORM_TARGET_MOVE_METHOD, FORM_COMMAND_BUTTON_WRITE_METHOD, MODULE_WRITE_APPLY_METHOD, METADATA_WRITE_PLAN_METHOD, METADATA_WRITE_PREFLIGHT_METHOD, METADATA_WRITE_METHOD, METADATA_WRITE_ROLLBACK_METHOD, "storage.saved_state.apply_proposal", "storage.saved_state.rollback", "storage.saved_state.backups.list", } WRITE_HISTORY_RECORDED_METHODS = { METADATA_WRITE_METHOD, OBJECT_PROPERTY_WRITE_METHOD, OBJECT_MEMBER_ADD_METHOD, FORM_COMMAND_BUTTON_WRITE_METHOD, FORM_ELEMENT_WRITE_APPLY_METHOD, MODULE_WRITE_APPLY_METHOD, CODE_WRITE_METHOD, METADATA_WRITE_ROLLBACK_METHOD, "infobase.user.password.set", "infobase.user.password.clear", } WRITE_LEARNING_METHODS = { "metadata.write_learning.capture_before", "metadata.write_learning.capture_after", "metadata.write_learning.diff", "metadata.write_learning.infer_rule", } TECHNICAL_WRITE_METHODS.update(WRITE_LEARNING_METHODS) FORM_ELEMENT_SAVED_STATE_TABLES = {"ConfigSave", "ConfigCASSave"} FORM_PROPERTY_REGISTRY = { "id": { "presentation": "Идентификатор", "aliases": ["id", "идентификатор"], "direct_path": "id_path", "value": "id", "targets": ["items", "commands", "attributes", "tables", "command_bars"], "value_type": "scalar", "verification": "readback_path", }, "name": { "presentation": "Имя", "aliases": ["name", "имя"], "direct_path": "name_path", "value": "name", "targets": ["items", "commands", "attributes", "tables", "command_bars"], "value_type": "string", "verification": "readback_path", }, "title": { "presentation": "Заголовок", "aliases": ["title", "caption", "заголовок", "синоним", "представление"], "direct_path": "title_path", "value": "title", "targets": ["items", "commands", "attributes", "tables", "command_bars"], "prefer_source": True, "value_type": "string", "verification": "source_aware_readback", }, "path_to_data": { "presentation": "ПутьКДанным", "aliases": ["path_to_data", "path to data", "путькданным", "путь к данным", "данные"], "direct_path": "path_to_data_path", "value": "path_to_data", "targets": ["items", "attributes", "tables"], "value_type": "string", "verification": "readback_path", }, "visible": { "presentation": "Видимость", "aliases": ["visible", "visibility", "видимость", "видимый", "отображать"], "targets": ["items", "commands", "attributes", "tables", "command_bars"], "value_type": "bool_atom", "verification": "readback_path", }, "enabled": { "presentation": "Доступность", "aliases": ["enabled", "available", "availability", "доступность", "доступный"], "targets": ["items", "commands", "attributes", "tables", "command_bars"], "value_type": "bool_atom", "verification": "readback_path", }, "read_only": { "presentation": "ТолькоПросмотр", "aliases": ["read_only", "readonly", "толькопросмотр", "только просмотр"], "targets": ["items", "attributes", "tables", "command_bars"], "value_type": "bool_atom", "verification": "readback_path", }, "use": { "presentation": "Использование", "aliases": ["use", "usage", "использование", "использовать"], "targets": ["items", "commands", "attributes", "tables", "command_bars"], "value_type": "bool_or_enum_atom", "verification": "readback_path", }, "group": { "presentation": "Группа", "aliases": ["group", "parent", "container", "группа", "подчинение", "родитель"], "targets": ["items", "command_bars"], "value_type": "scalar", "verification": "readback_path", }, "view": { "presentation": "Вид", "aliases": ["view", "kind", "type", "вид", "вид элемента"], "targets": ["items", "command_bars"], "value_type": "enum_atom", "verification": "readback_path", }, "representation": { "presentation": "Отображение", "aliases": ["representation", "display_mode", "отображение"], "targets": ["items", "command_bars"], "value_type": "enum_atom", "verification": "readback_path", }, "title_location": { "presentation": "ПоложениеЗаголовка", "aliases": ["title_location", "title position", "положениезаголовка", "положение заголовка"], "targets": ["items"], "value_type": "enum_atom", "verification": "readback_path", }, "command_bar_location": { "presentation": "ПоложениеВКоманднойПанели", "aliases": ["command_bar_location", "command bar location", "положениевкоманднойпанели", "положение в командной панели"], "targets": ["items", "command_bars"], "value_type": "enum_atom", "verification": "readback_path", }, "unique_command": { "presentation": "УникальностьКоманды", "aliases": ["unique_command", "уникальностькоманды", "уникальность команды"], "targets": ["items"], "value_type": "bool_atom", "verification": "readback_path", }, "command_name": { "presentation": "ИмяКоманды", "aliases": ["command_name", "command", "имякоманды", "имя команды", "команда"], "targets": ["items"], "value_type": "command_binding", "verification": "readback_command_binding", }, "background_color": { "presentation": "ЦветФона", "aliases": ["background_color", "background", "цветфона", "цвет фона"], "targets": ["items", "command_bars"], "value_type": "color_or_enum_atom", "verification": "readback_path", }, "text_color": { "presentation": "ЦветТекста", "aliases": ["text_color", "foreground", "цветтекста", "цвет текста"], "targets": ["items", "command_bars"], "value_type": "color_or_enum_atom", "verification": "readback_path", }, "border_color": { "presentation": "ЦветРамки", "aliases": ["border_color", "border", "цветрамки", "цвет рамки"], "targets": ["items", "command_bars"], "value_type": "color_or_enum_atom", "verification": "readback_path", }, } FORM_ELEMENT_PROPERTY_ALIASES = { alias.casefold(): canonical for canonical, rule in FORM_PROPERTY_REGISTRY.items() for alias in [canonical, *list(rule.get("aliases") or [])] } def normalize_form_property_name(value: Any) -> str: text = str(value or "").strip().casefold() return FORM_ELEMENT_PROPERTY_ALIASES.get(text, text) def form_property_rule(property_name: Any) -> dict[str, Any]: normalized = normalize_form_property_name(property_name) rule = FORM_PROPERTY_REGISTRY.get(normalized) if rule: return {"canonical": normalized, **rule} return { "canonical": normalized, "presentation": str(property_name or normalized), "aliases": [str(property_name or normalized)], "targets": ["items", "commands", "attributes", "tables", "command_bars"], "value_type": "scalar", "verification": "readback_path", } def form_property_alias_matches(property_name: Any, candidate: Any) -> bool: rule = form_property_rule(property_name) values = [rule.get("canonical"), rule.get("presentation"), *(rule.get("aliases") or [])] candidate_exact = normalize_exact(candidate) candidate_norm = normalize(candidate) return any(candidate_exact == normalize_exact(value) or candidate_norm == normalize(value) for value in values if value not in {None, ""}) def form_element_write_scalar(value: Any) -> Any: if isinstance(value, bool): return "1" if value else "0" return value def form_property_current_value(item: dict[str, Any], property_name: Any) -> Any: path, source = form_element_parameter_path(item, str(property_name or "")) if path and isinstance(source, dict): return source.get("value") normalized = normalize_form_property_name(property_name) if normalized == "title": return item.get("title") if normalized == "name": return item.get("name") if normalized == "id": return item.get("id") if normalized == "path_to_data": return item.get("path_to_data") return item.get(normalized) def form_write_target_public(item: dict[str, Any]) -> dict[str, Any]: target = {key: item.get(key) for key in ("section", "name", "id", "title", "path", "marker", "type_name", "match_by") if item.get(key) is not None} if "_profile_section" in item and "section" not in target: target["section"] = item.get("_profile_section") return target def form_write_target_writable_properties(item: dict[str, Any]) -> list[dict[str, Any]]: properties: list[dict[str, Any]] = [] for canonical, rule in FORM_PROPERTY_REGISTRY.items(): if canonical == "command_name": binding = item.get("command_binding") if isinstance(item.get("command_binding"), dict) else None if binding and binding.get("command_id_path") and binding.get("group_guid_path"): properties.append( { "property": canonical, "presentation": rule.get("presentation") or canonical, "path": binding.get("command_id_path"), "value": form_item_command_name(item), "value_type": rule.get("value_type"), "verification": rule.get("verification"), "paths": { "command_id": binding.get("command_id_path"), "group_guid": binding.get("group_guid_path"), }, } ) continue path_key = rule.get("direct_path") value_key = rule.get("value") if path_key and item.get(path_key): properties.append( { "property": canonical, "presentation": rule.get("presentation") or canonical, "path": item.get(path_key), "value": item.get(value_key), "value_type": rule.get("value_type"), "verification": rule.get("verification"), } ) seen_paths = {str(row.get("path") or "") for row in properties} for parameter in item.get("parameters") or []: if not isinstance(parameter, dict): continue presentation = str(parameter.get("presentation") or "") index = parameter.get("index") if not presentation or index is None or not item.get("path"): continue try: path = f"{item.get('path')}.{int(index)}" except (TypeError, ValueError): continue if path in seen_paths: continue seen_paths.add(path) matched_rule = next( ( (canonical, rule) for canonical, rule in FORM_PROPERTY_REGISTRY.items() if form_property_alias_matches(canonical, presentation) ), None, ) row = {"property": presentation, "presentation": presentation, "path": path, "value": parameter.get("value"), "parameter_index": index} if matched_rule: canonical, rule = matched_rule row.update({"canonical_property": canonical, "value_type": rule.get("value_type"), "verification": rule.get("verification")}) properties.append(row) return properties def form_semantic_property_for_parameter(item: dict[str, Any], parameter_index: Any) -> dict[str, Any] | None: try: wanted_index = int(parameter_index) except (TypeError, ValueError): return None semantic = item.get("semantic") if isinstance(item.get("semantic"), dict) else {} for group, props in (semantic.get("groups") or {}).items(): for prop in props or []: if not isinstance(prop, dict): continue if prop.get("parameter_index") == wanted_index: return {**prop, "group": group} return None def form_command_by_name(profile: dict[str, Any]) -> dict[str, dict[str, Any]]: return { str(command.get("name") or "").casefold(): command for command in profile.get("commands") or [] if isinstance(command, dict) and command.get("name") } def form_linked_command_for_item(profile: dict[str, Any], item: dict[str, Any]) -> dict[str, Any] | None: command_by_name = form_command_by_name(profile) item_name = str(item.get("name") or "") if item_name and item_name.casefold() in command_by_name and str(item.get("_profile_section") or "") != "commands": return command_by_name[item_name.casefold()] for link in profile.get("button_command_links") or []: if not isinstance(link, dict): continue if normalize_exact(link.get("button")) == normalize_exact(item_name): command_name = str(link.get("command") or "") if command_name.casefold() in command_by_name: return command_by_name[command_name.casefold()] return None def form_data_path_head(path_to_data: Any) -> str: text = str(path_to_data or "").strip() if not text: return "" return text.split(".", 1)[0].strip() def form_data_path_is_object_attribute(path_to_data: Any) -> bool: head = form_data_path_head(path_to_data) return normalize_exact(head) in {"объект", "object", "thisobject", "этотобъект"} def without_form_decode_selector_keys(payload: dict[str, Any]) -> dict[str, Any]: return {key: value for key, value in payload.items() if key not in FORM_DECODE_SELECTOR_KEYS} def form_attribute_for_data_path(profile: dict[str, Any], path_to_data: Any) -> dict[str, Any] | None: text = str(path_to_data or "").strip() head = form_data_path_head(text) if not text or not head: return None tail = text.split(".", 1)[1].strip() if "." in text else "" for attribute in profile.get("attributes") or []: if not isinstance(attribute, dict): continue name = str(attribute.get("name") or "") if normalize_exact(name) in {normalize_exact(text), normalize_exact(head)}: if tail: for field in attribute.get("dynamic_list_fields") or []: if not isinstance(field, dict): continue field_values = { normalize_exact(field.get("path_to_data")), normalize_exact(field.get("data_name")), normalize_exact(field.get("name")), } if normalize_exact(text) in field_values or normalize_exact(tail) in field_values: return { **field, "_profile_section": "attribute_fields", "owner_attribute": form_write_target_public({**attribute, "_profile_section": "attributes"}), "type_name": field.get("type_name") or "Поле табличного реквизита", } return {**attribute, "_profile_section": "attributes"} return None def form_edit_requests_local_override(edit: dict[str, Any] | None) -> bool: if not isinstance(edit, dict): return False source = str(edit.get("source") or edit.get("write_source") or "").strip().casefold() if source in {"local", "local_override", "element", "override"}: return True return edit.get("local_override") is True def form_effective_write_target( profile: dict[str, Any], item: dict[str, Any], property_name: Any, edit: dict[str, Any] | None = None, ) -> tuple[dict[str, Any], dict[str, Any] | None]: normalized_property = normalize_form_property_name(property_name) if normalized_property != "title": return item, None if form_edit_requests_local_override(edit): return item, { "kind": "local_override_title", "requested_target": form_write_target_public(item), "write_target": form_write_target_public(item), "writable": True, "message": "Local element title override was explicitly requested.", } if str(item.get("_profile_section") or "") == "commands": return item, None if item.get("title") not in {None, ""}: return item, None command = form_linked_command_for_item(profile, item) if command and command.get("title_path"): command_target = {**command, "_profile_section": "commands"} return command_target, { "kind": "linked_command_title", "requested_target": form_write_target_public(item), "write_target": form_write_target_public(command_target), "writable": True, "message": "Element title is empty; display title is inherited from the linked form command.", } if item.get("path_to_data"): if form_data_path_is_object_attribute(item.get("path_to_data")): return item, { "kind": "data_path_object_attribute_local_title", "requested_target": form_write_target_public(item), "write_target": form_write_target_public(item), "path_to_data": item.get("path_to_data"), "writable": True, "message": "Element title is empty and ПутьКДанным points outside form attributes; write the local form element title.", } attribute = form_attribute_for_data_path(profile, item.get("path_to_data")) if attribute and attribute.get("title_path"): routed_kind = "data_path_form_attribute_field_title" if str(attribute.get("_profile_section") or "") == "attribute_fields" else "data_path_form_attribute_title" return attribute, { "kind": routed_kind, "requested_target": form_write_target_public(item), "write_target": form_write_target_public(attribute), "path_to_data": item.get("path_to_data"), "writable": True, "message": "Element title is empty and ПутьКДанным points to a form attribute field; write the field title." if routed_kind == "data_path_form_attribute_field_title" else "Element title is empty and ПутьКДанным points to a form attribute; write the form attribute title.", } if not attribute: return item, { "kind": "data_path_object_attribute_local_title", "requested_target": form_write_target_public(item), "write_target": form_write_target_public(item), "path_to_data": item.get("path_to_data"), "writable": True, "message": "Element title is empty and ПутьКДанным was not found among form attributes; write the local form element title.", } return item, { "kind": "data_path_title", "requested_target": form_write_target_public(item), "path_to_data": item.get("path_to_data"), "writable": False, "requires": "form_attribute_title_path", "message": "Element title is empty and ПутьКДанным points to a form attribute, but its writable title path was not decoded.", } return item, None def form_profile_write_targets(profile: dict[str, Any]) -> list[dict[str, Any]]: targets: list[dict[str, Any]] = [] for section in ("items", "commands", "attributes", "tables", "command_bars"): for row in profile.get(section) or []: if not isinstance(row, dict): continue item = dict(row) item["_profile_section"] = section targets.append(item) if section == "attributes": owner = form_write_target_public(item) for field in row.get("dynamic_list_fields") or []: if not isinstance(field, dict): continue field_item = dict(field) field_item["_profile_section"] = "attribute_fields" field_item["owner_attribute"] = owner field_item.setdefault("type_name", "Поле табличного реквизита") targets.append(field_item) return targets def filter_form_profile_write_targets(targets: list[dict[str, Any]], selector: dict[str, Any]) -> list[dict[str, Any]]: element_path = str(selector.get("element_path") or selector.get("path") or "").strip() element_id = str(selector.get("element_id") or selector.get("id") or "").strip() element_name = str(selector.get("element") or selector.get("element_name") or "").strip() preferred_sections = selector.get("_preferred_sections") def with_match(items: list[dict[str, Any]], match_by: str) -> list[dict[str, Any]]: return [{**item, "match_by": match_by} for item in items] def prefer_sections(items: list[dict[str, Any]]) -> list[dict[str, Any]]: if not preferred_sections: return items allowed = {str(section) for section in preferred_sections if section} preferred = [item for item in items if str(item.get("_profile_section") or "") in allowed] return preferred or items if element_path: return prefer_sections(with_match([item for item in targets if str(item.get("path") or "") == element_path], "path_exact")) if element_id: return prefer_sections(with_match([item for item in targets if str(item.get("id") or "") == element_id], "id_exact")) if element_name: normalized = normalize(element_name) exact = normalize_exact(element_name) matches = [] for item in targets: match_by = None if normalize_exact(item.get("name")) == exact: match_by = "name_exact" elif normalize_exact(item.get("title")) == exact: match_by = "title_exact" elif normalize(item.get("name")) == normalized: match_by = "name_normalized" elif normalize(item.get("title")) == normalized: match_by = "title_normalized" if match_by: matches.append({**item, "match_by": match_by}) return prefer_sections(matches) return targets def form_write_target_candidates(profile: dict[str, Any], selector: dict[str, Any], *, limit: int = 20) -> list[dict[str, Any]]: targets = form_profile_write_targets(profile) if any(selector.get(key) not in {None, ""} for key in ("element", "element_name", "element_path", "path", "element_id", "id")): targets = filter_form_profile_write_targets(targets, selector) candidates = [] for item in targets[: max(1, limit)]: public = form_write_target_public(item) writable = form_write_target_writable_properties(item) if writable: public["writable_properties"] = writable[:20] candidates.append(public) return candidates def form_write_selector_from_payload(payload: dict[str, Any]) -> dict[str, Any]: selector = { "element": payload.get("element") or payload.get("command") or payload.get("attribute") or payload.get("element_name"), "element_path": payload.get("element_path") or payload.get("path"), "element_id": payload.get("element_id") or payload.get("id"), } if payload.get("command"): selector["_preferred_sections"] = ["commands"] elif payload.get("attribute"): selector["_preferred_sections"] = ["attributes", "attribute_fields"] elif payload.get("element") or payload.get("element_name"): selector["_preferred_sections"] = ["items", "commands"] return selector def public_non_empty_query_fields(payload: dict[str, Any]) -> dict[str, Any]: result: dict[str, Any] = {} for key, value in payload.items(): if key.startswith("_") or value is None: continue if isinstance(value, str) and value == "": continue if isinstance(value, (str, int, float, bool)): result[key] = value return result def form_element_parameter_path(item: dict[str, Any], property_name: str) -> tuple[str | None, dict[str, Any] | None]: normalized_property = normalize_form_property_name(property_name) rule = form_property_rule(property_name) if normalized_property == "visible" and str(item.get("marker") or "") == "22" and str(item.get("type_name") or "") == "Группа": parameters_by_index = { int(parameter.get("index")): parameter for parameter in item.get("parameters") or [] if isinstance(parameter, dict) and str(parameter.get("index") or "").lstrip("-").isdigit() } for variant_index in (26, 28): parameter = parameters_by_index.get(variant_index) if parameter is None or str(parameter.get("value") or "") not in {"0", "1"}: continue return ( f"{item.get('path')}.{variant_index}", { **parameter, "presentation": rule.get("presentation"), "source": "controlled_designer_group_visibility_variant", "variant_parameter_index": variant_index, }, ) direct_path_key = rule.get("direct_path") direct_value_key = rule.get("value") if direct_path_key and item.get(direct_path_key): return str(item.get(direct_path_key)), {"presentation": rule.get("presentation"), "value": item.get(direct_value_key)} for parameter in item.get("parameters") or []: if not isinstance(parameter, dict): continue presentation = str(parameter.get("presentation") or "") if form_property_alias_matches(property_name, presentation) or normalize(presentation) == normalize(normalized_property): index = parameter.get("index") if index is None: return None, parameter try: return f"{item.get('path')}.{int(index)}", parameter except (TypeError, ValueError): return None, parameter for _group, properties in ((item.get("semantic") or {}).get("groups") or {}).items(): for prop in properties or []: if not isinstance(prop, dict): continue if not form_property_alias_matches(property_name, prop.get("name")) and normalize(prop.get("name")) != normalize(property_name): continue index = prop.get("parameter_index") if index is None: return None, prop try: return f"{item.get('path')}.{int(index)}", prop except (TypeError, ValueError): return None, prop return None, None FORM_COMMAND_GROUP_GUID = "409b9a53-7f7e-4178-86c1-33176c7c7a7a" FORM_STANDARD_COMMAND_GUIDS = { "Form.StandardCommand.CustomizeForm": ("0", "198ea630-fda2-4cda-8a23-f999f4c67ee6"), } def form_item_command_name(item: dict[str, Any]) -> str | None: semantic = item.get("semantic") if isinstance(item.get("semantic"), dict) else {} for _group, props in (semantic.get("groups") or {}).items(): for prop in props or []: if isinstance(prop, dict) and form_property_alias_matches("command_name", prop.get("name")): value = prop.get("value") return str(value) if value not in {None, ""} else None binding = item.get("command_binding") if isinstance(item.get("command_binding"), dict) else None if binding and binding.get("command_name"): return str(binding.get("command_name")) return None def normalize_form_command_value(value: Any) -> str: text = str(value or "").strip() if not text: return "" if text.startswith("Form.Command.") or text.startswith("Form.StandardCommand."): return text return f"Form.Command.{text}" def form_command_binding_target(profile: dict[str, Any], command_value: Any) -> dict[str, Any] | None: command_name = normalize_form_command_value(command_value) if not command_name: return None standard = FORM_STANDARD_COMMAND_GUIDS.get(command_name) if standard: command_id, group_guid = standard return { "scope": "standard", "command_name": command_name, "command_id": command_id, "group_guid": group_guid, "match_by": "standard_command_guid", } if command_name.startswith("Form.Command."): local_name = command_name.removeprefix("Form.Command.") for command in profile.get("commands") or []: if not isinstance(command, dict): continue if normalize_exact(command.get("name")) == normalize_exact(local_name): return { "scope": "form", "command_name": command_name, "command_id": str(command.get("id") or ""), "group_guid": FORM_COMMAND_GROUP_GUID, "match_by": "form_command_name", } return None def form_command_name_write_edits( profile: dict[str, Any], item: dict[str, Any], edit: dict[str, Any], index: int, ) -> tuple[list[dict[str, Any]] | None, dict[str, Any] | None]: binding = item.get("command_binding") if isinstance(item.get("command_binding"), dict) else None if not binding or not binding.get("command_id_path") or not binding.get("group_guid_path"): return None, { "schema": "onec_adapter_request_error.v1", "method": FORM_ELEMENT_WRITE_METHOD, "status": "not_found", "error": "command_binding_not_writable", "argument": f"edits[{index}].property", "diagnostics": {"message": "Selected form element does not expose decoded command binding paths."}, "element": {key: item.get(key) for key in ("name", "id", "title", "path", "marker", "type_name")}, } target = form_command_binding_target(profile, edit.get("value")) if not target: return None, { "schema": "onec_adapter_request_error.v1", "method": FORM_ELEMENT_WRITE_METHOD, "status": "not_found", "error": "command_not_resolved", "argument": f"edits[{index}].value", "diagnostics": {"message": "Command value must be an existing Form.Command. or a known Form.StandardCommand.."}, "known_standard_commands": sorted(FORM_STANDARD_COMMAND_GUIDS), } old_name = form_item_command_name(item) result = [ { "path": str(binding.get("command_id_path")), "value": target["command_id"], "node_type": str(edit.get("node_type") or "auto"), "property": edit.get("property") or edit.get("name"), "canonical_property": "command_name", "old": str(binding.get("command_id") or ""), "semantic_old": old_name, "semantic_new": target["command_name"], "command_binding_part": "command_id", "rule": { "presentation": "ИмяКоманды", "value_type": "command_binding", "verification": "readback_command_binding", }, }, { "path": str(binding.get("group_guid_path")), "value": target["group_guid"], "node_type": str(edit.get("node_type") or "auto"), "property": edit.get("property") or edit.get("name"), "canonical_property": "command_name", "old": str(binding.get("group_guid") or ""), "semantic_old": old_name, "semantic_new": target["command_name"], "command_binding_part": "group_guid", "rule": { "presentation": "ИмяКоманды", "value_type": "command_binding", "verification": "readback_command_binding", }, }, ] if "expected_old" in edit: for row in result: row["expected_old"] = edit.get("expected_old") return result, None def form_element_write_edit(item: dict[str, Any], edit: dict[str, Any], index: int) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: if not isinstance(edit, dict): return None, invalid_argument(FORM_ELEMENT_WRITE_METHOD, f"edits[{index}]", "Each edit must be a JSON object.") property_name = edit.get("property") or edit.get("name") if not property_name or not isinstance(property_name, str): return None, invalid_argument(FORM_ELEMENT_WRITE_METHOD, f"edits[{index}].property", "Edit property must be a non-empty JSON string.") if "value" not in edit: return None, invalid_argument(FORM_ELEMENT_WRITE_METHOD, f"edits[{index}].value", "Edit value is required.") normalized_property = normalize_form_property_name(property_name) rule = form_property_rule(property_name) path, source = form_element_parameter_path(item, property_name) old = source.get("value") if isinstance(source, dict) else None if not path: return None, { "schema": "onec_adapter_request_error.v1", "method": FORM_ELEMENT_WRITE_METHOD, "status": "not_found", "error": "property_not_writable", "argument": f"edits[{index}].property", "diagnostics": { "message": "Property was not found as a decoded writable scalar for the selected form element. Try name/title/id or a decoded parameter presentation such as `Видимость`.", }, "element": {key: item.get(key) for key in ("name", "id", "title", "path", "marker", "type_name")}, } result = { "path": str(path), "value": form_element_write_scalar(edit.get("value")), "node_type": str(edit.get("node_type") or "auto"), "property": property_name, "canonical_property": normalized_property, "old": old, "rule": { "presentation": rule.get("presentation"), "value_type": rule.get("value_type"), "verification": rule.get("verification"), }, } if "expected_old" in edit: result["expected_old"] = edit.get("expected_old") return result, None def saved_state_form_descriptor_identity( *, base_id: str, table: str, file_name: str, timeout_seconds: int, ) -> dict[str, Any] | None: if not file_name.endswith(".0"): return None descriptor_file_name = file_name[:-2] if not descriptor_file_name: return None data, _config, read_error = read_storage_file_bytes(base_id, table, descriptor_file_name, timeout_seconds=timeout_seconds) if read_error or data is None: return None identity = config_identity_from_bytes(data) if not identity: return None synonyms = identity.get("synonyms") if isinstance(identity.get("synonyms"), dict) else {} synonym = next(iter(synonyms.values()), None) if synonyms else None return { "name": identity.get("name"), "synonym": synonym, "guid": identity.get("guid"), "descriptor_file_name": descriptor_file_name, "source": "saved_state_descriptor", **({"name_variants": identity.get("name_variants")} if identity.get("name_variants") else {}), **({"synonym_variants": identity.get("synonym_variants")} if identity.get("synonym_variants") else {}), } def saved_state_form_search_row( *, base_id: str, table: str, file_name: str, payload: dict[str, Any], timeout_seconds: int, ) -> dict[str, Any] | None: decoded = metadata_form_decode( { "base_id": base_id, "table": table, "file_name": file_name, "include_storage": True, "include_parameters": False, "max_items": 5000, "timeout_seconds": timeout_seconds, } ) if decoded.get("status") != "ok": return None descriptor_identity = saved_state_form_descriptor_identity( base_id=base_id, table=table, file_name=file_name, timeout_seconds=timeout_seconds, ) profile = decoded.get("profile") if isinstance(decoded.get("profile"), dict) else {} targets = form_profile_write_targets(profile) form_query = str(payload.get("form") or payload.get("form_name") or payload.get("name_filter") or "").strip() element_query = str(payload.get("element") or payload.get("command") or payload.get("attribute") or payload.get("element_name") or "").strip() text_query = str(payload.get("query") or payload.get("text") or "").strip() needles = [normalize_exact(value) for value in (form_query, element_query, text_query) if value] matched_targets = [] descriptor_blob = " ".join( str((descriptor_identity or {}).get(key) or "") for key in ("name", "synonym", "guid", "descriptor_file_name") ) for target in targets: blob = " ".join(str(target.get(key) or "") for key in ("name", "title", "id", "path", "_profile_section")) + " " + descriptor_blob if element_query: selector = {"element": element_query} if not filter_form_profile_write_targets([target], selector): continue elif text_query and normalize_exact(blob).find(normalize_exact(text_query)) < 0: continue matched_targets.append( { **form_write_target_public(target), "writable_properties": form_write_target_writable_properties(target)[:12], } ) if needles and not matched_targets: profile_blob = normalize_exact(json.dumps(profile, ensure_ascii=False)[:200000]) descriptor_blob_normalized = normalize_exact(descriptor_blob) if not any(needle in profile_blob or needle in normalize_exact(file_name) or needle in descriptor_blob_normalized for needle in needles): return None source = decoded.get("source") if isinstance(decoded.get("source"), dict) else {} form = {**(decoded.get("form") if isinstance(decoded.get("form"), dict) else {}), "file_name": file_name} if descriptor_identity: form["identity"] = descriptor_identity if descriptor_identity.get("name") and not form.get("name"): form["name"] = descriptor_identity.get("name") if descriptor_identity.get("synonym") and not form.get("synonym"): form["synonym"] = descriptor_identity.get("synonym") if descriptor_identity.get("guid") and not form.get("guid"): form["guid"] = descriptor_identity.get("guid") owner = decoded.get("owner") if isinstance(decoded.get("owner"), dict) else {} if not owner.get("name"): related_owner = saved_state_related_descriptor_identity( base_id=base_id, table=table, file_name=file_name, timeout_seconds=timeout_seconds, ) if related_owner and related_owner.get("name"): owner = { "status": "resolved", "kind": related_owner.get("kind") or "Catalog", "name": related_owner.get("name"), "synonym": related_owner.get("synonym"), "guid": related_owner.get("guid"), "source": "saved_state_descriptor", } return { "table": table, "file_name": file_name, **({"name": descriptor_identity.get("name")} if descriptor_identity and descriptor_identity.get("name") else {}), **({"synonym": descriptor_identity.get("synonym")} if descriptor_identity and descriptor_identity.get("synonym") else {}), "form": form, **({"owner": owner} if owner else {}), "source": { **{key: source.get(key) for key in ("kind", "database", "table", "file_name") if source.get(key) is not None}, "table": table, "file_name": file_name, }, "counts": decoded.get("counts"), "matches": matched_targets[: int(payload.get("max_targets") or 20)], } def public_saved_state_form_search_row( row: dict[str, Any], *, requested_owner_kind: str | None = None, requested_extension: str | None = None, ) -> dict[str, Any]: owner = row.get("owner") if isinstance(row.get("owner"), dict) else {} form = row.get("form") if isinstance(row.get("form"), dict) else {} identity = form.get("identity") if isinstance(form.get("identity"), dict) else {} owner_public = { key: value for key, value in owner.items() if key in {"status", "kind", "name", "synonym", "source"} and value is not None } owner_ref = object_selector_ref(owner_public.get("kind"), owner_public.get("name")) form_name = str(row.get("name") or form.get("name") or identity.get("name") or "").strip() form_synonym = row.get("synonym") or form.get("synonym") or identity.get("synonym") if not owner_ref and canonical_kind(str(requested_owner_kind or "")) == "CommonForm" and form_name: owner_ref = object_selector_ref("CommonForm", form_name) if owner_ref: owner_public["ref"] = owner_ref qualified_name = ".".join( part for part in [ str(owner_public.get("name") or ""), form_name if normalize(form_name) != normalize(owner_public.get("name")) else "", ] if part ) form_selector = { **({"extension": requested_extension} if requested_extension else {}), **({"ref": owner_ref} if owner_ref else {}), **({"form": form_name} if form_name else {}), **({"qualified_name": qualified_name} if qualified_name else {}), } public_matches = [] for match in row.get("matches") or []: if not isinstance(match, dict): continue section = str(match.get("section") or "") match_name = str(match.get("name") or "").strip() child_key = "command" if section == "commands" else ("attribute" if section == "attributes" else "element") writable_properties = [] for prop in match.get("writable_properties") or []: if not isinstance(prop, dict): continue writable_properties.append( { key: value for key, value in prop.items() if key in {"property", "presentation", "value", "value_type", "verification"} and value is not None } ) public_matches.append( { **{key: match.get(key) for key in ("name", "title", "type_name", "section") if match.get(key) is not None}, "selector": { **form_selector, **({child_key: match_name} if match_name else {}), }, **({"writable_properties": writable_properties} if writable_properties else {}), } ) return { **({"owner": owner_public} if owner_public else {}), "form": { **({"name": form_name} if form_name else {}), **({"synonym": form_synonym} if form_synonym is not None else {}), }, **({"qualified_name": qualified_name, "display_name": qualified_name} if qualified_name else {}), **({"selector": form_selector} if form_selector else {}), "counts": row.get("counts") or {}, "matches": public_matches, } def metadata_saved_state_forms_search(payload: dict[str, Any]) -> dict[str, Any]: method = SAVED_STATE_FORMS_SEARCH_METHOD payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1) if timeout_error: return timeout_error limit, limit_error = parse_int_argument(payload, "limit", method=method, default=50, minimum=1, maximum=1000) if limit_error: return limit_error scan_limit, scan_limit_error = parse_int_argument(payload, "scan_limit", method=method, default=1000, minimum=1, maximum=5000) if scan_limit_error: return scan_limit_error include_storage, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error include_storage = bool(include_storage) tables_arg = payload.get("tables") if tables_arg is None: tables = ["ConfigCASSave", "ConfigSave"] elif isinstance(tables_arg, list) and all(isinstance(item, str) for item in tables_arg): tables = [item for item in tables_arg if item in FORM_ELEMENT_SAVED_STATE_TABLES] else: return invalid_argument(method, "tables", "tables must be an array of saved-state table names.") if not tables: return invalid_argument(method, "tables", "Pass at least one saved-state table.", allowed_values=sorted(FORM_ELEMENT_SAVED_STATE_TABLES)) extension_filter = str(payload.get("extension") or "").strip() extension_guid: str | None = None if extension_filter: extension_guid, extension_error = extension_filter_to_guid(base_id, extension_filter, method=method) if extension_error: return extension_error tables = [table for table in tables if table == "ConfigCASSave"] if not tables: return invalid_argument(method, "tables", "Extension saved-state forms are stored in ConfigCASSave.", allowed_values=["ConfigCASSave"]) prefix = str(payload.get("prefix") or payload.get("extension_guid") or "").strip() if extension_guid and not prefix: prefix = f"{extension_guid}__" owner_kind = canonical_kind(str(first_non_empty_arg(payload, "kind", "object_type") or "")) owner_name = str(first_non_empty_arg(payload, "name", "object_name") or "").strip() owner_guid = str(first_non_empty_arg(payload, "guid", "object_guid") or "").strip().lower() explicit_form_name = str(first_non_empty_arg(payload, "form", "form_name", "name_filter") or "").strip() is_common_form_request = owner_kind == "CommonForm" form_name = explicit_form_name or (owner_name if is_common_form_request else "") form_guid = str(payload.get("form_guid") or "").strip().lower() row_payload = {**payload, **({"form": form_name} if form_name else {})} rows = [] scanned = 0 for table in tables: files_payload = { "base_id": base_id, "table": table, "limit": int(scan_limit or 1000), "diagnostic": True, "timeout_seconds": int(timeout_seconds or 60), } if prefix: files_payload["prefix"] = prefix files = storage_files_list(files_payload) if files.get("status") != "ok": continue for file_row in files.get("files") or []: file_name = str(file_row.get("FileName") or "") if not file_name or file_name.endswith("__configinfo"): continue if extension_guid and not file_name.lower().startswith(f"{extension_guid}__"): continue scanned += 1 row = saved_state_form_search_row( base_id=base_id, table=table, file_name=file_name, payload=row_payload, timeout_seconds=int(timeout_seconds or 60), ) if row: form_info = row.get("form") if isinstance(row.get("form"), dict) else {} identity = form_info.get("identity") if isinstance(form_info.get("identity"), dict) else {} candidate_form_name = str(row.get("name") or form_info.get("name") or identity.get("name") or "").strip() candidate_form_guid = str(form_info.get("guid") or identity.get("guid") or "").strip().lower() item_owner = row.get("owner") if isinstance(row.get("owner"), dict) else {} if form_name and (not candidate_form_name or normalize(candidate_form_name) != normalize(form_name)): continue if form_guid and candidate_form_guid != form_guid: continue if owner_name: if is_common_form_request: if item_owner.get("name") and normalize(item_owner.get("name")) != normalize(owner_name): continue if not item_owner.get("name") and normalize(candidate_form_name) != normalize(owner_name): continue elif normalize(item_owner.get("name")) != normalize(owner_name): continue if owner_kind and item_owner.get("kind"): if canonical_kind(str(item_owner.get("kind") or "")) != owner_kind: continue elif owner_kind and not is_common_form_request: continue if owner_guid and str(item_owner.get("guid") or "").strip().lower() != owner_guid: continue row["file"] = file_row rows.append(row) if len(rows) >= int(limit or 50): break if len(rows) >= int(limit or 50): break extension_names_by_guid: dict[str, str] = {} if not include_storage and not extension_filter and any(row.get("table") == "ConfigCASSave" for row in rows): extension_names_by_guid = { guid: str(item.get("name") or guid) for guid, item in extension_map_by_guid(base_id).items() } public_rows = rows if include_storage else [] if not include_storage: for row in rows: row_extension = extension_filter or None if not row_extension and row.get("table") == "ConfigCASSave": row_source = row.get("source") if isinstance(row.get("source"), dict) else {} row_file_name = str(row.get("file_name") or row_source.get("file_name") or "") extension_guid_from_file = row_file_name.split("__", 1)[0].strip().lower() if "__" in row_file_name else "" if extension_guid_from_file: row_extension = extension_names_by_guid.get(extension_guid_from_file) public_rows.append( public_saved_state_form_search_row( row, requested_owner_kind=owner_kind, requested_extension=row_extension, ) ) return { "schema": "onec_saved_state_form_search.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "tables": tables} if include_storage else {"kind": "saved_state"}, "query": { "owner": { "kind": owner_kind or None, "name": owner_name or None, **( {"ref": object_selector_ref(owner_kind, owner_name)} if not include_storage and object_selector_ref(owner_kind, owner_name) else {} ), **({"guid": owner_guid or None} if include_storage else {}), }, "form": form_name or None, "element": payload.get("element") or payload.get("command") or payload.get("attribute") or payload.get("element_name"), "query": payload.get("query") or payload.get("text"), "extension": extension_filter or None, "limit": int(limit or 50), "scan_limit": int(scan_limit or 1000), "include_storage": include_storage, **({"form_guid": form_guid or None, "prefix": prefix or None} if include_storage else {}), }, "forms": public_rows, "counts": { "forms": len(public_rows), **({"scanned": scanned} if include_storage else {}), "limit": int(limit or 50), }, "diagnostics": { "note": "Storage identities and low-level form write coordinates are hidden unless include_storage=true." }, } def saved_state_module_file_identity(file_name: str) -> dict[str, Any]: stem = re.sub(r"\.(?:0|1|2|3)$", "", file_name) if "__" not in stem: return {} owner_guid, module_guid = stem.split("__", 1) return { "owner_guid": owner_guid or None, "module_guid": module_guid or None, } def saved_state_module_suffix(file_name: str) -> str | None: match = re.search(r"\.(\d+)$", str(file_name or "")) return match.group(1) if match else None def saved_state_bsl_module_role(file_name: str, *, owner_kind: str | None = None) -> dict[str, Any]: suffix = saved_state_module_suffix(file_name) if suffix: role = public_module_role(owner_kind=owner_kind, suffix=suffix) if role.get("kind") != "object_module" or suffix == "0": return role if not suffix: return {"kind": "bsl_module", "name": "Модуль БСЛ"} return public_module_role(owner_kind=owner_kind, suffix=suffix) def saved_state_descriptor_identity_from_bytes(data: bytes, descriptor_file_name: str) -> dict[str, Any] | None: identity = config_identity_from_bytes(data) if not identity: try: from parser.payload import payload_to_text descriptor_text = str((payload_to_text(data) or {}).get("text") or "") except Exception: descriptor_text = "" name_match = re.search(r'"([A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]*)"', descriptor_text) if not name_match: return None descriptor_guid = descriptor_file_name.split("__", 1)[1] if "__" in descriptor_file_name else descriptor_file_name return { "name": repair_bsl_mojibake_text(name_match.group(1)), "synonym": None, "guid": descriptor_guid, "descriptor_file_name": descriptor_file_name, "source": "saved_state_descriptor_text", } synonyms = identity.get("synonyms") if isinstance(identity.get("synonyms"), dict) else {} synonym = next(iter(synonyms.values()), None) if synonyms else None return { "name": identity.get("name"), "synonym": synonym, "guid": identity.get("guid"), "descriptor_file_name": descriptor_file_name, "source": "saved_state_descriptor", **({"name_variants": identity.get("name_variants")} if identity.get("name_variants") else {}), **({"synonym_variants": identity.get("synonym_variants")} if identity.get("synonym_variants") else {}), } def saved_state_descriptor_identity( *, base_id: str, table: str, file_name: str, timeout_seconds: int, ) -> dict[str, Any] | None: descriptor_file_name = re.sub(r"\.(?:0|1|2|3)$", "", str(file_name or "")) if not descriptor_file_name or descriptor_file_name == file_name: return None try: data, _config, read_error = read_storage_file_bytes(base_id, table, descriptor_file_name, timeout_seconds=timeout_seconds) except Exception: return None if read_error or data is None: return None identity = saved_state_descriptor_identity_from_bytes(data, descriptor_file_name) if identity and identity.get("guid") and not identity.get("kind"): try: cached = metadata_cache_lookup_guid(base_id, str(identity.get("guid") or "")) except Exception: cached = None if isinstance(cached, dict) and cached.get("kind"): identity["kind"] = cached.get("kind") identity["kind_source"] = "metadata_cache" return identity def saved_state_related_descriptor_identity( *, base_id: str, table: str, file_name: str, timeout_seconds: int, scan_limit: int = 1000, ) -> dict[str, Any] | None: identity = saved_state_module_file_identity(file_name) owner_guid = str(identity.get("owner_guid") or "").strip() module_guid = str(identity.get("module_guid") or "").strip() if not owner_guid or not module_guid: return None direct_descriptor_file_name = re.sub(r"\.(?:0|1|2|3)$", "", str(file_name or "")) files = storage_files_list( { "base_id": base_id, "table": table, "prefix": f"{owner_guid}__", "limit": int(scan_limit or 1000), "diagnostic": True, "timeout_seconds": timeout_seconds, } ) if files.get("status") != "ok": return None for file_row in files.get("files") or []: descriptor_file_name = str(file_row.get("FileName") or "") if not descriptor_file_name or descriptor_file_name.endswith("__configinfo") or "." in descriptor_file_name: continue if descriptor_file_name == direct_descriptor_file_name: continue try: data, _config, read_error = read_storage_file_bytes(base_id, table, descriptor_file_name, timeout_seconds=timeout_seconds) except Exception: continue if read_error or data is None: continue decoded = payload_text_from_bytes(data) descriptor_text = str(decoded.get("text") or "") if module_guid.casefold() not in descriptor_text.casefold(): continue related = saved_state_descriptor_identity_from_bytes(data, descriptor_file_name) if related and related.get("name"): if related.get("guid") and not related.get("kind"): try: cached = metadata_cache_lookup_guid(base_id, str(related.get("guid") or "")) except Exception: cached = None if isinstance(cached, dict) and cached.get("kind"): related["kind"] = cached.get("kind") related["kind_source"] = "metadata_cache" related["source"] = "saved_state_related_descriptor" related["related_module_guid"] = module_guid return related return None def saved_state_module_owner_identity( *, base_id: str, table: str, file_name: str, timeout_seconds: int, ) -> dict[str, Any] | None: return saved_state_descriptor_identity( base_id=base_id, table=table, file_name=file_name, timeout_seconds=timeout_seconds, ) or saved_state_related_descriptor_identity( base_id=base_id, table=table, file_name=file_name, timeout_seconds=timeout_seconds, ) def saved_state_public_module_context( *, base_id: str, table: str, file_name: str, object_kind: str | None = None, timeout_seconds: int, prefer_form_module: bool = False, ) -> dict[str, Any]: if table not in FORM_ELEMENT_SAVED_STATE_TABLES: return {} module_role = saved_state_bsl_module_role(file_name, owner_kind=object_kind) if module_role.get("kind") != "bsl_module" and not prefer_form_module: owner_identity = saved_state_module_owner_identity( base_id=base_id, table=table, file_name=file_name, timeout_seconds=timeout_seconds, ) effective_owner_kind = object_kind or (owner_identity or {}).get("kind") module_role = saved_state_bsl_module_role(file_name, owner_kind=effective_owner_kind) owner_payload = ( { "status": "resolved", "kind": effective_owner_kind or "Catalog", "name": owner_identity.get("name"), "synonym": owner_identity.get("synonym"), "guid": owner_identity.get("guid"), "source": "saved_state_descriptor", } if owner_identity and owner_identity.get("name") else None ) qualified_name = public_code_qualified_name(owner=owner_payload, module=module_role) return { "module": module_role, **({"owner": owner_payload} if owner_payload else {}), **({"qualified_name": qualified_name, "display_name": qualified_name} if qualified_name else {}), } form_identity = saved_state_descriptor_identity( base_id=base_id, table=table, file_name=file_name, timeout_seconds=timeout_seconds, ) owner_identity = saved_state_related_descriptor_identity( base_id=base_id, table=table, file_name=file_name, timeout_seconds=timeout_seconds, ) form_payload = ( { "name": form_identity.get("name"), "synonym": form_identity.get("synonym"), "guid": form_identity.get("guid"), "source": "saved_state_descriptor", } if form_identity and form_identity.get("name") else None ) owner_payload = ( { "status": "resolved", "kind": object_kind or (owner_identity or {}).get("kind") or "Catalog", "name": owner_identity.get("name"), "synonym": owner_identity.get("synonym"), "guid": owner_identity.get("guid"), "source": "saved_state_descriptor", } if owner_identity and owner_identity.get("name") else None ) module_role = {"kind": "form_module", "name": "Модуль формы"} qualified_name = public_code_qualified_name(owner=owner_payload, form=form_payload, module=module_role) return { "module": module_role, **({"form": form_payload} if form_payload else {}), **({"owner": owner_payload} if owner_payload else {}), **({"qualified_name": qualified_name, "display_name": qualified_name} if qualified_name else {}), } def saved_state_form_embedded_module_search_row( *, base_id: str, table: str, file_name: str, file_row: dict[str, Any], payload: dict[str, Any], data: bytes, ) -> dict[str, Any] | None: query = str(payload.get("query") or payload.get("text") or "").strip() module_path = str(payload.get("module_path") or "2") try: from parser.payload import decode_payload_lossless, get_tree_path, parse_brace_text, patch_brace_text_path, scalar except Exception: return None try: decoded = decode_payload_lossless(data) tree = parse_brace_text(str(decoded.get("text") or "")) text = scalar(get_tree_path(tree, module_path)) except Exception: return None if not text: return None text = form_embedded_module_public_text(str(text or "")) if not text: return None if query and query.casefold() not in text.casefold() and query.casefold() not in file_name.casefold(): return None payload_sha1 = hashlib.sha1(data).hexdigest() identity = saved_state_module_file_identity(file_name) object_kind = canonical_kind(str(payload.get("object_type") or payload.get("kind") or "")) object_name = str(payload.get("object_name") or payload.get("name") or "").strip() if object_kind == "CommonForm" and object_name: form_identity = {"name": object_name, "synonym": None, "guid": identity.get("module_guid"), "source": "selector"} owner_identity = None else: form_identity = saved_state_descriptor_identity( base_id=base_id, table=table, file_name=file_name, timeout_seconds=int(payload.get("timeout_seconds") or 60), ) owner_identity = saved_state_related_descriptor_identity( base_id=base_id, table=table, file_name=file_name, timeout_seconds=int(payload.get("timeout_seconds") or 60), ) preview = text[: int(payload.get("preview_chars") or 500)] module_ref = f"{table}:{file_name}" owner_payload = ( { "status": "resolved", "kind": object_kind or owner_identity.get("kind") or "Catalog", "name": owner_identity.get("name"), "synonym": owner_identity.get("synonym"), "guid": owner_identity.get("guid"), "source": "saved_state_descriptor", } if owner_identity and owner_identity.get("name") else None ) form_payload = ( { "name": form_identity.get("name"), "synonym": form_identity.get("synonym"), "guid": form_identity.get("guid"), "source": "saved_state_descriptor", } if form_identity and form_identity.get("name") else None ) module_role = {"kind": "form_module", "name": "Модуль формы"} qualified_name = public_code_qualified_name(owner=owner_payload, form=form_payload, module=module_role) return { "table": table, "file_name": file_name, "identity": identity, **({"owner": owner_payload} if owner_payload else {}), **({"form": form_payload} if form_payload else {}), "module": module_role, **({"qualified_name": qualified_name, "display_name": qualified_name} if qualified_name else {}), "source": { "kind": "live_sql", "table": table, "file_name": file_name, }, "payload": { "sha1": payload_sha1, "bytes": len(data), "compression": decoded.get("compression"), "role": "form_embedded_module_payload", }, "file": file_row, "streams": [ { "module_ref": module_ref, "module_path": module_path, "encoding": decoded.get("encoding"), "text_sha1": hashlib.sha1(text.encode("utf-8")).hexdigest(), "text_bytes": len(text.encode("utf-8")), "preview": preview, **({"owner": owner_payload} if owner_payload else {}), **({"form": form_payload} if form_payload else {}), "module": module_role, **({"qualified_name": qualified_name, "display_name": qualified_name} if qualified_name else {}), "write_plan_target": { "kind": "module", "module_ref": module_ref, "file_name": file_name, "module_path": module_path, "expected_sha1": payload_sha1, **({"object_guid": identity.get("owner_guid")} if identity.get("owner_guid") else {}), **({"form_guid": identity.get("module_guid")} if identity.get("module_guid") else {}), }, "match": { "query": query or None, "in_text": bool(query and query.casefold() in text.casefold()), "in_file_name": bool(query and query.casefold() in file_name.casefold()), }, } ], "counts": {"streams": 1}, } def saved_state_module_search_row( *, base_id: str, table: str, file_name: str, file_row: dict[str, Any], payload: dict[str, Any], timeout_seconds: int, ) -> dict[str, Any] | None: query = str(payload.get("query") or payload.get("text") or "").strip() stream_index_filter = payload.get("stream_index") data, _config, read_error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=timeout_seconds) if read_error or data is None: return None try: from parser.cas_payload import classify_payload except Exception: return None classified = classify_payload(data, include_text=True) classified_streams = classified.get("stream_blocks") or [] if classified.get("role") != "bsl_module_payload" and not classified_streams: return saved_state_form_embedded_module_search_row( base_id=base_id, table=table, file_name=file_name, file_row=file_row, payload=payload, data=data, ) streams = [] payload_sha1 = hashlib.sha1(data).hexdigest() descriptor_identity = None if canonical_kind(str(payload.get("object_type") or payload.get("kind") or "")) != "CommonForm": descriptor_identity = saved_state_module_owner_identity( base_id=base_id, table=table, file_name=file_name, timeout_seconds=timeout_seconds, ) effective_owner_kind = canonical_kind(str(payload.get("object_type") or payload.get("kind") or "")) or ( descriptor_identity.get("kind") if isinstance(descriptor_identity, dict) else None ) module_role = saved_state_bsl_module_role( file_name, owner_kind=effective_owner_kind, ) owner_payload = ( { "status": "resolved", "kind": effective_owner_kind or "Catalog", "name": descriptor_identity.get("name"), "synonym": descriptor_identity.get("synonym"), "guid": descriptor_identity.get("guid"), "source": "saved_state_descriptor", } if descriptor_identity and descriptor_identity.get("name") else None ) qualified_name = public_code_qualified_name(owner=owner_payload, module=module_role) for index, stream in enumerate(classified_streams): if stream_index_filter is not None: try: wanted_index = int(stream_index_filter) except (TypeError, ValueError): wanted_index = -1 if index != wanted_index: continue raw_text = str(stream.get("text") or "") text = repair_bsl_mojibake_text(raw_text) has_bsl_marker = bool(stream.get("has_bsl_marker")) or is_bsl_like_text(text) if not has_bsl_marker and not query: continue if query and query.casefold() not in text.casefold() and query.casefold() not in file_name.casefold(): continue preview = text[: int(payload.get("preview_chars") or 500)] if text else "" module_ref = f"{table}:{file_name}#stream:{index}" identity = saved_state_module_file_identity(file_name) streams.append( { "stream_index": index, "module_ref": module_ref, "has_bsl_marker": has_bsl_marker, "encoding": stream.get("encoding"), **({"encoding_repaired": True} if text != raw_text else {}), "text_sha1": hashlib.sha1(text.encode("utf-8")).hexdigest() if text != raw_text else stream.get("sha1"), "text_bytes": len(text.encode("utf-8")) if text != raw_text else stream.get("bytes"), "preview": preview, "module": module_role, **({"owner": owner_payload} if owner_payload else {}), **({"qualified_name": qualified_name, "display_name": qualified_name} if qualified_name else {}), "write_plan_target": { "kind": "module", "module_ref": module_ref, "file_name": file_name, "stream_index": index, "expected_sha1": payload_sha1, **({"object_guid": identity.get("owner_guid")} if identity.get("owner_guid") else {}), **({"module_guid": identity.get("module_guid")} if identity.get("module_guid") else {}), }, "match": { "query": query or None, "in_text": bool(query and query.casefold() in text.casefold()), "in_file_name": bool(query and query.casefold() in file_name.casefold()), }, } ) if not streams: return saved_state_form_embedded_module_search_row( base_id=base_id, table=table, file_name=file_name, file_row=file_row, payload=payload, data=data, ) identity = saved_state_module_file_identity(file_name) return { "table": table, "file_name": file_name, "identity": identity, **({"owner": owner_payload} if owner_payload else {}), "module": module_role, **({"qualified_name": qualified_name, "display_name": qualified_name} if qualified_name else {}), "source": { "kind": "live_sql", "table": table, "file_name": file_name, }, "payload": { "sha1": payload_sha1, "bytes": len(data), "compression": classified.get("compression"), "role": "bsl_module_payload" if streams else classified.get("role"), }, "file": file_row, "streams": streams, "counts": {"streams": len(streams)}, } def metadata_saved_state_modules_owner_guid_from_selector( base_id: str, payload: dict[str, Any], *, timeout_seconds: int, ) -> tuple[str | None, dict[str, Any] | None]: explicit = str(payload.get("owner_guid") or payload.get("prefix") or "").strip() if explicit: return explicit, {"status": "provided", "owner_guid": explicit} object_guid = str(payload.get("object_guid") or payload.get("guid") or "").strip() if object_guid: return object_guid, {"status": "provided", "owner_guid": object_guid, "selector": "object_guid"} object_kind = canonical_kind(str(payload.get("object_type") or payload.get("kind") or "")) object_name = str(payload.get("object_name") or payload.get("name") or "").strip() if not object_kind or not object_name: return None, None cached = metadata_cache_lookup_row(base_id, object_kind, object_name) if cached and cached.get("guid"): return str(cached.get("guid")), { "status": "resolved", "method": "metadata_cache_lookup", "owner_guid": str(cached.get("guid")), "object": metadata_cache_public_row(cached), } result = list_objects( object_kind, base_id=base_id, limit=5, offset=0, include_storage=False, exact_counts=False, table="Config", name_filter=object_name, ) objects = [ item for item in result.get("objects") or [] if isinstance(item, dict) and canonical_kind(str(item.get("kind") or "")) == object_kind and normalize(str(item.get("name") or "")) == normalize(object_name) and item.get("guid") ] if len(objects) == 1: return str(objects[0].get("guid")), { "status": "resolved", "method": "metadata.objects.list", "owner_guid": str(objects[0].get("guid")), "object": objects[0], } if len(objects) > 1: return None, { "status": "ambiguous", "method": "metadata.objects.list", "selector": {"object_type": object_kind, "object_name": object_name}, "candidates": objects[:5], } return None, { "status": "not_found", "method": "metadata.objects.list", "selector": {"object_type": object_kind, "object_name": object_name}, "diagnostics": { "message": "Object name was not resolved to a GUID; saved-state module search will not narrow by owner." }, } def public_saved_state_modules_search_row(row: dict[str, Any]) -> dict[str, Any] | None: owner = row.get("owner") if isinstance(row.get("owner"), dict) else {} form = row.get("form") if isinstance(row.get("form"), dict) else {} module = row.get("module") if isinstance(row.get("module"), dict) else {} owner_public = { key: value for key, value in owner.items() if key in {"status", "kind", "name", "synonym", "source"} and value is not None } owner_ref = object_selector_ref(owner_public.get("kind"), owner_public.get("name")) if owner_ref: owner_public["ref"] = owner_ref form_public = { key: value for key, value in form.items() if key in {"name", "synonym", "source"} and value is not None } module_public = { key: value for key, value in module.items() if key in {"kind", "name"} and value is not None } qualified_name = str(row.get("qualified_name") or row.get("display_name") or "").strip() selector = { **({"ref": owner_ref} if owner_ref else {}), **({"form": form_public.get("name")} if form_public.get("name") else {}), **({"module": module_public.get("name")} if module_public.get("name") else {}), **({"qualified_name": qualified_name} if qualified_name else {}), } public_streams = [] for stream_ordinal, stream in enumerate(row.get("streams") or [], start=1): if not isinstance(stream, dict): continue match = stream.get("match") if isinstance(stream.get("match"), dict) else {} query = str(match.get("query") or "").strip() in_text = bool(match.get("in_text")) in_name = bool(query and qualified_name and normalize(query) in normalize(qualified_name)) # A public search must not return a hit that matched only an opaque SQL # file name. Name matching is against the 1C qualified name instead. if query and not in_text and not in_name: continue stream_owner = stream.get("owner") if isinstance(stream.get("owner"), dict) else owner stream_form = stream.get("form") if isinstance(stream.get("form"), dict) else form stream_module = stream.get("module") if isinstance(stream.get("module"), dict) else module stream_owner_ref = object_selector_ref(stream_owner.get("kind"), stream_owner.get("name")) stream_qualified_name = str(stream.get("qualified_name") or stream.get("display_name") or qualified_name).strip() write_plan_target = { "kind": "module", **({"ref": stream_owner_ref} if stream_owner_ref else {}), **({"form": stream_form.get("name")} if stream_form.get("name") else {}), **({"module": stream_module.get("name")} if stream_module.get("name") else {}), **({"qualified_name": stream_qualified_name} if stream_qualified_name else {}), "stream_ordinal": stream_ordinal, } public_stream = { "preview": stream.get("preview"), **({"encoding_repaired": True} if stream.get("encoding_repaired") else {}), **( { "owner": { key: value for key, value in stream_owner.items() if key in {"status", "kind", "name", "synonym", "source"} and value is not None } } if stream_owner else {} ), **( { "form": { key: value for key, value in stream_form.items() if key in {"name", "synonym", "source"} and value is not None } } if stream_form else {} ), **( { "module": { key: value for key, value in stream_module.items() if key in {"kind", "name"} and value is not None } } if stream_module else {} ), **({"qualified_name": stream_qualified_name, "display_name": stream_qualified_name} if stream_qualified_name else {}), **({"selector": selector} if selector else {}), "write_plan_target": write_plan_target, "match": {"query": query or None, "in_text": in_text, "in_name": in_name}, } public_streams.append(public_stream) if not public_streams: return None return { **({"owner": owner_public} if owner_public else {}), **({"form": form_public} if form_public else {}), **({"module": module_public} if module_public else {}), **({"qualified_name": qualified_name, "display_name": qualified_name} if qualified_name else {}), **({"selector": selector} if selector else {}), "streams": public_streams, "counts": {"streams": len(public_streams)}, } def metadata_saved_state_modules_search(payload: dict[str, Any]) -> dict[str, Any]: method = SAVED_STATE_MODULES_SEARCH_METHOD payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload payload = dict(payload) if "query" in payload and not str(payload.get("query") or "").strip(): payload["query"] = None if "text" in payload and not str(payload.get("text") or "").strip(): payload["text"] = None base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1) if timeout_error: return timeout_error limit, limit_error = parse_int_argument(payload, "limit", method=method, default=50, minimum=1, maximum=1000) if limit_error: return limit_error scan_limit, scan_limit_error = parse_int_argument(payload, "scan_limit", method=method, default=1000, minimum=1, maximum=5000) if scan_limit_error: return scan_limit_error include_storage, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error include_storage = bool(include_storage) layer = str(payload.get("layer") or "").strip() layer_tables = { "base_saved_state": ["ConfigSave"], "extension_saved_state": ["ConfigCASSave"], } if layer and layer not in layer_tables: return invalid_argument( method, "layer", "Unsupported saved-state layer.", allowed_values=sorted(layer_tables), ) tables_arg = payload.get("tables") if layer and tables_arg is not None: return invalid_argument(method, "tables", "Pass layer or tables, not both.") if layer: tables = layer_tables[layer] elif tables_arg is None: tables = ["ConfigCASSave", "ConfigSave"] elif isinstance(tables_arg, list) and all(isinstance(item, str) for item in tables_arg): tables = [item for item in tables_arg if item in FORM_ELEMENT_SAVED_STATE_TABLES] else: return invalid_argument(method, "tables", "tables must be an array of saved-state table names.") if not tables: return invalid_argument(method, "tables", "Pass at least one saved-state table.", allowed_values=sorted(FORM_ELEMENT_SAVED_STATE_TABLES)) extension_filter = str(payload.get("extension") or "").strip() extension_guid: str | None = None if extension_filter: extension_guid, extension_error = extension_filter_to_guid(base_id, extension_filter, method=method) if extension_error: return extension_error tables = [table for table in tables if table == "ConfigCASSave"] if not tables: return invalid_argument(method, "tables", "Extension saved-state modules are stored in ConfigCASSave.", allowed_values=["ConfigCASSave"]) prefix, owner_resolution = metadata_saved_state_modules_owner_guid_from_selector( base_id, payload, timeout_seconds=int(timeout_seconds or 60), ) prefix = str(prefix or "").strip() if extension_guid and not prefix: prefix = f"{extension_guid}__" file_name_filter = str(payload.get("file_name") or "").strip() file_name_candidates: set[str] = set() object_kind = canonical_kind(str(payload.get("object_type") or payload.get("kind") or "")) object_name = str(payload.get("object_name") or payload.get("name") or "").strip() if not file_name_filter and object_kind == "CommonForm" and object_name and not prefix: forms_search = metadata_saved_state_forms_search( { **payload, "base_id": base_id, "tables": tables, "form": object_name, "name_filter": object_name, "query": None, "text": None, "limit": int(payload.get("form_search_limit") or 20), "scan_limit": int(scan_limit or 1000), "timeout_seconds": int(timeout_seconds or 60), "include_storage": True, } ) form_rows = [row for row in forms_search.get("forms") or [] if isinstance(row, dict)] exact_form_rows = [ row for row in form_rows if normalize(str(row.get("name") or ((row.get("form") or {}).get("name") if isinstance(row.get("form"), dict) else "") or "")) == normalize(object_name) ] for form_row in exact_form_rows or form_rows: if not isinstance(form_row, dict): continue candidate = str(form_row.get("file_name") or ((form_row.get("source") or {}).get("file_name") if isinstance(form_row.get("source"), dict) else "") or "") if candidate: file_name_candidates.add(candidate) if file_name_candidates: owner_resolution = { "status": "resolved", "method": "metadata.saved_state.forms.search", "selector": {"object_type": object_kind, "object_name": object_name}, "file_names": sorted(file_name_candidates), "counts": {"forms": len(file_name_candidates)}, } modules = [] scanned = 0 for table in tables: files_payload = { "base_id": base_id, "table": table, "limit": int(scan_limit or 1000), "diagnostic": True, "timeout_seconds": int(timeout_seconds or 60), } if prefix: files_payload["prefix"] = prefix files = storage_files_list(files_payload) if files.get("status") != "ok": continue for file_row in files.get("files") or []: file_name = str(file_row.get("FileName") or "") if not file_name or file_name.endswith("__configinfo"): continue if extension_guid and not file_name.lower().startswith(f"{extension_guid}__"): continue if file_name_filter and file_name != file_name_filter: continue if file_name_candidates and file_name not in file_name_candidates: continue if "." not in file_name: continue scanned += 1 row = saved_state_module_search_row( base_id=base_id, table=table, file_name=file_name, file_row=file_row, payload=payload, timeout_seconds=int(timeout_seconds or 60), ) if row: modules.append(row) if len(modules) >= int(limit or 50): break if len(modules) >= int(limit or 50): break public_modules = modules if include_storage else [ public_row for row in modules if (public_row := public_saved_state_modules_search_row(row)) is not None ] result = { "schema": "onec_saved_state_module_search.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "tables": tables} if include_storage else {"kind": "saved_state"}, "query": { "query": payload.get("query") or payload.get("text"), "object_type": payload.get("object_type") or payload.get("kind"), "object_name": payload.get("object_name") or payload.get("name"), "extension": extension_filter or None, "layer": layer or None, "limit": int(limit or 50), "scan_limit": int(scan_limit or 1000), "include_storage": include_storage, **({"prefix": prefix or None, "file_name": file_name_filter or None, "stream_index": payload.get("stream_index")} if include_storage else {}), }, "modules": public_modules, "counts": { "modules": len(public_modules), **({"scanned": scanned} if include_storage else {}), "limit": int(limit or 50), }, "diagnostics": { "note": "Saved-state SQL coordinates and write handles are hidden unless include_storage=true." }, } if owner_resolution: if include_storage: result["owner_resolution"] = owner_resolution else: resolved_object = owner_resolution.get("object") if isinstance(owner_resolution.get("object"), dict) else {} public_resolution = { key: value for key, value in owner_resolution.items() if key in {"status", "method"} and value is not None } if resolved_object: public_object = public_metadata_row( { key: value for key, value in resolved_object.items() if key in {"kind", "kind_ru", "public_kind", "name", "synonym", "source"} and value is not None } ) public_resolution["object"] = public_object result["selector_resolution"] = public_resolution return result def metadata_form_write_target_resolve(payload: dict[str, Any]) -> dict[str, Any]: method = FORM_WRITE_TARGET_RESOLVE_METHOD payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1) if timeout_error: return timeout_error table = str(payload.get("table") or "ConfigCASSave") if table not in FORM_ELEMENT_SAVED_STATE_TABLES: return invalid_argument(method, "table", "Only saved-state tables may be resolved for writes.", allowed_values=sorted(FORM_ELEMENT_SAVED_STATE_TABLES)) owner_kind = canonical_kind(str(first_non_empty_arg(payload, "kind", "object_type") or "")) owner_name = str(first_non_empty_arg(payload, "name", "object_name") or "").strip() owner_guid = str(first_non_empty_arg(payload, "guid", "object_guid") or "").strip().lower() explicit_form_name = str(first_non_empty_arg(payload, "form", "form_name", "name_filter") or "").strip() is_common_form_request = owner_kind == "CommonForm" or ( owner_kind in {None, "", "Form"} and not explicit_form_name and bool(owner_name) ) legacy_guid_is_form = bool( payload.get("guid") and not payload.get("object_guid") and not owner_kind and not owner_name and not explicit_form_name ) form_name = explicit_form_name or (owner_name if is_common_form_request else "") if owner_name and not is_common_form_request and not form_name: return invalid_argument( method, "form_name", "Pass form or form_name when resolving a target in a nested owner form.", ) form_guid = str( first_non_empty_arg( payload, "form_guid", *(("guid", "object_guid") if is_common_form_request else ("guid",) if legacy_guid_is_form else ()), ) or "" ).strip().lower() decode_payload = { **without_form_decode_selector_keys(payload), "base_id": base_id, "table": table, "include_storage": True, "include_parameters": True, "max_items": int(payload.get("max_items") or 5000), "timeout_seconds": int(timeout_seconds or 60), } if not is_common_form_request: for owner_selector_key in ("ref", "kind", "name", "guid", "object_type", "object_name", "object_guid"): decode_payload.pop(owner_selector_key, None) if form_guid: decode_payload["form_guid"] = form_guid decode_payload.pop("guid", None) if not (payload.get("file_name") or form_guid): search = metadata_saved_state_forms_search( { **payload, "base_id": base_id, "tables": [table], **({"form": form_name} if form_name else {}), "limit": int(payload.get("search_limit") or 10), "scan_limit": int(payload.get("scan_limit") or 1000), "timeout_seconds": int(timeout_seconds or 60), "include_storage": True, } ) forms = search.get("forms") or [] exact_forms = [] for form_row in forms: if not isinstance(form_row, dict): continue form_info = form_row.get("form") if isinstance(form_row.get("form"), dict) else {} candidate_name = str(form_row.get("name") or form_info.get("name") or "").strip() if form_name and (not candidate_name or normalize(candidate_name) != normalize(form_name)): continue item_owner = form_row.get("owner") if isinstance(form_row.get("owner"), dict) else {} if owner_name and not is_common_form_request: if normalize(item_owner.get("name")) != normalize(owner_name): continue if owner_kind and canonical_kind(str(item_owner.get("kind") or "")) != owner_kind: continue if owner_guid and str(item_owner.get("guid") or "").strip().lower() != owner_guid: continue exact_forms.append(form_row) forms = exact_forms if len(forms) == 1: form_row = forms[0] if isinstance(forms[0], dict) else {} form_source = form_row.get("source") if isinstance(form_row.get("source"), dict) else {} form_info = form_row.get("form") if isinstance(form_row.get("form"), dict) else {} form_file = form_row.get("file") if isinstance(form_row.get("file"), dict) else {} decode_payload["file_name"] = form_row.get("file_name") or form_source.get("file_name") or form_info.get("file_name") or form_file.get("FileName") decode_payload["_resolved_by_search"] = search elif not forms: return { "schema": "onec_form_write_target_resolution.v1", "status": "not_found", "base_id": base_id, "query": { "owner": {"kind": owner_kind or None, "name": owner_name or None, "guid": owner_guid or None}, "form": form_name or None, **{key: payload.get(key) for key in ("table", "element", "command", "property", "query") if payload.get(key) is not None}, }, "diagnostics": {"message": "Saved-state form was not found. Pass file_name/form_guid or broaden search_limit/scan_limit."}, "search": search, } else: return { "schema": "onec_form_write_target_resolution.v1", "status": "ambiguous", "base_id": base_id, "query": { "owner": {"kind": owner_kind or None, "name": owner_name or None, "guid": owner_guid or None}, "form": form_name or None, **{key: payload.get(key) for key in ("table", "element", "command", "property", "query") if payload.get(key) is not None}, }, "candidates": forms[:10], "counts": {"forms": len(forms)}, "diagnostics": {"message": "More than one saved-state form matched the owner/form selector. Pass form_guid or file_name."}, } decoded = metadata_form_decode(decode_payload) if decoded.get("status") != "ok": result = dict(decoded) result["method"] = method return result profile = decoded.get("profile") if isinstance(decoded.get("profile"), dict) else {} selector = form_write_selector_from_payload(payload) targets = filter_form_profile_write_targets(form_profile_write_targets(profile), selector) if len(targets) != 1: return { "schema": "onec_form_write_target_resolution.v1", "status": "not_found" if not targets else "ambiguous", "base_id": base_id, "source": decoded.get("source"), "form": decoded.get("form"), "query": public_non_empty_query_fields(selector), "candidates": form_write_target_candidates(profile, selector, limit=20), "counts": {"matches": len(targets)}, "diagnostics": {"message": "Resolve must match exactly one form element/command/attribute."}, } requested_property = payload.get("property") requested_target = targets[0] target = requested_target effective_source = None if requested_property: target, effective_source = form_effective_write_target(profile, requested_target, requested_property, payload) writable_properties = form_write_target_writable_properties(target) property_resolution = None if requested_property: if isinstance(effective_source, dict) and effective_source.get("writable") is False: property_resolution = { "status": "source_not_routed", "property": requested_property, "source_kind": effective_source.get("kind"), "path_to_data": effective_source.get("path_to_data"), "requires": effective_source.get("requires"), } else: path_edit, error = form_element_write_edit(target, {"property": requested_property, "value": payload.get("value") if "value" in payload else ""}, 0) if error: property_resolution = {"status": "not_writable", "property": requested_property, "error": error} else: property_resolution = {key: path_edit.get(key) for key in ("property", "path", "old") if key in path_edit} property_resolution["status"] = "ok" if "value" in payload: property_resolution["new"] = form_element_write_scalar(payload.get("value")) source = decoded.get("source") if isinstance(decoded.get("source"), dict) else {} display = None write_target = None alternatives: list[dict[str, Any]] = [] if requested_property: display = { "property": requested_property, "actual": form_property_current_value(requested_target, requested_property), "source_kind": (effective_source or {}).get("kind") if isinstance(effective_source, dict) else "local", "requested_target": form_write_target_public(requested_target), } if normalize_form_property_name(requested_property) == "title": display["actual"] = requested_target.get("title") if display["actual"] in {None, ""} and isinstance(effective_source, dict) and effective_source.get("kind") == "linked_command_title": display["actual"] = target.get("title") if display["actual"] in {None, ""} and isinstance(effective_source, dict) and effective_source.get("kind") in {"data_path_form_attribute_title", "data_path_form_attribute_field_title"}: display["actual"] = target.get("title") write_target = { "section": (target.get("_profile_section") or target.get("section")), "name": target.get("name"), "path": property_resolution.get("path") if isinstance(property_resolution, dict) else None, "old": property_resolution.get("old") if isinstance(property_resolution, dict) else None, "status": property_resolution.get("status") if isinstance(property_resolution, dict) else None, } if isinstance(effective_source, dict) and effective_source.get("kind") in {"linked_command_title", "data_path_title", "data_path_form_attribute_title", "data_path_form_attribute_field_title"}: alternatives.append( { "kind": "local_override_title", "target": form_write_target_public(requested_target), "condition": "Pass source=local_override to write the element title itself.", } ) return { "schema": "onec_form_write_target_resolution.v1", "status": "ok", "base_id": base_id, "source": source, "form": decoded.get("form"), "target": form_write_target_public(requested_target), "effective_target": form_write_target_public(target), "effective_source": effective_source, "display": display, "write_target": write_target, "alternatives": alternatives, "writable_properties": writable_properties, "property": property_resolution, "semantic_diff": ( { "presentation": f"{target.get('name') or target.get('title')}.{requested_property}", "old": property_resolution.get("old"), "new": property_resolution.get("new"), } if isinstance(property_resolution, dict) and property_resolution.get("status") == "ok" and "new" in property_resolution else None ), "counts": {"matches": 1, "writable_properties": len(writable_properties)}, **({"search": decode_payload.get("_resolved_by_search")} if decode_payload.get("_resolved_by_search") else {}), } def metadata_form_write_target_verify(payload: dict[str, Any]) -> dict[str, Any]: method = FORM_WRITE_TARGET_VERIFY_METHOD payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error include_storage, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error table = str(payload.get("table") or "ConfigCASSave") if table not in FORM_ELEMENT_SAVED_STATE_TABLES: return invalid_argument(method, "table", "Only saved-state tables may be verified for writes.", allowed_values=sorted(FORM_ELEMENT_SAVED_STATE_TABLES)) resolve_payload = {**payload, "base_id": base_id, "table": table, "include_storage": True} resolved = metadata_form_write_target_resolve(resolve_payload) selected_form = None search = resolved.get("search") if isinstance(resolved.get("search"), dict) else None if isinstance(search, dict): forms = search.get("forms") if isinstance(search.get("forms"), list) else [] if len(forms) == 1 and isinstance(forms[0], dict): selected_form = forms[0] if selected_form is None and isinstance(resolved.get("form"), dict): selected_form = {"name": resolved["form"].get("name") or payload.get("form") or payload.get("form_name") or payload.get("object_name"), "form": resolved.get("form")} query = {key: payload.get(key) for key in ("extension", "kind", "object_type", "name", "object_name", "form", "element", "command", "attribute", "property", "query") if payload.get(key) not in {None, ""}} if resolved.get("status") == "ok": source = resolved.get("source") if isinstance(resolved.get("source"), dict) else {} write_target = resolved.get("write_target") if isinstance(resolved.get("write_target"), dict) else None if isinstance(write_target, dict) and write_target.get("path"): write_target = {**write_target, "form_path": write_target.get("path")} result = { "schema": "onec_form_write_target_verify.v1", "method": method, "status": "ok", "verified": True, "writable_now": True, "needs_prepare": False, "base_id": base_id, "source": ( source if include_storage else {"kind": "saved_state", "layer": SAVED_STATE_LAYER_BY_TABLE[source.get("table") or table]} ), "query": query, "form": resolved.get("form"), "target": resolved.get("target"), "effective_target": resolved.get("effective_target"), "display": resolved.get("display"), "write_target": write_target, "property": resolved.get("property"), "counts": resolved.get("counts"), } if search: result["saved_state_search"] = compact_saved_state_form_search_result(search, selected_form=selected_form, include_storage=bool(include_storage)) return result target_table = table or ("ConfigCASSave" if (payload.get("extension") or payload.get("extension_guid")) else "ConfigSave") prepare_payload = metadata_write_prepare_payload( payload, { **payload, "kind": payload.get("kind") or payload.get("object_type"), "name": payload.get("object_name") or payload.get("name") or payload.get("form"), "object_name": payload.get("object_name") or payload.get("form"), "file_name": payload.get("file_name"), }, target_table=target_table, mode="plan", auto_prepare=False, ) result = { "schema": "onec_form_write_target_verify.v1", "method": method, "status": "needs_prepare" if resolved.get("status") == "not_found" else resolved.get("status") or "error", "verified": False, "writable_now": False, "needs_prepare": resolved.get("status") == "not_found", "base_id": base_id, "source": ( {"table": target_table} if include_storage else {"kind": "saved_state", "layer": SAVED_STATE_LAYER_BY_TABLE[target_table]} ), "query": query, "resolve_status": resolved.get("status"), "diagnostics": resolved.get("diagnostics"), "counts": resolved.get("counts"), "next_resolution": { "method": "metadata.saved_state.prepare", "params": public_saved_state_prepare_call_payload(prepare_payload), } if resolved.get("status") == "not_found" else None, } if search: result["saved_state_search"] = compact_saved_state_form_search_result(search, selected_form=selected_form, include_storage=bool(include_storage)) if include_storage: result["resolve"] = resolved return result def metadata_form_element_write(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, FORM_ELEMENT_WRITE_METHOD) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, FORM_ELEMENT_WRITE_METHOD) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error allow_write, allow_write_error = strict_bool_argument(payload, "allow_saved_state_write", method=FORM_ELEMENT_WRITE_METHOD, default=False) if allow_write_error: return allow_write_error if not allow_write: return invalid_argument( FORM_ELEMENT_WRITE_METHOD, "allow_saved_state_write", "Saved-state write planning is opt-in; pass allow_saved_state_write=true. The adapter still returns a proposal and does not write SQL.", ) include_payload, include_payload_error = strict_bool_argument(payload, "include_payload", method=FORM_ELEMENT_WRITE_METHOD, default=False) if include_payload_error: return include_payload_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=FORM_ELEMENT_WRITE_METHOD, default=30, minimum=1) if timeout_error: return timeout_error table = str(payload.get("table") or "ConfigSave") if table not in FORM_ELEMENT_SAVED_STATE_TABLES: return invalid_argument( FORM_ELEMENT_WRITE_METHOD, "table", "Saved-state element write planning only targets saved-state tables.", allowed_values=sorted(FORM_ELEMENT_SAVED_STATE_TABLES), ) element_error = validate_optional_string_arguments( payload, FORM_ELEMENT_WRITE_METHOD, [ "ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "element", "element_name", "element_path", "path", "element_id", "id", "file_name", "form_guid", "form", "form_name", "name_filter", "command", "attribute", "extension", "extension_guid", ], ) if element_error: return element_error edits = payload.get("edits") if edits is None and payload.get("property"): edits = [{"property": payload.get("property"), "value": payload.get("value"), **({"expected_old": payload.get("expected_old")} if "expected_old" in payload else {})}] if not isinstance(edits, list) or not edits: return invalid_argument(FORM_ELEMENT_WRITE_METHOD, "edits", "Pass edits as a non-empty JSON array of {property, value, expected_old?}.") working_payload = dict(payload) owner_kind = canonical_kind(str(first_non_empty_arg(working_payload, "kind", "object_type") or "")) owner_name = str(first_non_empty_arg(working_payload, "name", "object_name") or "").strip() explicit_form_name = str(first_non_empty_arg(working_payload, "form", "form_name", "name_filter") or "").strip() is_common_form_request = owner_kind == "CommonForm" or ( owner_kind in {None, "", "Form"} and not explicit_form_name and bool(owner_name) ) legacy_guid_is_form = bool( working_payload.get("guid") and not working_payload.get("object_guid") and not owner_kind and not owner_name and not explicit_form_name ) concrete_form_guid = str( first_non_empty_arg( working_payload, "form_guid", *(("guid", "object_guid") if is_common_form_request else ("guid",) if legacy_guid_is_form else ()), ) or "" ).strip() resolver_result = None if not (working_payload.get("file_name") or concrete_form_guid): first_edit = edits[0] if isinstance(edits[0], dict) else {} resolver_result = metadata_form_write_target_resolve( { **working_payload, "base_id": base_id, "table": table, "property": first_edit.get("property") or first_edit.get("name"), **({"value": first_edit.get("value")} if isinstance(first_edit, dict) and "value" in first_edit else {}), "timeout_seconds": int(timeout_seconds or 30), } ) if resolver_result.get("status") == "ok": source = resolver_result.get("source") if isinstance(resolver_result.get("source"), dict) else {} target = resolver_result.get("effective_target") if isinstance(resolver_result.get("effective_target"), dict) else {} if not target: target = resolver_result.get("target") if isinstance(resolver_result.get("target"), dict) else {} working_payload["file_name"] = source.get("file_name") if target.get("path"): working_payload["element_path"] = target.get("path") elif resolver_result.get("status") in {"not_found", "ambiguous"}: result = dict(resolver_result) result["method"] = FORM_ELEMENT_WRITE_METHOD return result decode_payload = without_form_decode_selector_keys(working_payload) if working_payload.get("file_name"): for owner_selector_key in ("ref", "kind", "name", "guid", "object_type", "object_name", "object_guid"): decode_payload.pop(owner_selector_key, None) if concrete_form_guid: decode_payload["form_guid"] = concrete_form_guid decode_payload.pop("guid", None) decoded = metadata_form_decode( { **decode_payload, "base_id": base_id, "table": table, "include_storage": True, "include_parameters": True, "max_items": int(working_payload.get("max_items") or 5000), "timeout_seconds": int(timeout_seconds or 30), } ) if decoded.get("status") != "ok": result = dict(decoded) result["method"] = FORM_ELEMENT_WRITE_METHOD return result source = decoded.get("source") if isinstance(decoded.get("source"), dict) else {} file_name = str(source.get("file_name") or (decoded.get("form") or {}).get("file_name") or payload.get("file_name") or "") if not file_name: return { "schema": "onec_adapter_request_error.v1", "method": FORM_ELEMENT_WRITE_METHOD, "status": "error", "error": "source_required", "diagnostics": {"message": "Could not resolve saved-state form source file_name."}, } profile = decoded.get("profile") if isinstance(decoded.get("profile"), dict) else {} selector = form_write_selector_from_payload(working_payload) items = filter_form_profile_write_targets(form_profile_write_targets(profile), selector) if len(items) != 1: return { "schema": "onec_adapter_request_error.v1", "method": FORM_ELEMENT_WRITE_METHOD, "status": "not_found" if not items else "ambiguous", "error": "element_not_resolved", "base_id": base_id, "diagnostics": {"message": "Pass element, element_id, or element_path so exactly one decoded form element/command/attribute is selected."}, "candidates": form_write_target_candidates(profile, selector, limit=20), "counts": {"matches": len(items)}, } requested_item = items[0] path_edits = [] edit_targets: list[dict[str, Any]] = [] effective_sources: list[dict[str, Any]] = [] for index, edit in enumerate(edits): property_name = edit.get("property") or edit.get("name") if isinstance(edit, dict) else None item, effective_source = form_effective_write_target(profile, requested_item, property_name, edit if isinstance(edit, dict) else None) if isinstance(effective_source, dict) and effective_source.get("writable") is False: return { "schema": "onec_adapter_request_error.v1", "method": FORM_ELEMENT_WRITE_METHOD, "status": "not_routed", "error": "display_source_not_writable_yet", "argument": f"edits[{index}].property", "effective_source": effective_source, "diagnostics": { "message": "The displayed property is inherited from another metadata source. Pass source=local_override to write the local form element override, or use a metadata object write method when it is available." }, } edit_targets.append(item) if effective_source: effective_sources.append(effective_source) property_name = edit.get("property") or edit.get("name") if isinstance(edit, dict) else None if normalize_form_property_name(property_name) == "command_name": command_path_edits, error = form_command_name_write_edits(profile, item, edit, index) if error: return error path_edits.extend(command_path_edits or []) continue path_edit, error = form_element_write_edit(item, edit, index) if error: return error path_edits.append(path_edit) item = edit_targets[0] if edit_targets else requested_item if any(str(target.get("path") or "") != str(item.get("path") or "") for target in edit_targets): return { "schema": "onec_adapter_request_error.v1", "method": FORM_ELEMENT_WRITE_METHOD, "status": "invalid_argument", "error": "multi_target_write_not_supported", "diagnostics": {"message": "One request resolved edits to different physical form records. Split it into separate metadata.write calls."}, "targets": [form_write_target_public(target) for target in edit_targets], } change_path_edits = [ {key: edit.get(key) for key in ("path", "value", "node_type", "property", "old", "expected_old") if key in edit} for edit in path_edits ] proposal = changes_propose( { "base_id": base_id, "source": { "base_id": base_id, "table": table, "file_name": file_name, **({"expected_sha1": payload.get("expected_sha1")} if payload.get("expected_sha1") else {}), }, "edits": change_path_edits, "include_text": bool(payload.get("include_text") is True), "include_payload": bool(include_payload), "preserve_format": True, "timeout_seconds": int(timeout_seconds or 30), "summary": payload.get("summary") or "Saved-state form element edit proposal", } ) if isinstance(proposal, dict): proposal = dict(proposal) proposal["method"] = FORM_ELEMENT_WRITE_METHOD proposal["write_mode"] = { "requested": "saved_state", "target_table": table, "sql_write_performed": False, "requires_apply_gate": True, } proposal["requested_element"] = {key: requested_item.get(key) for key in ("name", "id", "title", "path", "marker", "type_name", "_profile_section")} if "_profile_section" in proposal["requested_element"]: proposal["requested_element"]["section"] = proposal["requested_element"].pop("_profile_section") proposal["element"] = {key: item.get(key) for key in ("name", "id", "title", "path", "marker", "type_name", "_profile_section")} if "_profile_section" in proposal["element"]: proposal["element"]["section"] = proposal["element"].pop("_profile_section") if effective_sources: proposal["effective_sources"] = effective_sources proposal["form_element_edits"] = [ {key: edit.get(key) for key in ("property", "value", "expected_old", "old") if key in edit} for edit in path_edits ] proposal["semantic_diff"] = [ { "section": proposal["element"].get("section"), "target": item.get("name") or item.get("title") or item.get("path"), "property": edit.get("property"), "old": edit.get("old"), "new": edit.get("value"), "presentation": f"{item.get('name') or item.get('title') or item.get('path')}.{edit.get('property')}: {edit.get('old')} -> {edit.get('value')}", } for edit in path_edits ] if resolver_result: proposal["resolution"] = {key: resolver_result.get(key) for key in ("schema", "status", "source", "form", "target", "property", "semantic_diff") if resolver_result.get(key) is not None} proposal["form"] = decoded.get("form") proposal["diagnostics"] = { **(proposal.get("diagnostics") if isinstance(proposal.get("diagnostics"), dict) else {}), "note": "Saved-state write mode is configured as proposal-only. No SQL rows were updated.", } original_bytes = (proposal.get("original") or {}).get("bytes") if isinstance(proposal.get("original"), dict) else None encoded_bytes = (proposal.get("encoded") or {}).get("bytes") if isinstance(proposal.get("encoded"), dict) else None validation_mode = (proposal.get("validation") or {}).get("mode") if isinstance(proposal.get("validation"), dict) else None if original_bytes != encoded_bytes and validation_mode != "path_preserve_format": proposal["safety"] = { "status": "unsafe_to_apply", "reason": "serialized_form_payload_size_changed", "original_bytes": original_bytes, "encoded_bytes": encoded_bytes, "message": "The current codec rewrote the serialized form payload layout. Applying this proposal is blocked by default because 1C may report a stream error.", } return proposal def sanitize_proposal_for_response(proposal: Any) -> Any: if not isinstance(proposal, dict): return proposal sanitized = dict(proposal) encoded = sanitized.get("encoded") if isinstance(encoded, dict) and "payload_hex" in encoded: sanitized["encoded"] = {key: value for key, value in encoded.items() if key != "payload_hex"} return sanitized def sanitize_object_property_write_result(result: dict[str, Any]) -> dict[str, Any]: sanitized = sanitize_public_result(result) proposal = sanitized.get("proposal") if isinstance(sanitized.get("proposal"), dict) else None if proposal is not None: proposal.pop("source", None) for key in ("apply_result", "rollback_result"): nested = sanitized.get(key) if isinstance(sanitized.get(key), dict) else None if nested is not None: nested.pop("source", None) return sanitized def normalize_object_property_member_payload(payload: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any] | None]: method = OBJECT_PROPERTY_WRITE_METHOD result = dict(payload) member_ref = str( first_non_empty_arg(payload, "member_ref", "child_ref", "canonical_path") or "" ).strip() member_path: list[str] = [] if member_ref: parsed = parse_1c_object_path(member_ref) member_path = list(parsed.get("member_path") or []) if not parsed.get("kind") or not parsed.get("name") or not member_path: return payload, invalid_argument( method, "member_ref", "member_ref must be a full public child path such as Catalog.Номенклатура.Attribute.Артикул.", ) existing_kind = canonical_kind(str(result.get("kind") or result.get("object_type") or "")) existing_name = str(result.get("name") or result.get("object_name") or "").strip() if existing_kind and existing_kind != parsed.get("kind"): return payload, invalid_argument(method, "member_ref", "member_ref object kind conflicts with the parent object selector.") if existing_name and existing_name != parsed.get("name"): return payload, invalid_argument(method, "member_ref", "member_ref object name conflicts with the parent object selector.") result["kind"] = parsed.get("kind") result["name"] = parsed.get("name") result["ref"] = parsed.get("canonical_ref") result["_member_path"] = member_path result.setdefault("member_name", member_path[-1]) if len(member_path) >= 2 and canonical_nested_member_kind(member_path[-2]): result.setdefault("member_kind", canonical_nested_member_kind(member_path[-2])) member_name = str(result.get("member_name") or "").strip() member_kind = str(result.get("member_kind") or "").strip() if member_kind and not canonical_nested_member_kind(member_kind): return payload, invalid_argument( method, "member_kind", "Unsupported child category.", allowed_values=["Attribute", "TabularSection", "Dimension", "Resource"], ) if member_kind and not member_name: return payload, invalid_argument(method, "member_name", "member_name is required when member_kind is provided.") result["_member_requested"] = bool(member_name or member_path) return result, None def metadata_object_property_write(payload: dict[str, Any]) -> dict[str, Any]: method = OBJECT_PROPERTY_WRITE_METHOD payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload payload, member_error = normalize_object_property_member_payload(payload) if member_error: return member_error payload, context_error = normalize_repository_write_context(payload, method) if context_error: return {"schema": "onec_metadata_object_property_write.v1", "method": method, **context_error} base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error mode = str(payload.get("execution_mode") or payload.get("mode") or "plan").strip().casefold() if mode not in FORM_ELEMENT_WRITE_APPLY_MODES: return invalid_argument(method, "execution_mode", "Unsupported execution mode.", allowed_values=sorted(FORM_ELEMENT_WRITE_APPLY_MODES)) repository_error = repository_apply_gate(payload, method, mode) if repository_error: return repository_error include_storage, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error include_storage = bool(include_storage) timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1) if timeout_error: return timeout_error timeout_seconds = int(timeout_seconds or 60) allow_write, allow_write_error = strict_bool_argument(payload, "allow_saved_state_write", method=method, default=False) if allow_write_error: return allow_write_error if not allow_write: return invalid_argument(method, "allow_saved_state_write", "Saved-state object property planning is opt-in; pass allow_saved_state_write=true.") auto_prepare, auto_prepare_error = strict_bool_argument(payload, "auto_prepare_saved_state", method=method, default=False) if auto_prepare_error: return auto_prepare_error requested_property_raw = payload.get("property") requested_property = normalize_object_identity_property(requested_property_raw) if not requested_property: raw_name = str(requested_property_raw or "").strip().casefold() if raw_name in {"name", "имя", "rename", "переименование"}: return invalid_argument( method, "property", "Object rename is intentionally disabled because reference-safe metadata rename rules are not yet proven.", allowed_values=["synonym", "comment"], ) return invalid_argument(method, "property", "Only synonym and comment are supported.", allowed_values=["synonym", "comment"]) if "value" not in payload or not isinstance(payload.get("value"), str): return invalid_argument(method, "value", "value is required and must be a JSON string; an empty string is allowed.") requested_value = str(payload.get("value")) language = str(payload.get("language") or "ru").strip() if requested_property == "synonym" and not language: return invalid_argument(method, "language", "language must be a non-empty locale already present in the synonym container.") extension = str(payload.get("extension") or "").strip() layer = str(payload.get("layer") or "").strip() or ("extension_saved_state" if extension else "base_saved_state") if layer not in SAVED_STATE_TABLE_BY_LAYER: return invalid_argument(method, "layer", "layer must be base_saved_state or extension_saved_state.", allowed_values=sorted(SAVED_STATE_TABLE_BY_LAYER)) if extension and layer != "extension_saved_state": return invalid_argument(method, "layer", "An extension selector requires extension_saved_state.", allowed_values=["extension_saved_state"]) target_table = SAVED_STATE_TABLE_BY_LAYER[layer] prepare_selector = { key: payload[key] for key in ("ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "extension") if payload.get(key) not in {None, ""} } prepare_payload = { **repository_write_context(payload), "base_id": base_id, "layer": layer, **prepare_selector, "mode": "plan", "include_storage": True, "timeout_seconds": timeout_seconds, } prepare_result = metadata_saved_state_prepare(prepare_payload) prepare_status = str(prepare_result.get("status") or "error") if prepare_status == "plan_ready": if mode == "plan" or not auto_prepare: next_params = public_saved_state_prepare_call_payload(prepare_payload) return { "schema": "onec_metadata_object_property_write.v1", "method": method, "status": "needs_prepare", "execution_mode": mode, "base_id": base_id, "layer": layer, "property": {"name": requested_property, **({"language": language} if requested_property == "synonym" else {}), "requested": requested_value}, "object": public_saved_state_prepare_object(prepare_result.get("object")), "prepare_plan": public_saved_state_prepare_embedded_result(prepare_result, prepare_payload), "next_call": {"method": "metadata.saved_state.prepare", "params": next_params}, "diagnostics": {"message": "The named object has no saved-state working copy yet. Prepare it before planning the scalar edit."}, } allow_prepare, allow_prepare_error = strict_bool_argument(payload, "allow_sql_saved_state_prepare", method=method, default=False) if allow_prepare_error: return allow_prepare_error if not allow_prepare: return invalid_argument(method, "allow_sql_saved_state_prepare", "Automatic saved-state preparation is opt-in; pass allow_sql_saved_state_prepare=true.") prepare_result = metadata_saved_state_prepare( { **prepare_payload, "mode": "apply_and_verify", "allow_sql_saved_state_prepare": True, } ) prepare_status = str(prepare_result.get("status") or "error") if prepare_status not in {"blocked_target_collision", "verified", "applied"}: result = { "schema": "onec_metadata_object_property_write.v1", "method": method, "status": prepare_status, "execution_mode": mode, "base_id": base_id, "layer": layer, "error": prepare_result.get("error") or "saved_state_not_ready", "prepare_result": prepare_result, "diagnostics": prepare_result.get("diagnostics") or {"message": "Could not resolve a writable saved-state copy for the named object."}, } return result if include_storage else sanitize_object_property_write_result(result) prepare_apply_result = prepare_result.get("apply_result") if isinstance(prepare_result.get("apply_result"), dict) else {} prepare_receipt = prepare_apply_result.get("prepare_receipt") if isinstance(prepare_apply_result.get("prepare_receipt"), dict) else {} prepare_receipt_id = str(prepare_receipt.get("receipt_id") or "") object_card = prepare_result.get("object") if isinstance(prepare_result.get("object"), dict) else {} object_guid = str(object_card.get("guid") or payload.get("guid") or payload.get("object_guid") or "").strip().lower() if not is_guid_text(object_guid): result = { "schema": "onec_metadata_object_property_write.v1", "method": method, "status": "not_found", "error": "resolved_object_guid_missing", "base_id": base_id, "object": public_saved_state_prepare_object(object_card), "diagnostics": {"message": "The name-first object resolver did not return an exact metadata GUID."}, } return result if include_storage else sanitize_object_property_write_result(result) file_names = [str(item) for item in (prepare_result.get("file_names") or []) if isinstance(item, str) and Path(item).name == item] matches: list[dict[str, Any]] = [] member_errors: list[dict[str, Any]] = [] property_errors: list[dict[str, Any]] = [] read_errors: list[dict[str, Any]] = [] for file_name in file_names: data, _config, read_error = read_storage_file_bytes(base_id, target_table, file_name, timeout_seconds=timeout_seconds) if read_error: read_errors.append({"file_name": file_name, "error": read_error}) continue tree = parse_config_tree_from_bytes(data or b"") target_guid = object_guid member: dict[str, Any] | None = None if payload.get("_member_requested"): member_resolution = config_tree_member_identity_resolve( tree, parent_guid=object_guid, parent_kind=str(object_card.get("kind") or payload.get("kind") or ""), parent_name=str(object_card.get("name") or payload.get("name") or ""), member_path=list(payload.get("_member_path") or []), member_kind=str(payload.get("member_kind") or "") or None, member_name=str(payload.get("member_name") or "") or None, ) if member_resolution.get("status") != "ok": member_errors.append({"file_name": file_name, **member_resolution}) continue member = member_resolution.get("member") target_guid = str((member or {}).get("guid") or "") target = config_tree_identity_property_target(tree, target_guid, requested_property, language=language) if target.get("status") == "ok": matches.append({"file_name": file_name, "data": data, "tree": tree, "target": target, "target_guid": target_guid, "member": member}) elif target.get("error") != "object_identity_not_found": property_errors.append({"file_name": file_name, "target": target}) if len(matches) != 1: member_ambiguous = any(item.get("status") == "ambiguous" for item in member_errors) status = "ambiguous" if len(matches) > 1 or member_ambiguous else ("unsupported" if property_errors else "not_found") result = { "schema": "onec_metadata_object_property_write.v1", "method": method, "status": status, "error": "ambiguous_identity_property_target" if len(matches) > 1 else ( "member_identity_ambiguous" if member_ambiguous else (property_errors[0].get("target") or {}).get("error") if property_errors else "identity_property_target_not_found" ), "base_id": base_id, "layer": layer, "object": { **{key: object_card.get(key) for key in ("kind", "name", "synonym", "guid") if object_card.get(key) is not None}, **({"ref": object_selector_ref(object_card.get("kind"), object_card.get("name"))} if object_selector_ref(object_card.get("kind"), object_card.get("name")) else {}), }, "property": {"name": requested_property, **({"language": language} if requested_property == "synonym" else {})}, "counts": {"matches": len(matches), "candidate_files": len(file_names), "read_errors": len(read_errors)}, "diagnostics": { "message": "The exact object GUID and scalar identity property must resolve in exactly one saved-state payload.", "property_errors": property_errors, "member_errors": member_errors, "read_errors": read_errors, }, } return result if include_storage else sanitize_object_property_write_result(result) resolved = matches[0] file_name = str(resolved["file_name"]) target = resolved["target"] resolved_member = resolved.get("member") if isinstance(resolved.get("member"), dict) else None target_guid = str(resolved.get("target_guid") or object_guid) current_value = str(target.get("current") or "") if "expected_old" in payload and payload.get("expected_old") != current_value: result = { "schema": "onec_metadata_object_property_write.v1", "method": method, "status": "precondition_failed", "error": "expected_old_mismatch", "base_id": base_id, "object": {key: object_card.get(key) for key in ("kind", "name", "guid") if object_card.get(key) is not None}, **({"member": resolved_member} if resolved_member else {}), "property": {"name": requested_property, "current": current_value, "expected_old": payload.get("expected_old"), "requested": requested_value}, "diagnostics": {"message": "The saved-state scalar changed after the caller read it."}, } return result if include_storage else sanitize_object_property_write_result(result) result: dict[str, Any] = { "schema": "onec_metadata_object_property_write.v1", "method": method, "status": "unchanged" if current_value == requested_value else "planned", "execution_mode": mode, "base_id": base_id, "layer": layer, "object": { **{key: object_card.get(key) for key in ("kind", "name", "synonym", "guid") if object_card.get(key) is not None}, **({"ref": object_selector_ref(object_card.get("kind"), object_card.get("name"))} if object_selector_ref(object_card.get("kind"), object_card.get("name")) else {}), }, **({"member": resolved_member} if resolved_member else {}), "property": { "name": requested_property, **({"language": language} if requested_property == "synonym" else {}), "current": current_value, "requested": requested_value, }, "write_mode": {"target": "saved_state", "active_configuration_write": False, "sql_write_performed": False}, } if current_value == requested_value: return result if include_storage else sanitize_object_property_write_result(result) proposal = changes_propose( { "base_id": base_id, "source": { "base_id": base_id, "table": target_table, "file_name": file_name, **({"expected_sha1": payload.get("expected_sha1")} if payload.get("expected_sha1") else {}), }, "edits": [ { "path": target.get("path"), "value": requested_value, "node_type": target.get("node_type"), "expected_old": current_value, "property": requested_property, } ], "include_payload": mode != "plan", "preserve_format": True, "timeout_seconds": timeout_seconds, "summary": payload.get("summary") or f"Saved-state object {requested_property} edit", } ) result["proposal"] = proposal if proposal.get("status") not in {"accepted_for_review", "ok"} or (proposal.get("validation") or {}).get("status") != "ok": result["status"] = proposal.get("status") if proposal.get("status") not in {"accepted_for_review", "ok"} else "proposal_validation_failed" return result if include_storage else sanitize_object_property_write_result(result) if mode == "plan": result["proposal"] = sanitize_proposal_for_response(proposal) return result if include_storage else sanitize_object_property_write_result(result) allow_apply, allow_apply_error = strict_bool_argument(payload, "allow_sql_saved_state_apply", method=method, default=False) if allow_apply_error: return allow_apply_error if not allow_apply: return invalid_argument(method, "allow_sql_saved_state_apply", "Apply mode is opt-in; pass allow_sql_saved_state_apply=true.") apply_result = storage_saved_state_apply_proposal( { **repository_write_context(payload), "base_id": base_id, "allow_sql_saved_state_apply": True, "proposal": proposal, "timeout_seconds": timeout_seconds, } ) result["proposal"] = sanitize_proposal_for_response(proposal) result["apply_result"] = apply_result result["applied"] = bool(apply_result.get("applied")) result["status"] = str(apply_result.get("status") or "error") result["write_mode"]["sql_write_performed"] = result["applied"] if not result["applied"] or mode == "apply": return result if include_storage else sanitize_object_property_write_result(result) readback_data, _readback_config, readback_error = read_storage_file_bytes(base_id, target_table, file_name, timeout_seconds=timeout_seconds) readback_target = ( config_tree_identity_property_target(parse_config_tree_from_bytes(readback_data or b""), target_guid, requested_property, language=language) if not readback_error else {"status": "error", "error": "readback_failed"} ) verified = readback_target.get("status") == "ok" and readback_target.get("current") == requested_value result["semantic_verification"] = { "status": "ok" if verified else "mismatch", "expected": requested_value, "actual": readback_target.get("current"), **({"error": readback_target.get("error")} if readback_target.get("error") else {}), } if mode == "apply_and_verify": result["status"] = "verified" if verified else "verification_failed" return result if include_storage else sanitize_object_property_write_result(result) allow_rollback, allow_rollback_error = strict_bool_argument(payload, "allow_sql_saved_state_rollback", method=method, default=False) if allow_rollback_error: return allow_rollback_error if not allow_rollback: return invalid_argument(method, "allow_sql_saved_state_rollback", "apply_and_rollback mode is opt-in; pass allow_sql_saved_state_rollback=true.") backup_id = ((apply_result.get("backup") or {}).get("backup_id") if isinstance(apply_result.get("backup"), dict) else None) if not backup_id: result["status"] = "rollback_unavailable" result["diagnostics"] = {"message": "Apply result did not return backup.backup_id; cannot rollback automatically."} return result if include_storage else sanitize_object_property_write_result(result) rollback_result = storage_saved_state_rollback( { **repository_write_context(payload), "base_id": base_id, "allow_sql_saved_state_rollback": True, "backup_id": backup_id, "timeout_seconds": timeout_seconds, } ) result["rollback_result"] = rollback_result payload_rolled_back = bool(rollback_result.get("applied")) prepare_rollback_result = None if payload_rolled_back and prepare_receipt_id: prepare_rollback_result = rollback_saved_state_prepare_receipt( base_id, prepare_receipt_id, timeout_seconds=timeout_seconds, ) result["prepare_rollback_result"] = prepare_rollback_result result["rolled_back"] = bool( payload_rolled_back and ( not prepare_receipt_id or (prepare_rollback_result or {}).get("applied") ) ) result["status"] = ( "verified_and_rolled_back" if result["applied"] and verified and result["rolled_back"] else ("applied_rollback_failed" if result["applied"] else result["status"]) ) return result if include_storage else sanitize_object_property_write_result(result) def metadata_object_member_add(payload: dict[str, Any]) -> dict[str, Any]: method = OBJECT_MEMBER_ADD_METHOD template_ref = str(payload.get("template_member_ref") or "").strip() normalized, selector_error = normalize_object_property_member_payload({**payload, "member_ref": template_ref}) if selector_error: return selector_error payload = normalized if canonical_nested_member_kind(payload.get("member_kind")) != "Attribute": return invalid_argument(method, "template_member_ref", "The first structural writer supports only an existing Attribute template.") base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error payload, context_error = normalize_repository_write_context(payload, method) if context_error: return {"schema": "onec_metadata_object_member_add.v1", "method": method, **context_error} mode = str(payload.get("execution_mode") or payload.get("mode") or "plan").strip().casefold() if mode not in FORM_ELEMENT_WRITE_APPLY_MODES: return invalid_argument(method, "execution_mode", "Unsupported execution mode.", allowed_values=sorted(FORM_ELEMENT_WRITE_APPLY_MODES)) repository_error = repository_apply_gate(payload, method, mode) if repository_error: return repository_error include_storage, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error include_storage = bool(include_storage) timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1) if timeout_error: return timeout_error timeout_seconds = int(timeout_seconds or 60) allow_write, allow_write_error = strict_bool_argument(payload, "allow_saved_state_write", method=method, default=False) if allow_write_error: return allow_write_error if not allow_write: return invalid_argument(method, "allow_saved_state_write", "Attribute add planning is opt-in; pass allow_saved_state_write=true.") auto_prepare, auto_prepare_error = strict_bool_argument(payload, "auto_prepare_saved_state", method=method, default=False) if auto_prepare_error: return auto_prepare_error new_name = str(payload.get("new_member_name") or "").strip() if not new_name or not re.fullmatch(r"[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]*", new_name): return invalid_argument(method, "new_member_name", "new_member_name must be a valid non-empty 1C identifier.") new_synonym = str(payload.get("new_member_synonym") if payload.get("new_member_synonym") is not None else new_name) template_parts = [part for part in template_ref.split(".") if part] if len(template_parts) < 4 or canonical_nested_member_kind(template_parts[-2]) != "Attribute": return invalid_argument(method, "template_member_ref", "template_member_ref must end with Attribute..") container_ref = ".".join(template_parts[:-2]) requested_ref = ".".join([container_ref, "Attribute", new_name]) container_scope = "tabular_section" if "TabularSection" in normalized_nested_member_path(payload.get("_member_path") or []) else "object" extension = str(payload.get("extension") or "").strip() layer = str(payload.get("layer") or "").strip() or ("extension_saved_state" if extension else "base_saved_state") if layer not in SAVED_STATE_TABLE_BY_LAYER: return invalid_argument(method, "layer", "layer must be base_saved_state or extension_saved_state.", allowed_values=sorted(SAVED_STATE_TABLE_BY_LAYER)) if extension and layer != "extension_saved_state": return invalid_argument(method, "layer", "An extension selector requires extension_saved_state.", allowed_values=["extension_saved_state"]) target_table = SAVED_STATE_TABLE_BY_LAYER[layer] prepare_payload = { **repository_write_context(payload), "base_id": base_id, "layer": layer, **{ key: payload[key] for key in ("ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "extension") if payload.get(key) not in {None, ""} }, "mode": "plan", "include_storage": True, "timeout_seconds": timeout_seconds, } prepare_result = metadata_saved_state_prepare(prepare_payload) prepare_status = str(prepare_result.get("status") or "error") if prepare_status == "plan_ready": if mode == "plan" or not auto_prepare: result = { "schema": "onec_metadata_object_member_add.v1", "method": method, "status": "needs_prepare", "execution_mode": mode, "base_id": base_id, "layer": layer, "object": public_saved_state_prepare_object(prepare_result.get("object")), "template": {"ref": template_ref, "kind": "Attribute", "name": payload.get("member_name")}, "container": {"ref": container_ref, "scope": container_scope}, "requested_member": {"kind": "Attribute", "name": new_name, "synonym": new_synonym, "ref": requested_ref}, "prepare_plan": public_saved_state_prepare_embedded_result(prepare_result, prepare_payload), "next_call": {"method": "metadata.saved_state.prepare", "params": public_saved_state_prepare_call_payload(prepare_payload)}, "diagnostics": {"message": "Prepare the named parent object in saved-state before building the structural append proposal."}, } return result if include_storage else sanitize_object_property_write_result(result) allow_prepare, allow_prepare_error = strict_bool_argument(payload, "allow_sql_saved_state_prepare", method=method, default=False) if allow_prepare_error: return allow_prepare_error if not allow_prepare: return invalid_argument(method, "allow_sql_saved_state_prepare", "Automatic saved-state preparation is opt-in; pass allow_sql_saved_state_prepare=true.") prepare_result = metadata_saved_state_prepare( {**prepare_payload, "mode": "apply_and_verify", "allow_sql_saved_state_prepare": True} ) prepare_status = str(prepare_result.get("status") or "error") if prepare_status not in {"blocked_target_collision", "verified", "applied"}: result = { "schema": "onec_metadata_object_member_add.v1", "method": method, "status": prepare_status, "error": prepare_result.get("error") or "saved_state_not_ready", "prepare_result": prepare_result, } return result if include_storage else sanitize_object_property_write_result(result) prepare_apply_result = prepare_result.get("apply_result") if isinstance(prepare_result.get("apply_result"), dict) else {} prepare_receipt = prepare_apply_result.get("prepare_receipt") if isinstance(prepare_apply_result.get("prepare_receipt"), dict) else {} prepare_receipt_id = str(prepare_receipt.get("receipt_id") or "") object_card = prepare_result.get("object") if isinstance(prepare_result.get("object"), dict) else {} object_guid = str(object_card.get("guid") or payload.get("guid") or payload.get("object_guid") or "").strip().lower() if not is_guid_text(object_guid): return {"schema": "onec_metadata_object_member_add.v1", "method": method, "status": "not_found", "error": "resolved_object_guid_missing"} new_guid = deterministic_member_guid(object_guid, "Attribute", new_name, scope=container_ref) file_names = [str(item) for item in (prepare_result.get("file_names") or []) if isinstance(item, str) and Path(item).name == item] candidates: list[dict[str, Any]] = [] diagnostics: list[dict[str, Any]] = [] for file_name in file_names: data, _config, read_error = read_storage_file_bytes(base_id, target_table, file_name, timeout_seconds=timeout_seconds) if read_error: diagnostics.append({"file_name": file_name, "error": read_error}) continue tree = parse_config_tree_from_bytes(data or b"") template = config_tree_member_identity_resolve( tree, parent_guid=object_guid, parent_kind=str(object_card.get("kind") or payload.get("kind") or ""), parent_name=str(object_card.get("name") or payload.get("name") or ""), member_path=list(payload.get("_member_path") or []), member_kind="Attribute", member_name=str(payload.get("member_name") or ""), ) if template.get("status") != "ok": diagnostics.append({"file_name": file_name, "template": template}) continue template_member = template.get("member") if isinstance(template.get("member"), dict) else {} identities = config_tree_identity_records(tree) record = config_tree_declared_record_for_guid(tree, str(template_member.get("guid") or "")) if record.get("status") != "ok": diagnostics.append({"file_name": file_name, "record": record}) continue duplicate: list[dict[str, Any]] = [] for guid, identity in identities.items(): if str(identity.get("name") or "").casefold() != new_name.casefold(): continue duplicate_record = config_tree_declared_record_for_guid(tree, guid) if duplicate_record.get("status") == "ok" and duplicate_record.get("parent_path") == record.get("parent_path"): duplicate.append({"guid": guid, "name": identity.get("name")}) if new_guid in identities and not duplicate: duplicate.append({"guid": new_guid, "name": identities[new_guid].get("name"), "reason": "deterministic_guid_collision"}) if duplicate: return { "schema": "onec_metadata_object_member_add.v1", "method": method, "status": "conflict", "error": "member_name_already_exists", "base_id": base_id, "container": {"ref": container_ref}, "requested_member": {"kind": "Attribute", "name": new_name, "ref": requested_ref}, "matches": duplicate if include_storage else sanitize_public_result(duplicate), } synonym_target = config_tree_identity_property_target(tree, str(template_member.get("guid") or ""), "synonym", language="ru") comment_target = config_tree_identity_property_target(tree, str(template_member.get("guid") or ""), "comment") if synonym_target.get("status") != "ok" or comment_target.get("status") != "ok": diagnostics.append({"file_name": file_name, "synonym": synonym_target, "comment": comment_target}) continue candidates.append( { "file_name": file_name, "tree": tree, "template": template_member, "record": record, "synonym": synonym_target, "comment": comment_target, } ) if len(candidates) != 1: result = { "schema": "onec_metadata_object_member_add.v1", "method": method, "status": "ambiguous" if len(candidates) > 1 else "not_found", "error": "attribute_template_target_ambiguous" if len(candidates) > 1 else "attribute_template_target_not_found", "base_id": base_id, "counts": {"matches": len(candidates), "candidate_files": len(file_names)}, "diagnostics": diagnostics, } return result if include_storage else sanitize_object_property_write_result(result) resolved = candidates[0] template_member = resolved["template"] new_comment = str(payload.get("new_member_comment") or "") cloned_node, clone_validation = clone_metadata_member_record( resolved["record"]["node"], template_guid=str(template_member.get("guid") or "").lower(), new_guid=new_guid, new_name=new_name, new_synonym=new_synonym, new_comment=new_comment, ) if cloned_node is None or clone_validation.get("status") != "ok": return { "schema": "onec_metadata_object_member_add.v1", "method": method, "status": "blocked", "error": clone_validation.get("error") or "unsafe_attribute_template", "base_id": base_id, "container": {"ref": container_ref, "scope": container_scope}, "template": { key: template_member.get(key) for key in ("kind", "name", "ref") if template_member.get(key) is not None }, "clone_validation": clone_validation, "diagnostics": { "message": "The template record must preserve every non-identity setting and must not reference its old identity outside declared identity fields.", }, } proposal = changes_propose( { "base_id": base_id, "source": { "base_id": base_id, "table": target_table, "file_name": resolved["file_name"], **({"expected_sha1": payload.get("expected_sha1")} if payload.get("expected_sha1") else {}), }, "edits": [{"append_child": {"parent_path": resolved["record"]["parent_path"], "node": cloned_node}}], "include_payload": mode != "plan", "preserve_format": True, "timeout_seconds": timeout_seconds, "summary": payload.get("summary") or f"Add Attribute {new_name} from template {template_member.get('name')}", } ) result: dict[str, Any] = { "schema": "onec_metadata_object_member_add.v1", "method": method, "status": "planned", "execution_mode": mode, "base_id": base_id, "layer": layer, "object": { **{key: object_card.get(key) for key in ("kind", "name", "guid") if object_card.get(key) is not None}, **({"ref": object_selector_ref(object_card.get("kind"), object_card.get("name"))} if object_selector_ref(object_card.get("kind"), object_card.get("name")) else {}), }, "container": {"ref": container_ref, "scope": container_scope}, "template": {key: template_member.get(key) for key in ("kind", "name", "ref") if template_member.get(key) is not None}, "requested_member": { "kind": "Attribute", "name": new_name, "synonym": new_synonym, "comment": new_comment, "guid": new_guid, "ref": requested_ref, }, "clone_validation": clone_validation, "proposal": proposal, "write_mode": {"target": "saved_state", "active_configuration_write": False, "sql_write_performed": False}, } if proposal.get("status") not in {"accepted_for_review", "ok"} or (proposal.get("validation") or {}).get("status") != "ok": result["status"] = proposal.get("status") if proposal.get("status") not in {"accepted_for_review", "ok"} else "proposal_validation_failed" return result if include_storage else sanitize_object_property_write_result(result) if mode == "plan": result["proposal"] = sanitize_proposal_for_response(proposal) return result if include_storage else sanitize_object_property_write_result(result) allow_apply, allow_apply_error = strict_bool_argument(payload, "allow_sql_saved_state_apply", method=method, default=False) if allow_apply_error: return allow_apply_error if not allow_apply: return invalid_argument(method, "allow_sql_saved_state_apply", "Apply mode is opt-in; pass allow_sql_saved_state_apply=true.") apply_result = storage_saved_state_apply_proposal( { **repository_write_context(payload), "base_id": base_id, "allow_sql_saved_state_apply": True, "proposal": proposal, "timeout_seconds": timeout_seconds, } ) result["proposal"] = sanitize_proposal_for_response(proposal) result["apply_result"] = apply_result result["applied"] = bool(apply_result.get("applied")) result["status"] = str(apply_result.get("status") or "error") result["write_mode"]["sql_write_performed"] = result["applied"] if not result["applied"] or mode == "apply": return result if include_storage else sanitize_object_property_write_result(result) readback_data, _readback_config, readback_error = read_storage_file_bytes( base_id, target_table, str(resolved["file_name"]), timeout_seconds=timeout_seconds ) readback_tree = parse_config_tree_from_bytes(readback_data or b"") if not readback_error else None identity = config_tree_identity_records(readback_tree).get(new_guid) if readback_tree is not None else None readback_record = config_tree_declared_record_for_guid(readback_tree, new_guid) if readback_tree is not None else {"status": "error"} readback_comment = ( config_tree_identity_property_target(readback_tree, new_guid, "comment") if readback_tree is not None else {"status": "error"} ) readback_shape_sha1 = ( metadata_member_record_shape_sha1(readback_record.get("node"), new_guid) if readback_record.get("status") == "ok" else None ) settings_preserved = bool( readback_shape_sha1 and readback_shape_sha1 == clone_validation.get("cloned_shape_sha1") ) verified = bool( identity and identity.get("name") == new_name and (identity.get("synonyms") or {}).get("ru") == new_synonym and readback_comment.get("status") == "ok" and readback_comment.get("current") == new_comment and readback_record.get("status") == "ok" and readback_record.get("parent_path") == resolved["record"].get("parent_path") and settings_preserved ) result["semantic_verification"] = { "status": "ok" if verified else "mismatch", "member": { "kind": "Attribute", "name": (identity or {}).get("name"), "synonym": ((identity or {}).get("synonyms") or {}).get("ru"), "comment": readback_comment.get("current"), }, "container_match": readback_record.get("parent_path") == resolved["record"].get("parent_path"), "settings_preserved": settings_preserved, "expected_shape_sha1": clone_validation.get("cloned_shape_sha1"), "actual_shape_sha1": readback_shape_sha1, } if mode == "apply_and_verify": result["status"] = "verified" if verified else "verification_failed" return result if include_storage else sanitize_object_property_write_result(result) allow_rollback, allow_rollback_error = strict_bool_argument(payload, "allow_sql_saved_state_rollback", method=method, default=False) if allow_rollback_error: return allow_rollback_error if not allow_rollback: return invalid_argument(method, "allow_sql_saved_state_rollback", "apply_and_rollback mode is opt-in; pass allow_sql_saved_state_rollback=true.") backup_id = ((apply_result.get("backup") or {}).get("backup_id") if isinstance(apply_result.get("backup"), dict) else None) if not backup_id: result["status"] = "rollback_unavailable" return result if include_storage else sanitize_object_property_write_result(result) rollback_result = storage_saved_state_rollback( { **repository_write_context(payload), "base_id": base_id, "allow_sql_saved_state_rollback": True, "backup_id": backup_id, "timeout_seconds": timeout_seconds, } ) result["rollback_result"] = rollback_result payload_rolled_back = bool(rollback_result.get("applied")) prepare_rollback_result = None if payload_rolled_back and prepare_receipt_id: prepare_rollback_result = rollback_saved_state_prepare_receipt( base_id, prepare_receipt_id, timeout_seconds=timeout_seconds, ) result["prepare_rollback_result"] = prepare_rollback_result result["rolled_back"] = bool( payload_rolled_back and ( not prepare_receipt_id or (prepare_rollback_result or {}).get("applied") ) ) result["status"] = "verified_and_rolled_back" if verified and result["rolled_back"] else "applied_rollback_failed" return result if include_storage else sanitize_object_property_write_result(result) def metadata_form_element_write_apply(payload: dict[str, Any]) -> dict[str, Any]: method = FORM_ELEMENT_WRITE_APPLY_METHOD payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload mode = str(payload.get("execution_mode") or payload.get("mode") or "plan").strip().lower() if mode not in FORM_ELEMENT_WRITE_APPLY_MODES: return invalid_argument(method, "execution_mode", "Unsupported execution mode.", allowed_values=sorted(FORM_ELEMENT_WRITE_APPLY_MODES)) repository_error = repository_apply_gate(payload, method, mode) if repository_error: return repository_error plan_payload = dict(payload) plan_payload["allow_saved_state_write"] = True if mode in {"apply", "apply_and_verify", "apply_and_rollback"}: plan_payload["include_payload"] = True proposal = metadata_form_element_write(plan_payload) result: dict[str, Any] = { "schema": "onec_form_element_write_apply.v1", "method": method, "status": "planned", "execution_mode": mode, "base_id": payload.get("base_id"), "proposal": proposal, } if proposal.get("status") not in {"accepted_for_review", "ok"}: result["status"] = proposal.get("status") or "error" result["diagnostics"] = proposal.get("diagnostics") return result source = proposal.get("source") if isinstance(proposal.get("source"), dict) else {} base_id = str(payload.get("base_id") or "") table = str(source.get("table") or payload.get("table") or "") file_name = str(source.get("file_name") or payload.get("file_name") or "") write_plan, write_plan_error = metadata_write_apply_plan_gate( method, payload, target_kind="form", target={ "kind": "form", "table": source.get("table") or payload.get("table"), "file_name": source.get("file_name") or payload.get("file_name"), "form_guid": payload.get("form_guid") or ( payload.get("guid") if ( canonical_kind(str(payload.get("kind") or payload.get("object_type") or "")) == "CommonForm" or ( not payload.get("object_guid") and not payload.get("kind") and not payload.get("object_type") and not payload.get("name") and not payload.get("object_name") and not payload.get("form") and not payload.get("form_name") ) ) else None ), }, ) result["write_plan"] = write_plan if write_plan_error: result.update(write_plan_error) return result if mode == "plan": return result allow_apply, allow_apply_error = strict_bool_argument(payload, "allow_sql_saved_state_apply", method=method, default=False) if allow_apply_error: return allow_apply_error if not allow_apply: return invalid_argument(method, "allow_sql_saved_state_apply", "Apply mode is opt-in; pass allow_sql_saved_state_apply=true.") timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error apply_result = storage_saved_state_apply_proposal( { **repository_write_context(payload), "base_id": payload.get("base_id"), "allow_sql_saved_state_apply": True, "proposal": proposal, "timeout_seconds": int(timeout_seconds or 30), } ) result["proposal"] = sanitize_proposal_for_response(proposal) result["apply_result"] = apply_result result["status"] = apply_result.get("status") or "error" result["applied"] = bool(apply_result.get("applied")) if result["applied"] and mode in {"apply", "apply_and_verify"}: result["code_index_refresh"] = code_index_refresh_form_embedded_module( base_id=base_id, table=table, file_name=file_name, timeout_seconds=int(timeout_seconds or 30), ) if mode == "apply": return result if mode == "apply_and_verify": semantic = apply_result.get("semantic_verification") if isinstance(apply_result.get("semantic_verification"), dict) else {} readback = apply_result.get("readback") if isinstance(apply_result.get("readback"), dict) else {} if result["applied"] and semantic.get("status") in {"ok", "skipped", None} and readback.get("verified") is not False: result["status"] = "verified" elif result["applied"]: result["status"] = apply_result.get("status") or "verification_failed" return result allow_rollback, allow_rollback_error = strict_bool_argument(payload, "allow_sql_saved_state_rollback", method=method, default=False) if allow_rollback_error: return allow_rollback_error if not allow_rollback: return invalid_argument(method, "allow_sql_saved_state_rollback", "apply_and_rollback mode is opt-in; pass allow_sql_saved_state_rollback=true.") backup_id = ((apply_result.get("backup") or {}).get("backup_id") if isinstance(apply_result.get("backup"), dict) else None) if not backup_id: result["status"] = "rollback_unavailable" result["diagnostics"] = {"message": "Apply result did not return backup.backup_id; cannot rollback automatically."} return result rollback_result = storage_saved_state_rollback( { **repository_write_context(payload), "base_id": payload.get("base_id"), "allow_sql_saved_state_rollback": True, "backup_id": backup_id, "timeout_seconds": int(timeout_seconds or 30), } ) result["rollback_result"] = rollback_result result["rolled_back"] = bool(rollback_result.get("applied")) if result["applied"] and result["rolled_back"]: result["status"] = "verified_and_rolled_back" elif result["applied"]: result["status"] = "applied_rollback_failed" else: result["status"] = apply_result.get("status") or "error" return result def form_move_path_parent(path: Any) -> str: parts = str(path or "").split(".") return ".".join(parts[:-1]) if len(parts) > 1 else "" def form_move_target_selector(payload: dict[str, Any], prefix: str) -> dict[str, Any]: path = payload.get(f"{prefix}_path") if path is None and prefix == "from": path = payload.get("element_path") or payload.get("path") if path is not None: return {"element_path": str(path)} element = payload.get(f"{prefix}_element") or payload.get(prefix) if element is None and prefix == "from": element = payload.get("element") if element is not None: return {"element": str(element)} element_id = payload.get(f"{prefix}_id") if element_id is not None: return {"element_id": str(element_id)} return {} def metadata_form_target_move(payload: dict[str, Any]) -> dict[str, Any]: method = FORM_TARGET_MOVE_METHOD payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error mode = str(payload.get("execution_mode") or payload.get("mode") or "plan").strip().casefold() if mode not in FORM_ELEMENT_WRITE_APPLY_MODES: return invalid_argument(method, "execution_mode", "Unsupported execution mode.", allowed_values=sorted(FORM_ELEMENT_WRITE_APPLY_MODES)) repository_error = repository_apply_gate(payload, method, mode) if repository_error: return repository_error allow_write, allow_write_error = strict_bool_argument(payload, "allow_saved_state_write", method=method, default=False) if allow_write_error: return allow_write_error if not allow_write: return invalid_argument(method, "allow_saved_state_write", "Saved-state structural move planning is opt-in; pass allow_saved_state_write=true.") include_payload, include_payload_error = strict_bool_argument(payload, "include_payload", method=method, default=False) if include_payload_error: return include_payload_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error table = str(payload.get("table") or "ConfigCASSave") if table not in FORM_ELEMENT_SAVED_STATE_TABLES: return invalid_argument(method, "table", "Saved-state target move only targets saved-state tables.", allowed_values=sorted(FORM_ELEMENT_SAVED_STATE_TABLES)) from_selector = form_move_target_selector(payload, "from") to_selector = form_move_target_selector(payload, "to") or form_move_target_selector(payload, "with") if not to_selector and payload.get("after_element"): to_selector = {"element": str(payload.get("after_element"))} if not from_selector or not to_selector: return invalid_argument(method, "from/to", "Pass from_element/from_path and to_element/to_path, with_element, or after_element.") working_payload = dict(payload) owner_kind = canonical_kind(str(first_non_empty_arg(working_payload, "kind", "object_type") or "")) owner_name = str(first_non_empty_arg(working_payload, "name", "object_name") or "").strip() explicit_form_name = str(first_non_empty_arg(working_payload, "form", "form_name", "name_filter") or "").strip() is_common_form_request = owner_kind == "CommonForm" or ( owner_kind in {None, "", "Form"} and not explicit_form_name and bool(owner_name) ) legacy_guid_is_form = bool( working_payload.get("guid") and not working_payload.get("object_guid") and not owner_kind and not owner_name and not explicit_form_name ) concrete_form_guid = str( first_non_empty_arg( working_payload, "form_guid", *(("guid", "object_guid") if is_common_form_request else ("guid",) if legacy_guid_is_form else ()), ) or "" ).strip() if not (working_payload.get("file_name") or concrete_form_guid): resolved = metadata_form_write_target_resolve( { **working_payload, **from_selector, "base_id": base_id, "table": table, "timeout_seconds": int(timeout_seconds or 30), } ) if resolved.get("status") != "ok": result = dict(resolved) result["method"] = method return result source = resolved.get("source") if isinstance(resolved.get("source"), dict) else {} working_payload["file_name"] = source.get("file_name") if source.get("table") in FORM_ELEMENT_SAVED_STATE_TABLES: table = str(source.get("table")) decode_payload = without_form_decode_selector_keys(working_payload) if working_payload.get("file_name"): for owner_selector_key in ("ref", "kind", "name", "guid", "object_type", "object_name", "object_guid"): decode_payload.pop(owner_selector_key, None) if concrete_form_guid: decode_payload["form_guid"] = concrete_form_guid decode_payload.pop("guid", None) decoded = metadata_form_decode( { **decode_payload, "base_id": base_id, "table": table, "include_storage": True, "include_parameters": True, "max_items": int(payload.get("max_items") or 5000), "timeout_seconds": int(timeout_seconds or 30), } ) if decoded.get("status") != "ok": result = dict(decoded) result["method"] = method return result source = decoded.get("source") if isinstance(decoded.get("source"), dict) else {} file_name = str(source.get("file_name") or (decoded.get("form") or {}).get("file_name") or working_payload.get("file_name") or "") if not file_name: return invalid_argument(method, "file_name", "Could not resolve saved-state form source file_name.") profile = decoded.get("profile") if isinstance(decoded.get("profile"), dict) else {} targets = [target for target in form_profile_write_targets(profile) if str(target.get("_profile_section") or "") == "items"] from_items = filter_form_profile_write_targets(targets, from_selector) to_items = filter_form_profile_write_targets(targets, to_selector) if len(from_items) != 1 or len(to_items) != 1: return { "schema": "onec_adapter_request_error.v1", "method": method, "status": "not_found" if not from_items or not to_items else "ambiguous", "error": "move_targets_not_resolved", "diagnostics": {"message": "Move requires exactly one source form item and exactly one destination/peer form item."}, "from_candidates": form_write_target_candidates(profile, from_selector, limit=20), "to_candidates": form_write_target_candidates(profile, to_selector, limit=20), "counts": {"from_matches": len(from_items), "to_matches": len(to_items)}, } from_item = from_items[0] to_item = to_items[0] from_path = str(from_item.get("path") or "") to_path = str(to_item.get("path") or "") if not from_path or not to_path or from_path == to_path: return invalid_argument(method, "from/to", "Move targets must resolve to two different form item paths.") if form_move_path_parent(from_path) != form_move_path_parent(to_path): return { "schema": "onec_adapter_request_error.v1", "method": method, "status": "unsupported", "error": "cross_parent_move_not_supported", "diagnostics": {"message": "Current structural writer supports only sibling slot swaps in the same parent container."}, "from": form_write_target_public(from_item), "to": form_write_target_public(to_item), } proposal = changes_propose( { "base_id": base_id, "source": { "base_id": base_id, "table": table, "file_name": file_name, **({"expected_sha1": payload.get("expected_sha1")} if payload.get("expected_sha1") else {}), }, "edits": [{"swap_paths": [from_path, to_path]}], "include_text": bool(payload.get("include_text") is True), "include_payload": bool(include_payload or mode in {"apply", "apply_and_verify", "apply_and_rollback"}), "preserve_format": True, "timeout_seconds": int(timeout_seconds or 30), "summary": payload.get("summary") or "Saved-state form target move proposal", } ) if proposal.get("status") not in {"accepted_for_review", "ok"}: return proposal proposal = dict(proposal) proposal["method"] = method proposal["operation"] = "swap_sibling_slots" proposal["write_mode"] = { "requested": "saved_state", "target_table": table, "sql_write_performed": False, "requires_apply_gate": True, } proposal["form"] = decoded.get("form") proposal["move"] = { "operation": "swap_sibling_slots", "from": form_write_target_public(from_item), "to": form_write_target_public(to_item), "from_path": from_path, "to_path": to_path, } proposal["target_moves"] = [ { "target": form_write_target_public(from_item), "old_path": from_path, "new_path": to_path, "presentation": f"{from_item.get('name') or from_path}: {from_path} -> {to_path}", }, { "target": form_write_target_public(to_item), "old_path": to_path, "new_path": from_path, "presentation": f"{to_item.get('name') or to_path}: {to_path} -> {from_path}", }, ] proposal["diagnostics"] = { **(proposal.get("diagnostics") if isinstance(proposal.get("diagnostics"), dict) else {}), "note": "Saved-state structural move mode is configured as proposal-only. No SQL rows were updated.", } result: dict[str, Any] = { "schema": "onec_form_target_move_result.v1", "method": method, "status": "planned", "execution_mode": mode, "base_id": base_id, "proposal": proposal if mode == "plan" else sanitize_proposal_for_response(proposal), } if mode == "plan": return proposal allow_apply, allow_apply_error = strict_bool_argument(payload, "allow_sql_saved_state_apply", method=method, default=False) if allow_apply_error: return allow_apply_error if not allow_apply: return invalid_argument(method, "allow_sql_saved_state_apply", "Apply mode is opt-in; pass allow_sql_saved_state_apply=true.") apply_result = storage_saved_state_apply_proposal( { **repository_write_context(payload), "base_id": base_id, "allow_sql_saved_state_apply": True, "proposal": proposal, "timeout_seconds": int(timeout_seconds or 30), } ) result["apply_result"] = apply_result result["status"] = apply_result.get("status") or "error" result["applied"] = bool(apply_result.get("applied")) if mode in {"apply", "apply_and_verify"}: return result allow_rollback, allow_rollback_error = strict_bool_argument(payload, "allow_sql_saved_state_rollback", method=method, default=False) if allow_rollback_error: return allow_rollback_error if not allow_rollback: return invalid_argument(method, "allow_sql_saved_state_rollback", "apply_and_rollback mode is opt-in; pass allow_sql_saved_state_rollback=true.") backup_id = ((apply_result.get("backup") or {}).get("backup_id") if isinstance(apply_result.get("backup"), dict) else None) if not backup_id: result["status"] = "rollback_unavailable" result["diagnostics"] = {"message": "Apply result did not return backup.backup_id; cannot rollback automatically."} return result rollback_result = storage_saved_state_rollback( { **repository_write_context(payload), "base_id": base_id, "allow_sql_saved_state_rollback": True, "backup_id": backup_id, "timeout_seconds": int(timeout_seconds or 30), } ) result["rollback_result"] = rollback_result result["rolled_back"] = bool(rollback_result.get("applied")) result["status"] = "verified_and_rolled_back" if result["applied"] and result["rolled_back"] else result["status"] return result def clone_form_structural_node(node: Any, replacements: dict[str, str]) -> Any: if isinstance(node, dict): cloned = {key: clone_form_structural_node(value, replacements) for key, value in node.items() if key not in {"pos", "end"}} if cloned.get("type") in {"atom", "string"}: value = str(cloned.get("value") or "") if value and value in replacements: cloned["value"] = replacements[value] else: for old, new in replacements.items(): if old and value.startswith(f"{old}РасширеннаяПодсказка"): cloned["value"] = value.replace(old, new, 1) break return cloned if isinstance(node, list): return [clone_form_structural_node(item, replacements) for item in node] return node def form_command_guid_from_profile(profile: dict[str, Any], command_name: str) -> str | None: wanted = normalize(command_name) for link in profile.get("command_links") or []: if not isinstance(link, dict): continue if normalize(link.get("command")) == wanted and link.get("command_guid"): return str(link.get("command_guid") or "").lower() for command in profile.get("commands") or []: if not isinstance(command, dict) or normalize(command.get("name")) != wanted: continue for value in command.get("guids_sample") or []: text = str(value or "").lower() if re.fullmatch(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", text): return text return None def form_profile_command_by_name(profile: dict[str, Any], command_name: str) -> dict[str, Any] | None: wanted = normalize(command_name) for command in profile.get("commands") or []: if isinstance(command, dict) and normalize(command.get("name")) == wanted: return command return None def form_profile_button_by_name(profile: dict[str, Any], button_name: str) -> dict[str, Any] | None: wanted = normalize(button_name) for item in profile.get("items") or []: if isinstance(item, dict) and normalize(item.get("name")) == wanted: return item return None def form_command_button_semantic_verify( *, base_id: str, table: str, file_name: str, command_name: str, button_name: str, handler_name: str, timeout_seconds: int, include_storage: bool = False, ) -> dict[str, Any]: decoded = metadata_form_decode( { "base_id": base_id, "table": table, "file_name": file_name, "include_module": True, "include_module_text": False, "include_storage": True, "evidence_mode": "none", "max_items": 5000, "timeout_seconds": int(timeout_seconds or 30), } ) result: dict[str, Any] = { "schema": "onec_form_command_button_verify.v1", "method": "metadata.form.command_button.verify", "status": "ok" if decoded.get("status") == "ok" else decoded.get("status") or "error", "base_id": base_id, "source": { "table": table, "file_name": file_name, **(decoded.get("source") if include_storage and isinstance(decoded.get("source"), dict) else {}), }, "expected": {"command": command_name, "button": button_name, "handler": handler_name}, } if decoded.get("status") != "ok": result["diagnostics"] = decoded.get("diagnostics") return result profile = decoded.get("profile") if isinstance(decoded.get("profile"), dict) else {} command = form_profile_command_by_name(profile, command_name) button = form_profile_button_by_name(profile, button_name) routines = (profile.get("module") or {}).get("routines_sample") or [] handler = next((item for item in routines if isinstance(item, dict) and normalize(item.get("name")) == normalize(handler_name)), None) command_link = next((item for item in profile.get("command_links") or [] if isinstance(item, dict) and normalize(item.get("command")) == normalize(command_name)), None) button_link = next((item for item in profile.get("button_command_links") or [] if isinstance(item, dict) and normalize(item.get("button")) == normalize(button_name)), None) checks = { "command": bool(command), "button": bool(button), "handler": bool(handler), "command_handler_link": bool(command_link and normalize(command_link.get("handler")) == normalize(handler_name)), "button_command_link": bool(button_link and normalize(button_link.get("command")) == normalize(command_name)), } result.update( { "verified": all(checks.values()), "checks": checks, "command": {"name": command.get("name"), "path": command.get("path"), "form_path": command.get("path"), "title": command.get("title")} if command else None, "button": {"name": button.get("name"), "path": button.get("path"), "form_path": button.get("path"), "title": button.get("title"), "type_name": button.get("type_name")} if button else None, "handler": handler, "links": {"command": command_link, "button": button_link}, "counts": (profile.get("counts") or {}), } ) if not result["verified"]: result["status"] = "not_verified" return result def code_index_refresh_form_embedded_module( *, base_id: str, table: str, file_name: str, timeout_seconds: int, ) -> dict[str, Any]: method = "metadata.code_index.refresh_form_module" config, config_error = sql_config_for_base(base_id) if not config: return public_error_result({"status": "source_missing", "diagnostics": config_error}, include_storage=False, method=method) data, _read_config, read_error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=int(timeout_seconds or 30)) if read_error: result = dict(read_error) result["method"] = method return result decoded = metadata_form_decode( { "base_id": base_id, "table": table, "file_name": file_name, "include_module": True, "include_module_text": True, "include_storage": False, "evidence_mode": "none", "max_items": 1, "timeout_seconds": int(timeout_seconds or 30), } ) if decoded.get("status") != "ok": return {"schema": "onec_code_index_refresh_form_module.v1", "method": method, "status": decoded.get("status") or "error", "diagnostics": decoded.get("diagnostics")} profile = decoded.get("profile") if isinstance(decoded.get("profile"), dict) else {} module = profile.get("module") if isinstance(profile.get("module"), dict) else {} text = str(module.get("text") or "") if not text.strip(): return {"schema": "onec_code_index_refresh_form_module.v1", "method": method, "status": "not_found", "diagnostics": {"message": "Embedded form module text was not found."}} module_ref = f"{table}:{file_name}" owner = code_module_owner_from_cache(config, module_ref) form = decoded.get("form") if isinstance(decoded.get("form"), dict) else {} if not owner.get("form_name"): owner = {**owner, "owner_kind": owner.get("owner_kind") or "CommonForm", "owner_name": owner.get("owner_name") or form.get("name"), "owner_guid": owner.get("owner_guid") or form.get("guid"), "form_name": owner.get("form_name") or form.get("name")} indexed = code_index_upsert( config, base_id=base_id, table=table, file_name=file_name, module_ref=module_ref, data=data or b"", text=text, owner=owner, bsl_offset=None, stream_index=None, verified=True, ) vector_chunks = code_vector_upsert_chunks(config, {**indexed, "module_ref": module_ref, "text": text, "payload_sha1": indexed.get("payload_sha1"), "text_sha1": indexed.get("text_sha1")}) return { "schema": "onec_code_index_refresh_form_module.v1", "method": method, "status": "updated", "base_id": base_id, "module_ref": module_ref, "payload_sha1": indexed.get("payload_sha1"), "text_sha1": indexed.get("text_sha1"), "routine_count": indexed.get("routine_count"), "vector_chunks": vector_chunks, } def compact_saved_state_form_search_result(saved_state: dict[str, Any] | None, *, selected_form: dict[str, Any] | None = None, include_storage: bool = False) -> dict[str, Any] | None: if not isinstance(saved_state, dict): return None compact = { "schema": saved_state.get("schema"), "status": saved_state.get("status"), "base_id": saved_state.get("base_id"), "query": saved_state.get("query"), "counts": saved_state.get("counts"), } if selected_form is not None: compact["selected_form"] = selected_form if include_storage: return compact return strip_storage_traces(compact) def compact_saved_state_target(target: dict[str, Any] | None, *, include_storage: bool = False) -> dict[str, Any] | None: if not isinstance(target, dict): return None compact = { "table": target.get("table"), "form": (target.get("form") or {}).get("name") if isinstance(target.get("form"), dict) else None, } if include_storage: compact["file_name"] = target.get("file_name") if isinstance(target.get("form"), dict): compact["selected_form"] = target.get("form") return compact def metadata_form_command_button_verify(payload: dict[str, Any]) -> dict[str, Any]: method = FORM_COMMAND_BUTTON_VERIFY_METHOD payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error string_error = validate_optional_string_arguments( payload, method, [ "extension", "extension_guid", "kind", "object_type", "name", "object_name", "guid", "object_guid", "form", "form_name", "name_filter", "command", "command_name", "command_title", "command_action", "handler", "handler_name", "button", "button_name", "table", "file_name", "form_guid", "state", "source_state", ], ) if string_error: return string_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error include_storage, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error owner_kind = canonical_kind(str(payload.get("kind") or payload.get("object_type") or "")) or str(payload.get("kind") or payload.get("object_type") or "") owner_name = str(payload.get("name") or payload.get("object_name") or "").strip() explicit_form_name = str(first_non_empty_arg(payload, "form", "form_name", "name_filter") or "").strip() form_name = explicit_form_name or (owner_name if owner_kind == "CommonForm" else "") if owner_name and owner_kind != "CommonForm" and not form_name: return invalid_argument( method, "form_name", "Pass form or form_name when verifying a command in a nested owner form.", ) command_name = str(first_non_empty_arg(payload, "command_name", "command") or "").strip() if not command_name: return invalid_argument(method, "command_name", "Pass command_name, for example РасчетС.") button_name = str(first_non_empty_arg(payload, "button_name", "button") or command_name).strip() handler_name = str(first_non_empty_arg(payload, "handler_name", "command_action", "handler") or command_name).strip() table = str(payload.get("table") or "").strip() file_name = str(payload.get("file_name") or "").strip() if table and table not in FORM_ELEMENT_SAVED_STATE_TABLES | {"Config", "ConfigCAS"}: return invalid_argument(method, "table", "Unsupported form storage table.", allowed_values=sorted(STORAGE_TABLES)) if file_name and Path(file_name).name != file_name: return invalid_argument(method, "file_name", "file_name must be a storage file name, not a path.") saved_state = None selected_saved_form = None if not file_name: saved_state_query = { "base_id": base_id, "limit": 20, "scan_limit": int(payload.get("scan_limit") or 5000), "timeout_seconds": int(timeout_seconds or 30), "include_storage": True, **({"form": form_name, "query": form_name} if form_name else {}), **({"extension": str(payload.get("extension") or "").strip()} if payload.get("extension") else {}), "tables": ["ConfigCASSave"] if payload.get("extension") else ["ConfigCASSave", "ConfigSave"], } saved_state = metadata_saved_state_forms_search(saved_state_query) if saved_state.get("status") != "ok": result = dict(saved_state) result["method"] = method return result for item in saved_state.get("forms") or []: if not isinstance(item, dict): continue form_info = item.get("form") if isinstance(item.get("form"), dict) else {} item_name = str(item.get("name") or form_info.get("name") or "").strip() if form_name and item_name and normalize(item_name) != normalize(form_name): continue item_owner = item.get("owner") if isinstance(item.get("owner"), dict) else {} if owner_name and owner_kind != "CommonForm": if normalize(item_owner.get("name")) != normalize(owner_name): continue if owner_kind and canonical_kind(str(item_owner.get("kind") or "")) != owner_kind: continue source = item.get("source") if isinstance(item.get("source"), dict) else {} file_info = item.get("file") if isinstance(item.get("file"), dict) else {} candidate_table = str(source.get("table") or item.get("table") or "") candidate_file_name = str(source.get("file_name") or item.get("file_name") or file_info.get("FileName") or "") if not candidate_table and candidate_file_name: candidate_table = "ConfigCASSave" if payload.get("extension") else "ConfigSave" if candidate_table in FORM_ELEMENT_SAVED_STATE_TABLES and candidate_file_name and Path(candidate_file_name).name == candidate_file_name: table = candidate_table file_name = candidate_file_name selected_saved_form = item break if not table: table = "ConfigCASSave" if payload.get("extension") else "ConfigSave" if not file_name: return { "schema": "onec_form_command_button_verify.v1", "method": method, "status": "not_found", "base_id": base_id, "error": "saved_state_form_not_found", "query": { "owner": {"kind": owner_kind or None, "name": owner_name or None}, "form": form_name or None, "command": command_name, "extension": payload.get("extension"), }, **({"saved_state_search": compact_saved_state_form_search_result(saved_state, selected_form=selected_saved_form, include_storage=bool(include_storage))} if isinstance(saved_state, dict) else {}), } result = form_command_button_semantic_verify( base_id=base_id, table=table, file_name=file_name, command_name=command_name, button_name=button_name, handler_name=handler_name, timeout_seconds=int(timeout_seconds or 30), include_storage=True, ) if not include_storage: result["source"] = {"table": table} result["method"] = method result["query"] = { "owner": {"kind": owner_kind or None, "name": owner_name or None}, "form": form_name or None, "command": command_name, "button": button_name, "handler": handler_name, "extension": payload.get("extension"), } if isinstance(saved_state, dict): result["saved_state_search"] = compact_saved_state_form_search_result(saved_state, selected_form=selected_saved_form, include_storage=bool(include_storage)) return result def first_form_command_template(profile: dict[str, Any]) -> dict[str, Any] | None: for command in profile.get("commands") or []: if isinstance(command, dict) and command.get("path") and command.get("name"): return command return None def first_form_button_template(profile: dict[str, Any]) -> dict[str, Any] | None: for item in profile.get("items") or []: if isinstance(item, dict) and str(item.get("marker") or "") == "34" and item.get("path") and item.get("name"): return item return None def form_structural_parent_path(path: Any) -> str: parts = str(path or "").split(".") return ".".join(parts[:-1]) if len(parts) > 1 else "" def saved_state_prepare_likely_payload_file_name(result: dict[str, Any]) -> str | None: rows = [row for row in result.get("source_rows") or [] if isinstance(row, dict) and row.get("file_name")] if rows: row = max(rows, key=lambda item: int(item.get("data_size") or item.get("binary_bytes") or 0)) return str(row.get("file_name") or "") names = [str(name or "") for name in result.get("file_names") or [] if name] return names[-1] if names else None def default_form_command_handler_routine(handler_name: str) -> str: return f"&НаКлиенте\nПроцедура {handler_name}(Команда)\n\t// Вставить содержимое обработчика.\nКонецПроцедуры\n" def preserve_bsl_routine_directives(current_text: str, routine_text: str, routine_name: str) -> str: stripped = str(routine_text or "").lstrip("\ufeff \t\r\n") if stripped.startswith("&"): return routine_text try: from parser.bsl_validation import directive_start, dominant_eol, normalize_name, routine_blocks except Exception: return routine_text wanted = normalize_name(routine_name) matches = [block for block in routine_blocks(str(current_text or "")) if block.get("normalized_name") == wanted] if len(matches) != 1: return routine_text declaration_start = int(matches[0].get("declaration_start") or matches[0].get("start") or 0) start = directive_start(current_text, declaration_start) if start >= declaration_start: return routine_text prefix = current_text[start:declaration_start] if not any(line.strip().startswith("&") for line in prefix.splitlines()): return routine_text eol = dominant_eol(current_text) prefix = prefix.rstrip("\r\n") if not prefix: return routine_text return prefix + eol + str(routine_text or "").lstrip("\r\n") def form_command_handler_write_payload(payload: dict[str, Any], *, base_id: str, table: str, file_name: str, handler_name: str, mode: str, timeout_seconds: int) -> dict[str, Any]: result = { "base_id": base_id, "module_ref": f"{table}:{file_name}#stream:{int(payload.get('module_stream_index') or 0)}", "mode": mode, "allow_saved_state_write": True, "routine_name": handler_name, "routine_operation": str(payload.get("handler_routine_operation") or payload.get("routine_operation") or "upsert"), "routine_text": str(payload.get("handler_routine_text") or payload.get("routine_text") or default_form_command_handler_routine(handler_name)), "timeout_seconds": timeout_seconds, } for key in ("allow_sql_saved_state_apply", "allow_sql_saved_state_rollback"): if key in payload: result[key] = payload.get(key) return result def form_embedded_module_handler_write_apply( payload: dict[str, Any], *, base_id: str, table: str, file_name: str, handler_name: str, mode: str, timeout_seconds: int, method_name: str = FORM_COMMAND_BUTTON_WRITE_METHOD, ) -> dict[str, Any]: method = method_name module_path = str(payload.get("module_path") or "2") operation_kind = str(payload.get("_embedded_form_module_operation") or "").strip().casefold() has_handler_routine_text = payload.get("handler_routine_text") is not None has_routine_text = payload.get("routine_text") is not None or has_handler_routine_text has_module_text = payload.get("module_text") is not None or (operation_kind == "module_text" and payload.get("text") is not None) has_fragment = payload.get("old") is not None or payload.get("new") is not None routine_operation = str(payload.get("handler_routine_operation") or payload.get("routine_operation") or "upsert") try: from parser.bsl_validation import replace_routine_text from parser.payload import decode_payload_lossless, get_tree_path, parse_brace_text, patch_brace_text_path, scalar except Exception as exc: return {"schema": "onec_form_embedded_module_write.v1", "status": "error", "diagnostics": {"message": f"Payload codec is unavailable: {exc}"}} data, _config, read_error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=timeout_seconds) if read_error: result = dict(read_error) result["method"] = method return result try: decoded = decode_payload_lossless(data or b"") tree = parse_brace_text(str(decoded.get("text") or "")) current_text = scalar(get_tree_path(tree, module_path)) if has_fragment: if payload.get("old") is None or payload.get("new") is None: return invalid_argument(method, "old/new", "Pass both old and new for an embedded form module fragment replacement.") old_fragment = str(payload.get("old") or "") if old_fragment == "": return invalid_argument(method, "old", "old must be a non-empty string for fragment replacement.") new_fragment = str(payload.get("new") or "") fragment_scope = "module" routine_selection = None search_text = current_text if handler_name: routine_text_for_fragment, routine_selection = _extract_bsl_routine_text_for_code_read(current_text, handler_name) if not routine_text_for_fragment: return { "schema": "onec_form_embedded_module_write.v1", "method": method, "status": "not_found", "base_id": base_id, "error": "routine_not_found", "diagnostics": {"message": f"Routine `{handler_name}` was not found in the current saved form module text."}, } search_text = routine_text_for_fragment fragment_scope = "routine" occurrence_count = search_text.count(old_fragment) if occurrence_count != 1: scope_label = "current routine text" if fragment_scope == "routine" else "current saved module text" return { "schema": "onec_form_embedded_module_write.v1", "method": method, "status": "not_found" if occurrence_count == 0 else "ambiguous", "base_id": base_id, "error": "fragment_not_found" if occurrence_count == 0 else "ambiguous_fragment", "diagnostics": { "message": f"Fragment replacement requires old to occur exactly once in the {scope_label}.", }, "counts": {"occurrences": occurrence_count}, "scope": {"kind": fragment_scope, **({"routine_name": handler_name} if handler_name else {})}, } if routine_selection: patched_routine_text = search_text.replace(old_fragment, new_fragment, 1) patched_routine_text = preserve_bsl_routine_directives(current_text, patched_routine_text, handler_name) new_text, routine_edit = replace_routine_text(current_text, patched_routine_text, operation="replace") if isinstance(routine_edit, dict): routine_edit.update({"status": "fragment_replaced", "operation": "fragment_replace", "occurrences": occurrence_count, "scope": fragment_scope}) else: new_text = current_text.replace(old_fragment, new_fragment, 1) routine_edit = {"status": "fragment_replaced", "operation": "fragment_replace", "occurrences": occurrence_count, "scope": fragment_scope} elif has_module_text: new_text = str(payload.get("module_text") if payload.get("module_text") is not None else payload.get("text") or "") new_text = preserve_form_embedded_module_suffix(current_text, new_text) routine_edit = {"status": "module_replaced", "operation": "module_text_replace"} else: routine_text = str( payload.get("handler_routine_text") if has_handler_routine_text else (payload.get("routine_text") if has_routine_text else default_form_command_handler_routine(handler_name)) ) routine_text = preserve_bsl_routine_directives(current_text, routine_text, handler_name) new_text, routine_edit = replace_routine_text(current_text, routine_text, operation=routine_operation) except Exception as exc: return {"schema": "onec_form_embedded_module_write.v1", "status": "error", "base_id": base_id, "diagnostics": {"message": str(exc)}} summary_operation = str(routine_edit.get("operation") or routine_edit.get("status") or "module_edit") if isinstance(routine_edit, dict) else "module_edit" proposal = changes_propose( { "base_id": base_id, "source": {"base_id": base_id, "table": table, "file_name": file_name}, "edits": [{"path": module_path, "value": new_text, "expected_old": current_text}], "preserve_format": True, "include_payload": bool(mode in {"apply", "apply_and_verify", "apply_and_rollback"}), "timeout_seconds": timeout_seconds, "summary": payload.get("handler_summary") or f"Embedded form module {summary_operation}", } ) result = { "schema": "onec_form_embedded_module_write.v1", "method": method, "status": "planned", "execution_mode": mode, "base_id": base_id, "source": {"table": table, "file_name": file_name, "module_path": module_path}, "routine": routine_edit, "proposal": proposal if mode == "plan" else sanitize_proposal_for_response(proposal), } if proposal.get("status") not in {"accepted_for_review", "ok"}: result["status"] = proposal.get("status") or "error" result["diagnostics"] = proposal.get("diagnostics") return result if mode == "plan": return result proposal_edits = proposal.get("edits") if isinstance(proposal.get("edits"), list) else [] if not proposal_edits or any((edit or {}).get("mode") != "path_preserve_format" for edit in proposal_edits if isinstance(edit, dict)): return { "schema": "onec_form_embedded_module_write.v1", "method": method, "status": "blocked", "base_id": base_id, "error": "unsafe_form_module_payload_write", "diagnostics": {"message": "Embedded form module writes must use byte-preserving path_preserve_format edits only."}, "proposal": sanitize_proposal_for_response(proposal), } encoded_hex = (((proposal.get("encoded") or {}).get("payload_hex")) if isinstance(proposal.get("encoded"), dict) else None) if not encoded_hex: return { "schema": "onec_form_embedded_module_write.v1", "method": method, "status": "blocked", "base_id": base_id, "error": "unsafe_form_module_payload_write", "diagnostics": {"message": "Embedded form module apply requires encoded payload evidence for byte-preserving verification."}, "proposal": sanitize_proposal_for_response(proposal), } try: expected_text, _patch_info = patch_brace_text_path(str(decoded.get("text") or ""), module_path, new_text) encoded_text = str(decode_payload_lossless(bytes.fromhex(str(encoded_hex))).get("text") or "") except Exception as exc: return { "schema": "onec_form_embedded_module_write.v1", "method": method, "status": "blocked", "base_id": base_id, "error": "unsafe_form_module_payload_write", "diagnostics": {"message": f"Embedded form module payload verification failed: {exc}"}, "proposal": sanitize_proposal_for_response(proposal), } if encoded_text != expected_text: return { "schema": "onec_form_embedded_module_write.v1", "method": method, "status": "blocked", "base_id": base_id, "error": "unsafe_form_module_payload_write", "diagnostics": {"message": "Encoded payload changed more than the embedded module string token; refusing to apply."}, "proposal": sanitize_proposal_for_response(proposal), } allow_apply, allow_apply_error = strict_bool_argument(payload, "allow_sql_saved_state_apply", method=method, default=False) if allow_apply_error: return allow_apply_error if not allow_apply: return invalid_argument(method, "allow_sql_saved_state_apply", "Apply mode is opt-in; pass allow_sql_saved_state_apply=true.") apply_result = storage_saved_state_apply_proposal( { **repository_write_context(payload), "base_id": base_id, "allow_sql_saved_state_apply": True, "proposal": proposal, "timeout_seconds": timeout_seconds, } ) result["apply_result"] = apply_result result["status"] = apply_result.get("status") or "error" result["applied"] = bool(apply_result.get("applied")) if mode == "apply": return result if mode == "apply_and_verify": readback = apply_result.get("readback") if isinstance(apply_result.get("readback"), dict) else {} if result["applied"] and readback.get("verified") is not False: result["status"] = "verified" return result allow_rollback, allow_rollback_error = strict_bool_argument(payload, "allow_sql_saved_state_rollback", method=method, default=False) if allow_rollback_error: return allow_rollback_error if not allow_rollback: return invalid_argument(method, "allow_sql_saved_state_rollback", "apply_and_rollback mode is opt-in; pass allow_sql_saved_state_rollback=true.") backup_id = ((apply_result.get("backup") or {}).get("backup_id") if isinstance(apply_result.get("backup"), dict) else None) if not backup_id: result["status"] = "rollback_unavailable" result["diagnostics"] = {"message": "Apply result did not return backup.backup_id; cannot rollback automatically."} return result rollback_result = storage_saved_state_rollback( { **repository_write_context(payload), "base_id": base_id, "allow_sql_saved_state_rollback": True, "backup_id": backup_id, "timeout_seconds": timeout_seconds, } ) result["rollback_result"] = rollback_result result["rolled_back"] = bool(rollback_result.get("applied")) if result["applied"] and result["rolled_back"]: result["status"] = "verified_and_rolled_back" elif result["applied"]: result["status"] = "applied_rollback_failed" return result def metadata_form_command_button_write(payload: dict[str, Any]) -> dict[str, Any]: method = FORM_COMMAND_BUTTON_WRITE_METHOD payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error string_error = validate_optional_string_arguments( payload, method, [ "extension", "extension_guid", "kind", "object_type", "name", "object_name", "guid", "object_guid", "form", "form_name", "name_filter", "command", "command_name", "command_title", "command_action", "handler", "handler_name", "handler_routine_operation", "handler_routine_text", "routine_operation", "routine_text", "button", "button_name", "button_title", "button_parent", "button_parent_name", "parent", "table", "file_name", "form_guid", "execution_mode", "mode", ], ) if string_error: return string_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error allow_saved_state_write, allow_error = strict_bool_argument(payload, "allow_saved_state_write", method=method, default=False) if allow_error: return allow_error include_handler, include_handler_error = strict_bool_argument(payload, "include_handler", method=method, default=True) if include_handler_error: return include_handler_error include_storage, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error mode = str(payload.get("execution_mode") or payload.get("mode") or "plan").strip().casefold() if mode not in FORM_ELEMENT_WRITE_APPLY_MODES: return invalid_argument(method, "execution_mode", "Unsupported execution mode.", allowed_values=sorted(FORM_ELEMENT_WRITE_APPLY_MODES)) repository_error = repository_apply_gate(payload, method, mode) if repository_error: return repository_error requested_kind = canonical_kind(str(first_non_empty_arg(payload, "kind", "object_type") or "")) owner_name = str(first_non_empty_arg(payload, "name", "object_name") or "").strip() explicit_form_name = str(first_non_empty_arg(payload, "form", "form_name", "name_filter") or "").strip() is_common_form_request = requested_kind == "CommonForm" or ( requested_kind in {None, "", "Form"} and not explicit_form_name and bool(owner_name) ) form_name = explicit_form_name or (owner_name if is_common_form_request else "") if owner_name and not is_common_form_request and not form_name: return invalid_argument( method, "form_name", "Pass form or form_name when writing a command to a nested owner form.", ) command_name = str(first_non_empty_arg(payload, "command_name", "command") or "").strip() if not command_name: return invalid_argument(method, "command_name", "Pass command_name, for example РасчетС.") command_title = str(first_non_empty_arg(payload, "command_title", "title") or command_name).strip() command_action = str(first_non_empty_arg(payload, "command_action", "handler_name", "handler") or command_name).strip() button_name = str(first_non_empty_arg(payload, "button_name", "button") or command_name).strip() button_title = str(first_non_empty_arg(payload, "button_title") or command_title).strip() button_parent = str(first_non_empty_arg(payload, "button_parent_name", "button_parent", "parent") or "ФормаКоманднаяПанель").strip() target = { "kind": "CommonForm" if is_common_form_request else (requested_kind or None), "form": form_name or None, "object": None if is_common_form_request else owner_name or None, "object_guid": None if is_common_form_request else str(first_non_empty_arg(payload, "guid", "object_guid") or "").strip().lower() or None, "extension": str(payload.get("extension") or "").strip() or None, "extension_guid": str(payload.get("extension_guid") or "").strip() or None, "table": str(payload.get("table") or "").strip() or None, "file_name": str(payload.get("file_name") or "").strip() or None, "form_guid": str( first_non_empty_arg( payload, "form_guid", *(("guid", "object_guid") if is_common_form_request else ()), ) or "" ).strip().lower() or None, } saved_state_query = { "base_id": base_id, "limit": 10, "scan_limit": 5000, "timeout_seconds": int(timeout_seconds or 30), "include_storage": True, **({"form": form_name} if form_name else {}), **({"query": form_name} if form_name else {}), **({"extension": target["extension"]} if target.get("extension") else {}), } if target["file_name"]: saved_state_query["prefix"] = target["file_name"] saved_state = metadata_saved_state_forms_search(saved_state_query) saved_forms = saved_state.get("forms") if saved_state.get("status") == "ok" else [] concrete_saved_state = None selected_saved_form = None if isinstance(saved_forms, list): for item in saved_forms: if not isinstance(item, dict): continue item_owner = item.get("owner") if isinstance(item.get("owner"), dict) else {} if owner_name and not is_common_form_request: if normalize(item_owner.get("name")) != normalize(owner_name): continue if requested_kind and canonical_kind(str(item_owner.get("kind") or "")) != requested_kind: continue source = item.get("source") if isinstance(item.get("source"), dict) else {} table = str(source.get("table") or item.get("table") or "") file_name = str(source.get("file_name") or item.get("file_name") or "") if table in FORM_ELEMENT_SAVED_STATE_TABLES and file_name: concrete_saved_state = {"table": table, "file_name": file_name, "form": item} selected_saved_form = item break if concrete_saved_state is None and target["table"] in FORM_ELEMENT_SAVED_STATE_TABLES and target["file_name"]: concrete_saved_state = {"table": str(target["table"]), "file_name": str(target["file_name"]), "form": {"source": {"table": target["table"], "file_name": target["file_name"]}}} selected_saved_form = concrete_saved_state["form"] def finalize_command_button_write_result(write_result: dict[str, Any]) -> dict[str, Any]: if isinstance(saved_state, dict): write_result["saved_state_search"] = compact_saved_state_form_search_result(saved_state, selected_form=selected_saved_form, include_storage=bool(include_storage)) if isinstance(write_result.get("saved_state_target"), dict): write_result["saved_state_target"] = compact_saved_state_target(concrete_saved_state, include_storage=bool(include_storage)) return write_result workflow_payload = { "operation": "upsert", "command_name": command_name, "command_title": command_title, "command_action": command_action, "button_parent_name": button_parent, "button_name": button_name, "button_title": button_title, "call_type": "Override", "button_type": "CommandBarButton", } result: dict[str, Any] = { "schema": "onec_form_command_button_write.v1", "status": "planned" if concrete_saved_state and allow_saved_state_write and mode == "plan" else "blocked", "method": method, "base_id": base_id, "target": target, "operation": { "class": "add_form_command_button", "command": {"name": command_name, "title": command_title, "action": command_action}, "button": {"name": button_name, "title": button_title, "parent_name": button_parent, "type": "CommandBarButton"}, }, "workflow_payload": workflow_payload, "saved_state_search": compact_saved_state_form_search_result(saved_state, selected_form=selected_saved_form, include_storage=bool(include_storage)), "counts": { "saved_state_forms": len(saved_forms or []) if isinstance(saved_forms, list) else 0, "has_concrete_saved_state": bool(concrete_saved_state), }, } if concrete_saved_state: result["saved_state_target"] = compact_saved_state_target(concrete_saved_state, include_storage=bool(include_storage)) if not allow_saved_state_write: result["diagnostics"] = { "message": "A saved-state form target exists. Pass allow_saved_state_write=true to build a reviewable structural append proposal.", "source_boundary": "The live adapter writes through SQL storage only; runtime apply targets ConfigSave/ConfigCASSave.", } return finalize_command_button_write_result(result) decoded = metadata_form_decode( { "base_id": base_id, "table": concrete_saved_state["table"], "file_name": concrete_saved_state["file_name"], "include_storage": True, "include_parameters": True, "max_items": int(payload.get("max_items") or 5000), "timeout_seconds": int(timeout_seconds or 30), } ) if decoded.get("status") != "ok": result.update({"status": decoded.get("status") or "error", "diagnostics": decoded.get("diagnostics"), "decode": decoded}) return finalize_command_button_write_result(result) profile = decoded.get("profile") if isinstance(decoded.get("profile"), dict) else {} existing_command = form_profile_command_by_name(profile, command_name) existing_button = form_profile_button_by_name(profile, button_name) command_guid = form_command_guid_from_profile(profile, command_name) if existing_command else None edits: list[dict[str, Any]] = [] structural: dict[str, Any] = { "operation": "upsert_form_command_button", "command_exists": bool(existing_command), "button_exists": bool(existing_button), } data, _config, read_error = read_storage_file_bytes(base_id, concrete_saved_state["table"], concrete_saved_state["file_name"], timeout_seconds=int(timeout_seconds or 30)) if read_error: result.update({"status": read_error.get("status") or "error", "diagnostics": read_error.get("diagnostics"), "read_error": read_error}) return finalize_command_button_write_result(result) try: from parser.payload import decode_payload_lossless, get_tree_path, parse_brace_text, serialize_brace_tree decoded_payload = decode_payload_lossless(data or b"") text = decoded_payload.get("text") if not text: raise ValueError("saved-state form payload is not a text brace payload") tree = parse_brace_text(text) if not existing_command: command_template = first_form_command_template(profile) if not command_template: return finalize_command_button_write_result({ **result, "status": "blocked", "error": "form_command_template_not_found", "diagnostics": {"message": "No existing form command template was found; cannot safely synthesize the first command yet."}, }) template_guid = form_command_guid_from_profile(profile, str(command_template.get("name") or "")) or "" command_guid = str(uuid.uuid4()).lower() old_title = str(command_template.get("title") or command_template.get("name") or "") old_action = "" for link in profile.get("command_links") or []: if isinstance(link, dict) and normalize(link.get("command")) == normalize(command_template.get("name")): old_action = str(link.get("handler") or "") break command_node = get_tree_path(tree, str(command_template["path"])) replacements = { str(command_template.get("name") or ""): command_name, old_title: command_title, old_action or str(command_template.get("name") or ""): command_action, template_guid: command_guid, } command_parent = form_structural_parent_path(command_template["path"]) edits.append({"append_child": {"parent_path": command_parent, "node_text": serialize_brace_tree(clone_form_structural_node(command_node, replacements))}}) structural["command_append"] = {"parent_path": command_parent, "guid": command_guid} if not existing_button: button_template = first_form_button_template(profile) if not button_template: return finalize_command_button_write_result({ **result, "status": "blocked", "error": "form_button_template_not_found", "diagnostics": {"message": "No existing command button template was found; cannot safely synthesize the first command button yet."}, }) if not command_guid: command_guid = form_command_guid_from_profile(profile, command_name) or str(uuid.uuid4()).lower() template_button_command_guid = None for link in profile.get("button_command_links") or []: if isinstance(link, dict) and normalize(link.get("button")) == normalize(button_template.get("name")): template_button_command_guid = str(link.get("command_guid") or "").lower() break button_node = get_tree_path(tree, str(button_template["path"])) replacements = { str(button_template.get("name") or ""): button_name, str(button_template.get("title") or button_template.get("name") or ""): button_title, template_button_command_guid or "": command_guid, } button_parent = form_structural_parent_path(button_template["path"]) edits.append({"append_child": {"parent_path": button_parent, "node_text": serialize_brace_tree(clone_form_structural_node(button_node, replacements))}}) structural["button_append"] = {"parent_path": button_parent, "command_guid": command_guid} except Exception as exc: result.update({"status": "error", "error": "form_structural_proposal_failed", "diagnostics": {"message": str(exc)}}) return finalize_command_button_write_result(result) if not edits: handler_result = None if include_handler: handler_result = form_embedded_module_handler_write_apply( payload, base_id=base_id, table=concrete_saved_state["table"], file_name=concrete_saved_state["file_name"], handler_name=command_action, mode=mode, timeout_seconds=int(timeout_seconds or 30), ) semantic_verify = form_command_button_semantic_verify( base_id=base_id, table=concrete_saved_state["table"], file_name=concrete_saved_state["file_name"], command_name=command_name, button_name=button_name, handler_name=command_action, timeout_seconds=int(timeout_seconds or 30), include_storage=include_storage, ) code_index_refresh = None if mode in {"apply", "apply_and_verify"} and (not isinstance(handler_result, dict) or handler_result.get("applied") or handler_result.get("status") in {"applied", "verified"}): code_index_refresh = code_index_refresh_form_embedded_module( base_id=base_id, table=concrete_saved_state["table"], file_name=concrete_saved_state["file_name"], timeout_seconds=int(timeout_seconds or 30), ) result.update( { "status": handler_result.get("status") if isinstance(handler_result, dict) else ("already_exists" if semantic_verify.get("verified") else "ok"), "applied": False, "structural": structural, "idempotency": {"status": "already_exists", "command": command_name, "button": button_name}, "semantic_verify": semantic_verify, "diagnostics": {"message": "Command and button already exist; no structural edits are required."}, **({"handler_result": handler_result} if handler_result is not None else {}), **({"code_index_refresh": code_index_refresh} if code_index_refresh is not None else {}), } ) if semantic_verify.get("verified") and result["status"] == "applied": result["status"] = "verified" return finalize_command_button_write_result(result) proposal = changes_propose( { "base_id": base_id, "source": { "base_id": base_id, "table": concrete_saved_state["table"], "file_name": concrete_saved_state["file_name"], **({"expected_sha1": payload.get("expected_sha1")} if payload.get("expected_sha1") else {}), }, "edits": edits, "preserve_format": True, "include_payload": bool(mode in {"apply", "apply_and_verify", "apply_and_rollback"}), "include_text": bool(payload.get("include_text") is True), "timeout_seconds": int(timeout_seconds or 30), "summary": payload.get("summary") or f"Add form command/button {command_name}", } ) result.update({"status": "planned", "proposal": proposal, "structural": structural}) if proposal.get("status") not in {"accepted_for_review", "ok"}: result["status"] = proposal.get("status") or "error" result["diagnostics"] = proposal.get("diagnostics") return finalize_command_button_write_result(result) write_plan, write_plan_error = metadata_write_apply_plan_gate( method, { **payload, "intent": { **(payload.get("intent") if isinstance(payload.get("intent"), dict) else {}), "operation": "add_form_command_button", }, }, target_kind="form", target={"kind": "form", "table": concrete_saved_state["table"], "file_name": concrete_saved_state["file_name"]}, ) result["write_plan"] = write_plan if write_plan_error: result.update(write_plan_error) return finalize_command_button_write_result(result) handler_payload = None handler_result = None if include_handler: handler_payload = { "base_id": base_id, "table": concrete_saved_state["table"], "file_name": concrete_saved_state["file_name"], "handler_name": command_action, "mode": mode, "timeout_seconds": int(timeout_seconds or 30), } if mode == "plan": handler_result = form_embedded_module_handler_write_apply(payload, **handler_payload) result["handler_result"] = handler_result if mode == "plan": return finalize_command_button_write_result(result) allow_apply, allow_apply_error = strict_bool_argument(payload, "allow_sql_saved_state_apply", method=method, default=False) if allow_apply_error: return allow_apply_error if not allow_apply: return invalid_argument(method, "allow_sql_saved_state_apply", "Apply mode is opt-in; pass allow_sql_saved_state_apply=true.") apply_result = storage_saved_state_apply_proposal( { **repository_write_context(payload), "base_id": base_id, "allow_sql_saved_state_apply": True, "proposal": proposal, "timeout_seconds": int(timeout_seconds or 30), } ) result["proposal"] = sanitize_proposal_for_response(proposal) result["apply_result"] = apply_result result["status"] = apply_result.get("status") or "error" result["applied"] = bool(apply_result.get("applied")) if result["applied"] and handler_payload is not None: handler_result = form_embedded_module_handler_write_apply(payload, **handler_payload) result["handler_result"] = handler_result if handler_result.get("status") not in {"planned", "applied", "verified", "verified_and_rolled_back"}: result["status"] = "handler_write_failed" backup_id = ((apply_result.get("backup") or {}).get("backup_id") if isinstance(apply_result.get("backup"), dict) else None) if backup_id and payload.get("allow_sql_saved_state_rollback") is True: rollback_result = storage_saved_state_rollback( { **repository_write_context(payload), "base_id": base_id, "allow_sql_saved_state_rollback": True, "backup_id": backup_id, "timeout_seconds": int(timeout_seconds or 30), } ) result["rollback_result"] = rollback_result result["rolled_back"] = bool(rollback_result.get("applied")) if result["rolled_back"]: result["status"] = "handler_write_failed_rolled_back" return finalize_command_button_write_result(result) if result["applied"] and mode in {"apply", "apply_and_verify"}: result["semantic_verify"] = form_command_button_semantic_verify( base_id=base_id, table=concrete_saved_state["table"], file_name=concrete_saved_state["file_name"], command_name=command_name, button_name=button_name, handler_name=command_action, timeout_seconds=int(timeout_seconds or 30), include_storage=include_storage, ) result["code_index_refresh"] = code_index_refresh_form_embedded_module( base_id=base_id, table=concrete_saved_state["table"], file_name=concrete_saved_state["file_name"], timeout_seconds=int(timeout_seconds or 30), ) if mode == "apply": if (result.get("semantic_verify") or {}).get("verified"): result["status"] = "verified" return finalize_command_button_write_result(result) if mode == "apply_and_verify": readback = apply_result.get("readback") if isinstance(apply_result.get("readback"), dict) else {} handler_ok = not isinstance(handler_result, dict) or handler_result.get("status") in {"verified", "applied"} semantic_ok = (result.get("semantic_verify") or {}).get("verified") is True if result["applied"] and readback.get("verified") is not False and handler_ok and semantic_ok: result["status"] = "verified" return finalize_command_button_write_result(result) allow_rollback, allow_rollback_error = strict_bool_argument(payload, "allow_sql_saved_state_rollback", method=method, default=False) if allow_rollback_error: return allow_rollback_error if not allow_rollback: return invalid_argument(method, "allow_sql_saved_state_rollback", "apply_and_rollback mode is opt-in; pass allow_sql_saved_state_rollback=true.") backup_id = ((apply_result.get("backup") or {}).get("backup_id") if isinstance(apply_result.get("backup"), dict) else None) if not backup_id: result["status"] = "rollback_unavailable" result["diagnostics"] = {"message": "Apply result did not return backup.backup_id; cannot rollback automatically."} return finalize_command_button_write_result(result) rollback_result = storage_saved_state_rollback( { **repository_write_context(payload), "base_id": base_id, "allow_sql_saved_state_rollback": True, "backup_id": backup_id, "timeout_seconds": int(timeout_seconds or 30), } ) result["rollback_result"] = rollback_result result["rolled_back"] = bool(rollback_result.get("applied")) if result["applied"] and result["rolled_back"]: result["status"] = "verified_and_rolled_back" elif result["applied"]: result["status"] = "applied_rollback_failed" return finalize_command_button_write_result(result) target_table = str(target.get("table") or "") if target_table not in FORM_ELEMENT_SAVED_STATE_TABLES: target_table = "ConfigCASSave" if (target.get("extension") or target.get("extension_guid")) else "ConfigSave" auto_prepare = mode != "plan" and payload.get("allow_sql_saved_state_apply") is True and payload.get("auto_prepare_saved_state") is not False and not payload.get("_prepared_once") prepare_payload = metadata_write_prepare_payload( payload, { **payload, "kind": target.get("kind") or payload.get("kind") or payload.get("object_type"), "name": target.get("object") or target.get("form") or payload.get("name") or payload.get("object_name"), "object_name": target.get("form") or payload.get("object_name"), "file_name": target.get("file_name"), }, target_table=target_table, mode=mode, auto_prepare=auto_prepare, ) prepare_plan = None if not payload.get("_prepared_once") and any(prepare_payload.get(key) for key in ("extension", "ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "file_name", "file_names", "query")): prepare_plan = metadata_saved_state_prepare(prepare_payload) if prepare_plan.get("status") in {"applied", "verified", "blocked_target_collision"}: retry_payload = {**payload, "_prepared_once": True, "table": target_table} prepared_file_name = saved_state_prepare_likely_payload_file_name(prepare_plan) if prepared_file_name and not retry_payload.get("file_name"): retry_payload["file_name"] = prepared_file_name retry_result = metadata_form_command_button_write(retry_payload) retry_result.setdefault("prepare_result", prepare_plan) return retry_result result["error"] = "saved_state_or_xml_form_target_required" result["diagnostics"] = { "message": "Adding a form command and visible button is a structural Form.xml/form-payload change. No ConfigSave/ConfigCASSave form row is available, so the adapter will not write applied ConfigCAS directly.", "source_boundary": "The live adapter works with 1C through SQL storage only. XML exports are allowed for analysis and learning, not as the adapter's live write transport.", "form_model": { "common_form": "CommonForm/ОбщаяФорма is a top-level form object.", "object_form": "Form/Форма can also be owned by catalogs, documents, data processors, reports, registers, and other metadata objects.", }, } if prepare_plan is not None: result["next_resolution"] = { "method": "metadata.saved_state.prepare", "params": public_saved_state_prepare_call_payload(prepare_payload), } result["prepare_plan"] = public_saved_state_prepare_embedded_result(prepare_plan, prepare_payload) result["next_resolution"] = [ { "method": "metadata.saved_state.forms.search", "params": { key: value for key, value in saved_state_query.items() if key not in {"table", "tables", "file_name", "include_storage"} and value is not None }, "purpose": "Re-check whether Designer has a saved-state form row that can be safely patched.", }, { "workflow": "xml_analysis_learning", "script": "scripts/add_1c_form_button_workflow.py", "payload": workflow_payload, "purpose": "Use exported Form.xml only to learn/verify the structural rule for SQL payload writes; do not treat XML as the adapter live write channel.", }, { "method": "metadata.saved_state.prepare", "params": public_saved_state_prepare_call_payload(prepare_payload), "purpose": "Copy the target form from Config/ConfigCAS into ConfigSave/ConfigCASSave, then call this method again.", }, ] return result def module_write_apply_edit(payload: dict[str, Any], method: str, stream_index: int | None) -> dict[str, Any] | dict[str, Any]: if stream_index is None: return invalid_argument(method, "stream_index", "Pass stream_index or use module_ref/module_id with #stream:.") edit: dict[str, Any] = {"stream_index": int(stream_index)} if "expected_contains" in payload: edit["expected_contains"] = str(payload.get("expected_contains") or "") if "expected_text_sha1" in payload: edit["expected_text_sha1"] = str(payload.get("expected_text_sha1") or "") if isinstance(payload.get("replace"), dict): edit["replace"] = dict(payload["replace"]) return edit if "old" in payload or "new" in payload: if "old" not in payload or "new" not in payload: return invalid_argument(method, "old/new", "Pass both old and new for a replace edit.") replace = { "old": str(payload.get("old") or ""), "new": str(payload.get("new") or ""), } if "count" in payload: count, count_error = parse_int_argument(payload, "count", method=method, default=1, minimum=1) if count_error: return count_error replace["count"] = int(count or 1) edit["replace"] = replace return edit if isinstance(payload.get("routine"), dict): edit["routine"] = dict(payload["routine"]) return edit if "routine_text" in payload or "routine_name" in payload: if "routine_text" not in payload: return invalid_argument(method, "routine_text", "Pass routine_text when using routine_name/routine_operation.") routine = { "text": str(payload.get("routine_text") or ""), "operation": str(payload.get("routine_operation") or payload.get("operation") or "replace"), } if payload.get("routine_name"): routine["name"] = str(payload.get("routine_name") or "") if payload.get("expected_old_sha1"): routine["expected_old_sha1"] = str(payload.get("expected_old_sha1") or "") if payload.get("expected_old_contains"): routine["expected_old_contains"] = str(payload.get("expected_old_contains") or "") edit["routine"] = routine return edit if "text" in payload: edit["text"] = str(payload.get("text") or "") return edit return invalid_argument(method, "edit", "Pass replace, old/new, routine, routine_name/routine_text, or text for a module stream write.") def metadata_module_write_scope_fragment_payload( payload: dict[str, Any], *, base_id: str, table: str, file_name: str, stream_index: int | None, timeout_seconds: int, method: str, ) -> dict[str, Any]: requested_operation = str(payload.get("operation") or payload.get("routine_operation") or "").strip().casefold() wants_fragment = bool(payload.get("_force_fragment_replace")) or requested_operation in {"fragment", "fragment_replace", "replace_fragment"} routine_name = str(payload.get("routine_name") or "").strip() if not wants_fragment or not routine_name or payload.get("old") is None or payload.get("new") is None: return payload if stream_index is None: return invalid_argument(method, "stream_index", "Pass stream_index or use module_ref/module_id with #stream:.") old_fragment = str(payload.get("old") or "") if old_fragment == "": return invalid_argument(method, "old", "old must be a non-empty string for fragment replacement.") data, _config, read_error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=int(timeout_seconds or 30)) if read_error: result = dict(read_error) result["method"] = method return result try: from parser.cas_payload import classify_payload classified = classify_payload(data or b"", include_text=True) streams = classified.get("stream_blocks") or [] stream = streams[int(stream_index)] if 0 <= int(stream_index) < len(streams) else {} stream_text = str(stream.get("text") or "") except Exception as exc: return {"schema": "onec_module_write_apply.v1", "method": method, "status": "error", "base_id": base_id, "diagnostics": {"message": str(exc)}} routine_text, _selection = _extract_bsl_routine_text_for_code_read(stream_text, routine_name) if not routine_text: return { "schema": "onec_module_write_apply.v1", "method": method, "status": "not_found", "base_id": base_id, "error": "routine_not_found", "diagnostics": {"message": f"Routine `{routine_name}` was not found in the current saved module stream text."}, } occurrence_count = routine_text.count(old_fragment) if occurrence_count != 1: return { "schema": "onec_module_write_apply.v1", "method": method, "status": "not_found" if occurrence_count == 0 else "ambiguous", "base_id": base_id, "error": "fragment_not_found" if occurrence_count == 0 else "ambiguous_fragment", "counts": {"occurrences": occurrence_count}, "scope": {"kind": "routine", "routine_name": routine_name}, "diagnostics": {"message": "Fragment replacement requires old to occur exactly once in the current routine text."}, } patched_routine_text = routine_text.replace(old_fragment, str(payload.get("new") or ""), 1) patched_routine_text = preserve_bsl_routine_directives(stream_text, patched_routine_text, routine_name) result = dict(payload) result.pop("old", None) result.pop("new", None) result.pop("replace", None) result["routine_name"] = routine_name result["routine_text"] = patched_routine_text result["routine_operation"] = "replace" result["_fragment_scope"] = {"kind": "routine", "routine_name": routine_name, "occurrences": occurrence_count} return result def metadata_module_write_apply(payload: dict[str, Any]) -> dict[str, Any]: method = MODULE_WRITE_APPLY_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error mode = str(payload.get("execution_mode") or payload.get("mode") or "plan").strip().casefold() if mode not in FORM_ELEMENT_WRITE_APPLY_MODES: return invalid_argument(method, "execution_mode", "Unsupported execution mode.", allowed_values=sorted(FORM_ELEMENT_WRITE_APPLY_MODES)) repository_error = repository_apply_gate(payload, method, mode) if repository_error: return repository_error allow_write, allow_write_error = strict_bool_argument(payload, "allow_saved_state_write", method=method, default=False) if allow_write_error: return allow_write_error if not allow_write: return invalid_argument(method, "allow_saved_state_write", "Saved-state module write planning is opt-in; pass allow_saved_state_write=true.") include_payload, include_payload_error = strict_bool_argument(payload, "include_payload", method=method, default=False) if include_payload_error: return include_payload_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error module_ref = str(payload.get("module_ref") or payload.get("module_id") or "").strip() module_table = module_file_name = None module_stream_index = None if module_ref: module_table, module_file_name, module_stream_index = parse_module_id(module_ref) if not module_table or not module_file_name: return invalid_argument(method, "module_ref", "Use module_ref in the form
:#stream:.") stream_index = module_stream_index if stream_index is None and "stream_index" in payload: stream_index, stream_index_error = parse_int_argument(payload, "stream_index", method=method, default=0, minimum=0) if stream_index_error: return stream_index_error table = str(payload.get("table") or module_table or "ConfigCASSave") file_name = str(payload.get("file_name") or module_file_name or "") if table not in FORM_ELEMENT_SAVED_STATE_TABLES: return invalid_argument(method, "table", "Module saved-state write only targets ConfigSave/ConfigCASSave.", allowed_values=sorted(FORM_ELEMENT_SAVED_STATE_TABLES)) if not file_name or Path(file_name).name != file_name: return invalid_argument(method, "file_name", "Pass a safe saved-state module file_name or module_ref.") payload = metadata_module_write_scope_fragment_payload( payload, base_id=base_id, table=table, file_name=file_name, stream_index=stream_index, timeout_seconds=int(timeout_seconds or 30), method=method, ) if isinstance(payload, dict) and payload.get("schema") == "onec_module_write_apply.v1": return payload if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload edit = module_write_apply_edit(payload, method, stream_index) if isinstance(edit, dict) and edit.get("status") == "invalid_argument": return edit proposal_payload = { "base_id": base_id, "source": { "base_id": base_id, "table": table, "file_name": file_name, **({"module_id": f"{table}:{file_name}#stream:{stream_index}"} if stream_index is not None else {}), **({"expected_sha1": payload.get("expected_sha1")} if payload.get("expected_sha1") else {}), }, "edits": [edit], "include_text": bool(payload.get("include_text") is True), "include_payload": bool(include_payload or mode in {"apply", "apply_and_verify", "apply_and_rollback"}), "timeout_seconds": int(timeout_seconds or 30), "summary": payload.get("summary") or "Saved-state module stream write proposal", } proposal = changes_propose(proposal_payload) result: dict[str, Any] = { "schema": "onec_module_write_apply.v1", "method": method, "status": "planned", "execution_mode": mode, "base_id": base_id, "module_ref": f"{table}:{file_name}#stream:{stream_index}" if stream_index is not None else f"{table}:{file_name}", "write_mode": { "requested": "saved_state", "target_table": table, "sql_write_performed": False, "requires_apply_gate": True, }, "proposal": proposal if mode == "plan" else sanitize_proposal_for_response(proposal), } if isinstance(payload.get("_fragment_scope"), dict): scope = {key: value for key, value in payload["_fragment_scope"].items() if key != "occurrences"} result["scope"] = scope result["counts"] = {"occurrences": int(payload["_fragment_scope"].get("occurrences") or 0)} if proposal.get("status") not in {"accepted_for_review", "ok"}: result["status"] = proposal.get("status") or "error" result["diagnostics"] = proposal.get("diagnostics") return result write_plan, write_plan_error = metadata_write_apply_plan_gate( method, payload, target_kind="module", target={ "kind": "module", "table": table, "file_name": file_name, **({"module_ref": f"{table}:{file_name}#stream:{stream_index}"} if stream_index is not None else {}), }, ) result["write_plan"] = write_plan if write_plan_error: result.update(write_plan_error) return result if mode == "plan": return result allow_apply, allow_apply_error = strict_bool_argument(payload, "allow_sql_saved_state_apply", method=method, default=False) if allow_apply_error: return allow_apply_error if not allow_apply: return invalid_argument(method, "allow_sql_saved_state_apply", "Apply mode is opt-in; pass allow_sql_saved_state_apply=true.") apply_result = storage_saved_state_apply_proposal( { **repository_write_context(payload), "base_id": base_id, "allow_sql_saved_state_apply": True, "proposal": proposal, "timeout_seconds": int(timeout_seconds or 30), } ) result["apply_result"] = apply_result result["status"] = apply_result.get("status") or "error" result["applied"] = bool(apply_result.get("applied")) if mode == "apply": return result if mode == "apply_and_verify": readback = apply_result.get("readback") if isinstance(apply_result.get("readback"), dict) else {} result["status"] = "verified" if result["applied"] and readback.get("verified") is not False else result["status"] return result allow_rollback, allow_rollback_error = strict_bool_argument(payload, "allow_sql_saved_state_rollback", method=method, default=False) if allow_rollback_error: return allow_rollback_error if not allow_rollback: return invalid_argument(method, "allow_sql_saved_state_rollback", "apply_and_rollback mode is opt-in; pass allow_sql_saved_state_rollback=true.") backup_id = ((apply_result.get("backup") or {}).get("backup_id") if isinstance(apply_result.get("backup"), dict) else None) if not backup_id: result["status"] = "rollback_unavailable" result["diagnostics"] = {"message": "Apply result did not return backup.backup_id; cannot rollback automatically."} return result rollback_result = storage_saved_state_rollback( { **repository_write_context(payload), "base_id": base_id, "allow_sql_saved_state_rollback": True, "backup_id": backup_id, "timeout_seconds": int(timeout_seconds or 30), } ) result["rollback_result"] = rollback_result result["rolled_back"] = bool(rollback_result.get("applied")) if result["applied"] and result["rolled_back"]: result["status"] = "verified_and_rolled_back" elif result["applied"]: result["status"] = "applied_rollback_failed" return result def metadata_write_embedded_form_module_payload( write_payload: dict[str, Any], *, module_table: str, module_file_name: str, method: str, ) -> tuple[dict[str, Any], dict[str, Any] | None] | dict[str, Any]: handler_name = str(write_payload.get("routine_name") or write_payload.get("handler_name") or "").strip() requested_operation = str(write_payload.get("operation") or write_payload.get("routine_operation") or "").strip().casefold() wants_fragment = bool(write_payload.get("_force_fragment_replace")) or requested_operation in {"fragment", "fragment_replace", "replace_fragment"} has_fragment = (write_payload.get("old") is not None or write_payload.get("new") is not None) and (wants_fragment or not handler_name) has_module_text = write_payload.get("module_text") is not None or (write_payload.get("text") is not None and not handler_name) has_routine_text = write_payload.get("routine_text") is not None or write_payload.get("handler_routine_text") is not None or ( write_payload.get("text") is not None and bool(handler_name) ) or (write_payload.get("new") is not None and bool(handler_name) and not wants_fragment) if has_fragment: if write_payload.get("old") is None or write_payload.get("new") is None: return invalid_argument(method, "old/new", "Pass both old and new for an embedded form module container fragment write.") operation_kind = "fragment" elif has_module_text: operation_kind = "module_text" else: operation_kind = "routine" if not handler_name: return invalid_argument(method, "routine_name", "Pass routine_name for an embedded form module routine write.") if not has_routine_text: return invalid_argument(method, "routine_text", "Pass routine_text for an embedded form module routine write.") routine_text = "" if has_routine_text: routine_text = str( write_payload.get("handler_routine_text") if write_payload.get("handler_routine_text") is not None else ( write_payload.get("routine_text") if write_payload.get("routine_text") is not None else (write_payload.get("text") if write_payload.get("text") is not None else write_payload.get("new") or "") ) ) write_payload.update( { "_embedded_form_module": True, "_embedded_form_module_operation": operation_kind, "table": module_table, "file_name": module_file_name, "handler_name": handler_name, "handler_routine_operation": write_payload.get("routine_operation") or write_payload.get("operation") or "replace", } ) if operation_kind == "routine": write_payload["routine_text"] = routine_text write_payload["handler_routine_text"] = routine_text elif operation_kind == "module_text" and write_payload.get("module_text") is None: write_payload["module_text"] = str(write_payload.get("text") or "") return write_payload, {"method": "form_embedded_module_handler_write_apply", "reason": "saved_state_form_payload_container"} def metadata_write_resolve_module_target(payload: dict[str, Any], target: dict[str, Any], mode: str) -> dict[str, Any] | tuple[dict[str, Any], dict[str, Any] | None]: method = METADATA_WRITE_METHOD auto_prepare = mode != "plan" and payload.get("allow_sql_saved_state_apply") is True and payload.get("auto_prepare_saved_state") is not False write_payload = dict(payload) for key, value in target.items(): if key in {"kind", "area"}: continue write_payload.setdefault(key, value) requested_path = str(target.get("canonical_path") or payload.get("canonical_path") or target.get("path") or payload.get("path") or "").strip() if requested_path: path_resolution = metadata_write_plan_path_parts(requested_path) if path_resolution.get("kind") == "CommonForm" and path_resolution.get("name"): write_payload.setdefault("object_type", "CommonForm") write_payload.setdefault("object_name", path_resolution.get("name")) if path_resolution.get("routine_name"): write_payload.setdefault("routine_name", path_resolution.get("routine_name")) if "target" in write_payload: write_payload.pop("target", None) for key in ("kind", "area"): if str(write_payload.get(key) or "").strip().casefold() in {"module", "модуль", "bsl"}: write_payload.pop(key, None) write_payload["execution_mode"] = mode write_payload["allow_saved_state_write"] = True module_ref_value = str(write_payload.get("module_ref") or write_payload.get("module_id") or "").strip() if module_ref_value: module_table, module_file_name, module_stream_index = parse_module_id(module_ref_value) if module_table in SAVED_STATE_TARGET_BY_SOURCE and module_file_name: target_table = SAVED_STATE_TARGET_BY_SOURCE[module_table] prepared_ref = f"{target_table}:{module_file_name}" + (f"#stream:{module_stream_index}" if module_stream_index is not None else "") prepare_payload = { **repository_write_context(write_payload), "base_id": payload.get("base_id"), "source_table": module_table, "target_table": target_table, "module_ref": module_ref_value, "include_storage": True, "mode": "apply_and_verify" if auto_prepare or payload.get("allow_sql_saved_state_prepare") else "plan", "timeout_seconds": int(write_payload.get("timeout_seconds") or 60), } for key in ("extension", "ref", "kind", "name", "object_type", "object_name", "query"): if write_payload.get(key) is not None: prepare_payload[key] = write_payload.get(key) if auto_prepare or payload.get("allow_sql_saved_state_prepare"): prepare_payload["allow_sql_saved_state_prepare"] = True prepare_result = metadata_saved_state_prepare(prepare_payload) if prepare_result.get("status") in {"applied", "verified"} or (prepare_result.get("status") == "blocked_target_collision" and payload.get("allow_existing_saved_state_target")): write_payload["module_ref"] = prepared_ref write_payload.pop("module_id", None) return write_payload, {"method": "metadata.saved_state.prepare", "result": prepare_result} return { "schema": "onec_metadata_write.v1", "method": method, "status": "blocked", "target_kind": "module", "base_id": payload.get("base_id"), "error": "saved_state_prepare_required", "diagnostics": {"message": "Module write target points to active storage. Prepare the saved-state layer first, then write to the saved-state module_ref."}, "next_resolution": { "method": "metadata.saved_state.prepare", "params": public_saved_state_prepare_call_payload(prepare_payload), }, "prepare_plan": public_saved_state_prepare_embedded_result(prepare_result, prepare_payload), "prepared_target": {"layer": SAVED_STATE_LAYER_BY_TABLE[target_table]}, **({"prepared_module_ref": prepared_ref} if payload.get("include_storage") is True else {}), } if module_table in FORM_ELEMENT_SAVED_STATE_TABLES and module_file_name and module_stream_index is None and write_payload.get("stream_index") is None: return metadata_write_embedded_form_module_payload( write_payload, module_table=module_table, module_file_name=module_file_name, method=method, ) return write_payload, None routine_operation = str(write_payload.get("routine_operation") or write_payload.get("operation") or "").strip().casefold() search_query = write_payload.get("query") or write_payload.get("expected_contains") or write_payload.get("old") if search_query is None and routine_operation not in {"append", "upsert", "append_routine", "upsert_routine"}: search_query = write_payload.get("routine_name") if search_query is None and not write_payload.get("routine_name"): search_query = write_payload.get("text") search_payload = { "base_id": payload.get("base_id"), "tables": write_payload.get("tables") or ([write_payload.get("table")] if write_payload.get("table") else None), "owner_guid": write_payload.get("owner_guid"), "object_type": write_payload.get("object_type"), "object_name": write_payload.get("object_name"), "object_guid": write_payload.get("object_guid"), "kind": write_payload.get("kind"), "name": write_payload.get("name"), "guid": write_payload.get("guid"), "prefix": write_payload.get("prefix"), "file_name": write_payload.get("file_name"), "query": search_query, "stream_index": write_payload.get("stream_index"), "limit": int(write_payload.get("search_limit") or 10), "scan_limit": int(write_payload.get("scan_limit") or 1000), "preview_chars": int(write_payload.get("preview_chars") or 200), "timeout_seconds": int(write_payload.get("timeout_seconds") or 60), "include_storage": True, } search_payload = {key: value for key, value in search_payload.items() if value is not None} search = metadata_saved_state_modules_search(search_payload) if search.get("status") != "ok": result = dict(search) result["method"] = method return result matches: list[tuple[dict[str, Any], dict[str, Any]]] = [] for module in search.get("modules") or []: if not isinstance(module, dict): continue for stream in module.get("streams") or []: if isinstance(stream, dict) and stream.get("module_ref"): matches.append((module, stream)) if len(matches) != 1: prepare_payload = { "base_id": payload.get("base_id"), "target_table": write_payload.get("target_table") or ("ConfigCASSave" if (write_payload.get("extension") or write_payload.get("preferred_extension")) else "ConfigSave"), "include_storage": True, "mode": "apply_and_verify" if auto_prepare or payload.get("allow_sql_saved_state_prepare") else "plan", "timeout_seconds": int(write_payload.get("timeout_seconds") or 60), } for key in ("extension", "ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "file_name", "file_names"): if write_payload.get(key) is not None: prepare_payload[key] = write_payload.get(key) if auto_prepare or payload.get("allow_sql_saved_state_prepare"): prepare_payload["allow_sql_saved_state_prepare"] = True prepare_plan = None if any(prepare_payload.get(key) for key in ("extension", "ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "file_name", "file_names")): prepare_plan = metadata_saved_state_prepare(prepare_payload) if prepare_plan.get("status") in {"applied", "verified"}: retry_search = metadata_saved_state_modules_search(search_payload) retry_matches: list[tuple[dict[str, Any], dict[str, Any]]] = [] for module in retry_search.get("modules") or []: if not isinstance(module, dict): continue for stream in module.get("streams") or []: if isinstance(stream, dict) and stream.get("module_ref"): retry_matches.append((module, stream)) if len(retry_matches) == 1: module, stream = retry_matches[0] write_payload["module_ref"] = stream["module_ref"] if not write_payload.get("expected_sha1") and isinstance(module.get("payload"), dict) and module["payload"].get("sha1"): write_payload["expected_sha1"] = module["payload"]["sha1"] return write_payload, {"method": "metadata.saved_state.prepare", "result": prepare_plan, "retry_search": retry_search} return { "schema": "onec_metadata_write.v1", "method": method, "status": "not_found" if not matches else "ambiguous", "target_kind": "module", "base_id": payload.get("base_id"), "error": "module_target_not_resolved", "diagnostics": {"message": "Module write requires exactly one saved-state module stream. Pass module_ref, prepare saved-state, or narrow owner_guid/file_name/query/stream_index."}, "search": search, **( { "next_resolution": { "method": "metadata.saved_state.prepare", "params": public_saved_state_prepare_call_payload(prepare_payload), }, "prepare_plan": public_saved_state_prepare_embedded_result(prepare_plan, prepare_payload), } if prepare_plan is not None else {} ), "counts": {"stream_matches": len(matches)}, } module, stream = matches[0] write_payload["module_ref"] = stream["module_ref"] if stream.get("module_path") and not write_payload.get("module_path"): write_payload["module_path"] = stream.get("module_path") if not write_payload.get("expected_sha1") and isinstance(module.get("payload"), dict) and module["payload"].get("sha1"): write_payload["expected_sha1"] = module["payload"]["sha1"] resolved_table, resolved_file_name, resolved_stream_index = parse_module_id(str(write_payload.get("module_ref") or "")) if resolved_table in FORM_ELEMENT_SAVED_STATE_TABLES and resolved_file_name and resolved_stream_index is None and write_payload.get("stream_index") is None: return metadata_write_embedded_form_module_payload( write_payload, module_table=resolved_table, module_file_name=resolved_file_name, method=method, ) return write_payload, search def metadata_write_prepare_payload( payload: dict[str, Any], write_payload: dict[str, Any], *, target_table: str, mode: str, auto_prepare: bool, ) -> dict[str, Any]: prepare_payload = { "base_id": payload.get("base_id"), "target_table": target_table, "include_storage": True, "mode": "apply_and_verify" if auto_prepare or payload.get("allow_sql_saved_state_prepare") else "plan", "timeout_seconds": int(write_payload.get("timeout_seconds") or 60), } for key in ("extension", "ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "file_name", "file_names", "module_ref", "module_id"): if write_payload.get(key) is not None: prepare_payload[key] = write_payload.get(key) if not any(prepare_payload.get(key) for key in ("query", "name", "object_name", "guid", "object_guid")): for key in ("form", "form_name"): if write_payload.get(key): prepare_payload["query"] = write_payload.get(key) break if auto_prepare or payload.get("allow_sql_saved_state_prepare"): prepare_payload["allow_sql_saved_state_prepare"] = True return prepare_payload def metadata_write_resolve_form_target(payload: dict[str, Any], target: dict[str, Any], mode: str) -> dict[str, Any] | tuple[dict[str, Any], dict[str, Any] | None]: method = METADATA_WRITE_METHOD auto_prepare = mode != "plan" and payload.get("allow_sql_saved_state_apply") is True and payload.get("auto_prepare_saved_state") is not False write_payload = dict(payload) for key, value in target.items(): if key in {"kind", "area"}: continue write_payload.setdefault(key, value) if "target" in write_payload: write_payload.pop("target", None) if str(write_payload.get("kind") or "").strip().casefold() in {"form", "форма"}: write_payload.pop("kind", None) if str(write_payload.get("area") or "").strip().casefold() in {"form", "форма"}: write_payload.pop("area", None) write_payload["execution_mode"] = mode write_payload["allow_saved_state_write"] = True table = str(write_payload.get("table") or "").strip() if not table: write_payload["table"] = "ConfigCASSave" if (write_payload.get("extension") or write_payload.get("preferred_extension")) else "ConfigSave" return write_payload, None if table in FORM_ELEMENT_SAVED_STATE_TABLES: return write_payload, None if table not in SAVED_STATE_TARGET_BY_SOURCE: return invalid_argument(method, "table", "Only Config/ConfigCAS can be auto-prepared for form writes; direct writes still target ConfigSave/ConfigCASSave.", allowed_values=sorted(FORM_ELEMENT_SAVED_STATE_TABLES | set(SAVED_STATE_TARGET_BY_SOURCE))) target_table = SAVED_STATE_TARGET_BY_SOURCE[table] write_payload["table"] = target_table prepare_payload = metadata_write_prepare_payload(payload, {**write_payload, "table": target_table}, target_table=target_table, mode=mode, auto_prepare=auto_prepare) prepare_payload["source_table"] = table prepare_result = metadata_saved_state_prepare(prepare_payload) if prepare_result.get("status") in {"applied", "verified"} or (prepare_result.get("status") == "blocked_target_collision" and payload.get("allow_existing_saved_state_target")): return write_payload, {"method": "metadata.saved_state.prepare", "result": prepare_result} return { "schema": "onec_metadata_write.v1", "method": method, "status": "blocked", "target_kind": "form", "base_id": payload.get("base_id"), "error": "saved_state_prepare_required", "diagnostics": {"message": "Form write target points to active storage. Prepare the saved-state layer first, then write to the saved-state form payload."}, "next_resolution": { "method": "metadata.saved_state.prepare", "params": public_saved_state_prepare_call_payload(prepare_payload), }, "prepare_plan": public_saved_state_prepare_embedded_result(prepare_result, prepare_payload), "prepared_target": ( {"table": target_table, **({"file_name": write_payload.get("file_name")} if write_payload.get("file_name") else {})} if payload.get("include_storage") is True else {"layer": SAVED_STATE_LAYER_BY_TABLE[target_table]} ), } def metadata_write_form_retry_after_prepare(payload: dict[str, Any], write_payload: dict[str, Any], mode: str, result: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any] | None]: if result.get("status") not in {"not_found", "source_missing"}: return result, None auto_prepare = mode != "plan" and payload.get("allow_sql_saved_state_apply") is True and payload.get("auto_prepare_saved_state") is not False target_table = str(write_payload.get("table") or ("ConfigCASSave" if (write_payload.get("extension") or write_payload.get("preferred_extension")) else "ConfigSave")) if target_table not in FORM_ELEMENT_SAVED_STATE_TABLES: return result, None prepare_payload = metadata_write_prepare_payload(payload, write_payload, target_table=target_table, mode=mode, auto_prepare=auto_prepare) has_prepare_selector = any(prepare_payload.get(key) for key in ("extension", "ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "file_name", "file_names", "query")) if not has_prepare_selector: return result, None prepare_result = metadata_saved_state_prepare(prepare_payload) if prepare_result.get("status") in {"applied", "verified"}: retry_result = metadata_form_element_write_apply(write_payload) return retry_result, {"method": "metadata.saved_state.prepare", "result": prepare_result, "retry_status": retry_result.get("status")} enriched = dict(result) enriched.setdefault( "next_resolution", { "method": "metadata.saved_state.prepare", "params": public_saved_state_prepare_call_payload(prepare_payload), }, ) enriched.setdefault("prepare_plan", public_saved_state_prepare_embedded_result(prepare_result, prepare_payload)) return enriched, {"method": "metadata.saved_state.prepare", "result": prepare_result} def metadata_write_plan_path_parts(path: str) -> dict[str, Any]: parsed = parse_1c_object_path(path) parts = parsed["parts"] result: dict[str, Any] = { "input": path, "parts": parts, "is_full_path": False, "path_kind": "unknown", } if len(parts) < 2: result["reason"] = "local_or_short_name" return result kind = parsed.get("kind") name = str(parsed.get("name") or "") if not parsed.get("recognized_root") or not kind or not name: result["reason"] = "unknown_object_kind" return result member_path = list(parsed.get("member_path") or []) path_kind = "metadata_object" if not member_path else "metadata_member" section = member_path[0] if member_path else None section_class = normalize(section or "") extra: dict[str, Any] = {} if section_class in {normalize("Форма"), normalize("Формы"), "form", "forms"}: path_kind = "form_member" if len(member_path) > 2 else "form" extra["section"] = "form" if len(member_path) > 1: extra["form_name"] = member_path[1] if len(member_path) > 2: extra["form_member_path"] = member_path[2:] elif section_class in {normalize("Модуль"), normalize("Модули"), "module", "modules"}: path_kind = "module_routine" if len(member_path) > 2 else "module" extra["section"] = "module" if len(member_path) > 1: extra["module_name"] = member_path[1] if len(member_path) > 2: extra["routine_name"] = member_path[-1] elif kind == "CommonModule" and member_path: path_kind = "module_routine" extra["section"] = "module" extra["module_name"] = name extra["routine_name"] = member_path[-1] elif kind == "CommonForm" and member_path: common_form_section = normalize(member_path[0]) if common_form_section in {normalize("Команда"), normalize("Команды"), "command", "commands"}: path_kind = "form_command" extra["section"] = "form_command" extra["form_name"] = name extra["form_member_path"] = member_path[1:] if len(member_path) > 1: extra["command_name"] = member_path[-1] elif common_form_section in {normalize("Кнопка"), normalize("Кнопки"), normalize("Элемент"), normalize("Элементы"), "button", "buttons", "element", "elements"}: path_kind = "form_element" extra["section"] = "form_element" extra["form_member_kind"] = "button" if common_form_section in {normalize("Кнопка"), normalize("Кнопки"), "button", "buttons"} else "element" extra["form_name"] = name extra["form_member_path"] = member_path[1:] if len(member_path) > 1: extra["element_name"] = member_path[-1] elif common_form_section in {normalize("Атрибут"), normalize("Атрибуты"), normalize("Реквизит"), normalize("Реквизиты"), "attribute", "attributes"}: path_kind = "form_attribute" extra["section"] = "form_attribute" extra["form_name"] = name extra["form_member_path"] = member_path[1:] if len(member_path) > 1: extra["attribute_name"] = member_path[-1] else: path_kind = "module_routine" extra["section"] = "form_module" extra["form_name"] = name extra["routine_name"] = member_path[-1] result.update( { "is_full_path": True, "kind": kind, "kind_ru": RU_KIND.get(kind, kind), "name": name, "canonical_path": ".".join([RU_KIND.get(kind, kind), name, *member_path]), "code_path": ".".join([ONEC_CODE_ROOT_BY_KIND.get(kind, RU_KIND.get(kind, kind)), name, *member_path]), "member_path": member_path, "path_kind": path_kind, **extra, } ) if len(member_path) >= 2: result["context_path"] = ".".join(member_path) return result def metadata_write_plan_operation(intent: dict[str, Any], payload: dict[str, Any]) -> str: routine = payload.get("routine") if isinstance(payload.get("routine"), dict) else intent.get("routine") routine_operation = routine.get("operation") if isinstance(routine, dict) else None raw = ( intent.get("operation") or payload.get("operation") or payload.get("routine_operation") or routine_operation or ("property_change" if (intent.get("property") or payload.get("property") or payload.get("edits")) else "") or ("replace" if (payload.get("old") is not None and payload.get("new") is not None) else "") or ("replace" if isinstance(payload.get("replace"), dict) else "") or ("replace" if payload.get("text") is not None else "") or "unknown" ) return str(raw).strip().casefold() def metadata_write_plan_extension_action(payload: dict[str, Any], target: dict[str, Any], intent: dict[str, Any]) -> dict[str, Any] | None: for source in (intent, target, payload): action = source.get("extension_action") if isinstance(source, dict) else None if isinstance(action, dict): return action actions = payload.get("extension_actions") if isinstance(actions, list) and len(actions) == 1 and isinstance(actions[0], dict): return actions[0] return None def metadata_write_plan_extension_actions(payload: dict[str, Any], target: dict[str, Any], intent: dict[str, Any]) -> list[dict[str, Any]]: for source in (intent, target, payload): actions = source.get("extension_actions") if isinstance(source, dict) else None if isinstance(actions, list): return [item for item in actions if isinstance(item, dict)] return [] def metadata_write_plan_infer_target_kind(path_resolution: dict[str, Any], explicit_kind: str) -> str: normalized_explicit = str(explicit_kind or "").strip().casefold() if normalized_explicit not in {"", "metadata", "метаданные"}: return normalized_explicit path_kind = str(path_resolution.get("path_kind") or "") if path_kind.startswith("form"): return "form" if path_kind.startswith("module"): return "module" return normalized_explicit or "metadata" def metadata_write_plan_operation_class(operation: str) -> str: normalized = re.sub(r"[\s._-]+", " ", str(operation or "").strip().casefold()) aliases = { "replace with control": "replace_with_control", "replace with check": "replace_with_control", "вместо с контролем": "replace_with_control", "заменить с контролем": "replace_with_control", "замена с контролем": "replace_with_control", "replace": "replace", "вместо": "replace", "заменить": "replace", "замена": "replace", "insert before": "insert_before", "before": "insert_before", "вставить до": "insert_before", "вставка до": "insert_before", "до": "insert_before", "insert after": "insert_after", "after": "insert_after", "вставить после": "insert_after", "вставка после": "insert_after", "после": "insert_after", "append routine": "append_routine", "append": "append_routine", "добавить процедуру": "append_routine", "добавить функцию": "append_routine", "upsert routine": "upsert_routine", "upsert": "upsert_routine", "add": "add", "добавить": "add", "property change": "property_change", "property_change": "property_change", "изменить свойство": "property_change", "move form item": "move_form_item", "move": "move_form_item", "переместить": "move_form_item", } return aliases.get(normalized, str(operation or "").strip().casefold()) def metadata_write_plan_layer_class(value: str) -> str: normalized = re.sub(r"[\s._-]+", " ", str(value or "").strip().casefold()) aliases = { "": "auto", "auto": "auto", "авто": "auto", "base": "base", "configuration": "base", "config": "base", "конфигурация": "base", "основная конфигурация": "base", "основная": "base", "extension": "extension", "extensions": "extension", "расширение": "extension", "расширения": "extension", "generated extension source": "generated_extension_source", "generated extension": "generated_extension_source", "сгенерированное расширение": "generated_extension_source", } return aliases.get(normalized, normalized.replace(" ", "_")) def metadata_write_plan_extension_action_problem( extension_action: dict[str, Any] | None, operation_class: str, operation_was_inferred: bool, ) -> dict[str, Any] | None: if not extension_action: return None action_operation = metadata_write_plan_operation_class(str(extension_action.get("operation_class") or extension_action.get("operation") or "")) if str(extension_action.get("status") or "").strip().casefold() == "unknown" or action_operation in {"", "unknown_extension_action"}: return { "code": "extension_action_unknown", "message": "Extension routine action is not resolved. Resolve whether it is insert_before, insert_after, replace, or replace_with_control before planning a code write.", "extension_action": extension_action, } if action_operation == "base_definition": return None if action_operation in {"insert_before", "insert_after", "replace", "replace_with_control"} and not operation_was_inferred and operation_class != action_operation: return { "code": "extension_action_operation_mismatch", "message": "Requested code operation does not match the extension action evidence. Preserve insert_before, insert_after, replace, or replace_with_control semantics.", "requested_operation": operation_class, "extension_operation": action_operation, "extension_action": extension_action, } return None def metadata_write_plan_extension_actions_problem(extension_actions: list[dict[str, Any]]) -> dict[str, Any] | None: if len(extension_actions) <= 1: return None return { "code": "extension_action_ambiguous", "message": "Multiple extension routine actions were provided. Narrow the extension/module before planning a code write.", "extension_actions": extension_actions, } def metadata_write_plan_required_guards(target_kind: str, operation: str) -> list[str]: guards = ["canonical_path_or_concrete_reference", "layer_provenance", "semantic_diff", "semantic_readback"] operation_class = metadata_write_plan_operation_class(operation) if target_kind == "module": guards.extend(["expected_sha1", "expected_old_text_or_guard_fragment", "bsl_syntax_check"]) if operation_class == "replace_with_control": guards.append("controlled_fragment_matches_current_source") elif target_kind == "form": guards.extend(["saved_state_sha1", "property_registry_resolution"]) else: guards.extend(["origin_read", "extension_conflict_scan"]) return list(dict.fromkeys(guards)) def metadata_write_plan_first_value(payload: dict[str, Any], target: dict[str, Any], intent: dict[str, Any], *names: str) -> Any: for source in (intent, target, payload): for name in names: if isinstance(source, dict) and source.get(name) not in (None, ""): return source.get(name) return None def metadata_write_plan_code_precondition_problems( payload: dict[str, Any], target: dict[str, Any], intent: dict[str, Any], *, target_kind: str, operation: str, ) -> list[dict[str, Any]]: if target_kind != "module": return [] normalized_operation = metadata_write_plan_operation_class(operation) problems: list[dict[str, Any]] = [] has_expected_stream_guard = metadata_write_plan_first_value(payload, target, intent, "expected_contains", "expected_sha1") is not None has_expected_routine_guard = metadata_write_plan_first_value(payload, target, intent, "expected_old_contains", "expected_old_sha1") is not None has_old_fragment = metadata_write_plan_first_value(payload, target, intent, "old") is not None has_new_fragment = metadata_write_plan_first_value(payload, target, intent, "new", "text", "routine_text") is not None has_anchor = metadata_write_plan_first_value(payload, target, intent, "anchor", "before", "after", "expected_contains") is not None control_fragment_value = metadata_write_plan_first_value( payload, target, intent, "control_fragment", "controlled_fragment", "expected_old_contains", ) has_control_fragment = control_fragment_value is not None current_text_value = metadata_write_plan_first_value( payload, target, intent, "current_text", "current_source", "source_text", "current_module_text", "module_text", ) if normalized_operation == "replace_with_control" and not has_control_fragment: problems.append( { "code": "missing_control_fragment", "message": "replace_with_control requires control_fragment, controlled_fragment, or expected_old_contains.", } ) if ( normalized_operation == "replace_with_control" and has_control_fragment and current_text_value is not None and str(control_fragment_value or "") not in str(current_text_value or "") ): problems.append( { "code": "control_fragment_drift", "message": "replace_with_control control fragment does not match the provided current source evidence.", } ) if normalized_operation in {"replace", "replace_with_control"}: if not (has_old_fragment or has_control_fragment or has_expected_routine_guard or has_expected_stream_guard): problems.append( { "code": "missing_expected_old_guard", "message": "Code replacement requires old, expected_old_contains, expected_old_sha1, expected_contains, or expected_sha1.", } ) if not has_new_fragment: problems.append( { "code": "missing_new_code", "message": "Code replacement requires new code through new, text, or routine_text.", } ) if normalized_operation in {"insert_before", "insert_after"} and not has_anchor: problems.append( { "code": "missing_insert_anchor", "message": "Code insertion requires an anchor through anchor, before, after, or expected_contains.", } ) return problems def metadata_write_plan_apply_payload_hint( payload: dict[str, Any], target: dict[str, Any], intent: dict[str, Any], *, path_resolution: dict[str, Any], target_kind: str, operation_class: str, concrete_reference: str, ) -> dict[str, Any] | None: concrete_reference_info = metadata_write_concrete_reference_info(payload, target) selector_kind = str( path_resolution.get("kind") or target.get("kind") or target.get("object_type") or payload.get("kind") or payload.get("object_type") or "" ).strip() selector_name = str( path_resolution.get("name") or target.get("name") or target.get("object_name") or payload.get("name") or payload.get("object_name") or "" ).strip() selector_ref = str(target.get("ref") or payload.get("ref") or object_selector_ref(selector_kind, selector_name) or "").strip() public_resolution_params: dict[str, Any] = { "base_id": payload.get("base_id"), **({"ref": selector_ref} if selector_ref and not is_guid_text(selector_ref) else {}), **({"kind": canonical_kind(selector_kind)} if selector_kind else {}), **({"name": selector_name} if selector_name else {}), **( {"extension": target.get("extension") or payload.get("extension")} if isinstance(target.get("extension") or payload.get("extension"), str) and (target.get("extension") or payload.get("extension")) else {} ), } if target_kind == "module": hint: dict[str, Any] = { "method": MODULE_WRITE_APPLY_METHOD, "payload": { "base_id": payload.get("base_id"), "allow_saved_state_write": True, "mode": "plan", }, } if concrete_reference: reference_field = concrete_reference_info.get("field") or "module_ref" if reference_field in {"module_ref", "module_id", "file_name"}: hint["payload"][reference_field] = concrete_reference else: hint["payload"]["module_ref"] = concrete_reference hint["ready_for_apply_method"] = True elif path_resolution.get("kind") and path_resolution.get("name"): hint["payload"]["kind"] = path_resolution.get("kind") hint["payload"]["name"] = path_resolution.get("name") hint["ready_for_apply_method"] = False hint["next_resolution"] = { "method": SAVED_STATE_MODULES_SEARCH_METHOD, "reason": "metadata.module.write_apply requires module_ref or saved-state file_name/stream_index.", "params": { **public_resolution_params, **({"query": path_resolution.get("routine_name")} if path_resolution.get("routine_name") else {}), }, } if path_resolution.get("routine_name") and "routine_name" not in hint["payload"]: hint["payload"]["routine_name"] = path_resolution.get("routine_name") for field in ( "expected_sha1", "expected_contains", "expected_old_sha1", "expected_old_contains", "table", "stream_index", "old", "new", "text", "routine_name", "routine_text", ): value = metadata_write_plan_first_value(payload, target, intent, field) if value is not None: hint["payload"][field] = value if hint.get("ready_for_apply_method") is True and not module_apply_payload_has_concrete_stream(hint["payload"]): hint["ready_for_apply_method"] = False hint["next_resolution"] = { "method": SAVED_STATE_MODULES_SEARCH_METHOD, "reason": "metadata.module.write_apply requires a concrete module stream (#stream:). Form embedded container modules must not be written as a plain stream.", "params": { **public_resolution_params, **({"query": hint["payload"].get("routine_name")} if hint["payload"].get("routine_name") else {}), **( {"selector_required": "Pass a public 1C ref or kind/name selector."} if not any(public_resolution_params.get(key) for key in ("ref", "name")) else {} ), }, } control_fragment = metadata_write_plan_first_value(payload, target, intent, "control_fragment", "controlled_fragment") if operation_class == "replace_with_control" and control_fragment is not None and "expected_old_contains" not in hint["payload"]: hint["payload"]["expected_old_contains"] = control_fragment if operation_class in {"insert_before", "insert_after"}: hint["payload"]["operation"] = operation_class anchor = metadata_write_plan_first_value(payload, target, intent, "anchor", "before", "after", "expected_contains") if anchor is not None and "expected_contains" not in hint["payload"]: hint["payload"]["expected_contains"] = anchor if operation_class in {"append_routine", "upsert_routine"} and "routine_operation" not in hint["payload"]: hint["payload"]["routine_operation"] = "append" if operation_class == "append_routine" else "upsert" return hint if target_kind == "form": form_member_path = path_resolution.get("form_member_path") if isinstance(path_resolution.get("form_member_path"), list) else [] resolution_element = path_resolution.get("element_name") or (form_member_path[-1] if form_member_path else None) hint = { "method": FORM_ELEMENT_WRITE_APPLY_METHOD, "payload": { "base_id": payload.get("base_id"), "allow_saved_state_write": True, "mode": "plan", }, } if concrete_reference: reference_field = concrete_reference_info.get("field") or "file_name" if reference_field in {"file_name", "form_guid"}: hint["payload"][reference_field] = concrete_reference else: hint["payload"]["file_name"] = concrete_reference hint["ready_for_apply_method"] = True elif path_resolution.get("kind") and path_resolution.get("name"): hint["payload"]["kind"] = path_resolution.get("kind") hint["payload"]["name"] = path_resolution.get("name") hint["ready_for_apply_method"] = False hint["next_resolution"] = { "method": FORM_WRITE_TARGET_RESOLVE_METHOD, "reason": "metadata.form.element.write_apply requires saved-state table/file/form target resolution.", "params": { **public_resolution_params, **({"form": path_resolution.get("form_name")} if path_resolution.get("form_name") else {}), **({"command": path_resolution.get("command_name")} if path_resolution.get("command_name") else {}), **({"element": resolution_element} if resolution_element else {}), **({"attribute": path_resolution.get("attribute_name")} if path_resolution.get("attribute_name") else {}), }, } if path_resolution.get("form_name"): hint["payload"]["form"] = path_resolution.get("form_name") if path_resolution.get("command_name"): hint["payload"]["command"] = path_resolution.get("command_name") elif path_resolution.get("element_name"): hint["payload"]["element"] = path_resolution.get("element_name") elif path_resolution.get("attribute_name"): hint["payload"]["attribute"] = path_resolution.get("attribute_name") elif form_member_path: hint["payload"]["element"] = form_member_path[-1] for field in ( "extension", "ref", "object_ref", "form", "form_name", "command", "element", "element_name", "attribute", "element_path", "element_id", "id", ): value = target.get(field) if target.get(field) not in (None, "") else payload.get(field) if value not in (None, ""): hint["payload"][field] = value edits = payload.get("edits") if isinstance(payload.get("edits"), list) else intent.get("edits") if isinstance(edits, list) and edits: hint["payload"]["edits"] = edits for field in ("table", "property", "value"): value = metadata_write_plan_first_value(payload, target, intent, field) if value is not None: hint["payload"][field] = value return hint return None def metadata_write_plan_origin_query(path_resolution: dict[str, Any], target_kind: str) -> dict[str, Any]: member_path = path_resolution.get("member_path") if isinstance(path_resolution.get("member_path"), list) else [] if member_path: areas = ["object", "extensions"] if target_kind == "module": areas = ["modules", "extensions"] elif target_kind == "form": areas = ["form", "extensions"] query = str(path_resolution.get("routine_name") or (path_resolution.get("form_member_path") or [None])[-1] or member_path[-1] or "") else: areas = ["metadata", "extensions"] query = str(path_resolution.get("canonical_path") or path_resolution.get("name") or "") return { "query": query, "kind": path_resolution.get("kind"), "name": path_resolution.get("name"), "areas": areas, "exact_only": True, } def metadata_write_plan_compact_origin_lookup(result: dict[str, Any]) -> dict[str, Any]: matches = [] for item in result.get("matches") or []: if not isinstance(item, dict): continue matches.append( { "area": item.get("area"), "kind": item.get("kind"), "name": item.get("name"), "synonym": item.get("synonym"), "match_by": item.get("match_by"), "location": item.get("location"), "origin": item.get("origin"), "read_selector": item.get("read_selector"), } ) if len(matches) >= 5: break related_selectors = result.get("related_selectors") if isinstance(result.get("related_selectors"), dict) else {} return { "method": "metadata.definition.find", "status": result.get("status"), "object": result.get("object"), "matches": matches, "related_selectors": related_selectors, "counts": result.get("counts"), "diagnostics": result.get("diagnostics") or [], } def metadata_write_plan_surface_from_origin(origin_lookup: dict[str, Any] | None, target_kind: str) -> dict[str, Any]: if not origin_lookup: return {"write_surface": "requires_origin_lookup", "status": "unknown", "reason": "origin_lookup_missing"} if origin_lookup.get("status") != "ok": return {"write_surface": "blocked_unknown", "status": "blocked", "reason": "origin_not_found"} layer_keys = set() extension_names = set() unresolved = 0 for match in origin_lookup.get("matches") or []: if not isinstance(match, dict): continue origin = match.get("origin") if isinstance(match.get("origin"), dict) else {} source = str(origin.get("source") or "").strip().casefold() status = str(origin.get("status") or "").strip().casefold() if source in {"configuration", "base"}: layer_keys.add("configuration") elif source == "extension": extension = origin.get("extension") if isinstance(origin.get("extension"), dict) else {} extension_name = str(extension.get("name") or extension.get("guid") or "").strip() layer_keys.add(f"extension:{extension_name or 'unknown'}") if extension_name: extension_names.add(extension_name) else: unresolved += 1 elif source == "saved_state": layer_keys.add("saved_state") else: unresolved += 1 if status and status not in {"ok"}: unresolved += 1 if not layer_keys: return {"write_surface": "blocked_unknown", "status": "blocked", "reason": "origin_layer_not_resolved", "unresolved": unresolved} if len(layer_keys) > 1: return { "write_surface": "blocked_conflict", "status": "blocked", "reason": "multiple_origin_layers", "layers": sorted(layer_keys), "extensions": sorted(extension_names), "unresolved": unresolved, } layer = next(iter(layer_keys)) if layer == "configuration": return {"write_surface": "base_saved_state", "status": "recommended", "reason": "configuration_origin", "table": "ConfigSave"} if layer.startswith("extension:"): extension_name = layer.split(":", 1)[1] if extension_name == "unknown": return {"write_surface": "blocked_unknown", "status": "blocked", "reason": "extension_owner_unresolved", "unresolved": unresolved} return { "write_surface": "extension_saved_state", "status": "recommended", "reason": "extension_origin", "table": "ConfigCASSave", "extension": {"name_or_guid": extension_name}, } if layer == "saved_state": return {"write_surface": "saved_state", "status": "recommended", "reason": "already_saved_state_origin"} return {"write_surface": "blocked_unknown", "status": "blocked", "reason": "unsupported_origin_layer", "layers": sorted(layer_keys)} def metadata_write_plan_origin_lookup_from_evidence( origin: dict[str, Any], *, path_resolution: dict[str, Any], target_kind: str, ) -> dict[str, Any] | None: if not isinstance(origin, dict) or not origin: return None canonical_path = str(path_resolution.get("canonical_path") or path_resolution.get("input") or "").strip() parts = path_resolution.get("parts") if isinstance(path_resolution.get("parts"), list) else [] return { "method": "provided_origin_evidence", "status": "ok", "object": None, "matches": [ { "area": target_kind, "kind": parts[0] if parts else None, "name": parts[1] if len(parts) > 1 else None, "canonical_path": canonical_path or None, "location": {"presentation": canonical_path or None}, "origin": dict(origin), "read_selector": None, "match_by": "provided_origin", } ], "related_selectors": {}, "counts": {"matches": 1}, "diagnostics": [ { "message": "Origin evidence was provided by a prior read/search result; planner did not need to repeat metadata.definition.find for layer selection.", } ], } def metadata_write_plan_origin_ambiguity_problem(origin_lookup: dict[str, Any] | None) -> dict[str, Any] | None: if not origin_lookup or origin_lookup.get("status") != "ok": return None matches = [item for item in (origin_lookup.get("matches") or []) if isinstance(item, dict)] if len(matches) <= 1: return None locations = [] for item in matches[:5]: location = item.get("location") if isinstance(item.get("location"), dict) else {} locations.append( { "area": item.get("area"), "kind": item.get("kind"), "name": item.get("name"), "presentation": location.get("presentation"), "origin": item.get("origin"), } ) return { "code": "ambiguous_origin_matches", "message": "The target path resolved to multiple definitions. Narrow the object/form/module/routine or selector before planning a write.", "match_count": len(matches), "candidates": locations, } def metadata_write_plan_preferred_layer_problem(preferred_layer: str, recommended_write: dict[str, Any] | None) -> dict[str, Any] | None: layer = metadata_write_plan_layer_class(preferred_layer) if layer == "auto" or not recommended_write: return None surface = str(recommended_write.get("write_surface") or "") if surface == "base_saved_state": recommended_layer = "base" elif surface == "extension_saved_state": recommended_layer = "extension" elif surface == "saved_state": recommended_layer = "saved_state" elif surface in {"blocked_conflict", "blocked_unknown"}: return None else: recommended_layer = surface if layer == recommended_layer: return None if layer == "generated_extension_source" and recommended_layer == "extension": return None return { "code": "preferred_layer_conflict", "message": "Requested preferred_layer does not match the resolved origin layer.", "preferred_layer": layer, "recommended_layer": recommended_layer, "recommended_write_surface": surface, } def metadata_write_plan_preferred_extension_problem(preferred_extension: str, recommended_write: dict[str, Any] | None) -> dict[str, Any] | None: wanted = str(preferred_extension or "").strip() if not wanted or not recommended_write: return None if str(recommended_write.get("write_surface") or "") != "extension_saved_state": return { "code": "preferred_extension_without_extension_origin", "message": "preferred_extension was requested, but the resolved origin is not a single extension.", "preferred_extension": wanted, "recommended_write_surface": recommended_write.get("write_surface"), } extension = recommended_write.get("extension") if isinstance(recommended_write.get("extension"), dict) else {} actual = str(extension.get("name_or_guid") or "").strip() if actual and normalize(actual) == normalize(wanted): return None return { "code": "preferred_extension_conflict", "message": "Requested preferred_extension does not match the resolved extension owner.", "preferred_extension": wanted, "recommended_extension": actual or None, } def metadata_write_concrete_reference(payload: dict[str, Any], target: dict[str, Any]) -> str: reference = metadata_write_concrete_reference_info(payload, target) return str(reference.get("value") or "").strip() if reference else "" def metadata_write_concrete_reference_info(payload: dict[str, Any], target: dict[str, Any]) -> dict[str, str]: for source_name, source in (("target", target), ("payload", payload)): if not isinstance(source, dict): continue for field in ("module_ref", "module_id", "file_name", "form_guid"): value = str(source.get(field) or "").strip() if value: return {"source": source_name, "field": field, "value": value} return {} def metadata_write_plan_apply_hint(plan: dict[str, Any] | None) -> dict[str, Any] | None: if not isinstance(plan, dict): return None route = plan.get("route") if isinstance(plan.get("route"), dict) else {} hint = route.get("apply_payload_hint") if isinstance(route.get("apply_payload_hint"), dict) else None return hint if isinstance(hint, dict) else None def module_ref_has_stream_index(module_ref: str) -> bool: table, file_name, stream_index = parse_module_id(module_ref) return bool(table and file_name and stream_index is not None) def module_apply_payload_has_concrete_stream(payload: dict[str, Any]) -> bool: module_ref = str(payload.get("module_ref") or payload.get("module_id") or "").strip() if module_ref and module_ref_has_stream_index(module_ref): return True return payload.get("stream_index") is not None def metadata_write_apply_hint_payload(plan: dict[str, Any] | None) -> dict[str, Any]: hint = metadata_write_plan_apply_hint(plan) if not hint: return {} hint_payload = hint.get("payload") if isinstance(hint.get("payload"), dict) else {} return dict(hint_payload) def metadata_write_plan_target_kind(plan: dict[str, Any] | None) -> str: if not isinstance(plan, dict): return "" target = plan.get("target") if isinstance(plan.get("target"), dict) else {} return str(target.get("target_kind") or "").strip().casefold() def metadata_write_concrete_reference_problem(reference_info: dict[str, str], target_kind: str) -> dict[str, Any] | None: field = str(reference_info.get("field") or "").strip() if not field: return None allowed_by_kind = { "module": {"module_ref", "module_id", "file_name"}, "form": {"file_name", "form_guid"}, } allowed = allowed_by_kind.get(str(target_kind or "").strip().casefold()) if not allowed or field in allowed: return None return { "code": "concrete_reference_kind_mismatch", "message": "Concrete saved-state reference field does not match target_kind.", "target_kind": target_kind, "concrete_reference_field": field, "allowed_fields": sorted(allowed), } def metadata_write_plan_resolve_public_module_target( payload: dict[str, Any], target: dict[str, Any], ) -> tuple[dict[str, Any], dict[str, Any]]: """Resolve a public 1C module selector to an internal saved-state handle.""" selector_ref = str(target.get("ref") or target.get("object_ref") or payload.get("ref") or payload.get("object_ref") or "").strip() selector_kind = str(target.get("object_kind") or target.get("object_type") or payload.get("object_kind") or payload.get("object_type") or "").strip() selector_name = str(target.get("object_name") or payload.get("object_name") or "").strip() requested_form = str(target.get("form") or target.get("form_name") or payload.get("form") or payload.get("form_name") or "").strip() requested_module = str(target.get("module") or target.get("module_name") or payload.get("module") or payload.get("module_name") or "").strip() requested_qualified_name = str(target.get("qualified_name") or payload.get("qualified_name") or "").strip() requested_ordinal_raw = target.get("stream_ordinal") if target.get("stream_ordinal") is not None else payload.get("stream_ordinal") try: requested_ordinal = int(requested_ordinal_raw) if requested_ordinal_raw is not None else None except (TypeError, ValueError): requested_ordinal = None public_selector = { **({"ref": selector_ref} if selector_ref else {}), **({"kind": canonical_kind(selector_kind)} if selector_kind else {}), **({"name": selector_name} if selector_name else {}), **({"form": requested_form} if requested_form else {}), **({"module": requested_module} if requested_module else {}), **({"qualified_name": requested_qualified_name} if requested_qualified_name else {}), **({"stream_ordinal": requested_ordinal} if requested_ordinal is not None else {}), } if not (selector_ref or (selector_kind and selector_name)): return target, { "status": "not_attempted", "error": "public_object_selector_missing", "selector": public_selector, } search_payload: dict[str, Any] = { "base_id": payload.get("base_id"), "include_storage": True, "limit": int(payload.get("search_limit") or 50), "scan_limit": int(payload.get("scan_limit") or 1000), "timeout_seconds": int(payload.get("timeout_seconds") or 60), } if selector_ref: search_payload["ref"] = selector_ref else: search_payload["object_type"] = selector_kind search_payload["object_name"] = selector_name for key in ("extension", "layer", "tables"): value = target.get(key) if target.get(key) is not None else payload.get(key) if value is not None: search_payload[key] = value search = metadata_saved_state_modules_search(search_payload) if search.get("status") != "ok": return target, { "status": "error", "error": "saved_state_module_search_failed", "selector": public_selector, "search_status": search.get("status"), } matches: list[tuple[dict[str, Any], dict[str, Any]]] = [] for module_row in search.get("modules") or []: if not isinstance(module_row, dict): continue row_owner = module_row.get("owner") if isinstance(module_row.get("owner"), dict) else {} row_form = module_row.get("form") if isinstance(module_row.get("form"), dict) else {} row_module = module_row.get("module") if isinstance(module_row.get("module"), dict) else {} row_qualified_name = str(module_row.get("qualified_name") or module_row.get("display_name") or "").strip() if requested_form and normalize(str(row_form.get("name") or "")) != normalize(requested_form): continue if requested_module and normalize(str(row_module.get("name") or "")) != normalize(requested_module): continue if requested_qualified_name and normalize(row_qualified_name) != normalize(requested_qualified_name): continue for stream in module_row.get("streams") or []: if not isinstance(stream, dict) or not stream.get("module_ref"): continue stream_form = stream.get("form") if isinstance(stream.get("form"), dict) else row_form stream_module = stream.get("module") if isinstance(stream.get("module"), dict) else row_module stream_qualified_name = str(stream.get("qualified_name") or stream.get("display_name") or row_qualified_name).strip() if requested_form and normalize(str(stream_form.get("name") or "")) != normalize(requested_form): continue if requested_module and normalize(str(stream_module.get("name") or "")) != normalize(requested_module): continue if requested_qualified_name and normalize(stream_qualified_name) != normalize(requested_qualified_name): continue matches.append((module_row, stream)) if requested_ordinal is not None: matches = [matches[requested_ordinal - 1]] if 1 <= requested_ordinal <= len(matches) else [] if len(matches) != 1: return target, { "status": "not_found" if not matches else "ambiguous", "error": "public_module_target_not_resolved", "selector": public_selector, "counts": {"matches": len(matches)}, } module_row, stream = matches[0] payload_meta = module_row.get("payload") if isinstance(module_row.get("payload"), dict) else {} resolved_target = { **target, "module_ref": stream.get("module_ref"), **({"module_path": stream.get("module_path")} if stream.get("module_path") else {}), **({"expected_sha1": payload_meta.get("sha1")} if payload_meta.get("sha1") else {}), } return resolved_target, { "status": "resolved", "method": SAVED_STATE_MODULES_SEARCH_METHOD, "selector": public_selector, "counts": {"matches": 1}, } def metadata_write_plan_resolve_public_form_target( payload: dict[str, Any], target: dict[str, Any], intent: dict[str, Any], ) -> tuple[dict[str, Any], dict[str, Any]]: """Resolve a public 1C form selector to an internal saved-state handle.""" selector_ref = str(target.get("ref") or target.get("object_ref") or payload.get("ref") or payload.get("object_ref") or "").strip() selector_kind = str( target.get("object_kind") or target.get("object_type") or payload.get("object_kind") or payload.get("object_type") or "" ).strip() selector_name = str(target.get("object_name") or payload.get("object_name") or "").strip() form_name = str(target.get("form") or target.get("form_name") or payload.get("form") or payload.get("form_name") or "").strip() parsed_ref = parse_1c_object_path(selector_ref) if selector_ref else {} if not selector_kind and parsed_ref.get("kind"): selector_kind = str(parsed_ref.get("kind") or "") if not selector_name and parsed_ref.get("name"): selector_name = str(parsed_ref.get("name") or "") if not form_name and canonical_kind(selector_kind) == "CommonForm": form_name = selector_name extension = str(target.get("extension") or payload.get("extension") or "").strip() member_selector: dict[str, Any] = {} for key in ("command", "element", "element_name", "attribute", "element_path", "element_id", "id"): value = target.get(key) if target.get(key) not in (None, "") else payload.get(key) if value not in (None, ""): member_selector[key] = value public_selector = { **({"extension": extension} if extension else {}), **({"ref": selector_ref} if selector_ref else {}), **({"kind": canonical_kind(selector_kind)} if selector_kind and not selector_ref else {}), **({"name": selector_name} if selector_name and not selector_ref else {}), **({"form": form_name} if form_name else {}), **member_selector, } if not (selector_ref or (selector_kind and selector_name)): return target, { "status": "not_attempted", "error": "public_object_selector_missing", "selector": public_selector, } if not form_name and canonical_kind(selector_kind) != "CommonForm": return target, { "status": "not_attempted", "error": "public_form_selector_missing", "selector": public_selector, } first_edit = next( (item for item in payload.get("edits") or [] if isinstance(item, dict)), {}, ) property_name = metadata_write_plan_first_value(payload, target, intent, "property") if property_name in (None, ""): property_name = first_edit.get("property") value_present = any( isinstance(source, dict) and "value" in source for source in (intent, target, payload, first_edit) ) property_value = metadata_write_plan_first_value(payload, target, intent, "value") if "value" in first_edit and property_value is None: property_value = first_edit.get("value") resolve_payload: dict[str, Any] = { "base_id": payload.get("base_id"), "table": str(target.get("table") or payload.get("table") or ("ConfigCASSave" if extension else "ConfigSave")), "include_storage": True, "search_limit": int(payload.get("search_limit") or 10), "scan_limit": int(payload.get("scan_limit") or 1000), "timeout_seconds": int(payload.get("timeout_seconds") or 60), **public_selector, **({"property": property_name} if property_name not in (None, "") else {}), **({"value": property_value} if value_present else {}), } resolved = metadata_form_write_target_resolve(resolve_payload) if resolved.get("status") != "ok": return target, { "status": str(resolved.get("status") or "error"), "error": "public_form_target_not_resolved", "selector": public_selector, "resolution_status": resolved.get("status"), **({"counts": resolved.get("counts")} if isinstance(resolved.get("counts"), dict) else {}), } source = resolved.get("source") if isinstance(resolved.get("source"), dict) else {} file_name = str(source.get("file_name") or "").strip() table = str(source.get("table") or resolve_payload.get("table") or "").strip() if not file_name or table not in FORM_ELEMENT_SAVED_STATE_TABLES: return target, { "status": "error", "error": "public_form_target_storage_handle_missing", "selector": public_selector, } resolved_target = { **target, "table": table, "file_name": file_name, } return resolved_target, { "status": "resolved", "method": FORM_WRITE_TARGET_RESOLVE_METHOD, "selector": public_selector, "counts": {"matches": int((resolved.get("counts") or {}).get("matches") or 1)}, } def metadata_write_plan(payload: dict[str, Any]) -> dict[str, Any]: method = METADATA_WRITE_PLAN_METHOD target = dict(payload.get("target")) if isinstance(payload.get("target"), dict) else {} intent = payload.get("intent") if isinstance(payload.get("intent"), dict) else {} target_kind_raw = ( payload.get("target_kind") or payload.get("kind") or target.get("kind") or target.get("area") or payload.get("area") or "metadata" ) target_kind_hint = str(target_kind_raw or "").strip().casefold() module_name_resolution: dict[str, Any] | None = None form_name_resolution: dict[str, Any] | None = None has_public_module_selector = bool( target.get("ref") or target.get("object_ref") or ((target.get("object_kind") or target.get("object_type")) and target.get("object_name")) ) and bool(target.get("module") or target.get("module_name") or target.get("qualified_name")) if ( target_kind_hint in {"module", "модуль", "bsl"} and not metadata_write_concrete_reference_info(payload, target) and has_public_module_selector ): target, module_name_resolution = metadata_write_plan_resolve_public_module_target(payload, target) has_public_form_selector = bool( target.get("ref") or target.get("object_ref") or ((target.get("object_kind") or target.get("object_type")) and target.get("object_name")) ) and bool(target.get("form") or target.get("form_name") or target.get("command") or target.get("element") or target.get("attribute")) if ( target_kind_hint in {"form", "форма"} and not metadata_write_concrete_reference_info(payload, target) and has_public_form_selector ): target, form_name_resolution = metadata_write_plan_resolve_public_form_target(payload, target, intent) canonical_path = str( target.get("canonical_path") or payload.get("canonical_path") or target.get("path") or payload.get("path") or "" ).strip() concrete_reference_info = metadata_write_concrete_reference_info(payload, target) concrete_reference = str(concrete_reference_info.get("value") or "").strip() path_resolution = metadata_write_plan_path_parts(canonical_path) if canonical_path else { "input": "", "parts": [], "is_full_path": False, "path_kind": "concrete_reference" if concrete_reference else "unknown", "reason": "concrete_reference" if concrete_reference else "missing_target", } target_kind = metadata_write_plan_infer_target_kind(path_resolution, str(target_kind_raw or "metadata")) if target_kind in {"форма"}: target_kind = "form" elif target_kind in {"модуль", "bsl"}: target_kind = "module" elif target_kind in {"metadata", "метаданные", ""}: target_kind = "metadata" operation = metadata_write_plan_operation(intent, payload) operation_class = metadata_write_plan_operation_class(operation) operation_was_inferred_from_extension_action = False extension_actions = metadata_write_plan_extension_actions(payload, target, intent) extension_action = metadata_write_plan_extension_action(payload, target, intent) if extension_action and operation_class == "unknown": action_operation = metadata_write_plan_operation_class(str(extension_action.get("operation_class") or extension_action.get("operation") or "")) if action_operation not in {"", "unknown_extension_action", "base_definition"}: operation = action_operation operation_class = action_operation operation_was_inferred_from_extension_action = True preferred_layer = str(payload.get("preferred_layer") or target.get("preferred_layer") or "auto").strip().casefold() preferred_layer = metadata_write_plan_layer_class(preferred_layer) preferred_extension = str(payload.get("preferred_extension") or target.get("preferred_extension") or target.get("extension") or payload.get("extension") or "").strip() resolve_origin = not (payload.get("resolve_origin") is False or str(payload.get("resolve_origin") or "").strip().casefold() in {"false", "0", "no", "off", "нет"}) provided_origin = target.get("origin") if isinstance(target.get("origin"), dict) else payload.get("origin") provided_origin = provided_origin if isinstance(provided_origin, dict) else None problems = [] if module_name_resolution and module_name_resolution.get("status") != "resolved": problems.append( { "code": str(module_name_resolution.get("error") or "public_module_target_not_resolved"), "message": "The public 1C module selector did not resolve to exactly one saved-state module stream.", "resolution_status": module_name_resolution.get("status"), } ) if form_name_resolution and form_name_resolution.get("status") != "resolved": problems.append( { "code": str(form_name_resolution.get("error") or "public_form_target_not_resolved"), "message": "The public 1C form selector did not resolve to exactly one saved-state form target.", "resolution_status": form_name_resolution.get("status"), } ) if not path_resolution.get("is_full_path") and not concrete_reference: problems.append( { "code": "target_not_resolved", "message": "Write planning requires a full 1C canonical path or a concrete saved-state/module reference.", } ) if canonical_path and not path_resolution.get("is_full_path"): problems.append( { "code": str(path_resolution.get("reason") or "invalid_canonical_path"), "message": "Target path is not a full 1C metadata path.", } ) if operation == "unknown": problems.append({"code": "operation_not_classified", "message": "Write intent operation is not classified."}) extension_actions_problem = metadata_write_plan_extension_actions_problem(extension_actions) if extension_actions_problem: problems.append(extension_actions_problem) extension_action_problem = metadata_write_plan_extension_action_problem( extension_action, operation_class, operation_was_inferred_from_extension_action, ) if extension_action_problem: problems.append(extension_action_problem) concrete_problem = metadata_write_concrete_reference_problem(concrete_reference_info, target_kind) if concrete_problem: problems.append(concrete_problem) problems.extend( metadata_write_plan_code_precondition_problems( payload, target, intent, target_kind=target_kind, operation=operation_class, ) ) origin_lookup = metadata_write_plan_origin_lookup_from_evidence( provided_origin or {}, path_resolution=path_resolution, target_kind=target_kind, ) if origin_lookup is None and resolve_origin and path_resolution.get("is_full_path"): origin_query = metadata_write_plan_origin_query(path_resolution, target_kind) try: origin_result = metadata_definition_find( { "base_id": payload.get("base_id"), **origin_query, "max_matches": int(payload.get("origin_max_matches") or 20), "timeout_seconds": int(payload.get("timeout_seconds") or 60), "include_storage": False, } ) origin_lookup = metadata_write_plan_compact_origin_lookup(origin_result) except Exception as exc: origin_lookup = { "method": "metadata.definition.find", "status": "error", "error": "origin_lookup_exception", "diagnostics": {"message": str(exc)}, } route = { "target_kind": target_kind, "operation": operation, "operation_class": operation_class, "preferred_layer": preferred_layer, "preferred_extension": preferred_extension or None, "write_surface": "saved_state" if concrete_reference else "requires_origin_lookup", "apply_method": None, } if module_name_resolution: route["name_resolution"] = module_name_resolution elif form_name_resolution: route["name_resolution"] = form_name_resolution if extension_action: route["extension_action"] = extension_action if operation_was_inferred_from_extension_action: route["operation_inferred_from"] = "extension_action" if extension_actions: route["extension_actions"] = extension_actions if target_kind == "form": route["apply_method"] = FORM_ELEMENT_WRITE_APPLY_METHOD elif target_kind == "module": route["apply_method"] = MODULE_WRITE_APPLY_METHOD elif target_kind == "metadata": route["apply_method"] = "extension_source_or_saved_state_metadata_writer" recommended_write = metadata_write_plan_surface_from_origin(origin_lookup, target_kind) if origin_lookup and origin_lookup.get("status") == "ok" else None if recommended_write: route["recommended_write"] = recommended_write ambiguity_problem = metadata_write_plan_origin_ambiguity_problem(origin_lookup) if ambiguity_problem: problems.append(ambiguity_problem) preferred_problem = metadata_write_plan_preferred_layer_problem(preferred_layer, recommended_write) if preferred_problem: problems.append(preferred_problem) preferred_extension_problem = metadata_write_plan_preferred_extension_problem(preferred_extension, recommended_write) if preferred_extension_problem: problems.append(preferred_extension_problem) apply_payload_hint = metadata_write_plan_apply_payload_hint( payload, target, intent, path_resolution=path_resolution, target_kind=target_kind, operation_class=operation_class, concrete_reference=concrete_reference, ) if apply_payload_hint: route["apply_payload_hint"] = apply_payload_hint if concrete_reference and target_kind in {"form", "module"}: route["write_surface"] = "saved_state" allowed = not problems status = "planned" if allowed else "blocked" else: allowed = False status = "needs_route" if origin_lookup and origin_lookup.get("status") == "ok" else "needs_origin" if not any(problem.get("code") == "target_not_resolved" for problem in problems): if origin_lookup and origin_lookup.get("status") == "ok": problem_code = "write_route_required" problem_message = "Origin was found, but a concrete saved-state or extension-source write route is still required before apply." if recommended_write and str(recommended_write.get("status") or "") == "blocked": problem_code = str(recommended_write.get("write_surface") or "write_route_blocked") problem_message = "Origin was found, but the write route is blocked until the layer conflict or unresolved owner is handled." problems.append( { "code": problem_code, "message": problem_message, } ) else: problems.append( { "code": "origin_lookup_required", "message": "Effective targets are read-only until origin/layer evidence selects base, extension, or generated extension source.", } ) return { "schema": "onec_metadata_write_plan.v1", "method": method, "status": status, "allowed": allowed, "base_id": payload.get("base_id"), "target": { "canonical_path": path_resolution.get("canonical_path"), "input_path": canonical_path or None, "path_kind": path_resolution.get("path_kind"), "target_kind": target_kind, "concrete_reference": concrete_reference or None, "concrete_reference_field": concrete_reference_info.get("field") or None, "concrete_reference_source": concrete_reference_info.get("source") or None, }, "path_resolution": path_resolution, **({"origin_lookup": origin_lookup} if origin_lookup is not None else {}), "route": route, "required_guards": metadata_write_plan_required_guards(target_kind, operation_class), "problems": problems, "diagnostics": { "read_only": True, "message": "metadata.write.plan does not apply changes. Use metadata.write only after this plan has a concrete saved-state or extension-source route.", }, } def metadata_write_apply_plan_gate( routed_method: str, payload: dict[str, Any], *, target_kind: str, target: dict[str, Any], ) -> tuple[dict[str, Any], dict[str, Any] | None]: plan_payload = dict(payload) plan_target = dict(target) if isinstance(payload.get("target"), dict): for key, value in payload["target"].items(): plan_target.setdefault(key, value) plan_target["kind"] = target_kind plan_payload["target"] = plan_target plan_payload["target_kind"] = target_kind plan_payload["resolve_origin"] = False plan = metadata_write_plan(plan_payload) if plan.get("allowed") is True: return plan, None return plan, { "status": "blocked", "error": "write_plan_blocked", "routed_method": METADATA_WRITE_PLAN_METHOD, "problems": plan.get("problems") if isinstance(plan.get("problems"), list) else [], "diagnostics": { "message": f"{routed_method} will not apply while metadata.write.plan reports blocking problems.", }, } def metadata_write_path_can_resolve_saved_state_module(path_plan: dict[str, Any] | None) -> bool: if not isinstance(path_plan, dict): return False if metadata_write_plan_target_kind(path_plan) != "module": return False path_resolution = path_plan.get("path_resolution") if isinstance(path_plan.get("path_resolution"), dict) else {} if path_resolution.get("kind") != "CommonForm" or path_resolution.get("section") != "form_module": return False apply_hint = metadata_write_plan_apply_hint(path_plan) next_resolution = apply_hint.get("next_resolution") if isinstance(apply_hint, dict) else None return isinstance(next_resolution, dict) and next_resolution.get("method") == SAVED_STATE_MODULES_SEARCH_METHOD def metadata_write_save_first_payload(payload: dict[str, Any], mode: str) -> dict[str, Any]: if str(mode or "").strip().casefold() not in {"apply", "apply_and_verify", "apply_and_rollback"}: return payload result = dict(payload) result.setdefault("allow_sql_saved_state_apply", True) result.setdefault("allow_sql_saved_state_prepare", True) result.setdefault("auto_prepare_saved_state", True) if str(mode or "").strip().casefold() == "apply_and_rollback": result.setdefault("allow_sql_saved_state_rollback", True) return result def metadata_write_preflight_saved_target(payload: dict[str, Any], plan: dict[str, Any]) -> dict[str, Any]: target = payload.get("target") if isinstance(payload.get("target"), dict) else {} hint_payload = metadata_write_apply_hint_payload(plan) result: dict[str, Any] = {} module_ref = str( hint_payload.get("module_ref") or hint_payload.get("module_id") or target.get("module_ref") or target.get("module_id") or payload.get("module_ref") or payload.get("module_id") or "" ).strip() if module_ref: module_table, module_file_name, stream_index = parse_module_id(module_ref) if module_table and module_file_name: result.update({"table": module_table, "file_name": module_file_name, "module_ref": module_ref}) if stream_index is not None: result["stream_index"] = stream_index return result table = str( hint_payload.get("table") or target.get("table") or target.get("target_table") or payload.get("table") or payload.get("target_table") or "" ).strip() file_name = str( hint_payload.get("file_name") or target.get("file_name") or payload.get("file_name") or "" ).strip() if table in SAVED_STATE_SOURCE_BY_TARGET and file_name and Path(file_name).name == file_name: result.update({"table": table, "file_name": file_name}) return result def metadata_write_preflight_status(plan: dict[str, Any], saved_state: dict[str, Any] | None) -> str: if plan.get("allowed") is not True: if any(str(problem.get("code") or "") == "origin_lookup_required" for problem in plan.get("problems") or [] if isinstance(problem, dict)): return "needs_resolution" return "blocked" hint = metadata_write_plan_apply_hint(plan) if isinstance(hint, dict) and hint.get("ready_for_apply_method") is False: return "needs_resolution" if saved_state and saved_state.get("needs_prepare") is True: return "needs_prepare" if saved_state and saved_state.get("status") in {"error"}: return "blocked" return "ready" def live_sql_support_gate(payload: dict[str, Any]) -> dict[str, Any]: """Evaluate support from live SQL, falling back fail-closed to configured policy.""" configured = repository_control.support_gate(payload) layer_id = repository_control.development_layer_id(payload) target = payload.get("target") if isinstance(payload.get("target"), dict) else {} object_guid = str( payload.get("support_object_guid") or payload.get("object_guid") or target.get("object_guid") or target.get("guid") or "" ).strip().lower() normalized = normalize_object_selector_aliases(payload, "metadata.support.decode") if not (isinstance(normalized, dict) and normalized.get("status") == "invalid_argument"): if not object_guid: object_guid = str(normalized.get("guid") or "").strip().lower() if not object_guid and normalized.get("name"): if layer_id == "base": card = get_object( normalized.get("kind"), str(normalized.get("name") or ""), base_id=str(payload.get("base_id") or ""), include_semantic=False, timeout_seconds=int(payload.get("timeout_seconds") or 60), ) object_guid = str((card.get("object") or {}).get("guid") or "").lower() else: extension_guid = layer_id.split(":", 1)[1] found = extension_objects_find( { "base_id": payload.get("base_id"), "extension_guid": extension_guid, "query": normalized.get("name"), "kind": normalized.get("kind"), "limit": 20, "scan_limit": 5000, } ) name_folded = str(normalized.get("name") or "").casefold() exact = next( ( item for item in (found.get("objects") or []) if str(item.get("name") or "").casefold() == name_folded ), None, ) object_guid = str((exact or {}).get("guid") or "").lower() if not is_guid_text(object_guid): return configured live = metadata_support_decode( { "base_id": payload.get("base_id"), "layer_id": layer_id, "object_guid": object_guid, "timeout_seconds": payload.get("timeout_seconds", 30), } ) object_support = live.get("object_support") if isinstance(live.get("object_support"), dict) else None if live.get("status") != "ok" or not object_support: return { **configured, "live_sql": { "status": live.get("status"), "diagnostics": live.get("diagnostics"), }, } allowed = object_support.get("edit_allowed_by_support") is True return { "required": object_support.get("status") != "not_supported", "allowed": allowed, "status": "support_live_sql_editable" if allowed else "blocked_by_support_live_sql", "layer_id": layer_id, "mode": "live_sql", "object_guid": object_guid, "rule": object_support.get("status"), "evidence": object_support.get("evidence"), "source_discovery": (live.get("diagnostics") or {}).get("source_discovery"), } def repository_layers_audit(payload: dict[str, Any]) -> dict[str, Any]: """Report policy per real configuration layer without consulting local caches.""" base_id_or_error = require_base_id(payload, "repository.layers.audit") if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error extensions_result = list_extensions({"base_id": base_id, "timeout_seconds": payload.get("timeout_seconds", 30)}) extensions = extensions_result.get("extensions") if extensions_result.get("status") == "ok" else [] candidates = [{"layer_id": "base", "kind": "base", "name": None, "guid": None}] for extension in extensions if isinstance(extensions, list) else []: if not isinstance(extension, dict) or not is_guid_text(str(extension.get("guid") or "")): continue guid = str(extension["guid"]).lower() candidates.append({"layer_id": f"extension:{guid}", "kind": "extension", "name": extension.get("name"), "guid": guid}) layers: list[dict[str, Any]] = [] for candidate in candidates: layer_id = str(candidate["layer_id"]) repository = repository_control.status({"base_id": base_id, "layer_id": layer_id}) support = metadata_support_decode({"base_id": base_id, "layer_id": layer_id, "timeout_seconds": payload.get("timeout_seconds", 30)}) repository_status = str(repository.get("status") or "unknown") if repository_status == "repository_not_connected": next_write_action = "repository_lock_not_required" elif repository_status in {"repository_connection_unknown", "repository_state_unknown"}: next_write_action = "configure_repository_connection_state" else: next_write_action = "request_and_confirm_repository_capture" suppliers = support.get("suppliers") if isinstance(support.get("suppliers"), list) else [] support_overview = { "status": support.get("status"), "supplier_count": len(suppliers), "general_modes": sorted({str(item.get("general_mode")) for item in suppliers if isinstance(item, dict) and item.get("general_mode")}), "support_payload_present": (support.get("source") or {}).get("support_payload_present"), "source_discovery": (support.get("diagnostics") or {}).get("source_discovery"), } layers.append({ **candidate, "repository": repository, "support": { **support_overview, "object_support": support.get("object_support"), "source": support.get("source"), }, "safe_next_write_action": next_write_action, }) return { "schema": "onec_repository_layers_audit.v1", "method": "repository.layers.audit", "status": "ok" if extensions_result.get("status") == "ok" else "partial", "base_id": base_id, "layers": layers, "extensions_discovery": {"status": extensions_result.get("status"), "diagnostics": extensions_result.get("diagnostics")}, } def repository_apply_gate(payload: dict[str, Any], method: str, mode: str) -> dict[str, Any] | None: if mode not in {"apply", "apply_and_verify", "apply_and_rollback"}: return None payload, context_error = normalize_repository_write_context(payload, method) if context_error: return { "schema": "onec_repository_write_gate.v1", "method": method, "base_id": payload.get("base_id"), **context_error, } gate = repository_control.write_gate(payload) support = live_sql_support_gate(payload) if gate.get("allowed") is True and support.get("allowed") is True: return None blocked_gate = gate if gate.get("allowed") is not True else support return { "schema": "onec_repository_write_gate.v1", "method": method, "status": "blocked", "error": str(blocked_gate.get("status") or "write_authorization_required"), "base_id": payload.get("base_id"), "repository": gate, "support": support, "diagnostics": {"message": "Saved-state apply requires both repository and support gates to allow the exact development layer and target."}, } def metadata_write_is_schedule_target(payload: dict[str, Any]) -> bool: target = payload.get("target") if isinstance(payload.get("target"), dict) else {} value = str(target.get("kind") or payload.get("target_kind") or "").strip().casefold() return value in {"schedule", "scheduled_job", "scheduledjob", "расписание", "регламентноезадание", "регламентное_задание"} def metadata_write_preflight(payload: dict[str, Any]) -> dict[str, Any]: method = METADATA_WRITE_PREFLIGHT_METHOD payload, context_error = normalize_repository_write_context(payload, method) if context_error: return { "schema": "onec_metadata_write_preflight.v1", "method": method, "base_id": payload.get("base_id"), "allowed": False, **context_error, } payload = resolve_write_gate_context(payload) if metadata_write_is_schedule_target(payload): schedule_plan = metadata_scheduled_job_schedule_write( { **payload, "allow_saved_state_write": True, "execution_mode": "plan", "include_payload": False, } ) object_card = schedule_plan.get("object") if isinstance(schedule_plan.get("object"), dict) else {} gate_payload = { **payload, **({"object_guid": object_card.get("guid")} if object_card.get("guid") else {}), } repository_gate = repository_control.write_gate(gate_payload) support_gate = live_sql_support_gate(gate_payload) schedule_status = str(schedule_plan.get("status") or "blocked") status = "ready" if schedule_status in {"planned", "unchanged"} else schedule_status if repository_gate.get("allowed") is not True: status = str(repository_gate.get("status") or "blocked") elif support_gate.get("allowed") is not True: status = str(support_gate.get("status") or "blocked") source = schedule_plan.get("source") if isinstance(schedule_plan.get("source"), dict) else {} return { "schema": "onec_metadata_write_preflight.v1", "method": method, "status": status, "allowed": status == "ready", "base_id": payload.get("base_id"), "target": { "target_kind": "schedule", "ref": object_card.get("ref"), "guid": object_card.get("guid"), }, "route": { "writer": "metadata.write:scheduled_job_schedule", "write_surface": "saved_state", "ready_for_apply_method": status == "ready", "auto_resolves_saved_state": True, }, "saved_state": { "status": schedule_status, "target": { **({"table": source.get("table")} if source.get("table") else {}), **({"file_name": source.get("file_name")} if source.get("file_name") else {}), }, "needs_prepare": schedule_status == "needs_prepare", "freshness": {"source": "live_sql", "status": "live_sql_verified", "verified_against_sql": True}, }, "repository": repository_gate, "support": support_gate, "guards": { "required": ["allow_saved_state_write", "expected_sha1", "backup", "readback_verification"], "requires_saved_state_prepare": schedule_status == "needs_prepare", "requires_backup": True, "rollback_available": schedule_status in {"planned", "unchanged"}, "expected_sha1": ((schedule_plan.get("proposal") or {}).get("original") or {}).get("sha1"), }, "plan": { "method": METADATA_WRITE_METHOD, "status": schedule_status, "allowed": schedule_status in {"planned", "unchanged"}, "problems": [] if schedule_status in {"planned", "unchanged"} else [schedule_plan.get("diagnostics") or {"message": schedule_status}], }, "diagnostics": { "read_only": True, "message": "metadata.write.preflight resolved the scheduled job by public 1C name and verified its ConfigSave schedule payload without applying SQL writes.", }, } plan_payload = dict(payload) plan_payload.setdefault("resolve_origin", payload.get("resolve_origin", False)) plan = metadata_write_plan(plan_payload) path_resolution = plan.get("path_resolution") if isinstance(plan.get("path_resolution"), dict) else {} command_button_route = ( str(path_resolution.get("path_kind") or "") == "form_command" or ( str(path_resolution.get("path_kind") or "") == "form_element" and str(path_resolution.get("form_member_kind") or "") == "button" ) ) saved_target = metadata_write_preflight_saved_target(payload, plan) saved_state: dict[str, Any] | None = None module_prepare: dict[str, Any] | None = None if saved_target.get("table") and saved_target.get("file_name"): diff_payload = { "base_id": payload.get("base_id"), "table": saved_target.get("table"), "file_name": saved_target.get("file_name"), "timeout_seconds": payload.get("timeout_seconds", 30), "max_changes": 1, "max_text_diff_lines": 0, "include_text_diff": False, "include_tree_diff": False, } if saved_target.get("module_ref"): diff_payload["module_ref"] = saved_target.get("module_ref") diff = metadata_saved_state_diff(diff_payload) saved_state = { "status": diff.get("status"), "target": diff.get("target") or saved_target, "source": diff.get("source"), "current_state": diff.get("current_state"), "needs_prepare": diff.get("needs_prepare") is True, "prepare_payload": diff.get("prepare_payload"), "freshness": diff.get("freshness") or { "source": "live_sql", "status": "live_sql_verified", "verified_against_sql": True, }, } if diff.get("error"): saved_state["error"] = diff.get("error") if diff.get("comparison"): saved_state["comparison"] = diff.get("comparison") else: saved_state = { "status": "unresolved", "target": None, "needs_prepare": False, "auto_prepare_on_write": bool(command_button_route), "freshness": { "source": "live_sql", "status": "live_sql_verified" if command_button_route else ("vector_candidate_unverified" if plan.get("allowed") is not True else "live_sql_verified"), "verified_against_sql": bool(command_button_route), }, } # A raw active module reference has enough information to plan the # Config -> ConfigSave transition without writing. Present that concrete # transition instead of the generic origin-resolution failure. target_for_module = payload.get("target") if isinstance(payload.get("target"), dict) else {} module_ref_for_prepare = str( target_for_module.get("module_ref") or target_for_module.get("module_id") or payload.get("module_ref") or payload.get("module_id") or "" ).strip() if ( module_ref_for_prepare and isinstance(payload.get("owner_resolution"), dict) and payload["owner_resolution"].get("status") == "resolved" ): module_resolution = metadata_write_resolve_module_target(payload, target_for_module, "plan") module_plan_result = module_resolution[0] if isinstance(module_resolution, tuple) else module_resolution if isinstance(module_plan_result, dict) and module_plan_result.get("error") == "saved_state_prepare_required": module_prepare = module_plan_result prepared_ref = str(module_plan_result.get("prepared_module_ref") or "").strip() prepared_table, prepared_file_name, prepared_stream_index = parse_module_id(prepared_ref) saved_target = { "table": prepared_table or "ConfigSave", "file_name": prepared_file_name or None, "module_ref": prepared_ref or None, **({"stream_index": prepared_stream_index} if prepared_stream_index is not None else {}), } saved_state = { "status": "needs_prepare", "target": saved_target, "needs_prepare": True, "prepare_payload": module_plan_result.get("next_resolution"), "prepare_plan": module_plan_result.get("prepare_plan"), "freshness": {"source": "live_sql", "status": "live_sql_verified", "verified_against_sql": True}, } status = metadata_write_preflight_status(plan, saved_state) if module_prepare: status = "needs_prepare" if command_button_route: status = "ready" # The metadata resolver is the authority for the configuration layer. Do # not require callers to select base/extension manually once origin is known. gate_payload = dict(payload) plan_target = plan.get("target") if isinstance(plan.get("target"), dict) else {} if plan_target: caller_target = payload.get("target") if isinstance(payload.get("target"), dict) else {} gate_payload["target"] = {**caller_target, **plan_target} repository_gate = repository_control.write_gate(gate_payload) support_gate = live_sql_support_gate(gate_payload) if repository_gate.get("allowed") is not True: status = str(repository_gate.get("status") or "blocked") elif support_gate.get("allowed") is not True: status = str(support_gate.get("status") or "blocked") hint = metadata_write_plan_apply_hint(plan) hint_payload = hint.get("payload") if isinstance(hint, dict) and isinstance(hint.get("payload"), dict) else {} guards = { "required": plan.get("required_guards") if isinstance(plan.get("required_guards"), list) else [], "requires_saved_state_prepare": saved_state.get("needs_prepare") is True if isinstance(saved_state, dict) else False, "requires_backup": True, "rollback_available": bool(saved_target.get("table") and saved_target.get("file_name")), "expected_sha1": hint_payload.get("expected_sha1"), "expected_text_sha1": hint_payload.get("expected_text_sha1"), } route = plan.get("route") if isinstance(plan.get("route"), dict) else {} return { "schema": "onec_metadata_write_preflight.v1", "method": method, "status": status, "allowed": status == "ready", "base_id": payload.get("base_id"), "target": plan.get("target"), "route": { "writer": FORM_COMMAND_BUTTON_WRITE_METHOD if command_button_route else ("metadata.module.write_apply" if module_prepare else route.get("apply_method")), "write_surface": "saved_state" if module_prepare else route.get("write_surface"), "ready_for_apply_method": hint.get("ready_for_apply_method") if isinstance(hint, dict) else None, "apply_payload_hint": hint, "auto_resolves_saved_state": bool(command_button_route or module_prepare), **({"next_resolution": module_prepare.get("next_resolution")} if module_prepare else {}), }, "saved_state": saved_state, "repository": repository_gate, "support": support_gate, **({"owner_resolution": payload["owner_resolution"]} if isinstance(payload.get("owner_resolution"), dict) else {}), "guards": guards, "plan": { "method": METADATA_WRITE_PLAN_METHOD, "status": plan.get("status"), "allowed": plan.get("allowed"), "problems": plan.get("problems") if isinstance(plan.get("problems"), list) else [], }, "diagnostics": { "read_only": True, "message": "metadata.write.preflight verifies route and saved-state freshness without applying SQL writes.", }, } def metadata_scheduled_job_schedule_write(payload: dict[str, Any]) -> dict[str, Any]: method = METADATA_WRITE_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error target = payload.get("target") if isinstance(payload.get("target"), dict) else {} mode = str(payload.get("execution_mode") or payload.get("mode") or "plan").strip().casefold() if mode not in FORM_ELEMENT_WRITE_APPLY_MODES: return invalid_argument(method, "execution_mode", "Unsupported execution mode.", allowed_values=sorted(FORM_ELEMENT_WRITE_APPLY_MODES)) allow_write, allow_write_error = strict_bool_argument(payload, "allow_saved_state_write", method=method, default=False) if allow_write_error: return allow_write_error include_storage, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error timeout_seconds = int(timeout_seconds or 30) table = str(target.get("table") or payload.get("table") or "ConfigSave") if table != "ConfigSave": return invalid_argument(method, "target.table", "Scheduled-job schedule writes currently target ConfigSave only.", allowed_values=["ConfigSave"]) schedule = payload.get("schedule") if schedule is None: schedule = target.get("schedule") if not isinstance(schedule, dict) or not schedule: return invalid_argument(method, "schedule", "Pass schedule as a non-empty JSON object with named schedule fields.") ref = str(target.get("ref") or payload.get("ref") or "").strip() requested_kind = None ref_name = "" if ref: requested_kind, ref_name = parse_object_query(None, ref) if requested_kind != "ScheduledJob" or not ref_name: return invalid_argument(method, "target.ref", "Use a scheduled-job reference such as РегламентныеЗадания.ОбменДанными.") guid = str(target.get("guid") or target.get("object_guid") or payload.get("guid") or payload.get("object_guid") or "").strip().lower() if guid and not is_guid_text(guid): return invalid_argument(method, "target.guid", "guid must be a GUID string.") name = str( ref_name or target.get("name") or target.get("object") or target.get("object_name") or payload.get("object_name") or payload.get("name") or "" ).strip() selector = guid or name if not selector: return invalid_argument(method, "target", "Pass target.ref, target.name, or target.guid for the scheduled job.") object_result = get_object( "ScheduledJob", selector, base_id=base_id, view="effective", limit=int(payload.get("limit") or 20), include_storage=True, include_semantic=False, timeout_seconds=timeout_seconds, table=table, ) if object_result.get("status") != "ok": result = dict(object_result) result.update({"schema": "onec_scheduled_job_schedule_write.v1", "method": method, "execution_mode": mode}) return result object_card = object_result.get("object") if isinstance(object_result.get("object"), dict) else {} object_guid = str(object_card.get("guid") or guid).lower() if not is_guid_text(object_guid): return { "schema": "onec_scheduled_job_schedule_write.v1", "method": method, "status": "blocked", "error": "scheduled_job_guid_unresolved", "base_id": base_id, "object": public_metadata_row(object_card, include_storage=bool(include_storage)), "diagnostics": {"message": "The scheduled job was found, but its public metadata GUID could not be resolved."}, } file_name = str(target.get("file_name") or payload.get("file_name") or f"{object_guid}.0") if Path(file_name).name != file_name or file_name != f"{object_guid}.0": return invalid_argument(method, "target.file_name", "The scheduled-job schedule file must be .0.") data, config, read_error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=timeout_seconds) if read_error: return { "schema": "onec_scheduled_job_schedule_write.v1", "method": method, "status": "needs_prepare", "execution_mode": mode, "base_id": base_id, "object": public_metadata_row(object_card, include_storage=bool(include_storage)), "diagnostics": { "message": "No saved-state schedule payload was found. Prepare the scheduled job in ConfigSave before writing its schedule.", "source_boundary": "The adapter does not write active Config rows.", }, "next_call": { "method": "metadata.saved_state.prepare", "params": { "base_id": base_id, "ref": object_card.get("ref") or object_selector_ref("ScheduledJob", str(object_card.get("name") or name)), "layer": "base_saved_state", }, }, } tree = parse_config_tree_from_bytes(data or b"") edit_plan = scheduled_job_schedule_write_edits(tree, schedule) if edit_plan.get("status") != "ok": return { "schema": "onec_scheduled_job_schedule_write.v1", "method": method, "execution_mode": mode, "base_id": base_id, "object": public_metadata_row(object_card, include_storage=bool(include_storage)), **edit_plan, } public_object = public_metadata_row(object_card, include_storage=bool(include_storage)) source = { "kind": "live_sql", **({"database": config.get("database")} if isinstance(config, dict) and config.get("database") and include_storage else {}), **({"table": table, "file_name": file_name} if include_storage else {}), } result: dict[str, Any] = { "schema": "onec_scheduled_job_schedule_write.v1", "method": method, "status": "blocked" if not allow_write else ("unchanged" if not edit_plan["edits"] else "planned"), "execution_mode": mode, "base_id": base_id, "object": public_object, "source": source, "current_schedule": edit_plan["current"], "requested_schedule": edit_plan["requested"], "counts": edit_plan["counts"], "write_mode": { "requested": "saved_state", "target_table": table, "sql_write_performed": False, "requires_apply_gate": True, }, } if not allow_write: result["diagnostics"] = { "message": "Schedule write planning is opt-in; pass allow_saved_state_write=true.", "read_only": True, } result["next_call"] = { "method": method, "params": { "base_id": base_id, "target": {"kind": "schedule", "ref": public_object.get("ref")}, "schedule": edit_plan["requested"], "allow_saved_state_write": True, "execution_mode": "plan", }, } return result if not edit_plan["edits"]: result["diagnostics"] = {"message": "The requested schedule already matches the saved-state payload."} return result proposal = changes_propose( { "base_id": base_id, "source": { "base_id": base_id, "table": table, "file_name": file_name, "expected_sha1": str(payload.get("expected_sha1") or hashlib.sha1(data or b"").hexdigest()), }, "edits": [{key: value for key, value in edit.items() if key != "field"} for edit in edit_plan["edits"]], "preserve_format": True, "include_payload": bool(payload.get("include_payload") is True or mode != "plan"), "include_text": bool(payload.get("include_text") is True), "timeout_seconds": timeout_seconds, "summary": payload.get("summary") or f"Update scheduled-job schedule {public_object.get('ref') or object_guid}", } ) result["field_edits"] = [ {key: value for key, value in edit.items() if key != "replace_root"} for edit in edit_plan["edits"] ] result["proposal"] = proposal if mode == "plan" else sanitize_proposal_for_response(proposal) if proposal.get("status") not in {"accepted_for_review", "ok"}: result["status"] = proposal.get("status") or "error" result["diagnostics"] = proposal.get("diagnostics") return result if mode == "plan": return result allow_apply, allow_apply_error = strict_bool_argument(payload, "allow_sql_saved_state_apply", method=method, default=False) if allow_apply_error: return allow_apply_error if not allow_apply: return invalid_argument(method, "allow_sql_saved_state_apply", "Apply mode is opt-in; pass allow_sql_saved_state_apply=true.") apply_result = storage_saved_state_apply_proposal( { **repository_write_context(payload), "base_id": base_id, "allow_sql_saved_state_apply": True, "proposal": proposal, "timeout_seconds": timeout_seconds, } ) result["apply_result"] = apply_result result["status"] = apply_result.get("status") or "error" result["applied"] = bool(apply_result.get("applied")) result["write_mode"]["sql_write_performed"] = bool(result["applied"]) if result["applied"]: readback, _, readback_error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=timeout_seconds) readback_schedule = scheduled_job_sql_schedule(parse_config_tree_from_bytes(readback or b""), include_storage=False) if not readback_error else {"status": "error"} checks = [ {"field": field, "expected": expected, "actual": readback_schedule.get(field), "ok": readback_schedule.get(field) == expected} for field, expected in edit_plan["requested"].items() ] result["semantic_verification"] = { "status": "ok" if checks and all(check["ok"] for check in checks) else "mismatch", "schedule": readback_schedule, "checks": checks, } if mode in {"apply", "apply_and_verify"} and all(check["ok"] for check in checks): result["status"] = "verified" if mode in {"apply", "apply_and_verify"}: return result allow_rollback, allow_rollback_error = strict_bool_argument(payload, "allow_sql_saved_state_rollback", method=method, default=False) if allow_rollback_error: return allow_rollback_error if not allow_rollback: return invalid_argument(method, "allow_sql_saved_state_rollback", "apply_and_rollback mode is opt-in; pass allow_sql_saved_state_rollback=true.") backup_id = ((apply_result.get("backup") or {}).get("backup_id") if isinstance(apply_result.get("backup"), dict) else None) if not backup_id: result["status"] = "rollback_unavailable" result["diagnostics"] = {"message": "Apply result did not return backup.backup_id; cannot rollback automatically."} return result rollback_result = storage_saved_state_rollback( { **repository_write_context(payload), "base_id": base_id, "allow_sql_saved_state_rollback": True, "backup_id": backup_id, "timeout_seconds": timeout_seconds, } ) result["rollback_result"] = rollback_result result["rolled_back"] = bool(rollback_result.get("applied")) result["status"] = "verified_and_rolled_back" if result["applied"] and result["rolled_back"] else ("applied_rollback_failed" if result["applied"] else result["status"]) return result def metadata_write(payload: dict[str, Any]) -> dict[str, Any]: method = METADATA_WRITE_METHOD mode = str(payload.get("execution_mode") or payload.get("mode") or "plan").strip().casefold() payload, context_error = normalize_repository_write_context(payload, method) if context_error: return {"schema": "onec_metadata_write.v1", "method": method, "base_id": payload.get("base_id"), **context_error} payload = resolve_write_gate_context(payload) repository_error = repository_apply_gate(payload, method, mode) if repository_error: return repository_error payload = metadata_write_save_first_payload(payload, mode) target = payload.get("target") if isinstance(payload.get("target"), dict) else {} intent = payload.get("intent") if isinstance(payload.get("intent"), dict) else {} explicit_route_kind = payload.get("target_kind") or target.get("target_kind") or target.get("area") or payload.get("area") if not explicit_route_kind and str(target.get("kind") or "").strip().casefold() in { "form", "форма", "module", "модуль", "bsl", "object", "объект", "metadata", "метаданные" }: explicit_route_kind = target.get("kind") if not explicit_route_kind and str(payload.get("kind") or "").strip().casefold() in {"form", "форма", "module", "модуль", "bsl"}: explicit_route_kind = payload.get("kind") explicit_target_kind = explicit_route_kind target_kind = str(explicit_target_kind or "form").strip().casefold() if metadata_write_is_schedule_target(payload): result = metadata_scheduled_job_schedule_write(payload) result.setdefault("routed_method", "metadata.write:scheduled_job_schedule") result.setdefault("target_kind", "schedule") return result if target_kind in {"object", "объект", "metadata", "метаданные"}: object_operation = str( first_non_empty_arg(payload, "operation", default=target.get("operation") or intent.get("operation")) or "" ).strip().casefold() if object_operation in {"add_attribute", "attribute_add", "добавить_реквизит", "добавитьреквизит"}: add_payload: dict[str, Any] = { **payload, "template_member_ref": first_non_empty_arg( payload, "template_member_ref", default=target.get("template_member_ref") or intent.get("template_member_ref"), ), "new_member_name": first_non_empty_arg( payload, "new_member_name", default=target.get("new_member_name") or intent.get("new_member_name"), ), "allow_saved_state_write": True, "execution_mode": mode, } for optional_key in ("new_member_synonym", "new_member_comment"): if optional_key not in add_payload: optional_value = target.get(optional_key) if optional_key in target else intent.get(optional_key) if optional_value is not None: add_payload[optional_key] = optional_value add_payload.pop("target", None) add_payload.pop("intent", None) validation_error = validate_metadata_object_member_add_payload(add_payload) if validation_error: return validation_error add_result = metadata_object_member_add(add_payload) return { "schema": "onec_metadata_write.v1", "method": method, "status": add_result.get("status"), "execution_mode": mode, "target_kind": "object", "operation": "add_attribute", "base_id": payload.get("base_id"), "routed_method": OBJECT_MEMBER_ADD_METHOD, "result": add_result, } property_name = first_non_empty_arg(payload, "property", default=target.get("property") or intent.get("property")) value_present = "value" in payload or "value" in target or "value" in intent or "new" in intent requested_value = ( payload.get("value") if "value" in payload else target.get("value") if "value" in target else intent.get("value") if "value" in intent else intent.get("new") ) object_payload: dict[str, Any] = { **payload, "property": property_name, "allow_saved_state_write": True, "execution_mode": mode, } if value_present: object_payload["value"] = requested_value for selector_key in ( "ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "extension", "layer", "member_ref", "child_ref", "member_kind", "member_name", "canonical_path", ): if target.get(selector_key) not in {None, ""}: object_payload[selector_key] = target.get(selector_key) if target.get("object_kind") not in {None, ""}: object_payload["kind"] = target.get("object_kind") if target.get("object_ref") not in {None, ""}: object_payload["ref"] = target.get("object_ref") object_payload.pop("target", None) object_payload.pop("intent", None) if str(object_payload.get("kind") or "").strip().casefold() in {"object", "объект", "metadata", "метаданные"}: object_payload.pop("kind", None) validation_error = validate_metadata_object_property_write_payload(object_payload) if validation_error: return validation_error object_result = metadata_object_property_write(object_payload) return { "schema": "onec_metadata_write.v1", "method": method, "status": object_result.get("status"), "execution_mode": mode, "target_kind": "object", "base_id": payload.get("base_id"), "routed_method": OBJECT_PROPERTY_WRITE_METHOD, "result": object_result, } if target_kind not in {"form", "форма", "module", "модуль", "bsl"}: return invalid_argument(method, "target.kind", "Only form, module, object-property, and scheduled-job schedule saved-state writes are currently routed.", allowed_values=["form", "module", "object", "schedule"]) requested_path = str(target.get("canonical_path") or payload.get("canonical_path") or target.get("path") or payload.get("path") or "").strip() path_plan = metadata_write_plan({**payload, "resolve_origin": False}) if requested_path else None planned_target_kind = metadata_write_plan_target_kind(path_plan) path_can_resolve_saved_state_module = metadata_write_path_can_resolve_saved_state_module(path_plan) path_resolution_for_route = path_plan.get("path_resolution") if isinstance(path_plan, dict) and isinstance(path_plan.get("path_resolution"), dict) else {} path_can_route_command_button = ( str(path_resolution_for_route.get("path_kind") or "") == "form_command" or ( str(path_resolution_for_route.get("path_kind") or "") == "form_element" and str(path_resolution_for_route.get("form_member_kind") or "") == "button" ) ) if requested_path and not explicit_target_kind and planned_target_kind in {"form", "module"}: target_kind = planned_target_kind if requested_path and not metadata_write_concrete_reference(payload, target) and not path_can_resolve_saved_state_module and not path_can_route_command_button: plan_target = path_plan.get("target") if isinstance(path_plan, dict) and isinstance(path_plan.get("target"), dict) else {} apply_hint = metadata_write_plan_apply_hint(path_plan) blocked_response = { "schema": "onec_metadata_write.v1", "method": method, "status": "blocked", "execution_mode": mode, "target_kind": plan_target.get("target_kind") or target_kind, "base_id": payload.get("base_id"), "error": "write_plan_required", "routed_method": METADATA_WRITE_PLAN_METHOD, "plan": path_plan, "diagnostics": { "message": "metadata.write cannot write an effective canonical path directly. Resolve origin/layer evidence or pass a concrete saved-state reference.", }, } if apply_hint: blocked_response["apply_payload_hint"] = apply_hint if isinstance(apply_hint.get("next_resolution"), dict): blocked_response["next_resolution"] = apply_hint.get("next_resolution") blocked_response["preflight"] = metadata_write_preflight({**payload, "resolve_origin": False}) return blocked_response if requested_path and isinstance(path_plan, dict) and path_plan.get("allowed") is False and not path_can_resolve_saved_state_module and not path_can_route_command_button: apply_hint = metadata_write_plan_apply_hint(path_plan) blocked_response = { "schema": "onec_metadata_write.v1", "method": method, "status": "blocked", "execution_mode": mode, "target_kind": planned_target_kind or target_kind, "base_id": payload.get("base_id"), "error": "write_plan_blocked", "routed_method": METADATA_WRITE_PLAN_METHOD, "plan": path_plan, "problems": path_plan.get("problems") if isinstance(path_plan.get("problems"), list) else [], "diagnostics": { "message": "metadata.write will not call apply while metadata.write.plan reports blocking problems.", }, } if apply_hint: blocked_response["apply_payload_hint"] = apply_hint blocked_response["preflight"] = metadata_write_preflight({**payload, "resolve_origin": False}) return blocked_response if target_kind in {"module", "модуль", "bsl"}: resolved = metadata_write_resolve_module_target(payload, target, mode) if isinstance(resolved, dict): return resolved write_payload, search = resolved for key, value in metadata_write_apply_hint_payload(path_plan).items(): write_payload.setdefault(key, value) if write_payload.get("_embedded_form_module"): result = form_embedded_module_handler_write_apply( write_payload, base_id=str(write_payload.get("base_id") or payload.get("base_id") or ""), table=str(write_payload.get("table") or "ConfigCASSave"), file_name=str(write_payload.get("file_name") or ""), handler_name=str(write_payload.get("handler_name") or write_payload.get("routine_name") or ""), mode=mode, timeout_seconds=int(write_payload.get("timeout_seconds") or 30), method_name=method, ) routed_method = "form_embedded_module_handler_write_apply" else: result = metadata_module_write_apply(write_payload) routed_method = MODULE_WRITE_APPLY_METHOD normalized_target_kind = "module" else: path_resolution = path_plan.get("path_resolution") if isinstance(path_plan, dict) and isinstance(path_plan.get("path_resolution"), dict) else {} route_as_command_button = ( str(path_resolution.get("path_kind") or "") == "form_command" and path_resolution.get("command_name") ) or ( str(path_resolution.get("path_kind") or "") == "form_element" and str(path_resolution.get("form_member_kind") or "") == "button" and path_resolution.get("element_name") ) if route_as_command_button: command_or_button_name = path_resolution.get("command_name") or path_resolution.get("element_name") command_payload = { **payload, "object_type": path_resolution.get("kind") or payload.get("object_type") or target.get("kind"), "form": path_resolution.get("form_name") or payload.get("form") or target.get("form"), "command_name": command_or_button_name, "button_name": payload.get("button_name") or target.get("button_name") or command_or_button_name, "command_title": payload.get("command_title") or payload.get("title") or payload.get("value") or target.get("title") or command_or_button_name, "mode": mode, "allow_saved_state_write": True, } result = metadata_form_command_button_write(command_payload) routed_method = FORM_COMMAND_BUTTON_WRITE_METHOD normalized_target_kind = "form" response = { "schema": "onec_metadata_write.v1", "method": method, "status": result.get("status"), "execution_mode": mode, "target_kind": normalized_target_kind, "base_id": payload.get("base_id"), "routed_method": routed_method, "path_resolution": path_resolution, "result": result, "preflight": metadata_write_preflight({**payload, "resolve_origin": False}), } return response resolved = metadata_write_resolve_form_target(payload, target, mode) if isinstance(resolved, dict): return resolved write_payload, search = resolved for key, value in metadata_write_apply_hint_payload(path_plan).items(): write_payload.setdefault(key, value) result = metadata_form_element_write_apply(write_payload) retry_result, retry_resolution = metadata_write_form_retry_after_prepare(payload, write_payload, mode, result) result = retry_result if retry_resolution is not None: search = retry_resolution routed_method = FORM_ELEMENT_WRITE_APPLY_METHOD normalized_target_kind = "form" response = { "schema": "onec_metadata_write.v1", "method": method, "status": result.get("status"), "execution_mode": mode, "target_kind": normalized_target_kind, "base_id": payload.get("base_id"), "routed_method": routed_method, "result": result, } if search is not None: resolution_method = str(search.get("method") or SAVED_STATE_MODULES_SEARCH_METHOD) if isinstance(search, dict) else SAVED_STATE_MODULES_SEARCH_METHOD response["resolution"] = {"method": resolution_method, "status": search.get("status"), "counts": search.get("counts")} if isinstance(search, dict) and isinstance(search.get("result"), dict): response["resolution"]["prepare_status"] = search["result"].get("status") if isinstance(search, dict) and isinstance(search.get("retry_search"), dict): response["resolution"]["retry_search_status"] = search["retry_search"].get("status") response["resolution"]["retry_search_counts"] = search["retry_search"].get("counts") if payload.get("include_preflight") is True: response["preflight"] = metadata_write_preflight({**payload, "resolve_origin": False}) return response def code_write_operation(payload: dict[str, Any]) -> str: if payload.get("old") is not None or payload.get("new") is not None: return "fragment_replace" if payload.get("routine_text") is not None or (payload.get("text") is not None and payload.get("routine_name")): return "routine_replace" if payload.get("module_text") is not None or payload.get("text") is not None: return "module_text_replace" return "code_write" def code_write_public_target(payload: dict[str, Any], target: dict[str, Any]) -> dict[str, Any]: result: dict[str, Any] = {"kind": "module"} for key in ("canonical_path", "path"): value = target.get(key) or payload.get(key) if value: result[key] = value break for source_key, public_key in ( ("object_type", "object_type"), ("object_name", "object_name"), ("object_guid", "object_guid"), ("form", "form"), ("form_name", "form_name"), ("routine_name", "routine_name"), ("extension", "extension"), ): value = target.get(source_key) if source_key in target else payload.get(source_key) if value: result[public_key] = value return result def code_write_metadata_payload(payload: dict[str, Any], *, mode: str) -> dict[str, Any]: target = dict(payload.get("target") or {}) target.setdefault("kind", "module") for source_key, target_key in (("kind", "object_type"), ("name", "object_name"), ("guid", "object_guid")): if payload.get(source_key) is not None and target.get(target_key) is None: target[target_key] = payload.get(source_key) for key in ( "canonical_path", "path", "object_type", "object_name", "object_guid", "form", "form_name", "routine_name", "extension", "preferred_extension", "module_ref", "module_id", "file_name", ): if payload.get(key) is not None and target.get(key) is None: target[key] = payload.get(key) requested_path = str(target.get("canonical_path") or target.get("path") or payload.get("canonical_path") or payload.get("path") or "").strip() extension_name = str(target.get("extension") or payload.get("extension") or "").strip() requested_parts = [part.strip() for part in requested_path.split(".") if part.strip()] first_part_kind = KIND_ALIASES.get(normalize(requested_parts[0])) if requested_parts else None if extension_name and len(requested_parts) >= 3 and normalize(requested_parts[0]) == normalize(extension_name) and not first_part_kind: rewritten_path = ".".join(["CommonForm", *requested_parts[1:]]) target["path"] = rewritten_path target.setdefault("object_type", "CommonForm") target.setdefault("object_name", requested_parts[1]) target.setdefault("routine_name", requested_parts[-1]) requested_path = rewritten_path if requested_path and not target.get("routine_name") and not payload.get("routine_name"): path_resolution = metadata_write_plan_path_parts(requested_path) if path_resolution.get("routine_name"): target["routine_name"] = path_resolution.get("routine_name") result = { key: value for key, value in payload.items() if key not in { "include_storage", "target", "mode", "execution_mode", "full_text", "code", "fragment", } } result["target"] = target result["target_kind"] = "module" result["mode"] = mode if target.get("routine_name") and result.get("routine_name") is None: result["routine_name"] = str(target.get("routine_name") or "") if payload.get("module_text") is None: for alias in ("full_text", "code"): if payload.get(alias) is not None: result["module_text"] = str(payload.get(alias) or "") break if result.get("module_text") is not None and result.get("text") is None: result["text"] = str(result.get("module_text") or "") if payload.get("old") is not None or payload.get("new") is not None: result["_force_fragment_replace"] = True return metadata_write_save_first_payload(result, mode) def code_write(payload: dict[str, Any]) -> dict[str, Any]: method = CODE_WRITE_METHOD payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload mode = str(payload.get("execution_mode") or payload.get("mode") or "apply").strip().casefold() payload, context_error = normalize_repository_write_context(payload, method) if context_error: return { "schema": "onec_code_write.v1", "method": method, "status": "blocked", "execution_mode": mode, "base_id": payload.get("base_id"), "applied": False, "write_mode": {"target": "saved_state", "activation_state": "not_activated", "production_apply": False}, **context_error, } resolved_payload = resolve_write_gate_context(payload) write_payload = code_write_metadata_payload(resolved_payload, mode=mode) target = write_payload.get("target") if isinstance(write_payload.get("target"), dict) else {} metadata_result = metadata_write(write_payload) nested_result = metadata_result.get("result") if isinstance(metadata_result.get("result"), dict) else {} apply_result = nested_result.get("apply_result") if isinstance(nested_result.get("apply_result"), dict) else {} applied = bool(nested_result.get("applied") or apply_result.get("applied")) response = { "schema": "onec_code_write.v1", "method": method, "status": metadata_result.get("status"), "execution_mode": mode, "base_id": payload.get("base_id"), "target": code_write_public_target(write_payload, target), "operation": code_write_operation(write_payload), "applied": applied, "write_mode": { "target": "saved_state", "activation_state": "not_activated", "production_apply": False, }, } if metadata_result.get("error"): response["error"] = metadata_result.get("error") elif nested_result.get("error"): response["error"] = nested_result.get("error") if isinstance(metadata_result.get("counts"), dict): response["counts"] = metadata_result.get("counts") elif isinstance(nested_result.get("counts"), dict): response["counts"] = nested_result.get("counts") if isinstance(metadata_result.get("scope"), dict): response["scope"] = metadata_result.get("scope") elif isinstance(nested_result.get("scope"), dict): response["scope"] = nested_result.get("scope") elif isinstance(nested_result.get("routine"), dict) and nested_result["routine"].get("scope") is not None: routine_scope = nested_result["routine"].get("scope") if str(routine_scope) == "routine": routine_name = ( response.get("target", {}).get("routine_name") if isinstance(response.get("target"), dict) else None ) or write_payload.get("routine_name") response["scope"] = {"kind": "routine", **({"routine_name": routine_name} if routine_name else {})} elif str(routine_scope): response["scope"] = {"kind": str(routine_scope)} if "counts" not in response and isinstance(nested_result.get("routine"), dict) and nested_result["routine"].get("occurrences") is not None: response["counts"] = {"occurrences": int(nested_result["routine"].get("occurrences") or 0)} if isinstance(metadata_result.get("diagnostics"), dict): response["diagnostics"] = metadata_result.get("diagnostics") elif isinstance(nested_result.get("diagnostics"), dict): response["diagnostics"] = nested_result.get("diagnostics") if isinstance(metadata_result.get("resolution"), dict): response["resolution"] = metadata_result.get("resolution") routed_method = metadata_result.get("routed_method") if routed_method: response["route"] = {"method": routed_method} backup_ids = collect_backup_ids(metadata_result) if backup_ids: response["_history_evidence"] = {"backup_ids": backup_ids} if payload.get("include_storage") is True: response["metadata_write"] = metadata_result if isinstance(resolved_payload.get("owner_resolution"), dict): response["owner_resolution"] = resolved_payload["owner_resolution"] return response def form_write_matrix_target_selector(target: dict[str, Any]) -> dict[str, Any]: path = str(target.get("path") or "").strip() section = str(target.get("_profile_section") or target.get("section") or "").strip() selector: dict[str, Any] = {"element_path": path} if path else {} name = target.get("name") if section == "commands" and name: selector["command"] = name elif section in {"attributes", "attribute_fields"} and name: selector["attribute"] = name elif name: selector["element"] = name return selector def form_write_matrix_infer_value_type(prop: dict[str, Any]) -> str: value_type = str(prop.get("value_type") or "").strip() if value_type: return value_type value = prop.get("value") text = str(value) if value in {True, False} or text in {"0", "1"}: return "bool_atom" if re.fullmatch(r"-?\d+", text or ""): return "integer_atom" return "string" if value is None or isinstance(value, str) else "scalar" def form_write_matrix_test_value(prop: dict[str, Any]) -> tuple[Any | None, str | None]: canonical = normalize_form_property_name(prop.get("canonical_property") or prop.get("property") or prop.get("presentation")) old = prop.get("value") value_type = form_write_matrix_infer_value_type(prop) if canonical in {"id", "name", "path_to_data"}: return None, "identity_or_binding_property" if value_type == "string": old_text = "" if old is None else str(old) if len(old_text) > 160: return None, "string_too_long_for_generic_smoke" suffix = "_SMOKE" if old_text.endswith(suffix): return old_text.removesuffix(suffix) or "SMOKE_VALUE", None return f"{old_text}{suffix}" if old_text else f"SMOKE_{canonical.upper()}", None if value_type == "bool_atom": old_text = str(old) if old_text == "1": return "0", None if old_text == "0": return "1", None return None, "bool_atom_not_0_or_1" if value_type == "bool_or_enum_atom": old_text = str(old) if old_text == "1": return "0", None if old_text == "0": return "1", None return None, "enum_values_unknown" return None, "value_type_not_smoke_safe" def form_write_matrix_build_entry(profile: dict[str, Any], requested_target: dict[str, Any], prop: dict[str, Any]) -> dict[str, Any]: property_name = prop.get("canonical_property") or prop.get("property") or prop.get("presentation") semantic_prop = form_semantic_property_for_parameter(requested_target, prop.get("parameter_index")) value_type = form_write_matrix_infer_value_type(prop) effective_target, effective_source = form_effective_write_target(profile, requested_target, property_name, {"property": property_name}) effective_old = form_property_current_value(effective_target, property_name) test_value, test_value_reason = form_write_matrix_test_value({**prop, "value": effective_old, "value_type": value_type}) if value_type == "string" and effective_old in {None, ""} and not effective_source: test_value = None test_value_reason = "empty_local_string_requires_codec_probe" probe_value = test_value if test_value_reason is None else prop.get("value") path_edit, error = form_element_write_edit(effective_target, {"property": property_name, "value": probe_value}, 0) write_path = path_edit.get("path") if isinstance(path_edit, dict) else None status = "candidate" if write_path else "not_writable" can_smoke = bool(write_path and test_value_reason is None) if status == "not_writable": reason = "property_not_writable" elif not can_smoke: reason = test_value_reason or "not_smoke_safe" else: reason = None return { "status": status, "can_smoke": can_smoke, "reason": reason, "selector": form_write_matrix_target_selector(requested_target), "requested_target": form_write_target_public(requested_target), "effective_target": form_write_target_public(effective_target), "effective_source": effective_source, "property": { "property": prop.get("property"), "canonical_property": normalize_form_property_name(property_name), "presentation": prop.get("presentation") or property_name, "semantic_name": semantic_prop.get("name") if semantic_prop else None, "semantic_group": semantic_prop.get("group") if semantic_prop else None, "semantic_source": semantic_prop.get("source") if semantic_prop else None, "semantic_status": semantic_prop.get("status") if semantic_prop else None, "parameter_index": prop.get("parameter_index"), "read_path": prop.get("path"), "write_path": write_path, "old": effective_old, "requested_old": prop.get("value"), "test_value": test_value if can_smoke else None, "value_type": value_type, "verification": prop.get("verification"), }, **({"error": error} if error else {}), } def metadata_form_write_matrix_build(payload: dict[str, Any]) -> dict[str, Any]: method = FORM_WRITE_MATRIX_BUILD_METHOD payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error resolved_source = write_learning_resolve_file(payload, method=method, timeout_seconds=int(timeout_seconds or 30)) if isinstance(resolved_source, dict): return resolved_source table, file_name = resolved_source decode_payload = dict(payload) for owner_selector_key in ("ref", "kind", "name", "guid", "object_type", "object_name", "object_guid"): decode_payload.pop(owner_selector_key, None) decoded = metadata_form_decode( { **decode_payload, "base_id": base_id, "table": table, "file_name": file_name, "include_storage": True, "include_parameters": True, "max_items": int(payload.get("max_items") or 5000), "timeout_seconds": int(timeout_seconds or 30), } ) if decoded.get("status") != "ok": result = dict(decoded) result["method"] = method return result profile = decoded.get("profile") if isinstance(decoded.get("profile"), dict) else {} codec_probe_by_path: dict[str, dict[str, Any]] = {} try: from parser.payload import decode_payload_lossless, inspect_brace_text_path data, _config, read_error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=int(timeout_seconds or 30)) if not read_error and data: decoded_payload = decode_payload_lossless(data) text = decoded_payload.get("text") if isinstance(text, str): probe_paths = set() for target in form_profile_write_targets(profile): for prop in form_write_target_writable_properties(target): if not isinstance(prop, dict): continue property_name = prop.get("canonical_property") or prop.get("property") or prop.get("presentation") value_type = form_write_matrix_infer_value_type(prop) effective_target, effective_source = form_effective_write_target(profile, target, property_name, {"property": property_name}) effective_old = form_property_current_value(effective_target, property_name) if value_type == "string" and effective_old in {None, ""} and not effective_source: path_edit, _error = form_element_write_edit(effective_target, {"property": property_name, "value": prop.get("value")}, 0) if isinstance(path_edit, dict) and path_edit.get("path"): probe_paths.add(str(path_edit.get("path"))) for probe_path in sorted(probe_paths): try: codec_probe_by_path[probe_path] = inspect_brace_text_path(text, probe_path, max_depth=2, max_children=8) except Exception as exc: codec_probe_by_path[probe_path] = {"path": probe_path, "error": str(exc)} except Exception: codec_probe_by_path = {} entries = [] for target in form_profile_write_targets(profile): for prop in form_write_target_writable_properties(target): if not isinstance(prop, dict) or not prop.get("path"): continue entry = form_write_matrix_build_entry(profile, target, prop) write_path = ((entry.get("property") or {}) if isinstance(entry.get("property"), dict) else {}).get("write_path") if entry.get("reason") == "empty_local_string_requires_codec_probe" and write_path in codec_probe_by_path: entry["codec_probe"] = codec_probe_by_path[str(write_path)] probe_node = entry["codec_probe"].get("node") if isinstance(entry["codec_probe"], dict) else {} if isinstance(probe_node, dict) and probe_node.get("type") == "string": entry["can_smoke"] = True entry["reason"] = None prop_info = entry.get("property") if isinstance(entry.get("property"), dict) else {} canonical = normalize_form_property_name(prop_info.get("canonical_property") or prop_info.get("property") or prop_info.get("presentation")) prop_info["test_value"] = f"SMOKE_{canonical.upper()}" entry["property"] = prop_info elif isinstance(probe_node, dict) and probe_node.get("type") == "list": entry["reason"] = "composite_node_requires_semantic_rule" entries.append(entry) can_smoke = [entry for entry in entries if entry.get("can_smoke")] return { "schema": "onec_form_write_matrix.v1", "method": method, "status": "ok", "base_id": base_id, "source": {"kind": "live_sql", "table": table, "file_name": file_name}, "form": decoded.get("form"), "entries": entries, "counts": { "entries": len(entries), "candidates": sum(1 for entry in entries if entry.get("status") == "candidate"), "can_smoke": len(can_smoke), "not_smoke_safe": sum(1 for entry in entries if entry.get("status") == "candidate" and not entry.get("can_smoke")), "not_writable": sum(1 for entry in entries if entry.get("status") == "not_writable"), }, } def metadata_form_write_matrix_smoke(payload: dict[str, Any]) -> dict[str, Any]: method = FORM_WRITE_MATRIX_SMOKE_METHOD payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error allow_apply, allow_apply_error = strict_bool_argument(payload, "allow_sql_saved_state_apply", method=method, default=False) if allow_apply_error: return allow_apply_error if not allow_apply: return invalid_argument(method, "allow_sql_saved_state_apply", "Write-matrix smoke is opt-in; pass allow_sql_saved_state_apply=true.") allow_rollback, allow_rollback_error = strict_bool_argument(payload, "allow_sql_saved_state_rollback", method=method, default=False) if allow_rollback_error: return allow_rollback_error if not allow_rollback: return invalid_argument(method, "allow_sql_saved_state_rollback", "Write-matrix smoke requires rollback opt-in; pass allow_sql_saved_state_rollback=true.") timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error max_candidates, max_candidates_error = parse_int_argument(payload, "max_candidates", method=method, default=100, minimum=1, maximum=5000) if max_candidates_error: return max_candidates_error matrix = metadata_form_write_matrix_build(payload) if matrix.get("status") != "ok": result = dict(matrix) result["method"] = method return result source = matrix.get("source") if isinstance(matrix.get("source"), dict) else {} results = [] candidates = [entry for entry in matrix.get("entries") or [] if isinstance(entry, dict) and entry.get("can_smoke")] for entry in candidates[: int(max_candidates or 100)]: prop = entry.get("property") if isinstance(entry.get("property"), dict) else {} selector = entry.get("selector") if isinstance(entry.get("selector"), dict) else {} write_payload = { "base_id": base_id, "target": { "kind": "form", "table": source.get("table"), "file_name": source.get("file_name"), **selector, }, "mode": "apply_and_rollback", "edits": [{"property": prop.get("canonical_property") or prop.get("property"), "value": prop.get("test_value")}], "allow_sql_saved_state_apply": True, "allow_sql_saved_state_rollback": True, "timeout_seconds": int(timeout_seconds or 30), } write_result = metadata_write(write_payload) results.append( { "status": "verified" if write_result.get("status") == "verified_and_rolled_back" else str(write_result.get("status") or "error"), "entry": entry, "metadata_write": write_payload, "result_status": write_result.get("status"), "error": (((write_result.get("result") or {}).get("proposal") or {}).get("error") if isinstance(write_result.get("result"), dict) else write_result.get("error")), "diagnostics": ((write_result.get("result") or {}).get("diagnostics") if isinstance(write_result.get("result"), dict) else write_result.get("diagnostics")), "rolled_back": ((write_result.get("result") or {}) if isinstance(write_result.get("result"), dict) else {}).get("rolled_back"), "semantic_verification": (((write_result.get("result") or {}).get("apply_result") or {}).get("semantic_verification") if isinstance(write_result.get("result"), dict) else None), } ) report = { "schema": "onec_form_write_matrix_smoke.v1", "method": method, "status": "ok" if all(row.get("status") == "verified" for row in results) else "partial", "base_id": base_id, "source": source, "form": matrix.get("form"), "results": results, "counts": { "matrix_entries": (matrix.get("counts") or {}).get("entries"), "can_smoke": len(candidates), "smoked": len(results), "verified": sum(1 for row in results if row.get("status") == "verified"), "failed": sum(1 for row in results if row.get("status") != "verified"), "not_smoked": max(0, len(candidates) - len(results)), }, } learning_id = str(payload.get("learning_id") or "").strip() if learning_id: if not re.fullmatch(r"[A-Za-z0-9_.-]{1,80}", learning_id): return invalid_argument(method, "learning_id", "learning_id must be 1-80 chars: letters, digits, dot, underscore, or dash.") root = write_learning_dir() / learning_id root.mkdir(parents=True, exist_ok=True) path = root / f"write-matrix-smoke-{uuid.uuid4().hex}.json" path.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") report["path"] = str(path) return report def write_learning_dir() -> Path: return Path(os.environ.get("ONEC_ADAPTER_WRITE_LEARNING_DIR") or "/data/adapter-write-learning") def write_learning_stage_path(learning_id: str, stage: str, snapshot_id: str) -> Path: return write_learning_dir() / learning_id / f"{stage}-{snapshot_id}.json" def write_learning_latest_path(learning_id: str, stage: str) -> Path: return write_learning_dir() / learning_id / f"latest-{stage}.json" def write_learning_capture_targets(profile: dict[str, Any]) -> list[dict[str, Any]]: targets = [] for target in form_profile_write_targets(profile): public = form_write_target_public(target) public["writable_properties"] = form_write_target_writable_properties(target) targets.append(public) return targets def write_learning_resolve_file(payload: dict[str, Any], *, method: str, timeout_seconds: int) -> dict[str, Any] | tuple[str, str]: table = str(payload.get("table") or "ConfigCASSave") if table not in FORM_ELEMENT_SAVED_STATE_TABLES: return invalid_argument(method, "table", "Only saved-state tables may be captured for write learning.", allowed_values=sorted(FORM_ELEMENT_SAVED_STATE_TABLES)) file_name = str(payload.get("file_name") or "").strip() if file_name: return table, file_name resolve_payload = { **payload, "table": table, "property": payload.get("property") or "Заголовок", "value": payload.get("value") if "value" in payload else "", "timeout_seconds": timeout_seconds, } resolved = metadata_form_write_target_resolve(resolve_payload) if resolved.get("status") != "ok": result = dict(resolved) result["method"] = method return result source = resolved.get("source") if isinstance(resolved.get("source"), dict) else {} file_name = str(source.get("file_name") or "") if not file_name: return invalid_argument(method, "file_name", "Could not resolve saved-state form file_name for write learning.") return table, file_name def metadata_write_learning_capture(payload: dict[str, Any], stage: str) -> dict[str, Any]: method = f"metadata.write_learning.capture_{stage}" payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error resolved_source = write_learning_resolve_file(payload, method=method, timeout_seconds=int(timeout_seconds or 30)) if isinstance(resolved_source, dict): return resolved_source table, file_name = resolved_source decode_payload = dict(payload) for owner_selector_key in ("ref", "kind", "name", "guid", "object_type", "object_name", "object_guid"): decode_payload.pop(owner_selector_key, None) decoded = metadata_form_decode( { **decode_payload, "base_id": base_id, "table": table, "file_name": file_name, "include_storage": True, "include_parameters": True, "max_items": int(payload.get("max_items") or 5000), "timeout_seconds": int(timeout_seconds or 30), } ) if decoded.get("status") != "ok": result = dict(decoded) result["method"] = method return result data, config, read_error = read_storage_file_bytes(base_id, table, file_name, timeout_seconds=int(timeout_seconds or 30)) if read_error: result = dict(read_error) result["method"] = method return result profile = decoded.get("profile") if isinstance(decoded.get("profile"), dict) else {} learning_id = str(payload.get("learning_id") or uuid.uuid4().hex).strip() if not re.fullmatch(r"[A-Za-z0-9_.-]{1,80}", learning_id): return invalid_argument(method, "learning_id", "learning_id must be 1-80 chars: letters, digits, dot, underscore, or dash.") snapshot_id = uuid.uuid4().hex captured_at = datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z") artifact = { "schema": "onec_write_learning_capture.v1", "method": method, "status": "ok", "learning_id": learning_id, "snapshot_id": snapshot_id, "stage": stage, "captured_at_utc": captured_at, "base_id": base_id, "source": { "kind": "live_sql", "server": (config or {}).get("server"), "database": (config or {}).get("database"), "table": table, "file_name": file_name, }, "form": decoded.get("form"), "storage": {"sha1": hashlib.sha1(data or b"").hexdigest(), "bytes": len(data or b"")}, "targets": write_learning_capture_targets(profile), "counts": { "targets": len(form_profile_write_targets(profile)), "writable_properties": sum(len(form_write_target_writable_properties(target)) for target in form_profile_write_targets(profile)), }, } root = write_learning_dir() / learning_id root.mkdir(parents=True, exist_ok=True) path = write_learning_stage_path(learning_id, stage, snapshot_id) path.write_text(json.dumps(artifact, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") write_learning_latest_path(learning_id, stage).write_text(json.dumps({"snapshot_id": snapshot_id, "path": str(path)}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") return { "schema": "onec_write_learning_capture_result.v1", "method": method, "status": "ok", "learning_id": learning_id, "snapshot_id": snapshot_id, "stage": stage, "path": str(path), "source": artifact["source"], "form": artifact.get("form"), "storage": artifact["storage"], "counts": artifact["counts"], } def write_learning_load_capture(payload: dict[str, Any], stage: str) -> dict[str, Any] | tuple[dict[str, Any], Path]: method = str(payload.get("_method") or "metadata.write_learning.diff") snapshot_key = f"{stage}_snapshot_id" path_key = f"{stage}_path" if payload.get(path_key): path = Path(str(payload.get(path_key))).resolve() else: learning_id = str(payload.get("learning_id") or "").strip() if not learning_id: return invalid_argument(method, "learning_id", "Pass learning_id or explicit before_path/after_path.") if not re.fullmatch(r"[A-Za-z0-9_.-]{1,80}", learning_id): return invalid_argument(method, "learning_id", "Invalid learning_id.") snapshot_id = str(payload.get(snapshot_key) or "").strip() if snapshot_id: path = write_learning_stage_path(learning_id, stage, snapshot_id).resolve() else: latest = write_learning_latest_path(learning_id, stage).resolve() if not latest.is_file(): return invalid_argument(method, snapshot_key, f"No latest {stage} capture found for learning_id.") try: pointer = json.loads(latest.read_text(encoding="utf-8")) except Exception as exc: return invalid_argument(method, snapshot_key, f"Could not read latest {stage} pointer: {exc}") path = Path(str(pointer.get("path") or "")).resolve() root = write_learning_dir().resolve() try: path.relative_to(root) except ValueError: return invalid_argument(method, path_key, "Capture path must be inside the write-learning directory.") if not path.is_file(): return invalid_argument(method, path_key, "Capture file was not found.") try: data = json.loads(path.read_text(encoding="utf-8")) except Exception as exc: return invalid_argument(method, path_key, f"Could not read capture JSON: {exc}") return data, path def write_learning_target_key(target: dict[str, Any]) -> str: return "|".join(str(target.get(key) or "") for key in ("section", "path", "name", "id")) def write_learning_stable_target_key(target: dict[str, Any]) -> str: return "|".join(str(target.get(key) or "") for key in ("section", "name", "id")) def write_learning_target_map(capture: dict[str, Any]) -> dict[str, dict[str, Any]]: result: dict[str, dict[str, Any]] = {} for target in capture.get("targets") or []: if not isinstance(target, dict): continue key = write_learning_stable_target_key(target) if key and key not in result: result[key] = target return result def write_learning_property_map(capture: dict[str, Any]) -> dict[str, dict[str, Any]]: result: dict[str, dict[str, Any]] = {} for target in capture.get("targets") or []: if not isinstance(target, dict): continue target_key = write_learning_target_key(target) for prop in target.get("writable_properties") or []: if not isinstance(prop, dict) or not prop.get("path"): continue key = f"{target_key}|{prop.get('path')}" result[key] = {"target": target, "property": prop} return result def metadata_write_learning_diff(payload: dict[str, Any]) -> dict[str, Any]: method = "metadata.write_learning.diff" before_loaded = write_learning_load_capture({**payload, "_method": method}, "before") if isinstance(before_loaded, dict): return before_loaded after_loaded = write_learning_load_capture({**payload, "_method": method}, "after") if isinstance(after_loaded, dict): return after_loaded before, before_path = before_loaded after, after_path = after_loaded before_targets = write_learning_target_map(before) after_targets = write_learning_target_map(after) before_props = write_learning_property_map(before) after_props = write_learning_property_map(after) changes = [] target_moves = [] for key, after_target in after_targets.items(): before_target = before_targets.get(key) if not before_target: continue before_target_path = str(before_target.get("path") or "") after_target_path = str(after_target.get("path") or "") if before_target_path == after_target_path: continue target_moves.append( { "target": { "section": after_target.get("section"), "name": after_target.get("name"), "id": after_target.get("id"), "marker": after_target.get("marker"), "type_name": after_target.get("type_name"), }, "old_path": before_target_path, "new_path": after_target_path, "presentation": f"{after_target.get('name') or after_target.get('title') or after_target_path}: {before_target_path} -> {after_target_path}", } ) for key, after_row in after_props.items(): before_row = before_props.get(key) if not before_row: continue before_prop = before_row["property"] after_prop = after_row["property"] if str(before_prop.get("value")) == str(after_prop.get("value")): continue changes.append( { "target": after_row["target"], "property": { "property": after_prop.get("property"), "canonical_property": after_prop.get("canonical_property"), "presentation": after_prop.get("presentation"), "path": after_prop.get("path"), "value_type": after_prop.get("value_type"), "verification": after_prop.get("verification"), }, "old": before_prop.get("value"), "new": after_prop.get("value"), "presentation": f"{after_row['target'].get('name') or after_row['target'].get('title') or after_row['target'].get('path')}.{after_prop.get('presentation') or after_prop.get('property')}: {before_prop.get('value')} -> {after_prop.get('value')}", } ) status = "changed" if changes or target_moves else "no_changes" return { "schema": "onec_write_learning_diff.v1", "method": method, "status": status, "learning_id": after.get("learning_id") or before.get("learning_id"), "base_id": after.get("base_id") or before.get("base_id"), "before": {"snapshot_id": before.get("snapshot_id"), "path": str(before_path), "storage": before.get("storage")}, "after": {"snapshot_id": after.get("snapshot_id"), "path": str(after_path), "storage": after.get("storage")}, "source": after.get("source") or before.get("source"), "form": after.get("form") or before.get("form"), "changes": changes, "target_moves": target_moves, "counts": {"changes": len(changes), "target_moves": len(target_moves), "before_targets": len(before.get("targets") or []), "after_targets": len(after.get("targets") or [])}, } def metadata_write_learning_infer_rule(payload: dict[str, Any]) -> dict[str, Any]: method = "metadata.write_learning.infer_rule" diff = payload.get("diff") if isinstance(payload.get("diff"), dict) else metadata_write_learning_diff(payload) if diff.get("status") not in {"changed", "ok"}: return { "schema": "onec_write_learning_rule.v1", "method": method, "status": diff.get("status") or "error", "diff": diff, "diagnostics": {"message": "No changed writable properties were found."}, } changes = [change for change in diff.get("changes") or [] if isinstance(change, dict)] target_moves = [move for move in diff.get("target_moves") or [] if isinstance(move, dict)] if target_moves and not changes: return { "schema": "onec_write_learning_rule.v1", "method": method, "status": "structural_move_rule_required", "diff": diff, "diagnostics": { "message": "The manual edit moved form targets in the element tree. Property write inference is not enough; add a reorder/move structural writer.", "next_action": "Implement a saved-state form move operation that swaps or reorders sibling nodes while preserving nested child nodes.", }, "rule": {"operation": "form_target_move", "moves": target_moves}, "counts": {"changes": len(changes), "target_moves": len(target_moves)}, } if not changes: return invalid_argument(method, "diff.changes", "No changes available to infer a rule.") if len(changes) > 1 and payload.get("allow_multiple") is not True: return { "schema": "onec_write_learning_rule.v1", "method": method, "status": "ambiguous", "diff": diff, "diagnostics": {"message": "More than one writable property changed. Pass allow_multiple=true or narrow the manual edit."}, "counts": {"changes": len(changes)}, } source = diff.get("source") if isinstance(diff.get("source"), dict) else {} edits = [] target = changes[0].get("target") if isinstance(changes[0].get("target"), dict) else {} for change in changes: prop = change.get("property") if isinstance(change.get("property"), dict) else {} edits.append( { "property": prop.get("canonical_property") or prop.get("presentation") or prop.get("property"), "value": change.get("new"), "expected_old": change.get("old"), } ) write_payload = { "method": "metadata.write", "payload": { "base_id": payload.get("base_id") or diff.get("base_id"), "target": { "kind": "form", "table": source.get("table"), "file_name": source.get("file_name"), "element_path": target.get("path"), }, "mode": payload.get("mode") or "plan", "edits": edits, }, } return { "schema": "onec_write_learning_rule.v1", "method": method, "status": "ok", "learning_id": diff.get("learning_id"), "rule": { "kind": "form_property_write", "source": source, "target": target, "edits": edits, "changes": changes, }, "metadata_write": write_payload, } def validate_metadata_object_full_payload(payload: dict[str, Any]) -> dict[str, Any] | None: selector_error = validate_object_selector_arguments(payload, "metadata.object.full") if selector_error: return selector_error table_or_error = metadata_storage_table(payload, "metadata.object.full") if isinstance(table_or_error, dict): return table_or_error if "sections" in payload: raw_sections = payload.get("sections") if not isinstance(raw_sections, list): return invalid_argument( "metadata.object.full", "sections", "sections must be a JSON array of section names.", allowed_values=sorted(FULL_METHOD_SECTIONS | {FULL_METHOD_ALL_KEY}), ) if not raw_sections: return invalid_argument( "metadata.object.full", "sections", "sections must contain at least one section name.", allowed_values=sorted(FULL_METHOD_SECTIONS | {FULL_METHOD_ALL_KEY}), ) requested_sections: list[str] = [] allowed_sections = set(FULL_METHOD_SECTIONS) for section in raw_sections: if not isinstance(section, str): return invalid_argument( "metadata.object.full", "sections", "sections must be a JSON array of section names.", allowed_values=sorted(allowed_sections | {FULL_METHOD_ALL_KEY}), ) section_name = section.strip().lower() if section_name == FULL_METHOD_ALL_KEY: for candidate in FULL_METHOD_SECTION_ORDER: if candidate not in requested_sections: requested_sections.append(candidate) continue if section_name not in allowed_sections: return invalid_argument( "metadata.object.full", "sections", f"Unsupported section `{section}`.", allowed_values=sorted(allowed_sections | {FULL_METHOD_ALL_KEY}), ) if section_name not in requested_sections: requested_sections.append(section_name) payload["_sections"] = requested_sections if "only" in payload: return invalid_argument( "metadata.object.full", "only", "metadata.object.full does not support `only`; use metadata.object.attributes for field subset selection.", allowed_values=["all", "attributes", "tabular_sections", "dimensions", "resources", "register_fields"], ) for name, default in [ ("include_storage", False), ("include_module_text", False), ("include_form_details", True), ("include_form_module_text", False), ("include_template_details", False), ("include_template_preview", True), ("include_parts_summary", False), ("include_parameters", True), ]: _, bool_error = strict_bool_argument(payload, name, method="metadata.object.full", default=default) if bool_error: return bool_error evidence_mode, evidence_mode_error = parse_evidence_mode_argument(payload, "metadata.object.full") if evidence_mode_error: return evidence_mode_error payload["_evidence_mode"] = evidence_mode for name, default, minimum, maximum in [ ("limit", 20, 1, None), ("max_forms", 20, 1, 100), ("max_form_items", 1000, 1, 5000), ("max_parameters", 80, 1, 500), ("max_items", 1000, 1, 5000), ("timeout_seconds", 60, 1, None), ("section_timeout_seconds", 0, 1, None), ("_section_timeout_seconds", 0, 1, None), ("adapter_timeout_seconds", 0, 1, None), ("_adapter_timeout_seconds", 0, 1, None), ]: _, int_error = parse_int_argument(payload, name, method="metadata.object.full", default=default, minimum=minimum, maximum=maximum) if int_error: return int_error return None def metadata_object_full(payload: dict[str, Any]) -> dict[str, Any]: base_id_or_error = require_base_id(payload, "metadata.object.full") if isinstance(base_id_or_error, dict): return base_id_or_error normalized_payload = normalize_object_selector_aliases(payload, "metadata.object.full") if isinstance(normalized_payload, dict) and normalized_payload.get("status") == "invalid_argument": return normalized_payload payload = normalized_payload if not has_object_selector(payload): return invalid_argument("metadata.object.full", "selector", OBJECT_SELECTOR_REQUIRED_MESSAGE) validation_error = validate_metadata_object_full_payload(payload) if validation_error: return validation_error return adapter_start_job({"method": "metadata.object.full", "payload": payload}) def adapter_now() -> float: return time.time() def adapter_job_store_path() -> Path: """Legacy JSON path used only for one-time migration to local SQLite.""" return Path(os.environ.get("ONEC_ADAPTER_JOB_STORE") or str(cache_db_path().with_name("adapter-jobs.json"))) def adapter_state_db_path() -> Path: """Local adapter state; never points at a configured 1C database.""" return Path(os.environ.get("ONEC_ADAPTER_STATE_DB") or str(cache_db_path())) def adapter_state_connection() -> sqlite3.Connection: path = adapter_state_db_path() path.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(path, timeout=30) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA synchronous=NORMAL") conn.execute( """ CREATE TABLE IF NOT EXISTS adapter_jobs ( job_id TEXT PRIMARY KEY, method TEXT, status TEXT NOT NULL, updated_at REAL NOT NULL, payload_json TEXT NOT NULL ) """ ) conn.execute("CREATE INDEX IF NOT EXISTS idx_adapter_jobs_updated_at ON adapter_jobs(updated_at)") conn.execute( """ CREATE TABLE IF NOT EXISTS adapter_state_meta ( key TEXT PRIMARY KEY, value TEXT, updated_at REAL NOT NULL ) """ ) conn.commit() return conn def adapter_load_legacy_jobs() -> dict[str, dict[str, Any]]: path = adapter_job_store_path() if not path.is_file(): return {} try: payload = json.loads(path.read_text(encoding="utf-8-sig")) except Exception: return {} jobs = payload.get("jobs") if isinstance(payload, dict) else None if not isinstance(jobs, dict): return {} return {str(job_id): dict(job) for job_id, job in jobs.items() if isinstance(job, dict)} ADAPTER_STATE_SECRET_KEYS = { "password", "newpassword", "oldpassword", "sqlpassword", "repositorypassword", "token", "servicetoken", "accesstoken", "refreshtoken", "authorization", "proxyauthorization", "clientsecret", } def adapter_state_safe_value(value: Any) -> Any: """Drop clear credentials before writing adapter-owned local state.""" if isinstance(value, dict): return { key: adapter_state_safe_value(item) for key, item in value.items() if normalize(key) not in ADAPTER_STATE_SECRET_KEYS } if isinstance(value, list): return [adapter_state_safe_value(item) for item in value] return value def adapter_load_jobs_from_store() -> None: global ADAPTER_JOB_STORE_LOADED if ADAPTER_JOB_STORE_LOADED: return ADAPTER_JOB_STORE_LOADED = True jobs: dict[str, dict[str, Any]] = {} try: with adapter_state_connection() as conn: rows = conn.execute("SELECT job_id, payload_json FROM adapter_jobs ORDER BY updated_at").fetchall() for row in rows: try: job = json.loads(str(row["payload_json"] or "")) except Exception: continue if isinstance(job, dict): jobs[str(row["job_id"])] = job migration_row = conn.execute("SELECT value FROM adapter_state_meta WHERE key='legacy_jobs_imported'").fetchone() if not migration_row: if not jobs: jobs.update(adapter_load_legacy_jobs()) conn.execute( "INSERT OR REPLACE INTO adapter_state_meta(key, value, updated_at) VALUES('legacy_jobs_imported', ?, ?)", ("yes", adapter_now()), ) conn.commit() except Exception: return now = adapter_now() with ADAPTER_JOB_LOCK: for job_id, job in jobs.items(): restored = dict(job) if restored.get("status") in {"queued", "running"}: restored.update( adapter_public_error( str(restored.get("method") or "adapter.job"), "adapter_restarted", { "message": "Adapter restarted before this job finished. Start a narrower request or rerun the job.", "previous_instance_id": restored.get("adapter_instance_id"), "current_instance_id": ADAPTER_INSTANCE_ID, }, ) ) restored["status"] = "error" restored["finished_at"] = now restored["updated_at"] = now restored["current_step"] = "adapter_restarted" ADAPTER_JOBS[str(job_id)] = restored if jobs: adapter_save_jobs_to_store() def adapter_save_jobs_to_store() -> None: try: rows = [ ( str(job_id), str(job.get("method") or ""), str(job.get("status") or "unknown"), float(job.get("updated_at") or job.get("created_at") or adapter_now()), json.dumps(adapter_state_safe_value(job), ensure_ascii=False, separators=(",", ":"), default=str), ) for job_id, job in ADAPTER_JOBS.items() if isinstance(job, dict) ] with adapter_state_connection() as conn: conn.execute("DELETE FROM adapter_jobs") conn.executemany( "INSERT INTO adapter_jobs(job_id, method, status, updated_at, payload_json) VALUES(?, ?, ?, ?, ?)", rows, ) conn.commit() except Exception: return def adapter_job_timeout_seconds(payload: dict[str, Any], *, method: str = "") -> float: raw = payload.get("adapter_timeout_seconds") or payload.get("_adapter_timeout_seconds") or payload.get("timeout_seconds") if raw is None or raw == "": raw = os.environ.get("ONEC_ADAPTER_FULL_TIMEOUT_SECONDS" if method == "metadata.object.full" else "ONEC_ADAPTER_JOB_TIMEOUT_SECONDS", "600" if method == "metadata.object.full" else "240") try: return max(1.0, float(raw)) except (TypeError, ValueError): return 240.0 def adapter_section_timeout_seconds(payload: dict[str, Any], remaining: float) -> float: raw = payload.get("section_timeout_seconds") or payload.get("_section_timeout_seconds") if raw is None or raw == "": raw = os.environ.get("ONEC_ADAPTER_SECTION_TIMEOUT_SECONDS", "180") try: value = max(1.0, float(raw)) except (TypeError, ValueError): value = 180.0 return max(1.0, min(value, remaining)) def adapter_column_type_timeout_seconds(payload: dict[str, Any], remaining: float) -> float: raw = payload.get("column_type_timeout_seconds") or payload.get("_column_type_timeout_seconds") if raw is None or raw == "": return adapter_section_timeout_seconds(payload, remaining) try: return max(1.0, min(float(raw), remaining)) except (TypeError, ValueError): return adapter_section_timeout_seconds(payload, remaining) def adapter_timeout_payload_value(timeout_seconds: float) -> int: return max(1, int(math.ceil(float(timeout_seconds)))) def adapter_max_columns(payload: dict[str, Any]) -> int | None: raw = payload.get("max_columns") if raw is None or raw == "": return None try: return max(1, int(raw)) except (TypeError, ValueError): return None def adapter_job_heartbeat_seconds() -> float: try: return max(1.0, min(float(os.environ.get("ONEC_ADAPTER_JOB_HEARTBEAT_SECONDS", "2")), 30.0)) except ValueError: return 2.0 def adapter_job_process_isolation_enabled(method: str) -> bool: if not truthy(os.environ.get("ONEC_ADAPTER_JOB_PROCESS_ISOLATION", "true")): return False return not method.startswith(("adapter.job.", "repository.")) def adapter_job_process_memory_limit_bytes() -> int: try: megabytes = max(0, int(os.environ.get("ONEC_ADAPTER_JOB_MEMORY_LIMIT_MB", "0"))) except ValueError: return 0 return megabytes * 1024 * 1024 def adapter_job_process_cpu_limit_seconds() -> int: try: return max(0, int(os.environ.get("ONEC_ADAPTER_JOB_CPU_LIMIT_SECONDS", "0"))) except ValueError: return 0 def adapter_apply_job_process_limits() -> dict[str, Any]: """Apply optional POSIX hard limits inside an isolated worker process.""" limits = { "memory_bytes": adapter_job_process_memory_limit_bytes(), "cpu_seconds": adapter_job_process_cpu_limit_seconds(), "applied": [], "unsupported": [], } try: import resource except ImportError: if limits["memory_bytes"] or limits["cpu_seconds"]: limits["unsupported"].append("resource_module") return limits if limits["memory_bytes"]: try: resource.setrlimit(resource.RLIMIT_AS, (limits["memory_bytes"], limits["memory_bytes"])) limits["applied"].append("memory") except (OSError, ValueError): limits["unsupported"].append("memory") if limits["cpu_seconds"]: try: resource.setrlimit(resource.RLIMIT_CPU, (limits["cpu_seconds"], limits["cpu_seconds"])) limits["applied"].append("cpu") except (OSError, ValueError): limits["unsupported"].append("cpu") return limits def adapter_isolated_job_entry( job_id: str, method: str, job_payload: dict[str, Any], timeout_seconds: float, event_queue: Any, ) -> None: """Child-process entry point. All progress is sent back to the parent.""" global ADAPTER_JOB_EVENT_SINK ADAPTER_JOB_EVENT_SINK = event_queue now = adapter_now() ADAPTER_JOBS[job_id] = { "schema": "onec_adapter_job.v1", "status": "running", "job_id": job_id, "method": method, "base_id": job_payload.get("base_id"), "created_at": now, "started_at": now, "updated_at": now, } try: event_queue.put({"type": "process_started", "pid": os.getpid(), "limits": adapter_apply_job_process_limits()}) if method == "metadata.object.full": adapter_run_metadata_object_full_job(job_id, job_payload, timeout_seconds) return if method == "metadata.object.attributes": adapter_run_metadata_object_attributes_job(job_id, job_payload, timeout_seconds) return if method == "metadata.object.special.details" and canonical_kind(str(job_payload.get("kind") or "")) == "DocumentJournal": adapter_run_document_journal_special_job(job_id, job_payload, timeout_seconds) return if method == "metadata.cache.rebuild": adapter_run_metadata_cache_rebuild_job(job_id, job_payload, timeout_seconds) return result = call_method_impl(method, job_payload) event_queue.put({"type": "result", "result": result}) except BaseException as exc: event_queue.put( { "type": "error", "error": adapter_public_error( method, "adapter_job_process_exception", {"message": str(exc), "traceback": traceback.format_exc(limit=8)}, ), } ) finally: try: event_queue.put({"type": "process_exited"}) except Exception: pass def adapter_stop_job_process(process: Any) -> None: if not process.is_alive(): process.join(timeout=0.2) return process.terminate() process.join(timeout=2) if process.is_alive() and hasattr(process, "kill"): process.kill() process.join(timeout=2) def adapter_apply_isolated_job_event(job_id: str, event: dict[str, Any]) -> bool: event_type = str(event.get("type") or "") if event_type == "set": adapter_job_set(job_id, **dict(event.get("updates") or {})) return False if event_type == "finish": adapter_job_finish(job_id, str(event.get("status") or "error"), **dict(event.get("updates") or {})) return True if event_type == "result": adapter_job_finish( job_id, "done", result=event.get("result"), progress={"current_step": "done", "completed_steps": 1, "total_steps": 1, "percent": 100}, ) return True if event_type == "error": adapter_job_finish(job_id, "error", **dict(event.get("error") or {})) return True if event_type == "process_started": adapter_job_set( job_id, worker={ "mode": "process", "pid": event.get("pid"), "limits": event.get("limits") or {}, }, ) return False def adapter_run_job_in_process( job_id: str, method: str, job_payload: dict[str, Any], timeout_seconds: float, ) -> None: context = multiprocessing.get_context("spawn") event_queue = context.Queue() process = context.Process( target=adapter_isolated_job_entry, args=(job_id, method, job_payload, timeout_seconds, event_queue), name=f"onec-adapter-process-{job_id[:8]}", daemon=True, ) process.start() deadline = adapter_now() + timeout_seconds terminal_event = False try: while True: if adapter_job_cancel_requested(job_id): adapter_stop_job_process(process) return remaining = deadline - adapter_now() if remaining <= 0: adapter_stop_job_process(process) adapter_job_finish( job_id, "error", **adapter_public_error( method, "job_timeout", {"message": f"Adapter job process exceeded {timeout_seconds:.0f} seconds and was terminated."}, ), ) return try: event = event_queue.get(timeout=min(0.25, remaining)) except queue.Empty: if process.is_alive(): continue break except (EOFError, OSError): break if isinstance(event, dict): terminal_event = adapter_apply_isolated_job_event(job_id, event) or terminal_event if terminal_event and not process.is_alive(): break process.join(timeout=0.5) while True: try: event = event_queue.get_nowait() except (queue.Empty, EOFError, OSError): break if isinstance(event, dict): terminal_event = adapter_apply_isolated_job_event(job_id, event) or terminal_event if not terminal_event and not adapter_job_cancel_requested(job_id): adapter_job_finish( job_id, "error", **adapter_public_error( method, "adapter_job_process_exited", {"message": f"Adapter job process exited without a result (exit_code={process.exitcode})."}, ), ) finally: adapter_stop_job_process(process) event_queue.close() def adapter_cleanup_jobs(now: float | None = None) -> None: adapter_load_jobs_from_store() effective_now = adapter_now() if now is None else now changed = False with ADAPTER_JOB_LOCK: expired = [ job_id for job_id, job in ADAPTER_JOBS.items() if effective_now - float(job.get("updated_at") or job.get("created_at") or effective_now) > ADAPTER_JOB_TTL_SECONDS ] for job_id in expired: ADAPTER_JOBS.pop(job_id, None) changed = True if changed: adapter_save_jobs_to_store() def adapter_job_set(job_id: str, **updates: Any) -> None: with ADAPTER_JOB_LOCK: job = ADAPTER_JOBS.get(job_id) if job: job.update(updates) job["updated_at"] = adapter_now() if ADAPTER_JOB_EVENT_SINK is not None: try: ADAPTER_JOB_EVENT_SINK.put({"type": "set", "updates": updates}) except Exception: pass else: adapter_save_jobs_to_store() def adapter_job_cancel_requested(job_id: str) -> bool: with ADAPTER_JOB_LOCK: job = ADAPTER_JOBS.get(job_id) or {} return truthy(job.get("cancel_requested")) or job.get("status") == "cancelled" def adapter_job_finish(job_id: str, job_status: str, **updates: Any) -> None: with ADAPTER_JOB_LOCK: job = ADAPTER_JOBS.get(job_id) if not job or (job.get("status") == "cancelled" and job_status != "cancelled"): return job.update(updates) job["status"] = job_status if job_status in {"done", "cancelled"} and "current_step" not in updates: job["current_step"] = job_status elif job_status == "error" and "current_step" not in updates: job["current_step"] = "error" job["finished_at"] = adapter_now() job["updated_at"] = job["finished_at"] if ADAPTER_JOB_EVENT_SINK is not None: try: ADAPTER_JOB_EVENT_SINK.put({"type": "finish", "status": job_status, "updates": updates}) except Exception: pass else: adapter_save_jobs_to_store() def adapter_card_failure_result_for_long_method(method: str, payload: dict[str, Any], card_result: dict[str, Any], elapsed_seconds: float) -> dict[str, Any] | None: if method == "metadata.object.attributes": partial = adapter_attributes_partial_result(payload) queued_sections = ["semantic", "type_resolution", "build_result"] skipped_status = "not_started_due_to_card_failure" elif method == "metadata.object.full": partial = adapter_full_partial_result(payload) evidence_mode = str(payload.get("_evidence_mode") or payload.get("evidence_mode") or payload.get("undecoded_evidence_mode") or "summary").casefold() requested_sections = list(payload.get("_sections") or []) if not requested_sections: requested_sections = list(FULL_METHOD_DEFAULT_SECTIONS) if truthy(payload.get("include_parts_summary")) or truthy(payload.get("include_storage")) or evidence_mode in {"full", "raw"}: requested_sections.append("parts_summary") queued_sections = [section for section in requested_sections if section != "card"] skipped_status = "not_started_due_to_not_found" else: return None diagnostics = card_result.get("diagnostics") if isinstance(card_result.get("diagnostics"), dict) else {} reason = diagnostics.get("message") or str(card_result.get("diagnostics") or "Object card was not resolved.") partial["status"] = str(card_result.get("status") or "not_found") partial["object"] = card_result.get("object") if card_result.get("matches") is not None: partial["matches"] = card_result.get("matches") partial.setdefault("sections", {})["card"] = "failed" card_failure = { "section": "card", "method": "metadata.object.get", "status": card_result.get("status") or "error", "diagnostics": card_result.get("diagnostics") or {"message": reason}, } partial.setdefault("failed_sections", []).append(card_failure) partial.setdefault("diagnostics", []).append(card_failure) for section in queued_sections: partial.setdefault("sections", {})[section] = skipped_status item = { "section": section, "method": "metadata.object.attributes" if section in {"semantic", "type_resolution", "build_result"} else method, "status": skipped_status, "diagnostics": {"message": reason}, } partial.setdefault("failed_sections", []).append(item) partial.setdefault("diagnostics", []).append(item) partial.setdefault("section_timings", {})[section] = {"method": item["method"], "status": skipped_status} partial.setdefault("section_timings", {})["card"] = { "method": "metadata.object.get", "status": "failed", "timeout_seconds": adapter_timeout_payload_value(payload.get("timeout_seconds") or 60), "elapsed_seconds": round(max(0.0, elapsed_seconds), 3), } partial["elapsed_seconds"] = round(max(0.0, elapsed_seconds), 3) partial["last_section_update_at"] = adapter_now() return partial def adapter_long_method_card_preflight(method: str, payload: dict[str, Any]) -> dict[str, Any] | None: if method not in {"metadata.object.attributes", "metadata.object.full"}: return None started_at = adapter_now() timeout_seconds = adapter_timeout_payload_value(payload.get("timeout_seconds") or 60) card_result = call_method_impl( "metadata.object.get", {**payload, "include_semantic": False, "timeout_seconds": timeout_seconds}, ) if isinstance(card_result, dict) and card_result.get("status") in {"not_found", "source_missing"}: return adapter_card_failure_result_for_long_method(method, payload, card_result, adapter_now() - started_at) return None def adapter_job_heartbeat(job_id: str, stop_event: threading.Event) -> None: while not stop_event.wait(adapter_job_heartbeat_seconds()): with ADAPTER_JOB_LOCK: job = ADAPTER_JOBS.get(job_id) if not job or job.get("status") not in {"queued", "running"}: return job["updated_at"] = adapter_now() progress = dict(job.get("progress") or {}) progress["heartbeat_at"] = job["updated_at"] if job.get("started_at"): progress["elapsed_seconds"] = round(max(0.0, job["updated_at"] - float(job.get("started_at") or job["updated_at"])), 3) partial = job.get("partial_result") if isinstance(partial, dict): partial["elapsed_seconds"] = progress["elapsed_seconds"] job["partial_result"] = partial job["progress"] = progress adapter_save_jobs_to_store() def adapter_public_error(method: str, error: str, diagnostics: Any | None = None) -> dict[str, Any]: return { "schema": "onec_adapter_method_error.v1", "status": "error", "method": method, "error": error, "diagnostics": diagnostics if diagnostics is not None else {"message": error}, } def adapter_public_object_query(payload: dict[str, Any]) -> dict[str, Any]: normalized_payload = normalize_object_ref_payload(payload, "metadata.object.full") if isinstance(normalized_payload, dict) and normalized_payload.get("status") == "invalid_argument": normalized_payload = payload query: dict[str, Any] = { "guid": normalized_payload.get("guid"), "kind": normalized_payload.get("kind"), "name": normalized_payload.get("name"), } if payload.get("ref") not in {None, ""}: query["ref"] = payload.get("ref") if normalized_payload.get("ordinal") not in {None, ""}: query["ordinal"] = normalized_payload.get("ordinal") query["include_storage"] = truthy(normalized_payload.get("include_storage")) if normalized_payload.get("timeout_seconds") not in {None, ""}: query["timeout_seconds"] = normalized_payload.get("timeout_seconds") section_timeout = normalized_payload.get("section_timeout_seconds") or normalized_payload.get("_section_timeout_seconds") if section_timeout not in {None, ""}: query["section_timeout_seconds"] = section_timeout return query def adapter_full_partial_result(payload: dict[str, Any]) -> dict[str, Any]: evidence_mode = str(payload.get("_evidence_mode") or payload.get("evidence_mode") or payload.get("undecoded_evidence_mode") or "summary").casefold() requested_sections = list(payload.get("_sections") or []) if not requested_sections: requested_sections = list(FULL_METHOD_DEFAULT_SECTIONS) if truthy(payload.get("include_parts_summary")) or truthy(payload.get("include_storage")) or evidence_mode in {"full", "raw"}: requested_sections.append("parts_summary") # Ensure stable section order in response and explicit selection visibility. sections = { "card": "pending", "semantic": "pending", "forms": "pending", "templates": "pending", "commands": "pending", "modules": "pending", "parts_summary": "not_requested", } for section in sections: if section not in requested_sections: sections[section] = "not_requested" return { "schema": "onec_metadata_object_full.v1", "status": "partial", "base_id": payload.get("base_id"), "source": {"kind": "live_metadata"}, "query": { **adapter_public_object_query(payload), "evidence_mode": evidence_mode, **({"sections": requested_sections} if payload.get("_sections") is not None else {}), }, "sections": sections, "failed_sections": [], "section_timings": {}, "diagnostics": [], "dimensions": [], "resources": [], "attributes": [], "tabular_sections": [], "counts": {}, } def adapter_merge_full_counts(partial: dict[str, Any]) -> None: semantic_sections = ((partial.get("semantic") or {}).get("sections") or []) if isinstance(partial.get("semantic"), dict) else [] existing_counts = partial.get("counts") if isinstance(partial.get("counts"), dict) else {} partial["counts"] = { "forms": len(partial.get("forms") or []), "templates": len(partial.get("templates") or []), "commands": len(partial.get("commands") or []), "modules": len(partial.get("modules") or []), "dimensions": int(existing_counts.get("dimensions") or len(partial.get("dimensions") or [])), "resources": int(existing_counts.get("resources") or len(partial.get("resources") or [])), "attributes": int( existing_counts.get("attributes") or len(partial.get("attributes") or []) or sum(int((section or {}).get("declared_record_count") or 0) for section in semantic_sections if (section or {}).get("category") == "Attribute") ), "tabular_sections": int( existing_counts.get("tabular_sections") or len(partial.get("tabular_sections") or []) or sum(int((section or {}).get("declared_record_count") or 0) for section in semantic_sections if (section or {}).get("category") == "TabularSection") ), **({"resolved_reference_types": existing_counts.get("resolved_reference_types")} if "resolved_reference_types" in existing_counts else {}), **({"unresolved_reference_types": existing_counts.get("unresolved_reference_types")} if "unresolved_reference_types" in existing_counts else {}), } def adapter_section_failed(partial: dict[str, Any], section: str, method: str, result: Any) -> None: status = result.get("status") if isinstance(result, dict) else "error" diagnostics = result.get("diagnostics") if isinstance(result, dict) else {"message": str(result)} partial.setdefault("sections", {})[section] = "failed" if any(item.get("section") == section for item in partial.get("failed_sections") or []): return item = {"section": section, "method": method, "status": status, "diagnostics": diagnostics} partial.setdefault("failed_sections", []).append(item) partial.setdefault("diagnostics", []).append(item) def adapter_section_not_started( partial: dict[str, Any], section: str, method: str, reason: str, *, status: str = "not_started_due_to_job_timeout" ) -> None: partial.setdefault("sections", {})[section] = status if any(item.get("section") == section for item in partial.get("failed_sections") or []): return item = { "section": section, "method": method, "status": status, "diagnostics": {"message": reason}, } partial.setdefault("failed_sections", []).append(item) partial.setdefault("diagnostics", []).append(item) partial.setdefault("section_timings", {})[section] = { "method": method, "status": status, } def adapter_section_ok_or_empty(partial: dict[str, Any], section: str, value: Any) -> None: partial.setdefault("sections", {})[section] = "ok" if value else "empty" def adapter_update_full_job( job_id: str, partial: dict[str, Any], current_step: str, completed: int, total: int, running_steps: list[str] | None = None, *, started_at: float | None = None, queued_steps: list[str] | None = None, done_steps: list[str] | None = None, failed_steps: list[str] | None = None, ) -> None: adapter_merge_full_counts(partial) now = adapter_now() if started_at is not None: partial["elapsed_seconds"] = round(max(0.0, now - started_at), 3) partial["last_section_update_at"] = now progress: dict[str, Any] = { "current_step": current_step, "completed_steps": completed, "total_steps": total, "percent": int((completed / total) * 100) if total else 0, } if started_at is not None: progress["elapsed_seconds"] = partial["elapsed_seconds"] progress["last_section_update_at"] = partial["last_section_update_at"] if running_steps is not None: progress["running_steps"] = running_steps if queued_steps is not None: progress["queued_steps"] = queued_steps if done_steps is not None: progress["done_steps"] = done_steps if failed_steps is not None: progress["failed_steps"] = failed_steps adapter_job_set(job_id, partial_result=partial, current_step=current_step, progress=progress) def adapter_section_running(partial: dict[str, Any], section: str) -> None: partial.setdefault("sections", {})[section] = "running" def adapter_section_timing_start(partial: dict[str, Any], section: str, method: str, timeout_seconds: float) -> float: started_at = adapter_now() partial.setdefault("section_timings", {})[section] = { "method": method, "status": "running", "started_at": started_at, "timeout_seconds": adapter_timeout_payload_value(timeout_seconds), } return started_at def adapter_section_timing_finish(partial: dict[str, Any], section: str, status: str, section_started_at: float) -> None: finished_at = adapter_now() timing = dict((partial.setdefault("section_timings", {}) or {}).get(section) or {}) timing.update( { "status": status, "finished_at": finished_at, "elapsed_seconds": round(max(0.0, finished_at - section_started_at), 3), } ) partial.setdefault("section_timings", {})[section] = timing def adapter_full_form_commands(partial: dict[str, Any]) -> list[dict[str, Any]]: commands = [] seen: set[tuple[str, str]] = set() for form in partial.get("forms") or []: form_name = form.get("name") for item in form.get("commands") or []: name = str(item.get("name") or "") key = (str(form_name or ""), name) if key in seen: continue seen.add(key) commands.append( { "scope": "form", "form": form_name, "name": name, "title": item.get("title"), "id": item.get("id"), } ) return commands def adapter_full_commands_result(payload: dict[str, Any], partial: dict[str, Any], timeout_seconds: float) -> dict[str, Any]: object_result = call_method_impl( "metadata.object.commands", {**payload, "include_form_commands": False, "timeout_seconds": adapter_timeout_payload_value(timeout_seconds)}, ) object_commands = [] if isinstance(object_result, dict) and object_result.get("status") == "ok": object_commands = object_result.get("object_commands") or object_result.get("commands") or [] command_modules = [] for command in object_commands: read_selector = command.get("read_selector") if isinstance(command, dict) and isinstance(command.get("read_selector"), dict) else None if not read_selector: continue command_name = str(command.get("name") or command.get("synonym") or "") owner_name = str(((object_result.get("object") or {}).get("name") if isinstance(object_result, dict) else "") or (partial.get("object") or {}).get("name") or "") qualified_name = ".".join(part for part in [owner_name, "Команда", command_name, "Модуль команды"] if part) command_modules.append( { "kind": "command_module", "name": "Модуль команды", "command": command_name, **({"qualified_name": qualified_name, "display_name": qualified_name} if qualified_name else {}), "read_selector": dict(read_selector), } ) form_commands = adapter_full_form_commands(partial) commands = [*object_commands, *form_commands] return { "schema": "onec_object_commands.v1", "status": "ok", "base_id": payload.get("base_id"), "source": {"kind": "live_metadata"}, "commands": commands, "object_commands": object_commands, "form_commands": form_commands, "modules": command_modules, "counts": { "commands": len(commands), "object_commands": len(object_commands), "form_commands": len(form_commands), "command_modules": len(command_modules), }, "capabilities": { "object_commands": True, "form_commands": True, "form_commands_source": "metadata.object.full.forms", }, } def adapter_document_journal_partial_result(payload: dict[str, Any]) -> dict[str, Any]: return { "schema": "onec_object_special_details.v1", "status": "partial", "base_id": payload.get("base_id"), "source": {"kind": "live_metadata"}, "query": { "guid": payload.get("guid"), "kind": payload.get("kind"), "name": payload.get("name"), **({"ordinal": payload.get("ordinal")} if payload.get("ordinal") not in {None, ""} else {}), "include_column_types": truthy(payload.get("include_column_types")), **({"timeout_seconds": payload.get("timeout_seconds")} if payload.get("timeout_seconds") not in {None, ""} else {}), **({"section_timeout_seconds": payload.get("section_timeout_seconds") or payload.get("_section_timeout_seconds")} if (payload.get("section_timeout_seconds") or payload.get("_section_timeout_seconds")) not in {None, ""} else {}), }, "sections": { "card": "pending", "document_types": "pending", "columns": "pending", "column_types": "pending" if truthy(payload.get("include_column_types")) else "not_requested", }, "details": {}, "counts": {"document_types": 0, "columns": 0, "typed_columns": 0}, "current_column": None, "failed_columns": [], "column_timings": {}, "failed_sections": [], "section_timings": {}, "diagnostics": [], } def adapter_document_journal_counts(partial: dict[str, Any]) -> None: details = partial.get("details") or {} document_types = details.get("document_types") if isinstance(details.get("document_types"), list) else [] columns = details.get("columns") if isinstance(details.get("columns"), list) else [] partial["counts"] = { "document_types": len(document_types), "columns": len(columns), "typed_columns": sum(1 for column in columns if isinstance(column, dict) and column.get("type")), } def adapter_document_journal_column_types_complete(partial: dict[str, Any]) -> bool: counts = partial.get("counts") or {} columns = int(counts.get("columns") or 0) typed_columns = int(counts.get("typed_columns") or 0) return bool(columns and typed_columns >= columns and not partial.get("failed_columns")) def adapter_update_special_job( job_id: str, partial: dict[str, Any], current_step: str, completed: int, total: int, *, started_at: float, queued_steps: list[str], running_steps: list[str], done_steps: list[str], failed_steps: list[str], ) -> None: adapter_document_journal_counts(partial) now = adapter_now() partial["elapsed_seconds"] = round(max(0.0, now - started_at), 3) partial["last_section_update_at"] = now adapter_job_set( job_id, partial_result=partial, current_step=current_step, progress={ "current_step": current_step, "completed_steps": completed, "total_steps": total, "percent": int((completed / total) * 100) if total else 0, "elapsed_seconds": partial["elapsed_seconds"], "last_section_update_at": partial["last_section_update_at"], "queued_steps": queued_steps, "running_steps": running_steps, "done_steps": done_steps, "failed_steps": failed_steps, }, ) def adapter_run_document_journal_special_job(job_id: str, payload: dict[str, Any], timeout_seconds: float) -> None: base_id = str(payload.get("base_id") or "") partial = adapter_document_journal_partial_result(payload) started_at = adapter_now() context: dict[str, Any] = {} table = str(payload.get("table") or "Config") steps: list[tuple[str, str, Any]] = [] def load_card() -> dict[str, Any]: guid, kind, object_card, error = resolve_object_guid( payload, base_id, timeout_seconds=int(payload.get("timeout_seconds") or 60), method="metadata.object.special.details", table=table, ) if error: return error data, _, read_error = read_storage_file_bytes(base_id, table, guid, timeout_seconds=int(payload.get("timeout_seconds") or 60)) if read_error: return public_error_result(read_error, include_storage=False, method="metadata.object.special.details") tree = parse_config_tree_from_bytes(data) identity = config_identity_from_bytes(data) or (object_card or {}).get("identity") or {} context.update({"guid": guid, "kind": kind, "object_card": object_card, "tree": tree, "identity": identity, "strings": tree_ordered_strings(tree)}) partial["object"] = object_card or {"guid": guid, "kind": kind, "identity": identity} partial.setdefault("details", {})["description"] = next((value for value in context["strings"] if " " in value), None) return {"status": "ok"} def load_document_types() -> dict[str, Any]: records, _ = live_dbnames_records(base_id, timeout_seconds=int(payload.get("timeout_seconds") or 60)) context["dbnames_records"] = records document_types = document_journal_document_types( base_id, context.get("tree"), dbnames_records=records, table=table, timeout_seconds=int(payload.get("timeout_seconds") or 60), ) context["document_types"] = document_types partial.setdefault("details", {})["document_types"] = document_types if document_types else {"status": "not_decoded_yet"} return {"status": "ok" if document_types else "partial"} def load_columns(include_types: bool = False, section_deadline: float | None = None) -> dict[str, Any]: if include_types: config, _ = sql_config_for_base(base_id) journal_field_guids = document_journal_all_column_field_guids(context.get("tree")) field_types: dict[str, dict[str, Any]] = metadata_field_type_cache_lookup( config, journal_field_guids, ) if field_types: columns = document_journal_columns_from_field_types( base_id, context.get("tree"), field_types, timeout_seconds=int(payload.get("timeout_seconds") or 60), max_columns=adapter_max_columns(payload), ) if columns: partial.setdefault("details", {})["columns"] = columns adapter_document_journal_counts(partial) partial.setdefault("column_timings", {})["field_type_cache"] = { "status": "ok", "typed_columns": partial.get("counts", {}).get("typed_columns", 0), } adapter_update_special_job( job_id, partial, "column_types:field_type_cache", completed, total, started_at=started_at, queued_steps=queued_steps, running_steps=running_steps, done_steps=done_steps, failed_steps=failed_steps, ) if adapter_document_journal_column_types_complete(partial): partial["current_column"] = None return {"status": "ok"} documents = context.get("document_types") or [] max_columns = adapter_max_columns(payload) partial["failed_columns"] = [] for index, document in enumerate(documents): if section_deadline and adapter_now() >= section_deadline: return adapter_public_error( "metadata.object.special.details.column_types", "section_timeout", {"message": "Column type decoding timed out", "processed_documents": index, "total_documents": len(documents)}, ) guid = str(document.get("guid") or "").lower() started_column_at = adapter_now() partial["current_column"] = { "document": document.get("name") or document.get("synonym") or guid, "document_guid": guid, "document_index": index + 1, "documents_total": len(documents), "phase": "read_metadata", } adapter_update_special_job( job_id, partial, "column_types", completed, total, started_at=started_at, queued_steps=queued_steps, running_steps=running_steps, done_steps=done_steps, failed_steps=failed_steps, ) try: data, _, read_error = read_storage_file_bytes(base_id, table, guid, timeout_seconds=int(payload.get("timeout_seconds") or 60)) if read_error: partial.setdefault("failed_columns", []).append({"document": document.get("name"), "guid": guid, "diagnostics": read_error.get("diagnostics")}) continue partial["current_column"]["phase"] = "decode_attributes" adapter_update_special_job( job_id, partial, "column_types:decode_attributes", completed, total, started_at=started_at, queued_steps=queued_steps, running_steps=running_steps, done_steps=done_steps, failed_steps=failed_steps, ) decoded = decode_config_object_full(data, kind="Document", dbnames_records=context.get("dbnames_records") or [], max_depth=3) semantic = decoded.get("semantic") if decoded.get("status") == "ok" else None sections = (semantic or {}).get("sections") or [] target_attributes: list[dict[str, Any]] = [] for semantic_section in sections: if semantic_section.get("category") != "Attribute": continue for attribute in semantic_section.get("records") or []: identity = attribute.get("identity") if isinstance(attribute.get("identity"), dict) else {} attribute_guid = str(identity.get("guid") or "").lower() if attribute_guid and attribute_guid in journal_field_guids and attribute.get("type"): target_attributes.append(attribute) type_guids: set[str] = set() for attribute in target_attributes: attribute_type = attribute.get("type") or {} if isinstance(attribute_type, dict) and attribute_type.get("kind") == "reference" and attribute_type.get("type_guid"): type_guids.add(str(attribute_type.get("type_guid")).lower()) partial["current_column"]["phase"] = "resolve_types" adapter_update_special_job( job_id, partial, "column_types:resolve_types", completed, total, started_at=started_at, queued_steps=queued_steps, running_steps=running_steps, done_steps=done_steps, failed_steps=failed_steps, ) resolved_types = resolve_type_guids( base_id, type_guids, timeout_seconds=int(payload.get("timeout_seconds") or 60), table=table, ) for attribute in target_attributes: identity = attribute.get("identity") if isinstance(attribute.get("identity"), dict) else {} attribute_guid = str(identity.get("guid") or "").lower() public_type = sanitize_public_result(public_type_info(attribute.get("type"), resolved_types, include_storage=False)) field_types[attribute_guid] = public_type metadata_field_type_cache_upsert(config, attribute_guid, public_type, owner=document, field_name=identity.get("name")) partial["current_column"]["phase"] = "match_columns" columns = document_journal_columns_from_field_types( base_id, context.get("tree"), field_types, table=table, timeout_seconds=int(payload.get("timeout_seconds") or 60), max_columns=max_columns, ) if columns: partial.setdefault("details", {})["columns"] = columns adapter_document_journal_counts(partial) partial.setdefault("column_timings", {})[guid] = { "document": document.get("name"), "status": "ok", "started_at": started_column_at, "finished_at": adapter_now(), "elapsed_seconds": round(max(0.0, adapter_now() - started_column_at), 3), "typed_columns": partial.get("counts", {}).get("typed_columns", 0), } total_columns = (partial.get("counts") or {}).get("columns", 0) typed_columns = (partial.get("counts") or {}).get("typed_columns", 0) if adapter_document_journal_column_types_complete(partial): partial["current_column"] = None adapter_update_special_job( job_id, partial, "column_types", completed, total, started_at=started_at, queued_steps=queued_steps, running_steps=running_steps, done_steps=done_steps, failed_steps=failed_steps, ) return {"status": "ok"} except Exception as exc: partial.setdefault("failed_columns", []).append({"document": document.get("name"), "guid": guid, "diagnostics": {"message": str(exc)}}) partial.setdefault("column_timings", {})[guid] = { "document": document.get("name"), "status": "failed", "started_at": started_column_at, "finished_at": adapter_now(), "elapsed_seconds": round(max(0.0, adapter_now() - started_column_at), 3), "diagnostics": {"message": str(exc)}, } adapter_update_special_job( job_id, partial, "column_types", completed, total, started_at=started_at, queued_steps=queued_steps, running_steps=running_steps, done_steps=done_steps, failed_steps=failed_steps, ) partial["current_column"] = None typed_columns = (partial.get("counts") or {}).get("typed_columns", 0) total_columns = (partial.get("counts") or {}).get("columns", 0) return {"status": "ok" if adapter_document_journal_column_types_complete(partial) else "partial"} columns = document_journal_columns( base_id, context.get("tree"), document_types=context.get("document_types") or [], dbnames_records=context.get("dbnames_records"), table=table, include_column_types=False, timeout_seconds=int(payload.get("timeout_seconds") or 60), max_columns=adapter_max_columns(payload), ) if columns: partial.setdefault("details", {})["columns"] = columns elif "columns" not in partial.setdefault("details", {}): partial["details"]["columns"] = {"status": "not_decoded_yet"} return {"status": "ok" if columns else "partial"} steps.append(("card", "metadata.object.special.details.card", load_card)) steps.append(("document_types", "metadata.object.special.details.document_types", load_document_types)) steps.append(("columns", "metadata.object.special.details.columns", lambda: load_columns(False))) if truthy(payload.get("include_column_types")): steps.append(("column_types", "metadata.object.special.details.column_types", lambda deadline=None: load_columns(True, section_deadline=deadline))) total = len(steps) completed = 0 queued_steps = [section for section, _, _ in steps] running_steps: list[str] = [] done_steps: list[str] = [] failed_steps: list[str] = [] adapter_update_special_job(job_id, partial, "starting", completed, total, started_at=started_at, queued_steps=queued_steps, running_steps=running_steps, done_steps=done_steps, failed_steps=failed_steps) for section, section_method, section_callable in steps: if adapter_job_cancel_requested(job_id): partial["status"] = "cancelled" adapter_job_finish(job_id, "cancelled", partial_result=partial, result=partial) return elapsed = adapter_now() - started_at if elapsed >= timeout_seconds: for queued_section, queued_method, _ in steps: if queued_section in queued_steps: adapter_section_not_started(partial, queued_section, queued_method, f"Adapter job timeout after {timeout_seconds:.0f} seconds before section start") failed_steps.append(queued_section) partial["status"] = "partial" adapter_job_finish(job_id, "error", error="job_timeout", partial_result=partial, result=partial, diagnostics={"message": f"Adapter job timeout after {timeout_seconds:.0f} seconds"}) return if section in queued_steps: queued_steps.remove(section) running_steps = [section] adapter_section_running(partial, section) remaining = max(1.0, timeout_seconds - elapsed) section_timeout = adapter_column_type_timeout_seconds(payload, remaining) if section == "column_types" else adapter_section_timeout_seconds(payload, remaining) section_started_at = adapter_section_timing_start(partial, section, section_method, section_timeout) adapter_update_special_job(job_id, partial, section, completed, total, started_at=started_at, queued_steps=queued_steps, running_steps=running_steps, done_steps=done_steps, failed_steps=failed_steps) try: executor = concurrent.futures.ThreadPoolExecutor(max_workers=1, thread_name_prefix=f"onec-special-{section}-{job_id[:8]}") if section == "column_types": future = executor.submit(section_callable, adapter_now() + section_timeout) else: future = executor.submit(section_callable) try: try: result = future.result(timeout=section_timeout) except concurrent.futures.TimeoutError: future.cancel() if section == "column_types" and adapter_document_journal_column_types_complete(partial): partial["current_column"] = None result = {"status": "ok"} else: result = adapter_public_error( section_method, "section_timeout", {"message": f"Section `{section}` timed out after {section_timeout:.0f} seconds", "section": section, "section_timeout_seconds": section_timeout}, ) finally: executor.shutdown(wait=False, cancel_futures=True) except Exception as exc: result = adapter_public_error(section_method, "adapter_section_exception", {"message": str(exc), "traceback": traceback.format_exc(limit=6)}) running_steps = [] completed += 1 if section == "column_types" and adapter_document_journal_column_types_complete(partial): result = {"status": "ok"} if not isinstance(result, dict) or result.get("status") not in {"ok", "partial"}: adapter_section_failed(partial, section, section_method, result) failed_steps.append(section) adapter_section_timing_finish(partial, section, "failed", section_started_at) else: partial["sections"][section] = "ok" if result.get("status") == "ok" else "partial" done_steps.append(section) adapter_section_timing_finish(partial, section, str(partial["sections"][section]), section_started_at) adapter_update_special_job(job_id, partial, section, completed, total, started_at=started_at, queued_steps=queued_steps, running_steps=running_steps, done_steps=done_steps, failed_steps=failed_steps) partial["status"] = "partial" if partial.get("failed_sections") else "ok" adapter_document_journal_counts(partial) finished_at = adapter_now() partial["elapsed_seconds"] = round(max(0.0, finished_at - started_at), 3) partial["last_section_update_at"] = finished_at adapter_job_finish( job_id, "done", result=partial, partial_result=partial, progress={ "current_step": "done", "completed_steps": total, "total_steps": total, "percent": 100, "queued_steps": [], "running_steps": [], "done_steps": done_steps, "failed_steps": failed_steps, "elapsed_seconds": partial["elapsed_seconds"], "last_section_update_at": partial["last_section_update_at"], }, ) def adapter_full_forms_method(payload: dict[str, Any]) -> str: if truthy(payload.get("include_form_details")) or truthy(payload.get("include_form_module_text")): return "metadata.object.form.details" return "metadata.object.forms" def adapter_run_metadata_object_full_job(job_id: str, payload: dict[str, Any], timeout_seconds: float) -> None: partial = adapter_full_partial_result(payload) started_at = adapter_now() evidence_mode = str(payload.get("_evidence_mode") or payload.get("evidence_mode") or payload.get("undecoded_evidence_mode") or "summary").casefold() section_payload_overrides = { "card": {"include_semantic": False}, "semantic": {"only": "all"}, "forms": { "max_items": int(payload.get("max_form_items") or payload.get("max_items") or 1000), "max_forms": int(payload.get("max_forms") or 20), "include_module_text": truthy(payload.get("include_form_module_text")), "include_parameters": bool(payload.get("include_parameters", True)), "max_parameters": int(payload.get("max_parameters") or 80), }, "parts_summary": { "include_text": evidence_mode in {"full", "raw"}, "include_tree": evidence_mode == "raw", "evidence_mode": evidence_mode, }, } section_methods = { "card": "metadata.object.get", "semantic": "metadata.object.attributes", "modules": "metadata.object.modules", "templates": "metadata.object.template.details" if truthy(payload.get("include_template_details")) else "metadata.object.templates", "forms": adapter_full_forms_method(payload), "commands": "metadata.object.commands", "parts_summary": "metadata.object.parts", } selected_sections = list(payload.get("_sections") or []) if not selected_sections: selected_sections = list(FULL_METHOD_DEFAULT_SECTIONS) if truthy(payload.get("include_parts_summary")) or truthy(payload.get("include_storage")) or evidence_mode in {"full", "raw"}: selected_sections.append("parts_summary") steps: list[tuple[str, str, dict[str, Any]]] = [] for section in selected_sections: section_method = section_methods[section] section_payload = dict(payload) section_payload.update(section_payload_overrides.get(section) or {}) steps.append((section, section_method, section_payload)) if section == "parts_summary": partial["sections"]["parts_summary"] = "pending" total = len(steps) completed = 0 queued_steps = [section for section, _, _ in steps] running_steps: list[str] = [] done_steps: list[str] = [] failed_steps: list[str] = [] adapter_update_full_job( job_id, partial, "starting", completed, total, running_steps, started_at=started_at, queued_steps=queued_steps, done_steps=done_steps, failed_steps=failed_steps, ) for section, section_method, section_payload in steps: if adapter_job_cancel_requested(job_id): partial["status"] = "cancelled" adapter_job_finish(job_id, "cancelled", partial_result=partial, result=partial) return elapsed = adapter_now() - started_at if elapsed >= timeout_seconds: adapter_section_failed(partial, section, section_method, {"status": "timeout", "diagnostics": {"message": f"Adapter job timeout after {timeout_seconds:.0f} seconds"}}) failed_steps.append(section) for queued_section, queued_method, _ in steps: if queued_section in queued_steps: adapter_section_not_started(partial, queued_section, queued_method, f"Adapter job timeout after {timeout_seconds:.0f} seconds before section start") failed_steps.append(queued_section) queued_steps = [] partial["status"] = "partial" adapter_job_finish( job_id, "error", error="job_timeout", partial_result=partial, result=partial, progress={ "current_step": "timeout", "running_steps": running_steps, "queued_steps": queued_steps, "done_steps": done_steps, "failed_steps": failed_steps, "completed_steps": completed, "total_steps": total, "percent": int((completed / total) * 100) if total else 0, "elapsed_seconds": round(max(0.0, adapter_now() - started_at), 3), "last_section_update_at": adapter_now(), }, diagnostics={"message": f"Adapter job timeout after {timeout_seconds:.0f} seconds", "running_steps": running_steps}, ) return if section in queued_steps: queued_steps.remove(section) running_steps = [section] adapter_section_running(partial, section) adapter_update_full_job( job_id, partial, f"{section}:{section_method}", completed, total, running_steps, started_at=started_at, queued_steps=queued_steps, done_steps=done_steps, failed_steps=failed_steps, ) remaining = max(1.0, timeout_seconds - elapsed) section_timeout = adapter_section_timeout_seconds(section_payload, remaining) section_started_at = adapter_section_timing_start(partial, section, section_method, section_timeout) adapter_update_full_job( job_id, partial, f"{section}:{section_method}", completed, total, running_steps, started_at=started_at, queued_steps=queued_steps, done_steps=done_steps, failed_steps=failed_steps, ) try: executor = concurrent.futures.ThreadPoolExecutor(max_workers=1, thread_name_prefix=f"onec-section-{section}-{job_id[:8]}") if section == "commands": future = executor.submit(adapter_full_commands_result, section_payload, partial, section_timeout) else: future = executor.submit(call_method_impl, section_method, {**section_payload, "timeout_seconds": adapter_timeout_payload_value(section_timeout)}) try: try: result = future.result(timeout=section_timeout) except concurrent.futures.TimeoutError: future.cancel() result = adapter_public_error( section_method, "section_timeout", { "message": f"Section `{section}` timed out after {section_timeout:.0f} seconds", "section": section, "section_timeout_seconds": section_timeout, }, ) finally: executor.shutdown(wait=False, cancel_futures=True) except Exception as exc: result = adapter_public_error(section_method, "adapter_section_exception", {"message": str(exc), "traceback": traceback.format_exc(limit=6)}) running_steps = [] completed += 1 if isinstance(result, dict) and result.get("status") == "not_found": partial["object"] = result.get("object") partial["matches"] = result.get("matches") adapter_section_failed(partial, section, section_method, result) failed_steps.append(section) adapter_section_timing_finish(partial, section, "failed", section_started_at) for queued_section, queued_method, _ in steps: if queued_section in queued_steps: adapter_section_not_started( partial, queued_section, queued_method, result.get("diagnostics", {}).get("message") if isinstance(result.get("diagnostics"), dict) else (str(result.get("diagnostics")) if result.get("diagnostics") is not None else "Object was not found."), status="not_started_due_to_not_found", ) failed_steps.append(queued_section) queued_steps = [] partial["status"] = "partial" partial["elapsed_seconds"] = round(max(0.0, adapter_now() - started_at), 3) partial["last_section_update_at"] = adapter_now() adapter_update_full_job( job_id, partial, "done", completed, total, started_at=started_at, queued_steps=queued_steps, done_steps=done_steps, failed_steps=failed_steps, ) adapter_job_finish( job_id, "done", result=partial, partial_result=partial, progress={ "current_step": "done", "completed_steps": completed, "total_steps": total, "percent": int((completed / total) * 100) if total else 0, "running_steps": [], "queued_steps": [], "done_steps": done_steps, "failed_steps": failed_steps, "elapsed_seconds": partial["elapsed_seconds"], "last_section_update_at": partial["last_section_update_at"], }, ) return if not isinstance(result, dict) or result.get("status") not in {"ok", "partial"}: adapter_section_failed(partial, section, section_method, result) failed_steps.append(section) adapter_section_timing_finish(partial, section, "failed", section_started_at) elif section == "card": partial["object"] = result.get("object") partial["matches"] = result.get("matches") adapter_section_ok_or_empty(partial, section, partial.get("object")) done_steps.append(section) adapter_section_timing_finish(partial, section, "ok" if partial.get("object") else "empty", section_started_at) elif section == "semantic": partial["object"] = result.get("object") if result.get("semantic") is not None: partial["semantic"] = result.get("semantic") else: partial.pop("semantic", None) partial["dimensions"] = result.get("dimensions") or [] partial["resources"] = result.get("resources") or [] partial["attributes"] = result.get("attributes") or [] partial["tabular_sections"] = result.get("tabular_sections") or [] partial.setdefault("counts", {}).update(result.get("counts") or {}) semantic_payload = partial.get("semantic") or partial.get("dimensions") or partial.get("resources") or partial.get("attributes") or partial.get("tabular_sections") adapter_section_ok_or_empty(partial, section, semantic_payload) done_steps.append(section) adapter_section_timing_finish(partial, section, "ok" if semantic_payload else "empty", section_started_at) elif section == "forms": partial["forms"] = result.get("forms") or [] adapter_section_ok_or_empty(partial, section, partial["forms"]) done_steps.append(section) adapter_section_timing_finish(partial, section, "ok" if partial["forms"] else "empty", section_started_at) elif section == "templates": partial["templates"] = result.get("templates") or [] adapter_section_ok_or_empty(partial, section, partial["templates"]) done_steps.append(section) adapter_section_timing_finish(partial, section, "ok" if partial["templates"] else "empty", section_started_at) elif section == "commands": partial["commands"] = result.get("commands") or [] if "modules" in selected_sections: existing_modules = [item for item in partial.get("modules") or [] if isinstance(item, dict)] seen_module_refs = { str((item.get("read_selector") or {}).get("module_ref") or "") for item in existing_modules if isinstance(item.get("read_selector"), dict) } for module in result.get("modules") or []: module_ref = str((module.get("read_selector") or {}).get("module_ref") or "") if isinstance(module, dict) else "" if module_ref and module_ref in seen_module_refs: continue existing_modules.append(module) if module_ref: seen_module_refs.add(module_ref) partial["modules"] = existing_modules if existing_modules: partial["sections"]["modules"] = "ok" adapter_section_ok_or_empty(partial, section, partial["commands"]) done_steps.append(section) adapter_section_timing_finish(partial, section, "ok" if partial["commands"] else "empty", section_started_at) elif section == "modules": partial["modules"] = result.get("modules") or [] adapter_section_ok_or_empty(partial, section, partial["modules"]) done_steps.append(section) adapter_section_timing_finish(partial, section, "ok" if partial["modules"] else "empty", section_started_at) elif section == "parts_summary": partial["parts_summary"] = {"counts": result.get("counts")} if evidence_mode in {"full", "raw"}: partial["parts_summary"]["parts"] = result.get("parts") or [] partial["sections"][section] = "ok" done_steps.append(section) adapter_section_timing_finish(partial, section, "ok", section_started_at) adapter_update_full_job( job_id, partial, section, completed, total, running_steps, started_at=started_at, queued_steps=queued_steps, done_steps=done_steps, failed_steps=failed_steps, ) partial["status"] = "partial" if partial.get("failed_sections") else "ok" adapter_merge_full_counts(partial) finished_at = adapter_now() partial["elapsed_seconds"] = round(max(0.0, finished_at - started_at), 3) partial["last_section_update_at"] = finished_at adapter_job_finish( job_id, "done", result=partial, partial_result=partial, progress={ "current_step": "done", "completed_steps": total, "total_steps": total, "percent": 100, "running_steps": [], "queued_steps": [], "done_steps": done_steps, "failed_steps": failed_steps, "elapsed_seconds": partial["elapsed_seconds"], "last_section_update_at": partial["last_section_update_at"], }, ) def adapter_attributes_partial_result(payload: dict[str, Any]) -> dict[str, Any]: return { "schema": "onec_metadata_object_attributes.v1", "status": "partial", "base_id": payload.get("base_id"), "source": {"kind": "live_metadata"}, "query": { "guid": payload.get("guid"), "kind": payload.get("kind"), "name": payload.get("name"), **({"ordinal": payload.get("ordinal")} if payload.get("ordinal") not in {None, ""} else {}), "only": payload.get("only") or payload.get("scope") or "all", "include_storage": truthy(payload.get("include_storage")), "use_cache": truthy(payload.get("use_cache")), **({"timeout_seconds": payload.get("timeout_seconds")} if payload.get("timeout_seconds") not in {None, ""} else {}), **({"section_timeout_seconds": payload.get("section_timeout_seconds") or payload.get("_section_timeout_seconds")} if (payload.get("section_timeout_seconds") or payload.get("_section_timeout_seconds")) not in {None, ""} else {}), }, "sections": { "card": "pending", "semantic": "pending", "type_resolution": "pending", "build_result": "pending", }, "object": None, "dimensions": [], "resources": [], "attributes": [], "tabular_sections": [], "counts": {"dimensions": 0, "resources": 0, "attributes": 0, "tabular_sections": 0, "resolved_reference_types": 0, "unresolved_reference_types": 0}, "failed_sections": [], "section_timings": {}, "diagnostics": [], } def adapter_mark_attributes_cancelled( partial: dict[str, Any], *, started_at: float, current_step: str = "cancelled", ) -> dict[str, Any]: now = adapter_now() partial["status"] = "cancelled" partial["elapsed_seconds"] = round(max(0.0, now - started_at), 3) partial["last_section_update_at"] = now for section, status in list((partial.get("sections") or {}).items()): if status in {"running", "pending"}: partial["sections"][section] = "cancelled" timing = partial.setdefault("section_timings", {}).setdefault(section, {"method": "metadata.object.attributes"}) timing["status"] = "cancelled" timing.setdefault("finished_at", now) if "started_at" in timing: timing["elapsed_seconds"] = round(max(0.0, now - float(timing.get("started_at") or now)), 3) partial.setdefault("diagnostics", []).append({"status": "cancelled", "message": "Запрос отменен пользователем."}) return { "current_step": current_step, "completed_steps": len([value for value in (partial.get("sections") or {}).values() if value == "ok"]), "total_steps": len(partial.get("sections") or {}), "percent": 100, "running_steps": [], "queued_steps": [], "done_steps": [key for key, value in (partial.get("sections") or {}).items() if value == "ok"], "failed_steps": [key for key, value in (partial.get("sections") or {}).items() if value in {"failed", "cancelled"}], "elapsed_seconds": partial["elapsed_seconds"], "last_section_update_at": partial["last_section_update_at"], } def validate_metadata_object_attributes_payload(payload: dict[str, Any]) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: selector_error = validate_object_selector_arguments(payload, "metadata.object.attributes") if selector_error: return None, selector_error if "extension_guid" in payload: extension_guid = payload.get("extension_guid") if not isinstance(extension_guid, str) or not is_guid_text(extension_guid.strip()): return None, invalid_argument( "metadata.object.attributes", "extension_guid", "extension_guid must be a GUID string.", ) table_or_error = metadata_storage_table(payload, "metadata.object.attributes") if isinstance(table_or_error, dict): return None, table_or_error table = table_or_error include_storage, include_storage_error = strict_bool_argument(payload, "include_storage", method="metadata.object.attributes", default=False) if include_storage_error: return None, include_storage_error refresh_cache, refresh_cache_error = strict_bool_argument(payload, "refresh_cache", method="metadata.object.attributes", default=False) if refresh_cache_error: return None, refresh_cache_error use_cache, use_cache_error = strict_bool_argument(payload, "use_cache", method="metadata.object.attributes", default=False) if use_cache_error: return None, use_cache_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.attributes", default=240, minimum=1) if timeout_error: return None, timeout_error _, adapter_timeout_error = parse_int_argument(payload, "adapter_timeout_seconds", method="metadata.object.attributes", default=0, minimum=1) if adapter_timeout_error: return None, adapter_timeout_error _, legacy_adapter_timeout_error = parse_int_argument(payload, "_adapter_timeout_seconds", method="metadata.object.attributes", default=0, minimum=1) if legacy_adapter_timeout_error: return None, legacy_adapter_timeout_error _, section_timeout_error = parse_int_argument(payload, "section_timeout_seconds", method="metadata.object.attributes", default=180, minimum=1) if section_timeout_error: return None, section_timeout_error _, legacy_section_timeout_error = parse_int_argument(payload, "_section_timeout_seconds", method="metadata.object.attributes", default=180, minimum=1) if legacy_section_timeout_error: return None, legacy_section_timeout_error if "offset" in payload: return None, invalid_argument( "metadata.object.attributes", "offset", "offset is not supported by metadata.object.attributes. Use only/limit to control returned attribute slices.", ) limit, limit_error = parse_int_argument(payload, "limit", method="metadata.object.attributes", default=20, minimum=1) if limit_error: return None, limit_error view, view_error = parse_view_argument(payload, "metadata.object.attributes") if view_error: return None, view_error ordinal_error = validate_explicit_ordinal_arguments(payload, "metadata.object.attributes") if ordinal_error: return None, ordinal_error raw_only = payload.get("only", payload.get("scope", "all")) raw_only_arg = payload.get("only") raw_scope_arg = payload.get("scope") if ("only" in payload and (raw_only_arg is None or raw_only_arg == "")) or ("scope" in payload and (raw_scope_arg is None or raw_scope_arg == "")): return None, invalid_argument( "metadata.object.attributes", "only" if "only" in payload else "scope", "only must be one of the allowed values.", allowed_values=["all", "attributes", "tabular_sections", "dimensions", "resources", "register_fields"], ) if not isinstance(raw_only, str): return None, invalid_argument( "metadata.object.attributes", "only", "only must be a string.", allowed_values=["all", "attributes", "tabular_sections", "dimensions", "resources", "register_fields"], ) only = raw_only.strip().casefold() if only not in ATTRIBUTE_ONLY_ALIASES: return None, invalid_argument( "metadata.object.attributes", "only", "only must be one of the allowed values.", allowed_values=["all", "attributes", "tabular_sections", "dimensions", "resources", "register_fields"], ) return { "include_storage": bool(include_storage), "refresh_cache": bool(refresh_cache), "use_cache": bool(use_cache), "only": ATTRIBUTE_ONLY_ALIASES[only], "limit": limit, "view": view, "table": table, }, None def adapter_update_attributes_job( job_id: str, partial: dict[str, Any], current_step: str, completed: int, total: int, *, started_at: float, running_steps: list[str] | None = None, queued_steps: list[str] | None = None, done_steps: list[str] | None = None, failed_steps: list[str] | None = None, ) -> None: now = adapter_now() partial["elapsed_seconds"] = round(max(0.0, now - started_at), 3) partial["last_section_update_at"] = now progress: dict[str, Any] = { "current_step": current_step, "completed_steps": completed, "total_steps": total, "percent": int((completed / total) * 100) if total else 0, "elapsed_seconds": partial["elapsed_seconds"], "last_section_update_at": partial["last_section_update_at"], } if running_steps is not None: progress["running_steps"] = running_steps if queued_steps is not None: progress["queued_steps"] = queued_steps if done_steps is not None: progress["done_steps"] = done_steps if failed_steps is not None: progress["failed_steps"] = failed_steps adapter_job_set(job_id, partial_result=partial, current_step=current_step, progress=progress) def adapter_run_metadata_object_attributes_job(job_id: str, payload: dict[str, Any], timeout_seconds: float) -> None: partial = adapter_attributes_partial_result(payload) started_at = adapter_now() steps = ["card", "semantic", "type_resolution", "build_result"] queued_steps = list(steps) done_steps: list[str] = [] failed_steps: list[str] = [] adapter_update_attributes_job( job_id, partial, "starting", 0, len(steps), started_at=started_at, running_steps=[], queued_steps=queued_steps, done_steps=done_steps, failed_steps=failed_steps, ) try: if "card" in queued_steps: queued_steps.remove("card") partial["sections"]["card"] = "running" card_timeout = adapter_timeout_payload_value(timeout_seconds) card_started_at = adapter_section_timing_start(partial, "card", "metadata.object.get", card_timeout) adapter_update_attributes_job( job_id, partial, "card:metadata.object.get", 0, len(steps), started_at=started_at, running_steps=["card"], queued_steps=queued_steps, done_steps=done_steps, failed_steps=failed_steps, ) card_result = call_method_impl("metadata.object.get", {**payload, "include_semantic": False, "timeout_seconds": card_timeout}) if isinstance(card_result, dict) and card_result.get("status") == "ok": partial["object"] = card_result.get("object") partial["sections"]["card"] = "ok" if partial.get("object") else "empty" adapter_section_timing_finish(partial, "card", "ok" if partial.get("object") else "empty", card_started_at) done_steps.append("card") else: adapter_section_failed(partial, "card", "metadata.object.get", card_result) adapter_section_timing_finish(partial, "card", "failed", card_started_at) failed_steps.append("card") reason = None if isinstance(card_result, dict): diagnostics = card_result.get("diagnostics") if isinstance(diagnostics, dict): reason = diagnostics.get("message") elif diagnostics is not None: reason = str(diagnostics) if card_result.get("object") is not None: partial["object"] = card_result.get("object") if card_result.get("matches") is not None: partial["matches"] = card_result.get("matches") reason = reason or "Object card was not resolved." for queued_section in list(queued_steps): partial.setdefault("sections", {})[queued_section] = "not_started_due_to_card_failure" partial.setdefault("failed_sections", []).append( { "section": queued_section, "method": "metadata.object.attributes", "status": "not_started_due_to_card_failure", "diagnostics": {"message": reason}, } ) failed_steps.append(queued_section) queued_steps = [] partial["status"] = str(card_result.get("status") or "partial") if isinstance(card_result, dict) else "partial" partial["elapsed_seconds"] = round(max(0.0, adapter_now() - started_at), 3) partial["last_section_update_at"] = adapter_now() adapter_update_attributes_job( job_id, partial, "done", 1, len(steps), started_at=started_at, running_steps=[], queued_steps=queued_steps, done_steps=done_steps, failed_steps=failed_steps, ) adapter_job_finish( job_id, "done", result=partial, partial_result=partial, progress={ "current_step": "done", "completed_steps": 1, "total_steps": len(steps), "percent": int((1 / len(steps)) * 100), "running_steps": [], "queued_steps": [], "done_steps": done_steps, "failed_steps": failed_steps, "elapsed_seconds": partial["elapsed_seconds"], "last_section_update_at": partial["last_section_update_at"], }, ) return adapter_update_attributes_job( job_id, partial, "card", 1, len(steps), started_at=started_at, running_steps=[], queued_steps=queued_steps, done_steps=done_steps, failed_steps=failed_steps, ) if adapter_job_cancel_requested(job_id): progress = adapter_mark_attributes_cancelled(partial, started_at=started_at) adapter_job_finish(job_id, "cancelled", partial_result=partial, result=partial, progress=progress) return if "semantic" in queued_steps: queued_steps.remove("semantic") partial["sections"]["semantic"] = "running" elapsed = adapter_now() - started_at remaining = max(1.0, timeout_seconds - elapsed) if (payload.get("section_timeout_seconds") or payload.get("_section_timeout_seconds")) in {None, ""}: section_timeout = min(120.0, remaining) else: section_timeout = adapter_section_timeout_seconds(payload, remaining) section_started_at = adapter_section_timing_start(partial, "semantic", "metadata.object.attributes", section_timeout) adapter_update_attributes_job( job_id, partial, "semantic:metadata.object.attributes", 1, len(steps), started_at=started_at, running_steps=["semantic"], queued_steps=queued_steps, done_steps=done_steps, failed_steps=failed_steps, ) executor = concurrent.futures.ThreadPoolExecutor(max_workers=1, thread_name_prefix=f"onec-attrs-semantic-{job_id[:8]}") future = executor.submit(metadata_object_attributes, {**payload, "timeout_seconds": adapter_timeout_payload_value(section_timeout)}) try: try: result = future.result(timeout=section_timeout) except concurrent.futures.TimeoutError: future.cancel() result = adapter_public_error( "metadata.object.attributes", "section_timeout", { "message": f"Section `semantic` timed out after {section_timeout:.0f} seconds", "section": "semantic", "section_timeout_seconds": section_timeout, }, ) finally: executor.shutdown(wait=False, cancel_futures=True) if adapter_job_cancel_requested(job_id): progress = adapter_mark_attributes_cancelled(partial, started_at=started_at) adapter_job_finish(job_id, "cancelled", partial_result=partial, result=partial, progress=progress) return if not isinstance(result, dict) or result.get("status") not in {"ok", "partial"}: partial["status"] = "partial" adapter_section_failed(partial, "semantic", "metadata.object.attributes", result) adapter_section_timing_finish(partial, "semantic", "failed", section_started_at) failed_steps.append("semantic") for queued_section in list(queued_steps): partial.setdefault("sections", {})[queued_section] = "not_started_due_to_section_failure" adapter_update_attributes_job( job_id, partial, "done", len(steps), len(steps), started_at=started_at, running_steps=[], queued_steps=[], done_steps=done_steps, failed_steps=failed_steps, ) adapter_job_finish( job_id, "done", result=partial, partial_result=partial, progress={ "current_step": "done", "completed_steps": len(steps), "total_steps": len(steps), "percent": 100, "running_steps": [], "queued_steps": [], "done_steps": done_steps, "failed_steps": failed_steps, "elapsed_seconds": partial.get("elapsed_seconds"), "last_section_update_at": partial.get("last_section_update_at"), }, ) return partial.update(result) partial.setdefault("sections", {})["semantic"] = "ok" partial.setdefault("sections", {})["type_resolution"] = "ok" partial.setdefault("sections", {})["build_result"] = "ok" adapter_section_timing_finish(partial, "semantic", "ok", section_started_at) done_steps.extend(["semantic", "type_resolution", "build_result"]) partial["status"] = result.get("status") or "ok" adapter_update_attributes_job( job_id, partial, "done", len(steps), len(steps), started_at=started_at, running_steps=[], queued_steps=[], done_steps=done_steps, failed_steps=failed_steps, ) adapter_job_finish( job_id, "done", result=partial, partial_result=partial, progress={ "current_step": "done", "completed_steps": len(steps), "total_steps": len(steps), "percent": 100, "running_steps": [], "queued_steps": [], "done_steps": done_steps, "failed_steps": failed_steps, "elapsed_seconds": partial.get("elapsed_seconds"), "last_section_update_at": partial.get("last_section_update_at"), }, ) except Exception as exc: adapter_job_finish(job_id, "error", **adapter_public_error("metadata.object.attributes", "adapter_job_exception", {"message": str(exc), "traceback": traceback.format_exc(limit=8)})) def validate_metadata_object_get_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "metadata.object.get") if isinstance(base_id_or_error, dict): return base_id_or_error selector_error = validate_object_selector_arguments(payload, "metadata.object.get") if selector_error: return selector_error guid_error = validate_explicit_guid_argument(payload, "metadata.object.get") if guid_error: return guid_error if "mode" in payload and (payload.get("mode") is None or payload.get("mode") == ""): return invalid_argument("metadata.object.get", "mode", "mode must be one of: card, semantic.", allowed_values=["card", "semantic"]) if "mode" in payload and not isinstance(payload.get("mode"), str): return invalid_argument("metadata.object.get", "mode", "mode must be a JSON string.", allowed_values=["card", "semantic"]) ordinal_error = validate_explicit_ordinal_arguments(payload, "metadata.object.get") if ordinal_error: return ordinal_error _, limit_error = parse_int_argument(payload, "limit", method="metadata.object.get", default=20, minimum=1, maximum=5000) if limit_error: return limit_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.get", default=60, minimum=1) if timeout_error: return timeout_error _, view_error = parse_view_argument(payload, "metadata.object.get") if view_error: return view_error mode = str(payload.get("mode") or "card").strip().casefold() if mode not in {"card", "semantic"}: return invalid_argument("metadata.object.get", "mode", "mode must be one of: card, semantic.", allowed_values=["card", "semantic"]) _, include_storage_error = strict_bool_argument(payload, "include_storage", method="metadata.object.get", default=False) if include_storage_error: return include_storage_error _, include_semantic_error = strict_bool_argument(payload, "include_semantic", method="metadata.object.get", default=False) if include_semantic_error: return include_semantic_error table_or_error = metadata_storage_table(payload, "metadata.object.get") if isinstance(table_or_error, dict): return table_or_error return None def validate_modules_search_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "modules.search") if isinstance(base_id_or_error, dict): return base_id_or_error selector_error = validate_object_selector_arguments(payload, "modules.search") if selector_error: return selector_error normalized_payload = normalize_object_selector_aliases(payload, "modules.search") if isinstance(normalized_payload, dict) and normalized_payload.get("status") == "invalid_argument": return normalized_payload payload = normalized_payload query_value = payload.get("query") if query_value is not None and not isinstance(query_value, str): return invalid_argument("modules.search", "query", "query must be a JSON string.") query = str(query_value or "").strip() _, include_storage_error = strict_include_storage(payload, "modules.search") if include_storage_error: return include_storage_error _, resolve_owners_error = strict_bool_argument(payload, "resolve_owners", method="modules.search", default=False) if resolve_owners_error: return resolve_owners_error _, full_scan_error = strict_bool_argument(payload, "full_scan", method="modules.search", default=False) if full_scan_error: return full_scan_error if not query: return invalid_argument("modules.search", "query", "Передайте непустой query.") string_error = validate_optional_string_arguments( payload, "modules.search", ["table", "prefix", "scope", "extension", "extension_guid", "routine_name", "state"], ) if string_error: return string_error saved_extension_guid = str(payload.get("extension_guid") or "").strip().lower() if "extension_guid" in payload and not is_guid_text(saved_extension_guid): return invalid_argument("modules.search", "extension_guid", "extension_guid must be a GUID string.") state = str(payload.get("state") or "working").strip().lower() if state not in EXTENSION_OBJECTS_FIND_STATES: return invalid_argument("modules.search", "state", "Unsupported saved-state mode.", allowed_values=sorted(EXTENSION_OBJECTS_FIND_STATES)) scope = str(payload.get("scope") or "auto").strip().casefold() if scope not in {"auto", "modules", "configcas", "config", "all"}: return invalid_argument("modules.search", "scope", "Unsupported scope. Allowed values: auto, modules, configcas, config, all.", allowed_values=["auto", "modules", "configcas", "config", "all"]) table = str(payload.get("table") or "auto") if table != "auto" and table not in STORAGE_TABLES: return invalid_argument("modules.search", "table", "Unsupported storage table.", allowed_values=["auto", *sorted(STORAGE_TABLES)]) table_for_read = table if table in STORAGE_TABLES else "Config" for argument, default, minimum, maximum in ( ("scan_limit", 300, 1, 5000), ("owner_scan_limit", 40, 1, 200), ("read_max_chars", 4000, 1, 100000), ("timeout_seconds", 60, 1, None), ): _, int_error = parse_int_argument(payload, argument, method="modules.search", default=default, minimum=minimum, maximum=maximum) if int_error: return int_error _, limit_error = parse_int_alias_argument(payload, "limit", "max_matches", method="modules.search", default=20, minimum=1, maximum=100) if limit_error: return limit_error requested_module_ordinal = first_non_empty_arg(payload, "module_ordinal", "module_index", "module_number") if requested_module_ordinal not in {None, ""}: _, ordinal_error = parse_ordinal(requested_module_ordinal, "modules.search", argument="module_ordinal") if ordinal_error: return ordinal_error object_ordinal_selector = first_non_empty_arg(payload, "ordinal", "index", "object_index") has_object_selector = bool(payload.get("guid") or payload.get("name") or (object_ordinal_selector is not None)) if has_object_selector: modules_result = metadata_object_modules({**payload, "include_storage": False, "table": table_for_read}) if modules_result.get("status") != "ok": result = dict(modules_result) result["method"] = "modules.search" return result available_modules = int((modules_result.get("counts") or {}).get("available_modules") or (modules_result.get("counts") or {}).get("modules") or len(modules_result.get("modules") or [])) if int(requested_module_ordinal or 1) > available_modules: return { "schema": "onec_modules_search.v1", "status": "not_found", "error": "module_not_found", "base_id": base_id_or_error, "source": {"kind": "live_metadata"}, "object": modules_result.get("object"), "query": { "query": query, "limit": payload.get("limit") or 20, "scope": "object_modules", "kind": payload.get("kind"), "name": payload.get("name"), "guid": payload.get("guid"), "module_ordinal": requested_module_ordinal, "include_storage": payload.get("include_storage") or False, }, "matches": [], "counts": {"matches": 0, "available_modules": available_modules}, "diagnostics": {"message": f"Module ordinal {requested_module_ordinal} was not found for the selected object."}, } return None def validate_metadata_definition_find_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "metadata.definition.find" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error query_value = first_non_empty_arg(payload, "query", "definition", "identifier", "field", "requisite", "name_filter") if query_value is None: return invalid_argument(method, "query", "query must be a non-empty JSON string.") if not isinstance(query_value, str): return invalid_argument(method, "query", "query must be a JSON string.") if not query_value.strip(): return invalid_argument(method, "query", "query must be a non-empty JSON string.") _, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error areas, areas_error = parse_definition_find_areas(payload) if areas_error: return areas_error _, exact_only_error = strict_bool_argument(payload, "exact_only", method=method, default=False) if exact_only_error: return exact_only_error _, refresh_cache_error = strict_bool_argument(payload, "refresh_cache", method=method, default=False) if refresh_cache_error: return refresh_cache_error _, use_cache_error = strict_bool_argument(payload, "use_cache", method=method, default=False) if use_cache_error: return use_cache_error for argument, default, minimum, maximum in ( ("timeout_seconds", 90, 1, None), ("max_items", 5000, 1, 5000), ("max_matches", 50, 1, 500), ("limit", 20, 1, 5000), ): _, int_error = parse_int_argument(payload, argument, method=method, default=default, minimum=minimum, maximum=maximum) if int_error: return int_error ordinal_error = validate_explicit_ordinal_arguments(payload, method) if ordinal_error: return ordinal_error _, view_error = parse_view_argument(payload, method) if view_error: return view_error string_error = validate_optional_string_arguments( payload, method, ["ref", "object_type", "object_name", "object_guid", "kind", "name", "guid", "view", "form"], ) if string_error: return string_error normalized_payload = normalize_object_selector_aliases(payload, method) if isinstance(normalized_payload, dict) and normalized_payload.get("status") == "invalid_argument": return normalized_payload if not normalized_payload.get("guid") and not normalized_payload.get("name") and not (set(areas) & {"metadata", "extensions"}): return invalid_argument(method, "name", OBJECT_SELECTOR_GLOBAL_REQUIRED_MESSAGE) return None def validate_metadata_resolve_overrides_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "metadata.resolve_overrides" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error method_name = payload.get("method_name") if method_name is None: return invalid_argument(method, "method_name", "method_name is required.") if not isinstance(method_name, str): return invalid_argument(method, "method_name", "method_name must be a JSON string.") if not str(method_name).strip(): return invalid_argument(method, "method_name", "method_name is required.") selector_error = validate_object_selector_arguments(payload, method, include_view=False) if selector_error: return selector_error string_error = validate_optional_non_empty_string_arguments(payload, method, ["extension"]) if string_error: return string_error string_error = validate_optional_string_arguments(payload, method, ["state"]) if string_error: return string_error if "state" in payload and str(payload.get("state") or "").strip().lower() not in EXTENSION_OBJECTS_FIND_STATES: return invalid_argument(method, "state", "Unsupported saved-state mode.", allowed_values=sorted(EXTENSION_OBJECTS_FIND_STATES)) normalized_payload = normalize_object_selector_aliases(payload, method) if isinstance(normalized_payload, dict) and normalized_payload.get("status") == "invalid_argument": return normalized_payload object_selector, _ = _code_query_object_selector(normalized_payload) if not object_selector.get("kind") and not object_selector.get("guid") and not object_selector.get("name"): return invalid_argument(method, "selector", OBJECT_SELECTOR_REQUIRED_MESSAGE_NO_ORDINAL) return None def validate_code_search_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "code.search" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error query_value = payload.get("query") or payload.get("pattern") if query_value is not None and not isinstance(query_value, str): return invalid_argument(method, "query", "query must be a JSON string.") query = str(query_value or "").strip() if not query: return invalid_argument(method, "query", "Передайте непустой query.") saved_extension_guid = str(payload.get("extension_guid") or "").strip().lower() if "extension_guid" in payload and not is_guid_text(saved_extension_guid): return invalid_argument(method, "extension_guid", "extension_guid must be a GUID string.") _, include_line_numbers_error = strict_bool_argument(payload, "include_line_numbers", method=method, default=False) if include_line_numbers_error: return include_line_numbers_error _, include_context_error = strict_bool_argument(payload, "include_context", method=method, default=True) if include_context_error: return include_context_error _, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error _, resolve_owners_error = strict_bool_argument(payload, "resolve_owners", method=method, default=False) if resolve_owners_error: return resolve_owners_error limit, limit_error = parse_int_alias_argument(payload, "limit", "max_matches", method=method, default=25, minimum=1, maximum=500) if limit_error: return limit_error offset, offset_error = parse_int_argument(payload, "offset", method=method, default=0, minimum=0) if offset_error: return offset_error scan_limit, scan_limit_error = parse_int_argument(payload, "scan_limit", method=method, default=300, minimum=1, maximum=5000) if scan_limit_error: return scan_limit_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1) if timeout_error: return timeout_error module_ordinal = payload.get("module_ordinal") if module_ordinal is not None: _, module_ordinal_error = parse_ordinal(module_ordinal, method, argument="module_ordinal") if module_ordinal_error: return module_ordinal_error selector_error = validate_object_selector_arguments(payload, method, include_view=False) if selector_error: return selector_error string_error = validate_optional_string_arguments(payload, method, ["scope", "table", "prefix", "extension", "routine_name", "state"]) if string_error: return string_error if "state" in payload and str(payload.get("state") or "").strip().lower() not in EXTENSION_OBJECTS_FIND_STATES: return invalid_argument(method, "state", "Unsupported saved-state mode.", allowed_values=sorted(EXTENSION_OBJECTS_FIND_STATES)) string_error = validate_optional_non_empty_string_arguments(payload, method, ["query"]) if string_error: return string_error return None def validate_code_read_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "code.read" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error _, include_line_numbers_error = strict_bool_argument(payload, "include_line_numbers", method=method, default=False) if include_line_numbers_error: return include_line_numbers_error _, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error _, include_text_error = strict_bool_argument(payload, "include_text", method=method, default=True) if include_text_error: return include_text_error selector_error = validate_object_selector_arguments(payload, method, include_view=False) if selector_error: return selector_error string_error = validate_optional_non_empty_string_arguments(payload, method, ["routine_name", "module_id", "module_ref", "state"]) if string_error: return string_error if "state" in payload and str(payload.get("state") or "").strip().lower() not in EXTENSION_OBJECTS_FIND_STATES: return invalid_argument(method, "state", "Unsupported saved-state mode.", allowed_values=sorted(EXTENSION_OBJECTS_FIND_STATES)) module_ref_or_id = first_non_empty_arg(payload, "module_ref", "module_id", "module_ordinal", "module_index", "module_number") normalized_payload = normalize_object_selector_aliases(payload, method) if isinstance(normalized_payload, dict) and normalized_payload.get("status") == "invalid_argument": return normalized_payload object_selector, _ = _code_query_object_selector(normalized_payload) if module_ref_or_id is None and not any([object_selector.get("kind"), object_selector.get("guid"), object_selector.get("name")]): return invalid_argument( method, "selector", OBJECT_SELECTOR_OR_MODULE_REQUIRED_MESSAGE, ) _, max_chars_error = parse_int_argument(payload, "max_chars", method=method, default=100000, minimum=1) if max_chars_error: return max_chars_error return validate_modules_read_arguments(payload) def validate_code_write_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = CODE_WRITE_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error target = payload.get("target") if target is not None and not isinstance(target, dict): return invalid_argument(method, "target", "target must be a JSON object when provided.") string_error = validate_optional_string_arguments( payload, method, [ "ref", "kind", "name", "guid", "canonical_path", "path", "object_type", "object_name", "object_guid", "form", "form_name", "routine_name", "routine_text", "routine_operation", "operation", "text", "module_text", "full_text", "code", "old", "new", "extension", "preferred_extension", "module_ref", "module_id", "file_name", "mode", "execution_mode", "summary", ], ) if string_error: return string_error mode = str(payload.get("execution_mode") or payload.get("mode") or "apply").strip().casefold() if mode not in FORM_ELEMENT_WRITE_APPLY_MODES: return invalid_argument(method, "execution_mode", "Unsupported execution mode.", allowed_values=sorted(FORM_ELEMENT_WRITE_APPLY_MODES)) _, include_storage_error = strict_bool_argument(payload, "include_storage", method=method, default=False) if include_storage_error: return include_storage_error has_fragment = payload.get("old") is not None or payload.get("new") is not None if has_fragment and (payload.get("old") is None or payload.get("new") is None): return invalid_argument(method, "old/new", "Pass both old and new for fragment replacement.") has_edit = any(payload.get(key) is not None for key in ("routine_text", "text", "module_text", "full_text", "code", "old", "new")) if not has_edit: return invalid_argument(method, "edit", "Pass module text, routine_text, or old/new fragment replacement.") has_path_with_routine = bool( payload.get("canonical_path") or payload.get("path") or (isinstance(target, dict) and (target.get("canonical_path") or target.get("path"))) ) if payload.get("routine_text") is not None and not ( payload.get("routine_name") or (isinstance(target, dict) and target.get("routine_name")) or has_path_with_routine ): return invalid_argument(method, "routine_name", "Pass routine_name when replacing a routine.") _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) return timeout_error def validate_code_symbol_resolve_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "code.symbol.resolve" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error expression_value = first_non_empty_arg(payload, "expression", "symbol", "path") if expression_value is None: return invalid_argument(method, "expression", "expression is required.") if not isinstance(expression_value, str): return invalid_argument(method, "expression", "expression must be a JSON string.") if not expression_value.strip(): return invalid_argument(method, "expression", "expression must be a non-empty BSL expression.") selector_error = validate_object_selector_arguments(payload, method, include_view=False) if selector_error: return selector_error string_error = validate_optional_non_empty_string_arguments(payload, method, ["expression", "symbol", "path", "routine_name", "module_id", "module_ref"]) if string_error: return string_error module_ref_or_id = first_non_empty_arg(payload, "module_ref", "module_id", "module_ordinal", "module_index", "module_number") normalized_payload = normalize_object_selector_aliases(payload, method) if isinstance(normalized_payload, dict) and normalized_payload.get("status") == "invalid_argument": return normalized_payload object_selector, _ = _code_query_object_selector(normalized_payload) if module_ref_or_id is None and not any([object_selector.get("kind"), object_selector.get("guid"), object_selector.get("name")]): return invalid_argument(method, "selector", OBJECT_SELECTOR_OR_MODULE_REQUIRED_MESSAGE) _, max_chars_error = parse_int_argument(payload, "max_chars", method=method, default=200000, minimum=1) if max_chars_error: return max_chars_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error return validate_modules_read_arguments(payload) def validate_extension_objects_find_payload(payload: dict[str, Any], method: str = "extension.objects.find") -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error selector_error = validate_object_selector_arguments(payload, method, include_view=False) if selector_error: return selector_error string_error = validate_optional_string_arguments(payload, method, ["query", "name_filter", "extension", "kind", "object_type", "name", "object_name", "guid", "object_guid", "table", "state"]) if string_error: return string_error if "state" in payload and str(payload.get("state") or "").strip().lower() not in EXTENSION_OBJECTS_FIND_STATES: return invalid_argument(method, "state", "Unsupported saved-state mode.", allowed_values=sorted(EXTENSION_OBJECTS_FIND_STATES)) _, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error for argument, default in (("use_cache", True), ("refresh_cache", False), ("full_scan", False)): _, bool_error = strict_bool_argument(payload, argument, method=method, default=default) if bool_error: return bool_error _, limit_error = parse_int_alias_argument(payload, "limit", "max_matches", method=method, default=50, minimum=1, maximum=500) if limit_error: return limit_error for argument, default, minimum, maximum in ( ("scan_limit", 5000, 1, 20000), ("cache_ttl_seconds", 300, 0, 86400), ("timeout_seconds", 90, 1, None), ): _, int_error = parse_int_argument(payload, argument, method=method, default=default, minimum=minimum, maximum=maximum) if int_error: return int_error return None def validate_extension_cache_rebuild_payload(payload: dict[str, Any], method: str = "extension.cache.rebuild") -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments(payload, method, ["extension", "kind", "object_type"]) if string_error: return string_error _, include_matches_error = strict_bool_argument(payload, "include_matches", method=method, default=False) if include_matches_error: return include_matches_error _, max_items_error = parse_int_argument(payload, "max_items", method=method, default=5000, minimum=1, maximum=50000) if max_items_error: return max_items_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=180, minimum=1) if timeout_error: return timeout_error return None def validate_extension_cache_status_payload(payload: dict[str, Any], method: str = "extension.cache.status") -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments(payload, method, ["extension", "kind", "object_type"]) if string_error: return string_error _, include_entries_error = strict_bool_argument(payload, "include_entries", method=method, default=False) if include_entries_error: return include_entries_error _, limit_error = parse_int_argument(payload, "limit", method=method, default=50, minimum=1, maximum=1000) if limit_error: return limit_error return None def validate_extension_cache_validate_payload(payload: dict[str, Any], method: str = "extension.cache.validate") -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments(payload, method, ["extension", "kind", "object_type"]) if string_error: return string_error _, include_entries_error = strict_bool_argument(payload, "include_entries", method=method, default=False) if include_entries_error: return include_entries_error _, limit_error = parse_int_argument(payload, "limit", method=method, default=1000, minimum=1, maximum=50000) if limit_error: return limit_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=120, minimum=1) if timeout_error: return timeout_error return None def validate_templates_read_payload(payload: dict[str, Any], method: str = "templates.read") -> dict[str, Any] | None: payload = normalize_template_route_ref_payload(payload) base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error selector_error = validate_object_selector_arguments(payload, method, include_view=False) if selector_error: return selector_error string_error = validate_optional_string_arguments(payload, method, ["template", "name_filter", "table", "extension", "owner_ref", "ref", "object_type", "object_name", "object_guid", "kind", "name", "guid", "file_name", "part_id", "route_ref", "view"]) if string_error: return string_error if payload.get("route_ref") and not parse_storage_route_ref(payload.get("route_ref")): return invalid_argument(method, "route_ref", "route_ref must have format
: where table is a supported storage table.") if payload.get("sections") is not None and not isinstance(payload.get("sections"), (str, list)): return invalid_argument(method, "sections", "sections must be a comma-separated string or a JSON array of strings.") if payload.get("moxel_record_heads") is not None and not isinstance(payload.get("moxel_record_heads"), (str, list)): return invalid_argument(method, "moxel_record_heads", "moxel_record_heads must be a comma-separated string or a JSON array of integers.") if payload.get("moxel_candidate_heads") is not None and not isinstance(payload.get("moxel_candidate_heads"), (str, list)): return invalid_argument(method, "moxel_candidate_heads", "moxel_candidate_heads must be a comma-separated string or a JSON array of integers.") if payload.get("moxel_candidate_reasons") is not None and not isinstance(payload.get("moxel_candidate_reasons"), (str, list)): return invalid_argument(method, "moxel_candidate_reasons", "moxel_candidate_reasons must be a comma-separated string or a JSON array of strings.") view = str(payload.get("view") or "").strip().lower() if view and view not in {"summary", "structure", "full"}: return invalid_argument(method, "view", "view must be one of: summary, structure, full.") _, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error _, include_content_error = strict_bool_argument(payload, "include_content", method=method, default=False) if include_content_error: return include_content_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1) if timeout_error: return timeout_error for argument, default, minimum, maximum in ( ("max_areas", 20, 0, 5000), ("max_cells", 20, 0, 5000), ("max_parameters", 50, 0, 5000), ("max_coverage", 20, 0, 5000), ("max_widths", 50, 0, 5000), ("max_merged", 20, 0, 5000), ("max_intersections", 20, 0, 5000), ("max_strings", 20, 0, 1000), ("max_moxel_records", 20, 0, 1000), ("moxel_record_start", 0, 0, 100000), ("moxel_record_end", 0, 0, 100000), ("moxel_record_context", 0, 0, 100), ("moxel_candidate_rank", 1, 1, 1000), ("moxel_candidate_window_index", 1, 1, 1000), ("moxel_candidate_start", 0, 0, 100000), ("moxel_candidate_end", 0, 0, 100000), ("moxel_candidate_min_score", 0, 0, 1000), ("max_content_bytes", TEMPLATE_CONTENT_DEFAULT_MAX_BYTES, 1, TEMPLATE_CONTENT_MAX_BYTES), ): _, int_error = parse_int_argument(payload, argument, method=method, default=default, minimum=minimum, maximum=maximum) if int_error: return int_error table_or_error = metadata_storage_table(payload, method) if isinstance(table_or_error, dict): return table_or_error normalized_payload = normalize_object_selector_aliases(payload, method) if isinstance(normalized_payload, dict) and normalized_payload.get("status") == "invalid_argument": return normalized_payload selector_value = first_non_empty_arg(normalized_payload, "ref", "guid", "name", "object_guid", "object_name", "file_name", "part_id", "route_ref", "ordinal", "index", "object_index") if selector_value in {None, ""}: return invalid_argument(method, "selector", OBJECT_SELECTOR_REQUIRED_MESSAGE) return None def validate_templates_areas_find_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "templates.areas.find" payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload payload = normalize_template_route_ref_payload(payload) base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments( payload, method, ["ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "query", "template", "name_filter", "extension", "table", "file_name", "part_id", "route_ref", "area_query", "area", "area_name", "area_match"], ) if string_error: return string_error area_match = str(payload.get("area_match") or "").strip().lower() if area_match and area_match not in {"contains", "exact"}: return invalid_argument(method, "area_match", "area_match must be one of: contains, exact.") if payload.get("route_ref") and not parse_storage_route_ref(payload.get("route_ref")): return invalid_argument(method, "route_ref", "route_ref must have format
: where table is a supported storage table.") if not str(first_non_empty_arg(payload, "query", "template", "name_filter", "name", "object_name", "file_name", "part_id", "route_ref") or "").strip(): return invalid_argument(method, "query", "Pass query/template/name or file_name.") for argument, default in (("refresh_cache", False), ("include_empty", True), ("include_coverage", True)): _, bool_error = strict_bool_argument(payload, argument, method=method, default=default) if bool_error: return bool_error for argument, default, minimum, maximum in ( ("limit", 5, 1, 50), ("max_areas", 500, 0, 5000), ("timeout_seconds", 90, 1, 600), ("cache_ttl_seconds", 300, 0, 86400), ): _, int_error = parse_int_argument(payload, argument, method=method, default=default, minimum=minimum, maximum=maximum) if int_error: return int_error _, occurrence_error = parse_int_alias_argument(payload, "area_occurrence", "occurrence", method=method, default=0, minimum=0, maximum=100000) if occurrence_error: return occurrence_error if ("area_occurrence" in payload or "occurrence" in payload) and int(payload.get("area_occurrence") if "area_occurrence" in payload else payload.get("occurrence")) < 1: return invalid_argument(method, "area_occurrence", "area_occurrence/occurrence is 1-based and must be >= 1.") return None def validate_templates_bindings_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "templates.bindings" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error _, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error selector_error = validate_object_selector_arguments(payload, method, include_view=False) if selector_error: return selector_error string_error = validate_optional_non_empty_string_arguments(payload, method, ["template"]) if string_error: return string_error normalized_payload = normalize_object_selector_aliases(payload, method) if isinstance(normalized_payload, dict) and normalized_payload.get("status") == "invalid_argument": return normalized_payload object_selector, _ = _code_query_object_selector(normalized_payload) if not object_selector.get("kind") and not object_selector.get("guid") and not object_selector.get("name"): return invalid_argument(method, "selector", OBJECT_SELECTOR_REQUIRED_MESSAGE_NO_ORDINAL) return None def validate_diagnostics_call_chain_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "diagnostics.call_chain" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error entry_method = payload.get("entry_method") if entry_method is None or entry_method == "": entry_method = payload.get("method_name") if entry_method is not None and not isinstance(entry_method, str): return invalid_argument(method, "entry_method", "entry_method must be a JSON string.") if not str(entry_method or "").strip(): return invalid_argument(method, "entry_method", "entry_method is required.") selector_error = validate_object_selector_arguments(payload, method, include_view=False) if selector_error: return selector_error normalized_payload = normalize_object_selector_aliases(payload, method) if isinstance(normalized_payload, dict) and normalized_payload.get("status") == "invalid_argument": return normalized_payload object_selector, _ = _code_query_object_selector(normalized_payload) if not object_selector.get("kind") and not object_selector.get("guid") and not object_selector.get("name"): return invalid_argument(method, "selector", OBJECT_SELECTOR_REQUIRED_MESSAGE_NO_ORDINAL) return None def validate_metadata_object_modules_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "metadata.object.modules") if isinstance(base_id_or_error, dict): return base_id_or_error selector_error = validate_object_selector_arguments(payload, "metadata.object.modules") if selector_error: return selector_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.modules", default=60, minimum=1) if timeout_error: return timeout_error table_or_error = metadata_storage_table(payload, "metadata.object.modules") if isinstance(table_or_error, dict): return table_or_error ordinal_error = validate_explicit_ordinal_arguments(payload, "metadata.object.modules") if ordinal_error: return ordinal_error _, lookup_limit_error = parse_object_lookup_limit(payload, "metadata.object.modules") if lookup_limit_error: return lookup_limit_error _, view_error = parse_view_argument(payload, "metadata.object.modules") if view_error: return view_error _, requested_module_error = optional_string_filter(payload, ["module", "name_filter"], method="metadata.object.modules") if requested_module_error: return requested_module_error _, include_storage_error = strict_include_storage(payload, "metadata.object.modules") if include_storage_error: return include_storage_error return None def validate_metadata_object_forms_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "metadata.object.forms") if isinstance(base_id_or_error, dict): return base_id_or_error selector_error = validate_object_selector_arguments(payload, "metadata.object.forms") if selector_error: return selector_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.forms", default=60, minimum=1) if timeout_error: return timeout_error table_or_error = metadata_storage_table(payload, "metadata.object.forms") if isinstance(table_or_error, dict): return table_or_error ordinal_error = validate_explicit_ordinal_arguments(payload, "metadata.object.forms") if ordinal_error: return ordinal_error _, lookup_limit_error = parse_object_lookup_limit(payload, "metadata.object.forms") if lookup_limit_error: return lookup_limit_error _, view_error = parse_view_argument(payload, "metadata.object.forms") if view_error: return view_error for argument in ("include_text", "include_tree"): _, bool_error = strict_bool_argument(payload, argument, method="metadata.object.forms", default=False) if bool_error: return bool_error _, requested_form_error = optional_string_filter(payload, ["form", "name_filter"], method="metadata.object.forms") if requested_form_error: return requested_form_error _, include_storage_error = strict_include_storage(payload, "metadata.object.forms") if include_storage_error: return include_storage_error return None def validate_metadata_object_templates_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "metadata.object.templates") if isinstance(base_id_or_error, dict): return base_id_or_error selector_error = validate_object_selector_arguments(payload, "metadata.object.templates") if selector_error: return selector_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.templates", default=60, minimum=1) if timeout_error: return timeout_error table_or_error = metadata_storage_table(payload, "metadata.object.templates") if isinstance(table_or_error, dict): return table_or_error for argument in ("include_text", "include_tree"): _, bool_error = strict_bool_argument(payload, argument, method="metadata.object.templates", default=False) if bool_error: return bool_error _, evidence_mode_error = parse_evidence_mode_argument(payload, "metadata.object.templates") if evidence_mode_error: return evidence_mode_error _, requested_template_error = optional_string_filter(payload, ["template", "name_filter"], method="metadata.object.templates") if requested_template_error: return requested_template_error _, include_storage_error = strict_include_storage(payload, "metadata.object.templates") if include_storage_error: return include_storage_error return None def validate_metadata_object_related_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "metadata.object.related") if isinstance(base_id_or_error, dict): return base_id_or_error selector_error = validate_object_selector_arguments(payload, "metadata.object.related") if selector_error: return selector_error guid_error = validate_explicit_guid_argument(payload, "metadata.object.related") if guid_error: return guid_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.related", default=60, minimum=1) if timeout_error: return timeout_error table_or_error = metadata_storage_table(payload, "metadata.object.related") if isinstance(table_or_error, dict): return table_or_error ordinal_error = validate_explicit_ordinal_arguments(payload, "metadata.object.related") if ordinal_error: return ordinal_error _, lookup_limit_error = parse_object_lookup_limit(payload, "metadata.object.related") if lookup_limit_error: return lookup_limit_error _, view_error = parse_view_argument(payload, "metadata.object.related") if view_error: return view_error _, include_storage_error = strict_include_storage(payload, "metadata.object.related") if include_storage_error: return include_storage_error _, include_text_error = strict_bool_argument(payload, "include_text", method="metadata.object.related", default=False) if include_text_error: return include_text_error _, guids_error = parse_int_argument(payload, "guids_per_record", method="metadata.object.related", default=5, minimum=1, maximum=50) if guids_error: return guids_error return None def validate_metadata_object_parts_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "metadata.object.parts") if isinstance(base_id_or_error, dict): return base_id_or_error selector_error = validate_object_selector_arguments(payload, "metadata.object.parts") if selector_error: return selector_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.parts", default=60, minimum=1) if timeout_error: return timeout_error table_or_error = metadata_storage_table(payload, "metadata.object.parts") if isinstance(table_or_error, dict): return table_or_error _, include_storage_error = strict_include_storage(payload, "metadata.object.parts") if include_storage_error: return include_storage_error for argument in ("include_text", "include_tree"): _, bool_error = strict_bool_argument(payload, argument, method="metadata.object.parts", default=False) if bool_error: return bool_error _, evidence_mode_error = parse_evidence_mode_argument(payload, "metadata.object.parts") if evidence_mode_error: return evidence_mode_error _, part_limit_error = parse_int_argument(payload, "part_limit", method="metadata.object.parts", default=200, minimum=1, maximum=5000) if part_limit_error: return part_limit_error guid_error = validate_explicit_guid_argument(payload, "metadata.object.parts") if guid_error: return guid_error ordinal_error = validate_explicit_ordinal_arguments(payload, "metadata.object.parts") if ordinal_error: return ordinal_error _, lookup_limit_error = parse_object_lookup_limit(payload, "metadata.object.parts") if lookup_limit_error: return lookup_limit_error _, view_error = parse_view_argument(payload, "metadata.object.parts") if view_error: return view_error return None def validate_metadata_object_form_details_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "metadata.object.form.details") if isinstance(base_id_or_error, dict): return base_id_or_error selector_error = validate_object_selector_arguments(payload, "metadata.object.form.details") if selector_error: return selector_error _, include_storage_error = strict_include_storage(payload, "metadata.object.form.details") if include_storage_error: return include_storage_error table_or_error = metadata_storage_table(payload, "metadata.object.form.details") if isinstance(table_or_error, dict): return table_or_error _, include_module_text_error = strict_bool_argument(payload, "include_module_text", method="metadata.object.form.details", default=False) if include_module_text_error: return include_module_text_error _, include_parameters_error = strict_bool_argument(payload, "include_parameters", method="metadata.object.form.details", default=True) if include_parameters_error: return include_parameters_error for argument, default, maximum in ( ("max_forms", 20, 100), ("max_items", 1000, 5000), ("max_attributes", 1000, 5000), ("max_commands", 1000, 5000), ("max_parameters", 80, 500), ): _, int_error = parse_int_argument(payload, argument, method="metadata.object.form.details", default=default, minimum=1, maximum=maximum) if int_error: return int_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.form.details", default=60, minimum=1) if timeout_error: return timeout_error element_error = validate_optional_string_arguments(payload, "metadata.object.form.details", ["element", "element_name", "element_path", "path", "element_id", "id"]) if element_error: return element_error return validate_metadata_object_forms_payload(payload) def validate_metadata_object_template_details_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "metadata.object.template.details") if isinstance(base_id_or_error, dict): return base_id_or_error selector_error = validate_object_selector_arguments(payload, "metadata.object.template.details") if selector_error: return selector_error _, include_storage_error = strict_include_storage(payload, "metadata.object.template.details") if include_storage_error: return include_storage_error table_or_error = metadata_storage_table(payload, "metadata.object.template.details") if isinstance(table_or_error, dict): return table_or_error _, include_preview_error = strict_bool_argument(payload, "include_preview", method="metadata.object.template.details", default=True) if include_preview_error: return include_preview_error _, evidence_mode_error = parse_evidence_mode_argument(payload, "metadata.object.template.details") if evidence_mode_error: return evidence_mode_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.template.details", default=60, minimum=1) if timeout_error: return timeout_error _, requested_template_error = optional_string_filter(payload, ["template", "name_filter"], method="metadata.object.template.details") if requested_template_error: return requested_template_error return validate_metadata_object_templates_payload(payload) def validate_metadata_form_decode_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "metadata.form.decode") if isinstance(base_id_or_error, dict): return base_id_or_error selector_error = validate_object_selector_arguments(payload, "metadata.form.decode") if selector_error: return selector_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.form.decode", default=60, minimum=1) if timeout_error: return timeout_error _, include_storage_error = strict_include_storage(payload, "metadata.form.decode") if include_storage_error: return include_storage_error for argument in ("include_module_text", "include_module"): _, bool_error = strict_bool_argument(payload, argument, method="metadata.form.decode", default=False) if bool_error: return bool_error _, evidence_mode_error = parse_evidence_mode_argument(payload, "metadata.form.decode") if evidence_mode_error: return evidence_mode_error _, include_parameters_error = strict_bool_argument(payload, "include_parameters", method="metadata.form.decode", default=True) if include_parameters_error: return include_parameters_error _, max_items_error = parse_int_argument(payload, "max_items", method="metadata.form.decode", default=500, minimum=1, maximum=5000) if max_items_error: return max_items_error _, max_parameters_error = parse_int_argument(payload, "max_parameters", method="metadata.form.decode", default=80, minimum=1, maximum=500) if max_parameters_error: return max_parameters_error for argument in ("table", "file_name", "form_guid", "guid"): if argument not in payload: continue value = payload.get(argument) if value is None or value == "": return invalid_argument("metadata.form.decode", argument, f"{argument} must be a non-empty JSON string when provided.") if not isinstance(value, str): return invalid_argument("metadata.form.decode", argument, f"{argument} must be a JSON string.") _, requested_form_error = optional_string_filter(payload, ["form", "name_filter"], method="metadata.form.decode") if requested_form_error: return requested_form_error element_error = validate_optional_string_arguments(payload, "metadata.form.decode", ["element", "element_name", "element_path", "path", "element_id", "id"]) if element_error: return element_error table = str(payload.get("table") or "Config") if table not in STORAGE_TABLES: return invalid_argument("metadata.form.decode", "table", "Unsupported storage table.", allowed_values=sorted(STORAGE_TABLES)) return None def validate_metadata_form_owner_index_build_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = FORM_OWNER_INDEX_BUILD_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments(payload, method, ["extension", "kind", "object_type", "name", "object_name", "form", "form_name", "table", "file_name"]) if string_error: return string_error table = str(payload.get("table") or "") if table and table not in STORAGE_TABLES: return invalid_argument(method, "table", "Unsupported storage table.", allowed_values=sorted(STORAGE_TABLES)) for argument, default, minimum, maximum in ( ("limit", 10, 1, 100), ("scan_limit", 5000, 1, 20000), ("timeout_seconds", 90, 1, None), ): _, int_error = parse_int_argument(payload, argument, method=method, default=default, minimum=minimum, maximum=maximum) if int_error: return int_error _, refresh_cache_error = strict_bool_argument(payload, "refresh_cache", method=method, default=False) if refresh_cache_error: return refresh_cache_error return None def validate_metadata_saved_state_forms_search_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = SAVED_STATE_FORMS_SEARCH_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1) if timeout_error: return timeout_error _, limit_error = parse_int_argument(payload, "limit", method=method, default=50, minimum=1, maximum=1000) if limit_error: return limit_error _, scan_limit_error = parse_int_argument(payload, "scan_limit", method=method, default=1000, minimum=1, maximum=5000) if scan_limit_error: return scan_limit_error string_error = validate_optional_string_arguments( payload, method, [ "ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "form", "form_name", "form_guid", "name_filter", "element", "element_name", "command", "attribute", "query", "text", "prefix", "extension", "extension_guid", ], ) if string_error: return string_error if "tables" in payload and not (isinstance(payload.get("tables"), list) and all(isinstance(item, str) for item in payload.get("tables") or [])): return invalid_argument(method, "tables", "tables must be an array of strings.") return None def validate_metadata_saved_state_prepare_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "metadata.saved_state.prepare" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments( payload, method, [ "layer", "target_table", "source_table", "table", "file_name", "module_ref", "module_id", "ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "extension", "mode", "execution_mode", ], ) if string_error: return string_error layer = str(payload.get("layer") or "").strip() if layer and layer not in SAVED_STATE_TABLE_BY_LAYER: return invalid_argument(method, "layer", "layer must be base_saved_state or extension_saved_state.", allowed_values=sorted(SAVED_STATE_TABLE_BY_LAYER)) target_table = str(payload.get("target_table") or payload.get("table") or "").strip() if target_table and target_table not in SAVED_STATE_SOURCE_BY_TARGET: return invalid_argument(method, "target_table", "target_table must be ConfigSave or ConfigCASSave.", allowed_values=sorted(SAVED_STATE_SOURCE_BY_TARGET)) if layer and target_table and target_table != SAVED_STATE_TABLE_BY_LAYER[layer]: return invalid_argument(method, "layer", "layer conflicts with target_table.") if layer == "base_saved_state" and str(payload.get("extension") or "").strip(): return invalid_argument(method, "layer", "Extension object preparation requires extension_saved_state.", allowed_values=["extension_saved_state"]) if "file_names" in payload and payload.get("file_names") is not None: file_names = payload.get("file_names") if not isinstance(file_names, list) or not all(isinstance(item, str) for item in file_names): return invalid_argument(method, "file_names", "file_names must be an array of strings.") mode = str(payload.get("mode") or payload.get("execution_mode") or "plan").strip().casefold() if mode not in {"plan", "apply", "apply_and_verify"}: return invalid_argument(method, "mode", "Unsupported mode.", allowed_values=["plan", "apply", "apply_and_verify"]) if mode in {"apply", "apply_and_verify"}: _, allow_error = strict_bool_argument(payload, "allow_sql_saved_state_prepare", method=method, default=False) if allow_error: return allow_error _, part_limit_error = parse_int_argument(payload, "part_limit", method=method, default=5000, minimum=1, maximum=20000) if part_limit_error: return part_limit_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1) if timeout_error: return timeout_error _, include_storage_error = strict_include_storage(payload, method) return include_storage_error def validate_metadata_saved_state_status_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = SAVED_STATE_STATUS_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments(payload, method, ["layer", "table", "target_table", "prefix"]) if string_error: return string_error layer = str(payload.get("layer") or "").strip() if layer and layer not in SAVED_STATE_TABLE_BY_LAYER: return invalid_argument(method, "layer", "layer must be base_saved_state or extension_saved_state.", allowed_values=sorted(SAVED_STATE_TABLE_BY_LAYER)) table = str(payload.get("table") or payload.get("target_table") or "") if table and table not in SAVED_STATE_SOURCE_BY_TARGET: return invalid_argument(method, "table", "table must be ConfigSave or ConfigCASSave.", allowed_values=sorted(SAVED_STATE_SOURCE_BY_TARGET)) if layer and table and table != SAVED_STATE_TABLE_BY_LAYER[layer]: return invalid_argument(method, "layer", "layer conflicts with table.") prefix = str(payload.get("prefix") or "").strip() if prefix and Path(prefix).name != prefix: return invalid_argument(method, "prefix", "prefix must be a safe FileName prefix.") for argument, default, minimum, maximum in ( ("timeout_seconds", 30, 1, None), ("limit", 500, 1, 5000), ): _, int_error = parse_int_argument(payload, argument, method=method, default=default, minimum=minimum, maximum=maximum) if int_error: return int_error for argument, default in (("include_files", True), ("include_unchanged", True)): _, bool_error = strict_bool_argument(payload, argument, method=method, default=default) if bool_error: return bool_error _, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error return None def validate_metadata_saved_state_diff_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = SAVED_STATE_DIFF_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments( payload, method, [ "table", "target_table", "source_table", "file_name", "module_ref", "module_id", "ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "extension", ], ) if string_error: return string_error for argument, default, minimum, maximum in ( ("timeout_seconds", 30, 1, None), ("max_changes", 200, 1, 5000), ("max_text_diff_lines", 200, 0, 5000), ): _, int_error = parse_int_argument(payload, argument, method=method, default=default, minimum=minimum, maximum=maximum) if int_error: return int_error for argument, default in ( ("include_text_diff", True), ("include_tree_diff", True), ("include_evidence", False), ("include_payload_diff", False), ("include_storage", False), ): _, bool_error = strict_bool_argument(payload, argument, method=method, default=default) if bool_error: return bool_error module_ref = str(payload.get("module_ref") or payload.get("module_id") or "").strip() module_table = module_file_name = None if module_ref: module_table, module_file_name, _stream_index = parse_module_id(module_ref) if not module_table or not module_file_name: return invalid_argument(method, "module_ref", "Use module_ref in the form
:[#stream:].") table = str(payload.get("table") or payload.get("target_table") or module_table or "").strip() if table and table not in SAVED_STATE_SOURCE_BY_TARGET: return invalid_argument(method, "table", "table must be ConfigSave or ConfigCASSave.", allowed_values=sorted(SAVED_STATE_SOURCE_BY_TARGET)) file_name = str(payload.get("file_name") or module_file_name or "").strip() if file_name and Path(file_name).name != file_name: return invalid_argument(method, "file_name", "file_name must be a safe FileName value.") requested_module_ordinal = first_non_empty_arg(payload, "module_ordinal", "module_index", "module_number") if requested_module_ordinal not in {None, ""}: _, ordinal_error = parse_ordinal(requested_module_ordinal, method, argument="module_ordinal") if ordinal_error: return ordinal_error if not module_ref and not file_name and not has_object_selector(payload): return invalid_argument( method, "selector", "Pass a 1C object selector plus module_ordinal, or a generated module_ref/saved-state file_name.", ) return None def validate_metadata_saved_state_changes_list_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = SAVED_STATE_CHANGES_LIST_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments(payload, method, ["layer", "table", "target_table", "prefix"]) if string_error: return string_error layer = str(payload.get("layer") or "").strip() if layer and layer not in SAVED_STATE_TABLE_BY_LAYER: return invalid_argument(method, "layer", "layer must be base_saved_state or extension_saved_state.", allowed_values=sorted(SAVED_STATE_TABLE_BY_LAYER)) table = str(payload.get("table") or payload.get("target_table") or "").strip() if table and table not in SAVED_STATE_SOURCE_BY_TARGET: return invalid_argument(method, "table", "table must be ConfigSave or ConfigCASSave.", allowed_values=sorted(SAVED_STATE_SOURCE_BY_TARGET)) if layer and table and table != SAVED_STATE_TABLE_BY_LAYER[layer]: return invalid_argument(method, "layer", "layer conflicts with table.") prefix = str(payload.get("prefix") or "").strip() if prefix and Path(prefix).name != prefix: return invalid_argument(method, "prefix", "prefix must be a safe FileName prefix.") _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error _, limit_error = parse_int_argument(payload, "limit", method=method, default=500, minimum=1, maximum=5000) if limit_error: return limit_error _, include_unchanged_error = strict_bool_argument(payload, "include_unchanged", method=method, default=False) if include_unchanged_error: return include_unchanged_error _, include_context_error = strict_bool_argument(payload, "include_context", method=method, default=False) if include_context_error: return include_context_error _, group_by_context_error = strict_bool_argument(payload, "group_by_context", method=method, default=False) if group_by_context_error: return group_by_context_error _, include_storage_error = strict_bool_argument(payload, "include_storage", method=method, default=False) if include_storage_error: return include_storage_error _, context_limit_error = parse_int_argument(payload, "context_limit", method=method, default=50, minimum=0, maximum=500) return context_limit_error def validate_metadata_saved_state_modules_search_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = SAVED_STATE_MODULES_SEARCH_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1) if timeout_error: return timeout_error _, limit_error = parse_int_argument(payload, "limit", method=method, default=50, minimum=1, maximum=1000) if limit_error: return limit_error _, scan_limit_error = parse_int_argument(payload, "scan_limit", method=method, default=1000, minimum=1, maximum=5000) if scan_limit_error: return scan_limit_error _, preview_chars_error = parse_int_argument(payload, "preview_chars", method=method, default=500, minimum=0, maximum=5000) if preview_chars_error: return preview_chars_error if "stream_index" in payload: _, stream_index_error = parse_int_argument(payload, "stream_index", method=method, default=0, minimum=0) if stream_index_error: return stream_index_error string_error = validate_optional_string_arguments( payload, method, ["query", "text", "prefix", "owner_guid", "ref", "object_type", "object_name", "object_guid", "kind", "name", "guid", "file_name", "extension", "extension_guid", "layer"], ) if string_error: return string_error if "tables" in payload and not (isinstance(payload.get("tables"), list) and all(isinstance(item, str) for item in payload.get("tables") or [])): return invalid_argument(method, "tables", "tables must be an array of strings.") layer = str(payload.get("layer") or "").strip() if layer and layer not in {"base_saved_state", "extension_saved_state"}: return invalid_argument( method, "layer", "Unsupported saved-state layer.", allowed_values=["base_saved_state", "extension_saved_state"], ) if layer and "tables" in payload: return invalid_argument(method, "tables", "Pass layer or tables, not both.") return None def validate_metadata_form_write_target_resolve_payload(payload: dict[str, Any], method: str = FORM_WRITE_TARGET_RESOLVE_METHOD) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1) if timeout_error: return timeout_error _, max_items_error = parse_int_argument(payload, "max_items", method=method, default=5000, minimum=1, maximum=5000) if max_items_error: return max_items_error _, search_limit_error = parse_int_argument(payload, "search_limit", method=method, default=10, minimum=1, maximum=1000) if search_limit_error: return search_limit_error _, scan_limit_error = parse_int_argument(payload, "scan_limit", method=method, default=1000, minimum=1, maximum=5000) if scan_limit_error: return scan_limit_error _, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error string_error = validate_optional_string_arguments( payload, method, ["ref", "kind", "name", "object_type", "object_name", "object_guid", "table", "file_name", "form_guid", "guid", "form", "form_name", "name_filter", "element", "element_name", "command", "attribute", "element_path", "path", "element_id", "id", "property", "query", "prefix", "extension", "extension_guid"], ) if string_error: return string_error return None def validate_metadata_form_element_write_apply_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = FORM_ELEMENT_WRITE_APPLY_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error mode = str(payload.get("execution_mode") or payload.get("mode") or "plan").strip().lower() if mode not in FORM_ELEMENT_WRITE_APPLY_MODES: return invalid_argument(method, "execution_mode", "Unsupported execution mode.", allowed_values=sorted(FORM_ELEMENT_WRITE_APPLY_MODES)) _, include_payload_error = strict_bool_argument(payload, "include_payload", method=method, default=False) if include_payload_error: return include_payload_error for argument in ("allow_sql_saved_state_prepare", "allow_existing_saved_state_target", "auto_prepare_saved_state"): if argument in payload: _, bool_error = strict_bool_argument(payload, argument, method=method, default=False) if bool_error: return bool_error if mode in {"apply", "apply_and_verify", "apply_and_rollback"}: _, allow_apply_error = strict_bool_argument(payload, "allow_sql_saved_state_apply", method=method, default=False) if allow_apply_error: return allow_apply_error if mode == "apply_and_rollback": _, allow_rollback_error = strict_bool_argument(payload, "allow_sql_saved_state_rollback", method=method, default=False) if allow_rollback_error: return allow_rollback_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error return None def validate_metadata_object_property_write_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = OBJECT_PROPERTY_WRITE_METHOD payload, member_error = normalize_object_property_member_payload(payload) if member_error: return member_error base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error selector_error = validate_object_selector_arguments(payload, method) if selector_error: return selector_error if not has_object_selector(payload): return invalid_argument(method, "selector", OBJECT_SELECTOR_REQUIRED_MESSAGE_NO_ORDINAL) string_error = validate_optional_string_arguments( payload, method, [ "ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "member_ref", "child_ref", "member_kind", "member_name", "canonical_path", "extension", "layer", "property", "language", "expected_old", "expected_sha1", "execution_mode", "mode", "summary", ], ) if string_error: return string_error if "property" not in payload or not str(payload.get("property") or "").strip(): return invalid_argument(method, "property", "property is required.", allowed_values=["synonym", "comment"]) if not normalize_object_identity_property(payload.get("property")): raw_property = str(payload.get("property") or "").strip().casefold() message = ( "Object rename is intentionally disabled because reference-safe metadata rename rules are not yet proven." if raw_property in {"name", "имя", "rename", "переименование"} else "Only synonym and comment are supported." ) return invalid_argument(method, "property", message, allowed_values=["synonym", "comment"]) if "value" not in payload or not isinstance(payload.get("value"), str): return invalid_argument(method, "value", "value is required and must be a JSON string; an empty string is allowed.") layer = str(payload.get("layer") or "").strip() if layer and layer not in SAVED_STATE_TABLE_BY_LAYER: return invalid_argument(method, "layer", "layer must be base_saved_state or extension_saved_state.", allowed_values=sorted(SAVED_STATE_TABLE_BY_LAYER)) mode = str(payload.get("execution_mode") or payload.get("mode") or "plan").strip().casefold() if mode not in FORM_ELEMENT_WRITE_APPLY_MODES: return invalid_argument(method, "execution_mode", "Unsupported execution mode.", allowed_values=sorted(FORM_ELEMENT_WRITE_APPLY_MODES)) for argument, default in ( ("allow_saved_state_write", False), ("auto_prepare_saved_state", False), ("allow_sql_saved_state_prepare", False), ("allow_sql_saved_state_apply", False), ("allow_sql_saved_state_rollback", False), ): _, bool_error = strict_bool_argument(payload, argument, method=method, default=default) if bool_error: return bool_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1) if timeout_error: return timeout_error _, include_storage_error = strict_include_storage(payload, method) return include_storage_error def validate_metadata_object_member_add_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = OBJECT_MEMBER_ADD_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments( payload, method, [ "ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", "template_member_ref", "new_member_name", "new_member_synonym", "new_member_comment", "extension", "layer", "expected_sha1", "execution_mode", "mode", "summary", ], ) if string_error: return string_error template_ref = str(payload.get("template_member_ref") or "").strip() if not template_ref: return invalid_argument(method, "template_member_ref", "template_member_ref is required.") normalized, selector_error = normalize_object_property_member_payload({**payload, "member_ref": template_ref}) if selector_error: return selector_error if canonical_nested_member_kind(normalized.get("member_kind")) != "Attribute": return invalid_argument(method, "template_member_ref", "template_member_ref must select an existing Attribute.") new_name = str(payload.get("new_member_name") or "").strip() if not new_name: return invalid_argument(method, "new_member_name", "new_member_name is required.") mode = str(payload.get("execution_mode") or payload.get("mode") or "plan").strip().casefold() if mode not in FORM_ELEMENT_WRITE_APPLY_MODES: return invalid_argument(method, "execution_mode", "Unsupported execution mode.", allowed_values=sorted(FORM_ELEMENT_WRITE_APPLY_MODES)) layer = str(payload.get("layer") or "").strip() if layer and layer not in SAVED_STATE_TABLE_BY_LAYER: return invalid_argument(method, "layer", "layer must be base_saved_state or extension_saved_state.", allowed_values=sorted(SAVED_STATE_TABLE_BY_LAYER)) for argument in ( "allow_saved_state_write", "auto_prepare_saved_state", "allow_sql_saved_state_prepare", "allow_sql_saved_state_apply", "allow_sql_saved_state_rollback", ): _, bool_error = strict_bool_argument(payload, argument, method=method, default=False) if bool_error: return bool_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1) if timeout_error: return timeout_error _, include_storage_error = strict_include_storage(payload, method) return include_storage_error def validate_metadata_form_target_move_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = FORM_TARGET_MOVE_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments( payload, method, [ "ref", "kind", "name", "object_type", "object_name", "object_guid", "table", "file_name", "form_guid", "guid", "form", "form_name", "name_filter", "from", "from_element", "from_path", "from_id", "to", "to_element", "to_path", "to_id", "with", "with_element", "with_path", "with_id", "after_element", "element", "element_path", "path", "expected_sha1", "summary", "execution_mode", "mode", "extension", "extension_guid", ], ) if string_error: return string_error table = str(payload.get("table") or "ConfigCASSave") if table not in FORM_ELEMENT_SAVED_STATE_TABLES: return invalid_argument(method, "table", "Only saved-state tables may be used for target move.", allowed_values=sorted(FORM_ELEMENT_SAVED_STATE_TABLES)) for argument in ("allow_saved_state_write", "include_payload"): _, bool_error = strict_bool_argument(payload, argument, method=method, default=False) if bool_error: return bool_error mode = str(payload.get("execution_mode") or payload.get("mode") or "plan").strip().casefold() if mode not in FORM_ELEMENT_WRITE_APPLY_MODES: return invalid_argument(method, "execution_mode", "Unsupported execution mode.", allowed_values=sorted(FORM_ELEMENT_WRITE_APPLY_MODES)) if mode in {"apply", "apply_and_verify", "apply_and_rollback"}: _, allow_apply_error = strict_bool_argument(payload, "allow_sql_saved_state_apply", method=method, default=False) if allow_apply_error: return allow_apply_error if mode == "apply_and_rollback": _, allow_rollback_error = strict_bool_argument(payload, "allow_sql_saved_state_rollback", method=method, default=False) if allow_rollback_error: return allow_rollback_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) return timeout_error def validate_metadata_form_command_button_write_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = FORM_COMMAND_BUTTON_WRITE_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments( payload, method, [ "extension", "extension_guid", "kind", "object_type", "name", "object_name", "guid", "object_guid", "form", "form_name", "name_filter", "command", "command_name", "command_title", "command_action", "handler", "handler_name", "handler_routine_operation", "handler_routine_text", "routine_operation", "routine_text", "button", "button_name", "button_title", "button_parent", "button_parent_name", "parent", "table", "file_name", "form_guid", "execution_mode", "mode", ], ) if string_error: return string_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error for argument in ("allow_saved_state_write", "include_storage", "include_handler", "allow_sql_saved_state_apply", "allow_sql_saved_state_rollback", "allow_sql_saved_state_prepare", "allow_existing_saved_state_target", "auto_prepare_saved_state"): _, bool_error = strict_bool_argument(payload, argument, method=method, default=False) if bool_error: return bool_error routine_operation = str(payload.get("handler_routine_operation") or payload.get("routine_operation") or "upsert") if routine_operation not in {"replace", "append", "upsert"}: return invalid_argument(method, "handler_routine_operation", "Unsupported handler routine operation.", allowed_values=["replace", "append", "upsert"]) mode = str(payload.get("execution_mode") or payload.get("mode") or "plan").strip().casefold() if mode not in FORM_ELEMENT_WRITE_APPLY_MODES: return invalid_argument(method, "execution_mode", "Unsupported execution mode.", allowed_values=sorted(FORM_ELEMENT_WRITE_APPLY_MODES)) return None def validate_metadata_form_command_button_verify_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = FORM_COMMAND_BUTTON_VERIFY_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments( payload, method, [ "extension", "extension_guid", "kind", "object_type", "name", "object_name", "guid", "object_guid", "form", "form_name", "name_filter", "command", "command_name", "command_title", "command_action", "handler", "handler_name", "button", "button_name", "table", "file_name", "form_guid", "state", "source_state", ], ) if string_error: return string_error _, include_storage_error = strict_include_storage(payload, method) if include_storage_error: return include_storage_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error _, scan_limit_error = parse_int_argument(payload, "scan_limit", method=method, default=5000, minimum=1, maximum=20000) if scan_limit_error: return scan_limit_error if "table" in payload and payload.get("table") not in {None, ""} and str(payload.get("table")) not in STORAGE_TABLES: return invalid_argument(method, "table", "Unsupported storage table.", allowed_values=sorted(STORAGE_TABLES)) return None def validate_metadata_module_write_apply_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = MODULE_WRITE_APPLY_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments( payload, method, [ "module_ref", "module_id", "table", "file_name", "expected_sha1", "expected_text_sha1", "expected_contains", "old", "new", "text", "routine_name", "routine_text", "routine_operation", "operation", "expected_old_sha1", "expected_old_contains", "summary", "execution_mode", "mode", ], ) if string_error: return string_error if "replace" in payload and payload.get("replace") is not None and not isinstance(payload.get("replace"), dict): return invalid_argument(method, "replace", "replace must be a JSON object when provided.") if "routine" in payload and payload.get("routine") is not None and not isinstance(payload.get("routine"), dict): return invalid_argument(method, "routine", "routine must be a JSON object when provided.") routine_operation = str(payload.get("routine_operation") or payload.get("operation") or "replace").strip().casefold() if ("routine_text" in payload or "routine_name" in payload) and routine_operation not in {"replace", "append", "upsert"}: return invalid_argument(method, "routine_operation", "Unsupported routine operation.", allowed_values=["replace", "append", "upsert"]) table = str(payload.get("table") or "") if table and table not in FORM_ELEMENT_SAVED_STATE_TABLES: return invalid_argument(method, "table", "Only saved-state tables may be used for module write.", allowed_values=sorted(FORM_ELEMENT_SAVED_STATE_TABLES)) module_ref = str(payload.get("module_ref") or payload.get("module_id") or "").strip() if module_ref: module_table, module_file_name, module_stream_index = parse_module_id(module_ref) if not module_table or not module_file_name: return invalid_argument(method, "module_ref", "Use module_ref in the form
:#stream:.") if module_table not in FORM_ELEMENT_SAVED_STATE_TABLES: return invalid_argument(method, "module_ref", "Only ConfigSave/ConfigCASSave module refs may be written.", allowed_values=sorted(FORM_ELEMENT_SAVED_STATE_TABLES)) if module_stream_index is None and "stream_index" not in payload: return invalid_argument(method, "stream_index", "Pass stream_index or use module_ref/module_id with #stream:.") elif not payload.get("file_name"): return invalid_argument(method, "module_ref", "Pass module_ref/module_id or table + file_name + stream_index.") if "stream_index" in payload: _, stream_index_error = parse_int_argument(payload, "stream_index", method=method, default=0, minimum=0) if stream_index_error: return stream_index_error if "count" in payload: _, count_error = parse_int_argument(payload, "count", method=method, default=1, minimum=1) if count_error: return count_error for argument in ("allow_saved_state_write", "include_payload", "include_text"): _, bool_error = strict_bool_argument(payload, argument, method=method, default=False) if bool_error: return bool_error mode = str(payload.get("execution_mode") or payload.get("mode") or "plan").strip().casefold() if mode not in FORM_ELEMENT_WRITE_APPLY_MODES: return invalid_argument(method, "execution_mode", "Unsupported execution mode.", allowed_values=sorted(FORM_ELEMENT_WRITE_APPLY_MODES)) if mode in {"apply", "apply_and_verify", "apply_and_rollback"}: _, allow_apply_error = strict_bool_argument(payload, "allow_sql_saved_state_apply", method=method, default=False) if allow_apply_error: return allow_apply_error if mode == "apply_and_rollback": _, allow_rollback_error = strict_bool_argument(payload, "allow_sql_saved_state_rollback", method=method, default=False) if allow_rollback_error: return allow_rollback_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) return timeout_error def validate_metadata_write_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = METADATA_WRITE_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error target = payload.get("target") if target is not None and not isinstance(target, dict): return invalid_argument(method, "target", "target must be a JSON object when provided.") target_dict = target if isinstance(target, dict) else {} route_kind = payload.get("target_kind") or target_dict.get("target_kind") or target_dict.get("area") or payload.get("area") if not route_kind and str(target_dict.get("kind") or "").strip().casefold() in { "form", "форма", "module", "модуль", "bsl", "object", "объект", "metadata", "метаданные", "schedule", "расписание" }: route_kind = target_dict.get("kind") if not route_kind and str(payload.get("kind") or "").strip().casefold() in {"form", "форма", "module", "модуль", "bsl"}: route_kind = payload.get("kind") target_kind = str(route_kind or "form").strip().casefold() if target_kind not in {"form", "форма", "module", "модуль", "bsl", "object", "объект", "metadata", "метаданные", "schedule", "расписание"}: return invalid_argument(method, "target.kind", "Only form, module, object, and scheduled-job schedule saved-state writes are currently routed.", allowed_values=["form", "module", "object", "schedule"]) mode = str(payload.get("execution_mode") or payload.get("mode") or "plan").strip().lower() if mode not in FORM_ELEMENT_WRITE_APPLY_MODES: return invalid_argument(method, "execution_mode", "Unsupported execution mode.", allowed_values=sorted(FORM_ELEMENT_WRITE_APPLY_MODES)) _, include_payload_error = strict_bool_argument(payload, "include_payload", method=method, default=False) if include_payload_error: return include_payload_error if mode in {"apply", "apply_and_verify", "apply_and_rollback"}: _, allow_apply_error = strict_bool_argument(payload, "allow_sql_saved_state_apply", method=method, default=False) if allow_apply_error: return allow_apply_error if mode == "apply_and_rollback": _, allow_rollback_error = strict_bool_argument(payload, "allow_sql_saved_state_rollback", method=method, default=False) if allow_rollback_error: return allow_rollback_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error return None def validate_metadata_write_plan_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = METADATA_WRITE_PLAN_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error target = payload.get("target") if target is not None and not isinstance(target, dict): return invalid_argument(method, "target", "target must be a JSON object when provided.") intent = payload.get("intent") if intent is not None and not isinstance(intent, dict): return invalid_argument(method, "intent", "intent must be a JSON object when provided.") string_error = validate_optional_string_arguments( payload, method, [ "canonical_path", "path", "target_kind", "kind", "area", "operation", "routine_operation", "property", "preferred_layer", "preferred_extension", "extension", "module_ref", "file_name", "form_guid", ], ) if string_error: return string_error _, resolve_origin_error = strict_bool_argument(payload, "resolve_origin", method=method, default=True) if resolve_origin_error: return resolve_origin_error _, origin_max_matches_error = parse_int_argument(payload, "origin_max_matches", method=method, default=20, minimum=1, maximum=100) if origin_max_matches_error: return origin_max_matches_error return None def validate_metadata_write_preflight_payload(payload: dict[str, Any]) -> dict[str, Any] | None: return validate_metadata_write_plan_payload(payload) def validate_metadata_write_learning_payload(payload: dict[str, Any], method: str) -> dict[str, Any] | None: if method in {FORM_WRITE_MATRIX_BUILD_METHOD, FORM_WRITE_MATRIX_SMOKE_METHOD}: base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error table = str(payload.get("table") or "ConfigCASSave") if table not in FORM_ELEMENT_SAVED_STATE_TABLES: return invalid_argument(method, "table", "Only saved-state tables may be used for write matrix.", allowed_values=sorted(FORM_ELEMENT_SAVED_STATE_TABLES)) _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error if method == FORM_WRITE_MATRIX_SMOKE_METHOD: _, max_candidates_error = parse_int_argument(payload, "max_candidates", method=method, default=100, minimum=1, maximum=5000) if max_candidates_error: return max_candidates_error _, allow_apply_error = strict_bool_argument(payload, "allow_sql_saved_state_apply", method=method, default=False) if allow_apply_error: return allow_apply_error _, allow_rollback_error = strict_bool_argument(payload, "allow_sql_saved_state_rollback", method=method, default=False) if allow_rollback_error: return allow_rollback_error return validate_optional_string_arguments( payload, method, ["learning_id", "ref", "kind", "name", "object_type", "object_name", "object_guid", "table", "file_name", "form_guid", "guid", "form", "form_name", "name_filter", "element", "command", "attribute", "element_name", "element_path", "path", "element_id", "id", "property", "value", "extension", "extension_guid"], ) if method in {"metadata.write_learning.capture_before", "metadata.write_learning.capture_after"}: base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error table = str(payload.get("table") or "ConfigCASSave") if table not in FORM_ELEMENT_SAVED_STATE_TABLES: return invalid_argument(method, "table", "Only saved-state tables may be captured for write learning.", allowed_values=sorted(FORM_ELEMENT_SAVED_STATE_TABLES)) _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error return validate_optional_string_arguments( payload, method, ["learning_id", "ref", "kind", "name", "object_type", "object_name", "object_guid", "table", "file_name", "form_guid", "guid", "form", "form_name", "name_filter", "element", "command", "attribute", "element_name", "element_path", "path", "element_id", "id", "property", "value", "extension", "extension_guid"], ) if method == "metadata.write_learning.diff": return validate_optional_string_arguments(payload, method, ["learning_id", "before_snapshot_id", "after_snapshot_id", "before_path", "after_path"]) if method == "metadata.write_learning.infer_rule": _, allow_multiple_error = strict_bool_argument(payload, "allow_multiple", method=method, default=False) if allow_multiple_error: return allow_multiple_error if "diff" in payload and payload.get("diff") is not None and not isinstance(payload.get("diff"), dict): return invalid_argument(method, "diff", "diff must be a JSON object when provided.") return validate_optional_string_arguments(payload, method, ["learning_id", "before_snapshot_id", "after_snapshot_id", "before_path", "after_path", "mode", "base_id"]) return None def validate_metadata_snapshot_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "metadata.snapshot") if isinstance(base_id_or_error, dict): return base_id_or_error _, include_modules_error = strict_bool_argument(payload, "include_modules", method="metadata.snapshot", default=False) if include_modules_error: return include_modules_error if "limit" in payload: return invalid_argument("metadata.snapshot", "limit", "metadata.snapshot does not support limit; use metadata.objects.list for paged object lists.") return None def validate_metadata_kinds_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "metadata.kinds") if isinstance(base_id_or_error, dict): return base_id_or_error if "limit" in payload: return invalid_argument("metadata.kinds", "limit", "metadata.kinds does not support limit; use metadata.objects.list for paged object lists.") if "include_storage" in payload: return invalid_argument("metadata.kinds", "include_storage", "metadata.kinds does not expose storage details.") return None def validate_help_methods_payload(payload: dict[str, Any]) -> dict[str, Any] | None: if "method" in payload and payload.get("method") is not None and not isinstance(payload.get("method"), str): return invalid_argument("help.methods", "method", "method must be a JSON string.") return None def validate_metadata_capabilities_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "metadata.capabilities") if isinstance(base_id_or_error, dict): return base_id_or_error kind_error = validate_optional_string_arguments(payload, "metadata.capabilities", ["kind"]) if kind_error: return kind_error _, include_missing_error = strict_bool_argument(payload, "include_missing", method="metadata.capabilities", default=False) if include_missing_error: return include_missing_error if payload.get("kind"): wanted_kind, requested_public = parse_kind_request(payload.get("kind")) if not any(kind_matches_request(kind, wanted_kind, requested_public) for kind in KIND_CAPABILITIES): return { "schema": "onec_metadata_capabilities.v1", "status": "not_found", "error": "not_found", "base_id": base_id_or_error, "source": {"kind": "live_metadata"}, "query": {"kind": payload.get("kind"), "include_missing": payload.get("include_missing") or False}, "capabilities": [], "counts": {"kinds": 0}, "diagnostics": {"message": "Вид метаданных не найден или не поддерживается адаптером."}, } return None def validate_metadata_adapter_audit_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "metadata.adapter.audit") if isinstance(base_id_or_error, dict): return base_id_or_error if "include_details" in payload: return invalid_argument( "metadata.adapter.audit", "include_details", "metadata.adapter.audit does not support include_details; use include_unmapped=true for additional audit sections.", ) for argument in ("include_missing", "include_unmapped"): _, bool_error = strict_bool_argument(payload, argument, method="metadata.adapter.audit", default=False) if bool_error: return bool_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.adapter.audit", default=60, minimum=1) if timeout_error: return timeout_error return None def validate_extensions_list_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "extensions.list") if isinstance(base_id_or_error, dict): return base_id_or_error _, include_storage_error = strict_include_storage(payload, "extensions.list") if include_storage_error: return include_storage_error if "limit" in payload: _, limit_error = parse_int_argument(payload, "limit", method="extensions.list", default=1, minimum=1) if limit_error: return limit_error if "offset" in payload: _, offset_error = parse_int_argument(payload, "offset", method="extensions.list", default=0, minimum=0) if offset_error: return offset_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="extensions.list", default=30, minimum=1) if timeout_error: return timeout_error return None def validate_schema_tables_list_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "schema.tables.list") if isinstance(base_id_or_error, dict): return base_id_or_error _, include_columns_error = strict_bool_argument(payload, "include_columns", method="schema.tables.list", default=False) if include_columns_error: return include_columns_error _, limit_error = parse_int_argument(payload, "limit", method="schema.tables.list", default=500, minimum=1, maximum=5000) if limit_error: return limit_error like_error = validate_optional_non_empty_string_arguments(payload, "schema.tables.list", ["like"]) if like_error: return like_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="schema.tables.list", default=30, minimum=1) if timeout_error: return timeout_error return require_diagnostic_mode(payload, "schema.tables.list") def validate_storage_files_list_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "storage.files.list") if isinstance(base_id_or_error, dict): return base_id_or_error table_or_error = storage_table(payload, "storage.files.list") if isinstance(table_or_error, dict): return table_or_error _, limit_error = parse_int_argument(payload, "limit", method="storage.files.list", default=200, minimum=1, maximum=5000) if limit_error: return limit_error prefix_error = validate_optional_non_empty_string_arguments(payload, "storage.files.list", ["prefix"]) if prefix_error: return prefix_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="storage.files.list", default=30, minimum=1) if timeout_error: return timeout_error return require_diagnostic_mode(payload, "storage.files.list") def validate_storage_file_get_payload(payload: dict[str, Any], method: str = "storage.file.get") -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error table_or_error = storage_table(payload, method) if isinstance(table_or_error, dict): return table_or_error if method == "storage.file.get": _, include_payload_error = strict_bool_argument(payload, "include_payload", method=method, default=False) if include_payload_error: return include_payload_error if "file_name" in payload and not isinstance(payload.get("file_name"), str): return invalid_argument(method, "file_name", "file_name must be a JSON string.") file_name = str(payload.get("file_name") or "") if not file_name or Path(file_name).name != file_name: return invalid_argument(method, "file_name", "Pass a single safe FileName value from the live SQL storage table.") _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error return require_diagnostic_mode(payload, method) def validate_storage_saved_state_apply_proposal_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "storage.saved_state.apply_proposal" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error _, allow_error = strict_bool_argument(payload, "allow_sql_saved_state_apply", method=method, default=False) if allow_error: return allow_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error proposal = payload.get("proposal") if proposal is not None and not isinstance(proposal, dict): return invalid_argument(method, "proposal", "proposal must be a JSON object.") return None def validate_storage_saved_state_rollback_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "storage.saved_state.rollback" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error _, allow_error = strict_bool_argument(payload, "allow_sql_saved_state_rollback", method=method, default=False) if allow_error: return allow_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error return validate_optional_string_arguments(payload, method, ["backup_id", "backup_path"]) def validate_storage_saved_state_backups_list_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "storage.saved_state.backups.list" if "base_id" in payload: base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments(payload, method, ["table", "file_name"]) if string_error: return string_error _, limit_error = parse_int_argument(payload, "limit", method=method, default=50, minimum=1, maximum=500) if limit_error: return limit_error return None def validate_metadata_dbnames_summary_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "metadata.dbnames.summary") if isinstance(base_id_or_error, dict): return base_id_or_error prefix_error = validate_optional_string_arguments(payload, "metadata.dbnames.summary", ["prefix"]) if prefix_error: return prefix_error _, limit_error = parse_int_argument(payload, "limit", method="metadata.dbnames.summary", default=50, minimum=1, maximum=5000) if limit_error: return limit_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.dbnames.summary", default=30, minimum=1) if timeout_error: return timeout_error return require_diagnostic_mode(payload, "metadata.dbnames.summary") def validate_codec_decode_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "codec.decode") if isinstance(base_id_or_error, dict): return base_id_or_error table_or_error = storage_table(payload, "codec.decode") if isinstance(table_or_error, dict): return table_or_error for argument, default in (("include_text", True), ("include_tree", False)): _, bool_error = strict_bool_argument(payload, argument, method="codec.decode", default=default) if bool_error: return bool_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="codec.decode", default=30, minimum=1) if timeout_error: return timeout_error if "file_name" in payload and not isinstance(payload.get("file_name"), str): return invalid_argument("codec.decode", "file_name", "file_name must be a JSON string.") file_name = str(payload.get("file_name") or "") if not file_name or Path(file_name).name != file_name: return invalid_argument("codec.decode", "file_name", "Pass a single safe FileName value from the live SQL storage table.") return require_diagnostic_mode(payload, "codec.decode") def validate_payload_diff_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "payload.diff" if "base_id" in payload: base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error for argument in ("before", "after"): if not isinstance(payload.get(argument), dict): return invalid_argument(method, argument, f"{argument} must be a JSON object source.") source = payload.get(argument) or {} for source_key in ("payload_base64", "payload_hex", "text", "base_id", "table", "file_name", "encoding"): if source_key in source and source.get(source_key) is not None and not isinstance(source.get(source_key), str): return invalid_argument(method, f"{argument}.{source_key}", f"{argument}.{source_key} must be a JSON string.") for argument, default in (("include_text_diff", True), ("include_tree_diff", True), ("include_evidence", True)): _, bool_error = strict_bool_argument(payload, argument, method=method, default=default) if bool_error: return bool_error for argument, default, minimum, maximum in ( ("timeout_seconds", 30, 1, None), ("max_changes", 200, 1, 5000), ("max_text_diff_lines", 200, 0, 5000), ): _, int_error = parse_int_argument(payload, argument, method=method, default=default, minimum=minimum, maximum=maximum) if int_error: return int_error return require_diagnostic_mode(payload, method) def validate_codec_encode_payload(payload: dict[str, Any]) -> dict[str, Any] | None: _, include_payload_error = strict_bool_argument(payload, "include_payload", method="codec.encode", default=False) if include_payload_error: return include_payload_error if "text" in payload and not isinstance(payload.get("text"), str): return invalid_argument("codec.encode", "text", "text must be a JSON string.") if "source" in payload and payload.get("source") is not None and not isinstance(payload.get("source"), dict): return invalid_argument("codec.encode", "source", "source must be a JSON object.") _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="codec.encode", default=30, minimum=1) if timeout_error: return timeout_error return require_diagnostic_mode(payload, "codec.encode") def validate_query_validate_payload(payload: dict[str, Any], method: str = "query.validate") -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error if "query" not in payload: return invalid_argument(method, "query", "query is required and must be a non-empty JSON string.") if not isinstance(payload.get("query"), str): return invalid_argument(method, "query", "query must be a JSON string.") if not str(payload.get("query") or "").strip(): return invalid_argument(method, "query", "query is required and must be a non-empty JSON string.") _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error return None def validate_query_run_payload(payload: dict[str, Any]) -> dict[str, Any] | None: query_error = validate_query_validate_payload(payload, "query.run") if query_error: return query_error _, limit_error = parse_int_argument(payload, "limit", method="query.run", default=100, minimum=1, maximum=1000) if limit_error: return limit_error return require_diagnostic_mode(payload, "query.run") def validate_object_ordinal_exists_for_job(method: str, payload: dict[str, Any]) -> dict[str, Any] | None: ordinal_value = first_non_empty_arg(payload, "ordinal", "index", "object_index") if ordinal_value in {None, ""}: return None base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error table_or_error = metadata_storage_table(payload, method) if isinstance(table_or_error, dict): return table_or_error table = str(payload.get("table") or "Config") kind = canonical_kind(str(payload.get("kind") or "")) if payload.get("kind") else None ordinal, ordinal_error = parse_ordinal(ordinal_value, method) if ordinal_error: return ordinal_error if not kind: return { "schema": "onec_adapter_request_error.v1", "method": method, "status": "error", "error": "kind_required", "diagnostics": {"message": "kind is required when selecting an object by ordinal."}, } page = list_objects(kind, base_id=base_id_or_error, limit=1, offset=int(ordinal or 1) - 1, include_storage=False, table=table) if page.get("status") != "ok" or not page.get("objects"): return { "schema": page.get("schema") or "onec_adapter_request_error.v1", "method": method, "status": "not_found", "error": "not_found", "base_id": base_id_or_error, "counts": page.get("counts"), "diagnostics": {"message": f"Object ordinal {ordinal} was not found for kind {kind}."}, } return None def validate_metadata_cache_status_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "metadata.cache.status") if isinstance(base_id_or_error, dict): return base_id_or_error _, include_samples_error = strict_bool_argument(payload, "include_samples", method="metadata.cache.status", default=False) if include_samples_error: return include_samples_error return None def validate_metadata_cache_lookup_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "metadata.cache.lookup" payload = normalize_object_selector_aliases(payload, method) if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error for argument in ("guid", "kind", "name"): if argument not in payload: continue value = payload.get(argument) if value is None or value == "": return invalid_argument("metadata.cache.lookup", argument, f"{argument} must be a non-empty JSON string when provided.") if not isinstance(value, str): return invalid_argument("metadata.cache.lookup", argument, f"{argument} must be a JSON string.") return None def validate_metadata_module_owner_cache_prune_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "metadata.module_owner_cache.prune" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments( payload, method, [ "owner_guid", "module_ref", "ref", "kind", "name", "guid", "object_type", "object_name", "object_guid", ], ) if string_error: return string_error owner_guid = str(payload.get("owner_guid") or "").strip() if owner_guid and not is_guid_text(owner_guid): return invalid_argument(method, "owner_guid", "owner_guid must be a valid GUID JSON string.") module_refs = payload.get("module_refs") if module_refs is not None and ( not isinstance(module_refs, list) or any(not isinstance(item, str) or not item.strip() for item in module_refs) ): return invalid_argument(method, "module_refs", "module_refs must be an array of non-empty strings.") if not owner_guid and not str(payload.get("module_ref") or "").strip() and not module_refs and not has_object_selector(payload): return invalid_argument( method, "selector", "Pass a 1C owner selector (ref or kind/name), owner_guid, module_ref, or module_refs.", ) for argument, default in (("dry_run", False), ("include_storage", False)): _, bool_error = strict_bool_argument(payload, argument, method=method, default=default) if bool_error: return bool_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1, maximum=300) return timeout_error def validate_semantic_cache_search_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "semantic.cache.search" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments(payload, method, ["query", "kind", "object_kind"]) if string_error: return string_error if payload.get("query_embedding") is not None and not isinstance(payload.get("query_embedding"), list): return invalid_argument(method, "query_embedding", "query_embedding must be a JSON array of numbers.") if not str(payload.get("query") or "").strip() and payload.get("query_embedding") is None: return invalid_argument(method, "query", "Pass query text or query_embedding.") _, include_vectors_error = strict_bool_argument(payload, "include_vectors", method=method, default=False) if include_vectors_error: return include_vectors_error _, validate_candidates_error = strict_bool_argument(payload, "validate_candidates", method=method, default=False) if validate_candidates_error: return validate_candidates_error _, limit_error = parse_int_alias_argument(payload, "limit", "max_matches", method=method, default=20, minimum=1, maximum=200) if limit_error: return limit_error _, scan_limit_error = parse_int_argument(payload, "scan_limit", method=method, default=1000, minimum=1, maximum=10000) if scan_limit_error: return scan_limit_error _, validation_limit_error = parse_int_argument(payload, "validation_limit", method=method, default=20, minimum=1, maximum=200) if validation_limit_error: return validation_limit_error _, validation_timeout_error = parse_int_argument(payload, "validation_timeout_seconds", method=method, default=30, minimum=1, maximum=300) if validation_timeout_error: return validation_timeout_error return None def validate_semantic_cache_pending_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "semantic.cache.pending" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments(payload, method, ["kind", "object_kind", "vector_status"]) if string_error: return string_error _, limit_error = parse_int_argument(payload, "limit", method=method, default=100, minimum=1, maximum=1000) if limit_error: return limit_error status_filter = str(payload.get("vector_status") or "pending_embedding").strip() if status_filter not in {"pending_embedding", "embedded", "error", "all"}: return invalid_argument(method, "vector_status", "vector_status must be one of: pending_embedding, embedded, error, all.") return None def validate_semantic_cache_status_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "semantic.cache.status" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments(payload, method, ["kind", "object_kind"]) if string_error: return string_error _, include_entries_error = strict_bool_argument(payload, "include_entries", method=method, default=False) if include_entries_error: return include_entries_error _, limit_error = parse_int_argument(payload, "limit", method=method, default=50, minimum=1, maximum=1000) if limit_error: return limit_error return None def validate_semantic_cache_validate_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "semantic.cache.validate" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error value = payload.get("document_id") if value is None or value == "": return invalid_argument(method, "document_id", "document_id is required.") if not isinstance(value, str): return invalid_argument(method, "document_id", "document_id must be a JSON string.") _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1, maximum=300) if timeout_error: return timeout_error return None def validate_semantic_cache_validate_batch_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "semantic.cache.validate_batch" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments(payload, method, ["kind", "object_kind", "vector_status"]) if string_error: return string_error ids_value = payload.get("document_ids") if ids_value is not None: if not isinstance(ids_value, list): return invalid_argument(method, "document_ids", "document_ids must be a JSON array of strings.") if not ids_value: return invalid_argument(method, "document_ids", "document_ids must not be empty when provided.") for item in ids_value: if not isinstance(item, str): return invalid_argument(method, "document_ids", "document_ids must contain only strings.") status_filter = str(payload.get("vector_status") or "all").strip() if status_filter not in {"pending_embedding", "embedded", "error", "all"}: return invalid_argument(method, "vector_status", "vector_status must be one of: pending_embedding, embedded, error, all.") _, limit_error = parse_int_argument(payload, "limit", method=method, default=100, minimum=1, maximum=1000) if limit_error: return limit_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1, maximum=300) if timeout_error: return timeout_error return None def validate_semantic_cache_refresh_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "semantic.cache.refresh" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error value = payload.get("document_id") if value is None or value == "": return invalid_argument(method, "document_id", "document_id is required.") if not isinstance(value, str): return invalid_argument(method, "document_id", "document_id must be a JSON string.") _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=60, minimum=1, maximum=300) if timeout_error: return timeout_error return None def validate_semantic_cache_rebuild_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "semantic.cache.rebuild" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments(payload, method, ["extension", "kind", "object_kind", "object_type"]) if string_error: return string_error object_kind = canonical_kind(str(payload.get("kind") or payload.get("object_kind") or payload.get("object_type") or "Template")) if object_kind != "Template": return invalid_argument(method, "kind", "semantic.cache.rebuild currently supports kind=Template only.") _, limit_error = parse_int_argument(payload, "limit", method=method, default=100, minimum=1, maximum=5000) if limit_error: return limit_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=120, minimum=1, maximum=600) if timeout_error: return timeout_error for argument, default in (("refresh_routes", False), ("include_entries", False)): _, bool_error = strict_bool_argument(payload, argument, method=method, default=default) if bool_error: return bool_error return None def validate_semantic_cache_embedding_upsert_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "semantic.cache.embedding.upsert" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error for argument in ("document_id", "content_sha1", "embedding_model"): value = payload.get(argument) if value is None or value == "": return invalid_argument(method, argument, f"{argument} is required.") if not isinstance(value, str): return invalid_argument(method, argument, f"{argument} must be a JSON string.") if payload.get("embedding") is None: return invalid_argument(method, "embedding", "embedding is required.") if not isinstance(payload.get("embedding"), list): return invalid_argument(method, "embedding", "embedding must be a JSON array of numbers.") if numeric_vector(payload.get("embedding")) is None: return invalid_argument(method, "embedding", "embedding must be a non-empty JSON array of numbers.") return None def validate_metadata_object_commands_payload(payload: dict[str, Any]) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, "metadata.object.commands") if isinstance(base_id_or_error, dict): return base_id_or_error selector_error = validate_object_selector_arguments(payload, "metadata.object.commands") if selector_error: return selector_error _, requested_command_error = optional_string_filter(payload, ["command", "name_filter"], method="metadata.object.commands") if requested_command_error: return requested_command_error _, include_storage_error = strict_include_storage(payload, "metadata.object.commands") if include_storage_error: return include_storage_error table_or_error = metadata_storage_table(payload, "metadata.object.commands") if isinstance(table_or_error, dict): return table_or_error for argument, default in (("include_form_commands", True), ("refresh_cache", False)): _, bool_error = strict_bool_argument(payload, argument, method="metadata.object.commands", default=default) if bool_error: return bool_error for argument, default, maximum in ( ("max_forms", 20, 100), ("max_items", 200, 5000), ("max_form_items", 200, 5000), ("max_attributes", 100, 5000), ("max_commands", 200, 5000), ("limit", 20, 5000), ): _, int_error = parse_int_argument(payload, argument, method="metadata.object.commands", default=default, minimum=1, maximum=maximum) if int_error: return int_error ordinal_error = validate_explicit_ordinal_arguments(payload, "metadata.object.commands") if ordinal_error: return ordinal_error _, view_error = parse_view_argument(payload, "metadata.object.commands") if view_error: return view_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.commands", default=60, minimum=1) if timeout_error: return timeout_error return None def validate_metadata_code_index_payload(payload: dict[str, Any], method: str) -> dict[str, Any] | None: base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_args = ["query", "pattern", "mode", "table", "prefix", "module_ref", "module_id", "ref", "object_type", "object_name", "object_guid", "kind", "name", "guid", "extension_guid"] string_error = validate_optional_string_arguments(payload, method, string_args) if string_error: return string_error extension_guid = str(payload.get("extension_guid") or "").strip().lower() if "extension_guid" in payload and not is_guid_text(extension_guid): return invalid_argument(method, "extension_guid", "extension_guid must be a GUID string.") if payload.get("query_embedding") is not None and not isinstance(payload.get("query_embedding"), list): return invalid_argument(method, "query_embedding", "query_embedding must be a JSON array of numbers.") for argument, default, minimum, maximum in ( ("limit", 20, 1, 500), ("max_matches", 20, 1, 500), ("scan_limit", 1000, 1, 50000), ("max_items", 500, 1, 50000), ("timeout_seconds", 60, 1, 600), ): if argument in payload: _, int_error = parse_int_argument(payload, argument, method=method, default=default, minimum=minimum, maximum=maximum) if int_error: return int_error for argument in ("verify", "include_vectors"): if argument in payload: _, bool_error = strict_bool_argument(payload, argument, method=method, default=False) if bool_error: return bool_error return None def validate_metadata_write_history_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = "metadata.write.history" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error _, limit_error = parse_int_argument(payload, "limit", method=method, default=20, minimum=1, maximum=200) if limit_error: return limit_error _, include_summary_error = strict_bool_argument(payload, "include_summary", method=method, default=False) if include_summary_error: return include_summary_error string_error = validate_optional_string_arguments(payload, method, ["operation_id", "operation_method", "write_method", "status", "routed_method", "backup_id"]) if string_error: return string_error return None def validate_metadata_write_rollback_payload(payload: dict[str, Any]) -> dict[str, Any] | None: method = METADATA_WRITE_ROLLBACK_METHOD base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments(payload, method, ["operation_id", "backup_id"]) if string_error: return string_error _, allow_error = strict_bool_argument(payload, "allow_sql_saved_state_rollback", method=method, default=False) if allow_error: return allow_error _, timeout_error = parse_int_argument(payload, "timeout_seconds", method=method, default=30, minimum=1) if timeout_error: return timeout_error return None def validate_adapter_job_payload(method: str, job_payload: dict[str, Any]) -> dict[str, Any] | None: if not isinstance(job_payload, dict): return invalid_argument("adapter.job.start", "payload", "payload must be a JSON object.") source_policy_error = validate_sql_only_runtime_payload(method, job_payload) if source_policy_error: return source_policy_error if "base_id" in job_payload and job_payload.get("base_id") is not None and not isinstance(job_payload.get("base_id"), str): return invalid_argument(method, "base_id", "base_id must be a JSON string.") if method in OBJECT_SELECTOR_ALIAS_METHODS: template_view_method = method in {"templates.read", "templates.analyze", "templates.map"} selector_argument_error = validate_object_selector_arguments(job_payload, method, include_view=not template_view_method) if selector_argument_error: return selector_argument_error normalized_payload = normalize_selector_payload_for_method(job_payload, method) if isinstance(normalized_payload, dict) and normalized_payload.get("status") == "invalid_argument": return normalized_payload if normalized_payload is not job_payload: job_payload.clear() job_payload.update(normalized_payload) selector_error = validate_optional_string_arguments(job_payload, method, ["view"]) if selector_error: return selector_error if method in {"metadata.object.attributes", "metadata.object.full"} and not has_object_selector(job_payload): return invalid_argument(method, "selector", OBJECT_SELECTOR_REQUIRED_MESSAGE) ordinal_exists_error = validate_object_ordinal_exists_for_job(method, job_payload) if ordinal_exists_error: return ordinal_exists_error if method == "help.methods": return validate_help_methods_payload(job_payload) if method == "repository.layer.connection.set": base_id_or_error = require_base_id(job_payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error string_error = validate_optional_string_arguments(job_payload, method, ["layer_id", "extension_guid", "connection_state", "repository_user"]) if string_error: return string_error _, confirmation_error = strict_bool_argument(job_payload, "confirm_repository_connection_change", method=method, default=False) return confirmation_error if method == "repository.layers.audit": base_id_or_error = require_base_id(job_payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error _, timeout_error = parse_int_argument(job_payload, "timeout_seconds", method=method, default=30, minimum=1, maximum=600) return timeout_error if method in repository_control.METHODS: base_required = method not in {repository_control.METHOD_LOCK_REQUEST_STATUS, repository_control.METHOD_LOCK_REQUEST_CANCEL, repository_control.METHOD_VERIFY, repository_control.METHOD_CLOSE, repository_control.METHOD_UNLOCK, repository_control.METHOD_COMMIT_PLAN, repository_control.METHOD_COMMIT} if base_required: base_id_or_error = require_base_id(job_payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error return None if method == "metadata.kinds": return validate_metadata_kinds_payload(job_payload) if method == "metadata.capabilities": return validate_metadata_capabilities_payload(job_payload) if method == "metadata.adapter.audit": return validate_metadata_adapter_audit_payload(job_payload) if method == "metadata.object.full": return validate_metadata_object_full_payload(job_payload) if method == "metadata.object.attributes": _, validation_error = validate_metadata_object_attributes_payload(job_payload) return validation_error if method == "metadata.write.history": return validate_metadata_write_history_payload(job_payload) if method == METADATA_WRITE_ROLLBACK_METHOD: return validate_metadata_write_rollback_payload(job_payload) if method == SAVED_STATE_STATUS_METHOD: return validate_metadata_saved_state_status_payload(job_payload) if method == SAVED_STATE_DIFF_METHOD: return validate_metadata_saved_state_diff_payload(job_payload) if method == SAVED_STATE_CHANGES_LIST_METHOD: return validate_metadata_saved_state_changes_list_payload(job_payload) if method == "metadata.object.special.details": return validate_metadata_object_special_details_payload(job_payload) if method == "metadata.cache.rebuild": return validate_metadata_cache_rebuild_payload(job_payload) if method == "metadata.objects.list": if job_payload.get("extension") not in {None, ""}: return metadata_objects_list_extension_not_supported(job_payload) if "query" in job_payload: return invalid_argument("metadata.objects.list", "query", "metadata.objects.list does not use query; pass name_filter/name_contains or kind/name_filter.") string_error = validate_optional_string_arguments(job_payload, "metadata.objects.list", ["name_filter", "name_contains", "extension"]) if string_error: return string_error for argument in ("include_storage", "include_missing", "only_missing", "exact_counts", "refresh_cache"): _, bool_error = strict_bool_argument(job_payload, argument, method="metadata.objects.list", default=False) if bool_error: return bool_error _, limit_error = parse_int_argument(job_payload, "limit", method="metadata.objects.list", default=200, minimum=1) if limit_error: return limit_error _, offset_error = parse_int_argument(job_payload, "offset", method="metadata.objects.list", default=0, minimum=0) if offset_error: return offset_error return None if method == "metadata.snapshot": return validate_metadata_snapshot_payload(job_payload) if method == "metadata.cache.status": return validate_metadata_cache_status_payload(job_payload) if method == "metadata.cache.lookup": return validate_metadata_cache_lookup_payload(job_payload) if method == "semantic.cache.search": return validate_semantic_cache_search_payload(job_payload) if method == "semantic.cache.status": return validate_semantic_cache_status_payload(job_payload) if method == "semantic.cache.validate": return validate_semantic_cache_validate_payload(job_payload) if method == "semantic.cache.validate_batch": return validate_semantic_cache_validate_batch_payload(job_payload) if method == "semantic.cache.refresh": return validate_semantic_cache_refresh_payload(job_payload) if method == "semantic.cache.rebuild": return validate_semantic_cache_rebuild_payload(job_payload) if method == "semantic.cache.pending": return validate_semantic_cache_pending_payload(job_payload) if method == "semantic.cache.embedding.upsert": return validate_semantic_cache_embedding_upsert_payload(job_payload) if method in CODE_INDEX_METHODS: return validate_metadata_code_index_payload(job_payload, method) if method == "metadata.object.get": return validate_metadata_object_get_payload(job_payload) if method == "metadata.object.decode": return validate_metadata_object_decode_payload(job_payload) if method == "metadata.object.parts": return validate_metadata_object_parts_payload(job_payload) if method == "metadata.object.modules": return validate_metadata_object_modules_payload(job_payload) if method == "metadata.object.related": return validate_metadata_object_related_payload(job_payload) if method == "metadata.object.forms": return validate_metadata_object_forms_payload(job_payload) if method == "metadata.object.form.details": return validate_metadata_object_form_details_payload(job_payload) if method == "metadata.object.templates": return validate_metadata_object_templates_payload(job_payload) if method == "metadata.object.template.details": return validate_metadata_object_template_details_payload(job_payload) if method in {"templates.read", "templates.analyze", "templates.map"}: return validate_templates_read_payload(job_payload, method) if method == "templates.areas.find": return validate_templates_areas_find_payload(job_payload) if method == "metadata.object.commands": return validate_metadata_object_commands_payload(job_payload) if method == "metadata.definition.find": return validate_metadata_definition_find_payload(job_payload) if method == "metadata.route.resolve": return validate_extension_objects_find_payload(job_payload, method) if method == "metadata.form.decode": return validate_metadata_form_decode_payload(job_payload) if method == FORM_OWNER_INDEX_BUILD_METHOD: return validate_metadata_form_owner_index_build_payload(job_payload) if method == "metadata.form.write_target.resolve": return validate_metadata_form_write_target_resolve_payload(job_payload) if method == FORM_WRITE_TARGET_VERIFY_METHOD: return validate_metadata_form_write_target_resolve_payload(job_payload, method=FORM_WRITE_TARGET_VERIFY_METHOD) if method in {FORM_WRITE_MATRIX_BUILD_METHOD, FORM_WRITE_MATRIX_SMOKE_METHOD}: return validate_metadata_write_learning_payload(job_payload, method) if method == "metadata.saved_state.forms.search": return validate_metadata_saved_state_forms_search_payload(job_payload) if method == "metadata.saved_state.prepare": return validate_metadata_saved_state_prepare_payload(job_payload) if method == SAVED_STATE_MODULES_SEARCH_METHOD: return validate_metadata_saved_state_modules_search_payload(job_payload) if method == "metadata.form.element.write_apply": return validate_metadata_form_element_write_apply_payload(job_payload) if method == OBJECT_PROPERTY_WRITE_METHOD: return validate_metadata_object_property_write_payload(job_payload) if method == OBJECT_MEMBER_ADD_METHOD: return validate_metadata_object_member_add_payload(job_payload) if method == FORM_TARGET_MOVE_METHOD: return validate_metadata_form_target_move_payload(job_payload) if method == FORM_COMMAND_BUTTON_WRITE_METHOD: return validate_metadata_form_command_button_write_payload(job_payload) if method == FORM_COMMAND_BUTTON_VERIFY_METHOD: return validate_metadata_form_command_button_verify_payload(job_payload) if method == MODULE_WRITE_APPLY_METHOD: return validate_metadata_module_write_apply_payload(job_payload) if method == METADATA_WRITE_PLAN_METHOD: return validate_metadata_write_plan_payload(job_payload) if method == METADATA_WRITE_PREFLIGHT_METHOD: return validate_metadata_write_preflight_payload(job_payload) if method == METADATA_WRITE_METHOD: return validate_metadata_write_payload(job_payload) if method in WRITE_LEARNING_METHODS: return validate_metadata_write_learning_payload(job_payload, method) if method == "metadata.cache.invalidate": base_id_or_error = require_base_id(job_payload, "metadata.cache.invalidate") if isinstance(base_id_or_error, dict): return base_id_or_error _, dry_run_error = strict_bool_argument(job_payload, "dry_run", method="metadata.cache.invalidate", default=False) return dry_run_error if method == "metadata.module_owner_cache.prune": return validate_metadata_module_owner_cache_prune_payload(job_payload) if method == "metadata.resolve_overrides": return validate_metadata_resolve_overrides_payload(job_payload) if method == "code.search": return validate_code_search_payload(job_payload) if method == "code.read": return validate_code_read_payload(job_payload) if method == CODE_WRITE_METHOD: return validate_code_write_payload(job_payload) if method == "templates.bindings": return validate_templates_bindings_payload(job_payload) if method == "diagnostics.call_chain": return validate_diagnostics_call_chain_payload(job_payload) if method == "extensions.list": return validate_extensions_list_payload(job_payload) if method == "extension.cache.status": return validate_extension_cache_status_payload(job_payload, method) if method == "extension.cache.rebuild": return validate_extension_cache_rebuild_payload(job_payload, method) if method == "extension.cache.validate": return validate_extension_cache_validate_payload(job_payload, method) if method == "extension.objects.find": return validate_extension_objects_find_payload(job_payload, method) if method == "schema.tables.list": return validate_schema_tables_list_payload(job_payload) if method == "storage.files.list": return validate_storage_files_list_payload(job_payload) if method == "storage.file.get": return validate_storage_file_get_payload(job_payload) if method == "storage.saved_state.apply_proposal": return validate_storage_saved_state_apply_proposal_payload(job_payload) if method == "storage.saved_state.rollback": return validate_storage_saved_state_rollback_payload(job_payload) if method == "storage.saved_state.backups.list": return validate_storage_saved_state_backups_list_payload(job_payload) if method == "metadata.dbnames.summary": return validate_metadata_dbnames_summary_payload(job_payload) if method == "payload.diff": return validate_payload_diff_payload(job_payload) if method == "codec.decode": return validate_codec_decode_payload(job_payload) if method == "codec.encode": return validate_codec_encode_payload(job_payload) if method == "query.validate": return validate_query_validate_payload(job_payload) if method == "query.run": return validate_query_run_payload(job_payload) if method == "modules.read": base_id_or_error = require_base_id(job_payload, "modules.read") if isinstance(base_id_or_error, dict): return base_id_or_error return validate_modules_read_arguments(job_payload) if method == "modules.search": return validate_modules_search_payload(job_payload) if method == "changes.propose": return validate_changes_propose_payload(job_payload) return None def adapter_start_job(payload: dict[str, Any]) -> dict[str, Any]: raw_method = payload.get("method") if raw_method is not None and not isinstance(raw_method, str): return invalid_argument("adapter.job.start", "method", "method must be a JSON string.") method = str(raw_method or "").strip() if "payload" in payload and not isinstance(payload.get("payload"), dict): return invalid_argument("adapter.job.start", "payload", "payload must be a JSON object.") job_payload = payload.get("payload") or {} if not method: return invalid_argument("adapter.job.start", "method", "payload.method is required.") if method.startswith("adapter.job."): return adapter_public_error("adapter.job.start", "unsupported_method", {"message": "adapter job methods cannot be nested"}) known_methods = set(adapter_method_registry()) if method not in known_methods: return invalid_argument("adapter.job.start", "method", "Unsupported adapter method.", allowed_values=sorted(known_methods)) requested_timeout, timeout_error = parse_int_argument(payload, "timeout_seconds", method="adapter.job.start", default=0, minimum=1) if timeout_error: return timeout_error preflight_error = validate_adapter_job_payload(method, job_payload) if preflight_error: return preflight_error early_result = adapter_long_method_card_preflight(method, job_payload) if early_result is not None: return early_result adapter_cleanup_jobs() job_id = uuid.uuid4().hex now = adapter_now() timeout_seconds = float(requested_timeout) if requested_timeout else adapter_job_timeout_seconds(job_payload, method=method) with ADAPTER_JOB_LOCK: ADAPTER_JOBS[job_id] = { "schema": "onec_adapter_job.v1", "status": "queued", "job_id": job_id, "method": method, "base_id": job_payload.get("base_id"), "adapter_instance_id": ADAPTER_INSTANCE_ID, "created_at": now, "updated_at": now, "timeout_seconds": timeout_seconds, "progress": {"current_step": "queued", "completed_steps": 0, "total_steps": None, "percent": 0}, } adapter_save_jobs_to_store() def worker() -> None: stop_heartbeat = threading.Event() heartbeat_thread = threading.Thread(target=adapter_job_heartbeat, args=(job_id, stop_heartbeat), name=f"onec-adapter-heartbeat-{job_id[:8]}", daemon=True) heartbeat_thread.start() adapter_job_set(job_id, status="running", started_at=adapter_now(), progress={"current_step": "running", "completed_steps": 0, "total_steps": None, "percent": 0}) try: if adapter_job_process_isolation_enabled(method): adapter_run_job_in_process(job_id, method, job_payload, timeout_seconds) return if method == "metadata.object.full": adapter_run_metadata_object_full_job(job_id, job_payload, timeout_seconds) return if method == "metadata.object.attributes": adapter_run_metadata_object_attributes_job(job_id, job_payload, timeout_seconds) return if method == "metadata.object.special.details" and canonical_kind(str(job_payload.get("kind") or "")) == "DocumentJournal": adapter_run_document_journal_special_job(job_id, job_payload, timeout_seconds) return if method == "metadata.cache.rebuild": adapter_run_metadata_cache_rebuild_job(job_id, job_payload, timeout_seconds) return result = call_method_impl(method, job_payload) if adapter_job_cancel_requested(job_id): adapter_job_finish(job_id, "cancelled", result={"status": "cancelled", "method": method}) return adapter_job_finish(job_id, "done", result=result, progress={"current_step": "done", "completed_steps": 1, "total_steps": 1, "percent": 100}) except Exception as exc: adapter_job_finish(job_id, "error", **adapter_public_error(method, "adapter_job_exception", {"message": str(exc), "traceback": traceback.format_exc(limit=8)})) finally: stop_heartbeat.set() heartbeat_thread.join(timeout=0.2) def watchdog() -> None: time.sleep(timeout_seconds) with ADAPTER_JOB_LOCK: job = ADAPTER_JOBS.get(job_id) if not job or job.get("status") not in {"queued", "running"}: return current_step = ((job.get("progress") or {}).get("current_step") or job.get("current_step") or "running") running_steps = (job.get("progress") or {}).get("running_steps") or [current_step] queued_steps = (job.get("progress") or {}).get("queued_steps") or [] partial = job.get("partial_result") if isinstance(partial, dict): partial["status"] = "partial" timings = partial.get("section_timings") or {} for section in running_steps: section_name = str(section or "").strip() if not section_name or section_name == "running_sections": continue partial.setdefault("sections", {})[section_name] = "failed" section_method = (timings.get(section_name) or {}).get("method") or method if not any(item.get("section") == section_name for item in partial.get("failed_sections") or []): partial.setdefault("failed_sections", []).append( {"section": section_name, "method": section_method, "status": "timeout", "diagnostics": {"message": f"Adapter job timeout after {timeout_seconds:.0f} seconds"}} ) for section in queued_steps: section_name = str(section or "").strip() if not section_name: continue partial.setdefault("sections", {})[section_name] = "not_started_due_to_job_timeout" section_method = (timings.get(section_name) or {}).get("method") or method if not any(item.get("section") == section_name for item in partial.get("failed_sections") or []): partial.setdefault("failed_sections", []).append( {"section": section_name, "method": section_method, "status": "not_started_due_to_job_timeout", "diagnostics": {"message": f"Adapter job timeout after {timeout_seconds:.0f} seconds before section start"}} ) job["partial_result"] = partial job["result"] = partial job.update(adapter_public_error(method, "job_timeout", {"message": f"Adapter job timeout after {timeout_seconds:.0f} seconds", "running_steps": running_steps})) job["status"] = "error" job["finished_at"] = adapter_now() job["updated_at"] = job["finished_at"] adapter_save_jobs_to_store() threading.Thread(target=worker, name=f"onec-adapter-job-{job_id[:8]}", daemon=True).start() if not adapter_job_process_isolation_enabled(method): threading.Thread(target=watchdog, name=f"onec-adapter-timeout-{job_id[:8]}", daemon=True).start() return { "schema": "onec_adapter_job.v1", "status": "accepted", "job_id": job_id, "method": method, "base_id": job_payload.get("base_id"), "timeout_seconds": timeout_seconds, "poll": {"method": "adapter.job.get", "payload": {"job_id": job_id}}, "cancel": {"method": "adapter.job.cancel", "payload": {"job_id": job_id}}, } def adapter_get_job(payload: dict[str, Any]) -> dict[str, Any]: adapter_load_jobs_from_store() raw_job_id = payload.get("job_id") if raw_job_id is not None and not isinstance(raw_job_id, str): return invalid_argument("adapter.job.get", "job_id", "job_id must be a JSON string.") consume, consume_error = strict_bool_argument(payload, "consume", method="adapter.job.get", default=False) if consume_error: return consume_error include_partial_result, include_partial_result_error = strict_bool_argument(payload, "include_partial_result", method="adapter.job.get", default=False) if include_partial_result_error: return include_partial_result_error job_id = str(raw_job_id or "").strip() if not job_id: return invalid_argument("adapter.job.get", "job_id", "payload.job_id is required.") adapter_cleanup_jobs() with ADAPTER_JOB_LOCK: job = dict(ADAPTER_JOBS.get(job_id) or {}) if consume and job.get("status") in {"done", "error", "cancelled", "not_found"}: ADAPTER_JOBS.pop(job_id, None) adapter_save_jobs_to_store() if not job: return { "schema": "onec_adapter_job.v1", "status": "not_found", "error": "not_found", "job_id": job_id, "diagnostics": {"message": "Job was not found. It may have expired or the adapter was restarted."}, } if job.get("status") == "done" and "result" in job and not include_partial_result: job.pop("partial_result", None) return job def adapter_cancel_job(payload: dict[str, Any]) -> dict[str, Any]: adapter_load_jobs_from_store() raw_job_id = payload.get("job_id") if raw_job_id is not None and not isinstance(raw_job_id, str): return invalid_argument("adapter.job.cancel", "job_id", "job_id must be a JSON string.") job_id = str(raw_job_id or "").strip() if not job_id: return invalid_argument("adapter.job.cancel", "job_id", "payload.job_id is required.") adapter_cleanup_jobs() with ADAPTER_JOB_LOCK: job = ADAPTER_JOBS.get(job_id) if not job: return { "schema": "onec_adapter_job.v1", "status": "not_found", "error": "not_found", "job_id": job_id, "diagnostics": {"message": "Job was not found. It may have expired or the adapter was restarted."}, } if job.get("status") in {"done", "error", "cancelled"}: return dict(job) job["cancel_requested"] = True job["status"] = "cancelled" job["finished_at"] = adapter_now() job["updated_at"] = job["finished_at"] isolated_process = adapter_job_process_isolation_enabled(str(job.get("method") or "")) job["diagnostics"] = { "message": ( "Cancellation requested. The isolated worker process will be terminated." if isolated_process else "Cancellation requested. A running in-process section may finish in the background, but this adapter job will remain cancelled." ), "termination": "hard_process" if isolated_process else "cooperative", } if job.get("method") == "metadata.object.attributes" and isinstance(job.get("partial_result"), dict): partial = dict(job.get("partial_result") or {}) progress = adapter_mark_attributes_cancelled(partial, started_at=float(job.get("started_at") or job.get("created_at") or adapter_now())) job["partial_result"] = partial job["result"] = partial job["progress"] = progress job["current_step"] = "cancelled" adapter_save_jobs_to_store() return dict(job) def metadata_object_attributes(payload: dict[str, Any]) -> dict[str, Any]: payload = normalize_object_selector_aliases(payload, "metadata.object.attributes") if isinstance(payload, dict) and payload.get("status") == "invalid_argument": return payload base_id_or_error = require_base_id(payload, "metadata.object.attributes") if isinstance(base_id_or_error, dict): return base_id_or_error base_id = base_id_or_error validated, validation_error = validate_metadata_object_attributes_payload(payload) if validation_error: return validation_error include_storage = bool((validated or {}).get("include_storage")) use_cache = bool((validated or {}).get("use_cache")) only = str((validated or {}).get("only") or "all") table = str((validated or {}).get("table") or "Config") lookup_limit = int((validated or {}).get("limit") or 20) view = str((validated or {}).get("view") or "effective") selector_kind = payload.get("kind") selector_name = str(payload.get("name") or payload.get("guid") or "") extension_guid = str(payload.get("extension_guid") or "").strip().lower() or None ordinal_value = first_non_empty_arg(payload, "ordinal", "index", "object_index") ordinal_object = None if ordinal_value not in {None, ""}: ordinal, ordinal_error = parse_ordinal(ordinal_value, "metadata.object.attributes") if ordinal_error: return ordinal_error ordinal_result = list_objects( selector_kind, base_id=base_id, limit=1, offset=int(ordinal or 1) - 1, include_storage=False, table=table, ) if ordinal_result.get("status") != "ok" or not ordinal_result.get("objects"): result = dict(ordinal_result) result["method"] = "metadata.object.attributes" result["status"] = "not_found" result["diagnostics"] = {"message": f"Object ordinal {ordinal} was not found for kind {selector_kind}."} return result ordinal_object = (ordinal_result.get("objects") or [])[0] selector_kind = ordinal_object.get("kind") or selector_kind selector_name = str(ordinal_object.get("guid") or "") object_result = get_object( selector_kind, selector_name, base_id=base_id, view=view, limit=lookup_limit, include_storage=include_storage, table=table, extension_guid=extension_guid, timeout_seconds=int(payload.get("timeout_seconds") or 60), resolve_semantic_types=False, include_semantic=False, ) if object_result.get("status") != "ok": result = dict(object_result) result["method"] = "metadata.object.attributes" return result object_card = object_result.get("object") or {} object_guid = str(object_card.get("guid") or "").lower() config, _ = sql_config_for_base(base_id) cache_role = metadata_attributes_cache_role(only) if config and object_guid and not include_storage and use_cache and not truthy(payload.get("refresh_cache")): cached_result = metadata_guid_index_lookup_payload(config, object_guid, cache_role) if cached_result: cached_public = dict(cached_result) cached_counts = dict(cached_public.get("counts") or {}) cached_counts.update( public_reference_type_counts( cached_public.get("dimensions") or [], cached_public.get("resources") or [], cached_public.get("attributes") or [], cached_public.get("tabular_sections") or [], ) ) cached_public["counts"] = cached_counts cached_public["cache"] = {"status": "hit", "role": cache_role} cached_public["query"] = { **(cached_public.get("query") or {}), "guid": payload.get("guid"), "kind": payload.get("kind"), "name": payload.get("name"), **({"extension_guid": extension_guid} if extension_guid else {}), **({"ordinal": int(ordinal_value)} if ordinal_value not in {None, ""} and str(ordinal_value).isdigit() else {}), "only": only, "include_storage": include_storage, "table": table, "use_cache": use_cache, } return cached_public include_attributes = only not in {"tabular_sections", "tabularsections", "tabs", "table_parts", "dimensions", "измерения", "resources", "ресурсы"} include_tabular_sections = only not in {"attributes", "requisites", "attrs", "dimensions", "измерения", "resources", "ресурсы"} include_dimensions = only in {"all", "", "dimensions", "измерения", "register_fields", "поля_регистра"} include_resources = only in {"all", "", "resources", "ресурсы", "register_fields", "поля_регистра"} semantic_categories: list[str] = [] if include_attributes: semantic_categories.append("Attribute") if include_tabular_sections: semantic_categories.append("TabularSection") if include_dimensions: semantic_categories.append("Dimension") if include_resources: semantic_categories.append("Resource") semantic_result = get_object( object_card.get("kind") or selector_kind, object_guid or selector_name, base_id=base_id, view=str(payload.get("view") or "effective"), limit=int(payload.get("limit") or 20), include_storage=True, timeout_seconds=int(payload.get("timeout_seconds") or 60), table=table, extension_guid=extension_guid, resolve_semantic_types=False, semantic_include_generic=False, semantic_categories=semantic_categories, semantic_lightweight=not include_storage, ) if semantic_result.get("status") != "ok": result = dict(semantic_result) result["method"] = "metadata.object.attributes" return result semantic = semantic_result.get("semantic") or {} sections = semantic.get("sections") or [] selected_sections = [ section for section in sections if (section.get("category") == "Attribute" and include_attributes) or (section.get("category") == "TabularSection" and include_tabular_sections) or (section.get("category") == "Dimension" and include_dimensions) or (section.get("category") == "Resource" and include_resources) ] type_guids = collect_reference_type_guids_from_sections(selected_sections) resolved_types = resolve_type_guids( base_id, type_guids, timeout_seconds=int(payload.get("timeout_seconds") or 60), table=table, resolve_generated_live=False, ) extensions_by_guid = extension_map_by_guid(base_id) def records(category: str) -> list[dict[str, Any]]: result = [] owner = semantic_result.get("object") or object_card or {} owner_kind = str(owner.get("kind") or "") for section in sections: if section.get("category") != category: continue for record in section.get("records") or []: item = public_metadata_item(record, resolved_types, include_storage=include_storage, owner_kind=owner_kind, extensions_by_guid=extensions_by_guid) identity = record.get("identity") if isinstance(record.get("identity"), dict) else {} metadata_field_type_cache_upsert(config, str(identity.get("guid") or ""), item.get("type"), owner=owner, field_name=item.get("name")) if category == "TabularSection": columns = [] for column in record.get("columns") or []: column_item = public_metadata_item(column, resolved_types, include_storage=include_storage, owner_kind=owner_kind, extensions_by_guid=extensions_by_guid) column_identity = column.get("identity") if isinstance(column.get("identity"), dict) else {} metadata_field_type_cache_upsert(config, str(column_identity.get("guid") or ""), column_item.get("type"), owner=owner, field_name=column_item.get("name")) columns.append(column_item) item["columns"] = columns item["counts"] = {"columns": len(columns)} result.append(item) return result attributes = records("Attribute") if include_attributes else [] tabular_sections = records("TabularSection") if include_tabular_sections else [] dimensions = records("Dimension") if include_dimensions else [] resources = records("Resource") if include_resources else [] reference_counts = public_reference_type_counts(dimensions, resources, attributes, tabular_sections) result = { "schema": "onec_metadata_object_attributes.v1", "status": "ok", "base_id": base_id, "source": {"kind": "live_metadata"}, "query": { "guid": payload.get("guid"), "kind": payload.get("kind"), "name": payload.get("name"), **({"extension_guid": extension_guid} if extension_guid else {}), **({"ordinal": int(ordinal_value)} if ordinal_value not in {None, ""} and str(ordinal_value).isdigit() else {}), "only": only, "include_storage": include_storage, "use_cache": use_cache, }, "object": public_metadata_row(semantic_result.get("object") or object_card, include_storage=include_storage), "dimensions": dimensions, "resources": resources, "attributes": attributes, "tabular_sections": tabular_sections, "counts": { "dimensions": len(dimensions), "resources": len(resources), "attributes": len(attributes), "tabular_sections": len(tabular_sections), **reference_counts, }, } if config and object_guid and not include_storage: metadata_guid_index_upsert( config, { "guid": object_guid, "guid_role": cache_role, "kind": (semantic_result.get("object") or object_card or {}).get("kind"), "kind_ru": (semantic_result.get("object") or object_card or {}).get("kind_ru"), "public_kind": (semantic_result.get("object") or object_card or {}).get("public_kind"), "name": (semantic_result.get("object") or object_card or {}).get("name"), "synonym": (semantic_result.get("object") or object_card or {}).get("synonym"), "presentation": ".".join( part for part in [ (semantic_result.get("object") or object_card or {}).get("kind_ru"), (semantic_result.get("object") or object_card or {}).get("name"), ] if part ), "payload": result, "source_file": object_guid, }, ) result["cache"] = {"status": "stored", "role": cache_role} return result REPOSITORY_FORM_SEGMENTS = {"form", "форма"} REPOSITORY_FORM_OWNER_KINDS = {"Catalog", "Document", "DataProcessor", "Report"} def repository_form_reference_parts(value: str) -> tuple[str, str, str] | None: """Return (owner_kind, owner_name, form_name) for a public child-form ref.""" parts = [part.strip() for part in value.strip().split(".")] if len(parts) < 4 or parts[2].casefold() not in REPOSITORY_FORM_SEGMENTS: return None owner_kind = canonical_kind(parts[0]) or parts[0] if owner_kind not in REPOSITORY_FORM_OWNER_KINDS or not parts[1] or not any(parts[3:]): return None return owner_kind, parts[1], ".".join(parts[3:]).strip() def repository_resolve_owned_form_sql( *, method: str, base_id: str, owner_kind: str, owner_name: str, form_name: str, expected_form_guid: str | None = None, extension_guid: str | None = None, timeout_seconds: int = 60, ) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: attempts: list[dict[str, Any]] = [] for table in ("Config", "ConfigSave", "ConfigCAS", "ConfigCASSave"): owner_result = get_object(owner_kind, owner_name, base_id=base_id, view="effective", limit=20, timeout_seconds=timeout_seconds, table=table, extension_guid=extension_guid, include_semantic=False) attempts.append({"stage": "owner", "table": table, "status": owner_result.get("status")}) owner = owner_result.get("object") if owner_result.get("status") == "ok" and isinstance(owner_result.get("object"), dict) else None if not owner: continue forms_payload = { "base_id": base_id, "kind": owner.get("kind") or owner_kind, "name": owner.get("name") or owner_name, "guid": owner.get("guid"), "form": form_name, "table": table, "include_storage": True, "timeout_seconds": timeout_seconds, } # `metadata.object.forms` deliberately distinguishes an absent optional # extension selector from an explicitly invalid empty selector. if extension_guid: forms_payload["extension_guid"] = extension_guid forms_result = metadata_object_forms(forms_payload) attempts.append({"stage": "form", "table": table, "status": forms_result.get("status")}) forms = forms_result.get("forms") if forms_result.get("status") == "ok" and isinstance(forms_result.get("forms"), list) else [] form = next((item for item in forms if isinstance(item, dict) and (not expected_form_guid or str(item.get("guid") or "").casefold() == expected_form_guid.casefold())), None) if not isinstance(form, dict): continue resolved_kind = str(owner.get("kind") or owner_kind).strip() resolved_owner_name = str(owner.get("name") or owner_name).strip() resolved_form_name = str(form.get("name") or form_name).strip() origin = owner.get("origin") if isinstance(owner.get("origin"), dict) else {} extension = origin.get("extension") if isinstance(origin.get("extension"), dict) else {} resolved_extension_guid = str(extension.get("guid") or extension_guid or "").lower() cache_config, _ = sql_config_for_base(base_id) metadata_form_owner_cache_upsert( cache_config, base_id=base_id, owner_kind=resolved_kind, owner_name=resolved_owner_name, owner_guid=str(owner.get("guid") or ""), form_name=resolved_form_name, form_guid=str(form.get("guid") or ""), table=table, file_name=f"{str(form.get('guid') or '').lower()}.0", extension={"guid": resolved_extension_guid} if is_guid_text(resolved_extension_guid) else None, ) return { "requested_object": f"{owner_kind}.{owner_name}.Form.{form_name}", "resolved_object": f"{resolved_kind}.{resolved_owner_name}.Form.{resolved_form_name}", "form_guid": str(form.get("guid") or "").lower(), "owner": {"kind": resolved_kind, "name": resolved_owner_name, "guid": owner.get("guid")}, "table": table, "source": "live_sql_form_owner", "attempts": attempts, **({"extension_guid": resolved_extension_guid} if is_guid_text(resolved_extension_guid) else {}), }, None return None, {"schema": "onec_repository_lock_request.v1", "method": method, "base_id": base_id, "status": "not_found", "error": "repository_form_not_found_in_sql", "object": f"{owner_kind}.{owner_name}.Form.{form_name}", "diagnostics": {"attempts": attempts}} def repository_resolve_owned_form_across_layers_sql( *, method: str, base_id: str, owner_kind: str, owner_name: str, form_name: str, extension_guid: str | None = None, timeout_seconds: int = 60, ) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: """Resolve a form in its SQL layer, discovering an extension only when needed.""" resolved, base_error = repository_resolve_owned_form_sql( method=method, base_id=base_id, owner_kind=owner_kind, owner_name=owner_name, form_name=form_name, extension_guid=extension_guid, timeout_seconds=timeout_seconds, ) if resolved or extension_guid: return resolved, base_error listed = list_extensions({"base_id": base_id, "timeout_seconds": timeout_seconds}) extensions = listed.get("extensions") if listed.get("status") == "ok" and isinstance(listed.get("extensions"), list) else [] matches: list[dict[str, Any]] = [] extension_attempts: list[dict[str, Any]] = [] for extension in extensions: guid = str(extension.get("guid") or "").strip().lower() if isinstance(extension, dict) else "" if not is_guid_text(guid): continue candidate, candidate_error = repository_resolve_owned_form_sql( method=method, base_id=base_id, owner_kind=owner_kind, owner_name=owner_name, form_name=form_name, extension_guid=guid, timeout_seconds=timeout_seconds, ) extension_attempts.append({"extension_guid": guid, "status": "ok" if candidate else str((candidate_error or {}).get("status") or "not_found")}) if candidate: matches.append(candidate) if len(matches) == 1: matches[0]["source"] = "live_sql_extension_form_owner" return matches[0], None if len(matches) > 1: return None, { "schema": "onec_repository_lock_request.v1", "method": method, "base_id": base_id, "status": "ambiguous", "error": "ambiguous_repository_form_extension_origin", "object": f"{owner_kind}.{owner_name}.Form.{form_name}", "diagnostics": {"extension_matches": [item.get("extension_guid") for item in matches]}, } error = dict(base_error or {}) diagnostics = dict(error.get("diagnostics") or {}) diagnostics["extension_attempts"] = extension_attempts if listed.get("status") != "ok": diagnostics["extensions_list"] = listed error["diagnostics"] = diagnostics return None, error def repository_resolve_form_guid_sql(*, method: str, base_id: str, form_guid: str, timeout_seconds: int = 60) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: config, _ = sql_config_for_base(base_id) for table in ("Config", "ConfigSave", "ConfigCAS", "ConfigCASSave"): cached = metadata_form_owner_cache_lookup(config, form_guid=form_guid) owner = cached.get("owner") if isinstance((cached or {}).get("owner"), dict) else {} form = cached.get("form") if isinstance((cached or {}).get("form"), dict) else {} owner_kind = canonical_kind(str(owner.get("kind") or "")) or str(owner.get("kind") or "") owner_name, form_name = str(owner.get("name") or "").strip(), str(form.get("name") or "").strip() if owner_kind in REPOSITORY_FORM_OWNER_KINDS and owner_name and form_name: extension = cached.get("extension") if isinstance(cached.get("extension"), dict) else {} resolved, error = repository_resolve_owned_form_sql(method=method, base_id=base_id, owner_kind=owner_kind, owner_name=owner_name, form_name=form_name, expected_form_guid=form_guid, extension_guid=str(extension.get("guid") or "") or None, timeout_seconds=timeout_seconds) if resolved: resolved.update({"requested_object": f"Form.{form_guid}", "source": "live_sql_form_guid_validated"}) return resolved, error # A cache miss is not a negative result. Build the owner relationship from # live SQL descriptors, keeping the scan bounded to form-capable owners. for owner_kind in sorted(REPOSITORY_FORM_OWNER_KINDS): owners_result = list_objects( owner_kind, base_id=base_id, limit=5000, include_storage=True, refresh_cache=True, table="Config", ) owners = owners_result.get("objects") if owners_result.get("status") == "ok" and isinstance(owners_result.get("objects"), list) else [] for owner in owners: if not isinstance(owner, dict) or not str(owner.get("guid") or "").strip(): continue related = metadata_object_related({ "base_id": base_id, "kind": owner_kind, "guid": owner.get("guid"), "table": "Config", "include_storage": False, "timeout_seconds": timeout_seconds, }) related_items = related.get("related") if related.get("status") == "ok" and isinstance(related.get("related"), list) else [] match = next( ( item for item in related_items if isinstance(item, dict) and str(item.get("category") or "").casefold() == "form" and str(((item.get("identity") or {}).get("guid") if isinstance(item.get("identity"), dict) else item.get("guid")) or "").casefold() == form_guid.casefold() ), None, ) identity = match.get("identity") if isinstance((match or {}).get("identity"), dict) else {} form_name = str(identity.get("name") or "").strip() owner_name = str(owner.get("name") or "").strip() if not form_name or not owner_name: continue resolved, error = repository_resolve_owned_form_sql( method=method, base_id=base_id, owner_kind=owner_kind, owner_name=owner_name, form_name=form_name, expected_form_guid=form_guid, timeout_seconds=timeout_seconds, ) if resolved: resolved.update({"requested_object": f"Form.{form_guid}", "source": "live_sql_form_guid_scan"}) return resolved, error return None, {"schema": "onec_repository_lock_request.v1", "method": method, "base_id": base_id, "status": "not_found", "error": "repository_form_owner_not_found_in_sql", "object": f"Form.{form_guid}", "diagnostics": {"message": "Pass OwnerKind.OwnerName.Form.FormName or build a live SQL form-owner index before using Form.."}} _FORM_MODULE_FILE_RE = re.compile(r"^(?:(?P[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})__)?(?P
[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.0$", re.IGNORECASE) def resolve_write_gate_context(payload: dict[str, Any]) -> dict[str, Any]: """Derive the exact development layer and form ownership before gates. This is read-only and deliberately does *not* select a lock session. The caller must still present the lock_session_id returned by confirmation. """ result = dict(payload) target = dict(payload.get("target") or {}) if isinstance(payload.get("target"), dict) else {} origin = target.get("origin") if isinstance(target.get("origin"), dict) else {} origin_extension = origin.get("extension") if isinstance(origin.get("extension"), dict) else {} explicit_extension_guid = str( payload.get("extension_guid") or target.get("extension_guid") or origin_extension.get("guid") or "" ).strip().lower() requested_extension_value = ( target.get("extension") or payload.get("extension") or target.get("preferred_extension") or payload.get("preferred_extension") or "" ) if isinstance(requested_extension_value, dict): explicit_extension_guid = str( explicit_extension_guid or requested_extension_value.get("guid") or "" ).strip().lower() requested_extension = str( requested_extension_value.get("name") or requested_extension_value.get("guid") or "" ).strip() else: requested_extension = str(requested_extension_value or "").strip() base_id = str(payload.get("base_id") or "").strip() resolved_extension_guid = explicit_extension_guid extension_error: dict[str, Any] | None = None if not resolved_extension_guid and requested_extension and base_id: resolved_extension_guid, extension_error = extension_filter_to_guid( base_id, requested_extension, method="write.gate_context", ) if resolved_extension_guid: resolved_extension_guid = resolved_extension_guid.lower() result["extension_guid"] = resolved_extension_guid target["extension_guid"] = resolved_extension_guid target["origin"] = { **origin, "source": "extension", "status": "ok", "extension": { **origin_extension, "guid": resolved_extension_guid, **({"name": requested_extension} if requested_extension and not is_guid_text(requested_extension) else {}), }, } result["development_layer_resolution"] = { "status": "resolved", "source": "explicit_extension_guid" if explicit_extension_guid else "extension_name", "extension": { "guid": resolved_extension_guid, **({"name": requested_extension} if requested_extension and not is_guid_text(requested_extension) else {}), }, "layer_id": f"extension:{resolved_extension_guid}", } elif requested_extension: # Never let an unresolved public extension selector inherit the base # repository/support policy. The synthetic layer cannot match a # configured GUID layer and therefore keeps every write gate closed. result["layer_id"] = "extension:unresolved" target["layer_id"] = "extension:unresolved" result["development_layer_resolution"] = { "status": "not_resolved", "source": "extension_name", "extension": {"name": requested_extension}, "layer_id": "extension:unresolved", "diagnostics": extension_error or { "status": "not_resolved", "error": "extension_resolution_requires_base_id", "base_id": base_id or None, }, } if target: result["target"] = target existing = payload.get("owner_resolution") if isinstance(payload.get("owner_resolution"), dict) else {} if existing.get("status") == "resolved" and str(existing.get("repository_object") or "").strip(): return result module_ref = str( target.get("module_ref") or target.get("module_id") or payload.get("module_ref") or payload.get("module_id") or "" ).strip() table, file_name, _stream = parse_module_id(module_ref) match = _FORM_MODULE_FILE_RE.fullmatch(file_name or "") if table and file_name else None if not match: return result if not base_id: return result form_guid = str(match.group("form") or "").lower() resolved, error = repository_resolve_form_guid_sql( method="write.gate_context", base_id=base_id, form_guid=form_guid, timeout_seconds=int(payload.get("timeout_seconds") or 60), ) if not resolved: result["owner_resolution"] = { "status": "not_resolved", "module_ref": module_ref, "form_guid": form_guid, "diagnostics": error, } return result canonical_object = str(resolved.get("resolved_object") or "").strip() resolved_extension_guid = str(resolved.get("extension_guid") or "").strip().lower() # For a physical form module, SQL ownership is authoritative. Do not let # a caller choose a different repository scope or configuration layer. result["repository_object"] = canonical_object result["support_object_guid"] = form_guid if resolved_extension_guid: result["extension_guid"] = resolved_extension_guid result.pop("layer_id", None) target["extension_guid"] = resolved_extension_guid target.pop("layer_id", None) origin = target.get("origin") if isinstance(target.get("origin"), dict) else {} previous_extension = origin.get("extension") if isinstance(origin.get("extension"), dict) else {} target["origin"] = { **origin, "source": "extension", "status": "ok", "extension": { "guid": resolved_extension_guid, **( {"name": previous_extension.get("name")} if str(previous_extension.get("guid") or "").strip().lower() == resolved_extension_guid and str(previous_extension.get("name") or "").strip() else {} ), }, } else: result.pop("extension_guid", None) result.pop("layer_id", None) target.pop("extension_guid", None) target.pop("layer_id", None) if isinstance(target.get("origin"), dict): origin = dict(target["origin"]) origin.pop("extension", None) origin["source"] = "configuration" origin["status"] = "ok" target["origin"] = origin if target: result["target"] = target result["owner_resolution"] = { "status": "resolved", "source": "live_sql_form_guid", "module_ref": module_ref, "form_guid": form_guid, "repository_object": canonical_object, "owner": resolved.get("owner"), "layer_id": f"extension:{resolved_extension_guid}" if resolved_extension_guid else "base", } result["development_layer_resolution"] = { "status": "resolved", "source": "live_sql_form_guid", "extension": {"guid": resolved_extension_guid} if resolved_extension_guid else None, "layer_id": f"extension:{resolved_extension_guid}" if resolved_extension_guid else "base", } return result def validate_repository_request_objects_sql(payload: dict[str, Any], *, method: str = repository_control.METHOD_LOCK_REQUEST) -> tuple[dict[str, Any], dict[str, Any] | None]: objects, object_error = repository_control._requested_objects(payload) if object_error: return payload, {"schema": "onec_repository_lock_request.v1", "method": method, **object_error} base_id = str(payload.get("base_id") or "").strip() canonical_objects: list[str] = [] resolutions: list[dict[str, Any]] = [] for public_ref in objects or []: form_parts = repository_form_reference_parts(public_ref) if form_parts: resolved_form, form_error = repository_resolve_owned_form_across_layers_sql( method=method, base_id=base_id, owner_kind=form_parts[0], owner_name=form_parts[1], form_name=form_parts[2], extension_guid=str(payload.get("extension_guid") or "").strip().lower() or None, timeout_seconds=int(payload.get("timeout_seconds") or 60), ) if form_error: return payload, form_error canonical_objects.append(str(resolved_form["resolved_object"])) resolutions.append(resolved_form) continue parts = public_ref.strip().split(".") if len(parts) == 2 and parts[0].casefold() in REPOSITORY_FORM_SEGMENTS and is_guid_text(parts[1].strip()): resolved_form, form_error = repository_resolve_form_guid_sql( method=method, base_id=base_id, form_guid=parts[1].strip().lower(), timeout_seconds=int(payload.get("timeout_seconds") or 60), ) if form_error: return payload, form_error canonical_objects.append(str(resolved_form["resolved_object"])) resolutions.append(resolved_form) continue kind_text, separator, name = public_ref.partition(".") if not separator or not kind_text.strip() or not name.strip(): return payload, invalid_argument(method, "objects", f"Repository object '{public_ref}' must use a public Kind.Name reference.") result: dict[str, Any] = {} resolved_table = "" attempts: list[dict[str, Any]] = [] for table in ("Config", "ConfigSave", "ConfigCASSave"): result = get_object( canonical_kind(kind_text), name, base_id=base_id, view="effective", limit=20, timeout_seconds=int(payload.get("timeout_seconds") or 60), table=table, include_semantic=False, ) attempts.append({"table": table, "status": result.get("status")}) if result.get("status") == "ok" and isinstance(result.get("object"), dict): resolved_table = table break if result.get("status") != "ok" or not isinstance(result.get("object"), dict): return payload, { "schema": "onec_repository_lock_request.v1", "method": method, "base_id": base_id, "status": "not_found", "error": "repository_object_not_found_in_sql", "object": public_ref, "diagnostics": {"attempts": attempts, "last_result": result}, } card = result["object"] resolved_kind = str(card.get("kind_ru") or card.get("public_kind") or card.get("kind") or kind_text).strip() resolved_name = str(card.get("name") or name).strip() canonical_objects.append(f"{resolved_kind}.{resolved_name}") resolutions.append({"requested_object": public_ref, "resolved_object": canonical_objects[-1], "table": resolved_table, "source": "live_sql"}) normalized = dict(payload) normalized["objects"] = canonical_objects normalized["sql_resolution"] = resolutions resolved_extensions = {str(item.get("extension_guid") or "").lower() for item in resolutions if isinstance(item, dict) and is_guid_text(str(item.get("extension_guid") or ""))} if len(resolved_extensions) > 1: return payload, invalid_argument(method, "objects", "One lock request cannot mix objects from different extension layers.") if resolved_extensions: resolved_extension = next(iter(resolved_extensions)) requested_extension = str(payload.get("extension_guid") or "").strip().lower() if requested_extension and requested_extension != resolved_extension: return payload, invalid_argument(method, "extension_guid", "extension_guid does not match the resolved object origin.") normalized["extension_guid"] = resolved_extension return normalized, None def call_method_impl(method: str, payload: dict[str, Any] | None) -> dict[str, Any]: if payload is None: payload = {} elif not isinstance(payload, dict): return invalid_argument(method, "payload", "payload must be a JSON object.") source_policy_error = validate_sql_only_runtime_payload(method, payload) if source_policy_error: return source_policy_error if "base_id" in payload and payload.get("base_id") is not None and not isinstance(payload.get("base_id"), str): return invalid_argument(method, "base_id", "base_id must be a JSON string.") if method in OBJECT_SELECTOR_ALIAS_METHODS: template_view_method = method in {"templates.read", "templates.analyze", "templates.map"} selector_argument_error = validate_object_selector_arguments(payload, method, include_view=not template_view_method) if selector_argument_error: return selector_argument_error normalized_payload = normalize_selector_payload_for_method(payload, method) if isinstance(normalized_payload, dict) and normalized_payload.get("status") == "invalid_argument": return normalized_payload payload = normalized_payload selector_error = validate_optional_string_arguments(payload, method, ["view"]) if selector_error: return selector_error base_id = str(payload.get("base_id")) if payload.get("base_id") else None if method == "health": return STATE.health(base_id=base_id) if method == "help.methods": validation_error = validate_help_methods_payload(payload) if validation_error: return validation_error selected = payload.get("method") registry = adapter_method_registry() methods = [row for name, row in registry.items() if not selected or name == selected] return { "schema": "onec_adapter_methods.v1", "contract_version": ADAPTER_CONTRACT_VERSION, "methods": methods, "count": len(methods), "registry": adapter_method_registry_diagnostics(), } if method == "repository.layer.connection.set": validation_error = validate_adapter_job_payload(method, payload) if validation_error: return validation_error return repository_layer_connection_set(payload) if method == "repository.layers.audit": validation_error = validate_adapter_job_payload(method, payload) if validation_error: return validation_error return repository_layers_audit(payload) if method in repository_control.METHODS: validation_error = validate_adapter_job_payload(method, payload) if validation_error: return validation_error if method in {repository_control.METHOD_LOCK_PLAN, repository_control.METHOD_LOCK_REQUEST}: payload, repository_object_error = validate_repository_request_objects_sql(payload, method=method) if repository_object_error: return repository_object_error return repository_control.call(method, payload) if method == "adapter.job.start": return adapter_start_job(payload) if method in {"adapter.job.get", "mcp.job.get", "onec.job.get"}: return adapter_get_job(payload) if method in {"adapter.job.cancel", "mcp.job.cancel", "onec.job.cancel"}: return adapter_cancel_job(payload) if method == "metadata.kinds": validation_error = validate_metadata_kinds_payload(payload) if validation_error: return validation_error return get_kinds(base_id) if method == "metadata.capabilities": return metadata_capabilities(payload) if method == "metadata.adapter.audit": return metadata_adapter_audit(payload) if method == "metadata.write.capabilities": return metadata_write_capabilities(payload) if method == "metadata.objects.list": validation_error = validate_adapter_job_payload(method, payload) if validation_error: return validation_error if payload.get("extension") not in {None, ""}: return metadata_objects_list_extension_not_supported(payload) table_or_error = metadata_storage_table(payload, "metadata.objects.list") if isinstance(table_or_error, dict): return table_or_error table = table_or_error include_storage, include_storage_error = strict_bool_argument(payload, "include_storage", method="metadata.objects.list", default=False) if include_storage_error: return include_storage_error include_missing, include_missing_error = strict_bool_argument(payload, "include_missing", method="metadata.objects.list", default=False) if include_missing_error: return include_missing_error only_missing, only_missing_error = strict_bool_argument(payload, "only_missing", method="metadata.objects.list", default=False) if only_missing_error: return only_missing_error exact_counts, exact_counts_error = strict_bool_argument(payload, "exact_counts", method="metadata.objects.list", default=False) if exact_counts_error: return exact_counts_error refresh_cache, refresh_cache_error = strict_bool_argument(payload, "refresh_cache", method="metadata.objects.list", default=False) if refresh_cache_error: return refresh_cache_error return list_objects( payload.get("kind"), base_id=base_id, limit=payload.get("limit", 200), offset=payload.get("offset", 0), include_storage=bool(include_storage), include_missing=bool(include_missing), only_missing=bool(only_missing), exact_counts=bool(exact_counts), refresh_cache=bool(refresh_cache), table=table, name_filter=payload.get("name_filter") or payload.get("name_contains"), ) if method == "metadata.object.get": if not has_object_selector(payload): return invalid_argument("metadata.object.get", "selector", OBJECT_SELECTOR_REQUIRED_MESSAGE) guid_error = validate_explicit_guid_argument(payload, "metadata.object.get") if guid_error: return guid_error if "mode" in payload and (payload.get("mode") is None or payload.get("mode") == ""): return invalid_argument("metadata.object.get", "mode", "mode must be one of: card, semantic.", allowed_values=["card", "semantic"]) if "mode" in payload and not isinstance(payload.get("mode"), str): return invalid_argument("metadata.object.get", "mode", "mode must be a JSON string.", allowed_values=["card", "semantic"]) ordinal_error = validate_explicit_ordinal_arguments(payload, "metadata.object.get") if ordinal_error: return ordinal_error limit, limit_error = parse_int_argument(payload, "limit", method="metadata.object.get", default=20, minimum=1, maximum=5000) if limit_error: return limit_error timeout_seconds, timeout_error = parse_int_argument(payload, "timeout_seconds", method="metadata.object.get", default=60, minimum=1) if timeout_error: return timeout_error view, view_error = parse_view_argument(payload, "metadata.object.get") if view_error: return view_error mode = str(payload.get("mode") or "card").strip().casefold() if mode not in {"card", "semantic"}: return { "schema": "onec_adapter_request_error.v1", "method": "metadata.object.get", "status": "invalid_argument", "error": "invalid_argument", "argument": "mode", "allowed_values": ["card", "semantic"], "diagnostics": {"message": "mode must be one of: card, semantic."}, } include_storage, include_storage_error = strict_bool_argument(payload, "include_storage", method="metadata.object.get", default=False) if include_storage_error: return include_storage_error include_semantic, include_semantic_error = strict_bool_argument(payload, "include_semantic", method="metadata.object.get", default=False) if include_semantic_error: return include_semantic_error table_or_error = metadata_storage_table(payload, "metadata.object.get") if isinstance(table_or_error, dict): return table_or_error table = table_or_error semantic_requested = bool(include_semantic or mode == "semantic") return get_object( payload.get("kind"), str(payload.get("guid") or payload.get("name") or ""), base_id=base_id, view=str(view or "effective"), limit=int(limit or 20), include_storage=bool(include_storage), ordinal=first_non_empty_arg(payload, "ordinal", "index", "object_index"), table=str(table or "Config"), file_name=str(payload.get("file_name") or "") or None, extension_guid=str(payload.get("extension_guid") or "") or None, include_semantic=semantic_requested, timeout_seconds=int(timeout_seconds or 60), ) if method == "metadata.object.properties": return metadata_object_properties(payload) if method == "metadata.object.decode": return decode_metadata_object(payload) if method == "metadata.object.parts": return metadata_object_parts(payload) if method == "metadata.object.modules": return metadata_object_modules(payload) if method == "metadata.object.related": return metadata_object_related(payload) if method == "metadata.object.forms": return metadata_object_forms(payload) if method == "metadata.object.form.details": return metadata_object_form_details(payload) if method == "metadata.object.templates": return metadata_object_templates(payload) if method == "metadata.object.template.details": return metadata_object_template_details(payload) if method == "templates.read": validation_error = validate_templates_read_payload(payload, method) if validation_error: return validation_error return templates_read(payload, analyze=False) if method == "templates.analyze": validation_error = validate_templates_read_payload(payload, method) if validation_error: return validation_error return templates_read(payload, analyze=True) if method == "templates.map": validation_error = validate_templates_read_payload(payload, method) if validation_error: return validation_error return templates_map(payload) if method == "templates.areas.find": validation_error = validate_templates_areas_find_payload(payload) if validation_error: return validation_error return templates_areas_find(payload) if method == "metadata.object.commands": return metadata_object_commands(payload) if method == "metadata.definition.find": validation_error = validate_metadata_definition_find_payload(payload) if validation_error: return validation_error return metadata_definition_find(payload) if method == "metadata.route.resolve": validation_error = validate_extension_objects_find_payload(payload, method) if validation_error: return validation_error return metadata_route_resolve(payload) if method == "metadata.object.special.details": return metadata_object_special_details(payload) if method == "metadata.form.decode": return metadata_form_decode(payload) if method == FORM_OWNER_INDEX_BUILD_METHOD: validation_error = validate_metadata_form_owner_index_build_payload(payload) if validation_error: return validation_error return metadata_form_owner_index_build(payload) if method == "metadata.form.write_target.resolve": validation_error = validate_metadata_form_write_target_resolve_payload(payload) if validation_error: return validation_error return metadata_form_write_target_resolve(payload) if method == FORM_WRITE_TARGET_VERIFY_METHOD: validation_error = validate_metadata_form_write_target_resolve_payload(payload, method=FORM_WRITE_TARGET_VERIFY_METHOD) if validation_error: return validation_error return metadata_form_write_target_verify(payload) if method == "metadata.saved_state.forms.search": return metadata_saved_state_forms_search(payload) if method == "metadata.saved_state.prepare": validation_error = validate_metadata_saved_state_prepare_payload(payload) if validation_error: return validation_error return metadata_saved_state_prepare(payload) if method == SAVED_STATE_STATUS_METHOD: validation_error = validate_metadata_saved_state_status_payload(payload) if validation_error: return validation_error return metadata_saved_state_status(payload) if method == SAVED_STATE_DIFF_METHOD: validation_error = validate_metadata_saved_state_diff_payload(payload) if validation_error: return validation_error return metadata_saved_state_diff(payload) if method == SAVED_STATE_CHANGES_LIST_METHOD: validation_error = validate_metadata_saved_state_changes_list_payload(payload) if validation_error: return validation_error return metadata_saved_state_changes_list(payload) if method == SAVED_STATE_MODULES_SEARCH_METHOD: return metadata_saved_state_modules_search(payload) if method == "metadata.form.element.write": return metadata_form_element_write(payload) if method == "metadata.form.element.write_apply": return metadata_form_element_write_apply(payload) if method == OBJECT_PROPERTY_WRITE_METHOD: validation_error = validate_metadata_object_property_write_payload(payload) if validation_error: return validation_error return metadata_object_property_write(payload) if method == OBJECT_MEMBER_ADD_METHOD: validation_error = validate_metadata_object_member_add_payload(payload) if validation_error: return validation_error return metadata_object_member_add(payload) if method == FORM_TARGET_MOVE_METHOD: return metadata_form_target_move(payload) if method == FORM_COMMAND_BUTTON_WRITE_METHOD: return metadata_form_command_button_write(payload) if method == FORM_COMMAND_BUTTON_VERIFY_METHOD: return metadata_form_command_button_verify(payload) if method == MODULE_WRITE_APPLY_METHOD: return metadata_module_write_apply(payload) if method == METADATA_WRITE_PLAN_METHOD: validation_error = validate_metadata_write_plan_payload(payload) if validation_error: return validation_error return metadata_write_plan(payload) if method == METADATA_WRITE_PREFLIGHT_METHOD: validation_error = validate_metadata_write_preflight_payload(payload) if validation_error: return validation_error return metadata_write_preflight(payload) if method == METADATA_WRITE_METHOD: return metadata_write(payload) if method == "metadata.write.history": validation_error = validate_metadata_write_history_payload(payload) if validation_error: return validation_error return metadata_write_history(payload) if method == METADATA_WRITE_ROLLBACK_METHOD: validation_error = validate_metadata_write_rollback_payload(payload) if validation_error: return validation_error return metadata_write_rollback(payload) if method == FORM_WRITE_MATRIX_BUILD_METHOD: return metadata_form_write_matrix_build(payload) if method == FORM_WRITE_MATRIX_SMOKE_METHOD: return metadata_form_write_matrix_smoke(payload) if method == "metadata.write_learning.capture_before": return metadata_write_learning_capture(payload, "before") if method == "metadata.write_learning.capture_after": return metadata_write_learning_capture(payload, "after") if method == "metadata.write_learning.diff": return metadata_write_learning_diff(payload) if method == "metadata.write_learning.infer_rule": return metadata_write_learning_infer_rule(payload) if method == "metadata.object.attributes": return metadata_object_attributes(payload) if method == "metadata.object.full": return metadata_object_full(payload) if method == "metadata.snapshot": return metadata_snapshot(payload) if method == "metadata.cache.status": return metadata_cache_status(payload) if method == "metadata.cache.lookup": return metadata_cache_lookup(payload) if method == "metadata.cache.rebuild": return metadata_cache_rebuild(payload) if method == "metadata.cache.invalidate": return metadata_cache_invalidate(payload) if method == "infobase.users.search": return infobase_users_search(payload) if method == "infobase.user.get": return infobase_user_get(payload) if method == "infobase.user.password.status": return infobase_user_password_status(payload) if method == "infobase.user.password.capabilities": return infobase_user_password_capabilities(payload) if method == "infobase.user.password.set": return infobase_user_password_change(payload, operation="set") if method == "infobase.user.password.clear": return infobase_user_password_change(payload, operation="clear") if method == "access.snapshot.extract": return access_snapshot_extract(payload) if method == "access.graph.build": return access_graph_build(payload) if method == "access.user.explain": return access_user_explain(payload) if method == "access.users.search": return access_users_search(payload) if method == "access.keys.query": return access_keys_query(payload) if method == "access.object_keys.resolve": return access_object_keys_resolve(payload) if method == "access.object.explain": return access_object_explain(payload) if method == "access.object.roles": return access_object_roles(payload) if method == "access.object.subjects": return access_object_subjects(payload) if method == "access.rls.discover": return access_rls_discover(payload) if method == "access.role.profiles": return access_role_profiles(payload) if method == "access.role.users": return access_role_users(payload) if method == "access.role.audit_export": return access_role_audit_export(payload) if method == "access.role.audit_analyze": return access_role_audit_analyze(payload) if method == "semantic.cache.search": validation_error = validate_semantic_cache_search_payload(payload) if validation_error: return validation_error return semantic_cache_search(payload) if method == "semantic.cache.status": validation_error = validate_semantic_cache_status_payload(payload) if validation_error: return validation_error return semantic_cache_status(payload) if method == "semantic.cache.validate": validation_error = validate_semantic_cache_validate_payload(payload) if validation_error: return validation_error return semantic_cache_validate(payload) if method == "semantic.cache.validate_batch": validation_error = validate_semantic_cache_validate_batch_payload(payload) if validation_error: return validation_error return semantic_cache_validate_batch(payload) if method == "semantic.cache.refresh": validation_error = validate_semantic_cache_refresh_payload(payload) if validation_error: return validation_error return semantic_cache_refresh(payload) if method == "semantic.cache.rebuild": validation_error = validate_semantic_cache_rebuild_payload(payload) if validation_error: return validation_error return semantic_cache_rebuild(payload) if method == "semantic.cache.pending": validation_error = validate_semantic_cache_pending_payload(payload) if validation_error: return validation_error return semantic_cache_pending(payload) if method == "semantic.cache.embedding.upsert": validation_error = validate_semantic_cache_embedding_upsert_payload(payload) if validation_error: return validation_error return semantic_cache_embedding_upsert(payload) if method == "metadata.code_index.build": return metadata_code_index_build(payload) if method == "metadata.code_index.status": return metadata_code_index_status(payload) if method == "metadata.code_index.search": return metadata_code_index_search(payload) if method == "metadata.code_index.verify": return metadata_code_index_verify(payload) if method == "metadata.code_index.refresh_changed": return metadata_code_index_refresh_changed(payload) if method == "metadata.code_vector.search": return metadata_code_vector_search(payload) if method == "metadata.module_owner_cache.prune": return metadata_module_owner_cache_prune(payload) if method == "extensions.list": return list_extensions(payload) if method == "extension.cache.status": validation_error = validate_extension_cache_status_payload(payload, method) if validation_error: return validation_error return extension_cache_status(payload) if method == "extension.cache.rebuild": validation_error = validate_extension_cache_rebuild_payload(payload, method) if validation_error: return validation_error return extension_cache_rebuild(payload) if method == "extension.cache.validate": validation_error = validate_extension_cache_validate_payload(payload, method) if validation_error: return validation_error return extension_cache_validate(payload) if method == "extension.objects.find": validation_error = validate_extension_objects_find_payload(payload, method) if validation_error: return validation_error return extension_objects_find(payload) if method == "schema.tables.list": return schema_tables_list(payload) if method == "storage.files.list": return storage_files_list(payload) if method == "storage.file.get": return storage_file_get(payload) if method == "storage.saved_state.apply_proposal": return storage_saved_state_apply_proposal(payload) if method == "storage.saved_state.rollback": return storage_saved_state_rollback(payload) if method == "storage.saved_state.backups.list": return storage_saved_state_backups_list(payload) if method == "metadata.dbnames.summary": return metadata_dbnames_summary(payload) if method == "metadata.support.decode": return metadata_support_decode(payload) if method == "repository.sql_state.snapshot": return repository_sql_state_snapshot(payload) if method == "repository.sql_state.diff": return repository_sql_state_diff(payload) if method == "payload.diff": validation_error = validate_payload_diff_payload(payload) if validation_error: return validation_error return payload_diff(payload) if method == "codec.decode": return codec_decode(payload) if method == "codec.encode": return codec_encode(payload) if method == "query.validate": return validate_query(payload) if method == "query.run": return run_readonly_query(payload) if method == "data.schema": return data_object_schema(payload) if method in {"data.list", "data.get"}: if method == "data.get" and not data_record_ref(payload): return invalid_argument(method, "record_ref", "record_ref is required for data.get.") return data_read(payload, method=method) if method == "data.count": return data_read(payload, count_only=True, method=method) if method == "data.query": return data_read(payload, count_only=payload.get("count_only") is True, method=method) if method == "data.present": return data_present(payload) if method == "data.movements": return data_movements(payload) if method == "data.virtual": return data_virtual(payload) if method == "changes.propose": return changes_propose(payload) if method == "modules.search": return search_modules(payload) if method == "modules.read": return read_module(payload) if method == "metadata.resolve_overrides": validation_error = validate_metadata_resolve_overrides_payload(payload) if validation_error: return validation_error return metadata_resolve_overrides(payload) if method == "code.search": validation_error = validate_code_search_payload(payload) if validation_error: return validation_error return code_search(payload) if method == "code.read": validation_error = validate_code_read_payload(payload) if validation_error: return validation_error return code_read(payload) if method == CODE_WRITE_METHOD: validation_error = validate_code_write_payload(payload) if validation_error: return validation_error return code_write(payload) if method == "code.symbol.resolve": validation_error = validate_code_symbol_resolve_payload(payload) if validation_error: return validation_error return code_symbol_resolve(payload) if method == "templates.bindings": validation_error = validate_templates_bindings_payload(payload) if validation_error: return validation_error return templates_bindings(payload) if method == "diagnostics.call_chain": validation_error = validate_diagnostics_call_chain_payload(payload) if validation_error: return validation_error return diagnostics_call_chain(payload) return {"schema": "onec_adapter_error.v1", "error": "unknown_method", "method": method, "known_methods": [row["name"] for row in METHODS]} def call_method(method: str, payload: dict[str, Any] | None) -> dict[str, Any]: if payload is None: payload = {} elif not isinstance(payload, dict): return invalid_argument(method, "payload", "payload must be a JSON object.") try: result = call_method_impl(method, payload) except Exception as exc: result = { "schema": "onec_adapter_method_error.v1", "status": "error", "method": method, "base_id": payload.get("base_id"), "error": "method_exception", "diagnostics": { "message": str(exc), }, } if truthy(payload.get("diagnostic") or payload.get("_allow_diagnostic")): result["diagnostics"]["traceback"] = traceback.format_exc(limit=8) if truthy(payload.get("diagnostic") or payload.get("_allow_diagnostic") or payload.get("include_storage")): return attach_write_history_operation(payload, method, result) if method in TECHNICAL_WRITE_METHODS: return attach_write_history_operation(payload, method, result) result = attach_write_history_operation(payload, method, result) return sanitize_public_result(result) GET_BOOL_PARAMS = { "include_storage", "include_semantic", "include_modules", "include_missing", "only_missing", "exact_counts", "refresh_cache", "preview", "include_text", "summary", "routines_only", "include_line_numbers", "include_context", "include_container_preview", "diagnostic", "_allow_diagnostic", } GET_INT_PARAMS = { "limit", "offset", "timeout_seconds", "ordinal", "index", "object_index", "module_ordinal", "module_index", "module_number", "scan_limit", "max_matches", "bsl_offset", "max_chars", "container_preview_chars", } def coerce_get_params(params: dict[str, Any]) -> dict[str, Any]: result = dict(params) for name in GET_BOOL_PARAMS: if name not in result: continue value = str(result.get(name) or "").strip().casefold() if value in {"true", "1", "yes", "on", "да"}: result[name] = True elif value in {"false", "0", "no", "off", "нет"}: result[name] = False for name in GET_INT_PARAMS: if name not in result: continue value = str(result.get(name) or "").strip() if re.fullmatch(r"-?\d+", value): result[name] = int(value) return result def adapter_service_token() -> str: return str(os.getenv("ONEC_ADAPTER_SERVICE_TOKEN") or "").strip() def adapter_request_authorized(authorization: Any) -> bool: expected = adapter_service_token() if not expected: return True provided = str(authorization or "").strip() if not provided.lower().startswith("bearer "): return False return hmac.compare_digest(provided[7:].strip(), expected) def sql_admin_config_path() -> Path: return Path(os.environ.get("ONEC_SQL_BASES_JSON_FILE") or "/data/onec-sql-bases.json") def sql_admin_load() -> dict[str, Any]: if str(os.environ.get("ONEC_SQL_BASES_JSON") or "").strip(): raise ValueError("Редактирование отключено: список задан через ONEC_SQL_BASES_JSON. Перенесите его в ONEC_SQL_BASES_JSON_FILE.") path = sql_admin_config_path() if not path.exists(): return {} data = json.loads(path.read_text(encoding="utf-8-sig")) if not isinstance(data, dict): raise ValueError("Файл списка баз должен содержать JSON-объект.") return data def sql_admin_public(config: dict[str, Any]) -> list[dict[str, Any]]: result = [] for base_id in sorted(config, key=str.casefold): item = config.get(base_id) if not isinstance(item, dict): continue password_env = str(item.get("password_env") or "") result.append({ "base_id": base_id, "server": str(item.get("server") or ""), "database": str(item.get("database") or ""), "user": str(item.get("user") or ""), "password_env": password_env, "has_password": bool(item.get("password") or (password_env and os.environ.get(password_env))), "repository": repository_control._public_config(item["repository"]) if isinstance(item.get("repository"), dict) else None, "development_layers": repository_control.public_development_layers(base_id), }) return result def sql_admin_save(config: dict[str, Any]) -> None: path = sql_admin_config_path() path.parent.mkdir(parents=True, exist_ok=True) temp = path.with_suffix(path.suffix + ".tmp") temp.write_text(json.dumps(config, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") try: os.chmod(temp, 0o600) except OSError: pass temp.replace(path) def repository_layer_connection_set(payload: dict[str, Any]) -> dict[str, Any]: """Persist one layer's repository connection fact in adapter-owned config.""" method = "repository.layer.connection.set" base_id_or_error = require_base_id(payload, method) if isinstance(base_id_or_error, dict): return base_id_or_error if payload.get("confirm_repository_connection_change") is not True: return invalid_argument( method, "confirm_repository_connection_change", "This changes adapter configuration only. Pass confirm_repository_connection_change=true after confirming the layer connection state.", ) base_id = base_id_or_error extension_guid = str(payload.get("extension_guid") or "").strip().lower() requested_layer = str(payload.get("layer_id") or "").strip().lower() if extension_guid: if not is_guid_text(extension_guid): return invalid_argument(method, "extension_guid", "extension_guid must be a GUID string.") layer_id = f"extension:{extension_guid}" else: layer_id = requested_layer or "base" if layer_id != "base" and not re.fullmatch(r"extension:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", layer_id): return invalid_argument(method, "layer_id", "Pass base or extension:.") connection_state = str(payload.get("connection_state") or "").strip().casefold() if connection_state not in repository_control.SUPPORTED_REPOSITORY_CONNECTION_STATES: return invalid_argument(method, "connection_state", "connection_state must be not_configured, configured, unavailable, or unknown.") repository_user = payload.get("repository_user") if repository_user is not None and (not isinstance(repository_user, str) or not repository_user.strip()): return invalid_argument(method, "repository_user", "repository_user must be a non-empty string when provided.") try: config = sql_admin_load() except ValueError as exc: return {"schema": "onec_repository_layer_connection_set.v1", "method": method, "base_id": base_id, "status": "blocked", "error": "adapter_configuration_not_writable", "message": str(exc)} item = config.get(base_id) if not isinstance(item, dict): return {"schema": "onec_repository_layer_connection_set.v1", "method": method, "base_id": base_id, "status": "not_found", "error": "base_not_configured"} layers = item.get("development_layers") if isinstance(item.get("development_layers"), dict) else {} if not layers and layer_id == "base": legacy_repository = item.get("repository") if isinstance(item.get("repository"), dict) else {} layers = {"base": {"repository": dict(legacy_repository), "support": {"mode": "unknown"}}} layer = layers.get(layer_id) if isinstance(layers.get(layer_id), dict) else {} repository = dict(layer.get("repository") or {}) if isinstance(layer.get("repository"), dict) else {} current_mode = str(repository.get("mode") or repository.get("lock_mode") or "unknown").strip().casefold() if connection_state == "not_configured": repository["mode"] = "none" repository["lock_mode"] = "manual" elif current_mode in {"none", "unknown"}: repository["mode"] = "manual" repository["lock_mode"] = "manual" repository["connection_state"] = connection_state if repository_user is not None: repository["repository_user"] = repository_user.strip() layers[layer_id] = {"repository": repository, "support": dict(layer.get("support") or {"mode": "unknown"})} item["development_layers"] = layers config[base_id] = item sql_admin_save(config) status = repository_control.status({"base_id": base_id, "layer_id": layer_id}) return { "schema": "onec_repository_layer_connection_set.v1", "method": method, "base_id": base_id, "layer_id": layer_id, "status": "saved", "connection_state": connection_state, "repository": status.get("repository"), "repository_status": status.get("status"), "message": "Saved in adapter configuration only; no 1C SQL database was changed.", } def sql_admin_validate(payload: dict[str, Any]) -> tuple[str, dict[str, Any]]: base_id = str(payload.get("base_id") or "").strip() if not re.fullmatch(r"[A-Za-z0-9_.-]{1,80}", base_id): raise ValueError("base_id: допустимы латинские буквы, цифры, точка, дефис и подчёркивание.") item = {key: str(payload.get(key) or "").strip() for key in ("server", "database", "user")} missing = [key for key, value in item.items() if not value] if missing: raise ValueError("Заполните обязательные поля: " + ", ".join(missing)) password = str(payload.get("password") or "") password_env = str(payload.get("password_env") or "").strip() if password_env: item["password_env"] = password_env elif password: item["password"] = password repository = payload.get("repository") development_layers = payload.get("development_layers") if development_layers is not None: if not isinstance(development_layers, dict): raise ValueError("development_layers должен быть JSON-объектом с ключами base и extension:.") normalized_layers: dict[str, Any] = {} for raw_layer_id, raw_layer in development_layers.items(): layer_id = str(raw_layer_id or "").strip().lower() if layer_id != "base" and not re.fullmatch(r"extension:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", layer_id): raise ValueError("development_layers: ключ должен быть base или extension:.") if not isinstance(raw_layer, dict): raise ValueError(f"development_layers.{layer_id} должен быть JSON-объектом.") raw_repository = raw_layer.get("repository") if isinstance(raw_layer.get("repository"), dict) else {"mode": "none"} repository_mode = str(raw_repository.get("mode") or raw_repository.get("lock_mode") or "none").strip().casefold() if repository_mode not in repository_control.SUPPORTED_REPOSITORY_MODES: raise ValueError(f"development_layers.{layer_id}.repository.mode: допустимы none, manual, automatic и unknown.") default_connection_state = "unknown" if repository_mode == "unknown" else ("not_configured" if repository_mode == "none" else "configured") connection_state = str(raw_repository.get("connection_state") or raw_repository.get("repository_connection") or raw_repository.get("connection") or default_connection_state).strip().casefold() if connection_state not in repository_control.SUPPORTED_REPOSITORY_CONNECTION_STATES: raise ValueError(f"development_layers.{layer_id}.repository.connection_state: допустимы not_configured, configured, unavailable и unknown.") raw_support = raw_layer.get("support") if isinstance(raw_layer.get("support"), dict) else {"mode": "unknown"} support_mode = str(raw_support.get("mode") or "unknown").strip().casefold() if support_mode not in repository_control.SUPPORTED_SUPPORT_MODES: raise ValueError(f"development_layers.{layer_id}.support.mode: допустимы none, editable, locked, rules и unknown.") rules = raw_support.get("rules") if isinstance(raw_support.get("rules"), dict) else {} normalized_layers[layer_id] = { "repository": {**raw_repository, "mode": repository_mode, "lock_mode": repository_mode if repository_mode != "none" else "manual", "connection_state": connection_state}, "support": {"mode": support_mode, **({"rules": {str(key): str(value) for key, value in rules.items()}} if rules else {})}, } item["development_layers"] = normalized_layers if repository is not None: if not isinstance(repository, dict): raise ValueError("repository должен быть JSON-объектом.") if repository.get("enabled") is False: item["repository"] = None else: backend = str(repository.get("backend") or "").strip().casefold() lock_mode = str(repository.get("lock_mode") or "automatic").strip().casefold() runner_url = str(repository.get("runner_url") or "").strip() runner_token_env = str(repository.get("runner_token_env") or "").strip() if backend not in repository_control.SUPPORTED_BACKENDS: raise ValueError("repository.backend: допустимы direct и karman_bridge.") if lock_mode not in repository_control.SUPPORTED_LOCK_MODES: raise ValueError("repository.lock_mode: допустимы automatic и manual.") if lock_mode == "automatic" and not runner_url: raise ValueError("Для repository требуется runner_url.") item["repository"] = { "backend": backend, "layer": str(repository.get("layer") or "base").strip().casefold(), "lock_mode": lock_mode, "bridge_id": str(repository.get("bridge_id") or "").strip(), "runtime_version": str(repository.get("runtime_version") or "").strip(), "repository_user": str(repository.get("repository_user") or "").strip(), "repository_password_env": str(repository.get("repository_password_env") or "").strip(), "infobase_user": str(repository.get("infobase_user") or "").strip(), "infobase_password_env": str(repository.get("infobase_password_env") or "").strip(), "runner": {"kind": "http", "url": runner_url, "token_env": runner_token_env}, } return base_id, item class Handler(BaseHTTPRequestHandler): server_version = "adapter-1c-rest/0.1" def log_message(self, fmt: str, *args: Any) -> None: print("%s - - [%s] %s" % (self.client_address[0], self.log_date_time_string(), fmt % args), flush=True) def read_json(self) -> dict[str, Any]: length = int(self.headers.get("Content-Length") or "0") if length <= 0: return {} raw = self.rfile.read(length).decode("utf-8-sig") return json.loads(raw) if raw.strip() else {} def write_json(self, status: int, payload: Any) -> None: body = json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8") self.send_response(status) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def write_static(self, path: Path, content_type: str) -> None: body = path.read_bytes() self.send_response(200) self.send_header("Content-Type", content_type) self.send_header("Content-Length", str(len(body))) self.send_header("Cache-Control", "no-store") self.end_headers() self.wfile.write(body) def handle_error(self, exc: BaseException) -> None: self.write_json( 500, { "schema": "onec_adapter_exception.v1", "error": str(exc), "traceback": traceback.format_exc(limit=8), }, ) def require_authorization(self, path: str) -> bool: allow_unauthenticated_admin = truthy(os.environ.get("ONEC_ADAPTER_ALLOW_UNAUTHENTICATED_ADMIN")) if path.startswith("/admin/api/") and allow_unauthenticated_admin: return True if path.startswith("/admin/api/") and not adapter_service_token() and not allow_unauthenticated_admin: self.write_json( 503, {"schema": "onec_adapter_auth.v1", "status": "not_configured", "error": "admin_auth_not_configured", "message": "Задайте ONEC_ADAPTER_SERVICE_TOKEN перед использованием управления базами."}, ) return False if path == "/health" or adapter_request_authorized(self.headers.get("Authorization")): return True self.write_json( 401, {"schema": "onec_adapter_auth.v1", "status": "unauthorized", "error": "bearer_token_required"}, ) return False def audit_request(self, method: str, path: str, rpc_method: str | None = None) -> None: print( json.dumps( { "event": "adapter_request", "time": datetime.now(timezone.utc).isoformat(), "client": self.client_address[0], "http_method": method, "path": path, **({"rpc_method": rpc_method} if rpc_method else {}), }, ensure_ascii=False, ), flush=True, ) def do_GET(self) -> None: try: parsed = urllib.parse.urlparse(self.path) if parsed.path in {"/admin", "/admin/", "/admin/app.js", "/admin/style.css"}: admin_dir = Path(__file__).resolve().parent / "admin" name, content_type = ({ "/admin/app.js": ("app.js", "application/javascript; charset=utf-8"), "/admin/style.css": ("style.css", "text/css; charset=utf-8"), }.get(parsed.path) or ("index.html", "text/html; charset=utf-8")) self.write_static(admin_dir / name, content_type) return if not self.require_authorization(parsed.path): return self.audit_request("GET", parsed.path) params = coerce_get_params({key: values[-1] for key, values in urllib.parse.parse_qs(parsed.query, keep_blank_values=True).items()}) if parsed.path == "/admin/api/bases": self.write_json(200, {"bases": sql_admin_public(sql_admin_load()), "config_path": str(sql_admin_config_path())}) elif parsed.path == "/admin/api/repository/requests": self.write_json(200, repository_control.admin_state(str(params.get("base_id") or ""))) elif parsed.path in HTTP_GET_METHOD_ROUTES: self.write_json(200, call_method(HTTP_GET_METHOD_ROUTES[parsed.path], params)) else: self.write_json(404, {"error": "not_found", "path": parsed.path}) except ValueError as exc: self.write_json(400, {"error": "invalid_config", "message": str(exc)}) except Exception as exc: self.handle_error(exc) def do_POST(self) -> None: try: parsed = urllib.parse.urlparse(self.path) if not self.require_authorization(parsed.path): return payload = self.read_json() if not isinstance(payload, dict): self.write_json(200, invalid_argument("http.post", "body", "HTTP JSON body must be an object.")) return self.audit_request("POST", parsed.path, str(payload.get("method") or "") if parsed.path == "/rpc" else None) if parsed.path == "/admin/api/bases": config = sql_admin_load() base_id, item = sql_admin_validate(payload) if base_id in config: self.write_json(409, {"error": "base_exists", "message": "База с таким base_id уже существует."}) return config[base_id] = item sql_admin_save(config) self.write_json(201, {"base": sql_admin_public({base_id: item})[0]}) elif parsed.path == "/rpc": self.write_json(200, call_method(str(payload.get("method") or ""), payload.get("payload") or {})) elif parsed.path in HTTP_POST_METHOD_ROUTES: self.write_json(200, call_method(HTTP_POST_METHOD_ROUTES[parsed.path], payload)) else: self.write_json(404, {"error": "not_found", "path": parsed.path}) except ValueError as exc: self.write_json(400, {"error": "invalid_config", "message": str(exc)}) except Exception as exc: self.handle_error(exc) def do_PUT(self) -> None: try: parsed = urllib.parse.urlparse(self.path) if not self.require_authorization(parsed.path): return parts = parsed.path.strip("/").split("/") if len(parts) != 4 or parts[:3] != ["admin", "api", "bases"]: self.write_json(404, {"error": "not_found"}) return old_id = urllib.parse.unquote(parts[3]) payload = self.read_json() config = sql_admin_load() if old_id not in config: self.write_json(404, {"error": "base_not_found"}) return base_id, item = sql_admin_validate(payload) old = config[old_id] if isinstance(config[old_id], dict) else {} if not item.get("password") and not item.get("password_env"): if old.get("password_env"): item["password_env"] = old["password_env"] elif old.get("password"): item["password"] = old["password"] if "repository" not in item and isinstance(old.get("repository"), dict): item["repository"] = old["repository"] if base_id != old_id and base_id in config: self.write_json(409, {"error": "base_exists", "message": "База с таким base_id уже существует."}) return del config[old_id] config[base_id] = item sql_admin_save(config) self.write_json(200, {"base": sql_admin_public({base_id: item})[0]}) except ValueError as exc: self.write_json(400, {"error": "invalid_config", "message": str(exc)}) except Exception as exc: self.handle_error(exc) def do_DELETE(self) -> None: try: parsed = urllib.parse.urlparse(self.path) if not self.require_authorization(parsed.path): return parts = parsed.path.strip("/").split("/") if len(parts) != 4 or parts[:3] != ["admin", "api", "bases"]: self.write_json(404, {"error": "not_found"}) return base_id = urllib.parse.unquote(parts[3]) config = sql_admin_load() if base_id not in config: self.write_json(404, {"error": "base_not_found"}) return del config[base_id] sql_admin_save(config) self.write_json(200, {"status": "deleted", "base_id": base_id}) except ValueError as exc: self.write_json(400, {"error": "invalid_config", "message": str(exc)}) except Exception as exc: self.handle_error(exc) def main() -> int: parser = argparse.ArgumentParser(description="Serve a read-first 1C REST adapter.") parser.add_argument("--host", default=os.environ.get("ONEC_ADAPTER_HOST", "0.0.0.0")) parser.add_argument("--port", type=int, default=int(os.environ.get("ONEC_ADAPTER_PORT", "8011"))) args = parser.parse_args() global STATE STATE = AdapterState() httpd = ThreadingHTTPServer((args.host, args.port), Handler) print(json.dumps({"event": "started", "host": args.host, "port": args.port}, ensure_ascii=False), flush=True) httpd.serve_forever() return 0 if __name__ == "__main__": raise SystemExit(main())