# form-decompile v0.150 — Decompile 1C managed Form.xml to JSON DSL (draft)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
[CmdletBinding(PositionalBinding=$false)]
param(
	[Parameter(Mandatory)]
	[Alias('Path')]
	[string]$FormPath,

	[string]$OutputPath
)

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

# --- 0. Resolve and validate input ---
if (-not (Test-Path $FormPath)) {
	Write-Error "Form not found: $FormPath"
	exit 1
}
$FormPath = (Resolve-Path $FormPath).Path

$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $false
$xmlDoc.Load($FormPath)
$root = $xmlDoc.DocumentElement

# Второй документ с сохранением whitespace — только для восстановления ТОЧНОГО числа пробелов
# в whitespace-only <v8:content> (декорации-распорки без width: число пробелов = ширина). Основной
# парс (PreserveWhitespace=false) не трогаем; элементная структура обоих документов идентична →
# навигация по индекс-пути элементов (Resolve-WS). Загрузка лениво-безопасная.
$script:xmlDocWS = $null
try { $script:xmlDocWS = New-Object System.Xml.XmlDocument; $script:xmlDocWS.PreserveWhitespace = $true; $script:xmlDocWS.Load($FormPath) } catch { $script:xmlDocWS = $null }

# Ring 2: not a managed Form
if ($root.LocalName -ne 'Form') {
	[Console]::Error.WriteLine("form-decompile: корневой элемент <$($root.LocalName)> не <Form> — это не управляемая форма.")
	exit 2
}

# --- 1. Namespaces ---
$NS_LF  = "http://v8.1c.ru/8.3/xcf/logform"
$NS_V8  = "http://v8.1c.ru/8.1/data/core"
$NS_XR  = "http://v8.1c.ru/8.3/xcf/readable"
$NS_XSI = "http://www.w3.org/2001/XMLSchema-instance"

$NS_DCSSET = "http://v8.1c.ru/8.1/data-composition-system/settings"
$NS_DCSSCH = "http://v8.1c.ru/8.1/data-composition-system/schema"
$NS_DCSCOR = "http://v8.1c.ru/8.1/data-composition-system/core"
$NS_V8UI   = "http://v8.1c.ru/8.1/data/ui"
$NS_APP    = "http://v8.1c.ru/8.2/managed-application/core"

$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$ns.AddNamespace("lf", $NS_LF)
$ns.AddNamespace("v8", $NS_V8)
$ns.AddNamespace("xr", $NS_XR)
$ns.AddNamespace("xsi", $NS_XSI)
$ns.AddNamespace("dcsset", $NS_DCSSET)
$ns.AddNamespace("dcssch", $NS_DCSSCH)
$ns.AddNamespace("dcscor", $NS_DCSCOR)
$ns.AddNamespace("v8ui", $NS_V8UI)
$ns.AddNamespace("app", $NS_APP)

# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм).
# Если ListSettings = пустой скелет с этими GUID → декомпилятор опускает настройки вовсе,
# компилятор регенерит тот же скелет → чистый раундтрип.
$CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb'
$CANON_ORDER_ID  = '88619765-ccb3-46c6-ac52-38e9c992ebd4'
$CANON_CA_ID     = 'b75fecce-942b-4aed-abc9-e6a02e460fb3'
$CANON_ITEMS_ID  = '911b6018-f537-43e8-a417-da56b22f9aec'

# --- Вынос запроса динсписка в .sql рядом с output (зеркало skd-decompile) ---
$script:outputDir = $null
$script:outputBasename = $null
if ($OutputPath) {
	$od = Split-Path -Parent $OutputPath
	if (-not $od) { $od = (Get-Location).Path }
	$script:outputDir = $od
	$script:outputBasename = [System.IO.Path]::GetFileNameWithoutExtension($OutputPath)
}
$script:queryFilesAccumulator = @()
$script:queryFileNamesUsed = @{}

# Запрос ≥3 строк + есть outputDir → вынести в `<basename>-<listName>.sql`, вернуть "@file".
function Maybe-ExternalizeQuery {
	param([string]$queryText, [string]$listName)
	if (-not $queryText) { return $queryText }
	if (-not $script:outputDir) { return $queryText }
	$lineCount = ([regex]::Matches($queryText, "`n")).Count + 1
	if ($lineCount -lt 3) { return $queryText }
	$safe = ($listName -replace '[^\w\-]', '_'); if (-not $safe) { $safe = 'query' }
	$prefix = if ($script:outputBasename) { "$($script:outputBasename)-" } else { '' }
	$fileName = "$prefix$safe.sql"
	$suffix = 1
	while ($script:queryFileNamesUsed.ContainsKey($fileName)) { $suffix++; $fileName = "$prefix$safe`_$suffix.sql" }
	$script:queryFileNamesUsed[$fileName] = $true
	$script:queryFilesAccumulator += [ordered]@{ fileName = $fileName; text = $queryText }
	return "@$fileName"
}
function Save-QueryFiles {
	if ($script:queryFilesAccumulator.Count -eq 0) { return }
	if (-not $script:outputDir) { return }
	$enc = New-Object System.Text.UTF8Encoding($false)
	foreach ($qf in $script:queryFilesAccumulator) {
		[System.IO.File]::WriteAllText((Join-Path $script:outputDir $qf.fileName), $qf.text, $enc)
	}
	[Console]::Error.WriteLine("Saved $($script:queryFilesAccumulator.Count) external query file(s)")
}

# Есть ли в ListSettings содержательные настройки (реальные items фильтра/порядка/
# условного оформления/параметров)? Пустой скелет (только viewMode+GUID) → false:
# декомпилятор опускает настройки, компилятор регенерит каноничный скелет, harness
# нормализует GUID → чистый раундтрип. true → контент захватывается (см. ниже).
function Test-ListSettingsHasContent {
	param($lsNode)
	if (-not $lsNode) { return $false }
	foreach ($cont in @('filter','order','conditionalAppearance','dataParameters')) {
		$cn = $lsNode.SelectSingleNode("dcsset:$cont", $ns)
		if ($cn -and $cn.SelectSingleNode("dcsset:item", $ns)) { return $true }
	}
	return $false
}

# Форма ListSettings: ordered-карта present top-level элементов (filter/order/conditionalAppearance →
# блок-мета 'v'/'u'/'vu'/''; itemsViewMode/itemsUserSettingID → $true). Возвращает $null, если форма ==
# полному каноничному скелету (компилятор регенерит сам) ИЛИ содержит неподдержанные top-level элементы
# (item/dataParameters/viewMode/userSettingID/… → fallback на канон). Иначе — дескриптор для компилятора.
function Get-ListSettingsShape {
	param($lsNode, [bool]$hasGrouping = $false)
	if (-not $lsNode) { return $null }
	$shape = [ordered]@{}
	foreach ($child in $lsNode.ChildNodes) {
		if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
		$tag = $child.LocalName
		if ($tag -in @('filter','order','conditionalAppearance')) {
			$hasVM = $null -ne $child.SelectSingleNode("dcsset:viewMode", $ns)
			$hasUS = $null -ne $child.SelectSingleNode("dcsset:userSettingID", $ns)
			$code = "$(if ($hasVM) {'v'})$(if ($hasUS) {'u'})"
			# Контейнер может нести собственный <dcsset:userSettingPresentation> (кастомная подпись
			# настройки) — сохраняем форму по xsi:type (Get-PresByType: ru-only LocalString ≠ xs:string).
			$uspNode = $child.SelectSingleNode("dcsset:userSettingPresentation", $ns)
			if ($uspNode) {
				$usp = Get-PresByType $uspNode
				$shape[$tag] = [ordered]@{ meta = $code; presentation = $usp }
			} else { $shape[$tag] = $code }
		} elseif ($tag -eq 'itemsViewMode') { $shape['itemsViewMode'] = $true }
		elseif ($tag -eq 'itemsUserSettingID') { $shape['itemsUserSettingID'] = $true }
		elseif ($tag -eq 'itemsUserSettingPresentation') { $shape['itemsUserSettingPresentation'] = Get-PresByType $child }   # items-уровневая подпись (форма по xsi:type)
		elseif ($tag -eq 'dataParameters') { $shape['dataParameters'] = $true }   # значения параметров запроса (контент в settings.dataParameters)
		elseif ($tag -eq 'item') { if ($hasGrouping) { $shape['structure'] = $true } else { return $null } }
		else { return $null }  # неизвестный top-level → канон-fallback
	}
	# Полный каноничный скелет → опускаем (компилятор регенерит)
	if ($shape.Count -eq 5 -and $shape['filter'] -eq 'vu' -and $shape['order'] -eq 'vu' -and `
		$shape['conditionalAppearance'] -eq 'vu' -and $shape['itemsViewMode'] -eq $true -and $shape['itemsUserSettingID'] -eq $true) { return $null }
	return $shape
}

# Группировка строк динамического списка: цепочка <dcsset:item StructureItemGroup> (каждый уровень =
# один groupItems-field; вложенность через дочерний <dcsset:item>). Возвращает ПЛОСКИЙ массив уровней
# (string для дефолтного поля; объект {field,groupType?,periodAdditionType?,periodAdditionBegin?,periodAdditionEnd?}
# для нестандартного) ИЛИ $null, если структура не «чистая линейная цепочка одно-польных уровней»
# (ветвление/мультиполе/доп.содержимое) → честный fallback на канон (LOST), а не тихая порча.
function Build-GroupLevel {
	param($fn)
	$field = Get-Child $fn 'field'
	$gt = Get-Child $fn 'groupType'
	$pat = Get-Child $fn 'periodAdditionType'
	$pabN = $fn.SelectSingleNode("dcsset:periodAdditionBegin", $ns)
	$paeN = $fn.SelectSingleNode("dcsset:periodAdditionEnd", $ns)
	$pab = $null; $pae = $null
	if ($pabN) { $pt = $pabN.GetAttribute("type", $NS_XSI); $pv = $pabN.InnerText; if (($pt -match 'Field$') -or ($pv -and $pv -ne '0001-01-01T00:00:00')) { $pab = $pv } }
	if ($paeN) { $pt = $paeN.GetAttribute("type", $NS_XSI); $pv = $paeN.InnerText; if (($pt -match 'Field$') -or ($pv -and $pv -ne '0001-01-01T00:00:00')) { $pae = $pv } }
	$isDefault = ((-not $gt) -or $gt -eq 'Items') -and ((-not $pat) -or $pat -eq 'None') -and (-not $pab) -and (-not $pae)
	if ($isDefault) { return $field }
	$o = [ordered]@{ field = $field }
	if ($gt -and $gt -ne 'Items') { $o['groupType'] = $gt }
	if ($pat -and $pat -ne 'None') { $o['periodAdditionType'] = $pat }
	if ($pab) { $o['periodAdditionBegin'] = $pab }
	if ($pae) { $o['periodAdditionEnd'] = $pae }
	return $o
}

function Build-ListGrouping {
	param($itemNode)
	$levels = New-Object System.Collections.ArrayList
	$cur = $itemNode
	while ($cur) {
		if (($cur.GetAttribute("type", $NS_XSI)) -notmatch 'StructureItemGroup$') { return $null }
		$gi = $null; $nested = @()
		foreach ($ch in $cur.ChildNodes) {
			if ($ch.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
			switch ($ch.LocalName) {
				'groupItems' { if ($gi) { return $null }; $gi = $ch }
				'item'       { $nested += $ch }
				default      { return $null }  # use/name/filter/order/… — здесь не поддержано
			}
		}
		if (-not $gi) { return $null }
		$fieldItems = @($gi.SelectNodes("dcsset:item", $ns))
		if ($fieldItems.Count -ne 1) { return $null }
		$fn = $fieldItems[0]
		if (($fn.GetAttribute("type", $NS_XSI)) -notmatch 'GroupItemField$') { return $null }
		[void]$levels.Add((Build-GroupLevel $fn))
		if ($nested.Count -eq 0) { break }
		if ($nested.Count -gt 1) { return $null }  # ветвление — не линейно
		$cur = $nested[0]
	}
	if ($levels.Count -eq 0) { return $null }
	return ,@($levels)
}

# --- 1b. Ring-3 scan: конструкции вне зоны поддержки (draft list) ---
function Fail-Ring3 {
	param([string]$kind, [string]$loc)
	[Console]::Error.WriteLine("form-decompile: декомпиляция пока не поддерживает $kind (path: $loc)")
	[Console]::Error.WriteLine("Для точечной работы с этой формой используй /form-edit.")
	exit 3
}
# ConditionalAppearance со scope (привязка к области) пока не воспроизводим — fail-ring3 только в этом случае.
foreach ($el in $xmlDoc.SelectNodes("//*[local-name()='ConditionalAppearance']/*[local-name()='item']/*[local-name()='scope'][node()]")) { Fail-Ring3 -kind "ConditionalAppearance со scope" -loc "form/ConditionalAppearance/item/scope" }
# Реквизит с design-time конфигурацией (<Settings> chart-типа: Диаграмма/ДиаграммаГанта/… —
# d4p1:GanttChart и т.п.). Поддержаны только TypeDescription (valueType) и DynamicList; прочие
# (встроенная конфигурация диаграммы) пока НЕ воспроизводим → честный скип, чтобы не потерять молча.
foreach ($s in $xmlDoc.SelectNodes("//*[local-name()='Attribute']/*[local-name()='Settings']")) {
	$st = $s.GetAttribute("type", $NS_XSI)
	if ($st -and $st -notmatch 'TypeDescription$' -and $st -notmatch 'DynamicList$' -and $st -notmatch 'Planner$' -and $st -notmatch 'd4p1:(Gantt)?Chart$') {
		Fail-Ring3 -kind "Attribute>Settings типа '$st' (design-time конфигурация, напр. диаграмма)" -loc "Attribute/Settings"
	}
	# Chart/GanttChart с точками/осями: типизированные значения (xsi:type), xsi:nil и ML с
	# префиксом d4p1: (а не v8:) генерик-движок не сохраняет → честный скип. Частые
	# дашборд-диаграммы/гант (серии/легенда/оформление/шкала) поддержаны.
	elseif ($st -match 'd4p1:(Gantt)?Chart$' -and ($s.OuterXml -match '<d4p1:\w+ xsi:type=' -or $s.OuterXml -match '<d4p1:\w+ xsi:nil=' -or $s.OuterXml -match '<d4p1:item[ >]')) {
		Fail-Ring3 -kind "Attribute>Settings $st с точками/осями (типизированные значения/d4p1-ML)" -loc "Attribute/Settings"
	}
}

# --- 1c. Compact JSON serializer (созвучно skd-decompile: 2-проб. indent, inline в пределах lineLimit) ---
function Convert-StringToJsonLiteral {
	param([string]$s)
	if ($null -eq $s) { return 'null' }
	$sb = New-Object System.Text.StringBuilder
	[void]$sb.Append('"')
	foreach ($ch in $s.ToCharArray()) {
		$code = [int]$ch
		if ($code -eq 0x22)     { [void]$sb.Append('\"') }
		elseif ($code -eq 0x5C) { [void]$sb.Append('\\') }
		elseif ($code -eq 0x08) { [void]$sb.Append('\b') }
		elseif ($code -eq 0x09) { [void]$sb.Append('\t') }
		elseif ($code -eq 0x0A) { [void]$sb.Append('\n') }
		elseif ($code -eq 0x0C) { [void]$sb.Append('\f') }
		elseif ($code -eq 0x0D) { [void]$sb.Append('\r') }
		elseif ($code -lt 0x20) { [void]$sb.AppendFormat('\u{0:x4}', $code) }
		else { [void]$sb.Append($ch) }
	}
	[void]$sb.Append('"')
	return $sb.ToString()
}
function Try-InlineJson {
	param($obj)
	if ($null -eq $obj) { return 'null' }
	if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } }
	if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) }
	if ($obj -is [int] -or $obj -is [long]) { return "$obj" }
	if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) {
		return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture))
	}
	if ($obj -is [System.Collections.IDictionary]) {
		if ($obj.Count -eq 0) { return '{}' }
		$parts = @()
		foreach ($k in $obj.Keys) {
			$v = Try-InlineJson $obj[$k]
			if ($null -eq $v) { return $null }
			$parts += "$(Convert-StringToJsonLiteral "$k"): $v"
		}
		return '{ ' + ($parts -join ', ') + ' }'
	}
	if ($obj -is [array] -or $obj -is [System.Collections.IList]) {
		$items = @($obj)
		if ($items.Count -eq 0) { return '[]' }
		$parts = @()
		foreach ($it in $items) {
			$v = Try-InlineJson $it
			if ($null -eq $v) { return $null }
			$parts += $v
		}
		return '[' + ($parts -join ', ') + ']'
	}
	return $null
}
function ConvertTo-CompactJson {
	param($obj, [int]$depth = 0, [string]$indentUnit = '  ', [int]$lineLimit = 120)
	$indent = $indentUnit * $depth
	$childIndent = $indentUnit * ($depth + 1)
	if ($null -eq $obj) { return 'null' }
	if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } }
	if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) }
	if ($obj -is [int] -or $obj -is [long]) { return "$obj" }
	if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) {
		return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture))
	}
	$isContainer = ($obj -is [System.Collections.IDictionary]) -or ($obj -is [array]) -or ($obj -is [System.Collections.IList])
	if ($isContainer) {
		$inlineAttempt = Try-InlineJson $obj
		if ($null -ne $inlineAttempt -and ($indent.Length + $inlineAttempt.Length) -le $lineLimit) { return $inlineAttempt }
	}
	if ($obj -is [System.Collections.IDictionary]) {
		$keys = @($obj.Keys)
		if ($keys.Count -eq 0) { return '{}' }
		$parts = @()
		foreach ($k in $keys) {
			$val = ConvertTo-CompactJson -obj $obj[$k] -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit
			$parts += "$childIndent$(Convert-StringToJsonLiteral "$k"): $val"
		}
		return "{`n" + ($parts -join ",`n") + "`n$indent}"
	}
	if ($obj -is [array] -or $obj -is [System.Collections.IList]) {
		$items = @($obj)
		if ($items.Count -eq 0) { return '[]' }
		$parts = @($items | ForEach-Object { "$childIndent$(ConvertTo-CompactJson -obj $_ -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit)" })
		return "[`n" + ($parts -join ",`n") + "`n$indent]"
	}
	return (Convert-StringToJsonLiteral "$obj")
}

# --- 2. Helpers ---

# Companion-элементы (авто-генерируемые компилятором) — пропускаем при обходе детей.
# Дополнения (Search*/ViewStatus) БОЛЬШЕ не companion — декомпилируются как тип-элементы
# (кастомные в AutoCommandBar/ChildItems → commandBar.children; стандартные на уровне таблицы → карта additions).
$COMPANION_TAGS = @('ContextMenu','ExtendedTooltip','AutoCommandBar')

# Извлечь мультиязычный Title/Presentation → string (ru) или ordered hash {ru,en,...}
function Get-LangText {
	param($node)
	if ($null -eq $node) { return $null }
	$items = @($node.SelectNodes("v8:item", $ns))
	if ($items.Count -eq 0) { return $null }
	$map = [ordered]@{}
	foreach ($it in $items) {
		$lang = $it.SelectSingleNode("v8:lang", $ns)
		$content = $it.SelectSingleNode("v8:content", $ns)
		if ($lang) { $map[$lang.InnerText] = if ($content) { $content.InnerText } else { "" } }
	}
	if ($map.Count -eq 1 -and $map.Contains('ru')) { return $map['ru'] }
	return $map
}

# Get-LangText с восстановлением значимого пробела: PreserveWhitespace=false стрипает
# <v8:content> </v8:content> → "" (неотличимо от суппресса). Платформа НЕ эмитит пустой
# Title/ToolTip, значит исходно был пробел → возвращаем " " (как Get-MLFormattedValue).
# Покрывает и одиночную строку (ru-only), и мультиязычную мапу (напр. декорация-разделитель
# «Пробел» с ru+en пробелами): восстанавливаем " " в каждом языке, где content-узел есть, но пуст.
# Точное число пробелов whitespace-only <v8:content> из PreserveWhitespace-документа (основной
# парс стрипает его в ""). Навигация по индекс-пути элементов (структура обоих документов идентична).
function Resolve-WS {
	param($contentNode)
	if (-not $contentNode -or -not $script:xmlDocWS) { return $null }
	$idxs = New-Object System.Collections.ArrayList
	$cur = $contentNode
	while ($cur.ParentNode -and $cur.ParentNode.NodeType -eq [System.Xml.XmlNodeType]::Element) {
		$i = 0; $sib = $cur.PreviousSibling
		while ($sib) { if ($sib.NodeType -eq [System.Xml.XmlNodeType]::Element) { $i++ }; $sib = $sib.PreviousSibling }
		[void]$idxs.Insert(0, $i)
		$cur = $cur.ParentNode
	}
	$wcur = $script:xmlDocWS.DocumentElement
	foreach ($ix in $idxs) {
		$els = @(); foreach ($ch in $wcur.ChildNodes) { if ($ch.NodeType -eq [System.Xml.XmlNodeType]::Element) { $els += $ch } }
		if ($ix -ge $els.Count) { return $null }
		$wcur = $els[$ix]
	}
	return $wcur.InnerText
}

# Точное восстановление пробела (число): whitespace-only content → реальная строка пробелов из WS-дока.
function Restore-WSContent {
	param($contentNode)
	$ws = Resolve-WS $contentNode
	if ($ws -and $ws.Trim() -eq '') { return $ws }   # только если действительно whitespace
	return ' '
}

function Get-LangTextWS {
	param($node)
	$t = Get-LangText $node
	if ($null -eq $t) { return $null }
	if ($t -is [string]) {
		$cn = $node.SelectSingleNode("v8:item/v8:content", $ns)
		if ($t -eq '' -and $cn) { return (Restore-WSContent $cn) }
		return $t
	}
	foreach ($it in @($node.SelectNodes("v8:item", $ns))) {
		$lang = $it.SelectSingleNode("v8:lang", $ns)
		$content = $it.SelectSingleNode("v8:content", $ns)
		if ($lang -and $content -and $t.Contains($lang.InnerText) -and $t[$lang.InnerText] -eq '') {
			$t[$lang.InnerText] = (Restore-WSContent $content)
		}
	}
	return $t
}

# Авто-вывод заголовка из имени — ТОЧНОЕ зеркало Title-FromName из form-compile.
# Нужен, чтобы опускать ru-only заголовки, которые компилятор воспроизведёт сам.
function Title-FromName {
	param([string]$name)
	if (-not $name) { return '' }
	$s = [regex]::Replace($name, '([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', '$1 $2')
	$s = [regex]::Replace($s, '([а-яa-z0-9])([А-ЯA-Z])', '$1 $2')
	$parts = $s -split ' '
	if ($parts.Count -eq 0) { return $s }
	$out = New-Object System.Collections.ArrayList
	[void]$out.Add($parts[0])
	for ($i = 1; $i -lt $parts.Count; $i++) {
		$p = $parts[$i]
		if ($p.Length -gt 1 -and $p -ceq $p.ToUpper()) { [void]$out.Add($p) }
		else { [void]$out.Add($p.ToLower()) }
	}
	return ($out -join ' ')
}

# Детектор «настоящей» inline-разметки форматированного текста (идентичен form-compile!).
$script:fmtMarkupRe = '</>|<\s*(?:link|b|i|u|s|color|colorStyle|bgColor|bgColorStyle|font|fontSize|fontStyle|img)(?:\s|>)'
function Test-HasRealMarkup {
	param($text)
	if ($null -eq $text) { return $false }
	$vals = if ($text -is [System.Collections.IDictionary]) { @($text.Values) } else { @("$text") }
	foreach ($v in $vals) { if ("$v" -match $script:fmtMarkupRe) { return $true } }
	return $false
}
# Title-узел → DSL-значение ML-поля (гибрид): строка/мапа когда авто-детект formatted
# совпал с атрибутом; иначе явный {text, formatted}.
function Get-MLFormattedValue {
	param($titleNode)
	if (-not $titleNode) { return $null }
	# Get-LangTextWS восстанавливает значимый пробел в <v8:content> </v8:content> (одиночный и
	# мультиязычный случай) — иначе декорация-разделитель «Пробел» спуталась бы с суппресс-маркером "".
	$text = Get-LangTextWS $titleNode
	if ($null -eq $text) { return $null }
	$fmtAttr = ($titleNode.GetAttribute('formatted') -eq 'true')
	if ($fmtAttr -eq (Test-HasRealMarkup $text)) { return $text }
	$o = [ordered]@{}; $o['text'] = $text; $o['formatted'] = $fmtAttr; return $o
}

# Прочитать дочерний скаляр (по local-name, без namespace)
function Get-Child {
	param($node, [string]$name)
	$c = $node.SelectSingleNode("*[local-name()='$name']")
	if ($c) { return $c.InnerText } else { return $null }
}
function Has-Child { param($node, [string]$name) return $null -ne $node.SelectSingleNode("*[local-name()='$name']") }
function To-Bool { param([string]$v) return ($v -eq 'true') }

# Значение с учётом xsi:type → нативный JSON-тип (число/булево/строка).
# Нужно, чтобы авто-детект типа в компиляторе восстановил тот же xsi:type.
function Convert-TypedValue {
	param([string]$raw, [string]$xsiType)
	switch -regex ($xsiType) {
		'decimal$' {
			if ($raw -match '^-?\d+$') { return [int]$raw }
			return [double]::Parse($raw, [System.Globalization.CultureInfo]::InvariantCulture)
		}
		'boolean$' { return ($raw -eq 'true') }
		default { return $raw }
	}
}

# =====================================================================
# Захват настроек компоновщика динамического списка (ListSettings):
# filter / order / conditionalAppearance. Логика портирована из навыка
# skd-decompile (Build-FilterItem/Build-Order/Build-ConditionalAppearance
# и сериализаторы оформления). Механизм New-Sentinel/Add-Warning из skd
# заменён на запись в stderr + пропуск элемента (form-decompile — draft,
# скрипт не падает на непокрытых конструкциях).
# =====================================================================

# Прочитать дочерний скаляр по xpath (с $ns). Аналог skd Get-Text.
function Get-Text {
	param($node, [string]$xpath)
	if (-not $node) { return $null }
	if ([string]::IsNullOrEmpty($xpath)) { return $node.InnerText }
	$n = $node.SelectSingleNode($xpath, $ns)
	if ($n) { return $n.InnerText } else { return $null }
}

# Мультиязычный текст (LocalStringType) → string (ru) или ordered hash.
# Алиас на уже существующий Get-LangText (тот же контракт).
function Get-MLText { param($node) return (Get-LangText $node) }

# Презентация: либо мультиязычный LocalStringType, либо плоский xs:string.
# Get-MLText даёт $null для xs:string (нет v8:item) → откат к InnerText.
function Get-PresText {
	param($node)
	if (-not $node) { return $null }
	$ml = Get-MLText $node
	if ($null -ne $ml) { return $ml }
	if ($node.InnerText) { return $node.InnerText }
	return $null
}

# Presentation, сохраняющий ФОРМУ по xsi:type (не схлопывает ru-only LocalStringType в строку):
# <… xsi:type="v8:LocalStringType"> → объект {lang:text} (даже один ru), иначе xs:string → плоская строка.
# Нужно, чтобы компилятор воспроизвёл точный xsi:type (LocalStringType vs xs:string). Пустой LocalStringType → $null.
function Get-PresByType {
	param($node)
	if (-not $node) { return $null }
	$xt = $node.GetAttribute("type", $NS_XSI)
	if ($xt -match 'LocalStringType$') {
		$d = [ordered]@{}
		foreach ($it in $node.SelectNodes("v8:item", $ns)) {
			$lang = Get-Text $it "v8:lang"; $content = Get-Text $it "v8:content"
			if ($lang) { $d[$lang] = $content }
		}
		if ($d.Count -gt 0) { return $d }
		return $null
	}
	if ($node.InnerText) { return $node.InnerText }
	return $null
}

# Снять namespace-префикс с xsi:type ("dcsset:Foo" → "Foo")
function Get-LocalXsiType {
	param($node)
	if (-not $node) { return $null }
	$t = $node.GetAttribute("type", $NS_XSI)
	if ($t -match ':(.+)$') { return $matches[1] }
	return $t
}

# Шрифт оформления → объект {@type:Font, ...} (bit-perfect для compile).
function Get-FontValue {
	param($valNode)
	$f = [ordered]@{ '@type' = 'Font' }
	foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) {
		$a = $valNode.Attributes[$attrName]
		if ($null -ne $a) { $f[$attrName] = $a.Value }
	}
	return $f
}

# Линия (граница) оформления → объект {@type:Line, width, gap, style}.
function Get-LineValue {
	param($valNode)
	$obj = [ordered]@{ '@type' = 'Line' }
	$w = $valNode.GetAttribute("width")
	$g = $valNode.GetAttribute("gap")
	if ($w -ne '') { $obj['width'] = if ($w -match '^-?\d+$') { [int]$w } else { $w } }
	if ($g -ne '') { $obj['gap']  = ($g -eq 'true') }
	$styleNode = $valNode.SelectSingleNode("v8ui:style", $ns)
	if ($styleNode) { $obj['style'] = $styleNode.InnerText }
	return $obj
}

# Прочитать <dcscor:value> в JSON-значение: Font/Line/Field/multilang/raw text.
function Read-AppearanceValueNode {
	param($valNode)
	if (-not $valNode) { return $null }
	$vt = Get-LocalXsiType $valNode
	if ($vt -eq 'LocalStringType') {
		# НЕ схлопываем одноязычный в строку: значение параметра оформления различает
		# xs:string (плоская строка) и LocalStringType (локализуемый текст) — обе формы
		# одноязычно дают одну строку. Всегда объект-карта языков → компилятор эмитит LocalStringType.
		$map = [ordered]@{}
		foreach ($it in @($valNode.SelectNodes("v8:item", $ns))) {
			$lang = $it.SelectSingleNode("v8:lang", $ns); $content = $it.SelectSingleNode("v8:content", $ns)
			if ($lang) { $map[$lang.InnerText] = if ($content) { $content.InnerText } else { "" } }
		}
		return $map
	}
	if ($vt -eq 'Font') { return (Get-FontValue $valNode) }
	if ($vt -eq 'Line') { return (Get-LineValue $valNode) }
	# dcscor:Field — значение = ссылка на поле компоновки → объект {field:путь}
	if ($vt -eq 'Field') { return [ordered]@{ field = $valNode.InnerText } }
	return $valNode.InnerText
}

# Обратная карта comparisonType → короткий оператор фильтра (зеркало skd).
$script:filterOpMap = @{
	'Equal'='='; 'NotEqual'='<>'; 'Greater'='>'; 'GreaterOrEqual'='>=';
	'Less'='<'; 'LessOrEqual'='<='; 'InList'='in'; 'NotInList'='notIn';
	'InHierarchy'='inHierarchy'; 'InListByHierarchy'='inListByHierarchy';
	'Contains'='contains'; 'NotContains'='notContains';
	'BeginsWith'='beginsWith'; 'NotBeginsWith'='notBeginsWith';
	'Like'='like'; 'NotLike'='notLike';
	'Filled'='filled'; 'NotFilled'='notFilled'
}

# Render filter value node → shorthand-acceptable scalar string
function Get-FilterValue {
	param($valNode)
	if (-not $valNode) { return '_' }
	$nil = $valNode.GetAttribute("nil", $NS_XSI)
	if ($nil -eq 'true') { return '_' }
	$vType = Get-LocalXsiType $valNode
	if ($vType -eq 'DesignTimeValue') { return $valNode.InnerText }
	if ($vType -eq 'LocalStringType') { return (Get-MLText $valNode) }
	$txt = $valNode.InnerText
	if (-not $txt) { return '_' }
	return $txt
}

# Get-FilterValue + xsi:type значения (для valueType, например dcscor:Field).
function Get-FilterValueWithType {
	param($valNode)
	if (-not $valNode) { return @{ value = '_'; type = $null } }
	$rawType = $valNode.GetAttribute("type", $NS_XSI)
	$nil = $valNode.GetAttribute("nil", $NS_XSI)
	if ($nil -eq 'true') { return @{ value = '_'; type = $null } }
	$vType = Get-LocalXsiType $valNode
	if ($vType -eq 'LocalStringType') {
		return @{ value = (Get-MLText $valNode); type = $rawType }
	}
	# Стандартная дата начала/окончания. Формы (от самой компактной):
	#   SBD Custom+date → голая ISO-дата без valueType (компилятор выводит SBD Custom — дефолт
	#     даты в фильтре, корпус 268 vs 2 xs:dateTime); именованный вариант → строка + valueType;
	#   SED Custom / нетипичное → объект {variant, date} + valueType. Иначе InnerText склеивал.
	if ($vType -eq 'StandardBeginningDate' -or $vType -eq 'StandardEndDate') {
		$variantN = $valNode.SelectSingleNode("v8:variant", $ns)
		$dateN = $valNode.SelectSingleNode("v8:date", $ns)
		$variantStr = if ($variantN) { $variantN.InnerText } else { '' }
		if ($dateN) {
			if ($vType -eq 'StandardBeginningDate' -and $variantStr -eq 'Custom') {
				return @{ value = $dateN.InnerText; type = $null }   # голая дата = SBD Custom шорткат
			}
			return @{ value = [ordered]@{ variant = $variantStr; date = $dateN.InnerText }; type = $rawType }
		}
		return @{ value = $variantStr; type = $rawType }   # именованный вариант
	}
	$txt = $valNode.InnerText
	if (-not $txt) { return @{ value = '_'; type = $rawType } }
	if ($vType -eq 'boolean') { return @{ value = ($txt -eq 'true'); type = $rawType } }
	if ($vType -eq 'decimal') {
		if ($txt -match '^-?\d+$') { return @{ value = [int]$txt; type = $rawType } }
		return @{ value = [double]$txt; type = $rawType }
	}
	return @{ value = $txt; type = $rawType }
}

# Convert filter item node → shorthand string или object form (рекурсивно для групп).
function Build-FilterItem {
	param($itemNode, [string]$loc)
	$xtype = Get-LocalXsiType $itemNode
	if ($xtype -eq 'FilterItemGroup') {
		$gt = Get-Text $itemNode "dcsset:groupType"
		$groupName = switch ($gt) { 'OrGroup' { 'Or' } 'NotGroup' { 'Not' } default { 'And' } }
		$items = @()
		foreach ($c in $itemNode.SelectNodes("dcsset:item", $ns)) {
			$bi = (Build-FilterItem -itemNode $c -loc "$loc/item")
			if ($null -ne $bi) { $items += $bi }
		}
		$gObj = [ordered]@{ group = $groupName; items = $items }
		if ((Get-Text $itemNode "dcsset:use") -eq 'false') { $gObj['use'] = $false }   # группа отключена (@off)
		$gPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns)
		if ($gPresNode) {
			# Сохраняем форму по xsi:type (LocalStringType ru-only ≠ xs:string)
			$gPres = Get-PresByType $gPresNode
			if ($null -ne $gPres -and $gPres -ne '') { $gObj['presentation'] = $gPres }
		}
		$gVMNode = $itemNode.SelectSingleNode("dcsset:viewMode", $ns)
		if ($gVMNode) { $gObj['viewMode'] = $gVMNode.InnerText }
		$gUSID = Get-Text $itemNode "dcsset:userSettingID"
		if ($gUSID) { $gObj['userSettingID'] = 'auto' }
		$gUSPN = $itemNode.SelectSingleNode("dcsset:userSettingPresentation", $ns)
		if ($gUSPN) {
			$gUSP = Get-PresText $gUSPN
			if ($gUSP) { $gObj['userSettingPresentation'] = $gUSP }
		}
		return $gObj
	}
	if ($xtype -ne 'FilterItemComparison') {
		[Console]::Error.WriteLine("form-decompile: пропущен фильтр неизвестного типа '$xtype' (path: $loc)")
		return $null
	}
	$leftNode = $itemNode.SelectSingleNode("dcsset:left", $ns)
	$field = if ($leftNode) { $leftNode.InnerText } else { $null }
	$ct = Get-Text $itemNode "dcsset:comparisonType"
	$op = $script:filterOpMap[$ct]
	if (-not $op) { $op = $ct }

	$rightNodes = @($itemNode.SelectNodes("dcsset:right", $ns))
	$value = $null
	$valueIsArrayFlag = $false
	$valueTypeAttr = $null
	if ($rightNodes.Count -eq 1) {
		$rn = $rightNodes[0]
		if ((Get-LocalXsiType $rn) -eq 'ValueListType') {
			$value = @()
			$valueIsArrayFlag = $true
		} else {
			$vt = Get-FilterValueWithType $rn
			$value = $vt.value
			$autoDetectsDTV = ($vt.type -eq 'dcscor:DesignTimeValue') -and `
				("$($vt.value)" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.')
			if ($vt.type -and $vt.type -notmatch '^xs:' -and -not $autoDetectsDTV) {
				$valueTypeAttr = $vt.type
			} elseif ($vt.type -eq 'xs:string' -and ($value -is [string]) -and ($value -match '^(-?\d+(\.\d+)?|\d{4}-\d{2}-\d{2}T)')) {
				# Значение-строка "1"/"2020-..." с xsi:type="xs:string": компилятор авто-детектит число/дату
				# (теряет xs:string). Когда авто-вывод дал бы ДРУГОЙ тип — фиксируем явный valueType.
				$valueTypeAttr = 'xs:string'
			}
		}
	} elseif ($rightNodes.Count -gt 1) {
		$arr = @()
		$rawTypes = @()
		foreach ($rn in $rightNodes) {
			$arr += (Get-FilterValue $rn)
			$rawTypes += $rn.GetAttribute("type", $NS_XSI)
		}
		$value = $arr
		$valueIsArrayFlag = $true
		$uniqTypes = @($rawTypes | Sort-Object -Unique)
		if ($uniqTypes.Count -eq 1 -and $uniqTypes[0]) {
			$autoDetectsDTV = ($uniqTypes[0] -eq 'dcscor:DesignTimeValue') -and `
				($arr.Count -gt 0) -and `
				(@($arr | Where-Object { "$_" -notmatch '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.' }).Count -eq 0)
			if (-not $autoDetectsDTV) {
				$valueTypeAttr = $uniqTypes[0]
			}
		}
	}

	$use = Get-Text $itemNode "dcsset:use"
	$userId = Get-Text $itemNode "dcsset:userSettingID"
	$vmNode = $itemNode.SelectSingleNode("dcsset:viewMode", $ns)
	$viewMode = if ($vmNode) { $vmNode.InnerText } else { $null }
	$userPresNode = $itemNode.SelectSingleNode("dcsset:userSettingPresentation", $ns)
	$fiPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns)
	$fiPres = $null
	if ($fiPresNode) {
		# Сохраняем форму по xsi:type (LocalStringType ru-only ≠ xs:string)
		$fiPres = Get-PresByType $fiPresNode
	}

	$flags = @()
	if ($use -eq 'false') { $flags += '@off' }
	if ($userId) { $flags += '@user' }
	if ($viewMode -eq 'QuickAccess') { $flags += '@quickAccess' }
	elseif ($viewMode -eq 'Inaccessible') { $flags += '@inaccessible' }
	elseif ($viewMode -eq 'Normal') { $flags += '@normal' }

	$noValueOps = @('filled','notFilled')

	# Пустой xs:string right (<dcsset:right xsi:type="xs:string"/>) ≠ отсутствие <right>:
	# Get-FilterValueWithType маппит наличие пустого/nil right в value='_' (отсутствие → $null).
	# Shorthand `_` неоднозначен (схлопывает оба, компилятор → нет right). Пробельные значения рвут
	# shorthand-парсинг (split по пробелам). → форсим объектную форму (value='_' → self-closing;
	# пробелы — как есть в <right>).
	$valNeedsObj = $false
	if ($rightNodes.Count -eq 1 -and -not $valueIsArrayFlag -and $op -notin $noValueOps) {
		if ("$value" -eq '_') {
			# Truly-empty <right xsi:type="xs:string"/> ИЛИ whitespace-only <right>   </right>
			# (PreserveWhitespace=false стрипнул пробелы в '' → Get-FilterValueWithType вернул '_').
			# Восстанавливаем реальные пробелы из WS-дока (как у whitespace-заголовков).
			$ws = Resolve-WS $rightNodes[0]
			if ($ws -and $ws.Length -gt 0 -and $ws.Trim() -eq '') { $value = $ws }
			$valNeedsObj = $true
		}
		elseif ($null -ne $value -and "$value" -match '\s') { $valNeedsObj = $true }
	}

	if ($userPresNode -or $valueIsArrayFlag -or $valueTypeAttr -or $fiPres -or $valNeedsObj) {
		$obj = [ordered]@{ field = $field; op = $op }
		if ($op -notin $noValueOps -and $null -ne $value) {
			if ($valueIsArrayFlag) {
				$arrAsList = New-Object System.Collections.ArrayList
				foreach ($vv in @($value)) { [void]$arrAsList.Add($vv) }
				$obj['value'] = $arrAsList
			} else {
				$obj['value'] = $value
			}
		}
		if ($valueTypeAttr) { $obj['valueType'] = $valueTypeAttr }
		if ($use -eq 'false') { $obj['use'] = $false }
		if ($userId) { $obj['userSettingID'] = 'auto' }
		if ($fiPres) { $obj['presentation'] = $fiPres }
		if ($viewMode) { $obj['viewMode'] = $viewMode }
		if ($userPresNode) { $obj['userSettingPresentation'] = Get-PresText $userPresNode }
		return $obj
	}

	$s = $field
	if ($op -in $noValueOps) {
		$s += " $op"
	} else {
		$vDisplay = '_'
		if ($null -ne $value) {
			if ($value -is [bool]) { $vDisplay = if ($value) { 'true' } else { 'false' } }
			elseif ("$value" -ne '') { $vDisplay = "$value" }
		}
		$s += " $op $vDisplay"
	}
	if ($flags) { $s += ' ' + ($flags -join ' ') }
	return $s
}

# Рекурсивный хелпер одного элемента selection (для conditionalAppearance).
function Build-SelectionItem {
	param($item, [string]$loc)
	$xt = Get-LocalXsiType $item
	if (-not $xt) {
		$fName = Get-Text $item "dcsset:field"
		if ($fName) { return $fName }
		$fieldEl = $item.SelectSingleNode("dcsset:field", $ns)
		if ($fieldEl) { return 'Auto' }
	}
	switch ($xt) {
		'SelectedItemAuto' {
			$useV = Get-Text $item "dcsset:use"
			if ($useV -eq 'false') {
				return [ordered]@{ auto = $true; use = $false }
			}
			return 'Auto'
		}
		'SelectedItemField' {
			$fName = Get-Text $item "dcsset:field"
			$titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns)
			$title = Get-MLText $titleNode
			$vmN = $item.SelectSingleNode("dcsset:viewMode", $ns)
			$useV = Get-Text $item "dcsset:use"
			$useFalse = ($useV -eq 'false')
			if ($title -or $vmN -or $useFalse) {
				$obj = [ordered]@{ field = $fName }
				if ($useFalse) { $obj['use'] = $false }
				if ($title) { $obj['title'] = $title }
				if ($vmN) { $obj['viewMode'] = $vmN.InnerText }
				return $obj
			}
			return $fName
		}
		'SelectedItemFolder' {
			$titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns)
			$folderTitle = Get-MLText $titleNode
			$inner = @()
			foreach ($sub in $item.SelectNodes("dcsset:item", $ns)) {
				$bi = (Build-SelectionItem -item $sub -loc "$loc/folder")
				if ($null -ne $bi) { $inner += $bi }
			}
			$entry = [ordered]@{ folder = $folderTitle; items = $inner }
			$folderField = Get-Text $item "dcsset:field"
			if ($folderField) { $entry['field'] = $folderField }
			$plN = $item.SelectSingleNode("dcsset:placement", $ns)
			if ($plN -and $plN.InnerText -and $plN.InnerText -ne 'Auto') {
				$entry['placement'] = $plN.InnerText
			}
			return $entry
		}
		default {
			[Console]::Error.WriteLine("form-decompile: пропущен элемент selection неизвестного типа '$xt' (path: $loc)")
			return $null
		}
	}
}

# Build selection items array (для conditionalAppearance).
function Build-Selection {
	param($selNode, [string]$loc)
	if (-not $selNode) { return @() }
	$out = @()
	foreach ($it in $selNode.SelectNodes("dcsset:item", $ns)) {
		$bi = (Build-SelectionItem -item $it -loc $loc)
		if ($null -ne $bi) { $out += $bi }
	}
	return ,$out
}

# Build order items array.
function Build-Order {
	param($ordNode, [string]$loc)
	if (-not $ordNode) { return @() }
	$out = @()
	foreach ($it in $ordNode.SelectNodes("dcsset:item", $ns)) {
		$xt = Get-LocalXsiType $it
		switch ($xt) {
			'OrderItemAuto'  { $out += 'Auto' }
			'OrderItemField' {
				$fn = Get-Text $it "dcsset:field"
				$ot = Get-Text $it "dcsset:orderType"
				$vmN = $it.SelectSingleNode("dcsset:viewMode", $ns)
				$useV = Get-Text $it "dcsset:use"
				$useFalse = ($useV -eq 'false')
				if ($vmN -or $useFalse) {
					$obj = [ordered]@{ field = $fn }
					if ($useFalse) { $obj['use'] = $false }
					if ($ot -eq 'Desc') { $obj['direction'] = 'desc' }
					if ($vmN) { $obj['viewMode'] = $vmN.InnerText }
					$out += $obj
				} else {
					if ($ot -eq 'Desc') { $out += "$fn desc" } else { $out += $fn }
				}
			}
			default {
				[Console]::Error.WriteLine("form-decompile: пропущен элемент сортировки неизвестного типа '$xt' (path: $loc)")
			}
		}
	}
	return ,$out
}

# Build appearance dict из <dcsset:appearance> (Line/Font/multilang/nested items).
function Get-SettingsAppearance {
	param($appNode)
	if (-not $appNode) { return $null }
	$dict = [ordered]@{}
	foreach ($it in $appNode.SelectNodes("dcscor:item", $ns)) {
		$pName = Get-Text $it "dcscor:parameter"
		$val = $it.SelectSingleNode("dcscor:value", $ns)
		if (-not $pName -or -not $val) { continue }
		$rawVal = Read-AppearanceValueNode $val
		$useV = Get-Text $it "dcscor:use"
		$nestedItems = [ordered]@{}
		foreach ($sub in $it.SelectNodes("dcscor:item", $ns)) {
			$subName = Get-Text $sub "dcscor:parameter"
			$subVal = $sub.SelectSingleNode("dcscor:value", $ns)
			if (-not $subName) { continue }
			$subRaw = Read-AppearanceValueNode $subVal
			$subUse = Get-Text $sub "dcscor:use"
			$subEntry = [ordered]@{ value = $subRaw }
			if ($subUse -eq 'false') { $subEntry['use'] = $false }
			$nestedItems[$subName] = $subEntry
		}
		$valIsLine = ($rawVal -is [System.Collections.IDictionary]) -and $rawVal.Contains('@type') -and ($rawVal['@type'] -eq 'Line')
		if ($valIsLine) {
			if ($useV -eq 'false') { $rawVal['use'] = $false }
			if ($nestedItems.Count -gt 0) { $rawVal['items'] = $nestedItems }
			$dict[$pName] = $rawVal
		} elseif (($useV -eq 'false') -or ($nestedItems.Count -gt 0)) {
			$wrap = [ordered]@{ value = $rawVal }
			if ($useV -eq 'false') { $wrap['use'] = $false }
			if ($nestedItems.Count -gt 0) { $wrap['items'] = $nestedItems }
			$dict[$pName] = $wrap
		} else {
			$dict[$pName] = $rawVal
		}
	}
	return $dict
}

# Build conditionalAppearance array.
function Build-ConditionalAppearance {
	param($caNode, [string]$loc)
	if (-not $caNode) { return @() }
	$out = @()
	$i = 0
	foreach ($it in $caNode.SelectNodes("dcsset:item", $ns)) {
		$entry = [ordered]@{}
		$scopeNode = $it.SelectSingleNode("dcsset:scope", $ns)
		if ($scopeNode -and $scopeNode.HasChildNodes) {
			[Console]::Error.WriteLine("form-decompile: conditionalAppearance item имеет scope — не воспроизводится в DSL (path: $loc/$i/scope)")
		}
		$selNode = $it.SelectSingleNode("dcsset:selection", $ns)
		if ($selNode -and $selNode.SelectNodes("dcsset:item", $ns).Count -gt 0) {
			$entry['selection'] = Build-Selection -selNode $selNode -loc "$loc/$i/selection"
		}
		$filterNode = $it.SelectSingleNode("dcsset:filter", $ns)
		if ($filterNode -and $filterNode.SelectNodes("dcsset:item", $ns).Count -gt 0) {
			$f = @()
			foreach ($fc in $filterNode.SelectNodes("dcsset:item", $ns)) {
				$bi = (Build-FilterItem -itemNode $fc -loc "$loc/$i/filter")
				if ($null -ne $bi) { $f += $bi }
			}
			$entry['filter'] = $f
		}
		$appNode = $it.SelectSingleNode("dcsset:appearance", $ns)
		$ap = Get-SettingsAppearance $appNode
		if ($ap -and $ap.Count -gt 0) { $entry['appearance'] = $ap }
		$presNode = $it.SelectSingleNode("dcsset:presentation", $ns)
		if ($presNode) {
			# Сохраняем форму по xsi:type: LocalStringType (даже ru-only) → объект → компилятор эмитит LocalStringType.
			$pres = Get-PresByType $presNode
			if ($null -ne $pres -and $pres -ne '') { $entry['presentation'] = $pres }
		}
		$vmN = $it.SelectSingleNode("dcsset:viewMode", $ns)
		if ($vmN) { $entry['viewMode'] = $vmN.InnerText }
		$usid = Get-Text $it "dcsset:userSettingID"
		if ($usid) { $entry['userSettingID'] = 'auto' }
		$uspN = $it.SelectSingleNode("dcsset:userSettingPresentation", $ns)
		if ($uspN) {
			$usp = Get-PresText $uspN
			if ($usp) { $entry['userSettingPresentation'] = $usp }
		}
		$useV = Get-Text $it "dcsset:use"
		if ($useV -eq 'false') { $entry['use'] = $false }
		$useInDontUse = @()
		foreach ($ch in $it.ChildNodes) {
			if ($ch.NodeType -ne 'Element' -or $ch.NamespaceURI -ne $NS_DCSSET) { continue }
			if ($ch.LocalName -match '^useIn(.+)$' -and $ch.InnerText -eq 'DontUse') {
				$shortName = ($matches[1]).Substring(0, 1).ToLower() + ($matches[1]).Substring(1)
				$useInDontUse += $shortName
			}
		}
		if ($useInDontUse.Count -gt 0) { $entry['useInDontUse'] = $useInDontUse }
		$out += $entry
		$i++
	}
	return ,$out
}

# Общие layout-свойства → в $obj (симметрично Emit-Layout компилятора).
# Вызывается один раз для любого элемента. Height тут — высота элемента (<Height>),
# в т.ч. у Table; высоту в строках (<HeightInTableRows>) Table ловит отдельно в heightInTableRows.
function Add-Layout {
	param($obj, $node)
	# Общие свойства элемента (любой тип): default/drag/skip
	if ((Get-Child $node 'DefaultItem') -eq 'true') { $obj['defaultItem'] = $true }
	$soi = Get-Child $node 'SkipOnInput'; if ($null -ne $soi) { $obj['skipOnInput'] = ($soi -eq 'true') }
	# EnableStartDrag/EnableDrag — фактическое значение (платформа эмитит и явный false, напр. SpreadSheet)
	$esd = Get-Child $node 'EnableStartDrag'; if ($null -ne $esd) { $obj['enableStartDrag'] = ($esd -eq 'true') }
	$edr = Get-Child $node 'EnableDrag'; if ($null -ne $edr) { $obj['enableDrag'] = ($edr -eq 'true') }
	$fdm = Get-Child $node 'FileDragMode'; if ($fdm) { $obj['fileDragMode'] = $fdm }
	# AutoMaxWidth: компилятор додумывает false для multiLine-input без явного ключа (multiLineDefault).
	# Захват факт. значения; multiLine-input без тега → autoMaxWidth:true (суппресс эвристики).
	$amwNode = Get-Child $node 'AutoMaxWidth'
	if ($amwNode -eq 'false') { $obj['autoMaxWidth'] = $false }
	elseif ($amwNode -eq 'true') { $obj['autoMaxWidth'] = $true }
	elseif ((Get-Child $node 'MultiLine') -eq 'true') { $obj['autoMaxWidth'] = $true }
	$mw = Get-Child $node 'MaxWidth'; if ($mw) { $obj['maxWidth'] = [int]$mw }
	if ((Get-Child $node 'AutoMaxHeight') -eq 'false') { $obj['autoMaxHeight'] = $false }
	$mh = Get-Child $node 'MaxHeight'; if ($mh) { $obj['maxHeight'] = [int]$mh }
	$w = Get-Child $node 'Width'; if ($w) { $obj['width'] = [int]$w }
	$h = Get-Child $node 'Height'; if ($h) { $obj['height'] = [int]$h }
	# Stretch: захват фактического значения (true И false — платформа эмитит явное)
	$hs = Get-Child $node 'HorizontalStretch'; if ($null -ne $hs) { $obj['horizontalStretch'] = ($hs -eq 'true') }
	$vs = Get-Child $node 'VerticalStretch'; if ($null -ne $vs) { $obj['verticalStretch'] = ($vs -eq 'true') }
	$gha = Get-Child $node 'GroupHorizontalAlign'; if ($gha) { $obj['groupHorizontalAlign'] = $gha }
	$gva = Get-Child $node 'GroupVerticalAlign'; if ($gva) { $obj['groupVerticalAlign'] = $gva }
	$ha = Get-Child $node 'HorizontalAlign'; if ($ha) { $obj['horizontalAlign'] = $ha }
	# Cell-свойства поля в таблице (общие для Input/Label/Picture/CheckBox): захват «как есть»
	foreach ($p in @('ShowInHeader','ShowInFooter','AutoCellHeight')) {
		$v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = ($v -eq 'true') }
	}
	$fha = Get-Child $node 'FooterHorizontalAlign'; if ($fha) { $obj['footerHorizontalAlign'] = $fha }
	$hha = Get-Child $node 'HeaderHorizontalAlign'; if ($hha) { $obj['headerHorizontalAlign'] = $hha }
	# ColumnGroup: динамический заголовок из данных + формат заголовка (ML-текст)
	$hdp = Get-Child $node 'HeaderDataPath'; if ($hdp) { $obj['headerDataPath'] = $hdp }
	$hfNode = $node.SelectSingleNode("lf:HeaderFormat", $ns); if ($hfNode) { $hf = Get-LangText $hfNode; if ($null -ne $hf) { $obj['headerFormat'] = $hf } }
}

# TitleLocation у check/radio (зеркало Emit-TitleLocation):
#   тега нет → "" (дефолт платформы); значение = умный дефолт → опускаем; иначе пишем.
function Add-TitleLocation {
	param($obj, $node, [string]$smartDefault)
	$tl = Get-Child $node 'TitleLocation'
	if ($null -eq $tl) { $obj['titleLocation'] = '' }
	elseif ($tl -ne $smartDefault) { $obj['titleLocation'] = $tl.ToLower() }
}

# Разобрать <Events> элемента → упорядоченная мапа { ИмяСобытия: ИмяОбработчика }
# в порядке документа. Имена обработчиков всегда явные (как у событий формы) —
# единый, консистентный с form-level формат. Legacy on/handlers больше не эмитим.
function Get-Events {
	param($node, [string]$elName)
	$ev = $node.SelectSingleNode("lf:Events", $ns)
	if (-not $ev) { return $null }
	$events = [ordered]@{}
	foreach ($e in @($ev.SelectNodes("lf:Event", $ns))) {
		$events[$e.GetAttribute("name")] = $e.InnerText
	}
	if ($events.Count -eq 0) { return $null }
	return $events
}

# Инверсия Emit-XrFlag: role-adjustable boolean (UserVisible/View/Edit/Use).
# <TAG><xr:Common/>[<xr:Value name="Role.X"/>…]</TAG> → скаляр bool (без ролей) или объект { common, roles:{Имя:bool} }.
# Имя роли отдаём без префикса "Role.". Возвращает $null, если тег отсутствует.
function Decompile-XrFlag {
	param($node, [string]$tag)
	$el = $node.SelectSingleNode("*[local-name()='$tag']")
	if (-not $el) { return $null }
	$commonNode = $el.SelectSingleNode("*[local-name()='Common']")
	$common = ($commonNode -and $commonNode.InnerText -eq 'true')
	$valNodes = @($el.SelectNodes("*[local-name()='Value']"))
	if ($valNodes.Count -eq 0) { return $common }
	$roles = [ordered]@{}
	foreach ($v in $valNodes) {
		$rn = $v.GetAttribute("name")
		if ($rn -match '^Role\.') { $rn = $rn.Substring(5) }
		$roles[$rn] = ($v.InnerText -eq 'true')
	}
	$o = [ordered]@{}
	$o['common'] = $common
	$o['roles'] = $roles
	return $o
}

# Командный интерфейс формы (<CommandInterface>): панели CommandBar + NavigationPanel,
# каждая — список переопределений команд (платформа эмитит ТОЛЬКО отклонения от авто-расстановки).
# Элемент: command (verbatim, "0"=пустой) + type (Auto опускаем) + attribute/group(CommandGroup)/index +
# defaultVisible(bool) + visible(xr-flag bool/{common,roles} — тот же механизм, что userVisible).
# Голый элемент (только команда, Type=Auto) → строковый shorthand.
function Decompile-CommandInterface {
	$ciNode = $root.SelectSingleNode("lf:CommandInterface", $ns)
	if (-not $ciNode) { return $null }
	$ci = [ordered]@{}
	foreach ($panel in @(@('CommandBar','commandBar'), @('NavigationPanel','navigationPanel'))) {
		$pn = $ciNode.SelectSingleNode("lf:$($panel[0])", $ns)
		if (-not $pn) { continue }
		$items = New-Object System.Collections.ArrayList
		foreach ($it in @($pn.SelectNodes("lf:Item", $ns))) {
			$o = [ordered]@{}
			$cmd = Get-Child $it 'Command'
			$o['command'] = "$cmd"
			$ty = Get-Child $it 'Type'; if ($ty -and $ty -ne 'Auto') { $o['type'] = $ty }
			$at = Get-Child $it 'Attribute'; if ($at) { $o['attribute'] = $at }
			$cg = Get-Child $it 'CommandGroup'; if ($cg) { $o['group'] = $cg }
			$idx = Get-Child $it 'Index'; if ($null -ne $idx) { $o['index'] = [int]$idx }
			$dv = Get-Child $it 'DefaultVisible'; if ($null -ne $dv) { $o['defaultVisible'] = ($dv -eq 'true') }
			$vis = Decompile-XrFlag $it 'Visible'; if ($null -ne $vis) { $o['visible'] = $vis }
			# Голый элемент (только command) → строка-shorthand; иначе объект
			if ($o.Count -eq 1) { [void]$items.Add("$cmd") } else { [void]$items.Add($o) }
		}
		if ($items.Count -gt 0) { $ci[$panel[1]] = @($items) }
	}
	if ($ci.Count -gt 0) { return $ci }
	return $null
}

# <FunctionalOptions><Item>FunctionalOption.X</Item>…> → массив строк (префикс FunctionalOption. снят; GUID — как есть).
function Decompile-FunctionalOptions {
	param($node)
	$foNode = $node.SelectSingleNode("lf:FunctionalOptions", $ns)
	if (-not $foNode) { return $null }
	$opts = New-Object System.Collections.ArrayList
	foreach ($it in @($foNode.SelectNodes("lf:Item", $ns))) {
		$t = $it.InnerText.Trim() -replace '^FunctionalOption\.', ''
		[void]$opts.Add($t)
	}
	if ($opts.Count -gt 0) { return ,@($opts) }
	return $null
}

# Колонка реквизита (прямая или внутри AdditionalColumns): name/type/title/functionalOptions.
function Decompile-AttrColumn {
	param($c)
	$co = [ordered]@{}; $co['name'] = $c.GetAttribute("name")
	$cty = Decompile-Type ($c.SelectSingleNode("lf:Type", $ns)); if ($cty) { $co['type'] = $cty }
	$ctNode = $c.SelectSingleNode("lf:Title", $ns); if ($ctNode) { $t = Get-LangTextWS $ctNode; if ($null -ne $t) { $co['title'] = $t } }
	$cfc = Get-Child $c 'FillCheck'; if ($cfc) { $co['fillCheck'] = $cfc }   # проверка заполнения колонки (как у реквизита)
	$cfo = Decompile-FunctionalOptions $c; if ($cfo) { $co['functionalOptions'] = $cfo }
	# Ролевой доступ колонки (View/Edit) — xr-флаг, как у самого реквизита (bool | {common,roles})
	$cv = Decompile-XrFlag $c 'View'; if ($null -ne $cv) { $co['view'] = $cv }
	$ce = Decompile-XrFlag $c 'Edit'; if ($null -ne $ce) { $co['edit'] = $ce }
	return $co
}

# Общие свойства элемента (visible/enabled/readonly/title/events) → в hash
# Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/ValuesPicture/Page Picture).
# Платформа ВСЕГДА эмитит <xr:LoadTransparent> (и true, и false) → дефолт DSL = false.
# Источник: <xr:Ref> (именованная/стилевая) ИЛИ <xr:Abs> (встроенная → префикс "abs:").
# Скаляр (src) при loadTransparent=false и без TransparentPixel; иначе объект
# {src, loadTransparent?, transparentPixel?}.
function Get-PictureRef {
	param($node, [string]$picTag)
	$ref = $node.SelectSingleNode("lf:$picTag/xr:Ref", $ns)
	$abs = $node.SelectSingleNode("lf:$picTag/xr:Abs", $ns)
	if (-not $ref -and -not $abs) { return $null }
	$src = if ($ref) { $ref.InnerText } else { "abs:$($abs.InnerText)" }
	$lt = $node.SelectSingleNode("lf:$picTag/xr:LoadTransparent", $ns)
	$ltTrue = ($lt -and $lt.InnerText -eq 'true')
	$tpx = $node.SelectSingleNode("lf:$picTag/xr:TransparentPixel", $ns)
	if (-not $ltTrue -and -not $tpx) { return $src }
	$o = [ordered]@{ src = $src }
	if ($ltTrue) { $o['loadTransparent'] = $true }
	if ($tpx) { $o['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } }
	return $o
}

# <Picture> кнопки/попапа/команды. Дефолт LoadTransparent=true (обратная конвенция к header/values):
# фиксируем только отклонение false. Источник <xr:Ref> или <xr:Abs> (→ "abs:"). При наличии
# <xr:TransparentPixel> → объектная форма {src, loadTransparent?, transparentPixel}, иначе скаляр picture
# + отдельный loadTransparent:false.
function Set-CommandPicture {
	param($obj, $node)
	$ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns)
	$abs = $node.SelectSingleNode("lf:Picture/xr:Abs", $ns)
	if (-not $ref -and -not $abs) { return }
	$src = if ($ref) { $ref.InnerText } else { "abs:$($abs.InnerText)" }
	$lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns)
	$ltFalse = ($lt -and $lt.InnerText -eq 'false')
	$tpx = $node.SelectSingleNode("lf:Picture/xr:TransparentPixel", $ns)
	if ($tpx) {
		$o = [ordered]@{ src = $src }
		if ($ltFalse) { $o['loadTransparent'] = $false }
		$o['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') }
		$obj['picture'] = $o
	} else {
		$obj['picture'] = $src
		if ($ltFalse) { $obj['loadTransparent'] = $false }
	}
}

# Шрифт <Font ...> → строка-ref (если только ref+kind=StyleItem) или объект-атрибуты.
function Build-FontValue {
	param($f)
	$present = @()
	foreach ($a in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) {
		if ($f.HasAttribute($a)) { $present += $a }
	}
	# Чистый style-ref (ref + kind=StyleItem) → строка-шорткат
	if ($present.Count -eq 2 -and ($present -contains 'ref') -and $f.GetAttribute('kind') -eq 'StyleItem') {
		return $f.GetAttribute('ref')
	}
	$o = [ordered]@{}
	foreach ($k in $present) {
		$v = $f.GetAttribute($k)
		if ($k -in @('height','scale') -and $v -match '^-?\d+$') { $o[$k] = [int]$v }
		elseif ($k -in @('bold','italic','underline','strikeout')) { $o[$k] = ($v -eq 'true') }
		else { $o[$k] = $v }
	}
	return $o
}

# Граница <Border> → строка-ref (из стиля) или объект {width, style}.
function Build-BorderValue {
	param($b)
	if ($b.HasAttribute('ref')) { return $b.GetAttribute('ref') }
	$o = [ordered]@{}
	if ($b.HasAttribute('width')) { $w = $b.GetAttribute('width'); $o['width'] = if ($w -match '^-?\d+$') { [int]$w } else { $w } }
	$st = $b.SelectSingleNode("v8ui:style", $ns)
	if ($st) { $o['style'] = $st.InnerText }
	return $o
}

# Оформление элемента (цвета/шрифты/граница) → canonical DSL-ключи. Цвет — verbatim-строка.
function Add-Appearance {
	param($obj, $node)
	$colorMap = @{
		'TitleTextColor'='titleTextColor'; 'TitleBackColor'='titleBackColor'
		'FooterTextColor'='footerTextColor'; 'FooterBackColor'='footerBackColor'
		'TextColor'='textColor'; 'BackColor'='backColor'; 'BorderColor'='borderColor'
	}
	foreach ($tag in $colorMap.Keys) {
		$c = $node.SelectSingleNode("lf:$tag", $ns)
		if ($c) { $obj[$colorMap[$tag]] = $c.InnerText }
	}
	foreach ($pair in @(@('Font','font'), @('TitleFont','titleFont'), @('FooterFont','footerFont'))) {
		$f = $node.SelectSingleNode("lf:$($pair[0])", $ns)
		if ($f) { $obj[$pair[1]] = (Build-FontValue $f) }
	}
	$b = $node.SelectSingleNode("lf:Border", $ns)
	if ($b) { $obj['border'] = (Build-BorderValue $b) }
}

function Add-CommonProps {
	param($obj, $node, [string]$elName)
	Add-Appearance $obj $node
	if ((Get-Child $node 'Visible') -eq 'false') { $obj['hidden'] = $true }
	if ((Get-Child $node 'Enabled') -eq 'false') { $obj['disabled'] = $true }
	if ((Get-Child $node 'ReadOnly') -eq 'true') { $obj['readOnly'] = $true }
	$uv = Decompile-XrFlag $node 'UserVisible'; if ($null -ne $uv) { $obj['userVisible'] = $uv }
	$titleNode = $node.SelectSingleNode("lf:Title", $ns)
	if ($titleNode) {
		$t = Get-LangTextWS $titleNode   # восстановление значимого пробела (whitespace-заголовок)
		if ($null -ne $t) { $obj['title'] = $t }
		# formatted у LabelDecoration выводится компилятором из hyperlink — отдельный ключ не нужен (#16 хвост)
	}
	$ttNode = $node.SelectSingleNode("lf:ToolTip", $ns)
	if ($ttNode) { $tt = Get-LangTextWS $ttNode; if ($null -ne $tt) { $obj['tooltip'] = $tt } }
	$ttr = Get-Child $node 'ToolTipRepresentation'; if ($ttr) { $obj['tooltipRepresentation'] = $ttr }
	# Картинки заголовка/подвала колонки (любой field-тип, эмитятся платформой как column header/footer icon)
	$hp = Get-PictureRef $node 'HeaderPicture'; if ($null -ne $hp) { $obj['headerPicture'] = $hp }
	$fp = Get-PictureRef $node 'FooterPicture'; if ($null -ne $fp) { $obj['footerPicture'] = $fp }
	$ev = Get-Events $node $elName
	if ($ev) { $obj['events'] = $ev }
	# CommandSet — общий для полей (input/label/check/spreadsheet/html/formatted/picture):
	# список отключённых команд редактора. Только <ExcludedCommand>, пустого не бывает.
	$csNode = $node.SelectSingleNode("lf:CommandSet", $ns)
	if ($csNode) {
		$exc = New-Object System.Collections.ArrayList
		foreach ($ec in @($csNode.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$exc.Add($ec.InnerText) }
		if ($exc.Count -gt 0) { $obj['excludedCommands'] = @($exc) }
	}
}

# --- 3. Type decompile (inverse of Emit-Type) ---
function Decompile-Type {
	param($typeNode)
	if (-not $typeNode) { return $null }
	$parts = New-Object System.Collections.ArrayList
	foreach ($vt in @($typeNode.SelectNodes("v8:Type", $ns))) {
		$raw = $vt.InnerText.Trim()
		$short = $raw
		# break обязателен: иначе общий case ^(v8|v8ui|cfg): перетирает специфичные (напр. v8:ValueListType → ValueList).
		switch -regex ($raw) {
			'^xs:string$' {
				$len = $typeNode.SelectSingleNode("v8:StringQualifiers/v8:Length", $ns)
				$al = $typeNode.SelectSingleNode("v8:StringQualifiers/v8:AllowedLength", $ns)
				$fixed = ($al -and $al.InnerText -eq 'Fixed')   # Variable = дефолт (опускаем); Fixed — явно
				if ($len -and [int]$len.InnerText -gt 0) {
					$short = if ($fixed) { "string($($len.InnerText),fixed)" } else { "string($($len.InnerText))" }
				} else { $short = "string" }   # Length=0 → всегда Variable (корпус)
				break
			}
			'^xs:decimal$' {
				$d = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:Digits", $ns)
				$f = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:FractionDigits", $ns)
				$sgn = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:AllowedSign", $ns)
				$dd = if ($d) { $d.InnerText } else { '0' }
				$ff = if ($f) { $f.InnerText } else { '0' }
				if ($sgn -and $sgn.InnerText -eq 'Nonnegative') { $short = "decimal($dd,$ff,nonneg)" } else { $short = "decimal($dd,$ff)" }
				break
			}
			'^xs:boolean$' { $short = "boolean"; break }
			'^xs:dateTime$' {
				$df = $typeNode.SelectSingleNode("v8:DateQualifiers/v8:DateFractions", $ns)
				$dfv = if ($df) { $df.InnerText } else { 'DateTime' }
				switch ($dfv) { 'Date' { $short = 'date' } 'Time' { $short = 'time' } default { $short = 'dateTime' } }
				break
			}
			'^cfg:(.+)$' { $short = $matches[1]; break }
			'^(v8|v8ui):' {
				# Платформенный тип: friendly-шорткат если есть, иначе оставляем с префиксом
				# (компилятор эмитит verbatim) — чтобы не терять v8:UUID и прочий хвост.
				$rev = @{
					'v8:ValueTable'='ValueTable'; 'v8:ValueTree'='ValueTree'; 'v8:ValueListType'='ValueList'
					'v8:TypeDescription'='TypeDescription'; 'v8:Universal'='Universal'
					'v8:FixedArray'='FixedArray'; 'v8:FixedStructure'='FixedStructure'
					'v8ui:FormattedString'='FormattedString'; 'v8ui:Picture'='Picture'; 'v8ui:Color'='Color'; 'v8ui:Font'='Font'
				}
				if ($rev.ContainsKey($raw)) { $short = $rev[$raw] } else { $short = $raw }
				break
			}
			default { $short = $raw }
		}
		[void]$parts.Add($short)
	}
	# TypeSet (набор типов): определяемый тип / характеристика / «любая ссылка вида».
	# Префикс cfg:/v8: снимаем — обратный роутинг в компиляторе по форме токена.
	foreach ($ts in @($typeNode.SelectNodes("v8:TypeSet", $ns))) {
		$raw = $ts.InnerText.Trim()
		$short = $raw -replace '^(v8ui|v8|cfg):', ''
		[void]$parts.Add($short)
	}
	# TypeId — тип, заданный глобальным стабильным GUID (<v8:TypeId>, не <v8:Type>). Платформа так
	# сериализует типы, чьё имя в этом контексте недоступно (определяемые/характеристики). GUID глобально
	# стабилен → эмитим verbatim как маркер 'typeid:GUID' (компилятор разворачивает обратно; как роль-по-GUID).
	foreach ($ti in @($typeNode.SelectNodes("v8:TypeId", $ns))) {
		[void]$parts.Add("typeid:" + $ti.InnerText.Trim())
	}
	if ($parts.Count -eq 0) { return $null }
	if ($parts.Count -eq 1) { return $parts[0] }
	return ($parts -join ' | ')
}

# Ограничения использования (useRestriction/attributeUseRestriction) → объект {field?,condition?,group?,order?}.
function Build-RestrictObj {
	param($node)
	$r = [ordered]@{}
	foreach ($k in 'field','condition','group','order') { if ((Get-Child $node $k) -eq 'true') { $r[$k] = $true } }
	return $r
}

# Вычисляемое поле DataSet динамического списка (<CalculatedField>) → объектная модель.
# Зеркало эмиссии form-compile (Emit-CalcFields). Грамматика — как skd calculatedFields,
# + форм-extras presentationExpression/orderExpression (в namespace dcscommon).
function Build-CalcField {
	param($cfNode)
	$o = [ordered]@{}
	$o['dataPath'] = Get-Child $cfNode 'dataPath'
	$o['expression'] = Get-Child $cfNode 'expression'
	$tn = $cfNode.SelectSingleNode("dcssch:title", $ns)
	if ($tn) { $t = Get-LangText $tn; if ($null -ne $t) { $o['title'] = $t } }
	$vt = $cfNode.SelectSingleNode("dcssch:valueType", $ns)
	if ($vt) { $v = Decompile-Type $vt; if ($v) { $o['valueType'] = $v } }
	$ur = $cfNode.SelectSingleNode("dcssch:useRestriction", $ns)
	if ($ur) {
		$r = [ordered]@{}
		foreach ($k in 'field','condition','group','order') { if ((Get-Child $ur $k) -eq 'true') { $r[$k] = $true } }
		if ($r.Count -gt 0) { $o['useRestriction'] = $r }
	}
	$pe = Get-Child $cfNode 'presentationExpression'
	if ($null -ne $pe -and $pe -ne '') { $o['presentationExpression'] = $pe }
	$oeNodes = @($cfNode.SelectNodes("dcssch:orderExpression", $ns))
	if ($oeNodes.Count -gt 0) {
		$oes = New-Object System.Collections.ArrayList
		foreach ($oen in $oeNodes) {
			$eo = [ordered]@{}
			$exprN = $oen.SelectSingleNode("*[local-name()='expression']")
			$otN = $oen.SelectSingleNode("*[local-name()='orderType']")
			$aoN = $oen.SelectSingleNode("*[local-name()='autoOrder']")
			$eo['expression'] = if ($exprN) { $exprN.InnerText } else { '' }
			if ($otN -and $otN.InnerText -ne 'Asc') { $eo['orderType'] = $otN.InnerText }
			if ($aoN -and $aoN.InnerText -eq 'true') { $eo['autoOrder'] = $true }
			[void]$oes.Add($eo)
		}
		$o['orderExpression'] = @($oes)
	}
	return $o
}

# Schema-параметры динамического списка (<Parameter> под <Settings>) — зеркало эмиссии
# form-compile (Emit-DLParameters). Инверсия контекстных дефолтов: useRestriction=true и
# title==Title-FromName опускаем (компилятор восстановит). Сущность = DataCompositionSchemaParameter.
function Build-DLInputParameters {
	param($ipNode)
	$items = New-Object System.Collections.ArrayList
	foreach ($it in @($ipNode.SelectNodes("dcscor:item", $ns))) {
		$io = [ordered]@{}
		$io['parameter'] = Get-Child $it 'parameter'
		$useN = $it.SelectSingleNode("dcscor:use", $ns)
		if ($useN -and $useN.InnerText -eq 'false') { $io['use'] = $false }
		$valN = $it.SelectSingleNode("dcscor:value", $ns)
		if ($valN) {
			$vt = $valN.GetAttribute("type", $NS_XSI)
			if ($vt -match 'ChoiceParameters$') {
				$cps = New-Object System.Collections.ArrayList
				foreach ($cpi in @($valN.SelectNodes("dcscor:item", $ns))) {
					$cpo = [ordered]@{}
					$cpo['name'] = Get-Child $cpi 'choiceParameter'
					$vals = New-Object System.Collections.ArrayList
					foreach ($cv in @($cpi.SelectNodes("dcscor:value", $ns))) {
						[void]$vals.Add((Convert-TypedValue -raw $cv.InnerText -xsiType ($cv.GetAttribute("type", $NS_XSI))))
					}
					$cpo['values'] = @($vals)
					[void]$cps.Add($cpo)
				}
				$io['choiceParameters'] = @($cps)
			} elseif ($vt -match 'ChoiceParameterLinks$') {
				$cpls = New-Object System.Collections.ArrayList
				foreach ($cpi in @($valN.SelectNodes("dcscor:item", $ns))) {
					$cpo = [ordered]@{}
					$cpo['name'] = Get-Child $cpi 'choiceParameter'
					$cpo['value'] = Get-Child $cpi 'value'
					$md = Get-Child $cpi 'mode'; if ($md -and $md -ne 'Auto') { $cpo['mode'] = $md }
					[void]$cpls.Add($cpo)
				}
				$io['choiceParameterLinks'] = @($cpls)
			} elseif ($vt -match 'TypeLink$') {
				# Связь по типу (dcscor:TypeLink): field + linkItem — структурное значение,
				# НЕ склеивать InnerText в строку ("СчетДт"+"1"="СчетДт1").
				$tlo = [ordered]@{}
				$tlf = Get-Child $valN 'field'; if ($null -ne $tlf) { $tlo['field'] = $tlf }
				$tli = Get-Child $valN 'linkItem'; if ($null -ne $tli) { $tlo['linkItem'] = if ($tli -match '^-?\d+$') { [int]$tli } else { $tli } }
				$io['typeLink'] = $tlo
			} else {
				if ($valN.GetAttribute("nil", $NS_XSI) -ne 'true') { $io['value'] = Convert-TypedValue -raw $valN.InnerText -xsiType $vt }
			}
		}
		[void]$items.Add($io)
	}
	return @($items)
}

# dcsset:dataParameters → массив (shorthand "Имя @off" для value-less / объект для типизированного
# значения). Грамматика зеркалит skd-compile Emit-DataParameters (form-контекст: значение опционально,
# в отличие от skd-settings). Без «auto»-компактизации (нужна машинерия сравнения с top-level).
function Build-FormDataParameters {
	param($dpNode)
	$entries = @()
	foreach ($it in @($dpNode.SelectNodes("dcscor:item", $ns))) {
		$pn = Get-Text $it "dcscor:parameter"
		$use = Get-Text $it "dcscor:use"
		$valNodes = @($it.SelectNodes("dcscor:value", $ns))
		$valNode = if ($valNodes.Count -ge 1) { $valNodes[0] } else { $null }
		$usidN = $it.SelectSingleNode("dcsset:userSettingID", $ns)
		$vmN = $it.SelectSingleNode("dcsset:viewMode", $ns)
		$uspN = $it.SelectSingleNode("dcsset:userSettingPresentation", $ns)
		if ($valNode -or $usidN -or $vmN -or $uspN) {
			$obj = [ordered]@{ parameter = $pn }
			if ($valNodes.Count -gt 1) {
				# Список значений параметра (valueListAllowed) — захватываем ВСЕ <dcscor:value> массивом
				$obj['value'] = @($valNodes | ForEach-Object { $_.InnerText })
				$vt0 = $valNodes[0].GetAttribute("type", $NS_XSI); if ($vt0) { $obj['valueType'] = $vt0 }
			} elseif ($valNode) {
				if ($valNode.GetAttribute("nil", $NS_XSI) -eq 'true') { $obj['nilValue'] = $true }
				else {
					$vType = $valNode.GetAttribute("type", $NS_XSI); $vVal = $valNode.InnerText
					if ($vType -match 'decimal$' -and $vVal -match '^-?\d+$') { $obj['value'] = [int]$vVal }
					elseif ($vType -match 'boolean$') { $obj['value'] = ($vVal -eq 'true') }
					else { $obj['value'] = $vVal }
					if ($vType) { $obj['valueType'] = $vType }
				}
			}
			if ($use -eq 'false') { $obj['use'] = $false }
			if ($usidN) { $obj['userSettingID'] = 'auto' }
			if ($vmN) { $obj['viewMode'] = $vmN.InnerText }
			if ($uspN) { $usp = Get-PresText $uspN; if ($null -ne $usp) { $obj['userSettingPresentation'] = $usp } }
			$entries += $obj
		} else {
			$s = $pn; if ($use -eq 'false') { $s += ' @off' }
			$entries += $s
		}
	}
	return ,$entries
}

function Build-DLParameter {
	param($pNode)
	$name = Get-Child $pNode 'name'
	$o = [ordered]@{}
	$o['name'] = $name
	# title — опускаем, если совпадает с авто-выводом из имени (ru-only)
	$titleNode = $pNode.SelectSingleNode("dcssch:title", $ns)
	if ($titleNode) {
		$t = Get-LangText $titleNode
		if ($null -ne $t) {
			$auto = Title-FromName -name $name
			if (-not (($t -is [string]) -and ($t -eq $auto))) { $o['title'] = $t }
		}
	}
	# valueType
	$vtNode = $pNode.SelectSingleNode("dcssch:valueType", $ns)
	$typeVal = $null
	if ($vtNode) { $typeVal = Decompile-Type $vtNode; if ($typeVal) { $o['type'] = $typeVal } }
	# value — опускаем nil (дефолт), КРОМЕ valueListAllowed+nil: платформа пишет <value xsi:nil/>
	# не всегда (корпус 27 с / 47 без), а компилятор при valueListAllowed по умолчанию его НЕ эмитит →
	# явный маркер value:null, чтобы реэмитить nil. (Различается через Has-DLProp в компиляторе.)
	$vNodes = @($pNode.SelectNodes("dcssch:value", $ns))
	if ($vNodes.Count -gt 1) {
		# valueListAllowed: список значений — захватываем ВСЕ <dcssch:value> массивом
		$o['value'] = @($vNodes | ForEach-Object { Convert-TypedValue -raw $_.InnerText -xsiType ($_.GetAttribute("type", $NS_XSI)) })
	} elseif ($vNodes.Count -eq 1) {
		$vNode = $vNodes[0]
		if ($vNode.GetAttribute("nil", $NS_XSI) -ne 'true') {
			$o['value'] = Convert-TypedValue -raw $vNode.InnerText -xsiType ($vNode.GetAttribute("type", $NS_XSI))
		} elseif ((Get-Child $pNode 'valueListAllowed') -eq 'true') {
			$o['value'] = $null
		}
	}
	# useRestriction — опускаем true (дефолт), фиксируем false
	if ((Get-Child $pNode 'useRestriction') -eq 'false') { $o['useRestriction'] = $false }
	# expression
	$expr = Get-Child $pNode 'expression'; if ($null -ne $expr -and $expr -ne '') { $o['expression'] = $expr }
	# availableValues
	$avNodes = @($pNode.SelectNodes("dcssch:availableValue", $ns))
	if ($avNodes.Count -gt 0) {
		$avs = New-Object System.Collections.ArrayList
		foreach ($avn in $avNodes) {
			$avo = [ordered]@{}
			$avv = $avn.SelectSingleNode("dcssch:value", $ns)
			if ($avv -and ($avv.GetAttribute("nil", $NS_XSI) -ne 'true')) { $avo['value'] = Convert-TypedValue -raw $avv.InnerText -xsiType ($avv.GetAttribute("type", $NS_XSI)) }
			else { $avo['value'] = $null }
			$avp = $avn.SelectSingleNode("dcssch:presentation", $ns)
			if ($avp) { $pres = Get-LangText $avp; if ($null -ne $pres) { $avo['presentation'] = $pres } }
			[void]$avs.Add($avo)
		}
		$o['availableValues'] = @($avs)
	}
	# valueListAllowed / availableAsField
	if ((Get-Child $pNode 'valueListAllowed') -eq 'true') { $o['valueListAllowed'] = $true }
	if ((Get-Child $pNode 'availableAsField') -eq 'false') { $o['availableAsField'] = $false }
	# inputParameters
	$ipNode = $pNode.SelectSingleNode("dcssch:inputParameters", $ns)
	if ($ipNode) { $ip = Build-DLInputParameters $ipNode; if (@($ip).Count -gt 0) { $o['inputParameters'] = $ip } }
	# denyIncompleteValues / use
	if ((Get-Child $pNode 'denyIncompleteValues') -eq 'true') { $o['denyIncompleteValues'] = $true }
	$use = Get-Child $pNode 'use'; if ($null -ne $use -and $use -ne '') { $o['use'] = $use }

	# Компактизация: {name} → строка "name"; {name, type} → "name: type"; иначе объект.
	$keys = @($o.Keys)
	if ($keys.Count -eq 1) { return $name }
	if ($keys.Count -eq 2 -and $o.Contains('type') -and ($typeVal -is [string])) { return ("{0}: {1}" -f $name, $typeVal) }
	return $o
}

# --- 4. Element dispatch ---
$ELEMENT_KEY = @{
	'UsualGroup'='group'; 'ColumnGroup'='columnGroup'; 'ButtonGroup'='buttonGroup'; 'InputField'='input'; 'CheckBoxField'='check';
	'RadioButtonField'='radio'; 'LabelDecoration'='label'; 'LabelField'='labelField';
	'PictureDecoration'='picture'; 'PictureField'='picField'; 'CalendarField'='calendar';
	'Table'='table'; 'Pages'='pages'; 'Page'='page'; 'Button'='button'; 'CommandBar'='cmdBar'; 'Popup'='popup';
	'SearchStringAddition'='searchString'; 'ViewStatusAddition'='viewStatus'; 'SearchControlAddition'='searchControl';
	'SpreadSheetDocumentField'='spreadsheet'; 'HTMLDocumentField'='html'; 'TextDocumentField'='textDoc';
	'FormattedDocumentField'='formattedDoc'; 'ProgressBarField'='progressBar'; 'TrackBarField'='trackBar';
	'ChartField'='chart'; 'GraphicalSchemaField'='graphicalSchema'; 'PlannerField'='planner';
	'PeriodField'='periodField'; 'DendrogramField'='dendrogram'; 'GanttChartField'='ganttChart'
}

# Простые скаляры элемента (pass-through, зеркало $script:genericScalars компилятора). kind bool/value.
$GENERIC_SCALARS = @(
	@{ Tag='VerticalAlign'; Key='verticalAlign'; Kind='value' }
	@{ Tag='ThroughAlign'; Key='throughAlign'; Kind='value' }
	@{ Tag='EnableContentChange'; Key='enableContentChange'; Kind='bool' }
	@{ Tag='PictureSize'; Key='pictureSize'; Kind='value' }
	@{ Tag='TitleHeight'; Key='titleHeight'; Kind='value' }
	@{ Tag='ChildItemsWidth'; Key='childItemsWidth'; Kind='value' }
	@{ Tag='ShowLeftMargin'; Key='showLeftMargin'; Kind='bool' }
	@{ Tag='CellHyperlink'; Key='cellHyperlink'; Kind='bool' }
	@{ Tag='ViewMode'; Key='viewMode'; Kind='value' }
	@{ Tag='VerticalScrollBar'; Key='verticalScrollBar'; Kind='value' }
	@{ Tag='RowInputMode'; Key='rowInputMode'; Kind='value' }
	@{ Tag='Mask'; Key='mask'; Kind='value' }
	@{ Tag='CreateButton'; Key='createButton'; Kind='bool' }
	@{ Tag='FixingInTable'; Key='fixingInTable'; Kind='value' }
	@{ Tag='VerticalSpacing'; Key='verticalSpacing'; Kind='value' }
	# Спец-поля (документ/датчик) — типоспец. enum/bool скаляры pass-through (зеркало компилятора)
	@{ Tag='HorizontalScrollBar'; Key='horizontalScrollBar'; Kind='value' }
	@{ Tag='ViewScalingMode'; Key='viewScalingMode'; Kind='value' }
	@{ Tag='Output'; Key='output'; Kind='value' }
	@{ Tag='SelectionShowMode'; Key='selectionShowMode'; Kind='value' }
	@{ Tag='PointerType'; Key='pointerType'; Kind='value' }
	@{ Tag='DrawingSelectionShowMode'; Key='drawingSelectionShowMode'; Kind='value' }
	@{ Tag='WarningOnEditRepresentation'; Key='warningOnEditRepresentation'; Kind='value' }
	@{ Tag='MarkingAppearance'; Key='markingAppearance'; Kind='value' }
	@{ Tag='Protection'; Key='protection'; Kind='bool' }
	@{ Tag='Edit'; Key='edit'; Kind='bool' }
	@{ Tag='ShowGrid'; Key='showGrid'; Kind='bool' }
	@{ Tag='ShowGroups'; Key='showGroups'; Kind='bool' }
	@{ Tag='ShowHeaders'; Key='showHeaders'; Kind='bool' }
	@{ Tag='ShowRowAndColumnNames'; Key='showRowAndColumnNames'; Kind='bool' }
	@{ Tag='ShowCellNames'; Key='showCellNames'; Kind='bool' }
	@{ Tag='ShowPercent'; Key='showPercent'; Kind='bool' }
	# Report-form контекст: интервал группы / представление кнопки в контекстном меню / детальное представление настройки таблицы
	@{ Tag='HorizontalSpacing'; Key='horizontalSpacing'; Kind='value' }
	@{ Tag='RepresentationInContextMenu'; Key='representationInContextMenu'; Kind='value' }
	@{ Tag='SettingsNamedItemDetailedRepresentation'; Key='settingsNamedItemDetailedRepresentation'; Kind='bool' }
	# Хвост: высота элемента списка (radio) / ширина выпадающего списка (input)
	@{ Tag='ItemHeight'; Key='itemHeight'; Kind='value' }
	@{ Tag='DropListWidth'; Key='dropListWidth'; Kind='value' }
	# Хвост CI-форм: динамический заголовок (Page/Group) / расширенное ред. (input) / высота таблицы по строкам
	@{ Tag='TitleDataPath'; Key='titleDataPath'; Kind='value' }
	@{ Tag='ExtendedEdit'; Key='extendedEdit'; Kind='bool' }
	@{ Tag='MaxRowsCount'; Key='maxRowsCount'; Kind='value' }
	@{ Tag='AutoMaxRowsCount'; Key='autoMaxRowsCount'; Kind='bool' }
	@{ Tag='HeightControlVariant'; Key='heightControlVariant'; Kind='value' }
	@{ Tag='EditTextUpdate'; Key='editTextUpdate'; Kind='value' }
	# Корпусный хвост (zеркало компилятора): свёртка группы / форма попапа / авто-добавление /
	# выделение отрицательных / нач. позиция списка / высота списка выбора / три состояния / прокрутка
	@{ Tag='ControlRepresentation'; Key='controlRepresentation'; Kind='value' }
	@{ Tag='ShapeRepresentation'; Key='shapeRepresentation'; Kind='value' }
	@{ Tag='AutoAddIncomplete'; Key='autoAddIncomplete'; Kind='bool' }
	@{ Tag='MarkNegatives'; Key='markNegatives'; Kind='bool' }
	@{ Tag='InitialListView'; Key='initialListView'; Kind='value' }
	@{ Tag='ChoiceListHeight'; Key='choiceListHeight'; Kind='value' }
	@{ Tag='ThreeState'; Key='threeState'; Kind='bool' }
	@{ Tag='ScrollOnCompress'; Key='scrollOnCompress'; Kind='bool' }
	# Сочетание клавиш — общее свойство элемента (команда — отдельный путь)
	@{ Tag='Shortcut'; Key='shortcut'; Kind='value' }
	# Батч простых скаляров (зеркало компилятора; Table HeaderHeight/FooterHeight/CurrentRowUse — отдельно)
	@{ Tag='IncompleteChoiceMode'; Key='incompleteChoiceMode'; Kind='value' }
	@{ Tag='EqualColumnsWidth'; Key='equalColumnsWidth'; Kind='bool' }
	@{ Tag='ChildrenAlign'; Key='childrenAlign'; Kind='value' }
	@{ Tag='ImageScale'; Key='imageScale'; Kind='value' }
	@{ Tag='Zoomable'; Key='zoomable'; Kind='bool' }
	@{ Tag='Shape'; Key='shape'; Kind='value' }
	@{ Tag='PictureLocation'; Key='pictureLocation'; Kind='value' }
	# Равная ширина элементов (check/radio) / высота заголовка пункта (radio)
	@{ Tag='EqualItemsWidth'; Key='equalItemsWidth'; Kind='bool' }
	@{ Tag='ItemTitleHeight'; Key='itemTitleHeight'; Kind='value' }
	# Спец-режим ввода текста (input, моб.: Email/PhoneNumber/...) — листовой enum-скаляр
	@{ Tag='SpecialTextInputMode'; Key='specialTextInputMode'; Kind='value' }
	# Ширина пункта (radio/check) / выбор нескольких значений из выпадающего (input)
	@{ Tag='ItemWidth'; Key='itemWidth'; Kind='value' }
	@{ Tag='ShowCheckBoxesInDropList'; Key='showCheckBoxesInDropList'; Kind='bool' }
	@{ Tag='MultipleValueDataPath'; Key='multipleValueDataPath'; Kind='value' }
	@{ Tag='MultipleValuePresentDataPath'; Key='multipleValuePresentDataPath'; Kind='value' }
	# Режим авто-показа кнопок открытия/очистки (input, enum)
	@{ Tag='AutoShowOpenButtonMode'; Key='autoShowOpenButtonMode'; Kind='value' }
	@{ Tag='AutoShowClearButtonMode'; Key='autoShowClearButtonMode'; Kind='value' }
	# Оформление/картинка множественного выбора (input, редко; цвета — текст-контент)
	@{ Tag='MultipleValuesTextColor'; Key='multipleValuesTextColor'; Kind='value' }
	@{ Tag='MultipleValuesBackColor'; Key='multipleValuesBackColor'; Kind='value' }
	@{ Tag='MultipleValuePictureShape'; Key='multipleValuePictureShape'; Kind='value' }
	@{ Tag='MultipleValuePictureDataPath'; Key='multipleValuePictureDataPath'; Kind='value' }
	# Хвост листовых скаляров (по 1): автокоррекция / уникальность команды / пустое множ.значение / гориз.сжатие
	@{ Tag='AutoCorrectionOnTextInput'; Key='autoCorrectionOnTextInput'; Kind='value' }
	@{ Tag='SpellCheckingOnTextInput'; Key='spellCheckingOnTextInput'; Kind='value' }
	@{ Tag='CommandUniqueness'; Key='commandUniqueness'; Kind='bool' }
	@{ Tag='AllowInputEmptyMultipleValues'; Key='allowInputEmptyMultipleValues'; Kind='bool' }
	@{ Tag='BehaviorOnHorizontalCompression'; Key='behaviorOnHorizontalCompression'; Kind='value' }
)

# Захват generic-скаляров. Специфичная обработка (если ключ уже задан) — побеждает.
function Add-GenericScalars {
	param($obj, $node)
	foreach ($s in $GENERIC_SCALARS) {
		if ($obj.Contains($s.Key)) { continue }
		$v = Get-Child $node $s.Tag
		if ($null -eq $v) { continue }
		if ($s.Kind -eq 'bool') { $obj[$s.Key] = ($v -eq 'true') } else { $obj[$s.Key] = $v }
	}
}

function Decompile-Children {
	param($parentNode, [string]$childContainer = 'ChildItems')
	$container = $parentNode.SelectSingleNode("lf:$childContainer", $ns)
	if (-not $container) { return $null }
	$list = New-Object System.Collections.ArrayList
	foreach ($child in $container.ChildNodes) {
		if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
		if ($COMPANION_TAGS -contains $child.LocalName) { continue }
		$el = Decompile-Element $child
		if ($el) { [void]$list.Add($el) }
	}
	if ($list.Count -eq 0) { return $null }
	return ,@($list)
}

# Инверсия Emit-CompanionPanel: companion-командная-панель (ContextMenu/AutoCommandBar) с контентом
# → { autofill?, horizontalAlign?, children?[] } либо $null, если companion пустой (self-closing).
# Дин-список-таблица: компилятор-эвристика додумывает Autofill=false. Выражаем только ОТКЛОНЕНИЕ:
#   autofill=false (нет детей) → совпадает с дефолтом → молчим;
#   голый <AutoCommandBar/> (autofill=true по умолчанию) → маркер { autofill: true } (панель не скрыта).
function Decompile-CompanionPanel {
	param($node, [string]$tag, [bool]$isDynListTable = $false)
	$p = $node.SelectSingleNode("lf:$tag", $ns)
	if (-not $p) { return $null }
	$autofillRaw = Get-Child $p 'Autofill'
	$halign = Get-Child $p 'HorizontalAlign'
	$kids = Decompile-Children $p
	$hasKids = $kids -and @($kids).Count -gt 0
	if ($isDynListTable -and $tag -eq 'AutoCommandBar' -and -not $hasKids -and -not $halign) {
		if ($autofillRaw -eq 'false') { return $null }            # = дефолт эвристики → молчим
		return [ordered]@{ autofill = $true }                      # голая панель → отклонение
	}
	if (-not $hasKids -and $null -eq $autofillRaw -and -not $halign) { return $null }
	$o = [ordered]@{}
	if ($halign) { $o['horizontalAlign'] = $halign }
	if ($autofillRaw -eq 'false') { $o['autofill'] = $false }
	elseif ($autofillRaw -eq 'true') { $o['autofill'] = $true }
	if ($hasKids) { $o['children'] = $kids }
	return $o
}

# Инверсия Emit-ChoiceList: <ChoiceList><xr:Item>… → [ { value, presentation? } ] либо $null.
# У RadioButtonField и InputField.
function Decompile-ChoiceList {
	param($node)
	$cl = $node.SelectSingleNode("lf:ChoiceList", $ns)
	if (-not $cl) { return $null }
	$items = New-Object System.Collections.ArrayList
	foreach ($it in @($cl.SelectNodes("xr:Item", $ns))) {
		$valNode = $it.SelectSingleNode("xr:Value/lf:Value", $ns)
		$presNode = $it.SelectSingleNode("xr:Value/lf:Presentation", $ns)
		$ci = [ordered]@{}
		if ($valNode) {
			if ($valNode.GetAttribute("nil", $NS_XSI) -eq 'true') {
				# nil-значение элемента choiceList — компилятор эмитит <Value xsi:nil="true"/>
				# (иначе Convert-TypedValue вернул бы "" → typed-empty xs:string).
				$ci['valueType'] = 'nil'
			} else {
				$xsiType = $valNode.GetAttribute("type", $NS_XSI)
				$ci['value'] = Convert-TypedValue $valNode.InnerText $xsiType
				# Системное перечисление (ent:*) / иной не-примитивный тип → сохраняем valueType
				# (Normalize-ChoiceValue вывела бы xs:string). DesignTimeRef обычно авто-детектится
				# компилятором по named-ref (Enum.X.Y), НО raw-ссылка по GUID (GUID.GUID) — нет → сохраняем.
				if ($xsiType -and $xsiType -notmatch '^xs:(string|decimal|boolean|dateTime)$' -and `
					($xsiType -ne 'xr:DesignTimeRef' -or "$($valNode.InnerText)" -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-')) {
					$ci['valueType'] = $xsiType
				}
			}
		}
		# Presentation: непустой → текст/мультиязык; пустой <Presentation/> → "" — суппресс-маркер,
		# подавляет авто-вывод компилятора (иначе компилятор додумает presentation из значения).
		if ($presNode) {
			$p = Get-LangTextWS $presNode   # восстановление значимого пробела (whitespace-presentation)
			if ($null -ne $p -and $p -ne '') { $ci['presentation'] = $p } else { $ci['presentation'] = '' }
		}
		[void]$items.Add($ci)
	}
	if ($items.Count -gt 0) { return ,@($items) }
	return $null
}

# Значение Параметра выбора (<lf:Value>): скаляр через Convert-TypedValue, либо
# v8:FixedArray → массив скаляров (каждый — FormChoiceListDesTimeValue с внутренним lf:Value).
function Convert-ChoiceParamValue {
	param($valNode)
	$vt = $valNode.GetAttribute("type", $NS_XSI)
	if ($vt -match 'FixedArray$') {
		$arr = New-Object System.Collections.ArrayList
		foreach ($it in @($valNode.SelectNodes("v8:Value", $ns))) {
			$inner = $it.SelectSingleNode("lf:Value", $ns)
			if ($inner) { [void]$arr.Add((Convert-TypedValue -raw $inner.InnerText -xsiType ($inner.GetAttribute("type", $NS_XSI)))) }
		}
		return ,@($arr)
	}
	return Convert-TypedValue -raw $valNode.InnerText -xsiType $vt
}

# Инверсия Emit-ChoiceParameters: <ChoiceParameters><app:item name="X"><app:value><Value…> → [{name, value}].
function Decompile-ChoiceParameters {
	param($node)
	$cpn = $node.SelectSingleNode("lf:ChoiceParameters", $ns)
	if (-not $cpn) { return $null }
	$items = New-Object System.Collections.ArrayList
	foreach ($it in @($cpn.SelectNodes("app:item", $ns))) {
		$o = [ordered]@{}
		$o['name'] = $it.GetAttribute("name")
		$valNode = $it.SelectSingleNode("app:value/lf:Value", $ns)
		if ($valNode) { $o['value'] = Convert-ChoiceParamValue $valNode }
		[void]$items.Add($o)
	}
	if ($items.Count -gt 0) { return ,@($items) }
	return $null
}

# Инверсия Emit-ChoiceParameterLinks: <ChoiceParameterLinks><xr:Link><xr:Name><xr:DataPath><xr:ValueChange> →
# [{name, dataPath, valueChange?}]. valueChange дефолт Clear → опускаем (компилятор восстановит).
function Decompile-ChoiceParameterLinks {
	param($node)
	$cln = $node.SelectSingleNode("lf:ChoiceParameterLinks", $ns)
	if (-not $cln) { return $null }
	$items = New-Object System.Collections.ArrayList
	foreach ($lk in @($cln.SelectNodes("xr:Link", $ns))) {
		$o = [ordered]@{}
		$o['name'] = Get-Text $lk "xr:Name"
		$o['dataPath'] = Get-Text $lk "xr:DataPath"
		$vc = Get-Text $lk "xr:ValueChange"
		if ($vc -and $vc -ne 'Clear') { $o['valueChange'] = $vc }
		[void]$items.Add($o)
	}
	if ($items.Count -gt 0) { return ,@($items) }
	return $null
}

# Инверсия Emit-TypeLink: <TypeLink><xr:DataPath><xr:LinkItem> → {dataPath, linkItem}.
function Decompile-TypeLink {
	param($node)
	$tn = $node.SelectSingleNode("lf:TypeLink", $ns)
	if (-not $tn) { return $null }
	$o = [ordered]@{}
	$o['dataPath'] = Get-Text $tn "xr:DataPath"
	$li = Get-Text $tn "xr:LinkItem"
	if ($null -ne $li -and $li -ne '') { $o['linkItem'] = [int]$li }
	return $o
}

# Захват <Format>/<EditFormat> (LocalStringType) → format/editFormat (строка или {ru,en}).
function Add-FormatProps {
	param($obj, $node)
	$fmt = $node.SelectSingleNode("lf:Format", $ns); if ($fmt) { $t = Get-LangText $fmt; if ($null -ne $t -and $t -ne '') { $obj['format'] = $t } }
	$efmt = $node.SelectSingleNode("lf:EditFormat", $ns); if ($efmt) { $t = Get-LangText $efmt; if ($null -ne $t -and $t -ne '') { $obj['editFormat'] = $t } }
}

# Ядро дополнения: source + общие свойства (Add-CommonProps) + horizontalLocation.
# Layout (Add-Layout) добавляется ОТДЕЛЬНО (в Decompile-Element — пост-обработкой, в standalone — явно).
function Add-AdditionCore {
	param($obj, $node, [string]$elName)
	$src = $node.SelectSingleNode("lf:AdditionSource/lf:Item", $ns); if ($src) { $obj['source'] = $src.InnerText }
	Add-CommonProps $obj $node $elName
	$hl = Get-Child $node 'HorizontalLocation'; if ($hl) { $obj['horizontalLocation'] = $hl.ToLower() }
}

# Стандартные дополнения уровня таблицы (прямые дети <Table>): извлечь ТОЛЬКО отклонения в карту
# { тип: {свойства} }. Имя (=tableName+suffix) и source (=tableName) — дефолтные, опускаем.
function Decompile-TableAdditions {
	param($tableNode, [string]$tableName)
	$tagToKey = @{ 'SearchStringAddition'='searchString'; 'ViewStatusAddition'='viewStatus'; 'SearchControlAddition'='searchControl' }
	$map = [ordered]@{}
	foreach ($child in $tableNode.ChildNodes) {
		if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
		if (-not $tagToKey.ContainsKey($child.LocalName)) { continue }
		$key = $tagToKey[$child.LocalName]
		$nm = $child.GetAttribute("name")
		$o = [ordered]@{}; $o[$key] = $nm
		Add-AdditionCore $o $child $nm
		Add-Layout $o $child
		$o.Remove($key)                                                          # имя авто
		if ($o.Contains('source') -and $o['source'] -eq $tableName) { $o.Remove('source') }  # source=таблица дефолт
		if ($o.Count -gt 0) { $map[$key] = $o }
	}
	if ($map.Count -gt 0) { return $map }
	return $null
}

# Спец-поля «документ/датчик» — общий скелет поля (имя/path/CommonProps/TitleLocation/editMode).
# Типоспец. enum/bool скаляры ловит пост-switch Add-GenericScalars; layout/companions — общий хвост.
function Decompile-SimpleField {
	param($obj, $node, [string]$name, [string]$key)
	$obj[$key] = $name
	$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
	Add-CommonProps $obj $node $name
	$tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() }
	$em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em }
}

# Числовые скаляры датчиков (ProgressBar/TrackBar) — без xsi:type (≠ типизированных InputField).
function Add-GaugeScalars {
	param($obj, $node, $tags)
	foreach ($p in $tags) {
		$v = Get-Child $node $p
		if ($null -eq $v) { continue }
		$key = $p.Substring(0,1).ToLower() + $p.Substring(1)
		if ($v -match '^-?\d+$') { $obj[$key] = [int]$v } else { $obj[$key] = $v }
	}
}

function Decompile-Element {
	param($node)
	$tag = $node.LocalName
	if (-not $ELEMENT_KEY.ContainsKey($tag)) {
		Fail-Ring3 -kind "элемент <$tag>" -loc "ChildItems/$tag"
	}
	$key = $ELEMENT_KEY[$tag]
	$name = $node.GetAttribute("name")
	$obj = [ordered]@{}

	switch ($tag) {
		'UsualGroup' {
			# group = направление (<Group>); behavior = <Behavior> (Авто = нет тега → ключ опускаем).
			# group = направление (<Group>). Нет тега → '' (тип-маркер сохраняется, направление
			# не эмитим). Платформа явно пишет Vertical в большинстве случаев, поэтому '' ≠ 'vertical'
			# — иначе компилятор додумает <Group>Vertical</Group> там, где его нет.
			$g = Get-Child $node 'Group'
			$gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'AlwaysHorizontal'='alwaysHorizontal'; 'AlwaysVertical'='alwaysVertical'; 'HorizontalIfPossible'='horizontalIfPossible' }
			if ($g -and $gmap.ContainsKey($g)) { $obj[$key] = $gmap[$g] } else { $obj[$key] = '' }
			$behavior = Get-Child $node 'Behavior'
			if ($behavior) {
				$bmap = @{ 'Usual'='usual'; 'Collapsible'='collapsible'; 'PopUp'='popup' }
				if ($bmap.ContainsKey($behavior)) { $obj['behavior'] = $bmap[$behavior] } else { $obj['behavior'] = $behavior }
			}
			$obj['name'] = $name
			Add-CommonProps $obj $node $name
			$rep = Get-Child $node 'Representation'
			if ($rep) { $repmap=@{'None'='none';'NormalSeparation'='normal';'WeakSeparation'='weak';'StrongSeparation'='strong'}; if ($repmap.ContainsKey($rep)) { $obj['representation']=$repmap[$rep] } else { $obj['representation']=$rep } }
			$st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') }  # факт. значение (явный true тоже)
			$cru = Get-Child $node 'CurrentRowUse'; if ($cru) { $obj['currentRowUse'] = $cru }   # использование текущей строки группы
			$crt = $node.SelectSingleNode("lf:CollapsedRepresentationTitle", $ns); if ($crt) { $ct = Get-LangText $crt; if ($null -ne $ct -and $ct -ne '') { $obj['collapsedTitle'] = $ct } }
			if ((Get-Child $node 'United') -eq 'false') { $obj['united'] = $false }
			if ((Get-Child $node 'Collapsed') -eq 'true') { $obj['collapsed'] = $true }
			# Формат значения пути к данным заголовка (<Format>; парный к titleDataPath группы)
			Add-FormatProps $obj $node
			$kids = Decompile-Children $node
			if ($kids) { $obj['children'] = $kids }
		}
		'ColumnGroup' {
			# columnGroup = направление (<Group>). Нет тега → '' (тип-маркер сохраняется, направление
			# не эмитим). Иначе компилятор додумает <Group>Horizontal</Group> там, где его нет.
			$g = Get-Child $node 'Group'
			$gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'InCell'='inCell' }
			if ($g -and $gmap.ContainsKey($g)) { $obj[$key] = $gmap[$g] } else { $obj[$key] = '' }
			$obj['name'] = $name
			Add-CommonProps $obj $node $name
			$st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') }  # факт. значение (явный true тоже)
			$sih = Get-Child $node 'ShowInHeader'; if ($null -ne $sih) { $obj['showInHeader'] = (To-Bool $sih) }
			$kids = Decompile-Children $node
			if ($kids) { $obj['children'] = $kids }
		}
		'InputField' {
			$obj[$key] = $name
			$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
			Add-CommonProps $obj $node $name
			# MultiLine: факт. значение (платформа эмитит и явный false — 425 в корпусе; ≠ «if true»)
			$mlIn = Get-Child $node 'MultiLine'; if ($null -ne $mlIn) { $obj['multiLine'] = ($mlIn -eq 'true') }
			# PasswordMode: факт. значение (платформа эмитит и false — 349/504 в корпусе; ≠ «if true»)
			$pmIn = Get-Child $node 'PasswordMode'; if ($null -ne $pmIn) { $obj['passwordMode'] = ($pmIn -eq 'true') }
			$mi = Get-Child $node 'AutoMarkIncomplete'; if ($null -ne $mi) { $obj['markIncomplete'] = ($mi -eq 'true') }
			$em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em }
			$tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() }
			$ih = $node.SelectSingleNode("lf:InputHint", $ns); if ($ih) { $t = Get-LangTextWS $ih; if ($t) { $obj['inputHint'] = $t } }
			$woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangTextWS $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } }
			$ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangTextWS $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } }
			foreach ($p in @('ChoiceButton','ClearButton','SpinButton','DropListButton','ChoiceListButton')) {
				$v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = (To-Bool $v) }
			}
			# InputField-специфичные bool-скаляры (захват «как есть»)
			foreach ($p in @('Wrap','OpenButton','ListChoiceMode','ExtendedEditMultipleValues','ChooseType','QuickChoice','AutoChoiceIncomplete')) {
				$v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = (To-Bool $v) }
			}
			# InputField-специфичные value-скаляры (захват «как есть»)
			foreach ($p in @('ChoiceForm','ChoiceHistoryOnInput','ChoiceFoldersAndItems','FooterDataPath')) {
				$v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = $v }
			}
			# MinValue/MaxValue — типизированное (<MinValue xsi:type="xs:decimal">N). Тип кодируем в JSON-тип:
			# xs:decimal/int → число (компилятор → xs:decimal), иначе → строка (компилятор → xs:string).
			foreach ($p in @('MinValue','MaxValue')) {
				$mn = $node.SelectSingleNode("lf:$p", $ns)
				if ($mn) {
					$xt = $mn.GetAttribute("type", $NS_XSI); $txt = $mn.InnerText
					$key = $p.Substring(0,1).ToLower() + $p.Substring(1)
					if ($xt -match 'decimal|int') {
						if ($txt -match '^-?\d+$') { $obj[$key] = [int]$txt } elseif ($txt -match '^-?\d+\.\d+$') { $obj[$key] = [decimal]$txt } else { $obj[$key] = $txt }
					} else { $obj[$key] = $txt }
				}
			}
			# Ограничение доступных типов (поле на составном/характеристика-типе): домен типов + явный набор.
			# availableTypes — тот же формат типа, что у реквизитов (§type), захват через Decompile-Type.
			$tde = Get-Child $node 'TypeDomainEnabled'; if ($null -ne $tde) { $obj['typeDomainEnabled'] = (To-Bool $tde) }
			$atNode = $node.SelectSingleNode("lf:AvailableTypes", $ns); if ($atNode) { $at = Decompile-Type $atNode; if ($at) { $obj['availableTypes'] = $at } }
			$cbr = Get-Child $node 'ChoiceButtonRepresentation'; if ($cbr) { $obj['choiceButtonRepresentation'] = $cbr }
			$cbp = Get-PictureRef $node 'ChoiceButtonPicture'; if ($null -ne $cbp) { $obj['choiceButtonPicture'] = $cbp }
			if ((Get-Child $node 'TextEdit') -eq 'false') { $obj['textEdit'] = $false }
			$cl = Decompile-ChoiceList $node; if ($cl) { $obj['choiceList'] = $cl }
			Add-FormatProps $obj $node
			# Параметры выбора / Связи параметров выбора / Связь по типу
			$cp = Decompile-ChoiceParameters $node; if ($cp) { $obj['choiceParameters'] = $cp }
			$cpl = Decompile-ChoiceParameterLinks $node; if ($cpl) { $obj['choiceParameterLinks'] = $cpl }
			$tlk = Decompile-TypeLink $node; if ($tlk) { $obj['typeLink'] = $tlk }
		}
		'CheckBoxField' {
			$obj[$key] = $name
			$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
			Add-CommonProps $obj $node $name
			$em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em }
			# CheckBoxType: Auto = умный дефолт → опустить; нет тега → ""; иначе значение
			$cbt = Get-Child $node 'CheckBoxType'
			if ($null -eq $cbt) { $obj['checkBoxType'] = '' }
			elseif ($cbt -ne 'Auto') { $obj['checkBoxType'] = $cbt.Substring(0,1).ToLower() + $cbt.Substring(1) }
			Add-TitleLocation $obj $node 'Right'
			$woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangTextWS $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } }
			# FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField)
			$fdp = Get-Child $node 'FooterDataPath'; if ($fdp) { $obj['footerDataPath'] = $fdp }
			$ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangTextWS $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } }
			Add-FormatProps $obj $node
		}
		'RadioButtonField' {
			$obj[$key] = $name
			$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
			Add-CommonProps $obj $node $name
			Add-TitleLocation $obj $node 'None'
			$em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em }
			$rbt = Get-Child $node 'RadioButtonType'; if ($rbt) { $obj['radioButtonType'] = $rbt }
			$cc = Get-Child $node 'ColumnsCount'; if ($cc) { $obj['columnsCount'] = [int]$cc }
			$woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangTextWS $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } }
			$cl = Decompile-ChoiceList $node; if ($cl) { $obj['choiceList'] = $cl }
		}
		'LabelDecoration' {
			$obj[$key] = $name
			Add-CommonProps $obj $node $name
			if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true }
			# title декорации — единая ML-text форма с авто-детектом formatted (как extendedTooltip)
			$tiNode = $node.SelectSingleNode("lf:Title", $ns)
			if ($tiNode) { $tv = Get-MLFormattedValue $tiNode; if ($null -ne $tv) { $obj['title'] = $tv } }
		}
		'LabelField' {
			$obj[$key] = $name
			$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
			Add-CommonProps $obj $node $name
			$tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() }
			$em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em }
			# LabelField: тег <Hiperlink> (опечатка платформы), не <Hyperlink>
			if ((Get-Child $node 'Hiperlink') -eq 'true') { $obj['hyperlink'] = $true }
			# PasswordMode на LabelField — платформа эмитит явный false (редко); захват факт. значения
			$pm = Get-Child $node 'PasswordMode'; if ($null -ne $pm) { $obj['passwordMode'] = ($pm -eq 'true') }
			$woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangTextWS $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } }
			# FooterDataPath / FooterText — общие cell-свойства колонки (как у input), не только input
			$fdp = Get-Child $node 'FooterDataPath'; if ($fdp) { $obj['footerDataPath'] = $fdp }
			$ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangTextWS $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } }
			Add-FormatProps $obj $node
		}
		'PictureDecoration' {
			$obj[$key] = $name
			Add-CommonProps $obj $node $name
			# title декорации — единая ML-text форма с formatted (атрибут <Title formatted> у PictureDecoration)
			$tiNode = $node.SelectSingleNode("lf:Title", $ns)
			if ($tiNode) { $tv = Get-MLFormattedValue $tiNode; if ($null -ne $tv) { $obj['title'] = $tv } }
			$npt = $node.SelectSingleNode("lf:NonselectedPictureText", $ns); if ($npt) { $t = Get-LangTextWS $npt; if ($null -ne $t) { $obj['nonselectedPictureText'] = $t } }
			$ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns)
			$abs = $node.SelectSingleNode("lf:Picture/xr:Abs", $ns)
			if ($ref) { $obj['src'] = $ref.InnerText } elseif ($abs) { $obj['src'] = "abs:$($abs.InnerText)" }  # встроенная картинка → префикс abs:
			$lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns); if ($lt -and $lt.InnerText -eq 'true') { $obj['loadTransparent'] = $true }
			# Прозрачный пиксель картинки (<xr:TransparentPixel x y/>) — координаты фона прозрачности
			$tpx = $node.SelectSingleNode("lf:Picture/xr:TransparentPixel", $ns)
			if ($tpx) { $obj['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } }
			if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true }
		}
		'PictureField' {
			$obj[$key] = $name
			$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
			Add-CommonProps $obj $node $name
			$em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em }
			$tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() }
			if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true }
			$vp = Get-PictureRef $node 'ValuesPicture'; if ($null -ne $vp) { $obj['valuesPicture'] = $vp }
			$npt = $node.SelectSingleNode("lf:NonselectedPictureText", $ns); if ($npt) { $t = Get-LangTextWS $npt; if ($null -ne $t) { $obj['nonselectedPictureText'] = $t } }
			# FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField)
			$fdp = Get-Child $node 'FooterDataPath'; if ($fdp) { $obj['footerDataPath'] = $fdp }
			$ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangTextWS $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } }
		}
		'CalendarField' {
			$obj[$key] = $name
			$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
			Add-CommonProps $obj $node $name
			$tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() }
			$sm = Get-Child $node 'SelectionMode'; if ($sm) { $obj['selectionMode'] = $sm }
			$scd = Get-Child $node 'ShowCurrentDate'; if ($null -ne $scd) { $obj['showCurrentDate'] = ($scd -eq 'true') }
			$wim = Get-Child $node 'WidthInMonths'; if ($null -ne $wim) { $obj['widthInMonths'] = [int]$wim }
			$him = Get-Child $node 'HeightInMonths'; if ($null -ne $him) { $obj['heightInMonths'] = [int]$him }
			$smp = Get-Child $node 'ShowMonthsPanel'; if ($null -ne $smp) { $obj['showMonthsPanel'] = ($smp -eq 'true') }
		}
		'Table' {
			$obj[$key] = $name
			$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
			Add-CommonProps $obj $node $name
			$tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() }
			$rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep }
			$crs = Get-Child $node 'ChangeRowSet'; if ($null -ne $crs) { $obj['changeRowSet'] = ($crs -eq 'true') }
			$cro = Get-Child $node 'ChangeRowOrder'; if ($null -ne $cro) { $obj['changeRowOrder'] = ($cro -eq 'true') }
			if ((Get-Child $node 'AutoInsertNewRow') -eq 'true') { $obj['autoInsertNewRow'] = $true }
			# enableDrag — теперь общий (Add-Layout, фактическое значение)
			if ($node.SelectSingleNode("lf:RowFilter", $ns)) { $obj['rowFilter'] = $null }
			if ((Get-Child $node 'Header') -eq 'false') { $obj['header'] = $false }
			if ((Get-Child $node 'Footer') -eq 'true') { $obj['footer'] = $true }
			# Высота в строках — отдельный ключ heightInTableRows (≠ height = <Height>, его ловит Add-Layout)
			$htr = Get-Child $node 'HeightInTableRows'; if ($htr) { $obj['heightInTableRows'] = [int]$htr }
			# Высота шапки/подвала таблицы в строках (pass-through; компилятор эмитит в Emit-Table)
			$hh = Get-Child $node 'HeaderHeight'; if ($null -ne $hh) { $obj['headerHeight'] = [int]$hh }
			$fh = Get-Child $node 'FooterHeight'; if ($null -ne $fh) { $obj['footerHeight'] = [int]$fh }
			# Использование текущей строки (Table-уровень; ≠ command-level CurrentRowUse) — pass-through
			$cru = Get-Child $node 'CurrentRowUse'; if ($cru) { $obj['currentRowUse'] = $cru }
			# Запрос обновления дин-списка (всегда PullFromTop в корпусе) — pass-through
			$rr = Get-Child $node 'RefreshRequest'; if ($rr) { $obj['refreshRequest'] = $rr }
			# CommandBarLocation: для дин-список-таблицы компилятор авто-инжектит "None" → инвертируем
			# (нет тега → суппресс-маркер ""; "None" → опускаем = авто-дефолт; иначе → захват).
			$cbl = Get-Child $node 'CommandBarLocation'
			if (Has-Child $node 'UpdateOnDataChange') {
				if ($null -eq $cbl) { $obj['commandBarLocation'] = '' }
				elseif ($cbl -ne 'None') { $obj['commandBarLocation'] = $cbl }
			} elseif ($cbl) { $obj['commandBarLocation'] = $cbl }
			$ssl = Get-Child $node 'SearchStringLocation'; if ($ssl) { $obj['searchStringLocation'] = $ssl }
			$vsl = Get-Child $node 'ViewStatusLocation'; if ($vsl) { $obj['viewStatusLocation'] = $vsl }
			$scl = Get-Child $node 'SearchControlLocation'; if ($scl) { $obj['searchControlLocation'] = $scl }
			# --- Общие свойства таблицы (любой тип таблицы, не только динсписок) ---
			if ((Get-Child $node 'ChoiceMode') -eq 'true') { $obj['choiceMode'] = $true }
			$selm = Get-Child $node 'SelectionMode'; if ($selm) { $obj['selectionMode'] = $selm }
			$rsm = Get-Child $node 'RowSelectionMode'; if ($rsm) { $obj['rowSelectionMode'] = $rsm }
			if ((Get-Child $node 'VerticalLines') -eq 'false') { $obj['verticalLines'] = $false }
			if ((Get-Child $node 'HorizontalLines') -eq 'false') { $obj['horizontalLines'] = $false }
			if ((Get-Child $node 'UseAlternationRowColor') -eq 'true') { $obj['useAlternationRowColor'] = $true }
			# Скаляры таблицы (захват «как есть»). Autofill — СВОЁ свойство таблицы (≠ AutoCommandBar autofill).
			$taf = Get-Child $node 'Autofill'; if ($null -ne $taf) { $obj['autofill'] = ($taf -eq 'true') }
			if ((Get-Child $node 'MultipleChoice') -eq 'true') { $obj['multipleChoice'] = $true }
			$soin = Get-Child $node 'SearchOnInput'; if ($soin) { $obj['searchOnInput'] = $soin }
			$mi = Get-Child $node 'AutoMarkIncomplete'; if ($null -ne $mi) { $obj['markIncomplete'] = ($mi -eq 'true') }
			$itv = Get-Child $node 'InitialTreeView'; if ($itv) { $obj['initialTreeView'] = $itv }
			# RowsPicture — конвенция ValuesPicture (Ref/Abs + LoadTransparent дефолт false + TransparentPixel)
			$rp = Get-PictureRef $node 'RowsPicture'; if ($null -ne $rp) { $obj['rowsPicture'] = $rp }
			$rpdp = Get-Child $node 'RowPictureDataPath'
			# --- Блок дин-список-таблицы (признак: дочерний <UpdateOnDataChange>) ---
			if (Has-Child $node 'UpdateOnDataChange') {
				if ((Get-Child $node 'AutoRefresh') -eq 'true') { $obj['autoRefresh'] = $true }
				$arp = Get-Child $node 'AutoRefreshPeriod'; if ($arp -and $arp -ne '60') { $obj['autoRefreshPeriod'] = [int]$arp }
				$cfi = Get-Child $node 'ChoiceFoldersAndItems'; if ($cfi -and $cfi -ne 'Items') { $obj['choiceFoldersAndItems'] = $cfi }
				if ((Get-Child $node 'RestoreCurrentRow') -eq 'true') { $obj['restoreCurrentRow'] = $true }
				if ((Get-Child $node 'ShowRoot') -eq 'false') { $obj['showRoot'] = $false }
				if ((Get-Child $node 'AllowRootChoice') -eq 'true') { $obj['allowRootChoice'] = $true }
				$uodc = Get-Child $node 'UpdateOnDataChange'; if ($uodc -and $uodc -ne 'Auto') { $obj['updateOnDataChange'] = $uodc }
				if ((Get-Child $node 'AllowGettingCurrentRowURL') -eq 'false') { $obj['allowGettingCurrentRowURL'] = $false }
				# RowPictureDataPath: инверсия умного дефолта <Список>.DefaultPicture
				if ($null -eq $rpdp) { $obj['rowPictureDataPath'] = '' }
				elseif ($rpdp -ne "$($obj['path']).DefaultPicture") { $obj['rowPictureDataPath'] = $rpdp }
				$usg = Get-Child $node 'UserSettingsGroup'
				if ($usg) { if ($usg -match '^\d+:[0-9a-fA-F]{8}-') { [Console]::Error.WriteLine("form-decompile: UserSettingsGroup '$usg' ($name) — ссылка по id, не воспроизводима, опущена") } else { $obj['userSettingsGroup'] = $usg } }
			} elseif ($rpdp) { $obj['rowPictureDataPath'] = $rpdp }
			$csNode = $node.SelectSingleNode("lf:CommandSet", $ns)
			if ($csNode) {
				$exc = New-Object System.Collections.ArrayList
				foreach ($ec in @($csNode.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$exc.Add($ec.InnerText) }
				if ($exc.Count -gt 0) { $obj['excludedCommands'] = @($exc) }
			}
			$cols = Decompile-Children $node
			if ($cols) { $obj['columns'] = $cols }
			# Стандартные дополнения уровня таблицы (прямые дети) → карта отклонений additions
			$addMap = Decompile-TableAdditions $node $name
			if ($addMap) { $obj['additions'] = $addMap }
		}
		'Pages' {
			$obj[$key] = $name
			Add-CommonProps $obj $node $name
			$pr = Get-Child $node 'PagesRepresentation'; if ($pr) { $obj['pagesRepresentation'] = $pr }
			$cru = Get-Child $node 'CurrentRowUse'; if ($cru) { $obj['currentRowUse'] = $cru }
			$kids = Decompile-Children $node
			if ($kids) { $obj['children'] = $kids }
		}
		'Page' {
			$obj[$key] = $name
			Add-CommonProps $obj $node $name
			$g = Get-Child $node 'Group'
			$gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'AlwaysHorizontal'='alwaysHorizontal'; 'AlwaysVertical'='alwaysVertical'; 'HorizontalIfPossible'='horizontalIfPossible' }
			if ($g -and $gmap.ContainsKey($g)) { $obj['group'] = $gmap[$g] }
			# Картинка страницы (иконка вкладки) — конвенция ValuesPicture (дефолт LoadTransparent=false)
			$pp = Get-PictureRef $node 'Picture'; if ($null -ne $pp) { $obj['picture'] = $pp }
			$st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') }  # факт. значение (явный true тоже)
			# Формат значения пути к данным заголовка (<Format>; парный к titleDataPath страницы)
			Add-FormatProps $obj $node
			$kids = Decompile-Children $node
			if ($kids) { $obj['children'] = $kids }
		}
		'Button' {
			$obj[$key] = $name
			$cmd = Get-Child $node 'CommandName'
			if ($cmd) {
				if ($cmd -match '^Form\.Command\.(.+)$') { $obj['command'] = $matches[1] }
				elseif ($cmd -match '^Form\.StandardCommand\.(.+)$') { $obj['stdCommand'] = $matches[1] }
				elseif ($cmd -match '^Form\.Item\.(.+)\.StandardCommand\.(.+)$') { $obj['stdCommand'] = "$($matches[1]).$($matches[2])" }
				else { $obj['commandName'] = $cmd }
			}
			$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
			# Parameter команды: xr:MDObjectRef (объект метаданных, строка) или v8:TypeDescription (тип → {type})
			$btnParam = $node.SelectSingleNode("lf:Parameter", $ns)
			if ($btnParam) {
				$pxt = $btnParam.GetAttribute("type", $NS_XSI)
				if ($pxt -match 'TypeDescription$') { $pt = Decompile-Type $btnParam; if ($pt) { $obj['parameter'] = [ordered]@{ type = $pt } } }
				elseif ($btnParam.InnerText) { $obj['parameter'] = $btnParam.InnerText }
			}
			Add-CommonProps $obj $node $name
			$type = Get-Child $node 'Type'
			if ($type) { $tmap=@{'CommandBarButton'='commandBar';'UsualButton'='usual';'Hyperlink'='hyperlink';'CommandBarHyperlink'='hyperlink'}; if ($tmap.ContainsKey($type)) { $obj['type']=$tmap[$type] } else { $obj['type']=$type } }
			if ((Get-Child $node 'DefaultButton') -eq 'true') { $obj['defaultButton'] = $true }
			if ((Get-Child $node 'Check') -eq 'true') { $obj['checked'] = $true }
			Set-CommandPicture $obj $node
			$rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep }
			$lic = Get-Child $node 'LocationInCommandBar'; if ($lic) { $obj['locationInCommandBar'] = $lic }
		}
		'ButtonGroup' {
			$obj[$key] = $name
			Add-CommonProps $obj $node $name
			$cs = Get-Child $node 'CommandSource'
			if ($cs) { if ($cs -match '^\d+:[0-9a-fA-F]{8}-') { [Console]::Error.WriteLine("form-decompile: CommandSource '$cs' ($name) — ссылка по id, не воспроизводима, опущена") } else { $obj['commandSource'] = $cs } }
			$rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep }
			$kids = Decompile-Children $node
			if ($kids) { $obj['children'] = $kids }
		}
		'CommandBar' {
			$obj[$key] = $name
			Add-CommonProps $obj $node $name
			$cs = Get-Child $node 'CommandSource'
			if ($cs) { if ($cs -match '^\d+:[0-9a-fA-F]{8}-') { [Console]::Error.WriteLine("form-decompile: CommandSource '$cs' ($name) — ссылка по id, не воспроизводима, опущена") } else { $obj['commandSource'] = $cs } }
			$hl = Get-Child $node 'HorizontalLocation'; if ($hl) { $obj['horizontalLocation'] = $hl.ToLower() }
			if ((Get-Child $node 'Autofill') -eq 'true') { $obj['autofill'] = $true }
			$kids = Decompile-Children $node
			if ($kids) { $obj['children'] = $kids }
		}
		'Popup' {
			$obj[$key] = $name
			Add-CommonProps $obj $node $name
			Set-CommandPicture $obj $node
			$rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep }
			# Источник команд попапа (Form / FormCommandPanelGlobalCommands / Item.X) — как у ButtonGroup/CommandBar
			$cs = Get-Child $node 'CommandSource'
			if ($cs) { if ($cs -match '^\d+:[0-9a-fA-F]{8}-') { [Console]::Error.WriteLine("form-decompile: CommandSource '$cs' ($name) — ссылка по id, не воспроизводима, опущена") } else { $obj['commandSource'] = $cs } }
			$kids = Decompile-Children $node
			if ($kids) { $obj['children'] = $kids }
		}
		'SearchStringAddition'  { $obj[$key] = $name; Add-AdditionCore $obj $node $name }
		'ViewStatusAddition'    { $obj[$key] = $name; Add-AdditionCore $obj $node $name }
		'SearchControlAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name }
		'SpreadSheetDocumentField' { Decompile-SimpleField $obj $node $name $key }
		'HTMLDocumentField'        { Decompile-SimpleField $obj $node $name $key }
		'TextDocumentField'        { Decompile-SimpleField $obj $node $name $key }
		'FormattedDocumentField'   { Decompile-SimpleField $obj $node $name $key }
		'ProgressBarField'         { Decompile-SimpleField $obj $node $name $key; Add-GaugeScalars $obj $node @('MinValue','MaxValue') }
		'TrackBarField'            { Decompile-SimpleField $obj $node $name $key; Add-GaugeScalars $obj $node @('MinValue','MaxValue','LargeStep','MarkingStep','Step') }
		'ChartField'               { Decompile-SimpleField $obj $node $name $key }
		'GraphicalSchemaField'     { Decompile-SimpleField $obj $node $name $key }
		'PlannerField'             { Decompile-SimpleField $obj $node $name $key }
		'PeriodField'              { Decompile-SimpleField $obj $node $name $key }
		'DendrogramField'          { Decompile-SimpleField $obj $node $name $key }
		'GanttChartField' {
			Decompile-SimpleField $obj $node $name $key
			# Вложенная <Table> (полноценная таблица) — переиспользуем общий Decompile-Element.
			# Ключ ganttTable (не 'table' — во избежание коллизии с тип-ключом таблицы в диспетчере).
			$tblNode = $node.SelectSingleNode("lf:Table", $ns)
			if ($tblNode) { $obj['ganttTable'] = Decompile-Element $tblNode }
		}
	}
	# DisplayImportance — атрибут открывающего тега (адаптивная важность отображения), захват «как есть».
	$di = $node.GetAttribute("DisplayImportance"); if ($di) { $obj['displayImportance'] = $di }
	# title: "" — подавление авто-вывода: для типов, где компилятор вывел бы
	# заголовок из имени, а в оригинале <Title> отсутствует.
	if (-not $obj.Contains('title')) {
		$autoTitle = $false
		if ($tag -in @('LabelDecoration','Page','Popup')) { $autoTitle = $true }
		elseif ($tag -eq 'Button') { $autoTitle = -not ($obj.Contains('command') -or $obj.Contains('commandName') -or $obj.Contains('stdCommand')) }
		elseif ($tag -in @('InputField','CheckBoxField','RadioButtonField','LabelField','Table','CalendarField')) { $autoTitle = -not $obj.Contains('path') }
		if ($autoTitle) { $obj['title'] = '' }
	}
	Add-Layout $obj $node
	Add-GenericScalars $obj $node
	# extendedTooltip: companion <ExtendedTooltip> (это LabelDecoration). Текст-форма (только <Title>) →
	# строка/{text,formatted}. Own-content (layout/оформление/флаги/hyperlink/events) → объект { text?, … }.
	$etNode = $node.SelectSingleNode("lf:ExtendedTooltip", $ns)
	if ($etNode) {
		$etTitle = $etNode.SelectSingleNode("lf:Title", $ns)
		$textVal = if ($etTitle) { Get-MLFormattedValue $etTitle } else { $null }
		$etObj = [ordered]@{}
		Add-Layout $etObj $etNode
		Add-GenericScalars $etObj $etNode
		Add-Appearance $etObj $etNode
		# ToolTip самого компаньона (подсказка расширенной подсказки) — реальный текст (ML), не пустой Title
		$etTT = $etNode.SelectSingleNode("lf:ToolTip", $ns)
		if ($etTT) { $ttVal = Get-LangText $etTT; if ($null -ne $ttVal) { $etObj['tooltip'] = $ttVal } }
		if ((Get-Child $etNode 'Visible') -eq 'false') { $etObj['hidden'] = $true }
		if ((Get-Child $etNode 'Enabled') -eq 'false') { $etObj['disabled'] = $true }
		if ((Get-Child $etNode 'Hyperlink') -eq 'true') { $etObj['hyperlink'] = $true }
		# События компаньона (напр. URLProcessing у hyperlink-подсказки) — переиспользуем механизм событий элемента
		$etEv = Get-Events $etNode $name; if ($etEv) { $etObj['events'] = $etEv }
		if ($etObj.Count -gt 0) {
			if ($null -ne $textVal) {
				if ($textVal -is [System.Collections.IDictionary] -and $textVal.Contains('text')) { $etObj['text'] = $textVal['text']; if ($textVal['formatted']) { $etObj['formatted'] = $true } }
				else { $etObj['text'] = $textVal }
				# formatted ЯВНО из атрибута Title — компилятор не re-детектит markup на мультиязычном тексте
				if (-not $etObj.Contains('formatted') -and $etTitle -and $etTitle.GetAttribute('formatted') -eq 'true') { $etObj['formatted'] = $true }
			}
			$obj['extendedTooltip'] = $etObj
		} elseif ($null -ne $textVal) {
			$obj['extendedTooltip'] = $textVal
		}
	}
	# companion-панели с контентом: AutoCommandBar → commandBar, ContextMenu → contextMenu (любой элемент)
	$isDynListTable = ($tag -eq 'Table') -and (Has-Child $node 'UpdateOnDataChange')
	$cb = Decompile-CompanionPanel $node 'AutoCommandBar' $isDynListTable
	if ($null -ne $cb) { $obj['commandBar'] = $cb }
	$cm = Decompile-CompanionPanel $node 'ContextMenu'
	if ($null -ne $cm) { $obj['contextMenu'] = $cm }
	return $obj
}

# ─────────────────────────────────────────────────────────────────────────────
# Planner design-time <Settings xsi:type="pl:Planner"> → объект planner на реквизите.
# Полный захват каждого поля (раундтрип бит-в-бит); зеркало Emit-PlannerSettings.
function PLD-Bool { param($v) if ($null -eq $v) { return $null } return ($v -eq 'true') }
function PLD-Int  { param($v) if ($null -eq $v) { return $null } return [int]$v }
# <pl:value> → текст (тип xr:DesignTimeRef/xs:string выводится компилятором из вида значения); nil → $null.
function Get-PlannerValue {
	param($node)
	if (-not $node) { return $null }
	if ($node.GetAttribute('nil', $NS_XSI) -eq 'true') { return $null }
	if ($node.InnerText) { return $node.InnerText } else { return $null }
}
function Build-PlannerFont {
	param($node)
	if (-not $node) { return $null }
	$o = [ordered]@{}
	foreach ($a in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) {
		$av = $node.GetAttribute($a); if ($av -ne '') { $o[$a] = $av }
	}
	if ($o.Count -eq 0) { return $null }
	return $o
}
function Build-PlannerBorder {
	param($node)
	if (-not $node) { return $null }
	$o = [ordered]@{}
	$w = $node.GetAttribute('width'); if ($w -ne '') { $o['width'] = [int]$w }
	$st = $node.SelectSingleNode("*[local-name()='style']"); if ($st) { $o['style'] = $st.InnerText }
	return $o
}
function Build-PlannerItem {
	param($itn)
	$o = [ordered]@{}
	$valNode = $itn.SelectSingleNode("*[local-name()='value']")
	if ($valNode -and $valNode.GetAttribute('nil', $NS_XSI) -ne 'true' -and $valNode.InnerText) { $o['value'] = $valNode.InnerText }
	$o['text'] = (Get-Child $itn 'text')
	$tt = Get-Child $itn 'tooltip'; if ($tt) { $o['tooltip'] = $tt }
	$o['begin'] = (Get-Child $itn 'begin')
	$o['end'] = (Get-Child $itn 'end')
	$o['borderColor'] = (Get-Child $itn 'borderColor')
	$o['backColor'] = (Get-Child $itn 'backColor')
	$o['textColor'] = (Get-Child $itn 'textColor')
	$fnt = Build-PlannerFont ($itn.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt }
	$o['replacementDate'] = (Get-Child $itn 'replacementDate')
	$o['deleted'] = (PLD-Bool (Get-Child $itn 'deleted'))
	$o['id'] = (Get-Child $itn 'id')
	$o['textFormatted'] = (PLD-Bool (Get-Child $itn 'textFormatted'))
	$brd = Build-PlannerBorder ($itn.SelectSingleNode("*[local-name()='border']")); if ($brd) { $o['border'] = $brd }
	$o['editMode'] = (Get-Child $itn 'editMode')
	return $o
}
function Build-PlannerDimElement {
	param($eln)
	$o = [ordered]@{}
	$v = Get-PlannerValue ($eln.SelectSingleNode("*[local-name()='value']")); if ($null -ne $v) { $o['value'] = $v }
	$o['text'] = (Get-Child $eln 'text')
	$o['borderColor'] = (Get-Child $eln 'borderColor')
	$o['backColor'] = (Get-Child $eln 'backColor')
	$o['textColor'] = (Get-Child $eln 'textColor')
	$fnt = Build-PlannerFont ($eln.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt }
	$subs = New-Object System.Collections.ArrayList
	foreach ($s in @($eln.SelectNodes("*[local-name()='item']"))) { [void]$subs.Add((Build-PlannerDimElement $s)) }
	if ($subs.Count -gt 0) { $o['elements'] = @($subs) }
	$sos = Get-Child $eln 'showOnlySubordinatesAreas'; if ($null -ne $sos) { $o['showOnlySubordinatesAreas'] = ($sos -eq 'true') }
	$o['textFormatted'] = (PLD-Bool (Get-Child $eln 'textFormatted'))
	return $o
}
function Build-PlannerDimension {
	param($dn)
	$o = [ordered]@{}
	$v = Get-PlannerValue ($dn.SelectSingleNode("*[local-name()='value']")); if ($null -ne $v) { $o['value'] = $v }
	$o['text'] = (Get-Child $dn 'text')
	$o['borderColor'] = (Get-Child $dn 'borderColor')
	$o['backColor'] = (Get-Child $dn 'backColor')
	$o['textColor'] = (Get-Child $dn 'textColor')
	$fnt = Build-PlannerFont ($dn.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt }
	$els = New-Object System.Collections.ArrayList
	foreach ($e in @($dn.SelectNodes("*[local-name()='item']"))) { [void]$els.Add((Build-PlannerDimElement $e)) }
	if ($els.Count -gt 0) { $o['elements'] = @($els) }
	$o['textFormatted'] = (PLD-Bool (Get-Child $dn 'textFormatted'))
	return $o
}
function Build-PlannerLevel {
	param($lvn)
	$o = [ordered]@{}
	$o['measure'] = (Get-Child $lvn 'measure')
	$o['interval'] = (PLD-Int (Get-Child $lvn 'interval'))
	$o['show'] = (PLD-Bool (Get-Child $lvn 'show'))
	$lineNode = $lvn.SelectSingleNode("*[local-name()='line']")
	if ($lineNode) {
		$ln = [ordered]@{}
		$w = $lineNode.GetAttribute('width'); if ($w -ne '') { $ln['width'] = [int]$w }
		$g = $lineNode.GetAttribute('gap'); if ($g -ne '') { $ln['gap'] = ($g -eq 'true') }
		$st = $lineNode.SelectSingleNode("*[local-name()='style']"); if ($st) { $ln['style'] = $st.InnerText }
		$o['line'] = $ln
	}
	$o['scaleColor'] = (Get-Child $lvn 'scaleColor')
	$o['dayFormatRule'] = (Get-Child $lvn 'dayFormatRule')
	$fmtNode = $lvn.SelectSingleNode("*[local-name()='format']")
	if ($fmtNode) { $f = Get-LangText $fmtNode; if ($null -ne $f) { $o['format'] = $f } }
	$labelsNode = $lvn.SelectSingleNode("*[local-name()='labels']")
	if ($labelsNode) { $o['labels'] = [ordered]@{ ticks = (PLD-Int (Get-Child $labelsNode 'ticks')) } }
	$o['backColor'] = (Get-Child $lvn 'backColor')
	$o['textColor'] = (Get-Child $lvn 'textColor')
	$o['showPereodicalLabels'] = (PLD-Bool (Get-Child $lvn 'showPereodicalLabels'))
	return $o
}
function Build-PlannerTimeScale {
	param($tsn)
	$o = [ordered]@{}
	$o['placement'] = (Get-Child $tsn 'placement')
	$levels = New-Object System.Collections.ArrayList
	foreach ($lvn in @($tsn.SelectNodes("*[local-name()='level']"))) { [void]$levels.Add((Build-PlannerLevel $lvn)) }
	$o['levels'] = @($levels)
	$o['transparent'] = (PLD-Bool (Get-Child $tsn 'transparent'))
	$o['backColor'] = (Get-Child $tsn 'backColor')
	$o['textColor'] = (Get-Child $tsn 'textColor')
	$o['currentLevel'] = (PLD-Int (Get-Child $tsn 'currentLevel'))
	return $o
}
function Build-PlannerSettings {
	param($setNode)
	$pl = [ordered]@{}
	$itemNodes = @($setNode.SelectNodes("*[local-name()='item']"))
	if ($itemNodes.Count -gt 0) {
		$items = New-Object System.Collections.ArrayList
		foreach ($itn in $itemNodes) { [void]$items.Add((Build-PlannerItem $itn)) }
		$pl['items'] = @($items)
	}
	$dimNodes = @($setNode.SelectNodes("*[local-name()='dimension']"))
	if ($dimNodes.Count -gt 0) {
		$dims = New-Object System.Collections.ArrayList
		foreach ($dn in $dimNodes) { [void]$dims.Add((Build-PlannerDimension $dn)) }
		$pl['dimensions'] = @($dims)
	}
	$pl['borderColor'] = (Get-Child $setNode 'borderColor')
	$pl['backColor'] = (Get-Child $setNode 'backColor')
	$pl['textColor'] = (Get-Child $setNode 'textColor')
	$pl['lineColor'] = (Get-Child $setNode 'lineColor')
	$fnt = Build-PlannerFont ($setNode.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $pl['font'] = $fnt }
	$pl['beginOfRepresentationPeriod'] = (Get-Child $setNode 'beginOfRepresentationPeriod')
	$pl['endOfRepresentationPeriod'] = (Get-Child $setNode 'endOfRepresentationPeriod')
	$pl['alignElementsOfTimeScale'] = (PLD-Bool (Get-Child $setNode 'alignElementsOfTimeScale'))
	$pl['displayTimeScaleWrapHeaders'] = (PLD-Bool (Get-Child $setNode 'displayTimeScaleWrapHeaders'))
	$pl['displayWrapHeaders'] = (PLD-Bool (Get-Child $setNode 'displayWrapHeaders'))
	$wfNode = $setNode.SelectSingleNode("*[local-name()='timeScaleWrapHeadersFormat']")
	if ($wfNode) { $wf = Get-LangText $wfNode; if ($null -ne $wf) { $pl['timeScaleWrapHeadersFormat'] = $wf } }
	$pl['periodicVariantUnit'] = (Get-Child $setNode 'periodicVariantUnit')
	$pl['periodicVariantRepetition'] = (PLD-Int (Get-Child $setNode 'periodicVariantRepetition'))
	$pl['timeScaleWrapBeginIndent'] = (PLD-Int (Get-Child $setNode 'timeScaleWrapBeginIndent'))
	$pl['timeScaleWrapEndIndent'] = (PLD-Int (Get-Child $setNode 'timeScaleWrapEndIndent'))
	$tsNode = $setNode.SelectSingleNode("*[local-name()='timeScale']")
	if ($tsNode) { $pl['timeScale'] = (Build-PlannerTimeScale $tsNode) }
	$perNode = $setNode.SelectSingleNode("*[local-name()='period']")
	if ($perNode) { $pl['period'] = [ordered]@{ begin = (Get-Child $perNode 'begin'); end = (Get-Child $perNode 'end') } }
	$pl['displayCurrentDate'] = (PLD-Bool (Get-Child $setNode 'displayCurrentDate'))
	$pl['itemsTimeRepresentation'] = (Get-Child $setNode 'itemsTimeRepresentation')
	$pl['itemsBehaviorWhenSpaceInsufficient'] = (Get-Child $setNode 'itemsBehaviorWhenSpaceInsufficient')
	$pl['autoMinColumnWidth'] = (PLD-Bool (Get-Child $setNode 'autoMinColumnWidth'))
	$pl['autoMinRowHeight'] = (PLD-Bool (Get-Child $setNode 'autoMinRowHeight'))
	$pl['minColumnWidth'] = (PLD-Int (Get-Child $setNode 'minColumnWidth'))
	$pl['minRowHeight'] = (PLD-Int (Get-Child $setNode 'minRowHeight'))
	$pl['fixDimensionsHeader'] = (Get-Child $setNode 'fixDimensionsHeader')
	$pl['fixTimeScaleHeader'] = (Get-Child $setNode 'fixTimeScaleHeader')
	$brd = Build-PlannerBorder ($setNode.SelectSingleNode("*[local-name()='border']")); if ($brd) { $pl['border'] = $brd }
	$pl['newItemsTextType'] = (Get-Child $setNode 'newItemsTextType')
	return $pl
}

# ─────────────────────────────────────────────────────────────────────────────
# Chart design-time <Settings xsi:type="d4p1:Chart"> → объект chart. Генерик-движок:
# рекурсивный захват поддерева d4p1; структуры (line/border/font/ML/области/серии)
# детектируются по форме узла. Малые name-set'ы: ML-поля (даже ru-only/пустые → ML),
# серии (всегда массив), attrs-узлы. Порядок ключей JSON = порядок XML (раундтрип).
$CHART_ML_FIELDS = @{ 'title'=1;'lbFormat'=1;'lbpFormat'=1;'vsFormat'=1;'dtFormat'=1;'dataSourceDescription'=1;'labelFormat'=1;'text'=1 }
$CHART_SERIES_FIELDS = @{ 'realSeriesData'=1;'realExSeriesData'=1;'realPointData'=1;'realDataItems'=1 }
$CHART_ATTR_FIELDS = @{ 'gaugeQualityBands'=1 }
function Conv-ChartScalar {
	param([string]$v)
	if ($v -eq 'true') { return $true }
	if ($v -eq 'false') { return $false }
	return $v
}
function Build-ChartNode {
	param($n, [string]$name)
	# ML-поле → строка/мапа/"" (даже ru-only форсим в ML на эмите по имени)
	if ($CHART_ML_FIELDS.Contains($name)) {
		$ml = Get-LangText $n
		if ($null -eq $ml) { return '' } else { return $ml }
	}
	$kids = @($n.SelectNodes("*"))
	if ($kids.Count -eq 0) {
		# лист: attrs-only (шрифт/gaugeQualityBands) или текст
		$attrs = @($n.Attributes | Where-Object { $_.Name -ne 'xmlns' -and -not $_.Name.StartsWith('xmlns:') -and $_.Name -ne 'xsi:type' -and $_.Name -ne 'xsi:nil' })
		if ($attrs.Count -gt 0) {
			$o = [ordered]@{}; foreach ($a in $attrs) { $o[$a.Name] = (Conv-ChartScalar $a.Value) }; return $o
		}
		return (Conv-ChartScalar $n.InnerText)
	}
	# line/border: дочерний v8ui:style (+ width[/gap])
	$styleChild = $n.SelectSingleNode("*[local-name()='style']")
	if ($styleChild) {
		$o = [ordered]@{}
		$w = $n.GetAttribute('width'); if ($w -ne '') { $o['width'] = [int]$w }
		$g = $n.GetAttribute('gap'); if ($g -ne '') { $o['gap'] = ($g -eq 'true') }
		$o['style'] = $styleChild.InnerText
		return $o
	}
	# вложенный объект d4p1 (область/шкала/titleArea/серия): группируем детей по имени
	$o = [ordered]@{}
	foreach ($c in $kids) {
		$ln = $c.LocalName
		$val = Build-ChartNode $c $ln
		if ($CHART_SERIES_FIELDS.Contains($ln)) {
			if (-not $o.Contains($ln)) { $o[$ln] = New-Object System.Collections.ArrayList }
			[void]$o[$ln].Add($val)
		} elseif ($o.Contains($ln)) {
			if ($o[$ln] -isnot [System.Collections.IList]) { $tmp = New-Object System.Collections.ArrayList; [void]$tmp.Add($o[$ln]); $o[$ln] = $tmp }
			[void]$o[$ln].Add($val)
		} else {
			$o[$ln] = $val
		}
	}
	# нормализуем ArrayList → @() для сериализации
	foreach ($k in @($o.Keys)) { if ($o[$k] -is [System.Collections.ArrayList]) { $o[$k] = @($o[$k]) } }
	return $o
}
function Build-ChartSettings {
	param($setNode)
	return (Build-ChartNode $setNode '')
}

# --- 5. Form-level assembly ---
$dsl = [ordered]@{}

$titleNode = $root.SelectSingleNode("lf:Title", $ns)
if ($titleNode) { $t = Get-LangText $titleNode; if ($null -ne $t) { $dsl['title'] = $t } }

# properties (прямые скаляры под <Form>, PascalCase → camelCase)
$KNOWN_FORM_PROPS = @('AutoTitle','ReportResult','DetailsData','ReportFormType','AutoShowState','ReportResultViewMode','ViewModeApplicationOnSetReportResult','WindowOpeningMode','CommandBarLocation','SaveDataInSettings','AutoSaveDataInSettings','AutoTime','UsePostingMode','RepostOnWrite','AutoURL','AutoFillCheck','Customizable','EnterKeyBehavior','VerticalScroll','Width','Height','Group','UseForFoldersAndItems','SaveWindowSettings','ScalingMode','VerticalSpacing','VariantAppearance','ShowCloseButton','HorizontalAlign','ChildrenAlign','ShowTitle','ConversationsRepresentation','CollapseItemsByImportanceVariant','GroupList','ChildItemsWidth','VerticalAlign','HorizontalSpacing','CustomSettingsFolder','SettingsStorage','Enabled','Scale')
$props = [ordered]@{}
foreach ($pn in $KNOWN_FORM_PROPS) {
	$v = Get-Child $root $pn
	if ($null -ne $v) {
		$camel = $pn.Substring(0,1).ToLower() + $pn.Substring(1)
		if ($v -eq 'true') { $props[$camel] = $true }
		elseif ($v -eq 'false') { $props[$camel] = $false }
		elseif ($v -match '^\d+$') { $props[$camel] = [int]$v }
		else { $props[$camel] = $v }
	}
}
# Ссылка на члена формы по id ("N:uuid") в groupList/customSettingsFolder НЕ воспроизводима: наш
# компилятор переназначает id, и ссылка указала бы не туда (либо вовсе dangling — платформа сама их
# часто не разрешает). Резолв в имя ненадёжен (N не всегда соответствует named-элементу). Игнорируем
# с предупреждением (имя задаётся вручную через form-edit). Имя-форма захватывается как есть.
foreach ($refKey in 'groupList','customSettingsFolder') {
	if ($props.Contains($refKey) -and "$($props[$refKey])" -match '^\d+:[0-9a-fA-F]{8}-') {
		[Console]::Error.WriteLine("form-decompile: $refKey = '$($props[$refKey])' — ссылка на члена формы по id, не воспроизводима (id переназначаются), опущена. Задайте по имени через form-edit.")
		$props.Remove($refKey)
	}
}
# AutoTitle при наличии title: компилятор инъектит false (~95% форм). Зеркалим:
#  - оригинал имеет AutoTitle=false → опускаем ключ (компилятор реинъектит при наличии title);
#  - оригинал НЕ имеет AutoTitle (редкие 5%, напр. вспом. формы) → суппресс-маркер "" (не инъектить).
if ($dsl.Contains('title')) {
	if (-not $props.Contains('autoTitle')) { $props['autoTitle'] = '' }
	elseif ($props['autoTitle'] -eq $false) { $props.Remove('autoTitle') }
}
if ($props.Count -gt 0) { $dsl['properties'] = $props }

# MobileDeviceCommandBarContent (form-level) → список имён командных панелей/кнопок
$mdcb = $root.SelectSingleNode("lf:MobileDeviceCommandBarContent", $ns)
if ($mdcb) {
	$names = New-Object System.Collections.ArrayList
	foreach ($it in @($mdcb.SelectNodes("xr:Item", $ns))) {
		$v = $it.SelectSingleNode("xr:Value", $ns); if ($v) { [void]$names.Add($v.InnerText) }
	}
	if ($names.Count -gt 0) { $dsl['mobileCommandBarContent'] = @($names) }
}

# excludedCommands (form-level <CommandSet>)
$csForm = $root.SelectSingleNode("lf:CommandSet", $ns)
if ($csForm) {
	$excForm = New-Object System.Collections.ArrayList
	foreach ($ec in @($csForm.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$excForm.Add($ec.InnerText) }
	if ($excForm.Count -gt 0) { $dsl['excludedCommands'] = @($excForm) }
}

# events (form-level)
$evForm = Get-Events $root $null
if ($evForm) {
	# form-level: компилятор хранит как {Event: handler} напрямую
	$evMap = [ordered]@{}
	$evNode = $root.SelectSingleNode("lf:Events", $ns)
	foreach ($e in @($evNode.SelectNodes("lf:Event", $ns))) { $evMap[$e.GetAttribute("name")] = $e.InnerText }
	if ($evMap.Count -gt 0) { $dsl['events'] = $evMap }
}

# Зеркало компилятор-эвристики B3 (Compute-MainAcbAutofill): наличие cmdBar-элемента где-либо
# в дереве → форменный AutoCommandBar autofill=false. Нужно, чтобы предсказать вывод и выразить
# отклонение (голый корень при наличии cmdBar).
function Test-AnyCmdBar {
	param($list)
	if (-not $list) { return $false }
	foreach ($e in $list) {
		if ($e -is [System.Collections.IDictionary] -and $e.Contains('cmdBar')) { return $true }
		if ($e -is [System.Collections.IDictionary]) {
			if ($e.Contains('children') -and (Test-AnyCmdBar $e['children'])) { return $true }
			if ($e.Contains('columns') -and (Test-AnyCmdBar $e['columns'])) { return $true }
		}
	}
	return $false
}

# elements (+ форменный AutoCommandBar как autoCmdBar-элемент, если у него есть содержимое/отклонение)
$elemList = New-Object System.Collections.ArrayList
$elements = Decompile-Children $root
$formHasCmdBar = Test-AnyCmdBar $elements
$acb = $root.SelectSingleNode("lf:AutoCommandBar", $ns)
if ($acb) {
	$haln = Get-Child $acb 'HorizontalAlign'
	$acbAutofill = Get-Child $acb 'Autofill'
	$acbDI = $acb.GetAttribute("DisplayImportance")   # адаптивная важность форменной панели (атрибут тега)
	$acbKids = Decompile-Children $acb
	$acbObj = $null
	if ($haln -or ($acbAutofill -eq 'false') -or $acbKids -or $acbDI) {
		$acbObj = [ordered]@{}
		$acbObj['autoCmdBar'] = $acb.GetAttribute("name")
		if ($acbDI) { $acbObj['displayImportance'] = $acbDI }
		if ($haln) { $acbObj['horizontalAlign'] = $haln }
		if ($acbAutofill -eq 'false') { $acbObj['autofill'] = $false }
		if ($acbKids) { $acbObj['children'] = $acbKids }
	} elseif ($formHasCmdBar -and $null -eq $acbAutofill) {
		# Корень голый (autofill=true по умолчанию), но эвристика B3 дала бы false (есть cmdBar) → маркер
		$acbObj = [ordered]@{}
		$acbObj['autoCmdBar'] = $acb.GetAttribute("name")
		$acbObj['autofill'] = $true
	}
	if ($acbObj) { [void]$elemList.Add($acbObj) }
}
if ($elements) { foreach ($e in $elements) { [void]$elemList.Add($e) } }
if ($elemList.Count -gt 0) { $dsl['elements'] = @($elemList) }

# attributes
# Объектный тип (зеркало Test-IsObjectLikeType компилятора) — кандидат на авто-main эвристики 11b.3.
function Test-IsObjectLikeTypeDec([string]$type) {
	if ([string]::IsNullOrEmpty($type)) { return $false }
	if ($type -eq 'DynamicList' -or $type -eq 'ConstantsSet') { return $true }
	return ($type -match '^(CatalogObject|DocumentObject|DataProcessorObject|ReportObject|ExternalDataProcessorObject|ExternalReportObject|BusinessProcessObject|TaskObject|ChartOfAccountsObject|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesObject|ExchangePlanObject|InformationRegisterRecordSet|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|CalculationRegisterRecordSet|InformationRegisterRecordManager)\.')
}
$attrsNode = $root.SelectSingleNode("lf:Attributes", $ns)
if ($attrsNode) {
	$attrs = New-Object System.Collections.ArrayList
	# Подавление авто-main (эвристика компилятора 11b.3): если НЕТ ни одного <MainAttribute> И ровно
	# один реквизит объектного типа — компилятор пометит его main. В оригинале он НЕ main (раз тега нет)
	# → ставим суппресс-маркер main:false. На формах с >1 объектным реквизитом / с явным main — не нужно.
	$allAttrNodes = @($attrsNode.SelectNodes("lf:Attribute", $ns))
	$anyMainAttr = $false; $objLikeNodes = @()
	foreach ($an in $allAttrNodes) {
		if ((Get-Child $an 'MainAttribute') -eq 'true') { $anyMainAttr = $true }
		$atype = Decompile-Type ($an.SelectSingleNode("lf:Type", $ns))
		if (Test-IsObjectLikeTypeDec "$atype") { $objLikeNodes += $an }
	}
	$suppressMainName = if ((-not $anyMainAttr) -and $objLikeNodes.Count -eq 1) { $objLikeNodes[0].GetAttribute("name") } else { $null }
	foreach ($a in $allAttrNodes) {
		$ao = [ordered]@{}
		$ao['name'] = $a.GetAttribute("name")
		$ty = Decompile-Type ($a.SelectSingleNode("lf:Type", $ns)); if ($ty) { $ao['type'] = $ty }
		# valueType: <Settings xsi:type="v8:TypeDescription"> — уточнение типа значений ValueList
		# (та же грамматика типа). Дин-список Settings (xsi:type="DynamicList") обрабатывается отдельно.
		$setNode = $a.SelectSingleNode("lf:Settings", $ns)
		if ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'TypeDescription$') {
			$vt = Decompile-Type $setNode
			$ao['valueType'] = if ($vt) { $vt } else { '' }   # пустой Settings → маркер ""
		}
		# Planner design-time <Settings xsi:type="pl:Planner"> → объект planner (полный захват).
		elseif ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'Planner$') {
			$ao['planner'] = Build-PlannerSettings $setNode
		}
		# Chart/GanttChart design-time <Settings xsi:type="d4p1:Chart"/"d4p1:GanttChart"> → chart (генерик).
		elseif ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'd4p1:(Gantt)?Chart$') {
			$ao['chart'] = Build-ChartSettings $setNode
		}
		if ((Get-Child $a 'MainAttribute') -eq 'true') { $ao['main'] = $true }
		elseif ($suppressMainName -and $ao['name'] -eq $suppressMainName) { $ao['main'] = $false }
		$vw = Decompile-XrFlag $a 'View'; if ($null -ne $vw) { $ao['view'] = $vw }
		$ed = Decompile-XrFlag $a 'Edit'; if ($null -ne $ed) { $ao['edit'] = $ed }
		# Title атрибута. Компилятор для не-main атрибута без ключа title додумывает заголовок
		# из имени. Поэтому: нет <Title> → суппресс-маркер ''; ru-only == авто-вывод → опускаем
		# ключ (компилятор воспроизведёт); иначе → явный заголовок.
		$isMain = ($ao['main'] -eq $true)   # именно true; main:false (суппресс-маркер) → не-main для Title
		$tNode = $a.SelectSingleNode("lf:Title", $ns)
		if ($tNode) {
			$t = Get-LangTextWS $tNode   # восстановление значимого пробела (whitespace-заголовок реквизита)
			if ($null -ne $t) {
				if ($isMain -or -not ($t -is [string]) -or $t -ne (Title-FromName $ao['name'])) { $ao['title'] = $t }
			}
		} elseif (-not $isMain) {
			$ao['title'] = ''
		}
		# SavedData: компилятор додумывает true для main-реквизита объектного типа (эвристика $mainSaved:
		# Catalog/Document/ChartOf*/ExchangePlan/BusinessProcess/Task Object + RecordManager). Если оригинал
		# тега не имеет — ставим суппресс-маркер savedData:false (как с MainAttribute).
		if ((Get-Child $a 'SavedData') -eq 'true') { $ao['savedData'] = $true }
		elseif ($ao['main'] -eq $true -and "$($ao['type'])" -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.|RecordManager\.') { $ao['savedData'] = $false }
		# Save: сохранение значения реквизита в пользовательских настройках. Один Field=имя → save:true;
		# иначе снимаем префикс "имя." (голое имя/UUID/прочее — как есть) → строка (1) или массив.
		$saveNode = $a.SelectSingleNode("lf:Save", $ns)
		if ($saveNode) {
			$nm = "$($ao['name'])"
			$flds = @($saveNode.SelectNodes("lf:Field", $ns) | ForEach-Object { $_.InnerText })
			if ($flds.Count -eq 1 -and $flds[0] -eq $nm) {
				$ao['save'] = $true
			} elseif ($flds.Count -gt 0) {
				# Снимаем префикс "имя." ТОЛЬКО когда остаток — простое подполе, которое компилятор
				# реинъектит (зеркало его условия: ≠имя, без точки, не UUID-ссылка ^N/M). Многоуровневый
				# путь "имя.Settings.Filter" и UUID-ссылка "имя.1/0:GUID" компилятор эмитит как есть
				# (префикс не вернёт) → храним ПОЛНЫЙ путь, иначе префикс теряется.
				$stripped = @($flds | ForEach-Object {
					if ($_ -match "^$([regex]::Escape($nm))\.([^.]+)$" -and $matches[1] -notmatch '^\d+/\d+') { $matches[1] } else { $_ }
				})
				if ($stripped.Count -eq 1) { $ao['save'] = $stripped[0] } else { $ao['save'] = $stripped }
			}
		}
		$fc = Get-Child $a 'FillCheck'; if ($fc) { $ao['fillCheck'] = $fc }
		$afo = Decompile-FunctionalOptions $a; if ($afo) { $ao['functionalOptions'] = $afo }
		$colsNode = $a.SelectSingleNode("lf:Columns", $ns)
		if ($colsNode) {
			$cols = New-Object System.Collections.ArrayList
			foreach ($c in @($colsNode.SelectNodes("lf:Column", $ns))) { [void]$cols.Add((Decompile-AttrColumn $c)) }
			if ($cols.Count -gt 0) { $ao['columns'] = @($cols) }
			# AdditionalColumns: доп. колонки табличных частей объекта (группа на табличную часть)
			$addNodes = @($colsNode.SelectNodes("lf:AdditionalColumns", $ns))
			if ($addNodes.Count -gt 0) {
				$addList = New-Object System.Collections.ArrayList
				foreach ($an in $addNodes) {
					$acObj = [ordered]@{}; $acObj['table'] = $an.GetAttribute("table")
					$acCols = New-Object System.Collections.ArrayList
					foreach ($c in @($an.SelectNodes("lf:Column", $ns))) { [void]$acCols.Add((Decompile-AttrColumn $c)) }
					$acObj['columns'] = @($acCols)
					[void]$addList.Add($acObj)
				}
				$ao['additionalColumns'] = @($addList)
			}
		}
		# UseAlways: поля, всегда читаемые. Префикс "ИмяРеквизита." снимаем.
		# ValueTable (есть columns): useAlways:true на совпавшей колонке; остальные → массив атрибута.
		# Дин-список/прочие (нет columns): массив useAlways на атрибуте.
		$uaNode = $a.SelectSingleNode("lf:UseAlways", $ns)
		if ($uaNode) {
			$prefix = "$($ao['name'])."
			$shorts = New-Object System.Collections.ArrayList
			foreach ($fn in @($uaNode.SelectNodes("lf:Field", $ns))) {
				$t = $fn.InnerText.Trim()
				# Снимаем префикс "ИмяРеквизита.". Маркер "~" (query-поле дин-списка) сохраняем,
				# префикс снимаем ПОСЛЕ него: ~Список.Остановлен → ~Остановлен (компилятор развернёт обратно).
				if ($t.StartsWith('~')) {
					$rest = $t.Substring(1)
					if ($rest.StartsWith($prefix)) { $rest = $rest.Substring($prefix.Length) }
					$t = "~$rest"
				} elseif ($t.StartsWith($prefix)) {
					$t = $t.Substring($prefix.Length)
				}
				[void]$shorts.Add($t)
			}
			if ($ao.Contains('columns')) {
				$rest = New-Object System.Collections.ArrayList
				foreach ($s in $shorts) {
					$col = $ao['columns'] | Where-Object { $_['name'] -eq $s } | Select-Object -First 1
					if ($col) { $col['useAlways'] = $true } else { [void]$rest.Add($s) }
				}
				if ($rest.Count -gt 0) { $ao['useAlways'] = @($rest) }
			} elseif ($shorts.Count -gt 0) {
				$ao['useAlways'] = @($shorts)
			}
		}
		# Settings динамического списка (только xsi:type=DynamicList; Planner/TypeDescription — выше)
		$setNode = $a.SelectSingleNode("lf:Settings", $ns)
		if ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'DynamicList$') {
			$so = [ordered]@{}
			# AutoFillAvailableFields — дефолт true, платформа эмитит только отклонение (false). Захват «как есть».
			$afaf = Get-Child $setNode 'AutoFillAvailableFields'; if ($null -ne $afaf) { $so['autoFillAvailableFields'] = ($afaf -eq 'true') }
			$mt = Get-Child $setNode 'MainTable'; if ($mt) { $so['mainTable'] = $mt }
			# GetInvisibleFieldPresentations — дефолт true, платформа эмитит только отклонение (false, корпус 20/20). Факт. значение.
			$gifp = Get-Child $setNode 'GetInvisibleFieldPresentations'; if ($null -ne $gifp) { $so['getInvisibleFieldPresentations'] = ($gifp -eq 'true') }
				# Ключ набора (query-based список): KeyType (RowNumber/FieldValue/RowKey) + KeyField* (0+).
				$kt = Get-Child $setNode 'KeyType'; if ($kt) { $so['keyType'] = $kt }
				$kfNodes = @($setNode.SelectNodes("lf:KeyField", $ns) | ForEach-Object { $_.InnerText })
				if ($kfNodes.Count -gt 0) { $so['keyFields'] = @($kfNodes) }
			# AutoSaveUserSettings — авто-сохранение польз. настроек дин-списка (в корпусе только false;
			# дефолт true → платформа эмитит отклонение). Захват факт. значения.
			$asus = Get-Child $setNode 'AutoSaveUserSettings'; if ($null -ne $asus) { $so['autoSaveUserSettings'] = ($asus -eq 'true') }
			$qtNode = $setNode.SelectSingleNode("lf:QueryText", $ns)
			$hasQ = [bool]($qtNode -and $qtNode.InnerText)
			if ($hasQ) { $so['query'] = Maybe-ExternalizeQuery -queryText $qtNode.InnerText -listName "$($ao['name'])" }
			# ManualQuery: компилятор выводит из наличия query (hasQuery → true). Платформа в редких
			# случаях (корпус 16) хранит QueryText при ManualQuery=false → фиксируем отклонение от эвристики.
			$mqV = Get-Child $setNode 'ManualQuery'
			if ($null -ne $mqV) { $mqActual = ($mqV -eq 'true'); if ($mqActual -ne $hasQ) { $so['manualQuery'] = $mqActual } }
			# DynamicDataRead: дефолт true → эмитим только false
			if ((Get-Child $setNode 'DynamicDataRead') -eq 'false') { $so['dynamicDataRead'] = $false }
			# Явные поля набора (редко, ~4.5%) — захват только при наличии Field
			$fieldNodes = @($setNode.SelectNodes("lf:Field", $ns))
			if ($fieldNodes.Count -gt 0) {
				$fields = New-Object System.Collections.ArrayList
				foreach ($fn in $fieldNodes) {
					$fo = [ordered]@{}
					$fld = Get-Child $fn 'field'
					$dp  = Get-Child $fn 'dataPath'
					if ($fld) { $fo['field'] = $fld }
					if ((Has-Child $fn 'dataPath') -and $dp -ne $fld) { $fo['dataPath'] = $dp }
					# Тип поля набора: DataSetFieldField (дефолт, компилятор хардкодит) vs
					# DataSetFieldNestedDataSet (поле-вложенный набор = реквизит табличной части). Маркер nested.
					$fTypeAttr = $fn.GetAttribute("type", $NS_XSI)
						if ($fTypeAttr -match 'NestedDataSet$') { $fo['nested'] = $true }
						elseif ($fTypeAttr -match 'Folder$') { $fo['folder'] = $true }
					$ftn = $fn.SelectSingleNode("dcssch:title", $ns)
					if ($ftn) { $t = Get-LangText $ftn; if ($null -ne $t) { $fo['title'] = $t } }
					# valueType поля набора (тип значения; вычисляемые/кастомные поля). Грамматика типа.
					$fvt = $fn.SelectSingleNode("dcssch:valueType", $ns)
					if ($fvt) { $fvtVal = Decompile-Type $fvt; if ($fvtVal) { $fo['valueType'] = $fvtVal } }
					# presentationExpression (выражение представления поля) + appearance (формат/оформление поля)
					$fpe = Get-Child $fn 'presentationExpression'
					if ($null -ne $fpe -and $fpe -ne '') { $fo['presentationExpression'] = $fpe }
					$fappNode = $fn.SelectSingleNode("dcssch:appearance", $ns)
					if ($fappNode) { $fap = Get-SettingsAppearance $fappNode; if ($fap -and $fap.Count -gt 0) { $fo['appearance'] = $fap } }
					$furNode = $fn.SelectSingleNode("dcssch:useRestriction", $ns)
					if ($furNode) { $fur = Build-RestrictObj $furNode; if ($fur.Count -gt 0) { $fo['useRestriction'] = $fur } }
					$faurNode = $fn.SelectSingleNode("dcssch:attributeUseRestriction", $ns)
					if ($faurNode) { $faur = Build-RestrictObj $faurNode; if ($faur.Count -gt 0) { $fo['attributeUseRestriction'] = $faur } }
					$fipNode = $fn.SelectSingleNode("dcssch:inputParameters", $ns)
					if ($fipNode) { $fip = Build-DLInputParameters $fipNode; if (@($fip).Count -gt 0) { $fo['inputParameters'] = $fip } }
					[void]$fields.Add($fo)
				}
				$so['fields'] = @($fields)
			}
			# Вычисляемые поля DataSet (<CalculatedField>) — после Field*, до Parameter*.
			$calcNodes = @($setNode.SelectNodes("lf:CalculatedField", $ns))
			if ($calcNodes.Count -gt 0) {
				$cfs = New-Object System.Collections.ArrayList
				foreach ($cn in $calcNodes) { [void]$cfs.Add((Build-CalcField $cn)) }
				$so['calculatedFields'] = @($cfs)
			}
			# Schema-параметры дин-списка (прямые <Parameter> под Settings, не в ListSettings)
			$paramNodes = @($setNode.SelectNodes("lf:Parameter", $ns))
			if ($paramNodes.Count -gt 0) {
				$dlPars = New-Object System.Collections.ArrayList
				foreach ($pn in $paramNodes) { [void]$dlPars.Add((Build-DLParameter $pn)) }
				$so['parameters'] = @($dlPars)
			}
			# ListSettings: пустой скелет (только viewMode+GUID) опускаем — компилятор
			# регенерит каноничный скелет. Захватываем только контейнеры с реальными
			# dcsset:item (filter/order/conditionalAppearance) в формат компилятора.
			$lsNode = $setNode.SelectSingleNode("lf:ListSettings", $ns)
			if ($lsNode) {
				$fNode = $lsNode.SelectSingleNode("dcsset:filter", $ns)
				if ($fNode -and $fNode.SelectSingleNode("dcsset:item", $ns)) {
					$flt = @()
					foreach ($fc in $fNode.SelectNodes("dcsset:item", $ns)) {
						$bi = (Build-FilterItem -itemNode $fc -loc "settings/filter")
						if ($null -ne $bi) { $flt += $bi }
					}
					if ($flt.Count -gt 0) { $so['filter'] = @($flt) }
				}
				$oNode = $lsNode.SelectSingleNode("dcsset:order", $ns)
				if ($oNode -and $oNode.SelectSingleNode("dcsset:item", $ns)) {
					$ord = Build-Order -ordNode $oNode -loc "settings/order"
					if (@($ord).Count -gt 0) { $so['order'] = @($ord) }
				}
				$caNode = $lsNode.SelectSingleNode("dcsset:conditionalAppearance", $ns)
				if ($caNode -and $caNode.SelectSingleNode("dcsset:item", $ns)) {
					$ca = Build-ConditionalAppearance -caNode $caNode -loc "settings/conditionalAppearance"
					if (@($ca).Count -gt 0) { $so['conditionalAppearance'] = @($ca) }
				}
				# Параметры данных компоновки (dcsset:dataParameters) — значения параметров запроса в
				# настройках. Грамматика как у СКД (shorthand "Имя @off" / объект). См. Build-FormDataParameters.
				$dpNode = $lsNode.SelectSingleNode("dcsset:dataParameters", $ns)
				if ($dpNode -and $dpNode.SelectSingleNode("dcscor:item", $ns)) {
					$dp = Build-FormDataParameters $dpNode
					if (@($dp).Count -gt 0) { $so['dataParameters'] = @($dp) }
				}
				# Форма скелета ListSettings: дескриптор только для НЕ-каноничных форм (частичные/минимальные).
				# Канон → $null (компилятор регенерит полный скелет, как раньше).
				# Группировка строк списка: прямой <dcsset:item> ListSettings (не в filter/order/CA).
				$grpItemNode = $lsNode.SelectSingleNode("dcsset:item", $ns)
				$grouping = $null
				if ($grpItemNode) { $grouping = Build-ListGrouping $grpItemNode }
				if ($null -ne $grouping) { $so['grouping'] = $grouping }
				$lsShape = Get-ListSettingsShape $lsNode ($null -ne $grouping)
				if ($null -ne $lsShape) { $so['listSettings'] = $lsShape }
			}
			if ($so.Count -gt 0) { $ao['settings'] = $so }
		}
		[void]$attrs.Add($ao)
	}
	if ($attrs.Count -gt 0) { $dsl['attributes'] = @($attrs) }
}

# conditionalAppearance формы (<ConditionalAppearance> — последний child <Attributes>;
# та же DCS-грамматика, что settings.conditionalAppearance → переиспользуем Build-ConditionalAppearance)
if ($attrsNode) {
	$caNode = $attrsNode.SelectSingleNode("lf:ConditionalAppearance", $ns)
	if ($caNode) {
		$ca = Build-ConditionalAppearance -caNode $caNode -loc "form/conditionalAppearance"
		if (@($ca).Count -gt 0) { $dsl['conditionalAppearance'] = @($ca) }
	}
}

# parameters
$parsNode = $root.SelectSingleNode("lf:Parameters", $ns)
if ($parsNode) {
	$pars = New-Object System.Collections.ArrayList
	foreach ($p in @($parsNode.SelectNodes("lf:Parameter", $ns))) {
		$po = [ordered]@{}; $po['name'] = $p.GetAttribute("name")
		$ty = Decompile-Type ($p.SelectSingleNode("lf:Type", $ns)); if ($ty) { $po['type'] = $ty }
		if ((Get-Child $p 'KeyParameter') -eq 'true') { $po['key'] = $true }
		[void]$pars.Add($po)
	}
	if ($pars.Count -gt 0) { $dsl['parameters'] = @($pars) }
}

# commands
$cmdsNode = $root.SelectSingleNode("lf:Commands", $ns)
if ($cmdsNode) {
	$cmds = New-Object System.Collections.ArrayList
	foreach ($c in @($cmdsNode.SelectNodes("lf:Command", $ns))) {
		$co = [ordered]@{}; $co['name'] = $c.GetAttribute("name")
		$act = Get-Child $c 'Action'; if ($act) { $co['action'] = $act }
		if ((Get-Child $c 'ModifiesSavedData') -eq 'true') { $co['modifiesSavedData'] = $true }
		# Заголовок команды: есть <Title> → захват; нет → суппресс-маркер "" (иначе компилятор
		# додумает из имени — авто-вывод неверен для ~0.13% команд без заголовка в оригинале).
		$tNode = $c.SelectSingleNode("lf:Title", $ns)
		if ($tNode) { $t = Get-LangText $tNode; if ($null -ne $t) { $co['title'] = $t } }
		else { $co['title'] = '' }
		$ttNode = $c.SelectSingleNode("lf:ToolTip", $ns); if ($ttNode) { $t = Get-LangText $ttNode; if ($null -ne $t) { $co['tooltip'] = $t } }
		$us = Decompile-XrFlag $c 'Use'; if ($null -ne $us) { $co['use'] = $us }
		$cfo = Decompile-FunctionalOptions $c; if ($cfo) { $co['functionalOptions'] = $cfo }
		$cru = Get-Child $c 'CurrentRowUse'; if ($cru) { $co['currentRowUse'] = $cru }
		# Используемая таблица — ссылка по имени элемента-таблицы (<AssociatedTableElementId xsi:type="xs:string">Имя</…>)
		$ate = Get-Child $c 'AssociatedTableElementId'; if ($ate) { $co['table'] = $ate }
		$sc = Get-Child $c 'Shortcut'; if ($sc) { $co['shortcut'] = $sc }
		Set-CommandPicture $co $c
		$rep = Get-Child $c 'Representation'; if ($rep) { $co['representation'] = $rep }
		[void]$cmds.Add($co)
	}
	if ($cmds.Count -gt 0) { $dsl['commands'] = @($cmds) }
}

# commandInterface (форменный <CommandInterface> — последний дочерний Form)
$ci = Decompile-CommandInterface
if ($null -ne $ci) { $dsl['commandInterface'] = $ci }

# --- 6. Output ---
$json = ConvertTo-CompactJson -obj $dsl
if ($OutputPath) {
	[System.IO.File]::WriteAllText($OutputPath, $json, (New-Object System.Text.UTF8Encoding($false)))
	Save-QueryFiles
	Write-Host "form-decompile: $OutputPath"
} else {
	Write-Output $json
}
