# meta-decompile v0.65 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
#
# Поддержаны: 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).
[CmdletBinding(PositionalBinding=$false)]
param(
	[Parameter(Mandatory)]
	[Alias('Path')]
	[string]$ObjectPath,

	[string]$OutputPath
)

$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

if (-not (Test-Path -LiteralPath $ObjectPath)) {
	[Console]::Error.WriteLine("meta-decompile: файл не найден: $ObjectPath"); exit 2
}

# --- JSON-эмиттер (контроль порядка/массивов/кириллицы) ---
function ConvertTo-CompactJson {
	param($node, [int]$depth = 0)
	$pad = "  " * $depth
	$pad1 = "  " * ($depth + 1)
	if ($null -eq $node) { return "null" }
	if ($node -is [bool]) { return $(if ($node) { "true" } else { "false" }) }
	if ($node -is [int] -or $node -is [long] -or $node -is [double]) { return "$node" }
	if ($node -is [System.Collections.IDictionary]) {
		if ($node.Count -eq 0) { return "{}" }
		$items = @()
		foreach ($k in $node.Keys) {
			$items += "$pad1$(Quote-Json ([string]$k)): $(ConvertTo-CompactJson $node[$k] ($depth + 1))"
		}
		return "{`n" + ($items -join ",`n") + "`n$pad}"
	}
	if ($node -is [System.Collections.IList]) {
		if ($node.Count -eq 0) { return "[]" }
		# Массив скаляров-строк — компактно в строку; массив объектов — по строкам.
		$allScalar = $true
		foreach ($e in $node) { if ($e -is [System.Collections.IDictionary] -or $e -is [System.Collections.IList]) { $allScalar = $false; break } }
		if ($allScalar) {
			$items = @(); foreach ($e in $node) { $items += (ConvertTo-CompactJson $e ($depth + 1)) }
			return "[" + ($items -join ", ") + "]"
		}
		$items = @(); foreach ($e in $node) { $items += "$pad1$(ConvertTo-CompactJson $e ($depth + 1))" }
		return "[`n" + ($items -join ",`n") + "`n$pad]"
	}
	return Quote-Json ([string]$node)
}
function Quote-Json {
	param([string]$s)
	$sb = New-Object System.Text.StringBuilder
	[void]$sb.Append('"')
	foreach ($ch in $s.ToCharArray()) {
		switch ($ch) {
			'"'  { [void]$sb.Append('\"') }
			'\'  { [void]$sb.Append('\\') }
			"`n" { [void]$sb.Append('\n') }
			"`r" { [void]$sb.Append('\r') }
			"`t" { [void]$sb.Append('\t') }
			default {
				if ([int]$ch -lt 32) { [void]$sb.Append(('\u{0:x4}' -f [int]$ch)) }
				else { [void]$sb.Append($ch) }
			}
		}
	}
	[void]$sb.Append('"')
	return $sb.ToString()
}

# --- XML загрузка + namespace manager ---
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $true
$doc.Load((Resolve-Path -LiteralPath $ObjectPath).Path)
$nsm = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
$nsm.AddNamespace('md',  'http://v8.1c.ru/8.3/MDClasses')
$nsm.AddNamespace('v8',  'http://v8.1c.ru/8.1/data/core')
$nsm.AddNamespace('xr',  'http://v8.1c.ru/8.3/xcf/readable')
$nsm.AddNamespace('xsi', 'http://www.w3.org/2001/XMLSchema-instance')
$nsm.AddNamespace('app', 'http://v8.1c.ru/8.2/managed-application/core')

$rootEl = $doc.DocumentElement
if ($rootEl.LocalName -ne 'MetaDataObject') {
	[Console]::Error.WriteLine("meta-decompile: ожидался root <MetaDataObject>, получен <$($rootEl.LocalName)>"); exit 3
}
# Первый элемент-потомок MetaDataObject = объект; его LocalName = тип.
$objNode = $null
foreach ($c in $rootEl.ChildNodes) { if ($c.NodeType -eq 'Element') { $objNode = $c; break } }
if (-not $objNode) { [Console]::Error.WriteLine("meta-decompile: пустой MetaDataObject"); exit 3 }
$objType = $objNode.LocalName

if ($objType -notin @('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')) {
	[Console]::Error.WriteLine("meta-decompile: тип '$objType' пока не поддержан (…, CommonPicture, CommonTemplate)"); exit 3
}

$props = $objNode.SelectSingleNode('md:Properties', $nsm)
function P { param([string]$tag) $n = $props.SelectSingleNode("md:$tag", $nsm); if ($n) { return $n.InnerText } else { return $null } }

# --- Synonym (ru) ---
function Get-MLru {
	param($node)
	if (-not $node) { return $null }
	$it = $node.SelectSingleNode("v8:item[v8:lang='ru']/v8:content", $nsm)
	if ($it) { return $it.InnerText }
	return $null
}
# ML-значение → строка (если единственный item ru) ЛИБО [ordered]{lang:content} (мультиязычно, порядок из XML).
# null если контента нет. Компактная строка для ru-only, объект для мультиязычных.
function Get-MLValue {
	param($node)
	if (-not $node) { return $null }
	$items = @($node.SelectNodes('v8:item', $nsm))
	if ($items.Count -eq 0) { return $null }
	if ($items.Count -eq 1) {
		$lang = $items[0].SelectSingleNode('v8:lang', $nsm).InnerText
		$content = $items[0].SelectSingleNode('v8:content', $nsm).InnerText
		# Единственный ru-item: пустое содержимое ≡ отсутствие значения → $null (иначе tooltip:"" ≠ self-close).
		if ($lang -eq 'ru') { if ($content -eq '') { return $null } else { return $content } }
	}
	$o = [ordered]@{}
	foreach ($it in $items) {
		$l = $it.SelectSingleNode('v8:lang', $nsm).InnerText
		$c = $it.SelectSingleNode('v8:content', $nsm).InnerText
		$o[$l] = $c
	}
	return $o
}
# Авто-синоним: точное зеркало Split-CamelCase из meta-compile (стр.354). Совпало → ключ опускаем.
# ВАЖНО: логика должна совпадать байт-в-байт с компилятором, иначе ложные «синоним==авто» → диффы.
function Split-CamelWords {
	param([string]$name)
	if (-not $name) { return $name }
	$result = [regex]::Replace($name, '([а-яё])([А-ЯЁ])', '$1 $2')
	$result = [regex]::Replace($result, '([a-z])([A-Z])', '$1 $2')
	# HE-эвристика (зеркало Split-CamelCase компилятора): сохраняем прогон заглавных >=2, если сразу за ним
	# НЕ буква (пробел/цифра/спецсимвол/конец); прилипшие предлоги/бренды перед буквой → лоуэркейз. Первый символ — как есть.
	if ($result.Length -gt 1) {
		$chars = $result.ToCharArray()
		$n = $chars.Length
		$keep = New-Object 'bool[]' $n
		$i = 0
		while ($i -lt $n) {
			if ([char]::IsUpper($chars[$i])) {
				$j = $i
				while ($j -lt $n -and [char]::IsUpper($chars[$j])) { $j++ }
				$afterBoundary = ($j -eq $n) -or (-not [char]::IsLetter($chars[$j]))
				if (($j - $i) -ge 2 -and $afterBoundary) { for ($k = $i; $k -lt $j; $k++) { $keep[$k] = $true } }
				$i = $j
			} else { $i++ }
		}
		$sb = New-Object System.Text.StringBuilder
		for ($idx = 0; $idx -lt $n; $idx++) {
			$c = $chars[$idx]
			if ($idx -eq 0 -or $keep[$idx]) { [void]$sb.Append($c) }
			elseif ([char]::IsUpper($c)) { [void]$sb.Append([char]::ToLower($c)) }
			else { [void]$sb.Append($c) }
		}
		$result = $sb.ToString()
	}
	return $result
}

$objName = P 'Name'

# --- Тип реквизита: <Type> → shorthand-строка ---
function Strip-NsPrefix { param([string]$s) if ($s -match ':') { return ($s -split ':', 2)[1] } else { return $s } }
function Get-TypeShorthand {
	param($typeNode)
	if (-not $typeNode) { return "" }
	$parts = @()
	$children = @($typeNode.ChildNodes | Where-Object { $_.NodeType -eq 'Element' })
	for ($i = 0; $i -lt $children.Count; $i++) {
		$c = $children[$i]
		$ln = $c.LocalName
		if ($ln -eq 'Type') {
			$raw = $c.InnerText.Trim()
			$next = if ($i + 1 -lt $children.Count) { $children[$i+1] } else { $null }
			switch -regex ($raw) {
				'(^|:)boolean$'    { $parts += 'Boolean'; break }
				'(^|:)string$'     {
					$len = '10'; $al = ''
					$sq = if ($next -and $next.LocalName -eq 'StringQualifiers') { $next } else { $typeNode.SelectSingleNode('v8:StringQualifiers', $nsm) }
					if ($sq) {
						$l = $sq.SelectSingleNode('v8:Length', $nsm); if ($l) { $len = $l.InnerText }
						$aln = $sq.SelectSingleNode('v8:AllowedLength', $nsm); if ($aln -and $aln.InnerText -eq 'Fixed') { $al = ',fixed' }
					}
					$parts += "String($len$al)"; break
				}
				'(^|:)decimal$'    {
					$d = '10'; $f = '0'; $sign = ''
					$nq = if ($next -and $next.LocalName -eq 'NumberQualifiers') { $next } else { $typeNode.SelectSingleNode('v8:NumberQualifiers', $nsm) }
					if ($nq) {
						$dn = $nq.SelectSingleNode('v8:Digits', $nsm); if ($dn) { $d = $dn.InnerText }
						$fn = $nq.SelectSingleNode('v8:FractionDigits', $nsm); if ($fn) { $f = $fn.InnerText }
						$sn = $nq.SelectSingleNode('v8:AllowedSign', $nsm); if ($sn -and $sn.InnerText -eq 'Nonnegative') { $sign = ',nonneg' }
					}
					$parts += "Number($d,$f$sign)"; break
				}
				'(^|:)dateTime$'   {
					$fr = 'DateTime'
					$dq = if ($next -and $next.LocalName -eq 'DateQualifiers') { $next } else { $typeNode.SelectSingleNode('v8:DateQualifiers', $nsm) }
					if ($dq) { $dn = $dq.SelectSingleNode('v8:DateFractions', $nsm); if ($dn) { $fr = $dn.InnerText } }
					$parts += $fr; break   # Date | DateTime
				}
				'(^|:)base64Binary$' { $parts += 'ValueStorage'; break }
				default            { $parts += (Strip-NsPrefix $raw) }   # cfg:CatalogRef.X → CatalogRef.X
			}
		} elseif ($ln -eq 'TypeSet') {
			$parts += (Strip-NsPrefix $c.InnerText.Trim())   # cfg:DefinedType.X → DefinedType.X
		}
	}
	return ($parts -join ' + ')
}

# Скалярное значение параметра выбора (<Value xsi:type=...>) → JSON-значение (bool/число/строка).
# string/dateTime/DesignTimeRef → строка (компилятор auto-детектит обратно).
function Convert-ChScalarNode {
	param($vN)
	# nil-элемент массива (<v8:Value xsi:nil="true"/>) → JSON null. Без этого он приезжал пустой
	# строкой и компилятор эмитил xs:string вместо nil.
	if ($vN.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance') -eq 'true') { return $null }
	$xt = $vN.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
	$txt = $vN.InnerText
	if ($xt -match 'boolean$') { return ($txt -eq 'true') }
	if ($xt -match 'decimal$') {
		if ($txt -match '^-?\d+$') { return [int]$txt }
		return [double]::Parse($txt, [System.Globalization.CultureInfo]::InvariantCulture)
	}
	# Пустой DesignTimeRef ≠ пустая строка: без маркера тип терялся, и компилятор эмитил xs:string.
	# Та же конвенция, что у fillValue (см. ниже) — маркер emptyRef.
	if ($xt -match 'DesignTimeRef$' -and $txt -eq '') { return [ordered]@{ emptyRef = $true } }
	return $txt
}
# app:value (тип прямо на узле) → значение ЛИБО массив (v8:FixedArray с детьми v8:Value).
function Get-ChoiceParamValue {
	param($valNode)
	$xt = $valNode.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
	if ($xt -match 'FixedArray$') {
		$arr = [System.Collections.ArrayList]@()
		foreach ($sub in @($valNode.SelectNodes('v8:Value', $nsm))) { [void]$arr.Add((Convert-ChScalarNode $sub)) }
		return ,$arr   # унарная запятая: PS иначе разворачивает 1-элементную коллекцию при return → FixedArray станет скаляром
	}
	return Convert-ChScalarNode $valNode
}

# <ChoiceParameterLinks> → [{name,dataPath,valueChange?}] | строки "name=dataPath". $tag: 'md:...' (реквизит) | 'xr:...' (станд.).
function Parse-ChoiceParameterLinks {
	param($parent, [string]$tag)
	$node = $parent.SelectSingleNode($tag, $nsm)
	if (-not $node) { return $null }
	$links = @($node.SelectNodes('xr:Link', $nsm))
	if ($links.Count -eq 0) { return $null }
	$arr = [System.Collections.ArrayList]@()
	foreach ($lk in $links) {
		$lName = $lk.SelectSingleNode('xr:Name', $nsm).InnerText
		$lDp = $lk.SelectSingleNode('xr:DataPath', $nsm).InnerText
		$vcN = $lk.SelectSingleNode('xr:ValueChange', $nsm); $vcv = if ($vcN) { $vcN.InnerText } else { 'Clear' }
		if ($vcv -eq 'Clear') { [void]$arr.Add("$lName=$lDp") }
		else { [void]$arr.Add([ordered]@{ name = $lName; dataPath = $lDp; valueChange = $vcv }) }
	}
	return $arr
}
# <ChoiceParameters> → [{name,value?}]. $tag: 'md:...' | 'xr:...'.
function Parse-ChoiceParameters {
	param($parent, [string]$tag)
	$node = $parent.SelectSingleNode($tag, $nsm)
	if (-not $node) { return $null }
	$items = @($node.SelectNodes('app:item', $nsm))
	if ($items.Count -eq 0) { return $null }
	$arr = [System.Collections.ArrayList]@()
	foreach ($it in $items) {
		$pName = $it.GetAttribute('name')
		$valN = $it.SelectSingleNode('app:value', $nsm)
		$nilAttr = if ($valN) { $valN.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance') } else { '' }
		if (-not $valN -or $nilAttr -eq 'true') { [void]$arr.Add([ordered]@{ name = $pName }) }
		else { $o = [ordered]@{ name = $pName }; $o['value'] = Get-ChoiceParamValue $valN; [void]$arr.Add($o) }
	}
	return $arr
}

# --- Реквизит → DSL: shorthand-строка "Имя: Тип | флаги" ЛИБО object-форма при кастомном синониме.
# (Синоним ≠ авто → object {name, type, synonym, [flags]}; иначе компактный shorthand.) ---
function Attr-ToDsl {
	param($attrNode)
	$ap = $attrNode.SelectSingleNode('md:Properties', $nsm)
	$nm = ($ap.SelectSingleNode('md:Name', $nsm)).InnerText
	$ts = Get-TypeShorthand ($ap.SelectSingleNode('md:Type', $nsm))
	$flags = @()
	$fc = $ap.SelectSingleNode('md:FillChecking', $nsm); if ($fc -and $fc.InnerText -eq 'ShowError') { $flags += 'req' }
	$ix = $ap.SelectSingleNode('md:Indexing', $nsm)
	if ($ix) { if ($ix.InnerText -eq 'Index') { $flags += 'index' } elseif ($ix.InnerText -eq 'IndexWithAdditionalOrder') { $flags += 'indexAdditional' } }
	$ml = $ap.SelectSingleNode('md:MultiLine', $nsm); if ($ml -and $ml.InnerText -eq 'true') { $flags += 'multiline' }

	# Синоним/подсказка (строка ru-only ИЛИ {ru,en}).
	$synNode = $ap.SelectSingleNode('md:Synonym', $nsm)
	$synVal = Get-MLValue $synNode
	$synCustom = $false
	# Пустой <Synonym/> (узел есть, значения нет) ≠ авто-синоним из имени → явный пустой (synonym:"").
	# У Catalog-реквизитов не встречается (0/4018), у части ExchangePlan — да.
	$synEmpty = ($synNode -and $null -eq $synVal)
	if ($synVal -is [string]) { if ($synVal -cne (Split-CamelWords $nm)) { $synCustom = $true } }
	elseif ($null -ne $synVal) { $synCustom = $true }   # {ru,en} = всегда кастом
	$ttVal = Get-MLValue ($ap.SelectSingleNode('md:ToolTip', $nsm))

	# Extra-свойства реквизита (omit-on-default). Наличие любого → object-форма.
	# $en(tag) → InnerText узла или $null.
	$en = { param($tag) $n = $ap.SelectSingleNode("md:$tag", $nsm); if ($n) { $n.InnerText } else { $null } }
	$extra = [ordered]@{}
	$v = & $en 'Comment'; if ($v) { $extra['comment'] = $v }
	$v = & $en 'FullTextSearch'; if ($v -and $v -ne 'Use') { $extra['fullTextSearch'] = $v }
	$v = & $en 'FillFromFillingValue'; if ($v -eq 'true') { $extra['fillFromFillingValue'] = $true }
	$v = & $en 'CreateOnInput'; if ($v -and $v -ne 'Auto') { $extra['createOnInput'] = $v }
	$v = & $en 'QuickChoice'; if ($v -and $v -ne 'Auto') { $extra['quickChoice'] = $v }
	$v = & $en 'DataHistory'; if ($v -and $v -ne 'Use') { $extra['dataHistory'] = $v }
	$v = & $en 'Use'; if ($v -and $v -ne 'ForItem') { $extra['use'] = $v }
	$v = & $en 'PasswordMode'; if ($v -eq 'true') { $extra['passwordMode'] = $true }
	$v = & $en 'Mask'; if ($v) { $extra['mask'] = $v }
	$v = & $en 'ChoiceHistoryOnInput'; if ($v -and $v -ne 'Auto') { $extra['choiceHistoryOnInput'] = $v }
	$v = & $en 'FillChecking'; if ($v -eq 'ShowWarning') { $extra['fillChecking'] = 'ShowWarning' }
	$v = & $en 'ExtendedEdit'; if ($v -eq 'true') { $extra['extendedEdit'] = $true }
	$v = & $en 'MarkNegatives'; if ($v -eq 'true') { $extra['markNegatives'] = $true }
	$v = & $en 'ChoiceFoldersAndItems'; if ($v -and $v -ne 'Items') { $extra['choiceFoldersAndItems'] = $v }
	$v = & $en 'ChoiceForm'; if ($v) { $extra['choiceForm'] = $v }
	# Регистро-специфика измерения (теги присутствуют только у Dimension → безвредно для прочих).
	$v = & $en 'Master'; if ($v -eq 'true') { $extra['master'] = $true }
	$v = & $en 'MainFilter'; if ($v -eq 'true') { $extra['mainFilter'] = $true }
	$v = & $en 'DenyIncompleteValues'; if ($v -eq 'true') { $extra['denyIncompleteValues'] = $true }
	$v = & $en 'UseInTotals'; if ($v -eq 'false') { $extra['useInTotals'] = $false }  # дефолт true → захват при false
	# Формат 2.20: режим приведения типов измерения РС. Дефолт TransformValues (его компилятор
	# эмитит сам) → захватываем только отклонение.
	$v = & $en 'TypeReductionMode'; if ($v -and $v -ne 'TransformValues') { $extra['typeReductionMode'] = $v }
	$v = & $en 'BaseDimension'; if ($v -eq 'true') { $extra['baseDimension'] = $true }
	$v = & $en 'ScheduleLink'; if ($v) { $extra['scheduleLink'] = $v }  # ссылка на измерение графика (пустой → пропуск)
	$v = & $en 'Balance'; if ($v -eq '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→число.
	foreach ($mm in @('MinValue','MaxValue')) {
		$mn = $ap.SelectSingleNode("md:$mm", $nsm)
		if (-not $mn) { continue }
		if ($mn.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance') -eq 'true') { continue }
		$key = if ($mm -eq 'MinValue') { 'minValue' } else { 'maxValue' }
		$xt = $mn.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
		$txt = $mn.InnerText
		if ($xt -match 'decimal|int|double|float') {
			if ($txt -match '^-?\d+$') { $extra[$key] = [long]$txt } else { $extra[$key] = [double]$txt }
		} else { $extra[$key] = $txt }
	}
	$fmtV = Get-MLValue ($ap.SelectSingleNode('md:Format', $nsm)); if ($null -ne $fmtV) { $extra['format'] = $fmtV }
	$efmtV = Get-MLValue ($ap.SelectSingleNode('md:EditFormat', $nsm)); if ($null -ne $efmtV) { $extra['editFormat'] = $efmtV }

	# FillValue (значение заполнения). Форма по умолчанию зависит от типа реквизита: String→typed-empty,
	# Number→zero, всё остальное→nil. Эмитим `fillValue` только при отклонении от дефолта (§4.2 spec).
	# nil у String/Number → JSON null (nil-override); реальное значение → строка/bool/число/DTR-путь verbatim.
	$fvNode = $ap.SelectSingleNode('md:FillValue', $nsm)
	if ($fvNode) {
		$fcat = 'Other'
		if ($ts -match '\+') { $fcat = 'Other' }
		elseif ($ts -match '^Boolean') { $fcat = 'Boolean' }
		elseif ($ts -match '^String') { $fcat = 'String' }
		elseif ($ts -match '^Number') { $fcat = 'Number' }
		elseif ($ts -match '^(Date|DateTime)') { $fcat = 'Date' }
		$nilAttr = $fvNode.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance')
		$xsiT = $fvNode.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
		$fvText = $fvNode.InnerText
		if ($nilAttr -eq 'true') {
			if ($fcat -eq 'String' -or $fcat -eq 'Number') { $extra['fillValue'] = $null }  # nil-override
			# иначе nil — это дефолт → пропускаем
		} elseif ($xsiT -match 'boolean$') {
			$extra['fillValue'] = ($fvText -eq 'true')
		} elseif ($xsiT -match 'decimal$') {
			# Захватываем как ЧИСЛО (не строку): на составном типе компилятор берёт xsi-тип из JSON-значения —
			# строка "0" дала бы xs:string, число 0 → xs:decimal. У плоского Number эмиссия и так type-aware.
			if (-not ($fcat -eq 'Number' -and ($fvText -eq '0' -or $fvText -eq ''))) {
				$extra['fillValue'] = if ($fvText -match '^-?\d+$') { [long]$fvText } else { [double]$fvText }
			}
		} elseif ($xsiT -match 'string$') {
			if (-not ($fcat -eq 'String' -and $fvText -eq '')) { $extra['fillValue'] = $fvText }
		} elseif ($xsiT -match 'dateTime$') {
			$extra['fillValue'] = $fvText
		} elseif ($xsiT -match 'DesignTimeRef$') {
			# Пустой DTR (ссылочный fillValue без значения) ≠ nil/xs:string → маркер emptyRef (иначе тип терялся в xs:string).
			if ($fvText -eq '') { $extra['fillValue'] = [ordered]@{ emptyRef = $true } } else { $extra['fillValue'] = $fvText }
		}
	}

	# LinkByType (связь по типу): DataPath + LinkItem. Пусто → пропускаем. linkItem=0 → компактно строкой.
	$lbtNode = $ap.SelectSingleNode('md:LinkByType', $nsm)
	if ($lbtNode) {
		$dpN = $lbtNode.SelectSingleNode('xr:DataPath', $nsm)
		if ($dpN -and $dpN.InnerText) {
			$liN = $lbtNode.SelectSingleNode('xr:LinkItem', $nsm)
			$li = if ($liN -and $liN.InnerText) { [int]$liN.InnerText } else { 0 }
			if ($li -eq 0) { $extra['linkByType'] = $dpN.InnerText }
			else { $extra['linkByType'] = [ordered]@{ dataPath = $dpN.InnerText; linkItem = $li } }
		}
	}

	$cplArr = Parse-ChoiceParameterLinks $ap 'md:ChoiceParameterLinks'; if ($null -ne $cplArr) { $extra['choiceParameterLinks'] = $cplArr }
	$cpArr = Parse-ChoiceParameters $ap 'md:ChoiceParameters'; if ($null -ne $cpArr) { $extra['choiceParameters'] = $cpArr }

	# Пустой <Type/> (реквизит без типа / произвольный) → $ts=''. Отличаем от «дефолтного» отсутствия:
	# заставляем объектную форму с явным type:'' (компилятор без маркера подставил бы xs:string).
	$typeEmpty = ($ts -eq '')
	if ($synCustom -or $synEmpty -or ($null -ne $ttVal) -or $extra.Count -gt 0 -or $typeEmpty) {
		$o = [ordered]@{ name = $nm }
		if ($ts) { $o['type'] = $ts } elseif ($typeEmpty) { $o['type'] = '' }
		if ($synCustom) { $o['synonym'] = $synVal }
		elseif ($synEmpty) { $o['synonym'] = '' }
		if ($null -ne $ttVal) { $o['tooltip'] = $ttVal }
		foreach ($k in $extra.Keys) { $o[$k] = $extra[$k] }
		if ($flags.Count -gt 0) { $o['flags'] = [System.Collections.ArrayList]@($flags) }
		return $o
	}
	$head = if ($ts) { "${nm}: $ts" } else { $nm }
	if ($flags.Count -gt 0) { return "$head | " + ($flags -join ', ') }
	return $head
}

# === Сборка DSL ===
$dsl = [ordered]@{ type = $objType; name = $objName }

# Синоним объекта: строка ru-only ИЛИ {ru,en} (мультиязычно). Кастом → эмитим.
# Пустой <Synonym/> (узел есть, значения нет) ≠ авто-синоним из имени → явный synonym:"" (иначе компилятор до-генерит из имени).
$synNodeObj = $props.SelectSingleNode('md:Synonym', $nsm)
$synVal = Get-MLValue $synNodeObj
if ($synVal -is [string]) { if ($synVal -cne (Split-CamelWords $objName)) { $dsl['synonym'] = $synVal } }
elseif ($null -ne $synVal) { $dsl['synonym'] = $synVal }
elseif ($synNodeObj) { $dsl['synonym'] = '' }
$cmt = P 'Comment'; if ($cmt) { $dsl['comment'] = $cmt }

# Свойства Catalog (omit-on-default). Порядок ключей — как удобно DSL.
function Add-BoolProp { param([string]$key, [string]$tag, [bool]$default) $v = P $tag; if ($null -ne $v) { $b = ($v -eq 'true'); if ($b -ne $default) { $dsl[$key] = $b } } }
# -cne: сравнение с дефолтом ВСЕГДА регистрочувствительное. PS -ne регистронезависим, и значение,
# отличающееся от дефолта только регистром, молча терялось (ловилось трижды: синонимы, RootURL).
function Add-EnumProp { param([string]$key, [string]$tag, [string]$default) $v = P $tag; if ($null -ne $v -and $v -ne '' -and $v -cne $default) { $dsl[$key] = $v } }
function Add-IntProp  { param([string]$key, [string]$tag, [int]$default) $v = P $tag; if ($null -ne $v -and $v -ne '') { $iv = [int]$v; if ($iv -ne $default) { $dsl[$key] = $iv } } }

Add-BoolProp 'hierarchical'   'Hierarchical'   $false
Add-EnumProp 'hierarchyType'  'HierarchyType'  'HierarchyFoldersAndItems'
Add-BoolProp 'limitLevelCount' 'LimitLevelCount' $false
Add-IntProp  'levelCount'     'LevelCount'     2
Add-BoolProp 'foldersOnTop'   'FoldersOnTop'   $true
# owners
$ownersNode = $props.SelectSingleNode('md:Owners', $nsm)
if ($ownersNode) {
	$items = @($ownersNode.SelectNodes('xr:Item', $nsm) | ForEach-Object { $_.InnerText })
	if ($items.Count -gt 0) { $dsl['owners'] = [System.Collections.ArrayList]@($items | ForEach-Object { if ($_ -match '^Catalog\.') { ($_ -split '\.', 2)[1] } else { $_ } }) }
}
Add-EnumProp 'subordinationUse' 'SubordinationUse' 'ToItems'
# Тип-зависимые дефолты (компилятор задаёт их по типу — декомпилятор обязан зеркалить, иначе omit ≠ значению).
$descrLenDef  = switch ($objType) { 'ExchangePlan' { 150 } 'ChartOfCharacteristicTypes' { 100 } 'ChartOfCalculationTypes' { 100 } default { 25 } }
$codeLenDef   = if ($objType -eq 'ChartOfCalculationTypes') { 5 } else { 9 }
$createInpDef = if ($objType -in @('Catalog', 'Document')) { 'Use' } else { 'DontUse' }
$dataLockDef  = 'Managed'   # компилятор эмитит Managed по умолчанию для всех типов (авторинг); Automatic несётся в DSL явно
$codeSeriesDef = switch ($objType) { 'ChartOfCharacteristicTypes' { 'WholeCharacteristicKind' } 'ChartOfAccounts' { 'WholeChartOfAccounts' } default { 'WholeCatalog' } }
$checkUniqueDef = ($objType -in @('ChartOfCharacteristicTypes', 'ChartOfAccounts', 'Document', 'DocumentNumerator'))   # ПВХ/ПС/Документ/Нумератор дефолт true, Catalog false
$defPresDef = if ($objType -eq 'ChartOfAccounts') { 'AsCode' } else { 'AsDescription' }   # ПС по умолчанию AsCode
Add-IntProp  'codeLength'        'CodeLength'        $codeLenDef
Add-IntProp  'descriptionLength' 'DescriptionLength' $descrLenDef
Add-EnumProp 'codeType'          'CodeType'          'String'
Add-EnumProp 'codeAllowedLength' 'CodeAllowedLength' 'Variable'
Add-BoolProp 'autonumbering'     'Autonumbering'     $true
Add-BoolProp 'checkUnique'       'CheckUnique'       $checkUniqueDef
Add-EnumProp 'codeSeries'        'CodeSeries'        $codeSeriesDef
Add-EnumProp 'defaultPresentation' 'DefaultPresentation' $defPresDef
if ($objType -ne 'Constant') { Add-BoolProp 'quickChoice' 'QuickChoice' $(if ($objType -eq 'Enum') { $true } else { $false }) }   # Enum дефолт true; прочие false. Constant: QuickChoice — ENUM (Auto), не bool → ловим отдельно ниже
Add-EnumProp 'choiceMode'        'ChoiceMode'        'BothWays'
Add-EnumProp 'dataLockControlMode' 'DataLockControlMode' $dataLockDef
Add-EnumProp 'fullTextSearch'    'FullTextSearch'    'Use'
Add-BoolProp 'useStandardCommands' 'UseStandardCommands' $(if ($objType -in @('Enum', 'CommonForm')) { $false } else { $true })   # Enum/CommonForm дефолт false (корпус); прочие (вкл. Report/DataProcessor) — true
Add-EnumProp 'createOnInput'     'CreateOnInput'     $createInpDef
Add-EnumProp 'editType'          'EditType'          'InDialog'
Add-BoolProp 'includeHelpInContents' 'IncludeHelpInContents' $false
Add-EnumProp 'choiceHistoryOnInput' 'ChoiceHistoryOnInput' 'Auto'
Add-EnumProp 'predefinedDataUpdate' 'PredefinedDataUpdate' 'Auto'
Add-EnumProp 'searchStringModeOnInputByString' 'SearchStringModeOnInputByString' 'Begin'
Add-EnumProp 'fullTextSearchOnInputByString' 'FullTextSearchOnInputByString' 'DontUse'
# ExchangePlan-специфичные свойства (у Catalog этих тегов нет → блок не трогает его).
if ($objType -eq 'ExchangePlan') {
	Add-BoolProp 'distributedInfoBase' 'DistributedInfoBase' $false
	Add-BoolProp 'includeConfigurationExtensions' 'IncludeConfigurationExtensions' $false
	Add-EnumProp 'dataHistory' 'DataHistory' 'DontUse'
	Add-BoolProp 'updateDataHistoryImmediatelyAfterWrite' 'UpdateDataHistoryImmediatelyAfterWrite' $false
	Add-BoolProp 'executeAfterWriteDataHistoryVersionProcessing' 'ExecuteAfterWriteDataHistoryVersionProcessing' $false
}
# ChartOfCharacteristicTypes-специфичные свойства.
if ($objType -eq 'ChartOfCharacteristicTypes') {
	Add-EnumProp 'dataHistory' 'DataHistory' 'DontUse'
	Add-BoolProp 'updateDataHistoryImmediatelyAfterWrite' 'UpdateDataHistoryImmediatelyAfterWrite' $false
	Add-BoolProp 'executeAfterWriteDataHistoryVersionProcessing' 'ExecuteAfterWriteDataHistoryVersionProcessing' $false
	$cev = P 'CharacteristicExtValues'; if ($cev) { $dsl['characteristicExtValues'] = $cev }
	# Type — тип значения характеристики; valueType при отличии от дефолта (Boolean+String(100)+Number(15,2)+DateTime).
	$vtNode = $props.SelectSingleNode('md:Type', $nsm)
	if ($vtNode) {
		$vtStr = Get-TypeShorthand $vtNode
		if ($vtStr -and $vtStr -ne 'Boolean + String(100) + Number(15,2) + DateTime') { $dsl['valueType'] = $vtStr }
	}
}
# ChartOfAccounts-специфичные свойства (у Catalog этих тегов нет → блок его не трогает).
if ($objType -eq 'ChartOfAccounts') {
	$edt = P 'ExtDimensionTypes'; if ($edt) { $dsl['extDimensionTypes'] = $edt }
	# Дефолт зеркалит компилятор: с ПВХ — 3, без — 0 (платформа не даёт > 0 без ПВХ).
	Add-IntProp  'maxExtDimensionCount' 'MaxExtDimensionCount' $(if ($edt) { 3 } else { 0 })
	$cm = P 'CodeMask'; if ($cm) { $dsl['codeMask'] = $cm }
	Add-BoolProp 'autoOrderByCode' 'AutoOrderByCode' $true
	Add-IntProp  'orderLength' 'OrderLength' 9
	Add-EnumProp 'dataHistory' 'DataHistory' 'DontUse'
	Add-BoolProp 'updateDataHistoryImmediatelyAfterWrite' 'UpdateDataHistoryImmediatelyAfterWrite' $false
	Add-BoolProp 'executeAfterWriteDataHistoryVersionProcessing' 'ExecuteAfterWriteDataHistoryVersionProcessing' $false
}
# ChartOfCalculationTypes-специфичные свойства.
if ($objType -eq 'ChartOfCalculationTypes') {
	Add-EnumProp 'dependenceOnCalculationTypes' 'DependenceOnCalculationTypes' 'DontUse'
	Add-BoolProp 'actionPeriodUse' 'ActionPeriodUse' $false
	# BaseCalculationTypes — список ссылок на ПВР (omit-on-empty, verbatim).
	$bctNode = $props.SelectSingleNode('md:BaseCalculationTypes', $nsm)
	if ($bctNode) {
		$bctItems = @($bctNode.SelectNodes('xr:Item', $nsm) | ForEach-Object { $_.InnerText })
		if ($bctItems.Count -gt 0) { $dsl['baseCalculationTypes'] = [System.Collections.ArrayList]@($bctItems) }
	}
	Add-EnumProp 'dataHistory' 'DataHistory' 'DontUse'
	Add-BoolProp 'updateDataHistoryImmediatelyAfterWrite' 'UpdateDataHistoryImmediatelyAfterWrite' $false
	Add-BoolProp 'executeAfterWriteDataHistoryVersionProcessing' 'ExecuteAfterWriteDataHistoryVersionProcessing' $false
}
# Document-специфичные свойства: нумерация, проведение, движения, DataHistory-триплет.
if ($objType -eq 'Document') {
	$numRef = P 'Numerator'; if ($numRef) { $dsl['numerator'] = $numRef }
	Add-EnumProp 'numberType'          'NumberType'          'String'
	Add-IntProp  'numberLength'        'NumberLength'        11
	Add-EnumProp 'numberAllowedLength' 'NumberAllowedLength' 'Variable'
	Add-EnumProp 'numberPeriodicity'   'NumberPeriodicity'   'Year'
	# CheckUnique/Autonumbering у Document уже покрыты общим блоком (дефолты true/true совпадают).
	Add-EnumProp 'posting'                      'Posting'                      'Allow'
	Add-EnumProp 'realTimePosting'              'RealTimePosting'              'Deny'
	Add-EnumProp 'registerRecordsDeletion'      'RegisterRecordsDeletion'      'AutoDelete'
	Add-EnumProp 'registerRecordsWritingOnPost' 'RegisterRecordsWritingOnPost' 'WriteSelected'
	Add-EnumProp 'sequenceFilling'              'SequenceFilling'              'AutoFill'
	Add-BoolProp 'postInPrivilegedMode'         'PostInPrivilegedMode'         $true
	Add-BoolProp 'unpostInPrivilegedMode'       'UnpostInPrivilegedMode'       $true
	# RegisterRecords — движения (список MDObjectRef, omit-on-empty, verbatim).
	$rrNode = $props.SelectSingleNode('md:RegisterRecords', $nsm)
	if ($rrNode) {
		$rrItems = @($rrNode.SelectNodes('xr:Item', $nsm) | ForEach-Object { $_.InnerText })
		if ($rrItems.Count -gt 0) { $dsl['registerRecords'] = [System.Collections.ArrayList]@($rrItems) }
	}
	Add-EnumProp 'dataHistory' 'DataHistory' 'DontUse'
	Add-BoolProp 'updateDataHistoryImmediatelyAfterWrite' 'UpdateDataHistoryImmediatelyAfterWrite' $false
	Add-BoolProp 'executeAfterWriteDataHistoryVersionProcessing' 'ExecuteAfterWriteDataHistoryVersionProcessing' $false
}
# InformationRegister-специфичные свойства: периодичность, режим записи, срезы, DataHistory-триплет.
if ($objType -eq 'InformationRegister') {
	Add-EnumProp 'periodicity'       'InformationRegisterPeriodicity' 'Nonperiodical'
	Add-EnumProp 'writeMode'         'WriteMode'                      'Independent'
	Add-BoolProp 'mainFilterOnPeriod' 'MainFilterOnPeriod'            $false
	Add-BoolProp 'enableTotalsSliceFirst' 'EnableTotalsSliceFirst'    $false
	Add-BoolProp 'enableTotalsSliceLast'  'EnableTotalsSliceLast'     $false
	Add-EnumProp 'dataHistory' 'DataHistory' 'DontUse'
	Add-BoolProp 'updateDataHistoryImmediatelyAfterWrite' 'UpdateDataHistoryImmediatelyAfterWrite' $false
	Add-BoolProp 'executeAfterWriteDataHistoryVersionProcessing' 'ExecuteAfterWriteDataHistoryVersionProcessing' $false
}
# AccumulationRegister-специфичные свойства: тип регистра, разделение итогов.
if ($objType -eq 'AccumulationRegister') {
	Add-EnumProp 'registerType' 'RegisterType' 'Balance'
	Add-BoolProp 'enableTotalsSplitting' 'EnableTotalsSplitting' $true
}
# AccountingRegister-специфичные свойства: ПС-связь, корреспонденция, коррекция периода, разделение итогов.
if ($objType -eq 'AccountingRegister') {
	$coa = P 'ChartOfAccounts'; if ($coa) { $dsl['chartOfAccounts'] = $coa }
	Add-BoolProp 'correspondence' 'Correspondence' $false
	Add-IntProp  'periodAdjustmentLength' 'PeriodAdjustmentLength' 0
	Add-BoolProp 'enableTotalsSplitting' 'EnableTotalsSplitting' $true
}
# CalculationRegister-специфичные свойства: ПВР-связь, периоды расчёта, график (все ссылки/enum — verbatim).
if ($objType -eq 'CalculationRegister') {
	$cct = P 'ChartOfCalculationTypes'; if ($cct) { $dsl['chartOfCalculationTypes'] = $cct }
	Add-EnumProp 'periodicity' 'Periodicity' 'Month'
	Add-BoolProp 'actionPeriod' 'ActionPeriod' $false
	Add-BoolProp '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-специфичные свойства: нумерация (Document-стиль), связь с задачей, привилегированный режим.
if ($objType -eq 'BusinessProcess') {
	Add-EnumProp 'numberType'          'NumberType'          'String'
	Add-IntProp  'numberLength'        'NumberLength'        11
	Add-EnumProp 'numberAllowedLength' 'NumberAllowedLength' 'Variable'
	Add-EnumProp 'numberPeriodicity'   'NumberPeriodicity'   'Nonperiodical'
	# CheckUnique/Autonumbering дефолт true (совпадает с компилятором → захват только при false).
	Add-BoolProp 'checkUnique'   'CheckUnique'   $true
	Add-BoolProp 'autonumbering' 'Autonumbering' $true
	$tsk = P 'Task'; if ($tsk) { $dsl['task'] = $tsk }
	Add-BoolProp 'createTaskInPrivilegedMode' 'CreateTaskInPrivilegedMode' $true
}
# Task-специфичные свойства: нумерация, адресация, текущий исполнитель.
if ($objType -eq 'Task') {
	Add-EnumProp 'numberType'          'NumberType'          'String'
	Add-IntProp  'numberLength'        'NumberLength'        14
	Add-EnumProp 'numberAllowedLength' 'NumberAllowedLength' 'Variable'
	Add-BoolProp 'checkUnique'   'CheckUnique'   $true
	Add-BoolProp 'autonumbering' 'Autonumbering' $true
	$tnap = P 'TaskNumberAutoPrefix'; if ($tnap -and $tnap -ne 'BusinessProcessNumber') { $dsl['taskNumberAutoPrefix'] = $tnap }
	Add-IntProp  '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-EnumProp 'defaultPresentation' 'DefaultPresentation' 'AsDescription'
}
# Report-специфичные свойства: формы (плоские ref, не *ObjectForm), схема компоновки, хранилища вариантов/настроек.
if ($objType -eq '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-MLValue ($props.SelectSingleNode('md:ExtendedPresentation', $nsm)); if ($null -ne $ep) { $dsl['extendedPresentation'] = $ep }
}
# DataProcessor-специфичные свойства: формы (плоские ref).
if ($objType -eq 'DataProcessor') {
	$dfm = P 'DefaultForm'; if ($dfm) { $dsl['defaultForm'] = $dfm }
	$afm = P 'AuxiliaryForm'; if ($afm) { $dsl['auxiliaryForm'] = $afm }
	$ep = Get-MLValue ($props.SelectSingleNode('md:ExtendedPresentation', $nsm)); if ($null -ne $ep) { $dsl['extendedPresentation'] = $ep }
}
# DefinedType — тип-псевдоним: только Name/Synonym/Comment/Type. valueType (составной через ' + ').
if ($objType -eq 'DefinedType') {
	$vt = Get-TypeShorthand ($props.SelectSingleNode('md:Type', $nsm))
	if ($vt) { $dsl['valueType'] = $vt }
}
# FunctionalOption — функциональная опция: Location (хранилище значения) + PrivilegedGetMode + Content (зависимые объекты).
if ($objType -eq 'FunctionalOption') {
	$loc = P 'Location'; if ($loc) { $dsl['location'] = $loc }
	Add-BoolProp 'privilegedGetMode' 'PrivilegedGetMode' $true   # корпус 2864/2864 true → дефолт true, ловим false
	$contentNode = $props.SelectSingleNode('md:Content', $nsm)
	if ($contentNode) {
		$items = @($contentNode.SelectNodes('xr:Object', $nsm) | ForEach-Object { $_.InnerText })
		if ($items.Count -gt 0) { $dsl['content'] = [System.Collections.ArrayList]@($items) }
	}
}
# DocumentJournal — журнал документов: формы (плоские ref) + регистрируемые документы. Колонки → ChildObjects (ниже).
if ($objType -eq 'DocumentJournal') {
	$dfm = P 'DefaultForm'; if ($dfm) { $dsl['defaultForm'] = $dfm }
	$afm = P 'AuxiliaryForm'; if ($afm) { $dsl['auxiliaryForm'] = $afm }
	$rdNode = $props.SelectSingleNode('md:RegisteredDocuments', $nsm)
	if ($rdNode) {
		$rdItems = @($rdNode.SelectNodes('xr:Item', $nsm) | ForEach-Object { $_.InnerText })
		if ($rdItems.Count -gt 0) { $dsl['registeredDocuments'] = [System.Collections.ArrayList]@($rdItems) }
	}
}
# Sequence — последовательность документов: граница, документы, движения, измерения (ChildObjects ниже).
if ($objType -eq 'Sequence') {
	Add-EnumProp 'moveBoundaryOnPosting' 'MoveBoundaryOnPosting' 'DontMove'
	foreach ($ll in @(@('Documents','documents'), @('RegisterRecords','registerRecords'))) {
		$ln = $props.SelectSingleNode("md:$($ll[0])", $nsm)
		if ($ln) {
			$items = @($ln.SelectNodes('xr:Item', $nsm) | ForEach-Object { $_.InnerText })
			if ($items.Count -gt 0) { $dsl[$ll[1]] = [System.Collections.ArrayList]@($items) }
		}
	}
	# dataLockControlMode покрыт общим блоком (дефолт Managed для всех типов).
}
# FilterCriterion — критерий отбора: тип значения + состав (объекты отбора) + формы.
if ($objType -eq 'FilterCriterion') {
	$vt = Get-TypeShorthand ($props.SelectSingleNode('md:Type', $nsm)); if ($vt) { $dsl['valueType'] = $vt }
	$cn = $props.SelectSingleNode('md:Content', $nsm)
	if ($cn) {
		$items = @($cn.SelectNodes('xr:Item', $nsm) | ForEach-Object { $_.InnerText })
		if ($items.Count -gt 0) { $dsl['content'] = [System.Collections.ArrayList]@($items) }
	}
	$dfm = P 'DefaultForm'; if ($dfm) { $dsl['defaultForm'] = $dfm }
	$afm = P 'AuxiliaryForm'; if ($afm) { $dsl['auxiliaryForm'] = $afm }
}
# DocumentNumerator — нумератор документов: параметры нумерации (без InternalInfo/ChildObjects).
if ($objType -eq 'DocumentNumerator') {
	Add-EnumProp 'numberType'          'NumberType'          'String'
	Add-IntProp  'numberLength'        'NumberLength'        11
	Add-EnumProp 'numberAllowedLength' 'NumberAllowedLength' 'Variable'
	Add-EnumProp 'numberPeriodicity'   'NumberPeriodicity'   'Year'
	# checkUnique покрыт общим блоком (дефолт true для DocumentNumerator).
}
# SettingsStorage — хранилище настроек: формы сохранения/загрузки (плоские ref).
if ($objType -eq 'SettingsStorage') {
	foreach ($fp in @(@('DefaultSaveForm','defaultSaveForm'), @('DefaultLoadForm','defaultLoadForm'), @('AuxiliarySaveForm','auxiliarySaveForm'), @('AuxiliaryLoadForm','auxiliaryLoadForm'))) {
		$fv = P $fp[0]; if ($fv) { $dsl[$fp[1]] = $fv }
	}
}
# CommonModule — общий модуль: флаги контекста компиляции + повторное использование значений (тело .bsl вне скоупа).
if ($objType -eq 'CommonModule') {
	Add-BoolProp 'global'                   'Global'                   $false
	Add-BoolProp 'clientManagedApplication' 'ClientManagedApplication' $false
	Add-BoolProp 'server'                   'Server'                   $false
	Add-BoolProp 'externalConnection'       'ExternalConnection'       $false
	Add-BoolProp 'clientOrdinaryApplication' 'ClientOrdinaryApplication' $false
	Add-BoolProp 'serverCall'               'ServerCall'               $false
	Add-BoolProp 'privileged'               'Privileged'               $false
	Add-EnumProp 'returnValuesReuse'        'ReturnValuesReuse'        'DontUse'
}
# EventSubscription — подписка на событие: источники (набор типов), событие, обработчик.
if ($objType -eq 'EventSubscription') {
	$srcNode = $props.SelectSingleNode('md:Source', $nsm)
	if ($srcNode) {
		# Источник — набор v8:Type (конкретный CatalogObject.X) И/ИЛИ v8:TypeSet (голый метатип ExchangePlanObject).
		$srcTypes = @($srcNode.SelectNodes('v8:Type|v8:TypeSet', $nsm) | ForEach-Object { Strip-NsPrefix $_.InnerText.Trim() })
		if ($srcTypes.Count -gt 0) { $dsl['source'] = [System.Collections.ArrayList]@($srcTypes) }
	}
	Add-EnumProp 'event' 'Event' 'BeforeWrite'
	$h = P 'Handler'; if ($h) { $dsl['handler'] = $h }
}
# CommonForm — общая форма (метаданные; содержимое формы Ext/Form.xml вне роундтрипа, территория form-compile).
if ($objType -eq 'CommonForm') {
	Add-EnumProp 'formType' 'FormType' 'Managed'
	# UsePurposes — дефолт [PlatformApplication, MobilePlatformApplication]; захват при отличии.
	$upNode = $props.SelectSingleNode('md:UsePurposes', $nsm)
	if ($upNode) {
		$ups = @($upNode.SelectNodes('v8:Value', $nsm) | ForEach-Object { $_.InnerText })
		$def2 = @('PlatformApplication', 'MobilePlatformApplication')
		$same = ($ups.Count -eq $def2.Count); if ($same) { for ($k=0; $k -lt $ups.Count; $k++) { if ($ups[$k] -cne $def2[$k]) { $same=$false; break } } }
		if (-not $same -and $ups.Count -gt 0) { $dsl['usePurposes'] = [System.Collections.ArrayList]@($ups) }
	}
	$ep = Get-MLValue ($props.SelectSingleNode('md:ExtendedPresentation', $nsm)); if ($null -ne $ep) { $dsl['extendedPresentation'] = $ep }
}
# SessionParameter — параметр сеанса: только тип значения.
if ($objType -eq 'SessionParameter') {
	$vt = Get-TypeShorthand ($props.SelectSingleNode('md:Type', $nsm)); if ($vt) { $dsl['valueType'] = $vt }
}
# FunctionalOptionsParameter — параметр функ. опции: Use (список MDObjectRef).
if ($objType -eq 'FunctionalOptionsParameter') {
	$un = $props.SelectSingleNode('md:Use', $nsm)
	if ($un) { $items = @($un.SelectNodes('xr:Item', $nsm) | ForEach-Object { $_.InnerText }); if ($items.Count -gt 0) { $dsl['use'] = [System.Collections.ArrayList]@($items) } }
}
# WSReference — WS-ссылка: URL расположения WSDL (+InternalInfo Manager).
if ($objType -eq 'WSReference') {
	$url = P 'LocationURL'; if ($url) { $dsl['locationURL'] = $url }
}
# CommonPicture — общая картинка: доступность (содержимое Ext/Picture вне скоупа).
if ($objType -eq 'CommonPicture') {
	Add-BoolProp 'availabilityForChoice'     'AvailabilityForChoice'     $false
	Add-BoolProp 'availabilityForAppearance' 'AvailabilityForAppearance' $false
}
# CommonTemplate — общий макет: тип макета (содержимое Ext/Template.* вне скоупа).
if ($objType -eq 'CommonTemplate') {
	Add-EnumProp 'templateType' 'TemplateType' 'SpreadsheetDocument'
}
# Общий захват структурного <Picture> (зеркало Emit-CommandPicture). Пишет в $tgt ключи picture/loadTransparent.
function Get-PictureToDsl { param($propsNode, $tgt)
	$refN = $propsNode.SelectSingleNode('md:Picture/xr:Ref', $nsm)
	$absN = $propsNode.SelectSingleNode('md:Picture/xr:Abs', $nsm)
	if ($refN -or $absN) {
		$psrc = if ($refN) { $refN.InnerText } else { "abs:$($absN.InnerText)" }
		$ltN = $propsNode.SelectSingleNode('md:Picture/xr:LoadTransparent', $nsm)
		$ltFalse = ($ltN -and $ltN.InnerText -eq 'false')
		$tpxN = $propsNode.SelectSingleNode('md:Picture/xr:TransparentPixel', $nsm)
		if ($tpxN) {
			$po = [ordered]@{ src = $psrc }; if ($ltFalse) { $po['loadTransparent'] = $false }
			$po['transparentPixel'] = [ordered]@{ x = [int]$tpxN.GetAttribute('x'); y = [int]$tpxN.GetAttribute('y') }
			$tgt['picture'] = $po
		} else { $tgt['picture'] = $psrc; if ($ltFalse) { $tgt['loadTransparent'] = $false } }
	}
}
# CommandGroup — группа команд: представление, подсказка, картинка, категория.
if ($objType -eq 'CommandGroup') {
	Add-EnumProp 'representation' 'Representation' 'Auto'
	$tt = Get-MLValue ($props.SelectSingleNode('md:ToolTip', $nsm)); if ($null -ne $tt) { $dsl['tooltip'] = $tt }
	Get-PictureToDsl $props $dsl
	Add-EnumProp 'category' 'Category' 'NavigationPanel'
}
# CommonCommand — общая команда: группа, представление, подсказка, картинка, параметр, режимы.
if ($objType -eq 'CommonCommand') {
	$grp = P 'Group'; if ($grp) { $dsl['group'] = $grp }
	Add-EnumProp 'representation' 'Representation' 'Auto'
	$tt = Get-MLValue ($props.SelectSingleNode('md:ToolTip', $nsm)); if ($null -ne $tt) { $dsl['tooltip'] = $tt }
	Get-PictureToDsl $props $dsl
	$sc = P 'Shortcut'; if ($sc) { $dsl['shortcut'] = $sc }
	$cpt = Get-TypeShorthand ($props.SelectSingleNode('md:CommandParameterType', $nsm)); if ($cpt) { $dsl['commandParameterType'] = $cpt }
	Add-EnumProp 'parameterUseMode' 'ParameterUseMode' 'Single'
	Add-BoolProp 'modifiesData' 'ModifiesData' $false
	Add-EnumProp 'onMainServerUnavalableBehavior' 'OnMainServerUnavalableBehavior' 'Auto'
}
# XDTO-тип из элемента: если значение с префиксом (d6p1:Local) — разворачиваем префикс в URI и
# отдаём в нотации Кларка "{uri}Local"; префиксы платформы (dNpM) произвольны и переносу не подлежат.
function Get-XDTOTypeValue {
	param($node)
	if (-not $node) { return $null }
	$txt = $node.InnerText
	if ($txt -match '^([\w.-]+):(.+)$') {
		$prefix = $Matches[1]; $local = $Matches[2]
		$uri = $node.GetNamespaceOfPrefix($prefix)
		# xs: и прочие стандартные оставляем как есть — компилятор их пишет дословно.
		if ($uri -and $prefix -notin @('xs','xsi','v8','xr')) { return "{$uri}$local" }
	}
	return $txt
}
# WebService — пространство имён, состав XDTO-пакетов, дескриптор, операции с параметрами.
if ($objType -eq 'WebService') {
	$ns = P 'Namespace'; if ($ns) { $dsl['namespace'] = $ns }
	$pkgNodes = @($props.SelectNodes('md:XDTOPackages/xr:Item/xr:Value', $nsm))
	if ($pkgNodes.Count -gt 0) {
		$pkgs = [System.Collections.ArrayList]@()
		foreach ($pn in $pkgNodes) { [void]$pkgs.Add($pn.InnerText) }
		$dsl['xdtoPackages'] = $pkgs
	}
	$dfn = P 'DescriptorFileName'
	if ($dfn -and $dfn -cne "$objName.1cws") { $dsl['descriptorFileName'] = $dfn }
	Add-EnumProp 'reuseSessions' 'ReuseSessions' 'DontUse'
	Add-IntProp  'sessionMaxAge' 'SessionMaxAge' 20
}
# HTTPService — корневой URL, повторное использование сеансов, время жизни сеанса.
# Шаблоны URL с методами разбираются в блоке ChildObjects.
if ($objType -eq 'HTTPService') {
	# -cne: дефолт — имя в нижнем регистре, но реальный RootURL часто отличается ТОЛЬКО регистром
	# (MobileAppReceiptScanner), и регистронезависимое сравнение считало его дефолтным.
	$ru = P 'RootURL'; if ($ru -and $ru -cne $objName.ToLower()) { $dsl['rootURL'] = $ru }
	Add-EnumProp 'reuseSessions' 'ReuseSessions' 'DontUse'
	Add-IntProp  'sessionMaxAge' 'SessionMaxAge' 20
}
# CommonAttribute — общий реквизит: тип + value-свойства + состав объектов + свойства разделения данных.
if ($objType -eq 'CommonAttribute') {
	$vt = Get-TypeShorthand ($props.SelectSingleNode('md:Type', $nsm)); if ($vt -and $vt -ne 'String(0)') { $dsl['valueType'] = $vt }
	Add-BoolProp 'passwordMode' 'PasswordMode' $false
	foreach ($mlp in @(@('Format','format'), @('EditFormat','editFormat'), @('ToolTip','tooltip'))) {
		$mv = Get-MLValue ($props.SelectSingleNode("md:$($mlp[0])", $nsm)); if ($null -ne $mv) { $dsl[$mlp[1]] = $mv }
	}
	Add-BoolProp 'markNegatives' 'MarkNegatives' $false
	$msk = P 'Mask'; if ($msk) { $dsl['mask'] = $msk }
	Add-BoolProp 'multiLine' 'MultiLine' $false
	Add-BoolProp 'extendedEdit' 'ExtendedEdit' $false
	foreach ($mm in @(@('MinValue','minValue'), @('MaxValue','maxValue'))) {
		$mn = $props.SelectSingleNode("md:$($mm[0])", $nsm)
		if ($mn -and $mn.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance') -ne 'true') {
			$mxt = $mn.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
			if ($mxt -match 'decimal$') { $dsl[$mm[1]] = if ($mn.InnerText -match '^-?\d+$') { [long]$mn.InnerText } else { [double]$mn.InnerText } } else { $dsl[$mm[1]] = $mn.InnerText }
		}
	}
	Add-BoolProp 'fillFromFillingValue' 'FillFromFillingValue' $false
	# FillValue: тип-зависимый дефолт (String→typed-empty, Number→0, прочее→nil). Захват при отклонении.
	# nil на String/Number-типе ≠ дефолт → маркер {nil:true} (напр. системный реквизит-разделитель ОбластьДанных).
	$catVt = if ($vt) { $vt } else { 'String(0)' }
	$fvN = $props.SelectSingleNode('md:FillValue', $nsm)
	if ($fvN) {
		$fvNil = ($fvN.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance') -eq 'true')
		if ($fvNil) {
			if ($catVt -match '^(String|Number)') { $dsl['fillValue'] = [ordered]@{ nil = $true } }
		} else {
			$fvXt = $fvN.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
			if ($fvXt -match 'DesignTimeRef$' -and $fvN.InnerText -eq '') { $dsl['fillValue'] = [ordered]@{ emptyRef = $true } }
			elseif ($fvXt -match 'decimal$') { if ($fvN.InnerText -ne '0') { $dsl['fillValue'] = if ($fvN.InnerText -match '^-?\d+$') { [long]$fvN.InnerText } else { [double]$fvN.InnerText } } }
			elseif (-not [string]::IsNullOrEmpty($fvN.InnerText)) { $dsl['fillValue'] = $fvN.InnerText }
		}
	}
	Add-EnumProp 'fillChecking' 'FillChecking' 'DontCheck'
	Add-EnumProp 'choiceFoldersAndItems' 'ChoiceFoldersAndItems' 'Items'
	$cpl = Parse-ChoiceParameterLinks $props 'md:ChoiceParameterLinks'; if ($null -ne $cpl) { $dsl['choiceParameterLinks'] = $cpl }
	$cp = Parse-ChoiceParameters $props 'md:ChoiceParameters'; if ($null -ne $cp) { $dsl['choiceParameters'] = $cp }
	Add-EnumProp 'quickChoice' 'QuickChoice' 'Auto'
	Add-EnumProp 'createOnInput' 'CreateOnInput' 'Auto'
	$cf = P 'ChoiceForm'; if ($cf) { $dsl['choiceForm'] = $cf }
	Add-EnumProp 'choiceHistoryOnInput' 'ChoiceHistoryOnInput' 'Auto'
	# Content — объекты, к которым добавлен общий реквизит.
	$cn = $props.SelectSingleNode('md:Content', $nsm)
	if ($cn) {
		$cArr = [System.Collections.ArrayList]@()
		foreach ($it in @($cn.SelectNodes('xr:Item', $nsm))) {
			$mdN = $it.SelectSingleNode('xr:Metadata', $nsm); $mdv = if ($mdN) { $mdN.InnerText } else { '' }
			$useN = $it.SelectSingleNode('xr:Use', $nsm); $usev = if ($useN) { $useN.InnerText } else { 'Use' }
			$csN = $it.SelectSingleNode('xr:ConditionalSeparation', $nsm); $csv = if ($csN) { $csN.InnerText } else { '' }
			if ($usev -eq 'Use' -and -not $csv) { [void]$cArr.Add($mdv) }
			else { $io = [ordered]@{ metadata = $mdv }; if ($usev -ne 'Use') { $io['use'] = $usev }; if ($csv) { $io['conditionalSeparation'] = $csv }; [void]$cArr.Add($io) }
		}
		if ($cArr.Count -gt 0) { $dsl['content'] = $cArr }
	}
	Add-EnumProp 'autoUse' 'AutoUse' 'DontUse'
	Add-EnumProp 'dataSeparation' 'DataSeparation' 'DontUse'
	Add-EnumProp '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-EnumProp 'usersSeparation' 'UsersSeparation' 'DontUse'
	Add-EnumProp 'authenticationSeparation' 'AuthenticationSeparation' 'DontUse'
	Add-EnumProp 'configurationExtensionsSeparation' 'ConfigurationExtensionsSeparation' 'DontUse'
	Add-EnumProp 'indexing' 'Indexing' 'DontIndex'
	Add-EnumProp 'dataHistory' 'DataHistory' 'Use'
	# fullTextSearch покрыт общим блоком (дефолт Use).
}
# ScheduledJob — регламентное задание: метод, ключ, флаги, рестарт.
if ($objType -eq '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-BoolProp 'use'        'Use'        $false
	Add-BoolProp 'predefined' 'Predefined' $false
	Add-IntProp  'restartCountOnFailure'    'RestartCountOnFailure'    3
	Add-IntProp  'restartIntervalOnFailure' 'RestartIntervalOnFailure' 10
}
# Constant — богатый одиночный реквизит: Type + свойства значения (как у реквизита) + object-уровень.
if ($objType -eq 'Constant') {
	$vt = Get-TypeShorthand ($props.SelectSingleNode('md:Type', $nsm))
	if ($vt) { $dsl['valueType'] = $vt } else { $dsl['valueType'] = '' }
	$dfm = P 'DefaultForm'; if ($dfm) { $dsl['defaultForm'] = $dfm }
	$ep = Get-MLValue ($props.SelectSingleNode('md:ExtendedPresentation', $nsm)); if ($null -ne $ep) { $dsl['extendedPresentation'] = $ep }
	Add-BoolProp 'passwordMode' 'PasswordMode' $false
	$fmt = Get-MLValue ($props.SelectSingleNode('md:Format', $nsm)); if ($null -ne $fmt) { $dsl['format'] = $fmt }
	$efmt = Get-MLValue ($props.SelectSingleNode('md:EditFormat', $nsm)); if ($null -ne $efmt) { $dsl['editFormat'] = $efmt }
	$tt = Get-MLValue ($props.SelectSingleNode('md:ToolTip', $nsm)); if ($null -ne $tt) { $dsl['tooltip'] = $tt }
	Add-BoolProp 'markNegatives' 'MarkNegatives' $false
	$msk = P 'Mask'; if ($msk) { $dsl['mask'] = $msk }
	Add-BoolProp 'multiLine' 'MultiLine' $false
	Add-BoolProp 'extendedEdit' 'ExtendedEdit' $false
	# MinValue/MaxValue (дефолт nil) — типизированное значение при наличии.
	foreach ($mm in @(@('MinValue','minValue'), @('MaxValue','maxValue'))) {
		$mn = $props.SelectSingleNode("md:$($mm[0])", $nsm)
		if ($mn -and $mn.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance') -ne 'true') {
			$mxt = $mn.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
			if ($mxt -match 'decimal$') { $dsl[$mm[1]] = if ($mn.InnerText -match '^-?\d+$') { [long]$mn.InnerText } else { [double]$mn.InnerText } }
			else { $dsl[$mm[1]] = $mn.InnerText }
		}
	}
	Add-EnumProp 'fillChecking' 'FillChecking' 'DontCheck'
	Add-EnumProp 'choiceFoldersAndItems' 'ChoiceFoldersAndItems' 'Items'
	$cpl = Parse-ChoiceParameterLinks $props 'md:ChoiceParameterLinks'; if ($null -ne $cpl) { $dsl['choiceParameterLinks'] = $cpl }
	$cp = Parse-ChoiceParameters $props 'md:ChoiceParameters'; if ($null -ne $cp) { $dsl['choiceParameters'] = $cp }
	Add-EnumProp 'quickChoice' 'QuickChoice' 'Auto'
	$cf = P 'ChoiceForm'; if ($cf) { $dsl['choiceForm'] = $cf }
	$lbtNode = $props.SelectSingleNode('md:LinkByType', $nsm)
	if ($lbtNode) {
		$dpN = $lbtNode.SelectSingleNode('md:DataPath', $nsm)
		if ($dpN -and $dpN.InnerText) {
			$liN = $lbtNode.SelectSingleNode('md:LinkItem', $nsm)
			$li = if ($liN -and $liN.InnerText) { [int]$liN.InnerText } else { 0 }
			$dsl['linkByType'] = if ($li -eq 0) { $dpN.InnerText } else { [ordered]@{ dataPath = $dpN.InnerText; linkItem = $li } }
		}
	}
	Add-EnumProp 'dataHistory' 'DataHistory' 'DontUse'
	Add-BoolProp 'updateDataHistoryImmediatelyAfterWrite' 'UpdateDataHistoryImmediatelyAfterWrite' $false
	Add-BoolProp 'executeAfterWriteDataHistoryVersionProcessing' 'ExecuteAfterWriteDataHistoryVersionProcessing' $false
}

# Короткая форма поля: <Type>.<Name>.StandardAttribute.X / .Attribute.X → StandardAttribute.X / Attribute.X
# (Expand-DataPath компилятора разворачивает частичную форму обратно — dogfood резолвера).
function Short-Field { param([string]$full) if ($full -match '\.(StandardAttribute|Attribute)\.(.+)$') { return "$($Matches[1]).$($Matches[2])" } return $full }

# InputByString — эмитим только при отличии от выведенного дефолта [Descr при D>0]+[Code при C>0].
$ibNode = $props.SelectSingleNode('md:InputByString', $nsm)
if ($ibNode) {
	$ibActual = @($ibNode.SelectNodes('xr:Field', $nsm) | ForEach-Object { $_.InnerText })
	$clv = P 'CodeLength'; $dlv = P 'DescriptionLength'
	$cl = if ($clv -and $clv -ne '') { [int]$clv } else { 9 }
	$dl = if ($dlv -and $dlv -ne '') { [int]$dlv } else { 25 }
	$ibDef = @()
	if ($dl -gt 0) { $ibDef += "StandardAttribute.Description" }
	if ($cl -gt 0) { $ibDef += "StandardAttribute.Code" }
	$ibShort = @($ibActual | ForEach-Object { Short-Field $_ })
	$same = ($ibShort.Count -eq $ibDef.Count)
	if ($same) { for ($k = 0; $k -lt $ibShort.Count; $k++) { if ($ibShort[$k] -cne $ibDef[$k]) { $same = $false; break } } }
	if (-not $same) { $dsl['inputByString'] = [System.Collections.ArrayList]@($ibShort) }
}

# BasedOn — «ввод на основании», список MDObjectRef (omit-on-empty).
$boNode = $props.SelectSingleNode('md:BasedOn', $nsm)
if ($boNode) {
	$boItems = @($boNode.SelectNodes('xr:Item', $nsm) | ForEach-Object { $_.InnerText })
	if ($boItems.Count -gt 0) { $dsl['basedOn'] = [System.Collections.ArrayList]@($boItems) }
}

# DataLockFields — поля блокировки данных (omit-on-empty).
$dlfNode = $props.SelectSingleNode('md:DataLockFields', $nsm)
if ($dlfNode) {
	$dlfFields = @($dlfNode.SelectNodes('xr:Field', $nsm) | ForEach-Object { Short-Field $_.InnerText })
	if ($dlfFields.Count -gt 0) { $dsl['dataLockFields'] = [System.Collections.ArrayList]@($dlfFields) }
}

# Формы по умолчанию (компилятор пишет пусто → omit-on-empty; значение = ссылка на форму verbatim).
function Add-FormRef { param([string]$key, [string]$tag) $v = P $tag; if ($v) { $dsl[$key] = $v } }
Add-FormRef 'defaultObjectForm'         'DefaultObjectForm'
Add-FormRef 'defaultFolderForm'         'DefaultFolderForm'
Add-FormRef 'defaultListForm'           'DefaultListForm'
Add-FormRef 'defaultChoiceForm'         'DefaultChoiceForm'
Add-FormRef 'defaultFolderChoiceForm'   'DefaultFolderChoiceForm'
Add-FormRef 'auxiliaryObjectForm'       'AuxiliaryObjectForm'
Add-FormRef 'auxiliaryFolderForm'       'AuxiliaryFolderForm'
Add-FormRef 'auxiliaryListForm'         'AuxiliaryListForm'
Add-FormRef 'auxiliaryChoiceForm'       'AuxiliaryChoiceForm'
Add-FormRef 'auxiliaryFolderChoiceForm' 'AuxiliaryFolderChoiceForm'
Add-FormRef 'defaultRecordForm'         'DefaultRecordForm'
Add-FormRef 'auxiliaryRecordForm'       'AuxiliaryRecordForm'

# Презентации (ML, компилятор пишет пусто → omit-on-empty).
foreach ($pp in @(
	@('ObjectPresentation','objectPresentation'), @('ExtendedObjectPresentation','extendedObjectPresentation'),
	@('RecordPresentation','recordPresentation'), @('ExtendedRecordPresentation','extendedRecordPresentation'),
	@('ListPresentation','listPresentation'), @('ExtendedListPresentation','extendedListPresentation'),
	@('Explanation','explanation'))) {
	$pv = Get-MLValue ($props.SelectSingleNode("md:$($pp[0])", $nsm))
	if ($null -ne $pv) { $dsl[$pp[1]] = $pv }
}

# --- Characteristics (привязка ПВХ). Короткая форма: поля bare/partial, filterValue без каталога, from полный.
function Shorten-CharField { param([string]$full, [string]$from)
	if ($full.StartsWith("$from.")) {
		$rest = $full.Substring($from.Length + 1)
		if ($rest -match '^StandardAttribute\.(Ref|Parent|Owner)$') { return $Matches[1] }  # ссылочные станд. → голое
		if ($rest -match '^Attribute\.(.+)$') { return $Matches[1] }                        # кастом → голое
		return $rest   # прочие StandardAttribute.X / Dimension.X / Resource.X → частичное (безопасно)
	}
	return $full
}
$charsNode = $props.SelectSingleNode('md:Characteristics', $nsm)
if ($charsNode) {
	$chList = @($charsNode.SelectNodes('xr:Characteristic', $nsm))
	if ($chList.Count -gt 0) {
		$chArr = [System.Collections.ArrayList]@()
		foreach ($ch in $chList) {
			$ct = $ch.SelectSingleNode('xr:CharacteristicTypes', $nsm)
			$cv = $ch.SelectSingleNode('xr:CharacteristicValues', $nsm)
			$tFrom = $ct.GetAttribute('from'); $vFrom = $cv.GetAttribute('from')
			$gt = { param($n, $node) $x = $node.SelectSingleNode("xr:$n", $nsm); if ($x) { $x.InnerText } else { "" } }
			$giv = { param($n, $node) $x = $node.SelectSingleNode("xr:$n", $nsm); if ($x -and $x.InnerText -ne '') { [int]$x.InnerText } else { -1 } }
			$tfvNode = $ct.SelectSingleNode('xr:TypesFilterValue', $nsm)
			$tfvNil = if ($tfvNode) { $tfvNode.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance') } else { '' }
			$types = [ordered]@{
				from = $tFrom
				key = Shorten-CharField (& $gt 'KeyField' $ct) $tFrom
				filterField = Shorten-CharField (& $gt 'TypesFilterField' $ct) $tFrom
				filterValue = if ($tfvNil -eq 'true') { $null } else { Convert-ChScalarNode $tfvNode }
			}
			# DataPathField полиморфно: обычно -1, но встречается ПУТЬ к полю (8 случаев на корпус).
			# Жёсткое [int] на нём роняло декомпиляцию всего объекта.
			$dpfN = $ct.SelectSingleNode('xr:DataPathField', $nsm)
			$dpfT = if ($dpfN) { $dpfN.InnerText } else { '' }
			if ($dpfT -ne '' -and $dpfT -cne '-1') {
				$types['dataPathField'] = if ($dpfT -match '^-?\d+$') { [int]$dpfT } else { Shorten-CharField $dpfT $tFrom }
			}
			$mvu = & $giv 'MultipleValuesUseField' $ct; if ($mvu -ne -1) { $types['multipleValuesUseField'] = $mvu }
			$values = [ordered]@{
				from = $vFrom
				object = Shorten-CharField (& $gt 'ObjectField' $cv) $vFrom
				type = Shorten-CharField (& $gt 'TypeField' $cv) $vFrom
				value = Shorten-CharField (& $gt 'ValueField' $cv) $vFrom
			}
			$mvk = & $giv 'MultipleValuesKeyField' $cv; if ($mvk -ne -1) { $values['multipleValuesKeyField'] = $mvk }
			$mvo = & $giv 'MultipleValuesOrderField' $cv; if ($mvo -ne -1) { $values['multipleValuesOrderField'] = $mvo }
			[void]$chArr.Add([ordered]@{ types = $types; values = $values })
		}
		$dsl['characteristics'] = $chArr
	}
}

# --- StandardAttributes: захватываем ОТКЛОНЕНИЯ от профиля материализованного блока (профиль компилятор
# восстановит сам). Профиль зеркалит stdAttrProfile компилятора — по типу объекта.
# Catalog: Owner{FC=ShowError,FFV=true}, Parent{FFV=true}, Description{FC=ShowError}.
# ExchangePlan: Description{FC=ShowError}, Code{FC=ShowError} (блок всегда материализован). ---
$stdProfileByType = @{
	'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' }
	}
}
$catStdProfile = if ($stdProfileByType.ContainsKey($objType)) { $stdProfileByType[$objType] } else { @{} }
# Фикс-список стандартных реквизитов типа (зеркало standardAttributesByType компилятора) — чтобы отличать
# доп./опциональные (напр. ExchangeDate у ПланОбмена), которые эмитим по факту присутствия даже all-default.
$stdFixedByType = @{
	'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')
}
$stdFixed = if ($stdFixedByType.ContainsKey($objType)) { $stdFixedByType[$objType] } else { @() }
# Условные типы: блок эмитим-как-триггер даже пустым (материализуется при отклонении ≥1 реквизита от schema-default;
# у ExchangePlan это почти всегда — Description/Code=ShowError; редкий all-default EP блок опускает).
$stdConditionalTypes = @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document')
$saNode = $props.SelectSingleNode('md:StandardAttributes', $nsm)
if ($saNode) {
	$saMap = [ordered]@{}
	foreach ($sa in @($saNode.SelectNodes('xr:StandardAttribute', $nsm))) {
		$an = $sa.GetAttribute('name')
		$prof = if ($catStdProfile.ContainsKey($an)) { $catStdProfile[$an] } else { @{} }
		$ov = [ordered]@{}
		# FillChecking (профиль или DontCheck)
		$fcN = $sa.SelectSingleNode('xr:FillChecking', $nsm); $fc = if ($fcN) { $fcN.InnerText } else { 'DontCheck' }
		$profFc = if ($prof.ContainsKey('fillChecking')) { $prof['fillChecking'] } else { 'DontCheck' }
		if ($fc -ne $profFc) { $ov['fillChecking'] = $fc }
		# FillFromFillingValue (профиль или false)
		$ffvN = $sa.SelectSingleNode('xr:FillFromFillingValue', $nsm); $ffv = ($ffvN -and $ffvN.InnerText -eq 'true')
		$profFfv = ($prof['fillFromFillingValue'] -eq $true)
		if ($ffv -ne $profFfv) { $ov['fillFromFillingValue'] = $ffv }
		# Synonym / ToolTip (профиль пуст) — строка ru | {ru,en}
		$syn = Get-MLValue ($sa.SelectSingleNode('xr:Synonym', $nsm))
		if ($null -ne $syn) { $ov['synonym'] = $syn }
		$tt = Get-MLValue ($sa.SelectSingleNode('xr:ToolTip', $nsm))
		if ($null -ne $tt) { $ov['tooltip'] = $tt }
		# FullTextSearch / DataHistory (профиль = Use)
		$ftsN = $sa.SelectSingleNode('xr:FullTextSearch', $nsm); if ($ftsN -and $ftsN.InnerText -ne 'Use') { $ov['fullTextSearch'] = $ftsN.InnerText }
		$dhN = $sa.SelectSingleNode('xr:DataHistory', $nsm); if ($dhN -and $dhN.InnerText -ne 'Use') { $ov['dataHistory'] = $dhN.InnerText }
		# FillValue (дефолт nil) — DTR-путь/строка/bool. Comment/Mask/ChoiceForm (дефолт пусто).
		$fvN = $sa.SelectSingleNode('xr:FillValue', $nsm)
		if ($fvN -and $fvN.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance') -ne 'true') {
			$fvXt = $fvN.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
			if ($fvXt -match 'DesignTimeRef$' -and $fvN.InnerText -eq '') { $ov['fillValue'] = [ordered]@{ emptyRef = $true } }
			elseif ($fvXt -match 'TypeDescription$' -and $fvN.InnerText -eq '') { $ov['fillValue'] = [ordered]@{ typeDescription = $true } }   # пустое типизированное (реквизит ValueType ПВХ) ≠ xs:string
			else { $ov['fillValue'] = Convert-ChScalarNode $fvN }
		}
		$saCmt = $sa.SelectSingleNode('xr:Comment', $nsm); if ($saCmt -and $saCmt.InnerText) { $ov['comment'] = $saCmt.InnerText }
		$saMsk = $sa.SelectSingleNode('xr:Mask', $nsm); if ($saMsk -and $saMsk.InnerText) { $ov['mask'] = $saMsk.InnerText }
			$saFmt = Get-MLValue ($sa.SelectSingleNode('xr:Format', $nsm)); if ($null -ne $saFmt) { $ov['format'] = $saFmt }
			$saEfmt = Get-MLValue ($sa.SelectSingleNode('xr:EditFormat', $nsm)); if ($null -ne $saEfmt) { $ov['editFormat'] = $saEfmt }
		$saCf = $sa.SelectSingleNode('xr:ChoiceForm', $nsm); if ($saCf -and $saCf.InnerText) { $ov['choiceForm'] = $saCf.InnerText }
		$saCpl = Parse-ChoiceParameterLinks $sa 'xr:ChoiceParameterLinks'; if ($null -ne $saCpl) { $ov['choiceParameterLinks'] = $saCpl }
		$saCp = Parse-ChoiceParameters $sa 'xr:ChoiceParameters'; if ($null -ne $saCp) { $ov['choiceParameters'] = $saCp }
		# LinkByType стандартного реквизита (ExtDimensionN→Account у регистра бухгалтерии). DataPath полный verbatim.
		$saLbt = $sa.SelectSingleNode('xr:LinkByType', $nsm)
		if ($saLbt) {
			$saLbtDp = $saLbt.SelectSingleNode('xr:DataPath', $nsm)
			if ($saLbtDp -and $saLbtDp.InnerText) {
				$saLbtLi = $saLbt.SelectSingleNode('xr:LinkItem', $nsm)
				$li = if ($saLbtLi -and $saLbtLi.InnerText) { [int]$saLbtLi.InnerText } else { 0 }
				$ov['linkByType'] = [ordered]@{ dataPath = $saLbtDp.InnerText; linkItem = $li }
			}
		}
		# Формат 2.20: режим приведения типов. Компилятор выводит его сам (TransformValues, у Owner —
		# Deny), поэтому захватываем только отклонение от этого правила.
		$saTrmN = $sa.SelectSingleNode('xr:TypeReductionMode', $nsm)
		if ($saTrmN -and $saTrmN.InnerText) {
			$saTrmDef = if ($an -ceq 'Owner') { 'Deny' } else { 'TransformValues' }
			if ($saTrmN.InnerText -ne $saTrmDef) { $ov['TypeReductionMode'] = $saTrmN.InnerText }
		}
		# Доп./опциональный реквизит (не в фикс-списке) — эмитим по присутствию даже без отклонений.
		if ($ov.Count -gt 0 -or ($stdFixed -notcontains $an)) { $saMap[$an] = $ov }
	}
	# Условный тип (Catalog): пустой $saMap = триггер блока. Не-условный (ExchangePlan): блок и так эмитится → пустой не пишем.
	if ($saMap.Count -gt 0 -or ($stdConditionalTypes -contains $objType)) { $dsl['standardAttributes'] = $saMap }
} elseif ($objType -in @('InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task', 'Enum', 'DocumentJournal')) {
	# Регистр/БП/Задача опускают all-default блок стандартных реквизитов (правило не выводимо) — компилятор эмитит его
	# по дефолту, поэтому отсутствие фиксируем opt-out `standardAttributes:""` (дом-конвенция суппресса).
	$dsl['standardAttributes'] = ''
}

# --- ChildObjects: Attributes + TabularSections ---
$childObjs = $objNode.SelectSingleNode('md:ChildObjects', $nsm)
if ($childObjs) {
	# WebService: операции с параметрами. Строчное сокращение — только тип возврата (когда всё
	# остальное дефолтно); иначе объект с nillable/transactioned/procedureName/параметрами.
	$opNodes = @($childObjs.SelectNodes('md:Operation', $nsm))
	if ($opNodes.Count -gt 0) {
		$ops = [ordered]@{}
		foreach ($op in $opNodes) {
			$op_p = $op.SelectSingleNode('md:Properties', $nsm)
			$opName = ($op_p.SelectSingleNode('md:Name', $nsm)).InnerText
			$o = [ordered]@{}
			$rt = Get-XDTOTypeValue ($op_p.SelectSingleNode('md:XDTOReturningValueType', $nsm))
			if ($rt -and $rt -cne 'xs:string') { $o['returnType'] = $rt }
			$nil = $op_p.SelectSingleNode('md:Nillable', $nsm)
			if ($nil -and $nil.InnerText -eq 'true') { $o['nillable'] = $true }
			$tr = $op_p.SelectSingleNode('md:Transactioned', $nsm)
			if ($tr -and $tr.InnerText -eq 'true') { $o['transactioned'] = $true }
			$pn = $op_p.SelectSingleNode('md:ProcedureName', $nsm)
			if ($pn -and $pn.InnerText -cne $opName) { $o['procedureName'] = $pn.InnerText }
			$dl = $op_p.SelectSingleNode('md:DataLockControlMode', $nsm)
			if ($dl -and $dl.InnerText -cne 'Managed') { $o['dataLockControlMode'] = $dl.InnerText }
			$osyn = Get-MLValue ($op_p.SelectSingleNode('md:Synonym', $nsm))
			if ($null -ne $osyn -and "$osyn" -cne (Split-CamelWords $opName)) { $o['synonym'] = $osyn }
			$ocmt = $op_p.SelectSingleNode('md:Comment', $nsm)
			if ($ocmt -and $ocmt.InnerText) { $o['comment'] = $ocmt.InnerText }

			$parNodes = @($op.SelectNodes('md:ChildObjects/md:Parameter', $nsm))
			if ($parNodes.Count -gt 0) {
				$pars = [ordered]@{}
				foreach ($par in $parNodes) {
					$pp = $par.SelectSingleNode('md:Properties', $nsm)
					$parName = ($pp.SelectSingleNode('md:Name', $nsm)).InnerText
					$po = [ordered]@{}
					$pt = Get-XDTOTypeValue ($pp.SelectSingleNode('md:XDTOValueType', $nsm))
					if ($pt) { $po['type'] = $pt }
					$pnil = $pp.SelectSingleNode('md:Nillable', $nsm)
					if ($pnil -and $pnil.InnerText -eq 'false') { $po['nillable'] = $false }
					$pdir = $pp.SelectSingleNode('md:TransferDirection', $nsm)
					if ($pdir -and $pdir.InnerText -cne 'In') { $po['direction'] = $pdir.InnerText }
					$psyn = Get-MLValue ($pp.SelectSingleNode('md:Synonym', $nsm))
					if ($null -ne $psyn -and "$psyn" -cne (Split-CamelWords $parName)) { $po['synonym'] = $psyn }
					$pcmt = $pp.SelectSingleNode('md:Comment', $nsm)
					if ($pcmt -and $pcmt.InnerText) { $po['comment'] = $pcmt.InnerText }
					# Только тип и дефолтное остальное → строчное сокращение.
					if ($po.Count -eq 1 -and $po.Contains('type')) { $pars[$parName] = $po['type'] } else { $pars[$parName] = $po }
				}
				$o['parameters'] = $pars
			}
			if ($o.Count -eq 1 -and $o.Contains('returnType')) { $ops[$opName] = $o['returnType'] } else { $ops[$opName] = $o }
		}
		$dsl['operations'] = $ops
	}
	# HTTPService: шаблоны URL и их методы. Шаблон — {template, methods{}}, метод — строка (только
	# HTTP-метод, когда обработчик совпадает с авто-выводом ИмяШаблона+ИмяМетода) либо объект.
	$tmplNodes = @($childObjs.SelectNodes('md:URLTemplate', $nsm))
	if ($tmplNodes.Count -gt 0) {
		$tmpls = [ordered]@{}
		foreach ($t in $tmplNodes) {
			$tp = $t.SelectSingleNode('md:Properties', $nsm)
			$tName = ($tp.SelectSingleNode('md:Name', $nsm)).InnerText
			$tObj = [ordered]@{}
			$tTemplate = $tp.SelectSingleNode('md:Template', $nsm)
			if ($tTemplate) { $tObj['template'] = $tTemplate.InnerText }
			$tSyn = Get-MLValue ($tp.SelectSingleNode('md:Synonym', $nsm))
			# -cne, не -ne: сравнение синонима с авто-выводом ДОЛЖНО быть регистрочувствительным,
			# иначе "Post" против "post" считается совпадением и синоним теряется.
			if ($null -ne $tSyn -and "$tSyn" -cne (Split-CamelWords $tName)) { $tObj['synonym'] = $tSyn }
			$tCmt = $tp.SelectSingleNode('md:Comment', $nsm)
			if ($tCmt -and $tCmt.InnerText) { $tObj['comment'] = $tCmt.InnerText }

			$mNodes = @($t.SelectNodes('md:ChildObjects/md:Method', $nsm))
			if ($mNodes.Count -gt 0) {
				$methods = [ordered]@{}
				foreach ($m in $mNodes) {
					$mp = $m.SelectSingleNode('md:Properties', $nsm)
					$mName = ($mp.SelectSingleNode('md:Name', $nsm)).InnerText
					$mHttp = $mp.SelectSingleNode('md:HTTPMethod', $nsm)
					$mHandler = $mp.SelectSingleNode('md:Handler', $nsm)
					$mSyn = Get-MLValue ($mp.SelectSingleNode('md:Synonym', $nsm))
					$mCmt = $mp.SelectSingleNode('md:Comment', $nsm)
					$httpVal = if ($mHttp) { $mHttp.InnerText } else { 'GET' }
					$handlerVal = if ($mHandler) { $mHandler.InnerText } else { '' }
					$synDefault = ($null -eq $mSyn) -or ("$mSyn" -ceq (Split-CamelWords $mName))
					$cmtEmpty = (-not $mCmt) -or (-not $mCmt.InnerText)
					if ($handlerVal -ceq "$tName$mName" -and $synDefault -and $cmtEmpty) {
						$methods[$mName] = $httpVal
					} else {
						$mo = [ordered]@{ httpMethod = $httpVal }
						if ($handlerVal) { $mo['handler'] = $handlerVal }
						if (-not $synDefault) { $mo['synonym'] = $mSyn }
						if (-not $cmtEmpty) { $mo['comment'] = $mCmt.InnerText }
						$methods[$mName] = $mo
					}
				}
				$tObj['methods'] = $methods
			}
			$tmpls[$tName] = $tObj
		}
		$dsl['urlTemplates'] = $tmpls
	}
	$attrs = @($childObjs.SelectNodes('md:Attribute', $nsm))
	if ($attrs.Count -gt 0) {
		$arr = [System.Collections.ArrayList]@()
		foreach ($a in $attrs) { [void]$arr.Add((Attr-ToDsl $a)) }
		$dsl['attributes'] = $arr
	}
	# Enum: значения перечисления. Плоский элемент name/synonym/comment. Короткая форма — строка "Имя"
	# (синоним == авто из имени, без comment, не мультиязычный); иначе объект {name, synonym?, comment?}.
	$evNodes = @($childObjs.SelectNodes('md:EnumValue', $nsm))
	if ($evNodes.Count -gt 0) {
		$evArr = [System.Collections.ArrayList]@()
		foreach ($ev in $evNodes) {
			$evp = $ev.SelectSingleNode('md:Properties', $nsm)
			$evName = ($evp.SelectSingleNode('md:Name', $nsm)).InnerText
			$evSynNode = $evp.SelectSingleNode('md:Synonym', $nsm)
			$evSyn = Get-MLValue $evSynNode
			$evCmtN = $evp.SelectSingleNode('md:Comment', $nsm); $evCmt = if ($evCmtN) { $evCmtN.InnerText } else { '' }
			# $evSynVal: строка ≠ авто → кастом; {ru,en} → кастом; пустой `<Synonym/>` (node есть, значения нет) →
			# явный '' (≠ авто-синоним из имени, аналог object-level фикса); авто/отсутствие → $null (короткая строка).
			$evSynVal = $null
			if ($evSyn -is [string]) { if ($evSyn -cne (Split-CamelWords $evName)) { $evSynVal = $evSyn } }
			elseif ($null -ne $evSyn) { $evSynVal = $evSyn }
			elseif ($evSynNode) { $evSynVal = '' }
			if (($null -ne $evSynVal) -or $evCmt) {
				$o = [ordered]@{ name = $evName }
				if ($null -ne $evSynVal) { $o['synonym'] = $evSynVal }
				if ($evCmt) { $o['comment'] = $evCmt }
				[void]$evArr.Add($o)
			} else {
				[void]$evArr.Add($evName)
			}
		}
		$dsl['values'] = $evArr
	}
	# DocumentJournal: колонки. Каждая — object {name, synonym?, comment?, indexing?, references[]}.
	# References — список MDObjectRef-путей к реквизитам регистрируемых документов (verbatim).
	$colNodes = @($childObjs.SelectNodes('md:Column', $nsm))
	if ($colNodes.Count -gt 0) {
		$colArr = [System.Collections.ArrayList]@()
		foreach ($col in $colNodes) {
			$cp = $col.SelectSingleNode('md:Properties', $nsm)
			$cName = ($cp.SelectSingleNode('md:Name', $nsm)).InnerText
			$o = [ordered]@{ name = $cName }
			$cSynNode = $cp.SelectSingleNode('md:Synonym', $nsm)
			$cSyn = Get-MLValue $cSynNode
			if ($cSyn -is [string]) { if ($cSyn -cne (Split-CamelWords $cName)) { $o['synonym'] = $cSyn } }
			elseif ($null -ne $cSyn) { $o['synonym'] = $cSyn }
			elseif ($cSynNode) { $o['synonym'] = '' }   # пустой <Synonym/> ≠ авто-синоним → явный ''
			$cCmtN = $cp.SelectSingleNode('md:Comment', $nsm); if ($cCmtN -and $cCmtN.InnerText) { $o['comment'] = $cCmtN.InnerText }
			$cIdxN = $cp.SelectSingleNode('md:Indexing', $nsm); if ($cIdxN -and $cIdxN.InnerText -ne 'DontIndex') { $o['indexing'] = $cIdxN.InnerText }
			$cRefNode = $cp.SelectSingleNode('md:References', $nsm)
			$refs = [System.Collections.ArrayList]@()
			if ($cRefNode) { foreach ($it in @($cRefNode.SelectNodes('xr:Item', $nsm))) { [void]$refs.Add($it.InnerText) } }
			$o['references'] = $refs
			[void]$colArr.Add($o)
		}
		$dsl['columns'] = $colArr
	}
	# ChartOfAccounts: признаки учёта (AccountingFlag) и признаки учёта субконто (ExtDimensionAccountingFlag) —
	# структурно как реквизит, захватываем тем же Attr-ToDsl (тип Boolean уходит в короткую запись).
	$acctFlagNodes = @($childObjs.SelectNodes('md:AccountingFlag', $nsm))
	if ($acctFlagNodes.Count -gt 0) {
		$arr = [System.Collections.ArrayList]@()
		foreach ($a in $acctFlagNodes) { [void]$arr.Add((Attr-ToDsl $a)) }
		$dsl['accountingFlags'] = $arr
	}
	$extDimFlagNodes = @($childObjs.SelectNodes('md:ExtDimensionAccountingFlag', $nsm))
	if ($extDimFlagNodes.Count -gt 0) {
		$arr = [System.Collections.ArrayList]@()
		foreach ($a in $extDimFlagNodes) { [void]$arr.Add((Attr-ToDsl $a)) }
		$dsl['extDimensionAccountingFlags'] = $arr
	}
	# Sequence: измерения несут DocumentMap/RegisterRecordsMap (соответствие реквизитам документов/движениям) —
	# Attr-ToDsl их не знает → отдельный захват объектной формой. Прочие типы (регистры) — общий Attr-ToDsl.
	$dimNodes = @($childObjs.SelectNodes('md:Dimension', $nsm))
	if ($dimNodes.Count -gt 0 -and $objType -eq 'Sequence') {
		$arr = [System.Collections.ArrayList]@()
		foreach ($dn in $dimNodes) {
			$dp = $dn.SelectSingleNode('md:Properties', $nsm)
			$dName = ($dp.SelectSingleNode('md:Name', $nsm)).InnerText
			$o = [ordered]@{ name = $dName }
			$dSyn = Get-MLValue ($dp.SelectSingleNode('md:Synonym', $nsm))
			if ($dSyn -is [string]) { if ($dSyn -cne (Split-CamelWords $dName)) { $o['synonym'] = $dSyn } }
			elseif ($null -ne $dSyn) { $o['synonym'] = $dSyn }
			$dCmtN = $dp.SelectSingleNode('md:Comment', $nsm); if ($dCmtN -and $dCmtN.InnerText) { $o['comment'] = $dCmtN.InnerText }
			$dt = Get-TypeShorthand ($dp.SelectSingleNode('md:Type', $nsm)); if ($dt) { $o['type'] = $dt }
			foreach ($mp in @(@('DocumentMap','documentMap'), @('RegisterRecordsMap','registerRecordsMap'))) {
				$mn = $dp.SelectSingleNode("md:$($mp[0])", $nsm)
				if ($mn) {
					$mItems = @($mn.SelectNodes('xr:Item', $nsm) | ForEach-Object { $_.InnerText })
					if ($mItems.Count -gt 0) { $o[$mp[1]] = [System.Collections.ArrayList]@($mItems) }
				}
			}
			[void]$arr.Add($o)
		}
		$dsl['dimensions'] = $arr
	} elseif ($dimNodes.Count -gt 0) {
		$arr = [System.Collections.ArrayList]@()
		foreach ($a in $dimNodes) { [void]$arr.Add((Attr-ToDsl $a)) }
		$dsl['dimensions'] = $arr
	}
	$resNodes = @($childObjs.SelectNodes('md:Resource', $nsm))
	if ($resNodes.Count -gt 0) {
		$arr = [System.Collections.ArrayList]@()
		foreach ($a in $resNodes) { [void]$arr.Add((Attr-ToDsl $a)) }
		$dsl['resources'] = $arr
	}
	# Задача: реквизиты адресации (AddressingAttribute) — структурно как реквизит + AddressingDimension.
	$addrNodes = @($childObjs.SelectNodes('md:AddressingAttribute', $nsm))
	if ($addrNodes.Count -gt 0) {
		$arr = [System.Collections.ArrayList]@()
		foreach ($a in $addrNodes) { [void]$arr.Add((Attr-ToDsl $a)) }
		$dsl['addressingAttributes'] = $arr
	}
	$tsNodes = @($childObjs.SelectNodes('md:TabularSection', $nsm))
	if ($tsNodes.Count -gt 0) {
		$tsMap = [ordered]@{}
		foreach ($ts in $tsNodes) {
			$tsp = $ts.SelectSingleNode('md:Properties', $nsm)
			$tsName = ($tsp.SelectSingleNode('md:Name', $nsm)).InnerText
			$tco = $ts.SelectSingleNode('md:ChildObjects', $nsm)
			$cols = [System.Collections.ArrayList]@()
			if ($tco) { foreach ($ca in @($tco.SelectNodes('md:Attribute', $nsm))) { [void]$cols.Add((Attr-ToDsl $ca)) } }
			# Синоним/подсказка/комментарий ТЧ. Кастом → объектная форма {synonym?, tooltip?, comment?, attributes}.
			$tsSyn = Get-MLValue ($tsp.SelectSingleNode('md:Synonym', $nsm))
			$tsSynCustom = $false
			if ($tsSyn -is [string]) { if ($tsSyn -cne (Split-CamelWords $tsName)) { $tsSynCustom = $true } }
			elseif ($null -ne $tsSyn) { $tsSynCustom = $true }
			$tsTt = Get-MLValue ($tsp.SelectSingleNode('md:ToolTip', $nsm))
			$tsCmtN = $tsp.SelectSingleNode('md:Comment', $nsm); $tsCmt = if ($tsCmtN) { $tsCmtN.InnerText } else { '' }
			# FillChecking ТЧ (обязательность заполнения; omit при DontCheck).
			$tsFcN = $tsp.SelectSingleNode('md:FillChecking', $nsm); $tsFc = if ($tsFcN -and $tsFcN.InnerText -ne 'DontCheck') { $tsFcN.InnerText } else { '' }
				# Use ТЧ (иерархические Catalog/ПВХ: ForItem/ForFolder/ForFolderAndItem; omit при дефолте ForItem).
				$tsUseN = $tsp.SelectSingleNode('md:Use', $nsm); $tsUse = if ($tsUseN -and $tsUseN.InnerText -ne 'ForItem') { $tsUseN.InnerText } else { '' }
			# TS-блок стандартных реквизитов (LineNumber). Наличие блока — пер-ТЧ артефакт (~6% ТЧ его опускают,
			# правило не выводимо). Faithful roundtrip: нет блока → маркер подавления `lineNumber: ""` (дом-конвенция);
			# есть блок → захват кастомизации LineNumber (omit-on-default по свойству), all-default → без ключа.
			$lnObj = [ordered]@{}
			$saTsNode = $tsp.SelectSingleNode('md:StandardAttributes', $nsm)
			$hasBlock = ($saTsNode -and @($saTsNode.SelectNodes('xr:StandardAttribute', $nsm)).Count -gt 0)
			$lnNode = if ($hasBlock) { $saTsNode.SelectSingleNode("xr:StandardAttribute[@name='LineNumber']", $nsm) } else { $null }
			if ($lnNode) {
				$lnSyn = Get-MLValue ($lnNode.SelectSingleNode('xr:Synonym', $nsm)); if ($null -ne $lnSyn) { $lnObj['synonym'] = $lnSyn }
				$lnCmtN = $lnNode.SelectSingleNode('xr:Comment', $nsm); if ($lnCmtN -and $lnCmtN.InnerText) { $lnObj['comment'] = $lnCmtN.InnerText }
				$lnFtsN = $lnNode.SelectSingleNode('xr:FullTextSearch', $nsm); if ($lnFtsN -and $lnFtsN.InnerText -ne 'Use') { $lnObj['fullTextSearch'] = $lnFtsN.InnerText }
				$lnTt = Get-MLValue ($lnNode.SelectSingleNode('xr:ToolTip', $nsm)); if ($null -ne $lnTt) { $lnObj['tooltip'] = $lnTt }
				$lnFmt = Get-MLValue ($lnNode.SelectSingleNode('xr:Format', $nsm)); if ($null -ne $lnFmt) { $lnObj['format'] = $lnFmt }
				$lnEfmt = Get-MLValue ($lnNode.SelectSingleNode('xr:EditFormat', $nsm)); if ($null -ne $lnEfmt) { $lnObj['editFormat'] = $lnEfmt }
				$lnChiN = $lnNode.SelectSingleNode('xr:ChoiceHistoryOnInput', $nsm); if ($lnChiN -and $lnChiN.InnerText -ne 'Auto') { $lnObj['choiceHistoryOnInput'] = $lnChiN.InnerText }
				# FillValue НомерСтроки: дефолт nil; редкая аномалия xs:decimal 0 → захват числом (иначе теряется в nil).
				$lnFvN = $lnNode.SelectSingleNode('xr:FillValue', $nsm)
				if ($lnFvN -and $lnFvN.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance') -ne 'true') {
					$lnFvT = $lnFvN.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
					if ($lnFvT -match 'decimal$') { $lnObj['fillValue'] = if ($lnFvN.InnerText -match '^-?\d+$') { [long]$lnFvN.InnerText } else { [double]$lnFvN.InnerText } }
				}
			}
			# Формат 2.20: длина номера строки ТЧ. Захватываем ВСЕГДА при наличии тега, а не
			# omit-on-default: дефолт зависит от режима совместимости конфигурации (≤8_3_26 → 5,
			# ≥8_3_27 → 9) и фиксируется платформой при создании ТЧ, так что вывести его здесь
			# значило бы продублировать логику компилятора с риском разойтись. Явный захват точен.
			$tsLnlN = $tsp.SelectSingleNode('md:LineNumberLength', $nsm)
			$tsLnl = if ($tsLnlN -and $tsLnlN.InnerText) { [int]$tsLnlN.InnerText } else { $null }
			if ($tsSynCustom -or ($null -ne $tsTt) -or $tsCmt -or $tsFc -or $tsUse -or $lnObj.Count -gt 0 -or (-not $hasBlock) -or ($null -ne $tsLnl)) {
				$to = [ordered]@{}
				if ($tsSynCustom) { $to['synonym'] = $tsSyn }
				if ($null -ne $tsTt) { $to['tooltip'] = $tsTt }
				if ($tsCmt) { $to['comment'] = $tsCmt }
				if ($tsFc) { $to['fillChecking'] = $tsFc }
				if ($tsUse) { $to['use'] = $tsUse }
				if ($null -ne $tsLnl) { $to['lineNumberLength'] = $tsLnl }
				if (-not $hasBlock) { $to['lineNumber'] = '' } elseif ($lnObj.Count -gt 0) { $to['lineNumber'] = $lnObj }
				$to['attributes'] = $cols
				$tsMap[$tsName] = $to
			} else {
				$tsMap[$tsName] = $cols
			}
		}
		$dsl['tabularSections'] = $tsMap
	}
	# --- Commands (полноблочные <Command> в ChildObjects) → DSL commands (map имя→объект, omit-on-default).
	# Тела модулей команд (CommandModule.bsl) — вне скоупа (как ObjectModule). ---
	$cmdNodes = @($childObjs.SelectNodes('md:Command', $nsm))
	if ($cmdNodes.Count -gt 0) {
		$cmdMap = [ordered]@{}
		foreach ($cm in $cmdNodes) {
			$cp = $cm.SelectSingleNode('md:Properties', $nsm)
			$cn = ($cp.SelectSingleNode('md:Name', $nsm)).InnerText
			$o = [ordered]@{}
			$syn = Get-MLValue ($cp.SelectSingleNode('md:Synonym', $nsm))
			if ($syn -is [string]) { if ($syn -cne (Split-CamelWords $cn)) { $o['synonym'] = $syn } }
			elseif ($null -ne $syn) { $o['synonym'] = $syn }
			$cmtN = $cp.SelectSingleNode('md:Comment', $nsm); if ($cmtN -and $cmtN.InnerText) { $o['comment'] = $cmtN.InnerText }
			$grpN = $cp.SelectSingleNode('md:Group', $nsm); if ($grpN -and $grpN.InnerText) { $o['group'] = $grpN.InnerText }
			$cpt = Get-TypeShorthand ($cp.SelectSingleNode('md:CommandParameterType', $nsm)); if ($cpt) { $o['commandParameterType'] = $cpt }
			$pumN = $cp.SelectSingleNode('md:ParameterUseMode', $nsm); if ($pumN -and $pumN.InnerText -ne 'Single') { $o['parameterUseMode'] = $pumN.InnerText }
			$mdN = $cp.SelectSingleNode('md:ModifiesData', $nsm); if ($mdN -and $mdN.InnerText -eq 'true') { $o['modifiesData'] = $true }
			$repN = $cp.SelectSingleNode('md:Representation', $nsm); if ($repN -and $repN.InnerText -ne 'Auto') { $o['representation'] = $repN.InnerText }
			$ctt = Get-MLValue ($cp.SelectSingleNode('md:ToolTip', $nsm)); if ($null -ne $ctt) { $o['tooltip'] = $ctt }
			# <Picture> — структурный блок (зеркало form-decompile Set-CommandPicture). Дефолт LoadTransparent=true:
			# скаляр `picture` + sibling `loadTransparent:false` при отклонении; объект {src,loadTransparent?,transparentPixel} при TransparentPixel.
			$refN = $cp.SelectSingleNode('md:Picture/xr:Ref', $nsm)
			$absN = $cp.SelectSingleNode('md:Picture/xr:Abs', $nsm)
			if ($refN -or $absN) {
				$psrc = if ($refN) { $refN.InnerText } else { "abs:$($absN.InnerText)" }
				$ltN = $cp.SelectSingleNode('md:Picture/xr:LoadTransparent', $nsm)
				$ltFalse = ($ltN -and $ltN.InnerText -eq 'false')
				$tpxN = $cp.SelectSingleNode('md:Picture/xr:TransparentPixel', $nsm)
				if ($tpxN) {
					$po = [ordered]@{ src = $psrc }
					if ($ltFalse) { $po['loadTransparent'] = $false }
					$po['transparentPixel'] = [ordered]@{ x = [int]$tpxN.GetAttribute('x'); y = [int]$tpxN.GetAttribute('y') }
					$o['picture'] = $po
				} else {
					$o['picture'] = $psrc
					if ($ltFalse) { $o['loadTransparent'] = $false }
				}
			}
			$scN = $cp.SelectSingleNode('md:Shortcut', $nsm); if ($scN -and $scN.InnerText) { $o['shortcut'] = $scN.InnerText }
			$osuN = $cp.SelectSingleNode('md:OnMainServerUnavalableBehavior', $nsm); if ($osuN -and $osuN.InnerText -ne 'Auto') { $o['onMainServerUnavalableBehavior'] = $osuN.InnerText }
			$cmdMap[$cn] = $o
		}
		$dsl['commands'] = $cmdMap
	}
}

# --- Предопределённые (соседний Ext/Predefined.xml) → DSL predefined.
# Плоский элемент → строка "(Код) Имя [Наименование]" (Наименование: ==авто → опустить; '' → []; иначе [текст]).
# Группа/иерархия → object {name, [code], [description], isFolder, childItems}. codeType — из свойства каталога. ---
$objDir = Split-Path -Parent (Resolve-Path -LiteralPath $ObjectPath).Path
$predefPath = Join-Path (Join-Path (Join-Path $objDir $objName) 'Ext') 'Predefined.xml'
if (Test-Path -LiteralPath $predefPath) {
	$pdoc = New-Object System.Xml.XmlDocument
	$pdoc.Load($predefPath)
	function PredefItem-ToDsl {
		param($itemEl)
		$name = ($itemEl.SelectSingleNode("*[local-name()='Name']")).InnerText
		$codeEl = $itemEl.SelectSingleNode("*[local-name()='Code']")
		$code = if ($codeEl -and $codeEl.InnerText) { $codeEl.InnerText } else { '' }
		$descEl = $itemEl.SelectSingleNode("*[local-name()='Description']")
		$desc = if ($descEl) { $descEl.InnerText } else { '' }
		$folderEl = $itemEl.SelectSingleNode("*[local-name()='IsFolder']")
		$isFolder = ($folderEl -and $folderEl.InnerText -eq 'true')
		$childContainer = $itemEl.SelectSingleNode("*[local-name()='ChildItems']")
		# ВАЖНО: обернуть весь if в @(), иначе PS распаковывает одноэлементный @(...) из if-блока
		# обратно в узел → $kids.Count = $null → папки с ОДНИМ ребёнком теряют его.
		$kids = @(if ($childContainer) { $childContainer.SelectNodes("*[local-name()='Item']") } else { @() })
		# Type — тип значения предопределённой характеристики (ПВХ). Наличие → object-форма (в строку не влезает).
		# Наличие узла <Type> (даже пустого <Type/>) → object-форма с ключом type ('' для пустого); нет узла (Catalog) → без.
		$typeEl = $itemEl.SelectSingleNode("*[local-name()='Type']")
		$typeStr = if ($typeEl) { Get-TypeShorthand $typeEl } else { $null }
		$auto = Split-CamelWords $name

		# Компактная строка для плоских: без узла Type (Catalog) ИЛИ с непустым типом → "(Код) Имя [Наим]: Тип".
		# Пустой <Type/> в короткую не влезает (нужен явный маркер) → object-форма с type:''.
		# Сокращение неоднозначно, если значение содержит собственные разделители грамматики
		# "(Код) Имя [Наим]: Тип": ')' или ':' в коде, пробел/скобка/':' в имени, скобки в наименовании.
		# Компилятор разбирает код как [^)]*, а имя как \S+ — на "114 (108)" разбор рассыпается,
		# и элемент терял и имя, и код (6 элементов в БП и столько же в ERP).
		$ambiguous = ($code -match '[):]') -or ($name -match '[\s:\[\]()]') -or ($desc -match '[\[\]]')
		if (-not $isFolder -and $kids.Count -eq 0 -and ($null -eq $typeStr -or $typeStr -ne '') -and -not $ambiguous) {
			$s = if ($code) { "($code) $name" } else { $name }
			if ($desc -eq '') { $s = "$s []" }
			elseif ($desc -cne $auto) { $s = "$s [$desc]" }
			if ($typeStr) { $s = "${s}: $typeStr" }
			return $s
		}
		# Группа/иерархия/с типом → объект.
		$o = [ordered]@{ name = $name }
		if ($code) { $o['code'] = $code }
		if ($desc -eq '') { $o['description'] = '' }
		elseif ($desc -cne $auto) { $o['description'] = $desc }
		if ($null -ne $typeStr) { $o['type'] = $typeStr }
		if ($isFolder) { $o['isFolder'] = $true }
		if ($kids.Count -gt 0) {
			$sub = [System.Collections.ArrayList]@()
			foreach ($k in $kids) { [void]$sub.Add((PredefItem-ToDsl $k)) }
			$o['childItems'] = $sub
		}
		return $o
	}
		# Предопределённые СЧЕТА Плана счетов — отдельная грамматика (AccountType/OffBalance/Order/AccountingFlags/
		# ExtDimensionTypes). Флаги: захватываем только TRUE (компилятор развернёт по def-порядку признаков плана).
		function PredefAccount-ToDsl {
			param($itemEl)
			$name = ($itemEl.SelectSingleNode("*[local-name()='Name']")).InnerText
			$codeEl = $itemEl.SelectSingleNode("*[local-name()='Code']")
			$code = if ($codeEl -and $codeEl.InnerText) { $codeEl.InnerText } else { '' }
			$descEl = $itemEl.SelectSingleNode("*[local-name()='Description']")
			$desc = if ($descEl) { $descEl.InnerText } else { '' }
			$atEl = $itemEl.SelectSingleNode("*[local-name()='AccountType']")
			$acctType = if ($atEl) { $atEl.InnerText } else { 'ActivePassive' }
			$offEl = $itemEl.SelectSingleNode("*[local-name()='OffBalance']")
			$off = ($offEl -and $offEl.InnerText -eq 'true')
			$ordEl = $itemEl.SelectSingleNode("*[local-name()='Order']")
			$order = if ($ordEl) { $ordEl.InnerText } else { '' }
			$auto = Split-CamelWords $name
			# TRUE-флаги (leaf после последней точки в ref).
			$trueFlags = @()
			foreach ($fl in @($itemEl.SelectNodes("*[local-name()='AccountingFlags']/*[local-name()='Flag']"))) {
				if ($fl.InnerText -eq 'true') { $r = $fl.GetAttribute('ref'); $trueFlags += ($r -split '\.')[-1] }
			}
			# ExtDimensionTypes → subconto. Короткая запись "Тип | Признак1, Признак2": срезаем префикс ПВХ видов субконто
			# плана (extDimensionTypes); «Только обороты» (Turnover=true) — предопределённый признак-токен `Turnover` первым
			# в списке (наравне с добавленными). Захватываем только TRUE-признаки (компилятор развернёт по def-порядку).
			$edtPfx = if ($dsl['extDimensionTypes']) { "$($dsl['extDimensionTypes'])." } else { $null }
			$subconto = [System.Collections.ArrayList]@()
			foreach ($edt in @($itemEl.SelectNodes("*[local-name()='ExtDimensionTypes']/*[local-name()='ExtDimensionType']"))) {
				$scT = $edt.GetAttribute('name')
				if ($edtPfx -and $scT.StartsWith($edtPfx)) { $scT = $scT.Substring($edtPfx.Length) }
				$tEl = $edt.SelectSingleNode("*[local-name()='Turnover']")
				$scFlagsOut = @()
				if ($tEl -and $tEl.InnerText -eq 'true') { $scFlagsOut += 'Turnover' }
				foreach ($fl in @($edt.SelectNodes("*[local-name()='AccountingFlags']/*[local-name()='Flag']"))) {
					if ($fl.InnerText -eq 'true') { $r = $fl.GetAttribute('ref'); $scFlagsOut += ($r -split '\.')[-1] }
				}
				if ($scFlagsOut.Count -gt 0) { [void]$subconto.Add("$scT | " + ($scFlagsOut -join ', ')) }
				else { [void]$subconto.Add($scT) }
			}
			$childContainer = $itemEl.SelectSingleNode("*[local-name()='ChildItems']")
			$kids = @(if ($childContainer) { $childContainer.SelectNodes("*[local-name()='Item']") } else { @() })

			$o = [ordered]@{ name = $name }
			if ($code) { $o['code'] = $code }
			# -cne (регистрочувствительно!): PS `-ne` регистронезависим → «…ОС» == «…ос» и description ошибочно
			# опускался, а компилятор регенерил lowercase (Split-CamelCase). Хвостовые аббревиатуры (ОС/НМА) теряли регистр.
			if ($desc -cne $auto) { $o['description'] = $desc }
			$o['accountType'] = $acctType
			if ($off) { $o['offBalance'] = $true }
			$o['order'] = $order
			if ($trueFlags.Count -gt 0) { $o['flags'] = [System.Collections.ArrayList]@($trueFlags) }
			if ($subconto.Count -gt 0) { $o['subconto'] = $subconto }
			if ($kids.Count -gt 0) {
				$sub = [System.Collections.ArrayList]@()
				foreach ($k in $kids) { [void]$sub.Add((PredefAccount-ToDsl $k)) }
				$o['childItems'] = $sub
			}
			return $o
		}

		# Предопределённые ВИДЫ РАСЧЁТА (плоские): Name/Code/Description/ActionPeriodIsBase. Строка при
		# ActionPeriodIsBase=false, объект — при true.
		function PredefCalcType-ToDsl {
			param($itemEl)
			$name = ($itemEl.SelectSingleNode("*[local-name()='Name']")).InnerText
			$codeEl = $itemEl.SelectSingleNode("*[local-name()='Code']"); $code = if ($codeEl -and $codeEl.InnerText) { $codeEl.InnerText } else { '' }
			$descEl = $itemEl.SelectSingleNode("*[local-name()='Description']"); $desc = if ($descEl) { $descEl.InnerText } else { '' }
			$apibEl = $itemEl.SelectSingleNode("*[local-name()='ActionPeriodIsBase']"); $apib = ($apibEl -and $apibEl.InnerText -eq 'true')
			$auto = Split-CamelWords $name
			if (-not $apib) {
				$s = if ($code) { "($code) $name" } else { $name }
				if ($desc -eq '') { $s = "$s []" } elseif ($desc -cne $auto) { $s = "$s [$desc]" }
				return $s
			}
			$o = [ordered]@{ name = $name }
			if ($code) { $o['code'] = $code }
			if ($desc -cne $auto) { $o['description'] = $desc }
			$o['actionPeriodIsBase'] = $true
			return $o
		}

	$rootItems = [System.Collections.ArrayList]@()
	if ($objType -eq 'ChartOfAccounts') {
		foreach ($it in @($pdoc.DocumentElement.SelectNodes("*[local-name()='Item']"))) { [void]$rootItems.Add((PredefAccount-ToDsl $it)) }
	} elseif ($objType -eq 'ChartOfCalculationTypes') {
		foreach ($it in @($pdoc.DocumentElement.SelectNodes("*[local-name()='Item']"))) { [void]$rootItems.Add((PredefCalcType-ToDsl $it)) }
	} else {
		foreach ($it in @($pdoc.DocumentElement.SelectNodes("*[local-name()='Item']"))) { [void]$rootItems.Add((PredefItem-ToDsl $it)) }
	}
	if ($rootItems.Count -gt 0) { $dsl['predefined'] = $rootItems }
}

# --- Состав плана обмена (соседний Ext/Content.xml) → DSL content (ExchangePlan).
# Каждый <Item>{<Metadata>MDRef</Metadata><AutoRecord>Deny|Allow</AutoRecord>}.
# Deny (дефолт) → строка "MDRef"; Allow → "MDRef: autoRecord". ---
if ($objType -eq 'ExchangePlan') {
	$contentPath = Join-Path (Join-Path (Join-Path $objDir $objName) 'Ext') 'Content.xml'
	if (Test-Path -LiteralPath $contentPath) {
		$cdoc = New-Object System.Xml.XmlDocument
		$cdoc.Load($contentPath)
		$contentItems = [System.Collections.ArrayList]@()
		foreach ($it in @($cdoc.DocumentElement.SelectNodes("*[local-name()='Item']"))) {
			$mdEl = $it.SelectSingleNode("*[local-name()='Metadata']")
			if (-not $mdEl -or -not $mdEl.InnerText) { continue }
			$ref = $mdEl.InnerText
			$arEl = $it.SelectSingleNode("*[local-name()='AutoRecord']")
			$ar = if ($arEl) { $arEl.InnerText } else { 'Deny' }
			if ($ar -eq 'Allow') { [void]$contentItems.Add("${ref}: autoRecord") } else { [void]$contentItems.Add($ref) }
		}
		if ($contentItems.Count -gt 0) { $dsl['content'] = $contentItems }
	}
}

# === Вывод ===
$json = ConvertTo-CompactJson $dsl 0
if ($OutputPath) {
	[System.IO.File]::WriteAllText($OutputPath, $json, (New-Object System.Text.UTF8Encoding($false)))
} else {
	[Console]::Out.WriteLine($json)
}
