diff --git a/.claude/skills/meta-edit/child-operations.md b/.claude/skills/meta-edit/child-operations.md index bc0b38ad..1e8d9447 100644 --- a/.claude/skills/meta-edit/child-operations.md +++ b/.claude/skills/meta-edit/child-operations.md @@ -84,6 +84,27 @@ Batch через `;;` — можно указать разные ТЧ: `"Тов Формат аналогичен `modify-attribute`: `ИмяТЧ: ключ=значение, ключ=значение`. +## add-predefined + +Добавить предопределённые элементы (Catalog, ChartOfCharacteristicTypes). Существующие элементы и их +идентификаторы сохраняются, новые получают свежий id. + +Inline — строка `(Код) Имя [Наименование]` (batch через `;;`; `[Наименование]` необязательно — иначе авто из имени): +```powershell +-Operation add-predefined -Value "(001) Основной ;; (002) Резервный [Резервный склад]" +``` + +JSON — строки и/или объекты (для групп с вложенными): +```json +{ "add": { "predefined": [ + "(001) Основной", + { "name": "Группа", "isFolder": true, "childItems": ["(002) Вложенный"] } +] } } +``` + +Ключи объекта: `name`, `code`, `description`, `isFolder`, `childItems` (дерево). Тип кода (строковый/числовой) +берётся из объекта автоматически. + ## add-enumValue / add-form / add-template / add-command Просто имена (batch через `;;`): diff --git a/.claude/skills/meta-edit/scripts/meta-edit.ps1 b/.claude/skills/meta-edit/scripts/meta-edit.ps1 index d8e6781f..136b45fc 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.18 — Edit existing 1C metadata object XML (+свойства-списки DataLockFields/RegisteredDocuments) +# meta-edit v1.19 — Edit existing 1C metadata object XML (+add-predefined предопределённые Ext/Predefined.xml) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$DefinitionFile, @@ -12,7 +12,7 @@ param( "add-attribute", "add-ts", "add-dimension", "add-resource", "add-enumValue", "add-column", "add-form", "add-template", "add-command", "add-owner", "add-registerRecord", "add-basedOn", "add-inputByString", - "add-dataLockField", "add-registeredDocument", + "add-dataLockField", "add-registeredDocument", "add-predefined", "remove-attribute", "remove-ts", "remove-dimension", "remove-resource", "remove-enumValue", "remove-column", "remove-form", "remove-template", "remove-command", "remove-owner", "remove-registerRecord", "remove-basedOn", "remove-inputByString", @@ -1519,6 +1519,16 @@ function Convert-InlineToDefinition([string]$operation, [string]$value) { return $def } + # Предопределённые (Ext/Predefined.xml) — отдельный файл; строим { : { predefined: [...] } }. + if ($target -eq 'predefined') { + $items = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $inner = New-Object PSCustomObject + $inner | Add-Member -NotePropertyName 'predefined' -NotePropertyValue $items + $def = New-Object PSCustomObject + $def | Add-Member -NotePropertyName $op -NotePropertyValue $inner + return $def + } + # TS attribute operations: dot notation "TSName.AttrDef" if ($target -eq "ts-attribute") { $items = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) @@ -1761,6 +1771,10 @@ function Process-Add($addDef) { $addDef.PSObject.Properties | ForEach-Object { $rawKey = $_.Name $items = $_.Value + if ($rawKey -in @('predefined','предопределенные','предопределённые')) { + Add-PredefinedItems $items + return + } $childType = Resolve-ChildTypeKey $rawKey if (-not $childType) { @@ -2956,6 +2970,95 @@ function Set-ComplexProperty([string]$propertyName, [string[]]$values) { # Section 13: Main processing # ============================================================ +# ============================================================ +# Predefined data (Ext/Predefined.xml) — add предопределённых (Catalog/ChartOfCharacteristicTypes). +# Существующие сохраняются побайтово (текстовый append), новые получают свежий GUID — +# инвариант «не менять id существующей сущности». +# ============================================================ + +$script:predefXsiTypeByObj = @{ + 'Catalog' = 'CatalogPredefinedItems' + 'ChartOfCharacteristicTypes' = 'PlanOfCharacteristicKindPredefinedItems' +} + +function Get-PredefinedPath { + $objDir = Join-Path (Split-Path $resolvedPath) $script:objName + return (Join-Path (Join-Path $objDir "Ext") "Predefined.xml") +} + +function Get-ObjectCodeType { + foreach ($ch in $script:propertiesEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'CodeType') { return $ch.InnerText.Trim() } + } + return 'String' +} + +# Элемент DSL: строка "(Код) Имя [Наименование]" ЛИБО объект {name,code,description,isFolder,childItems}. +function Resolve-PredefItem($val) { + if ($val -is [string]) { + $s = "$val"; $descRaw = $null; $hasDesc = $false + if ($s -match '\[(.*)\]') { $descRaw = $Matches[1]; $hasDesc = $true; $s = $s -replace '\s*\[.*\]', '' } + $m = [regex]::Match($s.Trim(), '^\s*(?:\(([^)]*)\)\s*)?(\S+)\s*$') + $name = $m.Groups[2].Value + $code = if ($m.Groups[1].Success) { $m.Groups[1].Value } else { '' } + $desc = if ($hasDesc) { $descRaw } else { Split-CamelCase $name } + return @{ name = $name; code = $code; desc = $desc; isFolder = $false; children = @() } + } + $gv = { param($o, [string[]]$keys) foreach ($k in $keys) { if ($o.PSObject.Properties[$k]) { return $o.$k } } return $null } + $name = "$(& $gv $val @('name','имя'))" + $codeV = & $gv $val @('code','код'); $code = if ($null -ne $codeV) { "$codeV" } else { '' } + $hasDesc = $val.PSObject.Properties['description'] -or $val.PSObject.Properties['наименование'] + $descV = & $gv $val @('description','наименование') + $desc = if ($hasDesc) { "$descV" } else { Split-CamelCase $name } + $isFolder = ((& $gv $val @('isFolder','группа')) -eq $true) + $subs = & $gv $val @('childItems','подчиненные') + return @{ name = $name; code = $code; desc = $desc; isFolder = $isFolder; children = @(if ($subs) { @($subs) } else { @() }) } +} + +function Build-PredefItemXml([string]$indent, $val, [string]$codeType) { + $r = Resolve-PredefItem $val + $sb = New-Object System.Text.StringBuilder + [void]$sb.Append("$indent`r`n") + [void]$sb.Append("$indent`t$(Esc-XmlText $r.name)`r`n") + if (-not $r.code) { [void]$sb.Append("$indent`t`r`n") } + elseif ($codeType -eq 'Number') { [void]$sb.Append("$indent`t$(Esc-XmlText $r.code)`r`n") } + else { [void]$sb.Append("$indent`t$(Esc-XmlText $r.code)`r`n") } + if ($r.desc -eq '') { [void]$sb.Append("$indent`t`r`n") } + else { [void]$sb.Append("$indent`t$(Esc-XmlText $r.desc)`r`n") } + [void]$sb.Append("$indent`t$(if ($r.isFolder) { 'true' } else { 'false' })`r`n") + if ($r.children.Count -gt 0) { + [void]$sb.Append("$indent`t`r`n") + foreach ($c in $r.children) { [void]$sb.Append((Build-PredefItemXml "$indent`t`t" $c $codeType)) } + [void]$sb.Append("$indent`t`r`n") + } + [void]$sb.Append("$indent`r`n") + return $sb.ToString() +} + +function Add-PredefinedItems($items) { + $xsiType = $script:predefXsiTypeByObj[$script:objType] + if (-not $xsiType) { Write-Error "add-predefined: тип объекта '$($script:objType)' не поддержан (только Catalog, ChartOfCharacteristicTypes)"; exit 1 } + $codeType = Get-ObjectCodeType + $version = $script:xmlDoc.DocumentElement.GetAttribute("version") + $path = Get-PredefinedPath + $itemsXml = "" + foreach ($it in @($items)) { $itemsXml += (Build-PredefItemXml "`t" $it $codeType) } + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + if (Test-Path $path) { + $text = [System.IO.File]::ReadAllText($path, $utf8Bom) + $text = $text.Replace("", "$itemsXml") + } else { + $extDir = Split-Path $path + if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null } + $hdr = "`r`n`r`n" + $text = "$hdr$itemsXml`r`n" + } + [System.IO.File]::WriteAllText($path, $text, $utf8Bom) + $n = @($items).Count + Info "Added $n predefined item(s) → $path" + $script:addCount += $n +} + # --- Inline mode conversion --- if ($Operation) { $def = Convert-InlineToDefinition $Operation $Value diff --git a/.claude/skills/meta-edit/scripts/meta-edit.py b/.claude/skills/meta-edit/scripts/meta-edit.py index b842f9e8..9d7241a9 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.18 — Edit existing 1C metadata object XML (+свойства-списки DataLockFields/RegisteredDocuments) +# meta-edit v1.19 — Edit existing 1C metadata object XML (+add-predefined предопределённые Ext/Predefined.xml) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -203,6 +203,7 @@ md_ns = "" properties_el = None child_objects_el = None obj_name = "" +resolved_path = "" add_count = 0 remove_count = 0 @@ -1458,6 +1459,11 @@ def convert_inline_to_definition(operation, value): complex_action = "set" if op == "set" else op return {"_complex": [{"action": complex_action, "property": prop_name, "values": values}]} + # Предопределённые (Ext/Predefined.xml) — отдельный файл; строим { : { predefined: [...] } }. + if target == "predefined": + items = [v.strip() for v in value.split(";;") if v.strip()] + return {op: {"predefined": items}} + # TS attribute operations: dot notation "TSName.AttrDef" if target == "ts-attribute": items = [v.strip() for v in value.split(";;") if v.strip()] @@ -1667,6 +1673,9 @@ def process_add(add_def): global add_count for raw_key, items in add_def.items(): + if raw_key in ('predefined', 'предопределенные', 'предопределённые'): + add_predefined_items(items) + continue child_type = resolve_child_type_key(raw_key) if not child_type: @@ -2872,18 +2881,126 @@ def save_xml(tree, path): # Main # ============================================================ +# ============================================================ +# Predefined data (Ext/Predefined.xml) — add предопределённых (Catalog/ChartOfCharacteristicTypes). +# Существующие сохраняются побайтово (текстовый append), новые получают свежий GUID. +# ============================================================ + +predef_xsi_type_by_obj = { + 'Catalog': 'CatalogPredefinedItems', + 'ChartOfCharacteristicTypes': 'PlanOfCharacteristicKindPredefinedItems', +} + + +def get_predefined_path(): + return os.path.join(os.path.dirname(resolved_path), obj_name, "Ext", "Predefined.xml") + + +def get_object_code_type(): + for ch in properties_el: + if localname(ch) == 'CodeType': + return (ch.text or '').strip() + return 'String' + + +def resolve_predef_item(val): + if isinstance(val, str): + s = val + desc_raw = None + has_desc = False + m = re.search(r'\[(.*)\]', s) + if m: + desc_raw = m.group(1) + has_desc = True + s = re.sub(r'\s*\[.*\]', '', s) + mm = re.match(r'^\s*(?:\(([^)]*)\)\s*)?(\S+)\s*$', s.strip()) + name = mm.group(2) if mm else s.strip() + code = mm.group(1) if (mm and mm.group(1) is not None) else '' + desc = desc_raw if has_desc else split_camel_case(name) + return {'name': name, 'code': code, 'desc': desc, 'isFolder': False, 'children': []} + + def gv(o, keys): + for k in keys: + if isinstance(o, dict) and k in o: + return o[k] + return None + name = str(gv(val, ['name', 'имя']) or '') + code_v = gv(val, ['code', 'код']) + code = str(code_v) if code_v is not None else '' + has_desc = isinstance(val, dict) and ('description' in val or 'наименование' in val) + desc_v = gv(val, ['description', 'наименование']) + desc = str(desc_v) if has_desc else split_camel_case(name) + is_folder = gv(val, ['isFolder', 'группа']) is True + subs = gv(val, ['childItems', 'подчиненные']) + return {'name': name, 'code': code, 'desc': desc if desc is not None else '', + 'isFolder': is_folder, 'children': list(subs) if subs else []} + + +def build_predef_item_xml(indent, val, code_type): + r = resolve_predef_item(val) + parts = [f'{indent}'] + parts.append(f'{indent}\t{esc_xml_text(r["name"])}') + if not r['code']: + parts.append(f'{indent}\t') + elif code_type == 'Number': + parts.append(f'{indent}\t{esc_xml_text(r["code"])}') + else: + parts.append(f'{indent}\t{esc_xml_text(r["code"])}') + if r['desc'] == '': + parts.append(f'{indent}\t') + else: + parts.append(f'{indent}\t{esc_xml_text(r["desc"])}') + parts.append(f'{indent}\t{"true" if r["isFolder"] else "false"}') + if r['children']: + parts.append(f'{indent}\t') + for c in r['children']: + parts.append(build_predef_item_xml(indent + '\t\t', c, code_type).rstrip('\r\n')) + parts.append(f'{indent}\t') + parts.append(f'{indent}') + return '\r\n'.join(parts) + '\r\n' + + +def add_predefined_items(items): + global add_count + xsi_type = predef_xsi_type_by_obj.get(obj_type) + if not xsi_type: + print(f"add-predefined: тип объекта '{obj_type}' не поддержан (только Catalog, ChartOfCharacteristicTypes)", file=sys.stderr) + sys.exit(1) + code_type = get_object_code_type() + version = xml_root.get("version") + path = get_predefined_path() + item_list = items if isinstance(items, list) else [items] + items_xml = ''.join(build_predef_item_xml('\t', it, code_type) for it in item_list) + if os.path.exists(path): + with open(path, 'r', encoding='utf-8-sig') as f: + text = f.read() + text = text.replace('', items_xml + '') + else: + os.makedirs(os.path.dirname(path), exist_ok=True) + hdr = ('\r\n\r\n') + text = hdr + items_xml + '\r\n' + with open(path, 'wb') as f: + f.write(b'\xef\xbb\xbf') + f.write(text.encode('utf-8')) + info(f"Added {len(item_list)} predefined item(s) -> {path}") + add_count += len(item_list) + + def main(): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") global xml_tree, xml_root, obj_element, obj_type, md_ns - global properties_el, child_objects_el, obj_name + global properties_el, child_objects_el, obj_name, resolved_path global add_count, remove_count, modify_count, warn_count valid_operations = [ "add-attribute", "add-ts", "add-dimension", "add-resource", "add-enumValue", "add-column", "add-form", "add-template", "add-command", "add-owner", "add-registerRecord", "add-basedOn", "add-inputByString", - "add-dataLockField", "add-registeredDocument", + "add-dataLockField", "add-registeredDocument", "add-predefined", "remove-attribute", "remove-ts", "remove-dimension", "remove-resource", "remove-enumValue", "remove-column", "remove-form", "remove-template", "remove-command", "remove-owner", "remove-registerRecord", "remove-basedOn", "remove-inputByString", diff --git a/tests/skills/cases/meta-edit/add-predefined.json b/tests/skills/cases/meta-edit/add-predefined.json new file mode 100644 index 00000000..33200ab7 --- /dev/null +++ b/tests/skills/cases/meta-edit/add-predefined.json @@ -0,0 +1,21 @@ +{ + "name": "add-predefined: предопределённые элементы + группа с вложенными (Ext/Predefined.xml)", + "setup": "empty-config", + "preRun": [ + { + "script": "meta-compile/scripts/meta-compile", + "input": { "type": "Catalog", "name": "Спр" }, + "args": { "-JsonPath": "{inputFile}", "-OutputDir": "{workDir}" } + } + ], + "params": { "objectPath": "Catalogs/Спр" }, + "input": { + "add": { + "predefined": [ + "(01) Первый", + "(02) Второй [Второй элемент]", + { "name": "Группа", "isFolder": true, "childItems": ["(03) Вложенный"] } + ] + } + } +} diff --git a/tests/skills/cases/meta-edit/snapshots/add-predefined/Catalogs/Спр.xml b/tests/skills/cases/meta-edit/snapshots/add-predefined/Catalogs/Спр.xml new file mode 100644 index 00000000..2ddf2d6b --- /dev/null +++ b/tests/skills/cases/meta-edit/snapshots/add-predefined/Catalogs/Спр.xml @@ -0,0 +1,91 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + UUID-008 + UUID-009 + + + UUID-010 + UUID-011 + + + + Спр + + + ru + Спр + + + + false + HierarchyFoldersAndItems + false + 2 + true + true + + ToItems + 9 + 25 + String + Variable + WholeCatalog + false + true + AsDescription + + Auto + InDialog + false + BothWays + + Catalog.Спр.StandardAttribute.Description + Catalog.Спр.StandardAttribute.Code + + Begin + DontUse + Directly + + + + + + + + + + + false + + + Managed + Use + + + + + + Use + Auto + DontUse + false + false + + + + diff --git a/tests/skills/cases/meta-edit/snapshots/add-predefined/Catalogs/Спр/Ext/ObjectModule.bsl b/tests/skills/cases/meta-edit/snapshots/add-predefined/Catalogs/Спр/Ext/ObjectModule.bsl new file mode 100644 index 00000000..e69de29b diff --git a/tests/skills/cases/meta-edit/snapshots/add-predefined/Catalogs/Спр/Ext/Predefined.xml b/tests/skills/cases/meta-edit/snapshots/add-predefined/Catalogs/Спр/Ext/Predefined.xml new file mode 100644 index 00000000..a171fc75 --- /dev/null +++ b/tests/skills/cases/meta-edit/snapshots/add-predefined/Catalogs/Спр/Ext/Predefined.xml @@ -0,0 +1,29 @@ + + + + Первый + 01 + Первый + false + + + Второй + 02 + Второй элемент + false + + + Группа + + Группа + true + + + Вложенный + 03 + Вложенный + false + + + + diff --git a/tests/skills/cases/meta-edit/snapshots/add-predefined/Configuration.xml b/tests/skills/cases/meta-edit/snapshots/add-predefined/Configuration.xml new file mode 100644 index 00000000..199f22d9 --- /dev/null +++ b/tests/skills/cases/meta-edit/snapshots/add-predefined/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 + + + + Русский + Спр + + + \ No newline at end of file diff --git a/tests/skills/cases/meta-edit/snapshots/add-predefined/Ext/ClientApplicationInterface.xml b/tests/skills/cases/meta-edit/snapshots/add-predefined/Ext/ClientApplicationInterface.xml new file mode 100644 index 00000000..3c1161b2 --- /dev/null +++ b/tests/skills/cases/meta-edit/snapshots/add-predefined/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/add-predefined/Languages/Русский.xml b/tests/skills/cases/meta-edit/snapshots/add-predefined/Languages/Русский.xml new file mode 100644 index 00000000..37c60d78 --- /dev/null +++ b/tests/skills/cases/meta-edit/snapshots/add-predefined/Languages/Русский.xml @@ -0,0 +1,16 @@ + + + + + Русский + + + ru + Русский + + + + ru + + + \ No newline at end of file diff --git a/tests/skills/check-uuid-invariant.mjs b/tests/skills/check-uuid-invariant.mjs index 341e98bf..a19f781b 100644 --- a/tests/skills/check-uuid-invariant.mjs +++ b/tests/skills/check-uuid-invariant.mjs @@ -76,13 +76,22 @@ for (const runtime of runtimes) { }, tabularSections: { 'Товары': { modify: { 'Цена': { name: 'ЦенаНовая' } } } }, }, - add: { attributes: ['НовыйРекв: Boolean'] }, + add: { attributes: ['НовыйРекв: Boolean'], predefined: ['(1) ПервыйПредоп', '(2) ВторойПредоп'] }, remove: { attributes: ['Удаляемый'] }, }), 'utf8'); skill(runtime, 'meta-edit', ['-ObjectPath', objXml, '-DefinitionFile', edit, '-NoValidate'], work); const after = collectUuids(objXml); + // 3b. Инвариант для предопределённых: добавление ещё элементов не меняет id существующих . + const predefXml = join(work, 'Catalogs', 'Спр', 'Ext', 'Predefined.xml'); + const predefIds = (p) => new Set([...readFileSync(p, 'utf8').matchAll(/ m[1])); + const predefBefore = predefIds(predefXml); + const edit2 = join(work, 'e2.json'); + writeFileSync(edit2, JSON.stringify({ add: { predefined: ['(3) ТретийПредоп'] } }), 'utf8'); + skill(runtime, 'meta-edit', ['-ObjectPath', objXml, '-DefinitionFile', edit2, '-NoValidate'], work); + const predefAfter = predefIds(predefXml); + // 3. Инвариант: каждый uuid, существовавший ДО правки (кроме намеренно удалённого), // должен присутствовать ПОСЛЕ (переименование/смена типа/структурные свойства НЕ меняют id). const fail = (msg) => { console.log(`[${runtime}] НАРУШЕНИЕ: ${msg}`); failures++; }; @@ -93,7 +102,10 @@ for (const runtime of runtimes) { if (!after.has(uuid)) fail(`uuid ${uuid} пропал после правки (перегенерирован?)`); } if (removedUuid && after.has(removedUuid)) fail(`uuid удалённого реквизита ${removedUuid} остался (не удалён?)`); - console.log(`[${runtime}] проверено ${checked} uuid (объект + GeneratedType + реквизиты/ТЧ), удалённый: ${removedUuid ? 'ок' : 'НЕ НАЙДЕН'}`); + for (const id of predefBefore) { + if (!predefAfter.has(id)) fail(`id предопределённого элемента ${id} пропал после добавления новых (перегенерирован?)`); + } + console.log(`[${runtime}] проверено ${checked} uuid объекта + ${predefBefore.size} id предопределённых (сохранены при add)`); } catch (e) { console.log(`[${runtime}] ОШИБКА прогона: ${(e.stderr || e.message || '').toString().slice(0, 300)}`); failures++;