Наполнение макета по свежему пути (Templates/<Имя>/Ext/Template.xml) на
PowerShell падало с «Could not find a part of the path». py-порт каталог
создаёт, form-compile и skd-compile — тоже; не создавал только этот порт.
Тестами не ловилось: во всех кейсах mxl-compile писал в уже существующий
каталог. Добавлен кейс с записью по несуществующему пути — проверено, что на
коде до правки он падает.
Найдено при разборе вопроса, нужна ли заглушка тела макета в template-add:
оказалось, заглушка держала не замысел, а этот дефект.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Из ~46 навыков-эмиттеров XML восемь не имели ни одной проверки `preserves`:
form-edit, form-remove, meta-remove, skd-edit, template-remove, xdto-edit,
support-edit, cfe-patch-method. Шесть из них правились в предыдущем коммите,
и канон у них держался на том, что правка была механической, а не на проверке.
Ожидания писались ПО КАНОНУ, а не по текущему поведению — и это сразу вскрыло
три дефекта:
1. form-remove очищал слот формы через InnerText="" / text="" — получалась
пустая пара <DefaultObjectForm></DefaultObjectForm>. Платформа пустых пар
не пишет (0 на 476 942 XML). Теперь IsEmpty / text=None.
2. Опустевший <ChildObjects> оставался парой, разнесённой по строкам, —
в PS-порту form-remove и template-remove. Корпус: 1394 самозакрывающихся
<ChildObjects/> на acc+erp, пустых пар 0 в обеих формах.
3. template-add писал пустой макет парой <SpreadsheetDocument></...>.
Во всей выгрузке acc_8.3.27 (65 040 XML) многострочных пустых пар нет
ни для одного тега.
Заодно усилена сама проверка noEmptyPairs: она ловила только СМЕЖНЫЕ теги,
поэтому дефект №2 проходил мимо неё. Добавлен вариант с переводом строки
внутри; дискриминатором служит сам перевод строки — значащий пробельный
текст-узел (<xr:FillValue xsi:type="xs:string"> </xr:FillValue>) его не
содержит и под проверку не попадает.
support-edit покрыт частично (BOM у ParentConfigurations.bin — проверено, что
платформа пишет его с BOM во всех трёх выгрузках), cfe-patch-method — BOM+EOL
у .bsl: хвостовой перевод строки у модулей неканоничен (1235 с ним, 766 без),
поэтому не утверждается.
Регресс: 647/647 ps1, 644/647 py (3 skipped). Эталоны переснятые: три места,
каждое — ровно ожидаемая пара строк.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Все 23 проверки preserves.eol ожидали crlf — ни одного LF-исходника в наборе
не было, поэтому расхождение портов по переводам строк жило целиком в
непокрытой ветке и не могло быть замечено.
Добавлены LF-фикстуры и кейсы roundtrip-lf-preserve:
- cf-edit, meta-edit, subsystem-edit — правка существующего файла через
XmlWriter (наследование стиля исходника);
- role-compile, meta-compile, xdto-compile — регистрация в существующем
Configuration.xml. Эти кейсы проверяют обе половины правила разом: правка
наследует LF, а создаваемый рядом файл получает канон CRLF.
Проверено в требуемом порядке: на коде ДО правки кейсы падают
(role-compile/meta-compile/xdto-compile — «expected lf, got 251 CRLF»,
meta-edit — «40 CRLF + 136 lone LF», subsystem-edit — «1 CRLF + 24 lone LF»),
после правки проходят на обоих рантаймах.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Скелетные генераторы писали Module.bsl с LF, а платформа пишет модули с CRLF:
в выгрузке БП 2643 CRLF и чисто-LF 0 из 3001 модуля (358 «смешанных» — LF внутри
строковых литералов кода), BOM 2001/2001. По тому же доводу сходимости, что и для
XML: платформа перепишет модуль в CRLF при первой выгрузке, значит наш LF — дифф,
созданный нами.
В репозитории уже была встречная конвенция: meta-compile (модуль команды) и
cfe-patch-method (base/local/remote.bsl) пишут .bsl явным CRLF в обоих портах.
Выбивались только эти три скелета.
Хвостовой перевод строки НЕ трогаем: у платформы он неканоничен — 1235 модулей
с ним, 766 без; это след автора кода, а не правило. Меняем только разделители.
Правка в точке записи, а не в шаблоне: так одна строка на скрипт и не трогаются
\u-экранированные кириллические литералы в py (Edit по ним переэкранирует).
Снэпшоты нормализуют EOL и увидеть это не могут — добавлен preserves на модуль
в epf-init/basic, erf-init/basic, form-add/basic.
Windows 641/641 обоими портами, мак 590/0/51, дрейфа снэпшотов нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Тот же writer без newline="", что чинился в form-add.py, остался ещё в четырёх
py-портах, и пишут они не модули, а 1С XML: заимствованные объекты и метаданные
формы (cfe-borrow), Template.xml и XML-макеты (template-add), Help.xml (help-add),
вновь создаваемый CommandInterface.xml (interface-edit).
Контент там собирается литералами с \n, поэтому текстовый режим Python давал CRLF
на Windows — «случайно правильно» — и LF на macOS, ломая канон #57 ровно там, где он
только что установлен. Проверено на маке ДО фикса: cfe-borrow 0 CRLF + 33 одиночных
LF, template-add 0 + 2, interface-edit 0 + 5. Порчи \r\r\n не было, но это везение:
подай такой writer CRLF-контент — каждая строка стала бы \r\r\n.
Почему не поймали раньше: аудит гонялся на Windows, где текстовый режим даёт канон;
снэпшоты нормализуют EOL; байтовый preserves ни разу не проверялся на маке.
Разделены два пути, которые я сперва смешал в cfe-borrow:
- save_xml_file — канон (CRLF, без хвоста) для файлов, которые СОЗДАЁМ;
- save_text_bom — пишет как есть, для файлов, которые ПРАВИМ: переводы строк уже
пришли из самого файла и менять их нельзя (контракт #44/#46/#47). Туда же
добавлено чтение с newline="" — иначе CRLF терялся ещё на входе.
interface-edit чинится и в PS-порте: ветка -CreateIfMissing давала смешанный EOL
(3 CRLF + 2 одиночных LF) уже на Windows. Аудит #57 её пропустил, потому что
фильтровал файлы по расширению .xml, а кейс создаёт файл с именем без расширения.
Канон, как выяснилось, зависит от типа артефакта: XML — CRLF, текстовый макет —
CRLF, а HTML-макет платформа хранит с LF (корпус: 399 LF из 400). Поэтому HTML
через канон-writer НЕ идёт.
preserves добавлен четырём навыкам (у них его не было вовсе) + отдельно на Help.xml.
Именно прогон на маке и даёт покрытие: на Windows эти кейсы зелены и без фикса.
Windows 641/641 обоими портами, мак 590/0/51, дрейфа снэпшотов нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Раннер сам прятал дефекты, которые чинит #57. normalizeXmlContent при
runtime=python снимал ровно четыре измерения: пробел перед `/>`, whitespace между
тегами, пустую пару <Tag></Tag> и хвостовой пробельный мусор. Паритет PS<->PY по
этим измерениям проверялся ЧЕРЕЗ маску — расхождение физически не могло упасть.
Снято три из четырёх (пробел, пустая пара, хвост). Схлопывание whitespace между
тегами оставлено: порты кое-где расставляют отступы иначе, это форматирование, а
не байтовый канон.
Ужесточён checkPreserves: eol теперь считает ОДИНОЧНЫЕ LF, а не «есть ли хоть один
CR». Прежняя проверка пропускала смешанный выход — cfe-init давал 10 CR на 70
строк и проходил её, то есть головной дефект тикета был ей невидим. Добавлены
ключи selfClose:"tight" и noEmptyPairs; preserves проставлен в 13 кейсах
навыков-эмиттеров (по одному на навык, на его СОБСТВЕННЫЙ артефакт).
Снятие масок сразу вскрыло четыре реальных расхождения портов:
- form-edit собирает выход из OuterXml и писал `<a />`; в списке 17 навыков его не
было, потому что искал по вызовам Save — здесь другой путь. Тот же случай, что
с Form.xml в cfe-borrow;
- meta-edit py дописывал хвостовой перевод в создаваемый Ext/Predefined.xml и
читал существующий без newline='' (терял CRLF);
- skd-info py писал отчёт -OutFile без хвостового перевода, PS через WriteAllLines
— с ним. Это текстовый отчёт, канон Конфигуратора к нему не относится, поэтому
выровнял py по существующему эталону;
- фикстуры кейсов содержали <Vendor></Vendor> — снимок нашего же старого вывода.
Конфигуратор пустых пар не пишет, .NET их сохраняет, lxml схлопывает. Поправлены
10 фикстур: пары → самозакрывающиеся, EOL и BOM не тронуты.
Результат: PS 641/641, python 638/641 (+3 runtimeOnly-скипа) — со снятыми масками.
Дрейф снэпшотов: 10 файлов, только пробельные теги и пустые пары.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Конфигуратор пишет <Vendor/>, а не <Vendor></Vendor>: в 8 выгрузках cfsrc
(476 943 XML) пустых пар ноль. Шаблоны подставляли значение ВНУТРЬ пары, поэтому
при пустом значении получалась пара.
Собираем элемент целиком: <Vendor>/<Version> (cf-init, cfe-init),
<DefaultRoles> (cfe-init при -NoRole), <Key> и <Namespace> (meta-compile —
план обмена и веб-сервис). Образец рядом же: <Comment> и <Description> в
meta-compile так делали и раньше.
Дефект был в ОБОИХ портах одинаково — здесь дело не в сериализаторе, а в шаблоне.
Пустая пара <Namespace> в cfe-borrow была не его: он копирует свойства
заимствуемого объекта, а порождал их meta-compile.
Дрейф снэпшотов: 340 <Vendor/>, 339 <Version/>, по одному <Key/>, <Namespace/>,
<DefaultRoles/>. Содержательных изменений ноль. Тесты 641/641.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Головной дефект тикета: cfe-init выдавал Configuration.xml с 10 CR на 70 строк, а
роль и язык — вовсе без CR. Теперь 70/70, последний байт `>`.
Правило: меняем только РАЗДЕЛИТЕЛИ строк, содержимое текстовых узлов не трогаем.
Платформа его не трогает тоже — в запросах СКД из чистой выгрузки встречаются и
CRLF, и одиночный LF. Поэтому сплошная нормализация файла применяется только там,
где многострочных текстовых узлов нет по построению (скелеты), а в объектном XML
и формах разделители правятся в точке сборки.
Источников оказалось четыре, а не один:
1. Скелеты (cf-init, cfe-init, epf-init, erf-init, form-add, help-add,
template-add) собираются here-string'ами, а .ps1/.py в репозитории хранятся с
LF — отсюда LF и смешанный EOL.
2. py-порты склеивали документ через '\n'.join(lines); PS в тех же местах давал
CRLF через AppendLine — порты расходились побайтово.
3. Билдеры Predefined в meta-compile собирались с явным LF и даже сворачивали
CRLF→LF из общего эмиттера типов.
4. Чтение существующего файла в python БЕЗ newline='' молча схлопывает CRLF в LF
(универсальные переводы строк), и запись потом кладёт LF. Так role-compile и
subsystem-compile переписывали в LF весь Configuration.xml. meta-compile это
уже делал правильно — там newline='' стоял с фикса #44/#46/#47.
Отдельно: XML-парсер по спецификации схлопывает CRLF при разборе, поэтому
lxml-порты (xdto-compile, xdto-edit) отдавали LF-документ там, где .NET возвращал
CRLF через NewLineHandling. Восстанавливаем EOL исходного файла.
Разделение канона и сохранения стиля:
- файл СОЗДАЁМ — канон (CRLF, без хвоста);
- существующий ПРАВИМ — наследуем его EOL, включая перевод строки вставки
(контракт #44/#46/#47). Иначе LF-проект получал бы смешанные файлы — ровно то,
на что заведён #57. Кейсы roundtrip-crlf-preserve остаются зелёными.
Хелпер записи скопирован в каждый навык (навыки автономны). У meta-compile он
называется Write-XmlFileKeepEol / write_xml_file_keep_eol: там нормализовать EOL
НЕЛЬЗЯ (многострочные запрос, синоним, значение заполнения), и одинаковое имя при
разном поведении было бы ловушкой.
Заодно: имя временного файла батча в meta-compile.ps1 получило GUID. Фиксированное
"meta-compile-batch-$idx.json" в общем %TEMP% сталкивало два параллельных запуска
(«file is being used by another process») — из-за этого полный набор приходилось
гонять с урезанной параллельностью. py-порт уже брал mkstemp.
Аудит: было ~250 файлов с дефектом EOL, стало 0 в обоих портах. Осталось четыре
законных случая — фикстуры roundtrip-crlf-preserve (сохранение стиля) и запрос
динсписка с многострочным текстом. Дрейф снэпшотов — 1 файл. Тесты 641/641.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Конфигуратор не пишет перевод строки в конце файла — последний байт `>`. Это видно
на чистой выгрузке пустой ИБ (Windows и macOS) и на всех 8 выгрузках в cfsrc.
Наши эмиттеры добавляли лишний, причём оба порта и по разным причинам:
- PS: StringBuilder.AppendLine дописывает Environment.NewLine после последней строки;
- PY: '\n'.join(lines) + '\n' — хвост добавлен явно.
В meta-compile заведена единая точка записи Write-XmlFile: там документ собирают
и AppendLine, и явные `n в билдерах Predefined/Content/Flowchart, а выходов семь.
В остальных обрезка стоит в самом вызове записи.
Модули .bsl, JSON-выход form-compile, HTML-справка help-add и XSD из xdto-decompile
сюда НЕ входят: канон Конфигуратора описывает XML метаданных, а у этих артефактов
свои правила. Результаты XmlDocument.Save тоже не трогаются — у них хвоста нет.
Навыки редактирования продолжают сохранять стиль ВХОДНОГО файла: кейс
subsystem-edit/roundtrip-crlf-preserve, где фикстура намеренно с хвостом, остаётся
зелёным. Это контракт #44/#46/#47, и он не отменяется.
Дрейф снэпшотов: 547 файлов, у каждого только последняя строка, содержательных
изменений ноль. Тесты 641/641.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
.NET XmlWriter пишет `<a />`, а Конфигуратор — `<a/>`. Поскольку Save переписывает
файл целиком, meta-edit при добавлении одного реквизита переводил в пробельную форму
все пустые теги документа. Дефект только у PS-порта: lxml (45 из 49 py-портов) уже
пишет плотно, то есть порты расходились побайтово.
Канон измерен, а не предположен: чистая выгрузка пустой ИБ на Windows и macOS плюс
сплошной скан 8 выгрузок в cfsrc — 476 943 XML, 21 294 119 самозакрывающихся тегов,
пробельных 0. Форма не зависит от ОС, версии платформы (8.3.20-8.5) и наличия
атрибутов. Правило уже было реализовано в skd-edit — оттуда и взято.
Замена безопасна доказуемо: .NET экранирует `>` как `>` и в тексте, и в атрибутах,
поэтому ` />` после Save — только конец тега. Лазейки (CDATA, комментарии) в
1С-метаданных не встречаются — 0 из 476 943 файлов; гард на них всё равно стоит.
Одиннадцать скриптов писали прямо в FileStream без пост-обработки — переведены на
MemoryStream, что заодно чинит `encoding="utf-8"` строчными (335 файлов в снэпшотах).
В cfe-borrow правится и сборка Form.xml: куски берутся из OuterXml, а он спацовывает
так же, как XmlWriter.
Дрейф снэпшотов: 12 838 строк тегов + 335 деклараций, содержательных изменений ноль.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README называл info/validate-навыки типичным случаем для noSnapshot: эталон
зафиксировал бы выход preRun, а не проверяемого навыка. Факт верный, вывод — нет.
Когда preRun собирает фикстуру, эталон фиксирует ВХОД теста. Без него дрейф
навыка-генератора меняет фикстуру молча, и ожидание вроде «Составной (6)»
начинает проверяться на другом объекте — либо падает без внятной причины, либо
сходится случайно и перестаёт что-либо проверять. meta-compile такой дрейф даёт
регулярно. Поэтому ни один из семи info-навыков noSnapshot не использует —
у всех эталоны, и это осознанно, а не упущение.
Правило сформулировано явно: есть preRun с генерацией фикстуры → эталон;
нет preRun или фикстура тривиальна → noSnapshot.
В шапке verify-snapshots отмечено, что он работает и с кейсами навыков, которые
сами ничего не пишут, — там он проверяет, что платформа принимает собранную
preRun фикстуру. Плюс предупреждение, что на кейсах setup: external проверка
вырождается в загрузку 1С собственной выгрузки: ~3 минуты на кейс без пользы.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Строка «Иерархический: …» печаталась в ветке Catalog, хотя свойство
Hierarchical есть и у планов видов характеристик — в корпусе acc/erp/ut/unf
таких 13. У них иерархия не выводилась ни в overview, ни в full.
Хуже того, вывод противоречил сам себе: блок стандартных реквизитов в full
показывал Родитель (он определяется по тому же Hierarchical), а строки про
иерархию рядом не было — родитель появлялся ниоткуда.
Иерархия вынесена из ветки Catalog и печатается по наличию свойства;
подчинение осталось специфичным для справочника. Отсутствие иерархии
по-прежнему не печатается: сообщаем о наличии, молчим об отсутствии —
тихую ошибку в коде даёт только необнаруженное наличие.
Свойство Hierarchical проверено по корпусу: оно есть только у Catalog и
ChartOfCharacteristicTypes (у плана счетов иерархия по природе, свойства нет).
Прогон по корпусу: ровно 13 изменённых строк, побочных эффектов нет.
Находка из отклонённого PR #59 — там иерархия тоже была вынесена из ветки
Catalog. Реализация не заимствована: PR печатал «Иерархический: нет» для
неиерархических объектов, то есть строку-шум примерно у 4000 объектов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Свёртку составного типа и её разворот через drill-down проверял только кейс
на выгрузке acc_8.3.24, а он привязан к локальному пути и на macOS
пропускается — то есть ключевая новая логика там не проверялась вовсе.
Добавлены два синтетических кейса на фикстуре от meta-compile: ПВХ с шестью
типами сворачивается в «Составной (6)» и печатает подсказку, а -Name
ТипЗначения разворачивает список. В составе фикстуры два v8:TypeSet
(cfg:CatalogRef, cfg:DocumentRef), поэтому кейсы заодно покрывают разбор
обобщённых ссылочных типов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ишью #58: сводка не показывала владельца подчинённого справочника, и код,
написанный по ней, падал на Записать(). Владелец оказался частным случаем:
шапка «Код(9) | Наименование(100)» смешивала свойства объекта с полями и не
давала по полям ни типа, ни обязательности. По acc_8.3.24 из 690 справочников
23 имеют числовой код, 122 — фиксированной длины, 426 — вовсе без кода; всё
это было невидимо. У ПВХ, планов счетов и обмена, бизнес-процессов и задач
шапки не было вообще — включая ТипЗначения у ПВХ.
Поля вынесены в блок «Стандартные реквизиты» на языке блока «Реквизиты»
(имя — тип — [флаги]), в шапке остались свойства объекта. В overview
показываются те, что нельзя вывести из остального вывода: Владелец, Код,
Наименование, Дата, Номер, ТипЗначения. Ссылка, ПометкаУдаления, Родитель,
ЭтоГруппа следуют из типа объекта и строки «Иерархический» — они в full.
Обязательность берётся из StandardAttributes/FillChecking. Блок в XML
опционален (meta-dsl-spec §7.1.1), поэтому при его отсутствии действует
профиль платформенных дефолтов, синхронный с meta-compile — иначе объекты,
созданные meta-compile, теряли бы флаг, то есть ровно кейс из ишью.
Составной ТипЗначения ПВХ доходит до 151 типа и сворачивается в счётчик
«Составной (110)»; полный состав даёт drill-down -Name ТипЗначения, о чём
сообщает единственная строка в конце вывода. brief получил строку
«Подчинён: …» — факт структуры без обещаний про обязательность, которых
brief выполнить не может.
Проверено сравнением с эталонным прогоном по 4768 объектам acc/erp/ut/unf:
все расхождения классифицированы, необъяснённых нет, ненулевых кодов возврата
нет. Владелец появился у 242 объектов, числовой код — у 56.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Заимствованные DataProcessor, Report, DocumentJournal, HTTPService,
WebService, Subsystem и др. выводились без <ChildObjects/> — платформа
отвергала расширение при загрузке исходников («ожидаемое ChildObjects»).
Признак теперь берётся у объекта-источника, список типов остаётся
страховкой и дополнен недостающими.
Следом всплывал второй отказ — «Отсутствует внутренняя информация
(узел InternalInfo)» для Sequence, FilterCriterion, SettingsStorage:
этих типов не было в карте генерируемых типов. Добавлены они, а также
IntegrationService и WSReference.
Проверено реальной загрузкой на 8.3.24 и 8.3.27: синтетический стенд
(9 типов) и заимствование из acc/ut в BP_DEMO и UT_DEMO.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Detect-FormatVersion читает префикс Configuration.xml как
ReadAllText(...).Substring(0, Min(2000, (Get-Item).Length)). Размер файла — в
БАЙТАХ, Substring считает СИМВОЛЫ: на кириллице байт больше, и если файл
короче 2000 символов, длина среза выходит за строку и навык падает
исключением. Проявлялось на маленьких конфигурациях — например на фикстуре
EPF внутри конфигурации на поддержке.
Функция расходится копиями по навыкам, поэтому правка одинаковая в восьми:
длина берётся по самой строке. В help-add так было изначально; в
meta-compile рядом (Detect-CompatibilityMode) уже стоял верный вариант.
py-порты иммунны: там f.read(2000) в текстовом режиме — читает символы и не
бросает исключение. Зеркалить нечего, версии выровнены по обоим портам.
Заодно tests/skills/verify-snapshots.mjs: outputPath кейса читался только из
params, тогда как runner.mjs берёт его с верхнего уровня — из-за расхождения
харнесс искал результат не там, где навык его написал, и это маскировало
падение как «выход не создан».
Проверка: 1С-сертификация mxl-compile 13/13 (кейс guard-allow-external падал
с начала кампании), полная сюита 630/630 на PowerShell и 627+3 skipped на python.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Правило платформы единое и подтверждено дважды. По корпусу трёх конфигураций:
92142 сырых кавычки в тексте элементов и НИ ОДНОЙ "; в условиях RLS —
16334 сырых против нуля экранированных (при этом & платформа пишет, то есть
амперсанд экранируется, а кавычка нет). Загрузка на стенде: оба варианта
принимаются, но выгружает платформа сырую кавычку — то есть " не ошибка,
а лишний шум в роундтрипе.
Решение было принято раньше в form-compile (там оно и записано комментарием) и
в skd-compile/skd-edit, но шесть навыков из него выпали. Приведены к общему виду:
где Esc-Xml использовался только для текста — функция стала текстовой; в meta-edit,
где она нужна и для атрибута, текстовые места переведены на существующий
Esc-XmlText. Атрибуты нигде не затронуты: там экранирование кавычек обязательно.
Дрейф эталонов — три строки, все условия RLS; role-info и role-validate строят
фикстуры прогоном role-compile (кросс-навыковый пересъём).
Проверка: полная сюита 630/630 на PowerShell и 627+3 skipped на python;
1С-сертификация role-compile 9/9, subsystem-compile 9/9, subsystem-edit 6/6,
form-edit 6/6, meta-edit 16/16. В mxl-compile кейс guard-allow-external падает
и до правки — не связан.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Прогон сервисов на конфигурациях формата 2.17 (ERP + БП, 46 объектов) вскрыл
то, чего не было в УТ: 25 совпадений из 46.
ERP двуязычна, и синоним шаблона, метода, операции и параметра приезжает
объектом {ru,en}. Компилятор интерполировал его в строку — в XML попадал
литерал "@{ru=Версия; en=Version}" вместо пары языковых элементов. Значение
теперь передаётся в эмиттер как есть, он и так умеет обе формы.
RootURL сравнивался с дефолтом (имя в нижнем регистре) регистронезависимо,
поэтому MobileAppReceiptScanner считался дефолтным и терял регистр при
регенерации. Сравнение сделано регистрочувствительным — тот же класс ошибки,
что уже ловили на синонимах методов.
Спека meta-dsl-spec дополнена: объектные формы шаблонов, методов, операций и
параметров; xdtoPackages как список; descriptorFileName; dataLockControlMode;
нотация Кларка для типов из своего пространства имён; Use в ReuseSessions.
Проверка: сервисы ERP+БП 46/46, сервисы УТ 25/25, сюита 76/76 на обоих
портах, 1С-сертификация кейса с многоязычным синонимом пройдена.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
meta-decompile не знал тип WebService — 18 сервисов УТ не проходили раундтрип.
Добавлен разбор (оба порта): namespace, состав XDTO-пакетов, дескриптор,
операции с параметрами.
Компилятор поддерживал тип поверхностно; раундтрип на реальных сервисах
показал, чего не хватало:
- XDTOPackages эмитился скаляром, а это СПИСОК элементов: ссылка на пакет
конфигурации (xr:MDObjectRef) либо URI внешнего пространства имён
(xs:string). Presentation пуст, CheckState 0 — 19/19 по корпусу;
- DescriptorFileName не эмитился вовсе, хотя есть у всех 18 сервисов и НЕ
выводится из имени (DMILService -> dmil.1cws) — нужен явный ключ;
- DataLockControlMode не эмитился (Managed у всех 192 операций);
- Comment не эмитился ни у сервиса, ни у операций и параметров;
- типы из собственного пространства имён (81 случай) писались без локального
xmlns. В DSL задаются нотацией Кларка "{uri}ИмяТипа", компилятор объявляет
xmlns сам — как это делает платформа.
Nillable захватывается явно у операций и параметров: по корпусу значения
смешанные (операции 103/89, параметры 128/395), дефолт угадать нельзя.
Порядок операций и параметров приведён к порядку DSL в обоих портах — PS шёл
в порядке хеш-таблицы, py сортировал.
Проверка: 25 сервисов УТ (18 WebService + 7 HTTPService) 25/25 без
расхождений; JSON декомпилятора побайтово совпадает у PS и py на всех 25;
сюита 76/76 на обоих портах; 1С-сертификация обоих кейсов пройдена.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Раундтрип по обработкам и отчётам УТ (7568 объектов) показал 265 лишних
строк: мы эмитили LineNumberLength каждой табличной части, а платформа
пишет его только объектам, у которых есть таблица в базе.
Проверено на двух конфигурациях независимо: у обработок 0 тегов из 235+257
табличных частей, у отчётов 0 из 106+8; у справочников, документов, ПВХ,
планов обмена и бизнес-процессов тег стоит поголовно. Свойство задаёт
разрядность физического номера строки в таблице БД — у обработок и отчётов
таблиц нет, хранить нечего.
Исключены DataProcessor и Report, а также ExternalDataProcessor и
ExternalReport: внешние обработки той же природы, и при сборке EPF на
формате 2.20 мы бы писали тег, которого платформа не пишет.
Кейс format-220-props дополнен обработкой с табличной частью: у неё тега
нет, у справочника в той же конфигурации LineNumberLength=9.
Проверка: УТ tier-2 7543/7543 без расхождений (было 71 diff),
сюита 76/76 на обоих портах, 1С-сертификация на 8.3.27 пройдена.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Значением параметра выбора и значения заполнения бывает ЗНАЧЕНИЕ — пустая
ссылка, предопределённый элемент или значение перечисления. Объект метаданных
значением быть не может, поэтому двухчастное "Тип.Имя" — это строка.
Правило уже существовало, но только для перечислений ("Enum.X" не значение);
для остальных корней его не было, и строка "Документ.РеализацияТоваровУслуг"
превращалась в xr:DesignTimeRef с переводом имени на английский — молчаливая
подмена смысла: вместо текста получалась ссылка на реальный документ.
Проверка по корпусу (acc+erp+ut): 5520 значений в ChoiceParameters и 8623 в
FillValue — двухчастных с типом метаданных НЕТ ни одного. Прощающий ввод не
пострадал: "Справочник.Номенклатура.ПустаяСсылка" -> Catalog.Номенклатура.EmptyRef,
"Перечисление.X.Y" -> Enum.X.EnumValue.Y работают как раньше (закреплено кейсом).
Проверка: УТ tier-1 1283 -> 1282 совпадения, справочники 519/519,
сюита 76/76 на обоих портах, 1С-сертификация кейса пройдена.
Остаётся 1 расхождение — план обмена без Ext/Content.xml (хвостовая аномалия
конфигурации: на одной БП все четыре платформы файл пишут).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Раундтрип по справочникам УТ 11.5.27 вскрыл: <app:value xsi:type="xr:DesignTimeRef"/>
с пустым содержимым декомпилировался в "value": "", и компилятор законно
эмитил свой дефолт xs:string — тип ссылки терялся.
Конвенция для этого случая уже была (маркер {emptyRef: true} у fillValue),
пробел был только в ChoiceParameters. Декомпилятор теперь ставит маркер,
компилятор его понимает — и в скалярном значении, и внутри FixedArray.
Форма редкая, поэтому прежние кампании её не поймали: в acc она встречается
1 раз, в erp — ни разу, в УТ — 7 раз.
Is-EmptyRef в PS явно отсекает коллекции: у массива $v.emptyRef разворачивается
в свойства элементов (member enumeration), и массив с одним таким элементом
схлопывал FixedArray в скаляр. В py-порте isinstance(v, dict) такого не допускает —
расхождение поймано снэпшотом.
Проверка: справочники УТ 519/519 без расхождений (было 519 diff),
сюита meta-compile 76/76 на обоих портах, 1С-сертификация кейса пройдена.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Версия формата выгрузки идёт лестницей, а не скачком 2.17 -> 2.20:
8.3.20-8.3.24 -> 2.17, 8.3.25 -> 2.18, 8.3.26 -> 2.19, 8.3.27 -> 2.20.
Прежняя дельта мерилась через две ступени и приписала версии 2.20 всё,
что появилось по дороге.
Замер на одной и той же конфигурации, выгруженной четырьмя платформами:
- 2.18: TypeReductionMode (4793 файла), TextToSpeech, 3 редких свойства форм;
- 2.19: новых тегов нет, роли перешли на omit-on-default;
- 2.20: только LineNumberLength.
meta-compile: единый гейт isFormat220 управлял обоими свойствами, поэтому на
проекте 2.18/2.19 TypeReductionMode не эмитился, хотя платформа этих версий
его пишет — роундтрип разъезжался. Порог расщеплён: >=2.18 для
TypeReductionMode, >=2.20 для LineNumberLength.
meta-validate: реестр versionedProps объявлял TypeReductionMode свойством
2.20, из-за чего проверка 18 давала ложную ошибку на корректном файле
2.18/2.19. Исправлено на 2.18.
Валидаторы (meta/form/cf/cfe/epf) считали 2.18 и 2.19 неизвестными версиями
и предупреждали «Unusual version»; cf-init не давал отскаффолдить
конфигурацию под 8.3.25/8.3.26. Обе версии впущены.
Тесты: фикстура empty-config-218, кейс meta-compile на границу свойств
(TypeReductionMode есть, LineNumberLength нет), два кейса meta-validate на
законность штампов 2.18/2.19; кейс error-220-props-in-217 теперь проверяет
оба сообщения с разными порогами. Кейс 2.18 проверен загрузкой в живую
8.3.25 через verify-snapshots.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Хвосты после разбора: db-run и stub-db-create остались единственными,
кто не прощал обрамляющие кавычки и хвостовой разделитель в путях.
Плюс db-create правился в коммите паритета без бампа версии, а раннер —
без бампа своего заголовка.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Кейсы, разведённые по портам из-за расхождения вывода, слиты обратно —
это и есть приёмочный признак паритета. Цепочка epf-build разделена на
два кейса: успешный запуск временной базы теперь молчит, поэтому её
командная строка проверяется на падающем фейке.
Новое: чтение вывода платформы в UTF-8 и cp866, отсутствие блока при
молчащей платформе, форма токена для пути с пробелом, отказ до запуска
при отсутствующей базе.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Фейк платформы, написанный как .cmd, на macOS не исполняется ни одним
портом — такие кейсы падали на маке вместо пропуска. runtimeOnly для
этого не годится: ограничение не по порту, а по ОС.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
expect.stdoutContains/stdoutNotContains жили в ветке успеха, поэтому у
кейса с expectError проверялся только ненулевой код возврата, а строки
не смотрелись вовсе. Пятнадцать кейсов (включая xdto-validate,
meta-validate, form-validate, meta-remove) были зелёными вхолостую —
у facet-conflicts текст навыка успел разойтись с ожиданием.
Плюс case-level "cwd": "workDir" — кейсу может понадобиться, чтобы
навык стартовал внутри рабочего каталога (фикстура .v8-project.json).
Кейсы на доп. аргументы платформы: pass-through 1cv8 и ibcmd, источник
из реестра проекта, цепочка epf-build → stub, конфликт ключа,
позиционный токен ibcmd, чужой движок, маскирование секрета.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Набор аргументов платформы был закрыт: общий ключ запуска (например
/UseHwLicenses+ на машине с аппаратной лицензией) передать было нельзя,
и сборка на автоматически созданной временной базе падала с «Не найдена
лицензия».
Добавлен escape hatch — по параметру на движок, плюс зеркальные ключи
в .v8-project.json (v8args / ibcmdargs) для машинно-специфичных флагов:
- -AdditionalV8Arguments → 1cv8.exe, ключи вида /Key
- -AdditionalIbcmdArguments → ibcmd, ключи вида --key=value
Аргументы уходят во все запуски платформы, которые делает навык:
epf-build без базы прогоняет CREATEINFOBASE, /LoadConfigFromFiles,
/UpdateDBCfg и саму сборку — ключ получает каждый.
До запуска отклоняются: аргумент, которым управляет сам скрипт (режим,
подключение, /Out, пакетная операция), позиционный токен для ibcmd и
параметр «не своего» движка. Значения секрето-опасных ключей (/P, /UC,
--password, --token) в логе маскируются.
Порядок источников: .v8-project.json, затем параметр. Поведение без
новых параметров не меняется.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
На боевой форме вскрылись случаи, которые прежнее правило не покрывало. Замеры показали,
что любая эвристика «по первому сиблингу за заголовком» нежизнеспособна:
- служебная обёртка .logicGroupContainer пишется двумя способами (у таблицы
<дочерний>#group_div, у вложенной группы <дочерний>_div) — знали только первый;
- display самой обёртки плавает: block до первого тогла, none после;
- первые узлы группы могут быть скрыты своей логикой, а видимое содержимое идёт дальше
по цепочке — раскрытая группа читалась как свёрнутая, и постусловие роняло успешный клик.
Теперь состояние берётся из контрола сворачивания: у варианта «картинка» — кадр gx спрайта
hideshow у каретки (полярность обратна дереву в dom/grid.mjs), у варианта «гиперссылка»
каретки нет, там принадлежность по отступу — дети группы смещены глубже её заголовка,
свободный сосед стоит на уровне заголовка. База отсчёта — левый край блока заголовка
(каретка сдвигает текст вправо), обход ограничен: у последней свёрнутой группы границы за
ней нет, и первый видимый узел нашёлся через 107 сиблингов в чужой ветке формы.
Клик по заголовку группы теперь сперва скроллит цель в вид: цель кликается по координатам,
и ниже вьюпорта клик молча не доходил.
Ответ на клик отдаёт clicked.group (техническое имя) и clicked.title (текущий заголовок):
заголовок ключом быть не может — он повторяется между блоками формы и меняется при
раскрытии (CollapsedRepresentationTitle), после чего клик по прежнему тексту не находит
элемент. При смене заголовка hint говорит, чем кликать дальше.
Фикстура: вложенная группа первым ребёнком, «скрыт первый узел» в двух вариантах контрола,
группа с меняющимся заголовком в конце формы (уезжает за вьюпорт). Каждый кейс проверен на
красноту без своей правки.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1С кладёт заголовок группы колонок и её листья в ОДНУ строку шапки, различая их
только высотой и шириной. Колонка заводилась на каждый бокс с текстом, а строка
ключуется по имени — поэтому одноимённые листья соседних групп («План» под «Цена»
и под «Количество») схлопывались в один ключ и склеивались через " / ", а
заголовки групп становились пустыми колонками.
Заголовок группы отличается от настоящей широкой колонки над узкими (паттерн
«Исполнитель» над «Срок»/«Выполнена») одним надёжным признаком: его colindex не
встречается ни в одной ячейке тела. По нему листья переименовываются в
«Группа / Лист» — той же конвенцией, что уже применяет читалка табличного
документа, — а заголовок из колонок убирается. Разворот объединённой шапки
(«Субконто 1/2/3») не задет: под ним боксов-листьев в шапке нет.
Заодно у пути записи заголовок ячейки резолвился x-сканом шапки и под группой
отдавал «Цена» всем трём листьям — fillTableRow по имени из readTable отвечал
notFilled. Теперь имя берётся из общей модели, как в чтении и клике.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Группа, содержимое которой — таблица, отдавала collapsed:false в обоих состояниях,
а свернуть её обратно было нельзя. Два независимых дефекта:
1. Первым сиблингом за <base>#title_div платформа кладёт пустую служебную обёртку
<дочерний>#group_div.logicGroupContainer (display:block, height:0), а контент идёт
дальше по сиблингам — внутри обёртки его нет. Чтение display первого сиблинга
давало collapsed:false и свёрнутой, и развёрнутой группе.
2. <base>#title_text растягивается по ширине содержимого (173px свёрнута → 1295px
развёрнута), кликабелен только вложенный label шириной по тексту. Клик в
геометрический центр попадал в пустоту: раскрыть удавалось, свернуть — нет.
GROUP_STATE_FN обходит сиблинги по id-префиксу обёртки (префикс обрывает обход на
чужих узлах — свободный элемент между группами по-прежнему не путает определение).
TEXT_CLICK_POINT_FN целится во вложенный текст с клампом влево, как rowClickPoint.
Обе — общие для getFormState().groups[] и резолвера цели клика.
Клик по заголовку, не изменивший состояние, теперь бросает ошибку вместо
toggled:true — идемпотентность {expand} не затронута, при нечитаемом collapsed
проверка пропускается.
Фикстура: свёрнутая группа с таблицей (прежние содержали только декорации, поэтому
регресс был зелёным) и растянутая гиперссылка с Сообщить в обработчике — у декораций
промаха нет, их внутренний узел тянется вместе с контейнером.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
В модели XDTO fixed — булев признак, а значение лежит в default; в XML-схеме
fixed="V" совмещает и признак, и значение. Компилятор оба идиома принимал, но
не проверял принятое: зеркало xdto:fixed="true" без default собиралось молча
в пакет, который платформа отвергает («Отсутствует фиксированное значение
свойства»). Прощающий ввод был сделан наполовину.
xdto-validate v1.1 — два ERROR: значение попало в признак (fixed не булев)
и признак без значения. Формулировка второго повторяет платформенную дословно,
чтобы отказ загрузки и наш вывод читались как одно и то же.
xdto-compile v1.1 — то же условие предупреждением на сборке, то есть на шаг
раньше db-update, где починить дешевле.
Кейсы: оба идиома плюс только default и атрибут (загружается в базу);
зеркало без значения — проверка диагностики, из платформенной верификации
исключено штатным skipPlatformVerify, пакет невалиден by design.
Правило откалибровано корпусом (760 пакетов, оба рантайма): 0 ложных
срабатываний, состав предупреждений не изменился. Round-trip остался
760/760 байт-в-байт.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
verify-snapshots загружает результат каждого кейса в 1С. Раньше навыки xdto
через него не проходили вовсе; первый прогон дал 5 из 9. Ни корпусная сверка,
ни валидатор такого не ловили: корпус состоит из заведомо валидных пакетов,
а синтетические кейсы до сих пор в базу не грузились.
1. fixed. В модели XDTO это булев флаг, значение лежит в default; в XSD наоборот —
fixed="V" несёт значение. Компилятор писал значение прямо в fixed, и платформа
отвергала пакет («Отсутствует фиксированное значение свойства»). Перевод сделан
в обе стороны; по принципу прощающего ввода принимается и модельная форма через
зеркало xdto:fixed. Отображение выведено по корпусу: fixed встречается только
вместе с default, значений всего два.
2. Импорт на несуществующий пакет платформа отвергает («xdto-package-3.3 …
не определен»), а у нас проверки не было. Добавлена ошибка валидатора и,
что важнее, предупреждение прямо на сборке — отказ при db-update дешевле
поймать на шаг раньше. Правило пришлось калибровать корпусом: сначала оно
дало 67 ложных срабатываний на платформенных пространствах имён, их список
выведен и исключён.
3. localName проверяется как NCName — фикстура с пробелом в имени была негодной,
заменена на реалистичный дефис (name="alpha_3" localName="alpha-3").
Харнесс получил skipPlatformVerify с обязательной причиной: результат
set-namespace невалиден by design, операция намеренно оставляет висящий импорт
у зависящего пакета.
Итог: 9/9 компилятора, 9/10 + 1 осознанный пропуск у edit, round-trip 760/760,
валидатор 0 ложных, 40 тестов на обоих рантаймах.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Харнесс платформенной верификации не знал про caseFiles — механизм файлового
входа кейса, добавленный в runner.mjs. Та же функция перенесена сюда,
xdto-compile и xdto-edit добавлены в список проверяемых навыков.
Первый прогон отвергает 4 кейса из 9 — разбор в debug/xdto/FINDINGS.md §15.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Рецепт для вложенного объекта учил окольной форме там, где она не нужна.
Именованный тип берётся так же, как корневой — ФабрикаXDTO.Тип(ns, имя);
через Свойства.Получить(...).Тип идут только к анонимному, у которого имени
нет. Теперь строка выдаётся по факту: для именованного одна форма, для
анонимного другая, и обе с настоящими именами из пакета.
Убрано утверждение «Узел = ФабрикаXDTO.Создать(ТипУзла, Значение)» для типа
со значением элемента. По синтакс-помощнику Создать(<Тип>, <Значение>)
принимает ТипЗначенияXDTO, а такой узел — объектный тип, то есть форма была
просто неверной. Вместо неё проверяемый факт: значение лежит в свойстве
__content.
Зато для типа значения эта форма как раз корректна, а рецепта там не было
вовсе — добавлен.
Попутно: строка-заглушка «(раскрыт выше)» создавалась без новых ключей, и
py-порт падал с KeyError там, где PowerShell молча возвращает $null на
отсутствующем свойстве. Ключи добавлены, доступ переведён на .get().
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Четырём субагентам выданы реалистичные задачи по песочнице, навыки в
формулировках не назывались. Разбор — в debug/xdto/FINDINGS.md, §13.
ГЛАВНОЕ — дефект компилятора. При уплощении вложенного xs:choice ветки
оставались обязательными: схема «самовывоз ИЛИ адрес доставки» давала пакет,
требующий заполнить оба, и ни один реальный документ в него не ложился.
Компилятор предупреждал о потере выбора, но молчал о последствии, а валидатор
показывал «0 ошибок, 0 предупреждений» — структурно пакет корректен,
семантически мёртв. Теперь ветки становятся необязательными (единственное
уплощение, оставляющее тип заполнимым), предупреждение называет их поимённо.
Корневой xs:choice не затронут: он отображается в ordered="false".
xdto-edit: -Value "@файл" по конвенции skd-edit — на кавычках при инлайновой
передаче XSD споткнулись двое агентов из четырёх, причём сырой LoadXml уводил
чинить схему вместо транспорта. При сбое разбора теперь понятное сообщение.
xdto-info: поиск, законно ничего не нашедший, падал throw'ом со стектрейсом и
читался как поломка инструмента — теперь строка и exit 1. Блок «Создание»
покрывал только корневой тип, хотя вся реальная работа в XDTO — вложенные и
анонимные типы; добавлены рецепты по факту наличия. Отсутствие раздела «Точки
входа» было неоднозначным — теперь явная строка. Новый -RequiredOnly даёт
скелет «заполни обязательное»: необязательный объект уходит вместе с поддеревом.
По умолчанию выключен — иначе список читался бы как полный.
xdto-validate: предупреждение про anyType описывало историю («платформа заменяет
при импорте»), хотя в файле уже зафиксирован anyType; сначала состояние, потом
происхождение.
Проверено: 40 тестов на обоих рантаймах, round-trip 760/760, валидатор
0 ложных срабатываний на корпусе.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Инструкция несла 14-строчный пример вывода — то самое, что модель увидит,
запустив навык, но читаемое при каждой загрузке инструкции. Та же логика,
по которой из xdto-validate убран каталог проверок.
Легенда при этом нужна: ← Имя, [значение элемента], · Пакет из вывода сами
не читаются. Поэтому она переехала в вывод и печатается только для тех
обозначений, которые в нём реально встретились — на плоском типе легенды
нет вовсе. В самом навыке уже был такой прецедент: режим списка пакетов
поясняет свои колонки прямо в выводе.
Инструкция сократилась с 99 до 77 строк.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Навык нужен ровно для одного: не втаскивать в контекст мегабайтную схему ради
одного поля. Чистота диффа тут ни при чём — она уже обеспечена round-trip'ом
(замер: правка двух вещей через decompile→compile даёт 3 изменённые строки из 241).
Поэтому edit не заводит второй эмиттер, а строится поверх round-trip'а: пакет
выгружается в XSD, операция применяется к схеме, пакет собирается обратно
компилятором. Байт-точность для нетронутого достаётся даром, а смена namespace
перегенерирует все объявления префиксов сама — в EnterpriseData_1_20_2 их 5280.
На лишний шаг (загрузка XSD в DOM и пересохранение) заведён отдельный харнесс:
холостая правка не меняет ни байта на всех 760 пакетах.
Операции: add/replace/remove-property, add/remove-type, add-enum, add-import,
rename, set-synonym, set-comment, set-namespace. Содержимое — всегда фрагмент
XSD, тем же языком, что в компиляторе; отдельных -MinOccurs нет, свойство
меняется целиком через replace-property. Адресация точкой, путь заходит внутрь
встроенных типов.
rename трогает три места (объект метаданных, имена файла и каталога, регистрацию
в Configuration.xml). set-namespace правит свой пакет и перечисляет зависящие,
но не меняет их: при версионировании они и должны смотреть на прежнее
пространство имён. После правки автоматически запускается xdto-validate.
Проверено загрузкой в базу 8.3.24: add-property, add-enum и set-namespace
переживают db-load-xml + db-update.
Попутные ловушки портирования (детали — debug/xdto/FINDINGS.md): пустой элемент
в lxml ложен, из-за чего "or"-цепочка создавала бы вторую частицу в типе
с пустой sequence; диапазон [Ѐ-ӿԀ-ӿ] валиден в .NET и не компилируется в Python.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Навык отвечает на вопрос «что присвоить и что обязательно», а не показывает
модель как есть. Типы переведены в нотацию 1С с учётом ограничений
(xs:decimal + totalDigits → Число(18,2)), псевдонимы развёрнуты со стрелкой
на исходное имя, кратность вынесена во флаги, для перечислимых типов выводятся
допустимые литералы. Различие атрибут/элемент в таблице свойств не показывается:
в коде 1С обращение одинаковое.
Флаг ставится на обязательные, хотя в модели XDTO умолчание обратное. Причина —
соседний meta-info, где непомеченный реквизит необязательный: один значок,
означающий в двух навыках противоположное, сам по себе источник ошибок.
Режимы: список пакетов конфигурации, состав пакета с точками входа, структура
типа с разузлованием на -Depth и used-by. Разузлование идёт через границы
пакетов с пометкой источника, анонимные типы раскрываются всегда, циклы
обрываются. Пакет адресуется путём, именем или namespace — последнее потому,
что модель приходит к задаче от строки ФабрикаXDTO.Тип(ns, имя), а не от имени
пакета в конфигурации.
Попутно закрыт баг паритета во всех четырёх py-портах: платформа допускает
в targetNamespace произвольную строку (в БП есть пакет с кириллическим
«ДопФайлУниверсальный»), .NET такое принимает, а libxml2 отвергает как
невалидный URI. Добавлено узкое отступление на восстанавливающий разбор —
только для этой ошибки, чтобы валидатор не перестал замечать битый XML.
Обнаружено это только потому, что корпус впервые прогнан на Python: раньше
все 760 гонялись лишь на PowerShell. Теперь 760/760 на обоих рантаймах.
Сортировка в PS переведена на ординальную: Sort-Object сортирует по культуре,
sorted() в Python — по кодам, и на смешанных латиница/кириллица имена
расходились бы.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xdto-compile терял свойства без единого слова: на реалистичной чужой схеме
из шести объявленных доезжало одно. Вложенные xs:sequence/xs:choice теперь
уплощаются (модель хранит плоский список), xs:all трактуется как
последовательность, xs:group и xs:attributeGroup раскрываются по ссылке —
и о каждом приближении навык пишет предупреждение. Молчаливая потеря — тот же
класс дефекта, что мы ловим у платформы, лечится так же: сообщением, не отказом.
xdto-validate получил проверки на грабли, найденные при разработке: порядок
элементов верхнего уровня (платформа отвергает пакет, не называя причины),
конфликты объявлений (name+ref, type+вложенный тип, тип без разновидности),
несовпадение рода базового типа, дубли имён свойств.
Новые правила прогнаны по всем 760 пакетам выгрузок: всё, что породила
платформа, валидно по определению, поэтому каждая ошибка там — ошибка правила.
Первый прогон дал 7, и все три класса оказались реальным поведением платформы:
length вместе с minLength/maxLength встречается, два пакета делят один
targetNamespace (Envelope и SOAP_Envelope_1_1 в БП), form="Text" называется
не только __content. Правила понижены до предупреждений либо сняты. Заодно
убран шум: предупреждение о неиспользуемом import срабатывало на четверти
корпуса — теперь только вместе с anyType, где оно и означает проблему.
Итог: 0 ошибок на корпусе, предупреждений 53 вместо 242.
Инструкции переписаны под читателя-исполнителя: убраны детали реализации
и наши мерки, каталог проверок валидатора (его вывод самодостаточен),
локальные пути в примерах заменены нейтральными. Таблица соответствий
XSD и справочник аннотаций вынесены в xdto-compile/xsd-reference.md.
Round-trip 760/760 сохранён, паритет PS/PY сохранён.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Три навыка для работы с пакетами XDTO. Формат описания — обычная XSD,
своего DSL нет: рутину снимает конвертер (локальные объявления префиксов
dNpM на каждой ссылке, инвертированная кратность lowerBound/upperBound,
фасеты атрибутами вместо дочерних элементов, обязательный порядок
элементов верхнего уровня). То, чего XSD выразить не может — nillable
у атрибута, qualified у свойства, «атрибут записан явно» — едет
атрибутами из пространства имён модели XDTO по правилу «то же имя,
что в Package.bin». Свойства объекта метаданных живут в xs:appinfo,
поэтому пара decompile → compile замыкается без потерь.
Инвариант bin → xsd → bin проверен побайтово на 760 пакетах выгрузок
Бухгалтерии и ERP 8.3.24 (харнесс debug/xdto/roundtrip-corpus.mjs).
Сборка из рукописной XSD проверена загрузкой в базу 8.3.24 — именно
она вскрыла обязательный порядок import→property→valueType→objectType,
невидимый для корпусной сверки: все выгрузки уже канонические.
xdto-validate ловит два класса тихих дефектов, которые платформа не
диагностирует: подмену неразрешённого чужого типа на xs:anyType при
импорте XML-схемы и nillable у свойства-атрибута, теряемый экспортом
схемы в Конфигураторе.
Тесты: 18 снэпшот-кейсов на синтетических схемах (типовые конфигурации
в репозиторий не тащим), паритет PS↔PY на общих эталонах. Раннер
получил caseFiles — копирование файлов кейса в workDir для навыков
с файловым, а не JSON входом.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Две проверки, обе про формат 2.20.
Проверка 18 — реестр versionedProps «тег → минимальная версия формата». Если
свойство присутствует в файле со слишком старым штампом, при сборке на платформе
той версии оно будет молча отброшено: платформа рапортует успех (exit 0), а
свойство теряется — проверено экспериментально на 8.3.24. Реестр расширяется
одной строкой на свойство и служит заделом под 2.21 (8.5) и последующие: он же
подсказывает, что конструкция требует более нового формата.
Проверка 19 — LineNumberLength вне диапазона 5..9 (границы из документации 1С).
Компаратор версий числовой по компонентам: строковое сравнение дало бы
"2.9" > "2.17".
Кейсы: error-lnl-out-of-range, error-220-props-in-217. Регресс 25/25 ps1+py.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Дельта формата 2.17→2.20 содержит три безусловных свойства, которых компилятор
не эмитил. Все три пишутся ТОЛЬКО при формате >= 2.20 (Detect-FormatVersion),
поэтому 2.17-проекты не меняются: полная сюита зелёная, ни один существующий
снэпшот не сдвинулся.
- xr:TypeReductionMode — каждому стандартному реквизиту, после CreateOnInput.
TransformValues, кроме Owner → Deny (правило проверено против выгрузки acc:
9 из 9 реквизитов совпали, включая Owner).
- TypeReductionMode — измерениям регистра СВЕДЕНИЙ (у прочих семейств и у
реквизитов/ресурсов платформа его не пишет).
- LineNumberLength — табличным частям, последним в Properties.
LineNumberLength — прикладная возможность 8.3.27 (5..9 → до 999 999 999 строк
вместо 99 999), поэтому получил полноценный DSL-ключ и описание в spec §5.2.
Его дефолт зависит НЕ от версии формата, а от режима совместимости на момент
создания ТЧ (<=8_3_26 → 5, >=8_3_27 → 9) — платформа фиксирует значение и позже
не пересчитывает, поэтому в одной конфигурации соседствуют ТЧ с 5 и 9. Отсюда
новая Detect-CompatibilityMode: читает CompatibilityMode из Configuration.xml
(префикс 64 КБ — тег лежит на ~11-12 КБ, существующим 2000 байт не хватает).
Декомпилятор: TypeReductionMode захватывается только при отклонении от правила
(компилятор выводит его сам), LineNumberLength — всегда при наличии тега:
выводить его дефолт значило бы дублировать логику компилятора с риском разойтись.
Компараторы версий числовые по компонентам — строковое сравнение неверно
("2.9" > "2.17" лексикографически).
Тест-инфра: setup-фикстуры empty-config-220 и empty-config-220-compat24
(строятся тем же cf-init), два кейса — по одному на каждую ось.
Проверка: роундтрип реального 2.20-документа БП (АвансовыйОтчет, 7 ТЧ) —
по новым тегам 0 расхождений, значения и позиции совпали; остаточный хвост
52/39 идентичен такому же на 2.17, то есть пред-существующий. Сюита 570/570
ps1, 567+3 skipped py, ps1==py. 1С-сертификация обоих кейсов на 8.3.27 ✓.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Таблица «Все поля кейса» отставала от раннера: не были описаны idempotent,
runtimeOnly, skipValidation, а expect ограничивался упоминанием files/
stdoutContains/stdoutNotContains — preserves и структура preRun не
документировались вовсе.
Из-за таких пробелов формат кейса приходится выяснять по коду — а это ровно
тот способ, который однажды дал 9 кейсов meta-edit с несуществующим ключом:
тесты зелёные, навык no-op, снэпшот фиксирует исходник.
Добавлено (сверено с runner.mjs и с реальными кейсами):
- idempotent, runtimeOnly, skipValidation в основную таблицу;
- таблица ключей expect + вложенная таблица preserves (file/bom/eol/encoding/
finalNewline/noCR13) с пометкой, что preserves и эталон дополняют друг друга:
первый следит за байтовым стилем, второй за структурой;
- формы шагов preRun (прогон навыка и writeFile).
editFile намеренно не описан — это шаг интеграционных тестов, не preRun кейса.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
compareSnapshot при отсутствии каталога эталона возвращал {match:true,
reason:'no snapshot (skipped)'}, причём reason никуда не выводился. Кейс без
эталона был молча зелёным, а «намеренно нет» и «эталон потерялся / не создан
при добавлении кейса» — неразличимы. README закреплял это как штатное
(«совпадает со snapshot (если есть)»).
Теперь эталон обязателен везде, кроме expectError, readonly external: и явного
opt-out. Диагностика — на месте кейса, с готовой командой; сводной статистики
не добавляем (вне контекста она ничего не сообщает).
- noSnapshot: "<причина>" — легальный пропуск. Причина обязательна: отключение
сверки должно стоить автору формулировки, а ревьюеру быть видно в diff'е;
осмысленность причины рантайм проверить не может. true/"" → падение.
- Нет эталона и нет opt-out → падение с рецептом (команда --update-snapshots
либо подсказка объявить noSnapshot).
- Мёртвый эталон (noSnapshot + существующий каталог) → падение: не сверяется,
но выглядит покрытием.
- updateSnapshot пропускает кейсы с noSnapshot — иначе --update-snapshots сам
порождал бы противоречие. Опечатка в имени поля fail-safe: opt-out не
сработает, кейс упадёт как «эталон отсутствует».
- Диагностика вынесена в общий snapshotErrors() — обе ветки (runCase /
runCaseAsync) больше не дублируют логику.
Размечены 3 кейса meta-validate: навык только читает и печатает, эталон
зафиксировал бы выход preRun (meta-compile), а не проверяемого навыка.
Проверка: до разметки сюита падала ровно на этих 3 кейсах (независимое
подтверждение аудита). Негативные сценарии проверены все пять: потерянный
эталон, мёртвый эталон, noSnapshot без причины, update на opt-out кейсе
(не создаёт), update на обычном (создаёт байт-в-байт прежний).
Полная сюита 566/566 ps1; python 563 passed + 3 skipped — идентично HEAD.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Кейсы проверяли только БАЙТОВЫЙ стиль файла (expect.preserves: BOM/CRLF/
encoding/finalNewline/noCR13) и что валидатор не ругнулся. Что в CRLF-файл
записан КОРРЕКТНЫЙ XML, не проверял никто: снэпшота не было, а
compareSnapshot при отсутствии эталона молча возвращает pass.
Снэпшот ортогонален preserves — сравнивается нормализованное содержимое
(структура), preserves остаётся на байтовых характеристиках. Дублирования нет.
Регресс 12/12, 20/20, 6/6 — ps1 и py. 1С-сертификация 3/3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Значения xsi:type="xr:MDObjectRef" не проверялись вообще. Ошибка «тип ссылки
вместо объекта метаданных» обнаруживалась только платформой при загрузке
(«Неизвестный объект метаданных»), причём в логе, а не в коде возврата.
Проверка 17 по первому сегменту пути (переиспользован $validTypes +
$structuralOnlyTypes):
- сегмент оканчивается на Ref → Error: вида метаданных с таким именем
не существует, ссылка гарантированно нерабочая; в тексте подсказана
исправленная форма;
- неизвестный сегмент без Ref → Warn (список видов может быть неполон).
Ловит дефект статически, без платформы, независимо от происхождения файла.
Кейс error-mdobjectref-type-form + фикстура. Регресс 23/23 ps1+py.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Та же дыра, что и в meta-compile, но здесь нормализации не было вообще —
Normalize-MDObjectRef отсутствовала как функция. set-owners "CatalogRef.Валюты"
(или modify.properties.Owners) записывал неверную ссылку молча.
- Перенесена мапа корней + Normalize-MDObjectRef (зеркало meta-compile).
- В complexPropertyMap добавлены флаги mdref/root; нормализация подключена
в Add-/Remove-/Set-ComplexPropertyItem рядом с существующим expand.
Покрывает Owners, RegisterRecords, BasedOn, RegisteredDocuments.
- References графы журнала документов — эмитились напрямую, тоже нормализуются.
Инструкция навыка не менялась: SKILL.md и json-dsl.md уже показывают
каноническую форму Catalog.Контрагенты.
Кейс modify-property-mdobjectref (документированный путь modify.properties).
Регресс 20/20 ps1+py, 1С-сертификация снэпшота пройдена.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MDObjectRef ссылается на ОБЪЕКТ метаданных (Catalog.Валюты), а не на тип ссылки.
Эмиттеры пропускали значение как есть, если в нём была точка, поэтому
"CatalogRef.Валюты" доходил до XML без изменений → при загрузке конфигурации
платформа отвечала «Неизвестный объект метаданных».
Инструкция вела в баг сама: reference/catalog.md документировал
owners: ["CatalogRef.Контрагенты"]. Тестами не ловилось — все кейсы
использовали каноническую форму.
- Normalize-MDObjectRef расширена ссылочными формами (англ. *Ref + рус. *Ссылка);
вида метаданных, оканчивающегося на Ref, не существует → схлопывание однозначно.
В ТИПАХ реквизитов запись CatalogRef.X верна — там мапа не применяется.
- Добавлен параметр defaultRoot (голое имя без точки), инлайн-подстановка
"Catalog.$ownerRef" в owners убрана — логика теперь в одном месте.
- Нормализация применена в 4 местах, где её не было: owners, basedOn,
registerRecords, baseCalculationTypes.
- Кейс catalog-inputbystring-datalock переведён на неканонический вход:
снэпшот не изменился ни на байт — прямое доказательство нормализации.
Регресс 73/73 ps1+py, полная сюита 566/566. Живая проверка на 8.3.27:
подчинённый справочник с owners CatalogRef./СправочникСсылка. грузится чисто.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>