feat(meta-compile,meta-decompile): предопределённые элементы Catalog (Ext/Predefined.xml) (v1.19/v0.5)

Раундтрип-находка (класс-3, непокрытый блок — крупнейшая категория ~56k). meta-compile вообще
не генерил Ext/Predefined.xml. DSL спроектирован по данным корпуса (9309 элементов) + совместно.

DSL `predefined` (массив; строка ИЛИ объект):
- строка "(Код) Имя [Наименование]": Имя обяз.; Наименование — нет [..] → авто(Split-CamelCase),
  [] → пусто (системные/placeholder, 29% корпуса), [текст] → задано;
- объект (группа/иерархия): name/code/description/isFolder/childItems + рус.синонимы
  имя/код/наименование/группа/подчиненные (прощающий ввод).
- Код: по codeType каталога (Number→xs:decimal, String→без типа), пусто→<Code/>.

meta-compile (дуал-порт): генерация Ext/Predefined.xml (root CatalogPredefinedItems + рекурсия Item).
Заодно фикс: Esc-XmlText для ТЕКСТА элемента (& < > без экранирования кавычек — в тексте 1С держит raw).
meta-decompile: захват predefined → компактный DSL (строка/объект). Фикс PS-ловушки распаковки
одноэлементного @() из if → папки с ОДНИМ ребёнком теряли его.
spec meta-dsl-spec.md v2.4 §7.1.2; тест-кейс catalog-predefined.

Валидация: PS==PY паритет; predefined-категории 0 (PredefinedData/Item/ChildItems/Description/Code);
полный прогон −56620 (218537→161917); регресс 36/36 (ps+py); 1С-cert зелёный (грузится в платформу).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-01 19:48:16 +03:00
parent 58bc95264e
commit 253a4fbce0
11 changed files with 640 additions and 16 deletions
@@ -330,6 +330,11 @@ function Esc-Xml {
param([string]$s)
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;').Replace('"','&quot;')
}
# Эскейп ТЕКСТА элемента: только & < > (кавычки в тексте 1С держит raw, экранирование только для атрибутов).
function Esc-XmlText {
param([string]$s)
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}
# ML-значение: строка → один <v8:item> ru; объект {lang: content} → item на язык (в порядке ключей).
function Emit-MLItems {
@@ -3070,6 +3075,59 @@ $typesNoSubDir = @("DefinedType","ScheduledJob","EventSubscription")
# Object subdirectory: {OutputDir}/{TypePlural}/{Name}/Ext/
$objSubDir = Join-Path $typeDir $objName
# --- Predefined data (Ext/Predefined.xml) ---
# Элемент DSL: строка "(Код) Имя [Наименование]" ЛИБО объект (+ русские синонимы ключей).
# Наименование: нет [..]/ключа → авто(Split-CamelCase Имени); [] / "" → пусто; [текст]/текст → как есть.
function Resolve-PredefItem {
param($val)
if ($val -is [string]) {
$m = [regex]::Match($val, '^\s*(?:\(([^)]*)\)\s*)?(\S+)(?:\s*\[(.*)\])?\s*$')
$name = $m.Groups[2].Value
$code = if ($m.Groups[1].Success) { $m.Groups[1].Value } else { '' }
$desc = if ($m.Groups[3].Success) { $m.Groups[3].Value } else { Split-CamelCase $name }
return @{ name = $name; code = $code; desc = $desc; isFolder = $false; children = @() }
}
# Объектная форма + русские синонимы (прощающий ввод).
$gv = { param($o, [string[]]$keys) foreach ($k in $keys) { if ($o.PSObject.Properties[$k]) { return $o.$k } } return $null }
$name = "$(& $gv $val @('name','имя'))"
$codeV = & $gv $val @('code','код')
$code = if ($null -ne $codeV) { "$codeV" } else { '' }
$hasDesc = $val.PSObject.Properties['description'] -or $val.PSObject.Properties['наименование']
$descV = & $gv $val @('description','наименование')
$desc = if ($hasDesc) { "$descV" } else { Split-CamelCase $name } # ключа нет → авто; '' → пусто
$folderV = & $gv $val @('isFolder','группа')
$isFolder = ($folderV -eq $true)
$subs = & $gv $val @('childItems','подчиненные')
return @{ name = $name; code = $code; desc = $desc; isFolder = $isFolder; children = @(if ($subs) { $subs } else { @() }) }
}
function Emit-PredefItem {
param($sb, $val, [string]$indent, [string]$codeType)
$r = Resolve-PredefItem $val
[void]$sb.Append("$indent<Item id=`"$(New-Guid-String)`">`n")
[void]$sb.Append("$indent`t<Name>$(Esc-XmlText $r.name)</Name>`n")
if (-not $r.code) { [void]$sb.Append("$indent`t<Code/>`n") }
elseif ($codeType -eq 'Number') { [void]$sb.Append("$indent`t<Code xsi:type=`"xs:decimal`">$(Esc-XmlText $r.code)</Code>`n") }
else { [void]$sb.Append("$indent`t<Code>$(Esc-XmlText $r.code)</Code>`n") }
if ($r.desc -eq '') { [void]$sb.Append("$indent`t<Description/>`n") }
else { [void]$sb.Append("$indent`t<Description>$(Esc-XmlText $r.desc)</Description>`n") }
[void]$sb.Append("$indent`t<IsFolder>$(if ($r.isFolder) { 'true' } else { 'false' })</IsFolder>`n")
if ($r.children.Count -gt 0) {
[void]$sb.Append("$indent`t<ChildItems>`n")
foreach ($c in $r.children) { Emit-PredefItem $sb $c "$indent`t`t" $codeType }
[void]$sb.Append("$indent`t</ChildItems>`n")
}
[void]$sb.Append("$indent</Item>`n")
}
function Build-PredefinedXml {
param($items, [string]$xsiType, [string]$codeType)
$sb = New-Object System.Text.StringBuilder
[void]$sb.Append("<?xml version=`"1.0`" encoding=`"UTF-8`"?>`n")
[void]$sb.Append("<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"$xsiType`" version=`"$($script:formatVersion)`">`n")
foreach ($it in $items) { Emit-PredefItem $sb $it "`t" $codeType }
[void]$sb.Append("</PredefinedData>`n")
return $sb.ToString()
}
$extDir = Join-Path $objSubDir "Ext"
if (-not (Test-Path $typeDir)) {
@@ -3168,6 +3226,16 @@ if ($objType -eq "BusinessProcess") {
}
}
# Предопределённые элементы (Ext/Predefined.xml) — пока Catalog. Пусто/нет ключа → файл не создаём.
if ($objType -eq "Catalog" -and $def.predefined -and @($def.predefined).Count -gt 0) {
Ensure-ExtDir
$catCodeType = if ($def.codeType) { "$($def.codeType)" } else { 'String' }
$predefXml = Build-PredefinedXml @($def.predefined) "CatalogPredefinedItems" $catCodeType
$predefPath = Join-Path $extDir "Predefined.xml"
[System.IO.File]::WriteAllText($predefPath, $predefXml, $enc)
$modulesCreated += $predefPath
}
# --- 17. Register in Configuration.xml ---
$configXmlPath = Join-Path $OutputDir "Configuration.xml"
@@ -181,6 +181,10 @@ def assert_edit_allowed(target_path, require):
def esc_xml(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
def esc_xml_text(s):
# Эскейп ТЕКСТА элемента: только & < > (кавычки в тексте 1С держит raw).
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def new_uuid():
return str(uuid.uuid4())
@@ -2806,6 +2810,60 @@ if obj_type in types_with_module:
write_utf8_bom(module_path, '')
modules_created.append(module_path)
# --- Predefined data (Ext/Predefined.xml). Элемент: "(Код) Имя [Наименование]" ИЛИ объект (+рус. синонимы).
# Наименование: нет [..]/ключа → авто(Split-CamelCase); [] / "" → пусто; [текст]/текст → как есть.
def resolve_predef_item(val):
if isinstance(val, str):
m = re.match(r'^\s*(?:\(([^)]*)\)\s*)?(\S+)(?:\s*\[(.*)\])?\s*$', val)
name = m.group(2)
code = m.group(1) if m.group(1) is not None else ''
desc = m.group(3) if m.group(3) is not None else split_camel_case(name)
return {'name': name, 'code': code, 'desc': desc, 'isFolder': False, 'children': []}
def gv(keys):
for k in keys:
if k in val:
return val[k]
return None
name = str(gv(['name', 'имя']) or '')
code_v = gv(['code', 'код'])
code = str(code_v) if code_v is not None else ''
has_desc = ('description' in val) or ('наименование' in val)
desc_v = gv(['description', 'наименование'])
desc = ('' if desc_v is None else str(desc_v)) if has_desc else split_camel_case(name)
is_folder = gv(['isFolder', 'группа']) is True
subs = gv(['childItems', 'подчиненные']) or []
return {'name': name, 'code': code, 'desc': desc, 'isFolder': is_folder, 'children': list(subs)}
def emit_predef_item(out, val, indent, code_type):
r = resolve_predef_item(val)
out.append(f'{indent}<Item id="{new_uuid()}">')
out.append(f'{indent}\t<Name>{esc_xml_text(r["name"])}</Name>')
if not r['code']:
out.append(f'{indent}\t<Code/>')
elif code_type == 'Number':
out.append(f'{indent}\t<Code xsi:type="xs:decimal">{esc_xml_text(r["code"])}</Code>')
else:
out.append(f'{indent}\t<Code>{esc_xml_text(r["code"])}</Code>')
if r['desc'] == '':
out.append(f'{indent}\t<Description/>')
else:
out.append(f'{indent}\t<Description>{esc_xml_text(r["desc"])}</Description>')
out.append(f'{indent}\t<IsFolder>{"true" if r["isFolder"] else "false"}</IsFolder>')
if r['children']:
out.append(f'{indent}\t<ChildItems>')
for c in r['children']:
emit_predef_item(out, c, indent + '\t\t', code_type)
out.append(f'{indent}\t</ChildItems>')
out.append(f'{indent}</Item>')
def build_predefined_xml(items, xsi_type, code_type):
out = ['<?xml version="1.0" encoding="UTF-8"?>']
out.append(f'<PredefinedData xmlns="http://v8.1c.ru/8.3/xcf/predef" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="{xsi_type}" version="{format_version}">')
for it in items:
emit_predef_item(out, it, '\t', code_type)
out.append('</PredefinedData>')
return '\n'.join(out) + '\n'
# Special files
if obj_type == 'ExchangePlan':
content_path = os.path.join(ext_dir, 'Content.xml')
@@ -2823,6 +2881,15 @@ if obj_type == 'BusinessProcess':
write_utf8_bom(flowchart_path, flowchart_xml)
modules_created.append(flowchart_path)
# Предопределённые элементы (Ext/Predefined.xml) — пока Catalog.
if obj_type == 'Catalog' and defn.get('predefined'):
ensure_ext_dir()
cat_code_type = str(defn['codeType']) if defn.get('codeType') else 'String'
predef_xml = build_predefined_xml(defn['predefined'], 'CatalogPredefinedItems', cat_code_type)
predef_path = os.path.join(ext_dir, 'Predefined.xml')
write_utf8_bom(predef_path, predef_xml)
modules_created.append(predef_path)
# ---------------------------------------------------------------------------
# 17. Register in Configuration.xml
# ---------------------------------------------------------------------------
@@ -1,4 +1,4 @@
# meta-decompile v0.4 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# meta-decompile v0.5 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
#
# Пилот: только Catalog. Инверс meta-compile (omit-on-default: ключ эмитим только
@@ -310,34 +310,51 @@ if ($childObjs) {
}
}
# --- Предопределённые (соседний Ext/Predefined.xml). meta-compile пока их НЕ потребляет (пробел DSL).
# ⚠️ ЧЕРНОВАЯ/ВРЕМЕННАЯ форма `predefined` — предложение по факту корпуса, НЕ утверждённый DSL.
# Правило: непокрытый блок проектируем в DSL СНАЧАЛА (совместно), потом переписываем этот захват под него. ---
# --- Предопределённые (соседний Ext/Predefined.xml) → DSL predefined.
# Плоский элемент → строка "(Код) Имя [Наименование]" (Наименование: ==авто → опустить; '' → []; иначе [текст]).
# Группа/иерархия → object {name, [code], [description], isFolder, childItems}. codeType — из свойства каталога. ---
$objDir = Split-Path -Parent (Resolve-Path -LiteralPath $ObjectPath).Path
$predefPath = Join-Path (Join-Path (Join-Path $objDir $objName) 'Ext') 'Predefined.xml'
if (Test-Path -LiteralPath $predefPath) {
$pdoc = New-Object System.Xml.XmlDocument
$pdoc.Load($predefPath)
function Parse-PredefItem {
function PredefItem-ToDsl {
param($itemEl)
$o = [ordered]@{}
$o['name'] = ($itemEl.SelectSingleNode("*[local-name()='Name']")).InnerText
$name = ($itemEl.SelectSingleNode("*[local-name()='Name']")).InnerText
$codeEl = $itemEl.SelectSingleNode("*[local-name()='Code']")
if ($codeEl -and $codeEl.InnerText) { $o['code'] = $codeEl.InnerText }
$code = if ($codeEl -and $codeEl.InnerText) { $codeEl.InnerText } else { '' }
$descEl = $itemEl.SelectSingleNode("*[local-name()='Description']")
if ($descEl -and $descEl.InnerText) { $o['description'] = $descEl.InnerText }
$desc = if ($descEl) { $descEl.InnerText } else { '' }
$folderEl = $itemEl.SelectSingleNode("*[local-name()='IsFolder']")
if ($folderEl -and $folderEl.InnerText -eq 'true') { $o['isFolder'] = $true }
$isFolder = ($folderEl -and $folderEl.InnerText -eq 'true')
$childContainer = $itemEl.SelectSingleNode("*[local-name()='ChildItems']")
if ($childContainer) {
$kids = [System.Collections.ArrayList]@()
foreach ($k in @($childContainer.SelectNodes("*[local-name()='Item']"))) { [void]$kids.Add((Parse-PredefItem $k)) }
if ($kids.Count -gt 0) { $o['children'] = $kids }
# ВАЖНО: обернуть весь if в @(), иначе PS распаковывает одноэлементный @(...) из if-блока
# обратно в узел → $kids.Count = $null → папки с ОДНИМ ребёнком теряют его.
$kids = @(if ($childContainer) { $childContainer.SelectNodes("*[local-name()='Item']") } else { @() })
$auto = Split-CamelWords $name
if (-not $isFolder -and $kids.Count -eq 0) {
# Плоский → компактная строка.
$s = if ($code) { "($code) $name" } else { $name }
if ($desc -eq '') { $s = "$s []" }
elseif ($desc -ne $auto) { $s = "$s [$desc]" }
return $s
}
# Группа/иерархия → объект.
$o = [ordered]@{ name = $name }
if ($code) { $o['code'] = $code }
if ($desc -eq '') { $o['description'] = '' }
elseif ($desc -ne $auto) { $o['description'] = $desc }
if ($isFolder) { $o['isFolder'] = $true }
if ($kids.Count -gt 0) {
$sub = [System.Collections.ArrayList]@()
foreach ($k in $kids) { [void]$sub.Add((PredefItem-ToDsl $k)) }
$o['childItems'] = $sub
}
return $o
}
$rootItems = [System.Collections.ArrayList]@()
foreach ($it in @($pdoc.DocumentElement.SelectNodes("*[local-name()='Item']"))) { [void]$rootItems.Add((Parse-PredefItem $it)) }
foreach ($it in @($pdoc.DocumentElement.SelectNodes("*[local-name()='Item']"))) { [void]$rootItems.Add((PredefItem-ToDsl $it)) }
if ($rootItems.Count -gt 0) { $dsl['predefined'] = $rootItems }
}