Значение с пробелом без кавычек разрывалось на токены, и лишние молча растекались по свободным
позиционным параметрам. На документированной форме вызова (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, а не выясняется опытным путём.
Индекс задач дополнен колонкой ключей: модель, увидевшая незнакомый ключ
в схеме, сразу находит нужный файл.
Прогнал каждый json-блок инструкций и спецификации через компилятор
и валидатор, фрагменты достраивая до минимального макета. Нашлось:
- пример колоночных раскладок показывал области с "rows": [] — макет
с именованными областями поверх нуля строк, валидатор на нём ругается;
- шапка таблицы в примере SKILL.md была записана пятью объектами с col
там, где хватает позиционного списка строк.
Оба поправлены; раундтрип примера подтверждает, что форма каноническая —
декомпилятор возвращает её же.
Утверждение «короткой формой не задать height и rowStyle» неверно: проверено
компилятором, позиционный список кладётся в cells и уживается со свойствами
строки, включая маркеры > и |. Это форма записи ЯЧЕЕК, а не строки — так
и сформулировано теперь в инструкции и спецификации.
Строка итога в примере переписана короче, заодно показывает эту форму.
Каскад повторял спецификацию: reference/dsl-spec.md был её копией, а styles
и format-properties — копиями двух других файлов документации. Модель, чтобы
что-то сделать, читала спеку целиком.
Теперь в SKILL.md лежит то, что нужно почти всегда: компактный пример
печатной формы, структура DSL, области, строки, ячейки с короткой формой,
rowStyle и оформление из десяти частых ключей. Остальное — по файлу на
задачу: layout, print, drawings, input-cells, groups, notes,
style-properties, каждый 26-81 строка; читать нужно только свой.
Состав ядра выбран по частоте в обычных макетах (корпус без регламентированной
отчётности, которая перекашивала статистику): шрифт 93%, выравнивание 88/87%,
размещение текста 86%, рамки 57-72%, формат 52%; поля ввода и группы, наоборот,
оказались редкими — 7%.
В mxl-decompile переехало то, что относится к чтению чужого макета, а не
к авторингу: пересборка — полная перегенерация, побайтового совпадения ждать
не всегда стоит, и перечень конструкций, которые цикл не переживают.
Оформление и полный перечень свойств стиля жили отдельными файлами, хотя
описывают тот же DSL. Теперь docs/mxl-dsl-spec.md — единственная полная
документация формата, как у остальных семейств навыков: структура,
оформление, колоночные раскладки, перечень свойств.
Ссылки в README и указателе спецификаций сведены к одной строке.
Пустой текст платформа хранит только самозакрывающимся тегом: в корпусе ERP
таких 1 224 460, а из 780 934 непустых блоков нет ни одного, где пусты все
языки. Компилятор же на `text: ""` писал блок с пустым элементом языка —
запись, которой в выгрузках не существует. Теперь любая пустая форма
(строка, пустой объект, все языки пустыми) даёт один и тот же тег, а
декомпилятор возвращает её канонической пустой строкой.
Отсюда же росло расхождение портов. Свёртка текста на пустой карте языков
возвращала null, если ДРУГОГО текста в макете нет вовсе: набор языков
документа при этом пуст. Дальше py писал пропуск колонки, а ps1 наступал
на разворачивание одноэлементного массива — список из одного $null
превращался в $null, и строка целиком уходила в пустые.
Проверено на 12 корпусных макетах: 1411 пустых текстов из 1411 вернулись
на место, JSON портов совпадает.
Ячейка получила третий параметр — pictureParameter, имя параметра, которым
подставляют картинку. Сама картинка задаётся оформлением (picIndex), а этот
тег живёт у ячейки, последним из её параметров, и уживается с текстом.
В корпусе таких ячеек 21 в 9 макетах; теперь все 21 возвращаются обратно.
Прозрачность картинки сведена к одному ключу transparent: false — фона нет,
{ x, y } — прозрачен цвет пикселя с этими координатами. Два способа записи
у платформы исключают друг друга (t принимает только false, включённую
прозрачность выражают tx/ty), так что двум ключам DSL соответствовал один
флажок диалога.
Заодно выровнен порядок ключей в проверке «объект описывает ячейку»: в
py-порте не хватало note.
Одна и та же запись `ref="v8ui:Имя"` стоит и за предопределённой картинкой
платформы, и за общей картинкой конфигурации — по макету источник не
определить. Загрузку это не ломает: платформа ссылку при загрузке не
проверяет, макет со ссылкой на отсутствующую общую картинку принимается.
Заодно снят прежний вывод «tx/ty с ref не встречаются»: на стенде такая
запись есть, координаты идут перед ссылкой. Раундтрип на ней сходится.
Атрибут прозрачности живёт не только у картинки с данными: платформа пишет
<picture t="false" ref="v8ui:Имя"/>, причём t перед ref. Компилятор в этой
ветке его терял, декомпилятор не читал — в «Бухгалтерии предприятия» на
8.3.27 таких записей 67.
Заодно уточнена картина по конфигурациям: t="false" стоит у 11 135 картинок
БП, значения true не бывает нигде — включённую прозрачность платформа
выражает координатами пикселя, и вместе с t они не встречаются.