#!/usr/bin/env python3
# meta-decompile v0.65 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
#
# Зеркало meta-decompile.ps1 (КАНОН). Структура 1:1 — те же имена функций, порядок, комментарии.
# Изменения вносить СНАЧАЛА в .ps1, затем переносить сюда. lxml.etree — для полного XPath 1.0
# (SelectSingleNode/union/предикаты), как XmlNamespaceManager в .ps1.
#
# Поддержаны: Catalog, ExchangePlan, ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes, Document,
# InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, BusinessProcess, Task, Enum, ...
# Инверс meta-compile (omit-on-default: ключ эмитим только когда значение в XML отличается от умолчания
# компилятора). Неподдерживаемый тип / не-MetaDataObject root → exit 3 (ring3, как form-decompile).

import argparse
import os
import re
import sys
from lxml import etree

sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")

# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
    """parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
    argv = list(sys.argv[1:] if argv is None else argv)
    names = {s.lower(): s for a in parser._actions for s in a.option_strings}
    for i, tok in enumerate(argv):
        if tok.startswith('-') and tok.lower() in names:
            argv[i] = names[tok.lower()]
    # choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
    choice_map = {}
    for a in parser._actions:
        if a.choices:
            for s in a.option_strings:
                choice_map[s] = {str(c).lower(): c for c in a.choices}
    for i in range(len(argv) - 1):
        m = choice_map.get(argv[i])
        if m and argv[i + 1].lower() in m:
            argv[i + 1] = m[argv[i + 1].lower()]
    return parser.parse_args(argv)


# --- Namespaces (зеркало XmlNamespaceManager) ---
NS_MD = "http://v8.1c.ru/8.3/MDClasses"
NS_V8 = "http://v8.1c.ru/8.1/data/core"
NS_XR = "http://v8.1c.ru/8.3/xcf/readable"
NS_XSI = "http://www.w3.org/2001/XMLSchema-instance"
NS_APP = "http://v8.1c.ru/8.2/managed-application/core"
NS = {'md': NS_MD, 'v8': NS_V8, 'xr': NS_XR, 'xsi': NS_XSI, 'app': NS_APP}

# Скрипт-скоуп состояние (зеркало $script:* / module-level $props/$dsl/$objType канона). Ставятся в main().
props = None
obj_node = None
obj_type = None
obj_name = None
dsl = None
object_path = None


# --- XML-обёртки (зеркало SelectSingleNode/SelectNodes/GetAttribute/InnerText) ---
def _single(node, xpath):
    if node is None:
        return None
    r = node.xpath(xpath, namespaces=NS)
    return r[0] if r else None


def _nodes(node, xpath):
    if node is None:
        return []
    return node.xpath(xpath, namespaces=NS)


def _text(node):
    """InnerText — конкатенация всего текста-потомка (PreserveWhitespace=true у lxml по умолчанию)."""
    if node is None:
        return None
    return ''.join(node.itertext())


def _ns_of_prefix(node, prefix):
    """URI по префиксу — зеркало GetNamespaceOfPrefix из PS. lxml отдаёт nsmap с учётом
    унаследованных объявлений, поэтому локальный xmlns:dNpM на самом теге тоже виден."""
    if node is None:
        return None
    return node.nsmap.get(prefix)


def _attr(node, name, ns=None):
    """GetAttribute(name[, ns]) — .NET возвращает '' для отсутствующего атрибута, lxml → None."""
    if node is None:
        return ''
    key = ('{%s}%s' % (ns, name)) if ns else name
    v = node.get(key)
    return v if v is not None else ''


def _localname(el):
    t = el.tag
    if not isinstance(t, str):
        return ''
    return t.split('}', 1)[1] if '}' in t else t


# --- JSON-эмиттер (контроль порядка/массивов/кириллицы) ---
def _json_num(n):
    """Зеркало "$node" для int/long/double."""
    if isinstance(n, bool):
        return 'true' if n else 'false'
    if isinstance(n, int):
        return str(n)
    r = repr(float(n))
    if r.endswith('.0'):
        r = r[:-2]
    return r


def convert_to_compact_json(node, depth=0):
    pad = "  " * depth
    pad1 = "  " * (depth + 1)
    if node is None:
        return "null"
    if isinstance(node, bool):
        return "true" if node else "false"
    if isinstance(node, (int, float)):
        return _json_num(node)
    if isinstance(node, dict):
        if len(node) == 0:
            return "{}"
        items = []
        for k in node.keys():
            items.append("%s%s: %s" % (pad1, quote_json(str(k)), convert_to_compact_json(node[k], depth + 1)))
        return "{\n" + ",\n".join(items) + "\n" + pad + "}"
    if isinstance(node, list):
        if len(node) == 0:
            return "[]"
        # Массив скаляров-строк — компактно в строку; массив объектов — по строкам.
        all_scalar = True
        for e in node:
            if isinstance(e, (dict, list)):
                all_scalar = False
                break
        if all_scalar:
            items = [convert_to_compact_json(e, depth + 1) for e in node]
            return "[" + ", ".join(items) + "]"
        items = ["%s%s" % (pad1, convert_to_compact_json(e, depth + 1)) for e in node]
        return "[\n" + ",\n".join(items) + "\n" + pad + "]"
    return quote_json(str(node))


def quote_json(s):
    sb = ['"']
    for ch in s:
        if ch == '"':
            sb.append('\\"')
        elif ch == '\\':
            sb.append('\\\\')
        elif ch == '\n':
            sb.append('\\n')
        elif ch == '\r':
            sb.append('\\r')
        elif ch == '\t':
            sb.append('\\t')
        else:
            if ord(ch) < 32:
                sb.append('\\u%04x' % ord(ch))
            else:
                sb.append(ch)
    sb.append('"')
    return ''.join(sb)


# --- Сравнение регистров (зеркало PS -ne / -cne) ---
def ne_cs(a, b):
    """PS -cne на строках (регистрочувствительно). True когда различаются с учётом регистра.
    Зеркало компилятора: авто-синоним/описание опускаем только при ТОЧНОМ совпадении со split_camel_words."""
    return (a if a is not None else '') != (b if b is not None else '')


def split_camel_words(name):
    """Авто-синоним: точное зеркало Split-CamelCase из meta-compile (split_camel_case, HE-эвристика).
    ВАЖНО: логика должна совпадать байт-в-байт с компилятором, иначе ложные «синоним==авто» → диффы."""
    if not name:
        return name
    result = re.sub(r'([а-яё])([А-ЯЁ])', r'\1 \2', name)
    result = re.sub(r'([a-z])([A-Z])', r'\1 \2', result)
    # HE: сохраняем прогон заглавных >=2, если сразу за ним НЕ буква (пробел/цифра/спецсимвол/конец);
    # предлоги/бренды перед буквой → лоуэркейз. Первый символ — как есть.
    if len(result) > 1:
        chars = list(result)
        n = len(chars)
        keep = [False] * n
        i = 0
        while i < n:
            if chars[i].isupper():
                j = i
                while j < n and chars[j].isupper():
                    j += 1
                after_boundary = (j == n) or (not chars[j].isalpha())
                if j - i >= 2 and after_boundary:
                    for k in range(i, j):
                        keep[k] = True
                i = j
            else:
                i += 1
        out = []
        for idx, c in enumerate(chars):
            if idx == 0 or keep[idx]:
                out.append(c)
            elif c.isupper():
                out.append(c.lower())
            else:
                out.append(c)
        result = ''.join(out)
    return result


def P(tag):
    n = _single(props, "md:" + tag)
    return _text(n) if n is not None else None


# --- Synonym (ru) ---
def get_ml_ru(node):
    if node is None:
        return None
    it = _single(node, "v8:item[v8:lang='ru']/v8:content")
    if it is not None:
        return _text(it)
    return None


def get_ml_value(node):
    """ML-значение → строка (если единственный item ru) ЛИБО dict{lang:content} (мультиязычно, порядок из XML).
    None если контента нет. Компактная строка для ru-only, объект для мультиязычных."""
    if node is None:
        return None
    items = _nodes(node, 'v8:item')
    if len(items) == 0:
        return None
    if len(items) == 1:
        lang = _text(_single(items[0], 'v8:lang'))
        content = _text(_single(items[0], 'v8:content'))
        # Единственный ru-item: пустое содержимое ≡ отсутствие значения → None (иначе tooltip:"" ≠ self-close).
        if lang == 'ru':
            return None if content == '' else content
    o = {}
    for it in items:
        l = _text(_single(it, 'v8:lang'))
        c = _text(_single(it, 'v8:content'))
        o[l] = c
    return o


# --- Тип реквизита: <Type> → shorthand-строка ---
def strip_ns_prefix(s):
    if ':' in s:
        return s.split(':', 1)[1]
    return s


def get_type_shorthand(type_node):
    if type_node is None:
        return ""
    parts = []
    children = [c for c in type_node if isinstance(c.tag, str)]
    for i in range(len(children)):
        c = children[i]
        ln = _localname(c)
        if ln == 'Type':
            raw = _text(c).strip()
            nxt = children[i + 1] if i + 1 < len(children) else None
            if re.search(r'(^|:)boolean$', raw, re.I):
                parts.append('Boolean')
            elif re.search(r'(^|:)string$', raw, re.I):
                length = '10'
                al = ''
                sq = nxt if (nxt is not None and _localname(nxt) == 'StringQualifiers') else _single(type_node, 'v8:StringQualifiers')
                if sq is not None:
                    l = _single(sq, 'v8:Length')
                    if l is not None:
                        length = _text(l)
                    aln = _single(sq, 'v8:AllowedLength')
                    if aln is not None and _text(aln) == 'Fixed':
                        al = ',fixed'
                parts.append("String(%s%s)" % (length, al))
            elif re.search(r'(^|:)decimal$', raw, re.I):
                d = '10'
                f = '0'
                sign = ''
                nq = nxt if (nxt is not None and _localname(nxt) == 'NumberQualifiers') else _single(type_node, 'v8:NumberQualifiers')
                if nq is not None:
                    dn = _single(nq, 'v8:Digits')
                    if dn is not None:
                        d = _text(dn)
                    fn = _single(nq, 'v8:FractionDigits')
                    if fn is not None:
                        f = _text(fn)
                    sn = _single(nq, 'v8:AllowedSign')
                    if sn is not None and _text(sn) == 'Nonnegative':
                        sign = ',nonneg'
                parts.append("Number(%s,%s%s)" % (d, f, sign))
            elif re.search(r'(^|:)dateTime$', raw, re.I):
                fr = 'DateTime'
                dq = nxt if (nxt is not None and _localname(nxt) == 'DateQualifiers') else _single(type_node, 'v8:DateQualifiers')
                if dq is not None:
                    dn = _single(dq, 'v8:DateFractions')
                    if dn is not None:
                        fr = _text(dn)
                parts.append(fr)   # Date | DateTime
            elif re.search(r'(^|:)base64Binary$', raw, re.I):
                parts.append('ValueStorage')
            else:
                parts.append(strip_ns_prefix(raw))   # cfg:CatalogRef.X → CatalogRef.X
        elif ln == 'TypeSet':
            parts.append(strip_ns_prefix(_text(c).strip()))   # cfg:DefinedType.X → DefinedType.X
    return ' + '.join(parts)


# Скалярное значение параметра выбора (<Value xsi:type=...>) → JSON-значение (bool/число/строка).
def get_xdto_type_value(node):
    """XDTO-тип: префиксное значение (d6p1:Local) -> нотация Кларка "{uri}Local".

    Префиксы платформы (dNpM) произвольны и переносу не подлежат; стандартные (xs:, v8:, xr:)
    оставляем дословно — компилятор их так и пишет.
    """
    if node is None:
        return None
    txt = _text(node)
    m = re.match(r'^([\w.\-]+):(.+)$', txt or '')
    if m:
        prefix, local = m.group(1), m.group(2)
        if prefix not in ('xs', 'xsi', 'v8', 'xr'):
            uri = _ns_of_prefix(node, prefix)
            if uri:
                return '{%s}%s' % (uri, local)
    return txt


def convert_ch_scalar_node(vN):
    # nil-элемент массива (<v8:Value xsi:nil="true"/>) -> JSON null. Без этого он приезжал пустой
    # строкой и компилятор эмитил xs:string вместо nil.
    if _attr(vN, 'nil', NS_XSI) == 'true':
        return None
    xt = _attr(vN, 'type', NS_XSI)
    txt = _text(vN)
    if re.search(r'boolean$', xt, re.I):
        return txt == 'true'
    if re.search(r'decimal$', xt, re.I):
        if re.match(r'^-?\d+$', txt):
            return int(txt)
        return float(txt)
    # Пустой DesignTimeRef != пустая строка: без маркера тип терялся, и компилятор эмитил xs:string.
    # Та же конвенция, что у fillValue — маркер emptyRef.
    if re.search(r'DesignTimeRef$', xt, re.I) and txt == '':
        return {'emptyRef': True}
    return txt


# app:value (тип прямо на узле) → значение ЛИБО список (v8:FixedArray с детьми v8:Value).
def get_choice_param_value(val_node):
    xt = _attr(val_node, 'type', NS_XSI)
    if re.search(r'FixedArray$', xt, re.I):
        arr = []
        for sub in _nodes(val_node, 'v8:Value'):
            arr.append(convert_ch_scalar_node(sub))
        return arr
    return convert_ch_scalar_node(val_node)


# <ChoiceParameterLinks> → [{name,dataPath,valueChange?}] | строки "name=dataPath". tag: 'md:...' | 'xr:...'.
def parse_choice_parameter_links(parent, tag):
    node = _single(parent, tag)
    if node is None:
        return None
    links = _nodes(node, 'xr:Link')
    if len(links) == 0:
        return None
    arr = []
    for lk in links:
        l_name = _text(_single(lk, 'xr:Name'))
        l_dp = _text(_single(lk, 'xr:DataPath'))
        vc_n = _single(lk, 'xr:ValueChange')
        vcv = _text(vc_n) if vc_n is not None else 'Clear'
        if vcv == 'Clear':
            arr.append("%s=%s" % (l_name, l_dp))
        else:
            arr.append({'name': l_name, 'dataPath': l_dp, 'valueChange': vcv})
    # PS-трап: `return $arr` БЕЗ унарной запятой → одноэлементный ArrayList разворачивается в скаляр
    # (в отличие от Get-ChoiceParamValue с `,$arr`). Зеркалим: len==1 → голый элемент.
    return arr[0] if len(arr) == 1 else arr


# <ChoiceParameters> → [{name,value?}]. tag: 'md:...' | 'xr:...'.
def parse_choice_parameters(parent, tag):
    node = _single(parent, tag)
    if node is None:
        return None
    items = _nodes(node, 'app:item')
    if len(items) == 0:
        return None
    arr = []
    for it in items:
        p_name = _attr(it, 'name')
        val_n = _single(it, 'app:value')
        nil_attr = _attr(val_n, 'nil', NS_XSI) if val_n is not None else ''
        if val_n is None or nil_attr == 'true':
            arr.append({'name': p_name})
        else:
            o = {'name': p_name}
            o['value'] = get_choice_param_value(val_n)
            arr.append(o)
    # PS-трап (как выше): одноэлементный ArrayList разворачивается в скаляр при `return $arr`.
    return arr[0] if len(arr) == 1 else arr


# --- Реквизит → DSL: shorthand-строка "Имя: Тип | флаги" ЛИБО object-форма при кастомном синониме. ---
def attr_to_dsl(attr_node):
    ap = _single(attr_node, 'md:Properties')
    nm = _text(_single(ap, 'md:Name'))
    ts = get_type_shorthand(_single(ap, 'md:Type'))
    flags = []
    fc = _single(ap, 'md:FillChecking')
    if fc is not None and _text(fc) == 'ShowError':
        flags.append('req')
    ix = _single(ap, 'md:Indexing')
    if ix is not None:
        ixt = _text(ix)
        if ixt == 'Index':
            flags.append('index')
        elif ixt == 'IndexWithAdditionalOrder':
            flags.append('indexAdditional')
    ml = _single(ap, 'md:MultiLine')
    if ml is not None and _text(ml) == 'true':
        flags.append('multiline')

    # Синоним/подсказка (строка ru-only ИЛИ {ru,en}).
    syn_node = _single(ap, 'md:Synonym')
    syn_val = get_ml_value(syn_node)
    syn_custom = False
    # Пустой <Synonym/> (узел есть, значения нет) ≠ авто-синоним из имени → явный пустой (synonym:"").
    syn_empty = (syn_node is not None and syn_val is None)
    if isinstance(syn_val, str):
        if ne_cs(syn_val, split_camel_words(nm)):
            syn_custom = True
    elif syn_val is not None:
        syn_custom = True   # {ru,en} = всегда кастом
    tt_val = get_ml_value(_single(ap, 'md:ToolTip'))

    # Extra-свойства реквизита (omit-on-default). Наличие любого → object-форма.
    def en(tag):
        n = _single(ap, "md:" + tag)
        return _text(n) if n is not None else None

    extra = {}
    v = en('Comment')
    if v:
        extra['comment'] = v
    v = en('FullTextSearch')
    if v and v != 'Use':
        extra['fullTextSearch'] = v
    v = en('FillFromFillingValue')
    if v == 'true':
        extra['fillFromFillingValue'] = True
    v = en('CreateOnInput')
    if v and v != 'Auto':
        extra['createOnInput'] = v
    v = en('QuickChoice')
    if v and v != 'Auto':
        extra['quickChoice'] = v
    v = en('DataHistory')
    if v and v != 'Use':
        extra['dataHistory'] = v
    v = en('Use')
    if v and v != 'ForItem':
        extra['use'] = v
    v = en('PasswordMode')
    if v == 'true':
        extra['passwordMode'] = True
    v = en('Mask')
    if v:
        extra['mask'] = v
    v = en('ChoiceHistoryOnInput')
    if v and v != 'Auto':
        extra['choiceHistoryOnInput'] = v
    v = en('FillChecking')
    if v == 'ShowWarning':
        extra['fillChecking'] = 'ShowWarning'
    v = en('ExtendedEdit')
    if v == 'true':
        extra['extendedEdit'] = True
    v = en('MarkNegatives')
    if v == 'true':
        extra['markNegatives'] = True
    v = en('ChoiceFoldersAndItems')
    if v and v != 'Items':
        extra['choiceFoldersAndItems'] = v
    v = en('ChoiceForm')
    if v:
        extra['choiceForm'] = v
    # Регистро-специфика измерения (теги присутствуют только у Dimension → безвредно для прочих).
    v = en('Master')
    if v == 'true':
        extra['master'] = True
    v = en('MainFilter')
    if v == 'true':
        extra['mainFilter'] = True
    v = en('DenyIncompleteValues')
    if v == 'true':
        extra['denyIncompleteValues'] = True
    v = en('UseInTotals')
    if v == 'false':
        extra['useInTotals'] = False   # дефолт true → захват при false
    # Формат 2.20: режим приведения типов измерения РС. Дефолт TransformValues (его компилятор
    # эмитит сам) → захватываем только отклонение.
    v = en('TypeReductionMode')
    if v and v != 'TransformValues':
        extra['typeReductionMode'] = v
    v = en('BaseDimension')
    if v == 'true':
        extra['baseDimension'] = True
    v = en('ScheduleLink')
    if v:
        extra['scheduleLink'] = v   # ссылка на измерение графика (пустой → пропуск)
    v = en('Balance')
    if v == 'true':
        extra['balance'] = True
    v = en('AccountingFlag')
    if v:
        extra['accountingFlag'] = v   # ссылка на признак учёта ПС (пустой → пропуск)
    v = en('ExtDimensionAccountingFlag')
    if v:
        extra['extDimensionAccountingFlag'] = v
    v = en('AddressingDimension')
    if v:
        extra['addressingDimension'] = v   # ссылка на измерение регистра исполнителей
    # MinValue/MaxValue — граница диапазона (omit при nil). Тип сохраняем: xs:string→строка, xs:decimal→число.
    for mm in ('MinValue', 'MaxValue'):
        mn = _single(ap, "md:" + mm)
        if mn is None:
            continue
        if _attr(mn, 'nil', NS_XSI) == 'true':
            continue
        key = 'minValue' if mm == 'MinValue' else 'maxValue'
        xt = _attr(mn, 'type', NS_XSI)
        txt = _text(mn)
        if re.search(r'decimal|int|double|float', xt, re.I):
            extra[key] = int(txt) if re.match(r'^-?\d+$', txt) else float(txt)
        else:
            extra[key] = txt
    fmt_v = get_ml_value(_single(ap, 'md:Format'))
    if fmt_v is not None:
        extra['format'] = fmt_v
    efmt_v = get_ml_value(_single(ap, 'md:EditFormat'))
    if efmt_v is not None:
        extra['editFormat'] = efmt_v

    # FillValue (значение заполнения). Форма по умолчанию зависит от типа реквизита: String→typed-empty,
    # Number→zero, всё остальное→nil. Эмитим `fillValue` только при отклонении от дефолта (§4.2 spec).
    fv_node = _single(ap, 'md:FillValue')
    if fv_node is not None:
        fcat = 'Other'
        if re.search(r'\+', ts):
            fcat = 'Other'
        elif re.search(r'^Boolean', ts):
            fcat = 'Boolean'
        elif re.search(r'^String', ts):
            fcat = 'String'
        elif re.search(r'^Number', ts):
            fcat = 'Number'
        elif re.search(r'^(Date|DateTime)', ts):
            fcat = 'Date'
        nil_attr = _attr(fv_node, 'nil', NS_XSI)
        xsi_t = _attr(fv_node, 'type', NS_XSI)
        fv_text = _text(fv_node)
        if nil_attr == 'true':
            if fcat == 'String' or fcat == 'Number':
                extra['fillValue'] = None   # nil-override
            # иначе nil — это дефолт → пропускаем
        elif re.search(r'boolean$', xsi_t, re.I):
            extra['fillValue'] = (fv_text == 'true')
        elif re.search(r'decimal$', xsi_t, re.I):
            # Захватываем как ЧИСЛО (не строку): на составном типе компилятор берёт xsi-тип из JSON-значения.
            if not (fcat == 'Number' and (fv_text == '0' or fv_text == '')):
                extra['fillValue'] = int(fv_text) if re.match(r'^-?\d+$', fv_text) else float(fv_text)
        elif re.search(r'string$', xsi_t, re.I):
            if not (fcat == 'String' and fv_text == ''):
                extra['fillValue'] = fv_text
        elif re.search(r'dateTime$', xsi_t, re.I):
            extra['fillValue'] = fv_text
        elif re.search(r'DesignTimeRef$', xsi_t, re.I):
            # Пустой DTR ≠ nil/xs:string → маркер emptyRef (иначе тип терялся в xs:string).
            if fv_text == '':
                extra['fillValue'] = {'emptyRef': True}
            else:
                extra['fillValue'] = fv_text

    # LinkByType (связь по типу): DataPath + LinkItem. Пусто → пропускаем. linkItem=0 → компактно строкой.
    lbt_node = _single(ap, 'md:LinkByType')
    if lbt_node is not None:
        dp_n = _single(lbt_node, 'xr:DataPath')
        if dp_n is not None and _text(dp_n):
            li_n = _single(lbt_node, 'xr:LinkItem')
            li = int(_text(li_n)) if (li_n is not None and _text(li_n)) else 0
            if li == 0:
                extra['linkByType'] = _text(dp_n)
            else:
                extra['linkByType'] = {'dataPath': _text(dp_n), 'linkItem': li}

    cpl_arr = parse_choice_parameter_links(ap, 'md:ChoiceParameterLinks')
    if cpl_arr is not None:
        extra['choiceParameterLinks'] = cpl_arr
    cp_arr = parse_choice_parameters(ap, 'md:ChoiceParameters')
    if cp_arr is not None:
        extra['choiceParameters'] = cp_arr

    # Пустой <Type/> (реквизит без типа) → ts=''. Отличаем от «дефолтного» отсутствия: явный type:''.
    type_empty = (ts == '')
    if syn_custom or syn_empty or (tt_val is not None) or len(extra) > 0 or type_empty:
        o = {'name': nm}
        if ts:
            o['type'] = ts
        elif type_empty:
            o['type'] = ''
        if syn_custom:
            o['synonym'] = syn_val
        elif syn_empty:
            o['synonym'] = ''
        if tt_val is not None:
            o['tooltip'] = tt_val
        for k in extra:
            o[k] = extra[k]
        if len(flags) > 0:
            o['flags'] = list(flags)
        return o
    head = ("%s: %s" % (nm, ts)) if ts else nm
    if len(flags) > 0:
        return head + " | " + ", ".join(flags)
    return head


# --- Свойства (omit-on-default). ---
def add_bool_prop(key, tag, default):
    v = P(tag)
    if v is not None:
        b = (v == 'true')
        if b != default:
            dsl[key] = b


def add_enum_prop(key, tag, default):
    v = P(tag)
    if v is not None and v != '' and v != default:
        dsl[key] = v


def add_int_prop(key, tag, default):
    v = P(tag)
    if v is not None and v != '':
        iv = int(v)
        if iv != default:
            dsl[key] = iv


# Общий захват структурного <Picture> (зеркало Emit-CommandPicture). Пишет в tgt ключи picture/loadTransparent.
def get_picture_to_dsl(props_node, tgt):
    ref_n = _single(props_node, 'md:Picture/xr:Ref')
    abs_n = _single(props_node, 'md:Picture/xr:Abs')
    if ref_n is not None or abs_n is not None:
        psrc = _text(ref_n) if ref_n is not None else ("abs:" + _text(abs_n))
        lt_n = _single(props_node, 'md:Picture/xr:LoadTransparent')
        lt_false = (lt_n is not None and _text(lt_n) == 'false')
        tpx_n = _single(props_node, 'md:Picture/xr:TransparentPixel')
        if tpx_n is not None:
            po = {'src': psrc}
            if lt_false:
                po['loadTransparent'] = False
            po['transparentPixel'] = {'x': int(_attr(tpx_n, 'x')), 'y': int(_attr(tpx_n, 'y'))}
            tgt['picture'] = po
        else:
            tgt['picture'] = psrc
            if lt_false:
                tgt['loadTransparent'] = False


# Короткая форма поля: <Type>.<Name>.StandardAttribute.X / .Attribute.X → StandardAttribute.X / Attribute.X
def short_field(full):
    m = re.search(r'\.(StandardAttribute|Attribute)\.(.+)$', full)
    if m:
        return "%s.%s" % (m.group(1), m.group(2))
    return full


def add_form_ref(key, tag):
    v = P(tag)
    if v:
        dsl[key] = v


# --- Characteristics: короткая форма поля bare/partial. ---
def shorten_char_field(full, frm):
    if full.startswith(frm + "."):
        rest = full[len(frm) + 1:]
        m = re.match(r'^StandardAttribute\.(Ref|Parent|Owner)$', rest)
        if m:
            return m.group(1)   # ссылочные станд. → голое
        m = re.match(r'^Attribute\.(.+)$', rest)
        if m:
            return m.group(1)   # кастом → голое
        return rest   # прочие StandardAttribute.X / Dimension.X / Resource.X → частичное
    return full


# --- Предопределённые (local-name xpath, без ns-менеджера) ---
def _lx1(el, xp):
    r = el.xpath(xp)
    return r[0] if r else None


def _lxn(el, xp):
    return el.xpath(xp)


def predef_item_to_dsl(item_el):
    name = _text(_lx1(item_el, "*[local-name()='Name']"))
    code_el = _lx1(item_el, "*[local-name()='Code']")
    code = _text(code_el) if (code_el is not None and _text(code_el)) else ''
    desc_el = _lx1(item_el, "*[local-name()='Description']")
    desc = _text(desc_el) if desc_el is not None else ''
    folder_el = _lx1(item_el, "*[local-name()='IsFolder']")
    is_folder = (folder_el is not None and _text(folder_el) == 'true')
    child_container = _lx1(item_el, "*[local-name()='ChildItems']")
    kids = _lxn(child_container, "*[local-name()='Item']") if child_container is not None else []
    # Type — тип значения предопределённой характеристики (ПВХ). Наличие узла → object-форма с ключом type.
    type_el = _lx1(item_el, "*[local-name()='Type']")
    type_str = get_type_shorthand(type_el) if type_el is not None else None
    auto = split_camel_words(name)

    # Компактная строка для плоских: без узла Type (Catalog) ИЛИ с непустым типом → "(Код) Имя [Наим]: Тип".
    # Сокращение неоднозначно, если значение содержит собственные разделители грамматики:
    # ')' или ':' в коде, пробел/скобка/':' в имени, скобки в наименовании. Компилятор читает код
    # как [^)]*, а имя как \S+ — на "114 (108)" разбор рассыпается и элемент терял имя и код.
    ambiguous = bool(re.search(r'[):]', code or '')) or bool(re.search(r'[\s:\[\]()]', name or ''))         or bool(re.search(r'[\[\]]', desc or ''))
    if (not is_folder) and len(kids) == 0 and (type_str is None or type_str != '') and not ambiguous:
        s = ("(%s) %s" % (code, name)) if code else name
        if desc == '':
            s = s + " []"
        elif ne_cs(desc, auto):
            s = s + (" [%s]" % desc)
        if type_str:
            s = "%s: %s" % (s, type_str)
        return s
    # Группа/иерархия/с типом → объект.
    o = {'name': name}
    if code:
        o['code'] = code
    if desc == '':
        o['description'] = ''
    elif ne_cs(desc, auto):
        o['description'] = desc
    if type_str is not None:
        o['type'] = type_str
    if is_folder:
        o['isFolder'] = True
    if len(kids) > 0:
        sub = []
        for k in kids:
            sub.append(predef_item_to_dsl(k))
        o['childItems'] = sub
    return o


def predef_account_to_dsl(item_el):
    name = _text(_lx1(item_el, "*[local-name()='Name']"))
    code_el = _lx1(item_el, "*[local-name()='Code']")
    code = _text(code_el) if (code_el is not None and _text(code_el)) else ''
    desc_el = _lx1(item_el, "*[local-name()='Description']")
    desc = _text(desc_el) if desc_el is not None else ''
    at_el = _lx1(item_el, "*[local-name()='AccountType']")
    acct_type = _text(at_el) if at_el is not None else 'ActivePassive'
    off_el = _lx1(item_el, "*[local-name()='OffBalance']")
    off = (off_el is not None and _text(off_el) == 'true')
    ord_el = _lx1(item_el, "*[local-name()='Order']")
    order = _text(ord_el) if ord_el is not None else ''
    auto = split_camel_words(name)
    # TRUE-флаги (leaf после последней точки в ref).
    true_flags = []
    for fl in _lxn(item_el, "*[local-name()='AccountingFlags']/*[local-name()='Flag']"):
        if _text(fl) == 'true':
            r = _attr(fl, 'ref')
            true_flags.append(r.split('.')[-1])
    # ExtDimensionTypes → subconto. Короткая запись "Тип | Признак1, Признак2".
    edt_pfx = (dsl['extDimensionTypes'] + ".") if dsl.get('extDimensionTypes') else None
    subconto = []
    for edt in _lxn(item_el, "*[local-name()='ExtDimensionTypes']/*[local-name()='ExtDimensionType']"):
        sc_t = _attr(edt, 'name')
        if edt_pfx and sc_t.startswith(edt_pfx):
            sc_t = sc_t[len(edt_pfx):]
        t_el = _lx1(edt, "*[local-name()='Turnover']")
        sc_flags_out = []
        if t_el is not None and _text(t_el) == 'true':
            sc_flags_out.append('Turnover')
        for fl in _lxn(edt, "*[local-name()='AccountingFlags']/*[local-name()='Flag']"):
            if _text(fl) == 'true':
                r = _attr(fl, 'ref')
                sc_flags_out.append(r.split('.')[-1])
        if len(sc_flags_out) > 0:
            subconto.append(sc_t + " | " + ", ".join(sc_flags_out))
        else:
            subconto.append(sc_t)
    child_container = _lx1(item_el, "*[local-name()='ChildItems']")
    kids = _lxn(child_container, "*[local-name()='Item']") if child_container is not None else []

    o = {'name': name}
    if code:
        o['code'] = code
    # -cne (регистрочувствительно!): хвостовые аббревиатуры (ОС/НМА) теряли бы регистр при -ne.
    if desc != auto:
        o['description'] = desc
    o['accountType'] = acct_type
    if off:
        o['offBalance'] = True
    o['order'] = order
    if len(true_flags) > 0:
        o['flags'] = list(true_flags)
    if len(subconto) > 0:
        o['subconto'] = subconto
    if len(kids) > 0:
        sub = []
        for k in kids:
            sub.append(predef_account_to_dsl(k))
        o['childItems'] = sub
    return o


def predef_calc_type_to_dsl(item_el):
    name = _text(_lx1(item_el, "*[local-name()='Name']"))
    code_el = _lx1(item_el, "*[local-name()='Code']")
    code = _text(code_el) if (code_el is not None and _text(code_el)) else ''
    desc_el = _lx1(item_el, "*[local-name()='Description']")
    desc = _text(desc_el) if desc_el is not None else ''
    apib_el = _lx1(item_el, "*[local-name()='ActionPeriodIsBase']")
    apib = (apib_el is not None and _text(apib_el) == 'true')
    auto = split_camel_words(name)
    if not apib:
        s = ("(%s) %s" % (code, name)) if code else name
        if desc == '':
            s = s + " []"
        elif desc != auto:   # -cne
            s = s + (" [%s]" % desc)
        return s
    o = {'name': name}
    if code:
        o['code'] = code
    if desc != auto:   # -cne
        o['description'] = desc
    o['actionPeriodIsBase'] = True
    return o


def build_dsl():
    """Линейный поток сборки DSL — зеркало script-body .ps1 (стр.392–1459)."""
    global dsl

    # === Сборка DSL ===
    dsl = {'type': obj_type, 'name': obj_name}

    # Синоним объекта: строка ru-only ИЛИ {ru,en}. Кастом → эмитим. Пустой <Synonym/> → явный synonym:"".
    syn_node_obj = _single(props, 'md:Synonym')
    syn_val = get_ml_value(syn_node_obj)
    if isinstance(syn_val, str):
        if ne_cs(syn_val, split_camel_words(obj_name)):
            dsl['synonym'] = syn_val
    elif syn_val is not None:
        dsl['synonym'] = syn_val
    elif syn_node_obj is not None:
        dsl['synonym'] = ''
    cmt = P('Comment')
    if cmt:
        dsl['comment'] = cmt

    # Свойства Catalog (omit-on-default).
    add_bool_prop('hierarchical', 'Hierarchical', False)
    add_enum_prop('hierarchyType', 'HierarchyType', 'HierarchyFoldersAndItems')
    add_bool_prop('limitLevelCount', 'LimitLevelCount', False)
    add_int_prop('levelCount', 'LevelCount', 2)
    add_bool_prop('foldersOnTop', 'FoldersOnTop', True)
    # owners
    owners_node = _single(props, 'md:Owners')
    if owners_node is not None:
        items = [_text(x) for x in _nodes(owners_node, 'xr:Item')]
        if len(items) > 0:
            dsl['owners'] = [(s.split('.', 1)[1] if re.match(r'^Catalog\.', s) else s) for s in items]
    add_enum_prop('subordinationUse', 'SubordinationUse', 'ToItems')
    # Тип-зависимые дефолты (компилятор задаёт их по типу — декомпилятор обязан зеркалить).
    descr_len_def = {'ExchangePlan': 150, 'ChartOfCharacteristicTypes': 100, 'ChartOfCalculationTypes': 100}.get(obj_type, 25)
    code_len_def = 5 if obj_type == 'ChartOfCalculationTypes' else 9
    create_inp_def = 'Use' if obj_type in ('Catalog', 'Document') else 'DontUse'
    data_lock_def = 'Managed'  # компилятор эмитит Managed по умолчанию для всех типов (авторинг); Automatic несётся в DSL явно
    code_series_def = {'ChartOfCharacteristicTypes': 'WholeCharacteristicKind', 'ChartOfAccounts': 'WholeChartOfAccounts'}.get(obj_type, 'WholeCatalog')
    check_unique_def = (obj_type in ('ChartOfCharacteristicTypes', 'ChartOfAccounts', 'Document', 'DocumentNumerator'))
    def_pres_def = 'AsCode' if obj_type == 'ChartOfAccounts' else 'AsDescription'
    add_int_prop('codeLength', 'CodeLength', code_len_def)
    add_int_prop('descriptionLength', 'DescriptionLength', descr_len_def)
    add_enum_prop('codeType', 'CodeType', 'String')
    add_enum_prop('codeAllowedLength', 'CodeAllowedLength', 'Variable')
    add_bool_prop('autonumbering', 'Autonumbering', True)
    add_bool_prop('checkUnique', 'CheckUnique', check_unique_def)
    add_enum_prop('codeSeries', 'CodeSeries', code_series_def)
    add_enum_prop('defaultPresentation', 'DefaultPresentation', def_pres_def)
    if obj_type != 'Constant':
        add_bool_prop('quickChoice', 'QuickChoice', True if obj_type == 'Enum' else False)
    add_enum_prop('choiceMode', 'ChoiceMode', 'BothWays')
    add_enum_prop('dataLockControlMode', 'DataLockControlMode', data_lock_def)
    add_enum_prop('fullTextSearch', 'FullTextSearch', 'Use')
    add_bool_prop('useStandardCommands', 'UseStandardCommands', False if obj_type in ('Enum', 'CommonForm') else True)
    add_enum_prop('createOnInput', 'CreateOnInput', create_inp_def)
    add_enum_prop('editType', 'EditType', 'InDialog')
    add_bool_prop('includeHelpInContents', 'IncludeHelpInContents', False)
    add_enum_prop('choiceHistoryOnInput', 'ChoiceHistoryOnInput', 'Auto')
    add_enum_prop('predefinedDataUpdate', 'PredefinedDataUpdate', 'Auto')
    add_enum_prop('searchStringModeOnInputByString', 'SearchStringModeOnInputByString', 'Begin')
    add_enum_prop('fullTextSearchOnInputByString', 'FullTextSearchOnInputByString', 'DontUse')
    # ExchangePlan-специфичные свойства.
    if obj_type == 'ExchangePlan':
        add_bool_prop('distributedInfoBase', 'DistributedInfoBase', False)
        add_bool_prop('includeConfigurationExtensions', 'IncludeConfigurationExtensions', False)
        add_enum_prop('dataHistory', 'DataHistory', 'DontUse')
        add_bool_prop('updateDataHistoryImmediatelyAfterWrite', 'UpdateDataHistoryImmediatelyAfterWrite', False)
        add_bool_prop('executeAfterWriteDataHistoryVersionProcessing', 'ExecuteAfterWriteDataHistoryVersionProcessing', False)
    # ChartOfCharacteristicTypes-специфичные свойства.
    if obj_type == 'ChartOfCharacteristicTypes':
        add_enum_prop('dataHistory', 'DataHistory', 'DontUse')
        add_bool_prop('updateDataHistoryImmediatelyAfterWrite', 'UpdateDataHistoryImmediatelyAfterWrite', False)
        add_bool_prop('executeAfterWriteDataHistoryVersionProcessing', 'ExecuteAfterWriteDataHistoryVersionProcessing', False)
        cev = P('CharacteristicExtValues')
        if cev:
            dsl['characteristicExtValues'] = cev
        vt_node = _single(props, 'md:Type')
        if vt_node is not None:
            vt_str = get_type_shorthand(vt_node)
            if vt_str and vt_str != 'Boolean + String(100) + Number(15,2) + DateTime':
                dsl['valueType'] = vt_str
    # ChartOfAccounts-специфичные свойства.
    if obj_type == 'ChartOfAccounts':
        edt = P('ExtDimensionTypes')
        if edt:
            dsl['extDimensionTypes'] = edt
        add_int_prop('maxExtDimensionCount', 'MaxExtDimensionCount', 3 if edt else 0)
        cm = P('CodeMask')
        if cm:
            dsl['codeMask'] = cm
        add_bool_prop('autoOrderByCode', 'AutoOrderByCode', True)
        add_int_prop('orderLength', 'OrderLength', 9)
        add_enum_prop('dataHistory', 'DataHistory', 'DontUse')
        add_bool_prop('updateDataHistoryImmediatelyAfterWrite', 'UpdateDataHistoryImmediatelyAfterWrite', False)
        add_bool_prop('executeAfterWriteDataHistoryVersionProcessing', 'ExecuteAfterWriteDataHistoryVersionProcessing', False)
    # ChartOfCalculationTypes-специфичные свойства.
    if obj_type == 'ChartOfCalculationTypes':
        add_enum_prop('dependenceOnCalculationTypes', 'DependenceOnCalculationTypes', 'DontUse')
        add_bool_prop('actionPeriodUse', 'ActionPeriodUse', False)
        bct_node = _single(props, 'md:BaseCalculationTypes')
        if bct_node is not None:
            bct_items = [_text(x) for x in _nodes(bct_node, 'xr:Item')]
            if len(bct_items) > 0:
                dsl['baseCalculationTypes'] = list(bct_items)
        add_enum_prop('dataHistory', 'DataHistory', 'DontUse')
        add_bool_prop('updateDataHistoryImmediatelyAfterWrite', 'UpdateDataHistoryImmediatelyAfterWrite', False)
        add_bool_prop('executeAfterWriteDataHistoryVersionProcessing', 'ExecuteAfterWriteDataHistoryVersionProcessing', False)
    # Document-специфичные свойства.
    if obj_type == 'Document':
        num_ref = P('Numerator')
        if num_ref:
            dsl['numerator'] = num_ref
        add_enum_prop('numberType', 'NumberType', 'String')
        add_int_prop('numberLength', 'NumberLength', 11)
        add_enum_prop('numberAllowedLength', 'NumberAllowedLength', 'Variable')
        add_enum_prop('numberPeriodicity', 'NumberPeriodicity', 'Year')
        add_enum_prop('posting', 'Posting', 'Allow')
        add_enum_prop('realTimePosting', 'RealTimePosting', 'Deny')
        add_enum_prop('registerRecordsDeletion', 'RegisterRecordsDeletion', 'AutoDelete')
        add_enum_prop('registerRecordsWritingOnPost', 'RegisterRecordsWritingOnPost', 'WriteSelected')
        add_enum_prop('sequenceFilling', 'SequenceFilling', 'AutoFill')
        add_bool_prop('postInPrivilegedMode', 'PostInPrivilegedMode', True)
        add_bool_prop('unpostInPrivilegedMode', 'UnpostInPrivilegedMode', True)
        rr_node = _single(props, 'md:RegisterRecords')
        if rr_node is not None:
            rr_items = [_text(x) for x in _nodes(rr_node, 'xr:Item')]
            if len(rr_items) > 0:
                dsl['registerRecords'] = list(rr_items)
        add_enum_prop('dataHistory', 'DataHistory', 'DontUse')
        add_bool_prop('updateDataHistoryImmediatelyAfterWrite', 'UpdateDataHistoryImmediatelyAfterWrite', False)
        add_bool_prop('executeAfterWriteDataHistoryVersionProcessing', 'ExecuteAfterWriteDataHistoryVersionProcessing', False)
    # InformationRegister-специфичные свойства.
    if obj_type == 'InformationRegister':
        add_enum_prop('periodicity', 'InformationRegisterPeriodicity', 'Nonperiodical')
        add_enum_prop('writeMode', 'WriteMode', 'Independent')
        add_bool_prop('mainFilterOnPeriod', 'MainFilterOnPeriod', False)
        add_bool_prop('enableTotalsSliceFirst', 'EnableTotalsSliceFirst', False)
        add_bool_prop('enableTotalsSliceLast', 'EnableTotalsSliceLast', False)
        add_enum_prop('dataHistory', 'DataHistory', 'DontUse')
        add_bool_prop('updateDataHistoryImmediatelyAfterWrite', 'UpdateDataHistoryImmediatelyAfterWrite', False)
        add_bool_prop('executeAfterWriteDataHistoryVersionProcessing', 'ExecuteAfterWriteDataHistoryVersionProcessing', False)
    # AccumulationRegister-специфичные свойства.
    if obj_type == 'AccumulationRegister':
        add_enum_prop('registerType', 'RegisterType', 'Balance')
        add_bool_prop('enableTotalsSplitting', 'EnableTotalsSplitting', True)
    # AccountingRegister-специфичные свойства.
    if obj_type == 'AccountingRegister':
        coa = P('ChartOfAccounts')
        if coa:
            dsl['chartOfAccounts'] = coa
        add_bool_prop('correspondence', 'Correspondence', False)
        add_int_prop('periodAdjustmentLength', 'PeriodAdjustmentLength', 0)
        add_bool_prop('enableTotalsSplitting', 'EnableTotalsSplitting', True)
    # CalculationRegister-специфичные свойства.
    if obj_type == 'CalculationRegister':
        cct = P('ChartOfCalculationTypes')
        if cct:
            dsl['chartOfCalculationTypes'] = cct
        add_enum_prop('periodicity', 'Periodicity', 'Month')
        add_bool_prop('actionPeriod', 'ActionPeriod', False)
        add_bool_prop('basePeriod', 'BasePeriod', False)
        sch = P('Schedule')
        if sch:
            dsl['schedule'] = sch
        schv = P('ScheduleValue')
        if schv:
            dsl['scheduleValue'] = schv
        schd = P('ScheduleDate')
        if schd:
            dsl['scheduleDate'] = schd
    # BusinessProcess-специфичные свойства.
    if obj_type == 'BusinessProcess':
        add_enum_prop('numberType', 'NumberType', 'String')
        add_int_prop('numberLength', 'NumberLength', 11)
        add_enum_prop('numberAllowedLength', 'NumberAllowedLength', 'Variable')
        add_enum_prop('numberPeriodicity', 'NumberPeriodicity', 'Nonperiodical')
        add_bool_prop('checkUnique', 'CheckUnique', True)
        add_bool_prop('autonumbering', 'Autonumbering', True)
        tsk = P('Task')
        if tsk:
            dsl['task'] = tsk
        add_bool_prop('createTaskInPrivilegedMode', 'CreateTaskInPrivilegedMode', True)
    # Task-специфичные свойства.
    if obj_type == 'Task':
        add_enum_prop('numberType', 'NumberType', 'String')
        add_int_prop('numberLength', 'NumberLength', 14)
        add_enum_prop('numberAllowedLength', 'NumberAllowedLength', 'Variable')
        add_bool_prop('checkUnique', 'CheckUnique', True)
        add_bool_prop('autonumbering', 'Autonumbering', True)
        tnap = P('TaskNumberAutoPrefix')
        if tnap and tnap != 'BusinessProcessNumber':
            dsl['taskNumberAutoPrefix'] = tnap
        add_int_prop('descriptionLength', 'DescriptionLength', 150)
        addr = P('Addressing')
        if addr:
            dsl['addressing'] = addr
        maa = P('MainAddressingAttribute')
        if maa:
            dsl['mainAddressingAttribute'] = maa
        cp = P('CurrentPerformer')
        if cp:
            dsl['currentPerformer'] = cp
        add_enum_prop('defaultPresentation', 'DefaultPresentation', 'AsDescription')
    # Report-специфичные свойства.
    if obj_type == 'Report':
        dfm = P('DefaultForm')
        if dfm:
            dsl['defaultForm'] = dfm
        afm = P('AuxiliaryForm')
        if afm:
            dsl['auxiliaryForm'] = afm
        mdcs = P('MainDataCompositionSchema')
        if mdcs:
            dsl['mainDataCompositionSchema'] = mdcs
        dsf = P('DefaultSettingsForm')
        if dsf:
            dsl['defaultSettingsForm'] = dsf
        asf = P('AuxiliarySettingsForm')
        if asf:
            dsl['auxiliarySettingsForm'] = asf
        dvf = P('DefaultVariantForm')
        if dvf:
            dsl['defaultVariantForm'] = dvf
        vs = P('VariantsStorage')
        if vs:
            dsl['variantsStorage'] = vs
        ss = P('SettingsStorage')
        if ss:
            dsl['settingsStorage'] = ss
        ep = get_ml_value(_single(props, 'md:ExtendedPresentation'))
        if ep is not None:
            dsl['extendedPresentation'] = ep
    # DataProcessor-специфичные свойства.
    if obj_type == 'DataProcessor':
        dfm = P('DefaultForm')
        if dfm:
            dsl['defaultForm'] = dfm
        afm = P('AuxiliaryForm')
        if afm:
            dsl['auxiliaryForm'] = afm
        ep = get_ml_value(_single(props, 'md:ExtendedPresentation'))
        if ep is not None:
            dsl['extendedPresentation'] = ep
    # DefinedType — тип-псевдоним.
    if obj_type == 'DefinedType':
        vt = get_type_shorthand(_single(props, 'md:Type'))
        if vt:
            dsl['valueType'] = vt
    # FunctionalOption.
    if obj_type == 'FunctionalOption':
        loc = P('Location')
        if loc:
            dsl['location'] = loc
        add_bool_prop('privilegedGetMode', 'PrivilegedGetMode', True)
        content_node = _single(props, 'md:Content')
        if content_node is not None:
            items = [_text(x) for x in _nodes(content_node, 'xr:Object')]
            if len(items) > 0:
                dsl['content'] = list(items)
    # DocumentJournal.
    if obj_type == 'DocumentJournal':
        dfm = P('DefaultForm')
        if dfm:
            dsl['defaultForm'] = dfm
        afm = P('AuxiliaryForm')
        if afm:
            dsl['auxiliaryForm'] = afm
        rd_node = _single(props, 'md:RegisteredDocuments')
        if rd_node is not None:
            rd_items = [_text(x) for x in _nodes(rd_node, 'xr:Item')]
            if len(rd_items) > 0:
                dsl['registeredDocuments'] = list(rd_items)
    # Sequence.
    if obj_type == 'Sequence':
        add_enum_prop('moveBoundaryOnPosting', 'MoveBoundaryOnPosting', 'DontMove')
        for ll in (('Documents', 'documents'), ('RegisterRecords', 'registerRecords')):
            ln = _single(props, "md:" + ll[0])
            if ln is not None:
                items = [_text(x) for x in _nodes(ln, 'xr:Item')]
                if len(items) > 0:
                    dsl[ll[1]] = list(items)
    # FilterCriterion.
    if obj_type == 'FilterCriterion':
        vt = get_type_shorthand(_single(props, 'md:Type'))
        if vt:
            dsl['valueType'] = vt
        cn = _single(props, 'md:Content')
        if cn is not None:
            items = [_text(x) for x in _nodes(cn, 'xr:Item')]
            if len(items) > 0:
                dsl['content'] = list(items)
        dfm = P('DefaultForm')
        if dfm:
            dsl['defaultForm'] = dfm
        afm = P('AuxiliaryForm')
        if afm:
            dsl['auxiliaryForm'] = afm
    # DocumentNumerator.
    if obj_type == 'DocumentNumerator':
        add_enum_prop('numberType', 'NumberType', 'String')
        add_int_prop('numberLength', 'NumberLength', 11)
        add_enum_prop('numberAllowedLength', 'NumberAllowedLength', 'Variable')
        add_enum_prop('numberPeriodicity', 'NumberPeriodicity', 'Year')
    # SettingsStorage.
    if obj_type == 'SettingsStorage':
        for fp in (('DefaultSaveForm', 'defaultSaveForm'), ('DefaultLoadForm', 'defaultLoadForm'),
                   ('AuxiliarySaveForm', 'auxiliarySaveForm'), ('AuxiliaryLoadForm', 'auxiliaryLoadForm')):
            fv = P(fp[0])
            if fv:
                dsl[fp[1]] = fv
    # CommonModule.
    if obj_type == 'CommonModule':
        add_bool_prop('global', 'Global', False)
        add_bool_prop('clientManagedApplication', 'ClientManagedApplication', False)
        add_bool_prop('server', 'Server', False)
        add_bool_prop('externalConnection', 'ExternalConnection', False)
        add_bool_prop('clientOrdinaryApplication', 'ClientOrdinaryApplication', False)
        add_bool_prop('serverCall', 'ServerCall', False)
        add_bool_prop('privileged', 'Privileged', False)
        add_enum_prop('returnValuesReuse', 'ReturnValuesReuse', 'DontUse')
    # EventSubscription.
    if obj_type == 'EventSubscription':
        src_node = _single(props, 'md:Source')
        if src_node is not None:
            src_types = [strip_ns_prefix(_text(x).strip()) for x in _nodes(src_node, 'v8:Type|v8:TypeSet')]
            if len(src_types) > 0:
                dsl['source'] = list(src_types)
        add_enum_prop('event', 'Event', 'BeforeWrite')
        h = P('Handler')
        if h:
            dsl['handler'] = h
    # CommonForm.
    if obj_type == 'CommonForm':
        add_enum_prop('formType', 'FormType', 'Managed')
        up_node = _single(props, 'md:UsePurposes')
        if up_node is not None:
            ups = [_text(x) for x in _nodes(up_node, 'v8:Value')]
            def2 = ['PlatformApplication', 'MobilePlatformApplication']
            same = (len(ups) == len(def2))
            if same:
                for k in range(len(ups)):
                    if ups[k] != def2[k]:
                        same = False
                        break
            if (not same) and len(ups) > 0:
                dsl['usePurposes'] = list(ups)
        ep = get_ml_value(_single(props, 'md:ExtendedPresentation'))
        if ep is not None:
            dsl['extendedPresentation'] = ep
    # SessionParameter.
    if obj_type == 'SessionParameter':
        vt = get_type_shorthand(_single(props, 'md:Type'))
        if vt:
            dsl['valueType'] = vt
    # FunctionalOptionsParameter.
    if obj_type == 'FunctionalOptionsParameter':
        un = _single(props, 'md:Use')
        if un is not None:
            items = [_text(x) for x in _nodes(un, 'xr:Item')]
            if len(items) > 0:
                dsl['use'] = list(items)
    # WSReference.
    if obj_type == 'WSReference':
        url = P('LocationURL')
        if url:
            dsl['locationURL'] = url
    # CommonPicture.
    if obj_type == 'CommonPicture':
        add_bool_prop('availabilityForChoice', 'AvailabilityForChoice', False)
        add_bool_prop('availabilityForAppearance', 'AvailabilityForAppearance', False)
    # CommonTemplate.
    if obj_type == 'CommonTemplate':
        add_enum_prop('templateType', 'TemplateType', 'SpreadsheetDocument')
    # CommandGroup.
    if obj_type == 'CommandGroup':
        add_enum_prop('representation', 'Representation', 'Auto')
        tt = get_ml_value(_single(props, 'md:ToolTip'))
        if tt is not None:
            dsl['tooltip'] = tt
        get_picture_to_dsl(props, dsl)
        add_enum_prop('category', 'Category', 'NavigationPanel')
    # CommonCommand.
    # WebService — пространство имён, состав XDTO-пакетов, дескриптор.
    if obj_type == 'WebService':
        ns_ = P('Namespace')
        if ns_:
            dsl['namespace'] = ns_
        pkg_nodes = _nodes(props, 'md:XDTOPackages/xr:Item/xr:Value')
        if len(pkg_nodes) > 0:
            dsl['xdtoPackages'] = [_text(pn) for pn in pkg_nodes]
        dfn = P('DescriptorFileName')
        if dfn and dfn != f'{obj_name}.1cws':
            dsl['descriptorFileName'] = dfn
        add_enum_prop('reuseSessions', 'ReuseSessions', 'DontUse')
        add_int_prop('sessionMaxAge', 'SessionMaxAge', 20)
    # HTTPService — корневой URL, повторное использование сеансов, время жизни сеанса.
    if obj_type == 'HTTPService':
        ru = P('RootURL')
        # Сравнение регистрочувствительное: реальный RootURL часто отличается от дефолта
        # только регистром (MobileAppReceiptScanner).
        if ru and ru != obj_name.lower():
            dsl['rootURL'] = ru
        add_enum_prop('reuseSessions', 'ReuseSessions', 'DontUse')
        add_int_prop('sessionMaxAge', 'SessionMaxAge', 20)
    if obj_type == 'CommonCommand':
        grp = P('Group')
        if grp:
            dsl['group'] = grp
        add_enum_prop('representation', 'Representation', 'Auto')
        tt = get_ml_value(_single(props, 'md:ToolTip'))
        if tt is not None:
            dsl['tooltip'] = tt
        get_picture_to_dsl(props, dsl)
        sc = P('Shortcut')
        if sc:
            dsl['shortcut'] = sc
        cpt = get_type_shorthand(_single(props, 'md:CommandParameterType'))
        if cpt:
            dsl['commandParameterType'] = cpt
        add_enum_prop('parameterUseMode', 'ParameterUseMode', 'Single')
        add_bool_prop('modifiesData', 'ModifiesData', False)
        add_enum_prop('onMainServerUnavalableBehavior', 'OnMainServerUnavalableBehavior', 'Auto')
    # CommonAttribute.
    if obj_type == 'CommonAttribute':
        vt = get_type_shorthand(_single(props, 'md:Type'))
        if vt and vt != 'String(0)':
            dsl['valueType'] = vt
        add_bool_prop('passwordMode', 'PasswordMode', False)
        for mlp in (('Format', 'format'), ('EditFormat', 'editFormat'), ('ToolTip', 'tooltip')):
            mv = get_ml_value(_single(props, "md:" + mlp[0]))
            if mv is not None:
                dsl[mlp[1]] = mv
        add_bool_prop('markNegatives', 'MarkNegatives', False)
        msk = P('Mask')
        if msk:
            dsl['mask'] = msk
        add_bool_prop('multiLine', 'MultiLine', False)
        add_bool_prop('extendedEdit', 'ExtendedEdit', False)
        for mm in (('MinValue', 'minValue'), ('MaxValue', 'maxValue')):
            mn = _single(props, "md:" + mm[0])
            if mn is not None and _attr(mn, 'nil', NS_XSI) != 'true':
                mxt = _attr(mn, 'type', NS_XSI)
                if re.search(r'decimal$', mxt, re.I):
                    dsl[mm[1]] = int(_text(mn)) if re.match(r'^-?\d+$', _text(mn)) else float(_text(mn))
                else:
                    dsl[mm[1]] = _text(mn)
        add_bool_prop('fillFromFillingValue', 'FillFromFillingValue', False)
        # FillValue: тип-зависимый дефолт (String→typed-empty, Number→0, прочее→nil).
        cat_vt = vt if vt else 'String(0)'
        fv_n = _single(props, 'md:FillValue')
        if fv_n is not None:
            fv_nil = (_attr(fv_n, 'nil', NS_XSI) == 'true')
            if fv_nil:
                if re.match(r'^(String|Number)', cat_vt):
                    dsl['fillValue'] = {'nil': True}
            else:
                fv_xt = _attr(fv_n, 'type', NS_XSI)
                if re.search(r'DesignTimeRef$', fv_xt, re.I) and _text(fv_n) == '':
                    dsl['fillValue'] = {'emptyRef': True}
                elif re.search(r'decimal$', fv_xt, re.I):
                    if _text(fv_n) != '0':
                        dsl['fillValue'] = int(_text(fv_n)) if re.match(r'^-?\d+$', _text(fv_n)) else float(_text(fv_n))
                elif _text(fv_n):
                    dsl['fillValue'] = _text(fv_n)
        add_enum_prop('fillChecking', 'FillChecking', 'DontCheck')
        add_enum_prop('choiceFoldersAndItems', 'ChoiceFoldersAndItems', 'Items')
        cpl = parse_choice_parameter_links(props, 'md:ChoiceParameterLinks')
        if cpl is not None:
            dsl['choiceParameterLinks'] = cpl
        cp = parse_choice_parameters(props, 'md:ChoiceParameters')
        if cp is not None:
            dsl['choiceParameters'] = cp
        add_enum_prop('quickChoice', 'QuickChoice', 'Auto')
        add_enum_prop('createOnInput', 'CreateOnInput', 'Auto')
        cf = P('ChoiceForm')
        if cf:
            dsl['choiceForm'] = cf
        add_enum_prop('choiceHistoryOnInput', 'ChoiceHistoryOnInput', 'Auto')
        cn = _single(props, 'md:Content')
        if cn is not None:
            c_arr = []
            for it in _nodes(cn, 'xr:Item'):
                md_n = _single(it, 'xr:Metadata')
                mdv = _text(md_n) if md_n is not None else ''
                use_n = _single(it, 'xr:Use')
                usev = _text(use_n) if use_n is not None else 'Use'
                cs_n = _single(it, 'xr:ConditionalSeparation')
                csv = _text(cs_n) if cs_n is not None else ''
                if usev == 'Use' and not csv:
                    c_arr.append(mdv)
                else:
                    io = {'metadata': mdv}
                    if usev != 'Use':
                        io['use'] = usev
                    if csv:
                        io['conditionalSeparation'] = csv
                    c_arr.append(io)
            if len(c_arr) > 0:
                dsl['content'] = c_arr
        add_enum_prop('autoUse', 'AutoUse', 'DontUse')
        add_enum_prop('dataSeparation', 'DataSeparation', 'DontUse')
        add_enum_prop('separatedDataUse', 'SeparatedDataUse', 'Independently')
        dsv = P('DataSeparationValue')
        if dsv:
            dsl['dataSeparationValue'] = dsv
        dsu = P('DataSeparationUse')
        if dsu:
            dsl['dataSeparationUse'] = dsu
        cs2 = P('ConditionalSeparation')
        if cs2:
            dsl['conditionalSeparation'] = cs2
        add_enum_prop('usersSeparation', 'UsersSeparation', 'DontUse')
        add_enum_prop('authenticationSeparation', 'AuthenticationSeparation', 'DontUse')
        add_enum_prop('configurationExtensionsSeparation', 'ConfigurationExtensionsSeparation', 'DontUse')
        add_enum_prop('indexing', 'Indexing', 'DontIndex')
        add_enum_prop('dataHistory', 'DataHistory', 'Use')
    # ScheduledJob.
    if obj_type == 'ScheduledJob':
        mn = P('MethodName')
        if mn:
            dsl['methodName'] = mn
        descr = P('Description')
        if descr:
            dsl['description'] = descr
        k = P('Key')
        if k:
            dsl['key'] = k
        add_bool_prop('use', 'Use', False)
        add_bool_prop('predefined', 'Predefined', False)
        add_int_prop('restartCountOnFailure', 'RestartCountOnFailure', 3)
        add_int_prop('restartIntervalOnFailure', 'RestartIntervalOnFailure', 10)
    # Constant — богатый одиночный реквизит.
    if obj_type == 'Constant':
        vt = get_type_shorthand(_single(props, 'md:Type'))
        if vt:
            dsl['valueType'] = vt
        else:
            dsl['valueType'] = ''
        dfm = P('DefaultForm')
        if dfm:
            dsl['defaultForm'] = dfm
        ep = get_ml_value(_single(props, 'md:ExtendedPresentation'))
        if ep is not None:
            dsl['extendedPresentation'] = ep
        add_bool_prop('passwordMode', 'PasswordMode', False)
        fmt = get_ml_value(_single(props, 'md:Format'))
        if fmt is not None:
            dsl['format'] = fmt
        efmt = get_ml_value(_single(props, 'md:EditFormat'))
        if efmt is not None:
            dsl['editFormat'] = efmt
        tt = get_ml_value(_single(props, 'md:ToolTip'))
        if tt is not None:
            dsl['tooltip'] = tt
        add_bool_prop('markNegatives', 'MarkNegatives', False)
        msk = P('Mask')
        if msk:
            dsl['mask'] = msk
        add_bool_prop('multiLine', 'MultiLine', False)
        add_bool_prop('extendedEdit', 'ExtendedEdit', False)
        for mm in (('MinValue', 'minValue'), ('MaxValue', 'maxValue')):
            mn = _single(props, "md:" + mm[0])
            if mn is not None and _attr(mn, 'nil', NS_XSI) != 'true':
                mxt = _attr(mn, 'type', NS_XSI)
                if re.search(r'decimal$', mxt, re.I):
                    dsl[mm[1]] = int(_text(mn)) if re.match(r'^-?\d+$', _text(mn)) else float(_text(mn))
                else:
                    dsl[mm[1]] = _text(mn)
        add_enum_prop('fillChecking', 'FillChecking', 'DontCheck')
        add_enum_prop('choiceFoldersAndItems', 'ChoiceFoldersAndItems', 'Items')
        cpl = parse_choice_parameter_links(props, 'md:ChoiceParameterLinks')
        if cpl is not None:
            dsl['choiceParameterLinks'] = cpl
        cp = parse_choice_parameters(props, 'md:ChoiceParameters')
        if cp is not None:
            dsl['choiceParameters'] = cp
        add_enum_prop('quickChoice', 'QuickChoice', 'Auto')
        cf = P('ChoiceForm')
        if cf:
            dsl['choiceForm'] = cf
        lbt_node = _single(props, 'md:LinkByType')
        if lbt_node is not None:
            dp_n = _single(lbt_node, 'md:DataPath')
            if dp_n is not None and _text(dp_n):
                li_n = _single(lbt_node, 'md:LinkItem')
                li = int(_text(li_n)) if (li_n is not None and _text(li_n)) else 0
                dsl['linkByType'] = _text(dp_n) if li == 0 else {'dataPath': _text(dp_n), 'linkItem': li}
        add_enum_prop('dataHistory', 'DataHistory', 'DontUse')
        add_bool_prop('updateDataHistoryImmediatelyAfterWrite', 'UpdateDataHistoryImmediatelyAfterWrite', False)
        add_bool_prop('executeAfterWriteDataHistoryVersionProcessing', 'ExecuteAfterWriteDataHistoryVersionProcessing', False)

    # InputByString — эмитим только при отличии от выведенного дефолта.
    ib_node = _single(props, 'md:InputByString')
    if ib_node is not None:
        ib_actual = [_text(x) for x in _nodes(ib_node, 'xr:Field')]
        clv = P('CodeLength')
        dlv = P('DescriptionLength')
        cl = int(clv) if (clv is not None and clv != '') else 9
        dl = int(dlv) if (dlv is not None and dlv != '') else 25
        ib_def = []
        if dl > 0:
            ib_def.append("StandardAttribute.Description")
        if cl > 0:
            ib_def.append("StandardAttribute.Code")
        ib_short = [short_field(x) for x in ib_actual]
        same = (len(ib_short) == len(ib_def))
        if same:
            for k in range(len(ib_short)):
                if ib_short[k] != ib_def[k]:
                    same = False
                    break
        if not same:
            dsl['inputByString'] = list(ib_short)

    # BasedOn.
    bo_node = _single(props, 'md:BasedOn')
    if bo_node is not None:
        bo_items = [_text(x) for x in _nodes(bo_node, 'xr:Item')]
        if len(bo_items) > 0:
            dsl['basedOn'] = list(bo_items)

    # DataLockFields.
    dlf_node = _single(props, 'md:DataLockFields')
    if dlf_node is not None:
        dlf_fields = [short_field(_text(x)) for x in _nodes(dlf_node, 'xr:Field')]
        if len(dlf_fields) > 0:
            dsl['dataLockFields'] = list(dlf_fields)

    # Формы по умолчанию (omit-on-empty).
    add_form_ref('defaultObjectForm', 'DefaultObjectForm')
    add_form_ref('defaultFolderForm', 'DefaultFolderForm')
    add_form_ref('defaultListForm', 'DefaultListForm')
    add_form_ref('defaultChoiceForm', 'DefaultChoiceForm')
    add_form_ref('defaultFolderChoiceForm', 'DefaultFolderChoiceForm')
    add_form_ref('auxiliaryObjectForm', 'AuxiliaryObjectForm')
    add_form_ref('auxiliaryFolderForm', 'AuxiliaryFolderForm')
    add_form_ref('auxiliaryListForm', 'AuxiliaryListForm')
    add_form_ref('auxiliaryChoiceForm', 'AuxiliaryChoiceForm')
    add_form_ref('auxiliaryFolderChoiceForm', 'AuxiliaryFolderChoiceForm')
    add_form_ref('defaultRecordForm', 'DefaultRecordForm')
    add_form_ref('auxiliaryRecordForm', 'AuxiliaryRecordForm')

    # Презентации (ML, omit-on-empty).
    for pp in (('ObjectPresentation', 'objectPresentation'), ('ExtendedObjectPresentation', 'extendedObjectPresentation'),
               ('RecordPresentation', 'recordPresentation'), ('ExtendedRecordPresentation', 'extendedRecordPresentation'),
               ('ListPresentation', 'listPresentation'), ('ExtendedListPresentation', 'extendedListPresentation'),
               ('Explanation', 'explanation')):
        pv = get_ml_value(_single(props, "md:" + pp[0]))
        if pv is not None:
            dsl[pp[1]] = pv

    # --- Characteristics (привязка ПВХ). ---
    chars_node = _single(props, 'md:Characteristics')
    if chars_node is not None:
        ch_list = _nodes(chars_node, 'xr:Characteristic')
        if len(ch_list) > 0:
            ch_arr = []
            for ch in ch_list:
                ct = _single(ch, 'xr:CharacteristicTypes')
                cv = _single(ch, 'xr:CharacteristicValues')
                t_from = _attr(ct, 'from')
                v_from = _attr(cv, 'from')

                def gt(n, node):
                    x = _single(node, "xr:" + n)
                    return _text(x) if x is not None else ""

                def giv(n, node):
                    x = _single(node, "xr:" + n)
                    return int(_text(x)) if (x is not None and _text(x) != '') else -1

                tfv_node = _single(ct, 'xr:TypesFilterValue')
                tfv_nil = _attr(tfv_node, 'nil', NS_XSI) if tfv_node is not None else ''
                types = {
                    'from': t_from,
                    'key': shorten_char_field(gt('KeyField', ct), t_from),
                    'filterField': shorten_char_field(gt('TypesFilterField', ct), t_from),
                    'filterValue': None if tfv_nil == 'true' else convert_ch_scalar_node(tfv_node),
                }
                # DataPathField полиморфно: обычно -1, но встречается ПУТЬ к полю (8 случаев на
                # корпус). Жёсткое int() на нём роняло декомпиляцию всего объекта.
                dpf_node = _lx1(ct, "*[local-name()='DataPathField']")
                dpf_txt = _text(dpf_node) if dpf_node is not None else ''
                if dpf_txt != '' and dpf_txt != '-1':
                    types['dataPathField'] = (int(dpf_txt) if re.fullmatch(r'-?\d+', dpf_txt)
                                              else shorten_char_field(dpf_txt, t_from))
                mvu = giv('MultipleValuesUseField', ct)
                if mvu != -1:
                    types['multipleValuesUseField'] = mvu
                values = {
                    'from': v_from,
                    'object': shorten_char_field(gt('ObjectField', cv), v_from),
                    'type': shorten_char_field(gt('TypeField', cv), v_from),
                    'value': shorten_char_field(gt('ValueField', cv), v_from),
                }
                mvk = giv('MultipleValuesKeyField', cv)
                if mvk != -1:
                    values['multipleValuesKeyField'] = mvk
                mvo = giv('MultipleValuesOrderField', cv)
                if mvo != -1:
                    values['multipleValuesOrderField'] = mvo
                ch_arr.append({'types': types, 'values': values})
            dsl['characteristics'] = ch_arr

    # --- StandardAttributes: захватываем ОТКЛОНЕНИЯ от профиля материализованного блока. ---
    std_profile_by_type = {
        'Catalog': {
            'Owner': {'fillChecking': 'ShowError', 'fillFromFillingValue': True},
            'Parent': {'fillFromFillingValue': True},
            'Description': {'fillChecking': 'ShowError'},
        },
        'ExchangePlan': {
            'Description': {'fillChecking': 'ShowError'},
            'Code': {'fillChecking': 'ShowError'},
        },
        'ChartOfCharacteristicTypes': {
            'Description': {'fillChecking': 'ShowError'},
            'Parent': {'fillFromFillingValue': True},
        },
        'ChartOfAccounts': {
            'Description': {'fillChecking': 'ShowError'},
            'Code': {'fillChecking': 'ShowError'},
            'Parent': {'fillFromFillingValue': True},
        },
        'ChartOfCalculationTypes': {
            'Description': {'fillChecking': 'ShowError'},
        },
        'Document': {
            'Date': {'fillChecking': 'ShowError'},
        },
    }
    cat_std_profile = std_profile_by_type.get(obj_type, {})
    std_fixed_by_type = {
        'Catalog': ['PredefinedDataName', 'Predefined', 'Ref', 'DeletionMark', 'IsFolder', 'Owner', 'Parent', 'Description', 'Code'],
        'ExchangePlan': ['Ref', 'DeletionMark', 'Code', 'Description', 'ThisNode', 'SentNo', 'ReceivedNo'],
        'ChartOfCharacteristicTypes': ['PredefinedDataName', 'Predefined', 'Ref', 'DeletionMark', 'Description', 'Code', 'Parent', 'ValueType'],
        'ChartOfAccounts': ['PredefinedDataName', 'Order', 'OffBalance', 'Type', 'Description', 'Code', 'Parent', 'Predefined', 'DeletionMark', 'Ref'],
        'ChartOfCalculationTypes': ['PredefinedDataName', 'Predefined', 'Ref', 'DeletionMark', 'ActionPeriodIsBasic', 'Description', 'Code'],
        'Document': ['Ref', 'DeletionMark', 'Date', 'Number', 'Posted'],
        'Enum': ['Order', 'Ref'],
        'DocumentJournal': ['Type', 'Ref', 'Date', 'Posted', 'DeletionMark', 'Number'],
    }
    std_fixed = std_fixed_by_type.get(obj_type, [])
    std_conditional_types = ['Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document']
    sa_node = _single(props, 'md:StandardAttributes')
    if sa_node is not None:
        sa_map = {}
        for sa in _nodes(sa_node, 'xr:StandardAttribute'):
            an = _attr(sa, 'name')
            prof = cat_std_profile.get(an, {})
            ov = {}
            # FillChecking (профиль или DontCheck)
            fc_n = _single(sa, 'xr:FillChecking')
            fc = _text(fc_n) if fc_n is not None else 'DontCheck'
            prof_fc = prof.get('fillChecking', 'DontCheck')
            if fc != prof_fc:
                ov['fillChecking'] = fc
            # FillFromFillingValue (профиль или false)
            ffv_n = _single(sa, 'xr:FillFromFillingValue')
            ffv = (ffv_n is not None and _text(ffv_n) == 'true')
            prof_ffv = (prof.get('fillFromFillingValue') is True)
            if ffv != prof_ffv:
                ov['fillFromFillingValue'] = ffv
            # Synonym / ToolTip (профиль пуст)
            syn = get_ml_value(_single(sa, 'xr:Synonym'))
            if syn is not None:
                ov['synonym'] = syn
            tt = get_ml_value(_single(sa, 'xr:ToolTip'))
            if tt is not None:
                ov['tooltip'] = tt
            # FullTextSearch / DataHistory (профиль = Use)
            fts_n = _single(sa, 'xr:FullTextSearch')
            if fts_n is not None and _text(fts_n) != 'Use':
                ov['fullTextSearch'] = _text(fts_n)
            dh_n = _single(sa, 'xr:DataHistory')
            if dh_n is not None and _text(dh_n) != 'Use':
                ov['dataHistory'] = _text(dh_n)
            # FillValue (дефолт nil). Comment/Mask/ChoiceForm (дефолт пусто).
            fv_n = _single(sa, 'xr:FillValue')
            if fv_n is not None and _attr(fv_n, 'nil', NS_XSI) != 'true':
                fv_xt = _attr(fv_n, 'type', NS_XSI)
                if re.search(r'DesignTimeRef$', fv_xt, re.I) and _text(fv_n) == '':
                    ov['fillValue'] = {'emptyRef': True}
                elif re.search(r'TypeDescription$', fv_xt, re.I) and _text(fv_n) == '':
                    ov['fillValue'] = {'typeDescription': True}   # пустое типизированное (реквизит ValueType ПВХ) ≠ xs:string
                else:
                    ov['fillValue'] = convert_ch_scalar_node(fv_n)
            sa_cmt = _single(sa, 'xr:Comment')
            if sa_cmt is not None and _text(sa_cmt):
                ov['comment'] = _text(sa_cmt)
            sa_msk = _single(sa, 'xr:Mask')
            if sa_msk is not None and _text(sa_msk):
                ov['mask'] = _text(sa_msk)
            sa_fmt = get_ml_value(_single(sa, 'xr:Format'))
            if sa_fmt is not None:
                ov['format'] = sa_fmt
            sa_efmt = get_ml_value(_single(sa, 'xr:EditFormat'))
            if sa_efmt is not None:
                ov['editFormat'] = sa_efmt
            sa_cf = _single(sa, 'xr:ChoiceForm')
            if sa_cf is not None and _text(sa_cf):
                ov['choiceForm'] = _text(sa_cf)
            sa_cpl = parse_choice_parameter_links(sa, 'xr:ChoiceParameterLinks')
            if sa_cpl is not None:
                ov['choiceParameterLinks'] = sa_cpl
            sa_cp = parse_choice_parameters(sa, 'xr:ChoiceParameters')
            if sa_cp is not None:
                ov['choiceParameters'] = sa_cp
            # LinkByType стандартного реквизита (ExtDimensionN→Account у регистра бухгалтерии).
            sa_lbt = _single(sa, 'xr:LinkByType')
            if sa_lbt is not None:
                sa_lbt_dp = _single(sa_lbt, 'xr:DataPath')
                if sa_lbt_dp is not None and _text(sa_lbt_dp):
                    sa_lbt_li = _single(sa_lbt, 'xr:LinkItem')
                    li = int(_text(sa_lbt_li)) if (sa_lbt_li is not None and _text(sa_lbt_li)) else 0
                    ov['linkByType'] = {'dataPath': _text(sa_lbt_dp), 'linkItem': li}
            # Доп./опциональный реквизит (не в фикс-списке) — эмитим по присутствию даже без отклонений.
            # Формат 2.20: режим приведения типов. Компилятор выводит его сам (TransformValues,
            # у Owner — Deny), поэтому захватываем только отклонение от этого правила.
            sa_trm_n = _single(sa, 'xr:TypeReductionMode')
            if sa_trm_n is not None and (sa_trm_n.text or '').strip():
                sa_trm_def = 'Deny' if an == 'Owner' else 'TransformValues'
                if sa_trm_n.text.strip() != sa_trm_def:
                    ov['TypeReductionMode'] = sa_trm_n.text.strip()
            if len(ov) > 0 or (an not in std_fixed):
                sa_map[an] = ov
        if len(sa_map) > 0 or (obj_type in std_conditional_types):
            dsl['standardAttributes'] = sa_map
    elif obj_type in ('InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task', 'Enum', 'DocumentJournal'):
        # Регистр/БП/Задача опускают all-default блок → opt-out `standardAttributes:""`.
        dsl['standardAttributes'] = ''

    # --- ChildObjects: Attributes + TabularSections ---
    child_objs = _single(obj_node, 'md:ChildObjects')
    if child_objs is not None:
        # WebService: операции с параметрами. Строчное сокращение — только тип возврата.
        op_nodes = _nodes(child_objs, 'md:Operation')
        if len(op_nodes) > 0:
            ops = {}
            for op in op_nodes:
                op_p = _single(op, 'md:Properties')
                op_name = _text(_single(op_p, 'md:Name'))
                o = {}
                rt = get_xdto_type_value(_single(op_p, 'md:XDTOReturningValueType'))
                if rt and rt != 'xs:string':
                    o['returnType'] = rt
                if _text(_single(op_p, 'md:Nillable')) == 'true':
                    o['nillable'] = True
                if _text(_single(op_p, 'md:Transactioned')) == 'true':
                    o['transactioned'] = True
                pn = _text(_single(op_p, 'md:ProcedureName'))
                if pn and pn != op_name:
                    o['procedureName'] = pn
                dl = _text(_single(op_p, 'md:DataLockControlMode'))
                if dl and dl != 'Managed':
                    o['dataLockControlMode'] = dl
                osyn = get_ml_value(_single(op_p, 'md:Synonym'))
                if osyn is not None and str(osyn) != split_camel_words(op_name):
                    o['synonym'] = osyn
                ocmt = _text(_single(op_p, 'md:Comment'))
                if ocmt:
                    o['comment'] = ocmt
                par_nodes = _nodes(op, 'md:ChildObjects/md:Parameter')
                if len(par_nodes) > 0:
                    pars = {}
                    for par in par_nodes:
                        pp = _single(par, 'md:Properties')
                        par_name = _text(_single(pp, 'md:Name'))
                        po = {}
                        pt = get_xdto_type_value(_single(pp, 'md:XDTOValueType'))
                        if pt:
                            po['type'] = pt
                        if _text(_single(pp, 'md:Nillable')) == 'false':
                            po['nillable'] = False
                        pdir = _text(_single(pp, 'md:TransferDirection'))
                        if pdir and pdir != 'In':
                            po['direction'] = pdir
                        psyn = get_ml_value(_single(pp, 'md:Synonym'))
                        if psyn is not None and str(psyn) != split_camel_words(par_name):
                            po['synonym'] = psyn
                        pcmt = _text(_single(pp, 'md:Comment'))
                        if pcmt:
                            po['comment'] = pcmt
                        pars[par_name] = po['type'] if list(po.keys()) == ['type'] else po
                    o['parameters'] = pars
                ops[op_name] = o['returnType'] if list(o.keys()) == ['returnType'] else o
            dsl['operations'] = ops
        # HTTPService: шаблоны URL и их методы.
        tmpl_nodes = _nodes(child_objs, 'md:URLTemplate')
        if len(tmpl_nodes) > 0:
            tmpls = {}
            for t in tmpl_nodes:
                tp = _single(t, 'md:Properties')
                t_name = _text(_single(tp, 'md:Name'))
                t_obj = {}
                t_template = _text(_single(tp, 'md:Template'))
                if t_template:
                    t_obj['template'] = t_template
                t_syn = get_ml_value(_single(tp, 'md:Synonym'))
                if t_syn is not None and str(t_syn) != split_camel_words(t_name):
                    t_obj['synonym'] = t_syn
                t_cmt = _text(_single(tp, 'md:Comment'))
                if t_cmt:
                    t_obj['comment'] = t_cmt
                m_nodes = _nodes(t, 'md:ChildObjects/md:Method')
                if len(m_nodes) > 0:
                    methods = {}
                    for m in m_nodes:
                        mp = _single(m, 'md:Properties')
                        m_name = _text(_single(mp, 'md:Name'))
                        http_val = _text(_single(mp, 'md:HTTPMethod')) or 'GET'
                        handler_val = _text(_single(mp, 'md:Handler')) or ''
                        m_syn = get_ml_value(_single(mp, 'md:Synonym'))
                        m_cmt = _text(_single(mp, 'md:Comment'))
                        syn_default = m_syn is None or str(m_syn) == split_camel_words(m_name)
                        if handler_val == f'{t_name}{m_name}' and syn_default and not m_cmt:
                            methods[m_name] = http_val
                        else:
                            mo = {'httpMethod': http_val}
                            if handler_val:
                                mo['handler'] = handler_val
                            if not syn_default:
                                mo['synonym'] = m_syn
                            if m_cmt:
                                mo['comment'] = m_cmt
                            methods[m_name] = mo
                    t_obj['methods'] = methods
                tmpls[t_name] = t_obj
            dsl['urlTemplates'] = tmpls
        attrs = _nodes(child_objs, 'md:Attribute')
        if len(attrs) > 0:
            arr = []
            for a in attrs:
                arr.append(attr_to_dsl(a))
            dsl['attributes'] = arr
        # Enum: значения перечисления.
        ev_nodes = _nodes(child_objs, 'md:EnumValue')
        if len(ev_nodes) > 0:
            ev_arr = []
            for ev in ev_nodes:
                evp = _single(ev, 'md:Properties')
                ev_name = _text(_single(evp, 'md:Name'))
                ev_syn_node = _single(evp, 'md:Synonym')
                ev_syn = get_ml_value(ev_syn_node)
                ev_cmt_n = _single(evp, 'md:Comment')
                ev_cmt = _text(ev_cmt_n) if ev_cmt_n is not None else ''
                ev_syn_val = None
                if isinstance(ev_syn, str):
                    if ne_cs(ev_syn, split_camel_words(ev_name)):
                        ev_syn_val = ev_syn
                elif ev_syn is not None:
                    ev_syn_val = ev_syn
                elif ev_syn_node is not None:
                    ev_syn_val = ''
                if (ev_syn_val is not None) or ev_cmt:
                    o = {'name': ev_name}
                    if ev_syn_val is not None:
                        o['synonym'] = ev_syn_val
                    if ev_cmt:
                        o['comment'] = ev_cmt
                    ev_arr.append(o)
                else:
                    ev_arr.append(ev_name)
            dsl['values'] = ev_arr
        # DocumentJournal: колонки.
        col_nodes = _nodes(child_objs, 'md:Column')
        if len(col_nodes) > 0:
            col_arr = []
            for col in col_nodes:
                cp = _single(col, 'md:Properties')
                c_name = _text(_single(cp, 'md:Name'))
                o = {'name': c_name}
                c_syn_node = _single(cp, 'md:Synonym')
                c_syn = get_ml_value(c_syn_node)
                if isinstance(c_syn, str):
                    if ne_cs(c_syn, split_camel_words(c_name)):
                        o['synonym'] = c_syn
                elif c_syn is not None:
                    o['synonym'] = c_syn
                elif c_syn_node is not None:
                    o['synonym'] = ''   # пустой <Synonym/> ≠ авто-синоним → явный ''
                c_cmt_n = _single(cp, 'md:Comment')
                if c_cmt_n is not None and _text(c_cmt_n):
                    o['comment'] = _text(c_cmt_n)
                c_idx_n = _single(cp, 'md:Indexing')
                if c_idx_n is not None and _text(c_idx_n) != 'DontIndex':
                    o['indexing'] = _text(c_idx_n)
                c_ref_node = _single(cp, 'md:References')
                refs = []
                if c_ref_node is not None:
                    for it in _nodes(c_ref_node, 'xr:Item'):
                        refs.append(_text(it))
                o['references'] = refs
                col_arr.append(o)
            dsl['columns'] = col_arr
        # ChartOfAccounts: признаки учёта.
        acct_flag_nodes = _nodes(child_objs, 'md:AccountingFlag')
        if len(acct_flag_nodes) > 0:
            arr = []
            for a in acct_flag_nodes:
                arr.append(attr_to_dsl(a))
            dsl['accountingFlags'] = arr
        ext_dim_flag_nodes = _nodes(child_objs, 'md:ExtDimensionAccountingFlag')
        if len(ext_dim_flag_nodes) > 0:
            arr = []
            for a in ext_dim_flag_nodes:
                arr.append(attr_to_dsl(a))
            dsl['extDimensionAccountingFlags'] = arr
        # Sequence: измерения несут DocumentMap/RegisterRecordsMap.
        dim_nodes = _nodes(child_objs, 'md:Dimension')
        if len(dim_nodes) > 0 and obj_type == 'Sequence':
            arr = []
            for dn in dim_nodes:
                dp = _single(dn, 'md:Properties')
                d_name = _text(_single(dp, 'md:Name'))
                o = {'name': d_name}
                d_syn = get_ml_value(_single(dp, 'md:Synonym'))
                if isinstance(d_syn, str):
                    if ne_cs(d_syn, split_camel_words(d_name)):
                        o['synonym'] = d_syn
                elif d_syn is not None:
                    o['synonym'] = d_syn
                d_cmt_n = _single(dp, 'md:Comment')
                if d_cmt_n is not None and _text(d_cmt_n):
                    o['comment'] = _text(d_cmt_n)
                dt = get_type_shorthand(_single(dp, 'md:Type'))
                if dt:
                    o['type'] = dt
                for mp in (('DocumentMap', 'documentMap'), ('RegisterRecordsMap', 'registerRecordsMap')):
                    mn = _single(dp, "md:" + mp[0])
                    if mn is not None:
                        m_items = [_text(x) for x in _nodes(mn, 'xr:Item')]
                        if len(m_items) > 0:
                            o[mp[1]] = list(m_items)
                arr.append(o)
            dsl['dimensions'] = arr
        elif len(dim_nodes) > 0:
            arr = []
            for a in dim_nodes:
                arr.append(attr_to_dsl(a))
            dsl['dimensions'] = arr
        res_nodes = _nodes(child_objs, 'md:Resource')
        if len(res_nodes) > 0:
            arr = []
            for a in res_nodes:
                arr.append(attr_to_dsl(a))
            dsl['resources'] = arr
        # Задача: реквизиты адресации.
        addr_nodes = _nodes(child_objs, 'md:AddressingAttribute')
        if len(addr_nodes) > 0:
            arr = []
            for a in addr_nodes:
                arr.append(attr_to_dsl(a))
            dsl['addressingAttributes'] = arr
        ts_nodes = _nodes(child_objs, 'md:TabularSection')
        if len(ts_nodes) > 0:
            ts_map = {}
            for ts in ts_nodes:
                tsp = _single(ts, 'md:Properties')
                ts_name = _text(_single(tsp, 'md:Name'))
                tco = _single(ts, 'md:ChildObjects')
                cols = []
                if tco is not None:
                    for ca in _nodes(tco, 'md:Attribute'):
                        cols.append(attr_to_dsl(ca))
                # Синоним/подсказка/комментарий ТЧ.
                ts_syn = get_ml_value(_single(tsp, 'md:Synonym'))
                ts_syn_custom = False
                if isinstance(ts_syn, str):
                    if ne_cs(ts_syn, split_camel_words(ts_name)):
                        ts_syn_custom = True
                elif ts_syn is not None:
                    ts_syn_custom = True
                ts_tt = get_ml_value(_single(tsp, 'md:ToolTip'))
                ts_cmt_n = _single(tsp, 'md:Comment')
                ts_cmt = _text(ts_cmt_n) if ts_cmt_n is not None else ''
                ts_fc_n = _single(tsp, 'md:FillChecking')
                ts_fc = _text(ts_fc_n) if (ts_fc_n is not None and _text(ts_fc_n) != 'DontCheck') else ''
                # Use ТЧ (иерархические Catalog/ПВХ: ForItem/ForFolder/ForFolderAndItem; omit при дефолте ForItem).
                ts_use_n = _single(tsp, 'md:Use')
                ts_use = _text(ts_use_n) if (ts_use_n is not None and _text(ts_use_n) != 'ForItem') else ''
                # TS-блок стандартных реквизитов (LineNumber).
                ln_obj = {}
                sa_ts_node = _single(tsp, 'md:StandardAttributes')
                has_block = (sa_ts_node is not None and len(_nodes(sa_ts_node, 'xr:StandardAttribute')) > 0)
                ln_node = _single(sa_ts_node, "xr:StandardAttribute[@name='LineNumber']") if has_block else None
                if ln_node is not None:
                    ln_syn = get_ml_value(_single(ln_node, 'xr:Synonym'))
                    if ln_syn is not None:
                        ln_obj['synonym'] = ln_syn
                    ln_cmt_n = _single(ln_node, 'xr:Comment')
                    if ln_cmt_n is not None and _text(ln_cmt_n):
                        ln_obj['comment'] = _text(ln_cmt_n)
                    ln_fts_n = _single(ln_node, 'xr:FullTextSearch')
                    if ln_fts_n is not None and _text(ln_fts_n) != 'Use':
                        ln_obj['fullTextSearch'] = _text(ln_fts_n)
                    ln_tt = get_ml_value(_single(ln_node, 'xr:ToolTip'))
                    if ln_tt is not None:
                        ln_obj['tooltip'] = ln_tt
                    ln_fmt = get_ml_value(_single(ln_node, 'xr:Format'))
                    if ln_fmt is not None:
                        ln_obj['format'] = ln_fmt
                    ln_efmt = get_ml_value(_single(ln_node, 'xr:EditFormat'))
                    if ln_efmt is not None:
                        ln_obj['editFormat'] = ln_efmt
                    ln_chi_n = _single(ln_node, 'xr:ChoiceHistoryOnInput')
                    if ln_chi_n is not None and _text(ln_chi_n) != 'Auto':
                        ln_obj['choiceHistoryOnInput'] = _text(ln_chi_n)
                    ln_fv_n = _single(ln_node, 'xr:FillValue')
                    if ln_fv_n is not None and _attr(ln_fv_n, 'nil', NS_XSI) != 'true':
                        ln_fv_t = _attr(ln_fv_n, 'type', NS_XSI)
                        if re.search(r'decimal$', ln_fv_t, re.I):
                            ln_obj['fillValue'] = int(_text(ln_fv_n)) if re.match(r'^-?\d+$', _text(ln_fv_n)) else float(_text(ln_fv_n))
                # Формат 2.20: длина номера строки ТЧ. Захватываем ВСЕГДА при наличии тега, а не
                # omit-on-default: дефолт зависит от режима совместимости конфигурации (<=8_3_26 → 5,
                # >=8_3_27 → 9) и фиксируется платформой при создании ТЧ, так что вывести его здесь
                # значило бы продублировать логику компилятора с риском разойтись. Явный захват точен.
                ts_lnl_n = _single(tsp, 'md:LineNumberLength')
                ts_lnl = int(ts_lnl_n.text) if ts_lnl_n is not None and (ts_lnl_n.text or '').strip() else None
                if ts_syn_custom or (ts_tt is not None) or ts_cmt or ts_fc or ts_use or len(ln_obj) > 0 or (not has_block) or (ts_lnl is not None):
                    to = {}
                    if ts_syn_custom:
                        to['synonym'] = ts_syn
                    if ts_tt is not None:
                        to['tooltip'] = ts_tt
                    if ts_cmt:
                        to['comment'] = ts_cmt
                    if ts_fc:
                        to['fillChecking'] = ts_fc
                    if ts_use:
                        to['use'] = ts_use
                    if ts_lnl is not None:
                        to['lineNumberLength'] = ts_lnl
                    if not has_block:
                        to['lineNumber'] = ''
                    elif len(ln_obj) > 0:
                        to['lineNumber'] = ln_obj
                    to['attributes'] = cols
                    ts_map[ts_name] = to
                else:
                    ts_map[ts_name] = cols
            dsl['tabularSections'] = ts_map
        # --- Commands (полноблочные <Command> в ChildObjects) → DSL commands. ---
        cmd_nodes = _nodes(child_objs, 'md:Command')
        if len(cmd_nodes) > 0:
            cmd_map = {}
            for cm in cmd_nodes:
                cp = _single(cm, 'md:Properties')
                cn = _text(_single(cp, 'md:Name'))
                o = {}
                syn = get_ml_value(_single(cp, 'md:Synonym'))
                if isinstance(syn, str):
                    if ne_cs(syn, split_camel_words(cn)):
                        o['synonym'] = syn
                elif syn is not None:
                    o['synonym'] = syn
                cmt_n = _single(cp, 'md:Comment')
                if cmt_n is not None and _text(cmt_n):
                    o['comment'] = _text(cmt_n)
                grp_n = _single(cp, 'md:Group')
                if grp_n is not None and _text(grp_n):
                    o['group'] = _text(grp_n)
                cpt = get_type_shorthand(_single(cp, 'md:CommandParameterType'))
                if cpt:
                    o['commandParameterType'] = cpt
                pum_n = _single(cp, 'md:ParameterUseMode')
                if pum_n is not None and _text(pum_n) != 'Single':
                    o['parameterUseMode'] = _text(pum_n)
                md_n = _single(cp, 'md:ModifiesData')
                if md_n is not None and _text(md_n) == 'true':
                    o['modifiesData'] = True
                rep_n = _single(cp, 'md:Representation')
                if rep_n is not None and _text(rep_n) != 'Auto':
                    o['representation'] = _text(rep_n)
                ctt = get_ml_value(_single(cp, 'md:ToolTip'))
                if ctt is not None:
                    o['tooltip'] = ctt
                # <Picture> — структурный блок.
                ref_n = _single(cp, 'md:Picture/xr:Ref')
                abs_n = _single(cp, 'md:Picture/xr:Abs')
                if ref_n is not None or abs_n is not None:
                    psrc = _text(ref_n) if ref_n is not None else ("abs:" + _text(abs_n))
                    lt_n = _single(cp, 'md:Picture/xr:LoadTransparent')
                    lt_false = (lt_n is not None and _text(lt_n) == 'false')
                    tpx_n = _single(cp, 'md:Picture/xr:TransparentPixel')
                    if tpx_n is not None:
                        po = {'src': psrc}
                        if lt_false:
                            po['loadTransparent'] = False
                        po['transparentPixel'] = {'x': int(_attr(tpx_n, 'x')), 'y': int(_attr(tpx_n, 'y'))}
                        o['picture'] = po
                    else:
                        o['picture'] = psrc
                        if lt_false:
                            o['loadTransparent'] = False
                sc_n = _single(cp, 'md:Shortcut')
                if sc_n is not None and _text(sc_n):
                    o['shortcut'] = _text(sc_n)
                osu_n = _single(cp, 'md:OnMainServerUnavalableBehavior')
                if osu_n is not None and _text(osu_n) != 'Auto':
                    o['onMainServerUnavalableBehavior'] = _text(osu_n)
                cmd_map[cn] = o
            dsl['commands'] = cmd_map

    # --- Предопределённые (соседний Ext/Predefined.xml) → DSL predefined. ---
    obj_dir = os.path.dirname(os.path.abspath(object_path))
    predef_path = os.path.join(obj_dir, obj_name, 'Ext', 'Predefined.xml')
    if os.path.isfile(predef_path):
        pdoc = etree.parse(predef_path).getroot()
        root_items = []
        if obj_type == 'ChartOfAccounts':
            for it in _lxn(pdoc, "*[local-name()='Item']"):
                root_items.append(predef_account_to_dsl(it))
        elif obj_type == 'ChartOfCalculationTypes':
            for it in _lxn(pdoc, "*[local-name()='Item']"):
                root_items.append(predef_calc_type_to_dsl(it))
        else:
            for it in _lxn(pdoc, "*[local-name()='Item']"):
                root_items.append(predef_item_to_dsl(it))
        if len(root_items) > 0:
            dsl['predefined'] = root_items

    # --- Состав плана обмена (соседний Ext/Content.xml) → DSL content (ExchangePlan). ---
    if obj_type == 'ExchangePlan':
        content_path = os.path.join(obj_dir, obj_name, 'Ext', 'Content.xml')
        if os.path.isfile(content_path):
            cdoc = etree.parse(content_path).getroot()
            content_items = []
            for it in _lxn(cdoc, "*[local-name()='Item']"):
                md_el = _lx1(it, "*[local-name()='Metadata']")
                if md_el is None or not _text(md_el):
                    continue
                ref = _text(md_el)
                ar_el = _lx1(it, "*[local-name()='AutoRecord']")
                ar = _text(ar_el) if ar_el is not None else 'Deny'
                if ar == 'Allow':
                    content_items.append("%s: autoRecord" % ref)
                else:
                    content_items.append(ref)
            if len(content_items) > 0:
                dsl['content'] = content_items


SUPPORTED_TYPES = (
    'Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document',
    'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task',
    'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence',
    'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob',
    'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter',
    'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService', 'WebService',
)


def main():
    global props, obj_node, obj_type, obj_name, object_path

    parser = argparse.ArgumentParser(description='Decompile 1C metadata object XML to JSON DSL (draft)', allow_abbrev=False)
    parser.add_argument('-ObjectPath', '-Path', dest='ObjectPath', type=str, required=True)
    parser.add_argument('-OutputPath', dest='OutputPath', type=str, default=None)
    args = ci_parse_args(parser)

    object_path = args.ObjectPath
    if not os.path.exists(object_path):
        sys.stderr.write("meta-decompile: файл не найден: %s\n" % object_path)
        sys.exit(2)

    # --- XML загрузка ---
    doc = etree.parse(object_path)
    root_el = doc.getroot()
    if _localname(root_el) != 'MetaDataObject':
        sys.stderr.write("meta-decompile: ожидался root <MetaDataObject>, получен <%s>\n" % _localname(root_el))
        sys.exit(3)
    # Первый элемент-потомок MetaDataObject = объект; его LocalName = тип.
    obj_node = None
    for c in root_el:
        if isinstance(c.tag, str):
            obj_node = c
            break
    if obj_node is None:
        sys.stderr.write("meta-decompile: пустой MetaDataObject\n")
        sys.exit(3)
    obj_type = _localname(obj_node)

    if obj_type not in SUPPORTED_TYPES:
        sys.stderr.write("meta-decompile: тип '%s' пока не поддержан (…, CommonPicture, CommonTemplate)\n" % obj_type)
        sys.exit(3)

    props = _single(obj_node, 'md:Properties')
    obj_name = P('Name')

    build_dsl()

    # === Вывод ===
    json_str = convert_to_compact_json(dsl, 0)
    if args.OutputPath:
        with open(args.OutputPath, 'w', encoding='utf-8', newline='') as f:
            f.write(json_str)
    else:
        sys.stdout.write(json_str + "\n")


if __name__ == '__main__':
    main()
