# meta-compile v1.102 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
	[Parameter(Mandatory)]
	[string]$JsonPath,

	[Parameter(Mandatory)]
	[string]$OutputDir
)

$ErrorActionPreference = "Stop"

# --- Разбор пользовательского JSON ---
# Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу
# идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только
# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то,
# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске.
# Возврат через -NoEnumerate: без него одноэлементный
# JSON-массив разворачивался бы в скаляр вторым анруллингом.
function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) {
	try {
		# PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null,
		# тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково.
		if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' }
		$parsed = $text | ConvertFrom-Json
	} catch {
		$what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" }
		if ($Inline) {
			$got = ($text -replace '\s+', ' ').Trim()
			$label = 'got'
			if (-not $got) { $got = '(empty)' }
			elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) }
			$what = "${what}, ${label}: ${got}"
		}
		[Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))")
		exit 1
	}
	Write-Output -NoEnumerate $parsed
}

# --- Чтение входного JSON-файла ---
# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8:
# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого
# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем:
# угаданное имя уйдёт в метаданные так же молча.
function Read-JsonInputFile([string]$path) {
	# Проверка здесь, а не по навыкам: часть навыков проверяла путь сама, часть — нет, и один и тот
	# же промах давал то внятную строку, то дамп MethodInvocationException. Навыки со своей
	# проверкой срабатывают раньше и сохраняют свой текст.
	if (-not (Test-Path -LiteralPath $path)) {
		[Console]::Error.WriteLine("[ERROR] File not found: $path")
		exit 1
	}
	if (Test-Path -LiteralPath $path -PathType Container) {
		[Console]::Error.WriteLine("[ERROR] Expected a JSON file, got a directory: $path")
		exit 1
	}
	$bytes = [System.IO.File]::ReadAllBytes($path)
	if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
		return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3)
	}
	if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
		return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2)
	}
	if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) {
		return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2)
	}
	try {
		return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes)
	} catch {
		$detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message }
		[Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16")
		exit 1
	}
}
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

# --- 1. Load and validate JSON ---

if (-not (Test-Path $JsonPath)) {
	Write-Error "File not found: $JsonPath"
	exit 1
}

$json = Read-JsonInputFile $JsonPath
$def = ConvertFrom-JsonInput $json $JsonPath

# --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
# read-only configs unless allowed. Trigger = bin present; reaction from
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
# throws — guard errors degrade to allow.
function Get-RootUuid([string]$xmlPath) {
	if (-not (Test-Path $xmlPath)) { return $null }
	try {
		[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
		$el = $mx.DocumentElement.FirstChild
		while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
		if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
	} catch {}
	return $null
}
function Test-ExternalObjectRoot([string]$xmlPath) {
	if (-not (Test-Path $xmlPath)) { return $false }
	try {
		[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
		$el = $mx.DocumentElement.FirstChild
		while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
		if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
	} catch {}
	return $false
}
function Find-V8Project([string]$startDir) {
	$d = $startDir
	for ($i = 0; $i -lt 20 -and $d; $i++) {
		$pj = Join-Path $d ".v8-project.json"
		if (Test-Path $pj) { return $pj }
		$parent = [System.IO.Path]::GetDirectoryName($d)
		if ($parent -eq $d) { break }
		$d = $parent
	}
	return $null
}
function Get-EditMode([string]$cfgDir) {
	try {
		$pj = Find-V8Project (Get-Location).Path
		if (-not $pj) { $pj = Find-V8Project $cfgDir }
		if (-not $pj) { return 'deny' }
		$proj = Get-Content -Raw $pj | ConvertFrom-Json
		$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
		if ($proj.databases) {
			foreach ($db in $proj.databases) {
				if ($db.configSrc) {
					$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
					if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
						if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
					}
				}
			}
		}
		if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
		return 'deny'
	} catch { return 'deny' }
}
function Assert-EditAllowed([string]$targetPath, [string]$require) {
	try {
		$rp = $targetPath
		try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
		# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
		if (Test-ExternalObjectRoot $rp) { return }
		$elemUuid = Get-RootUuid $rp
		$cfgDir = $null; $binPath = $null
		$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
		for ($i = 0; $i -lt 12 -and $d; $i++) {
			if (Test-ExternalObjectRoot "$d.xml") { return }
			if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
			if (-not $cfgDir) {
				$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
				if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
			}
			if ($elemUuid -and $cfgDir) { break }
			$parent = [System.IO.Path]::GetDirectoryName($d)
			if ($parent -eq $d) { break }
			$d = $parent
		}
		# New object (no element file): fall back to config root uuid.
		if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
		if (-not $binPath -or -not (Test-Path $binPath)) { return }
		$bytes = [System.IO.File]::ReadAllBytes($binPath)
		if ($bytes.Length -le 32) { return }
		$start = 0
		if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
		$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
		$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
		if (-not $hm.Success) { return }
		$G = [int]$hm.Groups[1].Value
		$K = [int]$hm.Groups[2].Value
		if ($K -eq 0) { return }
		$best = $null
		if ($elemUuid) {
			$u = [regex]::Escape($elemUuid.ToLower())
			foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
				$f1 = [int]$m.Groups[1].Value
				if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
			}
		}
		$blocked = $false; $code = ""; $reason = ""
		if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
		elseif ($require -eq 'removed') {
			if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
		}
		else {
			if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
		}
		if (-not $blocked) { return }
		$mode = Get-EditMode $cfgDir
		if ($mode -eq 'off') { return }
		# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
		# latter throws and would be swallowed by this function's own catch.
		if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
		$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
		$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
		$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
		if ($code -eq "capability-off") {
			$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
			$fix = "Либо снять защиту явно (навык support-edit, два шага):`n  1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n  2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n  Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
		} elseif ($code -eq "not-removed") {
			$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
			$fix = "Либо сначала снять объект с поддержки, затем удалять:`n  support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
		} else {
			$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
			$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n  support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n  support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
		}
		[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
		exit 1
	} catch { return }
}

Assert-EditAllowed $OutputDir 'editable'

# --- Batch mode: JSON array of objects ---
if ($def -is [array] -or ($null -ne $def -and $def.GetType().BaseType.Name -eq 'Array')) {
	$batchOk = 0
	$batchFail = 0
	$idx = 0
	foreach ($item in $def) {
		$idx++
		# Имя с GUID, а не "batch-$idx": фиксированное имя в общем %TEMP% сталкивало
		# два параллельных запуска навыка на одной машине — Set-Content падал с
		# «file is being used by another process». py-порт уже брал mkstemp.
		$tmpJson = Join-Path ([System.IO.Path]::GetTempPath()) "meta-compile-batch-$idx-$([guid]::NewGuid().ToString('N')).json"
		try {
			$item | ConvertTo-Json -Depth 20 | Set-Content -Encoding UTF8 $tmpJson
			$proc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$PSCommandPath`" -JsonPath `"$tmpJson`" -OutputDir `"$OutputDir`"" -NoNewWindow -Wait -PassThru
			if ($proc.ExitCode -eq 0) { $batchOk++ } else { $batchFail++ }
		} finally {
			Remove-Item $tmpJson -Force -ErrorAction SilentlyContinue
		}
	}
	Write-Host ""
	Write-Host "=== Batch: $idx objects, $batchOk compiled, $batchFail failed ==="
	if ($batchFail -gt 0) { exit 1 }
	exit 0
}

# Normalize field synonyms: accept "objectType" as alias for "type"
if (-not $def.type -and $def.objectType) {
	$def | Add-Member -NotePropertyName "type" -NotePropertyValue $def.objectType
}

# Object type synonyms (Russian → English)
$script:objectTypeSynonyms = @{
	"Справочник"              = "Catalog"
	"Каталог"                 = "Catalog"
	"Документ"                = "Document"
	"Перечисление"            = "Enum"
	"Константа"               = "Constant"
	"РегистрСведений"         = "InformationRegister"
	"РегистрНакопления"       = "AccumulationRegister"
	"РегистрБухгалтерии"      = "AccountingRegister"
	"РегистрРасчёта"          = "CalculationRegister"
	"РегистрРасчета"          = "CalculationRegister"
	"ПланСчетов"              = "ChartOfAccounts"
	"ПланВидовХарактеристик"  = "ChartOfCharacteristicTypes"
	"ПланВидовРасчёта"        = "ChartOfCalculationTypes"
	"ПланВидовРасчета"        = "ChartOfCalculationTypes"
	"БизнесПроцесс"           = "BusinessProcess"
	"Задача"                  = "Task"
	"ПланОбмена"              = "ExchangePlan"
	"ЖурналДокументов"        = "DocumentJournal"
	"Отчёт"                   = "Report"
	"Отчет"                   = "Report"
	"Обработка"               = "DataProcessor"
	"ОбщийМодуль"             = "CommonModule"
	"РегламентноеЗадание"     = "ScheduledJob"
	"ПодпискаНаСобытие"       = "EventSubscription"
	"HTTPСервис"              = "HTTPService"
	"ВебСервис"               = "WebService"
	"ОпределяемыйТип"         = "DefinedType"
	"ФункциональнаяОпция"     = "FunctionalOption"
}

# Enum property value synonyms — model often gets these slightly wrong
$script:enumValueAliases = @{
	# RegisterType (AccumulationRegister)
	"Balances"  = "Balance";  "Остатки" = "Balance";  "Обороты" = "Turnovers"
	# WriteMode (InformationRegister)
	"RecordSubordinate" = "RecorderSubordinate"; "Subordinate" = "RecorderSubordinate"
	"ПодчинениеРегистратору" = "RecorderSubordinate"; "Независимый" = "Independent"
	# DependenceOnCalculationTypes (ChartOfCalculationTypes)
	"NotDependOnCalculationTypes" = "DontUse"; "NoDependence" = "DontUse"; "NotUsed" = "DontUse"
	"Depend" = "OnActionPeriod"; "ПоПериодуДействия" = "OnActionPeriod"
	# InformationRegisterPeriodicity
	"None" = "Nonperiodical"; "Daily" = "Day"; "Monthly" = "Month"
	"Quarterly" = "Quarter"; "Yearly" = "Year"
	"Непериодический" = "Nonperiodical"; "Секунда" = "Second"; "День" = "Day"
	"Месяц" = "Month"; "Квартал" = "Quarter"; "Год" = "Year"
	"ПозицияРегистратора" = "RecorderPosition"
	# DataLockControlMode
	"Автоматический" = "Automatic"; "Управляемый" = "Managed"
	# FullTextSearch
	"Использовать" = "Use"; "НеИспользовать" = "DontUse"
	# Posting
	"Разрешить" = "Allow"; "Запретить" = "Deny"
	# EditType
	"ВДиалоге" = "InDialog"; "ВСписке" = "InList"; "ОбаСпособа" = "BothWays"
	# DefaultPresentation
	"ВВидеНаименования" = "AsDescription"; "ВВидеКода" = "AsCode"
	# FillChecking
	"НеПроверять" = "DontCheck"; "Ошибка" = "ShowError"; "Предупреждение" = "ShowWarning"
	# Indexing
	"НеИндексировать" = "DontIndex"; "Индексировать" = "Index"
	"ИндексироватьСДопУпорядочиванием" = "IndexWithAdditionalOrder"
}

# Valid enum values per property (from meta-validate)
$script:validEnumValues = @{
	"RegisterType"                   = @("Balance","Turnovers")
	"WriteMode"                      = @("Independent","RecorderSubordinate")
	"InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition")
	"DependenceOnCalculationTypes"   = @("DontUse","OnActionPeriod")
	"DataLockControlMode"            = @("Automatic","Managed")
	"FullTextSearch"                 = @("Use","DontUse")
	"DataHistory"                    = @("Use","DontUse")
	"DefaultPresentation"            = @("AsDescription","AsCode")
	"Posting"                        = @("Allow","Deny")
	"RealTimePosting"                = @("Allow","Deny")
	"EditType"                       = @("InDialog","InList","BothWays")
	"HierarchyType"                  = @("HierarchyFoldersAndItems","HierarchyOfItems")
	"CodeType"                       = @("String","Number")
	"CodeAllowedLength"              = @("Variable","Fixed")
	"NumberType"                     = @("String","Number")
	"NumberAllowedLength"            = @("Variable","Fixed")
	"RegisterRecordsDeletion"        = @("AutoDelete","AutoDeleteOnUnpost","AutoDeleteOff")
	"RegisterRecordsWritingOnPost"   = @("WriteModified","WriteSelected","WriteAll")
	"ReturnValuesReuse"              = @("DontUse","DuringRequest","DuringSession")
	"ReuseSessions"                  = @("DontUse","Use","AutoUse")
	"FillChecking"                   = @("DontCheck","ShowError","ShowWarning")
	"Indexing"                       = @("DontIndex","Index","IndexWithAdditionalOrder")
	"SubordinationUse"               = @("ToItems","ToFolders","ToFoldersAndItems")
	"CodeSeries"                     = @("WholeCatalog","WithinSubordination","WithinOwnerSubordination","WholeCharacteristicKind","WholeChartOfAccounts")
	"ChoiceMode"                     = @("BothWays","QuickChoice","FromForm")
	"CreateOnInput"                  = @("Auto","Use","DontUse")
	"ChoiceHistoryOnInput"           = @("Auto","DontUse")
	"PredefinedDataUpdate"           = @("Auto","DontAutoUpdate","AutoUpdate")
	"SearchStringModeOnInputByString"= @("Begin","AnyPart")
	"FullTextSearchOnInputByString"  = @("Use","DontUse")
	"Category"                       = @("NavigationPanel","ActionsPanel","FormCommandBar","FormNavigationPanel")
}

# --- Группы команд объекта (командный интерфейс) ---
# Группы командного интерфейса РАЗДЕЛА (панель навигации/действий): команда БЕЗ параметра. Группы формы: параметр доступен.
$script:sectionCommandGroups = @(
	"NavigationPanelImportant","NavigationPanelOrdinary","NavigationPanelSeeAlso",
	"ActionsPanelCreate","ActionsPanelReports","ActionsPanelTools"
)
$script:formCommandGroups = @(
	"FormCommandBarImportant","FormCommandBarCreateBasedOn",
	"FormNavigationPanelImportant","FormNavigationPanelGoTo","FormNavigationPanelSeeAlso"
)
$script:validCommandGroups = $script:sectionCommandGroups + $script:formCommandGroups
# Прощающий ввод: русские подписи групп → канон
$script:commandGroupAliases = @{
	"Панель навигации.Важное"                     = "NavigationPanelImportant"
	"Панель навигации.Обычное"                    = "NavigationPanelOrdinary"
	"Панель навигации.См. также"                  = "NavigationPanelSeeAlso"
	"Панель действий.Создать"                     = "ActionsPanelCreate"
	"Панель действий.Отчеты"                      = "ActionsPanelReports"
	"Панель действий.Отчёты"                      = "ActionsPanelReports"
	"Панель действий.Сервис"                      = "ActionsPanelTools"
	"Командная панель формы.Важное"               = "FormCommandBarImportant"
	"Командная панель формы.Создать на основании" = "FormCommandBarCreateBasedOn"
	"Панель навигации формы.Важное"               = "FormNavigationPanelImportant"
	"Панель навигации формы.Перейти"              = "FormNavigationPanelGoTo"
	"Панель навигации формы.См. также"            = "FormNavigationPanelSeeAlso"
}

# Резолв группы команды: рус-синоним → канон; ГруппаКоманд.X → CommandGroup.X; пусто → ошибка с подсказкой.
function Resolve-CommandGroup {
	param([string]$raw, [string]$cmdName)
	$g = "$raw".Trim()
	if (-not $g) {
		Write-Error "Команде '$cmdName' не задана группа (group). 1С требует группу. Валидные: $($script:validCommandGroups -join ', '); либо CommandGroup.<Имя> — кастомная группа."
		exit 1
	}
	if ($script:commandGroupAliases.ContainsKey($g)) { return $script:commandGroupAliases[$g] }
	if ($g -match '^(?:CommandGroup|ГруппаКоманд)\.(.+)$') { return "CommandGroup.$($Matches[1])" }
	return $g
}

function Normalize-EnumValue {
	param([string]$propName, [string]$value)
	# 1. Check alias dictionary — silent auto-correct
	if ($script:enumValueAliases.ContainsKey($value)) {
		return $script:enumValueAliases[$value]
	}
	# 2. Case-insensitive match against valid values — silent
	$valid = $script:validEnumValues[$propName]
	if ($valid) {
		foreach ($v in $valid) {
			if ($v -ieq $value) { return $v }
		}
		# 3. Known property, unknown value — error with hint
		Write-Error "Invalid value '$value' for property '$propName'. Valid values: $($valid -join ', ')"
		exit 1
	}
	# 4. Unknown property — pass-through (no validation data)
	return $value
}

# Helper: read enum property from $def with default and normalization
function Get-EnumProp {
	param([string]$propName, [string]$fieldName, [string]$default)
	$val = $def.$fieldName
	$raw = if ($val) { "$val" } else { $default }
	return (Normalize-EnumValue $propName $raw)
}

# Bool object-свойство: presence-aware (иначе false-значение спутать с отсутствием). Прощаем строки.
function Get-BoolProp {
	param([string]$fieldName, [bool]$default)
	$val = $def.$fieldName
	if ($null -eq $val) { return $default }
	if ($val -is [bool]) { return $val }
	return ("$val" -match '^(true|1|да|истина)$')
}

# Прощающая нормализация ссылки на форму: рус корень (Справочник→Catalog), сегмент Форма→Form,
# короткая запись "Тип.Объект.ИмяФормы" (без Form) → вставка Form. Уже канон англ. → без изменений.
function Normalize-FormRef {
	param([string]$s)
	if (-not $s) { return $s }
	$parts = $s -split '\.'
	if ($parts.Count -lt 3) { return $s }
	$root = $script:fillRefRoots[$parts[0].ToLower()]
	if ($root) { $parts[0] = $root }
	for ($k = 1; $k -lt $parts.Count; $k++) { if ($parts[$k] -ieq 'Форма') { $parts[$k] = 'Form' } }
	if (($parts -notcontains 'Form') -and $parts.Count -eq 3) { $parts = @($parts[0], $parts[1], 'Form', $parts[2]) }
	return ($parts -join '.')
}

# Ссылка на форму по умолчанию: непустая → <Tag>значение</Tag>, иначе <Tag/>.
function Emit-FormRef {
	param([string]$i, [string]$tag, $val)
	if ($val) { X "$i<$tag>$(Esc-XmlText (Normalize-FormRef "$val"))</$tag>" } else { X "$i<$tag/>" }
}

# Ссылка verbatim (без Normalize-FormRef): для форм/схем/хранилищ Report/DataProcessor, где имя формы может быть
# буквально «Форма» (Normalize-FormRef перевёл бы имя-сегмент Форма→Form, испортив ссылку) либо ref не-форменного
# вида (SettingsStorage.X / Report.X.Template.Y). Декомпилятор пишет полный путь → passthrough.
function Emit-VerbatimRef {
	param([string]$i, [string]$tag, $val)
	if ($val) { X "$i<$tag>$(Esc-XmlText "$val")</$tag>" } else { X "$i<$tag/>" }
}

if (-not $def.type) {
	Write-Error "JSON must have 'type' field"
	exit 1
}

# Resolve type synonym
$objType = "$($def.type)"
if ($script:objectTypeSynonyms.ContainsKey($objType)) {
	$objType = $script:objectTypeSynonyms[$objType]
}

$validTypes = @("Catalog","Document","Enum","Constant","InformationRegister","AccumulationRegister",
	"AccountingRegister","CalculationRegister","ChartOfAccounts","ChartOfCharacteristicTypes",
	"ChartOfCalculationTypes","BusinessProcess","Task","ExchangePlan","DocumentJournal",
	"Report","DataProcessor","CommonModule","ScheduledJob","EventSubscription",
	"HTTPService","WebService","DefinedType","FunctionalOption",
	"Sequence","FilterCriterion","DocumentNumerator","SettingsStorage","CommonForm",
	"SessionParameter","CommonCommand","CommandGroup","CommonAttribute","FunctionalOptionsParameter","WSReference",
	"CommonPicture","CommonTemplate")
# -notin регистронезависим, поэтому "catalog" проходил проверку и дальше шёл в ИМЯ ТЕГА и в
# Configuration.xml как есть — выгрузка получалась с <catalog>, которую платформа не принимает.
# Прощаем регистр, но приводим к канону списка.
$canonType = $validTypes | Where-Object { $_ -eq $objType } | Select-Object -First 1
if ($canonType) { $objType = $canonType }
if ($objType -notin $validTypes) {
	Write-Error "Unsupported type: $objType. Valid: $($validTypes -join ', ')"
	exit 1
}

if (-not $def.name) {
	Write-Error "JSON must have 'name' field"
	exit 1
}

$objName = "$($def.name)"

# --- 2. XML helpers ---

$script:xml = New-Object System.Text.StringBuilder 32768

function X {
	param([string]$text)
	$script:xml.AppendLine($text) | Out-Null
}

function Esc-Xml {
	param([string]$s)
	return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;').Replace('"','&quot;')
}
# Эскейп ТЕКСТА элемента: только & < > (кавычки в тексте 1С держит raw, экранирование только для атрибутов).
function Esc-XmlText {
	param([string]$s)
	return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}

# ML-значение: строка → один <v8:item> ru; объект {lang: content} → item на язык (в порядке ключей).
function Emit-MLItems {
	param([string]$indent, $val)
	if ($val -is [System.Collections.IDictionary]) {
		foreach ($k in $val.Keys) {
			X "$indent<v8:item>"; X "$indent`t<v8:lang>$k</v8:lang>"; X "$indent`t<v8:content>$(Esc-XmlText "$($val[$k])")</v8:content>"; X "$indent</v8:item>"
		}
	} elseif ($val -is [System.Management.Automation.PSCustomObject]) {
		foreach ($p in $val.PSObject.Properties) {
			X "$indent<v8:item>"; X "$indent`t<v8:lang>$($p.Name)</v8:lang>"; X "$indent`t<v8:content>$(Esc-XmlText "$($p.Value)")</v8:content>"; X "$indent</v8:item>"
		}
	} else {
		X "$indent<v8:item>"; X "$indent`t<v8:lang>ru</v8:lang>"; X "$indent`t<v8:content>$(Esc-XmlText "$val")</v8:content>"; X "$indent</v8:item>"
	}
}
function Emit-MLText {
	param([string]$indent, [string]$tag, $text)
	# Пусто (null / пустая строка) → самозакрывающийся тег.
	if (($null -eq $text) -or (($text -is [string]) -and ($text -eq ''))) {
		X "$indent<$tag/>"
		return
	}
	X "$indent<$tag>"
	Emit-MLItems "$indent`t" $text
	X "$indent</$tag>"
}

function New-Guid-String {
	return [System.Guid]::NewGuid().ToString()
}

# --- 3. CamelCase splitter ---

function Split-CamelCase {
	param([string]$name)
	if (-not $name) { return $name }
	# Insert space before uppercase that follows lowercase (Cyrillic + Latin)
	$result = [regex]::Replace($name, '([а-яё])([А-ЯЁ])', '$1 $2')
	$result = [regex]::Replace($result, '([a-z])([A-Z])', '$1 $2')
	# Лоуэркейзим хвост, СОХРАНЯЯ аббревиатуры (зеркало эвристики платформы): максимальный прогон
	# заглавных длиной >=2, если сразу за ним НЕ буква (пробел/цифра/спецсимвол/конец) — остаётся заглавным
	# (НДС, ЕГАИС, ОС, ЭП). Прилипшие предлоги (СКлиентами) и бренды (ЮКасса) идут перед буквой → лоуэркейз.
	# Первый символ строки — как есть.
	if ($result.Length -gt 1) {
		$chars = $result.ToCharArray()
		$n = $chars.Length
		$keep = New-Object 'bool[]' $n
		$i = 0
		while ($i -lt $n) {
			if ([char]::IsUpper($chars[$i])) {
				$j = $i
				while ($j -lt $n -and [char]::IsUpper($chars[$j])) { $j++ }
				$afterBoundary = ($j -eq $n) -or (-not [char]::IsLetter($chars[$j]))
				if (($j - $i) -ge 2 -and $afterBoundary) { for ($k = $i; $k -lt $j; $k++) { $keep[$k] = $true } }
				$i = $j
			} else { $i++ }
		}
		$sb = New-Object System.Text.StringBuilder
		for ($idx = 0; $idx -lt $n; $idx++) {
			$c = $chars[$idx]
			if ($idx -eq 0 -or $keep[$idx]) { [void]$sb.Append($c) }
			elseif ([char]::IsUpper($c)) { [void]$sb.Append([char]::ToLower($c)) }
			else { [void]$sb.Append($c) }
		}
		$result = $sb.ToString()
	}
	return $result
}

# Auto-synonym. Проброс без стрингификации (строка ИЛИ {ru,en} — мультиязычный синоним объекта).
$synonym = if ($null -ne $def.synonym) { $def.synonym } else { Split-CamelCase $objName }
$comment = if ($def.comment) { "$($def.comment)" } else { "" }

# --- 4. Type system ---

$script:typeSynonyms = New-Object System.Collections.Hashtable
$script:typeSynonyms["число"]    = "Number"
$script:typeSynonyms["строка"]   = "String"
$script:typeSynonyms["булево"]   = "Boolean"
$script:typeSynonyms["дата"]     = "Date"
$script:typeSynonyms["датавремя"]= "DateTime"
$script:typeSynonyms["время"]    = "Time"
$script:typeSynonyms["time"]     = "Time"
$script:typeSynonyms["number"]   = "Number"
$script:typeSynonyms["string"]   = "String"
$script:typeSynonyms["boolean"]  = "Boolean"
$script:typeSynonyms["date"]     = "Date"
$script:typeSynonyms["datetime"] = "DateTime"
$script:typeSynonyms["bool"]     = "Boolean"
# ValueStorage / UUID — прощающий ввод (модель может написать base64Binary / рус. форму → канон).
$script:typeSynonyms["valuestorage"]         = "ValueStorage"
$script:typeSynonyms["base64binary"]         = "ValueStorage"
$script:typeSynonyms["хранилищезначений"]    = "ValueStorage"
$script:typeSynonyms["хранилищезначения"]    = "ValueStorage"
$script:typeSynonyms["uuid"]                 = "UUID"
$script:typeSynonyms["уникальныйидентификатор"] = "UUID"
# Платформенные типы, требующие префикса v8: (коллекции/периоды, частые в реквизитах обработок/отчётов).
$script:v8PlatformTypes = @("ValueTable","ValueTree","ValueList","ValueListType","StandardPeriod",
	"StandardBeginningDate","PointInTime","TypeDescription","FixedArray","FixedMap","FixedStructure")
# Типы со ВЫДЕЛЕННЫМ пространством имён (локальный xmlns на <v8:Type>). prefix — канон корпуса
# (dcsset/mxl — семантические из корневых деклараций 1С; chart — генерируемый dNpM, любой подойдёт).
$script:typeNamespaceMap = @{
	"Chart"               = @{ ns = "http://v8.1c.ru/8.2/data/chart";                      prefix = "d5p1" }
	"SettingsComposer"    = @{ ns = "http://v8.1c.ru/8.1/data-composition-system/settings"; prefix = "dcsset" }
	"SpreadsheetDocument" = @{ ns = "http://v8.1c.ru/8.2/data/spreadsheet";                 prefix = "mxl" }
}
# Типы current-config пространства (cfg:, объявлено в корне): объектные (CatalogObject.X/DataProcessorObject.X/…)
# и голые (ConstantsSet/ReportBuilder). Ссылочные (*Ref.X/DefinedType.X) идут ОТДЕЛЬНО через локальный d5p1 (§memory).
# Префикс current-config для ссылочных типов. 'cfg' — для файлов, чья шапка его объявляет
# (объектный XML, Ext/Form.xml общей формы). $null на время сборки Ext/Predefined.xml, чья
# шапка его НЕ объявляет: там и платформа уходит на локальное объявление.
$script:cfgPrefix = 'cfg'
$script:cfgBareTypes = @("ConstantsSet", "ReportBuilder", "FilterCriterion")
$script:cfgObjectKinds = @("Catalog","Document","Enum","ChartOfAccounts","ChartOfCharacteristicTypes",
	"ChartOfCalculationTypes","ExchangePlan","BusinessProcess","Task","InformationRegister","AccumulationRegister",
	"AccountingRegister","CalculationRegister","DataProcessor","Report","DocumentJournal","Constant","ConstantValue","Sequence","Recalculation")
$script:typeSynonyms["таблицазначений"]      = "ValueTable"
$script:typeSynonyms["деревозначений"]       = "ValueTree"
$script:typeSynonyms["списокзначений"]       = "ValueListType"
$script:typeSynonyms["стандартныйпериод"]    = "StandardPeriod"
# Reference synonyms (Russian, lowercase)
$script:typeSynonyms["справочникссылка"]             = "CatalogRef"
$script:typeSynonyms["документссылка"]               = "DocumentRef"
$script:typeSynonyms["перечислениессылка"]            = "EnumRef"
$script:typeSynonyms["плансчетовссылка"]              = "ChartOfAccountsRef"
$script:typeSynonyms["планвидовхарактеристикссылка"]  = "ChartOfCharacteristicTypesRef"
$script:typeSynonyms["планвидоврасчётассылка"]         = "ChartOfCalculationTypesRef"
$script:typeSynonyms["планвидоврасчетассылка"]         = "ChartOfCalculationTypesRef"
$script:typeSynonyms["планобменассылка"]               = "ExchangePlanRef"
$script:typeSynonyms["бизнеспроцессссылка"]            = "BusinessProcessRef"
$script:typeSynonyms["задачассылка"]                   = "TaskRef"
$script:typeSynonyms["определяемыйтип"]              = "DefinedType"
$script:typeSynonyms["definedtype"]                   = "DefinedType"
# English lowercase ref synonyms
$script:typeSynonyms["catalogref"]                    = "CatalogRef"
$script:typeSynonyms["documentref"]                   = "DocumentRef"
$script:typeSynonyms["enumref"]                       = "EnumRef"

function Resolve-TypeStr {
	param([string]$typeStr)
	if (-not $typeStr) { return $typeStr }

	# Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
	# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
	# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
	# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
	# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
	# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
	# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
	if ($typeStr.StartsWith('cfg:')) {
		$typeStr = $typeStr.Substring(4)
	} elseif ($typeStr.Contains('.') -and $typeStr -match '^d\d+p\d+:') {
		$typeStr = $typeStr.Substring($typeStr.IndexOf(':') + 1)
	}

	# Параметризованные типы: Number(15,2), Строка(100)
	if ($typeStr -match '^([^(]+)\((.+)\)$') {
		$baseName = $Matches[1].Trim()
		$params = $Matches[2]
		$resolved = $script:typeSynonyms[$baseName.ToLower()]
		if ($resolved) { return "$resolved($params)" }
		return $typeStr
	}

	# Ссылочные типы: СправочникСсылка.Организации → CatalogRef.Организации
	if ($typeStr.Contains('.')) {
		$dotIdx = $typeStr.IndexOf('.')
		$prefix = $typeStr.Substring(0, $dotIdx)
		$suffix = $typeStr.Substring($dotIdx)  # includes the dot
		$resolved = $script:typeSynonyms[$prefix.ToLower()]
		if ($resolved) { return "$resolved$suffix" }
		return $typeStr
	}

	# Простое имя
	$resolved = $script:typeSynonyms[$typeStr.ToLower()]
	if ($resolved) { return $resolved }
	return $typeStr
}

function Emit-TypeContent {
	param([string]$indent, [string]$typeStr)
	if (-not $typeStr) { return }

	# Composite type: "Type1 + Type2 + Type3"
	# Платформа пишет сначала ВСЕ <v8:Type>/<v8:TypeSet>, и только потом блоки
	# квалификаторов — а рекурсия ниже печатала бы каждую часть целиком (тип вместе со
	# своими квалификаторами). На одиночном типе оба порядка совпадают, поэтому
	# расхождение вылезало только на составном.
	# Порядок самих блоков квалификаторов тоже канонический и НЕ зеркалит порядок типов:
	# Number, String, Date (корпус acc+erp, контрпримеров нет — при типах
	# boolean,string,dateTime,decimal квалификаторы идут Number,String,Date).
	# Порядок типов при этом сохраняем как в DSL: он и есть порядок источника.
	if ($typeStr.Contains(' + ')) {
		$parts = $typeStr -split '\s*\+\s*'
		$typeLines = New-Object System.Collections.ArrayList
		$qualBlocks = @{}   # 'Number'|'String'|'Date' → строки блока
		foreach ($part in $parts) {
			# X пишет в StringBuilder, поэтому «перехват» — это запомнить длину, вызвать
			# эмиттер и откатить добавленное. В py-порту X добавляет в список, и там тот
			# же алгоритм выражен срезом — различие рантаймов, не логики.
			$before = $script:xml.Length
			Emit-TypeContent $indent $part.Trim()
			$chunk = $script:xml.ToString($before, $script:xml.Length - $before)
			[void]$script:xml.Remove($before, $script:xml.Length - $before)
			$curQual = $null
			foreach ($line in ($chunk -split "`r?`n")) {
				if ($line -eq '') { continue }
				if ($line -match '<v8:(String|Number|Date)Qualifiers>') {
					$curQual = $Matches[1]
					$qualBlocks[$curQual] = New-Object System.Collections.ArrayList
				}
				if ($curQual) {
					[void]$qualBlocks[$curQual].Add($line)
					if ($line -match '</v8:(String|Number|Date)Qualifiers>') { $curQual = $null }
				} else {
					[void]$typeLines.Add($line)
				}
			}
		}
		foreach ($line in $typeLines) { X $line }
		foreach ($q in @('Number', 'String', 'Date')) {
			if ($qualBlocks.ContainsKey($q)) { foreach ($line in $qualBlocks[$q]) { X $line } }
		}
		return
	}

	$typeStr = Resolve-TypeStr $typeStr

	# Boolean
	if ($typeStr -eq "Boolean") {
		X "$indent<v8:Type>xs:boolean</v8:Type>"
		return
	}

	# String or String(N) or String(N,fixed|variable) — AllowedLength: Variable дефолт / Fixed (фикс. длина).
	if ($typeStr -match '^String(\((\d+)(\s*,\s*(fixed|variable))?\))?$') {
		$len = if ($Matches[2]) { $Matches[2] } else { "10" }
		$al = if ($Matches[4] -and $Matches[4].ToLower() -eq 'fixed') { 'Fixed' } else { 'Variable' }
		X "$indent<v8:Type>xs:string</v8:Type>"
		X "$indent<v8:StringQualifiers>"
		X "$indent`t<v8:Length>$len</v8:Length>"
		X "$indent`t<v8:AllowedLength>$al</v8:AllowedLength>"
		X "$indent</v8:StringQualifiers>"
		return
	}

	# Number without params → Number(10,0)
	if ($typeStr -eq "Number") {
		X "$indent<v8:Type>xs:decimal</v8:Type>"
		X "$indent<v8:NumberQualifiers>"
		X "$indent`t<v8:Digits>10</v8:Digits>"
		X "$indent`t<v8:FractionDigits>0</v8:FractionDigits>"
		X "$indent`t<v8:AllowedSign>Any</v8:AllowedSign>"
		X "$indent</v8:NumberQualifiers>"
		return
	}

	# Number(D,F) or Number(D,F,nonneg)
	if ($typeStr -match '^Number\((\d+),(\d+)(,nonneg)?\)$') {
		$digits = $Matches[1]
		$fraction = $Matches[2]
		$sign = if ($Matches[3]) { "Nonnegative" } else { "Any" }
		X "$indent<v8:Type>xs:decimal</v8:Type>"
		X "$indent<v8:NumberQualifiers>"
		X "$indent`t<v8:Digits>$digits</v8:Digits>"
		X "$indent`t<v8:FractionDigits>$fraction</v8:FractionDigits>"
		X "$indent`t<v8:AllowedSign>$sign</v8:AllowedSign>"
		X "$indent</v8:NumberQualifiers>"
		return
	}

	# Date / DateTime / Time — общая структура xs:dateTime + DateFractions (различаются лишь составом).
	if ($typeStr -match '^(Date|DateTime|Time)$') {
		X "$indent<v8:Type>xs:dateTime</v8:Type>"
		X "$indent<v8:DateQualifiers>"
		X "$indent`t<v8:DateFractions>$typeStr</v8:DateFractions>"
		X "$indent</v8:DateQualifiers>"
		return
	}

	# TypeSet — тип-множество: ОпределяемыйТип (DefinedType) ИЛИ Характеристика ПВХ (Characteristic).
	if ($typeStr -match '^(DefinedType|Characteristic)\.(.+)$') {
		X "$indent<v8:TypeSet>cfg:$typeStr</v8:TypeSet>"
		return
	}
	# Голый метатип-категория (CatalogRef/DocumentRef/…/AnyRef/AnyIBRef без имени объекта) — множество
	# «любой объект категории» → TypeSet (а не конкретный Type с именем).
	if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef|AnyRef|AnyIBRef)$') {
		X "$indent<v8:TypeSet>cfg:$typeStr</v8:TypeSet>"
		return
	}

	# ValueStorage (ХранилищеЗначения) — канон v8:ValueStorage (не xs:base64Binary, хоть 1С и принимает оба).
	if ($typeStr -eq "ValueStorage") {
		X "$indent<v8:Type>v8:ValueStorage</v8:Type>"
		return
	}
	# UUID (УникальныйИдентификатор)
	if ($typeStr -eq "UUID") {
		X "$indent<v8:Type>v8:UUID</v8:Type>"
		return
	}
	# Платформенные типы-коллекции/периоды (ТаблицаЗначений/ДеревоЗначений/СписокЗначений/StandardPeriod/…) —
	# канон с префиксом v8: (декомпилятор снимает префикс через Strip-NsPrefix, компилятор возвращает).
	if ($script:v8PlatformTypes -contains $typeStr) {
		X "$indent<v8:Type>v8:$typeStr</v8:Type>"
		return
	}
	# Типы с выделенным пространством имён (Chart/SettingsComposer/SpreadsheetDocument) — локальный xmlns.
	if ($script:typeNamespaceMap.ContainsKey($typeStr)) {
		$m = $script:typeNamespaceMap[$typeStr]
		X "$indent<v8:Type xmlns:$($m.prefix)=`"$($m.ns)`">$($m.prefix):$typeStr</v8:Type>"
		return
	}
	# Типы current-config (cfg:): голые (ConstantsSet/…) и объектные (CatalogObject.X/DataProcessorObject.X/…).
	if ($script:cfgBareTypes -contains $typeStr) {
		X "$indent<v8:Type>cfg:$typeStr</v8:Type>"
		return
	}
	if ($typeStr -match '^(\w+)(Object|List|Manager|Selection|RecordSet|RecordKey|RecordManager)\.(.+)$' -and $script:cfgObjectKinds -contains $Matches[1]) {
		X "$indent<v8:Type>cfg:$typeStr</v8:Type>"
		return
	}
	# Голый объектный метатип (без имени) — напр. в Source подписки на событие:
	#  - Object/RecordSet → «любой объект категории» = TypeSet cfg: (множество);
	#  - Manager/List/Selection/RecordKey/RecordManager → сам тип менеджера/списка = Type cfg: (единичный).
	# ConstantValueManager (голый) — исключение: множество менеджеров значений констант = TypeSet (прочие *Manager → Type).
	if (($typeStr -match '^(\w+)(Object|RecordSet)$' -and $script:cfgObjectKinds -contains $Matches[1]) -or $typeStr -eq 'ConstantValueManager') {
		X "$indent<v8:TypeSet>cfg:$typeStr</v8:TypeSet>"
		return
	}
	if ($typeStr -match '^(\w+)(Manager|List|Selection|RecordKey|RecordManager)$' -and $script:cfgObjectKinds -contains $Matches[1]) {
		X "$indent<v8:Type>cfg:$typeStr</v8:Type>"
		return
	}

	# Ссылочные типы — корневой cfg:, как пишет платформа. Раньше здесь объявлялся
	# ЛОКАЛЬНЫЙ xmlns:d5p1 на тот же URI, что уже объявлен в шапке ($script:xmlnsDecl):
	# формально эквивалентно (значим URI, не префикс) и платформой принималось, но
	# первый же цикл «загрузить в базу → выгрузить» переписывал каждый ссылочный тип
	# в cfg: — то есть давал diff-шум на ровном месте. Форма пришла из СКД, где cfg:
	# действительно не работает; в метаданных такого ограничения нет.
	# NB: локальная xmlns остаётся законной для ЧУЖИХ пространств — см. $script:typeNamespaceMap.
	# $script:cfgPrefix = $null означает «пишем файл, корень которого cfg НЕ объявляет»
	# (Ext/Predefined.xml — его шапка это predef/v8/xr/xs/xsi). Там платформа сама уходит
	# на локальную форму: в корпусе `<v8:Type xmlns:d6p1="…current-config">d6p1:CatalogRef.Валюты`.
	if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$') {
		if ($script:cfgPrefix) {
			X "$indent<v8:Type>$($script:cfgPrefix):$typeStr</v8:Type>"
		} else {
			X "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$typeStr</v8:Type>"
		}
		return
	}

	# Fallback — emit as-is
	X "$indent<v8:Type>$typeStr</v8:Type>"
}

function Emit-ValueType {
	param([string]$indent, [string]$typeStr)
	X "$indent<Type>"
	Emit-TypeContent "$indent`t" $typeStr
	X "$indent</Type>"
}

# --- FillValue (значение заполнения реквизита) ---
# Пара FillFromFillingValue+FillValue — единый блок «заполнения» (недоступен у реквизитов ТЧ).
# Форма пустого FillValue зависит от типа реквизита (то же значение по умолчанию, что и «пустое»
# значение типа): String→typed-empty, Number→0, всё остальное (Boolean/Date/Ref/составной/TypeSet)→nil.
# Реальное значение задаётся ключом `fillValue` (интерпретация по типу реквизита; см. §4.2 spec).

# Категория типа реквизита для выбора формы FillValue.
function Get-FillTypeCategory {
	param([string]$typeStr)
	if (-not $typeStr) { return 'String' }        # реквизит без типа → неквалифиц. строка
	if ($typeStr -match '\+') { return 'Other' }  # составной тип → nil-дефолт
	$t = Resolve-TypeStr $typeStr
	if ($t -match '^Boolean$')          { return 'Boolean' }
	if ($t -match '^String(\(|$)')      { return 'String' }
	if ($t -match '^Number(\(|$)')      { return 'Number' }
	if ($t -match '^(Date|DateTime)$')  { return 'Date' }
	return 'Other'                                 # ссылки, TypeSet, ValueStorage, … → nil-дефолт
}

# Прощающий ввод для ссылочных путей DTR: рус/англ корни, ПустаяСсылка/EmptyRef, ЗначениеПеречисления/EnumValue.
$script:fillRefRoots = @{
	'перечисление'='Enum'; 'справочник'='Catalog'; 'документ'='Document';
	'плансчетов'='ChartOfAccounts'; 'планвидовхарактеристик'='ChartOfCharacteristicTypes';
	'планвидоврасчета'='ChartOfCalculationTypes'; 'планвидоврасчёта'='ChartOfCalculationTypes';
	'планобмена'='ExchangePlan'; 'бизнеспроцесс'='BusinessProcess'; 'задача'='Task';
	'enum'='Enum'; 'catalog'='Catalog'; 'document'='Document'; 'chartofaccounts'='ChartOfAccounts';
	'chartofcharacteristictypes'='ChartOfCharacteristicTypes'; 'chartofcalculationtypes'='ChartOfCalculationTypes';
	'exchangeplan'='ExchangePlan'; 'businessprocess'='BusinessProcess'; 'task'='Task'
}
$script:fillEmptyRefWords = @('emptyref','пустаяссылка')
$script:fillEnumValWords  = @('enumvalue','значениеперечисления')
$script:fillBoolTrue  = @('true','истина','да')
$script:fillBoolFalse = @('false','ложь','нет')
# Прощающий ввод MDObjectRef-путей (Location/Content функц. опции, registerRecords и т.п.): русские корни
# метаданных + подвиды → английские. Виды стоят на ЧЁТНЫХ позициях (0,2,4…), имена (нечётные) не трогаем.
# Английские пути проходят без изменений (в мапе только русские ключи) → роундтрип byte-exact сохраняется.
$script:mdRefRoots = @{
	'справочник'='Catalog'; 'документ'='Document'; 'перечисление'='Enum'; 'константа'='Constant';
	'регистрсведений'='InformationRegister'; 'регистрнакопления'='AccumulationRegister';
	'регистрбухгалтерии'='AccountingRegister'; 'регистррасчета'='CalculationRegister'; 'регистррасчёта'='CalculationRegister';
	'плансчетов'='ChartOfAccounts'; 'планвидовхарактеристик'='ChartOfCharacteristicTypes';
	'планвидоврасчета'='ChartOfCalculationTypes'; 'планвидоврасчёта'='ChartOfCalculationTypes';
	'планобмена'='ExchangePlan'; 'бизнеспроцесс'='BusinessProcess'; 'задача'='Task';
	'журналдокументов'='DocumentJournal'; 'отчет'='Report'; 'отчёт'='Report'; 'обработка'='DataProcessor';
	'табличнаячасть'='TabularSection'; 'реквизит'='Attribute'; 'измерение'='Dimension'; 'ресурс'='Resource';
	'стандартныйреквизит'='StandardAttribute'; 'значениеперечисления'='EnumValue'; 'команда'='Command';
	'признакучета'='AccountingFlag'; 'признакучёта'='AccountingFlag';
	# Ссылочные формы (тип ссылки вместо объекта метаданных): в MDObjectRef-пути нужен ОБЪЕКТ, т.е.
	# "CatalogRef.Валюты" → "Catalog.Валюты". Вид метаданных, оканчивающийся на Ref, не существует,
	# поэтому схлопывание однозначно. В ТИПАХ реквизитов запись CatalogRef.X верна — там эта мапа не применяется.
	'catalogref'='Catalog'; 'documentref'='Document'; 'enumref'='Enum';
	'chartofaccountsref'='ChartOfAccounts'; 'chartofcharacteristictypesref'='ChartOfCharacteristicTypes';
	'chartofcalculationtypesref'='ChartOfCalculationTypes'; 'exchangeplanref'='ExchangePlan';
	'businessprocessref'='BusinessProcess'; 'taskref'='Task';
	'справочникссылка'='Catalog'; 'документссылка'='Document'; 'перечислениессылка'='Enum';
	'плансчетовссылка'='ChartOfAccounts'; 'планвидовхарактеристикссылка'='ChartOfCharacteristicTypes';
	'планвидоврасчетассылка'='ChartOfCalculationTypes'; 'планвидоврасчётассылка'='ChartOfCalculationTypes';
	'планобменассылка'='ExchangePlan'; 'бизнеспроцессссылка'='BusinessProcess'; 'задачассылка'='Task'
}
# $defaultRoot — корень для ГОЛОГО имени без точки (напр. owners: "Валюты" → "Catalog.Валюты").
# Без него голое имя возвращается как есть (прежнее поведение вызывающих без подстановки).
function Normalize-MDObjectRef {
	param([string]$ref, [string]$defaultRoot)
	if (-not $ref) { return $ref }
	if (-not $ref.Contains('.')) {
		if ($defaultRoot) { return "$defaultRoot.$ref" }
		return $ref
	}
	$parts = $ref -split '\.'
	for ($k = 0; $k -lt $parts.Count; $k += 2) {
		$t = $script:mdRefRoots[$parts[$k].ToLower()]
		if ($t) { $parts[$k] = $t }
	}
	return ($parts -join '.')
}
# Значения платформенного перечисления ВидСчета (ent:AccountType) — FillValue стандартного реквизита Тип у Плана счетов.
$script:accountTypeValues = @('Active','Passive','ActivePassive')
# XxxRef (тип реквизита) → корень DTR-пути (для разворота короткой записи значения).
$script:fillRefKindRoot = @{
	'catalogref'='Catalog'; 'documentref'='Document'; 'enumref'='Enum';
	'chartofaccountsref'='ChartOfAccounts'; 'chartofcharacteristictypesref'='ChartOfCharacteristicTypes';
	'chartofcalculationtypesref'='ChartOfCalculationTypes'; 'exchangeplanref'='ExchangePlan';
	'businessprocessref'='BusinessProcess'; 'taskref'='Task'
}

# Короткая запись значения ссылочного реквизита (без точки): имя разворачиваем по типу реквизита.
# "EmptyRef"/"ПустаяСсылка" → <Root>.<Тип>.EmptyRef; для Enum — EnumValue; прочие — предопределённое.
# $null, если развернуть нельзя (тип не одиночный ссылочный).
function Expand-FillShortRef {
	param([string]$s, [string]$typeStr)
	if (-not $typeStr) { return $null }
	if ($typeStr -match '\+') { return $null }   # составной тип — короткая форма неоднозначна
	$t = Resolve-TypeStr $typeStr
	if ($t -notmatch '^(\w+Ref)\.(.+)$') { return $null }
	$root = $script:fillRefKindRoot[$Matches[1].ToLower()]
	if (-not $root) { return $null }
	$typeName = $Matches[2]
	if ($script:fillEmptyRefWords -contains $s.ToLower()) { return "$root.$typeName.EmptyRef" }
	if ($root -eq 'Enum') { return "Enum.$typeName.EnumValue.$s" }
	return "$root.$typeName.$s"
}

# Строка → нормализованный DTR-путь ("Catalog.X.EmptyRef" / "Enum.X.EnumValue.Y" / GUID.GUID) ЛИБО $null (не ссылка).
function Normalize-FillRef {
	param([string]$s)
	if ([string]::IsNullOrEmpty($s)) { return $null }
	# Raw-ссылка по паре GUID (метаданные.значение) — всегда ссылка.
	if ($s -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\.[0-9a-fA-F-]+$') { return $s }
	$parts = $s -split '\.'
	if ($parts.Count -lt 2) { return $null }
	$root = $script:fillRefRoots[$parts[0].ToLower()]
	if (-not $root) { return $null }
	$typeName = $parts[1]
	# Двухчастное "Тип.Имя" — это объект метаданных, а не ЗНАЧЕНИЕ. Значением бывают пустая ссылка,
	# предопределённый элемент или значение перечисления — все они длиннее. Правило было выведено
	# только для Enum; без него строка вида "Документ.РеализацияТоваров" молча становилась ссылкой
	# (на корпусе: 5520 значений в ChoiceParameters и 8623 в FillValue — двухчастных НЕТ ни одного).
	if ($parts.Count -lt 3) { return $null }
	if ($root -eq 'Enum') {
		if ($parts.Count -eq 3) {
			if ($script:fillEmptyRefWords -contains $parts[2].ToLower()) { return "Enum.$typeName.EmptyRef" }
			return "Enum.$typeName.EnumValue.$($parts[2])"
		}
		$member = $parts[2]
		if ($script:fillEnumValWords -contains $member.ToLower()) { $rest = $parts[3..($parts.Count-1)] -join '.' }
		else { $rest = $parts[2..($parts.Count-1)] -join '.' }
		return "Enum.$typeName.EnumValue.$rest"
	}
	# Прочие корни: переводим корень, ПустаяСсылка→EmptyRef в хвосте.
	$tail = @($parts[1..($parts.Count-1)])
	for ($i = 0; $i -lt $tail.Count; $i++) {
		if ($script:fillEmptyRefWords -contains $tail[$i].ToLower()) { $tail[$i] = 'EmptyRef' }
	}
	return "$root." + ($tail -join '.')
}

# Строковый spec → @{ XsiType; Text }. Интерпретация по типу реквизита ($typeStr).
function Resolve-FillValueSpec {
	param([string]$s, [string]$typeStr)
	$cat = Get-FillTypeCategory $typeStr
	if ($s -eq '') { return @{ XsiType='xs:string'; Text='' } }
	# String-реквизит: значение заполнения — всегда строковый литерал (без ref/date-детекции).
	if ($cat -eq 'String') { return @{ XsiType='xs:string'; Text=$s } }
	# Булевы слова (для Boolean-реквизита ИЛИ явное истина/ложь).
	if ($cat -eq 'Boolean' -or ($script:fillBoolTrue -contains $s.ToLower()) -or ($script:fillBoolFalse -contains $s.ToLower())) {
		if ($script:fillBoolTrue  -contains $s.ToLower()) { return @{ XsiType='xs:boolean'; Text='true' } }
		if ($script:fillBoolFalse -contains $s.ToLower()) { return @{ XsiType='xs:boolean'; Text='false' } }
	}
	if ($cat -eq 'Number') { return @{ XsiType='xs:decimal'; Text=$s } }
	# Дата: явный Date-реквизит ИЛИ ISO-паттерн. "2020-01-01" → добавить время.
	if ($cat -eq 'Date' -or $s -match '^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?$') {
		if ($s -match '^\d{4}-\d{2}-\d{2}$') { $s = "${s}T00:00:00" }
		return @{ XsiType='xs:dateTime'; Text=$s }
	}
	# Полный ссылочный путь DTR (с точкой: "Catalog.X.EmptyRef", "Enum.X.EnumValue.Y", GUID.GUID).
	$ref = Normalize-FillRef $s
	if ($ref) { return @{ XsiType='xr:DesignTimeRef'; Text=$ref } }
	# Короткая запись значения ссылочного реквизита (одно имя — разворачиваем по типу).
	$short = Expand-FillShortRef $s $typeStr
	if ($short) { return @{ XsiType='xr:DesignTimeRef'; Text=$short } }
	# Фолбэк — строковый литерал.
	return @{ XsiType='xs:string'; Text=$s }
}

# Формат числа-значения без привязки к культуре (точка-разделитель).
function Format-FillNum {
	param($n)
	if ($n -is [double] -or $n -is [decimal]) { return $n.ToString([System.Globalization.CultureInfo]::InvariantCulture) }
	return "$n"
}

# $spec — значение ключа `fillValue` ($null при явном nil-override), $hasSpec — присутствует ли ключ.
function Emit-FillValue {
	param([string]$indent, [string]$typeStr, $spec, $hasSpec, [bool]$typeEmpty = $false)
	# Пустой <Type/> (реквизит без типа) → форма пустого значения nil (как составной/ссылочный), НЕ xs:string:
	# у бестипового реквизита нет типизированного «пустого» значения.
	$cat = if ($typeEmpty) { 'Other' } else { Get-FillTypeCategory $typeStr }

	if ($hasSpec -ne $true) {
		# Значение не задано — форма по умолчанию для типа.
		switch ($cat) {
			'String' { X "$indent<FillValue xsi:type=`"xs:string`"/>"; return }
			'Number' { X "$indent<FillValue xsi:type=`"xs:decimal`">0</FillValue>"; return }
			default  { X "$indent<FillValue xsi:nil=`"true`"/>"; return }
		}
	}

	if ($null -eq $spec) { X "$indent<FillValue xsi:nil=`"true`"/>"; return }   # явный nil-override
	if ($spec.nil -eq $true) { X "$indent<FillValue xsi:nil=`"true`"/>"; return }   # явный nil на типизированном (маркер декомпилятора)
	if ($spec.emptyRef -eq $true) { X "$indent<FillValue xsi:type=`"xr:DesignTimeRef`"/>"; return }   # пустая ссылка (маркер декомпилятора)
	if ($spec -is [bool]) {
		X "$indent<FillValue xsi:type=`"xs:boolean`">$(if ($spec) { 'true' } else { 'false' })</FillValue>"; return
	}
	if ($spec -is [int] -or $spec -is [long] -or $spec -is [double] -or $spec -is [decimal]) {
		X "$indent<FillValue xsi:type=`"xs:decimal`">$(Format-FillNum $spec)</FillValue>"; return
	}
	$r = Resolve-FillValueSpec "$spec" $typeStr
	if ($r.Text -eq '' -and $r.XsiType -eq 'xs:string') { X "$indent<FillValue xsi:type=`"xs:string`"/>"; return }
	X "$indent<FillValue xsi:type=`"$($r.XsiType)`">$(Esc-XmlText $r.Text)</FillValue>"
}

# --- 5. Attribute shorthand parser ---

function Build-TypeStr {
	param($obj)
	$t = if ($obj.valueType) { "$($obj.valueType)" } elseif ($obj.type) { "$($obj.type)" } else { "" }
	if ($t -and -not $t.Contains('(')) {
		if ($t -eq "String" -and $obj.length) {
			$t = "String($($obj.length))"
		} elseif ($t -eq "Number" -and $obj.length) {
			$p = if ($obj.precision) { $obj.precision } else { 0 }
			$nn = if ($obj.nonneg -or $obj.nonnegative) { ",nonneg" } else { "" }
			$t = "Number($($obj.length),$p$nn)"
		}
	}
	return $t
}

function Parse-AttributeShorthand {
	param($val)

	if ($val -is [string]) {
		$str = "$val"
		$parsed = @{
			name = ""
			type = ""
			typeEmpty = $false
			synonym = ""
			comment = ""
			flags = @()
			hasFillValue = $false
			fillValue = $null
		}

		# Split by | for flags
		$parts = $str -split '\|', 2
		$mainPart = $parts[0].Trim()
		if ($parts.Count -gt 1) {
			$flagStr = $parts[1].Trim()
			$parsed.flags = @($flagStr -split ',' | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ })
		}

		# Split by : for name and type
		$colonParts = $mainPart -split ':', 2
		$parsed.name = $colonParts[0].Trim()
		if ($colonParts.Count -gt 1) {
			$parsed.type = $colonParts[1].Trim()
		}

		$parsed.synonym = Split-CamelCase $parsed.name
		return $parsed
	}

	# Object form. synonym/tooltip/format/editFormat — сквозной проброс (строка ИЛИ {ru,en}), НЕ стрингифаим.
	# fillCheck — синоним fillChecking (из формы; bool true→ShowError). quickChoice — прощаем bool (true→Use, false→DontUse).
	$name = "$($val.name)"
	$fc = if ($val.fillChecking) { "$($val.fillChecking)" }
	      elseif ($null -ne $val.fillCheck) { if ($val.fillCheck -is [bool]) { if ($val.fillCheck) { 'ShowError' } else { '' } } else { "$($val.fillCheck)" } }
	      else { "" }
	$qc = if ($null -ne $val.quickChoice) { if ($val.quickChoice -is [bool]) { if ($val.quickChoice) { 'Use' } else { 'DontUse' } } else { "$($val.quickChoice)" } } else { "" }
	return @{
		name    = $name
		type    = Build-TypeStr $val
		# Явный `type: ""` (ключ присутствует, значение пустое) ≠ отсутствие типа: означает пустой <Type/>
		# (реквизит без типа / произвольный). Отличаем present-"" от absent через $null-проверку (PSCustomObject).
		typeEmpty = ($null -ne $val.type -and "$($val.type)".Trim() -eq '' -and -not $val.valueType)
		synonym = if ($null -ne $val.synonym) { $val.synonym } else { Split-CamelCase $name }
		tooltip = $val.tooltip
		comment = if ($val.comment) { "$($val.comment)" } else { "" }
		flags   = @(if ($val.flags) { $val.flags } else { @() })
		fillChecking = $fc
		indexing = if ($val.indexing) { "$($val.indexing)" } else { "" }
		multiLine = if ($val.multiLine -eq $true) { $true } else { $false }
		choiceHistoryOnInput = if ($val.choiceHistoryOnInput) { "$($val.choiceHistoryOnInput)" } else { "" }
		fullTextSearch = if ($val.fullTextSearch) { "$($val.fullTextSearch)" } else { "" }
		fillFromFillingValue = if ($val.fillFromFillingValue -eq $true) { $true } else { $false }
		createOnInput = if ($val.createOnInput) { "$($val.createOnInput)" } else { "" }
		quickChoice = $qc
		dataHistory = if ($val.dataHistory) { "$($val.dataHistory)" } else { "" }
		use = if ($val.use) { "$($val.use)" } else { "" }
		passwordMode = if ($val.passwordMode -eq $true) { $true } else { $false }
		format = $val.format
		editFormat = $val.editFormat
		mask = if ($val.mask) { "$($val.mask)" } else { "" }
		extendedEdit = if ($val.extendedEdit -eq $true) { $true } else { $false }
		markNegatives = if ($val.markNegatives -eq $true) { $true } else { $false }
		choiceForm = if ($val.choiceForm) { "$($val.choiceForm)" } else { "" }
		choiceFoldersAndItems = if ($val.choiceFoldersAndItems) { "$($val.choiceFoldersAndItems)" } else { "" }
		minValue = $val.minValue
		maxValue = $val.maxValue
		hasFillValue = ($val.PSObject -and $val.PSObject.Properties -and ($val.PSObject.Properties.Name -contains 'fillValue'))
		fillValue = $val.fillValue
		linkByType = $val.linkByType
		choiceParameterLinks = $val.choiceParameterLinks
		choiceParameters = $val.choiceParameters
		master = if ($val.master -eq $true) { $true } else { $false }
		mainFilter = if ($val.mainFilter -eq $true) { $true } else { $false }
		denyIncompleteValues = if ($val.denyIncompleteValues -eq $true) { $true } else { $false }
		useInTotals = if ($null -ne $val.useInTotals) { ($val.useInTotals -eq $true) } else { $true }
		baseDimension = if ($val.baseDimension -eq $true) { $true } else { $false }
		scheduleLink = $val.scheduleLink
		balance = if ($val.balance -eq $true) { $true } else { $false }
		accountingFlag = $val.accountingFlag
		extDimensionAccountingFlag = $val.extDimensionAccountingFlag
		addressingDimension = $val.addressingDimension
		# Режим приведения типов измерения РС (формат 2.18). Ключ обязан доехать до эмиттера:
		# без него не-дефолтное значение (Deny / DeleteData) молча заменялось на TransformValues.
		typeReductionMode = $val.typeReductionMode
	}
}

function Parse-EnumValueShorthand {
	param($val)

	if ($val -is [string]) {
		$name = "$val"
		return @{
			name    = $name
			synonym = Split-CamelCase $name
			comment = ""
		}
	}

	$name = "$($val.name)"
	return @{
		name    = $name
		synonym = if ($null -ne $val.synonym) { $val.synonym } else { Split-CamelCase $name }   # строка ИЛИ {ru,en} → Emit-MLText
		comment = if ($val.comment) { "$($val.comment)" } else { "" }
	}
}

# --- 6. GeneratedType categories ---

$script:generatedTypes = @{
	"Catalog" = @(
		@{ prefix = "CatalogObject";    category = "Object" }
		@{ prefix = "CatalogRef";       category = "Ref" }
		@{ prefix = "CatalogSelection"; category = "Selection" }
		@{ prefix = "CatalogList";      category = "List" }
		@{ prefix = "CatalogManager";   category = "Manager" }
	)
	"Document" = @(
		@{ prefix = "DocumentObject";    category = "Object" }
		@{ prefix = "DocumentRef";       category = "Ref" }
		@{ prefix = "DocumentSelection"; category = "Selection" }
		@{ prefix = "DocumentList";      category = "List" }
		@{ prefix = "DocumentManager";   category = "Manager" }
	)
	"Enum" = @(
		@{ prefix = "EnumRef";     category = "Ref" }
		@{ prefix = "EnumManager"; category = "Manager" }
		@{ prefix = "EnumList";    category = "List" }
	)
	"Constant" = @(
		@{ prefix = "ConstantManager";      category = "Manager" }
		@{ prefix = "ConstantValueManager"; category = "ValueManager" }
		@{ prefix = "ConstantValueKey";     category = "ValueKey" }
	)
	"InformationRegister" = @(
		@{ prefix = "InformationRegisterRecord";        category = "Record" }
		@{ prefix = "InformationRegisterManager";       category = "Manager" }
		@{ prefix = "InformationRegisterSelection";     category = "Selection" }
		@{ prefix = "InformationRegisterList";          category = "List" }
		@{ prefix = "InformationRegisterRecordSet";     category = "RecordSet" }
		@{ prefix = "InformationRegisterRecordKey";     category = "RecordKey" }
		@{ prefix = "InformationRegisterRecordManager"; category = "RecordManager" }
	)
	"AccumulationRegister" = @(
		@{ prefix = "AccumulationRegisterRecord";    category = "Record" }
		@{ prefix = "AccumulationRegisterManager";   category = "Manager" }
		@{ prefix = "AccumulationRegisterSelection"; category = "Selection" }
		@{ prefix = "AccumulationRegisterList";      category = "List" }
		@{ prefix = "AccumulationRegisterRecordSet"; category = "RecordSet" }
		@{ prefix = "AccumulationRegisterRecordKey"; category = "RecordKey" }
	)
	"AccountingRegister" = @(
		@{ prefix = "AccountingRegisterRecord";         category = "Record" }
		@{ prefix = "AccountingRegisterExtDimensions";  category = "ExtDimensions" }
		@{ prefix = "AccountingRegisterRecordSet";      category = "RecordSet" }
		@{ prefix = "AccountingRegisterRecordKey";      category = "RecordKey" }
		@{ prefix = "AccountingRegisterSelection";      category = "Selection" }
		@{ prefix = "AccountingRegisterList";           category = "List" }
		@{ prefix = "AccountingRegisterManager";        category = "Manager" }
	)
	"CalculationRegister" = @(
		@{ prefix = "CalculationRegisterRecord";    category = "Record" }
		@{ prefix = "CalculationRegisterManager";   category = "Manager" }
		@{ prefix = "CalculationRegisterSelection"; category = "Selection" }
		@{ prefix = "CalculationRegisterList";      category = "List" }
		@{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" }
		@{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" }
		@{ prefix = "RecalculationsManager";        category = "Recalcs" }
	)
	"ChartOfAccounts" = @(
		@{ prefix = "ChartOfAccountsObject";              category = "Object" }
		@{ prefix = "ChartOfAccountsRef";                 category = "Ref" }
		@{ prefix = "ChartOfAccountsSelection";           category = "Selection" }
		@{ prefix = "ChartOfAccountsList";                category = "List" }
		@{ prefix = "ChartOfAccountsManager";             category = "Manager" }
		@{ prefix = "ChartOfAccountsExtDimensionTypes";   category = "ExtDimensionTypes" }
		@{ prefix = "ChartOfAccountsExtDimensionTypesRow"; category = "ExtDimensionTypesRow" }
	)
	"ChartOfCharacteristicTypes" = @(
		@{ prefix = "ChartOfCharacteristicTypesObject";         category = "Object" }
		@{ prefix = "ChartOfCharacteristicTypesRef";            category = "Ref" }
		@{ prefix = "ChartOfCharacteristicTypesSelection";      category = "Selection" }
		@{ prefix = "ChartOfCharacteristicTypesList";           category = "List" }
		@{ prefix = "Characteristic";                          category = "Characteristic" }
		@{ prefix = "ChartOfCharacteristicTypesManager";        category = "Manager" }
	)
	"ChartOfCalculationTypes" = @(
		@{ prefix = "ChartOfCalculationTypesObject";    category = "Object" }
		@{ prefix = "ChartOfCalculationTypesRef";       category = "Ref" }
		@{ prefix = "ChartOfCalculationTypesSelection"; category = "Selection" }
		@{ prefix = "ChartOfCalculationTypesList";      category = "List" }
		@{ prefix = "ChartOfCalculationTypesManager";   category = "Manager" }
		@{ prefix = "DisplacingCalculationTypes";       category = "DisplacingCalculationTypes" }
		@{ prefix = "DisplacingCalculationTypesRow";    category = "DisplacingCalculationTypesRow" }
		@{ prefix = "BaseCalculationTypes";             category = "BaseCalculationTypes" }
		@{ prefix = "BaseCalculationTypesRow";          category = "BaseCalculationTypesRow" }
		@{ prefix = "LeadingCalculationTypes";          category = "LeadingCalculationTypes" }
		@{ prefix = "LeadingCalculationTypesRow";       category = "LeadingCalculationTypesRow" }
	)
	"BusinessProcess" = @(
		@{ prefix = "BusinessProcessObject";        category = "Object" }
		@{ prefix = "BusinessProcessRef";            category = "Ref" }
		@{ prefix = "BusinessProcessSelection";      category = "Selection" }
		@{ prefix = "BusinessProcessList";           category = "List" }
		@{ prefix = "BusinessProcessManager";        category = "Manager" }
		@{ prefix = "BusinessProcessRoutePointRef";  category = "RoutePointRef" }
	)
	"Task" = @(
		@{ prefix = "TaskObject";    category = "Object" }
		@{ prefix = "TaskRef";       category = "Ref" }
		@{ prefix = "TaskSelection"; category = "Selection" }
		@{ prefix = "TaskList";      category = "List" }
		@{ prefix = "TaskManager";   category = "Manager" }
	)
	"ExchangePlan" = @(
		@{ prefix = "ExchangePlanObject";    category = "Object" }
		@{ prefix = "ExchangePlanRef";       category = "Ref" }
		@{ prefix = "ExchangePlanSelection"; category = "Selection" }
		@{ prefix = "ExchangePlanList";      category = "List" }
		@{ prefix = "ExchangePlanManager";   category = "Manager" }
	)
	"DefinedType" = @(
		@{ prefix = "DefinedType"; category = "DefinedType" }
	)
	"DocumentJournal" = @(
		@{ prefix = "DocumentJournalSelection"; category = "Selection" }
		@{ prefix = "DocumentJournalList";      category = "List" }
		@{ prefix = "DocumentJournalManager";   category = "Manager" }
	)
	"Report" = @(
		@{ prefix = "ReportObject";  category = "Object" }
		@{ prefix = "ReportManager"; category = "Manager" }
	)
	"DataProcessor" = @(
		@{ prefix = "DataProcessorObject";  category = "Object" }
		@{ prefix = "DataProcessorManager"; category = "Manager" }
	)
	"Sequence" = @(
		@{ prefix = "SequenceRecord";    category = "Record" }
		@{ prefix = "SequenceManager";   category = "Manager" }
		@{ prefix = "SequenceRecordSet"; category = "RecordSet" }
	)
	"FilterCriterion" = @(
		@{ prefix = "FilterCriterionManager"; category = "Manager" }
		@{ prefix = "FilterCriterionList";    category = "List" }
	)
	"SettingsStorage" = @(
		@{ prefix = "SettingsStorageManager"; category = "Manager" }
	)
	"WSReference" = @(
		@{ prefix = "WSReferenceManager"; category = "Manager" }
	)
}

function Emit-InternalInfo {
	param([string]$indent, [string]$objectType, [string]$objectName)
	$types = $script:generatedTypes[$objectType]
	if (-not $types) { return }

	X "$indent<InternalInfo>"
	# ExchangePlan: ThisNode UUID before GeneratedTypes
	if ($objectType -eq "ExchangePlan") {
		X "$indent`t<xr:ThisNode>$(New-Guid-String)</xr:ThisNode>"
	}
	foreach ($gt in $types) {
		$fullName = "$($gt.prefix).$objectName"
		X "$indent`t<xr:GeneratedType name=`"$fullName`" category=`"$($gt.category)`">"
		X "$indent`t`t<xr:TypeId>$(New-Guid-String)</xr:TypeId>"
		X "$indent`t`t<xr:ValueId>$(New-Guid-String)</xr:ValueId>"
		X "$indent`t</xr:GeneratedType>"
	}
	X "$indent</InternalInfo>"
}

# --- 7. StandardAttributes ---

$script:standardAttributesByType = @{
	"Catalog" = @("PredefinedDataName","Predefined","Ref","DeletionMark","IsFolder","Owner","Parent","Description","Code")
	"Document" = @("Posted","Ref","DeletionMark","Date","Number")
	"Enum" = @("Order","Ref")
	"InformationRegister" = @("Active","LineNumber","Recorder","Period")
	"AccumulationRegister" = @("RecordType","Active","LineNumber","Recorder","Period")
	"AccountingRegister" = @("PeriodAdjustment","Account","RecordType","Active","LineNumber","Recorder","Period")
	"CalculationRegister" = @("RegistrationPeriod","ReversingEntry","Active","EndOfBasePeriod","BegOfBasePeriod","EndOfActionPeriod","BegOfActionPeriod","ActionPeriod","CalculationType","LineNumber","Recorder")
	"ChartOfAccounts" = @("PredefinedDataName","Order","OffBalance","Type","Description","Code","Parent","Predefined","DeletionMark","Ref")
	"ChartOfCharacteristicTypes" = @("PredefinedDataName","ValueType","Description","Code","IsFolder","Parent","Predefined","DeletionMark","Ref")
	"ChartOfCalculationTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","ActionPeriodIsBasic","Description","Code")
	"BusinessProcess" = @("Started","HeadTask","Completed","Ref","DeletionMark","Date","Number")
	"Task" = @("Executed","Description","RoutePoint","BusinessProcess","Ref","DeletionMark","Date","Number")
	# Порядок в каждом списке — канон выгрузки, снят с корпуса acc+erp (внутри типа разброса нет).
	# Условные члены перечислены в $script:stdAttrConditions — позицию они берут отсюда, а
	# присутствие определяется свойствами объекта.
	# У ПВХ IsFolder входит в фикс-список: он есть у всех 23 объектов корпуса с этим блоком.
	# У регистра расчёта список безусловен: реквизиты периода действия и базового периода
	# платформа пишет при любых ActionPeriod/BasePeriod/Periodicity (синтетика, все 4 комбинации).
	"ExchangePlan" = @("ThisNode","ReceivedNo","SentNo","Ref","DeletionMark","Description","Code")
	"DocumentJournal" = @("Type","Ref","Date","Posted","DeletionMark","Number")
}

# Профиль материализованного блока StandardAttributes (значения, которые платформа заполняет
# автоматически при материализации блока, независимо от структуры каталога). Выведено из корпуса
# (acc+erp: Owner.FFV=true 1592/1596, Owner.FC=ShowError 1589, Parent.FFV=true 1593, Description.FC=ShowError 1467)
# и подтверждено синтетикой. Пока только Catalog (у прочих типов свои профили — добавим при их пилоте).
$script:stdAttrProfile = @{
	"Catalog" = @{
		"Owner"       = @{ FillChecking = "ShowError"; FillFromFillingValue = "true" }
		"Parent"      = @{ FillFromFillingValue = "true" }
		"Description"  = @{ FillChecking = "ShowError" }
	}
	# ExchangePlan: Наименование/Код → FillChecking=ShowError (корпус 40/38 из 41).
	"ExchangePlan" = @{
		"Description" = @{ FillChecking = "ShowError" }
		"Code"        = @{ FillChecking = "ShowError" }
	}
	# ChartOfCharacteristicTypes: Наименование → FillChecking=ShowError (21/23), Родитель → FFV=true (23/23).
	"ChartOfCharacteristicTypes" = @{
		"Description" = @{ FillChecking = "ShowError" }
		"Parent"      = @{ FillFromFillingValue = "true" }
	}
	# ChartOfAccounts: Наименование/Код → FillChecking=ShowError (3/3), Родитель → FFV=true (3/3). Тип (АктивПассив)
	# и FillValue Родителя (self EmptyRef) кастомизируются пообъектно → захват override, не профиль.
	"ChartOfAccounts" = @{
		"Description" = @{ FillChecking = "ShowError" }
		"Code"        = @{ FillChecking = "ShowError" }
		"Parent"      = @{ FillFromFillingValue = "true" }
	}
	# ChartOfCalculationTypes: Наименование → FillChecking=ShowError (Код здесь DontCheck).
	"ChartOfCalculationTypes" = @{
		"Description" = @{ FillChecking = "ShowError" }
	}
	# Document: Дата → FillChecking=ShowError (974/1010 доков acc+erp; дата обязательна).
	"Document" = @{
		"Date" = @{ FillChecking = "ShowError" }
	}
}

# $ov — hashtable переопределений (профиль + DSL) для полей: FillChecking, FillFromFillingValue,
# Synonym, FullTextSearch, DataHistory. Прочие поля — фиксированный schema-дефолт.
function Emit-StandardAttribute {
	param([string]$indent, [string]$attrName, $ov = $null)
	function OvOr { param($k, $d) if ($ov -and $ov.ContainsKey($k)) { return $ov[$k] } else { return $d } }
	$fc  = OvOr 'FillChecking' 'DontCheck'
	$ffv = OvOr 'FillFromFillingValue' 'false'
	$dh  = OvOr 'DataHistory' 'Use'
	$fts = OvOr 'FullTextSearch' 'Use'
	$syn = OvOr 'Synonym' ''
	$tt  = OvOr 'ToolTip' ''
	$cf  = OvOr 'ChoiceForm' ''
	$cmt = OvOr 'Comment' ''
	$msk = OvOr 'Mask' ''
	$fmt = OvOr 'Format' $null
	$efmt = OvOr 'EditFormat' $null
	$chi = OvOr 'ChoiceHistoryOnInput' 'Auto'
	X "$indent<xr:StandardAttribute name=`"$attrName`">"
	# LinkByType стандартного реквизита (напр. ExtDimensionN→Account у регистра бухгалтерии). DataPath verbatim (полный).
	$lbt = OvOr 'LinkByType' $null
	if ($lbt) {
		$lbtDp = if ($lbt.dataPath) { "$($lbt.dataPath)" } else { "$lbt" }
		$lbtLi = if ($null -ne $lbt.linkItem) { $lbt.linkItem } else { 0 }
		X "$indent`t<xr:LinkByType>"
		X "$indent`t`t<xr:DataPath>$(Esc-XmlText $lbtDp)</xr:DataPath>"
		X "$indent`t`t<xr:LinkItem>$lbtLi</xr:LinkItem>"
		X "$indent`t</xr:LinkByType>"
	} else {
		X "$indent`t<xr:LinkByType/>"
	}
	X "$indent`t<xr:FillChecking>$fc</xr:FillChecking>"
	X "$indent`t<xr:MultiLine>false</xr:MultiLine>"
	X "$indent`t<xr:FillFromFillingValue>$ffv</xr:FillFromFillingValue>"
	X "$indent`t<xr:CreateOnInput>Auto</xr:CreateOnInput>"
	# Формат 2.18 (8.3.25): режим приведения типов. Платформа пишет его КАЖДОМУ стандартному
	# реквизиту; значение всегда TransformValues, кроме владельца (Owner) — там Deny.
	if ($script:isFormat218) {
		$trm = OvOr 'TypeReductionMode' $(if ($attrName -ceq 'Owner') { 'Deny' } else { 'TransformValues' })
		X "$indent`t<xr:TypeReductionMode>$trm</xr:TypeReductionMode>"
	}
	X "$indent`t<xr:MaxValue xsi:nil=`"true`"/>"
	Emit-MLText "$indent`t" "xr:ToolTip" $tt
	X "$indent`t<xr:ExtendedEdit>false</xr:ExtendedEdit>"
	Emit-MLText "$indent`t" "xr:Format" $fmt
	if ($cf) { X "$indent`t<xr:ChoiceForm>$(Esc-XmlText "$cf")</xr:ChoiceForm>" } else { X "$indent`t<xr:ChoiceForm/>" }
	X "$indent`t<xr:QuickChoice>Auto</xr:QuickChoice>"
	X "$indent`t<xr:ChoiceHistoryOnInput>$chi</xr:ChoiceHistoryOnInput>"
	Emit-MLText "$indent`t" "xr:EditFormat" $efmt
	X "$indent`t<xr:PasswordMode>false</xr:PasswordMode>"
	X "$indent`t<xr:DataHistory>$dh</xr:DataHistory>"
	X "$indent`t<xr:MarkNegatives>false</xr:MarkNegatives>"
	X "$indent`t<xr:MinValue xsi:nil=`"true`"/>"
	Emit-MLText "$indent`t" "xr:Synonym" $syn
	if ($cmt) { X "$indent`t<xr:Comment>$(Esc-XmlText "$cmt")</xr:Comment>" } else { X "$indent`t<xr:Comment/>" }
	X "$indent`t<xr:FullTextSearch>$fts</xr:FullTextSearch>"
	Emit-ChoiceParameterLinks "$indent`t" (OvOr 'ChoiceParameterLinks' $null) 'xr:ChoiceParameterLinks'
	# FillValue: дефолт nil; override-значение → типизированное (Normalize-ChoiceValue: DTR-путь/строка/bool).
	$fvRaw = OvOr 'FillValue' $null
	if ($null -eq $fvRaw) { X "$indent`t<xr:FillValue xsi:nil=`"true`"/>" }
	elseif ($fvRaw.emptyRef -eq $true) { X "$indent`t<xr:FillValue xsi:type=`"xr:DesignTimeRef`"/>" }
	elseif ($fvRaw.typeDescription -eq $true) { X "$indent`t<xr:FillValue xsi:type=`"v8:TypeDescription`"/>" }   # пустое типизированное (ValueType ПВХ)
	else {
		$fvN = Normalize-ChoiceValue $fvRaw
		if ([string]::IsNullOrEmpty($fvN.Text)) { X "$indent`t<xr:FillValue xsi:type=`"$($fvN.XsiType)`"/>" }
		else { X "$indent`t<xr:FillValue xsi:type=`"$($fvN.XsiType)`">$(Esc-XmlText $fvN.Text)</xr:FillValue>" }
	}
	if ($msk) { X "$indent`t<xr:Mask>$(Esc-XmlText "$msk")</xr:Mask>" } else { X "$indent`t<xr:Mask/>" }
	Emit-ChoiceParameters "$indent`t" (OvOr 'ChoiceParameters' $null) 'xr:ChoiceParameters'
	X "$indent</xr:StandardAttribute>"
}

# Единый эмиттер блока StandardAttributes — поведение правят ДАННЫЕ, не форк кода:
#  - stdAttrConditionalTypes: типы, где блок материализуется платформой ТОЛЬКО при кастомизации
#    ≥1 стандартного реквизита → в DSL это наличие ключа `standardAttributes`. Нет ключа → блок опущен.
#    Прочие типы (не в множестве) → блок эмитится всегда (текущее поведение, пока их правило не выведено).
#  - stdAttrProfile[тип]: профиль материализованного блока (пусто = schema-дефолт), поверх — DSL-override.
# Миграция типа = добавить его в stdAttrConditionalTypes + stdAttrProfile и переснять снэпшоты; КОД НЕ ТРОГАЕМ.
$script:stdAttrConditionalTypes = @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document')

# Условные члены списка типа: позиция берётся из $script:standardAttributesByType, а присутствие —
# из свойств самого объекта, как у платформы. Предикат ОБЯЗАН принимать определение параметром:
# scriptblock не видит $def вызывающей функции, и отказ был бы молчаливым.
$script:stdAttrConditions = @{
	"AccountingRegister" = @{
		"PeriodAdjustment" = { param($d) $v = 0; if ($null -ne $d.periodAdjustmentLength) { $v = [int]"$($d.periodAdjustmentLength)" }; $v -gt 0 }
		"RecordType"       = { param($d) -not ($d.correspondence -eq $true) }
	}
	"AccumulationRegister" = @{
		"RecordType" = { param($d) $raw = if ($d.registerType) { "$($d.registerType)" } else { "Balance" }; (Normalize-EnumValue "RegisterType" $raw) -eq "Balance" }
	}
}

# Хвостовая группа: реквизиты, которых нет в списке типа и которые идут ПОСЛЕ него.
# У бухрегистра это пары субконто. Именами их не перечислить: их количество задаётся
# свойством MaxExtDimensionCount плана счетов, а не константой (в корпусе везде 3, но
# это однородность выборки, а не правило). Поэтому — шаблон, а не список.
$script:stdAttrTailPattern = @{
	"AccountingRegister" = '^ExtDimension(Type)?\d+$'
}

# Состав хвоста задаёт не DSL, а объект, на который регистр ссылается: пар субконто столько,
# сколько у плана счетов MaxExtDimensionCount. Читаем его из выгрузки — как версию формата из
# Configuration.xml, — чтобы регистр, описанный неполным DSL, совпал с тем, что материализует
# платформа. План не найден → хвост не генерируем и говорим об этом в выводе.
$script:stdAttrTailHint = $null
$script:stdAttrTailDerived = @{
	"AccountingRegister" = {
		param($d, $objectName, $outDir)
		$ref = "$($d.chartOfAccounts)"
		if (-not $ref) { return @() }
		$chartName = $ref -replace '^.*\.', ''    # ссылка вида ChartOfAccounts.X (имя объекта точек не содержит)
		$path = Join-Path (Join-Path $outDir "ChartsOfAccounts") "$chartName.xml"
		if (-not (Test-Path -LiteralPath $path)) {
			$script:stdAttrTailHint = "ChartOfAccounts '$chartName' not found in dump — ExtDimension pairs not generated (platform will add them on load)"
			return @()
		}
		$n = 0
		if ([System.IO.File]::ReadAllText($path) -match '<MaxExtDimensionCount>(\d+)</MaxExtDimensionCount>') { $n = [int]$matches[1] }
		$out = @()
		for ($i = 1; $i -le $n; $i++) {
			# ExtDimensionN связан с Account через LinkByType (LinkItem = номер), ExtDimensionTypeN — нет.
			$out += @{ name = "ExtDimension$i"; ov = @{ LinkByType = @{ dataPath = "AccountingRegister.$objectName.StandardAttribute.Account"; linkItem = $i } } }
			$out += @{ name = "ExtDimensionType$i"; ov = @{} }
		}
		return $out
	}
}
function Emit-StandardAttributes {
	param([string]$indent, [string]$objectType)
	$attrs = $script:standardAttributesByType[$objectType]
	if (-not $attrs) { return }
	$conditional = $script:stdAttrConditionalTypes -contains $objectType
	$sa = $def.standardAttributes
	if ($conditional -and $null -eq $sa) { return }   # условный тип без кастомизации → блока нет
	if ($sa -is [string] -and $sa -eq '') { return }  # opt-out `standardAttributes:""` (дом-конвенция суппресса, ~5% регистров опускают all-default блок — правило не выводимо)
	$profile = $script:stdAttrProfile[$objectType]; if (-not $profile) { $profile = @{} }
	# Список типа задаёт ПОРЯДОК всех известных стандартных реквизитов, включая условные:
	# их позиция бывает и до, и после обязательных (у бухрегистра PeriodAdjustment идёт
	# перед Account, RecordType — после, а ExtDimension1..3/ExtDimensionType1..3 — после Period),
	# поэтому «условные скопом вперёд» не выражает канон.
	$cond = $script:stdAttrConditions[$objectType]
	# Ключи, которых нет в списке типа ВООБЩЕ. По умолчанию их позиция — ПЕРЕД списком
	# (легаси вроде ExchangeDate у части планов обмена). Подходящие под хвостовой шаблон
	# типа идут ПОСЛЕ, в порядке номера, а внутри номера — сначала ExtDimensionN, затем
	# ExtDimensionTypeN (порядок платформы).
	$tailRe = $script:stdAttrTailPattern[$objectType]
	$extra = @(); $tail = @()
	if ($sa) {
		foreach ($k in $sa.PSObject.Properties.Name) {
			if ($attrs -contains $k) { continue }
			if ($tailRe -and $k -match $tailRe) { $tail += $k } else { $extra += $k }
		}
	}
	# Хвост, выведенный из связанного объекта: дополняет DSL, а не заменяет его — лишнее из DSL
	# остаётся (прощаем), недостающее добавляется вместе со своими значениями по умолчанию.
	$derivedOv = @{}
	$gen = $script:stdAttrTailDerived[$objectType]
	if ($gen) {
		foreach ($e in @(& $gen $def $objName $OutputDir)) {
			$derivedOv[$e.name] = $e.ov
			if ($tail -notcontains $e.name) { $tail += $e.name }
		}
	}
	$tail = @($tail | Sort-Object @{e={[int]([regex]::Match($_, '\d+').Value)}}, @{e={ if ($_ -match 'Type\d+$') { 1 } else { 0 } }})
	X "$indent<StandardAttributes>"
	foreach ($a in ($extra + $attrs + $tail)) {
		# Условный реквизит: эмитим, если так велят свойства объекта ЛИБО если ключ есть в DSL.
		# Дизъюнкция страхует роундтрип — декомпилятор перечисляет все имена блока.
		if ($cond -and $cond.ContainsKey($a)) {
			$present = ($sa -and $sa.PSObject.Properties[$a]) -or (& $cond[$a] $def)
			if (-not $present) { continue }
		}
		$ov = @{}
		if ($profile.ContainsKey($a)) { foreach ($k in $profile[$a].Keys) { $ov[$k] = $profile[$a][$k] } }
		if ($derivedOv.ContainsKey($a)) { foreach ($k in $derivedOv[$a].Keys) { $ov[$k] = $derivedOv[$a][$k] } }
		if ($sa) {   # DSL-override применяем всегда при наличии ключа (для не-условных типов тоже, напр. ExchangePlan)
			$d = $sa.$a
			if ($d) {
				if ($null -ne $d.synonym) { $ov['Synonym'] = $d.synonym }   # строка ИЛИ {ru,en}
				if ($null -ne $d.tooltip) { $ov['ToolTip'] = $d.tooltip }   # строка ИЛИ {ru,en}
				if ($d.fillChecking) { $ov['FillChecking'] = "$($d.fillChecking)" }
				if ($null -ne $d.fillFromFillingValue) { $ov['FillFromFillingValue'] = if ($d.fillFromFillingValue) { 'true' } else { 'false' } }
				if ($d.fullTextSearch) { $ov['FullTextSearch'] = "$($d.fullTextSearch)" }
				if ($d.dataHistory) { $ov['DataHistory'] = "$($d.dataHistory)" }
				if ($null -ne $d.fillValue) { $ov['FillValue'] = $d.fillValue }   # DTR-путь/строка/bool
				if ($null -ne $d.choiceParameterLinks) { $ov['ChoiceParameterLinks'] = $d.choiceParameterLinks }
				if ($null -ne $d.choiceParameters) { $ov['ChoiceParameters'] = $d.choiceParameters }
				if ($d.comment) { $ov['Comment'] = "$($d.comment)" }
				if ($d.mask) { $ov['Mask'] = "$($d.mask)" }
				if ($null -ne $d.format) { $ov['Format'] = $d.format }         # строка ИЛИ {ru,en}
				if ($null -ne $d.editFormat) { $ov['EditFormat'] = $d.editFormat }
				if ($d.choiceForm) { $ov['ChoiceForm'] = "$($d.choiceForm)" }
				if ($null -ne $d.linkByType) { $ov['LinkByType'] = $d.linkByType }
			}
		}
		Emit-StandardAttribute "$indent`t" $a $ov
	}
	X "$indent</StandardAttributes>"
}

# TabularSection standard attributes (единственный — LineNumber/НомерСтроки). Блок эмитится всегда (платформа
# опускает его лишь у редкого хвоста ТЧ — правило не выведено, см. WORKFLOW). DSL `lineNumber` на объектной форме ТЧ
# переопределяет свойства (synonym/comment/fullTextSearch/tooltip/format/editFormat/choiceHistoryOnInput).
function Emit-TabularStandardAttributes {
	param([string]$indent, $lineNumber = $null)
	$ov = $null
	if ($lineNumber) {
		$ov = @{}
		if ($null -ne $lineNumber.synonym)            { $ov['Synonym'] = $lineNumber.synonym }
		if ($lineNumber.comment)                      { $ov['Comment'] = "$($lineNumber.comment)" }
		if ($lineNumber.fullTextSearch)               { $ov['FullTextSearch'] = "$($lineNumber.fullTextSearch)" }
		if ($null -ne $lineNumber.tooltip)            { $ov['ToolTip'] = $lineNumber.tooltip }
		if ($null -ne $lineNumber.format)             { $ov['Format'] = $lineNumber.format }
		if ($null -ne $lineNumber.editFormat)         { $ov['EditFormat'] = $lineNumber.editFormat }
		if ($lineNumber.choiceHistoryOnInput)         { $ov['ChoiceHistoryOnInput'] = "$($lineNumber.choiceHistoryOnInput)" }
		if ($null -ne $lineNumber.fillValue)          { $ov['FillValue'] = $lineNumber.fillValue }
	}
	X "$indent<StandardAttributes>"
	Emit-StandardAttribute "$indent`t" "LineNumber" $ov
	X "$indent</StandardAttributes>"
}

# --- 8. Attribute emitter ---

$script:reservedAttrNames = @{
	"Ref"="Ссылка"; "DeletionMark"="ПометкаУдаления"; "Code"="Код"; "Description"="Наименование"
	"Date"="Дата"; "Number"="Номер"; "Posted"="Проведен"; "Parent"="Родитель"; "Owner"="Владелец"
	"IsFolder"="ЭтоГруппа"; "Predefined"="Предопределенный"; "PredefinedDataName"="ИмяПредопределенныхДанных"
	"Recorder"="Регистратор"; "Period"="Период"; "LineNumber"="НомерСтроки"; "Active"="Активность"
	"Order"="Порядок"; "Type"="Тип"; "OffBalance"="Забалансовый"
	"Started"="Стартован"; "Completed"="Завершен"; "HeadTask"="ВедущаяЗадача"
	"Executed"="Выполнена"; "RoutePoint"="ТочкаМаршрута"; "BusinessProcess"="БизнесПроцесс"
	"ThisNode"="ЭтотУзел"; "SentNo"="НомерОтправленного"; "ReceivedNo"="НомерПринятого"
	"CalculationType"="ВидРасчета"; "RegistrationPeriod"="ПериодРегистрации"; "ReversingEntry"="СторноЗапись"
	"Account"="Счет"; "ValueType"="ТипЗначения"; "ActionPeriodIsBasic"="ПериодДействияБазовый"
}

# Стандартные реквизиты по типу объекта (ключи из reservedAttrNames). Имя реквизита, совпадающее
# с ними (англ. ИЛИ рус.), платформа не позволит — жёсткий отказ. Контексты вне карты → мягкое предупреждение.
$script:reservedByContext = @{
	"catalog"  = @("Ref","DeletionMark","Predefined","PredefinedDataName","Code","Description","Owner","Parent","IsFolder")
	"document" = @("Ref","DeletionMark","Date","Number","Posted")
}

# Стандартный реквизит текущего типа по имени (EN/RU) → EN-имя, либо $null (обычный/неизвестный).
function Resolve-StdAttrEn {
	param([string]$name)
	$ctx = switch ("$objType") { 'Catalog' { 'catalog' } 'Document' { 'document' } default { $null } }
	if (-not $ctx) { return $null }
	$stdSet = $script:reservedByContext[$ctx]
	foreach ($en in $stdSet) {
		$ru = $script:reservedAttrNames[$en]
		if (($name -ieq $en) -or ($ru -and $name -ieq $ru)) { return $en }
	}
	return $null
}

# Прощающий ввод пути к реквизиту САМОГО объекта (dataPath в linkByType/choiceParameterLinks):
#   "Ссылка"/"Ref"/станд. → <Тип>.<Имя>.StandardAttribute.<EN>;  обычное имя → <Тип>.<Имя>.Attribute.<Имя>;
#   частичное "StandardAttribute.X"/"Attribute.X" → префикс <Тип>.<Имя>;  полный путь → verbatim.
function Expand-DataPath {
	param([string]$dp)
	if (-not $dp) { return $dp }
	$s = "$dp"
	if ($s -match '[:/]') { return $s }   # спец-путь (напр. 0:GUID/0:GUID в зависимостях ПВХ) — не разворачиваем
	if ($s -match '^-?\d+$') { return $s }  # голый (отрицательный) индекс-маркер (напр. -8 в ChoiceParameterLinks) — verbatim, не имя реквизита
	if ($s -match '^(StandardAttribute|Attribute)\.') { return "$objType.$objName.$s" }
	if (-not $s.Contains('.')) {
		$en = Resolve-StdAttrEn $s
		if ($en) { return "$objType.$objName.StandardAttribute.$en" }
		return "$objType.$objName.Attribute.$s"
	}
	return $s
}

# <LinkByType> (связь по типу — тип значения реквизита-Характеристики определяется другим реквизитом).
# Структура как <TypeLink> формы: DataPath + LinkItem. DSL `linkByType`: {dataPath, linkItem?} ИЛИ строка-путь.
# Нет ключа → <LinkByType/> (пусто).
function Emit-LinkByType {
	param([string]$indent, $spec)
	if (-not $spec) { X "$indent<LinkByType/>"; return }
	if ($spec -is [string]) { $dp = "$spec"; $li = 0 }
	else {
		$dp = if ($spec.dataPath) { "$($spec.dataPath)" } elseif ($spec.path) { "$($spec.path)" } elseif ($spec.путь) { "$($spec.путь)" } else { "" }
		$li = if ($null -ne $spec.linkItem) { $spec.linkItem } elseif ($null -ne $spec.элементСвязи) { $spec.элементСвязи } else { 0 }
	}
	if (-not $dp) { X "$indent<LinkByType/>"; return }
	$dp = Expand-DataPath $dp
	X "$indent<LinkByType>"
	X "$indent`t<xr:DataPath>$(Esc-XmlText "$dp")</xr:DataPath>"
	X "$indent`t<xr:LinkItem>$li</xr:LinkItem>"
	X "$indent</LinkByType>"
}

# Есть ли ключ в $def (отличаем отсутствие от пустого массива [] = явно пусто).
function Test-DefKey { param([string]$name) return ($def.PSObject -and $def.PSObject.Properties -and ($def.PSObject.Properties.Name -contains $name)) }

# <Tag> со списком <xr:Field> (InputByString/DataLockFields). $fields — готовые полные пути. Пусто → self-close.
function Emit-FieldBlock {
	param([string]$indent, [string]$tag, $fields)
	$arr = @($fields | Where-Object { "$_" -ne '' })
	if ($arr.Count -eq 0) { X "$indent<$tag/>"; return }
	X "$indent<$tag>"
	foreach ($f in $arr) { X "$indent`t<xr:Field>$(Esc-XmlText "$f")</xr:Field>" }
	X "$indent</$tag>"
}

# <BasedOn> — «ввод на основании», список MDObjectRef ("Catalog.X"/"Document.Y"). Нет ключа/пусто → self-close.
function Emit-BasedOn {
	param([string]$indent, $items)
	$arr = @($items | Where-Object { $_ })
	if ($arr.Count -eq 0) { X "$indent<BasedOn/>"; return }
	X "$indent<BasedOn>"
	foreach ($it in $arr) { X "$indent`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-XmlText (Normalize-MDObjectRef "$it"))</xr:Item>" }
	X "$indent</BasedOn>"
}

# --- Параметры/связи выбора (порт из form-compile; структура реквизита ⟷ элемента формы совпадает) ---

# Свойство из dict/PSCustomObject по списку синонимов (первый найденный, иначе $null).
function Get-ChElProp {
	param($obj, [string[]]$names)
	if ($null -eq $obj) { return $null }
	foreach ($n in $names) {
		if ($obj -is [System.Collections.IDictionary]) { if ($obj.Contains($n)) { return $obj[$n] } }
		elseif ($obj.PSObject -and $obj.PSObject.Properties[$n]) { return $obj.PSObject.Properties[$n].Value }
	}
	return $null
}

# Строковый литерал shorthand → скаляр: true/false→bool, целое/дробное→число, иначе строка.
function ConvertTo-ChScalar {
	param([string]$s)
	$t = "$s".Trim()
	if ($t -match '^(?i:true|истина)$')  { return $true }
	if ($t -match '^(?i:false|ложь)$') { return $false }
	if ($t -match '^-?\d+$')       { return [int]$t }
	if ($t -match '^-?\d+\.\d+$')  { return [double]::Parse($t, [System.Globalization.CultureInfo]::InvariantCulture) }
	return $t
}

# Голое значение (без точки) + тип параметра → полный DTR-путь, либо $null. Принимает EnumRef.X / Enum.X / рус.
function Expand-ChoiceRefValue {
	param([string]$value, [string]$typeStr)
	if (-not $typeStr) { return $null }
	$t = Resolve-TypeStr $typeStr
	$root = $null; $tn = $null
	if ($t -match '^(\w+Ref)\.(.+)$') { $root = $script:fillRefKindRoot[$Matches[1].ToLower()]; $tn = $Matches[2] }
	elseif ($t -match '^([^.]+)\.(.+)$') { $root = $script:fillRefRoots[$Matches[1].ToLower()]; $tn = $Matches[2] }
	if (-not $root) { return $null }
	if ($script:fillEmptyRefWords -contains "$value".ToLower()) { return "$root.$tn.EmptyRef" }
	if ($root -eq 'Enum') { return "Enum.$tn.EnumValue.$value" }
	return "$root.$tn.$value"
}

# Значение параметра выбора → @{XsiType; Text}. $typeStr (тип параметра) разворачивает голые ref-имена.
function Normalize-ChoiceValueT {
	param($value, [string]$typeStr)
	if ($typeStr -and ($value -is [string]) -and (-not "$value".Contains('.'))) {
		$ex = Expand-ChoiceRefValue "$value" $typeStr
		if ($ex) { return @{ XsiType='xr:DesignTimeRef'; Text=$ex } }
	}
	return Normalize-ChoiceValue $value
}

# Значение параметра выбора → @{XsiType; Text}. Авто-детект по значению (без типа реквизита).
function Normalize-ChoiceValue {
	param($value)
	if ($value -is [bool]) { return @{ XsiType='xs:boolean'; Text=$(if ($value) { 'true' } else { 'false' }) } }
	if ($value -is [int] -or $value -is [long] -or $value -is [double] -or $value -is [decimal]) {
		return @{ XsiType='xs:decimal'; Text=(Format-FillNum $value) }
	}
	$s = "$value"
	if ($s -eq '') { return @{ XsiType='xs:string'; Text='' } }
	if ($s -match '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$') { return @{ XsiType='xs:dateTime'; Text=$s } }
	$ref = Normalize-FillRef $s
	if ($ref) { return @{ XsiType='xr:DesignTimeRef'; Text=$ref } }
	if ($script:accountTypeValues -contains $s) { return @{ XsiType='ent:AccountType'; Text=$s } }
	return @{ XsiType='xs:string'; Text=$s }
}

# Shorthand "name=value" | "name=v1, v2" → {name, value}. "name=path" для links.
function ConvertFrom-ChParamShorthand {
	param([string]$s)
	$eq = $s.IndexOf('=')
	if ($eq -lt 0) { return @{ name = $s.Trim() } }
	$name = $s.Substring(0, $eq).Trim(); $rest = $s.Substring($eq + 1)
	if ($rest -match ',') {
		$vals = @(); foreach ($p in ($rest -split ',')) { $vals += ,(ConvertTo-ChScalar $p) }
		return @{ name = $name; value = $vals }
	}
	return @{ name = $name; value = (ConvertTo-ChScalar $rest) }
}
function ConvertFrom-ChLinkShorthand {
	param([string]$s)
	$eq = $s.IndexOf('=')
	if ($eq -lt 0) { return @{ name = $s.Trim() } }
	$o = @{ name = $s.Substring(0, $eq).Trim() }; $rest = $s.Substring($eq + 1).Trim()
	if ($rest -match '^(.*):(?i:(Clear|DontChange|очистить|неизменять))$') { $o['dataPath'] = $matches[1].Trim(); $o['valueChange'] = $matches[2] }
	else { $o['dataPath'] = $rest }
	return $o
}

# <ChoiceParameters> — [{name, value?}]. Значение ПРЯМО на app:value (xsi:type=тип); массив → v8:FixedArray
# с детьми v8:Value; без value → app:value nil.
# Маркер пустой ссылки от декомпилятора: {emptyRef: true}. Работает и на Hashtable, и на PSCustomObject.
# Массивы отсекаем ЯВНО: у коллекции `$v.emptyRef` разворачивается в свойства элементов (member
# enumeration), и массив с одним таким элементом дал бы -eq $true → FixedArray схлопнулся бы в скаляр.
function Is-EmptyRef($v) {
	if ($null -eq $v -or $v -is [string]) { return $false }
	if (($v -is [System.Array]) -or ($v -is [System.Collections.IList])) { return $false }
	return ($v.emptyRef -eq $true)
}

function Emit-ChoiceParameters {
	param([string]$indent, $cp, [string]$tag = 'ChoiceParameters')
	if (-not $cp -or @($cp).Count -eq 0) { X "$indent<$tag/>"; return }
	X "$indent<$tag>"
	foreach ($item in @($cp)) {
		if ($item -is [string]) { $item = ConvertFrom-ChParamShorthand $item }
		$name = Get-ChElProp $item @('name','имя')
		$ptype = Get-ChElProp $item @('type','тип')
		$hasVal = $false; $val = $null
		if ($item -is [System.Collections.IDictionary]) {
			if ($item.Contains('value')) { $hasVal = $true; $val = $item['value'] }
			elseif ($item.Contains('значение')) { $hasVal = $true; $val = $item['значение'] }
		} elseif ($item.PSObject) {
			if ($item.PSObject.Properties['value']) { $hasVal = $true; $val = $item.PSObject.Properties['value'].Value }
			elseif ($item.PSObject.Properties['значение']) { $hasVal = $true; $val = $item.PSObject.Properties['значение'].Value }
		}
		$valIsArray = ($val -is [System.Array]) -or ($val -is [System.Collections.IList] -and $val -isnot [string])
		X "$indent`t<app:item name=`"$(Esc-Xml "$name")`">"
		if (-not $hasVal) {
			X "$indent`t`t<app:value xsi:nil=`"true`"/>"
		} elseif ((Is-EmptyRef $val)) {
			# Пустая ссылка (маркер декомпилятора emptyRef) — тип обязан остаться DesignTimeRef.
			X "$indent`t`t<app:value xsi:type=`"xr:DesignTimeRef`"/>"
		} elseif ($valIsArray) {
			X "$indent`t`t<app:value xsi:type=`"v8:FixedArray`">"
			foreach ($v in $val) {
				if ($null -eq $v) { X "$indent`t`t`t<v8:Value xsi:nil=`"true`"/>"; continue }
				if (Is-EmptyRef $v) { X "$indent`t`t`t<v8:Value xsi:type=`"xr:DesignTimeRef`"/>"; continue }
				$norm = Normalize-ChoiceValueT $v $ptype
				if ([string]::IsNullOrEmpty($norm.Text)) { X "$indent`t`t`t<v8:Value xsi:type=`"$($norm.XsiType)`"/>" }
				else { X "$indent`t`t`t<v8:Value xsi:type=`"$($norm.XsiType)`">$(Esc-XmlText $norm.Text)</v8:Value>" }
			}
			X "$indent`t`t</app:value>"
		} else {
			$norm = Normalize-ChoiceValueT $val $ptype
			if ([string]::IsNullOrEmpty($norm.Text)) { X "$indent`t`t<app:value xsi:type=`"$($norm.XsiType)`"/>" }
			else { X "$indent`t`t<app:value xsi:type=`"$($norm.XsiType)`">$(Esc-XmlText $norm.Text)</app:value>" }
		}
		X "$indent`t</app:item>"
	}
	X "$indent</$tag>"
}

# <ChoiceParameterLinks> — [{name, dataPath, valueChange?}]. valueChange дефолт Clear.
function Emit-ChoiceParameterLinks {
	param([string]$indent, $cpl, [string]$tag = 'ChoiceParameterLinks')
	if (-not $cpl -or @($cpl).Count -eq 0) { X "$indent<$tag/>"; return }
	X "$indent<$tag>"
	foreach ($lk in @($cpl)) {
		if ($lk -is [string]) { $lk = ConvertFrom-ChLinkShorthand $lk }
		$name = Get-ChElProp $lk @('name','имя')
		$dp = Expand-DataPath (Get-ChElProp $lk @('dataPath','path','путь'))
		$vcRaw = Get-ChElProp $lk @('valueChange','режимИзменения')
		$vc = 'Clear'
		if ($vcRaw) {
			$vc = switch -Regex ("$vcRaw".ToLower()) {
				'^(clear|очистить|очистка)$'             { 'Clear'; break }
				'^(dontchange|неизменять|неменять|нет)$' { 'DontChange'; break }
				default                                  { "$vcRaw" }
			}
		}
		X "$indent`t<xr:Link>"
		X "$indent`t`t<xr:Name>$(Esc-XmlText "$name")</xr:Name>"
		X "$indent`t`t<xr:DataPath xsi:type=`"xs:string`">$(Esc-XmlText "$dp")</xr:DataPath>"
		X "$indent`t`t<xr:ValueChange>$vc</xr:ValueChange>"
		X "$indent`t</xr:Link>"
	}
	X "$indent</$tag>"
}

# --- Characteristics (привязка ПВХ «Дополнительные реквизиты и сведения») ---

# from: рус. корень (Справочник→Catalog) + член (ТабличнаяЧасть→TabularSection); короткая 3-сегментная
# "<Тип>.X.Y" → вставить TabularSection (from — всегда таблица, не реквизит). Полный путь → как есть.
function Normalize-CharFrom {
	param([string]$from)
	if (-not $from) { return $from }
	$parts = @("$from" -split '\.')
	if ($script:objectTypeSynonyms.ContainsKey($parts[0])) { $parts[0] = $script:objectTypeSynonyms[$parts[0]] }
	for ($i = 1; $i -lt $parts.Count; $i++) {
		switch -Regex ($parts[$i]) {
			'^ТабличнаяЧасть$' { $parts[$i] = 'TabularSection' }
			'^Измерение$'      { $parts[$i] = 'Dimension' }
			'^Ресурс$'         { $parts[$i] = 'Resource' }
			'^Реквизит$'       { $parts[$i] = 'Attribute' }
		}
	}
	if ($parts.Count -eq 3 -and $parts[0] -in @('Catalog','Document','ChartOfCharacteristicTypes','ChartOfCalculationTypes','ChartOfAccounts','ExchangePlan','BusinessProcess','Task')) {
		$parts = @($parts[0], $parts[1], 'TabularSection', $parts[2])
	}
	return ($parts -join '.')
}

# Стандартный реквизит ссылочного типа в полях Characteristics: Ref/Parent/Owner (по имени EN/RU).
# Прочие стандартные реквизиты редки в полях — их задают частичной формой StandardAttribute.X.
function Resolve-CharStdEn {
	param([string]$name)
	$n = "$name".ToLower()
	if ($n -eq 'ref' -or $n -eq 'ссылка') { return 'Ref' }
	if ($n -eq 'parent' -or $n -eq 'родитель') { return 'Parent' }
	if ($n -eq 'owner' -or $n -eq 'владелец') { return 'Owner' }
	return $null
}

# Поле: голое→StandardAttribute.<EN>/Attribute.<имя>; частичное Member.X→<from>.Member.X; полный путь→verbatim.
function Expand-CharField {
	param([string]$field, [string]$from)
	$s = "$field"
	if (-not $s) { return $s }
	if ($s -eq '-1') { return '-1' }   # поле не задано (empty-характеристика) — как есть
	if ($s -match '^(StandardAttribute|Attribute|Dimension|Resource)\.') { return "$from.$s" }
	if (-not $s.Contains('.')) {
		$en = Resolve-CharStdEn $s
		if ($en) { return "$from.StandardAttribute.$en" }
		return "$from.Attribute.$s"
	}
	return $s
}

# Поле-флаг Characteristics (DataPathField/MultipleValues*) — дефолт -1. ПОЛИМОРФНО: обычно число,
# но DataPathField в некоторых конфигурациях содержит ПУТЬ к полю (8 случаев на корпус) — тогда
# пропускаем строку как есть, приведение к [int] на ней падало.
function Get-CharIntField { param($obj, [string[]]$names) $v = Get-ChElProp $obj $names; if ($null -eq $v -or "$v" -eq '') { return -1 }
	if ("$v" -match '^-?\d+$') { return [int]$v }
	return "$v" }

function Emit-Characteristics {
	param([string]$indent, $chars)
	if (-not $chars -or @($chars).Count -eq 0) { X "$indent<Characteristics/>"; return }
	X "$indent<Characteristics>"
	foreach ($ch in @($chars)) {
		$types  = Get-ChElProp $ch @('types','characteristicTypes','типы')
		$values = Get-ChElProp $ch @('values','characteristicValues','значения')
		$tFrom = Normalize-CharFrom "$(Get-ChElProp $types @('from','source','источник'))"
		$vFrom = Normalize-CharFrom "$(Get-ChElProp $values @('from','source','источник'))"
		$key = Expand-CharField "$(Get-ChElProp $types @('key','keyField'))" $tFrom
		$tff = Expand-CharField "$(Get-ChElProp $types @('filterField','typesFilterField'))" $tFrom
		$obj = Expand-CharField "$(Get-ChElProp $values @('object','objectField'))" $vFrom
		$typ = Expand-CharField "$(Get-ChElProp $values @('type','typeField'))" $vFrom
		$val = Expand-CharField "$(Get-ChElProp $values @('value','valueField'))" $vFrom
		# числовые поля-флаги (обычно -1; иногда 0)
		$dpf = Get-CharIntField $types @('dataPathField')
		$mvu = Get-CharIntField $types @('multipleValuesUseField')
		$mvk = Get-CharIntField $values @('multipleValuesKeyField')
		$mvo = Get-CharIntField $values @('multipleValuesOrderField')
		X "$indent`t<xr:Characteristic>"
		X "$indent`t`t<xr:CharacteristicTypes from=`"$(Esc-Xml $tFrom)`">"
		X "$indent`t`t`t<xr:KeyField>$(Esc-XmlText $key)</xr:KeyField>"
		X "$indent`t`t`t<xr:TypesFilterField>$(Esc-XmlText $tff)</xr:TypesFilterField>"
		# filterValue: $null→nil; голое→xs:string, полный путь→DTR, bool→xs:boolean.
		$tfvRaw = Get-ChElProp $types @('filterValue','typesFilterValue')
		if ($null -eq $tfvRaw) { X "$indent`t`t`t<xr:TypesFilterValue xsi:nil=`"true`"/>" }
		else {
			$tfvN = Normalize-ChoiceValue $tfvRaw
			if ([string]::IsNullOrEmpty($tfvN.Text)) { X "$indent`t`t`t<xr:TypesFilterValue xsi:type=`"$($tfvN.XsiType)`"/>" }
			else { X "$indent`t`t`t<xr:TypesFilterValue xsi:type=`"$($tfvN.XsiType)`">$(Esc-XmlText $tfvN.Text)</xr:TypesFilterValue>" }
		}
		# Числовое значение (обычно -1 или 0) — как есть; разворачивать через Expand-CharField нельзя,
		# оно примет "0" за короткое имя поля и выдаст "<from>.Attribute.0".
		$dpfOut = if ("$dpf" -match '^-?\d+$') { "$dpf" } else { Esc-XmlText (Expand-CharField "$dpf" $tFrom) }
		X "$indent`t`t`t<xr:DataPathField>$dpfOut</xr:DataPathField>"
		X "$indent`t`t`t<xr:MultipleValuesUseField>$mvu</xr:MultipleValuesUseField>"
		X "$indent`t`t</xr:CharacteristicTypes>"
		X "$indent`t`t<xr:CharacteristicValues from=`"$(Esc-Xml $vFrom)`">"
		X "$indent`t`t`t<xr:ObjectField>$(Esc-XmlText $obj)</xr:ObjectField>"
		X "$indent`t`t`t<xr:TypeField>$(Esc-XmlText $typ)</xr:TypeField>"
		X "$indent`t`t`t<xr:ValueField>$(Esc-XmlText $val)</xr:ValueField>"
		X "$indent`t`t`t<xr:MultipleValuesKeyField>$mvk</xr:MultipleValuesKeyField>"
		X "$indent`t`t`t<xr:MultipleValuesOrderField>$mvo</xr:MultipleValuesOrderField>"
		X "$indent`t`t</xr:CharacteristicValues>"
		X "$indent`t</xr:Characteristic>"
	}
	X "$indent</Characteristics>"
}

# <MinValue>/<MaxValue> — граница диапазона реквизита. Нет ключа → nil (не задано). Значение типизировано
# (зеркало form-compile): число → xs:decimal, строка → xs:string (тип сохранён декомпилятором).
function Emit-MinMaxValue {
	param([string]$indent, [string]$tag, $val)
	if ($null -eq $val) { X "$indent<$tag xsi:nil=`"true`"/>"; return }
	$t = if ($val -is [string]) { 'xs:string' } else { 'xs:decimal' }
	X "$indent<$tag xsi:type=`"$t`">$(Esc-XmlText "$val")</$tag>"
}

function Emit-Attribute {
	param([string]$indent, $parsed, [string]$context, [string]$elemTag = "Attribute")
	# $context: "catalog", "document", "object", "processor", "tabular", "processor-tabular", "register",
	#           "account" (реквизит Плана счетов: как catalog, но без <Use>), "account-flag" (признак учёта ПС:
	#           как account, но без <Indexing>/<FullTextSearch>, тип по умолчанию Boolean; $elemTag = AccountingFlag/ExtDimensionAccountingFlag)
	$attrName = $parsed.name
	$ctxReserved = $script:reservedByContext[$context]
	if ($ctxReserved) {
		foreach ($en in $ctxReserved) {
			$ru = $script:reservedAttrNames[$en]
			if (($attrName -ieq $en) -or ($ru -and $attrName -ieq $ru)) {
				Write-Error "Имя реквизита '$attrName' зарезервировано стандартным реквизитом ($en/$ru) объекта '$context'. Выберите другое имя."
				exit 1
			}
		}
	} elseif ($context -notin @("tabular", "processor-tabular") -and
		($script:reservedAttrNames.ContainsKey($attrName) -or $script:reservedAttrNames.ContainsValue($attrName))) {
		Write-Warning "Attribute '$attrName' conflicts with a standard attribute name. This may cause errors when loading into 1C."
	}
	$uuid = New-Guid-String
	X "$indent<$elemTag uuid=`"$uuid`">"
	X "$indent`t<Properties>"
	X "$indent`t`t<Name>$(Esc-XmlText $parsed.name)</Name>"
	Emit-MLText "$indent`t`t" "Synonym" $parsed.synonym
	if ($parsed.comment) { X "$indent`t`t<Comment>$(Esc-XmlText $parsed.comment)</Comment>" } else { X "$indent`t`t<Comment/>" }

	# Type
	$typeStr = $parsed.type
	if ($parsed.typeEmpty) {
		# Явный пустой тип (реквизит без типа / произвольный) → <Type/>.
		X "$indent`t`t<Type/>"
	} elseif ($typeStr) {
		Emit-ValueType "$indent`t`t" $typeStr
	} elseif ($context -eq "account-flag") {
		# Признак учёта — по умолчанию Boolean.
		X "$indent`t`t<Type>"
		X "$indent`t`t`t<v8:Type>xs:boolean</v8:Type>"
		X "$indent`t`t</Type>"
	} else {
		# Default: unqualified string
		X "$indent`t`t<Type>"
		X "$indent`t`t`t<v8:Type>xs:string</v8:Type>"
		X "$indent`t`t</Type>"
	}

	$pwMode = if ($parsed.passwordMode -eq $true) { "true" } else { "false" }
	X "$indent`t`t<PasswordMode>$pwMode</PasswordMode>"
	Emit-MLText "$indent`t`t" "Format" $parsed.format
	Emit-MLText "$indent`t`t" "EditFormat" $parsed.editFormat
	Emit-MLText "$indent`t`t" "ToolTip" $parsed.tooltip
	X "$indent`t`t<MarkNegatives>$(if ($parsed.markNegatives -eq $true) { 'true' } else { 'false' })</MarkNegatives>"
	if ($parsed.mask) { X "$indent`t`t<Mask>$(Esc-XmlText $parsed.mask)</Mask>" } else { X "$indent`t`t<Mask/>" }
	$multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" }
	X "$indent`t`t<MultiLine>$multiLine</MultiLine>"
	$extEdit = if ($parsed.extendedEdit -eq $true) { "true" } else { "false" }
	X "$indent`t`t<ExtendedEdit>$extEdit</ExtendedEdit>"
	Emit-MinMaxValue "$indent`t`t" "MinValue" $parsed.minValue
	Emit-MinMaxValue "$indent`t`t" "MaxValue" $parsed.maxValue

	# FillFromFillingValue — not for tabular/processor/chart/register-other/register-accum
	# (Chart*, AccumulationRegister/AccountingRegister/CalculationRegister don't support these)
	if ($context -notin @("tabular", "processor", "chart", "register-other", "register-accum", "register-calc", "register-account")) {
		# Флаг-shorthand `master` у ведущего измерения регистра конвенционально ставит и FillFromFillingValue=true
		# (эргономика авторинга; декомпилятор пишет key-форму master:true + явный fillFromFillingValue → роундтрип цел).
		$ffv = if ($parsed.fillFromFillingValue -eq $true -or ($elemTag -eq "Dimension" -and $parsed.flags -contains "master")) { "true" } else { "false" }
		X "$indent`t`t<FillFromFillingValue>$ffv</FillFromFillingValue>"
	}

	# FillValue — same restriction
	if ($context -notin @("tabular", "processor", "chart", "register-other", "register-accum", "register-calc", "register-account")) {
		Emit-FillValue "$indent`t`t" $typeStr $parsed.fillValue $parsed.hasFillValue ([bool]$parsed.typeEmpty)
	}

	# FillChecking
	$fillChecking = "DontCheck"
	if ($parsed.flags -contains "req") { $fillChecking = "ShowError" }
	if ($parsed.fillChecking) { $fillChecking = $parsed.fillChecking }
	X "$indent`t`t<FillChecking>$fillChecking</FillChecking>"

	X "$indent`t`t<ChoiceFoldersAndItems>$(if ($parsed.choiceFoldersAndItems) { "$($parsed.choiceFoldersAndItems)" } else { 'Items' })</ChoiceFoldersAndItems>"
	Emit-ChoiceParameterLinks "$indent`t`t" $parsed.choiceParameterLinks
	Emit-ChoiceParameters "$indent`t`t" $parsed.choiceParameters
	$qc = if ($parsed.quickChoice) { $parsed.quickChoice } else { "Auto" }
	X "$indent`t`t<QuickChoice>$qc</QuickChoice>"
	$coi = if ($parsed.createOnInput) { $parsed.createOnInput } else { "Auto" }
	X "$indent`t`t<CreateOnInput>$coi</CreateOnInput>"
	if ($parsed.choiceForm) { X "$indent`t`t<ChoiceForm>$(Esc-XmlText "$($parsed.choiceForm)")</ChoiceForm>" } else { X "$indent`t`t<ChoiceForm/>" }
	Emit-LinkByType "$indent`t`t" $parsed.linkByType
	$chi = if ($parsed.choiceHistoryOnInput) { $parsed.choiceHistoryOnInput } else { "Auto" }
	X "$indent`t`t<ChoiceHistoryOnInput>$chi</ChoiceHistoryOnInput>"

	# Измерение регистра сведений: Master/MainFilter/DenyIncompleteValues (между ChoiceHistoryOnInput и Indexing).
	if ($elemTag -eq "Dimension" -and $context -eq "register-info") {
		$master = if ($parsed.master -eq $true -or $parsed.flags -contains "master") { "true" } else { "false" }
		$mainFilter = if ($parsed.mainFilter -eq $true -or $parsed.flags -contains "mainfilter") { "true" } else { "false" }
		$denyIncomplete = if ($parsed.denyIncompleteValues -eq $true -or $parsed.flags -contains "denyincomplete") { "true" } else { "false" }
		X "$indent`t`t<Master>$master</Master>"
		X "$indent`t`t<MainFilter>$mainFilter</MainFilter>"
		X "$indent`t`t<DenyIncompleteValues>$denyIncomplete</DenyIncompleteValues>"
	}

	# Измерение регистра накопления: DenyIncompleteValues (между ChoiceHistoryOnInput и Indexing).
	if ($elemTag -eq "Dimension" -and $context -eq "register-accum") {
		$denyIncomplete = if ($parsed.denyIncompleteValues -eq $true -or $parsed.flags -contains "denyincomplete") { "true" } else { "false" }
		X "$indent`t`t<DenyIncompleteValues>$denyIncomplete</DenyIncompleteValues>"
	}

	# Измерение регистра расчёта: DenyIncompleteValues + BaseDimension (между ChoiceHistoryOnInput и ScheduleLink/Indexing).
	if ($elemTag -eq "Dimension" -and $context -eq "register-calc") {
		$denyIncomplete = if ($parsed.denyIncompleteValues -eq $true -or $parsed.flags -contains "denyincomplete") { "true" } else { "false" }
		$baseDimension = if ($parsed.baseDimension -eq $true -or $parsed.flags -contains "base") { "true" } else { "false" }
		X "$indent`t`t<DenyIncompleteValues>$denyIncomplete</DenyIncompleteValues>"
		X "$indent`t`t<BaseDimension>$baseDimension</BaseDimension>"
	}
	# Регистр расчёта: ScheduleLink у измерений и реквизитов (НЕ ресурсов), перед Indexing. Дефолт пустой.
	if ($context -eq "register-calc" -and $elemTag -in @("Dimension", "Attribute")) {
		if ($parsed.scheduleLink) { X "$indent`t`t<ScheduleLink>$(Esc-XmlText "$($parsed.scheduleLink)")</ScheduleLink>" }
		else { X "$indent`t`t<ScheduleLink/>" }
	}

	# Измерение/ресурс регистра бухгалтерии: Balance + AccountingFlag (ссылка на признак учёта ПС), затем
	# DenyIncompleteValues (измерение) / ExtDimensionAccountingFlag (ресурс). Всё между ChoiceHistoryOnInput и Indexing.
	if ($context -eq "register-account" -and $elemTag -in @("Dimension", "Resource")) {
		$balance = if ($parsed.balance -eq $true -or $parsed.flags -contains "balance") { "true" } else { "false" }
		X "$indent`t`t<Balance>$balance</Balance>"
		if ($parsed.accountingFlag) { X "$indent`t`t<AccountingFlag>$(Esc-XmlText "$($parsed.accountingFlag)")</AccountingFlag>" } else { X "$indent`t`t<AccountingFlag/>" }
		if ($elemTag -eq "Dimension") {
			$denyIncomplete = if ($parsed.denyIncompleteValues -eq $true -or $parsed.flags -contains "denyincomplete") { "true" } else { "false" }
			X "$indent`t`t<DenyIncompleteValues>$denyIncomplete</DenyIncompleteValues>"
		} else {
			if ($parsed.extDimensionAccountingFlag) { X "$indent`t`t<ExtDimensionAccountingFlag>$(Esc-XmlText "$($parsed.extDimensionAccountingFlag)")</ExtDimensionAccountingFlag>" } else { X "$indent`t`t<ExtDimensionAccountingFlag/>" }
		}
	}

	# Use — у реквизитов справочника и ПВХ. Позиция РАЗНАЯ: справочник пишет Use ПЕРЕД
	# Indexing, ПВХ — ПОСЛЕ него (корпус acc+erp: Catalog `Use,Indexing,FullTextSearch`,
	# ПВХ `Indexing,Use,FullTextSearch,DataHistory`). Отсюда отдельный контекст "cct":
	# структурно реквизит ПВХ совпадает со справочником, расходится только этим порядком.
	$use = if ($parsed.use) { $parsed.use } else { "ForItem" }
	if ($context -eq "catalog") {
		X "$indent`t`t<Use>$use</Use>"
	}

	# Indexing/FullTextSearch/DataHistory — not for non-stored objects (processor, processor-tabular)
	if ($context -notin @("processor", "processor-tabular")) {
		# Признаки учёта ПС (account-flag) не имеют <Indexing>/<FullTextSearch>, но имеют <DataHistory>.
		if ($context -ne "account-flag") {
			# Ресурс регистра накопления/расчёта/бухгалтерии НЕ имеет <Indexing> (только <FullTextSearch>); измерение/реквизит — имеют.
			if (-not ($context -in @("register-accum", "register-calc", "register-account") -and $elemTag -eq "Resource")) {
				$indexing = "DontIndex"
				if ($parsed.flags -contains "index") { $indexing = "Index" }
				if ($parsed.flags -contains "indexadditional") { $indexing = "IndexWithAdditionalOrder" }
				if ($parsed.indexing) { $indexing = $parsed.indexing }
				X "$indent`t`t<Indexing>$indexing</Indexing>"
			}
			if ($context -eq "cct") { X "$indent`t`t<Use>$use</Use>" }

			# Реквизит адресации задачи: AddressingDimension (ссылка на измерение регистра исполнителей), между Indexing и FullTextSearch.
			if ($context -eq "task-addressing" -and $elemTag -eq "AddressingAttribute") {
				if ($parsed.addressingDimension) { X "$indent`t`t<AddressingDimension>$(Esc-XmlText "$($parsed.addressingDimension)")</AddressingDimension>" } else { X "$indent`t`t<AddressingDimension/>" }
			}
			$fts = if ($parsed.fullTextSearch) { $parsed.fullTextSearch } else { "Use" }
			X "$indent`t`t<FullTextSearch>$fts</FullTextSearch>"
		}
		# Измерение регистра накопления: UseInTotals (после FullTextSearch, дефолт true).
		if ($elemTag -eq "Dimension" -and $context -eq "register-accum") {
			$useInTotals = if ($parsed.useInTotals -eq $false -or $parsed.flags -contains "nouseintotals") { "false" } else { "true" }
			X "$indent`t`t<UseInTotals>$useInTotals</UseInTotals>"
		}
		# DataHistory — not for Chart* types and non-InformationRegister register family
		if ($context -notin @("chart", "register-other", "register-accum", "register-calc", "register-account")) {
			$dh = if ($parsed.dataHistory) { $parsed.dataHistory } else { "Use" }
			X "$indent`t`t<DataHistory>$dh</DataHistory>"
		}
	}
	# Формат 2.18 (8.3.25): режим приведения типов — последним в Properties и ТОЛЬКО у измерений
	# регистра сведений (у реквизитов/ресурсов и у прочих семейств регистров платформа его не пишет).
	if ($script:isFormat218 -and $elemTag -eq "Dimension" -and $context -eq "register-info") {
		$trm = if ($parsed.typeReductionMode) { "$($parsed.typeReductionMode)" } else { "TransformValues" }
		X "$indent`t`t<TypeReductionMode>$trm</TypeReductionMode>"
	}

	X "$indent`t</Properties>"
	X "$indent</$elemTag>"
}

# <Picture> команды — структурный блок (зеркало form-compile). Дефолт LoadTransparent=true (конвенция
# кнопки/команды): фиксируем только false. Значение: строка-ref + sibling `loadTransparent` ЛИБО объект
# {src, loadTransparent?, transparentPixel?}. src с префиксом "abs:" → <xr:Abs>, иначе <xr:Ref>. Нет → <Picture/>.
function Emit-CommandPicture {
	param([string]$indent, $cmd)
	$pic = $cmd.picture
	if (-not $pic) { X "$indent<Picture/>"; return }
	$src = $null; $lt = $true; $tpx = $null
	if ($pic -is [string]) { $src = "$pic"; if ($cmd.loadTransparent -eq $false) { $lt = $false } }
	else {
		$src = if ($pic.src) { "$($pic.src)" } elseif ($pic.ref) { "$($pic.ref)" } else { "" }
		if ($pic.loadTransparent -eq $false) { $lt = $false }
		$tpx = $pic.transparentPixel
	}
	if (-not $src) { X "$indent<Picture/>"; return }
	X "$indent<Picture>"
	if ($src -match '^abs:(.*)$') { X "$indent`t<xr:Abs>$(Esc-XmlText $matches[1])</xr:Abs>" }
	else { X "$indent`t<xr:Ref>$(Esc-XmlText $src)</xr:Ref>" }
	X "$indent`t<xr:LoadTransparent>$(if ($lt) { 'true' } else { 'false' })</xr:LoadTransparent>"
	if ($tpx) { X "$indent`t<xr:TransparentPixel x=`"$($tpx.x)`" y=`"$($tpx.y)`"/>" }
	X "$indent</Picture>"
}

# --- 8b. Command emitter ---
# $cmd — объект свойств команды. Поля (omit-on-default): synonym/tooltip (ML), comment, group,
# commandParameterType (тип), parameterUseMode (Single), modifiesData (false), representation (Auto),
# picture, shortcut, onMainServerUnavalableBehavior (Auto).
function Emit-Command {
	param([string]$indent, [string]$cmdName, $cmd)
	X "$indent<Command uuid=`"$(New-Guid-String)`">"
	X "$indent`t<Properties>"
	X "$indent`t`t<Name>$(Esc-XmlText $cmdName)</Name>"
	$syn = if ($null -ne $cmd.synonym) { $cmd.synonym } else { Split-CamelCase $cmdName }
	Emit-MLText "$indent`t`t" "Synonym" $syn
	if ($cmd.comment) { X "$indent`t`t<Comment>$(Esc-XmlText "$($cmd.comment)")</Comment>" } else { X "$indent`t`t<Comment/>" }
	$group = Resolve-CommandGroup $cmd.group $cmdName
	if ($cmd.commandParameterType -and ($script:sectionCommandGroups -contains $group)) {
		Write-Error "Команда '$cmdName': тип параметра (commandParameterType) недоступен для команд командного интерфейса раздела ('$group'). Тип параметра — только для групп формы (FormCommandBar*/FormNavigationPanel*) или CommandGroup.<Имя>."
		exit 1
	}
	X "$indent`t`t<Group>$(Esc-XmlText $group)</Group>"
	if ($cmd.commandParameterType) {
		X "$indent`t`t<CommandParameterType>"
		Emit-TypeContent "$indent`t`t`t" "$($cmd.commandParameterType)"
		X "$indent`t`t</CommandParameterType>"
	} else {
		X "$indent`t`t<CommandParameterType/>"
	}
	$pum = if ($cmd.parameterUseMode) { "$($cmd.parameterUseMode)" } else { "Single" }
	X "$indent`t`t<ParameterUseMode>$pum</ParameterUseMode>"
	$md = if ($cmd.modifiesData -eq $true) { "true" } else { "false" }
	X "$indent`t`t<ModifiesData>$md</ModifiesData>"
	$rep = if ($cmd.representation) { "$($cmd.representation)" } else { "Auto" }
	X "$indent`t`t<Representation>$rep</Representation>"
	Emit-MLText "$indent`t`t" "ToolTip" $cmd.tooltip
	Emit-CommandPicture "$indent`t`t" $cmd
	if ($cmd.shortcut) { X "$indent`t`t<Shortcut>$(Esc-XmlText "$($cmd.shortcut)")</Shortcut>" } else { X "$indent`t`t<Shortcut/>" }
	$osu = if ($cmd.onMainServerUnavalableBehavior) { "$($cmd.onMainServerUnavalableBehavior)" } else { "Auto" }
	X "$indent`t`t<OnMainServerUnavalableBehavior>$osu</OnMainServerUnavalableBehavior>"
	X "$indent`t</Properties>"
	X "$indent</Command>"
}

# --- 9. TabularSection emitter ---

function Emit-TabularSection {
	param([string]$indent, [string]$tsName, $columns, [string]$objectType, [string]$objectName, $tsSynonymArg = $null, $tsTooltip = $null, $tsComment = $null, $tsLineNumber = $null, $tsFillChecking = $null, $tsUse = $null, $tsLineNumberLength = $null)
	$uuid = New-Guid-String
	X "$indent<TabularSection uuid=`"$uuid`">"

	# InternalInfo for TabularSection
	$typePrefix = "${objectType}TabularSection"
	$rowPrefix = "${objectType}TabularSectionRow"

	X "$indent`t<InternalInfo>"
	X "$indent`t`t<xr:GeneratedType name=`"$typePrefix.$objectName.$tsName`" category=`"TabularSection`">"
	X "$indent`t`t`t<xr:TypeId>$(New-Guid-String)</xr:TypeId>"
	X "$indent`t`t`t<xr:ValueId>$(New-Guid-String)</xr:ValueId>"
	X "$indent`t`t</xr:GeneratedType>"
	X "$indent`t`t<xr:GeneratedType name=`"$rowPrefix.$objectName.$tsName`" category=`"TabularSectionRow`">"
	X "$indent`t`t`t<xr:TypeId>$(New-Guid-String)</xr:TypeId>"
	X "$indent`t`t`t<xr:ValueId>$(New-Guid-String)</xr:ValueId>"
	X "$indent`t`t</xr:GeneratedType>"
	X "$indent`t</InternalInfo>"

	$tsSynonym = if ($null -ne $tsSynonymArg) { $tsSynonymArg } else { Split-CamelCase $tsName }

	X "$indent`t<Properties>"
	X "$indent`t`t<Name>$(Esc-XmlText $tsName)</Name>"
	Emit-MLText "$indent`t`t" "Synonym" $tsSynonym
	if ($tsComment) { X "$indent`t`t<Comment>$(Esc-XmlText $tsComment)</Comment>" } else { X "$indent`t`t<Comment/>" }
	Emit-MLText "$indent`t`t" "ToolTip" $tsTooltip
	$tsFc = if ($tsFillChecking) { "$tsFillChecking" } else { "DontCheck" }
	X "$indent`t`t<FillChecking>$tsFc</FillChecking>"
	# TS-блок стандартных реквизитов (LineNumber) эмитим ВСЕГДА, кроме подавления `lineNumber: ""` (дом-конвенция
	# суппресса): ~6% ТЧ исторически опускают блок (правило не выводимо — Товары all-default его имеет, соседи нет).
	if (-not ($tsLineNumber -is [string] -and $tsLineNumber -eq '')) {
		Emit-TabularStandardAttributes "$indent`t`t" $tsLineNumber
	}
	# Use у ТЧ иерархических ссылочных типов (Catalog, ChartOfCharacteristicTypes); Document не имеет Use.
	# Дефолт ForItem; ForFolderAndItem/ForFolder — при явном ключе `use` объектной формы ТЧ.
	if ($objectType -in @("Catalog", "ChartOfCharacteristicTypes")) {
		$use = if ($tsUse) { "$tsUse" } else { "ForItem" }
		X "$indent`t`t<Use>$use</Use>"
	}
	# Формат 2.20 (8.3.27): длина номера строки ТЧ (5..9 → до 999 999 999 строк вместо 99 999).
	# Последним в Properties. Дефолт платформа берёт из режима совместимости на момент создания ТЧ.
	# ТОЛЬКО у объектов с хранением в БД: обработкам и отчётам платформа тег не пишет вовсе
	# (проверено на двух конфигурациях: 0 из 235+257 ТЧ обработок и 0 из 106+8 ТЧ отчётов).
	# Свойство задаёт разрядность физического номера строки в таблице — у обработок таблицы нет.
	if ($script:isFormat220 -and $objectType -notin @("DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport")) {
		$lnl = if ($null -ne $tsLineNumberLength -and "$tsLineNumberLength" -ne '') { [int]$tsLineNumberLength } else { $script:lineNumberLengthDefault }
		X "$indent`t`t<LineNumberLength>$lnl</LineNumberLength>"
	}
	X "$indent`t</Properties>"

	$tsContext = if ($objectType -in @("DataProcessor","Report")) { "processor-tabular" } else { "tabular" }
	X "$indent`t<ChildObjects>"
	foreach ($col in $columns) {
		$parsed = Parse-AttributeShorthand $col
		Emit-Attribute "$indent`t`t" $parsed $tsContext
	}
	X "$indent`t</ChildObjects>"

	X "$indent</TabularSection>"
}

# --- 10. EnumValue emitter ---

function Emit-EnumValue {
	param([string]$indent, $parsed)
	$uuid = New-Guid-String
	X "$indent<EnumValue uuid=`"$uuid`">"
	X "$indent`t<Properties>"
	X "$indent`t`t<Name>$(Esc-XmlText $parsed.name)</Name>"
	Emit-MLText "$indent`t`t" "Synonym" $parsed.synonym
	if ($parsed.comment) { X "$indent`t`t<Comment>$(Esc-XmlText $parsed.comment)</Comment>" } else { X "$indent`t`t<Comment/>" }
	# Цвет значения перечисления — свойство формата 2.21 (8.5), последним в Properties.
	if ($script:isFormat221) {
		$color = if ($parsed.color) { "$($parsed.color)" } else { "auto" }
		X "$indent`t`t<Color>$(Esc-XmlText $color)</Color>"
	}
	X "$indent`t</Properties>"
	X "$indent</EnumValue>"
}

# --- 11. Dimension emitter ---

function Emit-Dimension {
	param([string]$indent, $parsed, [string]$registerType)
	# $registerType: "InformationRegister" or "AccumulationRegister"
	$uuid = New-Guid-String
	X "$indent<Dimension uuid=`"$uuid`">"
	X "$indent`t<Properties>"
	X "$indent`t`t<Name>$(Esc-XmlText $parsed.name)</Name>"
	Emit-MLText "$indent`t`t" "Synonym" $parsed.synonym
	X "$indent`t`t<Comment/>"

	$typeStr = $parsed.type
	if ($typeStr) {
		Emit-ValueType "$indent`t`t" $typeStr
	} else {
		X "$indent`t`t<Type>"
		X "$indent`t`t`t<v8:Type>xs:string</v8:Type>"
		X "$indent`t`t</Type>"
	}

	X "$indent`t`t<PasswordMode>false</PasswordMode>"
	X "$indent`t`t<Format/>"
	X "$indent`t`t<EditFormat/>"
	X "$indent`t`t<ToolTip/>"
	X "$indent`t`t<MarkNegatives>$(if ($parsed.markNegatives -eq $true) { 'true' } else { 'false' })</MarkNegatives>"
	X "$indent`t`t<Mask/>"
	$multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" }
	X "$indent`t`t<MultiLine>$multiLine</MultiLine>"
	$extEdit = if ($parsed.extendedEdit -eq $true) { "true" } else { "false" }
	X "$indent`t`t<ExtendedEdit>$extEdit</ExtendedEdit>"
	Emit-MinMaxValue "$indent`t`t" "MinValue" $parsed.minValue
	Emit-MinMaxValue "$indent`t`t" "MaxValue" $parsed.maxValue

	# InformationRegister dimensions have FillFromFillingValue
	if ($registerType -eq "InformationRegister") {
		$fillFrom = if ($parsed.flags -contains "master") { "true" } else { "false" }
		X "$indent`t`t<FillFromFillingValue>$fillFrom</FillFromFillingValue>"
		X "$indent`t`t<FillValue xsi:nil=`"true`"/>"
	}

	$fillChecking = "DontCheck"
	if ($parsed.flags -contains "req") { $fillChecking = "ShowError" }
	X "$indent`t`t<FillChecking>$fillChecking</FillChecking>"

	X "$indent`t`t<ChoiceFoldersAndItems>$(if ($parsed.choiceFoldersAndItems) { "$($parsed.choiceFoldersAndItems)" } else { 'Items' })</ChoiceFoldersAndItems>"
	X "$indent`t`t<ChoiceParameterLinks/>"
	X "$indent`t`t<ChoiceParameters/>"
	X "$indent`t`t<QuickChoice>Auto</QuickChoice>"
	X "$indent`t`t<CreateOnInput>Auto</CreateOnInput>"
	if ($parsed.choiceForm) { X "$indent`t`t<ChoiceForm>$(Esc-XmlText "$($parsed.choiceForm)")</ChoiceForm>" } else { X "$indent`t`t<ChoiceForm/>" }
	X "$indent`t`t<LinkByType/>"
	X "$indent`t`t<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>"

	# InformationRegister dimensions: Master, MainFilter, DenyIncompleteValues
	if ($registerType -eq "InformationRegister") {
		$master = if ($parsed.flags -contains "master") { "true" } else { "false" }
		$mainFilter = if ($parsed.flags -contains "mainfilter") { "true" } else { "false" }
		$denyIncomplete = if ($parsed.flags -contains "denyincomplete") { "true" } else { "false" }
		X "$indent`t`t<Master>$master</Master>"
		X "$indent`t`t<MainFilter>$mainFilter</MainFilter>"
		X "$indent`t`t<DenyIncompleteValues>$denyIncomplete</DenyIncompleteValues>"
	}

	# AccumulationRegister dimensions: DenyIncompleteValues
	if ($registerType -eq "AccumulationRegister") {
		$denyIncomplete = if ($parsed.flags -contains "denyincomplete") { "true" } else { "false" }
		X "$indent`t`t<DenyIncompleteValues>$denyIncomplete</DenyIncompleteValues>"
	}

	$indexing = "DontIndex"
	if ($parsed.flags -contains "index") { $indexing = "Index" }
	X "$indent`t`t<Indexing>$indexing</Indexing>"

	X "$indent`t`t<FullTextSearch>Use</FullTextSearch>"

	# AccumulationRegister dimensions: UseInTotals
	if ($registerType -eq "AccumulationRegister") {
		$useInTotals = if ($parsed.flags -contains "nouseintotals") { "false" } else { "true" }
		X "$indent`t`t<UseInTotals>$useInTotals</UseInTotals>"
	}

	# InformationRegister dimensions: DataHistory
	if ($registerType -eq "InformationRegister") {
		X "$indent`t`t<DataHistory>Use</DataHistory>"
	}

	X "$indent`t</Properties>"
	X "$indent</Dimension>"
}

# --- 12. Resource emitter ---

function Emit-Resource {
	param([string]$indent, $parsed, [string]$registerType)
	$uuid = New-Guid-String
	X "$indent<Resource uuid=`"$uuid`">"
	X "$indent`t<Properties>"
	X "$indent`t`t<Name>$(Esc-XmlText $parsed.name)</Name>"
	Emit-MLText "$indent`t`t" "Synonym" $parsed.synonym
	X "$indent`t`t<Comment/>"

	$typeStr = $parsed.type
	if ($typeStr) {
		Emit-ValueType "$indent`t`t" $typeStr
	} else {
		X "$indent`t`t<Type>"
		X "$indent`t`t`t<v8:Type>xs:decimal</v8:Type>"
		X "$indent`t`t`t<v8:NumberQualifiers>"
		X "$indent`t`t`t`t<v8:Digits>15</v8:Digits>"
		X "$indent`t`t`t`t<v8:FractionDigits>2</v8:FractionDigits>"
		X "$indent`t`t`t`t<v8:AllowedSign>Any</v8:AllowedSign>"
		X "$indent`t`t`t</v8:NumberQualifiers>"
		X "$indent`t`t</Type>"
	}

	X "$indent`t`t<PasswordMode>false</PasswordMode>"
	X "$indent`t`t<Format/>"
	X "$indent`t`t<EditFormat/>"
	X "$indent`t`t<ToolTip/>"
	X "$indent`t`t<MarkNegatives>$(if ($parsed.markNegatives -eq $true) { 'true' } else { 'false' })</MarkNegatives>"
	X "$indent`t`t<Mask/>"
	$multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" }
	X "$indent`t`t<MultiLine>$multiLine</MultiLine>"
	$extEdit = if ($parsed.extendedEdit -eq $true) { "true" } else { "false" }
	X "$indent`t`t<ExtendedEdit>$extEdit</ExtendedEdit>"
	Emit-MinMaxValue "$indent`t`t" "MinValue" $parsed.minValue
	Emit-MinMaxValue "$indent`t`t" "MaxValue" $parsed.maxValue

	# InformationRegister resources have FillFromFillingValue, FillValue
	if ($registerType -eq "InformationRegister") {
		X "$indent`t`t<FillFromFillingValue>false</FillFromFillingValue>"
		X "$indent`t`t<FillValue xsi:nil=`"true`"/>"
	}

	$fillChecking = "DontCheck"
	if ($parsed.flags -contains "req") { $fillChecking = "ShowError" }
	X "$indent`t`t<FillChecking>$fillChecking</FillChecking>"

	X "$indent`t`t<ChoiceFoldersAndItems>$(if ($parsed.choiceFoldersAndItems) { "$($parsed.choiceFoldersAndItems)" } else { 'Items' })</ChoiceFoldersAndItems>"
	X "$indent`t`t<ChoiceParameterLinks/>"
	X "$indent`t`t<ChoiceParameters/>"
	X "$indent`t`t<QuickChoice>Auto</QuickChoice>"
	X "$indent`t`t<CreateOnInput>Auto</CreateOnInput>"
	if ($parsed.choiceForm) { X "$indent`t`t<ChoiceForm>$(Esc-XmlText "$($parsed.choiceForm)")</ChoiceForm>" } else { X "$indent`t`t<ChoiceForm/>" }
	X "$indent`t`t<LinkByType/>"
	X "$indent`t`t<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>"

	# InformationRegister resources: Indexing, FullTextSearch, DataHistory
	if ($registerType -eq "InformationRegister") {
		X "$indent`t`t<Indexing>DontIndex</Indexing>"
		X "$indent`t`t<FullTextSearch>Use</FullTextSearch>"
		X "$indent`t`t<DataHistory>Use</DataHistory>"
	}

	# AccumulationRegister resources: FullTextSearch (no Indexing, no DataHistory)
	if ($registerType -eq "AccumulationRegister") {
		X "$indent`t`t<FullTextSearch>Use</FullTextSearch>"
	}

	X "$indent`t</Properties>"
	X "$indent</Resource>"
}

# --- 13. Property emitters per type ---

function Emit-CatalogProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }

	$hierarchical = if ($def.hierarchical -eq $true) { "true" } else { "false" }
	$hierarchyType = Get-EnumProp "HierarchyType" "hierarchyType" "HierarchyFoldersAndItems"
	X "$i<Hierarchical>$hierarchical</Hierarchical>"
	X "$i<HierarchyType>$hierarchyType</HierarchyType>"
	$limitLevelCount = if ($def.limitLevelCount -eq $true) { "true" } else { "false" }
	$levelCount = if ($null -ne $def.levelCount) { "$($def.levelCount)" } else { "2" }
	$foldersOnTop = if ($def.foldersOnTop -eq $false) { "false" } else { "true" }
	X "$i<LimitLevelCount>$limitLevelCount</LimitLevelCount>"
	X "$i<LevelCount>$levelCount</LevelCount>"
	X "$i<FoldersOnTop>$foldersOnTop</FoldersOnTop>"
	$useStdCmds = if (Get-BoolProp "useStandardCommands" $true) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmds</UseStandardCommands>"
	if ($def.owners -and $def.owners.Count -gt 0) {
		X "$i<Owners>"
		foreach ($ownerRef in $def.owners) {
			X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-XmlText (Normalize-MDObjectRef "$ownerRef" 'Catalog'))</xr:Item>"
		}
		X "$i</Owners>"
	} else {
		X "$i<Owners/>"
	}
	$subordinationUse = Get-EnumProp "SubordinationUse" "subordinationUse" "ToItems"
	X "$i<SubordinationUse>$subordinationUse</SubordinationUse>"

	$codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" }
	$descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "25" }
	$codeType = Get-EnumProp "CodeType" "codeType" "String"
	$codeAllowedLength = Get-EnumProp "CodeAllowedLength" "codeAllowedLength" "Variable"
	$autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" }
	$checkUnique = if ($def.checkUnique -eq $true) { "true" } else { "false" }

	X "$i<CodeLength>$codeLength</CodeLength>"
	X "$i<DescriptionLength>$descriptionLength</DescriptionLength>"
	X "$i<CodeType>$codeType</CodeType>"
	X "$i<CodeAllowedLength>$codeAllowedLength</CodeAllowedLength>"
	$codeSeries = Get-EnumProp "CodeSeries" "codeSeries" "WholeCatalog"
	X "$i<CodeSeries>$codeSeries</CodeSeries>"
	X "$i<CheckUnique>$checkUnique</CheckUnique>"
	X "$i<Autonumbering>$autonumbering</Autonumbering>"

	$defaultPresentation = Get-EnumProp "DefaultPresentation" "defaultPresentation" "AsDescription"
	X "$i<DefaultPresentation>$defaultPresentation</DefaultPresentation>"

	Emit-StandardAttributes $i "Catalog"
	Emit-Characteristics $i $def.characteristics
	X "$i<PredefinedDataUpdate>$(Get-EnumProp 'PredefinedDataUpdate' 'predefinedDataUpdate' 'Auto')</PredefinedDataUpdate>"
	X "$i<EditType>$(Get-EnumProp 'EditType' 'editType' 'InDialog')</EditType>"
	$quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" }
	$choiceMode = Get-EnumProp "ChoiceMode" "choiceMode" "BothWays"
	X "$i<QuickChoice>$quickChoice</QuickChoice>"
	X "$i<ChoiceMode>$choiceMode</ChoiceMode>"
	# InputByString: override `inputByString` (массив имён, авто-резолв; [] = пусто) ЛИБО дефолт [Descr при D>0]+[Code при C>0].
	if (Test-DefKey 'inputByString') {
		$ibFields = @($def.inputByString | ForEach-Object { Expand-DataPath "$_" })
	} else {
		$ibFields = @()
		if ([int]$descriptionLength -gt 0) { $ibFields += "Catalog.$objName.StandardAttribute.Description" }
		if ([int]$codeLength -gt 0)        { $ibFields += "Catalog.$objName.StandardAttribute.Code" }
	}
	Emit-FieldBlock $i "InputByString" $ibFields
	X "$i<SearchStringModeOnInputByString>$(Get-EnumProp 'SearchStringModeOnInputByString' 'searchStringModeOnInputByString' 'Begin')</SearchStringModeOnInputByString>"
	X "$i<FullTextSearchOnInputByString>$(Get-EnumProp 'FullTextSearchOnInputByString' 'fullTextSearchOnInputByString' 'DontUse')</FullTextSearchOnInputByString>"
	X "$i<ChoiceDataGetModeOnInputByString>Directly</ChoiceDataGetModeOnInputByString>"
	Emit-FormRef $i "DefaultObjectForm"       $def.defaultObjectForm
	Emit-FormRef $i "DefaultFolderForm"       $def.defaultFolderForm
	Emit-FormRef $i "DefaultListForm"         $def.defaultListForm
	Emit-FormRef $i "DefaultChoiceForm"       $def.defaultChoiceForm
	Emit-FormRef $i "DefaultFolderChoiceForm" $def.defaultFolderChoiceForm
	Emit-FormRef $i "AuxiliaryObjectForm"       $def.auxiliaryObjectForm
	Emit-FormRef $i "AuxiliaryFolderForm"       $def.auxiliaryFolderForm
	Emit-FormRef $i "AuxiliaryListForm"         $def.auxiliaryListForm
	Emit-FormRef $i "AuxiliaryChoiceForm"       $def.auxiliaryChoiceForm
	Emit-FormRef $i "AuxiliaryFolderChoiceForm" $def.auxiliaryFolderChoiceForm
	$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
	X "$i<IncludeHelpInContents>$inclHelp</IncludeHelpInContents>"
	Emit-BasedOn $i $def.basedOn
	$dlFields = if (Test-DefKey 'dataLockFields') { @($def.dataLockFields | ForEach-Object { Expand-DataPath "$_" }) } else { @() }
	Emit-FieldBlock $i "DataLockFields" $dlFields

	$dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Managed"
	X "$i<DataLockControlMode>$dataLockControlMode</DataLockControlMode>"

	$fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use"
	X "$i<FullTextSearch>$fullTextSearch</FullTextSearch>"

	Emit-MLText $i "ObjectPresentation" $def.objectPresentation
	Emit-MLText $i "ExtendedObjectPresentation" $def.extendedObjectPresentation
	Emit-MLText $i "ListPresentation" $def.listPresentation
	Emit-MLText $i "ExtendedListPresentation" $def.extendedListPresentation
	Emit-MLText $i "Explanation" $def.explanation
	X "$i<CreateOnInput>$(Get-EnumProp 'CreateOnInput' 'createOnInput' 'Use')</CreateOnInput>"
	X "$i<ChoiceHistoryOnInput>$(Get-EnumProp 'ChoiceHistoryOnInput' 'choiceHistoryOnInput' 'Auto')</ChoiceHistoryOnInput>"
	X "$i<DataHistory>DontUse</DataHistory>"
	X "$i<UpdateDataHistoryImmediatelyAfterWrite>false</UpdateDataHistoryImmediatelyAfterWrite>"
	X "$i<ExecuteAfterWriteDataHistoryVersionProcessing>false</ExecuteAfterWriteDataHistoryVersionProcessing>"
}

function Emit-DocumentProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText "$($def.comment)")</Comment>" } else { X "$i<Comment/>" }
	$useStdCmd = if (Get-BoolProp "useStandardCommands" $true) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmd</UseStandardCommands>"
	if ($def.numerator) { X "$i<Numerator>$(Esc-XmlText "$($def.numerator)")</Numerator>" } else { X "$i<Numerator/>" }

	$numberType = Get-EnumProp "NumberType" "numberType" "String"
	$numberLength = if ($null -ne $def.numberLength) { "$($def.numberLength)" } else { "11" }
	$numberAllowedLength = Get-EnumProp "NumberAllowedLength" "numberAllowedLength" "Variable"
	$numberPeriodicity = Get-EnumProp "NumberPeriodicity" "numberPeriodicity" "Year"
	$checkUnique = if ($def.checkUnique -eq $false) { "false" } else { "true" }
	$autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" }

	X "$i<NumberType>$numberType</NumberType>"
	X "$i<NumberLength>$numberLength</NumberLength>"
	X "$i<NumberAllowedLength>$numberAllowedLength</NumberAllowedLength>"
	X "$i<NumberPeriodicity>$numberPeriodicity</NumberPeriodicity>"
	X "$i<CheckUnique>$checkUnique</CheckUnique>"
	X "$i<Autonumbering>$autonumbering</Autonumbering>"

	Emit-StandardAttributes $i "Document"
	Emit-Characteristics $i $def.characteristics
	Emit-BasedOn $i $def.basedOn

	# InputByString: override `inputByString` ЛИБО дефолт [Номер].
	if (Test-DefKey 'inputByString') {
		$ibFields = @($def.inputByString | ForEach-Object { Expand-DataPath "$_" })
	} else {
		$ibFields = @("Document.$objName.StandardAttribute.Number")
	}
	Emit-FieldBlock $i "InputByString" $ibFields
	X "$i<CreateOnInput>$(Get-EnumProp 'CreateOnInput' 'createOnInput' 'Use')</CreateOnInput>"
	X "$i<SearchStringModeOnInputByString>$(Get-EnumProp 'SearchStringModeOnInputByString' 'searchStringModeOnInputByString' 'Begin')</SearchStringModeOnInputByString>"
	X "$i<FullTextSearchOnInputByString>$(Get-EnumProp 'FullTextSearchOnInputByString' 'fullTextSearchOnInputByString' 'DontUse')</FullTextSearchOnInputByString>"
	X "$i<ChoiceDataGetModeOnInputByString>Directly</ChoiceDataGetModeOnInputByString>"
	Emit-FormRef $i "DefaultObjectForm"   $def.defaultObjectForm
	Emit-FormRef $i "DefaultListForm"     $def.defaultListForm
	Emit-FormRef $i "DefaultChoiceForm"   $def.defaultChoiceForm
	Emit-FormRef $i "AuxiliaryObjectForm" $def.auxiliaryObjectForm
	Emit-FormRef $i "AuxiliaryListForm"   $def.auxiliaryListForm
	Emit-FormRef $i "AuxiliaryChoiceForm" $def.auxiliaryChoiceForm

	X "$i<Posting>$(Get-EnumProp 'Posting' 'posting' 'Allow')</Posting>"
	X "$i<RealTimePosting>$(Get-EnumProp 'RealTimePosting' 'realTimePosting' 'Deny')</RealTimePosting>"
	X "$i<RegisterRecordsDeletion>$(Get-EnumProp 'RegisterRecordsDeletion' 'registerRecordsDeletion' 'AutoDelete')</RegisterRecordsDeletion>"
	X "$i<RegisterRecordsWritingOnPost>$(Get-EnumProp 'RegisterRecordsWritingOnPost' 'registerRecordsWritingOnPost' 'WriteSelected')</RegisterRecordsWritingOnPost>"
	X "$i<SequenceFilling>$(Get-EnumProp 'SequenceFilling' 'sequenceFilling' 'AutoFill')</SequenceFilling>"

	# RegisterRecords — движения (список MDObjectRef, синонимы типов резолвятся).
	$regRecords = @()
	if ($def.registerRecords) {
		foreach ($rr in $def.registerRecords) {
			$rrStr = "$rr"
			if ($rrStr.Contains('.')) {
				$dotIdx = $rrStr.IndexOf('.')
				$rrPrefix = $rrStr.Substring(0, $dotIdx)
				$rrSuffix = $rrStr.Substring($dotIdx + 1)
				if ($script:objectTypeSynonyms.ContainsKey($rrPrefix)) { $rrPrefix = $script:objectTypeSynonyms[$rrPrefix] }
				$regRecords += "$rrPrefix.$rrSuffix"
			} else { $regRecords += $rrStr }
		}
	}
	if ($regRecords.Count -gt 0) {
		X "$i<RegisterRecords>"
		foreach ($rr in $regRecords) { X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-XmlText (Normalize-MDObjectRef "$rr"))</xr:Item>" }
		X "$i</RegisterRecords>"
	} else {
		X "$i<RegisterRecords/>"
	}

	$postInPrivilegedMode = if ($def.postInPrivilegedMode -eq $false) { "false" } else { "true" }
	$unpostInPrivilegedMode = if ($def.unpostInPrivilegedMode -eq $false) { "false" } else { "true" }
	X "$i<PostInPrivilegedMode>$postInPrivilegedMode</PostInPrivilegedMode>"
	X "$i<UnpostInPrivilegedMode>$unpostInPrivilegedMode</UnpostInPrivilegedMode>"
	$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
	X "$i<IncludeHelpInContents>$inclHelp</IncludeHelpInContents>"
	$dlFields = if (Test-DefKey 'dataLockFields') { @($def.dataLockFields | ForEach-Object { Expand-DataPath "$_" }) } else { @() }
	Emit-FieldBlock $i "DataLockFields" $dlFields
	X "$i<DataLockControlMode>$(Get-EnumProp 'DataLockControlMode' 'dataLockControlMode' 'Managed')</DataLockControlMode>"
	X "$i<FullTextSearch>$(Get-EnumProp 'FullTextSearch' 'fullTextSearch' 'Use')</FullTextSearch>"

	Emit-MLText $i "ObjectPresentation" $def.objectPresentation
	Emit-MLText $i "ExtendedObjectPresentation" $def.extendedObjectPresentation
	Emit-MLText $i "ListPresentation" $def.listPresentation
	Emit-MLText $i "ExtendedListPresentation" $def.extendedListPresentation
	Emit-MLText $i "Explanation" $def.explanation
	X "$i<ChoiceHistoryOnInput>$(Get-EnumProp 'ChoiceHistoryOnInput' 'choiceHistoryOnInput' 'Auto')</ChoiceHistoryOnInput>"
	X "$i<DataHistory>$(Get-EnumProp 'DataHistory' 'dataHistory' 'DontUse')</DataHistory>"
	$updDH = if (Get-BoolProp "updateDataHistoryImmediatelyAfterWrite" $false) { "true" } else { "false" }
	X "$i<UpdateDataHistoryImmediatelyAfterWrite>$updDH</UpdateDataHistoryImmediatelyAfterWrite>"
	$execDH = if (Get-BoolProp "executeAfterWriteDataHistoryVersionProcessing" $false) { "true" } else { "false" }
	X "$i<ExecuteAfterWriteDataHistoryVersionProcessing>$execDH</ExecuteAfterWriteDataHistoryVersionProcessing>"
}

function Emit-EnumProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
	$useStdCmds = if (Get-BoolProp "useStandardCommands" $false) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmds</UseStandardCommands>"

	Emit-StandardAttributes $i "Enum"
	Emit-Characteristics $i $def.characteristics

	$quickChoice = if ($def.quickChoice -eq $false) { "false" } else { "true" }
	X "$i<QuickChoice>$quickChoice</QuickChoice>"
	X "$i<ChoiceMode>$(Get-EnumProp 'ChoiceMode' 'choiceMode' 'BothWays')</ChoiceMode>"
	Emit-FormRef $i "DefaultListForm"     $def.defaultListForm
	Emit-FormRef $i "DefaultChoiceForm"   $def.defaultChoiceForm
	Emit-FormRef $i "AuxiliaryListForm"   $def.auxiliaryListForm
	Emit-FormRef $i "AuxiliaryChoiceForm" $def.auxiliaryChoiceForm
	Emit-MLText $i "ListPresentation" $def.listPresentation
	Emit-MLText $i "ExtendedListPresentation" $def.extendedListPresentation
	Emit-MLText $i "Explanation" $def.explanation
	X "$i<ChoiceHistoryOnInput>$(Get-EnumProp 'ChoiceHistoryOnInput' 'choiceHistoryOnInput' 'Auto')</ChoiceHistoryOnInput>"
}

function Emit-ConstantProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }

	# Type — valueType (пустой явный '' → <Type/>, реквизит без типа; отсутствие → String дефолт).
	$valueType = Build-TypeStr $def
	$typeEmpty = ($null -ne $def.valueType -and "$($def.valueType)".Trim() -eq '') -or ($null -ne $def.type -and "$($def.type)".Trim() -eq '')
	if ($typeEmpty) { X "$i<Type/>" }
	else { if (-not $valueType) { $valueType = "String" }; Emit-ValueType $i $valueType }

	$useStdCmds = if (Get-BoolProp "useStandardCommands" $true) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmds</UseStandardCommands>"
	Emit-VerbatimRef $i "DefaultForm" $def.defaultForm
	Emit-MLText $i "ExtendedPresentation" $def.extendedPresentation
	Emit-MLText $i "Explanation" $def.explanation
	X "$i<PasswordMode>$(if (Get-BoolProp 'passwordMode' $false) { 'true' } else { 'false' })</PasswordMode>"
	Emit-MLText $i "Format" $def.format
	Emit-MLText $i "EditFormat" $def.editFormat
	Emit-MLText $i "ToolTip" $def.tooltip
	X "$i<MarkNegatives>$(if (Get-BoolProp 'markNegatives' $false) { 'true' } else { 'false' })</MarkNegatives>"
	if ($def.mask) { X "$i<Mask>$(Esc-XmlText $def.mask)</Mask>" } else { X "$i<Mask/>" }
	X "$i<MultiLine>$(if (Get-BoolProp 'multiLine' $false) { 'true' } else { 'false' })</MultiLine>"
	X "$i<ExtendedEdit>$(if (Get-BoolProp 'extendedEdit' $false) { 'true' } else { 'false' })</ExtendedEdit>"
	Emit-MinMaxValue $i "MinValue" $def.minValue
	Emit-MinMaxValue $i "MaxValue" $def.maxValue
	X "$i<FillChecking>$(Get-EnumProp 'FillChecking' 'fillChecking' 'DontCheck')</FillChecking>"
	X "$i<ChoiceFoldersAndItems>$(Get-EnumProp 'ChoiceFoldersAndItems' 'choiceFoldersAndItems' 'Items')</ChoiceFoldersAndItems>"
	Emit-ChoiceParameterLinks $i $def.choiceParameterLinks
	Emit-ChoiceParameters $i $def.choiceParameters
	X "$i<QuickChoice>$(Get-EnumProp 'QuickChoice' 'quickChoice' 'Auto')</QuickChoice>"
	if ($def.choiceForm) { X "$i<ChoiceForm>$(Esc-XmlText "$($def.choiceForm)")</ChoiceForm>" } else { X "$i<ChoiceForm/>" }
	Emit-LinkByType $i $def.linkByType
	X "$i<ChoiceHistoryOnInput>$(Get-EnumProp 'ChoiceHistoryOnInput' 'choiceHistoryOnInput' 'Auto')</ChoiceHistoryOnInput>"

	X "$i<DataLockControlMode>$(Get-EnumProp 'DataLockControlMode' 'dataLockControlMode' 'Managed')</DataLockControlMode>"
	X "$i<DataHistory>$(Get-EnumProp 'DataHistory' 'dataHistory' 'DontUse')</DataHistory>"
	X "$i<UpdateDataHistoryImmediatelyAfterWrite>$(if (Get-BoolProp 'updateDataHistoryImmediatelyAfterWrite' $false) { 'true' } else { 'false' })</UpdateDataHistoryImmediatelyAfterWrite>"
	X "$i<ExecuteAfterWriteDataHistoryVersionProcessing>$(if (Get-BoolProp 'executeAfterWriteDataHistoryVersionProcessing' $false) { 'true' } else { 'false' })</ExecuteAfterWriteDataHistoryVersionProcessing>"
}

function Emit-InformationRegisterProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText "$($def.comment)")</Comment>" } else { X "$i<Comment/>" }
	$useStdCmd = if (Get-BoolProp "useStandardCommands" $true) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmd</UseStandardCommands>"
	X "$i<EditType>$(Get-EnumProp 'EditType' 'editType' 'InDialog')</EditType>"
	Emit-FormRef $i "DefaultRecordForm"   $def.defaultRecordForm
	Emit-FormRef $i "DefaultListForm"     $def.defaultListForm
	Emit-FormRef $i "AuxiliaryRecordForm" $def.auxiliaryRecordForm
	Emit-FormRef $i "AuxiliaryListForm"   $def.auxiliaryListForm

	Emit-StandardAttributes $i "InformationRegister"

	$periodicity = Get-EnumProp "InformationRegisterPeriodicity" "periodicity" "Nonperiodical"
	$writeMode = Get-EnumProp "WriteMode" "writeMode" "Independent"

	# MainFilterOnPeriod: захватывается независимо (авто-вывод из periodicity неверен — см. корпус).
	$mainFilterOnPeriod = if (Get-BoolProp "mainFilterOnPeriod" $false) { "true" } else { "false" }

	X "$i<InformationRegisterPeriodicity>$periodicity</InformationRegisterPeriodicity>"
	X "$i<WriteMode>$writeMode</WriteMode>"
	X "$i<MainFilterOnPeriod>$mainFilterOnPeriod</MainFilterOnPeriod>"
	$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
	X "$i<IncludeHelpInContents>$inclHelp</IncludeHelpInContents>"

	$dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Managed"
	X "$i<DataLockControlMode>$dataLockControlMode</DataLockControlMode>"

	$fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use"
	X "$i<FullTextSearch>$fullTextSearch</FullTextSearch>"

	$enTotFirst = if (Get-BoolProp "enableTotalsSliceFirst" $false) { "true" } else { "false" }
	$enTotLast  = if (Get-BoolProp "enableTotalsSliceLast" $false) { "true" } else { "false" }
	X "$i<EnableTotalsSliceFirst>$enTotFirst</EnableTotalsSliceFirst>"
	X "$i<EnableTotalsSliceLast>$enTotLast</EnableTotalsSliceLast>"
	Emit-MLText $i "RecordPresentation" $def.recordPresentation
	Emit-MLText $i "ExtendedRecordPresentation" $def.extendedRecordPresentation
	Emit-MLText $i "ListPresentation" $def.listPresentation
	Emit-MLText $i "ExtendedListPresentation" $def.extendedListPresentation
	Emit-MLText $i "Explanation" $def.explanation
	X "$i<DataHistory>$(Get-EnumProp 'DataHistory' 'dataHistory' 'DontUse')</DataHistory>"
	$updDH = if (Get-BoolProp "updateDataHistoryImmediatelyAfterWrite" $false) { "true" } else { "false" }
	X "$i<UpdateDataHistoryImmediatelyAfterWrite>$updDH</UpdateDataHistoryImmediatelyAfterWrite>"
	$execDH = if (Get-BoolProp "executeAfterWriteDataHistoryVersionProcessing" $false) { "true" } else { "false" }
	X "$i<ExecuteAfterWriteDataHistoryVersionProcessing>$execDH</ExecuteAfterWriteDataHistoryVersionProcessing>"
}

function Emit-AccumulationRegisterProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText "$($def.comment)")</Comment>" } else { X "$i<Comment/>" }
	$useStdCmd = if (Get-BoolProp "useStandardCommands" $true) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmd</UseStandardCommands>"
	Emit-FormRef $i "DefaultListForm"   $def.defaultListForm
	Emit-FormRef $i "AuxiliaryListForm" $def.auxiliaryListForm

	$registerType = Get-EnumProp "RegisterType" "registerType" "Balance"
	X "$i<RegisterType>$registerType</RegisterType>"

	$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
	X "$i<IncludeHelpInContents>$inclHelp</IncludeHelpInContents>"

	Emit-StandardAttributes $i "AccumulationRegister"

	$dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Managed"
	X "$i<DataLockControlMode>$dataLockControlMode</DataLockControlMode>"

	$fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use"
	X "$i<FullTextSearch>$fullTextSearch</FullTextSearch>"

	$enableTotalsSplitting = if ($def.enableTotalsSplitting -eq $false) { "false" } else { "true" }
	X "$i<EnableTotalsSplitting>$enableTotalsSplitting</EnableTotalsSplitting>"

	Emit-MLText $i "ListPresentation" $def.listPresentation
	Emit-MLText $i "ExtendedListPresentation" $def.extendedListPresentation
	Emit-MLText $i "Explanation" $def.explanation
}

# --- 13a. Wave 1: DefinedType, CommonModule, ScheduledJob, EventSubscription ---

function Emit-DefinedTypeProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }

	# Type — тип-значение (составной через ' + '); принимаем valueType (строка) или valueTypes (массив).
	# Единый эмиттер Emit-ValueType/Emit-TypeContent (refs d5p1, cfg:, платформенные, квалификаторы). Пусто → <Type/>.
	$vt = if ($def.valueType) { "$($def.valueType)" }
	      elseif ($def.valueTypes) { (@($def.valueTypes) | ForEach-Object { "$_" }) -join ' + ' }
	      else { '' }
	if ($vt) { Emit-ValueType $i $vt } else { X "$i<Type/>" }
}

function Emit-FunctionalOptionProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
	# Location — хранилище значения опции (Constant.X / InformationRegister.X.Resource.Y / <Тип>.X.Attribute.Y).
	# Ссылка verbatim (MDObjectRef-путь; принимаем location или value).
	$loc = if ($def.location) { "$($def.location)" } elseif ($def.value) { "$($def.value)" } else { "" }
	if ($loc) { X "$i<Location>$(Esc-XmlText (Normalize-MDObjectRef $loc))</Location>" } else { X "$i<Location/>" }
	# PrivilegedGetMode — привилегированный режим чтения (корпус 2864/2864 = true → дефолт true).
	X "$i<PrivilegedGetMode>$(if (Get-BoolProp 'privilegedGetMode' $true) { 'true' } else { 'false' })</PrivilegedGetMode>"
	# Content — объекты, зависящие от опции (список MDObjectRef-путей к реквизитам/измерениям/ресурсам). omit-on-empty.
	$content = @()
	if ($def.content) { $content = @($def.content) }
	if ($content.Count -gt 0) {
		X "$i<Content>"
		foreach ($obj in $content) { X "$i`t<xr:Object>$(Esc-XmlText (Normalize-MDObjectRef "$obj"))</xr:Object>" }
		X "$i</Content>"
	} else {
		X "$i<Content/>"
	}
}

# Общий эмиттер списка MDObjectRef (Documents/RegisterRecords с обёрткой <xr:Item>). omit-on-empty.
function Emit-MDRefList {
	param([string]$indent, [string]$tag, $items)
	$arr = @(); if ($items) { $arr = @($items) }
	if ($arr.Count -gt 0) {
		X "$indent<$tag>"
		foreach ($it in $arr) { X "$indent`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-XmlText (Normalize-MDObjectRef "$it"))</xr:Item>" }
		X "$indent</$tag>"
	} else {
		X "$indent<$tag/>"
	}
}

function Emit-SequenceProperties {
	param([string]$indent)
	$i = $indent
	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
	X "$i<MoveBoundaryOnPosting>$(Get-EnumProp 'MoveBoundaryOnPosting' 'moveBoundaryOnPosting' 'DontMove')</MoveBoundaryOnPosting>"
	Emit-MDRefList $i "Documents" $def.documents
	Emit-MDRefList $i "RegisterRecords" $def.registerRecords
	X "$i<DataLockControlMode>$(Get-EnumProp 'DataLockControlMode' 'dataLockControlMode' 'Managed')</DataLockControlMode>"
}

function Emit-FilterCriterionProperties {
	param([string]$indent)
	$i = $indent
	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
	$vt = if ($def.valueType) { "$($def.valueType)" } elseif ($def.valueTypes) { (@($def.valueTypes) | ForEach-Object { "$_" }) -join ' + ' } else { '' }
	if ($vt) { Emit-ValueType $i $vt } else { X "$i<Type/>" }
	$useStdCmds = if (Get-BoolProp "useStandardCommands" $true) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmds</UseStandardCommands>"
	# Content — объекты (реквизиты), по которым идёт отбор.
	$content = @(); if ($def.content) { $content = @($def.content) }
	if ($content.Count -gt 0) {
		X "$i<Content>"
		foreach ($obj in $content) { X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-XmlText (Normalize-MDObjectRef "$obj"))</xr:Item>" }
		X "$i</Content>"
	} else {
		X "$i<Content/>"
	}
	Emit-VerbatimRef $i "DefaultForm"   $def.defaultForm
	Emit-VerbatimRef $i "AuxiliaryForm" $def.auxiliaryForm
	Emit-MLText $i "ListPresentation" $def.listPresentation
	Emit-MLText $i "ExtendedListPresentation" $def.extendedListPresentation
	Emit-MLText $i "Explanation" $def.explanation
}

function Emit-DocumentNumeratorProperties {
	param([string]$indent)
	$i = $indent
	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
	X "$i<NumberType>$(Get-EnumProp 'NumberType' 'numberType' 'String')</NumberType>"
	X "$i<NumberLength>$(if ($null -ne $def.numberLength) { "$($def.numberLength)" } else { '11' })</NumberLength>"
	X "$i<NumberAllowedLength>$(Get-EnumProp 'NumberAllowedLength' 'numberAllowedLength' 'Variable')</NumberAllowedLength>"
	X "$i<NumberPeriodicity>$(Get-EnumProp 'NumberPeriodicity' 'numberPeriodicity' 'Year')</NumberPeriodicity>"
	X "$i<CheckUnique>$(if (Get-BoolProp 'checkUnique' $true) { 'true' } else { 'false' })</CheckUnique>"
}

function Emit-SettingsStorageProperties {
	param([string]$indent)
	$i = $indent
	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
	Emit-VerbatimRef $i "DefaultSaveForm"   $def.defaultSaveForm
	Emit-VerbatimRef $i "DefaultLoadForm"   $def.defaultLoadForm
	Emit-VerbatimRef $i "AuxiliarySaveForm" $def.auxiliarySaveForm
	Emit-VerbatimRef $i "AuxiliaryLoadForm" $def.auxiliaryLoadForm
}

function Emit-CommonFormProperties {
	param([string]$indent)
	$i = $indent
	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
	X "$i<FormType>$(Get-EnumProp 'FormType' 'formType' 'Managed')</FormType>"
	$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
	X "$i<IncludeHelpInContents>$inclHelp</IncludeHelpInContents>"
	# UsePurposes — назначения (использование в приложениях). Дефолт [PlatformApplication, MobilePlatformApplication].
	$purposes = if ($def.usePurposes) { @($def.usePurposes) } else { @('PlatformApplication', 'MobilePlatformApplication') }
	if ($purposes.Count -gt 0) {
		X "$i<UsePurposes>"
		foreach ($p in $purposes) { X "$i`t<v8:Value xsi:type=`"app:ApplicationUsePurpose`">$p</v8:Value>" }
		X "$i</UsePurposes>"
	} else {
		X "$i<UsePurposes/>"
	}
	# Использование в режиме совместимости интерфейса — свойство формата 2.21 (8.5),
	# между UsePurposes и UseStandardCommands.
	if ($script:isFormat221) {
		X "$i<UseInInterfaceCompatibilityMode>$(Get-EnumProp 'UseInInterfaceCompatibilityMode' 'useInInterfaceCompatibilityMode' 'Any')</UseInInterfaceCompatibilityMode>"
	}
	$useStdCmds = if (Get-BoolProp "useStandardCommands" $false) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmds</UseStandardCommands>"
	Emit-MLText $i "ExtendedPresentation" $def.extendedPresentation
	Emit-MLText $i "Explanation" $def.explanation
}

function Emit-SessionParameterProperties {
	param([string]$indent)
	$i = $indent
	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
	$vt = if ($def.valueType) { "$($def.valueType)" } elseif ($def.valueTypes) { (@($def.valueTypes) | ForEach-Object { "$_" }) -join ' + ' } else { '' }
	if ($vt) { Emit-ValueType $i $vt } else { X "$i<Type/>" }
}

function Emit-FunctionalOptionsParameterProperties {
	param([string]$indent)
	$i = $indent
	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
	# Use — измерения регистров/реквизиты, к которым привязан параметр (список MDObjectRef).
	Emit-MDRefList $i "Use" $def.use
}

function Emit-WSReferenceProperties {
	param([string]$indent)
	$i = $indent
	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
	$url = if ($def.locationURL) { "$($def.locationURL)" } elseif ($def.locationUrl) { "$($def.locationUrl)" } else { "" }
	if ($url) { X "$i<LocationURL>$(Esc-XmlText $url)</LocationURL>" } else { X "$i<LocationURL/>" }
}

function Emit-CommonPictureProperties {
	param([string]$indent)
	$i = $indent
	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
	X "$i<AvailabilityForChoice>$(if (Get-BoolProp 'availabilityForChoice' $false) { 'true' } else { 'false' })</AvailabilityForChoice>"
	X "$i<AvailabilityForAppearance>$(if (Get-BoolProp 'availabilityForAppearance' $false) { 'true' } else { 'false' })</AvailabilityForAppearance>"
}

function Emit-CommonTemplateProperties {
	param([string]$indent)
	$i = $indent
	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
	X "$i<TemplateType>$(Get-EnumProp 'TemplateType' 'templateType' 'SpreadsheetDocument')</TemplateType>"
}

function Emit-CommandGroupProperties {
	param([string]$indent)
	$i = $indent
	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
	X "$i<Representation>$(Get-EnumProp 'Representation' 'representation' 'Auto')</Representation>"
	Emit-MLText $i "ToolTip" $def.tooltip
	Emit-CommandPicture $i $def
	X "$i<Category>$(Get-EnumProp 'Category' 'category' 'NavigationPanel')</Category>"
}

function Emit-CommonCommandProperties {
	param([string]$indent)
	$i = $indent
	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
	$group = if ($def.group) { "$($def.group)" } else { "" }
	if ($group) { X "$i<Group>$(Esc-XmlText $group)</Group>" } else { X "$i<Group/>" }
	X "$i<Representation>$(Get-EnumProp 'Representation' 'representation' 'Auto')</Representation>"
	Emit-MLText $i "ToolTip" $def.tooltip
	Emit-CommandPicture $i $def
	if ($def.shortcut) { X "$i<Shortcut>$(Esc-XmlText "$($def.shortcut)")</Shortcut>" } else { X "$i<Shortcut/>" }
	$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
	X "$i<IncludeHelpInContents>$inclHelp</IncludeHelpInContents>"
	if ($def.commandParameterType) {
		X "$i<CommandParameterType>"
		Emit-TypeContent "$i`t" "$($def.commandParameterType)"
		X "$i</CommandParameterType>"
	} else {
		X "$i<CommandParameterType/>"
	}
	X "$i<ParameterUseMode>$(Get-EnumProp 'ParameterUseMode' 'parameterUseMode' 'Single')</ParameterUseMode>"
	X "$i<ModifiesData>$(if (Get-BoolProp 'modifiesData' $false) { 'true' } else { 'false' })</ModifiesData>"
	X "$i<OnMainServerUnavalableBehavior>$(Get-EnumProp 'OnMainServerUnavalableBehavior' 'onMainServerUnavalableBehavior' 'Auto')</OnMainServerUnavalableBehavior>"
}

function Emit-CommonAttributeProperties {
	param([string]$indent)
	$i = $indent
	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
	# Дефолт типа — String(0) (переменная длина 0), НЕ $def.type (это тип метаобъекта «CommonAttribute»).
	$vt = if ($def.valueType) { "$($def.valueType)" } else { 'String(0)' }
	Emit-ValueType $i $vt
	X "$i<PasswordMode>$(if (Get-BoolProp 'passwordMode' $false) { 'true' } else { 'false' })</PasswordMode>"
	Emit-MLText $i "Format" $def.format
	Emit-MLText $i "EditFormat" $def.editFormat
	Emit-MLText $i "ToolTip" $def.tooltip
	X "$i<MarkNegatives>$(if (Get-BoolProp 'markNegatives' $false) { 'true' } else { 'false' })</MarkNegatives>"
	if ($def.mask) { X "$i<Mask>$(Esc-XmlText $def.mask)</Mask>" } else { X "$i<Mask/>" }
	X "$i<MultiLine>$(if (Get-BoolProp 'multiLine' $false) { 'true' } else { 'false' })</MultiLine>"
	X "$i<ExtendedEdit>$(if (Get-BoolProp 'extendedEdit' $false) { 'true' } else { 'false' })</ExtendedEdit>"
	Emit-MinMaxValue $i "MinValue" $def.minValue
	Emit-MinMaxValue $i "MaxValue" $def.maxValue
	$ffv = if (Get-BoolProp 'fillFromFillingValue' $false) { 'true' } else { 'false' }
	X "$i<FillFromFillingValue>$ffv</FillFromFillingValue>"
	Emit-FillValue $i $vt $def.fillValue ($null -ne $def.fillValue)
	X "$i<FillChecking>$(Get-EnumProp 'FillChecking' 'fillChecking' 'DontCheck')</FillChecking>"
	X "$i<ChoiceFoldersAndItems>$(Get-EnumProp 'ChoiceFoldersAndItems' 'choiceFoldersAndItems' 'Items')</ChoiceFoldersAndItems>"
	Emit-ChoiceParameterLinks $i $def.choiceParameterLinks
	Emit-ChoiceParameters $i $def.choiceParameters
	X "$i<QuickChoice>$(Get-EnumProp 'QuickChoice' 'quickChoice' 'Auto')</QuickChoice>"
	X "$i<CreateOnInput>$(Get-EnumProp 'CreateOnInput' 'createOnInput' 'Auto')</CreateOnInput>"
	if ($def.choiceForm) { X "$i<ChoiceForm>$(Esc-XmlText "$($def.choiceForm)")</ChoiceForm>" } else { X "$i<ChoiceForm/>" }
	Emit-LinkByType $i $def.linkByType
	X "$i<ChoiceHistoryOnInput>$(Get-EnumProp 'ChoiceHistoryOnInput' 'choiceHistoryOnInput' 'Auto')</ChoiceHistoryOnInput>"
	# Content — объекты, к которым добавлен общий реквизит: {metadata, use?, conditionalSeparation?}.
	$content = @(); if ($def.content) { $content = @($def.content) }
	if ($content.Count -gt 0) {
		X "$i<Content>"
		foreach ($c in $content) {
			$md = if ($c -is [string]) { "$c" } else { "$($c.metadata)" }
			$use = if ($c -is [string]) { 'Use' } elseif ($c.use) { "$($c.use)" } else { 'Use' }
			X "$i`t<xr:Item>"
			X "$i`t`t<xr:Metadata>$(Esc-XmlText (Normalize-MDObjectRef $md))</xr:Metadata>"
			X "$i`t`t<xr:Use>$use</xr:Use>"
			$cs = if ($c -isnot [string] -and $c.conditionalSeparation) { "$($c.conditionalSeparation)" } else { "" }
			if ($cs) { X "$i`t`t<xr:ConditionalSeparation>$(Esc-XmlText $cs)</xr:ConditionalSeparation>" } else { X "$i`t`t<xr:ConditionalSeparation/>" }
			X "$i`t</xr:Item>"
		}
		X "$i</Content>"
	} else {
		X "$i<Content/>"
	}
	X "$i<AutoUse>$(Get-EnumProp 'AutoUse' 'autoUse' 'DontUse')</AutoUse>"
	X "$i<DataSeparation>$(Get-EnumProp 'DataSeparation' 'dataSeparation' 'DontUse')</DataSeparation>"
	X "$i<SeparatedDataUse>$(Get-EnumProp 'SeparatedDataUse' 'separatedDataUse' 'Independently')</SeparatedDataUse>"
	$dsv = if ($def.dataSeparationValue) { "$($def.dataSeparationValue)" } else { "" }
	if ($dsv) { X "$i<DataSeparationValue>$(Esc-XmlText $dsv)</DataSeparationValue>" } else { X "$i<DataSeparationValue/>" }
	$dsu = if ($def.dataSeparationUse) { "$($def.dataSeparationUse)" } else { "" }
	if ($dsu) { X "$i<DataSeparationUse>$(Esc-XmlText $dsu)</DataSeparationUse>" } else { X "$i<DataSeparationUse/>" }
	$cs2 = if ($def.conditionalSeparation) { "$($def.conditionalSeparation)" } else { "" }
	if ($cs2) { X "$i<ConditionalSeparation>$(Esc-XmlText $cs2)</ConditionalSeparation>" } else { X "$i<ConditionalSeparation/>" }
	X "$i<UsersSeparation>$(Get-EnumProp 'UsersSeparation' 'usersSeparation' 'DontUse')</UsersSeparation>"
	X "$i<AuthenticationSeparation>$(Get-EnumProp 'AuthenticationSeparation' 'authenticationSeparation' 'DontUse')</AuthenticationSeparation>"
	X "$i<ConfigurationExtensionsSeparation>$(Get-EnumProp 'ConfigurationExtensionsSeparation' 'configurationExtensionsSeparation' 'DontUse')</ConfigurationExtensionsSeparation>"
	X "$i<Indexing>$(Get-EnumProp 'Indexing' 'indexing' 'DontIndex')</Indexing>"
	X "$i<FullTextSearch>$(Get-EnumProp 'FullTextSearch' 'fullTextSearch' 'Use')</FullTextSearch>"
	X "$i<DataHistory>$(Get-EnumProp 'DataHistory' 'dataHistory' 'Use')</DataHistory>"
}

# Измерение последовательности: Name/Synonym/Comment/Type + DocumentMap/RegisterRecordsMap (списки MDObjectRef —
# соответствие измерения реквизитам документов/движениям регистров).
function Emit-SequenceDimension {
	param([string]$indent, $dimDef)
	$uuid = New-Guid-String
	$parsed = Parse-AttributeShorthand $dimDef
	X "$indent<Dimension uuid=`"$uuid`">"
	X "$indent`t<Properties>"
	X "$indent`t`t<Name>$(Esc-XmlText $parsed.name)</Name>"
	Emit-MLText "$indent`t`t" "Synonym" $parsed.synonym
	if ($parsed.comment) { X "$indent`t`t<Comment>$(Esc-XmlText $parsed.comment)</Comment>" } else { X "$indent`t`t<Comment/>" }
	if ($parsed.typeEmpty) { X "$indent`t`t<Type/>" }
	elseif ($parsed.type) { Emit-ValueType "$indent`t`t" $parsed.type }
	else { X "$indent`t`t<Type/>" }
	$dm = if ($dimDef -is [string]) { $null } else { $dimDef.documentMap }
	$rrm = if ($dimDef -is [string]) { $null } else { $dimDef.registerRecordsMap }
	Emit-MDRefList "$indent`t`t" "DocumentMap" $dm
	Emit-MDRefList "$indent`t`t" "RegisterRecordsMap" $rrm
	X "$indent`t</Properties>"
	X "$indent</Dimension>"
}

function Emit-CommonModuleProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }

	# Context shortcuts
	$context = if ($def.context) { "$($def.context)" } else { "" }

	$global = if ($def.global -eq $true) { "true" } else { "false" }
	$server = "false"; $serverCall = "false"; $clientManaged = "false"
	$clientOrdinary = "false"; $externalConnection = "false"; $privileged = "false"

	switch ($context) {
		"server"       { $server = "true"; $serverCall = "true" }
		"serverCall"   { $server = "true"; $serverCall = "true" }
		"client"       { $clientManaged = "true" }
		"serverClient" { $server = "true"; $clientManaged = "true" }
		default {
			if ($def.server -eq $true) { $server = "true" }
			if ($def.serverCall -eq $true) { $serverCall = "true" }
			if ($def.clientManagedApplication -eq $true) { $clientManaged = "true" }
			if ($def.clientOrdinaryApplication -eq $true) { $clientOrdinary = "true" }
			if ($def.externalConnection -eq $true) { $externalConnection = "true" }
			if ($def.privileged -eq $true) { $privileged = "true" }
		}
	}

	X "$i<Global>$global</Global>"
	X "$i<ClientManagedApplication>$clientManaged</ClientManagedApplication>"
	X "$i<Server>$server</Server>"
	X "$i<ExternalConnection>$externalConnection</ExternalConnection>"
	X "$i<ClientOrdinaryApplication>$clientOrdinary</ClientOrdinaryApplication>"
	X "$i<ServerCall>$serverCall</ServerCall>"
	X "$i<Privileged>$privileged</Privileged>"

	$returnValuesReuse = Get-EnumProp "ReturnValuesReuse" "returnValuesReuse" "DontUse"
	X "$i<ReturnValuesReuse>$returnValuesReuse</ReturnValuesReuse>"
}

function Emit-ScheduledJobProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }

	$methodName = if ($def.methodName) { "$($def.methodName)" } else { "" }
	# Ensure CommonModule. prefix
	if ($methodName -and -not $methodName.StartsWith("CommonModule.")) {
		$methodName = "CommonModule.$methodName"
	}
	X "$i<MethodName>$(Esc-XmlText $methodName)</MethodName>"

	# Description — плоская строка (дефолт ПУСТО: корпус 662 пустых / 209 заданы; не подставляем синоним — иначе роундтрип рвётся).
	$description = if ($def.description) { "$($def.description)" } else { "" }
	if ($description) { X "$i<Description>$(Esc-XmlText $description)</Description>" } else { X "$i<Description/>" }

	$key = if ($def.key) { "$($def.key)" } else { "" }
	# Пустое значение → самозакрывающийся, как у <Description> выше: Конфигуратор
	# не пишет пустых пар.
	if ($key) { X "$i<Key>$(Esc-XmlText $key)</Key>" } else { X "$i<Key/>" }

	$use = if ($def.use -eq $true) { "true" } else { "false" }
	X "$i<Use>$use</Use>"

	$predefined = if ($def.predefined -eq $true) { "true" } else { "false" }
	X "$i<Predefined>$predefined</Predefined>"

	$restartCount = if ($null -ne $def.restartCountOnFailure) { "$($def.restartCountOnFailure)" } else { "3" }
	$restartInterval = if ($null -ne $def.restartIntervalOnFailure) { "$($def.restartIntervalOnFailure)" } else { "10" }
	X "$i<RestartCountOnFailure>$restartCount</RestartCountOnFailure>"
	X "$i<RestartIntervalOnFailure>$restartInterval</RestartIntervalOnFailure>"
}

function Emit-EventSubscriptionProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }

	# Source — набор типов-источников (объектные типы CatalogObject.X/DocumentObject.X/…RecordSet/…Manager →
	# cfg:; ссылочные → d5p1). Единый эмиттер Emit-TypeContent (см. §cfg-типы). Прощающий ввод русских корней типа.
	$sources = @()
	if ($def.source) { $sources = @($def.source) }
	if ($sources.Count -gt 0) {
		X "$i<Source>"
		foreach ($src in $sources) { Emit-TypeContent "$i`t" (Resolve-TypeStr "$src") }
		X "$i</Source>"
	} else {
		X "$i<Source/>"
	}

	$event = if ($def.event) { "$($def.event)" } else { "BeforeWrite" }
	X "$i<Event>$event</Event>"

	$handler = if ($def.handler) { "$($def.handler)" } else { "" }
	# Ensure CommonModule. prefix
	if ($handler -and -not $handler.StartsWith("CommonModule.")) {
		$handler = "CommonModule.$handler"
	}
	X "$i<Handler>$(Esc-XmlText $handler)</Handler>"
}

# --- 13b. Wave 2: Report, DataProcessor ---

function Emit-ReportProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
	# UseStandardCommands: дефолт true (авторски-безопасно — доступность объекта через стандартный командный
	# интерфейс; при false и без переопределения размещения команд объект доступен лишь по навигационной ссылке).
	$useStdCmds = if (Get-BoolProp "useStandardCommands" $true) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmds</UseStandardCommands>"

	Emit-VerbatimRef $i "DefaultForm"              $def.defaultForm
	Emit-VerbatimRef $i "AuxiliaryForm"            $def.auxiliaryForm
	Emit-VerbatimRef $i "MainDataCompositionSchema" $def.mainDataCompositionSchema
	Emit-VerbatimRef $i "DefaultSettingsForm"      $def.defaultSettingsForm
	Emit-VerbatimRef $i "AuxiliarySettingsForm"    $def.auxiliarySettingsForm
	Emit-VerbatimRef $i "DefaultVariantForm"       $def.defaultVariantForm
	# Вспомогательная форма варианта отчёта — свойство формата 2.21 (8.5).
	if ($script:isFormat221) { Emit-VerbatimRef $i "AuxiliaryVariantForm" $def.auxiliaryVariantForm }
	Emit-VerbatimRef $i "VariantsStorage"          $def.variantsStorage
	Emit-VerbatimRef $i "SettingsStorage"          $def.settingsStorage
	$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
	X "$i<IncludeHelpInContents>$inclHelp</IncludeHelpInContents>"
	Emit-MLText $i "ExtendedPresentation" $def.extendedPresentation
	Emit-MLText $i "Explanation" $def.explanation
}

function Emit-DataProcessorProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
	$useStdCmds = if (Get-BoolProp "useStandardCommands" $true) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmds</UseStandardCommands>"

	Emit-VerbatimRef $i "DefaultForm"   $def.defaultForm
	Emit-VerbatimRef $i "AuxiliaryForm" $def.auxiliaryForm
	$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
	X "$i<IncludeHelpInContents>$inclHelp</IncludeHelpInContents>"
	Emit-MLText $i "ExtendedPresentation" $def.extendedPresentation
	Emit-MLText $i "Explanation" $def.explanation
}

# --- 13c. Wave 3: ExchangePlan, ChartOfCharacteristicTypes, DocumentJournal ---

function Emit-ExchangePlanProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText "$($def.comment)")</Comment>" } else { X "$i<Comment/>" }
	$useStdCmd = if (Get-BoolProp "useStandardCommands" $true) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmd</UseStandardCommands>"

	$codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" }
	$descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "150" }
	$codeAllowedLength = Get-EnumProp "CodeAllowedLength" "codeAllowedLength" "Variable"

	X "$i<CodeLength>$codeLength</CodeLength>"
	X "$i<CodeAllowedLength>$codeAllowedLength</CodeAllowedLength>"
	X "$i<DescriptionLength>$descriptionLength</DescriptionLength>"
	X "$i<DefaultPresentation>$(Get-EnumProp 'DefaultPresentation' 'defaultPresentation' 'AsDescription')</DefaultPresentation>"
	X "$i<EditType>$(Get-EnumProp 'EditType' 'editType' 'InDialog')</EditType>"
	$quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" }
	X "$i<QuickChoice>$quickChoice</QuickChoice>"
	X "$i<ChoiceMode>$(Get-EnumProp 'ChoiceMode' 'choiceMode' 'BothWays')</ChoiceMode>"

	# InputByString: override `inputByString` ЛИБО дефолт [Descr при D>0]+[Code при C>0] (prefix ExchangePlan).
	if (Test-DefKey 'inputByString') {
		$ibFields = @($def.inputByString | ForEach-Object { Expand-DataPath "$_" })
	} else {
		$ibFields = @()
		if ([int]$descriptionLength -gt 0) { $ibFields += "ExchangePlan.$objName.StandardAttribute.Description" }
		if ([int]$codeLength -gt 0)        { $ibFields += "ExchangePlan.$objName.StandardAttribute.Code" }
	}
	Emit-FieldBlock $i "InputByString" $ibFields
	X "$i<SearchStringModeOnInputByString>$(Get-EnumProp 'SearchStringModeOnInputByString' 'searchStringModeOnInputByString' 'Begin')</SearchStringModeOnInputByString>"
	X "$i<FullTextSearchOnInputByString>$(Get-EnumProp 'FullTextSearchOnInputByString' 'fullTextSearchOnInputByString' 'DontUse')</FullTextSearchOnInputByString>"
	X "$i<ChoiceDataGetModeOnInputByString>Directly</ChoiceDataGetModeOnInputByString>"
	Emit-FormRef $i "DefaultObjectForm"   $def.defaultObjectForm
	Emit-FormRef $i "DefaultListForm"     $def.defaultListForm
	Emit-FormRef $i "DefaultChoiceForm"   $def.defaultChoiceForm
	Emit-FormRef $i "AuxiliaryObjectForm" $def.auxiliaryObjectForm
	Emit-FormRef $i "AuxiliaryListForm"   $def.auxiliaryListForm
	Emit-FormRef $i "AuxiliaryChoiceForm" $def.auxiliaryChoiceForm

	Emit-StandardAttributes $i "ExchangePlan"
	Emit-Characteristics $i $def.characteristics
	Emit-BasedOn $i $def.basedOn

	$distributed = if ($def.distributedInfoBase -eq $true) { "true" } else { "false" }
	$includeExt = if ($def.includeConfigurationExtensions -eq $true) { "true" } else { "false" }
	X "$i<DistributedInfoBase>$distributed</DistributedInfoBase>"
	X "$i<IncludeConfigurationExtensions>$includeExt</IncludeConfigurationExtensions>"

	X "$i<CreateOnInput>$(Get-EnumProp 'CreateOnInput' 'createOnInput' 'DontUse')</CreateOnInput>"
	X "$i<ChoiceHistoryOnInput>$(Get-EnumProp 'ChoiceHistoryOnInput' 'choiceHistoryOnInput' 'Auto')</ChoiceHistoryOnInput>"
	$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
	X "$i<IncludeHelpInContents>$inclHelp</IncludeHelpInContents>"
	$dlFields = if (Test-DefKey 'dataLockFields') { @($def.dataLockFields | ForEach-Object { Expand-DataPath "$_" }) } else { @() }
	Emit-FieldBlock $i "DataLockFields" $dlFields
	X "$i<DataLockControlMode>$(Get-EnumProp 'DataLockControlMode' 'dataLockControlMode' 'Managed')</DataLockControlMode>"
	X "$i<FullTextSearch>$(Get-EnumProp 'FullTextSearch' 'fullTextSearch' 'Use')</FullTextSearch>"

	Emit-MLText $i "ObjectPresentation" $def.objectPresentation
	Emit-MLText $i "ExtendedObjectPresentation" $def.extendedObjectPresentation
	Emit-MLText $i "ListPresentation" $def.listPresentation
	Emit-MLText $i "ExtendedListPresentation" $def.extendedListPresentation
	Emit-MLText $i "Explanation" $def.explanation
	X "$i<DataHistory>$(Get-EnumProp 'DataHistory' 'dataHistory' 'DontUse')</DataHistory>"
	$updDH = if (Get-BoolProp "updateDataHistoryImmediatelyAfterWrite" $false) { "true" } else { "false" }
	X "$i<UpdateDataHistoryImmediatelyAfterWrite>$updDH</UpdateDataHistoryImmediatelyAfterWrite>"
	$execDH = if (Get-BoolProp "executeAfterWriteDataHistoryVersionProcessing" $false) { "true" } else { "false" }
	X "$i<ExecuteAfterWriteDataHistoryVersionProcessing>$execDH</ExecuteAfterWriteDataHistoryVersionProcessing>"
}

function Emit-ChartOfCharacteristicTypesProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText "$($def.comment)")</Comment>" } else { X "$i<Comment/>" }
	$useStdCmd = if (Get-BoolProp "useStandardCommands" $true) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmd</UseStandardCommands>"
	$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
	X "$i<IncludeHelpInContents>$inclHelp</IncludeHelpInContents>"

	# CharacteristicExtValues — ссылка на справочник доп. значений характеристик (обычно пусто).
	if ($def.characteristicExtValues) { X "$i<CharacteristicExtValues>$(Esc-XmlText "$($def.characteristicExtValues)")</CharacteristicExtValues>" }
	else { X "$i<CharacteristicExtValues/>" }

	# Type — тип значения характеристики (составной). DSL `valueType` строка "A + B + C" ИЛИ массив; нет ключа → дефолт.
	$vt = $def.valueType; if (-not $vt -and $def.valueTypes) { $vt = ($def.valueTypes -join ' + ') }
	if ($vt) {
		X "$i<Type>"
		Emit-TypeContent "$i`t" "$vt"
		X "$i</Type>"
	} else {
		X "$i<Type>"
		X "$i`t<v8:Type>xs:boolean</v8:Type>"
		X "$i`t<v8:Type>xs:string</v8:Type>"
		X "$i`t<v8:StringQualifiers>"
		X "$i`t`t<v8:Length>100</v8:Length>"
		X "$i`t`t<v8:AllowedLength>Variable</v8:AllowedLength>"
		X "$i`t</v8:StringQualifiers>"
		X "$i`t<v8:Type>xs:decimal</v8:Type>"
		X "$i`t<v8:NumberQualifiers>"
		X "$i`t`t<v8:Digits>15</v8:Digits>"
		X "$i`t`t<v8:FractionDigits>2</v8:FractionDigits>"
		X "$i`t`t<v8:AllowedSign>Any</v8:AllowedSign>"
		X "$i`t</v8:NumberQualifiers>"
		X "$i`t<v8:Type>xs:dateTime</v8:Type>"
		X "$i`t<v8:DateQualifiers>"
		X "$i`t`t<v8:DateFractions>DateTime</v8:DateFractions>"
		X "$i`t</v8:DateQualifiers>"
		X "$i</Type>"
	}

	$hierarchical = if ($def.hierarchical -eq $true) { "true" } else { "false" }
	X "$i<Hierarchical>$hierarchical</Hierarchical>"
	$foldersOnTop = if ($def.foldersOnTop -eq $false) { "false" } else { "true" }
	X "$i<FoldersOnTop>$foldersOnTop</FoldersOnTop>"

	$codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" }
	$descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "100" }
	X "$i<CodeLength>$codeLength</CodeLength>"
	X "$i<CodeAllowedLength>$(Get-EnumProp 'CodeAllowedLength' 'codeAllowedLength' 'Variable')</CodeAllowedLength>"
	X "$i<DescriptionLength>$descriptionLength</DescriptionLength>"
	X "$i<CodeSeries>$(Get-EnumProp 'CodeSeries' 'codeSeries' 'WholeCharacteristicKind')</CodeSeries>"
	$checkUnique = if ($def.checkUnique -eq $false) { "false" } else { "true" }
	X "$i<CheckUnique>$checkUnique</CheckUnique>"
	$autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" }
	X "$i<Autonumbering>$autonumbering</Autonumbering>"
	X "$i<DefaultPresentation>$(Get-EnumProp 'DefaultPresentation' 'defaultPresentation' 'AsDescription')</DefaultPresentation>"

	Emit-StandardAttributes $i "ChartOfCharacteristicTypes"
	Emit-Characteristics $i $def.characteristics
	X "$i<PredefinedDataUpdate>$(Get-EnumProp 'PredefinedDataUpdate' 'predefinedDataUpdate' 'Auto')</PredefinedDataUpdate>"
	X "$i<EditType>$(Get-EnumProp 'EditType' 'editType' 'InDialog')</EditType>"
	$quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" }
	X "$i<QuickChoice>$quickChoice</QuickChoice>"
	X "$i<ChoiceMode>$(Get-EnumProp 'ChoiceMode' 'choiceMode' 'BothWays')</ChoiceMode>"

	# InputByString: override ЛИБО дефолт [Descr при D>0]+[Code при C>0] (prefix ChartOfCharacteristicTypes).
	if (Test-DefKey 'inputByString') {
		$ibFields = @($def.inputByString | ForEach-Object { Expand-DataPath "$_" })
	} else {
		$ibFields = @()
		if ([int]$descriptionLength -gt 0) { $ibFields += "ChartOfCharacteristicTypes.$objName.StandardAttribute.Description" }
		if ([int]$codeLength -gt 0)        { $ibFields += "ChartOfCharacteristicTypes.$objName.StandardAttribute.Code" }
	}
	Emit-FieldBlock $i "InputByString" $ibFields
	X "$i<CreateOnInput>$(Get-EnumProp 'CreateOnInput' 'createOnInput' 'DontUse')</CreateOnInput>"
	X "$i<SearchStringModeOnInputByString>$(Get-EnumProp 'SearchStringModeOnInputByString' 'searchStringModeOnInputByString' 'Begin')</SearchStringModeOnInputByString>"
	X "$i<ChoiceDataGetModeOnInputByString>Directly</ChoiceDataGetModeOnInputByString>"
	X "$i<FullTextSearchOnInputByString>$(Get-EnumProp 'FullTextSearchOnInputByString' 'fullTextSearchOnInputByString' 'DontUse')</FullTextSearchOnInputByString>"
	X "$i<ChoiceHistoryOnInput>$(Get-EnumProp 'ChoiceHistoryOnInput' 'choiceHistoryOnInput' 'Auto')</ChoiceHistoryOnInput>"
	Emit-FormRef $i "DefaultObjectForm"       $def.defaultObjectForm
	Emit-FormRef $i "DefaultFolderForm"       $def.defaultFolderForm
	Emit-FormRef $i "DefaultListForm"         $def.defaultListForm
	Emit-FormRef $i "DefaultChoiceForm"       $def.defaultChoiceForm
	Emit-FormRef $i "DefaultFolderChoiceForm" $def.defaultFolderChoiceForm
	Emit-FormRef $i "AuxiliaryObjectForm"       $def.auxiliaryObjectForm
	Emit-FormRef $i "AuxiliaryFolderForm"       $def.auxiliaryFolderForm
	Emit-FormRef $i "AuxiliaryListForm"         $def.auxiliaryListForm
	Emit-FormRef $i "AuxiliaryChoiceForm"       $def.auxiliaryChoiceForm
	Emit-FormRef $i "AuxiliaryFolderChoiceForm" $def.auxiliaryFolderChoiceForm
	Emit-BasedOn $i $def.basedOn
	$dlFields = if (Test-DefKey 'dataLockFields') { @($def.dataLockFields | ForEach-Object { Expand-DataPath "$_" }) } else { @() }
	Emit-FieldBlock $i "DataLockFields" $dlFields
	X "$i<DataLockControlMode>$(Get-EnumProp 'DataLockControlMode' 'dataLockControlMode' 'Managed')</DataLockControlMode>"
	X "$i<FullTextSearch>$(Get-EnumProp 'FullTextSearch' 'fullTextSearch' 'Use')</FullTextSearch>"
	Emit-MLText $i "ObjectPresentation" $def.objectPresentation
	Emit-MLText $i "ExtendedObjectPresentation" $def.extendedObjectPresentation
	Emit-MLText $i "ListPresentation" $def.listPresentation
	Emit-MLText $i "ExtendedListPresentation" $def.extendedListPresentation
	Emit-MLText $i "Explanation" $def.explanation
	X "$i<DataHistory>$(Get-EnumProp 'DataHistory' 'dataHistory' 'DontUse')</DataHistory>"
	$updDH = if (Get-BoolProp "updateDataHistoryImmediatelyAfterWrite" $false) { "true" } else { "false" }
	X "$i<UpdateDataHistoryImmediatelyAfterWrite>$updDH</UpdateDataHistoryImmediatelyAfterWrite>"
	$execDH = if (Get-BoolProp "executeAfterWriteDataHistoryVersionProcessing" $false) { "true" } else { "false" }
	X "$i<ExecuteAfterWriteDataHistoryVersionProcessing>$execDH</ExecuteAfterWriteDataHistoryVersionProcessing>"
}

function Emit-DocumentJournalProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }

	Emit-VerbatimRef $i "DefaultForm"   $def.defaultForm
	Emit-VerbatimRef $i "AuxiliaryForm" $def.auxiliaryForm
	$useStdCmds = if (Get-BoolProp "useStandardCommands" $true) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmds</UseStandardCommands>"

	# RegisteredDocuments — регистрируемые документы (список MDObjectRef, прощающий ввод русских корней).
	$regDocs = @()
	if ($def.registeredDocuments) { $regDocs = @($def.registeredDocuments) }
	if ($regDocs.Count -gt 0) {
		X "$i<RegisteredDocuments>"
		foreach ($rd in $regDocs) { X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-XmlText (Normalize-MDObjectRef "$rd"))</xr:Item>" }
		X "$i</RegisteredDocuments>"
	} else {
		X "$i<RegisteredDocuments/>"
	}

	$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
	X "$i<IncludeHelpInContents>$inclHelp</IncludeHelpInContents>"

	Emit-StandardAttributes $i "DocumentJournal"

	Emit-MLText $i "ListPresentation" $def.listPresentation
	Emit-MLText $i "ExtendedListPresentation" $def.extendedListPresentation
	Emit-MLText $i "Explanation" $def.explanation
}

# --- 13d. Wave 4: ChartOfAccounts, AccountingRegister, ChartOfCalculationTypes, CalculationRegister ---

# Ссылка на объект метаданных: русский префикс типа → английский (ПланВидовХарактеристик.X → ChartOfCharacteristicTypes.X).
function Resolve-TypePrefixSyn {
	param([string]$ref)
	if ($ref -and $ref.Contains('.')) {
		$d = $ref.IndexOf('.'); $p = $ref.Substring(0, $d); $s = $ref.Substring($d + 1)
		if ($script:objectTypeSynonyms.ContainsKey($p)) { $p = $script:objectTypeSynonyms[$p] }
		return "$p.$s"
	}
	return $ref
}

function Emit-ChartOfAccountsProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText "$($def.comment)")</Comment>" } else { X "$i<Comment/>" }
	$useStdCmd = if (Get-BoolProp "useStandardCommands" $true) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmd</UseStandardCommands>"
	$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
	X "$i<IncludeHelpInContents>$inclHelp</IncludeHelpInContents>"
	Emit-BasedOn $i $def.basedOn

	# ExtDimensionTypes — ссылка на ПВХ видов субконто (прощающий ввод: ПланВидовХарактеристик.X → ChartOfCharacteristicTypes.X).
	$extDimTypes = if ($def.extDimensionTypes) { Resolve-TypePrefixSyn "$($def.extDimensionTypes)" } else { "" }
	if ($extDimTypes) { X "$i<ExtDimensionTypes>$(Esc-XmlText $extDimTypes)</ExtDimensionTypes>" } else { X "$i<ExtDimensionTypes/>" }

	# Количество субконто: без ПВХ (extDimensionTypes) платформа не даёт > 0 → дефолт 0; с ПВХ — 3.
	$maxExtDim = if ($null -ne $def.maxExtDimensionCount) { "$($def.maxExtDimensionCount)" } elseif ($extDimTypes) { "3" } else { "0" }
	X "$i<MaxExtDimensionCount>$maxExtDim</MaxExtDimensionCount>"

	if ($def.codeMask) { X "$i<CodeMask>$(Esc-XmlText "$($def.codeMask)")</CodeMask>" } else { X "$i<CodeMask/>" }

	$codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" }
	$descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "25" }
	X "$i<CodeLength>$codeLength</CodeLength>"
	X "$i<DescriptionLength>$descriptionLength</DescriptionLength>"
	X "$i<CodeSeries>$(Get-EnumProp 'CodeSeries' 'codeSeries' 'WholeChartOfAccounts')</CodeSeries>"
	$checkUnique = if ($def.checkUnique -eq $false) { "false" } else { "true" }
	X "$i<CheckUnique>$checkUnique</CheckUnique>"
	X "$i<DefaultPresentation>$(Get-EnumProp 'DefaultPresentation' 'defaultPresentation' 'AsCode')</DefaultPresentation>"

	Emit-StandardAttributes $i "ChartOfAccounts"
	Emit-Characteristics $i $def.characteristics

	# StandardTabularSections — ExtDimensionTypes (обёртка платформенно-константна: Synonym с пустым lang «Виды субконто»,
	# Comment/ToolTip/FillChecking; вложены 4 стандартных реквизита all-default). Кастомизация — не выведена (см. WORKFLOW).
	X "$i<StandardTabularSections>"
	X "$i`t<xr:StandardTabularSection name=`"ExtDimensionTypes`">"
	X "$i`t`t<xr:Synonym>"
	X "$i`t`t`t<v8:item>"
	X "$i`t`t`t`t<v8:lang/>"
	X "$i`t`t`t`t<v8:content>Виды субконто</v8:content>"
	X "$i`t`t`t</v8:item>"
	X "$i`t`t</xr:Synonym>"
	X "$i`t`t<xr:Comment/>"
	X "$i`t`t<xr:ToolTip/>"
	X "$i`t`t<xr:FillChecking>DontCheck</xr:FillChecking>"
	X "$i`t`t<xr:StandardAttributes>"
	foreach ($stAttr in @("TurnoversOnly","Predefined","ExtDimensionType","LineNumber")) {
		$stOv = if ($stAttr -eq "ExtDimensionType") { @{ FillChecking = "ShowError" } } else { $null }
		Emit-StandardAttribute "$i`t`t`t" $stAttr $stOv
	}
	X "$i`t`t</xr:StandardAttributes>"
	X "$i`t</xr:StandardTabularSection>"
	X "$i</StandardTabularSections>"

	X "$i<PredefinedDataUpdate>$(Get-EnumProp 'PredefinedDataUpdate' 'predefinedDataUpdate' 'Auto')</PredefinedDataUpdate>"
	X "$i<EditType>$(Get-EnumProp 'EditType' 'editType' 'InDialog')</EditType>"
	$quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" }
	X "$i<QuickChoice>$quickChoice</QuickChoice>"
	X "$i<ChoiceMode>$(Get-EnumProp 'ChoiceMode' 'choiceMode' 'BothWays')</ChoiceMode>"

	# InputByString: override ЛИБО дефолт [Descr при D>0]+[Code при C>0] (prefix ChartOfAccounts).
	if (Test-DefKey 'inputByString') {
		$ibFields = @($def.inputByString | ForEach-Object { Expand-DataPath "$_" })
	} else {
		$ibFields = @()
		if ([int]$descriptionLength -gt 0) { $ibFields += "ChartOfAccounts.$objName.StandardAttribute.Description" }
		if ([int]$codeLength -gt 0)        { $ibFields += "ChartOfAccounts.$objName.StandardAttribute.Code" }
	}
	Emit-FieldBlock $i "InputByString" $ibFields
	X "$i<SearchStringModeOnInputByString>$(Get-EnumProp 'SearchStringModeOnInputByString' 'searchStringModeOnInputByString' 'Begin')</SearchStringModeOnInputByString>"
	X "$i<FullTextSearchOnInputByString>$(Get-EnumProp 'FullTextSearchOnInputByString' 'fullTextSearchOnInputByString' 'DontUse')</FullTextSearchOnInputByString>"
	X "$i<ChoiceDataGetModeOnInputByString>Directly</ChoiceDataGetModeOnInputByString>"
	X "$i<CreateOnInput>$(Get-EnumProp 'CreateOnInput' 'createOnInput' 'DontUse')</CreateOnInput>"
	X "$i<ChoiceHistoryOnInput>$(Get-EnumProp 'ChoiceHistoryOnInput' 'choiceHistoryOnInput' 'Auto')</ChoiceHistoryOnInput>"
	Emit-FormRef $i "DefaultObjectForm"   $def.defaultObjectForm
	Emit-FormRef $i "DefaultListForm"     $def.defaultListForm
	Emit-FormRef $i "DefaultChoiceForm"   $def.defaultChoiceForm
	Emit-FormRef $i "AuxiliaryObjectForm" $def.auxiliaryObjectForm
	Emit-FormRef $i "AuxiliaryListForm"   $def.auxiliaryListForm
	Emit-FormRef $i "AuxiliaryChoiceForm" $def.auxiliaryChoiceForm

	$autoOrder = if ($def.autoOrderByCode -eq $false) { "false" } else { "true" }
	X "$i<AutoOrderByCode>$autoOrder</AutoOrderByCode>"
	$orderLength = if ($null -ne $def.orderLength) { "$($def.orderLength)" } else { "9" }
	X "$i<OrderLength>$orderLength</OrderLength>"

	$dlFields = if (Test-DefKey 'dataLockFields') { @($def.dataLockFields | ForEach-Object { Expand-DataPath "$_" }) } else { @() }
	Emit-FieldBlock $i "DataLockFields" $dlFields
	X "$i<DataLockControlMode>$(Get-EnumProp 'DataLockControlMode' 'dataLockControlMode' 'Managed')</DataLockControlMode>"
	X "$i<FullTextSearch>$(Get-EnumProp 'FullTextSearch' 'fullTextSearch' 'Use')</FullTextSearch>"
	X "$i<DataHistory>$(Get-EnumProp 'DataHistory' 'dataHistory' 'DontUse')</DataHistory>"
	$updDH = if (Get-BoolProp "updateDataHistoryImmediatelyAfterWrite" $false) { "true" } else { "false" }
	X "$i<UpdateDataHistoryImmediatelyAfterWrite>$updDH</UpdateDataHistoryImmediatelyAfterWrite>"
	$execDH = if (Get-BoolProp "executeAfterWriteDataHistoryVersionProcessing" $false) { "true" } else { "false" }
	X "$i<ExecuteAfterWriteDataHistoryVersionProcessing>$execDH</ExecuteAfterWriteDataHistoryVersionProcessing>"

	Emit-MLText $i "ObjectPresentation" $def.objectPresentation
	Emit-MLText $i "ExtendedObjectPresentation" $def.extendedObjectPresentation
	Emit-MLText $i "ListPresentation" $def.listPresentation
	Emit-MLText $i "ExtendedListPresentation" $def.extendedListPresentation
	Emit-MLText $i "Explanation" $def.explanation
}

function Emit-AccountingRegisterProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText "$($def.comment)")</Comment>" } else { X "$i<Comment/>" }
	$useStdCmd = if (Get-BoolProp "useStandardCommands" $true) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmd</UseStandardCommands>"
	$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
	X "$i<IncludeHelpInContents>$inclHelp</IncludeHelpInContents>"

	$chartOfAccounts = if ($def.chartOfAccounts) { "$($def.chartOfAccounts)" } else { "" }
	if ($chartOfAccounts) { X "$i<ChartOfAccounts>$(Esc-XmlText $chartOfAccounts)</ChartOfAccounts>" }
	else { X "$i<ChartOfAccounts/>" }

	$correspondence = if ($def.correspondence -eq $true) { "true" } else { "false" }
	X "$i<Correspondence>$correspondence</Correspondence>"

	$periodAdjLen = if ($null -ne $def.periodAdjustmentLength) { "$($def.periodAdjustmentLength)" } else { "0" }
	X "$i<PeriodAdjustmentLength>$periodAdjLen</PeriodAdjustmentLength>"

	Emit-FormRef $i "DefaultListForm"   $def.defaultListForm
	Emit-FormRef $i "AuxiliaryListForm" $def.auxiliaryListForm

	Emit-StandardAttributes $i "AccountingRegister"

	$dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Managed"
	X "$i<DataLockControlMode>$dataLockControlMode</DataLockControlMode>"

	$enableTotalsSplitting = if ($def.enableTotalsSplitting -eq $false) { "false" } else { "true" }
	X "$i<EnableTotalsSplitting>$enableTotalsSplitting</EnableTotalsSplitting>"

	$fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use"
	X "$i<FullTextSearch>$fullTextSearch</FullTextSearch>"

	Emit-MLText $i "ListPresentation" $def.listPresentation
	Emit-MLText $i "ExtendedListPresentation" $def.extendedListPresentation
	Emit-MLText $i "Explanation" $def.explanation
}

# Стандартные ТЧ Плана видов расчёта: Ведущие/Вытесняющие/Базовые виды расчёта. Обёртка платформенно-константна
# (Synonym с пустым lang, Comment/ToolTip/FillChecking); вложены Predefined/CalculationType(ShowError)/LineNumber.
$script:calcTypesStdTabular = @(
	@{ name = "LeadingCalculationTypes";    synonym = "Ведущие виды расчета" }
	@{ name = "DisplacingCalculationTypes"; synonym = "Вытесняющие виды расчета" }
	@{ name = "BaseCalculationTypes";       synonym = "Базовые виды расчета" }
)
function Emit-CalcTypesStdTabular {
	param([string]$i)
	X "$i<StandardTabularSections>"
	foreach ($sts in $script:calcTypesStdTabular) {
		X "$i`t<xr:StandardTabularSection name=`"$($sts.name)`">"
		X "$i`t`t<xr:Synonym>"
		X "$i`t`t`t<v8:item>"
		X "$i`t`t`t`t<v8:lang/>"
		X "$i`t`t`t`t<v8:content>$(Esc-XmlText $sts.synonym)</v8:content>"
		X "$i`t`t`t</v8:item>"
		X "$i`t`t</xr:Synonym>"
		X "$i`t`t<xr:Comment/>"
		X "$i`t`t<xr:ToolTip/>"
		X "$i`t`t<xr:FillChecking>DontCheck</xr:FillChecking>"
		X "$i`t`t<xr:StandardAttributes>"
		foreach ($stAttr in @("Predefined","CalculationType","LineNumber")) {
			$stOv = if ($stAttr -eq "CalculationType") { @{ FillChecking = "ShowError" } } else { $null }
			Emit-StandardAttribute "$i`t`t`t" $stAttr $stOv
		}
		X "$i`t`t</xr:StandardAttributes>"
		X "$i`t</xr:StandardTabularSection>"
	}
	X "$i</StandardTabularSections>"
}

function Emit-ChartOfCalculationTypesProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText "$($def.comment)")</Comment>" } else { X "$i<Comment/>" }
	$useStdCmd = if (Get-BoolProp "useStandardCommands" $true) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmd</UseStandardCommands>"

	$codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "5" }
	$descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "100" }
	X "$i<CodeLength>$codeLength</CodeLength>"
	X "$i<DescriptionLength>$descriptionLength</DescriptionLength>"
	X "$i<CodeType>$(Get-EnumProp 'CodeType' 'codeType' 'String')</CodeType>"
	X "$i<CodeAllowedLength>$(Get-EnumProp 'CodeAllowedLength' 'codeAllowedLength' 'Variable')</CodeAllowedLength>"
	X "$i<DefaultPresentation>$(Get-EnumProp 'DefaultPresentation' 'defaultPresentation' 'AsDescription')</DefaultPresentation>"
	X "$i<EditType>$(Get-EnumProp 'EditType' 'editType' 'InDialog')</EditType>"
	$quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" }
	X "$i<QuickChoice>$quickChoice</QuickChoice>"
	X "$i<ChoiceMode>$(Get-EnumProp 'ChoiceMode' 'choiceMode' 'BothWays')</ChoiceMode>"

	# InputByString: override ЛИБО дефолт [Descr при D>0]+[Code при C>0].
	if (Test-DefKey 'inputByString') {
		$ibFields = @($def.inputByString | ForEach-Object { Expand-DataPath "$_" })
	} else {
		$ibFields = @()
		if ([int]$descriptionLength -gt 0) { $ibFields += "ChartOfCalculationTypes.$objName.StandardAttribute.Description" }
		if ([int]$codeLength -gt 0)        { $ibFields += "ChartOfCalculationTypes.$objName.StandardAttribute.Code" }
	}
	Emit-FieldBlock $i "InputByString" $ibFields
	X "$i<SearchStringModeOnInputByString>$(Get-EnumProp 'SearchStringModeOnInputByString' 'searchStringModeOnInputByString' 'Begin')</SearchStringModeOnInputByString>"
	X "$i<FullTextSearchOnInputByString>$(Get-EnumProp 'FullTextSearchOnInputByString' 'fullTextSearchOnInputByString' 'DontUse')</FullTextSearchOnInputByString>"
	X "$i<ChoiceDataGetModeOnInputByString>Directly</ChoiceDataGetModeOnInputByString>"
	X "$i<CreateOnInput>$(Get-EnumProp 'CreateOnInput' 'createOnInput' 'DontUse')</CreateOnInput>"
	X "$i<ChoiceHistoryOnInput>$(Get-EnumProp 'ChoiceHistoryOnInput' 'choiceHistoryOnInput' 'Auto')</ChoiceHistoryOnInput>"
	Emit-FormRef $i "DefaultObjectForm"   $def.defaultObjectForm
	Emit-FormRef $i "DefaultListForm"     $def.defaultListForm
	Emit-FormRef $i "DefaultChoiceForm"   $def.defaultChoiceForm
	Emit-FormRef $i "AuxiliaryObjectForm" $def.auxiliaryObjectForm
	Emit-FormRef $i "AuxiliaryListForm"   $def.auxiliaryListForm
	Emit-FormRef $i "AuxiliaryChoiceForm" $def.auxiliaryChoiceForm
	Emit-BasedOn $i $def.basedOn

	X "$i<DependenceOnCalculationTypes>$(Get-EnumProp 'DependenceOnCalculationTypes' 'dependenceOnCalculationTypes' 'DontUse')</DependenceOnCalculationTypes>"
	# BaseCalculationTypes — список ссылок на ПВР (прощающий ввод ПланВидовРасчета.X → ChartOfCalculationTypes.X).
	$baseTypes = @(); if ($def.baseCalculationTypes) { $baseTypes = @($def.baseCalculationTypes | ForEach-Object { Resolve-TypePrefixSyn "$_" }) }
	if ($baseTypes.Count -gt 0) {
		X "$i<BaseCalculationTypes>"
		foreach ($bt in $baseTypes) { X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-XmlText (Normalize-MDObjectRef "$bt"))</xr:Item>" }
		X "$i</BaseCalculationTypes>"
	} else { X "$i<BaseCalculationTypes/>" }
	$actionPeriodUse = if ($def.actionPeriodUse -eq $true) { "true" } else { "false" }
	X "$i<ActionPeriodUse>$actionPeriodUse</ActionPeriodUse>"

	Emit-StandardAttributes $i "ChartOfCalculationTypes"
	Emit-Characteristics $i $def.characteristics
	Emit-CalcTypesStdTabular $i

	X "$i<PredefinedDataUpdate>$(Get-EnumProp 'PredefinedDataUpdate' 'predefinedDataUpdate' 'Auto')</PredefinedDataUpdate>"
	$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
	X "$i<IncludeHelpInContents>$inclHelp</IncludeHelpInContents>"
	$dlFields = if (Test-DefKey 'dataLockFields') { @($def.dataLockFields | ForEach-Object { Expand-DataPath "$_" }) } else { @() }
	Emit-FieldBlock $i "DataLockFields" $dlFields
	X "$i<DataLockControlMode>$(Get-EnumProp 'DataLockControlMode' 'dataLockControlMode' 'Managed')</DataLockControlMode>"
	X "$i<FullTextSearch>$(Get-EnumProp 'FullTextSearch' 'fullTextSearch' 'Use')</FullTextSearch>"

	Emit-MLText $i "ObjectPresentation" $def.objectPresentation
	Emit-MLText $i "ExtendedObjectPresentation" $def.extendedObjectPresentation
	Emit-MLText $i "ListPresentation" $def.listPresentation
	Emit-MLText $i "ExtendedListPresentation" $def.extendedListPresentation
	Emit-MLText $i "Explanation" $def.explanation
	X "$i<DataHistory>$(Get-EnumProp 'DataHistory' 'dataHistory' 'DontUse')</DataHistory>"
	$updDH = if (Get-BoolProp "updateDataHistoryImmediatelyAfterWrite" $false) { "true" } else { "false" }
	X "$i<UpdateDataHistoryImmediatelyAfterWrite>$updDH</UpdateDataHistoryImmediatelyAfterWrite>"
	$execDH = if (Get-BoolProp "executeAfterWriteDataHistoryVersionProcessing" $false) { "true" } else { "false" }
	X "$i<ExecuteAfterWriteDataHistoryVersionProcessing>$execDH</ExecuteAfterWriteDataHistoryVersionProcessing>"
}

function Emit-CalculationRegisterProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText "$($def.comment)")</Comment>" } else { X "$i<Comment/>" }
	$useStdCmd = if (Get-BoolProp "useStandardCommands" $true) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmd</UseStandardCommands>"
	Emit-FormRef $i "DefaultListForm"   $def.defaultListForm
	Emit-FormRef $i "AuxiliaryListForm" $def.auxiliaryListForm

	$periodicity = Get-EnumProp "InformationRegisterPeriodicity" "periodicity" "Month"
	X "$i<Periodicity>$periodicity</Periodicity>"

	$actionPeriod = if ($def.actionPeriod -eq $true) { "true" } else { "false" }
	X "$i<ActionPeriod>$actionPeriod</ActionPeriod>"

	$basePeriod = if ($def.basePeriod -eq $true) { "true" } else { "false" }
	X "$i<BasePeriod>$basePeriod</BasePeriod>"

	$schedule = if ($def.schedule) { "$($def.schedule)" } else { "" }
	if ($schedule) { X "$i<Schedule>$(Esc-XmlText $schedule)</Schedule>" } else { X "$i<Schedule/>" }

	$scheduleValue = if ($def.scheduleValue) { "$($def.scheduleValue)" } else { "" }
	if ($scheduleValue) { X "$i<ScheduleValue>$(Esc-XmlText $scheduleValue)</ScheduleValue>" } else { X "$i<ScheduleValue/>" }

	$scheduleDate = if ($def.scheduleDate) { "$($def.scheduleDate)" } else { "" }
	if ($scheduleDate) { X "$i<ScheduleDate>$(Esc-XmlText $scheduleDate)</ScheduleDate>" } else { X "$i<ScheduleDate/>" }

	$chartOfCalcTypes = if ($def.chartOfCalculationTypes) { "$($def.chartOfCalculationTypes)" } else { "" }
	if ($chartOfCalcTypes) { X "$i<ChartOfCalculationTypes>$(Esc-XmlText $chartOfCalcTypes)</ChartOfCalculationTypes>" }
	else { X "$i<ChartOfCalculationTypes/>" }

	$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
	X "$i<IncludeHelpInContents>$inclHelp</IncludeHelpInContents>"

	Emit-StandardAttributes $i "CalculationRegister"

	$dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Managed"
	X "$i<DataLockControlMode>$dataLockControlMode</DataLockControlMode>"

	$fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use"
	X "$i<FullTextSearch>$fullTextSearch</FullTextSearch>"

	Emit-MLText $i "ListPresentation" $def.listPresentation
	Emit-MLText $i "ExtendedListPresentation" $def.extendedListPresentation
	Emit-MLText $i "Explanation" $def.explanation
}

# --- 13e. Wave 5: BusinessProcess, Task ---

function Emit-BusinessProcessProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText "$($def.comment)")</Comment>" } else { X "$i<Comment/>" }
	$useStdCmd = if (Get-BoolProp "useStandardCommands" $true) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmd</UseStandardCommands>"
	X "$i<EditType>$(Get-EnumProp 'EditType' 'editType' 'InDialog')</EditType>"

	if (Test-DefKey 'inputByString') { $ibFields = @($def.inputByString | ForEach-Object { Expand-DataPath "$_" }) }
	else { $ibFields = @("BusinessProcess.$objName.StandardAttribute.Number") }
	Emit-FieldBlock $i "InputByString" $ibFields
	X "$i<CreateOnInput>$(Get-EnumProp 'CreateOnInput' 'createOnInput' 'DontUse')</CreateOnInput>"
	X "$i<SearchStringModeOnInputByString>$(Get-EnumProp 'SearchStringModeOnInputByString' 'searchStringModeOnInputByString' 'Begin')</SearchStringModeOnInputByString>"
	X "$i<ChoiceDataGetModeOnInputByString>Directly</ChoiceDataGetModeOnInputByString>"
	X "$i<FullTextSearchOnInputByString>$(Get-EnumProp 'FullTextSearchOnInputByString' 'fullTextSearchOnInputByString' 'DontUse')</FullTextSearchOnInputByString>"
	Emit-FormRef $i "DefaultObjectForm"   $def.defaultObjectForm
	Emit-FormRef $i "DefaultListForm"     $def.defaultListForm
	Emit-FormRef $i "DefaultChoiceForm"   $def.defaultChoiceForm
	Emit-FormRef $i "AuxiliaryObjectForm" $def.auxiliaryObjectForm
	Emit-FormRef $i "AuxiliaryListForm"   $def.auxiliaryListForm
	Emit-FormRef $i "AuxiliaryChoiceForm" $def.auxiliaryChoiceForm
	X "$i<ChoiceHistoryOnInput>$(Get-EnumProp 'ChoiceHistoryOnInput' 'choiceHistoryOnInput' 'Auto')</ChoiceHistoryOnInput>"

	$numberType = Get-EnumProp "NumberType" "numberType" "String"
	$numberLength = if ($null -ne $def.numberLength) { "$($def.numberLength)" } else { "11" }
	$numberAllowedLength = Get-EnumProp "NumberAllowedLength" "numberAllowedLength" "Variable"
	$checkUnique = if ($def.checkUnique -eq $false) { "false" } else { "true" }
	X "$i<NumberType>$numberType</NumberType>"
	X "$i<NumberLength>$numberLength</NumberLength>"
	X "$i<NumberAllowedLength>$numberAllowedLength</NumberAllowedLength>"
	X "$i<CheckUnique>$checkUnique</CheckUnique>"

	Emit-StandardAttributes $i "BusinessProcess"
	Emit-Characteristics $i $def.characteristics

	$autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" }
	X "$i<Autonumbering>$autonumbering</Autonumbering>"
	Emit-BasedOn $i $def.basedOn
	X "$i<NumberPeriodicity>$(Get-EnumProp 'NumberPeriodicity' 'numberPeriodicity' 'Nonperiodical')</NumberPeriodicity>"

	if ($def.task) { X "$i<Task>$(Esc-XmlText "$($def.task)")</Task>" } else { X "$i<Task/>" }
	$createTaskPriv = if (Get-BoolProp "createTaskInPrivilegedMode" $true) { "true" } else { "false" }
	X "$i<CreateTaskInPrivilegedMode>$createTaskPriv</CreateTaskInPrivilegedMode>"

	$dlFields = if (Test-DefKey 'dataLockFields') { @($def.dataLockFields | ForEach-Object { Expand-DataPath "$_" }) } else { @() }
	Emit-FieldBlock $i "DataLockFields" $dlFields
	X "$i<DataLockControlMode>$(Get-EnumProp 'DataLockControlMode' 'dataLockControlMode' 'Managed')</DataLockControlMode>"
	$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
	X "$i<IncludeHelpInContents>$inclHelp</IncludeHelpInContents>"
	X "$i<FullTextSearch>$(Get-EnumProp 'FullTextSearch' 'fullTextSearch' 'Use')</FullTextSearch>"

	Emit-MLText $i "ObjectPresentation" $def.objectPresentation
	Emit-MLText $i "ExtendedObjectPresentation" $def.extendedObjectPresentation
	Emit-MLText $i "ListPresentation" $def.listPresentation
	Emit-MLText $i "ExtendedListPresentation" $def.extendedListPresentation
	Emit-MLText $i "Explanation" $def.explanation
	X "$i<DataHistory>$(Get-EnumProp 'DataHistory' 'dataHistory' 'DontUse')</DataHistory>"
	$updDH = if (Get-BoolProp "updateDataHistoryImmediatelyAfterWrite" $false) { "true" } else { "false" }
	X "$i<UpdateDataHistoryImmediatelyAfterWrite>$updDH</UpdateDataHistoryImmediatelyAfterWrite>"
	$execDH = if (Get-BoolProp "executeAfterWriteDataHistoryVersionProcessing" $false) { "true" } else { "false" }
	X "$i<ExecuteAfterWriteDataHistoryVersionProcessing>$execDH</ExecuteAfterWriteDataHistoryVersionProcessing>"
}

function Emit-TaskProperties {
	param([string]$indent)
	$i = $indent
	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText "$($def.comment)")</Comment>" } else { X "$i<Comment/>" }
	$useStdCmd = if (Get-BoolProp "useStandardCommands" $true) { "true" } else { "false" }
	X "$i<UseStandardCommands>$useStdCmd</UseStandardCommands>"
	$numberType = Get-EnumProp "NumberType" "numberType" "String"
	$numberLength = if ($null -ne $def.numberLength) { "$($def.numberLength)" } else { "14" }
	$numberAllowedLength = Get-EnumProp "NumberAllowedLength" "numberAllowedLength" "Variable"
	$checkUnique = if ($def.checkUnique -eq $false) { "false" } else { "true" }
	$autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" }
	$taskNumberAutoPrefix = if ($def.taskNumberAutoPrefix) { "$($def.taskNumberAutoPrefix)" } else { "BusinessProcessNumber" }
	$descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "150" }
	X "$i<NumberType>$numberType</NumberType>"
	X "$i<NumberLength>$numberLength</NumberLength>"
	X "$i<NumberAllowedLength>$numberAllowedLength</NumberAllowedLength>"
	X "$i<CheckUnique>$checkUnique</CheckUnique>"
	X "$i<Autonumbering>$autonumbering</Autonumbering>"
	X "$i<TaskNumberAutoPrefix>$taskNumberAutoPrefix</TaskNumberAutoPrefix>"
	X "$i<DescriptionLength>$descriptionLength</DescriptionLength>"
	if ($def.addressing) { X "$i<Addressing>$(Esc-XmlText "$($def.addressing)")</Addressing>" } else { X "$i<Addressing/>" }
	if ($def.mainAddressingAttribute) { X "$i<MainAddressingAttribute>$(Esc-XmlText "$($def.mainAddressingAttribute)")</MainAddressingAttribute>" } else { X "$i<MainAddressingAttribute/>" }
	if ($def.currentPerformer) { X "$i<CurrentPerformer>$(Esc-XmlText "$($def.currentPerformer)")</CurrentPerformer>" } else { X "$i<CurrentPerformer/>" }
	Emit-BasedOn $i $def.basedOn
	Emit-StandardAttributes $i "Task"
	Emit-Characteristics $i $def.characteristics
	X "$i<DefaultPresentation>$(Get-EnumProp 'DefaultPresentation' 'defaultPresentation' 'AsDescription')</DefaultPresentation>"
	X "$i<EditType>$(Get-EnumProp 'EditType' 'editType' 'InDialog')</EditType>"
	if (Test-DefKey 'inputByString') { $ibFields = @($def.inputByString | ForEach-Object { Expand-DataPath "$_" }) }
	else { $ibFields = @("Task.$objName.StandardAttribute.Number") }
	Emit-FieldBlock $i "InputByString" $ibFields
	X "$i<SearchStringModeOnInputByString>$(Get-EnumProp 'SearchStringModeOnInputByString' 'searchStringModeOnInputByString' 'Begin')</SearchStringModeOnInputByString>"
	X "$i<FullTextSearchOnInputByString>$(Get-EnumProp 'FullTextSearchOnInputByString' 'fullTextSearchOnInputByString' 'DontUse')</FullTextSearchOnInputByString>"
	X "$i<ChoiceDataGetModeOnInputByString>Directly</ChoiceDataGetModeOnInputByString>"
	X "$i<CreateOnInput>$(Get-EnumProp 'CreateOnInput' 'createOnInput' 'DontUse')</CreateOnInput>"
	Emit-FormRef $i "DefaultObjectForm"   $def.defaultObjectForm
	Emit-FormRef $i "DefaultListForm"     $def.defaultListForm
	Emit-FormRef $i "DefaultChoiceForm"   $def.defaultChoiceForm
	Emit-FormRef $i "AuxiliaryObjectForm" $def.auxiliaryObjectForm
	Emit-FormRef $i "AuxiliaryListForm"   $def.auxiliaryListForm
	Emit-FormRef $i "AuxiliaryChoiceForm" $def.auxiliaryChoiceForm
	X "$i<ChoiceHistoryOnInput>$(Get-EnumProp 'ChoiceHistoryOnInput' 'choiceHistoryOnInput' 'Auto')</ChoiceHistoryOnInput>"
	$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
	X "$i<IncludeHelpInContents>$inclHelp</IncludeHelpInContents>"
	$dlFields = if (Test-DefKey 'dataLockFields') { @($def.dataLockFields | ForEach-Object { Expand-DataPath "$_" }) } else { @() }
	Emit-FieldBlock $i "DataLockFields" $dlFields
	X "$i<DataLockControlMode>$(Get-EnumProp 'DataLockControlMode' 'dataLockControlMode' 'Managed')</DataLockControlMode>"
	X "$i<FullTextSearch>$(Get-EnumProp 'FullTextSearch' 'fullTextSearch' 'Use')</FullTextSearch>"
	Emit-MLText $i "ObjectPresentation" $def.objectPresentation
	Emit-MLText $i "ExtendedObjectPresentation" $def.extendedObjectPresentation
	Emit-MLText $i "ListPresentation" $def.listPresentation
	Emit-MLText $i "ExtendedListPresentation" $def.extendedListPresentation
	Emit-MLText $i "Explanation" $def.explanation
	X "$i<DataHistory>$(Get-EnumProp 'DataHistory' 'dataHistory' 'DontUse')</DataHistory>"
	$updDH = if (Get-BoolProp "updateDataHistoryImmediatelyAfterWrite" $false) { "true" } else { "false" }
	X "$i<UpdateDataHistoryImmediatelyAfterWrite>$updDH</UpdateDataHistoryImmediatelyAfterWrite>"
	$execDH = if (Get-BoolProp "executeAfterWriteDataHistoryVersionProcessing" $false) { "true" } else { "false" }
	X "$i<ExecuteAfterWriteDataHistoryVersionProcessing>$execDH</ExecuteAfterWriteDataHistoryVersionProcessing>"
}

# --- 13f. Wave 6: HTTPService, WebService ---

function Emit-HTTPServiceProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText "$($def.comment)")</Comment>" } else { X "$i<Comment/>" }

	$rootURL = if ($def.rootURL) { "$($def.rootURL)" } else { $objName.ToLower() }
	X "$i<RootURL>$(Esc-XmlText $rootURL)</RootURL>"

	$reuseSessions = Get-EnumProp "ReuseSessions" "reuseSessions" "DontUse"
	X "$i<ReuseSessions>$reuseSessions</ReuseSessions>"

	$sessionMaxAge = if ($null -ne $def.sessionMaxAge) { "$($def.sessionMaxAge)" } else { "20" }
	X "$i<SessionMaxAge>$sessionMaxAge</SessionMaxAge>"
}

# XDTO-тип значения: "{uri}Local" (нотация Кларка) → тег с ЛОКАЛЬНЫМ объявлением xmlns, как пишет
# платформа (<XDTOValueType xmlns:d1p1="uri">d1p1:Local</...>). Простой "xs:string" — как есть.
function Emit-XDTOType {
	param([string]$indent, [string]$tag, [string]$value)
	if ($value -match '^\{([^}]+)\}(.+)$') {
		$uri = $Matches[1]; $local = $Matches[2]
		X "$indent<$tag xmlns:d1p1=`"$(Esc-Xml $uri)`">d1p1:$(Esc-Xml $local)</$tag>"
	} else {
		X "$indent<$tag>$(Esc-XmlText $value)</$tag>"
	}
}

function Emit-WebServiceProperties {
	param([string]$indent)
	$i = $indent

	X "$i<Name>$(Esc-XmlText $objName)</Name>"
	Emit-MLText $i "Synonym" $synonym
	if ($def.comment) { X "$i<Comment>$(Esc-XmlText "$($def.comment)")</Comment>" } else { X "$i<Comment/>" }

	$namespace = if ($def.namespace) { "$($def.namespace)" } else { "" }
	# Пустое значение → самозакрывающийся, как у <Comment> выше.
	if ($namespace) { X "$i<Namespace>$(Esc-XmlText $namespace)</Namespace>" } else { X "$i<Namespace/>" }

	# XDTOPackages — СПИСОК элементов, а не скаляр: значение либо ссылка на пакет конфигурации
	# (xr:MDObjectRef "XDTOPackage.Имя"), либо URI внешнего пространства имён (xs:string).
	# Presentation всегда пуст, CheckState всегда 0 (проверено на корпусе: 19/19).
	$pkgs = @()
	if ($def.xdtoPackages) { $pkgs = @($def.xdtoPackages) }
	if ($pkgs.Count -gt 0) {
		X "$i<XDTOPackages>"
		foreach ($p in $pkgs) {
			$pv = "$p"
			$xt = if ($pv -like 'XDTOPackage.*') { 'xr:MDObjectRef' } else { 'xs:string' }
			X "$i`t<xr:Item>"
			X "$i`t`t<xr:Presentation/>"
			X "$i`t`t<xr:CheckState>0</xr:CheckState>"
			X "$i`t`t<xr:Value xsi:type=`"$xt`">$(Esc-XmlText $pv)</xr:Value>"
			X "$i`t</xr:Item>"
		}
		X "$i</XDTOPackages>"
	} else { X "$i<XDTOPackages/>" }

	# Имя файла дескриптора не выводится из имени сервиса (DMILService → dmil.1cws) — только дефолт.
	$descriptor = if ($def.descriptorFileName) { "$($def.descriptorFileName)" } else { "$objName.1cws" }
	X "$i<DescriptorFileName>$(Esc-XmlText $descriptor)</DescriptorFileName>"

	$reuseSessions = Get-EnumProp "ReuseSessions" "reuseSessions" "DontUse"
	X "$i<ReuseSessions>$reuseSessions</ReuseSessions>"

	$sessionMaxAge = if ($null -ne $def.sessionMaxAge) { "$($def.sessionMaxAge)" } else { "20" }
	X "$i<SessionMaxAge>$sessionMaxAge</SessionMaxAge>"
}

# --- 13g. ChildObjects emitters for new types ---

function Emit-Column {
	param([string]$indent, $colDef)
	$uuid = New-Guid-String

	$name = ""
	$synonym = $null
	$comment = ""
	$indexing = "DontIndex"
	$references = @()

	if ($colDef -is [string]) {
		$name = "$colDef"
		$synonym = Split-CamelCase $name
	} else {
		$name = "$($colDef.name)"
		$synonym = if ($null -ne $colDef.synonym) { $colDef.synonym } else { Split-CamelCase $name }   # строка ИЛИ {ru,en}
		if ($colDef.comment) { $comment = "$($colDef.comment)" }
		if ($colDef.indexing) { $indexing = "$($colDef.indexing)" }
		if ($colDef.references) { $references = @($colDef.references) }
	}

	X "$indent<Column uuid=`"$uuid`">"
	X "$indent`t<Properties>"
	X "$indent`t`t<Name>$(Esc-XmlText $name)</Name>"
	Emit-MLText "$indent`t`t" "Synonym" $synonym
	if ($comment) { X "$indent`t`t<Comment>$(Esc-XmlText $comment)</Comment>" } else { X "$indent`t`t<Comment/>" }
	X "$indent`t`t<Indexing>$indexing</Indexing>"
	if ($references.Count -gt 0) {
		X "$indent`t`t<References>"
		foreach ($ref in $references) {
			X "$indent`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-XmlText (Normalize-MDObjectRef "$ref"))</xr:Item>"
		}
		X "$indent`t`t</References>"
	} else {
		X "$indent`t`t<References/>"
	}
	X "$indent`t</Properties>"
	X "$indent</Column>"
}

function Emit-URLTemplate {
	param([string]$indent, [string]$tmplName, $tmplDef)
	$uuid = New-Guid-String
	$tmplSynonym = Split-CamelCase $tmplName

	$template = ""
	# [ordered], а не @{}: порядок методов должен совпадать с DSL (он же порядок исходного XML),
	# иначе PS и py расходятся между собой и с выгрузкой платформы.
	$methods = [ordered]@{}

	$tmplComment = ""
	if ($tmplDef -is [string]) {
		$template = "$tmplDef"
	} else {
		$template = if ($tmplDef.template) { "$($tmplDef.template)" } else { "/$($tmplName.ToLower())" }
		if ($tmplDef.synonym) { $tmplSynonym = $tmplDef.synonym }   # объект {ru,en} НЕ интерполировать
		if ($tmplDef.comment) { $tmplComment = "$($tmplDef.comment)" }
		if ($tmplDef.methods) {
			$tmplDef.methods.PSObject.Properties | ForEach-Object {
				$methods[$_.Name] = $_.Value   # строка (HTTP-метод) ЛИБО объект {httpMethod, handler, synonym, comment}
			}
		}
	}

	X "$indent<URLTemplate uuid=`"$uuid`">"
	X "$indent`t<Properties>"
	X "$indent`t`t<Name>$(Esc-XmlText $tmplName)</Name>"
	Emit-MLText "$indent`t`t" "Synonym" $tmplSynonym
	if ($tmplComment) { X "$indent`t`t<Comment>$(Esc-XmlText $tmplComment)</Comment>" } else { X "$indent`t`t<Comment/>" }
	X "$indent`t`t<Template>$(Esc-XmlText $template)</Template>"
	X "$indent`t</Properties>"

	if ($methods.Count -gt 0) {
		X "$indent`t<ChildObjects>"
		foreach ($methodName in $methods.Keys) {
			$methodUuid = New-Guid-String
			$mDef = $methods[$methodName]
			# Строка — сокращение "только HTTP-метод"; объект — полная форма. Обработчик по умолчанию
			# выводится как ИмяШаблона+ИмяМетода, но в реальных конфигурациях он произвольный,
			# поэтому задаётся явно ключом handler.
			if ($mDef -is [string]) {
				$httpMethod = "$mDef"; $handler = "${tmplName}${methodName}"
				$methodSynonym = Split-CamelCase $methodName; $methodComment = ""
			} else {
				$httpMethod = if ($mDef.httpMethod) { "$($mDef.httpMethod)" } else { 'GET' }
				$handler = if ($mDef.handler) { "$($mDef.handler)" } else { "${tmplName}${methodName}" }
				$methodSynonym = if ($mDef.synonym) { $mDef.synonym } else { Split-CamelCase $methodName }
				$methodComment = if ($mDef.comment) { "$($mDef.comment)" } else { "" }
			}

			X "$indent`t`t<Method uuid=`"$methodUuid`">"
			X "$indent`t`t`t<Properties>"
			X "$indent`t`t`t`t<Name>$(Esc-XmlText $methodName)</Name>"
			Emit-MLText "$indent`t`t`t`t" "Synonym" $methodSynonym
			if ($methodComment) { X "$indent`t`t`t`t<Comment>$(Esc-XmlText $methodComment)</Comment>" } else { X "$indent`t`t`t`t<Comment/>" }
			X "$indent`t`t`t`t<HTTPMethod>$httpMethod</HTTPMethod>"
			X "$indent`t`t`t`t<Handler>$(Esc-XmlText $handler)</Handler>"
			X "$indent`t`t`t</Properties>"
			X "$indent`t`t</Method>"
		}
		X "$indent`t</ChildObjects>"
	} else {
		X "$indent`t<ChildObjects/>"
	}

	X "$indent</URLTemplate>"
}

function Emit-Operation {
	param([string]$indent, [string]$opName, $opDef)
	$uuid = New-Guid-String
	$opSynonym = Split-CamelCase $opName

	$returnType = "xs:string"
	$nillable = "false"
	$transactioned = "false"
	$handler = $opName
	$opComment = ""
	$dataLock = "Managed"   # у всех 192 операций корпуса
	# [ordered]: порядок параметров — как в DSL (он же порядок исходного XML).
	$params = [ordered]@{}

	if ($opDef -is [string]) {
		$returnType = "$opDef"
	} else {
		if ($opDef.returnType) { $returnType = "$($opDef.returnType)" }
		if ($opDef.nillable -eq $true) { $nillable = "true" }
		if ($opDef.transactioned -eq $true) { $transactioned = "true" }
		if ($opDef.handler) { $handler = "$($opDef.handler)" }
		if ($opDef.procedureName) { $handler = "$($opDef.procedureName)" }
		if ($opDef.synonym) { $opSynonym = $opDef.synonym }
		if ($opDef.comment) { $opComment = "$($opDef.comment)" }
		if ($opDef.dataLockControlMode) { $dataLock = "$($opDef.dataLockControlMode)" }
		if ($opDef.parameters) {
			$opDef.parameters.PSObject.Properties | ForEach-Object {
				$params[$_.Name] = $_.Value
			}
		}
	}

	X "$indent<Operation uuid=`"$uuid`">"
	X "$indent`t<Properties>"
	X "$indent`t`t<Name>$(Esc-XmlText $opName)</Name>"
	Emit-MLText "$indent`t`t" "Synonym" $opSynonym
	if ($opComment) { X "$indent`t`t<Comment>$(Esc-XmlText $opComment)</Comment>" } else { X "$indent`t`t<Comment/>" }
	Emit-XDTOType "$indent`t`t" "XDTOReturningValueType" $returnType
	X "$indent`t`t<Nillable>$nillable</Nillable>"
	X "$indent`t`t<Transactioned>$transactioned</Transactioned>"
	X "$indent`t`t<ProcedureName>$(Esc-XmlText $handler)</ProcedureName>"
	X "$indent`t`t<DataLockControlMode>$dataLock</DataLockControlMode>"
	X "$indent`t</Properties>"

	if ($params.Count -gt 0) {
		X "$indent`t<ChildObjects>"
		foreach ($paramName in $params.Keys) {
			$paramUuid = New-Guid-String
			$paramDef = $params[$paramName]
			$paramSynonym = Split-CamelCase $paramName
			$paramType = "xs:string"
			$paramNillable = "true"
			$paramDir = "In"
			$paramComment = ""

			if ($paramDef -is [string]) {
				$paramType = "$paramDef"
			} else {
				if ($paramDef.type) { $paramType = "$($paramDef.type)" }
				if ($paramDef.nillable -eq $false) { $paramNillable = "false" }
				if ($paramDef.direction) { $paramDir = "$($paramDef.direction)" }
				if ($paramDef.synonym) { $paramSynonym = $paramDef.synonym }
				if ($paramDef.comment) { $paramComment = "$($paramDef.comment)" }
			}

			X "$indent`t`t<Parameter uuid=`"$paramUuid`">"
			X "$indent`t`t`t<Properties>"
			X "$indent`t`t`t`t<Name>$(Esc-XmlText $paramName)</Name>"
			Emit-MLText "$indent`t`t`t`t" "Synonym" $paramSynonym
			if ($paramComment) { X "$indent`t`t`t`t<Comment>$(Esc-XmlText $paramComment)</Comment>" } else { X "$indent`t`t`t`t<Comment/>" }
			Emit-XDTOType "$indent`t`t`t`t" "XDTOValueType" $paramType
			X "$indent`t`t`t`t<Nillable>$paramNillable</Nillable>"
			X "$indent`t`t`t`t<TransferDirection>$paramDir</TransferDirection>"
			X "$indent`t`t`t</Properties>"
			X "$indent`t`t</Parameter>"
		}
		X "$indent`t</ChildObjects>"
	} else {
		X "$indent`t<ChildObjects/>"
	}

	X "$indent</Operation>"
}

function Emit-AddressingAttribute {
	param([string]$indent, $addrDef)
	# Реквизит адресации = полный object-слой реквизита (контекст task-addressing) + AddressingDimension.
	$parsed = Parse-AttributeShorthand $addrDef
	Emit-Attribute $indent $parsed "task-addressing" "AddressingAttribute"
}

# --- 14. Namespaces ---

$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'

# --- 14a. Detect format version from existing Configuration.xml ---

function Detect-FormatVersion([string]$dir) {
	$d = $dir
	while ($d) {
		# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
		# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
		$extPath = "$d.xml"
		if (Test-Path $extPath) {
			$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
			$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
			if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
		}
		$cfgPath = Join-Path $d "Configuration.xml"
		if (Test-Path $cfgPath) {
			$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
			# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
			# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
			$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
			if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
		}
		$parent = Split-Path $d -Parent
		if ($parent -eq $d) { break }
		$d = $parent
	}
	return "2.17"
}

# Режим совместимости конфигурации — из него выводится дефолт <LineNumberLength> табличной части
# (≤Version8_3_26 → 5, ≥Version8_3_27 → 9; платформа фиксирует значение при СОЗДАНии ТЧ).
# NB: версия ФОРМАТА от режима совместимости не зависит (её задаёт платформа выгрузки) — это
# независимые вещи, читаются из одного файла разными функциями.
# Читаем префикс побольше: <CompatibilityMode> лежит ~11-12 КБ от начала (в отличие от version=
# в первой строке), 2000 байт Detect-FormatVersion сюда не хватает.
function Detect-CompatibilityMode([string]$dir) {
	$d = $dir
	while ($d) {
		$cfgPath = Join-Path $d "Configuration.xml"
		if (Test-Path $cfgPath) {
			# NB: длина файла — в БАЙТАХ, а Substring режет по СИМВОЛАМ (кириллица = 2 байта),
			# поэтому ограничиваем по длине уже декодированной строки.
			$text = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
			$head = $text.Substring(0, [Math]::Min(65536, $text.Length))
			if ($head -match '<CompatibilityMode>([^<]+)</CompatibilityMode>') { return $Matches[1].Trim() }
		}
		$parent = Split-Path $d -Parent
		if ($parent -eq $d) { break }
		$d = $parent
	}
	return "Version8_3_24"
}

# Номер версии режима совместимости для сравнений: "Version8_3_27" → 80327, "Version8_5_1" → 80501.
function Get-CompatModeRank([string]$mode) {
	if ($mode -match '^Version(\d+)_(\d+)_(\d+)$') {
		return [int]$Matches[1] * 10000 + [int]$Matches[2] * 100 + [int]$Matches[3]
	}
	return 0
}

# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
function Get-FormatRank([string]$ver) {
	if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
	return 0
}

$script:formatVersion = Detect-FormatVersion $OutputDir
$script:compatMode = Detect-CompatibilityMode $OutputDir
# У каждого свойства свой порог: <TypeReductionMode> появился в формате 2.18 (платформа 8.3.25),
# <LineNumberLength> — в 2.20 (8.3.27). Один общий флаг здесь неверен: на проекте 2.18/2.19 он
# глушил бы TypeReductionMode, который платформа этих версий пишет, и роундтрип бы разъезжался.
$script:isFormat218 = (Get-FormatRank $script:formatVersion) -ge 218
$script:isFormat220 = (Get-FormatRank $script:formatVersion) -ge 220
$script:isFormat221 = (Get-FormatRank $script:formatVersion) -ge 221
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту.
# Только для шапок MetaDataObject и Form — в файлах с корнем extrnprops
# (Ext/ClientApplicationInterface.xml и т.п.) платформа его не пишет.
if ($script:isFormat221) {
	$palNs = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette"'
	$script:xmlnsDecl = $script:xmlnsDecl -replace ' xmlns:style=', "$palNs xmlns:style="
}
# Дефолт длины номера строки ТЧ: с режима 8.3.27 платформа заводит новые ТЧ с 9 разрядами.
$script:lineNumberLengthDefault = if ((Get-CompatModeRank $script:compatMode) -ge 80327) { 9 } else { 5 }

# --- 15. Main assembler ---

$uuid = New-Guid-String

# XML declaration
X '<?xml version="1.0" encoding="UTF-8"?>'
X "<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">"
X "`t<$objType uuid=`"$uuid`">"

# InternalInfo
Emit-InternalInfo "`t`t" $objType $objName

# Properties
X "`t`t<Properties>"

switch ($objType) {
	"Catalog"                    { Emit-CatalogProperties "`t`t`t" }
	"Document"                   { Emit-DocumentProperties "`t`t`t" }
	"Enum"                       { Emit-EnumProperties "`t`t`t" }
	"Constant"                   { Emit-ConstantProperties "`t`t`t" }
	"InformationRegister"        { Emit-InformationRegisterProperties "`t`t`t" }
	"AccumulationRegister"       { Emit-AccumulationRegisterProperties "`t`t`t" }
	"DefinedType"                { Emit-DefinedTypeProperties "`t`t`t" }
	"FunctionalOption"           { Emit-FunctionalOptionProperties "`t`t`t" }
	"Sequence"                   { Emit-SequenceProperties "`t`t`t" }
	"FilterCriterion"            { Emit-FilterCriterionProperties "`t`t`t" }
	"DocumentNumerator"          { Emit-DocumentNumeratorProperties "`t`t`t" }
	"SettingsStorage"            { Emit-SettingsStorageProperties "`t`t`t" }
	"CommonForm"                 { Emit-CommonFormProperties "`t`t`t" }
	"SessionParameter"           { Emit-SessionParameterProperties "`t`t`t" }
	"CommonCommand"              { Emit-CommonCommandProperties "`t`t`t" }
	"CommandGroup"               { Emit-CommandGroupProperties "`t`t`t" }
	"CommonAttribute"            { Emit-CommonAttributeProperties "`t`t`t" }
	"FunctionalOptionsParameter" { Emit-FunctionalOptionsParameterProperties "`t`t`t" }
	"WSReference"                { Emit-WSReferenceProperties "`t`t`t" }
	"CommonPicture"              { Emit-CommonPictureProperties "`t`t`t" }
	"CommonTemplate"             { Emit-CommonTemplateProperties "`t`t`t" }
	"CommonModule"               { Emit-CommonModuleProperties "`t`t`t" }
	"ScheduledJob"               { Emit-ScheduledJobProperties "`t`t`t" }
	"EventSubscription"          { Emit-EventSubscriptionProperties "`t`t`t" }
	"Report"                     { Emit-ReportProperties "`t`t`t" }
	"DataProcessor"              { Emit-DataProcessorProperties "`t`t`t" }
	"ExchangePlan"               { Emit-ExchangePlanProperties "`t`t`t" }
	"ChartOfCharacteristicTypes" { Emit-ChartOfCharacteristicTypesProperties "`t`t`t" }
	"DocumentJournal"            { Emit-DocumentJournalProperties "`t`t`t" }
	"ChartOfAccounts"            { Emit-ChartOfAccountsProperties "`t`t`t" }
	"AccountingRegister"         { Emit-AccountingRegisterProperties "`t`t`t" }
	"ChartOfCalculationTypes"    { Emit-ChartOfCalculationTypesProperties "`t`t`t" }
	"CalculationRegister"        { Emit-CalculationRegisterProperties "`t`t`t" }
	"BusinessProcess"            { Emit-BusinessProcessProperties "`t`t`t" }
	"Task"                       { Emit-TaskProperties "`t`t`t" }
	"HTTPService"                { Emit-HTTPServiceProperties "`t`t`t" }
	"WebService"                 { Emit-WebServiceProperties "`t`t`t" }
}

X "`t`t</Properties>"

# ChildObjects
$hasChildren = $false

# --- Types with Attributes + TabularSections ---
$typesWithAttrTS = @("Catalog","Document","Report","DataProcessor","ExchangePlan",
	"ChartOfCharacteristicTypes","ChartOfAccounts","ChartOfCalculationTypes",
	"BusinessProcess","Task")

if ($objType -in $typesWithAttrTS) {
	$attrs = @()
	if ($def.attributes) {
		foreach ($a in $def.attributes) {
			$attrs += Parse-AttributeShorthand $a
		}
	}
	$tsSections = [ordered]@{}
	if ($def.tabularSections) {
		# Значение ТЧ: массив колонок (синоним авто) ЛИБО объект {attributes/columns, synonym, tooltip, comment}.
		# Нормализуем в $tsSections[name] = @{ columns; synonym; tooltip; comment }.
		function New-TsEntry { param($val)
			if ($val -is [array] -or $val.GetType().Name -eq 'Object[]') {
				return @{ columns = @($val); synonym = $null; tooltip = $null; comment = $null; lineNumber = $null; fillChecking = $null; use = $null; lineNumberLength = $null }
			}
			$cols = if ($val.attributes) { @($val.attributes) } elseif ($val.columns) { @($val.columns) } else { @() }
			return @{ columns = $cols; synonym = $val.synonym; tooltip = $val.tooltip; comment = if ($val.comment) { "$($val.comment)" } else { $null }; lineNumber = $val.lineNumber; fillChecking = $val.fillChecking; use = $val.use; lineNumberLength = $val.lineNumberLength }
		}
		if ($def.tabularSections -is [array] -or $def.tabularSections.GetType().Name -eq "Object[]") {
			foreach ($ts in $def.tabularSections) { $tsSections[$ts.name] = New-TsEntry $ts }
		} else {
			$def.tabularSections.PSObject.Properties | ForEach-Object { $tsSections[$_.Name] = New-TsEntry $_.Value }
		}
	}

	# ChartOfAccounts: AccountingFlags + ExtDimensionAccountingFlags (признаки учёта — структурно как реквизит,
	# но без Indexing/FullTextSearch/Use; тип по умолчанию Boolean). Парсим как реквизиты.
	$acctFlags = @()
	$extDimFlags = @()
	if ($objType -eq "ChartOfAccounts") {
		if ($def.accountingFlags) { foreach ($af in $def.accountingFlags) { $acctFlags += Parse-AttributeShorthand $af } }
		if ($def.extDimensionAccountingFlags) { foreach ($edf in $def.extDimensionAccountingFlags) { $extDimFlags += Parse-AttributeShorthand $edf } }
	}

	# Task: AddressingAttributes
	$addrAttrs = @()
	if ($objType -eq "Task" -and $def.addressingAttributes) {
		$addrAttrs = @($def.addressingAttributes)
	}

	# Commands (map имя→объект ИЛИ array [{name,...}]) — генерируем блок + CommandModule.bsl-заготовку.
	$commands = @()
	if ($def.commands) {
		if ($def.commands -is [array] -or $def.commands.GetType().Name -eq 'Object[]') {
			foreach ($c in $def.commands) { $commands += @{ name = "$($c.name)"; def = $c } }
		} else {
			$def.commands.PSObject.Properties | ForEach-Object { $commands += @{ name = $_.Name; def = $_.Value } }
		}
	}
	$childCount = $attrs.Count + $tsSections.Count + $acctFlags.Count + $extDimFlags.Count + $addrAttrs.Count + $commands.Count
	if ($childCount -gt 0) {
		$hasChildren = $true
		X "`t`t<ChildObjects>"
		$context = switch ($objType) {
			"Catalog"  { "catalog" }
			"Document" { "document" }
			{ $_ -in @("DataProcessor","Report") } { "processor" }
			"ChartOfCharacteristicTypes" { "cct" }   # как catalog (Use/FillFromFillingValue/DataHistory), но Use ПОСЛЕ Indexing
			{ $_ -in @("ChartOfAccounts","ChartOfCalculationTypes") } { "account" }   # как catalog, но БЕЗ <Use> (реквизиты ПС/ПВР не иерархичны как справочник)
			default    { "object" }
		}
		foreach ($a in $attrs) {
			Emit-Attribute "`t`t`t" $a $context
		}
		foreach ($tsName in $tsSections.Keys) {
			$tsE = $tsSections[$tsName]
			Emit-TabularSection "`t`t`t" $tsName $tsE.columns $objType $objName $tsE.synonym $tsE.tooltip $tsE.comment $tsE.lineNumber $tsE.fillChecking $tsE.use $tsE.lineNumberLength
		}
		foreach ($af in $acctFlags) {
			Emit-Attribute "`t`t`t" $af "account-flag" "AccountingFlag"
		}
		foreach ($edf in $extDimFlags) {
			Emit-Attribute "`t`t`t" $edf "account-flag" "ExtDimensionAccountingFlag"
		}
		foreach ($aa in $addrAttrs) {
			Emit-AddressingAttribute "`t`t`t" $aa
		}
		foreach ($cmd in $commands) {
			Emit-Command "`t`t`t" $cmd.name $cmd.def
		}
		X "`t`t</ChildObjects>"
	} else {
		X "`t`t<ChildObjects/>"
	}
}

# --- Enum: enum values ---
if ($objType -eq "Enum") {
	$values = @()
	if ($def.values) {
		foreach ($v in $def.values) {
			$values += Parse-EnumValueShorthand $v
		}
	}
	if ($values.Count -gt 0) {
		$hasChildren = $true
		X "`t`t<ChildObjects>"
		foreach ($v in $values) {
			Emit-EnumValue "`t`t`t" $v
		}
		X "`t`t</ChildObjects>"
	} else {
		X "`t`t<ChildObjects/>"
	}
}

# --- Constant, DefinedType, ScheduledJob, EventSubscription: no ChildObjects ---

# --- Registers: dimensions + resources + attributes ---
if ($objType -in @("InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister")) {
	$dims = @()
	$resources = @()
	$regAttrs = @()
	if ($def.dimensions) {
		foreach ($d in $def.dimensions) {
			$dims += Parse-AttributeShorthand $d
		}
	}
	if ($def.resources) {
		foreach ($r in $def.resources) {
			$resources += Parse-AttributeShorthand $r
		}
	}
	if ($def.attributes) {
		foreach ($a in $def.attributes) {
			$regAttrs += Parse-AttributeShorthand $a
		}
	}
	$regCommands = @()
	if ($def.commands) {
		if ($def.commands -is [array] -or $def.commands.GetType().Name -eq 'Object[]') {
			foreach ($c in $def.commands) { $regCommands += @{ name = "$($c.name)"; def = $c } }
		} else {
			$def.commands.PSObject.Properties | ForEach-Object { $regCommands += @{ name = $_.Name; def = $_.Value } }
		}
	}

	if ($dims.Count -gt 0 -or $resources.Count -gt 0 -or $regAttrs.Count -gt 0 -or $regCommands.Count -gt 0) {
		$hasChildren = $true
		X "`t`t<ChildObjects>"
		# InformationRegister.Attribute supports FillFromFillingValue/FillValue/DataHistory; прочие — нет.
		# CalculationRegister.Attribute несёт <ScheduleLink> → отдельный контекст register-calc.
		$regCtx = switch ($objType) { "InformationRegister" { "register-info" } "CalculationRegister" { "register-calc" } default { "register-other" } }
		# Все семейства регистров: ресурсы/измерения — через богатый Emit-Attribute (общий слой object-свойств).
		$dimResCtx = switch ($objType) { "InformationRegister" { "register-info" } "AccumulationRegister" { "register-accum" } "CalculationRegister" { "register-calc" } "AccountingRegister" { "register-account" } default { $null } }
		# Порядок видов детей — канон выгрузки, снят с корпуса (acc+erp, разброса внутри
		# типа нет): у большинства регистров Resource, Attribute, Dimension, а у
		# бухгалтерского — Dimension, Resource, Attribute. Команды у платформы идут
		# последними, как и здесь.
		$kindOrder = if ($objType -eq "AccountingRegister") { @('dim','res','attr') } else { @('res','attr','dim') }
		foreach ($kind in $kindOrder) {
			switch ($kind) {
				'res' {
					foreach ($r in $resources) {
						if ($dimResCtx) { Emit-Attribute "`t`t`t" $r $dimResCtx "Resource" }
						else { Emit-Resource "`t`t`t" $r $objType }
					}
				}
				'dim' {
					foreach ($d in $dims) {
						if ($dimResCtx) { Emit-Attribute "`t`t`t" $d $dimResCtx "Dimension" }
						else { Emit-Dimension "`t`t`t" $d $objType }
					}
				}
				'attr' {
					foreach ($a in $regAttrs) {
						Emit-Attribute "`t`t`t" $a $regCtx
					}
				}
			}
		}
		foreach ($cmd in $regCommands) {
			Emit-Command "`t`t`t" $cmd.name $cmd.def
		}
		X "`t`t</ChildObjects>"
	} else {
		X "`t`t<ChildObjects/>"
	}
}

# --- DocumentJournal: columns + commands ---
if ($objType -eq "DocumentJournal") {
	$columns = @()
	if ($def.columns) { $columns = @($def.columns) }
	$djCommands = @()
	if ($def.commands) {
		if ($def.commands -is [array] -or $def.commands.GetType().Name -eq 'Object[]') {
			foreach ($c in $def.commands) { $djCommands += @{ name = "$($c.name)"; def = $c } }
		} else {
			$def.commands.PSObject.Properties | ForEach-Object { $djCommands += @{ name = $_.Name; def = $_.Value } }
		}
	}
	if ($columns.Count -gt 0 -or $djCommands.Count -gt 0) {
		$hasChildren = $true
		X "`t`t<ChildObjects>"
		foreach ($col in $columns) {
			Emit-Column "`t`t`t" $col
		}
		foreach ($cmd in $djCommands) {
			Emit-Command "`t`t`t" $cmd.name $cmd.def
		}
		X "`t`t</ChildObjects>"
	} else {
		X "`t`t<ChildObjects/>"
	}
}

# --- Sequence: dimensions ---
if ($objType -eq "Sequence") {
	$seqDims = @()
	if ($def.dimensions) { $seqDims = @($def.dimensions) }
	if ($seqDims.Count -gt 0) {
		$hasChildren = $true
		X "`t`t<ChildObjects>"
		foreach ($d in $seqDims) { Emit-SequenceDimension "`t`t`t" $d }
		X "`t`t</ChildObjects>"
	} else {
		X "`t`t<ChildObjects/>"
	}
}

# --- FilterCriterion / SettingsStorage: ChildObjects (формы вне скоупа; FilterCriterion может нести <Command>) ---
if ($objType -in @("FilterCriterion", "SettingsStorage")) {
	$fcCommands = @()
	if ($def.commands) {
		if ($def.commands -is [array] -or $def.commands.GetType().Name -eq 'Object[]') {
			foreach ($c in $def.commands) { $fcCommands += @{ name = "$($c.name)"; def = $c } }
		} else {
			$def.commands.PSObject.Properties | ForEach-Object { $fcCommands += @{ name = $_.Name; def = $_.Value } }
		}
	}
	if ($fcCommands.Count -gt 0) {
		$hasChildren = $true
		X "`t`t<ChildObjects>"
		foreach ($cmd in $fcCommands) { Emit-Command "`t`t`t" $cmd.name $cmd.def }
		X "`t`t</ChildObjects>"
	} else {
		X "`t`t<ChildObjects/>"
	}
}
# DocumentNumerator: ChildObjects нет вовсе (не эмитим).

# --- HTTPService: URLTemplates ---
if ($objType -eq "HTTPService") {
	# [ordered]: порядок шаблонов — как в DSL (он же порядок исходного XML). @{} давало произвольный
	# порядок в PS и расходилось с py, который сортировал; обе ветки приведены к порядку DSL.
	$urlTemplates = [ordered]@{}
	if ($def.urlTemplates) {
		$def.urlTemplates.PSObject.Properties | ForEach-Object {
			$urlTemplates[$_.Name] = $_.Value
		}
	}
	if ($urlTemplates.Count -gt 0) {
		$hasChildren = $true
		X "`t`t<ChildObjects>"
		foreach ($tmplName in $urlTemplates.Keys) {
			Emit-URLTemplate "`t`t`t" $tmplName $urlTemplates[$tmplName]
		}
		X "`t`t</ChildObjects>"
	} else {
		X "`t`t<ChildObjects/>"
	}
}

# --- WebService: Operations ---
if ($objType -eq "WebService") {
	# [ordered]: порядок операций — как в DSL (он же порядок исходного XML).
	$operations = [ordered]@{}
	if ($def.operations) {
		$def.operations.PSObject.Properties | ForEach-Object {
			$operations[$_.Name] = $_.Value
		}
	}
	if ($operations.Count -gt 0) {
		$hasChildren = $true
		X "`t`t<ChildObjects>"
		foreach ($opName in $operations.Keys) {
			Emit-Operation "`t`t`t" $opName $operations[$opName]
		}
		X "`t`t</ChildObjects>"
	} else {
		X "`t`t<ChildObjects/>"
	}
}

# --- CommonModule: no ChildObjects ---

X "`t</$objType>"
X "</MetaDataObject>"

$metadataXml = $script:xml.ToString()

# --- 16. Write files ---

# Type → plural directory mapping
$script:typePluralMap = @{
	"Catalog"                   = "Catalogs"
	"Document"                  = "Documents"
	"Enum"                      = "Enums"
	"Constant"                  = "Constants"
	"InformationRegister"       = "InformationRegisters"
	"AccumulationRegister"      = "AccumulationRegisters"
	"AccountingRegister"        = "AccountingRegisters"
	"CalculationRegister"       = "CalculationRegisters"
	"ChartOfAccounts"           = "ChartsOfAccounts"
	"ChartOfCharacteristicTypes"= "ChartsOfCharacteristicTypes"
	"ChartOfCalculationTypes"   = "ChartsOfCalculationTypes"
	"BusinessProcess"           = "BusinessProcesses"
	"Task"                      = "Tasks"
	"ExchangePlan"              = "ExchangePlans"
	"DocumentJournal"           = "DocumentJournals"
	"Report"                    = "Reports"
	"DataProcessor"             = "DataProcessors"
	"CommonModule"              = "CommonModules"
	"ScheduledJob"              = "ScheduledJobs"
	"EventSubscription"         = "EventSubscriptions"
	"HTTPService"               = "HTTPServices"
	"WebService"                = "WebServices"
	"DefinedType"               = "DefinedTypes"
	"FunctionalOption"          = "FunctionalOptions"
	"Sequence"                  = "Sequences"
	"FilterCriterion"           = "FilterCriteria"
	"DocumentNumerator"         = "DocumentNumerators"
	"SettingsStorage"           = "SettingsStorages"
	"CommonForm"                = "CommonForms"
	"SessionParameter"          = "SessionParameters"
	"CommonCommand"             = "CommonCommands"
	"CommandGroup"              = "CommandGroups"
	"CommonAttribute"           = "CommonAttributes"
	"FunctionalOptionsParameter" = "FunctionalOptionsParameters"
	"WSReference"               = "WSReferences"
	"CommonPicture"             = "CommonPictures"
	"CommonTemplate"            = "CommonTemplates"
}

$typePlural = $script:typePluralMap[$objType]
$typeDir = Join-Path $OutputDir $typePlural

# Main XML file: {OutputDir}/{TypePlural}/{Name}.xml
$mainXmlPath = Join-Path $typeDir "$objName.xml"

# Types that don't have subdirectory structure (no Ext/, no modules)
$typesNoSubDir = @("DefinedType","ScheduledJob","EventSubscription")

# Object subdirectory: {OutputDir}/{TypePlural}/{Name}/Ext/
$objSubDir = Join-Path $typeDir $objName
# --- Predefined data (Ext/Predefined.xml) ---
# Элемент DSL: строка "(Код) Имя [Наименование]" ЛИБО объект (+ русские синонимы ключей).
# Наименование: нет [..]/ключа → авто(Split-CamelCase Имени); [] / "" → пусто; [текст]/текст → как есть.
function Resolve-PredefItem {
	param($val)
	if ($val -is [string]) {
		# Грамматика "(Код) Имя [Наименование]: Тип" (тип — как в полях СКД/реквизитах). Порядок разбора зеркалит
		# Parse-CalcShorthand: сначала вынуть [Наим] (может содержать ':'), затем отделить тип по ':'.
		$s = "$val"; $type = $null; $descRaw = $null; $hasDesc = $false
		if ($s -match '\[(.*)\]') { $descRaw = $Matches[1]; $hasDesc = $true; $s = $s -replace '\s*\[.*\]', '' }
		if ($s.Contains(':')) { $p = $s -split ':', 2; $s = $p[0]; $type = $p[1].Trim() }   # '' → пустой <Type/>
		$m = [regex]::Match($s.Trim(), '^\s*(?:\(([^)]*)\)\s*)?(\S+)\s*$')
		$name = $m.Groups[2].Value
		$code = if ($m.Groups[1].Success) { $m.Groups[1].Value } else { '' }
		$desc = if ($hasDesc) { $descRaw } else { Split-CamelCase $name }
		return @{ name = $name; code = $code; desc = $desc; isFolder = $false; children = @(); type = $type }
	}
	# Объектная форма + русские синонимы (прощающий ввод).
	$gv = { param($o, [string[]]$keys) foreach ($k in $keys) { if ($o.PSObject.Properties[$k]) { return $o.$k } } return $null }
	$name = "$(& $gv $val @('name','имя'))"
	$codeV = & $gv $val @('code','код')
	$code = if ($null -ne $codeV) { "$codeV" } else { '' }
	$hasDesc = $val.PSObject.Properties['description'] -or $val.PSObject.Properties['наименование']
	$descV = & $gv $val @('description','наименование')
	$desc = if ($hasDesc) { "$descV" } else { Split-CamelCase $name }   # ключа нет → авто; '' → пусто
	$folderV = & $gv $val @('isFolder','группа')
	$isFolder = ($folderV -eq $true)
	$subs = & $gv $val @('childItems','подчиненные')
	$typeV = & $gv $val @('type','тип')   # тип значения характеристики (ПВХ): строка "A + B" ИЛИ массив
	if ($typeV -is [System.Array]) { $typeV = ($typeV -join ' + ') }
	return @{ name = $name; code = $code; desc = $desc; isFolder = $isFolder; children = @(if ($subs) { $subs } else { @() }); type = $typeV }
}
function Emit-PredefItem {
	param($sb, $val, [string]$indent, [string]$codeType)
	$r = Resolve-PredefItem $val
	[void]$sb.Append("$indent<Item id=`"$(New-Guid-String)`">`n")
	[void]$sb.Append("$indent`t<Name>$(Esc-XmlText $r.name)</Name>`n")
	if (-not $r.code) { [void]$sb.Append("$indent`t<Code/>`n") }
	elseif ($codeType -eq 'Number') { [void]$sb.Append("$indent`t<Code xsi:type=`"xs:decimal`">$(Esc-XmlText $r.code)</Code>`n") }
	else { [void]$sb.Append("$indent`t<Code>$(Esc-XmlText $r.code)</Code>`n") }
	if ($r.desc -eq '') { [void]$sb.Append("$indent`t<Description/>`n") }
	else { [void]$sb.Append("$indent`t<Description>$(Esc-XmlText $r.desc)</Description>`n") }
	# Type — тип значения предопределённой характеристики (ПВХ); между Description и IsFolder.
	# type=$null → блока нет (Catalog); type='' → пустой <Type/>; type='A + B' → наполненный.
	if ($null -ne $r.type -and "$($r.type)" -eq '') { [void]$sb.Append("$indent`t<Type/>`n") }
	elseif ($r.type) {
		[void]$sb.Append("$indent`t<Type>`n")
		$tmp = New-Object System.Text.StringBuilder
		$saveXml = $script:xml; $script:xml = $tmp
		Emit-TypeContent "$indent`t`t" "$($r.type)"
		$script:xml = $saveXml
		[void]$sb.Append(($tmp.ToString() -replace "`r`n", "`n"))
		[void]$sb.Append("$indent`t</Type>`n")
	}
	[void]$sb.Append("$indent`t<IsFolder>$(if ($r.isFolder) { 'true' } else { 'false' })</IsFolder>`n")
	if ($r.children.Count -gt 0) {
		[void]$sb.Append("$indent`t<ChildItems>`n")
		foreach ($c in $r.children) { Emit-PredefItem $sb $c "$indent`t`t" $codeType }
		[void]$sb.Append("$indent`t</ChildItems>`n")
	}
	[void]$sb.Append("$indent</Item>`n")
}
function Build-PredefinedXml {
	param($items, [string]$xsiType, [string]$codeType)
	$sb = New-Object System.Text.StringBuilder
	[void]$sb.Append("<?xml version=`"1.0`" encoding=`"UTF-8`"?>`n")
	[void]$sb.Append("<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"$xsiType`" version=`"$($script:formatVersion)`">`n")
	# Шапка Predefined.xml не объявляет cfg (predef/v8/xr/xs/xsi) — на время сборки этого
	# файла ссылочный тип уходит на локальную форму, как делает и платформа.
	$savedCfgPrefix = $script:cfgPrefix; $script:cfgPrefix = $null
	try { foreach ($it in $items) { Emit-PredefItem $sb $it "`t" $codeType } }
	finally { $script:cfgPrefix = $savedCfgPrefix }
	[void]$sb.Append("</PredefinedData>`n")
	return ($sb.ToString() -replace "`r`n", "`n") -replace "`n", "`r`n"
}

# --- Предопределённые СЧЕТА Плана счетов (отдельная грамматика: AccountType/OffBalance/Order/AccountingFlags/
# ExtDimensionTypes/ChildItems). Флаги перечисляем по def-порядку списков признаков плана; в DSL — только TRUE. ---
$script:predefAccGet = { param($o, [string[]]$keys) foreach ($k in $keys) { if ($o -is [System.Collections.IDictionary]) { if ($o.Contains($k)) { return $o[$k] } } elseif ($o.PSObject -and $o.PSObject.Properties[$k]) { return $o.$k } } return $null }
# «Только обороты» (<Turnover>) — предопределённый признак учёта субконто. В DSL — токен в списке flags
# наравне с добавленными признаками (ЛИБО отдельный ключ turnover). Распознаём по имени (регистронезависимо).
$script:subcontoTurnoverTokens = @('turnover', 'толькообороты', 'только обороты', 'оборотный')
function Emit-PredefAccountFlags {
	param($sb, [string]$indent, [string]$tag, [string]$refKind, [string]$objName, [string[]]$flagNames, $trueSet)
	if (-not $flagNames -or $flagNames.Count -eq 0) { [void]$sb.Append("$indent<$tag/>`n"); return }
	$set = @{}; if ($trueSet) { foreach ($t in @($trueSet)) { $set["$t"] = $true } }
	[void]$sb.Append("$indent<$tag>`n")
	foreach ($fn in $flagNames) {
		$v = if ($set.ContainsKey($fn)) { 'true' } else { 'false' }
		[void]$sb.Append("$indent`t<Flag ref=`"ChartOfAccounts.$objName.$refKind.$fn`">$v</Flag>`n")
	}
	[void]$sb.Append("$indent</$tag>`n")
}
function Emit-PredefAccount {
	param($sb, $val, [string]$indent, [string]$objName, [string[]]$acctFlagNames, [string[]]$extDimFlagNames, [string]$extDimTypesRef = '')
	$gv = $script:predefAccGet
	$name = "$(& $gv $val @('name','имя'))"
	$codeV = & $gv $val @('code','код'); $code = if ($null -ne $codeV) { "$codeV" } else { '' }
	$hasDesc = ($val -is [System.Collections.IDictionary] -and ($val.Contains('description') -or $val.Contains('наименование'))) -or ($val.PSObject -and ($val.PSObject.Properties['description'] -or $val.PSObject.Properties['наименование']))
	$descV = & $gv $val @('description','наименование')
	$desc = if ($hasDesc) { "$descV" } else { Split-CamelCase $name }
	$acctType = "$(& $gv $val @('accountType','видСчета','вид'))"; if (-not $acctType) { $acctType = 'ActivePassive' }
	$offV = & $gv $val @('offBalance','забалансовый'); $off = if ($offV -eq $true) { 'true' } else { 'false' }
	$order = "$(& $gv $val @('order','порядок'))"
	$flags = & $gv $val @('flags','признаки')
	$subconto = & $gv $val @('subconto','extDimensionTypes','видыСубконто')
	$children = & $gv $val @('childItems','подчиненные')

	[void]$sb.Append("$indent<Item id=`"$(New-Guid-String)`">`n")
	[void]$sb.Append("$indent`t<Name>$(Esc-XmlText $name)</Name>`n")
	if (-not $code) { [void]$sb.Append("$indent`t<Code/>`n") } else { [void]$sb.Append("$indent`t<Code>$(Esc-XmlText $code)</Code>`n") }
	if ($desc -eq '') { [void]$sb.Append("$indent`t<Description/>`n") } else { [void]$sb.Append("$indent`t<Description>$(Esc-XmlText $desc)</Description>`n") }
	[void]$sb.Append("$indent`t<AccountType>$acctType</AccountType>`n")
	[void]$sb.Append("$indent`t<OffBalance>$off</OffBalance>`n")
	[void]$sb.Append("$indent`t<Order>$(Esc-XmlText $order)</Order>`n")
	Emit-PredefAccountFlags $sb "$indent`t" 'AccountingFlags' 'AccountingFlag' $objName $acctFlagNames $flags
	# ExtDimensionTypes — субконто: пусто → self-close; иначе список <ExtDimensionType name="..."> с Turnover + признаками.
	$subArr = @(); if ($subconto) { $subArr = @($subconto) }
	if ($subArr.Count -eq 0) { [void]$sb.Append("$indent`t<ExtDimensionTypes/>`n") }
	else {
		[void]$sb.Append("$indent`t<ExtDimensionTypes>`n")
		foreach ($sc in $subArr) {
			# Строковая форма "Тип | Признак1, Признак2" (флаги после |, turnover=false). Объектная — {type, turnover?, flags?}.
			if ($sc -is [string]) {
				$scTurnV = $null; $scFlags = $null; $scStr = "$sc"
				if ($scStr.Contains('|')) {
					$scParts = $scStr.Split('|', 2); $scType = $scParts[0].Trim()
					$scFlags = @($scParts[1].Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ })
				} else { $scType = $scStr.Trim() }
			}
			else { $scType = "$(& $gv $sc @('type','тип'))"; $scTurnV = & $gv $sc @('turnover','толькоОбороты','оборотный'); $scFlags = & $gv $sc @('flags','признаки') }
			# Короткая запись: голое имя значения → префикс ПВХ видов субконто плана (extDimensionTypes); иначе резолв синонима.
			if (-not $scType.Contains('.')) { if ($extDimTypesRef) { $scType = "$extDimTypesRef.$scType" } }
			else { $scType = Resolve-TypePrefixSyn $scType }
			# «Только обороты» — токен в списке flags (или отдельный ключ turnover); вынимаем из настоящих признаков.
			$scTurn = if ($scTurnV -eq $true) { 'true' } else { 'false' }
			$scFlagsReal = @()
			foreach ($f in @($scFlags)) { if ("$f".Trim().ToLower() -in $script:subcontoTurnoverTokens) { $scTurn = 'true' } else { $scFlagsReal += $f } }
			$scFlags = $scFlagsReal
			[void]$sb.Append("$indent`t`t<ExtDimensionType name=`"$(Esc-Xml $scType)`">`n")
			[void]$sb.Append("$indent`t`t`t<Turnover>$scTurn</Turnover>`n")
			Emit-PredefAccountFlags $sb "$indent`t`t`t" 'AccountingFlags' 'ExtDimensionAccountingFlag' $objName $extDimFlagNames $scFlags
			[void]$sb.Append("$indent`t`t</ExtDimensionType>`n")
		}
		[void]$sb.Append("$indent`t</ExtDimensionTypes>`n")
	}
	$childArr = @(); if ($children) { $childArr = @($children) }
	if ($childArr.Count -gt 0) {
		[void]$sb.Append("$indent`t<ChildItems>`n")
		foreach ($c in $childArr) { Emit-PredefAccount $sb $c "$indent`t`t" $objName $acctFlagNames $extDimFlagNames $extDimTypesRef }
		[void]$sb.Append("$indent`t</ChildItems>`n")
	}
	[void]$sb.Append("$indent</Item>`n")
}
function Build-PredefinedAccountXml {
	param($items, [string]$objName, [string[]]$acctFlagNames, [string[]]$extDimFlagNames, [string]$extDimTypesRef = '')
	$sb = New-Object System.Text.StringBuilder
	[void]$sb.Append("<?xml version=`"1.0`" encoding=`"UTF-8`"?>`n")
	[void]$sb.Append("<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"ChartOfAccountsPredefinedItems`" version=`"$($script:formatVersion)`">`n")
	# См. Build-PredefinedXml: шапка этого файла cfg не объявляет.
	$savedCfgPrefix = $script:cfgPrefix; $script:cfgPrefix = $null
	try { foreach ($it in $items) { Emit-PredefAccount $sb $it "`t" $objName $acctFlagNames $extDimFlagNames $extDimTypesRef } }
	finally { $script:cfgPrefix = $savedCfgPrefix }
	[void]$sb.Append("</PredefinedData>`n")
	return ($sb.ToString() -replace "`r`n", "`n") -replace "`n", "`r`n"
}

# --- Предопределённые ВИДЫ РАСЧЁТА (плоские: Name/Code/Description/ActionPeriodIsBase). Строка "(Код) Имя [Наим]"
# ЛИБО объект {name, code, description, actionPeriodIsBase}. ---
function Emit-PredefCalcType {
	param($sb, $val, [string]$indent)
	$r = Resolve-PredefItem $val
	$apib = 'false'
	if ($val -isnot [string]) {
		$apibV = & $script:predefAccGet $val @('actionPeriodIsBase','периодДействияБазовый')
		if ($apibV -eq $true) { $apib = 'true' }
	}
	[void]$sb.Append("$indent<Item id=`"$(New-Guid-String)`">`n")
	[void]$sb.Append("$indent`t<Name>$(Esc-XmlText $r.name)</Name>`n")
	if (-not $r.code) { [void]$sb.Append("$indent`t<Code/>`n") } else { [void]$sb.Append("$indent`t<Code>$(Esc-XmlText $r.code)</Code>`n") }
	if ($r.desc -eq '') { [void]$sb.Append("$indent`t<Description/>`n") } else { [void]$sb.Append("$indent`t<Description>$(Esc-XmlText $r.desc)</Description>`n") }
	[void]$sb.Append("$indent`t<ActionPeriodIsBase>$apib</ActionPeriodIsBase>`n")
	[void]$sb.Append("$indent</Item>`n")
}
function Build-PredefinedCalcTypeXml {
	param($items)
	$sb = New-Object System.Text.StringBuilder
	[void]$sb.Append("<?xml version=`"1.0`" encoding=`"UTF-8`"?>`n")
	[void]$sb.Append("<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"CalculationTypePredefinedItems`" version=`"$($script:formatVersion)`">`n")
	foreach ($it in $items) { Emit-PredefCalcType $sb $it "`t" }
	[void]$sb.Append("</PredefinedData>`n")
	return ($sb.ToString() -replace "`r`n", "`n") -replace "`n", "`r`n"
}

$extDir = Join-Path $objSubDir "Ext"

if (-not (Test-Path $typeDir)) {
	New-Item -ItemType Directory -Path $typeDir -Force | Out-Null
}
if ($objType -notin $typesNoSubDir) {
	if (-not (Test-Path $objSubDir)) {
		New-Item -ItemType Directory -Path $objSubDir -Force | Out-Null
	}
}

$enc = New-Object System.Text.UTF8Encoding($true)

# Единая точка записи XML. Конфигуратор не пишет перевод строки в конце файла —
# последний байт `>`; сборка через AppendLine добавляла лишний.
# KeepEol в имени — отличие от одноимённой функции в скелетных навыках
# (cf-init и др.): та ЕЩЁ и нормализует EOL к CRLF, а здесь этого делать
# НЕЛЬЗЯ — в объектном XML бывают многострочные текстовые узлы (запрос,
# синоним, значение заполнения), и сплошная нормализация меняла бы содержимое.
# Разделители тут и так CRLF: документ собран через AppendLine.
# Модули .bsl сюда НЕ идут: у них свой хвост.
function Write-XmlFileKeepEol([string]$path, [string]$text, $encoding) {
	[System.IO.File]::WriteAllText($path, $text.TrimEnd("`r", "`n"), $encoding)
}

Write-XmlFileKeepEol $mainXmlPath $metadataXml $enc

# Module files
$modulesCreated = @()

# Helper: create Ext/ only when needed (avoids empty Ext/ for Constant, Enum, etc.)
function Ensure-ExtDir {
	if (-not (Test-Path $extDir)) {
		New-Item -ItemType Directory -Path $extDir -Force | Out-Null
	}
}

# Types with ObjectModule.bsl
$typesWithObjectModule = @("Catalog","Document","Report","DataProcessor","ExchangePlan",
	"ChartOfAccounts","ChartOfCharacteristicTypes","ChartOfCalculationTypes",
	"BusinessProcess","Task")
# Types with RecordSetModule.bsl
$typesWithRecordSetModule = @("InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister")
# Types with ManagerModule.bsl
$typesWithManagerModule = @("Report","DataProcessor","Constant","Enum")
# Types with ValueManagerModule.bsl
$typesWithValueManagerModule = @("Constant")
# Types with Module.bsl (general)
$typesWithModule = @("CommonModule","HTTPService","WebService")

if ($objType -in $typesWithObjectModule) {
	$modulePath = Join-Path $extDir "ObjectModule.bsl"
	if (-not (Test-Path $modulePath)) {
		Ensure-ExtDir
		[System.IO.File]::WriteAllText($modulePath, "", $enc)
		$modulesCreated += $modulePath
	}
}
if ($objType -in $typesWithManagerModule) {
	$modulePath = Join-Path $extDir "ManagerModule.bsl"
	if (-not (Test-Path $modulePath)) {
		Ensure-ExtDir
		[System.IO.File]::WriteAllText($modulePath, "", $enc)
		$modulesCreated += $modulePath
	}
}
if ($objType -in $typesWithValueManagerModule) {
	$modulePath = Join-Path $extDir "ValueManagerModule.bsl"
	if (-not (Test-Path $modulePath)) {
		Ensure-ExtDir
		[System.IO.File]::WriteAllText($modulePath, "", $enc)
		$modulesCreated += $modulePath
	}
}
if ($objType -in $typesWithRecordSetModule) {
	$modulePath = Join-Path $extDir "RecordSetModule.bsl"
	if (-not (Test-Path $modulePath)) {
		Ensure-ExtDir
		[System.IO.File]::WriteAllText($modulePath, "", $enc)
		$modulesCreated += $modulePath
	}
}
if ($objType -in $typesWithModule) {
	$modulePath = Join-Path $extDir "Module.bsl"
	if (-not (Test-Path $modulePath)) {
		Ensure-ExtDir
		[System.IO.File]::WriteAllText($modulePath, "", $enc)
		$modulesCreated += $modulePath
	}
}
# CommonCommand — заготовка модуля команды (CommandModule.bsl).
if ($objType -eq "CommonCommand") {
	$modulePath = Join-Path $extDir "CommandModule.bsl"
	if (-not (Test-Path $modulePath)) {
		Ensure-ExtDir
		[System.IO.File]::WriteAllText($modulePath, "", $enc)
		$modulesCreated += $modulePath
	}
}
# CommonForm — заготовка структуры формы под компиляцию: Ext/Form.xml (пустая управляемая форма) + Ext/Form/Module.bsl.
# Содержимое формы наполняет form-compile/form-edit (не перезаписываем существующее).
if ($objType -eq "CommonForm") {
	Ensure-ExtDir
	$cfFormXmlPath = Join-Path $extDir "Form.xml"
	if (-not (Test-Path $cfFormXmlPath)) {
		$cfFormNs = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
		# Шапка Form на 2.21 тоже несёт палитру — см. комментарий у $script:xmlnsDecl.
		if ($script:isFormat221) { $cfFormNs = $cfFormNs -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=' }
		$cfFormXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<Form $cfFormNs version=`"$($script:formatVersion)`">`r`n`t<AutoCommandBar name=`"ФормаКоманднаяПанель`" id=`"-1`">`r`n`t`t<Autofill>true</Autofill>`r`n`t</AutoCommandBar>`r`n`t<ChildItems/>`r`n</Form>`r`n"
		Write-XmlFileKeepEol $cfFormXmlPath $cfFormXml $enc
		$modulesCreated += $cfFormXmlPath
	}
	$cfModuleDir = Join-Path $extDir "Form"
	if (-not (Test-Path $cfModuleDir)) { New-Item -ItemType Directory -Path $cfModuleDir -Force | Out-Null }
	$cfModulePath = Join-Path $cfModuleDir "Module.bsl"
	if (-not (Test-Path $cfModulePath)) {
		[System.IO.File]::WriteAllText($cfModulePath, "", $enc)
		$modulesCreated += $cfModulePath
	}
}

# Special files
# --- Состав плана обмена (ExchangePlan, Ext/Content.xml). Ключ `content`/`Состав`:
# [ "MDRef" (AutoRecord=Deny, дефолт) | "MDRef: autoRecord" (Allow) | {metadata, autoRecord} ].
# Токен-признак autoRecord/АвтоРегистрация (или Allow/Разрешить) → авторегистрация вкл. Метаданные — MDObjectRef verbatim. ---
function Parse-ExchangeContentItem($entry) {
	if ($entry -is [string]) {
		$s = "$entry"; $ref = $s; $ar = 'Deny'
		$ci = $s.LastIndexOf(':')
		if ($ci -ge 0) {
			$ref = $s.Substring(0, $ci).Trim()
			$flag = $s.Substring($ci + 1).Trim()
			if ($flag -match '^(autoRecord|АвтоРегистрация|Allow|Разрешить)$') { $ar = 'Allow' }
			elseif ($flag -match '^(Deny|Запретить)$') { $ar = 'Deny' }
		}
		return @{ metadata = $ref.Trim(); autoRecord = $ar }
	}
	$ref = if ($null -ne $entry.metadata) { "$($entry.metadata)" } elseif ($null -ne $entry.Метаданные) { "$($entry.Метаданные)" } elseif ($null -ne $entry.объект) { "$($entry.объект)" } else { '' }
	$rawAr = if ($null -ne $entry.autoRecord) { $entry.autoRecord } elseif ($null -ne $entry.АвтоРегистрация) { $entry.АвтоРегистрация } else { $false }
	$ar = 'Deny'
	if ($rawAr -is [bool]) { if ($rawAr) { $ar = 'Allow' } }
	elseif ("$rawAr" -match '^(Allow|Разрешить|true|autoRecord|АвтоРегистрация)$') { $ar = 'Allow' }
	return @{ metadata = $ref.Trim(); autoRecord = $ar }
}
if ($objType -eq "ExchangePlan") {
	$contentPath = Join-Path $extDir "Content.xml"
	$xepNs = 'xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
	$cItems = @()
	$cSrc = if ($null -ne $def.content) { $def.content } elseif ($null -ne $def.Состав) { $def.Состав } else { $null }
	if ($cSrc) { foreach ($e in @($cSrc)) { $it = Parse-ExchangeContentItem $e; if ($it.metadata) { $cItems += $it } } }
	if ($cItems.Count -gt 0) {
		Ensure-ExtDir
		$sbC = New-Object System.Text.StringBuilder
		[void]$sbC.Append("<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n")
		[void]$sbC.Append("<ExchangePlanContent $xepNs version=`"$($script:formatVersion)`">`r`n")
		foreach ($it in $cItems) {
			[void]$sbC.Append("`t<Item>`r`n")
			[void]$sbC.Append("`t`t<Metadata>$(Esc-XmlText $it.metadata)</Metadata>`r`n")
			[void]$sbC.Append("`t`t<AutoRecord>$($it.autoRecord)</AutoRecord>`r`n")
			[void]$sbC.Append("`t</Item>`r`n")
		}
		[void]$sbC.Append("</ExchangePlanContent>`r`n")
		Write-XmlFileKeepEol $contentPath $sbC.ToString() $enc
		$modulesCreated += $contentPath
	} elseif (-not (Test-Path $contentPath)) {
		# При пустом составе платформа всё равно пишет пустой <ExchangePlanContent/> — проверено на
		# ОДНОЙ конфигурации, снятой 8.3.24/8.3.25/8.3.26/8.3.27: во всех четырёх файл есть.
		# (Единственный план обмена БЕЗ файла найден в УТ — хвостовая аномалия конфигурации, не правило.)
		Ensure-ExtDir
		$contentXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<ExchangePlanContent $xepNs version=`"$($script:formatVersion)`"/>`r`n"
		Write-XmlFileKeepEol $contentPath $contentXml $enc
		$modulesCreated += $contentPath
	}
}
if ($objType -eq "BusinessProcess") {
	$flowchartPath = Join-Path $extDir "Flowchart.xml"
	if (-not (Test-Path $flowchartPath)) {
		Ensure-ExtDir
		$flowchartXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<Flowchart xmlns=`"http://v8.1c.ru/8.3/MDClasses`" version=`"$($script:formatVersion)`"/>`r`n"
		Write-XmlFileKeepEol $flowchartPath $flowchartXml $enc
		$modulesCreated += $flowchartPath
	}
}

# Предопределённые элементы (Ext/Predefined.xml). Root-элемент — по типу. Пусто/нет ключа → файл не создаём.
$predefRootByType = @{ 'Catalog' = 'CatalogPredefinedItems'; 'ChartOfCharacteristicTypes' = 'PlanOfCharacteristicKindPredefinedItems' }
if ($objType -eq 'ChartOfAccounts' -and $def.predefined -and @($def.predefined).Count -gt 0) {
	# Предопределённые СЧЕТА — отдельная грамматика (флаги разворачиваются по def-порядку признаков плана).
	Ensure-ExtDir
	$afNames = @(); if ($def.accountingFlags) { foreach ($af in $def.accountingFlags) { $afNames += (Parse-AttributeShorthand $af).name } }
	$edfNames = @(); if ($def.extDimensionAccountingFlags) { foreach ($edf in $def.extDimensionAccountingFlags) { $edfNames += (Parse-AttributeShorthand $edf).name } }
	$edtRef = if ($def.extDimensionTypes) { Resolve-TypePrefixSyn "$($def.extDimensionTypes)" } else { '' }
	$predefXml = Build-PredefinedAccountXml @($def.predefined) $objName $afNames $edfNames $edtRef
	$predefPath = Join-Path $extDir "Predefined.xml"
	Write-XmlFileKeepEol $predefPath $predefXml $enc
	$modulesCreated += $predefPath
} elseif ($objType -eq 'ChartOfCalculationTypes' -and $def.predefined -and @($def.predefined).Count -gt 0) {
	Ensure-ExtDir
	$predefXml = Build-PredefinedCalcTypeXml @($def.predefined)
	$predefPath = Join-Path $extDir "Predefined.xml"
	Write-XmlFileKeepEol $predefPath $predefXml $enc
	$modulesCreated += $predefPath
} elseif ($predefRootByType.ContainsKey($objType) -and $def.predefined -and @($def.predefined).Count -gt 0) {
	Ensure-ExtDir
	$catCodeType = if ($def.codeType) { "$($def.codeType)" } else { 'String' }
	$predefXml = Build-PredefinedXml @($def.predefined) $predefRootByType[$objType] $catCodeType
	$predefPath = Join-Path $extDir "Predefined.xml"
	Write-XmlFileKeepEol $predefPath $predefXml $enc
	$modulesCreated += $predefPath
}

# Модули команд (Commands/<Имя>/Ext/CommandModule.bsl) — заготовка обработчика.
if ($commands -and $commands.Count -gt 0) {
	$cmdModuleStub = "&НаКлиенте`r`nПроцедура ОбработкаКоманды(ПараметрКоманды, ПараметрыВыполненияКоманды)`r`n`r`n`t// Вставьте обработчик команды.`r`n`r`nКонецПроцедуры`r`n"
	foreach ($cmd in $commands) {
		$cmdDir = Join-Path (Join-Path (Join-Path $objSubDir "Commands") $cmd.name) "Ext"
		if (-not (Test-Path $cmdDir)) { New-Item -ItemType Directory -Path $cmdDir -Force | Out-Null }
		$cmdModPath = Join-Path $cmdDir "CommandModule.bsl"
		[System.IO.File]::WriteAllText($cmdModPath, $cmdModuleStub, $enc)
		$modulesCreated += $cmdModPath
	}
}

# --- 17. Register in Configuration.xml ---

# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
# Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
# его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
# остаётся рабочим каталогом проекта.
# configSrc считается от каталога .v8-project.json, как задокументировано в
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
function Get-NewObjectPosition([string]$cfgDir) {
	try {
		if (-not $cfgDir) { $cfgDir = "." }
		$pj = Find-V8Project (Get-Location).Path
		if (-not $pj) { $pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir)) }
		if (-not $pj) { return "end" }
		$proj = Get-Content -Raw $pj | ConvertFrom-Json
		$projDir = [System.IO.Path]::GetDirectoryName($pj)
		$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
		if ($proj.databases) {
			foreach ($db in $proj.databases) {
				if ($db.configSrc -and $db.newObjectPosition) {
					$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
					if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
						if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
						return "end"
					}
				}
			}
		}
		if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
		return "end"
	} catch { return "end" }
}

# Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
# CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
# порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
# пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
# порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
# (в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
# без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
# Явно названный вид сортируется в любом случае.
# Реестр семьи: tests/skills/check-inline-drift.mjs.
function Test-OrderSensitiveType([string]$typeName) {
	return @("CommonAttribute", "Subsystem", "CommandGroup", "Language") -ccontains $typeName
}

# Порядок имён объектов метаданных, как в дереве Конфигуратора.
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
function Compare-MetadataNames([string]$a, [string]$b) {
	$keys = @("", "")
	$names = @($a, $b)
	for ($i = 0; $i -lt 2; $i++) {
		$sb = New-Object System.Text.StringBuilder
		foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
			if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
			if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
			elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
			else { [void]$sb.Append('0') }
			[void]$sb.Append($ch)
		}
		$keys[$i] = $sb.ToString()
	}
	$r = [string]::CompareOrdinal($keys[0], $keys[1])
	if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
	if ($r -lt 0) { return -1 }
	if ($r -gt 0) { return 1 }
	return 0
}

# Регистрация объекта в <ChildObjects> родительского XML. Общая реализация: эталон —
# meta-compile, копии — role-compile, xdto-compile. Реестр семьи: tests/skills/check-inline-drift.mjs.
# Возвращает исход: added | already | no-childobj | no-config.
# Канонический порядок видов в <ChildObjects> — эталон в docs/1c-configuration-spec.md,
# таблица «Порядок типов в ChildObjects». Нужен, чтобы новая группа вида вставала на своё
# место: иначе платформа переставит её при первой же выгрузке и даст диф на ровном месте.
# Реестр карт: tests/skills/check-type-maps.mjs.
$childObjectTypes = @(
	"Language","Subsystem","StyleItem","Style",
	"CommonPicture","SessionParameter","Role","CommonTemplate",
	"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
	"XDTOPackage","WebService","HTTPService","WSReference",
	"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
	"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
	"Constant","CommonForm","Catalog","Document",
	"DocumentNumerator","Sequence","DocumentJournal","Enum",
	"Report","DataProcessor","InformationRegister","AccumulationRegister",
	"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
	"ChartOfCalculationTypes","CalculationRegister",
	"BusinessProcess","Task","IntegrationService"
)

function Register-InChildObjects([string]$ParentXmlPath, [string]$ParentTag, [string]$ChildTag, [string]$ChildName) {
	if (-not (Test-Path $ParentXmlPath)) { return "no-config" }

	$doc = New-Object System.Xml.XmlDocument
	$doc.PreserveWhitespace = $true
	$doc.Load($ParentXmlPath)

	$nsMgr = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
	$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")

	$childObjects = $doc.SelectSingleNode("//md:$ParentTag/md:ChildObjects", $nsMgr)
	if (-not $childObjects) { return "no-childobj" }

	$existing = $childObjects.SelectNodes("md:$ChildTag", $nsMgr)
	foreach ($e in $existing) {
		if ($e.InnerText -eq $ChildName) { return "already" }
	}

	# Правка по сырому тексту, зеркально py-порту: сериализация DOM переписала бы файл целиком
	# (регистр encoding, `<a />` вместо `<a/>`), а текстовая вставка хранит его байт-в-байт —
	# дельта ровно в одну строку. Правим чужой файл, значит наследуем его стиль (#44/#46/#47).
	# DOM выше — только на чтение: найти ChildObjects и отсечь дубликат.
	$configContent = [System.IO.File]::ReadAllText($ParentXmlPath, (New-Object System.Text.UTF8Encoding($false)))
	$eol = if ($configContent.Contains("`r`n")) { "`r`n" } else { "`n" }
	$entry = "<$ChildTag>$(Esc-XmlText $ChildName)</$ChildTag>"
	$enc = New-Object System.Text.UTF8Encoding($true)

	$block = [regex]::Match($configContent, '(?s)<ChildObjects\s*>.*?</ChildObjects>')
	if (-not $block.Success) {
		# Самозакрытый <ChildObjects/> раскрываем первой записью
		$empty = [regex]::Match($configContent, '<ChildObjects\s*/>')
		if (-not $empty.Success) { return "no-childobj" }
		$replacement = "<ChildObjects>$eol`t`t`t$entry$eol`t`t</ChildObjects>"
		$newContent = $configContent.Substring(0, $empty.Index) + $replacement + $configContent.Substring($empty.Index + $empty.Length)
		[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
		return "added"
	}

	# byName: перед первым объектом того же вида, чьё имя больше нового.
	# Виды с осмысленным порядком в дереве пропускаем — см. Test-OrderSensitiveType.
	if (-not (Test-OrderSensitiveType $ChildTag) -and (Get-NewObjectPosition ([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetFullPath($ParentXmlPath)))) -eq "byName") {
		$lineRx = [regex]"(?m)^([ \t]*)<$ChildTag>([^<]*)</$ChildTag>"
		$m = $lineRx.Match($configContent, $block.Index, $block.Length)
		while ($m.Success) {
			if ((Compare-MetadataNames $m.Groups[2].Value $ChildName) -gt 0) {
				$newContent = $configContent.Substring(0, $m.Index) + $m.Groups[1].Value + $entry + $eol + $configContent.Substring($m.Index)
				[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
				return "added"
			}
			$m = $m.NextMatch()
		}
	}

	$closeSame = "</$ChildTag>"
	$blockEnd = $block.Index + $block.Length
	$lastSame = $configContent.LastIndexOf($closeSame, $blockEnd - 1, $block.Length, [System.StringComparison]::Ordinal)
	if ($lastSame -ge 0) {
		# После последнего объекта того же вида (группы по видам сохраняются)
		$insertAt = $lastSame + $closeSame.Length
		$newContent = $configContent.Substring(0, $insertAt) + "$eol`t`t`t$entry" + $configContent.Substring($insertAt)
	} else {
		# Группы своего вида ещё нет: ставим её в канонический порядок видов — перед первой
		# группой вида старше по $childObjectTypes. Дописать в конец блока нельзя: платформа
		# переставит группу при первой же выгрузке и даст диф на ровном месте.
		$ownIdx = $childObjectTypes.IndexOf($ChildTag)
		$anchor = $null
		if ($ownIdx -ge 0) {
			$typeRx = [regex]"(?m)^([ \t]*)<(\w+)>[^<]*</\2>"
			$tm = $typeRx.Match($configContent, $block.Index, $block.Length)
			while ($tm.Success) {
				$otherIdx = $childObjectTypes.IndexOf($tm.Groups[2].Value)
				if ($otherIdx -gt $ownIdx) { $anchor = $tm; break }
				$tm = $tm.NextMatch()
			}
		}
		if ($anchor) {
			$newContent = $configContent.Substring(0, $anchor.Index) + $anchor.Groups[1].Value + $entry + $eol + $configContent.Substring($anchor.Index)
		} else {
			# Видов старше в файле нет — новая строка перед </ChildObjects>,
			# отступ закрывающего тега переиспользуется
			$closeAt = $configContent.LastIndexOf("</ChildObjects>", $blockEnd - 1, $block.Length, [System.StringComparison]::Ordinal)
			$newContent = $configContent.Substring(0, $closeAt) + "`t$entry$eol`t`t" + $configContent.Substring($closeAt)
		}
	}
	[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
	return "added"
}

# XML tag name for Configuration.xml ChildObjects
$childTag = $objType

$configXmlPath = Join-Path $OutputDir "Configuration.xml"
$regResult = Register-InChildObjects $configXmlPath "Configuration" $childTag $objName

# --- 18. Summary ---

$attrCount = 0
$tsCount = 0
$dimCount = 0
$resCount = 0
$valCount = 0
$colCount = 0

if ($def.attributes) { $attrCount = @($def.attributes).Count }
if ($def.tabularSections) {
	if ($def.tabularSections -is [array] -or $def.tabularSections.GetType().Name -eq "Object[]") {
		$tsCount = @($def.tabularSections).Count
	} else {
		$tsCount = @($def.tabularSections.PSObject.Properties).Count
	}
}
if ($def.dimensions) { $dimCount = @($def.dimensions).Count }
if ($def.resources) { $resCount = @($def.resources).Count }
if ($def.values) { $valCount = @($def.values).Count }
if ($def.columns) { $colCount = @($def.columns).Count }

Write-Host "[OK] $objType '$objName' compiled"
Write-Host "     UUID: $uuid"
Write-Host "     File: $mainXmlPath"

$details = @()
if ($attrCount -gt 0) { $details += "Attributes: $attrCount" }
if ($tsCount -gt 0)   { $details += "TabularSections: $tsCount" }
if ($dimCount -gt 0)  { $details += "Dimensions: $dimCount" }
if ($resCount -gt 0)  { $details += "Resources: $resCount" }
if ($valCount -gt 0)  { $details += "Values: $valCount" }
if ($colCount -gt 0)  { $details += "Columns: $colCount" }

if ($details.Count -gt 0) {
	Write-Host "     $($details -join ', ')"
}

foreach ($mc in $modulesCreated) {
	Write-Host "     Module: $mc"
}

switch ($regResult) {
	"added"       { Write-Host "     Configuration.xml: <$childTag>$objName</$childTag> added to ChildObjects" }
	"already"     { Write-Host "     Configuration.xml: <$childTag>$objName</$childTag> already registered" }
	"no-childobj" { Write-Warning "Configuration.xml found but <ChildObjects> not found" }
	"no-config"   { Write-Host "     Configuration.xml: not found at $configXmlPath (register manually)" }
}

# Cross-reference hints
if ($script:stdAttrTailHint) {
	Write-Host "[HINT] $($script:stdAttrTailHint)"
}
if ($objType -eq "AccountingRegister" -and -not $def.chartOfAccounts) {
	Write-Host "[HINT] AccountingRegister requires ChartOfAccounts reference:"
	Write-Host "       /meta-edit -Operation modify-property -Value `"ChartOfAccounts=ChartOfAccounts.XXX`""
}
if ($objType -eq "CalculationRegister" -and -not $def.chartOfCalculationTypes) {
	Write-Host "[HINT] CalculationRegister requires ChartOfCalculationTypes reference:"
	Write-Host "       /meta-edit -Operation modify-property -Value `"ChartOfCalculationTypes=ChartOfCalculationTypes.XXX`""
}
if ($objType -eq "BusinessProcess" -and -not $def.task) {
	Write-Host "[HINT] BusinessProcess requires Task reference:"
	Write-Host "       /meta-edit -Operation modify-property -Value `"Task=Task.XXX`""
}
if ($objType -eq "ChartOfAccounts") {
	$maxExtDim = if ($null -ne $def.maxExtDimensionCount) { [int]$def.maxExtDimensionCount } else { 0 }
	if ($maxExtDim -gt 0 -and -not $def.extDimensionTypes) {
		Write-Host "[HINT] ChartOfAccounts with MaxExtDimensionCount>0 requires ExtDimensionTypes:"
		Write-Host "       /meta-edit -Operation modify-property -Value `"ExtDimensionTypes=ChartOfCharacteristicTypes.XXX`""
	}
}
