fix(cf-edit): reject add-childObject when object file is missing

cf-edit add-childObject was a low-level XML-manipulation operation
with no file-existence validation — callers could register a reference
to any Type.Name in Configuration.xml's ChildObjects without the
underlying file existing on disk. Platform then refused to load:
"Файл объекта не существует".

The 4 failing tests (add-objects, remove-object, add-default-role,
set-default-roles) all used this operation with fake references in
either main input or preRun, and had no way to pass verify-snapshots
because the cf-init-ed config had no actual object files.

User observation: this is the tests being wrong, not the skill.
meta-compile/role-compile/subsystem-compile already auto-register
every new object in Configuration.xml as part of their normal flow
(meta-compile.ps1:2949-3068, role-compile.ps1:667-747,
subsystem-compile.ps1:430-506). Nobody should be calling cf-edit
add-childObject to create a new object — they should be calling the
profile skill. cf-edit add-childObject is only for rare recovery
scenarios: rolled-back Configuration.xml with intact object files,
re-import from DB dump that clobbered the root but left srcfiles.

Changes:

1. cf-edit.ps1/py: Do-AddChildObject now checks that the target file
   exists at {ConfigDir}/{PluralDir}/{Name}.xml before registering.
   On miss, exits 1 with a message that names the expected path and
   points the user at the right skill (/meta-compile, /role-compile,
   or /subsystem-compile depending on type). TYPE_TO_DIR mapping for
   all 44 metadata types covers irregular plurals (FilterCriteria,
   BusinessProcesses, ChartsOfAccounts, ChartsOfCharacteristicTypes,
   ChartsOfCalculationTypes).

2. Tests: 4 existing cases rewritten to build realistic fixtures via
   meta-compile/role-compile preRun (both skills auto-register, so
   the resulting Configuration.xml already references the preRun
   objects). add-objects now exercises the round-trip recovery
   scenario: meta-compile creates Catalog.Товары and Document.ПриходТоваров
   (auto-registered) → cf-edit remove-childObject un-registers both
   (files remain) → main run re-registers via add-childObject. This
   tests exactly the rollback-recovery use case the operation exists for.

3. New add-missing-errors case: negative test with expectError:
   "Object file not found". Verifies the new hard-error path.

4. verify-snapshots.mjs: added symmetric expectError handling (runner.mjs
   already had it at line 514). If caseData.expectError is set,
   expect skill to fail; check stderr substring match; skip db-load
   and mark passed. Without this, negative tests would go red in
   verify-snapshots even though runner.mjs accepts them.

5. SKILL.md / reference.md: documented the new constraint and the
   redirection to profile skills. Kept mention of legitimate use case
   (rollback recovery).

Bumped cf-edit.ps1/py v1.0→v1.1.

Verification:
- runner --filter cf-edit (PS1): 2/6 → 7/7 (6 positive + 1 negative)
- runner --filter cf-edit --runtime python: 7/7 (dual-port clean)
- verify-snapshots --skill cf-edit: 2/6 → 7/7

With this landed P3 from debug/snapshot-verify/NEXT-STEPS.md is closed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-04-11 20:18:05 +03:00
parent 7a8f437e77
commit 0d5d3451ff
25 changed files with 1461 additions and 14 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ powershell.exe -NoProfile -File .claude/skills/cf-edit/scripts/cf-edit.ps1 -Conf
| Операция | Формат Value | Описание |
|----------|-------------|----------|
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
| `add-childObject` | `Type.Name` (batch `;;`) | Добавить объект в ChildObjects |
| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
+4
View File
@@ -35,6 +35,10 @@
Формат: `Type.Name` — XML-тип и имя объекта через точку.
**Важно про `add-childObject`**: операция регистрирует в `<ChildObjects>` Configuration.xml только объект, **файл которого уже существует на диске** (например `Catalogs/Товары.xml`). Если файла нет — скрипт падает с exit 1 и подсказкой. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его в Configuration.xml за один вызов.
Когда `add-childObject` всё-таки нужен: откатили Configuration.xml (или перезаписали из выгрузки БД), а файлы объектов остались — нужно восстановить ссылки в `<ChildObjects>`.
При добавлении объект вставляется в каноническую позицию:
1. Находит последний элемент того же типа → вставляет после
2. Если тип отсутствует → находит последний элемент предшествующего типа → вставляет после
+41 -1
View File
@@ -1,4 +1,4 @@
# cf-edit v1.0 — Edit 1C configuration root (Configuration.xml)
# cf-edit v1.1 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$ConfigPath,
@@ -27,6 +27,7 @@ if (Test-Path $ConfigPath -PathType Container) {
}
if (-not (Test-Path $ConfigPath)) { Write-Error "File not found: $ConfigPath"; exit 1 }
$resolvedPath = (Resolve-Path $ConfigPath).Path
$script:configDir = [System.IO.Path]::GetDirectoryName($resolvedPath)
# --- Load XML with PreserveWhitespace ---
$script:xmlDoc = New-Object System.Xml.XmlDocument
@@ -87,6 +88,22 @@ $script:typeOrder = @(
"BusinessProcess","Task","IntegrationService"
)
# --- Type → on-disk directory name (plural) ---
$script:typeToDir = @{
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
"Constant"="Constants"; "CommonForm"="CommonForms"; "Catalog"="Catalogs"; "Document"="Documents"
"DocumentNumerator"="DocumentNumerators"; "Sequence"="Sequences"; "DocumentJournal"="DocumentJournals"; "Enum"="Enums"
"Report"="Reports"; "DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"; "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters"
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"; "IntegrationService"="IntegrationServices"
}
# --- XML manipulation helpers (from subsystem-edit pattern) ---
function Get-ChildIndent($container) {
foreach ($child in $container.ChildNodes) {
@@ -247,6 +264,29 @@ function Do-AddChildObject([string]$batchVal) {
exit 1
}
# Check that the referenced object actually exists on disk.
# cf-edit add-childObject is a low-level operation for rare scenarios
# (e.g. restoring a rolled-back Configuration.xml when object files are intact).
# For creating NEW objects, meta-compile/role-compile/subsystem-compile already
# auto-register in Configuration.xml — calling cf-edit add-childObject there is
# unnecessary and error-prone.
$typeDir = $script:typeToDir[$typeName]
$objFile = Join-Path (Join-Path $script:configDir $typeDir) "$objNameVal.xml"
if (-not (Test-Path $objFile)) {
$hintSkill = switch ($typeName) {
"Subsystem" { "subsystem-compile" }
"Role" { "role-compile" }
default { "meta-compile" }
}
Write-Error @"
Object file not found: $typeDir/$objNameVal.xml
cf-edit add-childObject only references objects that already exist on disk.
To create a new $typeName, use $hintSkill (auto-registers in Configuration.xml):
/$hintSkill with {"type":"$typeName","name":"$objNameVal"}
"@
exit 1
}
# Dedup check
$existing = $false
foreach ($child in $script:childObjsEl.ChildNodes) {
+37 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-edit v1.0 — Edit 1C configuration root (Configuration.xml)
# cf-edit v1.1 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -32,6 +32,22 @@ TYPE_ORDER = [
"BusinessProcess", "Task", "IntegrationService",
]
# Type → on-disk directory name (plural)
TYPE_TO_DIR = {
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
"Constant": "Constants", "CommonForm": "CommonForms", "Catalog": "Catalogs", "Document": "Documents",
"DocumentNumerator": "DocumentNumerators", "Sequence": "Sequences", "DocumentJournal": "DocumentJournals", "Enum": "Enums",
"Report": "Reports", "DataProcessor": "DataProcessors", "InformationRegister": "InformationRegisters", "AccumulationRegister": "AccumulationRegisters",
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes", "ChartOfAccounts": "ChartsOfAccounts", "AccountingRegister": "AccountingRegisters",
"ChartOfCalculationTypes": "ChartsOfCalculationTypes", "CalculationRegister": "CalculationRegisters",
"BusinessProcess": "BusinessProcesses", "Task": "Tasks", "IntegrationService": "IntegrationServices",
}
ML_PROPS = ["Synonym", "BriefInformation", "DetailedInformation", "Copyright", "VendorInformationAddress", "ConfigurationInformationAddress"]
SCALAR_PROPS = ["Name", "Version", "Vendor", "Comment", "NamePrefix", "UpdateCatalogAddress"]
REF_PROPS = ["DefaultLanguage"]
@@ -171,6 +187,7 @@ def main():
print(f"File not found: {config_path}", file=sys.stderr)
sys.exit(1)
resolved_path = os.path.abspath(config_path)
config_dir = os.path.dirname(resolved_path)
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(resolved_path, xml_parser)
@@ -285,6 +302,25 @@ def main():
sys.exit(1)
type_idx = TYPE_ORDER.index(type_name)
# Check that the referenced object actually exists on disk.
# cf-edit add-childObject is a low-level operation for rare scenarios
# (e.g. restoring a rolled-back Configuration.xml when object files are intact).
# For creating NEW objects, meta-compile/role-compile/subsystem-compile already
# auto-register in Configuration.xml — calling cf-edit add-childObject there is
# unnecessary and error-prone.
type_dir = TYPE_TO_DIR.get(type_name)
obj_file = os.path.join(config_dir, type_dir, f"{obj_name_val}.xml")
if not os.path.exists(obj_file):
hint_skill = {"Subsystem": "subsystem-compile", "Role": "role-compile"}.get(type_name, "meta-compile")
print(
f"Object file not found: {type_dir}/{obj_name_val}.xml\n"
f"cf-edit add-childObject only references objects that already exist on disk.\n"
f"To create a new {type_name}, use {hint_skill} (auto-registers in Configuration.xml):\n"
f' /{hint_skill} with {{"type":"{type_name}","name":"{obj_name_val}"}}',
file=sys.stderr
)
sys.exit(1)
# Dedup
exists = False
for child in child_objs_el: