From 01c45581efaa054787c00a6ff31d6890252f5123 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 1 Aug 2026 21:19:28 +0300 Subject: [PATCH] =?UTF-8?q?feat(meta-decompile):=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D0=B0=20HTTPService=20+=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=87=D0=B8=D0=BD=D0=BA=D0=B0=20=D0=B5=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BF=D0=B8=D0=BB=D1=8F=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit meta-decompile не знал тип HTTPService, поэтому 7 сервисов УТ не проходили раундтрип вовсе. Добавлен разбор: rootURL, reuseSessions, sessionMaxAge и дерево urlTemplates -> methods. Раундтрип на реальных сервисах вскрыл три пробела компилятора: 1. ReuseSessions: в allowlist были только DontUse и AutoUse, а платформа пишет ещё и Use (4 объекта в корпусе) — компиляция падала с ошибкой. 2. Обработчик метода выводился по формуле ИмяШаблона+ИмяМетода; в реальных конфигурациях он произвольный (УдаленныйВызовМетодаЧерезТелоЗапроса). Метод получил объектную форму {httpMethod, handler, synonym, comment} рядом со строчным сокращением "только HTTP-метод". 3. Comment не эмитился ни у объекта, ни у шаблонов и методов, хотя платформа пишет тег всегда. Порядок шаблонов и методов приведён к порядку DSL в обоих портах: PS шёл в порядке хеш-таблицы, py сортировал — снэпшоты бы разъехались между портами. В декомпиляторе сравнение синонима с авто-выводом сделано регистрочувствительным (-ceq): "Post" против "post" считалось совпадением, и синоним терялся. Проверка: HTTP-сервисы УТ 7/7 без расхождений (было 0 — тип не поддерживался), сюита 76/76 на обоих портах, 1С-сертификация кейса пройдена. Co-Authored-By: Claude Opus 5 (1M context) --- .../meta-compile/scripts/meta-compile.ps1 | 37 ++++++++--- .../meta-compile/scripts/meta-compile.py | 36 ++++++++--- .../meta-decompile/scripts/meta-decompile.ps1 | 59 +++++++++++++++++- .../meta-decompile/scripts/meta-decompile.py | 2 +- .../cases/meta-compile/http-service.json | 10 ++- .../http-service/HTTPServices/ТоварныйAPI.xml | 62 ++++++++++++++----- 6 files changed, 170 insertions(+), 36 deletions(-) diff --git a/.claude/skills/meta-compile/scripts/meta-compile.ps1 b/.claude/skills/meta-compile/scripts/meta-compile.ps1 index e5a0b997..a75eed03 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.70 — Compile 1C metadata object from JSON +# meta-compile v1.71 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -267,7 +267,7 @@ $script:validEnumValues = @{ "RegisterRecordsDeletion" = @("AutoDelete","AutoDeleteOnUnpost","AutoDeleteOff") "RegisterRecordsWritingOnPost" = @("WriteModified","WriteSelected","WriteAll") "ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession") - "ReuseSessions" = @("DontUse","AutoUse") + "ReuseSessions" = @("DontUse","Use","AutoUse") "FillChecking" = @("DontCheck","ShowError","ShowWarning") "Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder") "SubordinationUse" = @("ToItems","ToFolders","ToFoldersAndItems") @@ -3800,7 +3800,7 @@ function Emit-HTTPServiceProperties { X "$i$(Esc-Xml $objName)" Emit-MLText $i "Synonym" $synonym - X "$i" + if ($def.comment) { X "$i$(Esc-Xml "$($def.comment)")" } else { X "$i" } $rootURL = if ($def.rootURL) { "$($def.rootURL)" } else { $objName.ToLower() } X "$i$(Esc-Xml $rootURL)" @@ -3881,15 +3881,20 @@ function Emit-URLTemplate { $tmplSynonym = Split-CamelCase $tmplName $template = "" - $methods = @{} + # [ordered], а не @{}: порядок методов должен совпадать с DSL (он же порядок исходного XML), + # иначе PS и py расходятся между собой и с выгрузкой платформы. + $methods = [ordered]@{} + $tmplComment = "" if ($tmplDef -is [string]) { $template = "$tmplDef" } else { $template = if ($tmplDef.template) { "$($tmplDef.template)" } else { "/$($tmplName.ToLower())" } + if ($tmplDef.synonym) { $tmplSynonym = "$($tmplDef.synonym)" } + if ($tmplDef.comment) { $tmplComment = "$($tmplDef.comment)" } if ($tmplDef.methods) { $tmplDef.methods.PSObject.Properties | ForEach-Object { - $methods[$_.Name] = "$($_.Value)" + $methods[$_.Name] = $_.Value # строка (HTTP-метод) ЛИБО объект {httpMethod, handler, synonym, comment} } } } @@ -3898,6 +3903,7 @@ function Emit-URLTemplate { X "$indent`t" X "$indent`t`t$(Esc-Xml $tmplName)" Emit-MLText "$indent`t`t" "Synonym" $tmplSynonym + if ($tmplComment) { X "$indent`t`t$(Esc-Xml $tmplComment)" } else { X "$indent`t`t" } X "$indent`t`t" X "$indent`t" @@ -3905,14 +3911,25 @@ function Emit-URLTemplate { X "$indent`t" foreach ($methodName in $methods.Keys) { $methodUuid = New-Guid-String - $httpMethod = $methods[$methodName] - $methodSynonym = Split-CamelCase $methodName - $handler = "${tmplName}${methodName}" + $mDef = $methods[$methodName] + # Строка — сокращение "только HTTP-метод"; объект — полная форма. Обработчик по умолчанию + # выводится как ИмяШаблона+ИмяМетода, но в реальных конфигурациях он произвольный, + # поэтому задаётся явно ключом handler. + if ($mDef -is [string]) { + $httpMethod = "$mDef"; $handler = "${tmplName}${methodName}" + $methodSynonym = Split-CamelCase $methodName; $methodComment = "" + } else { + $httpMethod = if ($mDef.httpMethod) { "$($mDef.httpMethod)" } else { 'GET' } + $handler = if ($mDef.handler) { "$($mDef.handler)" } else { "${tmplName}${methodName}" } + $methodSynonym = if ($mDef.synonym) { "$($mDef.synonym)" } else { Split-CamelCase $methodName } + $methodComment = if ($mDef.comment) { "$($mDef.comment)" } else { "" } + } X "$indent`t`t" X "$indent`t`t`t" X "$indent`t`t`t`t$(Esc-Xml $methodName)" Emit-MLText "$indent`t`t`t`t" "Synonym" $methodSynonym + if ($methodComment) { X "$indent`t`t`t`t$(Esc-Xml $methodComment)" } else { X "$indent`t`t`t`t" } X "$indent`t`t`t`t$httpMethod" X "$indent`t`t`t`t$(Esc-Xml $handler)" X "$indent`t`t`t" @@ -4369,7 +4386,9 @@ if ($objType -in @("FilterCriterion", "SettingsStorage")) { # --- HTTPService: URLTemplates --- if ($objType -eq "HTTPService") { - $urlTemplates = @{} + # [ordered]: порядок шаблонов — как в DSL (он же порядок исходного XML). @{} давало произвольный + # порядок в PS и расходилось с py, который сортировал; обе ветки приведены к порядку DSL. + $urlTemplates = [ordered]@{} if ($def.urlTemplates) { $def.urlTemplates.PSObject.Properties | ForEach-Object { $urlTemplates[$_.Name] = $_.Value diff --git a/.claude/skills/meta-compile/scripts/meta-compile.py b/.claude/skills/meta-compile/scripts/meta-compile.py index 4014521f..470cdd00 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.70 — Compile 1C metadata object from JSON +# meta-compile v1.71 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -416,7 +416,7 @@ valid_enum_values = { 'RegisterRecordsDeletion': ['AutoDelete', 'AutoDeleteOnUnpost', 'AutoDeleteOff'], 'RegisterRecordsWritingOnPost': ['WriteModified', 'WriteSelected', 'WriteAll'], 'ReturnValuesReuse': ['DontUse', 'DuringRequest', 'DuringSession'], - 'ReuseSessions': ['DontUse', 'AutoUse'], + 'ReuseSessions': ['DontUse', 'Use', 'AutoUse'], 'FillChecking': ['DontCheck', 'ShowError', 'ShowWarning'], 'Indexing': ['DontIndex', 'Index', 'IndexWithAdditionalOrder'], 'SubordinationUse': ['ToItems', 'ToFolders', 'ToFoldersAndItems'], @@ -3729,7 +3729,7 @@ def emit_http_service_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) - X(f'{i}') + X(f'{i}{esc_xml(str(defn["comment"]))}' if defn.get('comment') else f'{i}') root_url = str(defn['rootURL']) if defn.get('rootURL') else obj_name.lower() X(f'{i}{esc_xml(root_url)}') reuse_sessions = get_enum_prop('ReuseSessions', 'reuseSessions', 'DontUse') @@ -3800,29 +3800,48 @@ def emit_url_template(indent, tmpl_name, tmpl_def): tmpl_synonym = split_camel_case(tmpl_name) template = '' methods = {} + tmpl_comment = '' if isinstance(tmpl_def, str): template = tmpl_def else: template = str(tmpl_def['template']) if tmpl_def.get('template') else f'/{tmpl_name.lower()}' + if tmpl_def.get('synonym'): + tmpl_synonym = str(tmpl_def['synonym']) + if tmpl_def.get('comment'): + tmpl_comment = str(tmpl_def['comment']) if tmpl_def.get('methods'): for k, v in tmpl_def['methods'].items(): - methods[k] = str(v) + methods[k] = v # строка (HTTP-метод) ЛИБО объект {httpMethod, handler, synonym, comment} X(f'{indent}') X(f'{indent}\t') X(f'{indent}\t\t{esc_xml(tmpl_name)}') emit_mltext(f'{indent}\t\t', 'Synonym', tmpl_synonym) + X(f'{indent}\t\t{esc_xml(tmpl_comment)}' if tmpl_comment else f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t') if methods: X(f'{indent}\t') - for method_name, http_method in sorted(methods.items()): + # Порядок — как в DSL (он же порядок исходного XML), а не отсортированный: иначе + # порты расходятся между собой и с выгрузкой платформы. + for method_name, m_def in methods.items(): method_uuid = new_uuid() - method_synonym = split_camel_case(method_name) - handler = f'{tmpl_name}{method_name}' + # Строка — сокращение "только HTTP-метод"; объект — полная форма. Обработчик по + # умолчанию ИмяШаблона+ИмяМетода, но в реальных конфигурациях он произвольный. + if isinstance(m_def, str): + http_method = m_def + handler = f'{tmpl_name}{method_name}' + method_synonym = split_camel_case(method_name) + method_comment = '' + else: + http_method = str(m_def.get('httpMethod') or 'GET') + handler = str(m_def.get('handler') or f'{tmpl_name}{method_name}') + method_synonym = str(m_def['synonym']) if m_def.get('synonym') else split_camel_case(method_name) + method_comment = str(m_def.get('comment') or '') X(f'{indent}\t\t') X(f'{indent}\t\t\t') X(f'{indent}\t\t\t\t{esc_xml(method_name)}') emit_mltext(f'{indent}\t\t\t\t', 'Synonym', method_synonym) + X(f'{indent}\t\t\t\t{esc_xml(method_comment)}' if method_comment else f'{indent}\t\t\t\t') X(f'{indent}\t\t\t\t{http_method}') X(f'{indent}\t\t\t\t{esc_xml(handler)}') X(f'{indent}\t\t\t') @@ -4260,7 +4279,8 @@ if obj_type == 'HTTPService': if url_templates: has_children = True X('\t\t') - for tmpl_name in sorted(url_tmpl_order): + # Порядок — как в DSL (он же порядок исходного XML), а не отсортированный. + for tmpl_name in url_tmpl_order: emit_url_template('\t\t\t', tmpl_name, url_templates[tmpl_name]) X('\t\t') else: diff --git a/.claude/skills/meta-decompile/scripts/meta-decompile.ps1 b/.claude/skills/meta-decompile/scripts/meta-decompile.ps1 index f48a51a6..714e4e5c 100644 --- a/.claude/skills/meta-decompile/scripts/meta-decompile.ps1 +++ b/.claude/skills/meta-decompile/scripts/meta-decompile.ps1 @@ -1,4 +1,4 @@ -# meta-decompile v0.56 — XML объекта метаданных 1С → JSON-черновик формата meta-compile +# meta-decompile v0.57 — XML объекта метаданных 1С → JSON-черновик формата meta-compile # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # # Поддержаны: Catalog, ExchangePlan, ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes, Document, @@ -92,7 +92,7 @@ foreach ($c in $rootEl.ChildNodes) { if ($c.NodeType -eq 'Element') { $objNode = if (-not $objNode) { [Console]::Error.WriteLine("meta-decompile: пустой MetaDataObject"); exit 3 } $objType = $objNode.LocalName -if ($objType -notin @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document', 'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task', 'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob', 'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference', 'CommonPicture', 'CommonTemplate')) { +if ($objType -notin @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document', 'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task', 'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob', 'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService')) { [Console]::Error.WriteLine("meta-decompile: тип '$objType' пока не поддержан (…, CommonPicture, CommonTemplate)"); exit 3 } @@ -781,6 +781,13 @@ if ($objType -eq 'CommonCommand') { Add-BoolProp 'modifiesData' 'ModifiesData' $false Add-EnumProp 'onMainServerUnavalableBehavior' 'OnMainServerUnavalableBehavior' 'Auto' } +# HTTPService — корневой URL, повторное использование сеансов, время жизни сеанса. +# Шаблоны URL с методами разбираются в блоке ChildObjects. +if ($objType -eq 'HTTPService') { + $ru = P 'RootURL'; if ($ru -and $ru -ne $objName.ToLower()) { $dsl['rootURL'] = $ru } + Add-EnumProp 'reuseSessions' 'ReuseSessions' 'DontUse' + Add-IntProp 'sessionMaxAge' 'SessionMaxAge' 20 +} # CommonAttribute — общий реквизит: тип + value-свойства + состав объектов + свойства разделения данных. if ($objType -eq 'CommonAttribute') { $vt = Get-TypeShorthand ($props.SelectSingleNode('md:Type', $nsm)); if ($vt -and $vt -ne 'String(0)') { $dsl['valueType'] = $vt } @@ -1122,6 +1129,54 @@ if ($saNode) { # --- ChildObjects: Attributes + TabularSections --- $childObjs = $objNode.SelectSingleNode('md:ChildObjects', $nsm) if ($childObjs) { + # HTTPService: шаблоны URL и их методы. Шаблон — {template, methods{}}, метод — строка (только + # HTTP-метод, когда обработчик совпадает с авто-выводом ИмяШаблона+ИмяМетода) либо объект. + $tmplNodes = @($childObjs.SelectNodes('md:URLTemplate', $nsm)) + if ($tmplNodes.Count -gt 0) { + $tmpls = [ordered]@{} + foreach ($t in $tmplNodes) { + $tp = $t.SelectSingleNode('md:Properties', $nsm) + $tName = ($tp.SelectSingleNode('md:Name', $nsm)).InnerText + $tObj = [ordered]@{} + $tTemplate = $tp.SelectSingleNode('md:Template', $nsm) + if ($tTemplate) { $tObj['template'] = $tTemplate.InnerText } + $tSyn = Get-MLValue ($tp.SelectSingleNode('md:Synonym', $nsm)) + # -cne, не -ne: сравнение синонима с авто-выводом ДОЛЖНО быть регистрочувствительным, + # иначе "Post" против "post" считается совпадением и синоним теряется. + if ($null -ne $tSyn -and "$tSyn" -cne (Split-CamelWords $tName)) { $tObj['synonym'] = $tSyn } + $tCmt = $tp.SelectSingleNode('md:Comment', $nsm) + if ($tCmt -and $tCmt.InnerText) { $tObj['comment'] = $tCmt.InnerText } + + $mNodes = @($t.SelectNodes('md:ChildObjects/md:Method', $nsm)) + if ($mNodes.Count -gt 0) { + $methods = [ordered]@{} + foreach ($m in $mNodes) { + $mp = $m.SelectSingleNode('md:Properties', $nsm) + $mName = ($mp.SelectSingleNode('md:Name', $nsm)).InnerText + $mHttp = $mp.SelectSingleNode('md:HTTPMethod', $nsm) + $mHandler = $mp.SelectSingleNode('md:Handler', $nsm) + $mSyn = Get-MLValue ($mp.SelectSingleNode('md:Synonym', $nsm)) + $mCmt = $mp.SelectSingleNode('md:Comment', $nsm) + $httpVal = if ($mHttp) { $mHttp.InnerText } else { 'GET' } + $handlerVal = if ($mHandler) { $mHandler.InnerText } else { '' } + $synDefault = ($null -eq $mSyn) -or ("$mSyn" -ceq (Split-CamelWords $mName)) + $cmtEmpty = (-not $mCmt) -or (-not $mCmt.InnerText) + if ($handlerVal -eq "$tName$mName" -and $synDefault -and $cmtEmpty) { + $methods[$mName] = $httpVal + } else { + $mo = [ordered]@{ httpMethod = $httpVal } + if ($handlerVal) { $mo['handler'] = $handlerVal } + if (-not $synDefault) { $mo['synonym'] = $mSyn } + if (-not $cmtEmpty) { $mo['comment'] = $mCmt.InnerText } + $methods[$mName] = $mo + } + } + $tObj['methods'] = $methods + } + $tmpls[$tName] = $tObj + } + $dsl['urlTemplates'] = $tmpls + } $attrs = @($childObjs.SelectNodes('md:Attribute', $nsm)) if ($attrs.Count -gt 0) { $arr = [System.Collections.ArrayList]@() diff --git a/.claude/skills/meta-decompile/scripts/meta-decompile.py b/.claude/skills/meta-decompile/scripts/meta-decompile.py index 1073df9a..e9b8dc83 100644 --- a/.claude/skills/meta-decompile/scripts/meta-decompile.py +++ b/.claude/skills/meta-decompile/scripts/meta-decompile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# meta-decompile v0.56 — XML объекта метаданных 1С → JSON-черновик формата meta-compile +# meta-decompile v0.57 — XML объекта метаданных 1С → JSON-черновик формата meta-compile # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # # Зеркало meta-decompile.ps1 (КАНОН). Структура 1:1 — те же имена функций, порядок, комментарии. diff --git a/tests/skills/cases/meta-compile/http-service.json b/tests/skills/cases/meta-compile/http-service.json index e59d6a99..3a839607 100644 --- a/tests/skills/cases/meta-compile/http-service.json +++ b/tests/skills/cases/meta-compile/http-service.json @@ -4,8 +4,9 @@ "type": "HTTPService", "name": "ТоварныйAPI", "rootURL": "api/v1", - "reuseSessions": "AutoUse", + "reuseSessions": "Use", "sessionMaxAge": 30, + "comment": "Публичный API", "urlTemplates": { "Товары": { "template": "/products/{id}", @@ -13,6 +14,13 @@ "Получить": "GET", "Обновить": "PUT" } + }, + "rpc": { + "template": "/rpc", + "comment": "Удалённый вызов", + "methods": { + "post": { "httpMethod": "POST", "handler": "ВызовЧерезТелоЗапроса", "synonym": "Post" } + } } } }, diff --git a/tests/skills/cases/meta-compile/snapshots/http-service/HTTPServices/ТоварныйAPI.xml b/tests/skills/cases/meta-compile/snapshots/http-service/HTTPServices/ТоварныйAPI.xml index bce6ee06..d446b118 100644 --- a/tests/skills/cases/meta-compile/snapshots/http-service/HTTPServices/ТоварныйAPI.xml +++ b/tests/skills/cases/meta-compile/snapshots/http-service/HTTPServices/ТоварныйAPI.xml @@ -9,9 +9,9 @@ ТоварныйAPI - + Публичный API api/v1 - AutoUse + Use 30 @@ -24,23 +24,11 @@ Товары + - - Обновить - - - ru - Обновить - - - PUT - ТоварыОбновить - - - Получить @@ -49,10 +37,54 @@ Получить + GET ТоварыПолучить + + + Обновить + + + ru + Обновить + + + + PUT + ТоварыОбновить + + + + + + + rpc + + + ru + rpc + + + Удалённый вызов + + + + + + post + + + ru + Post + + + + POST + ВызовЧерезТелоЗапроса + +