From 123d6e326b49bc4c4ad3c55549bb69d4e02888ca Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sun, 6 Sep 2026 13:55:01 +0300 Subject: [PATCH] =?UTF-8?q?feat(meta-edit):=20=D1=82=D0=B0=D0=B1=D0=BB?= =?UTF-8?q?=D0=B8=D1=86=D0=B0=20=D0=B8=20=D1=84=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D1=8F=D1=8E?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D0=B2=20=D1=81=D1=83=D1=89=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D1=83=D1=8E=D1=89=D0=B8=D0=B9=20=D0=B2=D0=BD=D0=B5?= =?UTF-8?q?=D1=88=D0=BD=D0=B8=D0=B9=20=D0=B8=D1=81=D1=82=D0=BE=D1=87=D0=BD?= =?UTF-8?q?=D0=B8=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Дыра в сценарии: добавить таблицу или функцию к уже существующему источнику было нечем. Совет «пересоберите источник целиком через meta-compile» оказался вредным — повторная компиляция заменяет файл источника, выдаёт объекту НОВЫЙ uuid и оставляет файлы не упомянутых таблиц сиротами: в ChildObjects их уже нет, а на диске они остались. meta-edit на файле источника принимает add.tables и add.functions. Таблица — единственная операция навыка, создающая файл: <Источник>/Tables/<Имя>.xml плюс имя в ChildObjects. Синтаксис тот же, что у meta-compile. Формат файла таблицы обязан быть один и тот же, кем бы файл ни был создан, поэтому эмиттеры скопированы из meta-compile механически и зарегистрированы в check-inline-drift.mjs — гард теперь сверяет семь функций между двумя навыками. Чтобы копии не тянули за собой пол-navыка, две из них развязаны от эмиттеров-специфик: Build-EdsTableXml принимает готовый XML полей, Emit-EdsFunction — готовый XML типа. Каждый навык рендерит их своим эмиттером, вывод не изменился. meta-compile теперь предупреждает о перезаписи существующего объекта — для всех видов, не только внешних источников: молчаливая замена uuid ломает ссылки, а узнать об этом было неоткуда. Раундтрип на платформе 8.3.24.1691: таблица, добавленная через meta-edit, возвращается из выгрузки байт в байт. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi --- .../reference/external-data-source.md | 15 + .../meta-compile/scripts/meta-compile.ps1 | 57 ++- .../meta-compile/scripts/meta-compile.py | 57 ++- .claude/skills/meta-edit/child-operations.md | 18 +- .../skills/meta-edit/scripts/meta-edit.ps1 | 376 ++++++++++++++++- .claude/skills/meta-edit/scripts/meta-edit.py | 380 +++++++++++++++++- .../skills/cases/meta-edit/eds-add-table.json | 48 +++ .../snapshots/eds-add-table/Configuration.xml | 252 ++++++++++++ .../Ext/ClientApplicationInterface.xml | 18 + .../eds-add-table/ExternalDataSources/PG.xml | 50 +++ .../PG/Tables/products.xml | 130 ++++++ .../ExternalDataSources/PG/Tables/sales.xml | 213 ++++++++++ .../eds-add-table/Languages/Русский.xml | 16 + tests/skills/check-inline-drift.mjs | 19 + 14 files changed, 1603 insertions(+), 46 deletions(-) create mode 100644 tests/skills/cases/meta-edit/eds-add-table.json create mode 100644 tests/skills/cases/meta-edit/snapshots/eds-add-table/Configuration.xml create mode 100644 tests/skills/cases/meta-edit/snapshots/eds-add-table/Ext/ClientApplicationInterface.xml create mode 100644 tests/skills/cases/meta-edit/snapshots/eds-add-table/ExternalDataSources/PG.xml create mode 100644 tests/skills/cases/meta-edit/snapshots/eds-add-table/ExternalDataSources/PG/Tables/products.xml create mode 100644 tests/skills/cases/meta-edit/snapshots/eds-add-table/ExternalDataSources/PG/Tables/sales.xml create mode 100644 tests/skills/cases/meta-edit/snapshots/eds-add-table/Languages/Русский.xml diff --git a/.claude/skills/meta-compile/reference/external-data-source.md b/.claude/skills/meta-compile/reference/external-data-source.md index dfc5f8731..a05445626 100644 --- a/.claude/skills/meta-compile/reference/external-data-source.md +++ b/.claude/skills/meta-compile/reference/external-data-source.md @@ -122,6 +122,21 @@ } ``` +## Добавить в существующий источник + +`meta-compile` описывает источник **целиком**: повторный запуск заменяет его файл и выдаёт новый +uuid, а таблицы, которых нет в описании, останутся на диске сиротами. Чтобы дописать таблицу или +функцию в уже существующий источник, есть `meta-edit`: + +```json +{ "add": { + "tables": { "sales": { "keyFields": ["id"], "fields": ["id: Number(10,0)", "summa: Number(15,2)"] } }, + "functions": { "nextKey": "NEXT VALUE FOR public.seq_key" } +} } +``` + +Удалить таблицу — `meta-remove ExternalDataSource.<Источник>.Table.<Таблица>`. + ## Не поддерживается - **Кубы OLAP** (`Cube`, `DimensionTable`, `Dimension`, `Resource`). diff --git a/.claude/skills/meta-compile/scripts/meta-compile.ps1 b/.claude/skills/meta-compile/scripts/meta-compile.ps1 index 6e51a7c2b..d82dbb021 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.105 — Compile 1C metadata object from JSON +# meta-compile v1.106 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills [CmdletBinding(PositionalBinding=$false)] param( @@ -4411,7 +4411,9 @@ function Emit-ExternalDataSourceProperties { # Функция внешнего источника. Параметров как объектов метаданных нет: они записаны прямо # в выражении как &1, &2 (см. reference/external-data-source.md). function Emit-EdsFunction { - param([string]$indent, [string]$fnName, $val) + # $typeXml — уже собранный узел возвращаемого значения: его рендерит вызывающий навык + # своим эмиттером типов. Так тело функции не зависит от того, какой это навык. + param([string]$indent, [string]$fnName, $val, [string]$typeXml) $expr = "" $returns = "" $returnValue = $true @@ -4437,8 +4439,8 @@ function Emit-EdsFunction { Emit-MLText "$indent`t`t" "Synonym" $fnSynonym if ($fnComment) { X "$indent`t`t$(Esc-XmlText $fnComment)" } else { X "$indent`t`t" } X "$indent`t`t$(if ($returnValue) { 'true' } else { 'false' })" - if ($returnValue) { - Emit-ValueType "$indent`t`t" $(if ($returns) { $returns } else { "String" }) + if ($returnValue -and $typeXml) { + X $typeXml.TrimEnd("`r", "`n") } else { X "$indent`t`t" } @@ -4474,10 +4476,7 @@ function Emit-EdsTableProperties { # ВАЖНО: платформа при загрузке XML сбрасывает заданное значение в пустую строку — проверено # на её собственной выгрузке. Задать его можно только интерактивно, поэтому дефолт у таблицы # с полем родителя — пустая строка (как после загрузки), а без него — nil. - $upv = if ($t) { $t.unfilledParentValue } else { $null } - $hasParent = ($t -and $t.parentField) - if ($null -ne $upv) { Emit-MinMaxValue $i "UnfilledParentValue" $upv } - elseif ($hasParent) { X "$i" } + if ($t -and $t.parentField) { X "$i" } else { X "$i" } Emit-Characteristics $i $(if ($t) { $t.characteristics } else { $null }) @@ -4516,7 +4515,9 @@ function Emit-EdsTableProperties { # поэтому «перехват» — запомнить длину, отдать эмиттерам, вырезать добавленное # (тот же приём, что у составного типа). function Build-EdsTableXml { - param([string]$srcName, [string]$tableName, $entry) + # $fieldsXml — уже собранные узлы : их рендерит вызывающий навык своим эмиттером + # реквизита. Так тело функции не зависит от того, какой это навык. + param([string]$srcName, [string]$tableName, $entry, [string]$fieldsXml) $before = $script:xml.Length $tableUuid = New-Guid-String @@ -4546,13 +4547,9 @@ function Build-EdsTableXml { Emit-EdsTableProperties "`t`t`t" $srcName $tableName $entry.props X "`t`t" - $fields = @($entry.fields) - if ($fields.Count -gt 0) { + if ($fieldsXml) { X "`t`t" - foreach ($f in $fields) { - $parsed = Parse-AttributeShorthand $f - Emit-Attribute "`t`t`t" $parsed "eds-field" "Field" - } + X $fieldsXml.TrimEnd("`r", "`n") X "`t`t" } else { X "`t`t" @@ -5021,7 +5018,19 @@ if ($objType -eq "ExternalDataSource") { X "`t`t`t$(Esc-XmlText $tblName)
" } foreach ($fnName in $functions.Keys) { - Emit-EdsFunction "`t`t`t" $fnName $functions[$fnName] + $fnVal = $functions[$fnName] + $fnReturns = if ($fnVal -is [string]) { "String" } + elseif ($fnVal.returns) { "$($fnVal.returns)" } + elseif ($fnVal.returnType) { "$($fnVal.returnType)" } else { "String" } + $fnNoValue = (-not ($fnVal -is [string])) -and ($null -ne $fnVal.returnValue) -and ($fnVal.returnValue -ne $true) + $fnTypeXml = "" + if (-not $fnNoValue) { + $typeBefore = $script:xml.Length + Emit-ValueType "`t`t`t`t`t" $fnReturns + $fnTypeXml = $script:xml.ToString($typeBefore, $script:xml.Length - $typeBefore) + [void]$script:xml.Remove($typeBefore, $script:xml.Length - $typeBefore) + } + Emit-EdsFunction "`t`t`t" $fnName $fnVal $fnTypeXml } X "`t`t
" } else { @@ -5310,6 +5319,13 @@ function Write-XmlFileKeepEol([string]$path, [string]$text, $encoding) { [System.IO.File]::WriteAllText($path, $text.TrimEnd("`r", "`n"), $encoding) } +# Объект с таким именем уже есть: компиляция заменит его файл ЦЕЛИКОМ и выдаст новый uuid — +# ссылки на прежний объект (из кода, состава подсистем, типов реквизитов) станут висячими. +# Для доработки существующего объекта есть meta-edit; молчать об этом нельзя. +if (Test-Path $mainXmlPath) { + Write-Warning "$objType '$objName' уже существует ($typePlural/$objName.xml) — файл будет перезаписан, объект получит НОВЫЙ uuid, ссылки на прежний сломаются. Для правки существующего объекта используйте meta-edit." +} + Write-XmlFileKeepEol $mainXmlPath $metadataXml $enc # Таблицы внешнего источника — отдельными файлами в <Источник>/Tables/. @@ -5319,7 +5335,14 @@ if ($objType -eq "ExternalDataSource" -and $script:edsTables.Count -gt 0) { $tablesDir = Join-Path $objSubDir "Tables" if (-not (Test-Path $tablesDir)) { New-Item -ItemType Directory -Path $tablesDir -Force | Out-Null } foreach ($tblName in $script:edsTables.Keys) { - $tableXml = Build-EdsTableXml $objName $tblName $script:edsTables[$tblName] + $entry = $script:edsTables[$tblName] + $fieldsBefore = $script:xml.Length + foreach ($f in @($entry.fields)) { + Emit-Attribute "`t`t`t" (Parse-AttributeShorthand $f) "eds-field" "Field" + } + $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 $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 5887eed92..7adbc4ef6 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.105 — Compile 1C metadata object from JSON +# meta-compile v1.106 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -4403,9 +4403,12 @@ def emit_external_data_source_properties(indent): X(f'{i}{dlcm}') -def emit_eds_function(indent, fn_name, val): +def emit_eds_function(indent, fn_name, val, type_xml): """Функция внешнего источника. Параметров как объектов метаданных нет: они записаны прямо - в выражении как &1, &2 (см. reference/external-data-source.md).""" + в выражении как &1, &2 (см. reference/external-data-source.md). + + type_xml — уже собранный узел возвращаемого значения: его рендерит вызывающий навык + своим эмиттером типов. Так тело функции не зависит от того, какой это навык.""" fn_synonym = None fn_comment = '' returns = '' @@ -4432,8 +4435,8 @@ def emit_eds_function(indent, fn_name, val): else: X(f'{indent}\t\t') X(f'{indent}\t\t{"true" if return_value else "false"}') - if return_value: - emit_value_type(f'{indent}\t\t', returns or 'String') + if return_value and type_xml: + X(type_xml.rstrip('\r\n')) else: X(f'{indent}\t\t') X(f'{indent}\t\t{esc_xml_text(expr)}') @@ -4475,10 +4478,7 @@ def emit_eds_table_properties(indent, src_name, table_name, t): # ВАЖНО: платформа при загрузке XML сбрасывает заданное значение в пустую строку — проверено # на её собственной выгрузке. Задать его можно только интерактивно, поэтому дефолт у таблицы # с полем родителя — пустая строка (как после загрузки), а без него — nil. - upv = t.get('unfilledParentValue') - if upv is not None: - emit_min_max_value(i, 'UnfilledParentValue', upv) - elif t.get('parentField'): + if t.get('parentField'): X(f'{i}') else: X(f'{i}') @@ -4529,8 +4529,10 @@ EDS_TABLE_GENERATED_TYPES = ( ) -def build_eds_table_xml(src_name, table_name, entry): - """Отдельный XML-документ таблицы. Возвращает строку: X пишет в общий список строк, +def build_eds_table_xml(src_name, table_name, entry, fields_xml): + """Отдельный XML-документ таблицы. fields_xml — уже собранные узлы : их рендерит + вызывающий навык своим эмиттером реквизита, поэтому тело не зависит от того, какой это навык. + Возвращает строку: X пишет в общий список строк, поэтому «перехват» — запомнить длину, отдать эмиттерам, срезать добавленное (в ps1-порте тот же приём выражен через StringBuilder — различие рантаймов, не логики).""" before = len(lines) @@ -4552,11 +4554,9 @@ def build_eds_table_xml(src_name, table_name, entry): emit_eds_table_properties('\t\t\t', src_name, table_name, entry['props']) X('\t\t') - fields = entry['fields'] - if fields: + if fields_xml: X('\t\t') - for f in fields: - emit_attribute('\t\t\t', parse_attribute_shorthand(f), 'eds-field', 'Field') + X(fields_xml.rstrip('\r\n')) X('\t\t') else: X('\t\t') @@ -4996,7 +4996,18 @@ if obj_type == 'ExternalDataSource': for tbl_name in eds_tables: X(f'\t\t\t{esc_xml_text(tbl_name)}
') for fn_name, fn_val in functions.items(): - emit_eds_function('\t\t\t', fn_name, fn_val) + if isinstance(fn_val, str): + fn_returns, fn_no_value = 'String', False + else: + fn_returns = str(fn_val.get('returns') or fn_val.get('returnType') or 'String') + fn_no_value = fn_val.get('returnValue') is not None and fn_val.get('returnValue') is not True + fn_type_xml = '' + if not fn_no_value: + type_before = len(lines) + emit_value_type('\t\t\t\t\t', fn_returns) + fn_type_xml = '\r\n'.join(lines[type_before:]) + del lines[type_before:] + emit_eds_function('\t\t\t', fn_name, fn_val, fn_type_xml) X('\t\t
') else: X('\t\t') @@ -5069,6 +5080,12 @@ os.makedirs(type_dir, exist_ok=True) if obj_type not in types_no_sub_dir: os.makedirs(obj_sub_dir, exist_ok=True) +# Объект с таким именем уже есть: компиляция заменит его файл ЦЕЛИКОМ и выдаст новый uuid — +# ссылки на прежний объект (из кода, состава подсистем, типов реквизитов) станут висячими. +# Для доработки существующего объекта есть meta-edit; молчать об этом нельзя. +if os.path.exists(main_xml_path): + print(f"WARNING: {obj_type} '{obj_name}' уже существует ({type_plural}/{obj_name}.xml) — файл будет перезаписан, объект получит НОВЫЙ uuid, ссылки на прежний сломаются. Для правки существующего объекта используйте meta-edit.", file=sys.stderr) + write_xml_file_keep_eol(main_xml_path, metadata_xml) # Таблицы внешнего источника — отдельными файлами в <Источник>/Tables/. @@ -5078,7 +5095,13 @@ if obj_type == 'ExternalDataSource' and eds_tables: tables_dir = os.path.join(obj_sub_dir, 'Tables') os.makedirs(tables_dir, exist_ok=True) for tbl_name in eds_tables: - table_xml = build_eds_table_xml(obj_name, tbl_name, eds_tables[tbl_name]) + entry = eds_tables[tbl_name] + fields_before = len(lines) + for f in entry['fields']: + 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) 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/child-operations.md b/.claude/skills/meta-edit/child-operations.md index 4be02f942..8857cb582 100644 --- a/.claude/skills/meta-edit/child-operations.md +++ b/.claude/skills/meta-edit/child-operations.md @@ -55,9 +55,21 @@ ] } } ``` -Сам внешний источник точечно не правится: и таблица (отдельный файл), и функция (узел с полным -набором свойств) собираются `meta-compile` по описанию источника целиком. Удалить таблицу — -`meta-remove ExternalDataSource.<Источник>.Table.<Таблица>`. +## add-tables / add-functions (внешний источник данных) + +На файле источника (`ExternalDataSources/<Имя>.xml`) добавляются таблицы и функции. Таблица — +единственная операция навыка, создающая **файл**: `<Источник>/Tables/<Имя>.xml` плюс имя в +`ChildObjects` источника. Синтаксис таблицы тот же, что в `meta-compile` +(см. `reference/external-data-source.md` там же). + +```json +{ "add": { + "tables": { "sales": { "keyFields": ["id"], "fields": ["id: Number(10,0)", "summa: Number(15,2)"] } }, + "functions": { "nextKey": "NEXT VALUE FOR public.seq_key" } +} } +``` + +Удалить таблицу — `meta-remove ExternalDataSource.<Источник>.Table.<Таблица>`. ## add-ts diff --git a/.claude/skills/meta-edit/scripts/meta-edit.ps1 b/.claude/skills/meta-edit/scripts/meta-edit.ps1 index 90511deb7..bb2e2860c 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.44 — Edit existing 1C metadata object XML +# meta-edit v1.45 — Edit existing 1C metadata object XML # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills [CmdletBinding(PositionalBinding=$false)] param( @@ -466,6 +466,8 @@ $script:childTypeSynonyms = @{ "commands" = "commands"; "команды" = "commands" "properties" = "properties"; "свойства" = "properties" "fields" = "fields"; "поля" = "fields" + "tables" = "tables"; "таблицы" = "tables" + "functions" = "functions"; "функции" = "functions" } # Type synonyms (from meta-compile) @@ -1561,9 +1563,7 @@ $script:validChildTypes = @{ "CalculationRegister" = @("dimensions","resources","attributes","forms","templates","commands") "DocumentJournal" = @("columns","forms","templates","commands") "Constant" = @("forms") - # Внешний источник данных правится целиком через meta-compile: и таблица (отдельный файл), - # и функция (узел с полным набором свойств) требуют эмиттера, который живёт там. - "ExternalDataSource" = @() + "ExternalDataSource" = @("tables","functions") "Table" = @("fields","forms","templates","commands") } @@ -1587,6 +1587,309 @@ $script:childTypeToXmlTag = @{ "templates" = "Template" "commands" = "Command" "fields" = "Field" + "tables" = "Table" + "functions" = "Function" +} + +# ============================================================ +# Section 8b: Внешние источники данных — копии из meta-compile +# ============================================================ +# Тела ниже скопированы из meta-compile и обязаны совпадать с ним байт в байт: +# таблица внешнего источника собирается в ОТДЕЛЬНЫЙ файл, и формат этого файла +# должен быть один и тот же, кем бы он ни был создан. Держит 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"' + +$script:xml = New-Object System.Text.StringBuilder 32768 + +function X { + param([string]$text) + $script:xml.AppendLine($text) | Out-Null +} + +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-XmlText "$($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-XmlText "$($p.Value)")"; X "$indent" + } + } else { + X "$indent"; X "$indent`tru"; X "$indent`t$(Esc-XmlText "$val")"; X "$indent" + } +} + +function Emit-MLText { + param([string]$indent, [string]$tag, $text) + # Пусто (null / пустая строка) → самозакрывающийся тег. + if (($null -eq $text) -or (($text -is [string]) -and ($text -eq ''))) { + X "$indent<$tag/>" + return + } + X "$indent<$tag>" + Emit-MLItems "$indent`t" $text + X "$indent" +} + +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) + $arr = @(); if ($items) { $arr = @($items) } + if ($arr.Count -gt 0) { + X "$indent<$tag>" + foreach ($it in $arr) { X "$indent`t$(Esc-XmlText (Normalize-MDObjectRef "$it"))" } + X "$indent" + } else { + X "$indent<$tag/>" + } +} + +function Get-EdsTables { + param($val) + $tables = [ordered]@{} + if (-not $val) { return $tables } + function New-EdsTableEntry { param($v) + if ($v -is [array] -or $v.GetType().Name -eq 'Object[]') { + return @{ props = $null; fields = @($v) } + } + $f = if ($null -ne $v.fields) { @($v.fields) } elseif ($null -ne $v.columns) { @($v.columns) } else { @() } + return @{ props = $v; fields = $f } + } + if ($val -is [array] -or $val.GetType().Name -eq 'Object[]') { + foreach ($t in $val) { $tables["$($t.name)"] = New-EdsTableEntry $t } + } else { + $val.PSObject.Properties | ForEach-Object { $tables[$_.Name] = New-EdsTableEntry $_.Value } + } + return $tables +} + +function Get-EdsFieldRef { + param([string]$srcName, [string]$tableName, [string]$fieldName) + if (-not $fieldName) { return "" } + if ($fieldName -like "ExternalDataSource.*") { return $fieldName } + return "ExternalDataSource.$srcName.Table.$tableName.Field.$fieldName" +} + +function Emit-EdsFieldRefList { + param([string]$indent, [string]$tag, $names, [string]$srcName, [string]$tableName) + $list = @($names | Where-Object { $_ }) + if ($list.Count -eq 0) { X "$indent<$tag/>"; return } + X "$indent<$tag>" + foreach ($n in $list) { + X "$indent`t$(Esc-XmlText (Get-EdsFieldRef $srcName $tableName "$n"))" + } + X "$indent" +} + +function Emit-EdsFieldRefScalar { + param([string]$indent, [string]$tag, $name, [string]$srcName, [string]$tableName) + if (-not $name) { X "$indent<$tag/>"; return } + X "$indent<$tag>$(Esc-XmlText (Get-EdsFieldRef $srcName $tableName "$name"))" +} + +function Emit-EdsFunction { + # $typeXml — уже собранный узел возвращаемого значения: его рендерит вызывающий навык + # своим эмиттером типов. Так тело функции не зависит от того, какой это навык. + param([string]$indent, [string]$fnName, $val, [string]$typeXml) + $expr = "" + $returns = "" + $returnValue = $true + $fnSynonym = $null + $fnComment = "" + if ($val -is [string]) { + $expr = "$val" + } else { + $expr = if ($val.expression) { "$($val.expression)" } elseif ($val.expressionInDataSource) { "$($val.expressionInDataSource)" } else { "" } + $returns = if ($val.returns) { "$($val.returns)" } elseif ($val.returnType) { "$($val.returnType)" } else { "" } + if ($null -ne $val.returnValue) { $returnValue = ($val.returnValue -eq $true) } + $fnSynonym = $val.synonym + $fnComment = if ($val.comment) { "$($val.comment)" } else { "" } + } + if (-not $expr) { + Write-Error "Функция '$fnName' внешнего источника данных: не задано выражение (ключ expression)." + exit 1 + } + $uuid = New-Guid-String + X "$indent" + X "$indent`t" + X "$indent`t`t$(Esc-XmlText $fnName)" + Emit-MLText "$indent`t`t" "Synonym" $fnSynonym + if ($fnComment) { X "$indent`t`t$(Esc-XmlText $fnComment)" } else { X "$indent`t`t" } + X "$indent`t`t$(if ($returnValue) { 'true' } else { 'false' })" + if ($returnValue -and $typeXml) { + X $typeXml.TrimEnd("`r", "`n") + } else { + X "$indent`t`t" + } + X "$indent`t`t$(Esc-XmlText $expr)" + X "$indent`t" + X "$indent" +} + +function Emit-EdsTableProperties { + param([string]$indent, [string]$srcName, [string]$tableName, $t) + $i = $indent + $tblSynonym = if ($t -and $null -ne $t.synonym) { $t.synonym } else { Split-CamelCase $tableName } + X "$i$(Esc-XmlText $tableName)" + Emit-MLText $i "Synonym" $tblSynonym + if ($t -and $t.comment) { X "$i$(Esc-XmlText "$($t.comment)")" } else { X "$i" } + + $tableType = if ($t -and $t.tableType) { "$($t.tableType)" } else { "Table" } + X "$i$tableType" + # Имя в источнике по умолчанию равно имени объекта — так поступает и платформа. + $nids = if ($t -and $t.nameInDataSource) { "$($t.nameInDataSource)" } elseif ($tableType -eq "Expression") { "" } else { $tableName } + if ($nids) { X "$i$(Esc-XmlText $nids)" } else { X "$i" } + $expr = if ($t -and $t.expressionInDataSource) { "$($t.expressionInDataSource)" } elseif ($t -and $t.expression) { "$($t.expression)" } else { "" } + if ($expr) { X "$i$(Esc-XmlText $expr)" } else { X "$i" } + $dataType = if ($t -and $t.tableDataType) { "$($t.tableDataType)" } else { "NonobjectData" } + X "$i$dataType" + + Emit-EdsFieldRefList $i "KeyFields" $(if ($t) { $t.keyFields } else { $null }) $srcName $tableName + Emit-EdsFieldRefScalar $i "PresentationField" $(if ($t) { $t.presentationField } else { $null }) $srcName $tableName + Emit-EdsFieldRefScalar $i "ParentField" $(if ($t) { $t.parentField } else { $null }) $srcName $tableName + # Признака незаполненного родителя отдельным узлом нет: NULL против «Заданного значения» + # различаются формой самого значения (xsi:nil против типизированного). + # ВАЖНО: платформа при загрузке XML сбрасывает заданное значение в пустую строку — проверено + # на её собственной выгрузке. Задать его можно только интерактивно, поэтому дефолт у таблицы + # с полем родителя — пустая строка (как после загрузки), а без него — nil. + if ($t -and $t.parentField) { X "$i" } + else { X "$i" } + Emit-Characteristics $i $(if ($t) { $t.characteristics } else { $null }) + + X "$i$(if ($t -and $t.useStandardCommands -eq $false) { 'false' } else { 'true' })" + X "$i$(if ($t -and $t.quickChoice -eq $true) { 'true' } else { 'false' })" + # Ввод по строке: ключа нет → выводим из поля представления (так делает платформа при загрузке). + # Явный список, в том числе пустой, уважаем как есть — отсюда presence-aware проверка. + $ibsGiven = ($t -and $t.PSObject -and $t.PSObject.Properties -and ($t.PSObject.Properties.Name -contains 'inputByString')) + $ibs = if ($ibsGiven) { $t.inputByString } elseif ($t -and $t.presentationField) { @($t.presentationField) } else { $null } + Emit-EdsFieldRefList $i "InputByString" $ibs $srcName $tableName + X "$i$(if ($t -and $t.createOnInput) { "$($t.createOnInput)" } else { 'Auto' })" + X "$i$(if ($t -and $t.searchStringModeOnInputByString) { "$($t.searchStringModeOnInputByString)" } else { 'Begin' })" + 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 }) + } + foreach ($presTag in @("ObjectPresentation","ExtendedObjectPresentation","RecordPresentation", + "ExtendedRecordPresentation","ListPresentation","ExtendedListPresentation","Explanation")) { + $key = $presTag.Substring(0,1).ToLower() + $presTag.Substring(1) + Emit-MLText $i $presTag $(if ($t) { $t.$key } else { $null }) + } + X "$i$(if ($t -and $t.includeHelpInContents -eq $true) { 'true' } else { 'false' })" + X "$i$(if ($t -and $t.readOnly -eq $true) { 'true' } else { 'false' })" + X "$i$(if ($t -and $t.transactionsIsolationLevel) { "$($t.transactionsIsolationLevel)" } else { 'Auto' })" + Emit-EdsFieldRefScalar $i "DataVersionField" $(if ($t) { $t.dataVersionField } else { $null }) $srcName $tableName + X "$i$(if ($t -and $t.editType) { "$($t.editType)" } else { 'InDialog' })" + Emit-MDRefList $i "BasedOn" $(if ($t) { $t.basedOn } else { $null }) + Emit-EdsFieldRefList $i "DataLockFields" $(if ($t) { $t.dataLockFields } else { $null }) $srcName $tableName + X "$i$(if ($t -and $t.dataLockControlMode) { "$($t.dataLockControlMode)" } else { 'Automatic' })" +} + +function Build-EdsTableXml { + # $fieldsXml — уже собранные узлы : их рендерит вызывающий навык своим эмиттером + # реквизита. Так тело функции не зависит от того, какой это навык. + param([string]$srcName, [string]$tableName, $entry, [string]$fieldsXml) + $before = $script:xml.Length + + $tableUuid = New-Guid-String + X '' + X "" + X "`t" + # InternalInfo у таблицы эмитится здесь, а не через $script:generatedTypes: имя элемента + # трёхчастное (Префикс.Источник.Таблица), общая карта такой формы не знает. + X "`t`t" + foreach ($pair in @( + @("ExternalDataSourceTableManager", "Manager"), + @("ExternalDataSourceTableObject", "Object"), + @("ExternalDataSourceTableRef", "Ref"), + @("ExternalDataSourceTableList", "List"), + @("ExternalDataSourceTableRecord", "Record"), + @("ExternalDataSourceTableRecordSet", "RecordSet"), + @("ExternalDataSourceTableRecordKey", "RecordKey"), + @("ExternalDataSourceTableRecordManager", "RecordManager"))) { + X "`t`t`t" + X "`t`t`t`t$(New-Guid-String)" + X "`t`t`t`t$(New-Guid-String)" + X "`t`t`t" + } + X "`t`t" + + X "`t`t" + Emit-EdsTableProperties "`t`t`t" $srcName $tableName $entry.props + X "`t`t" + + if ($fieldsXml) { + X "`t`t" + X $fieldsXml.TrimEnd("`r", "`n") + X "`t`t" + } else { + X "`t`t" + } + X "`t
" + X "
" + + $chunk = $script:xml.ToString($before, $script:xml.Length - $before) + [void]$script:xml.Remove($before, $script:xml.Length - $before) + return $chunk } # ============================================================ @@ -1973,6 +2276,71 @@ function Process-Add($addDef) { $existingNames[$parsed.name] = "Attribute" } } + "tables" { + # Таблица внешнего источника — ОТДЕЛЬНЫЙ файл рядом с файлом источника плюс имя + # в его ChildObjects. Единственная операция навыка, создающая файл: без неё + # добавить таблицу в существующий источник было нечем (пересборка источника + # целиком меняет его uuid и оставляет файлы выброшенных таблиц сиротами). + $srcDir = Join-Path (Split-Path -Parent $resolvedPath) $script:objName + $tablesDir = Join-Path $srcDir "Tables" + foreach ($entry in (Get-EdsTables $items).GetEnumerator()) { + $tblName = $entry.Key + if ($existingNames.ContainsKey($tblName)) { + Warn "Table '$tblName' already exists, skipping" + continue + } + $tablePath = Join-Path $tablesDir "$tblName.xml" + if (Test-Path $tablePath) { + Warn "Файл таблицы уже существует: $tablePath — пропускаю" + continue + } + $fieldParts = @() + foreach ($f in @($entry.Value.fields)) { + $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 + 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)
" + $nodes = Import-Fragment $fragmentXml + $refNode = Find-InsertionPoint "Table" @{ name = $tblName } + foreach ($node in $nodes) { + Insert-BeforeElement $script:childObjectsEl $node $refNode $indent + } + Info "Added table: $tblName ($tablePath)" + $script:addCount++ + $existingNames[$tblName] = "Table" + } + } + "functions" { + # Функция живёт узлом внутри файла источника — отдельного файла у неё нет. + foreach ($prop in $items.PSObject.Properties) { + $fnName = $prop.Name + if ($existingNames.ContainsKey($fnName)) { + Warn "Function '$fnName' already exists, skipping" + continue + } + $before = $script:xml.Length + $fnVal = $prop.Value + $fnReturns = if ($fnVal -is [string]) { "String" } + elseif ($fnVal.returns) { "$($fnVal.returns)" } + elseif ($fnVal.returnType) { "$($fnVal.returnType)" } else { "String" } + $fnNoValue = (-not ($fnVal -is [string])) -and ($null -ne $fnVal.returnValue) -and ($fnVal.returnValue -ne $true) + $fnTypeXml = if ($fnNoValue) { "" } else { Build-ValueTypeXml "$indent`t`t" $fnReturns } + Emit-EdsFunction $indent $fnName $fnVal $fnTypeXml + $fragmentXml = $script:xml.ToString($before, $script:xml.Length - $before) + [void]$script:xml.Remove($before, $script:xml.Length - $before) + $nodes = Import-Fragment $fragmentXml + $refNode = Find-InsertionPoint "Function" @{ name = $fnName } + foreach ($node in $nodes) { + Insert-BeforeElement $script:childObjectsEl $node $refNode $indent + } + Info "Added function: $fnName" + $script:addCount++ + $existingNames[$fnName] = "Function" + } + } "fields" { # Поле таблицы внешнего источника: тот же парсер реквизита, свой тег и контекст. foreach ($item in $items) { diff --git a/.claude/skills/meta-edit/scripts/meta-edit.py b/.claude/skills/meta-edit/scripts/meta-edit.py index e3a4759c0..f1a4ff60f 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.44 — Edit existing 1C metadata object XML +# meta-edit v1.45 — Edit existing 1C metadata object XML # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -322,6 +322,8 @@ CFG_NS = "http://v8.1c.ru/8.1/data/enterprise/current-config" # Версия формата правимого файла — из его же корня. Нужна эмиттерам: часть свойств # появилась в поздних версиях (напр. у значения перечисления — в 2.21). is_format_221 = False +# Версия формата файла объекта: ею же помечается файл таблицы внешнего источника. +format_version = "2.17" # Префикс current-config, объявленный в КОРНЕ правимого файла (у платформы — cfg). # None = корень его не объявляет → эмиттер ссылочных типов остаётся на локальной форме. cfg_prefix = None @@ -504,6 +506,8 @@ child_type_synonyms = { "commands": "commands", "команды": "commands", "properties": "properties", "свойства": "properties", "fields": "fields", "поля": "fields", + "tables": "tables", "таблицы": "tables", + "functions": "functions", "функции": "functions", } type_synonyms = { @@ -1551,9 +1555,7 @@ valid_child_types = { "CalculationRegister": ["dimensions", "resources", "attributes", "forms", "templates", "commands"], "DocumentJournal": ["columns", "forms", "templates", "commands"], "Constant": ["forms"], - # Внешний источник данных правится целиком через meta-compile: и таблица (отдельный файл), - # и функция (узел с полным набором свойств) требуют эмиттера, который живёт там. - "ExternalDataSource": [], + "ExternalDataSource": ["tables", "functions"], "Table": ["fields", "forms", "templates", "commands"], } @@ -1577,8 +1579,320 @@ child_type_to_xml_tag = { "templates": "Template", "commands": "Command", "fields": "Field", + "tables": "Table", + "functions": "Function", } +# ============================================================ +# Section 8b: Внешние источники данных — копии из meta-compile +# ============================================================ +# Тела ниже скопированы из meta-compile и обязаны совпадать с ним байт в байт: +# таблица внешнего источника собирается в ОТДЕЛЬНЫЙ файл, и формат этого файла +# должен быть один и тот же, кем бы он ни был создан. Держит check-inline-drift.mjs. + +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) + +# 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_text(str(v))}') + X(f'{indent}') + else: + X(f'{indent}') + X(f'{indent}\tru') + X(f'{indent}\t{esc_xml_text(str(val))}') + X(f'{indent}') + +def emit_mltext(indent, tag, text): + # Пусто (None / '') → самозакрывающийся тег. + if text is None or (isinstance(text, str) and text == ''): + X(f'{indent}<{tag}/>') + return + X(f'{indent}<{tag}>') + 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.""" + arr = list(items) if items else [] + if arr: + X(f'{indent}<{tag}>') + for it in arr: + X(f'{indent}\t{esc_xml_text(normalize_md_object_ref(str(it)))}') + X(f'{indent}') + else: + X(f'{indent}<{tag}/>') + +def get_eds_tables(val): + """Таблицы: dict имя → массив полей ЛИБО объект со свойствами и ключом fields/columns.""" + tables = {} + if not val: + return tables + + def entry(v): + if isinstance(v, list): + return {'props': None, 'fields': list(v)} + f = v.get('fields') if v.get('fields') is not None else v.get('columns') + return {'props': v, 'fields': list(f) if f else []} + + if isinstance(val, list): + for t in val: + tables[str(t.get('name'))] = entry(t) + else: + for k, v in val.items(): + tables[k] = entry(v) + return tables + +def get_eds_field_ref(src_name, table_name, field_name): + """Ссылка на поле таблицы: в DSL короткое имя, в XML — полный путь.""" + if not field_name: + return '' + if str(field_name).startswith('ExternalDataSource.'): + return str(field_name) + return f'ExternalDataSource.{src_name}.Table.{table_name}.Field.{field_name}' + +def emit_eds_field_ref_list(indent, tag, names, src_name, table_name): + items = [n for n in (names or []) if n] + if not items: + X(f'{indent}<{tag}/>') + return + X(f'{indent}<{tag}>') + for n in items: + X(f'{indent}\t{esc_xml_text(get_eds_field_ref(src_name, table_name, n))}') + X(f'{indent}') + +def emit_eds_field_ref_scalar(indent, tag, name, src_name, table_name): + if not name: + X(f'{indent}<{tag}/>') + return + X(f'{indent}<{tag}>{esc_xml_text(get_eds_field_ref(src_name, table_name, name))}') + +def emit_eds_function(indent, fn_name, val, type_xml): + """Функция внешнего источника. Параметров как объектов метаданных нет: они записаны прямо + в выражении как &1, &2 (см. reference/external-data-source.md). + + type_xml — уже собранный узел возвращаемого значения: его рендерит вызывающий навык + своим эмиттером типов. Так тело функции не зависит от того, какой это навык.""" + fn_synonym = None + fn_comment = '' + returns = '' + return_value = True + if isinstance(val, str): + expr = val + else: + expr = str(val.get('expression') or val.get('expressionInDataSource') or '') + returns = str(val.get('returns') or val.get('returnType') or '') + if val.get('returnValue') is not None: + return_value = val.get('returnValue') is True + fn_synonym = val.get('synonym') + fn_comment = str(val['comment']) if val.get('comment') else '' + if not expr: + print(f"ERROR: Функция '{fn_name}' внешнего источника данных: не задано выражение (ключ expression).", + file=sys.stderr) + sys.exit(1) + X(f'{indent}') + X(f'{indent}\t') + X(f'{indent}\t\t{esc_xml_text(fn_name)}') + emit_mltext(f'{indent}\t\t', 'Synonym', fn_synonym) + if fn_comment: + X(f'{indent}\t\t{esc_xml_text(fn_comment)}') + else: + X(f'{indent}\t\t') + X(f'{indent}\t\t{"true" if return_value else "false"}') + if return_value and type_xml: + X(type_xml.rstrip('\r\n')) + else: + X(f'{indent}\t\t') + X(f'{indent}\t\t{esc_xml_text(expr)}') + X(f'{indent}\t') + X(f'{indent}') + +def emit_eds_table_properties(indent, src_name, table_name, t): + """Свойства таблицы: 38 узлов в порядке выгрузки платформы.""" + i = indent + t = t or {} + tbl_synonym = t['synonym'] if t.get('synonym') is not None else split_camel_case(table_name) + X(f'{i}{esc_xml_text(table_name)}') + emit_mltext(i, 'Synonym', tbl_synonym) + if t.get('comment'): + X(f'{i}{esc_xml_text(str(t["comment"]))}') + else: + X(f'{i}') + + table_type = str(t.get('tableType') or 'Table') + X(f'{i}{table_type}') + # Имя в источнике по умолчанию равно имени объекта — так поступает и платформа. + if t.get('nameInDataSource'): + nids = str(t['nameInDataSource']) + elif table_type == 'Expression': + nids = '' + else: + nids = table_name + X(f'{i}{esc_xml_text(nids)}' if nids else f'{i}') + expr = str(t.get('expressionInDataSource') or t.get('expression') or '') + X(f'{i}{esc_xml_text(expr)}' if expr else f'{i}') + X(f'{i}{t.get("tableDataType") or "NonobjectData"}') + + emit_eds_field_ref_list(i, 'KeyFields', t.get('keyFields'), src_name, table_name) + emit_eds_field_ref_scalar(i, 'PresentationField', t.get('presentationField'), src_name, table_name) + emit_eds_field_ref_scalar(i, 'ParentField', t.get('parentField'), src_name, table_name) + # Признака незаполненного родителя отдельным узлом нет: NULL против «Заданного значения» + # различаются формой самого значения (xsi:nil против типизированного). + # ВАЖНО: платформа при загрузке XML сбрасывает заданное значение в пустую строку — проверено + # на её собственной выгрузке. Задать его можно только интерактивно, поэтому дефолт у таблицы + # с полем родителя — пустая строка (как после загрузки), а без него — nil. + if t.get('parentField'): + X(f'{i}') + else: + X(f'{i}') + emit_characteristics(i, t.get('characteristics')) + + X(f'{i}{"false" if t.get("useStandardCommands") is False else "true"}') + X(f'{i}{"true" if t.get("quickChoice") is True else "false"}') + # Ввод по строке: ключа нет → выводим из поля представления (так делает платформа при загрузке). + # Явный список, в том числе пустой, уважаем как есть — отсюда presence-aware проверка. + if 'inputByString' in t: + ibs = t.get('inputByString') + elif t.get('presentationField'): + ibs = [t['presentationField']] + else: + ibs = None + emit_eds_field_ref_list(i, 'InputByString', ibs, src_name, table_name) + X(f'{i}{t.get("createOnInput") or "Auto"}') + X(f'{i}{t.get("searchStringModeOnInputByString") or "Begin"}') + 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)) + for pres_tag in ('ObjectPresentation', 'ExtendedObjectPresentation', 'RecordPresentation', + 'ExtendedRecordPresentation', 'ListPresentation', 'ExtendedListPresentation', 'Explanation'): + key = pres_tag[0].lower() + pres_tag[1:] + emit_mltext(i, pres_tag, t.get(key)) + X(f'{i}{"true" if t.get("includeHelpInContents") is True else "false"}') + X(f'{i}{"true" if t.get("readOnly") is True else "false"}') + X(f'{i}{t.get("transactionsIsolationLevel") or "Auto"}') + emit_eds_field_ref_scalar(i, 'DataVersionField', t.get('dataVersionField'), src_name, table_name) + X(f'{i}{t.get("editType") or "InDialog"}') + emit_md_ref_list(i, 'BasedOn', t.get('basedOn')) + emit_eds_field_ref_list(i, 'DataLockFields', t.get('dataLockFields'), src_name, table_name) + X(f'{i}{t.get("dataLockControlMode") or "Automatic"}') + +EDS_TABLE_GENERATED_TYPES = ( + ('ExternalDataSourceTableManager', 'Manager'), + ('ExternalDataSourceTableObject', 'Object'), + ('ExternalDataSourceTableRef', 'Ref'), + ('ExternalDataSourceTableList', 'List'), + ('ExternalDataSourceTableRecord', 'Record'), + ('ExternalDataSourceTableRecordSet', 'RecordSet'), + ('ExternalDataSourceTableRecordKey', 'RecordKey'), + ('ExternalDataSourceTableRecordManager', 'RecordManager'), +) + + +def build_eds_table_xml(src_name, table_name, entry, fields_xml): + """Отдельный XML-документ таблицы. fields_xml — уже собранные узлы : их рендерит + вызывающий навык своим эмиттером реквизита, поэтому тело не зависит от того, какой это навык. + Возвращает строку: X пишет в общий список строк, + поэтому «перехват» — запомнить длину, отдать эмиттерам, срезать добавленное + (в ps1-порте тот же приём выражен через StringBuilder — различие рантаймов, не логики).""" + before = len(lines) + + X('') + X(f'') + X(f'\t') + # InternalInfo у таблицы эмитится здесь, а не через generated_types: имя элемента + # трёхчастное (Префикс.Источник.Таблица), общая карта такой формы не знает. + X('\t\t') + for prefix, category in EDS_TABLE_GENERATED_TYPES: + X(f'\t\t\t') + X(f'\t\t\t\t{new_uuid()}') + X(f'\t\t\t\t{new_uuid()}') + X('\t\t\t') + X('\t\t') + + X('\t\t') + emit_eds_table_properties('\t\t\t', src_name, table_name, entry['props']) + X('\t\t') + + if fields_xml: + X('\t\t') + X(fields_xml.rstrip('\r\n')) + X('\t\t') + else: + X('\t\t') + X('\t
') + X('
') + + chunk = '\r\n'.join(lines[before:]) + del lines[before:] + return chunk + # ============================================================ # DSL key normalization # ============================================================ @@ -1914,6 +2228,61 @@ def process_add(add_def): add_count += 1 existing_names[parsed["name"]] = "Attribute" + elif child_type == "tables": + # Таблица внешнего источника — ОТДЕЛЬНЫЙ файл рядом с файлом источника плюс имя + # в его ChildObjects. Единственная операция навыка, создающая файл: без неё + # добавить таблицу в существующий источник было нечем (пересборка источника + # целиком меняет его uuid и оставляет файлы выброшенных таблиц сиротами). + 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(): + if tbl_name in existing_names: + warn(f"Table '{tbl_name}' already exists, skipping") + continue + table_path = os.path.join(tables_dir, f"{tbl_name}.xml") + if os.path.exists(table_path): + warn(f"\u0424\u0430\u0439\u043b \u0442\u0430\u0431\u043b\u0438\u0446\u044b \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442: {table_path} \u2014 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u044e") + continue + field_parts = [] + 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) + 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")) + nodes = import_fragment(f"{indent}{esc_xml_text(tbl_name)}
") + ref_node = find_insertion_point("Table", {"name": tbl_name}) + for node in nodes: + insert_before_element(child_objects_el, node, ref_node, indent) + info(f"Added table: {tbl_name} ({table_path})") + add_count += 1 + existing_names[tbl_name] = "Table" + + elif child_type == "functions": + # Функция живёт узлом внутри файла источника — отдельного файла у неё нет. + for fn_name, fn_val in items.items(): + if fn_name in existing_names: + warn(f"Function '{fn_name}' already exists, skipping") + continue + if isinstance(fn_val, str): + fn_returns, fn_no_value = "String", False + else: + fn_returns = str(fn_val.get("returns") or fn_val.get("returnType") or "String") + fn_no_value = fn_val.get("returnValue") is not None and fn_val.get("returnValue") is not True + fn_type_xml = "" if fn_no_value else build_value_type_xml(f"{indent}\t\t", fn_returns) + before = len(lines) + emit_eds_function(indent, fn_name, fn_val, fn_type_xml) + fragment_xml = "\r\n".join(lines[before:]) + del lines[before:] + nodes = import_fragment(fragment_xml) + ref_node = find_insertion_point("Function", {"name": fn_name}) + for node in nodes: + insert_before_element(child_objects_el, node, ref_node, indent) + info(f"Added function: {fn_name}") + add_count += 1 + existing_names[fn_name] = "Function" + elif child_type == "fields": # Поле таблицы внешнего источника: тот же парсер реквизита, свой тег и контекст. for item in items: @@ -3399,11 +3768,12 @@ def main(): xml_root = xml_tree.getroot() # Префикс current-config берём из объявлений корня — им и пишем ссылочные типы. - global cfg_prefix, is_format_221 + global cfg_prefix, is_format_221, format_version cfg_prefix = next((p for p, u in (xml_root.nsmap or {}).items() if u == CFG_NS and p), None) _fv = xml_root.get("version") or "2.17" _m = re.match(r'^(\d+)\.(\d+)$', _fv) is_format_221 = bool(_m) and int(_m.group(1)) * 100 + int(_m.group(2)) >= 221 + format_version = _fv # --- Detect object type --- if localname(xml_root) != "MetaDataObject": diff --git a/tests/skills/cases/meta-edit/eds-add-table.json b/tests/skills/cases/meta-edit/eds-add-table.json new file mode 100644 index 000000000..bbbe5027f --- /dev/null +++ b/tests/skills/cases/meta-edit/eds-add-table.json @@ -0,0 +1,48 @@ +{ + "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": { + "nameInDataSource": "eds.public.sales", + "tableDataType": "ObjectData", + "keyFields": ["id"], + "fields": ["id: Number(10,0)", "summa: Number(15,2)", "comment: String(0) | nullable"] + } + }, + "functions": { "nextKey": "NEXT VALUE FOR public.seq_key" } + } + }, + "expect": { + "stdoutContains": ["Added table: sales", "Added function: nextKey"], + "files": ["ExternalDataSources/PG/Tables/sales.xml", "ExternalDataSources/PG/Tables/products.xml"], + "fileContains": [ + { + "file": "ExternalDataSources/PG.xml", + "text": ["products
", "sales
", "NEXT VALUE FOR public.seq_key"] + }, + { + "file": "ExternalDataSources/PG/Tables/sales.xml", + "text": [ + "eds.public.sales", + "ObjectData", + "ExternalDataSource.PG.Table.sales.Field.id", + "" + ] + } + ] + } +} diff --git a/tests/skills/cases/meta-edit/snapshots/eds-add-table/Configuration.xml b/tests/skills/cases/meta-edit/snapshots/eds-add-table/Configuration.xml new file mode 100644 index 000000000..da0e784ee --- /dev/null +++ b/tests/skills/cases/meta-edit/snapshots/eds-add-table/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/Ext/ClientApplicationInterface.xml b/tests/skills/cases/meta-edit/snapshots/eds-add-table/Ext/ClientApplicationInterface.xml new file mode 100644 index 000000000..3c1161b2d --- /dev/null +++ b/tests/skills/cases/meta-edit/snapshots/eds-add-table/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/ExternalDataSources/PG.xml b/tests/skills/cases/meta-edit/snapshots/eds-add-table/ExternalDataSources/PG.xml new file mode 100644 index 000000000..33c800bdc --- /dev/null +++ b/tests/skills/cases/meta-edit/snapshots/eds-add-table/ExternalDataSources/PG.xml @@ -0,0 +1,50 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + + PG + + + ru + PG + + + + Automatic + + + products
+ sales
+ + + nextKey + + + true + + xs:string + + 10 + Variable + + + NEXT VALUE FOR public.seq_key + + +
+
+
\ No newline at end of file diff --git a/tests/skills/cases/meta-edit/snapshots/eds-add-table/ExternalDataSources/PG/Tables/products.xml b/tests/skills/cases/meta-edit/snapshots/eds-add-table/ExternalDataSources/PG/Tables/products.xml new file mode 100644 index 000000000..205ef9363 --- /dev/null +++ b/tests/skills/cases/meta-edit/snapshots/eds-add-table/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/ExternalDataSources/PG/Tables/sales.xml b/tests/skills/cases/meta-edit/snapshots/eds-add-table/ExternalDataSources/PG/Tables/sales.xml new file mode 100644 index 000000000..544a27a4d --- /dev/null +++ b/tests/skills/cases/meta-edit/snapshots/eds-add-table/ExternalDataSources/PG/Tables/sales.xml @@ -0,0 +1,213 @@ + + + + + + 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 + + + + sales + + + ru + sales + + + + Table + eds.public.sales + + ObjectData + + ExternalDataSource.PG.Table.sales.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 + + + + + summa + + + ru + summa + + + + + xs:decimal + + 15 + 2 + Any + + + false + + + + false + + false + false + + + false + 0 + DontCheck + + + Auto + Auto + Auto + + summa + false + false + + + + + comment + + + ru + comment + + + + + xs:string + + 0 + Variable + + + false + + + + false + + false + false + + + false + + DontCheck + + + Auto + Auto + Auto + + comment + false + true + + + +
+
\ No newline at end of file diff --git a/tests/skills/cases/meta-edit/snapshots/eds-add-table/Languages/Русский.xml b/tests/skills/cases/meta-edit/snapshots/eds-add-table/Languages/Русский.xml new file mode 100644 index 000000000..37c60d786 --- /dev/null +++ b/tests/skills/cases/meta-edit/snapshots/eds-add-table/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 9716295b5..c9f7022a6 100644 --- a/tests/skills/check-inline-drift.mjs +++ b/tests/skills/check-inline-drift.mjs @@ -31,6 +31,25 @@ const SKILLS = join(ROOT, '.claude', 'skills'); // Заготовку по новой семье печатает: node debug/inline-utils/scan-dupes.mjs --stub : const FAMILIES = [ + // ─── внешние источники данных: формат файла таблицы один на всех ───────── + // Таблица внешнего источника — отдельный файл, и создать его может как meta-compile + // (источник целиком), так и meta-edit (добавить таблицу в существующий). Формат обязан + // быть один и тот же, поэтому эмиттеры скопированы, а не написаны заново. + ...[ + // Общие утилиты (Emit-MLText, Emit-FormRef, X и т.п.) сюда не входят: они живут в доброй + // половине навыков и сводить их — отдельная работа, не относящаяся к внешним источникам. + ['Get-EdsTables', 'get_eds_tables'], + ['Get-EdsFieldRef', 'get_eds_field_ref'], + ['Emit-EdsFieldRefList', 'emit_eds_field_ref_list'], + ['Emit-EdsFieldRefScalar', 'emit_eds_field_ref_scalar'], + ['Emit-EdsFunction', 'emit_eds_function'], + ['Emit-EdsTableProperties', 'emit_eds_table_properties'], + ['Build-EdsTableXml', 'build_eds_table_xml'], + ].map(([ps1, py]) => ({ + name: `внешние источники: ${py}`, py, ps1, + variants: [{ id: 'full', authority: 'meta-compile', consumers: ['meta-edit'] }], + })), + // ─── support-guard: запрет правки объекта на поддержке ─────────────────── { name: 'support-guard: assert_edit_allowed', py: 'assert_edit_allowed', ps1: 'Assert-EditAllowed',