From 58bc95264e24b124d33fd0bdd446c75c8cf42875 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 1 Jul 2026 17:18:01 +0300 Subject: [PATCH] =?UTF-8?q?feat(meta-compile,meta-decompile):=20=D0=BC?= =?UTF-8?q?=D1=83=D0=BB=D1=8C=D1=82=D0=B8=D1=8F=D0=B7=D1=8B=D1=87=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20ML=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=B8=D0=BD?= =?UTF-8?q?=D0=BE=D0=BD=D0=B8=D0=BC=D0=B0=20=D0=B8=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D1=81=D0=BA=D0=B0=D0=B7=D0=BA=D0=B8=20=D1=80=D0=B5=D0=BA=D0=B2?= =?UTF-8?q?=D0=B8=D0=B7=D0=B8=D1=82=D0=B0=20(v1.18/v0.4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раундтрип-находка (класс-1 синоним + класс-3 подсказка). Корпус: у реквизитов 21677 мультиязычных синонимов (ru+en) и 16063 непустых подсказки (10689 мультиязычных). meta-compile писал ВСЁ ML только ru (Emit-MLText брал строку) и хардкодил пустым → массовая потеря en + подсказок. - meta-compile (дуал-порт): Emit-MLText/emit_mltext принимают строку (→ru) ИЛИ объект {lang:content} (→ на язык, в порядке ключей) через новый Emit-MLItems/emit_ml_items. Parse object-форма реквизита пробрасывает synonym/tooltip без стрингификации; Emit-Attribute эмитит из tooltip. - meta-decompile: Get-MLValue → строка (ru-only) | {ru,en} (мультиязычно, порядок из XML); object-форма реквизита при кастомном синониме ИЛИ наличии подсказки. - spec meta-dsl-spec.md v2.3: §4.2 (tooltip) + §4.4 ML-значения (строка/{ru,en}), консистентно с form-compile. - тест-кейс catalog-attr-ml (мультиязычный синоним + подсказка, обе формы). Валидация: PS==PY паритет; полный прогон −228748 строк (447285→218537, >половины); регресс 35/35 (ps+py); 1С-cert зелёный. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../meta-compile/scripts/meta-compile.ps1 | 32 ++- .../meta-compile/scripts/meta-compile.py | 29 +- .../meta-decompile/scripts/meta-decompile.ps1 | 36 ++- docs/meta-dsl-spec.md | 14 +- .../cases/meta-compile/catalog-attr-ml.json | 12 + .../catalog-attr-ml/Catalogs/Контрагенты.xml | 198 ++++++++++++++ .../Catalogs/Контрагенты/Ext/ObjectModule.bsl | 0 .../catalog-attr-ml/Configuration.xml | 252 ++++++++++++++++++ .../Ext/ClientApplicationInterface.xml | 18 ++ .../catalog-attr-ml/Languages/Русский.xml | 16 ++ 10 files changed, 584 insertions(+), 23 deletions(-) create mode 100644 tests/skills/cases/meta-compile/catalog-attr-ml.json create mode 100644 tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Catalogs/Контрагенты.xml create mode 100644 tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Catalogs/Контрагенты/Ext/ObjectModule.bsl create mode 100644 tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Configuration.xml create mode 100644 tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Ext/ClientApplicationInterface.xml create mode 100644 tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Languages/Русский.xml diff --git a/.claude/skills/meta-compile/scripts/meta-compile.ps1 b/.claude/skills/meta-compile/scripts/meta-compile.ps1 index 5ca0ca74..7d0683bc 100644 --- a/.claude/skills/meta-compile/scripts/meta-compile.ps1 +++ b/.claude/skills/meta-compile/scripts/meta-compile.ps1 @@ -331,17 +331,30 @@ function Esc-Xml { return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') } +# ML-значение: строка → один ru; объект {lang: content} → item на язык (в порядке ключей). +function Emit-MLItems { + param([string]$indent, $val) + if ($val -is [System.Collections.IDictionary]) { + foreach ($k in $val.Keys) { + X "$indent"; X "$indent`t$k"; X "$indent`t$(Esc-Xml "$($val[$k])")"; X "$indent" + } + } elseif ($val -is [System.Management.Automation.PSCustomObject]) { + foreach ($p in $val.PSObject.Properties) { + X "$indent"; X "$indent`t$($p.Name)"; X "$indent`t$(Esc-Xml "$($p.Value)")"; X "$indent" + } + } else { + X "$indent"; X "$indent`tru"; X "$indent`t$(Esc-Xml "$val")"; X "$indent" + } +} function Emit-MLText { - param([string]$indent, [string]$tag, [string]$text) - if (-not $text) { + param([string]$indent, [string]$tag, $text) + # Пусто (null / пустая строка) → самозакрывающийся тег. + if (($null -eq $text) -or (($text -is [string]) -and ($text -eq ''))) { X "$indent<$tag/>" return } X "$indent<$tag>" - X "$indent`t" - X "$indent`t`tru" - X "$indent`t`t$(Esc-Xml $text)" - X "$indent`t" + Emit-MLItems "$indent`t" $text X "$indent" } @@ -611,12 +624,13 @@ function Parse-AttributeShorthand { return $parsed } - # Object form + # Object form. synonym/tooltip — сквозной проброс (строка ИЛИ объект {ru,en}), НЕ стрингифаим. $name = "$($val.name)" return @{ name = $name type = Build-TypeStr $val - synonym = if ($val.synonym) { "$($val.synonym)" } else { Split-CamelCase $name } + synonym = if ($null -ne $val.synonym) { $val.synonym } else { Split-CamelCase $name } + tooltip = $val.tooltip comment = if ($val.comment) { "$($val.comment)" } else { "" } flags = @(if ($val.flags) { $val.flags } else { @() }) fillChecking = if ($val.fillChecking) { "$($val.fillChecking)" } else { "" } @@ -962,7 +976,7 @@ function Emit-Attribute { X "$indent`t`tfalse" X "$indent`t`t" X "$indent`t`t" - X "$indent`t`t" + Emit-MLText "$indent`t`t" "ToolTip" $parsed.tooltip X "$indent`t`tfalse" X "$indent`t`t" $multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" } diff --git a/.claude/skills/meta-compile/scripts/meta-compile.py b/.claude/skills/meta-compile/scripts/meta-compile.py index 26d1339a..c254aec9 100644 --- a/.claude/skills/meta-compile/scripts/meta-compile.py +++ b/.claude/skills/meta-compile/scripts/meta-compile.py @@ -197,15 +197,27 @@ lines = [] def X(text): lines.append(text) +# ML-значение: строка → один ru; dict {lang: content} → item на язык (в порядке ключей). +def emit_ml_items(indent, val): + if isinstance(val, dict): + for k, v in val.items(): + X(f'{indent}') + X(f'{indent}\t{k}') + X(f'{indent}\t{esc_xml(str(v))}') + X(f'{indent}') + else: + X(f'{indent}') + X(f'{indent}\tru') + X(f'{indent}\t{esc_xml(str(val))}') + X(f'{indent}') + def emit_mltext(indent, tag, text): - if not text: + # Пусто (None / '') → самозакрывающийся тег. + if text is None or (isinstance(text, str) and text == ''): X(f'{indent}<{tag}/>') return X(f'{indent}<{tag}>') - X(f'{indent}\t') - X(f'{indent}\t\tru') - X(f'{indent}\t\t{esc_xml(text)}') - X(f'{indent}\t') + emit_ml_items(f'{indent}\t', text) X(f'{indent}') # --------------------------------------------------------------------------- @@ -615,12 +627,13 @@ def parse_attribute_shorthand(val): parsed['type'] = colon_parts[1].strip() parsed['synonym'] = split_camel_case(parsed['name']) return parsed - # Object form + # Object form. synonym/tooltip — сквозной проброс (строка ИЛИ dict {ru,en}), НЕ стрингифаим. name = str(val.get('name', '')) return { 'name': name, 'type': build_type_str(val), - 'synonym': str(val['synonym']) if val.get('synonym') else split_camel_case(name), + 'synonym': val['synonym'] if val.get('synonym') is not None else split_camel_case(name), + 'tooltip': val.get('tooltip'), 'comment': str(val['comment']) if val.get('comment') else '', 'flags': list(val.get('flags', [])), 'fillChecking': str(val['fillChecking']) if val.get('fillChecking') else '', @@ -957,7 +970,7 @@ def emit_attribute(indent, parsed, context): X(f'{indent}\t\tfalse') X(f'{indent}\t\t') X(f'{indent}\t\t') - X(f'{indent}\t\t') + emit_mltext(f'{indent}\t\t', 'ToolTip', parsed.get('tooltip')) X(f'{indent}\t\tfalse') X(f'{indent}\t\t') multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false' diff --git a/.claude/skills/meta-decompile/scripts/meta-decompile.ps1 b/.claude/skills/meta-decompile/scripts/meta-decompile.ps1 index 9826a094..779308cf 100644 --- a/.claude/skills/meta-decompile/scripts/meta-decompile.ps1 +++ b/.claude/skills/meta-decompile/scripts/meta-decompile.ps1 @@ -1,4 +1,4 @@ -# meta-decompile v0.3 — XML объекта метаданных 1С → JSON-черновик формата meta-compile +# meta-decompile v0.4 — XML объекта метаданных 1С → JSON-черновик формата meta-compile # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # # Пилот: только Catalog. Инверс meta-compile (omit-on-default: ключ эмитим только @@ -105,6 +105,26 @@ function Get-MLru { if ($it) { return $it.InnerText } return $null } +# ML-значение → строка (если единственный item ru) ЛИБО [ordered]{lang:content} (мультиязычно, порядок из XML). +# null если контента нет. Компактная строка для ru-only, объект для мультиязычных. +function Get-MLValue { + param($node) + if (-not $node) { return $null } + $items = @($node.SelectNodes('v8:item', $nsm)) + if ($items.Count -eq 0) { return $null } + if ($items.Count -eq 1) { + $lang = $items[0].SelectSingleNode('v8:lang', $nsm).InnerText + $content = $items[0].SelectSingleNode('v8:content', $nsm).InnerText + if ($lang -eq 'ru') { return $content } + } + $o = [ordered]@{} + foreach ($it in $items) { + $l = $it.SelectSingleNode('v8:lang', $nsm).InnerText + $c = $it.SelectSingleNode('v8:content', $nsm).InnerText + $o[$l] = $c + } + return $o +} # Авто-синоним: точное зеркало Split-CamelCase из meta-compile (стр.354). Совпало → ключ опускаем. # ВАЖНО: логика должна совпадать байт-в-байт с компилятором, иначе ложные «синоним==авто» → диффы. function Split-CamelWords { @@ -175,12 +195,18 @@ function Attr-ToDsl { if ($ix) { if ($ix.InnerText -eq 'Index') { $flags += 'index' } elseif ($ix.InnerText -eq 'IndexWithAdditionalOrder') { $flags += 'indexAdditional' } } $ml = $ap.SelectSingleNode('md:MultiLine', $nsm); if ($ml -and $ml.InnerText -eq 'true') { $flags += 'multiline' } - $syn = Get-MLru ($ap.SelectSingleNode('md:Synonym', $nsm)) - # Кастомный синоним (есть и ≠ авто) → object-форма (shorthand его не выражает). - if ($syn -and $syn -ne (Split-CamelWords $nm)) { + # Синоним/подсказка (строка ru-only ИЛИ {ru,en}). Кастомный синоним ИЛИ наличие подсказки → object-форма. + $synVal = Get-MLValue ($ap.SelectSingleNode('md:Synonym', $nsm)) + $synCustom = $false + if ($synVal -is [string]) { if ($synVal -ne (Split-CamelWords $nm)) { $synCustom = $true } } + elseif ($null -ne $synVal) { $synCustom = $true } # {ru,en} = всегда кастом + $ttVal = Get-MLValue ($ap.SelectSingleNode('md:ToolTip', $nsm)) + + if ($synCustom -or ($null -ne $ttVal)) { $o = [ordered]@{ name = $nm } if ($ts) { $o['type'] = $ts } - $o['synonym'] = $syn + if ($synCustom) { $o['synonym'] = $synVal } + if ($null -ne $ttVal) { $o['tooltip'] = $ttVal } if ($flags.Count -gt 0) { $o['flags'] = [System.Collections.ArrayList]@($flags) } return $o } diff --git a/docs/meta-dsl-spec.md b/docs/meta-dsl-spec.md index 7c65f2bf..cfbdbf57 100644 --- a/docs/meta-dsl-spec.md +++ b/docs/meta-dsl-spec.md @@ -1,6 +1,6 @@ # Meta DSL — спецификация JSON-формата для объектов метаданных 1С -Версия: 2.2 +Версия: 2.3 ## Обзор @@ -124,12 +124,15 @@ JSON DSL для описания объектов метаданных конф "name": "Имя", "type": "String(100)", "synonym": "Мой синоним", + "tooltip": "Всплывающая подсказка", "comment": "Комментарий", "fillChecking": "ShowError", "indexing": "Index" } ``` +**`synonym` и `tooltip` — ML-значения** (см. §4.4): строка → русский текст; объект `{ "ru": "…", "en": "…" }` → мультиязычный (`` на язык, в порядке ключей). `tooltip` → `` реквизита; нет ключа → `` (пусто). `synonym` не задан → авто из имени (§2). + Тип можно задать единой строкой (`"type": "String(100)"`) или раздельными полями: ```json @@ -157,6 +160,15 @@ JSON DSL для описания объектов метаданных конф Флаги разделяются запятой после `|`. +### 4.4 ML-значения (многоязычный текст) + +Текстовые поля, попадающие в ``-структуру 1С (`synonym`, `tooltip`), принимают две формы: + +- **строка** → один язык `ru`: `"synonym": "Наименование"`; +- **объект `{ lang: content }`** → по `` на язык, **в порядке ключей**: `"tooltip": { "ru": "Подсказка", "en": "Hint" }`. + +Пустая строка `""` / отсутствие ключа → самозакрывающийся тег (``). Консистентно с формой `title`/`tooltip` в form-compile. + --- ## 5. Табличные части diff --git a/tests/skills/cases/meta-compile/catalog-attr-ml.json b/tests/skills/cases/meta-compile/catalog-attr-ml.json new file mode 100644 index 00000000..51bb479b --- /dev/null +++ b/tests/skills/cases/meta-compile/catalog-attr-ml.json @@ -0,0 +1,12 @@ +{ + "name": "Справочник с ML-синонимом и подсказкой реквизита", + "input": { + "type": "Catalog", "name": "Контрагенты", + "attributes": [ + { "name": "ИНН", "type": "String(12)", "synonym": { "ru": "ИНН", "en": "TIN" }, "tooltip": "Идентификационный номер налогоплательщика" }, + { "name": "Комментарий", "type": "String(200)", "tooltip": { "ru": "Произвольный комментарий", "en": "Free-form comment" } } + ] + }, + "validatePath": "Catalogs/Контрагенты", + "expect": { "files": ["Catalogs/Контрагенты.xml"] } +} diff --git a/tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Catalogs/Контрагенты.xml b/tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Catalogs/Контрагенты.xml new file mode 100644 index 00000000..cf2a78f2 --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Catalogs/Контрагенты.xml @@ -0,0 +1,198 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + UUID-008 + UUID-009 + + + UUID-010 + UUID-011 + + + + Контрагенты + + + ru + Контрагенты + + + + false + HierarchyFoldersAndItems + false + 2 + true + true + + ToItems + 9 + 25 + String + Variable + WholeCatalog + false + true + AsDescription + + Auto + InDialog + false + BothWays + + Catalog.Контрагенты.StandardAttribute.Description + Catalog.Контрагенты.StandardAttribute.Code + + Begin + DontUse + Directly + + + + + + + + + + + false + + + Automatic + Use + + + + + + DontUse + Auto + DontUse + false + false + + + + + ИНН + + + ru + ИНН + + + en + TIN + + + + + xs:string + + 12 + Variable + + + false + + + + + ru + Идентификационный номер налогоплательщика + + + false + + false + false + + + false + + DontCheck + Items + + + Auto + Auto + + + Auto + ForItem + DontIndex + Use + Use + + + + + Комментарий + + + ru + Комментарий + + + + + xs:string + + 200 + Variable + + + false + + + + + ru + Произвольный комментарий + + + en + Free-form comment + + + false + + false + false + + + false + + DontCheck + Items + + + Auto + Auto + + + Auto + ForItem + DontIndex + Use + Use + + + + + diff --git a/tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Catalogs/Контрагенты/Ext/ObjectModule.bsl b/tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Catalogs/Контрагенты/Ext/ObjectModule.bsl new file mode 100644 index 00000000..e69de29b diff --git a/tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Configuration.xml b/tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Configuration.xml new file mode 100644 index 00000000..c07959c0 --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Configuration.xml @@ -0,0 +1,252 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + UUID-008 + UUID-009 + + + UUID-010 + UUID-011 + + + UUID-012 + UUID-013 + + + UUID-014 + UUID-015 + + + + TestConfig + + + ru + TestConfig + + + + + Version8_3_24 + ManagedApplication + + PlatformApplication + + Russian + + + + + false + false + false + + + + + + + + + + + + + + + + + + + + + + Biometrics + true + + + Location + false + + + BackgroundLocation + false + + + BluetoothPrinters + false + + + WiFiPrinters + false + + + Contacts + false + + + Calendars + false + + + PushNotifications + false + + + LocalNotifications + false + + + InAppPurchases + false + + + PersonalComputerFileExchange + false + + + Ads + false + + + NumberDialing + false + + + CallProcessing + false + + + CallLog + false + + + AutoSendSMS + false + + + ReceiveSMS + false + + + SMSLog + false + + + Camera + false + + + Microphone + false + + + MusicLibrary + false + + + PictureAndVideoLibraries + false + + + AudioPlaybackAndVibration + false + + + BackgroundAudioPlaybackAndVibration + false + + + InstallPackages + false + + + OSBackup + true + + + ApplicationUsageStatistics + false + + + BarcodeScanning + false + + + BackgroundAudioRecording + false + + + AllFilesAccess + false + + + Videoconferences + false + + + NFC + false + + + DocumentScanning + false + + + SpeechToText + false + + + Geofences + false + + + IncomingShareRequests + false + + + AllIncomingShareRequestsTypesProcessing + false + + + + + + Normal + + + Language.Русский + + + + + + Managed + NotAutoFree + DontUse + DontUse + TaxiEnableVersion8_2 + DontUse + Version8_3_24 + + + + Русский + Контрагенты + + + \ No newline at end of file diff --git a/tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Ext/ClientApplicationInterface.xml b/tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Ext/ClientApplicationInterface.xml new file mode 100644 index 00000000..3c1161b2 --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Ext/ClientApplicationInterface.xml @@ -0,0 +1,18 @@ + + + + + UUID-002 + + + + + UUID-004 + + + + + + + + \ No newline at end of file diff --git a/tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Languages/Русский.xml b/tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Languages/Русский.xml new file mode 100644 index 00000000..37c60d78 --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/catalog-attr-ml/Languages/Русский.xml @@ -0,0 +1,16 @@ + + + + + Русский + + + ru + Русский + + + + ru + + + \ No newline at end of file