From 8d1670468aab8f14c53f7976f1a6b1d6571e9a45 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sun, 6 Sep 2026 17:10:43 +0300 Subject: [PATCH] =?UTF-8?q?fix(meta-edit):=20=D0=BA=D0=BE=D0=BF=D0=B8?= =?UTF-8?q?=D1=8F=20=D1=8D=D0=BC=D0=B8=D1=82=D1=82=D0=B5=D1=80=D0=B0=20?= =?UTF-8?q?=D0=B7=D0=B2=D0=B0=D0=BB=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D0=B8,=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D1=85=20?= =?UTF-8?q?=D0=B2=20=D0=BD=D0=B0=D0=B2=D1=8B=D0=BA=D0=B5=20=D0=BD=D0=B5?= =?UTF-8?q?=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Скопированные из meta-compile Emit-FormRef и Emit-Characteristics тянули за собой хвост хелперов (Normalize-FormRef, Normalize-CharFrom, Expand-CharField, Get-CharIntField), которых в meta-edit нет. Добавление таблицы с ключом defaultListForm или characteristics давало в PS CommandNotFoundException при коде возврата 0: навык отчитывался успехом, а файл таблицы не создавался и в ChildObjects ничего не появлялось. В python то же место падало трейсбеком — порты расходились ещё и потоком ошибки. Причина не в копировании как таковом: копия обязана быть не только идентичной, но и самодостаточной. Форма функции для этого не годилась, поэтому изменена в эталоне и перекопирована — тем же приёмом, что уже применён в этой семье дважды: Emit-EdsTableProperties принимает готовые блоки и четырёх слотов , рендерит их вызывающий навык своим эмиттером. Зависимостей у тела не осталось. meta-edit передаёт пустые блоки и отвергает ключи characteristics и default*Form с объяснением, куда идти: форму назначает form-add, характеристики — meta-compile. Попутно исправлено то, что вскрылось при разборе: короткое имя формы в defaultListForm эмитилось как есть, а платформа отвечает «Неизвестный объект метаданных». Теперь оно разворачивается в полный путь ExternalDataSource.И.Table.Т.Form.Ф — как и ссылки на поля. check-inline-drift получил проверку РАЗРЕШИМОСТИ: всё, что зовёт скопированное тело, обязано быть определено в том же навыке. Критерий «своей» функции — имя, определённое в навыке-эталоне, поэтому список командлетов-исключений не нужен. Проверено обратным экспериментом: гард ловит исходный дефект с прямой формулировкой «копия неразрешима». Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi --- .../meta-compile/scripts/meta-compile.ps1 | 44 ++- .../meta-compile/scripts/meta-compile.py | 53 +++- .../skills/meta-edit/scripts/meta-edit.ps1 | 91 ++----- .claude/skills/meta-edit/scripts/meta-edit.py | 102 +++---- .../eds-add-table-unsupported-keys.json | 33 +++ .../Configuration.xml | 252 ++++++++++++++++++ .../Ext/ClientApplicationInterface.xml | 18 ++ .../ExternalDataSources/PG.xml | 33 +++ .../PG/Tables/products.xml | 130 +++++++++ .../Languages/Русский.xml | 16 ++ tests/skills/check-inline-drift.mjs | 28 ++ 11 files changed, 642 insertions(+), 158 deletions(-) create mode 100644 tests/skills/cases/meta-edit/eds-add-table-unsupported-keys.json create mode 100644 tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/Configuration.xml create mode 100644 tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/Ext/ClientApplicationInterface.xml create mode 100644 tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/ExternalDataSources/PG.xml create mode 100644 tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/ExternalDataSources/PG/Tables/products.xml create mode 100644 tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/Languages/Русский.xml diff --git a/.claude/skills/meta-compile/scripts/meta-compile.ps1 b/.claude/skills/meta-compile/scripts/meta-compile.ps1 index a427b792f..c18dadccf 100644 --- a/.claude/skills/meta-compile/scripts/meta-compile.ps1 +++ b/.claude/skills/meta-compile/scripts/meta-compile.ps1 @@ -1,4 +1,4 @@ -# meta-compile v1.107 — Compile 1C metadata object from JSON +# meta-compile v1.108 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills [CmdletBinding(PositionalBinding=$false)] param( @@ -4466,7 +4466,10 @@ function Emit-EdsFunction { # Свойства таблицы: 38 узлов в порядке выгрузки платформы. function Emit-EdsTableProperties { - param([string]$indent, [string]$srcName, [string]$tableName, $t) + # $charXml и $defaultFormsXml — уже собранные блоки и четыре слота + # : их рендерит вызывающий навык своим эмиттером. Так тело не зависит + # от хелперов конкретного навыка и годится для копирования (check-inline-drift). + param([string]$indent, [string]$srcName, [string]$tableName, $t, [string]$charXml, [string]$defaultFormsXml) $i = $indent $tblSynonym = if ($t -and $null -ne $t.synonym) { $t.synonym } else { Split-CamelCase $tableName } X "$i$(Esc-XmlText $tableName)" @@ -4493,7 +4496,7 @@ function Emit-EdsTableProperties { # с полем родителя — пустая строка (как после загрузки), а без него — nil. if ($t -and $t.parentField) { X "$i" } else { X "$i" } - Emit-Characteristics $i $(if ($t) { $t.characteristics } else { $null }) + if ($charXml) { X $charXml.TrimEnd("`r", "`n") } else { X "$i" } X "$i$(if ($t -and $t.useStandardCommands -eq $false) { 'false' } else { 'true' })" X "$i$(if ($t -and $t.quickChoice -eq $true) { 'true' } else { 'false' })" @@ -4507,10 +4510,9 @@ function Emit-EdsTableProperties { X "$i$(if ($t -and $t.choiceDataGetModeOnInputByString) { "$($t.choiceDataGetModeOnInputByString)" } else { 'Directly' })" X "$i$(if ($t -and $t.choiceHistoryOnInput) { "$($t.choiceHistoryOnInput)" } else { 'Auto' })" - foreach ($formTag in @("DefaultObjectForm","DefaultRecordForm","DefaultListForm","DefaultChoiceForm")) { - $key = $formTag.Substring(0,1).ToLower() + $formTag.Substring(1) - Emit-FormRef $i $formTag $(if ($t) { $t.$key } else { $null }) - } + # Пустая строка — четыре слота всё равно обязаны быть: в свойствах таблицы их ровно 38. + if ($defaultFormsXml) { X $defaultFormsXml.TrimEnd("`r", "`n") } + else { foreach ($formTag in @("DefaultObjectForm","DefaultRecordForm","DefaultListForm","DefaultChoiceForm")) { X "$i<$formTag/>" } } foreach ($presTag in @("ObjectPresentation","ExtendedObjectPresentation","RecordPresentation", "ExtendedRecordPresentation","ListPresentation","ExtendedListPresentation","Explanation")) { $key = $presTag.Substring(0,1).ToLower() + $presTag.Substring(1) @@ -4530,9 +4532,9 @@ function Emit-EdsTableProperties { # поэтому «перехват» — запомнить длину, отдать эмиттерам, вырезать добавленное # (тот же приём, что у составного типа). function Build-EdsTableXml { - # $fieldsXml — уже собранные узлы : их рендерит вызывающий навык своим эмиттером - # реквизита. Так тело функции не зависит от того, какой это навык. - param([string]$srcName, [string]$tableName, $entry, [string]$fieldsXml) + # $fieldsXml, $charXml, $defaultFormsXml — уже собранные узлы: их рендерит вызывающий навык + # своими эмиттерами. Так тело функции не зависит от того, какой это навык. + param([string]$srcName, [string]$tableName, $entry, [string]$fieldsXml, [string]$charXml, [string]$defaultFormsXml) $before = $script:xml.Length $tableUuid = New-Guid-String @@ -4559,7 +4561,7 @@ function Build-EdsTableXml { X "`t`t" X "`t`t" - Emit-EdsTableProperties "`t`t`t" $srcName $tableName $entry.props + Emit-EdsTableProperties "`t`t`t" $srcName $tableName $entry.props $charXml $defaultFormsXml X "`t`t" if ($fieldsXml) { @@ -5357,7 +5359,25 @@ if ($objType -eq "ExternalDataSource" -and $script:edsTables.Count -gt 0) { } $fieldsXml = $script:xml.ToString($fieldsBefore, $script:xml.Length - $fieldsBefore) [void]$script:xml.Remove($fieldsBefore, $script:xml.Length - $fieldsBefore) - $tableXml = Build-EdsTableXml $objName $tblName $entry $fieldsXml + $tp = $entry.props + $charBefore = $script:xml.Length + Emit-Characteristics "`t`t`t" $(if ($tp) { $tp.characteristics } else { $null }) + $charXml = $script:xml.ToString($charBefore, $script:xml.Length - $charBefore) + [void]$script:xml.Remove($charBefore, $script:xml.Length - $charBefore) + + # Слот формы: короткое имя разворачивается в полный путь таблицы внешнего источника — + # голое имя платформа отвергает («Неизвестный объект метаданных»). + $formsBefore = $script:xml.Length + foreach ($formTag in @("DefaultObjectForm","DefaultRecordForm","DefaultListForm","DefaultChoiceForm")) { + $key = $formTag.Substring(0,1).ToLower() + $formTag.Substring(1) + $formVal = if ($tp) { $tp.$key } else { $null } + if ($formVal -and "$formVal" -notmatch '\.') { $formVal = "ExternalDataSource.$objName.Table.$tblName.Form.$formVal" } + Emit-FormRef "`t`t`t" $formTag $formVal + } + $defaultFormsXml = $script:xml.ToString($formsBefore, $script:xml.Length - $formsBefore) + [void]$script:xml.Remove($formsBefore, $script:xml.Length - $formsBefore) + + $tableXml = Build-EdsTableXml $objName $tblName $entry $fieldsXml $charXml $defaultFormsXml $tablePath = Join-Path $tablesDir "$tblName.xml" Write-XmlFileKeepEol $tablePath $tableXml $enc $edsTablesCreated += $tablePath diff --git a/.claude/skills/meta-compile/scripts/meta-compile.py b/.claude/skills/meta-compile/scripts/meta-compile.py index 96ef5e955..a55a14beb 100644 --- a/.claude/skills/meta-compile/scripts/meta-compile.py +++ b/.claude/skills/meta-compile/scripts/meta-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# meta-compile v1.107 — Compile 1C metadata object from JSON +# meta-compile v1.108 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -4459,8 +4459,12 @@ def emit_eds_function(indent, fn_name, val, type_xml): X(f'{indent}') -def emit_eds_table_properties(indent, src_name, table_name, t): - """Свойства таблицы: 38 узлов в порядке выгрузки платформы.""" +def emit_eds_table_properties(indent, src_name, table_name, t, char_xml, default_forms_xml): + """Свойства таблицы: 38 узлов в порядке выгрузки платформы. + + char_xml и default_forms_xml — уже собранные блоки и четыре слота + : их рендерит вызывающий навык своим эмиттером. Так тело не зависит + от хелперов конкретного навыка и годится для копирования (check-inline-drift).""" i = indent t = t or {} tbl_synonym = t['synonym'] if t.get('synonym') is not None else split_camel_case(table_name) @@ -4497,7 +4501,10 @@ def emit_eds_table_properties(indent, src_name, table_name, t): X(f'{i}') else: X(f'{i}') - emit_characteristics(i, t.get('characteristics')) + if char_xml: + X(char_xml.rstrip('\r\n')) + else: + X(f'{i}') X(f'{i}{"false" if t.get("useStandardCommands") is False else "true"}') X(f'{i}{"true" if t.get("quickChoice") is True else "false"}') @@ -4515,9 +4522,12 @@ def emit_eds_table_properties(indent, src_name, table_name, t): X(f'{i}{t.get("choiceDataGetModeOnInputByString") or "Directly"}') X(f'{i}{t.get("choiceHistoryOnInput") or "Auto"}') - for form_tag in ('DefaultObjectForm', 'DefaultRecordForm', 'DefaultListForm', 'DefaultChoiceForm'): - key = form_tag[0].lower() + form_tag[1:] - emit_form_ref(i, form_tag, t.get(key)) + # Пустая строка — четыре слота всё равно обязаны быть: в свойствах таблицы их ровно 38. + if default_forms_xml: + X(default_forms_xml.rstrip('\r\n')) + else: + for form_tag in ('DefaultObjectForm', 'DefaultRecordForm', 'DefaultListForm', 'DefaultChoiceForm'): + X(f'{i}<{form_tag}/>') for pres_tag in ('ObjectPresentation', 'ExtendedObjectPresentation', 'RecordPresentation', 'ExtendedRecordPresentation', 'ListPresentation', 'ExtendedListPresentation', 'Explanation'): key = pres_tag[0].lower() + pres_tag[1:] @@ -4544,9 +4554,10 @@ EDS_TABLE_GENERATED_TYPES = ( ) -def build_eds_table_xml(src_name, table_name, entry, fields_xml): - """Отдельный XML-документ таблицы. fields_xml — уже собранные узлы : их рендерит - вызывающий навык своим эмиттером реквизита, поэтому тело не зависит от того, какой это навык. +def build_eds_table_xml(src_name, table_name, entry, fields_xml, char_xml, default_forms_xml): + """Отдельный XML-документ таблицы. fields_xml, char_xml, default_forms_xml — уже собранные + узлы: их рендерит вызывающий навык своими эмиттерами, поэтому тело не зависит от того, + какой это навык. Возвращает строку: X пишет в общий список строк, поэтому «перехват» — запомнить длину, отдать эмиттерам, срезать добавленное (в ps1-порте тот же приём выражен через StringBuilder — различие рантаймов, не логики).""" @@ -4566,7 +4577,7 @@ def build_eds_table_xml(src_name, table_name, entry, fields_xml): X('\t\t') X('\t\t') - emit_eds_table_properties('\t\t\t', src_name, table_name, entry['props']) + emit_eds_table_properties('\t\t\t', src_name, table_name, entry['props'], char_xml, default_forms_xml) X('\t\t') if fields_xml: @@ -5116,7 +5127,25 @@ if obj_type == 'ExternalDataSource' and eds_tables: emit_attribute('\t\t\t', parse_attribute_shorthand(f), 'eds-field', 'Field') fields_xml = '\r\n'.join(lines[fields_before:]) del lines[fields_before:] - table_xml = build_eds_table_xml(obj_name, tbl_name, entry, fields_xml) + tp = entry['props'] or {} + char_before = len(lines) + emit_characteristics('\t\t\t', tp.get('characteristics')) + char_xml = '\r\n'.join(lines[char_before:]) + del lines[char_before:] + + # Слот формы: короткое имя разворачивается в полный путь таблицы внешнего источника — + # голое имя платформа отвергает («Неизвестный объект метаданных»). + forms_before = len(lines) + for form_tag in ('DefaultObjectForm', 'DefaultRecordForm', 'DefaultListForm', 'DefaultChoiceForm'): + key = form_tag[0].lower() + form_tag[1:] + form_val = tp.get(key) + if form_val and '.' not in str(form_val): + form_val = f'ExternalDataSource.{obj_name}.Table.{tbl_name}.Form.{form_val}' + emit_form_ref('\t\t\t', form_tag, form_val) + default_forms_xml = '\r\n'.join(lines[forms_before:]) + del lines[forms_before:] + + table_xml = build_eds_table_xml(obj_name, tbl_name, entry, fields_xml, char_xml, default_forms_xml) table_path = os.path.join(tables_dir, f'{tbl_name}.xml') write_xml_file_keep_eol(table_path, table_xml) eds_tables_created.append(table_path) diff --git a/.claude/skills/meta-edit/scripts/meta-edit.ps1 b/.claude/skills/meta-edit/scripts/meta-edit.ps1 index bb2e2860c..7f089f95e 100644 --- a/.claude/skills/meta-edit/scripts/meta-edit.ps1 +++ b/.claude/skills/meta-edit/scripts/meta-edit.ps1 @@ -1,4 +1,4 @@ -# meta-edit v1.45 — Edit existing 1C metadata object XML +# meta-edit v1.46 — Edit existing 1C metadata object XML # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills [CmdletBinding(PositionalBinding=$false)] param( @@ -1598,10 +1598,6 @@ $script:childTypeToXmlTag = @{ # таблица внешнего источника собирается в ОТДЕЛЬНЫЙ файл, и формат этого файла # должен быть один и тот же, кем бы он ни был создан. Держит check-inline-drift.mjs. -function Emit-FormRef { - param([string]$i, [string]$tag, $val) - if ($val) { X "$i<$tag>$(Esc-XmlText (Normalize-FormRef "$val"))" } else { X "$i<$tag/>" } -} # Шапка пространств имён файла таблицы внешнего источника — копия из meta-compile. $script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' @@ -1640,54 +1636,6 @@ function Emit-MLText { X "$indent" } -function Emit-Characteristics { - param([string]$indent, $chars) - if (-not $chars -or @($chars).Count -eq 0) { X "$indent"; return } - X "$indent" - foreach ($ch in @($chars)) { - $types = Get-ChElProp $ch @('types','characteristicTypes','типы') - $values = Get-ChElProp $ch @('values','characteristicValues','значения') - $tFrom = Normalize-CharFrom "$(Get-ChElProp $types @('from','source','источник'))" - $vFrom = Normalize-CharFrom "$(Get-ChElProp $values @('from','source','источник'))" - $key = Expand-CharField "$(Get-ChElProp $types @('key','keyField'))" $tFrom - $tff = Expand-CharField "$(Get-ChElProp $types @('filterField','typesFilterField'))" $tFrom - $obj = Expand-CharField "$(Get-ChElProp $values @('object','objectField'))" $vFrom - $typ = Expand-CharField "$(Get-ChElProp $values @('type','typeField'))" $vFrom - $val = Expand-CharField "$(Get-ChElProp $values @('value','valueField'))" $vFrom - # числовые поля-флаги (обычно -1; иногда 0) - $dpf = Get-CharIntField $types @('dataPathField') - $mvu = Get-CharIntField $types @('multipleValuesUseField') - $mvk = Get-CharIntField $values @('multipleValuesKeyField') - $mvo = Get-CharIntField $values @('multipleValuesOrderField') - X "$indent`t" - X "$indent`t`t" - X "$indent`t`t`t$(Esc-XmlText $key)" - X "$indent`t`t`t$(Esc-XmlText $tff)" - # filterValue: $null→nil; голое→xs:string, полный путь→DTR, bool→xs:boolean. - $tfvRaw = Get-ChElProp $types @('filterValue','typesFilterValue') - if ($null -eq $tfvRaw) { X "$indent`t`t`t" } - else { - $tfvN = Normalize-ChoiceValue $tfvRaw - if ([string]::IsNullOrEmpty($tfvN.Text)) { X "$indent`t`t`t" } - else { X "$indent`t`t`t$(Esc-XmlText $tfvN.Text)" } - } - # Числовое значение (обычно -1 или 0) — как есть; разворачивать через Expand-CharField нельзя, - # оно примет "0" за короткое имя поля и выдаст ".Attribute.0". - $dpfOut = if ("$dpf" -match '^-?\d+$') { "$dpf" } else { Esc-XmlText (Expand-CharField "$dpf" $tFrom) } - X "$indent`t`t`t$dpfOut" - X "$indent`t`t`t$mvu" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`t`t$(Esc-XmlText $obj)" - X "$indent`t`t`t$(Esc-XmlText $typ)" - X "$indent`t`t`t$(Esc-XmlText $val)" - X "$indent`t`t`t$mvk" - X "$indent`t`t`t$mvo" - X "$indent`t`t" - X "$indent`t" - } - X "$indent" -} function Emit-MDRefList { param([string]$indent, [string]$tag, $items) @@ -1784,7 +1732,10 @@ function Emit-EdsFunction { } function Emit-EdsTableProperties { - param([string]$indent, [string]$srcName, [string]$tableName, $t) + # $charXml и $defaultFormsXml — уже собранные блоки и четыре слота + # : их рендерит вызывающий навык своим эмиттером. Так тело не зависит + # от хелперов конкретного навыка и годится для копирования (check-inline-drift). + param([string]$indent, [string]$srcName, [string]$tableName, $t, [string]$charXml, [string]$defaultFormsXml) $i = $indent $tblSynonym = if ($t -and $null -ne $t.synonym) { $t.synonym } else { Split-CamelCase $tableName } X "$i$(Esc-XmlText $tableName)" @@ -1811,7 +1762,7 @@ function Emit-EdsTableProperties { # с полем родителя — пустая строка (как после загрузки), а без него — nil. if ($t -and $t.parentField) { X "$i" } else { X "$i" } - Emit-Characteristics $i $(if ($t) { $t.characteristics } else { $null }) + if ($charXml) { X $charXml.TrimEnd("`r", "`n") } else { X "$i" } X "$i$(if ($t -and $t.useStandardCommands -eq $false) { 'false' } else { 'true' })" X "$i$(if ($t -and $t.quickChoice -eq $true) { 'true' } else { 'false' })" @@ -1825,10 +1776,9 @@ function Emit-EdsTableProperties { X "$i$(if ($t -and $t.choiceDataGetModeOnInputByString) { "$($t.choiceDataGetModeOnInputByString)" } else { 'Directly' })" X "$i$(if ($t -and $t.choiceHistoryOnInput) { "$($t.choiceHistoryOnInput)" } else { 'Auto' })" - foreach ($formTag in @("DefaultObjectForm","DefaultRecordForm","DefaultListForm","DefaultChoiceForm")) { - $key = $formTag.Substring(0,1).ToLower() + $formTag.Substring(1) - Emit-FormRef $i $formTag $(if ($t) { $t.$key } else { $null }) - } + # Пустая строка — четыре слота всё равно обязаны быть: в свойствах таблицы их ровно 38. + if ($defaultFormsXml) { X $defaultFormsXml.TrimEnd("`r", "`n") } + else { foreach ($formTag in @("DefaultObjectForm","DefaultRecordForm","DefaultListForm","DefaultChoiceForm")) { X "$i<$formTag/>" } } foreach ($presTag in @("ObjectPresentation","ExtendedObjectPresentation","RecordPresentation", "ExtendedRecordPresentation","ListPresentation","ExtendedListPresentation","Explanation")) { $key = $presTag.Substring(0,1).ToLower() + $presTag.Substring(1) @@ -1845,9 +1795,9 @@ function Emit-EdsTableProperties { } function Build-EdsTableXml { - # $fieldsXml — уже собранные узлы : их рендерит вызывающий навык своим эмиттером - # реквизита. Так тело функции не зависит от того, какой это навык. - param([string]$srcName, [string]$tableName, $entry, [string]$fieldsXml) + # $fieldsXml, $charXml, $defaultFormsXml — уже собранные узлы: их рендерит вызывающий навык + # своими эмиттерами. Так тело функции не зависит от того, какой это навык. + param([string]$srcName, [string]$tableName, $entry, [string]$fieldsXml, [string]$charXml, [string]$defaultFormsXml) $before = $script:xml.Length $tableUuid = New-Guid-String @@ -1874,7 +1824,7 @@ function Build-EdsTableXml { X "`t`t" X "`t`t" - Emit-EdsTableProperties "`t`t`t" $srcName $tableName $entry.props + Emit-EdsTableProperties "`t`t`t" $srcName $tableName $entry.props $charXml $defaultFormsXml X "`t`t" if ($fieldsXml) { @@ -2285,6 +2235,19 @@ function Process-Add($addDef) { $tablesDir = Join-Path $srcDir "Tables" foreach ($entry in (Get-EdsTables $items).GetEnumerator()) { $tblName = $entry.Key + # Характеристики и слоты форм точечным добавлением не задаются: первые требуют + # эмиттера характеристик (живёт в meta-compile), вторые назначает form-add при + # добавлении формы. Молча проглотить ключ нельзя — модель решит, что он сработал. + $tv = $entry.Value.props + if ($tv) { + foreach ($k in @("characteristics","defaultObjectForm","defaultRecordForm","defaultListForm","defaultChoiceForm")) { + if ($tv.$k) { + Warn "Ключ '$k' не поддержан при добавлении таблицы: форму назначает form-add, характеристики — meta-compile. Таблица '$tblName' пропущена." + $tblName = $null; break + } + } + } + if (-not $tblName) { continue } if ($existingNames.ContainsKey($tblName)) { Warn "Table '$tblName' already exists, skipping" continue @@ -2299,7 +2262,7 @@ function Process-Add($addDef) { $fieldParts += Build-AttributeFragment (Parse-AttributeShorthand $f) "eds-field" "`t`t`t" "Field" } $fieldsXml = $fieldParts -join "`r`n" - $tableXml = Build-EdsTableXml $script:objName $tblName $entry.Value $fieldsXml + $tableXml = Build-EdsTableXml $script:objName $tblName $entry.Value $fieldsXml "" "" if (-not (Test-Path $tablesDir)) { New-Item -ItemType Directory -Path $tablesDir -Force | Out-Null } [System.IO.File]::WriteAllText($tablePath, $tableXml.TrimEnd("`r", "`n"), (New-Object System.Text.UTF8Encoding($true))) $fragmentXml = "$indent$(Esc-XmlText $tblName)
" diff --git a/.claude/skills/meta-edit/scripts/meta-edit.py b/.claude/skills/meta-edit/scripts/meta-edit.py index f1a4ff60f..bcb0d145b 100644 --- a/.claude/skills/meta-edit/scripts/meta-edit.py +++ b/.claude/skills/meta-edit/scripts/meta-edit.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# meta-edit v1.45 — Edit existing 1C metadata object XML +# meta-edit v1.46 — Edit existing 1C metadata object XML # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -1593,12 +1593,6 @@ child_type_to_xml_tag = { xmlns_decl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' lines = [] -def emit_form_ref(i, tag, val): - """Ссылка на форму по умолчанию: непустая → значение, иначе .""" - if val: - X(f'{i}<{tag}>{esc_xml_text(normalize_form_ref(str(val)))}') - else: - X(f'{i}<{tag}/>') def X(text): lines.append(text) @@ -1627,54 +1621,6 @@ def emit_mltext(indent, tag, text): emit_ml_items(f'{indent}\t', text) X(f'{indent}') -def emit_characteristics(indent, chars): - if not chars: - X(f'{indent}') - return - X(f'{indent}') - for ch in chars: - types = ch_el_prop(ch, ['types', 'characteristicTypes', 'типы']) - values = ch_el_prop(ch, ['values', 'characteristicValues', 'значения']) - t_from = normalize_char_from(ch_el_prop(types, ['from', 'source', 'источник']) or '') - v_from = normalize_char_from(ch_el_prop(values, ['from', 'source', 'источник']) or '') - key = expand_char_field(ch_el_prop(types, ['key', 'keyField']), t_from) - tff = expand_char_field(ch_el_prop(types, ['filterField', 'typesFilterField']), t_from) - obj = expand_char_field(ch_el_prop(values, ['object', 'objectField']), v_from) - typ = expand_char_field(ch_el_prop(values, ['type', 'typeField']), v_from) - val = expand_char_field(ch_el_prop(values, ['value', 'valueField']), v_from) - dpf = char_int_field(types, ['dataPathField']) - mvu = char_int_field(types, ['multipleValuesUseField']) - mvk = char_int_field(values, ['multipleValuesKeyField']) - mvo = char_int_field(values, ['multipleValuesOrderField']) - X(f'{indent}\t') - X(f'{indent}\t\t') - X(f'{indent}\t\t\t{esc_xml_text(key)}') - X(f'{indent}\t\t\t{esc_xml_text(tff)}') - # filterValue: None→nil; голое→xs:string, полный путь→DTR, bool→xs:boolean. - tfv_raw = ch_el_prop(types, ['filterValue', 'typesFilterValue']) - if tfv_raw is None: - X(f'{indent}\t\t\t') - else: - tfv_xt, tfv_tx = normalize_choice_value(tfv_raw) - if tfv_tx == '' or tfv_tx is None: - X(f'{indent}\t\t\t') - else: - X(f'{indent}\t\t\t{esc_xml_text(tfv_tx)}') - # Числовое значение (обычно -1 или 0) — как есть; expand_char_field примет "0" за короткое - # имя поля и выдаст ".Attribute.0". - dpf_out = str(dpf) if re.fullmatch(r'-?\d+', str(dpf)) else esc_xml_text(expand_char_field(str(dpf), t_from)) - X(f'{indent}\t\t\t{dpf_out}') - X(f'{indent}\t\t\t{mvu}') - X(f'{indent}\t\t') - X(f'{indent}\t\t') - X(f'{indent}\t\t\t{esc_xml_text(obj)}') - X(f'{indent}\t\t\t{esc_xml_text(typ)}') - X(f'{indent}\t\t\t{esc_xml_text(val)}') - X(f'{indent}\t\t\t{mvk}') - X(f'{indent}\t\t\t{mvo}') - X(f'{indent}\t\t') - X(f'{indent}\t') - X(f'{indent}') def emit_md_ref_list(indent, tag, items): """Список MDObjectRef (Documents/RegisterRecords/DocumentMap/…) с . omit-on-empty.""" @@ -1771,8 +1717,12 @@ def emit_eds_function(indent, fn_name, val, type_xml): X(f'{indent}\t') X(f'{indent}') -def emit_eds_table_properties(indent, src_name, table_name, t): - """Свойства таблицы: 38 узлов в порядке выгрузки платформы.""" +def emit_eds_table_properties(indent, src_name, table_name, t, char_xml, default_forms_xml): + """Свойства таблицы: 38 узлов в порядке выгрузки платформы. + + char_xml и default_forms_xml — уже собранные блоки и четыре слота + : их рендерит вызывающий навык своим эмиттером. Так тело не зависит + от хелперов конкретного навыка и годится для копирования (check-inline-drift).""" i = indent t = t or {} tbl_synonym = t['synonym'] if t.get('synonym') is not None else split_camel_case(table_name) @@ -1809,7 +1759,10 @@ def emit_eds_table_properties(indent, src_name, table_name, t): X(f'{i}') else: X(f'{i}') - emit_characteristics(i, t.get('characteristics')) + if char_xml: + X(char_xml.rstrip('\r\n')) + else: + X(f'{i}') X(f'{i}{"false" if t.get("useStandardCommands") is False else "true"}') X(f'{i}{"true" if t.get("quickChoice") is True else "false"}') @@ -1827,9 +1780,12 @@ def emit_eds_table_properties(indent, src_name, table_name, t): X(f'{i}{t.get("choiceDataGetModeOnInputByString") or "Directly"}') X(f'{i}{t.get("choiceHistoryOnInput") or "Auto"}') - for form_tag in ('DefaultObjectForm', 'DefaultRecordForm', 'DefaultListForm', 'DefaultChoiceForm'): - key = form_tag[0].lower() + form_tag[1:] - emit_form_ref(i, form_tag, t.get(key)) + # Пустая строка — четыре слота всё равно обязаны быть: в свойствах таблицы их ровно 38. + if default_forms_xml: + X(default_forms_xml.rstrip('\r\n')) + else: + for form_tag in ('DefaultObjectForm', 'DefaultRecordForm', 'DefaultListForm', 'DefaultChoiceForm'): + X(f'{i}<{form_tag}/>') for pres_tag in ('ObjectPresentation', 'ExtendedObjectPresentation', 'RecordPresentation', 'ExtendedRecordPresentation', 'ListPresentation', 'ExtendedListPresentation', 'Explanation'): key = pres_tag[0].lower() + pres_tag[1:] @@ -1855,9 +1811,10 @@ EDS_TABLE_GENERATED_TYPES = ( ) -def build_eds_table_xml(src_name, table_name, entry, fields_xml): - """Отдельный XML-документ таблицы. fields_xml — уже собранные узлы : их рендерит - вызывающий навык своим эмиттером реквизита, поэтому тело не зависит от того, какой это навык. +def build_eds_table_xml(src_name, table_name, entry, fields_xml, char_xml, default_forms_xml): + """Отдельный XML-документ таблицы. fields_xml, char_xml, default_forms_xml — уже собранные + узлы: их рендерит вызывающий навык своими эмиттерами, поэтому тело не зависит от того, + какой это навык. Возвращает строку: X пишет в общий список строк, поэтому «перехват» — запомнить длину, отдать эмиттерам, срезать добавленное (в ps1-порте тот же приём выражен через StringBuilder — различие рантаймов, не логики).""" @@ -1877,7 +1834,7 @@ def build_eds_table_xml(src_name, table_name, entry, fields_xml): X('\t\t') X('\t\t') - emit_eds_table_properties('\t\t\t', src_name, table_name, entry['props']) + emit_eds_table_properties('\t\t\t', src_name, table_name, entry['props'], char_xml, default_forms_xml) X('\t\t') if fields_xml: @@ -1893,10 +1850,6 @@ def build_eds_table_xml(src_name, table_name, entry, fields_xml): del lines[before:] return chunk -# ============================================================ -# DSL key normalization -# ============================================================ - def resolve_operation_key(key): k = key.lower().strip() @@ -2236,6 +2189,15 @@ def process_add(add_def): src_dir = os.path.join(os.path.dirname(resolved_path), obj_name) tables_dir = os.path.join(src_dir, "Tables") for tbl_name, entry in get_eds_tables(items).items(): + # Характеристики и слоты форм точечным добавлением не задаются: первые требуют + # эмиттера характеристик (живёт в meta-compile), вторые назначает form-add при + # добавлении формы. Молча проглотить ключ нельзя — модель решит, что он сработал. + tv = entry['props'] or {} + bad_key = next((k for k in ('characteristics', 'defaultObjectForm', 'defaultRecordForm', + 'defaultListForm', 'defaultChoiceForm') if tv.get(k)), None) + if bad_key: + warn(f"Ключ '{bad_key}' не поддержан при добавлении таблицы: форму назначает form-add, характеристики — meta-compile. Таблица '{tbl_name}' пропущена.") + continue if tbl_name in existing_names: warn(f"Table '{tbl_name}' already exists, skipping") continue @@ -2247,7 +2209,7 @@ def process_add(add_def): for f in entry["fields"]: field_parts.append(build_attribute_fragment(parse_attribute_shorthand(f), "eds-field", "\t\t\t", "Field")) fields_xml = "\r\n".join(field_parts) - table_xml = build_eds_table_xml(obj_name, tbl_name, entry, fields_xml) + table_xml = build_eds_table_xml(obj_name, tbl_name, entry, fields_xml, '', '') os.makedirs(tables_dir, exist_ok=True) with open(table_path, "w", encoding="utf-8-sig", newline="") as fh: fh.write(table_xml.rstrip("\r\n")) diff --git a/tests/skills/cases/meta-edit/eds-add-table-unsupported-keys.json b/tests/skills/cases/meta-edit/eds-add-table-unsupported-keys.json new file mode 100644 index 000000000..d3b55298d --- /dev/null +++ b/tests/skills/cases/meta-edit/eds-add-table-unsupported-keys.json @@ -0,0 +1,33 @@ +{ + "name": "Таблица с формой по умолчанию: отказ с объяснением, а не тихий сбой", + "setup": "empty-config", + "preRun": [ + { + "script": "meta-compile/scripts/meta-compile", + "input": { + "type": "ExternalDataSource", + "name": "PG", + "tables": { "products": { "keyFields": ["id"], "fields": ["id: Number(10,0)"] } } + }, + "args": { "-JsonPath": "{inputFile}", "-OutputDir": "{workDir}" } + } + ], + "params": { "objectPath": "ExternalDataSources/PG.xml" }, + "input": { + "add": { + "tables": { + "sales": { + "defaultListForm": "ФормаСписка", + "fields": ["id: Number(10,0)"] + } + } + } + }, + "expect": { + "stdoutContains": ["не поддержан при добавлении таблицы", "form-add"], + "filesAbsent": ["ExternalDataSources/PG/Tables/sales.xml"], + "fileNotContains": [ + { "file": "ExternalDataSources/PG.xml", "text": "sales
" } + ] + } +} diff --git a/tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/Configuration.xml b/tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/Configuration.xml new file mode 100644 index 000000000..da0e784ee --- /dev/null +++ b/tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/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 + + + + Русский + PG + + + \ No newline at end of file diff --git a/tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/Ext/ClientApplicationInterface.xml b/tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/Ext/ClientApplicationInterface.xml new file mode 100644 index 000000000..3c1161b2d --- /dev/null +++ b/tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/Ext/ClientApplicationInterface.xml @@ -0,0 +1,18 @@ + + + + + UUID-002 + + + + + UUID-004 + + + + + + + + \ No newline at end of file diff --git a/tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/ExternalDataSources/PG.xml b/tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/ExternalDataSources/PG.xml new file mode 100644 index 000000000..a1b89619b --- /dev/null +++ b/tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/ExternalDataSources/PG.xml @@ -0,0 +1,33 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + + PG + + + ru + PG + + + + Automatic + + + products
+
+
+
\ No newline at end of file diff --git a/tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/ExternalDataSources/PG/Tables/products.xml b/tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/ExternalDataSources/PG/Tables/products.xml new file mode 100644 index 000000000..205ef9363 --- /dev/null +++ b/tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/ExternalDataSources/PG/Tables/products.xml @@ -0,0 +1,130 @@ + + + + + + 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 + + + UUID-016 + UUID-017 + + + + products + + + ru + products + + + + Table + products + + NonobjectData + + ExternalDataSource.PG.Table.products.Field.id + + + + + + true + false + + Auto + Begin + Directly + Auto + + + + + + + + + + + + false + false + Auto + + InDialog + + + Automatic + + + + + id + + + ru + id + + + + + xs:decimal + + 10 + 0 + Any + + + false + + + + false + + false + false + + + false + 0 + DontCheck + + + Auto + Auto + Auto + + id + false + false + + + +
+
\ No newline at end of file diff --git a/tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/Languages/Русский.xml b/tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/Languages/Русский.xml new file mode 100644 index 000000000..37c60d786 --- /dev/null +++ b/tests/skills/cases/meta-edit/snapshots/eds-add-table-unsupported-keys/Languages/Русский.xml @@ -0,0 +1,16 @@ + + + + + Русский + + + ru + Русский + + + + ru + + + \ No newline at end of file diff --git a/tests/skills/check-inline-drift.mjs b/tests/skills/check-inline-drift.mjs index c9f7022a6..35f5c96b6 100644 --- a/tests/skills/check-inline-drift.mjs +++ b/tests/skills/check-inline-drift.mjs @@ -753,6 +753,34 @@ for (const family of FAMILIES) { if (l !== lang || declared.has(skill) || !fns.has(fnName)) continue; errors.push(`${family.name} [${lang}]: ${skill} содержит ${fnName}, но в реестре не объявлен`); } + + // 4. Копия обязана быть не только ИДЕНТИЧНОЙ, но и РАЗРЕШИМОЙ: всё, что её тело зовёт, + // должно быть определено в том же навыке. Иначе получаем вызов несуществующей функции — + // в PowerShell это тихий ложный успех (CommandNotFoundException, но код возврата 0), + // а в python трейсбек, то есть ещё и расхождение потоков ошибок между портами. + // Что считать «вызовом своей функции»: имя, определённое в НАВЫКЕ-ЭТАЛОНЕ. Командлеты и + // встроенные функции в эталоне не определены, поэтому список исключений не нужен. + for (const { v, members } of effective) { + const authorityFns = index.get(`${v.authority}|${lang}`); + if (!authorityFns) continue; + for (const skill of members) { + if (skill === v.authority) continue; + const skillFns = index.get(`${skill}|${lang}`); + if (!skillFns) continue; + for (const { file, body: bodyLines } of copiesOf(index, skill, lang, fnName)) { + const body = bodyLines.join('\n'); // тело хранится списком строк + const called = lang === 'ps1' + ? body.match(/(? c !== fnName && authorityFns.has(c) && !skillFns.has(c)); + if (missing.length) { + const label = file.startsWith(`${skill}.`) ? skill : `${skill} (${file})`; + errors.push(`${family.name} [${lang}]: ${label} зовёт ${missing.join(', ')} — этих функций в навыке нет, копия неразрешима`); + } + } + } + } } // 4. Долг: отклоняющийся вариант без обоснования. Базовым считаем самый массовый — ему