Раннер получил новое поле — формат кейса обязан его описывать, иначе следующий
автор снова напишет .cmd руками и пометит кейс osOnly.
Отдельно сказано, что таким кейсам osOnly не нужен, и почему: именно забытый
-posix двойник давал дыру, из-за которой на маке выполнялся один кейс из
двенадцати.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Кейс описывал механику: сам клал fake.cmd с batch-кодом, прописывал путь к нему
и помечался osOnly win32, потому что .cmd на маке не исполнится. Тот же сценарий
на POSIX требовал второго файла с fake.sh — отсюда двойники, а забытый двойник
означал дыру: у db-repo на маке выполнялся один кейс из двенадцати, и заметили
это случайно.
Теперь кейс объявляет намерение — лог и код возврата в поле fakePlatform, — а
раннер сам кладёт .cmd или .sh под текущую ОС, пишет лог и заглушку базы и
подставляет {fakePlatform} в аргументы. Механизм включается только по объявлению
поля; остальные кейсы не затронуты.
Мигрированы db-repo (11), db-update (4), db-load-xml (4); 19 двойников удалены.
Пропусков на Windows стало 0 вместо 19 — это и были двойники.
Кейсы db-create, db-dump-cf, db-run, epf-build оставлены как есть: часть из них
однопортовая по существу (cp866 в выводе, гейт «ложного успеха» на runtimeOnly),
и каждый требует отдельного решения.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Кейсы используют фейковую платформу в виде .cmd и потому помечены osOnly win32.
На Mac mini из двенадцати выполнялся один, то есть py-порт там почти не
проверялся — ровно та ловушка, о которой предупреждает памятка по стенду.
Добавлены двойники на fake.sh для всех кейсов, кроме уже имевшегося.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Операция над всей конфигурацией перечисляет тысячи объектов, и весь список
уезжал в вывод модели. Списки обрезаются на двадцати позициях, полный уходит
в файл, путь к которому назван. Сырой лог платформы при отказе показывается
хвостом в 200 строк: итог и причину платформа пишет в конце.
Тесты на фейковой платформе — 12 кейсов, разбор лога проверяется без 1С на
логах, снятых со стенда. Ключевой кейс закрывает главное правило навыка:
платформа вернула 1, часть объектов захвачена, навык возвращает 0 с поимённым
предупреждением. Обрезка списка проверена на логе из тридцати объектов —
кейс требует и строку «и ещё 10», и отсутствие двадцать первого объекта
в выводе.
Проверено негативным контролем: намеренно сломанное ожидание роняет кейс,
то есть тесты действительно сверяют вывод, а не проходят вхолостую.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт .py зеркалит .ps1 по порядку функций, именам и комментариям — расхождения
только там, где их диктует рантайм. Общие функции взяты из соседних портов
дословно.
Копии зарегистрированы в check-inline-drift.mjs: db-repo присоединён к шести
платформенным семьям, четыре функции блока реквизитов хранилища заведены новыми
семьями, разбор сообщений хранилища — семьёй с эталоном db-load-xml. Гард сразу
нашёл настоящее расхождение: копии Get-RepositoryArgs в четырёх соседях остались
со старым comma-return, эталон правился позже. Ресинхронизировано.
Разбор сообщений хранилища добавлен и в db-load-git: он тоже грузит в базу
частично и получает те же три отказа платформы.
Подкоманда стала именованной (-Command): в группе скрипты принимают только
именованные параметры, слэш-форму в них переводит модель. Позиционный параметр
у пишущего навыка запрещён гардом check-positional-binding. Mandatory при этом
не ставим — обязательный параметр PowerShell запрашивает интерактивно, а в
пакетном запуске это зависание.
Все девять гардов и функциональные тесты db-* зелёные в обоих рантаймах.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
На Windows fs.rmSync и fs.cpSync молча не делают ничего, когда не-ASCII символы
есть в самом аргументе пути (nodejs/node#61067). Для аудитории проекта это боевой
сценарий: кириллическое имя пользователя даёт кириллический %TEMP%, где раннер
создаёт воркспейсы, плюс кириллические имена объектов 1С в путях внутри кейсов.
Замерено на восьми сборках. Затронуты 22.18.0–22.23.2 (последняя LTS Jod) и
24.12.0–24.14.0; исправны 22.15.1, 24.15.0+, 25.x, 26.x. Фикс приехал в fs-слой
Node 24.15 и в ветку 22.x не бэкпортирован, поэтому «обновить Node» вопрос не
закрывает. Зависимость не монотонна по версиям (24.14 чинит rmSync, но не
cpSync) — гард по номеру версии невозможен, решает только сам путь.
Симптомы: rmSync молча ничего не удаляет; cpSync с не-ASCII приёмником молча
ничего не копирует; cpSync с не-ASCII источником валит процесс нативно
(0xC0000409) мимо try/catch. Не затронуты mkdirSync, readdirSync, lstatSync,
copyFileSync (включая перезапись), unlinkSync, rmdirSync — на них стоит обход.
Что ломалось: под кириллическим %TEMP% фикстуры не доезжали до воркспейсов
(meta-info — 15 ложных падений из 26, один кейс ложно-зелёный на пустом
воркспейсе); cfe-validate/module-state-flag-without-file был красным даже при
ASCII %TEMP% (кириллица в самом deletePath); --update-snapshots молча не сносил
старый эталон, то есть портил коммитимые артефакты.
Реализация — единый fsutil в двух побайтно одинаковых копиях (tests/common/ и
внутри автономного навыка web-test), раскатанный на все 42 точки вызова в 11
файлах: tests/skills/*, tests/web-test/*, hooks/test/run.mjs, движок web-test.
Предикат судит по resolve(p), а не по строке аргумента: относительный
ASCII-аргумент при не-ASCII cwd платформа роняет так же молча. Ретраи доживают
до ручного обхода, перезапись как у cpSync с force: true, настоящие ошибки не
глотаются, выживший после удаления путь — громкая ошибка.
Гард tests/skills/check-nonascii-fs.mjs в check-all.mjs проверяет хелпер, а не
платформу (поэтому зелёный и на исправной Node), сверяет хеши обеих копий и
печатает справкой состояние текущей сборки.
Co-Authored-By: androman.pro <5669019+andromanpro@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Фикстуры childobjects-missing и childobjects-selfclosed невалидны by design: первая вовсе без
<ChildObjects>, вторая с пустым. Кейсы проверяют реакцию навыка на такую конфигурацию, а платформа
её не принимает — «читаемое свойство не соответствует ожидаемому. Текущее Configuration, ожидаемое
ChildObjects» и «Неизвестный объект метаданных - Language.Русский» соответственно.
Причина пропуска записана по факту отказа платформы, а не по предположению.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Полный прогон верификации давал шесть падений; четыре из них — дефекты самого инструмента.
Вход кейса. Файл писался только из caseData.input и только UTF-8, поэтому кейсы, задающие вход
через inputRaw (сырая строка) и inputEncoding (UTF-16, cp1251), оставляли навык без входного
файла: meta-compile/encoding-utf16-bom и interface-edit/bad-definition-file падали на «Cannot bind
argument to parameter». Хуже того, кейс, у которого нет ни input, ни preRun, ни params, отбрасывался
в discoverCases как «ничего не подающий навыку» — inputRaw там тоже не учитывался, и такие кейсы
в верификацию не попадали вовсе (meta-compile/bad-encoding-cp1251,
subsystem-compile/bad-definition-file). Теперь оба ключа поддержаны, encodeInput перенесён копией
из runner.mjs — общего модуля у раннеров нет, и README предписывает добавлять ключ в оба.
Это ровно та «тихая дыра», о которой README предупреждает: ключ, известный лишь одному раннеру,
даёт кейс, зелёный в функциональном прогоне и не доезжающий до платформы в верификации.
Версия формата. Гейт вызывался только при наличии каталога конфигурации, поэтому кейсы
epf-init/format-221 и erf-init/format-221 доходили до сборки и падали на стенде без 8.5 — красным,
хотя это свойство стенда. Ядро гейта выделено и читает версию из шапки любого MetaDataObject,
включая исходник обработки и отчёта; в EPF-ветке проверка выполняется до epf-build.
Проверено точечно: два кейса с inputRaw проходят, два format-221 честно пропускаются по версии
платформы, два ранее невидимых кейса берутся в прогон и проходят. Функциональный раннер не задет:
role-compile 26/26, meta-compile 85/85.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
configDir приходил только из скилл-уровневого setup (empty-config) и из ветки external, поэтому
у навыков с setup: none кейс на фикстуре до платформы не доезжал — и всё равно получал PASS.
Таких кейсов девять: пять support-edit, три meta-validate и один skd-decompile. В наборе по
умолчанию дефект не проявлялся: там у всех навыков setup: empty-config, и загрузка шла.
Теперь фикстура-конфигурация грузится как external-выгрузка, а путь «грузить нечего и спец-маршрут
не подошёл» вместо молчаливого PASS требует объявить skipPlatformVerify с причиной. skd-decompile
отнесён к standalone-навыкам: его выход — JSON-описание, грузить нечего.
Отдельно — видимость: «прошло» и «проверено платформой» больше не одно и то же число. В консоли и
в отчёте печатается, сколько кейсов реально обратилось к платформе, а сколько прошло мимо и почему
(external, standalone, ожидаемый отказ навыка, недоступная платформа); в таблице отчёта появилась
колонка «Платформа». Падения в эту долю не попадают: у них обращение было и не удалось.
Девяти кейсам проставлен пропуск с проверенной причиной. Для support-edit причина выяснялась
опытом: фикстуры содержат рукотворный Ext/ParentConfigurations.bin, и платформа отвечает «Ошибка
формата потока», тогда как настоящий bin из типовой грузится, а выгрузка без bin — тоже. Попытка
пересобрать фикстуры полноценными объектами вылечила XML, но не bin, и была откачена.
Документация: в README добавлена таблица маршрутов платформенной проверки и правила появления
configDir — раньше это знание жило только в коде, и «зелёный» отчёт не отличался от «проверенного».
В docs/1c-support-state-spec.md записано, что по описанной грамматике bin можно читать, но нельзя
собрать файл, который примет платформа.
Полный прогон verify-snapshots до и после правки даёт одинаковый состав: 461 passed, 6 failed,
30 skipped из 497 — новых падений нет. Тесты: 871/879, интеграционные 13/13.
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>
Неверный входной 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>
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>
Три файла из 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>
Разбор довели до механизма. Сборка не разрешает поля динамического списка заимствованной
формы в авто-режиме (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>
Пустой текст платформа хранит только самозакрывающимся тегом: в корпусе 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 они не встречаются.
Одна и та же картинка 25×30 вставлена в стенд дважды: без флажка
«прозрачный фон» платформа пишет t="false", с флажком — tx="24" ty="29",
то есть координату правого нижнего пикселя. Два способа записи исключают
друг друга. Стенд с обоими случаями положен в фикстуры.
Рисунок поверх сетки — картинка, фигура, надпись — теперь описывается в DSL
и возвращается из макета: два якоря «ячейка + смещение», тип, ссылка на
картинку, надпись, расшифровка, имя, порядок перекрытия.
Оформление рисунка разложено надвое: общее (заливка, шрифт, выравнивание)
берётся из именованного стиля, а линия и её стороны — собственные ключи
рисунка. У ячейки таких свойств не бывает: все 2 135 записей палитры с ними
принадлежат рисункам.
Палитра картинок: ссылка на библиотеку платформы, данные base64, пустая
запись «картинка не задана» (151 в корпусе) и координата пикселя прозрачного
цвета (tx/ty — проверено, это не размеры картинки).
Заодно: висячий пустой колонтитул в ps1-порте (пустой словарь в PowerShell
истинен, порты расходились на 3 макетах из 40) и проверка индекса формата
рисунка в mxl-validate.
Стенды Рисунки, Рисунки2, КартинкаВЯчейке проходят раундтрип байт в байт
в обоих портах; на 35 корпусных макетах с рисунками расхождение сократилось
у всех 35.
Именованная область ссылается на дополнительный набор колонок тегом
columnsID, и эта ссылка терялась целиком: на пилоте кампании она давала
3572 расхождения из 3572 в своей категории, на корпусе таких областей
913 483 прямоугольных, 20 063 полосы строк и 743 полосы колонок.
Ключ namedAreas[].columnSet имеет три состояния: ключа нет — привязка
выводится из накрытых строк (совпадает у 1 021 570 областей корпуса из
1 044 339), "" — привязки нет вовсе, имя набора — явная привязка.
Переопределение обязательно: у областей типа Rows 13 623 повторяют
раскладку строк, а 8 179 её не несут при тех же строках.
Декомпилятор пишет ключ, только когда он не выводится, а область, чья
привязка расходится с раскладкой её строк, уводит из блочной формы в
namedAreas — иначе блоком её не выразить.
Пустая строка тоже попадает в карту «строка → раскладка»: без этого
привязка области, накрывающей пустые строки, не выводилась.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>