py-порт в batch-режиме запускал сам себя через subprocess на каждый объект, и на разбор в единицы
миллисекунд приходилось ~146 мс старта интерпретатора с импортом lxml. На корпусном прогоне это
давало 92 мс на объект против 40 мс у PS — при том, что обычно py быстрее в разы. Причина
асимметрии: PS вызывает себя через & в том же процессе, платя свой дорогой старт один раз на батч.
Теперь py делает то же самое: файл компилируется один раз и выполняется в свежем globals() на
каждый объект. Изоляция состояния получается той же, что у & в PowerShell (script-переменные не
переживают вызов), а старт и компиляция платятся однократно.
Прогон по acc_8.3.24 и erp_8.3.24 (33 895 объектов): 274 c против ~2900 c, вывод побайтово тот же
и совпадает с PS-портом строка в строку. Батч с битым объектом в середине по-прежнему доходит до
конца и возвращает ненулевой код, -OutFile пишет те же файлы на объект.
Заодно уточнён комментарий про кэш состава конфигурации: «порождает процесс на объект» было верно
только для py, тогда как в PS кэш не переживает объект из-за новой области видимости.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Оба исключения по IntegrationService стояли с пустой причиной, а такое гард лишь помечает как долг
и продолжает. Именно так вид метаданных и выпал незамеченным: meta-validate не знал его вовсе и
отвергал штатный объект типовой конфигурации.
Причины проставлены по смыслу своих записей: meta-compile такие объекты не создаёт, meta-validate
проверяет их структурно. Долг исключений без причины закрыт.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Прогон по acc_8.3.24 и erp_8.3.24 (33 895 объектов) давал 246 ERROR и 1662 WARN на выгрузках,
которые платформа принимает без единой ошибки, — то есть ложным был весь вывод валидатора.
Две группы при этом роняли exit=1 на типовой конфигурации: то же, из-за чего заводились #79 и #84,
только зеркально.
Разобрано платформой на стенде 8.3.24.1691, каждый случай — копия эталона с одной мутацией:
- пустой <Type/> у реквизита — это тип «Произвольный»; загрузка проходит, round-trip сохраняет
конструкцию без изменений. Было ERROR (245 раз), стало молчание;
- блока Type нет вовсе — загрузка тоже проходит, но платформа молча подставляет тип нового
реквизита, Строка(10). Замысел теряется, загрузка нет: ERROR заменён на WARN;
- IntegrationService и Bot отсутствовали в списках видов метаданных (в спеке 45 типов, навык знал
43) — штатный объект типовой давал «Unrecognized metadata type» и exit=1;
- HierarchyType платформа пишет всегда и при выключенной иерархии игнорирует: из 1254 срабатываний
1253 приходились на её собственный дефолт. Предупреждаем только о явно заданном другом типе;
- источник подписки задают и наборами типов (<v8:TypeSet>), а состав журнала — элементами
<xr:Item>; проверки искали только <v8:Type> и потому не видели ни одной такой подписки и ни
одного журнала. Зато реально пустой источник и пустой состав платформа отвергает
(«Источник событий должен быть задан», «Для журнала не заданы регистрируемые документы») —
там уровень поднят с WARN до ERROR;
- сигнатура процедуры переносится на несколько строк, и Экспорт оказывается не на строке с именем;
регулярка требовала одну строку. Модуль поставщика в двоичном виде (Module.bin) больше не даёт
предупреждения «файл не найден» — текста там нет by design;
- ExchangeDate — легаси-реквизит планов обмена, поддержан в meta-compile и meta-decompile:
добавлен как допустимый, но не обязательный.
После правки корпус даёт 0 ERROR и 1 WARN — тот самый единственный недефолтный тип иерархии.
Объём проверки не потерян: объектов провалидировано 33 895 против 33 894.
Восемь новых кейсов держат обе стороны каждой правки: валидная конструкция молчит, настоящая
ошибка по-прежнему ловится.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Таблица параметров и примеры в SKILL.md описывали слэш-нотацию (--set-default, --purpose),
а скрипт принимает -SetDefault и -Purpose. Модель, читающая таблицу, получала отказ биндинга
на первом же вызове. Документация приведена к исполняемой форме — она теперь одна.
Заодно навык принимает обиходные написания молча: назначение формы русским названием вида
формы или английским с суффиксом Form (регистр, пробелы и разделители не значимы) и флаг
основной формы с дефисом внутри имени. Допустимость назначения для вида объекта по-прежнему
проверяется — неизвестное значение отвергается со списком доступных.
Порты выровнены: PS принимает это через алиас параметра, py — через перечисление опций;
проверено девятью написаниями на обоих, результат совпадает.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Оба назывались «корректными», но объект создавался со ссылкой на CatalogRef.Валюты, которого
в конфигурации нет — платформа такую выгрузку не принимает. Пока уровень был WARN, кейсы
проходили и закрепляли неверное: «валидный» объект с висячей ссылкой.
Справочник добавлен в preRun, эталоны пересняты и проверены платформой.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Проверки 16 и 20 выбирали уровень по наличию файла, тогда как платформа судит по составу
конфигурации. Из-за этого валидатор ошибался в обе стороны: пропускал ссылку на форму
объекта, которого в конфигурации нет (WARN, exit=0 — платформа такую выгрузку не грузит),
и ложно отказывал на фрагменте частичной выгрузки, где файла формы нет намеренно.
Единое правило: ChildObjects отвечает за существование объекта, наличие файла — только за
полноту выгрузки. Нет в составе -> ERROR, в составе есть, а файла нет -> WARN. Уровень для
фрагмента остаётся мягким сознательно: один и тот же фрагмент грузится или нет в зависимости
от состояния конфигурации БД, которого валидатору не видно.
Матрица снята на платформе 8.3.24.1691 (полная и частичная загрузка, семь ситуаций).
Прогноз про загрузку убран из текста WARN — там он не гарантирован.
Состав читается поиском подстроки в границах секции ChildObjects, без разбора XML:
batch-режим порождает процесс на объект, и полная карта оплачивалась бы каждым объектом.
Корпусный прогон по acc_8.3.24 и erp_8.3.24 (33 895 объектов): расхождений до/после нет.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Значение с пробелом без кавычек разрывалось на токены, и лишние молча растекались по свободным
позиционным параметрам. На документированной форме вызова (powershell -NoProfile -File):
form-compile.ps1 -JsonPath a.json -OutputPath b.xml -Purpose Форма списка
→ Purpose=[Форма] ObjectPath=[списка], ошибки НЕТ
Навык отрабатывал успешно с усечённым значением; реальный триггер — путь с пробелом в -EmitDsl
или -ObjectPath, результат уезжал не туда молча. У навыков с парой -DefinitionFile/-Operation
осколок уезжал в -DefinitionFile, и навык жаловался на параметр, которого в команде не было.
Расхождение портов: py на том же вызове отвечает «unrecognized arguments: списка» — там все
аргументы объявлены опциями. Правка выравнивает PS по py, python не менялся.
41 навык получает [CmdletBinding(PositionalBinding=$false)] и ни одного позиционного параметра.
Без Position=0 (в отличие от read-only навыков): «главный» параметр механически не выводится —
у meta-edit первым объявлен -DefinitionFile, а путь к объекту вторым, — а ошибка выбора тестами
не ловится, раннер передаёт только именованные флаги. Позиционной формы вызова нет ни в одной из
140 строк SKILL.md, внутренние вызовы навыков друг из друга тоже именованные.
check-positional-binding.mjs расширен на пишущие навыки статической проверкой. Поведенческая
(канареечный файл) остаётся только у read-only: у web-stop и db-run все параметры
Mandatory=$false, связывание прошло бы и тело выполнилось — гард остановил бы Apache и запустил 1С.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Часть навыков проверяла путь ко входному JSON сама и отвечала внятной строкой, часть — нет и
роняла дамп: FileNotFoundError с traceback в py, MethodInvocationException с CategoryInfo в PS1.
Один и тот же промах давал разный ответ в зависимости от навыка, а у form-edit — ещё и от порта.
Проверка перенесена в тело семьи read_json_file / Read-JsonInputFile: пол одинаков везде по
построению, и новые навыки получают его вместе с функцией. Навыки со своей проверкой срабатывают
раньше и сохраняют прежний текст, поэтому существующие кейсы не двигаются.
Заодно каталог вместо файла: раньше IsADirectoryError / отказ доступа, теперь
«Expected a JSON file, got a directory».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Этажом ниже разбора, на чтении файла, жил тот же класс дефектов в худшей форме. Файл в cp1251
с кириллицей: PS1 `Get-Content -Encoding UTF8` менял имя на 12 символов U+FFFD, JSON после этого
разбирался УСПЕШНО, и навык создавал объект с именем из «замен» — молча. Py-порт на том же файле
падал traceback-ом. Файл в UTF-16 давал ту же пару: traceback против ложного «JSON must have
'type' field».
Новая семья read_json_file / Read-JsonInputFile: кодировка берётся из BOM (UTF-8, UTF-16 LE/BE),
без BOM — строгий UTF-8, при провале сообщение называет файл и байт. Кодовую страницу не
подбираем: угаданное имя уехало бы в метаданные так же молча.
Эхо полученного значения печатается теперь только для inline-входа. Для файла оно показывало
первые 60 символов первой строки независимо от того, что ошибка на 120-й, и спорило с позицией
от парсера.
Заодно уравнен пустой вход: PS 5.1 на пустой строке отдаёт $null, а не ошибку, и навык уходил
дальше с $null, тогда как py-порт падал.
Раннеру добавлен ключ inputEncoding (utf-16le / utf-16be / cp1251) — иначе кейс про кодировку
не выразить, writeFileSync пишет только UTF-8.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Эхо полученного значения обрезалось многоточием, и обрыв читался как обрыв самих данных:
агент шёл дописывать «неполный» файл, хотя ошибка была на 57-й строке. Плюс усечение спорило
с позицией от парсера — два сигнала об одном месте, которые не сходятся.
Теперь усечение называет себя: got (first 60 chars, whitespace collapsed). Число символов
не печатаем — после схлопывания пробелов оно не сходилось со смещением из сообщения парсера.
На коротком входе, ради которого эхо и заводилось (съеденные оболочкой кавычки дают
{group:X,commands:[Y]} — 22 символа), усечения нет вовсе.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Неверный входной JSON ронял скрипты необработанным исключением: PS1 отдавал дамп
ConvertFrom-Json с CategoryInfo, py-порт — traceback с внутренностями json/decoder.py.
Имя файла в сообщении не фигурировало, а для полиморфного -Value не было видно, какую
форму ждёт операция.
Общий хелпер ConvertFrom-JsonInput / parse_json_input в 12 навыках × 2 порта (24 места),
зарегистрирован семьёй в check-inline-drift.mjs — копии держит гард. Сообщение в одну
строку: ожидаемая форма, полученное значение, текст парсера в скобках.
Эхо полученного значения нужно потому, что съеденные оболочкой кавычки дают почти тот же
JSON ({group:X} вместо {"group":"X"}), и без него агент считает свой вызов верным. PS 5.1
печатает лишь огрызок и локализованно, Python — только номер колонки.
Возврат в PS1 через Write-Output -NoEnumerate: вынос разбора в функцию добавляет второй
анруллинг, и одноэлементный JSON-массив стал бы скаляром. Импорты внутри тела py-хелпера —
skd-decompile импортирует json локально как _json, а тело семьи обязано быть одинаковым.
В раннер добавлен inputRaw (запись входного файла дословно): через case.input битый JSON
невыразим, JSON.stringify всегда даёт валидный документ.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Вид искался как первое совпадение //md:<Вид> по всему файлу и потому зависел
от порядка перебора видов. У бизнес-процесса есть свойство <Task>, и объект
определялся как задача, после чего имя не находилось вовсе: «Не удалось
определить имя объекта из Properties/Name». Прежний упорядоченный список видов
это маскировал — в нём BusinessProcess стоял раньше Task.
Теперь вид — первый элемент-потомок MetaDataObject, и неподдерживаемый вид
отвергается по имени, а не проваливается в поиск имени объекта.
Нашлось платформенной проверкой всех видов разом: собрана конфигурация с
формой каждого поддержанного вида (15 видов, 43 формы) и загружена в 1С.
Заодно покрытие назначений доведено до полного (добавились FolderChoice, Load,
Record), а мелкие кейсы на один объект объединены: несколько назначений в одном
кейсе, эталон показывает все слоты сразу.
Гард дополнен инвариантом «ровно одна основная форма на вид» и его паритетом
между портами — именно им пользуется умолчание -Purpose.
Инструкция: в таблице параметров умолчание Purpose исправлено с Object на
основную форму вида.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пропуск по версии платформы гасит проверку: кривую фикстуру он тоже спрячет,
если её формат объявлен выше платформы стенда. Обычный прогон теперь прямо
говорит, что пропуски не проверены, а на стенде с нужными платформами
--strict не оставляет непроверенных кейсов.
Опечатка в версии формата при этом не маскируется в любом режиме: версии вне
лестницы в таблице соответствия нет, пропуск не срабатывает, и кейс доезжает
до платформы.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пропуск кейса решался только режимом совместимости. Кейс на формате 2.21 с
совместимостью 8.3.27 на платформе 8.3.24 пропускался, а на 8.3.27 доходил до
загрузки и падал: формат 2.21 требует 8.5. На стендах с промежуточной
платформой (в том числе macOS с 8.3.27) это давало постоянный ложный красный.
Эталон соответствия «формат → платформа» — таблица «Лестница версий» из
docs/1c-configuration-spec.md, как и в check-format-versions.mjs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Десять кейсов на то, что раньше не проверялось вовсе: журнал документов
(список и отказ для формы объекта), перечисление, план видов расчёта, наборы
записей регистров, форма группы справочника, произвольная форма, формы
хранилища настроек и отказ для константы. Дыра в покрытии и позволила дефекту
дожить: журналов и регистров среди кейсов не было.
Пять чужих кейсов (form-compile-from-object, cfe-validate) звали form-add без
-Purpose для регистров и полагались на умолчание Object — назначение им
проставлено явно. Их эталоны пересняты: слот основной формы, который раньше
молча оставался пустым, теперь заполняется. Эталоны проверены загрузкой в 1С.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Шаг preRun deletePath поддерживал только runner.mjs. В платформенной проверке
такой шаг проваливался в ветку запуска скрипта и падал на step.script.split,
то есть кейс не проверялся вовсе, а выглядел упавшим инструментом.
Шаг без script/writeFile/deletePath теперь отвергается с внятным текстом, а не
падает на undefined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Держит три инварианта form-add: состав видов сходится со спецификацией,
свойство «основная форма» у назначения есть у этого вида по спецификации,
таблицы PS и PY совпадают между собой.
Проверено на регрессии: если вернуть журналу DefaultListForm, гард падает
дважды — по спецификации и по паритету портов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Для журнала документов навык писал в форму битый главный реквизит —
cfg:.Журнал с пустым префиксом. Платформа такую выгрузку не принимает
(«Исключение XDTO при чтении файла»), а навык рапортовал успех.
Причина не в забытой строке: знание о видах было размазано по четырём
независимым спискам (поддерживаемые типы, объектные, обработко-подобные,
карта типов реквизита) плюс switch по свойствам DefaultForm. DocumentJournal
попал в поддерживаемые, но не в карту типов — и подстановка $null дала точку
без типа. Теперь одна плоская таблица: строка на (вид, назначение), в ней тип
главного реквизита и свойство «основная форма».
Того же корня и починено заодно:
- Purpose=Object принимался для видов, у которых формы объекта не бывает
(журнал, перечисление, регистры накопления и бухгалтерии);
- свойство «основная форма» выбиралось без учёта вида: журналу писался
DefaultListForm, которого у него нет, и слот молча не находился;
- AccumulationRegister описывался как RecordSet, хотя у регистра такого
главного реквизита не бывает, а слота под него нет вовсе;
- ChartOfCalculationTypes отвергался, хотя формы объекта у него обычные.
Новое: назначения Folder и FolderChoice (формы группы), RecordSet для всех
регистров, Save/Load для хранилища настроек и Custom — произвольная форма без
главного реквизита, самая частая в типовых после объектной. Умолчание Purpose
теперь основная форма вида, а не жёсткий Object: для регистра сведений это
форма записи, для журнала — форма списка.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Таблица «Свойства DefaultForm по типам объектов» была неполной: в ней не было
журнала документов, перечисления, регистров накопления/бухгалтерии/расчёта,
плана видов расчёта, критерия отбора, хранилища настроек и внешних объектов.
Сверять код было не с чем, и код оказался неполон ровно так же.
Добавлена вторая таблица — главный реквизит формы по назначению. Она разводит
случаи, которые легко перепутать: форма группы это форма ОБЪЕКТА группы, а
форма выбора группы — динамический список; у формы набора записей свойства,
чтобы назначить её основной, нет вовсе; форма без главного реквизита
(«произвольная») — обычное дело, а не полуфабрикат.
Источник — выгрузки типовых: слоты и типы главного реквизита сняты по
конфигурациям acc, erp, ut, unf.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
form-remove: удаление заблокировано ссылками; -Force чистит слот реквизита,
ChoiceForm внутри формы и элемент начальной страницы; регресс на матч
ссылки — удаление своей формы не трогает ссылку на чужую форму с тем же
именем.
meta-remove: общая форма, на которую смотрит Configuration.xml, — ссылка
попадает в отчёт, -Force обнуляет слот.
meta-validate: незарегистрированная форма и отсутствующий файл формы дают
ошибку, GUID в слоте — нет.
Эталоны проверены платформой: verify-snapshots поднимает базу и грузит
результат.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
verify-snapshots отбирал кейсы по правилу «нет input, preRun и params →
read-only, пропускаем». Кейс на `setup: fixture:` под него подходил, хотя
навык правит скопированную фикстуру, — и молча выпадал из проверки, ради
которой инструмент и существует. Отчёт при этом оставался зелёным.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Непустое Default*Form / Auxiliary*Form / ChoiceForm могло указывать на
форму, которой нет, а валидатор отвечал OK. Платформа такую выгрузку не
принимает: «Неизвестный объект метаданных».
Новая проверка разбирает обе формы записи. Для своего объекта проверяется
регистрация формы в ChildObjects — это работает и на одиночном файле, без
конфигурации; для общей формы и чужого объекта (форма журнала документов в
слоте документа) нужна конфигурация, и проверяется наличие файла формы.
Отдельно ловится случай, когда форма зарегистрирована, а файла нет.
Значением слота бывает GUID — такие пропускаются, как и в cf-validate.
Заимствованные объекты расширения пропускаются целиком: их формы живут в
основной конфигурации.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Файл целиком исключался из проверки ссылок как «чистится автоматически»,
хотя автоматически в нём чистится только ChildObjects. Ссылки уровня
конфигурации (DefaultReportForm и соседи) после удаления общей формы
оставались висеть и в отчёте не упоминались.
Теперь такие слоты видны в списке ссылок, а с -Force очищаются — наравне со
слотами других объектов и элементами начальной страницы. Ссылки на типы и
вызовы в .bsl по-прежнему не трогаем: чем их заменить, неизвестно.
Два полных обхода конфигурации свёрнуты в один, Get-ChildItem -Recurse
заменён на Directory.EnumerateFiles: на большой конфигурации проход занимал
180 с против 47 с, а проходов было два.
Общие паттерны ищутся в тексте без form-слотов — иначе файл со слотом
попадал в список дважды, а пропуск файла целиком спрятал бы настоящую
ссылку рядом со слотом.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Форма могла удаляться, оставляя ссылки на себя в других объектах: платформа
затем отвергала загрузку с «Неизвестный объект метаданных». Навык правил
только файл своего объекта и про остальную конфигурацию не знал.
Теперь перед удалением конфигурация сканируется на ссылки на эту форму
(слоты Default*/Auxiliary*Form и ChoiceForm, ChoiceForm и SettingsStorage
внутри форм, элементы начальной страницы). Есть ссылки — удаление
останавливается со списком мест; с -Force форма удаляется, а ссылки
очищаются: слот пустеет, элемент начальной страницы вырезается.
Заодно починен матч ссылки: сравнение шло по хвосту «Form.<Имя>» без вида и
объекта, поэтому при удалении своей ФормаСписка обнулялась и ссылка на
DocumentJournal.Ж.Form.ФормаСписка в том же файле.
Сравнение путей переведено на длинную форму: Resolve-Path отдаёт короткое имя
8.3, перечисление файлов — длинное, из-за чего собственный файл объекта не
исключался из скана.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три файла из 77 навыков нарушали соглашение о переносимости, и сборка
Codex-порта уносила их as is (#75).
Функциональный из трёх один: json-dsl.md писал путь к скрипту литералом
.claude/skills/meta-edit/..., то есть на любой платформе кроме claude-code
команда указывала в несуществующий каталог и молча не находила скрипт.
Заменено на ${CLAUDE_SKILL_DIR}/ — дальше путь разворачивает switch.py.
Ещё два — тексты: проза с названием агента в mxl-compile (заодно избыточная:
у соседних *-compile это одна строка «принимает X → генерирует Y») и
.claude-путь в комментарии state.mjs.
От регресса — check-agent-portability.mjs: вырезает разрешённый
${CLAUDE_SKILL_DIR}/ и падает на любом оставшемся claude. Ловит и плейсхолдер
без завершающего слеша: switch.py разворачивает только форму со слешем,
остальные проехали бы насквозь. Сторонний node_modules исключён — playwright
содержит ClaudeGenerator и .claude/agents, переписывать его нельзя.
switch.py не менялся: ни одного плейсхолдера и ни одного вызова скрипта вне
*.md верхнего уровня навыка нет, рекурсия не нужна.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
При пересборке поля навык сохранял <valueType> через OuterXml/tostring и срезал
из фрагмента ВСЕ объявления namespace. Для префиксов, объявленных в корне схемы,
это верно — DOM переобъявляет их на фрагменте избыточно. Но объявление, которого
в корне нет, значимо: xmlns:d5p1 живёт только локально на <v8:Type> и связывает
префикс, которым квалифицировано значение узла (d5p1:CatalogRef.X). Срез оставлял
висячий префикс — XML остаётся well-formed (префикс в тексте парсер не проверяет),
а 1С отвергает тип. Радиус — modify-field.
Слепой срез заменён на Strip-InheritedXmlns / strip_inherited_xmlns: объявление
выбрасывается, только если корень объявляет тот же префикс с тем же URI. Карта
корня снимается с уже имевшегося RawRootOpening; разделитель \s+, поскольку
корневой тег бывает разложен по строкам. Префикс и URI сравниваются ординально —
в PS -eq и @{} регистронезависимы. Подставлено во все пять точек среза каждого
порта; для корневых префиксов поведение не меняется.
Кейс modify-field-ref-type: modify-field по ref-полю не был покрыт. Проверено,
что кейс краснеет до правки — и по снэпшоту, и по skd-validate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Три навыка делали одну работу тремя инлайновыми копиями в main(), и одна копия
молча разъехалась: role-compile.py рапортовал об успехе, не тронув файл. Это
ровно тот класс, от которого заведён check-inline-drift.mjs, но в реестр
регистрацию было не записать — он работает по именованным функциям.
Регистрация вынесена в Register-InChildObjects / register_in_childobjects
с контрактом (родительский XML, тег родителя, тег потомка, имя) и исходом
added | already | no-childobj | no-config. Печать сообщений осталась на
вызывающей стороне: тексты у навыков разные, и сведение их меняло бы вывод.
Эталон — meta-compile, форма функции задана на .ps1. role-compile берёт его
тело копией, и гард это подтверждает. subsystem-compile идёт отдельным
вариантом с обоснованием: родителем бывает вложенный Subsystem.xml
произвольной глубины, поэтому отступ он берёт из документа, а запись
дописывает в конец блока — фиксированные три табуляции там неверны,
и группировать по типу нечего.
Вынос поведенчески нейтрален: 837 кейсов зелёные на обоих рантаймах,
снэпшоты не дрейфовали, матрица «навык × порт × изломанная форма
Configuration.xml» даёт те же исходы, а порты — байт-в-байт одинаковый файл.
Попутно из subsystem-compile.py убран мёртвый ET.SubElement перед pass:
дерево мутировалось и выбрасывалось, правка идёт по сырому тексту.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Configuration.xml без <ChildObjects> и с самозакрытым <ChildObjects/> — в обеих
формах py печатал «<Role>X</Role> added to ChildObjects», а файл оставался
байт-в-байт прежним. Модель считала роль включённой в состав конфигурации,
хотя её там не было. PS-порт обе формы обрабатывал верно: самозакрытый тег
раскрывал, при отсутствующем честно предупреждал.
Ветвление перенесено из meta-compile.py, где та же задача давно решена: есть
блок ChildObjects — вставляем внутрь; есть самозакрытый тег — раскрываем первой
записью; нет ни того ни другого — исход no-childobj и файл не трогаем. Ветка
вывода no-childobj в py уже была и до сих пор оставалась недостижимой.
Порядок вставки и способ правки файла не менялись: проверка на боевых выгрузках
(acc/erp, CRLF и LF) показала, что действующая реализация даёт дельту ровно
в одну вставленную строку, а платформа порядок ChildObjects не нормализует.
Кейсы на обе формы. На неисправленном коде оба падают на python и проходят
на powershell. Проверка «успех не объявлен» опирается на stdoutNotContains
и снапшот: Write-Warning в PS 5.1 идёт в stdout с локализованным префиксом,
а py пишет в stderr, поэтому портируемого stderrContains тут быть не может.
Co-Authored-By: Sergei Pleshanov <2357qwr@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Поведенческая секция считала успехом любой ненулевой код возврата, а не
запустившийся интерпретатор даёт status: null — проверка проходила вакуумно,
ничего не проверив. Теперь spawn-ошибка сама является нарушением.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Тот же класс дефекта, что закрыт в role-validate, был возможен во всей семье
*-info / *-validate / cfe-diff: лишний позиционный аргумент связывается со
следующим параметром по порядку объявления. Остальные скрипты спасала
случайность типов — на втором слоте оказался [int]MaxErrors/Limit или строка
с [ValidateSet], которые падают на конвертации. Любая перестановка параметров
в param() открывала дыру заново, а снапшот-тесты сверяют вывод, а не связывание.
Во всех 21 скрипте объявлен [CmdletBinding(PositionalBinding=$false)], входной
путь помечен Position=0. Инвариант: анализирующий навык не пишет в файл, который
ему не назвали по имени. Документированные вызовы в репозитории все именованные,
поведение навыков не меняется — 835 кейсов зелёные на обоих рантаймах без дрейфа
снэпшотов.
Гард check-positional-binding.mjs держит инвариант статически (объявление и
единственный Position=0; в py-порте все add_argument именованные) и поведенчески
(лишний позиционный аргумент роняет вызов, канареечный файл остаётся цел).
Семья определяется по имени навыка, поэтому новый *-info/*-validate попадает под
гард сам. На состоянии до правки гард падает на role-validate обеими проверками.
Версии обоих портов подняты синхронно; из шапок убраны чужие хвосты.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Вызов role-validate.ps1 с двумя позиционными аргументами — естественная ошибка
по документации role-compile/SKILL.md, где значилось
`/role-validate <RightsPath> [MetadataPath]`, хотя параметра MetadataPath у
навыка нет. Второй аргумент связывался с -OutFile, и скрипт перезаписывал
указанный файл (например, Roles/Имя.xml) текстом отчёта — исходник роли терялся.
PositionalBinding=$false оставляет позиционным только путь ко входу; -OutFile
передаётся строго по имени. Справка приведена к факту. Python-порт уже иммунен:
argparse принимает только именованные параметры.
Co-Authored-By: Sergei Pleshanov <2357qwr@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Разбор довели до механизма. Сборка не разрешает поля динамического списка заимствованной
формы в авто-режиме (ManualQuery=false + MainTable). Проверено и отброшено: порт скрипта,
ОС, имена латиницей, заимствованность таблицы, порядок загрузки, BaseForm/UseAlways/Settings,
полный эталонный вид с ListSettings и Список.Ref. Проходит только ручной запрос, что меняет
семантику списка.
Решающий факт: ту же базу, где расширение валидно загружено платформой 8.3.24, сборка 1688
выгружает с битыми путями (~Список.Склад) — то есть не разрешает их даже из собственного
хранилища. Наш эмит при этом совпадает с эталоном Конфигуратора (upload/cfe/ut/ФормаСписка2).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Кейсы form-main-attr-dynamic-list и form-main-attr-list-query падали на маке. Диагностика:
дело не в порте (Windows+python проходит) и не в ОС (мак на 8.3.24.1691 проходит) — отвергает
конкретная сборка 8.3.27.1688. Решающий опыт: расширение, загруженное на 8.3.24 и выгруженное
самой платформой, сборка 1688 тоже отвергает с теми же «Неверный путь к данным» — то есть она
не принимает собственный артефакт Конфигуратора, а не наш эмит.
Глухой skipPlatformVerify снял бы проверку и на стендах, где она работает, поэтому он теперь
принимает объект {reason, platforms:[…]} — пропуск только на перечисленных сборках.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Консольная сводка пропуски различала, а файл отчёта нет: статус считался как
`passed ? OK : FAIL`, поэтому пропущенный кейс печатался как FAIL, попадал в счётчик
падений и в раздел Findings. Отчёт читают глазами и по нему решают, есть ли проблема, —
расхождение с консолью здесь дороже всего.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ps1 печатает строку ошибки в stdout, py — в stderr, а лог платформы оба кладут в stdout.
Верификатор брал `stderr || stdout`, поэтому на python-порте лог терялся целиком: падение
выглядело как «Error loading configuration (code: 1)» без причины, и распознать по нему
неподдерживаемую версию формата было нечем — кейсы format-221 на маке падали вместо пропуска.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Кейсы клали расширение в `ext`, а cf-init создаёт в корне конфигурации платформенный `Ext/`.
На регистронезависимой ФС (Windows, APFS) это один каталог: эталоны годами фиксировали
слипшееся дерево — в snapshots/<case>/ext/ лежали вперемешку ClientApplicationInterface.xml
конфигурации и Configuration.xml расширения, а имя каталога в коммите зависело от того, кто
создал его первым (ext у одних кейсов, Ext у других при одинаковом входе). На Linux те же
кейсы дали бы два каталога и другой снапшот.
Переименование механическое: 64 кейса пяти навыков, 349 файлов эталонов переехали без
единой правки содержимого. Заодно переписан регистр 21 пути в индексе — git с core.ignorecase
не показывал, что файл конфигурации числится под ext/, а лежит в Ext/.
Гард в runner.mjs: имя каталога расширения из params, совпавшее без учёта регистра с тем, что
уже есть в фикстуре, валит кейс с объяснением. Проверено, что старое имя теперь не проходит.
verify-snapshots.mjs чинится тем же заходом — там нашлись две дыры:
- каталог расширения был захардкожен как 'ext', поэтому кейсы с другим именем МОЛЧА теряли
вторую половину проверки: блок загрузки расширения обходился по existsSync и в отчёте это
выглядело успехом;
- setup брался только из _skill.json, и кейсы с гейтом по версии формата (218/220/221)
проверялись на конфигурации 2.17 — то есть платформа не видела ровно того поведения, ради
которого кейс написан.
Плюс два honest-skip вместо ложных падений: режим совместимости выше платформы и формат
выгрузки новее платформы (второе читаем из ответа платформы, а не дублируем лестницу версий).
Проверено: регресс 827/0/8 (ps1) и 824/0/11 (py), гарды 5/5. Платформенная верификация:
cfe-borrow 26/28 на 8.3.27, cfe-patch-method 22/23, cfe-init 6/7, все 14 кейсов format-221
на 8.5 — остальное осознанные пропуски.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Новая проверка (сверка «файл модуля ↔ пометка») переиспользовала $objName — ту же
переменную, из которой собирается финальная строка отчёта. В расширении с заимствованным
объектом ps1 печатал «Validation OK: Extension.Цены» — имя последнего проверенного объекта
вместо имени расширения. py-порт был прав, то есть порты разошлись молча.
Расхождение видно только на непустом расширении с чистым результатом, поэтому ни один
существующий кейс его не ловил: тесты сверяют снапшот, а итоговую строку — нет. Добавлены
утверждения на неё в кейсы valid, with-borrowed-object и module-state-file-without-flag;
проверено, что на старой версии скрипта они падают.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Объект заимствуют, чтобы дописать в него модуль, — теперь пустой файл модуля создаётся
навыком, а не руками. Тип с единственным модулем (CommonModule, HTTPService, WebService)
получает его без указаний, остальные — по -Module; -Module None отменяет. Существующий
файл не перезаписывается никогда.
Вместе с файлом проставляется <xr:PropertyState> со State=Extended. Замер по лестнице
платформ 8.3.20…8.5.1: элемент появился в формате 2.19 (8.3.26), ниже платформа молча
выбрасывает его при загрузке — отсюда гейт по версии формата. Имя свойства равно базовому
имени файла модуля, у заимствованной формы — Form. Правило владения одно: пометку ставит
тот, кто создал файл, поэтому её ставит и cfe-patch-method.
Заодно закрыта перезапись при повторном заимствовании: прямая ветка писала XML уже
заимствованного объекта начисто, унося собственные реквизиты расширения и регистрацию
формы, — молча, с успешным отчётом. Повторный вызов теперь безопасен и служит способом
дозаимствовать модуль.
cfe-validate сверяет «файл модуля ↔ пометка» в обе стороны (предупреждение: перекос
платформа принимает, но выгрузка Конфигуратора так не выглядит).
Проверено: полный регресс 827/0/8 (ps1) и 824/0/11 (py), гарды 5/5, сквозной раундтрип
на 8.3.26 и 8.3.27 — InternalInfo совпадает с выгрузкой платформы.
Закрывает #70. Разбор и эталоны выгрузки — Romandredan.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Кейс собирал через meta-compile регистр сведений без измерений и ресурсов —
платформа такую конфигурацию не принимает («Регистр без измерений, ресурсов и
реквизитов»), и verify-snapshots падал на нём на обеих ОС. Регистру добавлены
измерение и ресурс, эталон переснят: дельта только в XML регистра, права роли
не изменились.
Заодно построчный вывод verify рисовал скипы как ✗ (иконка выбиралась по
result.passed, который скип не выставляет): десяток «✗» под «0 failed» читался
как сломанный набор.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
runner.mjs при отсутствии external-пути возвращает SKIP, verify-snapshots писал
ошибку. Путь к дампу ERP/БП машинозависим: на Mac mini корпуса нет, и два кейса
role-validate краснели при полностью исправном навыке. README предупреждает ровно
об этом: раннеры читают один DSL каждый своей реализацией.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Кейсы: раскрытие HTTP- и веб-сервиса, раскрытие сервиса интеграции, раскрытие
заимствованного сервиса, дедупликация раскрытого листа с явным, отказ без
метаданных, отказ основной роли расширения, право на корень сервиса в
role-validate и в cfe-validate.
Фикстуры HTTP- и веб-сервиса строит preRun через meta-compile — рукописный XML
платформа отвергала (у Operation обязателен замыкающий ChildObjects, у сервиса
нужен Ext/Module.bsl). Оба кейса проходят verify-snapshots: 1С принимает
конфигурацию с раскрытыми правами при полной загрузке.
Вручную написаны только фикстуры, которые навыками не собрать, — сервис
интеграции (meta-compile его не поддерживает) и расширение с заимствованным
сервисом (выгрузка Конфигуратора); оба с skipPlatformVerify и причиной.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Платформа: «Назначение прав доступа на заимствованные объекты основными ролями
в расширениях недопустимо». Роль вне DefaultRoles так делать вправе, поэтому
проверяются только основные. Ловится статически, а по симптому — отказ загрузки
расширения — причина не читается.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Роль с правом на `HTTPService.Имя` формально валидна и молча не работает:
платформа проверяет вложенный объект (метод шаблона URL, операцию, канал),
маршруты отвечают 403, и по симптому причина не читается. В Конфигураторе
галки на корне сервиса нет вовсе — это недостижимое состояние, а не стилистика.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Короткая запись `HTTPService.X: Use` писала узел, которого платформа не видит:
роль проходила role-validate и cfe-validate, а маршруты отвечали 403. В корпусе
(acc/erp/ut/unf, 907 записей прав на сервисы) корневого узла нет ни разу — право
живёт на методе шаблона URL, операции, канале; в Конфигураторе галки на корне нет.
Короткая запись теперь раскрывается во все вложенные объекты сервиса по его
метаданным в OutputDir. Заимствованный сервис несёт заимствованные шаблоны и
методы, поэтому раскрытие даёт ровно тот набор, на который права дать можно.
Нет метаданных или вложенных объектов — отказ с подсказкой формата.
Плюс отказ, когда основная роль расширения (из DefaultRoles) даёт права на
заимствованный сервис: платформа такое расширение не принимает.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Выгрузка конфигуратора кладёт модель пакета в Ext/Package.bin
(текстовый XML в UTF-8 с BOM). Файла Package.xdto не существует.
Closes#69
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Таблица из пяти записей читалась как меню возможностей и звала писать
apply руками — ровно та развилка, которой мы избегаем в прощающем вводе.
Осталась одна мысль, ради которой это вообще упомянуто: незнакомый ключ
в разобранном макете нельзя удалять «для чистоты» — он несёт состояние
исходного файла.
Перечитал каскад как модель, идущая от задачи, и нашёл три места, где задача
упиралась в пустоту:
- ключ pictureParameter стоял в схеме DSL, но не был описан нигде, а задача
«картинка в ячейке» из индекса вела в drawings.md, где её не было. Добавлен
раздел: picIndex считается с единицы по порядку объявления в pictures,
выравнивания и положение текста — ключи стиля, pictureParameter — ключ ячейки;
- объектная форма rowStyle с модификатором apply нигде не описана, хотя её
пишет декомпилятор: модель, разобравшая чужой макет, встречала непонятный
ключ. Такие формы собраны в mxl-decompile отдельной таблицей — вместе с
controlType "none", пустым valueType, пустой привязкой к раскладке и записью
палитры без картинки;
- область печати и повторение шапки при печати DSL не выражает — теперь это
сказано прямо в print.md, а не выясняется опытным путём.
Индекс задач дополнен колонкой ключей: модель, увидевшая незнакомый ключ
в схеме, сразу находит нужный файл.