From 71f047291c63e480fdae4213d7881ddbef773e27 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 12 Aug 2026 19:28:11 +0300 Subject: [PATCH] =?UTF-8?q?fix(form-validate):=20=D0=B2=D0=B5=D1=80=D1=81?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D0=B0=20?= =?UTF-8?q?=E2=80=94=20=D0=BE=D0=B1=D1=89=D0=B8=D0=BC=20helper-=D0=BE?= =?UTF-8?q?=D0=BC,=20=D1=81=20=D1=83=D1=87=D1=91=D1=82=D0=BE=D0=BC=20?= =?UTF-8?q?=D0=B2=D0=BD=D0=B5=D1=88=D0=BD=D0=B8=D1=85=20=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проверка версии читала Configuration.xml собственной регуляркой. Отсюда два пробела: форма автономной внешней обработки не проверялась вовсе (своего Configuration.xml у неё нет, версию несёт корень обработки), а разбор дублировал уже существующий эталон. Теперь используются копии общих эталонов — detect_format_version (авторитет form-compile) и support-guard: is_external_root (авторитет cf-edit), оба навыка добавлены в реестр check-inline-drift. Попутно вскрылось следствие для соседней проверки: обработка, чьи исходники лежат внутри дерева с Configuration.xml (обычная раскладка src/cf рядом с src/epf), считалась «конфигурационным контекстом», и Check 12 ругался на её собственные External*-типы. Теперь климб останавливается на ближайшем якоре — том же правиле границы автономного объекта, что в support-guard. Плюс паритет: PS отчитывался «Data bindings: none», PY эту ветку не имел. Кейс на EPF-контекст добавлен. Регресс 16/16 на обоих рантаймах, корпус 21 097 форм без новых срабатываний, гарды зелёные. --- .../form-validate/scripts/form-validate.ps1 | 77 +++++- .../form-validate/scripts/form-validate.py | 84 ++++-- .../form-validate/epf-format-version.json | 16 ++ .../epf-format-version/Configuration.xml | 251 ++++++++++++++++++ .../Ext/ClientApplicationInterface.xml | 18 ++ .../epf-format-version/Languages/Русский.xml | 16 ++ .../epf-format-version/ТестВерсия.xml | 30 +++ .../ТестВерсия/Ext/ObjectModule.bsl | 11 + .../ТестВерсия/Forms/Форма.xml | 22 ++ .../ТестВерсия/Forms/Форма/Ext/Form.xml | 15 ++ .../Forms/Форма/Ext/Form/Module.bsl | 19 ++ tests/skills/check-inline-drift.mjs | 6 +- 12 files changed, 534 insertions(+), 31 deletions(-) create mode 100644 tests/skills/cases/form-validate/epf-format-version.json create mode 100644 tests/skills/cases/form-validate/snapshots/epf-format-version/Configuration.xml create mode 100644 tests/skills/cases/form-validate/snapshots/epf-format-version/Ext/ClientApplicationInterface.xml create mode 100644 tests/skills/cases/form-validate/snapshots/epf-format-version/Languages/Русский.xml create mode 100644 tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия.xml create mode 100644 tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия/Ext/ObjectModule.bsl create mode 100644 tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия/Forms/Форма.xml create mode 100644 tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия/Forms/Форма/Ext/Form.xml create mode 100644 tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия/Forms/Форма/Ext/Form/Module.bsl diff --git a/.claude/skills/form-validate/scripts/form-validate.ps1 b/.claude/skills/form-validate/scripts/form-validate.ps1 index 5ad34e62..19d0790a 100644 --- a/.claude/skills/form-validate/scripts/form-validate.ps1 +++ b/.claude/skills/form-validate/scripts/form-validate.ps1 @@ -60,6 +60,19 @@ $nsMgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") $root = $xmlDoc.DocumentElement +# Корень автономной внешней обработки/отчёта. Копия общего эталона (семья +# support-guard: is_external_root, авторитет — cf-edit). +function Test-ExternalObjectRoot([string]$xmlPath) { + if (-not (Test-Path $xmlPath)) { return $false } + try { + [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 + $el = $mx.DocumentElement.FirstChild + while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } + if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName } + } catch {} + return $false +} + # --- Detect context: config vs EPF/ERF --- # Walk up from FormPath looking for Configuration.xml → config context # No Configuration.xml → external data processor / report (EPF/ERF) @@ -67,14 +80,56 @@ $script:isConfigContext = $false $walkDir = Split-Path (Resolve-Path $FormPath) -Parent for ($i = 0; $i -lt 15; $i++) { if (-not $walkDir -or $walkDir -eq (Split-Path $walkDir)) { break } + # Порядок проверок тот же, что у Detect-FormatVersion: сначала корень автономной обработки, + # потом Configuration.xml — иначе форма внутри EPF, лежащей в дереве конфигурации, взяла бы + # версию конфигурации. + $extRoot = "$walkDir.xml" + if (-not $script:versionAnchor) { + if (Test-ExternalObjectRoot $extRoot) { + # Ближайший якорь побеждает: автономная обработка остаётся автономной, даже если её + # исходники лежат внутри дерева с Configuration.xml (типовая раскладка проекта: + # src/cf рядом с src/epf). Иначе её собственные External*-типы считались бы ошибкой. + $script:versionAnchor = $extRoot + break + } + } if (Test-Path (Join-Path $walkDir "Configuration.xml")) { $script:isConfigContext = $true $script:configXmlPath = Join-Path $walkDir "Configuration.xml" + if (-not $script:versionAnchor) { $script:versionAnchor = $script:configXmlPath } break } $walkDir = Split-Path $walkDir } +# Версия формата выгрузки. Копия общего эталона (семья detect_format_version, авторитет — +# form-compile): та же ветка для автономной EPF/ERF, где версию несёт корень обработки. +function Detect-FormatVersion([string]$dir) { + $d = $dir + while ($d) { + # Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт + # корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17. + $extPath = "$d.xml" + if (Test-Path $extPath) { + $extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8) + $extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length)) + if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match ']+version="(\d+\.\d+)"') { return $Matches[1] } + } + $cfgPath = Join-Path $d "Configuration.xml" + if (Test-Path $cfgPath) { + $cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8) + # Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает + # СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением. + $head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length)) + if ($head -match ']+version="(\d+\.\d+)"') { return $Matches[1] } + } + $parent = Split-Path $d -Parent + if ($parent -eq $d) { break } + $d = $parent + } + return "2.17" +} + # --- Counters --- $errors = 0 @@ -927,26 +982,22 @@ if (-not $stopped) { } } -# --- Check 14: версия формата формы совпадает с версией конфигурации --- +# --- Check 14: версия формата формы совпадает с версией выгрузки --- # Версию задаёт платформа, которой выгружали, и в пределах одной выгрузки она едина. Форма из # другой версии — «Неизвестная версия формата N загружаемого файла»: платформа не читает файл, -# который новее её самой. Типичный след ручной сборки: форму скопировали из свежей конфигурации. +# который новее её самой. Источник версии ищем общим helper-ом: он же покрывает автономную +# внешнюю обработку/отчёт, где Configuration.xml нет и версию несёт корень самой обработки. -if (-not $stopped -and $script:configXmlPath) { +if (-not $stopped -and $script:versionAnchor) { $formVer = $root.GetAttribute("version") - $cfgVer = "" - try { - $cfgHead = [System.IO.File]::ReadAllText($script:configXmlPath, [System.Text.Encoding]::UTF8) - $vm = [regex]::Match($cfgHead.Substring(0, [Math]::Min(4000, $cfgHead.Length)), ']*\bversion="([^"]+)"') - if ($vm.Success) { $cfgVer = $vm.Groups[1].Value } - } catch { } + $dumpVer = Detect-FormatVersion (Split-Path (Resolve-Path $FormPath) -Parent) - if (-not $cfgVer -or -not $formVer) { + if (-not $formVer) { Report-OK "14. Format version: not comparable" - } elseif ($formVer -ne $cfgVer) { - Report-Error "14. Format version $formVer differs from configuration ($cfgVer) — a dump carries one version, the platform refuses a file it cannot read" + } elseif ($formVer -ne $dumpVer) { + Report-Error "14. Format version $formVer differs from the dump ($dumpVer) — a dump carries one version, the platform refuses a file it cannot read" } else { - Report-OK "14. Format version: $formVer, matches configuration" + Report-OK "14. Format version: $formVer, matches the dump" } } diff --git a/.claude/skills/form-validate/scripts/form-validate.py b/.claude/skills/form-validate/scripts/form-validate.py index 8b6f809f..4bff2ae2 100644 --- a/.claude/skills/form-validate/scripts/form-validate.py +++ b/.claude/skills/form-validate/scripts/form-validate.py @@ -71,6 +71,49 @@ VALID_CFG_PREFIXES = { } +# Корень автономной внешней обработки/отчёта. Копия общего эталона (семья +# support-guard: is_external_root, авторитет — cf-edit). +def _sg_is_external_root(xml_path): + if not os.path.isfile(xml_path): + return False + try: + mx = etree.parse(xml_path).getroot() + for child in mx: + if isinstance(child.tag, str): + return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport") + except Exception: + return False + return False + + +# Версия формата выгрузки. Копия общего эталона (семья detect_format_version, авторитет — +# form-compile): та же ветка для автономной EPF/ERF, где версию несёт корень обработки. +def detect_format_version(d): + while d: + # Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт + # корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17. + ext_path = d + ".xml" + if os.path.isfile(ext_path): + with open(ext_path, "r", encoding="utf-8-sig") as f: + ext_head = f.read(2000) + if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head): + m = re.search(r']+version="(\d+\.\d+)"', ext_head) + if m: + return m.group(1) + cfg_path = os.path.join(d, "Configuration.xml") + if os.path.isfile(cfg_path): + with open(cfg_path, "r", encoding="utf-8-sig") as f: + head = f.read(2000) + m = re.search(r']+version="(\d+\.\d+)"', head) + if m: + return m.group(1) + parent = os.path.dirname(d) + if parent == d: + break + d = parent + return "2.17" + + def localname(el): return etree.QName(el.tag).localname @@ -129,14 +172,28 @@ def main(): # Detect context: config vs EPF/ERF is_config_context = False config_xml_path = '' + version_anchor = '' walk_dir = os.path.dirname(os.path.abspath(form_path)) for _ in range(15): parent = os.path.dirname(walk_dir) if parent == walk_dir: break + # Порядок проверок тот же, что у detect_format_version: сначала корень автономной обработки, + # потом Configuration.xml — иначе форма внутри EPF, лежащей в дереве конфигурации, взяла бы + # версию конфигурации. + ext_root = walk_dir + '.xml' + if not version_anchor: + if _sg_is_external_root(ext_root): + # Ближайший якорь побеждает: автономная обработка остаётся автономной, даже если её + # исходники лежат внутри дерева с Configuration.xml (типовая раскладка проекта: + # src/cf рядом с src/epf). Иначе её собственные External*-типы считались бы ошибкой. + version_anchor = ext_root + break if os.path.isfile(os.path.join(walk_dir, 'Configuration.xml')): is_config_context = True config_xml_path = os.path.join(walk_dir, 'Configuration.xml') + if not version_anchor: + version_anchor = config_xml_path break walk_dir = parent @@ -489,6 +546,8 @@ def main(): path_msg = f"{path_msg}, {skip_note}" if path_msg else skip_note if path_errors == 0 and path_msg: report_ok(f"Data bindings: {path_msg}") + elif path_errors == 0: + report_ok("Data bindings: none") # --- Check 6: Button command references --- if not stopped: @@ -849,29 +908,22 @@ def main(): elif prefix_errors == 0: report_ok(f'13. Namespace prefixes: {prefix_checked} values, all declared') - # --- Check 14: версия формата формы совпадает с версией конфигурации --- + # --- Check 14: версия формата формы совпадает с версией выгрузки --- # Версию задаёт платформа, которой выгружали, и в пределах одной выгрузки она едина. Форма из # другой версии — «Неизвестная версия формата N загружаемого файла»: платформа не читает файл, - # который новее её самой. Типичный след ручной сборки: форму скопировали из свежей конфигурации. - if not stopped and config_xml_path: + # который новее её самой. Источник версии ищем общим helper-ом: он же покрывает автономную + # внешнюю обработку/отчёт, где Configuration.xml нет и версию несёт корень самой обработки. + if not stopped and version_anchor: form_ver = root.get('version', '') - cfg_ver = '' - try: - with open(config_xml_path, 'r', encoding='utf-8-sig', errors='ignore') as fh: - head = fh.read(4000) - vm = re.search(r']*[ \t]version="([^"]+)"', head) - if vm: - cfg_ver = vm.group(1) - except OSError: - pass + dump_ver = detect_format_version(os.path.dirname(os.path.abspath(form_path))) - if not cfg_ver or not form_ver: + if not form_ver: report_ok('14. Format version: not comparable') - elif form_ver != cfg_ver: - report_error(f'14. Format version {form_ver} differs from configuration ({cfg_ver}) ' + elif form_ver != dump_ver: + report_error(f'14. Format version {form_ver} differs from the dump ({dump_ver}) ' '— a dump carries one version, the platform refuses a file it cannot read') else: - report_ok(f'14. Format version: {form_ver}, matches configuration') + report_ok(f'14. Format version: {form_ver}, matches the dump') # --- Finalize --- checks = ok_count + errors + warnings diff --git a/tests/skills/cases/form-validate/epf-format-version.json b/tests/skills/cases/form-validate/epf-format-version.json new file mode 100644 index 00000000..b7531c1e --- /dev/null +++ b/tests/skills/cases/form-validate/epf-format-version.json @@ -0,0 +1,16 @@ +{ + "name": "Внешняя обработка: версия формы сверяется с корнем обработки, а не с конфигурацией", + "preRun": [ + { + "script": "epf-init/scripts/init", + "args": { "-Name": "ТестВерсия", "-SrcDir": "{workDir}", "-FormatVersion": "2.20" } + }, + { + "script": "form-add/scripts/form-add", + "args": { "-ObjectPath": "{workDir}/ТестВерсия.xml", "-FormName": "Форма" } + } + ], + "params": { "formPath": "ТестВерсия/Forms/Форма" }, + "expect": { "stdoutContains": "Validation OK" }, + "skipPlatformVerify": "автономная внешняя обработка: верификатор грузит каталог как конфигурацию, EPF так не проверить" +} diff --git a/tests/skills/cases/form-validate/snapshots/epf-format-version/Configuration.xml b/tests/skills/cases/form-validate/snapshots/epf-format-version/Configuration.xml new file mode 100644 index 00000000..2ed58586 --- /dev/null +++ b/tests/skills/cases/form-validate/snapshots/epf-format-version/Configuration.xml @@ -0,0 +1,251 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + UUID-008 + UUID-009 + + + UUID-010 + UUID-011 + + + UUID-012 + UUID-013 + + + UUID-014 + UUID-015 + + + + TestConfig + + + ru + TestConfig + + + + + Version8_3_24 + ManagedApplication + + PlatformApplication + + Russian + + + + + false + false + false + + + + + + + + + + + + + + + + + + + + + + Biometrics + true + + + Location + false + + + BackgroundLocation + false + + + BluetoothPrinters + false + + + WiFiPrinters + false + + + Contacts + false + + + Calendars + false + + + PushNotifications + false + + + LocalNotifications + false + + + InAppPurchases + false + + + PersonalComputerFileExchange + false + + + Ads + false + + + NumberDialing + false + + + CallProcessing + false + + + CallLog + false + + + AutoSendSMS + false + + + ReceiveSMS + false + + + SMSLog + false + + + Camera + false + + + Microphone + false + + + MusicLibrary + false + + + PictureAndVideoLibraries + false + + + AudioPlaybackAndVibration + false + + + BackgroundAudioPlaybackAndVibration + false + + + InstallPackages + false + + + OSBackup + true + + + ApplicationUsageStatistics + false + + + BarcodeScanning + false + + + BackgroundAudioRecording + false + + + AllFilesAccess + false + + + Videoconferences + false + + + NFC + false + + + DocumentScanning + false + + + SpeechToText + false + + + Geofences + false + + + IncomingShareRequests + false + + + AllIncomingShareRequestsTypesProcessing + false + + + + + + Normal + + + Language.Русский + + + + + + Managed + NotAutoFree + DontUse + DontUse + TaxiEnableVersion8_2 + DontUse + Version8_3_24 + + + + Русский + + + \ No newline at end of file diff --git a/tests/skills/cases/form-validate/snapshots/epf-format-version/Ext/ClientApplicationInterface.xml b/tests/skills/cases/form-validate/snapshots/epf-format-version/Ext/ClientApplicationInterface.xml new file mode 100644 index 00000000..3c1161b2 --- /dev/null +++ b/tests/skills/cases/form-validate/snapshots/epf-format-version/Ext/ClientApplicationInterface.xml @@ -0,0 +1,18 @@ + + + + + UUID-002 + + + + + UUID-004 + + + + + + + + \ No newline at end of file diff --git a/tests/skills/cases/form-validate/snapshots/epf-format-version/Languages/Русский.xml b/tests/skills/cases/form-validate/snapshots/epf-format-version/Languages/Русский.xml new file mode 100644 index 00000000..37c60d78 --- /dev/null +++ b/tests/skills/cases/form-validate/snapshots/epf-format-version/Languages/Русский.xml @@ -0,0 +1,16 @@ + + + + + Русский + + + ru + Русский + + + + ru + + + \ No newline at end of file diff --git a/tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия.xml b/tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия.xml new file mode 100644 index 00000000..38b6a845 --- /dev/null +++ b/tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия.xml @@ -0,0 +1,30 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + + ТестВерсия + + + ru + ТестВерсия + + + + ExternalDataProcessor.ТестВерсия.Form.Форма + + + +
Форма
+
+
+
\ No newline at end of file diff --git a/tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия/Ext/ObjectModule.bsl b/tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия/Ext/ObjectModule.bsl new file mode 100644 index 00000000..15543d27 --- /dev/null +++ b/tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия/Ext/ObjectModule.bsl @@ -0,0 +1,11 @@ +#Область ОписаниеПеременных + +#КонецОбласти + +#Область ПрограммныйИнтерфейс + +#КонецОбласти + +#Область СлужебныеПроцедурыИФункции + +#КонецОбласти \ No newline at end of file diff --git a/tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия/Forms/Форма.xml b/tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия/Forms/Форма.xml new file mode 100644 index 00000000..5218449e --- /dev/null +++ b/tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия/Forms/Форма.xml @@ -0,0 +1,22 @@ + + +
+ + Форма + + + ru + Форма + + + + Managed + false + + PlatformApplication + MobilePlatformApplication + + + +
+
\ No newline at end of file diff --git a/tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия/Forms/Форма/Ext/Form.xml b/tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия/Forms/Форма/Ext/Form.xml new file mode 100644 index 00000000..09f46437 --- /dev/null +++ b/tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия/Forms/Форма/Ext/Form.xml @@ -0,0 +1,15 @@ + +
+ + true + + + + + + cfg:ExternalDataProcessorObject.ТестВерсия + + true + + + \ No newline at end of file diff --git a/tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия/Forms/Форма/Ext/Form/Module.bsl b/tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия/Forms/Форма/Ext/Form/Module.bsl new file mode 100644 index 00000000..8ead4cec --- /dev/null +++ b/tests/skills/cases/form-validate/snapshots/epf-format-version/ТестВерсия/Forms/Форма/Ext/Form/Module.bsl @@ -0,0 +1,19 @@ +#Область ОбработчикиСобытийФормы + +#КонецОбласти + +#Область ОбработчикиСобытийЭлементовФормы + +#КонецОбласти + +#Область ОбработчикиКомандФормы + +#КонецОбласти + +#Область ОбработчикиОповещений + +#КонецОбласти + +#Область СлужебныеПроцедурыИФункции + +#КонецОбласти \ No newline at end of file diff --git a/tests/skills/check-inline-drift.mjs b/tests/skills/check-inline-drift.mjs index f4a2b4bb..a2c0bd23 100644 --- a/tests/skills/check-inline-drift.mjs +++ b/tests/skills/check-inline-drift.mjs @@ -59,7 +59,9 @@ const FAMILIES = [ 'subsystem-compile', 'subsystem-edit', 'template-add', 'xdto-compile', 'xdto-edit', // *-info навыки читают тем же хелпером СОСТОЯНИЕ поддержки для вывода, а не запрещают // правку. Тело то же, поэтому семья общая. - 'form-info', 'meta-info', 'mxl-info', 'role-info', 'skd-info', 'subsystem-info'] }, + 'form-info', 'meta-info', 'mxl-info', 'role-info', 'skd-info', 'subsystem-info', + // form-validate отличает автономную обработку от конфигурации: границей служит тот же корень + 'form-validate'] }, ], }, { @@ -92,7 +94,7 @@ const FAMILIES = [ // если <каталог>.xml — корень ExternalDataProcessor/ExternalReport, чего в дереве // конфигурации не бывает. Поэтому вариант один на всех, переключателя не нужно. { id: 'base', authority: 'form-compile', - consumers: ['cfe-borrow', 'form-add', 'help-add', 'interface-edit', 'meta-compile', + consumers: ['cfe-borrow', 'form-add', 'form-validate', 'help-add', 'interface-edit', 'meta-compile', 'mxl-compile', 'role-compile', 'subsystem-compile', 'template-add', 'xdto-compile'] }, ], },