fix(meta-edit): внешнюю обработку/отчёт проверять epf-validate (#108)

Автопроверка после правки жёстко звала meta-validate, который
ExternalDataProcessor/ExternalReport не знает: корректная правка
заканчивалась «Unrecognized metadata type». Валидатор теперь выбирается
по типу объекта. В py-порту stdout сбрасывается перед запуском
валидатора — его вывод больше не обгоняет строки meta-edit.

verify-snapshots: кейс meta-edit с setup none собирается epf-build,
а не грузится пустой конфигурацией, где обработку платформа не видит.

Fixes #108

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-09-26 14:59:54 +03:00
co-authored by Claude Opus 5.5
parent 68a2a11a6c
commit 372510bbc4
10 changed files with 421 additions and 11 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1" -Def
| Operation | Inline-операция (альтернатива DefinitionFile) |
| Value | Значение для inline-операции |
| DefinitionFile | JSON-файл с операциями (альтернатива Operation) |
| NoValidate | Не запускать meta-validate после правки |
| NoValidate | Не запускать валидацию после правки |
## Частые операции
@@ -1,4 +1,4 @@
# meta-edit v1.53 — Edit existing 1C metadata object XML
# meta-edit v1.54 — Edit existing 1C metadata object XML
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
@@ -3897,15 +3897,17 @@ Info "Saved: $resolvedPath"
# ============================================================
if (-not $NoValidate) {
$validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\meta-validate") "scripts\meta-validate.ps1"
# Внешняя обработка/отчёт — автономный объект, meta-validate его не знает (#108).
$validateSkill = if (@('ExternalDataProcessor','ExternalReport') -contains $script:objType) { "epf-validate" } else { "meta-validate" }
$validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\$validateSkill") "scripts\$validateSkill.ps1"
$validateScript = [System.IO.Path]::GetFullPath($validateScript)
if (Test-Path $validateScript) {
Write-Host ""
Write-Host "--- Running meta-validate ---" -ForegroundColor DarkGray
Write-Host "--- Running $validateSkill ---" -ForegroundColor DarkGray
& powershell.exe -NoProfile -File $validateScript -ObjectPath $resolvedPath
} else {
Write-Host ""
Write-Host "[SKIP] meta-validate not found at: $validateScript" -ForegroundColor DarkGray
Write-Host "[SKIP] $validateSkill not found at: $validateScript" -ForegroundColor DarkGray
}
}
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-edit v1.53 — Edit existing 1C metadata object XML
# meta-edit v1.54 — Edit existing 1C metadata object XML
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -3985,16 +3985,20 @@ def main():
# --- Auto-validate ---
if not args.NoValidate:
# Внешняя обработка/отчёт — автономный объект, meta-validate его не знает (#108).
validate_skill = "epf-validate" if obj_type in ("ExternalDataProcessor", "ExternalReport") else "meta-validate"
script_dir = os.path.dirname(os.path.abspath(__file__))
validate_script = os.path.normpath(os.path.join(script_dir, "..", "..", "meta-validate", "scripts", "meta-validate.py"))
validate_script = os.path.normpath(os.path.join(script_dir, "..", "..", validate_skill, "scripts", f"{validate_skill}.py"))
if os.path.exists(validate_script):
print()
print("--- Running meta-validate ---")
print(f"--- Running {validate_skill} ---")
python_exe = sys.executable
# Буфер stdout сбросить до запуска: иначе вывод дочернего процесса обгоняет наш.
sys.stdout.flush()
subprocess.run([python_exe, validate_script, "-ObjectPath", resolved_path])
else:
print()
print(f"[SKIP] meta-validate not found at: {validate_script}")
print(f"[SKIP] {validate_skill} not found at: {validate_script}")
# --- Summary ---
print()
@@ -0,0 +1,24 @@
{
"name": "Внешняя обработка: автопроверка через epf-validate, не meta-validate (#108)",
"setup": "none",
"skipValidation": true,
"preRun": [
{
"script": "epf-init/scripts/init",
"args": { "-Name": "Проба", "-SrcDir": "{workDir}" }
}
],
"params": { "objectPath": "Проба.xml", "objectName": "Проба" },
"input": {
"add": {
"attributes": ["Текст: String(50)"],
"tabularSections": [
{ "name": "Строки", "attrs": ["Сумма: Number(15,2)"] }
]
}
},
"expect": {
"stdoutContains": ["--- Running epf-validate ---", "Validation OK"],
"stdoutNotContains": ["Unrecognized metadata type", "meta-validate"]
}
}
@@ -0,0 +1,24 @@
{
"name": "Внешний отчёт: автопроверка через epf-validate, не meta-validate (#108)",
"setup": "none",
"skipValidation": true,
"preRun": [
{
"script": "erf-init/scripts/init",
"args": { "-Name": "ПробаОтчёт", "-SrcDir": "{workDir}" }
}
],
"params": { "objectPath": "ПробаОтчёт.xml", "objectName": "ПробаОтчёт" },
"input": {
"add": {
"attributes": ["Текст: String(50)"],
"tabularSections": [
{ "name": "Строки", "attrs": ["Сумма: Number(15,2)"] }
]
}
},
"expect": {
"stdoutContains": ["--- Running epf-validate ---", "Validation OK"],
"stdoutNotContains": ["Unrecognized metadata type", "meta-validate"]
}
}
@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject 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" version="2.17">
<ExternalDataProcessor uuid="UUID-001">
<InternalInfo>
<xr:ContainedObject>
<xr:ClassId>UUID-002</xr:ClassId>
<xr:ObjectId>UUID-003</xr:ObjectId>
</xr:ContainedObject>
<xr:GeneratedType name="ExternalDataProcessorObject.Проба" category="Object">
<xr:TypeId>UUID-004</xr:TypeId>
<xr:ValueId>UUID-005</xr:ValueId>
</xr:GeneratedType>
</InternalInfo>
<Properties>
<Name>Проба</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Проба</v8:content>
</v8:item>
</Synonym>
<Comment/>
<DefaultForm/>
<AuxiliaryForm/>
</Properties>
<ChildObjects>
<Attribute uuid="UUID-006">
<Properties>
<Name>Текст</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Текст</v8:content>
</v8:item>
</Synonym>
<Comment/>
<Type>
<v8:Type>xs:string</v8:Type>
<v8:StringQualifiers>
<v8:Length>50</v8:Length>
<v8:AllowedLength>Variable</v8:AllowedLength>
</v8:StringQualifiers>
</Type>
<PasswordMode>false</PasswordMode>
<Format/>
<EditFormat/>
<ToolTip/>
<MarkNegatives>false</MarkNegatives>
<Mask/>
<MultiLine>false</MultiLine>
<ExtendedEdit>false</ExtendedEdit>
<MinValue xsi:nil="true"/>
<MaxValue xsi:nil="true"/>
<FillChecking>DontCheck</FillChecking>
<ChoiceFoldersAndItems>Items</ChoiceFoldersAndItems>
<ChoiceParameterLinks/>
<ChoiceParameters/>
<QuickChoice>Auto</QuickChoice>
<CreateOnInput>Auto</CreateOnInput>
<ChoiceForm/>
<LinkByType/>
<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>
</Properties>
</Attribute>
<TabularSection uuid="UUID-007">
<InternalInfo>
<xr:GeneratedType name="ExternalDataProcessorTabularSection.Проба.Строки" category="TabularSection">
<xr:TypeId>UUID-008</xr:TypeId>
<xr:ValueId>UUID-009</xr:ValueId>
</xr:GeneratedType>
<xr:GeneratedType name="ExternalDataProcessorTabularSectionRow.Проба.Строки" category="TabularSectionRow">
<xr:TypeId>UUID-010</xr:TypeId>
<xr:ValueId>UUID-011</xr:ValueId>
</xr:GeneratedType>
</InternalInfo>
<Properties>
<Name>Строки</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Строки</v8:content>
</v8:item>
</Synonym>
<Comment/>
<ToolTip/>
<FillChecking>DontCheck</FillChecking>
<StandardAttributes>
<xr:StandardAttribute name="LineNumber">
<xr:LinkByType/>
<xr:FillChecking>DontCheck</xr:FillChecking>
<xr:MultiLine>false</xr:MultiLine>
<xr:FillFromFillingValue>false</xr:FillFromFillingValue>
<xr:CreateOnInput>Auto</xr:CreateOnInput>
<xr:MaxValue xsi:nil="true"/>
<xr:ToolTip/>
<xr:ExtendedEdit>false</xr:ExtendedEdit>
<xr:Format/>
<xr:ChoiceForm/>
<xr:QuickChoice>Auto</xr:QuickChoice>
<xr:ChoiceHistoryOnInput>Auto</xr:ChoiceHistoryOnInput>
<xr:EditFormat/>
<xr:PasswordMode>false</xr:PasswordMode>
<xr:DataHistory>Use</xr:DataHistory>
<xr:MarkNegatives>false</xr:MarkNegatives>
<xr:MinValue xsi:nil="true"/>
<xr:Synonym/>
<xr:Comment/>
<xr:FullTextSearch>Use</xr:FullTextSearch>
<xr:ChoiceParameterLinks/>
<xr:FillValue xsi:nil="true"/>
<xr:Mask/>
<xr:ChoiceParameters/>
</xr:StandardAttribute>
</StandardAttributes>
</Properties>
<ChildObjects>
<Attribute uuid="UUID-012">
<Properties>
<Name>Сумма</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Сумма</v8:content>
</v8:item>
</Synonym>
<Comment/>
<Type>
<v8:Type>xs:decimal</v8:Type>
<v8:NumberQualifiers>
<v8:Digits>15</v8:Digits>
<v8:FractionDigits>2</v8:FractionDigits>
<v8:AllowedSign>Any</v8:AllowedSign>
</v8:NumberQualifiers>
</Type>
<PasswordMode>false</PasswordMode>
<Format/>
<EditFormat/>
<ToolTip/>
<MarkNegatives>false</MarkNegatives>
<Mask/>
<MultiLine>false</MultiLine>
<ExtendedEdit>false</ExtendedEdit>
<MinValue xsi:nil="true"/>
<MaxValue xsi:nil="true"/>
<FillFromFillingValue>false</FillFromFillingValue>
<FillValue xsi:type="xs:decimal">0</FillValue>
<FillChecking>DontCheck</FillChecking>
<ChoiceFoldersAndItems>Items</ChoiceFoldersAndItems>
<ChoiceParameterLinks/>
<ChoiceParameters/>
<QuickChoice>Auto</QuickChoice>
<CreateOnInput>Auto</CreateOnInput>
<ChoiceForm/>
<LinkByType/>
<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>
</Properties>
</Attribute>
</ChildObjects>
</TabularSection>
</ChildObjects>
</ExternalDataProcessor>
</MetaDataObject>
@@ -0,0 +1,11 @@
#Область ОписаниеПеременных
#КонецОбласти
#Область ПрограммныйИнтерфейс
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти
@@ -0,0 +1,168 @@
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject 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" version="2.17">
<ExternalReport uuid="UUID-001">
<InternalInfo>
<xr:ContainedObject>
<xr:ClassId>UUID-002</xr:ClassId>
<xr:ObjectId>UUID-003</xr:ObjectId>
</xr:ContainedObject>
<xr:GeneratedType name="ExternalReportObject.ПробаОтчёт" category="Object">
<xr:TypeId>UUID-004</xr:TypeId>
<xr:ValueId>UUID-005</xr:ValueId>
</xr:GeneratedType>
</InternalInfo>
<Properties>
<Name>ПробаОтчёт</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>ПробаОтчёт</v8:content>
</v8:item>
</Synonym>
<Comment/>
<DefaultForm/>
<AuxiliaryForm/>
<MainDataCompositionSchema/>
<DefaultSettingsForm/>
<AuxiliarySettingsForm/>
<DefaultVariantForm/>
<VariantsStorage/>
<SettingsStorage/>
</Properties>
<ChildObjects>
<Attribute uuid="UUID-006">
<Properties>
<Name>Текст</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Текст</v8:content>
</v8:item>
</Synonym>
<Comment/>
<Type>
<v8:Type>xs:string</v8:Type>
<v8:StringQualifiers>
<v8:Length>50</v8:Length>
<v8:AllowedLength>Variable</v8:AllowedLength>
</v8:StringQualifiers>
</Type>
<PasswordMode>false</PasswordMode>
<Format/>
<EditFormat/>
<ToolTip/>
<MarkNegatives>false</MarkNegatives>
<Mask/>
<MultiLine>false</MultiLine>
<ExtendedEdit>false</ExtendedEdit>
<MinValue xsi:nil="true"/>
<MaxValue xsi:nil="true"/>
<FillChecking>DontCheck</FillChecking>
<ChoiceFoldersAndItems>Items</ChoiceFoldersAndItems>
<ChoiceParameterLinks/>
<ChoiceParameters/>
<QuickChoice>Auto</QuickChoice>
<CreateOnInput>Auto</CreateOnInput>
<ChoiceForm/>
<LinkByType/>
<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>
</Properties>
</Attribute>
<TabularSection uuid="UUID-007">
<InternalInfo>
<xr:GeneratedType name="ExternalReportTabularSection.ПробаОтчёт.Строки" category="TabularSection">
<xr:TypeId>UUID-008</xr:TypeId>
<xr:ValueId>UUID-009</xr:ValueId>
</xr:GeneratedType>
<xr:GeneratedType name="ExternalReportTabularSectionRow.ПробаОтчёт.Строки" category="TabularSectionRow">
<xr:TypeId>UUID-010</xr:TypeId>
<xr:ValueId>UUID-011</xr:ValueId>
</xr:GeneratedType>
</InternalInfo>
<Properties>
<Name>Строки</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Строки</v8:content>
</v8:item>
</Synonym>
<Comment/>
<ToolTip/>
<FillChecking>DontCheck</FillChecking>
<StandardAttributes>
<xr:StandardAttribute name="LineNumber">
<xr:LinkByType/>
<xr:FillChecking>DontCheck</xr:FillChecking>
<xr:MultiLine>false</xr:MultiLine>
<xr:FillFromFillingValue>false</xr:FillFromFillingValue>
<xr:CreateOnInput>Auto</xr:CreateOnInput>
<xr:MaxValue xsi:nil="true"/>
<xr:ToolTip/>
<xr:ExtendedEdit>false</xr:ExtendedEdit>
<xr:Format/>
<xr:ChoiceForm/>
<xr:QuickChoice>Auto</xr:QuickChoice>
<xr:ChoiceHistoryOnInput>Auto</xr:ChoiceHistoryOnInput>
<xr:EditFormat/>
<xr:PasswordMode>false</xr:PasswordMode>
<xr:DataHistory>Use</xr:DataHistory>
<xr:MarkNegatives>false</xr:MarkNegatives>
<xr:MinValue xsi:nil="true"/>
<xr:Synonym/>
<xr:Comment/>
<xr:FullTextSearch>Use</xr:FullTextSearch>
<xr:ChoiceParameterLinks/>
<xr:FillValue xsi:nil="true"/>
<xr:Mask/>
<xr:ChoiceParameters/>
</xr:StandardAttribute>
</StandardAttributes>
</Properties>
<ChildObjects>
<Attribute uuid="UUID-012">
<Properties>
<Name>Сумма</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Сумма</v8:content>
</v8:item>
</Synonym>
<Comment/>
<Type>
<v8:Type>xs:decimal</v8:Type>
<v8:NumberQualifiers>
<v8:Digits>15</v8:Digits>
<v8:FractionDigits>2</v8:FractionDigits>
<v8:AllowedSign>Any</v8:AllowedSign>
</v8:NumberQualifiers>
</Type>
<PasswordMode>false</PasswordMode>
<Format/>
<EditFormat/>
<ToolTip/>
<MarkNegatives>false</MarkNegatives>
<Mask/>
<MultiLine>false</MultiLine>
<ExtendedEdit>false</ExtendedEdit>
<MinValue xsi:nil="true"/>
<MaxValue xsi:nil="true"/>
<FillFromFillingValue>false</FillFromFillingValue>
<FillValue xsi:type="xs:decimal">0</FillValue>
<FillChecking>DontCheck</FillChecking>
<ChoiceFoldersAndItems>Items</ChoiceFoldersAndItems>
<ChoiceParameterLinks/>
<ChoiceParameters/>
<QuickChoice>Auto</QuickChoice>
<CreateOnInput>Auto</CreateOnInput>
<ChoiceForm/>
<LinkByType/>
<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>
</Properties>
</Attribute>
</ChildObjects>
</TabularSection>
</ChildObjects>
</ExternalReport>
</MetaDataObject>
@@ -0,0 +1,11 @@
#Область ОписаниеПеременных
#КонецОбласти
#Область ПрограммныйИнтерфейс
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти
+6 -2
View File
@@ -713,7 +713,7 @@ const EPF_SKILLS = new Map([
// Skills that produce either an EPF/ERF source or a full Configuration —
// route is auto-detected after the main script runs.
const EPF_OR_CONFIG_SKILLS = new Set(['template-add', 'help-add']);
const EPF_OR_CONFIG_SKILLS = new Set(['template-add', 'help-add', 'meta-edit']);
// Диагностика падения навыка. Оба потока вместе: ps1 печатает строку ошибки в stdout, py — в
// stderr, а лог платформы оба кладут в stdout. Читать только `stderr || stdout` значило на
@@ -918,8 +918,12 @@ async function verifyCase(skillName, caseName, skillConfig, caseData, opts) {
// setup кейса перекрывает setup навыка — как в runner.mjs. Без этого кейсы с гейтом по версии
// формата (empty-config-218/220/221) проверялись на конфигурации 2.17, то есть платформа не
// видела ровно того поведения, ради которого кейс написан.
// Кейс навыка «EPF или конфигурация» с setup `none` правит внешнюю обработку: пустая конфигурация
// вокруг неё — ложный PASS (корневой XML обработки платформа при загрузке конфигурации не видит).
const caseSetup = typeof caseData.setup === 'string' ? caseData.setup : null;
const setupType = (caseSetup && caseSetup.startsWith('empty-config')) ? caseSetup : (skillConfig.setup || 'empty-config');
const caseSetupWins = caseSetup && (caseSetup.startsWith('empty-config') ||
(caseSetup === 'none' && EPF_OR_CONFIG_SKILLS.has(skillName)));
const setupType = caseSetupWins ? caseSetup : (skillConfig.setup || 'empty-config');
const isStandalone = STANDALONE_SKILLS.has(skillName);
let epfExt = EPF_SKILLS.get(skillName);
let isEpf = !!epfExt;