# form-compile v1.21 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
	[string]$JsonPath,

	[Parameter(Mandatory)]
	[string]$OutputPath,

	[switch]$FromObject,
	[string]$ObjectPath,
	[string]$Purpose,
	[string]$Preset = "erp-standard",
	[string]$EmitDsl
)

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

# ═══════════════════════════════════════════════════════════════════════════
# FROM-OBJECT MODE: functions for metadata parsing, presets, DSL generation
# ═══════════════════════════════════════════════════════════════════════════

function Parse-ObjectMeta([string]$ObjectPath) {
	$doc = New-Object System.Xml.XmlDocument
	$doc.PreserveWhitespace = $false
	$doc.Load($ObjectPath)

	$ns = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
	$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
	$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
	$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")

	# Detect object type from root child
	$metaRoot = $doc.SelectSingleNode("md:MetaDataObject", $ns)
	if (-not $metaRoot) { Write-Error "Not a 1C metadata XML: $ObjectPath"; exit 1 }
	$typeNode = $metaRoot.FirstChild
	$objType = $typeNode.LocalName  # "Document", "Catalog", etc.

	$propsNode = $typeNode.SelectSingleNode("md:Properties", $ns)
	$childObjs = $typeNode.SelectSingleNode("md:ChildObjects", $ns)

	# Name
	$objName = $propsNode.SelectSingleNode("md:Name", $ns).InnerText

	# Synonym (Russian)
	$synonym = $objName
	$synNode = $propsNode.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns)
	if ($synNode) { $synonym = $synNode.InnerText }

	# Helper: extract type string from md:Type
	$extractType = {
		param($typeParent)
		if (-not $typeParent) { return "string" }
		$types = @()
		foreach ($t in $typeParent.SelectNodes("v8:Type", $ns)) {
			$types += $t.InnerText
		}
		if ($types.Count -eq 0) { return "string" }
		return ($types -join " | ")
	}

	# Helper: check if type is a reference
	$isRefType = {
		param([string]$t)
		return ($t -match 'Ref\.' -or $t -match 'ссылка\.')
	}

	# Helper: extract field list from ChildObjects by tag name (Attribute, Dimension, Resource, AccountingFlag, ExtDimensionAccountingFlag)
	$extractFields = {
		param($parentNode, [string]$tagName)
		$result = @()
		if (-not $parentNode) { return $result }
		foreach ($fieldNode in $parentNode.SelectNodes("md:$tagName", $ns)) {
			$fp = $fieldNode.SelectSingleNode("md:Properties", $ns)
			$fName = $fp.SelectSingleNode("md:Name", $ns).InnerText
			$fSynNode = $fp.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns)
			$fSyn = if ($fSynNode) { $fSynNode.InnerText } else { $fName }
			$fTypeNode = $fp.SelectSingleNode("md:Type", $ns)
			$fType = & $extractType $fTypeNode
			$result += @{
				Name = $fName
				Synonym = $fSyn
				Type = $fType
				IsRef = (& $isRefType $fType)
			}
		}
		return $result
	}

	# Attributes
	$attributes = @(& $extractFields $childObjs "Attribute")

	# Tabular sections
	$tabularSections = @()
	if ($childObjs) {
		foreach ($tsNode in $childObjs.SelectNodes("md:TabularSection", $ns)) {
			$tsp = $tsNode.SelectSingleNode("md:Properties", $ns)
			$tsName = $tsp.SelectSingleNode("md:Name", $ns).InnerText
			$tsSynNode = $tsp.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns)
			$tsSyn = if ($tsSynNode) { $tsSynNode.InnerText } else { $tsName }
			$tsCo = $tsNode.SelectSingleNode("md:ChildObjects", $ns)
			$tsCols = @(& $extractFields $tsCo "Attribute")
			$tabularSections += @{
				Name = $tsName
				Synonym = $tsSyn
				Columns = $tsCols
			}
		}
	}

	$meta = @{
		Type = $objType
		Name = $objName
		Synonym = $synonym
		Attributes = $attributes
		TabularSections = $tabularSections
	}

	# Type-specific properties
	switch ($objType) {
		"Document" {
			$ntNode = $propsNode.SelectSingleNode("md:NumberType", $ns)
			$meta.NumberType = if ($ntNode) { $ntNode.InnerText } else { "String" }
		}
		"Catalog" {
			$clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns)
			$meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 }
			$dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns)
			$meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 }
			$hiNode = $propsNode.SelectSingleNode("md:Hierarchical", $ns)
			$meta.Hierarchical = ($hiNode -and $hiNode.InnerText -eq "true")
			$htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns)
			$meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" }
			# Owners
			$owners = @()
			foreach ($ow in $propsNode.SelectNodes("md:Owners/xr:Item", $ns)) {
				$owners += $ow.InnerText
			}
			$meta.Owners = $owners
		}
		"InformationRegister" {
			$meta.Dimensions = @(& $extractFields $childObjs "Dimension")
			$meta.Resources  = @(& $extractFields $childObjs "Resource")
			$prdNode = $propsNode.SelectSingleNode("md:InformationRegisterPeriodicity", $ns)
			$meta.Periodicity = if ($prdNode) { $prdNode.InnerText } else { "Nonperiodical" }
			$wmNode = $propsNode.SelectSingleNode("md:WriteMode", $ns)
			$meta.WriteMode = if ($wmNode) { $wmNode.InnerText } else { "Independent" }
		}
		"AccumulationRegister" {
			$meta.Dimensions = @(& $extractFields $childObjs "Dimension")
			$meta.Resources  = @(& $extractFields $childObjs "Resource")
			$rtNode = $propsNode.SelectSingleNode("md:RegisterType", $ns)
			$meta.RegisterType = if ($rtNode) { $rtNode.InnerText } else { "Balances" }
		}
		"ChartOfCharacteristicTypes" {
			$clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns)
			$meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 }
			$dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns)
			$meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 }
			$hiNode = $propsNode.SelectSingleNode("md:Hierarchical", $ns)
			$meta.Hierarchical = ($hiNode -and $hiNode.InnerText -eq "true")
			$htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns)
			$meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" }
			$owners = @()
			foreach ($ow in $propsNode.SelectNodes("md:Owners/xr:Item", $ns)) {
				$owners += $ow.InnerText
			}
			$meta.Owners = $owners
			$meta.HasValueType = $true
		}
		"ExchangePlan" {
			$clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns)
			$meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 }
			$dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns)
			$meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 }
			$meta.Hierarchical = $false
			$meta.HierarchyType = $null
			$meta.Owners = @()
		}
		"ChartOfAccounts" {
			$clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns)
			$meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 }
			$dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns)
			$meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 }
			$meta.Hierarchical = $true
			$htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns)
			$meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" }
			$meta.Owners = @()
			$maxEdNode = $propsNode.SelectSingleNode("md:MaxExtDimensionCount", $ns)
			$meta.MaxExtDimensionCount = if ($maxEdNode) { [int]$maxEdNode.InnerText } else { 0 }
			$meta.AccountingFlags = @(& $extractFields $childObjs "AccountingFlag")
			$meta.ExtDimensionAccountingFlags = @(& $extractFields $childObjs "ExtDimensionAccountingFlag")
		}
	}

	return $meta
}

function Load-Preset([string]$PresetName, [string]$ScriptDir) {
	# Hardcoded defaults (ERP-oriented)
	$defaults = @{
		"document.item" = @{
			header = @{ position = "insidePage"; layout = "2col"; distribute = "even"; dateTitle = "от" }
			footer = @{ fields = @("Комментарий"); position = "insidePage" }
			tabularSections = @{ container = "pages"; exclude = @("ДополнительныеРеквизиты"); lineNumber = $true }
			additional = @{ position = "page"; layout = "2col"; bspGroup = $true }
			fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } }
			commandBar = "auto"
			properties = @{ autoTitle = $false }
		}
		"document.list" = @{
			columns = "all"; columnType = "labelField"; hiddenRef = $true
			tableCommandBar = "none"; commandBar = "auto"
			properties = @{}
		}
		"document.choice" = @{
			basedOn = "document.list"
			properties = @{ windowOpeningMode = "LockOwnerWindow" }
		}
		"catalog.item" = @{
			header = @{ layout = "1col"; distribute = "left" }
			codeDescription = @{ layout = "horizontal"; order = "descriptionFirst" }
			parent = @{ title = "Входит в группу"; position = "afterCodeDescription" }
			owner = @{ readOnly = $true; position = "first" }
			tabularSections = @{ container = "inline"; exclude = @("ДополнительныеРеквизиты","Представления"); lineNumber = $true }
			footer = @{ fields = @(); position = "none" }
			additional = @{ position = "none"; bspGroup = $true }
			fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } }
			commandBar = "auto"
			properties = @{}
		}
		"catalog.folder" = @{
			parent = @{ title = "Входит в группу" }
			properties = @{ windowOpeningMode = "LockOwnerWindow" }
		}
		"catalog.list" = @{
			columns = "all"; columnType = "labelField"; hiddenRef = $true
			tableCommandBar = "none"; commandBar = "auto"
			properties = @{}
		}
		"catalog.choice" = @{
			basedOn = "catalog.list"; choiceMode = $true
			properties = @{ windowOpeningMode = "LockOwnerWindow" }
		}
		# ─── Register defaults ───
		"informationRegister.record" = @{
			fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } }
			properties = @{ windowOpeningMode = "LockOwnerWindow" }
		}
		"informationRegister.list" = @{
			columns = "all"; columnType = "labelField"
			tableCommandBar = "none"; commandBar = "auto"
			properties = @{}
		}
		"accumulationRegister.list" = @{
			columns = "all"; columnType = "labelField"
			tableCommandBar = "none"; commandBar = "auto"
			properties = @{}
		}
		# ─── Catalog-like type defaults ───
		"chartOfCharacteristicTypes.item"   = @{ basedOn = "catalog.item" }
		"chartOfCharacteristicTypes.folder" = @{ basedOn = "catalog.folder" }
		"chartOfCharacteristicTypes.list"   = @{ basedOn = "catalog.list" }
		"chartOfCharacteristicTypes.choice" = @{ basedOn = "catalog.choice" }
		"exchangePlan.item"   = @{ basedOn = "catalog.item" }
		"exchangePlan.list"   = @{ basedOn = "catalog.list" }
		"exchangePlan.choice" = @{ basedOn = "catalog.choice" }
		# ─── ChartOfAccounts defaults ───
		"chartOfAccounts.item" = @{
			parent = @{ title = "Подчинен счету" }
			fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } }
			properties = @{}
		}
		"chartOfAccounts.folder" = @{
			parent = @{ title = "Подчинен счету" }
			properties = @{ windowOpeningMode = "LockOwnerWindow" }
		}
		"chartOfAccounts.list"   = @{ basedOn = "catalog.list" }
		"chartOfAccounts.choice" = @{ basedOn = "catalog.choice" }
	}

	# Deep merge helper
	$deepMerge = {
		param($base, $overlay)
		if (-not $overlay) { return $base }
		if (-not $base) { return $overlay }
		$result = @{}
		foreach ($k in $base.Keys) { $result[$k] = $base[$k] }
		foreach ($k in $overlay.Keys) {
			if ($result.ContainsKey($k) -and $result[$k] -is [hashtable] -and $overlay[$k] -is [hashtable]) {
				$result[$k] = & $deepMerge $result[$k] $overlay[$k]
			} else {
				$result[$k] = $overlay[$k]
			}
		}
		return $result
	}

	# Try built-in preset
	$presetDir = Join-Path (Split-Path $ScriptDir -Parent) "presets"
	$builtInPath = Join-Path $presetDir "$PresetName.json"
	if (Test-Path $builtInPath) {
		$presetJson = Get-Content -Raw -Encoding UTF8 $builtInPath | ConvertFrom-Json
		# Convert PSCustomObject to hashtable recursively
		$toHash = {
			param($obj)
			if ($obj -is [System.Management.Automation.PSCustomObject]) {
				$h = @{}
				foreach ($p in $obj.PSObject.Properties) {
					$h[$p.Name] = & $toHash $p.Value
				}
				return $h
			}
			if ($obj -is [System.Object[]]) {
				return @($obj | ForEach-Object { & $toHash $_ })
			}
			return $obj
		}
		$presetHash = & $toHash $presetJson
		foreach ($k in @($presetHash.Keys)) {
			$defaults[$k] = & $deepMerge $defaults[$k] $presetHash[$k]
		}
	}

	# Try project-level preset (scan up from output path)
	$scanDir = [System.IO.Path]::GetDirectoryName($script:outPathResolved)
	while ($scanDir) {
		$projPreset = Join-Path (Join-Path (Join-Path (Join-Path $scanDir "presets") "skills") "form") "$PresetName.json"
		if (Test-Path $projPreset) {
			$projJson = Get-Content -Raw -Encoding UTF8 $projPreset | ConvertFrom-Json
			$projHash = & $toHash $projJson
			foreach ($k in @($projHash.Keys)) {
				$defaults[$k] = & $deepMerge $defaults[$k] $projHash[$k]
			}
			break
		}
		$parentDir = Split-Path $scanDir -Parent
		if ($parentDir -eq $scanDir) { break }
		$scanDir = $parentDir
	}

	# Resolve basedOn references
	foreach ($k in @($defaults.Keys)) {
		$sect = $defaults[$k]
		if ($sect -is [hashtable] -and $sect.ContainsKey("basedOn")) {
			$baseName = $sect["basedOn"]
			if ($defaults.ContainsKey($baseName)) {
				$merged = & $deepMerge $defaults[$baseName] $sect
				$merged.Remove("basedOn")
				$defaults[$k] = $merged
			}
		}
	}

	return $defaults
}

# --- Helper: build a field element DSL entry ---
# Non-displayable types — cannot be bound to form elements
$script:nonDisplayableTypes = @('v8:ValueStorage', 'ValueStorage', 'ХранилищеЗначения')

function Test-DisplayableType([string]$typeStr) {
	foreach ($nd in $script:nonDisplayableTypes) {
		if ($typeStr -match [regex]::Escape($nd)) { return $false }
	}
	return $true
}

function New-FieldElement {
	param([string]$attrName, [string]$dataPath, [string]$attrType, [hashtable]$fieldDefaults, [hashtable]$extraProps)

	$isRef = ($attrType -match 'Ref\.')
	$isBool = ($attrType -match '^\s*xs:boolean\s*$' -or $attrType -eq 'boolean' -or $attrType -match 'Boolean')

	# Determine element type
	$elType = "input"
	if ($isBool -and $fieldDefaults -and $fieldDefaults.boolean -and $fieldDefaults.boolean.element -eq "check") {
		$elType = "check"
	}

	$el = [ordered]@{ $elType = $attrName; path = $dataPath }

	# Apply ref defaults
	if ($isRef -and $fieldDefaults -and $fieldDefaults.ref) {
		if ($fieldDefaults.ref.choiceButton -eq $true) { $el["choiceButton"] = $true }
	}

	# Extra props
	if ($extraProps) {
		foreach ($k in $extraProps.Keys) { $el[$k] = $extraProps[$k] }
	}

	return $el
}

# --- Catalog DSL generators ---
function Generate-CatalogDSL {
	param($meta, [hashtable]$presetData, [string]$purpose)

	$purposeKey = "catalog.$($purpose.ToLower())"
	$p = if ($presetData.ContainsKey($purposeKey)) { $presetData[$purposeKey] } else { @{} }
	$fd = if ($p.ContainsKey("fieldDefaults")) { $p.fieldDefaults } else { @{} }

	switch ($purpose) {
		"Folder" { return Generate-CatalogFolderDSL $meta $p }
		"List"   { return Generate-CatalogListDSL $meta $p }
		"Choice" { return Generate-CatalogChoiceDSL $meta $p $presetData }
		"Item"   { return Generate-CatalogItemDSL $meta $p $fd }
	}
}

function Generate-CatalogFolderDSL($meta, [hashtable]$p) {
	$elements = @()
	# Code (if CodeLength > 0)
	if ($meta.CodeLength -gt 0) {
		$elements += [ordered]@{ input = "Код"; path = "Объект.Code" }
	}
	# Description
	$elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" }
	# Parent
	$parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { $null }
	$parentEl = [ordered]@{ input = "Родитель"; path = "Объект.Parent" }
	if ($parentTitle) { $parentEl["title"] = $parentTitle }
	$elements += $parentEl

	$props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" }
	if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } }

	$formProps = [ordered]@{ useForFoldersAndItems = "Folders" }
	foreach ($k in $props.Keys) { $formProps[$k] = $props[$k] }

	return [ordered]@{
		title = $meta.Synonym
		properties = $formProps
		elements = $elements
		attributes = @(
			[ordered]@{ name = "Объект"; type = "CatalogObject.$($meta.Name)"; main = $true }
		)
	}
}

function Generate-CatalogListDSL($meta, [hashtable]$p) {
	# Columns
	$columns = @()
	# Description always first
	$columns += [ordered]@{ labelField = "Наименование"; path = "Список.Description" }
	# Code if present
	if ($meta.CodeLength -gt 0) {
		$columns += [ordered]@{ labelField = "Код"; path = "Список.Code" }
	}
	# Custom attributes
	foreach ($attr in $meta.Attributes) {
		if (-not (Test-DisplayableType $attr.Type)) { continue }
		$columns += [ordered]@{ labelField = $attr.Name; path = "Список.$($attr.Name)" }
	}
	# Hidden ref
	if (-not $p.ContainsKey("hiddenRef") -or $p.hiddenRef -eq $true) {
		$columns += [ordered]@{ labelField = "Ссылка"; path = "Список.Ref"; userVisible = $false }
	}

	$tableEl = [ordered]@{
		table = "Список"; path = "Список"
		rowPictureDataPath = "Список.DefaultPicture"
		commandBarLocation = "None"
		tableAutofill = $false
		columns = $columns
	}
	# Hierarchical properties
	if ($meta.Hierarchical) {
		$tableEl["initialTreeView"] = "ExpandTopLevel"
		$tableEl["enableStartDrag"] = $true
		$tableEl["enableDrag"] = $true
	}

	$formProps = [ordered]@{}
	if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } }

	return [ordered]@{
		title = $meta.Synonym
		properties = $formProps
		elements = @($tableEl)
		attributes = @(
			[ordered]@{
				name = "Список"; type = "DynamicList"; main = $true
				settings = [ordered]@{ mainTable = "Catalog.$($meta.Name)"; dynamicDataRead = $true }
			}
		)
	}
}

function Generate-CatalogChoiceDSL($meta, [hashtable]$p, [hashtable]$presetData) {
	# Start from list
	$listKey = "catalog.list"
	$lp = if ($presetData.ContainsKey($listKey)) { $presetData[$listKey] } else { @{} }
	$dsl = Generate-CatalogListDSL $meta $lp

	# Add choice-specific properties
	$dsl.properties["windowOpeningMode"] = "LockOwnerWindow"
	if ($p.properties) { foreach ($k in $p.properties.Keys) { $dsl.properties[$k] = $p.properties[$k] } }

	# Set ChoiceMode on table
	$dsl.elements[0]["choiceMode"] = $true

	return $dsl
}

function Generate-CatalogItemDSL($meta, [hashtable]$p, [hashtable]$fd) {
	$headerChildren = @()

	# Owner (if subordinate)
	if ($meta.Owners -and $meta.Owners.Count -gt 0) {
		$ownerEl = [ordered]@{ input = "Владелец"; path = "Объект.Owner"; readOnly = $true }
		$headerChildren += $ownerEl
	}

	# Code + Description
	$cdLayout = if ($p.codeDescription -and $p.codeDescription.layout) { $p.codeDescription.layout } else { "horizontal" }
	$cdOrder = if ($p.codeDescription -and $p.codeDescription.order) { $p.codeDescription.order } else { "descriptionFirst" }
	$hasCode = ($meta.CodeLength -gt 0)

	if ($cdLayout -eq "horizontal" -and $hasCode) {
		$cdChildren = @()
		$descEl = [ordered]@{ input = "Наименование"; path = "Объект.Description" }
		$codeEl = [ordered]@{ input = "Код"; path = "Объект.Code" }
		if ($cdOrder -eq "descriptionFirst") {
			$cdChildren = @($descEl, $codeEl)
		} else {
			$cdChildren = @($codeEl, $descEl)
		}
		$headerChildren += [ordered]@{
			group = "horizontal"; name = "ГруппаКодНаименование"; showTitle = $false
			representation = "none"; children = $cdChildren
		}
	} else {
		# Vertical or no code
		$headerChildren += [ordered]@{ input = "Наименование"; path = "Объект.Description" }
		if ($hasCode) {
			$headerChildren += [ordered]@{ input = "Код"; path = "Объект.Code" }
		}
	}

	# Parent (for hierarchical catalogs)
	$parentPos = if ($p.parent -and $p.parent.position) { $p.parent.position } else { "afterCodeDescription" }
	$parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { $null }
	if ($meta.Hierarchical) {
		$parentEl = [ordered]@{ input = "Родитель"; path = "Объект.Parent" }
		if ($parentTitle) { $parentEl["title"] = $parentTitle }
		if ($parentPos -eq "beforeCodeDescription") {
			# Insert before Code/Description (after Owner if present)
			$insertIdx = if ($meta.Owners -and $meta.Owners.Count -gt 0) { 1 } else { 0 }
			$newChildren = @()
			for ($i = 0; $i -lt $headerChildren.Count; $i++) {
				if ($i -eq $insertIdx) { $newChildren += $parentEl }
				$newChildren += $headerChildren[$i]
			}
			$headerChildren = $newChildren
		} else {
			# afterCodeDescription (default)
			$headerChildren += $parentEl
		}
	}

	# Custom attributes → header
	$footerFieldNames = @()
	if ($p.footer -and $p.footer.fields) { $footerFieldNames = @($p.footer.fields) }

	foreach ($attr in $meta.Attributes) {
		if ($footerFieldNames -contains $attr.Name) { continue }
		if (-not (Test-DisplayableType $attr.Type)) { continue }
		$headerChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{})
	}

	# Build root elements
	$rootElements = @()

	# ГруппаШапка
	$rootElements += [ordered]@{
		group = "vertical"; name = "ГруппаШапка"; showTitle = $false
		representation = "none"; children = $headerChildren
	}

	# Tabular sections
	$tsExclude = @("ДополнительныеРеквизиты", "Представления")
	if ($p.tabularSections -and $p.tabularSections.exclude) { $tsExclude = @($p.tabularSections.exclude) }
	$tsLineNumber = if ($p.tabularSections -and $null -ne $p.tabularSections.lineNumber) { $p.tabularSections.lineNumber } else { $true }
	$tsContainer = if ($p.tabularSections -and $p.tabularSections.container) { $p.tabularSections.container } else { "inline" }

	$visibleTS = @()
	foreach ($ts in $meta.TabularSections) {
		if ($tsExclude -contains $ts.Name) { continue }
		$visibleTS += $ts
	}

	foreach ($ts in $visibleTS) {
		$tsCols = @()
		if ($tsLineNumber) {
			$tsCols += [ordered]@{ labelField = "$($ts.Name)НомерСтроки"; path = "Объект.$($ts.Name).LineNumber" }
		}
		foreach ($col in $ts.Columns) {
			$colEl = New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd -extraProps @{}
			$tsCols += $colEl
		}
		$tableEl = [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols }
		$rootElements += $tableEl
	}

	# Footer fields
	foreach ($fn in $footerFieldNames) {
		$fAttr = $meta.Attributes | Where-Object { $_.Name -eq $fn }
		if ($fAttr) {
			$rootElements += (New-FieldElement -attrName $fAttr.Name -dataPath "Объект.$($fAttr.Name)" -attrType $fAttr.Type -fieldDefaults $fd -extraProps @{})
		}
	}

	# BSP group
	$bspGroup = if ($p.additional -and $null -ne $p.additional.bspGroup) { $p.additional.bspGroup } else { $true }
	if ($bspGroup) {
		$rootElements += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" }
	}

	# Properties
	$formProps = [ordered]@{}
	if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } }
	# UseForFoldersAndItems
	if ($meta.Hierarchical -and $meta.HierarchyType -eq "HierarchyFoldersAndItems") {
		$formProps["useForFoldersAndItems"] = "Items"
	}

	return [ordered]@{
		title = $meta.Synonym
		properties = $formProps
		elements = $rootElements
		attributes = @(
			[ordered]@{ name = "Объект"; type = "CatalogObject.$($meta.Name)"; main = $true }
		)
	}
}

# --- Document DSL generators ---
function Generate-DocumentDSL {
	param($meta, [hashtable]$presetData, [string]$purpose)

	$purposeKey = "document.$($purpose.ToLower())"
	$p = if ($presetData.ContainsKey($purposeKey)) { $presetData[$purposeKey] } else { @{} }
	$fd = if ($p.ContainsKey("fieldDefaults")) { $p.fieldDefaults } else { @{} }

	switch ($purpose) {
		"List"   { return Generate-DocumentListDSL $meta $p }
		"Choice" { return Generate-DocumentChoiceDSL $meta $p $presetData }
		"Item"   { return Generate-DocumentItemDSL $meta $p $fd }
	}
}

function Generate-DocumentListDSL($meta, [hashtable]$p) {
	$columns = @()
	# Standard columns: Number + Date
	$columns += [ordered]@{ labelField = "Номер"; path = "Список.Number" }
	$columns += [ordered]@{ labelField = "Дата"; path = "Список.Date" }
	# All custom attributes as labelField
	foreach ($attr in $meta.Attributes) {
		if (-not (Test-DisplayableType $attr.Type)) { continue }
		$columns += [ordered]@{ labelField = $attr.Name; path = "Список.$($attr.Name)" }
	}
	# Hidden ref
	if (-not $p.ContainsKey("hiddenRef") -or $p.hiddenRef -eq $true) {
		$columns += [ordered]@{ labelField = "Ссылка"; path = "Список.Ref"; userVisible = $false }
	}

	$tableEl = [ordered]@{
		table = "Список"; path = "Список"
		rowPictureDataPath = "Список.DefaultPicture"
		commandBarLocation = "None"
		tableAutofill = $false
		columns = $columns
	}

	$formProps = [ordered]@{}
	if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } }

	return [ordered]@{
		title = $meta.Synonym
		properties = $formProps
		elements = @($tableEl)
		attributes = @(
			[ordered]@{
				name = "Список"; type = "DynamicList"; main = $true
				settings = [ordered]@{ mainTable = "Document.$($meta.Name)"; dynamicDataRead = $true }
			}
		)
	}
}

function Generate-DocumentChoiceDSL($meta, [hashtable]$p, [hashtable]$presetData) {
	$listKey = "document.list"
	$lp = if ($presetData.ContainsKey($listKey)) { $presetData[$listKey] } else { @{} }
	$dsl = Generate-DocumentListDSL $meta $lp

	$dsl.properties["windowOpeningMode"] = "LockOwnerWindow"
	if ($p.properties) { foreach ($k in $p.properties.Keys) { $dsl.properties[$k] = $p.properties[$k] } }

	return $dsl
}

function Generate-DocumentItemDSL($meta, [hashtable]$p, [hashtable]$fd) {
	$headerPos = if ($p.header -and $p.header.position) { $p.header.position } else { "insidePage" }
	$headerLayout = if ($p.header -and $p.header.layout) { $p.header.layout } else { "2col" }
	$headerDistribute = if ($p.header -and $p.header.distribute) { $p.header.distribute } else { "even" }
	$dateTitle = if ($p.header -and $p.header.dateTitle) { $p.header.dateTitle } else { "от" }

	$footerFields = @()
	if ($p.footer -and $p.footer.fields) { $footerFields = @($p.footer.fields) }
	$footerPos = if ($p.footer -and $p.footer.position) { $p.footer.position } else { "insidePage" }

	$addPos = if ($p.additional -and $p.additional.position) { $p.additional.position } else { "page" }
	$addLayout = if ($p.additional -and $p.additional.layout) { $p.additional.layout } else { "2col" }
	$addBspGroup = if ($p.additional -and $null -ne $p.additional.bspGroup) { $p.additional.bspGroup } else { $true }
	$addLeft = @(); $addRight = @()
	if ($p.additional -and $p.additional.left) { $addLeft = @($p.additional.left) }
	if ($p.additional -and $p.additional.right) { $addRight = @($p.additional.right) }

	$headerRight = @()
	if ($p.header -and $p.header.right) { $headerRight = @($p.header.right) }

	$tsExclude = @("ДополнительныеРеквизиты")
	if ($p.tabularSections -and $p.tabularSections.exclude) { $tsExclude = @($p.tabularSections.exclude) }
	$tsLineNumber = if ($p.tabularSections -and $null -ne $p.tabularSections.lineNumber) { $p.tabularSections.lineNumber } else { $true }

	# Classify attributes
	$claimed = @{}
	foreach ($fn in $footerFields) { $claimed[$fn] = "footer" }
	foreach ($fn in $headerRight) { $claimed[$fn] = "header.right" }
	foreach ($fn in $addLeft) { $claimed[$fn] = "additional.left" }
	foreach ($fn in $addRight) { $claimed[$fn] = "additional.right" }

	$unclaimed = @()
	foreach ($attr in $meta.Attributes) {
		if (-not $claimed.ContainsKey($attr.Name) -and (Test-DisplayableType $attr.Type)) { $unclaimed += $attr }
	}

	# Distribute unclaimed
	$leftAttrs = @(); $rightExtraAttrs = @()
	switch ($headerDistribute) {
		"left"  { $leftAttrs = $unclaimed }
		"right" { $rightExtraAttrs = $unclaimed }
		default { # "even"
			$half = [Math]::Ceiling($unclaimed.Count / 2)
			for ($i = 0; $i -lt $unclaimed.Count; $i++) {
				if ($i -lt $half) { $leftAttrs += $unclaimed[$i] }
				else { $rightExtraAttrs += $unclaimed[$i] }
			}
		}
	}

	# Build ГруппаНомерДата
	$numDateChildren = @(
		[ordered]@{ input = "Номер"; path = "Объект.Number"; autoMaxWidth = $false; width = 9 }
		[ordered]@{ input = "Дата"; path = "Объект.Date"; title = $dateTitle }
	)
	$numDateGroup = [ordered]@{
		group = "horizontal"; name = "ГруппаНомерДата"; showTitle = $false; children = $numDateChildren
	}

	# Build left column
	$leftChildren = @($numDateGroup)
	foreach ($attr in $leftAttrs) {
		$leftChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{})
	}

	# Build right column
	$rightChildren = @()
	foreach ($rn in $headerRight) {
		$rAttr = $meta.Attributes | Where-Object { $_.Name -eq $rn }
		if ($rAttr) {
			$rightChildren += (New-FieldElement -attrName $rAttr.Name -dataPath "Объект.$($rAttr.Name)" -attrType $rAttr.Type -fieldDefaults $fd -extraProps @{})
		}
	}
	foreach ($attr in $rightExtraAttrs) {
		$rightChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{})
	}

	# Header group
	$headerGroup = $null
	if ($headerLayout -eq "2col" -and $rightChildren.Count -gt 0) {
		$headerGroup = [ordered]@{
			group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none"
			children = @(
				[ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $leftChildren }
				[ordered]@{ group = "vertical"; name = "ГруппаШапкаПраво"; showTitle = $false; children = $rightChildren }
			)
		}
	} else {
		# 1col or no right items
		$allHeaderFields = $leftChildren + $rightChildren
		$headerGroup = [ordered]@{
			group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none"
			children = @(
				[ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $allHeaderFields }
			)
		}
	}

	# Footer elements
	$footerElements = @()
	foreach ($fn in $footerFields) {
		$fAttr = $meta.Attributes | Where-Object { $_.Name -eq $fn }
		if ($fAttr -and (Test-DisplayableType $fAttr.Type)) {
			$footerElements += (New-FieldElement -attrName $fAttr.Name -dataPath "Объект.$($fAttr.Name)" -attrType $fAttr.Type -fieldDefaults $fd -extraProps @{})
		}
	}

	# Visible tabular sections
	$visibleTS = @()
	foreach ($ts in $meta.TabularSections) {
		if ($tsExclude -contains $ts.Name) { continue }
		$visibleTS += $ts
	}

	# Additional page content
	$additionalPage = $null
	if ($addPos -eq "page") {
		$addLeftEls = @(); $addRightEls = @()
		foreach ($aln in $addLeft) {
			$alAttr = $meta.Attributes | Where-Object { $_.Name -eq $aln }
			if ($alAttr) {
				$addLeftEls += (New-FieldElement -attrName $alAttr.Name -dataPath "Объект.$($alAttr.Name)" -attrType $alAttr.Type -fieldDefaults $fd -extraProps @{})
			}
		}
		foreach ($arn in $addRight) {
			$arAttr = $meta.Attributes | Where-Object { $_.Name -eq $arn }
			if ($arAttr) {
				$addRightEls += (New-FieldElement -attrName $arAttr.Name -dataPath "Объект.$($arAttr.Name)" -attrType $arAttr.Type -fieldDefaults $fd -extraProps @{})
			}
		}
		$addPageChildren = @()
		if ($addLayout -eq "2col") {
			$addPageChildren += [ordered]@{
				group = "horizontal"; name = "ГруппаПараметры"; showTitle = $false
				children = @(
					[ordered]@{ group = "vertical"; name = "ГруппаПараметрыЛево"; showTitle = $false; children = $addLeftEls }
					[ordered]@{ group = "vertical"; name = "ГруппаПараметрыПраво"; showTitle = $false; children = $addRightEls }
				)
			}
		} else {
			$addPageChildren += @($addLeftEls + $addRightEls)
		}
		if ($addBspGroup) {
			$addPageChildren += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" }
		}
		$additionalPage = [ordered]@{ page = "ГруппаДополнительно"; title = "Дополнительно"; children = $addPageChildren }
	}

	# Build TS page elements
	$tsPages = @()
	foreach ($ts in $visibleTS) {
		$tsCols = @()
		if ($tsLineNumber) {
			$tsCols += [ordered]@{ labelField = "$($ts.Name)НомерСтроки"; path = "Объект.$($ts.Name).LineNumber" }
		}
		foreach ($col in $ts.Columns) {
			$tsCols += (New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd -extraProps @{})
		}
		$tsPages += [ordered]@{
			page = "Группа$($ts.Name)"; title = $ts.Synonym
			children = @(
				[ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols }
			)
		}
	}

	# Assemble root elements
	$rootElements = @()

	if ($visibleTS.Count -eq 0) {
		# Simple form — no Pages
		$rootElements += $headerGroup
		if ($footerElements.Count -gt 0) { $rootElements += $footerElements }
		if ($addBspGroup -and $addPos -ne "none") {
			$rootElements += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" }
		}
	} else {
		# Pages form
		if ($headerPos -eq "abovePages") {
			$rootElements += $headerGroup
			$pagesChildren = @()
			$pagesChildren += $tsPages
			if ($additionalPage) { $pagesChildren += $additionalPage }
			$rootElements += [ordered]@{ pages = "ГруппаСтраницы"; children = $pagesChildren }
		} else {
			# insidePage (default)
			$osnovnoeChildren = @($headerGroup)
			if ($footerPos -eq "insidePage" -and $footerElements.Count -gt 0) {
				$osnovnoeChildren += $footerElements
			}
			$pagesChildren = @()
			$pagesChildren += [ordered]@{ page = "ГруппаОсновное"; title = "Основное"; children = $osnovnoeChildren }
			$pagesChildren += $tsPages
			if ($additionalPage) { $pagesChildren += $additionalPage }
			$rootElements += [ordered]@{ pages = "ГруппаСтраницы"; children = $pagesChildren }
		}

		# Footer below pages
		if ($footerPos -eq "belowPages" -and $footerElements.Count -gt 0) {
			$rootElements += $footerElements
		}
	}

	# Properties
	$formProps = [ordered]@{ autoTitle = $false }
	if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } }

	return [ordered]@{
		title = $meta.Synonym
		properties = $formProps
		elements = $rootElements
		attributes = @(
			[ordered]@{ name = "Объект"; type = "DocumentObject.$($meta.Name)"; main = $true }
		)
	}
}

# ─── InformationRegister ──────────────────────────────────────────────────

function Generate-InformationRegisterDSL {
	param($meta, [hashtable]$presetData, [string]$purpose)
	$pKey = "informationRegister.$($purpose.ToLower())"
	$p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} }
	$fd = if ($p.fieldDefaults) { $p.fieldDefaults } else { @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } }
	switch ($purpose) {
		"Record" { return Generate-InformationRegisterRecordDSL $meta $p $fd }
		"List"   { return Generate-InformationRegisterListDSL $meta $p }
	}
}

function Generate-InformationRegisterRecordDSL($meta, [hashtable]$p, [hashtable]$fd) {
	$elements = @()
	$isPeriodic = $meta.Periodicity -and $meta.Periodicity -ne "Nonperiodical"

	# Period first (if periodic)
	if ($isPeriodic) {
		$elements += [ordered]@{ input = "Период"; path = "Запись.Period" }
	}
	# Dimensions
	foreach ($dim in $meta.Dimensions) {
		if (-not (Test-DisplayableType $dim.Type)) { continue }
		$elements += (New-FieldElement -attrName $dim.Name -dataPath "Запись.$($dim.Name)" -attrType $dim.Type -fieldDefaults $fd)
	}
	# Resources
	foreach ($res in $meta.Resources) {
		if (-not (Test-DisplayableType $res.Type)) { continue }
		$elements += (New-FieldElement -attrName $res.Name -dataPath "Запись.$($res.Name)" -attrType $res.Type -fieldDefaults $fd)
	}
	# Attributes
	foreach ($attr in $meta.Attributes) {
		if (-not (Test-DisplayableType $attr.Type)) { continue }
		$elements += (New-FieldElement -attrName $attr.Name -dataPath "Запись.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd)
	}

	$props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" }
	if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } }

	return [ordered]@{
		title = $meta.Synonym
		properties = $props
		elements = $elements
		attributes = @(
			@{ name = "Запись"; type = "InformationRegisterRecordManager.$($meta.Name)"; main = $true; savedData = $true }
		)
	}
}

function Generate-InformationRegisterListDSL($meta, [hashtable]$p) {
	$isPeriodic = $meta.Periodicity -and $meta.Periodicity -ne "Nonperiodical"
	$isRecorderSubordinate = $meta.WriteMode -eq "RecorderSubordinate"

	$columns = @()
	# Period
	if ($isPeriodic) {
		$columns += [ordered]@{ labelField = "Период"; path = "Список.Period" }
	}
	# Recorder/LineNumber for subordinate registers
	if ($isRecorderSubordinate) {
		$columns += [ordered]@{ labelField = "Регистратор"; path = "Список.Recorder" }
		$columns += [ordered]@{ labelField = "НомерСтроки"; path = "Список.LineNumber" }
	}
	# Dimensions
	foreach ($dim in $meta.Dimensions) {
		if (-not (Test-DisplayableType $dim.Type)) { continue }
		$columns += [ordered]@{ labelField = $dim.Name; path = "Список.$($dim.Name)" }
	}
	# Resources
	foreach ($res in $meta.Resources) {
		if (-not (Test-DisplayableType $res.Type)) { continue }
		$elKey = "labelField"
		if ($res.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" }
		$columns += [ordered]@{ $elKey = $res.Name; path = "Список.$($res.Name)" }
	}
	# Attributes
	foreach ($attr in $meta.Attributes) {
		if (-not (Test-DisplayableType $attr.Type)) { continue }
		$elKey = "labelField"
		if ($attr.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" }
		$columns += [ordered]@{ $elKey = $attr.Name; path = "Список.$($attr.Name)" }
	}

	$tableEl = [ordered]@{
		table = "Список"; path = "Список"
		rowPictureDataPath = "Список.DefaultPicture"
		commandBarLocation = "None"
		tableAutofill = $false
		columns = $columns
	}

	$props = [ordered]@{}
	if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } }

	return [ordered]@{
		title = $meta.Synonym
		properties = $props
		elements = @($tableEl)
		attributes = @(
			@{ name = "Список"; type = "DynamicList"; main = $true; settings = @{ mainTable = "InformationRegister.$($meta.Name)"; dynamicDataRead = $true } }
		)
	}
}

# ─── AccumulationRegister ─────────────────────────────────────────────────

function Generate-AccumulationRegisterDSL {
	param($meta, [hashtable]$presetData, [string]$purpose)
	$pKey = "accumulationRegister.$($purpose.ToLower())"
	$p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} }
	switch ($purpose) {
		"List" { return Generate-AccumulationRegisterListDSL $meta $p }
	}
}

function Generate-AccumulationRegisterListDSL($meta, [hashtable]$p) {
	$columns = @()
	# AccumulationRegisters always have Period, Recorder, LineNumber
	$columns += [ordered]@{ labelField = "Период"; path = "Список.Period" }
	$columns += [ordered]@{ labelField = "Регистратор"; path = "Список.Recorder" }
	$columns += [ordered]@{ labelField = "НомерСтроки"; path = "Список.LineNumber" }
	# Dimensions
	foreach ($dim in $meta.Dimensions) {
		if (-not (Test-DisplayableType $dim.Type)) { continue }
		$columns += [ordered]@{ labelField = $dim.Name; path = "Список.$($dim.Name)" }
	}
	# Resources
	foreach ($res in $meta.Resources) {
		if (-not (Test-DisplayableType $res.Type)) { continue }
		$elKey = "labelField"
		if ($res.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" }
		$columns += [ordered]@{ $elKey = $res.Name; path = "Список.$($res.Name)" }
	}
	# Attributes
	foreach ($attr in $meta.Attributes) {
		if (-not (Test-DisplayableType $attr.Type)) { continue }
		$elKey = "labelField"
		if ($attr.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" }
		$columns += [ordered]@{ $elKey = $attr.Name; path = "Список.$($attr.Name)" }
	}

	$tableEl = [ordered]@{
		table = "Список"; path = "Список"
		rowPictureDataPath = "Список.DefaultPicture"
		commandBarLocation = "None"
		tableAutofill = $false
		columns = $columns
	}

	$props = [ordered]@{}
	if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } }

	return [ordered]@{
		title = $meta.Synonym
		properties = $props
		elements = @($tableEl)
		attributes = @(
			@{ name = "Список"; type = "DynamicList"; main = $true; settings = @{ mainTable = "AccumulationRegister.$($meta.Name)"; dynamicDataRead = $true } }
		)
	}
}

# ─── ChartOfCharacteristicTypes (delegates to Catalog) ────────────────────

function Generate-ChartOfCharacteristicTypesDSL {
	param($meta, [hashtable]$presetData, [string]$purpose)
	# Delegate to Catalog generators — meta already has CodeLength, DescriptionLength, etc.
	$dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $purpose

	# Post-patch: replace Catalog types with ChartOfCharacteristicTypes types
	$catObjType = "CatalogObject.$($meta.Name)"
	$ccoctObjType = "ChartOfCharacteristicTypesObject.$($meta.Name)"
	$catListType = "Catalog.$($meta.Name)"
	$ccoctListType = "ChartOfCharacteristicTypes.$($meta.Name)"

	foreach ($a in $dsl.attributes) {
		if ($a.type -eq $catObjType) { $a.type = $ccoctObjType }
		if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq $catListType) {
			$a.settings.mainTable = $ccoctListType
		}
	}

	# For Item forms: inject ValueType field after Code/Description
	if ($purpose -eq "Item" -and $dsl.elements) {
		$vtEl = [ordered]@{ input = "ТипЗначения"; path = "Объект.ValueType" }
		$newElements = @()
		$inserted = $false
		foreach ($el in $dsl.elements) {
			$newElements += $el
			if (-not $inserted) {
				$elName = if ($el.input) { $el.input } elseif ($el.name) { $el.name } elseif ($el.group) { $el.group } else { "" }
				if ($elName -eq "Наименование" -or $elName -eq "ГруппаКодНаименование") {
					$newElements += $vtEl
					$inserted = $true
				}
			}
		}
		if (-not $inserted) { $newElements += $vtEl }
		$dsl.elements = $newElements
	}

	return $dsl
}

# ─── ExchangePlan (delegates to Catalog) ──────────────────────────────────

function Generate-ExchangePlanDSL {
	param($meta, [hashtable]$presetData, [string]$purpose)
	# ExchangePlans are not hierarchical and have no Folder form
	$dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $purpose

	# Post-patch: replace Catalog types with ExchangePlan types
	$catObjType = "CatalogObject.$($meta.Name)"
	$epObjType = "ExchangePlanObject.$($meta.Name)"
	$catListType = "Catalog.$($meta.Name)"
	$epListType = "ExchangePlan.$($meta.Name)"

	foreach ($a in $dsl.attributes) {
		if ($a.type -eq $catObjType) { $a.type = $epObjType }
		if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq $catListType) {
			$a.settings.mainTable = $epListType
		}
	}

	# For Item forms: inject SentNo, ReceivedNo after Code/Description
	if ($purpose -eq "Item" -and $dsl.elements) {
		$sentEl = [ordered]@{ input = "НомерОтправленного"; path = "Объект.SentNo"; readOnly = $true }
		$recvEl = [ordered]@{ input = "НомерПринятого"; path = "Объект.ReceivedNo"; readOnly = $true }
		$newElements = @()
		$inserted = $false
		foreach ($el in $dsl.elements) {
			$newElements += $el
			if (-not $inserted) {
				$elName = if ($el.input) { $el.input } elseif ($el.name) { $el.name } elseif ($el.group) { $el.group } else { "" }
				if ($elName -eq "Наименование" -or $elName -eq "ГруппаКодНаименование") {
					$newElements += $sentEl
					$newElements += $recvEl
					$inserted = $true
				}
			}
		}
		if (-not $inserted) { $newElements += $sentEl; $newElements += $recvEl }
		$dsl.elements = $newElements
	}

	return $dsl
}

# ─── ChartOfAccounts ──────────────────────────────────────────────────────

function Generate-ChartOfAccountsDSL {
	param($meta, [hashtable]$presetData, [string]$purpose)
	$pKey = "chartOfAccounts.$($purpose.ToLower())"
	$p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} }
	$fd = if ($p.fieldDefaults) { $p.fieldDefaults } else { @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } }
	switch ($purpose) {
		"Item"   { return Generate-ChartOfAccountsItemDSL $meta $p $fd $presetData }
		"Folder" { return Generate-ChartOfAccountsFolderDSL $meta $p }
		"List"   { return Generate-ChartOfAccountsListDSL $meta $presetData }
		"Choice" { return Generate-ChartOfAccountsChoiceDSL $meta $presetData }
	}
}

function Generate-ChartOfAccountsItemDSL($meta, [hashtable]$p, [hashtable]$fd, [hashtable]$presetData) {
	$elements = @()

	# Header: Code + Parent
	$headerLeftChildren = @()
	if ($meta.CodeLength -gt 0) {
		$headerLeftChildren += [ordered]@{ input = "Код"; path = "Объект.Code" }
	}
	$headerRightChildren = @()
	if ($meta.Hierarchical) {
		$parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { "Подчинен счету" }
		$headerRightChildren += [ordered]@{ input = "Родитель"; path = "Объект.Parent"; title = $parentTitle }
	}

	if ($headerRightChildren.Count -gt 0) {
		$elements += [ordered]@{
			group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none"
			children = @(
				[ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $headerLeftChildren }
				[ordered]@{ group = "vertical"; name = "ГруппаШапкаПраво"; showTitle = $false; children = $headerRightChildren }
			)
		}
	} elseif ($headerLeftChildren.Count -gt 0) {
		$elements += $headerLeftChildren
	}

	# Description
	if ($meta.DescriptionLength -gt 0) {
		$elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" }
	}

	# OffBalance
	$elements += [ordered]@{ check = "Забалансовый"; path = "Объект.OffBalance" }

	# AccountingFlags as checkboxes
	if ($meta.AccountingFlags -and $meta.AccountingFlags.Count -gt 0) {
		$flagChildren = @()
		foreach ($flag in $meta.AccountingFlags) {
			$flagChildren += [ordered]@{ check = $flag.Name; path = "Объект.$($flag.Name)" }
		}
		$elements += [ordered]@{
			group = "vertical"; name = "ГруппаПризнакиУчета"; title = "Признаки учета"
			children = $flagChildren
		}
	}

	# ExtDimensionTypes table
	if ($meta.MaxExtDimensionCount -gt 0) {
		$edCols = @()
		$edCols += [ordered]@{ input = "ВидСубконто"; path = "Объект.ExtDimensionTypes.ExtDimensionType" }
		$edCols += [ordered]@{ check = "ТолькоОбороты"; path = "Объект.ExtDimensionTypes.TurnoversOnly" }
		if ($meta.ExtDimensionAccountingFlags) {
			foreach ($edFlag in $meta.ExtDimensionAccountingFlags) {
				$edCols += [ordered]@{ check = $edFlag.Name; path = "Объект.ExtDimensionTypes.$($edFlag.Name)" }
			}
		}
		$elements += [ordered]@{
			table = "ВидыСубконто"
			path = "Объект.ExtDimensionTypes"
			columns = $edCols
		}
	}

	# Custom attributes
	foreach ($attr in $meta.Attributes) {
		if (-not (Test-DisplayableType $attr.Type)) { continue }
		$elements += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd)
	}

	# Tabular sections
	$tsExclude = @("ДополнительныеРеквизиты","Представления")
	foreach ($ts in $meta.TabularSections) {
		if ($tsExclude -contains $ts.Name) { continue }
		$tsCols = @()
		foreach ($col in $ts.Columns) {
			if (-not (Test-DisplayableType $col.Type)) { continue }
			$tsCols += (New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd)
		}
		$elements += [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols }
	}

	$props = [ordered]@{}
	if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } }

	return [ordered]@{
		title = $meta.Synonym
		properties = $props
		elements = $elements
		attributes = @(
			@{ name = "Объект"; type = "ChartOfAccountsObject.$($meta.Name)"; main = $true; savedData = $true }
		)
	}
}

function Generate-ChartOfAccountsFolderDSL($meta, [hashtable]$p) {
	$elements = @()
	if ($meta.CodeLength -gt 0) {
		$elements += [ordered]@{ input = "Код"; path = "Объект.Code" }
	}
	if ($meta.DescriptionLength -gt 0) {
		$elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" }
	}
	if ($meta.Hierarchical) {
		$parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { "Подчинен счету" }
		$elements += [ordered]@{ input = "Родитель"; path = "Объект.Parent"; title = $parentTitle }
	}

	$props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" }
	if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } }

	return [ordered]@{
		title = $meta.Synonym
		useForFoldersAndItems = "Folders"
		properties = $props
		elements = $elements
		attributes = @(
			@{ name = "Объект"; type = "ChartOfAccountsObject.$($meta.Name)"; main = $true; savedData = $true }
		)
	}
}

function Generate-ChartOfAccountsListDSL($meta, [hashtable]$presetData) {
	# Delegate to Catalog List and patch types
	$dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose "List"
	foreach ($a in $dsl.attributes) {
		if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq "Catalog.$($meta.Name)") {
			$a.settings.mainTable = "ChartOfAccounts.$($meta.Name)"
		}
	}
	return $dsl
}

function Generate-ChartOfAccountsChoiceDSL($meta, [hashtable]$presetData) {
	$dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose "Choice"
	foreach ($a in $dsl.attributes) {
		if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq "Catalog.$($meta.Name)") {
			$a.settings.mainTable = "ChartOfAccounts.$($meta.Name)"
		}
	}
	return $dsl
}

# ═══════════════════════════════════════════════════════════════════════════
# END OF FROM-OBJECT MODE FUNCTIONS
# ═══════════════════════════════════════════════════════════════════════════

# --- Detect XML format version ---

function Detect-FormatVersion([string]$dir) {
	$d = $dir
	while ($d) {
		$cfgPath = Join-Path $d "Configuration.xml"
		if (Test-Path $cfgPath) {
			$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).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"
}

$script:outPathResolved = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath }
$script:formatVersion = Detect-FormatVersion ([System.IO.Path]::GetDirectoryName($script:outPathResolved))

# --- 0. Path normalization and mode dispatch ---

# Form name → purpose mapping
$script:formNameToPurpose = @{
	"ФормаДокумента"  = "Item"
	"ФормаЭлемента"   = "Item"
	"ФормаСписка"     = "List"
	"ФормаВыбора"     = "Choice"
	"ФормаГруппы"     = "Folder"
	"ФормаЗаписи"     = "Record"
	"ФормаСчета"      = "Item"
	"ФормаУзла"       = "Item"
}

if ($FromObject -and $JsonPath) {
	Write-Error "Cannot use both -JsonPath and -FromObject. Choose one mode."
	exit 1
}
if (-not $FromObject -and -not $JsonPath) {
	Write-Error "Either -JsonPath or -FromObject is required."
	exit 1
}

if ($FromObject) {
	# Normalize OutputPath: append /Ext/Form.xml if missing
	$outNorm = $OutputPath -replace '[\\/]$', ''
	if ($outNorm -notmatch '[/\\]Ext[/\\]Form\.xml$') {
		if ($outNorm -match '[/\\]Ext$') {
			$OutputPath = "$outNorm/Form.xml"
		} else {
			$OutputPath = "$outNorm/Ext/Form.xml"
		}
		Write-Host "[resolved] OutputPath -> $OutputPath"
	}

	# Resolve object path and purpose from OutputPath convention:
	# .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml
	$outAbs = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath }
	$pathParts = $outAbs -split '[/\\]'
	# Find "Forms" segment
	$formsIdx = -1
	for ($i = $pathParts.Count - 1; $i -ge 0; $i--) {
		if ($pathParts[$i] -eq "Forms") { $formsIdx = $i; break }
	}

	$resolvedObjectPath = $null
	$resolvedPurpose = $null

	if ($formsIdx -ge 2) {
		$formName = $pathParts[$formsIdx + 1]
		$objectName = $pathParts[$formsIdx - 1]
		$typePluralAndAbove = $pathParts[0..($formsIdx - 2)] -join [IO.Path]::DirectorySeparatorChar

		# Derive purpose from form name
		if ($script:formNameToPurpose.ContainsKey($formName)) {
			$resolvedPurpose = $script:formNameToPurpose[$formName]
		}

		# Derive object XML path
		$candidateObjPath = Join-Path $typePluralAndAbove "$objectName.xml"
		if (Test-Path $candidateObjPath) {
			$resolvedObjectPath = $candidateObjPath
		}
	}

	# Apply: explicit -ObjectPath / -Purpose override resolved values
	$fromObjPath = $null
	if ($ObjectPath) {
		$fromObjPath = if ([System.IO.Path]::IsPathRooted($ObjectPath)) { $ObjectPath } else { Join-Path (Get-Location) $ObjectPath }
		# Append .xml if missing
		if (-not $fromObjPath.EndsWith(".xml")) { $fromObjPath = "$fromObjPath.xml" }
	} elseif ($resolvedObjectPath) {
		$fromObjPath = $resolvedObjectPath
		Write-Host "[resolved] ObjectPath -> $fromObjPath"
	} else {
		Write-Error "Cannot derive object path from OutputPath. Use -ObjectPath explicitly."
		exit 1
	}

	if (-not (Test-Path $fromObjPath)) {
		Write-Error "Object file not found: $fromObjPath"
		exit 1
	}

	$effectivePurpose = if ($Purpose) { $Purpose } elseif ($resolvedPurpose) { $resolvedPurpose } else { "Item" }
	if ($resolvedPurpose -and -not $Purpose) {
		Write-Host "[resolved] Purpose -> $effectivePurpose"
	}

	$meta = Parse-ObjectMeta $fromObjPath
	Write-Host "[from-object] Type=$($meta.Type), Name=$($meta.Name), Attrs=$($meta.Attributes.Count), TS=$($meta.TabularSections.Count)"

	$presetData = Load-Preset -PresetName $Preset -ScriptDir $PSScriptRoot

	$supportedPurposes = switch ($meta.Type) {
		"Document"                    { @("Item","List","Choice") }
		"Catalog"                     { @("Item","Folder","List","Choice") }
		"InformationRegister"         { @("Record","List") }
		"AccumulationRegister"        { @("List") }
		"ChartOfCharacteristicTypes"  { @("Item","Folder","List","Choice") }
		"ExchangePlan"                { @("Item","List","Choice") }
		"ChartOfAccounts"             { @("Item","Folder","List","Choice") }
		default                       { @() }
	}
	if ($supportedPurposes.Count -eq 0) {
		Write-Error "Object type '$($meta.Type)' is not yet supported by --from-object. Supported: Document, Catalog, InformationRegister, AccumulationRegister, ChartOfCharacteristicTypes, ExchangePlan, ChartOfAccounts."
		exit 1
	}
	if ($supportedPurposes -notcontains $effectivePurpose) {
		Write-Error "Purpose '$effectivePurpose' is not valid for $($meta.Type). Valid: $($supportedPurposes -join ', ')"
		exit 1
	}

	# Generate DSL
	$dsl = switch ($meta.Type) {
		"Document"                    { Generate-DocumentDSL -meta $meta -presetData $presetData -purpose $effectivePurpose }
		"Catalog"                     { Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $effectivePurpose }
		"InformationRegister"         { Generate-InformationRegisterDSL -meta $meta -presetData $presetData -purpose $effectivePurpose }
		"AccumulationRegister"        { Generate-AccumulationRegisterDSL -meta $meta -presetData $presetData -purpose $effectivePurpose }
		"ChartOfCharacteristicTypes"  { Generate-ChartOfCharacteristicTypesDSL -meta $meta -presetData $presetData -purpose $effectivePurpose }
		"ExchangePlan"                { Generate-ExchangePlanDSL -meta $meta -presetData $presetData -purpose $effectivePurpose }
		"ChartOfAccounts"             { Generate-ChartOfAccountsDSL -meta $meta -presetData $presetData -purpose $effectivePurpose }
	}

	# Emit DSL if requested
	if ($EmitDsl) {
		$dslJson = $dsl | ConvertTo-Json -Depth 20
		$dslPath = if ([System.IO.Path]::IsPathRooted($EmitDsl)) { $EmitDsl } else { Join-Path (Get-Location) $EmitDsl }
		$enc = New-Object System.Text.UTF8Encoding($true)
		[System.IO.File]::WriteAllText($dslPath, $dslJson, $enc)
		Write-Host "[from-object] DSL saved: $dslPath"
	}

	# Feed DSL into existing compiler
	$dslJson = $dsl | ConvertTo-Json -Depth 20
	$def = $dslJson | ConvertFrom-Json
} else {
	# --- 1. Load and validate JSON (original mode) ---

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

	$json = Get-Content -Raw -Encoding UTF8 $JsonPath
	$def = $json | ConvertFrom-Json
}

# --- 2. ID allocator ---

$script:nextId = 1
function New-Id {
	$id = $script:nextId
	$script:nextId++
	return $id
}

# --- 3. XML helper ---

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

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;')
}

# --- 4. Multilang helper ---

function Emit-MLText {
	param([string]$tag, [string]$text, [string]$indent)
	X "$indent<$tag>"
	X "$indent`t<v8:item>"
	X "$indent`t`t<v8:lang>ru</v8:lang>"
	X "$indent`t`t<v8:content>$(Esc-Xml $text)</v8:content>"
	X "$indent`t</v8:item>"
	X "$indent</$tag>"
}

# --- 5. Type emitter ---

$script:formTypeSynonyms = New-Object System.Collections.Hashtable
$script:formTypeSynonyms["строка"]   = "string"
$script:formTypeSynonyms["число"]    = "decimal"
$script:formTypeSynonyms["булево"]   = "boolean"
$script:formTypeSynonyms["дата"]     = "date"
$script:formTypeSynonyms["датавремя"]= "dateTime"
$script:formTypeSynonyms["number"]   = "decimal"
$script:formTypeSynonyms["bool"]     = "boolean"
$script:formTypeSynonyms["справочникссылка"]            = "CatalogRef"
$script:formTypeSynonyms["справочникобъект"]            = "CatalogObject"
$script:formTypeSynonyms["документссылка"]              = "DocumentRef"
$script:formTypeSynonyms["документобъект"]              = "DocumentObject"
$script:formTypeSynonyms["перечислениессылка"]           = "EnumRef"
$script:formTypeSynonyms["плансчетовссылка"]             = "ChartOfAccountsRef"
$script:formTypeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef"
$script:formTypeSynonyms["планвидоврасчётассылка"]        = "ChartOfCalculationTypesRef"
$script:formTypeSynonyms["планвидоврасчетассылка"]        = "ChartOfCalculationTypesRef"
$script:formTypeSynonyms["планобменассылка"]              = "ExchangePlanRef"
$script:formTypeSynonyms["бизнеспроцессссылка"]           = "BusinessProcessRef"
$script:formTypeSynonyms["задачассылка"]                  = "TaskRef"
$script:formTypeSynonyms["определяемыйтип"]             = "DefinedType"

# Known invalid types (runtime/UI types that don't exist in XDTO schema)
$script:knownInvalidTypes = @{
	"FormDataStructure"     = "Runtime type. Use object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)"
	"FormDataCollection"    = "Runtime type. Use ValueTable"
	"FormDataTree"          = "Runtime type. Use ValueTree"
	"FormDataTreeItem"      = "Runtime type, not valid in XML"
	"FormDataCollectionItem"= "Runtime type, not valid in XML"
	"FormGroup"             = "UI element type, not a data type"
	"FormField"             = "UI element type, not a data type"
	"FormButton"            = "UI element type, not a data type"
	"FormDecoration"        = "UI element type, not a data type"
	"FormTable"             = "UI element type, not a data type"
}

function Resolve-TypeStr {
	param([string]$typeStr)
	if (-not $typeStr) { return $typeStr }
	# Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix)
	if ($typeStr -match '^cfg:(.+)$') { $typeStr = $Matches[1] }
	if ($typeStr -match '^([^(]+)\((.+)\)$') {
		$base = $Matches[1].Trim(); $params = $Matches[2]
		$r = $script:formTypeSynonyms[$base.ToLower()]
		if ($r) { return "$r($params)" }
		return $typeStr
	}
	if ($typeStr.Contains('.')) {
		$i = $typeStr.IndexOf('.')
		$prefix = $typeStr.Substring(0, $i); $suffix = $typeStr.Substring($i)
		$r = $script:formTypeSynonyms[$prefix.ToLower()]
		if ($r) { return "$r$suffix" }
		return $typeStr
	}
	$r = $script:formTypeSynonyms[$typeStr.ToLower()]
	if ($r) { return $r }
	return $typeStr
}

function Emit-Type {
	param($typeStr, [string]$indent)

	if (-not $typeStr) {
		X "$indent<Type/>"
		return
	}

	$typeString = "$typeStr"

	# Composite type: "Type1 | Type2" or "Type1 + Type2"
	$parts = $typeString -split '\s*[|+]\s*'

	X "$indent<Type>"
	foreach ($part in $parts) {
		$part = $part.Trim()
		Emit-SingleType -typeStr $part -indent "$indent`t"
	}
	X "$indent</Type>"
}

function Emit-SingleType {
	param([string]$typeStr, [string]$indent)

	$typeStr = Resolve-TypeStr $typeStr

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

	# string or string(N)
	if ($typeStr -match '^string(\((\d+)\))?$') {
		$len = if ($Matches[2]) { $Matches[2] } else { "0" }
		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>Variable</v8:AllowedLength>"
		X "$indent</v8:StringQualifiers>"
		return
	}

	# decimal(D,F) or decimal(D,F,nonneg)
	if ($typeStr -match '^decimal\((\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
	if ($typeStr -match '^(date|dateTime|time)$') {
		$fractions = switch ($typeStr) {
			"date"     { "Date" }
			"dateTime" { "DateTime" }
			"time"     { "Time" }
		}
		X "$indent<v8:Type>xs:dateTime</v8:Type>"
		X "$indent<v8:DateQualifiers>"
		X "$indent`t<v8:DateFractions>$fractions</v8:DateFractions>"
		X "$indent</v8:DateQualifiers>"
		return
	}

	# ValueTable, ValueTree, ValueList, etc.
	$v8Types = @{
		"ValueTable"       = "v8:ValueTable"
		"ValueTree"        = "v8:ValueTree"
		"ValueList"        = "v8:ValueListType"
		"TypeDescription"  = "v8:TypeDescription"
		"Universal"        = "v8:Universal"
		"FixedArray"       = "v8:FixedArray"
		"FixedStructure"   = "v8:FixedStructure"
	}
	if ($v8Types.ContainsKey($typeStr)) {
		X "$indent<v8:Type>$($v8Types[$typeStr])</v8:Type>"
		return
	}

	# UI types
	$uiTypes = @{
		"FormattedString" = "v8ui:FormattedString"
		"Picture"         = "v8ui:Picture"
		"Color"           = "v8ui:Color"
		"Font"            = "v8ui:Font"
	}
	if ($uiTypes.ContainsKey($typeStr)) {
		X "$indent<v8:Type>$($uiTypes[$typeStr])</v8:Type>"
		return
	}

	# DCS types
	if ($typeStr -match '^DataComposition') {
		$dcsMap = @{
			"DataCompositionSettings"      = "dcsset:DataCompositionSettings"
			"DataCompositionSchema"        = "dcssch:DataCompositionSchema"
			"DataCompositionComparisonType" = "dcscor:DataCompositionComparisonType"
		}
		if ($dcsMap.ContainsKey($typeStr)) {
			X "$indent<v8:Type>$($dcsMap[$typeStr])</v8:Type>"
			return
		}
	}

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

	# cfg: references (CatalogRef.XXX, DocumentObject.XXX, etc.)
	if ($typeStr -match '^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|ChartOfAccountsRef|ChartOfAccountsObject|ChartOfCharacteristicTypesRef|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesRef|ChartOfCalculationTypesObject|ExchangePlanRef|ExchangePlanObject|BusinessProcessRef|BusinessProcessObject|TaskRef|TaskObject|InformationRegisterRecordSet|InformationRegisterRecordManager|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|ConstantsSet|DataProcessorObject|ReportObject)\.') {
		X "$indent<v8:Type>cfg:$typeStr</v8:Type>"
		return
	}

	# Fallback with validation
	if ($script:knownInvalidTypes.ContainsKey($typeStr)) {
		throw "Invalid form attribute type '$typeStr': $($script:knownInvalidTypes[$typeStr])"
	}
	if ($typeStr.Contains('.')) {
		X "$indent<v8:Type>cfg:$typeStr</v8:Type>"
	} else {
		Write-Warning "Unrecognized bare type '$typeStr' — will be emitted without namespace prefix"
		X "$indent<v8:Type>$typeStr</v8:Type>"
	}
}

# --- 6. Event handler name generator ---

$script:eventSuffixMap = @{
	"OnChange"             = "ПриИзменении"
	"StartChoice"          = "НачалоВыбора"
	"ChoiceProcessing"     = "ОбработкаВыбора"
	"AutoComplete"         = "АвтоПодбор"
	"Clearing"             = "Очистка"
	"Opening"              = "Открытие"
	"Click"                = "Нажатие"
	"OnActivateRow"        = "ПриАктивизацииСтроки"
	"BeforeAddRow"         = "ПередНачаломДобавления"
	"BeforeDeleteRow"      = "ПередУдалением"
	"BeforeRowChange"      = "ПередНачаломИзменения"
	"OnStartEdit"          = "ПриНачалеРедактирования"
	"OnEndEdit"            = "ПриОкончанииРедактирования"
	"Selection"            = "ВыборСтроки"
	"OnCurrentPageChange"  = "ПриСменеСтраницы"
	"TextEditEnd"          = "ОкончаниеВводаТекста"
	"URLProcessing"        = "ОбработкаНавигационнойСсылки"
	"DragStart"            = "НачалоПеретаскивания"
	"Drag"                 = "Перетаскивание"
	"DragCheck"            = "ПроверкаПеретаскивания"
	"Drop"                 = "Помещение"
	"AfterDeleteRow"       = "ПослеУдаления"
}

function Get-HandlerName {
	param([string]$elementName, [string]$eventName)
	$suffix = $script:eventSuffixMap[$eventName]
	if ($suffix) {
		return "$elementName$suffix"
	}
	return "$elementName$eventName"
}

# --- 7. Element emitters ---

function Get-ElementName {
	param($el, [string]$typeKey)
	if ($el.name) { return "$($el.name)" }
	return "$($el.$typeKey)"
}

$script:knownEvents = @{
	"input"     = @("OnChange","StartChoice","ChoiceProcessing","AutoComplete","TextEditEnd","Clearing","Creating","EditTextChange")
	"check"     = @("OnChange")
	"radio"     = @("OnChange")
	"label"     = @("Click","URLProcessing")
	"labelField"= @("OnChange","StartChoice","ChoiceProcessing","Click","URLProcessing","Clearing")
	"table"     = @("Selection","BeforeAddRow","AfterDeleteRow","BeforeDeleteRow","OnActivateRow","OnEditEnd","OnStartEdit","BeforeRowChange","BeforeEditEnd","ValueChoice","OnActivateCell","OnActivateField","Drag","DragStart","DragCheck","DragEnd","OnGetDataAtServer","BeforeLoadUserSettingsAtServer","OnUpdateUserSettingSetAtServer","OnChange")
	"pages"     = @("OnCurrentPageChange")
	"page"      = @("OnCurrentPageChange")
	"button"    = @("Click")
	"picField"  = @("OnChange","StartChoice","ChoiceProcessing","Click","Clearing")
	"calendar"  = @("OnChange","OnActivate")
	"picture"   = @("Click")
	"cmdBar"    = @()
	"popup"     = @()
	"group"     = @()
}
$script:knownFormEvents = @("OnCreateAtServer","OnOpen","BeforeClose","OnClose","NotificationProcessing","ChoiceProcessing","OnReadAtServer","AfterWriteAtServer","BeforeWriteAtServer","AfterWrite","BeforeWrite","OnWriteAtServer","FillCheckProcessingAtServer","OnLoadDataFromSettingsAtServer","BeforeLoadDataFromSettingsAtServer","OnSaveDataInSettingsAtServer","ExternalEvent","OnReopen","Opening")

function Emit-Events {
	param($el, [string]$elementName, [string]$indent, [string]$typeKey)

	if (-not $el.on) { return }

	# Validate event names
	if ($typeKey -and $script:knownEvents.ContainsKey($typeKey)) {
		$allowed = $script:knownEvents[$typeKey]
		foreach ($evt in $el.on) {
			if ($allowed.Count -gt 0 -and $allowed -notcontains "$evt") {
				Write-Host "[WARN] Unknown event '$evt' for $typeKey '$elementName'. Known: $($allowed -join ', ')"
			}
		}
	}

	X "$indent<Events>"
	foreach ($evt in $el.on) {
		$evtName = "$evt"
		$handler = if ($el.handlers -and $el.handlers.$evtName) {
			"$($el.handlers.$evtName)"
		} else {
			Get-HandlerName -elementName $elementName -eventName $evtName
		}
		X "$indent`t<Event name=`"$evtName`">$handler</Event>"
	}
	X "$indent</Events>"
}

function Emit-Companion {
	param([string]$tag, [string]$name, [string]$indent)
	$id = New-Id
	X "$indent<$tag name=`"$name`" id=`"$id`"/>"
}

function Emit-Element {
	param($el, [string]$indent, [bool]$inCmdBar = $false)

	# Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio).
	# Maps any synonym to canonical short DSL key.
	$synonyms = @{
		"commandBar"        = "cmdBar"
		"autoCommandBar"    = "autoCmdBar"
		"КоманднаяПанель"   = "cmdBar"
		"InputField"        = "input"
		"ПолеВвода"         = "input"
		"CheckBoxField"     = "check"
		"ПолеФлажка"        = "check"
		"RadioButtonField"  = "radio"
		"ПолеПереключателя" = "radio"
		"radioButton"       = "radio"
		"PictureField"      = "picField"
		"ПолеКартинки"      = "picField"
		"LabelField"        = "labelField"
		"ПолеНадписи"       = "labelField"
		"CalendarField"     = "calendar"
		"ПолеКалендаря"     = "calendar"
		"LabelDecoration"   = "label"
		"Надпись"           = "label"
		"PictureDecoration" = "picture"
		"Картинка"          = "picture"
		"UsualGroup"        = "group"
		"Группа"            = "group"
		"ОбычнаяГруппа"     = "group"
		"ColumnGroup"       = "columnGroup"
		"ГруппаКолонок"     = "columnGroup"
		"Pages"             = "pages"
		"ГруппаСтраниц"     = "pages"
		"Page"              = "page"
		"Страница"          = "page"
		"Table"             = "table"
		"Таблица"           = "table"
		"Button"            = "button"
		"Кнопка"            = "button"
		"Popup"             = "popup"
		"ВсплывающееМеню"   = "popup"
	}
	foreach ($pair in $synonyms.GetEnumerator()) {
		if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) {
			$val = $el.($pair.Key)
			$el.PSObject.Properties.Remove($pair.Key) | Out-Null
			$el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force
		}
	}

	# Determine element type from key
	$typeKey = $null
	$xmlTag = $null

	foreach ($key in @("columnGroup","group","input","check","radio","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) {
		if ($el.$key -ne $null) {
			$typeKey = $key
			break
		}
	}

	if (-not $typeKey) {
		Write-Warning "Unknown element type, skipping"
		return
	}

	# Validate known keys — warn about typos and unknown properties
	$knownKeys = @{
		# type keys
		"group"=1;"columnGroup"=1;"input"=1;"check"=1;"radio"=1;"label"=1;"labelField"=1;"table"=1;"pages"=1;"page"=1
		"button"=1;"picture"=1;"picField"=1;"calendar"=1;"cmdBar"=1;"popup"=1
		# columnGroup-specific
		"showInHeader"=1
		# radio-specific
		"radioButtonType"=1;"choiceList"=1;"columnsCount"=1
		# naming & binding
		"name"=1;"path"=1;"title"=1
		# visibility & state
		"visible"=1;"hidden"=1;"enabled"=1;"disabled"=1;"readOnly"=1;"userVisible"=1
		# events
		"on"=1;"handlers"=1
		# layout
		"titleLocation"=1;"representation"=1;"width"=1;"height"=1
		"horizontalStretch"=1;"verticalStretch"=1;"autoMaxWidth"=1;"autoMaxHeight"=1
		"maxWidth"=1;"maxHeight"=1
		# input-specific
		"multiLine"=1;"passwordMode"=1;"choiceButton"=1;"clearButton"=1
		"spinButton"=1;"dropListButton"=1;"markIncomplete"=1;"skipOnInput"=1;"inputHint"=1
		"textEdit"=1
		# label/hyperlink
		"hyperlink"=1
		# group-specific
		"showTitle"=1;"united"=1;"collapsed"=1
		# hierarchy
		"children"=1;"columns"=1
		# table-specific
		"changeRowSet"=1;"changeRowOrder"=1;"header"=1;"footer"=1
		"commandBarLocation"=1;"searchStringLocation"=1
		"choiceMode"=1;"initialTreeView"=1;"enableDrag"=1;"enableStartDrag"=1
		"rowPictureDataPath"=1;"tableAutofill"=1
		# pages-specific
		"pagesRepresentation"=1
		# button-specific
		"type"=1;"command"=1;"stdCommand"=1;"defaultButton"=1;"locationInCommandBar"=1
		# picture/decoration
		"src"=1
		# cmdBar-specific
		"autofill"=1
	}
	foreach ($p in $el.PSObject.Properties) {
		if (-not $knownKeys.ContainsKey($p.Name)) {
			Write-Warning "Element '$($el.$typeKey)': unknown key '$($p.Name)' — ignored. Check SKILL.md for valid keys."
		}
	}

	$name = Get-ElementName -el $el -typeKey $typeKey
	$id = New-Id

	switch ($typeKey) {
		"group"    { Emit-Group -el $el -name $name -id $id -indent $indent }
		"columnGroup" { Emit-ColumnGroup -el $el -name $name -id $id -indent $indent }
		"input"    { Emit-Input -el $el -name $name -id $id -indent $indent }
		"check"    { Emit-Check -el $el -name $name -id $id -indent $indent }
		"radio"    { Emit-Radio -el $el -name $name -id $id -indent $indent }
		"label"    { Emit-Label -el $el -name $name -id $id -indent $indent }
		"labelField" { Emit-LabelField -el $el -name $name -id $id -indent $indent }
		"table"    { Emit-Table -el $el -name $name -id $id -indent $indent }
		"pages"    { Emit-Pages -el $el -name $name -id $id -indent $indent }
		"page"     { Emit-Page -el $el -name $name -id $id -indent $indent }
		"button"   { Emit-Button -el $el -name $name -id $id -indent $indent -inCmdBar $inCmdBar }
		"picture"  { Emit-PictureDecoration -el $el -name $name -id $id -indent $indent }
		"picField" { Emit-PictureField -el $el -name $name -id $id -indent $indent }
		"calendar" { Emit-Calendar -el $el -name $name -id $id -indent $indent }
		"cmdBar"   { Emit-CommandBar -el $el -name $name -id $id -indent $indent }
		"popup"    { Emit-Popup -el $el -name $name -id $id -indent $indent }
	}
}

function Emit-CommonFlags {
	param($el, [string]$indent)
	if ($el.visible -eq $false -or $el.hidden -eq $true) { X "$indent<Visible>false</Visible>" }
	if ($el.userVisible -eq $false) {
		X "$indent<UserVisible>"
		X "$indent`t<xr:Common>false</xr:Common>"
		X "$indent</UserVisible>"
	}
	if ($el.enabled -eq $false -or $el.disabled -eq $true) { X "$indent<Enabled>false</Enabled>" }
	if ($el.readOnly -eq $true) { X "$indent<ReadOnly>true</ReadOnly>" }
}

function Title-FromName {
	param([string]$name)
	if (-not $name) { return '' }
	$s = [regex]::Replace($name, '([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', '$1 $2')
	$s = [regex]::Replace($s, '([а-яa-z0-9])([А-ЯA-Z])', '$1 $2')
	$parts = $s -split ' '
	if ($parts.Count -eq 0) { return $s }
	$out = New-Object System.Collections.ArrayList
	[void]$out.Add($parts[0])
	for ($i = 1; $i -lt $parts.Count; $i++) {
		$p = $parts[$i]
		if ($p.Length -gt 1 -and $p -ceq $p.ToUpper()) {
			[void]$out.Add($p)
		} else {
			[void]$out.Add($p.ToLower())
		}
	}
	return ($out -join ' ')
}

function Emit-Title {
	param($el, [string]$name, [string]$indent, [switch]$auto)
	$title = $el.title
	if (-not $title -and $auto -and $name) {
		$title = Title-FromName -name $name
	}
	if ($title) {
		Emit-MLText -tag "Title" -text "$title" -indent $indent
	}
}

function Emit-Group {
	param($el, [string]$name, [int]$id, [string]$indent)

	X "$indent<UsualGroup name=`"$name`" id=`"$id`">"
	$inner = "$indent`t"

	Emit-Title -el $el -name $name -indent $inner

	# Group orientation
	$groupVal = "$($el.group)"
	$orientation = switch ($groupVal) {
		"horizontal"       { "Horizontal" }
		"vertical"         { "Vertical" }
		"alwaysHorizontal" { "AlwaysHorizontal" }
		"alwaysVertical"   { "AlwaysVertical" }
		default            { $null }
	}
	if ($orientation) { X "$inner<Group>$orientation</Group>" }

	# Behavior
	if ($groupVal -eq "collapsible") {
		X "$inner<Group>Vertical</Group>"
		X "$inner<Behavior>Collapsible</Behavior>"
		if ($el.collapsed -eq $true) { X "$inner<Collapsed>true</Collapsed>" }
	}

	# Representation
	if ($el.representation) {
		$repr = switch ("$($el.representation)") {
			"none"             { "None" }
			"normal"           { "NormalSeparation" }
			"weak"             { "WeakSeparation" }
			"strong"           { "StrongSeparation" }
			default            { "$($el.representation)" }
		}
		X "$inner<Representation>$repr</Representation>"
	}

	# ShowTitle
	if ($el.showTitle -eq $false) { X "$inner<ShowTitle>false</ShowTitle>" }

	# United
	if ($el.united -eq $false) { X "$inner<United>false</United>" }

	Emit-CommonFlags -el $el -indent $inner

	# Companion: ExtendedTooltip
	Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner

	# Children
	if ($el.children -and $el.children.Count -gt 0) {
		X "$inner<ChildItems>"
		foreach ($child in $el.children) {
			Emit-Element -el $child -indent "$inner`t"
		}
		X "$inner</ChildItems>"
	}

	X "$indent</UsualGroup>"
}

function Emit-ColumnGroup {
	param($el, [string]$name, [int]$id, [string]$indent)

	X "$indent<ColumnGroup name=`"$name`" id=`"$id`">"
	$inner = "$indent`t"

	Emit-Title -el $el -name $name -indent $inner

	# Group orientation (horizontal / vertical / inCell — последнее только здесь)
	$groupVal = "$($el.columnGroup)"
	$orientation = switch ($groupVal) {
		"horizontal" { "Horizontal" }
		"vertical"   { "Vertical" }
		"inCell"     { "InCell" }
		default      { $null }
	}
	if ($orientation) { X "$inner<Group>$orientation</Group>" }

	if ($el.showTitle -eq $false) { X "$inner<ShowTitle>false</ShowTitle>" }
	if ($null -ne $el.showInHeader) {
		$shVal = if ($el.showInHeader) { "true" } else { "false" }
		X "$inner<ShowInHeader>$shVal</ShowInHeader>"
	}
	if ($el.width) { X "$inner<Width>$($el.width)</Width>" }

	Emit-CommonFlags -el $el -indent $inner

	# Companion: ExtendedTooltip
	Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner

	# Children
	if ($el.children -and $el.children.Count -gt 0) {
		X "$inner<ChildItems>"
		foreach ($child in $el.children) {
			Emit-Element -el $child -indent "$inner`t"
		}
		X "$inner</ChildItems>"
	}

	X "$indent</ColumnGroup>"
}

function Emit-Input {
	param($el, [string]$name, [int]$id, [string]$indent)

	X "$indent<InputField name=`"$name`" id=`"$id`">"
	$inner = "$indent`t"

	if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }

	Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
	Emit-CommonFlags -el $el -indent $inner

	if ($el.titleLocation) {
		$loc = switch ("$($el.titleLocation)") {
			"none"   { "None" }
			"left"   { "Left" }
			"right"  { "Right" }
			"top"    { "Top" }
			"bottom" { "Bottom" }
			default  { "$($el.titleLocation)" }
		}
		X "$inner<TitleLocation>$loc</TitleLocation>"
	}

	if ($el.multiLine -eq $true) { X "$inner<MultiLine>true</MultiLine>" }
	if ($el.passwordMode -eq $true) { X "$inner<PasswordMode>true</PasswordMode>" }
	if ($el.choiceButton -eq $false) { X "$inner<ChoiceButton>false</ChoiceButton>" }
	if ($el.clearButton -eq $true) { X "$inner<ClearButton>true</ClearButton>" }
	if ($el.spinButton -eq $true) { X "$inner<SpinButton>true</SpinButton>" }
	if ($el.dropListButton -eq $true) { X "$inner<DropListButton>true</DropListButton>" }
	if ($el.markIncomplete -eq $true) { X "$inner<AutoMarkIncomplete>true</AutoMarkIncomplete>" }
	if ($el.textEdit -eq $false) { X "$inner<TextEdit>false</TextEdit>" }
	if ($el.skipOnInput -eq $true) { X "$inner<SkipOnInput>true</SkipOnInput>" }
	$hasAmw = $el.PSObject.Properties.Name -contains 'autoMaxWidth'
	if ($hasAmw) {
		if ($el.autoMaxWidth -eq $false) { X "$inner<AutoMaxWidth>false</AutoMaxWidth>" }
	} elseif ($el.multiLine -eq $true) {
		X "$inner<AutoMaxWidth>false</AutoMaxWidth>"
	}
	if ($null -ne $el.maxWidth) { X "$inner<MaxWidth>$($el.maxWidth)</MaxWidth>" }
	if ($el.autoMaxHeight -eq $false) { X "$inner<AutoMaxHeight>false</AutoMaxHeight>" }
	if ($null -ne $el.maxHeight) { X "$inner<MaxHeight>$($el.maxHeight)</MaxHeight>" }
	if ($el.width) { X "$inner<Width>$($el.width)</Width>" }
	if ($el.height) { X "$inner<Height>$($el.height)</Height>" }
	if ($el.horizontalStretch -eq $true) { X "$inner<HorizontalStretch>true</HorizontalStretch>" }
	if ($el.verticalStretch -eq $true) { X "$inner<VerticalStretch>true</VerticalStretch>" }

	if ($el.inputHint) {
		Emit-MLText -tag "InputHint" -text "$($el.inputHint)" -indent $inner
	}

	# Companions
	Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
	Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner

	Emit-Events -el $el -elementName $name -indent $inner -typeKey "input"

	X "$indent</InputField>"
}

function Emit-Check {
	param($el, [string]$name, [int]$id, [string]$indent)

	X "$indent<CheckBoxField name=`"$name`" id=`"$id`">"
	$inner = "$indent`t"

	if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }

	Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
	Emit-CommonFlags -el $el -indent $inner

	$tl = if ($el.titleLocation) { "$($el.titleLocation)" } else { "Right" }
	X "$inner<TitleLocation>$tl</TitleLocation>"

	# Companions
	Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
	Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner

	Emit-Events -el $el -elementName $name -indent $inner -typeKey "check"

	X "$indent</CheckBoxField>"
}

# Maps Russian/English root of a typed reference path to canonical English root.
# Used to normalize ChoiceList values like "Перечисление.X.Y" → "Enum.X.EnumValue.Y".
$script:refRootSynonyms = @{
	"Перечисление"            = "Enum"
	"Справочник"              = "Catalog"
	"Документ"                = "Document"
	"ПланСчетов"              = "ChartOfAccounts"
	"ПланВидовХарактеристик"  = "ChartOfCharacteristicTypes"
	"ПланВидовРасчета"        = "ChartOfCalculationTypes"
	"ПланВидовРасчёта"        = "ChartOfCalculationTypes"
	"ПланОбмена"              = "ExchangePlan"
	"БизнесПроцесс"           = "BusinessProcess"
	"Задача"                  = "Task"
	"РегистрСведений"         = "InformationRegister"
	"РегистрНакопления"       = "AccumulationRegister"
	"РегистрБухгалтерии"      = "AccountingRegister"
	"РегистрРасчета"          = "CalculationRegister"
	"РегистрРасчёта"          = "CalculationRegister"
}
$script:enumValueSynonyms = @("EnumValue","ЗначениеПеречисления")

# Normalize a choiceList item value: returns @{ XsiType = "..."; Text = "..." }
function Normalize-ChoiceValue {
	param($value)

	# Booleans
	if ($value -is [bool]) {
		return @{ XsiType = "xs:boolean"; Text = if ($value) { "true" } else { "false" } }
	}
	# Numbers (int / decimal / double)
	if ($value -is [int] -or $value -is [long] -or $value -is [double] -or $value -is [decimal]) {
		return @{ XsiType = "xs:decimal"; Text = "$value" }
	}

	$s = "$value"
	if ([string]::IsNullOrEmpty($s)) {
		return @{ XsiType = "xs:string"; Text = "" }
	}

	# Try to detect typed reference path: "<Root>.<Type>[.<Member>.<Value>]"
	$parts = $s -split '\.'
	if ($parts.Count -ge 2) {
		$root = $parts[0]
		$canonRoot = $null
		if ($script:refRootSynonyms.ContainsKey($root)) { $canonRoot = $script:refRootSynonyms[$root] }
		elseif ($script:refRootSynonyms.Values -contains $root) { $canonRoot = $root }

		if ($canonRoot) {
			$typeName = $parts[1]
			$normalized = $null

			if ($canonRoot -eq "Enum") {
				if ($parts.Count -eq 2) {
					# "Enum.X" alone — not a value, treat as string
				} elseif ($parts.Count -eq 3) {
					# "Enum.X.Y" — insert .EnumValue.
					$normalized = "Enum.$typeName.EnumValue.$($parts[2])"
				} else {
					# "Enum.X.<member>.Y..."  — replace member with EnumValue (handles ЗначениеПеречисления too)
					$member = $parts[2]
					if ($script:enumValueSynonyms -contains $member) {
						$rest = $parts[3..($parts.Count-1)] -join '.'
						$normalized = "Enum.$typeName.EnumValue.$rest"
					} else {
						$rest = $parts[2..($parts.Count-1)] -join '.'
						$normalized = "Enum.$typeName.EnumValue.$rest"
					}
				}
			} else {
				# Other ref roots: just translate root, keep tail as-is
				if ($parts.Count -ge 3) {
					$tail = $parts[1..($parts.Count-1)] -join '.'
					$normalized = "$canonRoot.$tail"
				}
			}

			if ($normalized) {
				return @{ XsiType = "xr:DesignTimeRef"; Text = $normalized }
			}
		}
	}

	return @{ XsiType = "xs:string"; Text = $s }
}

# Emit Presentation block for a choiceList item.
# Accepts string (ru only), or hashtable/PSCustomObject {ru, en, ...}.
# Empty/null → emits empty <Presentation/>.
function Emit-ChoicePresentation {
	param($pres, [string]$indent)
	if ($null -eq $pres -or ($pres -is [string] -and [string]::IsNullOrEmpty($pres))) {
		X "$indent<Presentation/>"
		return
	}

	$pairs = @()
	if ($pres -is [string]) {
		$pairs += ,@("ru", $pres)
	} elseif ($pres -is [hashtable] -or $pres -is [System.Collections.IDictionary]) {
		foreach ($k in $pres.Keys) { $pairs += ,@("$k", "$($pres[$k])") }
	} elseif ($pres.PSObject -and $pres.PSObject.Properties) {
		foreach ($p in $pres.PSObject.Properties) { $pairs += ,@("$($p.Name)", "$($p.Value)") }
	} else {
		$pairs += ,@("ru", "$pres")
	}

	X "$indent<Presentation>"
	foreach ($pair in $pairs) {
		X "$indent`t<v8:item>"
		X "$indent`t`t<v8:lang>$($pair[0])</v8:lang>"
		X "$indent`t`t<v8:content>$(Esc-Xml $pair[1])</v8:content>"
		X "$indent`t</v8:item>"
	}
	X "$indent</Presentation>"
}

function Emit-Radio {
	param($el, [string]$name, [int]$id, [string]$indent)

	X "$indent<RadioButtonField name=`"$name`" id=`"$id`">"
	$inner = "$indent`t"

	if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }

	Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
	Emit-CommonFlags -el $el -indent $inner

	# TitleLocation default is None for radio (matches typical configurator behavior)
	$tl = if ($el.titleLocation) {
		switch ("$($el.titleLocation)") {
			"none"   { "None" }
			"left"   { "Left" }
			"right"  { "Right" }
			"top"    { "Top" }
			"bottom" { "Bottom" }
			default  { "$($el.titleLocation)" }
		}
	} else { "None" }
	X "$inner<TitleLocation>$tl</TitleLocation>"

	# RadioButtonType: Auto | RadioButtons | Tumbler. Accept synonyms.
	$rbtRaw = if ($el.radioButtonType) { "$($el.radioButtonType)".Trim() } else { "Auto" }
	$rbt = switch -Regex ($rbtRaw.ToLower()) {
		'^(auto|авто)$'                        { "Auto"; break }
		'^(radiobuttons?|переключатель|радио)$' { "RadioButtons"; break }
		'^(tumbler|тумблер)$'                  { "Tumbler"; break }
		default                                { $rbtRaw }
	}
	X "$inner<RadioButtonType>$rbt</RadioButtonType>"

	if ($null -ne $el.columnsCount) {
		X "$inner<ColumnsCount>$($el.columnsCount)</ColumnsCount>"
	}

	# ChoiceList
	if ($el.choiceList -and $el.choiceList.Count -gt 0) {
		X "$inner<ChoiceList>"
		$itemIndent = "$inner`t"
		foreach ($item in $el.choiceList) {
			# Pull value (and tolerate Russian synonym "значение")
			$valRaw = $null
			if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) {
				if ($item.Contains("value")) { $valRaw = $item["value"] }
				elseif ($item.Contains("значение")) { $valRaw = $item["значение"] }
			} else {
				if ($item.PSObject.Properties["value"])    { $valRaw = $item.value }
				elseif ($item.PSObject.Properties["значение"]) { $valRaw = $item."значение" }
			}

			# Pull presentation (presentation OR title synonym)
			$presRaw = $null
			$hasPres = $false
			if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) {
				if ($item.Contains("presentation")) { $presRaw = $item["presentation"]; $hasPres = $true }
				elseif ($item.Contains("представление")) { $presRaw = $item["представление"]; $hasPres = $true }
				elseif ($item.Contains("title")) { $presRaw = $item["title"]; $hasPres = $true }
			} else {
				if ($item.PSObject.Properties["presentation"]) { $presRaw = $item.presentation; $hasPres = $true }
				elseif ($item.PSObject.Properties["представление"]) { $presRaw = $item."представление"; $hasPres = $true }
				elseif ($item.PSObject.Properties["title"]) { $presRaw = $item.title; $hasPres = $true }
			}

			$norm = Normalize-ChoiceValue -value $valRaw

			# Auto-derive presentation if missing
			if (-not $hasPres) {
				if ($norm.XsiType -eq "xr:DesignTimeRef") {
					$tail = ($norm.Text -split '\.')[-1]
					$presRaw = Title-FromName -name $tail
				} elseif ($norm.XsiType -eq "xs:string") {
					$presRaw = $norm.Text
				} else {
					$presRaw = $norm.Text
				}
			}

			X "$itemIndent<xr:Item>"
			$valIndent = "$itemIndent`t"
			X "$valIndent<xr:Presentation/>"
			X "$valIndent<xr:CheckState>0</xr:CheckState>"
			X "$valIndent<xr:Value xsi:type=`"FormChoiceListDesTimeValue`">"
			Emit-ChoicePresentation -pres $presRaw -indent "$valIndent`t"
			X "$valIndent`t<Value xsi:type=`"$($norm.XsiType)`">$(Esc-Xml $norm.Text)</Value>"
			X "$valIndent</xr:Value>"
			X "$itemIndent</xr:Item>"
		}
		X "$inner</ChoiceList>"
	}

	# Companions
	Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
	Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner

	Emit-Events -el $el -elementName $name -indent $inner -typeKey "radio"

	X "$indent</RadioButtonField>"
}

function Emit-Label {
	param($el, [string]$name, [int]$id, [string]$indent)

	X "$indent<LabelDecoration name=`"$name`" id=`"$id`">"
	$inner = "$indent`t"

	$labelTitle = if ($el.title) { "$($el.title)" } else { Title-FromName -name $name }
	if ($labelTitle) {
		$formatted = if ($el.hyperlink -eq $true) { "true" } else { "false" }
		X "$inner<Title formatted=`"$formatted`">"
		X "$inner`t<v8:item>"
		X "$inner`t`t<v8:lang>ru</v8:lang>"
		X "$inner`t`t<v8:content>$(Esc-Xml "$labelTitle")</v8:content>"
		X "$inner`t</v8:item>"
		X "$inner</Title>"
	}

	Emit-CommonFlags -el $el -indent $inner

	if ($el.hyperlink -eq $true) { X "$inner<Hyperlink>true</Hyperlink>" }
	if ($el.autoMaxWidth -eq $false) { X "$inner<AutoMaxWidth>false</AutoMaxWidth>" }
	if ($null -ne $el.maxWidth) { X "$inner<MaxWidth>$($el.maxWidth)</MaxWidth>" }
	if ($el.autoMaxHeight -eq $false) { X "$inner<AutoMaxHeight>false</AutoMaxHeight>" }
	if ($null -ne $el.maxHeight) { X "$inner<MaxHeight>$($el.maxHeight)</MaxHeight>" }
	if ($el.width) { X "$inner<Width>$($el.width)</Width>" }
	if ($el.height) { X "$inner<Height>$($el.height)</Height>" }

	# Companions
	Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
	Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner

	Emit-Events -el $el -elementName $name -indent $inner -typeKey "label"

	X "$indent</LabelDecoration>"
}

function Emit-LabelField {
	param($el, [string]$name, [int]$id, [string]$indent)

	X "$indent<LabelField name=`"$name`" id=`"$id`">"
	$inner = "$indent`t"

	if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }

	Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
	Emit-CommonFlags -el $el -indent $inner

	if ($el.hyperlink -eq $true) { X "$inner<Hyperlink>true</Hyperlink>" }

	# Companions
	Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
	Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner

	Emit-Events -el $el -elementName $name -indent $inner -typeKey "labelField"

	X "$indent</LabelField>"
}

function Emit-Table {
	param($el, [string]$name, [int]$id, [string]$indent)

	X "$indent<Table name=`"$name`" id=`"$id`">"
	$inner = "$indent`t"

	if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }

	Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
	Emit-CommonFlags -el $el -indent $inner

	if ($el.representation) {
		X "$inner<Representation>$($el.representation)</Representation>"
	}
	if ($el.changeRowSet -eq $true) { X "$inner<ChangeRowSet>true</ChangeRowSet>" }
	if ($el.changeRowOrder -eq $true) { X "$inner<ChangeRowOrder>true</ChangeRowOrder>" }
	if ($el.height) { X "$inner<HeightInTableRows>$($el.height)</HeightInTableRows>" }
	if ($el.header -eq $false) { X "$inner<Header>false</Header>" }
	if ($el.footer -eq $true) { X "$inner<Footer>true</Footer>" }

	if ($el.commandBarLocation) {
		X "$inner<CommandBarLocation>$($el.commandBarLocation)</CommandBarLocation>"
	}
	if ($el.searchStringLocation) {
		X "$inner<SearchStringLocation>$($el.searchStringLocation)</SearchStringLocation>"
	}
	if ($el.choiceMode -eq $true) { X "$inner<ChoiceMode>true</ChoiceMode>" }
	if ($el.initialTreeView) { X "$inner<InitialTreeView>$($el.initialTreeView)</InitialTreeView>" }
	if ($el.enableStartDrag -eq $true) { X "$inner<EnableStartDrag>true</EnableStartDrag>" }
	if ($el.enableDrag -eq $true) { X "$inner<EnableDrag>true</EnableDrag>" }
	if ($el.rowPictureDataPath) { X "$inner<RowPictureDataPath>$($el.rowPictureDataPath)</RowPictureDataPath>" }

	# Companions
	Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
	# AutoCommandBar — with optional Autofill control
	if ($null -ne $el.tableAutofill) {
		$acbId = New-Id
		X "$inner<AutoCommandBar name=`"${name}КоманднаяПанель`" id=`"$acbId`">"
		$afVal = if ($el.tableAutofill) { "true" } else { "false" }
		X "$inner`t<Autofill>$afVal</Autofill>"
		X "$inner</AutoCommandBar>"
	} else {
		Emit-Companion -tag "AutoCommandBar" -name "${name}КоманднаяПанель" -indent $inner
	}
	Emit-Companion -tag "SearchStringAddition" -name "${name}СтрокаПоиска" -indent $inner
	Emit-Companion -tag "ViewStatusAddition" -name "${name}СостояниеПросмотра" -indent $inner
	Emit-Companion -tag "SearchControlAddition" -name "${name}УправлениеПоиском" -indent $inner

	# Columns
	if ($el.columns -and $el.columns.Count -gt 0) {
		X "$inner<ChildItems>"
		foreach ($col in $el.columns) {
			Emit-Element -el $col -indent "$inner`t"
		}
		X "$inner</ChildItems>"
	}

	Emit-Events -el $el -elementName $name -indent $inner -typeKey "table"

	X "$indent</Table>"
}

function Emit-Pages {
	param($el, [string]$name, [int]$id, [string]$indent)

	X "$indent<Pages name=`"$name`" id=`"$id`">"
	$inner = "$indent`t"

	if ($el.pagesRepresentation) {
		X "$inner<PagesRepresentation>$($el.pagesRepresentation)</PagesRepresentation>"
	}

	Emit-CommonFlags -el $el -indent $inner

	# Companion
	Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner

	Emit-Events -el $el -elementName $name -indent $inner -typeKey "pages"

	# Children (pages)
	if ($el.children -and $el.children.Count -gt 0) {
		X "$inner<ChildItems>"
		foreach ($child in $el.children) {
			Emit-Element -el $child -indent "$inner`t"
		}
		X "$inner</ChildItems>"
	}

	X "$indent</Pages>"
}

function Emit-Page {
	param($el, [string]$name, [int]$id, [string]$indent)

	X "$indent<Page name=`"$name`" id=`"$id`">"
	$inner = "$indent`t"

	Emit-Title -el $el -name $name -indent $inner -auto
	Emit-CommonFlags -el $el -indent $inner

	if ($el.group) {
		$orientation = switch ("$($el.group)") {
			"horizontal"       { "Horizontal" }
			"vertical"         { "Vertical" }
			"alwaysHorizontal" { "AlwaysHorizontal" }
			"alwaysVertical"   { "AlwaysVertical" }
			default            { $null }
		}
		if ($orientation) { X "$inner<Group>$orientation</Group>" }
	}

	# Companion
	Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner

	# Children
	if ($el.children -and $el.children.Count -gt 0) {
		X "$inner<ChildItems>"
		foreach ($child in $el.children) {
			Emit-Element -el $child -indent "$inner`t"
		}
		X "$inner</ChildItems>"
	}

	X "$indent</Page>"
}

function Emit-Button {
	param($el, [string]$name, [int]$id, [string]$indent, [bool]$inCmdBar = $false)

	X "$indent<Button name=`"$name`" id=`"$id`">"
	$inner = "$indent`t"

	# Type — context-aware:
	# Inside command bar (cmdBar/autoCmdBar/popup) only CommandBarButton/CommandBarHyperlink are valid.
	# UsualButton/Hyperlink would be silently ignored by 1C.
	$btnType = $null
	if ($el.type) {
		$rawType = "$($el.type)"
		if ($inCmdBar) {
			# Be forgiving: any "ordinary button" hint resolves to CommandBarButton,
			# any "hyperlink" hint resolves to CommandBarHyperlink. The model can pass
			# either DSL ("usual"/"hyperlink") or XML names — all map to the right kind.
			switch ($rawType) {
				"usual"                { $btnType = "CommandBarButton" }
				"UsualButton"          { $btnType = "CommandBarButton" }
				"commandBar"           { $btnType = "CommandBarButton" }
				"CommandBarButton"     { $btnType = "CommandBarButton" }
				"hyperlink"            { $btnType = "CommandBarHyperlink" }
				"Hyperlink"            { $btnType = "CommandBarHyperlink" }
				"CommandBarHyperlink"  { $btnType = "CommandBarHyperlink" }
				default                { $btnType = $rawType }
			}
		} else {
			# Symmetric: any "ordinary button" hint → UsualButton, any "hyperlink" → Hyperlink.
			switch ($rawType) {
				"usual"                { $btnType = "UsualButton" }
				"UsualButton"          { $btnType = "UsualButton" }
				"commandBar"           { $btnType = "UsualButton" }
				"CommandBarButton"     { $btnType = "UsualButton" }
				"hyperlink"            { $btnType = "Hyperlink" }
				"Hyperlink"            { $btnType = "Hyperlink" }
				"CommandBarHyperlink"  { $btnType = "Hyperlink" }
				default                { $btnType = $rawType }
			}
		}
	} elseif ($inCmdBar) {
		$btnType = "CommandBarButton"
	}
	if ($btnType) {
		X "$inner<Type>$btnType</Type>"
	}

	# CommandName
	if ($el.command) {
		X "$inner<CommandName>Form.Command.$($el.command)</CommandName>"
	}
	if ($el.stdCommand) {
		$sc = "$($el.stdCommand)"
		if ($sc -match '^(.+)\.(.+)$') {
			X "$inner<CommandName>Form.Item.$($Matches[1]).StandardCommand.$($Matches[2])</CommandName>"
		} else {
			X "$inner<CommandName>Form.StandardCommand.$sc</CommandName>"
		}
	}

	$btnAuto = -not ($el.command -or $el.stdCommand)
	Emit-Title -el $el -name $name -indent $inner -auto:$btnAuto
	Emit-CommonFlags -el $el -indent $inner

	if ($el.defaultButton -eq $true) { X "$inner<DefaultButton>true</DefaultButton>" }

	# Picture
	if ($el.picture) {
		X "$inner<Picture>"
		X "$inner`t<xr:Ref>$($el.picture)</xr:Ref>"
		X "$inner`t<xr:LoadTransparent>true</xr:LoadTransparent>"
		X "$inner</Picture>"
	}

	if ($el.representation) {
		X "$inner<Representation>$($el.representation)</Representation>"
	}

	if ($el.locationInCommandBar) {
		X "$inner<LocationInCommandBar>$($el.locationInCommandBar)</LocationInCommandBar>"
	}

	# Companion
	Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner

	Emit-Events -el $el -elementName $name -indent $inner -typeKey "button"

	X "$indent</Button>"
}

function Emit-PictureDecoration {
	param($el, [string]$name, [int]$id, [string]$indent)

	X "$indent<PictureDecoration name=`"$name`" id=`"$id`">"
	$inner = "$indent`t"

	Emit-Title -el $el -name $name -indent $inner
	Emit-CommonFlags -el $el -indent $inner

	if ($el.picture -or $el.src) {
		$ref = if ($el.src) { "$($el.src)" } else { "$($el.picture)" }
		X "$inner<Picture>"
		X "$inner`t<xr:Ref>$ref</xr:Ref>"
		X "$inner`t<xr:LoadTransparent>true</xr:LoadTransparent>"
		X "$inner</Picture>"
	}

	if ($el.hyperlink -eq $true) { X "$inner<Hyperlink>true</Hyperlink>" }
	if ($el.width) { X "$inner<Width>$($el.width)</Width>" }
	if ($el.height) { X "$inner<Height>$($el.height)</Height>" }

	# Companions
	Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
	Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner

	Emit-Events -el $el -elementName $name -indent $inner -typeKey "picture"

	X "$indent</PictureDecoration>"
}

function Emit-PictureField {
	param($el, [string]$name, [int]$id, [string]$indent)

	X "$indent<PictureField name=`"$name`" id=`"$id`">"
	$inner = "$indent`t"

	if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }

	Emit-Title -el $el -name $name -indent $inner
	Emit-CommonFlags -el $el -indent $inner

	if ($el.width) { X "$inner<Width>$($el.width)</Width>" }
	if ($el.height) { X "$inner<Height>$($el.height)</Height>" }

	# Companions
	Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
	Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner

	Emit-Events -el $el -elementName $name -indent $inner -typeKey "picField"

	X "$indent</PictureField>"
}

function Emit-Calendar {
	param($el, [string]$name, [int]$id, [string]$indent)

	X "$indent<CalendarField name=`"$name`" id=`"$id`">"
	$inner = "$indent`t"

	if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }

	Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
	Emit-CommonFlags -el $el -indent $inner

	# Companions
	Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
	Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner

	Emit-Events -el $el -elementName $name -indent $inner -typeKey "calendar"

	X "$indent</CalendarField>"
}

function Emit-CommandBar {
	param($el, [string]$name, [int]$id, [string]$indent)

	X "$indent<CommandBar name=`"$name`" id=`"$id`">"
	$inner = "$indent`t"

	if ($el.autofill -eq $true) { X "$inner<Autofill>true</Autofill>" }

	Emit-CommonFlags -el $el -indent $inner

	# Children
	if ($el.children -and $el.children.Count -gt 0) {
		X "$inner<ChildItems>"
		foreach ($child in $el.children) {
			Emit-Element -el $child -indent "$inner`t" -inCmdBar $true
		}
		X "$inner</ChildItems>"
	}

	X "$indent</CommandBar>"
}

function Emit-Popup {
	param($el, [string]$name, [int]$id, [string]$indent)

	X "$indent<Popup name=`"$name`" id=`"$id`">"
	$inner = "$indent`t"

	Emit-Title -el $el -name $name -indent $inner -auto
	Emit-CommonFlags -el $el -indent $inner

	if ($el.picture) {
		X "$inner<Picture>"
		X "$inner`t<xr:Ref>$($el.picture)</xr:Ref>"
		X "$inner`t<xr:LoadTransparent>true</xr:LoadTransparent>"
		X "$inner</Picture>"
	}

	if ($el.representation) {
		X "$inner<Representation>$($el.representation)</Representation>"
	}

	# Children
	if ($el.children -and $el.children.Count -gt 0) {
		X "$inner<ChildItems>"
		foreach ($child in $el.children) {
			Emit-Element -el $child -indent "$inner`t" -inCmdBar $true
		}
		X "$inner</ChildItems>"
	}

	X "$indent</Popup>"
}

# --- 8. Attribute emitter ---

function Emit-Attributes {
	param($attrs, [string]$indent)

	if (-not $attrs -or $attrs.Count -eq 0) { return }

	X "$indent<Attributes>"
	foreach ($attr in $attrs) {
		$attrId = New-Id
		$attrName = "$($attr.name)"

		X "$indent`t<Attribute name=`"$attrName`" id=`"$attrId`">"
		$inner = "$indent`t`t"

		$attrTitle = if ($attr.title) { "$($attr.title)" } elseif ($attr.main -ne $true) { Title-FromName -name $attrName } else { '' }
		if ($attrTitle) {
			Emit-MLText -tag "Title" -text "$attrTitle" -indent $inner
		}

		# Type
		if ($attr.type) {
			Emit-Type -typeStr "$($attr.type)" -indent $inner
		} else {
			X "$inner<Type/>"
		}

		if ($attr.main -eq $true) {
			X "$inner<MainAttribute>true</MainAttribute>"
		}
		$mainSaved = $false
		if ($attr.main -eq $true -and $attr.type) {
			$mainSaved = ("$($attr.type)") -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.' -or ("$($attr.type)") -match 'RecordManager\.'
		}
		if ($attr.savedData -eq $true -or $mainSaved) {
			X "$inner<SavedData>true</SavedData>"
		}
		if ($attr.fillChecking) {
			X "$inner<FillChecking>$($attr.fillChecking)</FillChecking>"
		}

		# Columns (for ValueTable/ValueTree)
		if ($attr.columns -and $attr.columns.Count -gt 0) {
			X "$inner<Columns>"
			foreach ($col in $attr.columns) {
				$colId = New-Id
				X "$inner`t<Column name=`"$($col.name)`" id=`"$colId`">"
				if ($col.title) {
					Emit-MLText -tag "Title" -text "$($col.title)" -indent "$inner`t`t"
				}
				Emit-Type -typeStr "$($col.type)" -indent "$inner`t`t"
				X "$inner`t</Column>"
			}
			X "$inner</Columns>"
		}

		# Settings (for DynamicList)
		if ($attr.settings) {
			X "$inner<Settings xsi:type=`"DynamicList`">"
			$si = "$inner`t"
			if ($attr.settings.mainTable) { X "$si<MainTable>$($attr.settings.mainTable)</MainTable>" }
			$mq = if ($attr.settings.manualQuery -eq $true) { "true" } else { "false" }
			X "$si<ManualQuery>$mq</ManualQuery>"
			$ddr = if ($attr.settings.dynamicDataRead -eq $true) { "true" } else { "false" }
			X "$si<DynamicDataRead>$ddr</DynamicDataRead>"
			X "$inner</Settings>"
		}

		X "$indent`t</Attribute>"
	}
	X "$indent</Attributes>"
}

# --- 9. Parameter emitter ---

function Emit-Parameters {
	param($params, [string]$indent)

	if (-not $params -or $params.Count -eq 0) { return }

	X "$indent<Parameters>"
	foreach ($param in $params) {
		X "$indent`t<Parameter name=`"$($param.name)`">"
		$inner = "$indent`t`t"

		Emit-Type -typeStr "$($param.type)" -indent $inner

		if ($param.key -eq $true) {
			X "$inner<KeyParameter>true</KeyParameter>"
		}

		X "$indent`t</Parameter>"
	}
	X "$indent</Parameters>"
}

# --- 10. Command emitter ---

function Emit-Commands {
	param($cmds, [string]$indent)

	if (-not $cmds -or $cmds.Count -eq 0) { return }

	X "$indent<Commands>"
	foreach ($cmd in $cmds) {
		$cmdId = New-Id
		X "$indent`t<Command name=`"$($cmd.name)`" id=`"$cmdId`">"
		$inner = "$indent`t`t"

		$cmdTitle = if ($cmd.title) { "$($cmd.title)" } else { Title-FromName -name "$($cmd.name)" }
		if ($cmdTitle) {
			Emit-MLText -tag "Title" -text "$cmdTitle" -indent $inner
		}

		if ($cmd.action) {
			X "$inner<Action>$($cmd.action)</Action>"
		}

		if ($cmd.shortcut) {
			X "$inner<Shortcut>$($cmd.shortcut)</Shortcut>"
		}

		if ($cmd.picture) {
			X "$inner<Picture>"
			X "$inner`t<xr:Ref>$($cmd.picture)</xr:Ref>"
			X "$inner`t<xr:LoadTransparent>true</xr:LoadTransparent>"
			X "$inner</Picture>"
		}

		if ($cmd.representation) {
			X "$inner<Representation>$($cmd.representation)</Representation>"
		}

		X "$indent`t</Command>"
	}
	X "$indent</Commands>"
}

# --- 11. Properties emitter ---

function Emit-Properties {
	param($props, [string]$indent)

	if (-not $props) { return }

	# camelCase -> PascalCase mapping for known properties
	$propMap = @{
		"autoTitle"              = "AutoTitle"
		"windowOpeningMode"      = "WindowOpeningMode"
		"commandBarLocation"     = "CommandBarLocation"
		"saveDataInSettings"     = "SaveDataInSettings"
		"autoSaveDataInSettings" = "AutoSaveDataInSettings"
		"autoTime"               = "AutoTime"
		"usePostingMode"         = "UsePostingMode"
		"repostOnWrite"          = "RepostOnWrite"
		"autoURL"                = "AutoURL"
		"autoFillCheck"          = "AutoFillCheck"
		"customizable"           = "Customizable"
		"enterKeyBehavior"       = "EnterKeyBehavior"
		"verticalScroll"         = "VerticalScroll"
		"scalingMode"            = "ScalingMode"
		"useForFoldersAndItems"  = "UseForFoldersAndItems"
		"reportResult"           = "ReportResult"
		"detailsData"            = "DetailsData"
		"reportFormType"         = "ReportFormType"
		"autoShowState"          = "AutoShowState"
		"width"                  = "Width"
		"height"                 = "Height"
		"group"                  = "Group"
	}

	foreach ($p in $props.PSObject.Properties) {
		$xmlName = if ($propMap.ContainsKey($p.Name)) { $propMap[$p.Name] } else {
			# Auto PascalCase: first letter uppercase
			$p.Name.Substring(0,1).ToUpper() + $p.Name.Substring(1)
		}
		# Convert boolean to lowercase string (PS renders as True/False)
		$val = $p.Value
		if ($val -is [bool]) {
			$val = if ($val) { "true" } else { "false" }
		}
		X "$indent<$xmlName>$val</$xmlName>"
	}
}

# --- 11b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction ---

function Normalize-ElementSynonyms {
	param($el)
	if ($null -eq $el) { return }
	$synonyms = @{ "commandBar" = "cmdBar"; "autoCommandBar" = "autoCmdBar" }
	foreach ($pair in $synonyms.GetEnumerator()) {
		if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) {
			$val = $el.($pair.Key)
			$el.PSObject.Properties.Remove($pair.Key) | Out-Null
			$el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force
		}
	}
	if ($el.PSObject.Properties["children"] -and $el.children) {
		foreach ($child in $el.children) { Normalize-ElementSynonyms $child }
	}
	if ($el.PSObject.Properties["columns"] -and $el.columns) {
		foreach ($child in $el.columns) { Normalize-ElementSynonyms $child }
	}
}

function HasCmdBarRecursive {
	param($el)
	if ($null -eq $el) { return $false }
	if ($el.PSObject.Properties["cmdBar"] -and $null -ne $el.cmdBar) { return $true }
	if ($el.PSObject.Properties["children"] -and $el.children) {
		foreach ($child in $el.children) { if (HasCmdBarRecursive $child) { return $true } }
	}
	if ($el.PSObject.Properties["columns"] -and $el.columns) {
		foreach ($child in $el.columns) { if (HasCmdBarRecursive $child) { return $true } }
	}
	return $false
}

function ApplyDynamicListTableHeuristic {
	param($el, [string]$listName, [bool]$hasMainTable)
	if ($null -eq $el) { return }
	if ($el.PSObject.Properties["table"] -and $null -ne $el.table -and "$($el.path)" -eq $listName) {
		if ($null -eq $el.PSObject.Properties["tableAutofill"]) {
			$el | Add-Member -NotePropertyName "tableAutofill" -NotePropertyValue $false -Force
		}
		if ($null -eq $el.PSObject.Properties["commandBarLocation"]) {
			$el | Add-Member -NotePropertyName "commandBarLocation" -NotePropertyValue "None" -Force
		}
		# DefaultPicture доступен только если у DynamicList есть основная таблица
		if ($hasMainTable -and ($null -eq $el.PSObject.Properties["rowPictureDataPath"] -or [string]::IsNullOrEmpty("$($el.rowPictureDataPath)"))) {
			$el | Add-Member -NotePropertyName "rowPictureDataPath" -NotePropertyValue "$listName.DefaultPicture" -Force
		}
	}
	if ($el.PSObject.Properties["children"] -and $el.children) {
		foreach ($child in $el.children) { ApplyDynamicListTableHeuristic $child $listName $hasMainTable }
	}
}

function Test-IsObjectLikeType {
	param([string]$type)
	if ([string]::IsNullOrEmpty($type)) { return $false }
	if ($type -eq "DynamicList" -or $type -eq "ConstantsSet") { return $true }
	$objectSuffixes = @(
		"CatalogObject", "DocumentObject", "DataProcessorObject", "ReportObject",
		"ExternalDataProcessorObject", "ExternalReportObject", "BusinessProcessObject",
		"TaskObject", "ChartOfAccountsObject", "ChartOfCharacteristicTypesObject",
		"ChartOfCalculationTypesObject", "ExchangePlanObject"
	)
	$recordSetPrefixes = @(
		"InformationRegisterRecordSet", "AccumulationRegisterRecordSet",
		"AccountingRegisterRecordSet", "CalculationRegisterRecordSet",
		"InformationRegisterRecordManager"
	)
	foreach ($suffix in $objectSuffixes) {
		if ($type -like "$suffix.*") { return $true }
	}
	foreach ($prefix in $recordSetPrefixes) {
		if ($type -like "$prefix.*") { return $true }
	}
	return $false
}

# 11b.1: Normalize synonyms recursively
if ($def.elements) {
	foreach ($el in $def.elements) { Normalize-ElementSynonyms $el }
}

# 11b.2: Extract autoCmdBar element from def.elements
$script:mainAcbDef = $null
if ($def.elements) {
	$autoBars = @()
	$rest = @()
	foreach ($el in $def.elements) {
		if ($null -ne $el.PSObject.Properties["autoCmdBar"] -and $null -ne $el.autoCmdBar) {
			$autoBars += $el
		} else {
			$rest += $el
		}
	}
	if ($autoBars.Count -gt 1) {
		Write-Error "form-compile: more than one autoCmdBar in def.elements (found $($autoBars.Count)); only one allowed."
		exit 1
	}
	if ($autoBars.Count -eq 1) {
		$script:mainAcbDef = $autoBars[0]
		# Replace def.elements with the filtered list
		$def.PSObject.Properties.Remove("elements") | Out-Null
		$def | Add-Member -NotePropertyName "elements" -NotePropertyValue $rest -Force
	}
}

# 11b.3: Infer main attribute (only if no attribute has main:true)
if ($def.attributes) {
	$hasExplicitMain = $false
	foreach ($attr in $def.attributes) {
		if ($attr.main -eq $true) { $hasExplicitMain = $true; break }
	}
	if (-not $hasExplicitMain) {
		$candidates = @()
		foreach ($attr in $def.attributes) {
			# Skip if user explicitly opted out via main:false
			if ($null -ne $attr.PSObject.Properties["main"] -and $attr.main -eq $false) { continue }
			if (Test-IsObjectLikeType "$($attr.type)") {
				$candidates += $attr
			}
		}
		if ($candidates.Count -eq 1) {
			$candidates[0] | Add-Member -NotePropertyName "main" -NotePropertyValue $true -Force
			Write-Host "[INFO] Inferred main attribute: $($candidates[0].name) ($($candidates[0].type))"
		} elseif ($candidates.Count -gt 1) {
			$names = ($candidates | ForEach-Object { $_.name }) -join ", "
			Write-Host "[WARN] Multiple main-attribute candidates: $names; specify ""main"": true explicitly"
		}
	}
}

# 11b.4: DynamicList → table heuristic
if ($def.attributes -and $def.elements) {
	$mainAttr = $null
	foreach ($attr in $def.attributes) {
		if ($attr.main -eq $true) { $mainAttr = $attr; break }
	}
	if ($mainAttr -and "$($mainAttr.type)" -eq "DynamicList") {
		$mt = $null
		if ($mainAttr.PSObject.Properties["settings"] -and $null -ne $mainAttr.settings) {
			if ($mainAttr.settings -is [hashtable]) {
				if ($mainAttr.settings.ContainsKey("mainTable")) { $mt = $mainAttr.settings["mainTable"] }
			} elseif ($mainAttr.settings.PSObject.Properties["mainTable"]) {
				$mt = $mainAttr.settings.mainTable
			}
		}
		$hasMt = -not [string]::IsNullOrEmpty("$mt")
		foreach ($el in $def.elements) {
			ApplyDynamicListTableHeuristic $el $mainAttr.name $hasMt
		}
	}
}

# 11b.5: Compute main AutoCommandBar Autofill via heuristic B3
function Compute-MainAcbAutofill {
	if ($script:mainAcbDef) {
		if ($null -ne $script:mainAcbDef.PSObject.Properties["autofill"]) {
			return [bool]$script:mainAcbDef.autofill
		}
		return $true
	}
	if ($def.elements) {
		foreach ($el in $def.elements) {
			if (HasCmdBarRecursive $el) { return $false }
		}
	}
	return $true
}

# --- 12. Main compilation ---

# Title
if ($def.title) {
	Emit-MLText -tag "Title" -text "$($def.title)" -indent "`t"
}

# Header
X '<?xml version="1.0" encoding="UTF-8"?>'
X "<Form 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:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" 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`" version=`"$($script:formatVersion)`">"

# Oops — Title was emitted before header. Need to fix the order.
# Actually, let me restructure: build the body into a separate buffer, then assemble

# Reset and rebuild properly
$script:xml = New-Object System.Text.StringBuilder 8192
$script:nextId = 1

X '<?xml version="1.0" encoding="UTF-8"?>'
X "<Form 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:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" 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`" version=`"$($script:formatVersion)`">"

# 12a. Title (from def.title or properties.title — must be multilingual XML)
$formTitle = $def.title
if (-not $formTitle -and $def.properties -and $def.properties.title) {
	$formTitle = $def.properties.title
}
if ($formTitle) {
	Emit-MLText -tag "Title" -text "$formTitle" -indent "`t"
}

# 12b. Properties (skip 'title' — handled above as multilingual)
# When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this;
# otherwise platform appends synonym → "Title: Synonym" double-titles).
$propsClone = New-Object PSObject
$hasAutoTitle = $false
if ($def.properties) {
	foreach ($p in $def.properties.PSObject.Properties) {
		if ($p.Name -eq "autoTitle") { $hasAutoTitle = $true }
	}
}
if ($formTitle -and -not $hasAutoTitle) {
	$propsClone | Add-Member -NotePropertyName "autoTitle" -NotePropertyValue $false
}
if ($def.properties) {
	foreach ($p in $def.properties.PSObject.Properties) {
		if ($p.Name -ne "title") {
			$propsClone | Add-Member -NotePropertyName $p.Name -NotePropertyValue $p.Value
		}
	}
}
Emit-Properties -props $propsClone -indent "`t"

# 12c. CommandSet (excluded commands)
if ($def.excludedCommands -and $def.excludedCommands.Count -gt 0) {
	X "`t<CommandSet>"
	foreach ($cmd in $def.excludedCommands) {
		X "`t`t<ExcludedCommand>$cmd</ExcludedCommand>"
	}
	X "`t</CommandSet>"
}

# 12d. AutoCommandBar (always present, id=-1)
$acbAutofill = Compute-MainAcbAutofill
$acbName = "ФормаКоманднаяПанель"
$acbHAlign = $null
if ($script:mainAcbDef) {
	if ($null -ne $script:mainAcbDef.PSObject.Properties["autoCmdBar"] -and "$($script:mainAcbDef.autoCmdBar)" -ne "") {
		$acbName = "$($script:mainAcbDef.autoCmdBar)"
	}
	if ($null -ne $script:mainAcbDef.PSObject.Properties["name"] -and "$($script:mainAcbDef.name)" -ne "") {
		$acbName = "$($script:mainAcbDef.name)"
	}
	if ($null -ne $script:mainAcbDef.PSObject.Properties["horizontalAlign"] -and "$($script:mainAcbDef.horizontalAlign)" -ne "") {
		$acbHAlign = "$($script:mainAcbDef.horizontalAlign)"
	}
}
$hasAcbChildren = ($script:mainAcbDef -and $script:mainAcbDef.children -and $script:mainAcbDef.children.Count -gt 0)
$acbHasInner = ($acbHAlign -or (-not $acbAutofill) -or $hasAcbChildren)
if ($acbHasInner) {
	X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`">"
	if ($acbHAlign) { X "`t`t<HorizontalAlign>$acbHAlign</HorizontalAlign>" }
	if (-not $acbAutofill) { X "`t`t<Autofill>false</Autofill>" }
	if ($hasAcbChildren) {
		X "`t`t<ChildItems>"
		foreach ($child in $script:mainAcbDef.children) {
			Emit-Element -el $child -indent "`t`t`t" -inCmdBar $true
		}
		X "`t`t</ChildItems>"
	}
	X "`t</AutoCommandBar>"
} else {
	X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`"/>"
}

# 12e. Events
if ($def.events) {
	foreach ($p in $def.events.PSObject.Properties) {
		if ($script:knownFormEvents -notcontains $p.Name) {
			Write-Host "[WARN] Unknown form event '$($p.Name)'. Known: $($script:knownFormEvents -join ', ')"
		}
	}
	X "`t<Events>"
	foreach ($p in $def.events.PSObject.Properties) {
		X "`t`t<Event name=`"$($p.Name)`">$($p.Value)</Event>"
	}
	X "`t</Events>"
}

# 12f. ChildItems (elements)
if ($def.elements -and $def.elements.Count -gt 0) {
	X "`t<ChildItems>"
	foreach ($el in $def.elements) {
		Emit-Element -el $el -indent "`t`t"
	}
	X "`t</ChildItems>"
}

# 12g. Attributes
Emit-Attributes -attrs $def.attributes -indent "`t"

# 12h. Parameters
Emit-Parameters -params $def.parameters -indent "`t"

# 12i. Commands
Emit-Commands -cmds $def.commands -indent "`t"

# 12j. Close
X '</Form>'

# --- 13. Write output ---

$outPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath }
$outDir = [System.IO.Path]::GetDirectoryName($outPath)
if (-not (Test-Path $outDir)) {
	New-Item -ItemType Directory -Path $outDir -Force | Out-Null
}

$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($outPath, $xml.ToString(), $enc)

# --- 13b. Auto-register form in parent object XML ---

# Infer parent from OutputPath: .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml
$formXmlDir   = [System.IO.Path]::GetDirectoryName($outPath)
$formNameDir  = [System.IO.Path]::GetDirectoryName($formXmlDir)
$formsDir     = [System.IO.Path]::GetDirectoryName($formNameDir)
$objectDir    = [System.IO.Path]::GetDirectoryName($formsDir)
$typePluralDir = [System.IO.Path]::GetDirectoryName($objectDir)

$formName   = [System.IO.Path]::GetFileName($formNameDir)
$objectName = [System.IO.Path]::GetFileName($objectDir)
$formsLeaf  = [System.IO.Path]::GetFileName($formsDir)

if ($formsLeaf -eq 'Forms') {
	$objectXmlPath = Join-Path $typePluralDir "$objectName.xml"
	if (Test-Path $objectXmlPath) {
		$objDoc = New-Object System.Xml.XmlDocument
		$objDoc.PreserveWhitespace = $true
		$objDoc.Load($objectXmlPath)

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

		$childObjects = $objDoc.SelectSingleNode("//md:ChildObjects", $nsMgr)
		if ($childObjects) {
			$existing = $childObjects.SelectSingleNode("md:Form[text()='$formName']", $nsMgr)
			if (-not $existing) {
				$formElem = $objDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
				$formElem.InnerText = $formName

				$insertBefore = $childObjects.SelectSingleNode("md:Template", $nsMgr)
				if (-not $insertBefore) { $insertBefore = $childObjects.SelectSingleNode("md:TabularSection", $nsMgr) }

				if ($insertBefore) {
					$childObjects.InsertBefore($formElem, $insertBefore) | Out-Null
					$ws = $objDoc.CreateWhitespace("`n`t`t`t")
					$childObjects.InsertBefore($ws, $insertBefore) | Out-Null
				} else {
					$lastChild = $childObjects.LastChild
					if ($lastChild -and $lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
						$childObjects.InsertBefore($objDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null
						$childObjects.InsertBefore($formElem, $lastChild) | Out-Null
					} else {
						$childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
						$childObjects.AppendChild($formElem) | Out-Null
						$childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t")) | Out-Null
					}
				}

				$regEnc = New-Object System.Text.UTF8Encoding($true)
				$regSettings = New-Object System.Xml.XmlWriterSettings
				$regSettings.Encoding = $regEnc
				$regSettings.Indent = $false
				$regStream = New-Object System.IO.FileStream($objectXmlPath, [System.IO.FileMode]::Create)
				$regWriter = [System.Xml.XmlWriter]::Create($regStream, $regSettings)
				$objDoc.Save($regWriter)
				$regWriter.Close()
				$regStream.Close()

				Write-Host "     Registered: <Form>$formName</Form> in $objectName.xml"
			}
		}
	}
}

# --- 14. Summary ---

$elCount = $script:nextId - 1
Write-Host "[OK] Compiled: $OutputPath"
Write-Host "     Elements+IDs: $elCount"
if ($def.attributes) { Write-Host "     Attributes: $($def.attributes.Count)" }
if ($def.commands)   { Write-Host "     Commands: $($def.commands.Count)" }
if ($def.parameters) { Write-Host "     Parameters: $($def.parameters.Count)" }
