Commit Graph
1631 Commits
Author SHA1 Message Date
Nick ShirokovandClaude Opus 5 877fde8dd4 test(role-compile,role-validate): платформенная верификация снапшотов и учёт пропусков
Прогон verify-snapshots вскрыл, что три новых кейса не доезжают до платформы:
роль ссылается на объекты, которых нет в фикстуре и которые не создаёт ни один
навык (веб-сервис, внешний источник данных, перерасчёт), а фикстуры валидатора —
это только каталог Roles/ без Configuration.xml. Объявлены skipPlatformVerify
с причиной, чтобы пропуск был видимым, а не молчаливым.

expect.filesAbsent внесён в таблицу ключей README: недокументированный ключ,
который понимает только один раннер, даёт тихую дыру.

Проверено: verify-snapshots на 8.3.27 — role-compile 13/0/1, role-validate 5/0/2;
число пропусков сверено с expected-skips на обеих ОС и обоих портах.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 20:38:00 +03:00
Nick ShirokovandClaude Opus 5 9b0bff1386 fix(role-compile,role-validate): закрытый белый список типов и прав вместо предупреждений
Неизвестный тип объекта обрабатывался веткой «warning» и всё равно попадал в
Rights.xml: и генератор, и валидатор рапортовали успех. Блок прав на тип, который
прав не имеет, платформа не отвергает — конфигурация с правами на перечисление не
загружается в информационную базу, конфигуратор зависает без сообщений.

Белый список — дерево редактора ролей: 27 типов (добавлен ExternalDataSource,
которого не было ни в одной таблице) плюс таблица видов вложенности с наборами прав
и привязкой вида к типу-родителю. Значения сняты с корпуса (acc/erp/ut/unf, ~2750
ролей) и с выгрузок роли со всеми проставленными правами — для внешнего источника
данных и перерасчёта регистра расчёта, которых в корпусе нет.

- role-compile: отказ ДО записи файлов, все причины разом, exit 1; ни файлов роли,
  ни записи в Configuration.xml. Русские алиасы для типов без прав — ради внятного
  отказа, а не ради генерации.
- role-validate: те же случаи — ошибка вместо предупреждения.
- Побочно снято 424 ложных предупреждения на корпусе: права Use у операций
  веб-сервисов и методов HTTP-сервисов считались недопустимыми для вложенных объектов.
- Проверка имён прав и вложенных путей больше не пропускает мусор: неизвестное право
  и путь вида Enum.Х.Attribute.Y раньше не проверялись вовсе.
- docs/1c-role-spec.md: ExternalDataSource ошибочно числился типом без прав.
- dsl-reference.md переписан под «что можно написать» (тип → права, виды
  вложенности) вместо таблиц прощающего ввода.

Тесты: expect.filesAbsent в раннере; новые кейсы на отказ и на вложенные объекты.
Починены кейсы, которые ничего не проверяли: три задавали права одной строкой
"Read View" (навык писал в эталон несуществующее право), valid-role собирал роль
неизвестным ключом и валидировал пустую, bad-root проходил на «файл не найден».

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 20:12:15 +03:00
Nick ShirokovandClaude Opus 5 30c8901615 docs(1c-configuration-spec): 8.3.23 → 2.16 замерена, дельта вниз от 2.17 уточнена
Платформа 8.3.23 доустановлена, замер тем же способом (пустая ИБ создаётся и выгружается
одной платформой) дал 2.16 — сообщение из issue #63 подтверждено собственным измерением,
пометка «по issue #63» из таблицы снята.

Незамеренными остались только 8.3.21/8.3.22, и пробел зажат с обеих сторон: 2.13 на 8.3.20
и 2.16 на 8.3.23 — ровно +3 версии на 3 релиза, то есть равномерный шаг подтверждается
арифметически.

Заодно расщепилась дельта пустой конфигурации ниже 2.17, которая раньше была снята одной
точкой: AllowedIncomingShareRequestTypes приезжает ровно в 2.17, DatabaseTablespacesUseMode
и DefaultReportAppearanceTemplate — где-то в 2.14–2.16. Гейт по версии ни одному из них не
нужен: это свойства корня конфигурации (реестр meta-validate — про объекты), а внутри
проверенного диапазона все три существуют всегда.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 18:27:51 +03:00
Nick ShirokovandClaude Opus 5 a1b33cdda0 fix(cf-init): предупреждать про CompatibilityMode DontUse вместо строки в SKILL.md
«Не использовать» в Конфигураторе хранится как версия ТЕКУЩЕЙ платформы: свежая база
получает Version8_3_<своя> (замерено на 8.3.20/8.3.24/8.3.25/8.3.27/8.5.1), и ни одна из
пяти типовых в корпусе DontUse не содержит. Само значение легально — платформа принимает
его без ошибок, — но не выживает: на 8.3.25 и на 8.3.27 выгрузка возвращает Version8_3_8.
Контроль: тот же шаблон cf-init с явным Version8_3_25 роундтрипится без изменений.

Отсюда предупреждение, а не запрет: запрещать значение, которое платформа принимает, —
то же самое, от чего мы только что ушли в -FormatVersion. Канал тот же (stderr, exit 0),
что и у предупреждения о версии формата.

Абзац про DontUse убран из SKILL.md: инструкция читается всегда, а ловушка актуальна почти
никогда, и текст сам ЗНАКОМИЛ модель со значением, которое выбирать не следует.

Сравнение регистронезависимо явно в обоих портах: в PS -eq таков по умолчанию, в py — нет,
и молчаливое расхождение портов началось бы прямо здесь.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 18:05:57 +03:00
Nick ShirokovandClaude Opus 5 48f9bc1d52 fix(tests): check-uuid-invariant — ошибку прогона не называть нарушением инварианта
Счётчик был один: несобранное окружение давало «1 НАРУШЕНИЙ инварианта uuid» и
отправляло искать баг там, где его нет. Счётчики разведены, exit по-прежнему 1
в обоих случаях — непрогнанный порт это не «зелено».

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 16:03:26 +03:00
Nick ShirokovandClaude Opus 5 f38fcce692 fix(tests): check-uuid-invariant работает на macOS
Гард запускает навыки в обоих портах и зашивал `powershell.exe` и `python`. Вне Windows
он падал с «spawnSync powershell.exe ENOENT» и красил весь check-all, хотя PowerShell на
маке не исполняется в принципе — это природа платформы, а не пробел в покрытии.

PowerShell отсеивается по ОС с явной строкой о пропуске; интерпретатор python на *nix —
python3. Если запрошен только powershell и ОС не Windows, выходим с 1: молча зеленеть,
ничего не проверив, хуже, чем упасть.

Гард запускает НАВЫКИ, а им нужен интерпретатор с lxml — системный python3 на маке его не
имеет. На ошибке импорта печатается подсказка про PYTHON=<venv>, иначе окружение читается
как нарушение инварианта.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 16:01:32 +03:00
Nick ShirokovandClaude Opus 5 26ca7276e6 test(skills): expected-skips.mjs — ожидаемое число пропусков вместо запомненного
«skipped» — норма, а не падение, но само число ни о чём не говорит, пока не с чем сверить,
а запоминать его нельзя: оно растёт с набором кейсов. Прежняя сверка жила в личной памятке
как grep по 'external:|runtimeOnly|osOnly' и уже сломалась — она считает скипом ЛЮБОЙ osOnly,
а posix-кейсы фейка платформы на маке как раз выполняются (grep давал 67 против 59 реальных).

Скрипт повторяет правила гейтинга раннера и живёт рядом с ним, поэтому расходиться им негде.
--list печатает пропуски поимённо с причиной. Сверено: win32/powershell 8, win32/python 11,
darwin/python 59 — совпало с прогонами.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 15:55:07 +03:00
Nick ShirokovandClaude Opus 5 64c629a897 test(skills): sh-фейк снимает кавычки со значения /Out
Значение /Out несёт кавычки внутри токена — это соглашение 1С (run_v8 передаёт
File="путь" так же). В batch их снимает %~2, поэтому win32-фейк работал; в sh
их надо снять явно, иначе cp целится в имя файла с кавычками и лог не появляется.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 15:47:41 +03:00
Nick ShirokovandClaude Opus 5 793cb8f3d6 test(skills): фейк платформы для *nix — детектор тихих отказов проверяется и на macOS
Кейсы детектора были написаны на batch-фейке и потому гейтились osOnly: win32. На маке
py-порт единственный, то есть ровно там, где проверять важнее всего, детектор оставался
без автоматического покрытия.

Добавлен sh-фейк и восемь зеркальных кейсов (osOnly: darwin/linux, runtimeOnly: python).
Для этого потребовались два расширения DSL:

- `writeFile.executable` — на *nix навык запускает платформу через exec, и файл без бита
  исполнения не стартует вовсе; Node пишет файлы без +x. На Windows chmod — no-op.
  Реализовано во всех трёх местах разбора шага (оба в runner.mjs и в verify-snapshots.mjs):
  ключ, который понимает только один раннер, даёт тихую дыру.
- `osOnly` принимает массив, а не только строку. Иначе darwin и linux требовали бы двух
  копий одного кейса.

Оба ключа описаны в tests/skills/README.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 15:46:47 +03:00
Nick ShirokovandClaude Opus 5 a024b7da9c fix(db-load-git,db-update): ловить тихие отказы платформы, как db-load-xml
Платформа рапортует об успехе (exit 0) и одновременно пишет в /Out-лог, что часть
метаданных отброшена. Детектор этого был только в db-load-xml, хотя db-load-git гоняет
тот же /LoadConfigFromFiles (и дописывает /UpdateDBCfg в тот же вызов), а db-update —
вторую половину той же цепочки. Оба лог печатали, но не разбирали.

Детектор извлечён в Find-SilentRejections / find_silent_rejections и внесён в реестр
семей check-inline-drift: инлайн-код гард сверять не умеет, а именно расхождение копий
и было бы главным риском такого дублирования.

Добавлен восьмой паттерн — «Для работы с конфигурацией необходима версия платформы не
меньше». Замерено при работе над issue #63: конфигурация с режимом совместимости выше
платформы грузится с кодом 0, db-update тоже отвечает 0, объекты в базу не попадают, а
отказ приходит только в рантайме. Строка обрезана до инвариантной части — конкретная
версия в сообщении меняется.

Текст предупреждения переписан. Убрана подсказка «pass -StrictLog to treat as error»:
ключ предназначен для регрессов (его передаёт verify-snapshots), а совет бессмысленный —
операция уже выполнена, и повторять её ради того же текста незачем. Формулировка больше
не утверждает «dropped properties/refs»: класс проблемы разный, а строки лога печатаются
следом и говорят за себя.

Две правки по дороге:

- `return ,$found` в паре с `@()` у вызывающего давал массив из одного пустого массива,
  то есть предупреждение «1 problem(s)» на чистом логе. Возврат без запятой-обёртки.
- py-порт писал предупреждение в stderr, PS1 — в stdout. В самом py-порте stderr занят
  исключительно фатальными «Error:» перед exit 1, так что не-фатальное предупреждение там
  было единственным исключением. Приведено к stdout — и к соглашению своего же файла, и к
  поведению PS1; verify-snapshots на падении читает `stderr || stdout`, поэтому диагностика
  не теряется.

Тесты: фейковая платформа .cmd, которая вычитывает путь из /Out и кладёт туда готовый лог,
— по четыре кейса на db-load-xml и db-update (отбраковка, она же под -StrictLog, новый
паттерн, чистый лог без предупреждения). Раньше детектор не был покрыт вообще.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 15:14:36 +03:00
Nick ShirokovandClaude Opus 5 06f21ab3d1 test(skills): гард проверенного диапазона версий формата
Допустимый список версий был независимым литералом в десяти файлах, и сверять его
было не с чем. Именно так волна 2.21 прошла по четырём валидаторам и молча обошла
пятый — form-validate остался на 2.17–2.20 (issue #63).

check-format-versions.mjs держит три инварианта: границы диапазона одинаковы во всех
навыках и на обоих портах; дефолт -FormatVersion у *-init лежит внутри диапазона;
верхняя граница совпадает с последней ЗАМЕРЕННОЙ ступенью таблицы §7.1 из
1c-configuration-spec.md — так расхождение спеки и кода падает здесь, а не на чужой
выгрузке. Отдельно ловится возврат ValidateSet/choices в *-init.

Get-FormatRank разъехался бы по восьми новым копиям — они внесены в реестр семьи
format_rank в check-inline-drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 14:21:07 +03:00
Nick ShirokovandClaude Opus 5 beaa75c742 fix(epf-build): версия и режим совместимости заглушки — из исходников
Заглушечная конфигурация зашивала version="2.17" и CompatibilityMode=Version8_3_24
независимо от собираемых исходников. Ограничение платформы одностороннее — она читает
формат не новее себя, — поэтому на 8.3.20 такая заглушка не грузилась вовсе:
«Неизвестная версия формата 2.17 загружаемого файла».

Режим совместимости бил тише и потому опаснее: платформа рапортовала успешную
загрузку с кодом 0, писала в лог «Для работы с конфигурацией необходима версия
платформы не меньше, чем 8.3.24» и не создавала объекты — сборка падала уже на
«Неизвестное имя типа», уводя диагностику в сторону.

Оба значения выводятся из версии исходников. Для версии взят min(исходники, 2.17):
заглушке нужна самая низкая работающая версия, а не версия исходников, поэтому на
2.17+ поведение остаётся прежним и опускается только под 2.13–2.16. Режим — по той же
лестнице.

Проверено на живой платформе: обработка 2.13 со ссылочным реквизитом собирается на
8.3.20 обоими портами; для исходников 2.17 заглушка по-прежнему 2.17/Version8_3_24.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 14:20:55 +03:00
Nick ShirokovandClaude Opus 5 de76fde275 fix(*-validate,*-init): проверенный диапазон версий формата 2.17–2.21
Валидаторы объявляли «ожидаемым» литеральный список версий и ругались на всё
остальное как на подозрительное. Но 2.13–2.16 не подозрительны — они просто не
проверялись, а form-validate вдобавок отстал на 2.21 и предупреждал о форме, которую
сам же создаёт цепочкой epf-init 2.21 → form-add (issue #63).

Теперь сравнение числовое, через общий Get-FormatRank, и исходов три: ниже 2.17 и
выше 2.21 — предупреждение «not verified on it», нечисловое значение — ошибка. Раньше
мусор вида «abc» и реальная версия 2.16 давали одно и то же предупреждение.

В *-init ValidateSet/choices сняты: версия вне диапазона больше не запрет, а
предупреждение в stderr — скаффолд выпускается. Опечатка (2,17) остаётся ошибкой.
Предупреждение печатается напрямую в stderr и после настройки кодировки консоли:
Write-Warning в PS 5.1 уходит в stdout, получает локализованный префикс и перенос по
80 символов, а до [Console]::OutputEncoding em-dash уезжал в вопросы — оба расхождения
ломали паритет с py-портом.

Отдельно в cf-init исправлен гейт TextToSpeech: свойство приезжает форматом 2.18
(8.3.25), а не 2.21. Замер: выгрузки пустой ИБ шести платформ — 37 записей
UsedMobileApplicationFunctionalities на 2.13/2.17 и 38 на 2.18–2.21. Ролики загрузки
показали, что это настоящее новое свойство, а не смена дефолта эмиссии: на 2.17 тег
роняет загрузку XDTO-ошибкой (8.3.24), значение значимо (true переживает роундтрип),
а пропуск на 2.18+ разводит роундтрип — платформа допишет тег сама. Снапшоты трёх
кейсов meta-compile обновлены: их фикстуры строятся cf-init, дельта — только вставка
тега.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 14:20:40 +03:00
Nick ShirokovandClaude Opus 5 35c8618813 test(skills): expect.stderrContains и захват stderr на успешном прогоне
execSkillAsync отдавал только stdout, а stderr сохранялся исключительно в ветке
ошибки. Поэтому предупреждение навыка, который отработал успешно (exit 0), проверить
было нечем: кейс мог убедиться лишь в том, что навык не упал, — то есть в молчании
вместо текста.

Резолв теперь отдаёт оба потока, добавлен ключ expect.stderrContains.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 14:20:19 +03:00
Nick ShirokovandClaude Opus 5 9949039d98 docs(1c-*): лестница версий формата — по одной на релиз платформы
Утверждение «2.17 = платформы 8.3.20–8.3.24» повторялось в 7 местах и было неверным:
точка «8.3.20» получена не замером, а из имени каталога cfsrc/acc_8.3.20, который
побайтово совпадает с acc_8.3.24 (91807/91807 файлов; обе — БП 3.0.181.31 с режимом
Version8_3_24, который 8.3.20 открыть не может). Интерполяция между двумя одинаковыми
точками и дала мнимый интервал.

Замер пустыми ИБ на шести установленных платформах (создать базу платформой X и
выгрузить ею же): 8.3.20 → 2.13, 8.3.24 → 2.17, 8.3.25 → 2.18, 8.3.26 → 2.19,
8.3.27 → 2.20, 8.5.1 → 2.21. Версия меняется каждым релизом.

Полная лестница теперь в одном месте — §7.1 1c-configuration-spec.md, с колонкой
«замерено»: 8.3.21/8.3.22 не проверялись, 2.16 подтверждена только сторонним
сообщением. Остальные спеки на неё ссылаются.

Заодно сняты два вывода, выведенных из каталога-дубля: «содержимое Form.xml идентично
между 8.3.20 и 8.3.24» и строка «8.3.20 | 2.17 | Базовая» в спецификации ролей.
Дописан 2.21 в таблицы, которые о нём не знали, и уточнён TextToSpeech: он приезжает
в 2.18, а на 2.17 роняет загрузку XDTO-ошибкой, а не отбраковывается молча.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 14:20:10 +03:00
Nick Shirokov f522d29cc0 fix(cfe-borrow): не переносить FoldersOnTop в оболочку заимствованного объекта
Сквозной прогон (собрали расширение навыками → загрузили → обновили БД →
выгрузили обратно) показал: записанный нами <FoldersOnTop> из выгрузки
пропадает. Платформа его у заимствованной оболочки не хранит — принимает
молча и выбрасывает. Соседние свойства того же списка (Hierarchical,
CodeLength, DescriptionLength, CodeType, CodeAllowedLength) сохраняются,
так что дело именно в этом свойстве. Эталон Конфигуратора его тоже не
переносит.

Список propsToExtract собирался на глаз при появлении -BorrowMainAttribute;
это второе свойство из него, которое платформа не принимает (первым был
NumberPeriodicity, ронявший UpdateDBCfg).

Одноразовый прогонщик сценариев — debug/roundtrip/ (в .gitignore).
Регресс cfe-borrow 12/12 на обоих рантаймах, гарды зелёные.
2026-08-12 20:34:13 +03:00
Nick Shirokov af00aa4711 fix(form-validate,cfe-borrow): остаточные ложные ошибки на формах платформы
Корпусный прогон (УТ/БП/ERP, 21 097 форм) после понижения Command/Action
оставлял 8 форм с ошибками. Разобраны все, дефектов оказалось два.

1. Вложенная таблица. Путь Items.<Таблица>.CurrentData.<Поле> разрешался
   ОДНИМ шагом: если таблица сама привязана через Items.*, корнем
   оставался литерал «Items», и типовая форма объявлялась битой
   (НастройкаПравилОбработкиЗаявокСотрудников в БП и ERP). Теперь
   разрешение идёт цепочкой, со страховкой от кольца ссылок.

2. AutoCommandBar с обычным id вместо -1. Это соглашение, а не требование:
   21 094 формы из 21 097 используют -1, но три платформа выгружает с
   обычным id и грузит их без нареканий. Понижено до предупреждения;
   ошибка осталась на случай, когда id вообще не число.

Оставшиеся три формы — дубли id элементов и команд. Это НЕ ложные
срабатывания: измерение по корпусу показало ровно по одному случаю на
21 097 форм, то есть опечатки вендора, а не структурное правило (были бы
пулы id раздельными, пересечений были бы тысячи). Плюс кейс duplicate-id
прямо требует считать дубль ошибкой.

Итого по корпусу: 283 формы с ошибками → 3.

Заодно дооформлена фикстура cfe-borrow/container-types (пространство имён
веб-сервису, документ журналу и последовательности): verify-snapshots по
cfe-borrow теперь 12/12.
2026-08-12 20:20:02 +03:00
Nick Shirokov 71f047291c fix(form-validate): версия формата — общим helper-ом, с учётом внешних обработок
Проверка версии читала Configuration.xml собственной регуляркой. Отсюда два
пробела: форма автономной внешней обработки не проверялась вовсе (своего
Configuration.xml у неё нет, версию несёт корень обработки), а разбор
дублировал уже существующий эталон.

Теперь используются копии общих эталонов — detect_format_version (авторитет
form-compile) и support-guard: is_external_root (авторитет cf-edit), оба
навыка добавлены в реестр check-inline-drift.

Попутно вскрылось следствие для соседней проверки: обработка, чьи исходники
лежат внутри дерева с Configuration.xml (обычная раскладка src/cf рядом с
src/epf), считалась «конфигурационным контекстом», и Check 12 ругался на её
собственные External*-типы. Теперь климб останавливается на ближайшем
якоре — том же правиле границы автономного объекта, что в support-guard.

Плюс паритет: PS отчитывался «Data bindings: none», PY эту ветку не имел.

Кейс на EPF-контекст добавлен. Регресс 16/16 на обоих рантаймах, корпус
21 097 форм без новых срабатываний, гарды зелёные.
2026-08-12 19:28:11 +03:00
Nick Shirokov 7dc2e6e443 feat(form-validate): проверки необъявленного префикса и версии формата
Обе проверки — про ошибки, которые делает не платформа, а тот, кто пишет
XML руками. Обе вскрыты на наших же фикстурах, обе платформенно-фатальны:
файл не читается вовсе, а прежний валидатор говорил OK.

Check 13 — префикс в значении типа обязан резолвиться. `cfg:CatalogRef.X`
в <v8:Type> при незадекларированном xmlns:cfg даёт «Исключение XDTO при
чтении файла». Область видимости считается по узлу, а не по корню:
локальная xmlns на элементе законна и в типовых встречается (d4p1, mxl).

Check 14 — версия формата формы против версии конфигурации. В пределах
одной выгрузки версия едина; форма из более новой выгрузки даёт
«Неизвестная версия формата N загружаемого файла».

Ложных срабатываний нет: корпус УТ/БП/ERP, 21 097 форм — те же 8 форм с
ошибками, что и до правки. Регресс 15/15 на обоих рантаймах, паритет
портов сверен построчно.
2026-08-12 18:59:55 +03:00
Nick Shirokov 1b40dc5b03 test(form-validate): фикстуры доведены до загружаемых конфигураций
verify-snapshots считает фикстуру готовой конфигурацией и грузит её в базу,
а фикстуры form-validate состояли из одного Form.xml — поэтому по этому
навыку верификация падала целиком и по факту не выполнялась никогда.

Что вскрылось при доведении, по нарастающей:

1. Нет Configuration.xml и объекта — «Файл объекта не существует».
   Дописаны cf-init + meta-compile + form-add, рукописный Form.xml сохранён.
2. Префикс `cfg:` в значении <v8:Type> при НЕобъявленном xmlns:cfg —
   «Исключение XDTO произошло при чтении файла». Тот же класс, что ишью #38,
   только в наших собственных тестовых данных: четыре фикстуры платформа не
   читала вовсе, а кейсы на них считались зелёными.
3. Привязка Объект.Наименование у обработки — у неё нет такого стандартного
   реквизита. Добавлен обычный реквизит.
4. Динамический список без источника — «Неверный путь к данным: Список.Ссылка».
   Добавлен справочник-источник и MainTable.

Кейс с формой расширения помечен skipPlatformVerify: верификатор грузит
каталог кейса как конфигурацию, фрагмент расширения так не проверить. Ключ
существовал в verify-snapshots, но не был описан — добавлен в README.

form-validate: 14/14 на обоих рантаймах, платформенная верификация 13/14
(один пропуск с причиной, было 10/14 с четырьмя падениями).
2026-08-12 18:36:09 +03:00
Nick Shirokov e1d5d1f903 fix(form-validate): команда без Action — предупреждение, а не ошибка
Проверка объявляла ошибкой любую команду формы без <Action>. Корпусный
прогон (УТ 8.3.27, БП 8.3.27, ERP 8.3.24 — 21 097 форм) показал 406 таких
команд на 275 формах, и все они произведены самой платформой: формат это
допускает, конфигурации грузятся.

Приём типовых: действие назначается в рантайме, в ПриСозданииНаСервере —
`Команда.Действие = "Подключаемый_" + Имя + "Локализация"`. Назначать может
и чужой модуль (переопределяемый слой, подключаемые команды), поэтому по
одному Form.xml вердикт не вынести — отсюда предупреждение, а не ошибка, и
никакого подглядывания в соседний модуль.

Корпус после правки: форм с ошибками 283 → 8, новых срабатываний нет.
Оставшиеся 8 — четыре других класса, разбираются отдельно.

Кейс с фикстурой: команда без Action на кнопке → предупреждение, exit 0.
2026-08-12 17:24:34 +03:00
Nick Shirokov c9b64f3a0d fix(form-compile): внятный отказ на группу additionalColumns без ключа columns
PS-порт падал с «Не удается индексировать в массив NULL»: @($null).Count в
PowerShell равен единице, поэтому ветка самозакрывающегося тега была
недостижима, и код шёл эмитить несуществующую колонку. PY на том же входе
молча писал пустую группу — портируемого поведения не было вовсе.

Разведены два случая, которые до сих пор путались:
- `"columns": []` — явно пустая группа, законная форма (платформа так
  пишет таблицу без доп. колонок). Работает как работала, self-closing;
- ключа `columns` нет вовсе — недосказанность автора: «доп. колонки есть»,
  а какие, не сказано. Теперь отказ с указанием, что делать.

Валидатор такое по-прежнему пропускает: в выгрузке платформы пустая группа
встречается, запрет на авторинг не равен запрету на существование.

Заодно кейс cfe-borrow/form-main-attr-columns вернулся к форме эталона
Конфигуратора — таблица без колонок описана явно пустым списком, как в
выгрузке, а не обходным манёвром вокруг падения.
2026-08-12 17:01:10 +03:00
Nick Shirokov a6cb656a2d fix(form-validate): паритет портов по счётчику проверок
На одной и той же форме PS сообщал 12 проверок, PY — 9. Расходился не
вердикт, а учёт: три проверки (ссылки команд, обработчики событий,
действия команд) в PS отчитываются строкой «none», когда проверять нечего,
а в PY эта ветка отсутствовала — и проверка не попадала в счётчик.

Добавлены недостающие ветки. Выборка из 40 форм корпуса (УТ, БП, ERP):
число строк отчёта совпадает у портов на всех сорока.
2026-08-12 16:55:28 +03:00
Nick Shirokov 53ee51a37a fix(cfe-borrow): не переносить NumberPeriodicity в оболочку заимствованного документа
Платформа считает это свойство модификацией настроек нумерации и тогда
требует объявить ещё и <Numerator/>: /UpdateDBCfg падает с «Для
заимствованного документа, настройки нумерации которого модифицированы,
отключать контролируемость свойства "Нумератор" недопустимо». Загрузка при
этом проходит — ошибка вылезает только на обновлении конфигурации БД,
поэтому ручной E2E её не видел, а verify-snapshots поймал.

Эталон Конфигуратора переносит NumberType/NumberLength/NumberAllowedLength
и не переносит NumberPeriodicity — приводим к тому же. Свойство попало в
список ещё при появлении -BorrowMainAttribute, набор тогда был угадан, а
не выверен по эталону.

Заодно две мои фикстуры доведены до платформенной валидности:
form-choice-param-links задавала в ИСХОДНОЙ форме ссылку на несуществующий
реквизит — такая конфигурация не грузится сама, проверять на ней вырезание
висячей связи нельзя; gentypes-full не имела регистратора для регистра
бухгалтерии и задачи для бизнес-процесса.

verify-snapshots по cfe-borrow: 11 из 12 (container-types падал и раньше,
дефект его фикстуры). Регресс 12/12 на обоих рантаймах, все гарды зелёные.
2026-08-12 16:52:42 +03:00
Nick Shirokov 9650085fff docs(cfe-init): каталог расширения в примерах повторяет его имя
В примерах имя расширения задано явно (-Name Расш1), а рядом стоял
плейсхолдер -OutputDir src\cfe\extname — связь между именем и каталогом
из такого примера не читается. Теперь каталог называется по имени, и
конвенция «подкаталог = имя расширения» видна из самих примеров.

Базовая команда дополнена -OutputDir и -ConfigPath: без них расширение
уезжает в src (тот самый неоднозначный каталог), а совместимость и UUID
языка берутся по умолчанию вместо базовой конфигурации.
2026-08-12 16:29:16 +03:00
Nick Shirokov c0c37532a7 docs(cfe-*): единая конвенция путей в примерах и передача ConfigPath валидатору
В группе cfe-* примеры расходились: cfe-patch-method уже использовал
src\cfe\<расширение> и src\cf, остальные три навыка — голый src и
абсолютный C:\cfsrc\erp. Голый src особенно вреден: в репозитории с
конфигурацией и расширением он неоднозначен, и модель подставляет в
-ExtensionPath конфигурацию.

Везде одна пара: src\cfe\extname и src\cf.

Заодно по итогам появления -ConfigPath у cfe-validate: блоки «Верификация»
теперь показывают его передачу, а описание параметра в самом валидаторе
переписано с устройства проверки на повод её включить — с последствием
(расширение пройдёт валидацию и будет отвергнуто платформой) и с
алгоритмом поиска пути прямо на месте, без отсылки к соседнему навыку.

У cfe-init отмечено, что CompatibilityMode не нужен при заданном
ConfigPath, и что расширение стоит класть в отдельный подкаталог.
2026-08-12 16:24:57 +03:00
Nick Shirokov 9b1c3de642 feat(cfe-validate): полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath
Три вещи, на которых платформа отвергала расширение, а валидатор молчал.

Check 9 — полнота набора GeneratedType у заимствованной оболочки: неполный
набор платформа не читает («отсутствует один или более типов объекта»).
Карта категорий взята из той же таблицы спецификации (§2.5) и заведена в
реестр check-type-maps.mjs, чтобы копия не разошлась с остальными.

Check 12 — <AdditionalColumns table="Объект.X"> при незаимствованной
табличной части. В отличие от соседних проверок блока, здесь сигнал точный
(имя из атрибута), а последствие жёсткое, поэтому ошибка, а не warning.

Check 14 — пути Объект.* заимствованных форм против конфигурации-источника,
по новому опциональному -ConfigPath. Без него проверка пропускается с явной
строкой в отчёте. Отличить живой путь от висячего иначе нельзя: Объект.Партнер
валиден и без заимствования (наследуется от базы), а Объект.Товары.Артикул не
разрешится нигде. Итоги колонок (Total<Колонка>) и стандартные реквизиты
пропускаются — иначе ложные срабатывания на типовых формах.

Проверено на пяти расширениях: наши (оба режима) и оба эталона Конфигуратора
проходят чисто, расширение с дефектом ловится. Регресс cfe-* 51/51 на обоих
рантаймах, гарды дрейфа зелёные.
2026-08-12 16:17:56 +03:00
Nick Shirokov 9a28fbfacb feat(form-validate): путь на необъявленный основной реквизит заимствованной формы
Форма, которую платформа отвергала с «Неверный путь к полю - Объект.Партнер»,
проходила валидацию с вердиктом OK. Check 5 такое не видит по двум причинам:
у формы с BaseForm он пропускает базовые элементы (id < 1000000), а привязка
внутри <ChoiceParameterLinks> лежит в <xr:DataPath> и в его список тегов не
входит вовсе.

Проверка 11d: если форма не объявляет основной реквизит, любой путь с корнем
«Объект» не разрешится — ошибка. Непрозрачные формы пути (1/0:uuid), которыми
как раз и заменяет такие ссылки Конфигуратор, ошибкой не считаются.

Две фикстуры: висячая ссылка ловится, та же форма в uuid-форме проходит.
Регресс 13/13 на обоих рантаймах.
2026-08-12 15:58:45 +03:00
Nick Shirokov 1bd0c7724e feat(cfe-borrow): ссылки параметров выбора по uuid реквизита
Заимствование формы без основного реквизита копировало
<ChoiceParameterLinks>/<xr:Link> как есть, с текстовым путём
«Объект.Партнер». В расширении такой путь не разрешается — платформа
отвергала загрузку: «Неверный путь к полю - Объект.Партнер». Привязка
лежит в <xr:DataPath> внутри xr:Link, и общий стриппинг её не видел.

Конфигуратор ссылку не выбрасывает, а переводит путь в непрозрачную форму
«1/0:<uuid реквизита объекта>» — связь остаётся рабочей. Делаем так же;
реквизит, которого в источнике нет, недоступен и по uuid — такую связь
вырезаем вместе с опустевшим контейнером. Путь односегментный во всём
корпусе УТ (285 из 285), глубже не бывает.

Результат совпал с эталоном Конфигуратора вплоть до uuid, E2E на UT_DEMO:
«Load completed successfully». Регресс 12/12 на обоих рантаймах.
2026-08-12 15:53:59 +03:00
Nick Shirokov 3fafa099b2 feat(cfe-borrow): перенос UseAlways/Columns основного реквизита формы
Заимствование формы с -BorrowMainAttribute синтезировало <Attribute
name="Объект"> с нуля — Type/MainAttribute/SavedData и всё. Секции
<UseAlways> и <Columns> исходной формы терялись, а вместе с ними и
дополнительные колонки табличных частей, объявленные прямо в форме:
платформа отвергала загрузку («Неверный путь к данным: Объект.Товары.Артикул»).

Три связанных следствия:
- секции переносятся из исходной формы (helper Get-MainAttributeExtraXml
  на оба порта, отступ в BaseForm — тем же приёмом, что у ChildItems);
- табличная часть, упомянутая только в <AdditionalColumns table="…">,
  теперь попадает в сбор путей и заимствуется — иначе «Колонки не могут
  быть добавлены к реквизиту»;
- типы колонок дозаимствуются через collect_reference_types, как это
  делает Конфигуратор (DefinedTypes/Артикул в эталоне).

Попутно снят PS↔PY-дрейф: lxml включал хвостовой пробельный узел в
tostring, из-за чего .py вставлял пустые строки, которых нет у .ps1
(with_tail=False в четырёх местах).

E2E на UT_DEMO (8.3.27, формат 2.20, форма заказа поставщику): режим с
основным реквизитом грузится «Load completed successfully» без ручных
правок. Регресс 11/11 на обоих рантаймах.
2026-08-12 15:49:34 +03:00
Nick Shirokov 902b643a3e feat(cfe-borrow): полный набор GeneratedType у заимствованных оболочек
Платформа отвергала заимствованный план видов характеристик: «отсутствует
один или более типов объекта ChartOfCharacteristicTypes». В карте
$script:generatedTypes не хватало категории Characteristic — а вместе с ней
ещё пяти категорий у пяти типов (планы счетов и видов расчёта, регистры
бухгалтерии и расчёта, бизнес-процессы).

Дефект был чистым дрейфом копий: карта живёт в трёх навыках, и в
meta-compile с meta-validate она верна. Чтобы расхождение больше не
копилось молча, каноническая таблица наборов GeneratedType заведена в
спецификации (§2.5), а check-type-maps.mjs получил виды gentypes/gencats и
сверяет по ней все три карты.

Регресс cfe-* 47/47 на обоих рантаймах, оба гарда дрейфа зелёные.
2026-08-12 15:38:52 +03:00
Nick ShirokovandClaude Opus 5 37108b15c4 test(skills): свести verify-snapshots с runner по разбору кейса
Верификатор читает тот же DSL, что и функциональный раннер, но своей
реализацией, и отставал на пять ключей: preRun[].cwd, inputFrom, cwd на уровне
кейса, раскрытие {workDir} в args_extra и маппинг from: outputPath. Кейсы,
опирающиеся на них, до платформы не доезжали вовсе — падали на подготовке
фикстуры, причём шаг preRun с относительным путём писал её в корень репозитория.

Незнакомый from теперь роняет кейс с внятным сообщением: молчаливый default
маскировал расхождение под дефект навыка (флаг уходил без значения).

Платформенная верификация: mxl-compile 28/36 -> 36/36, mxl-decompile 0/7 -> 7/7,
mxl-info 2/7 -> 7/7, mxl-validate 0/4 -> 4/4, skd-decompile 0/17 -> 17/17.

Оба ключа, которых не было в документации DSL, дописаны в README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:40:18 +03:00
Nick ShirokovandClaude Opus 5 241a56a29f docs(mxl): актуализировать спецификации после серии находок на стенде
Спецификация XML — дописано то, что вскрыли контролируемые макеты и замеры
по корпусу:

- шрифт-ссылка на СИСТЕМНЫЙ шрифт: префикс sys в корне не объявлен, поэтому
  объявление xmlns дописывается прямо на узел. Плюс правило вывода kind
  из префикса и то, что неиспользуемый шрифт в палитру не попадает;
- новый раздел «Устройство палитр»: порядок документный и НЕ зависит от
  последовательности действий автора (проверено опытом с оформлением снизу
  вверх), формат по умолчанию последний, палитра дедуплицирована по содержимому;
- у текста ячейки ТРИ состояния: тега нет, тег с элементами, пустой <tl/>.
  Третье — 57% макетов корпуса;
- языковые настройки: набор языков не выводится из языков текста, description
  бывает самозакрывающимся, currentLanguage бывает отсутствующим и бывает
  указывающим на необъявленный язык.

Спецификация DSL — из ограничений убрано объявление языков макета: оно больше
не теряется. Добавлено пояснение, почему побайтовое совпадение достижимо не на
любом макете: в долго правленных макетах остаются следы прежних состояний,
которые из итогового документа не выводятся.

В инструкции навыка отражена только форма шрифта-ссылки — остальное из этой
серии либо уже там, либо для авторинга не нужно.

Примеры из справочника скомпилированы и проверены валидатором.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 12:53:10 +03:00
Nick ShirokovandClaude Opus 5 98bcf79b41 feat(mxl-compile,mxl-decompile): шрифт ссылкой на стиль или системный
Шрифт бывает не собственным описанием, а ссылкой: на элемент стиля конфигурации
(style:) или на системный шрифт (sys:). Своих атрибутов у такого шрифта нет, и мы
превращали его в пустую запись — faceName="" height="0". ps1 при сборке подставлял
туда Arial 10, то есть подменял данные молча.

В корпусе таких шрифтов 272 в 213 макетах из 10 924: StyleItem 209, WindowsFont 63.

Запись — та же, что у шрифта в описании формы: { "ref": "style:TextFont" }.
kind выводится из префикса, ключом быть не обязан.

Синтетический стенд показал деталь, которую по корпусу было не разглядеть: префикс
sys в корне документа не объявлен, поэтому платформа дописывает объявление xmlns
прямо на узел шрифта — тот же приём, что с цветами из web-палитры.

Порты после этого сошлись ПОЛНОСТЬЮ: на пилоте из 40 макетов совпадают и JSON
декомпиляторов, и собранный XML. Макет со шрифтами добавлен в побайтовую
регрессию, стенд 8 из 13.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 12:47:24 +03:00
Nick ShirokovandClaude Opus 5 0a8671a166 feat(mxl-compile,mxl-decompile): пустой тег текста ячейки
У текста ячейки три состояния, а выражались два: тега нет вовсе, тег с пустым
текстом на каждый язык — и третье, <tl/>, которое мы теряли. Это не редкость:
38 075 ячеек на 1200 макетов, встречается в 57% макетов корпуса.

Выражается пустым объектом: "text": {}. Не новый ключ и не новое понятие, а
вырожденный случай уже принятой записи «язык → текст» — языков нет вовсе.
В позиционной строке двусмысленности не создаёт: {} не несёт ключей ячейки,
значит по общему правилу читается как текст, а null там по-прежнему «пропустить
колонку».

В описании DSL записи нет намеренно. Для авторинга она бесполезна — визуально
это тот же пустой текст, что и "", — а документировать две пустые формы рядом
значило бы завести развилку, у которой нет правильного ответа.

Платформа пишет такой тег самозакрывающимся, поэтому эмиссия отдельной веткой.

На пилоте потери в категории row[].cell[].tl упали с 12 488 до 2, заодно
row[].cell[].fmt с 41 645 до 30 088; совпадение строк документа 17.6% → 18.5%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 12:21:02 +03:00
Nick ShirokovandClaude Opus 5 6619a4e7ad fix(mxl-compile,mxl-decompile): висячие ссылки на стиль и культурная сортировка
Два дефекта, найденных при сведении портов.

Стиль, на который ссылалась только дополнительная колоночная раскладка,
отсекался как неиспользуемый: проверка смотрела в result, а columnSets
попадает туда ПОЗЖЕ неё. Ссылка оставалась висячей — 5 макетов пилота
из 40 указывали на стиль, которого в styles нет. Берём стили из самих
раскладок.

Именованные элементы платформа хранит отсортированными по имени ординально.
ps1 сортировал их Sort-Object -CaseSensitive, а он всё равно сравнивает по
текущей культуре — комментарий рядом сам об этом предупреждал. На кириллице
порядок расходился с py-портом (4800 строк разницы на одном макете).
Сортируем по ключу из кодов символов: в нём только 0-9A-F, культура его
переупорядочить не может.

Порты сошлись: JSON декомпиляторов совпадает на всех 40 макетах пилота
(было 26 расхождений), собранный XML — на 39 из 40 (было 20 расхождений).
Остаток — один макет, где расходятся компиляторы: ps1 подставляет
faceName="Arial" height="10" там, где py пишет пустой шрифт.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 12:03:53 +03:00
Nick ShirokovandClaude Opus 5 c11f4837fd fix(mxl-decompile): свести порты — пробелы, порядок и свойства значения ячейки
Расхождение JSON между портами было 26 макетов из 40, стало 5. Расхождение
собранного XML — 20 из 40, стало 7. Причин оказалось четыре, и три из них
дефекты, а не просто разнобой.

1. ps1 грузил XML с PreserveWhitespace = $false, и текст ячейки из одних
   пробелов схлопывался в пустой. Молчаливая потеря данных в каноничном
   порте, 25 макетов пилота.

2. Стили обнаруживались обходом хэш-таблицы строк, а её порядок в PowerShell
   НЕ определён. Платформа же кладёт записи палитры в порядке документа, так
   что порядок обхода — часть верности вывода, а не деталь. Обход теперь по
   возрастанию номера строки. ([ordered] тут не годится: с целочисленными
   ключами он индексируется по позиции, а не по ключу.)

3. Именованные области сортировались нестабильным Sort-Object (-Stable
   появился только в PowerShell 6.2), поэтому области с одинаковыми границами
   получали произвольный порядок. Добавлен явный ключ исходного порядка —
   в оба порта, чтобы совпадение было по построению, а не по совпадению.

4. containsValue / valueType / controlType протекали в styles сквозным
   пробросом неизвестных тегов. Это свойства ЗНАЧЕНИЯ ячейки, а не оформления,
   и компилятор таких ключей не знает — в DSL они были чистым шумом. Заодно
   вложенный элемент больше не читается как скаляр: ps1 брал InnerText и
   получал склейку поддерева, py брал .text и получал пустоту.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 21:40:44 +03:00
Nick ShirokovandClaude Opus 5 e46db618e3 docs(mxl): уточнить, как читается объект в позиционной записи строки
Прежняя формулировка противопоставляла «ячейку» и «многоязычный текст», хотя
текст тоже даёт ячейку. Разница не в этом: объект либо описывает СВОЙСТВА
ячейки (среди ключей есть ключ её схемы), либо является её ЗНАЧЕНИЕМ, и тогда
ключи — идентификаторы языков.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 21:16:17 +03:00
Nick ShirokovandClaude Opus 5 b5fef09844 feat(mxl-compile,mxl-decompile): многоязычный текст элементом строки-массива
В позиционной записи строки элемент — это значение содержимого ячейки, а значение
текста по общей конвенции бывает строкой либо объектом «язык → текст». Значит
объект {ru, en} там законен так же, как строка, и новой формы это не заводит.

Раньше ячейка с многоязычным текстом не считалась «простой», поэтому строка
двуязычного макета оставалась объектной:

  { "cells": [{ "text": { "ru": "Пусто", "en": "Empty" } }, { … }] }
  [{ "ru": "Пусто", "en": "Empty" }, { … }]

Объект-элемент читается как ячейка, если несёт хоть один её ключ, и как текст
в противном случае: идентификаторы языков с ключами ячейки не пересекаются
(в корпусе это ru, en, ru1, Русский).

Попутно исправлен признак «список позиционный»: он искал элемент-строку, поэтому
строка из одних многоязычных текстов позиционной не признавалась и ключ cells
у неё оставался.

Правка не должна менять скомпилированный XML — и не меняет: пилот из 40 макетов
собрался побайтово так же, JSON при этом изменился у 13. Стенд 7 из 10.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 21:13:32 +03:00
Nick ShirokovandClaude Opus 5 f926acd4f4 fix(mxl-decompile): убрать из вывода бессмысленный style и лишние объектные строки
Две правки читаемости DSL, обе замечены на живом выводе.

Ячейка получала "style": "default" — ссылку на стиль, которого в styles нет
вовсе. Условие сверялось с обнаруженным стилем пустых колонок, а он бывает
равен "default": строка такой стиль не пишет, а ячейка получала ключ на
пустое место. Теперь сверяемся со стилем, который РЕАЛЬНО раздаётся ячейкам.

Позиционная запись строки отбрасывалась, как только первая ячейка стояла не
в первой колонке. Но один-два null впереди обычно короче объектной записи
с col: строка вида { "cells": [{ "col": 2, "text": "…" }] } сворачивается
в [null, "…"]. Выбираем ту форму, которая короче.

Правка не должна менять скомпилированный XML — и не меняет: пилот из 40
макетов собрался побайтово так же, как до неё, при том что JSON изменился
у 31 макета. Стенд по-прежнему 7 из 10 байт в байт.

Попутно блок JSON-сериализатора в ps1 перенесён выше первого использования:
в PowerShell функция должна быть объявлена до вызова.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 20:59:54 +03:00
Nick ShirokovandClaude Opus 5 96e8616bb4 test(mxl): побайтовый раундтрип макетов, произведённых платформой
До сих пор строгая дорожка кампании жила только в отладочных прогонах, и её
некому было защищать: снэпшот сравнивает наш вывод с нашим же прежним выводом,
поэтому дрейф от платформы он не ловит.

Семь макетов, собранных вручную в Конфигураторе, положены в фикстуры. Кейс
декомпилирует такой макет и компилирует обратно, а результат сверяется с
исходником побайтово. Проверено, что сеть кусается: подмена одного тега в
фикстуре роняет кейс.

Раннеру добавлены две ручки:
- expect.filesEqual — сравнение двух файлов побайтово. Снэпшот его не заменяет:
  --update-snapshots молча принял бы расхождение с платформой;
- inputFrom — брать вход из файла в рабочем каталоге, а не из case.input.
  Нужно, когда вход производит preRun: case.input пишется ПОСЛЕ preRun и затёр
  бы его.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 20:27:58 +03:00
Nick ShirokovandClaude Opus 5 fc792bfee6 feat(mxl-compile,mxl-decompile): языки макета и шрифт как у платформы
Контролируемый стенд (upload/epf/МакетТабличныйДокумент) вскрыл четыре
системных расхождения подряд. После правок 7 макетов стенда из 10 проходят
раундтрип БАЙТ В БАЙТ обоими портами — до этого ни один.

1. Объявление языков макета жёстко писалось как «только русский». Это не то же
   самое, что textLanguages: почти во всех макетах ERP объявлен один ru, а текст
   лежит и под ru, и под en, поэтому выводить одно из другого нельзя. Заведены
   недокументированные ключи languages / currentLanguage / defaultLanguage —
   автору они не нужны, нужны раундтрипу. defaultLanguage несём, а не выводим:
   «всегда ru» — наблюдение на четырёх русских типовых, а не правило формата.

2. <font> писался в каждом формате. Платформа пишет его только когда шрифт
   задан: треть форматов корпуса (23 003 из 69 581) обходится без него.

3. Декомпилятор считал шрифт с индексом 0 «обычным» и не писал его в стиль.
   Нулевой шрифт вовсе не обязан быть обычным — в макетах стенда он курсивный
   и жирный, и начертание терялось.

4. Формат, несущий ТОЛЬКО шрифт, выглядел пустым: у ячейки он получал
   зарезервированное имя default и не записывался, у строки терялся целиком.

Остаток на стенде — три макета, и все три про одно: ссылка ячейки на запись
ширины, оставшуюся от прежнего состояния документа. Это не воспроизводится
принципиально, разобрано в docs/1c-spreadsheet-spec.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 20:08:28 +03:00
Nick ShirokovandClaude Opus 5 f72dd544dd fix(mxl-compile): не класть в палитру шрифт, на который никто не ссылается
Мы всегда заводили шрифт Arial 10 по умолчанию, даже когда его никто не
использует. Платформа так не делает: у макета без оформления элемента <font>
нет вовсе. Неиспользуемые шрифты теперь отбрасываются, ссылки перенумеровываются
(индексы шрифтов позиционные).

Проверено на контролируемом стенде: после правки простейший макет расходится
с платформенным ровно одной строкой — объявлением языков в шапке.

Кейс font-fractional-size дополнен: стиль, задающий ТОЛЬКО шрифт по умолчанию,
равнозначен отсутствию оформления (ячейка получает <f>0</f>), поэтому шрифт
остаётся неиспользованным. Чтобы кейс продолжал проверять целый размер, стилю
добавлено второе свойство.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 19:40:39 +03:00
Nick ShirokovandClaude Opus 5 3f80f936ae fix(mxl-compile): формат по умолчанию — последняя запись палитры
Мы регистрировали формат по умолчанию первым, из-за чего вся палитра шла
со сдвигом относительно платформенной. На корпусе он последний в 8285 макетах
из 10 863, первым — в 25.

Подтверждено контролируемым стендом: в макетах, где ширины заданы на уровне
документа, палитра идёт «ширины колонок в порядке колонок, умолчание последним».

Порядок палитры не виден в семантическом диффе (правило palette-index разрешает
ссылки и тем самым его прячет), поэтому мерялся отдельно: на пилоте совпавших
с оригиналом записей с начала палитры стало 467 из 2274 против 23, палитр,
совпавших целиком, — 4 против 2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 19:23:18 +03:00
Nick ShirokovandClaude Opus 5 250ec9ef0d docs(mxl): актуализировать спецификацию XML табличного документа
За кампанию XML-уровень изучен заметно глубже, чем был описан. Внесено то,
что проверено на корпусе ERP (10 924 макета) и на контролируемом стенде:

- <i> платформа пишет только при разрыве последовательности;
- <indexTo> схлопывает только ПУСТЫЕ строки — прежняя формулировка «строки
  с одинаковым содержимым» неверна: одинаковых непустых схлопнутых нет ни одной
  при 98 153 несхлопнутых;
- <f>0</f> — у ячейки формата нет вовсе, это не индекс записи;
- канонический порядок тегов внутри <format> (height раньше width);
- единица ширины — 1/8 символа;
- свёртка четырёх одинаковых сторон рамки в <border>;
- цвет — значение с префиксом пространства имён, web/win объявляются прямо
  на узле; в Form.xml те же цвета выглядят как web:/win:;
- формат строки несёт не только высоту, а формат колонки не только ширину;
  оформление строки материализуется и в ячейки, кроме hidden;
- columnsItem с formatIndex 0 и с индексом за пределами size;
- полный список стилей линии вместо Solid/None.

Отдельно описана ловушка: width в формате ЯЧЕЙКИ — устаревшая ссылка, она не
описывает итоговое состояние документа и воспроизведению не подлежит.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 19:08:52 +03:00
Nick ShirokovandClaude Opus 5 ba25aa4a00 feat(mxl-compile,mxl-decompile): формат самой строки
У строки есть собственный формат: платформа хранит в нём скрытие (17 423 вхождения
в корпусе), шрифт (9 216), фон, выравнивания, защиту. Мы писали туда только высоту,
всё остальное теряли.

Разбор на контролируемом стенде показал, что это два разных случая, а не один.
Оформление, применённое к строке целиком, платформа пишет И строке, И каждой
ячейке (backColor: 13 899 ячеек повторяют против 96). Скрытие — только строке
(24 026 против 62 856). Поэтому одного правила «rowStyle красит ячейки» мало.

Теперь rowStyle — стиль строки: по умолчанию ложится и на строку, и на ячейки,
как это делает платформа. Объектная форма { style, apply } задаёт исключения:
"row" — только строке, "cells" — только ячейкам. Модификатор нужен раундтрипу,
в описании DSL его нет.

Скрытие и высота — собственные свойства строки, ключами рядом: у ячейки таких
свойств не бывает, и в её оформление они не попадают.

На пилоте категория row[].formatIndex упала с 1964 до 201 потерянного факта.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 18:36:41 +03:00
Nick ShirokovandClaude Opus 5 ccbd488d63 fix(mxl-compile): у неоформленной ячейки формата нет вовсе
Ячейка без собственного оформления ссылалась на формат по умолчанию. Платформа
в этом случае пишет <f>0</f>, где ноль — не индекс записи, а «формата нет»:
на корпусе так у 170 710 ячеек против 50 635, ссылающихся на умолчание, и
<f>0</f> встречается в 71% макетов.

Подтверждено контролируемым стендом: в макете, где колонкам заданы ширины, а
ячейки ничем не оформлены, платформа пишет ячейкам ноль.

На пилоте верхняя категория row[].cell[].fmt упала с 63 900 до 55 855 фактов,
совпадение строк документа выросло с 16.7% до 17.5%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 17:32:56 +03:00
Nick ShirokovandClaude Opus 5 08503c72e9 docs(mxl): разделить справочник DSL и описать новые ключи
Справочник был одним файлом на 273 строки, а полная таблица свойств стиля его
бы удвоила. Разделён по частоте обращения, как в meta-compile: в инструкции
маршрутная таблица «что нужно → какой файл».

  reference/dsl-spec.md          — верхний уровень, области, строки, ячейки
  reference/styles.md            — шрифты, стили, цвет, рамка, колонки
  reference/format-properties.md — полный перечень свойств стиля

Описаны columnStyles и новая запись стиля: ключ = имя свойства как в выгрузке,
рамка пятью ключами, цвет в четырёх формах. Прежние align/valign/wrap работают
и дальше, но в описании их нет — иначе у модели появляется развилка.

Ограничения переписаны по факту: цвета, скрытие, отступ, посторонние рамки и
стили линий больше не теряются; зато честно названо то, что теряется до сих пор —
оформление строки помимо высоты.

Пример из спеки скомпилирован и проверен валидатором.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 17:14:31 +03:00
Nick ShirokovandClaude Opus 5 3c7f7345bb fix(mxl-compile,mxl-decompile): не терять колонку, объявленную без формата
Платформа иногда перечисляет колонку в раскладке с <formatIndex>0</formatIndex> —
колонка объявлена, формата у неё нет (4% макетов корпуса). Компилятор такие
опускал, декомпилятор их не видел, и элемент терялся целиком.

Выражается пустым значением в columnStyles. Для авторинга это бесполезно —
ни одна задача не звучит как «объяви колонку без свойств», — поэтому в описании
DSL записи нет: она нужна только чтобы раундтрип не терял байты.

На пилоте потери в категории colset[].col[].formatIndex упали с 39 до 35.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 17:08:03 +03:00
Nick ShirokovandClaude Opus 5 4e389d0ef1 feat(mxl-compile,mxl-decompile): columnStyles — оформление колонки
У ячейки есть style, у строки rowStyle, у колонки не было ничего — при том что
колонка ссылается в ту же палитру <format> и несёт те же свойства. В корпусе
ERP колонки используют шрифт (3006 форматов), выравнивание, рамки, скрытие и
прочее; всё это терялось при раундтрипе.

Колонка получает такой же именованный стиль: columnStyles рядом с columnWidths,
ключи той же грамматики диапазонов ("1", "2-8", "5,7,9"), значение — имя из
styles. Внутри columnSets тот же ключ. Формат колонки собирается из ширины и
свойств стиля: запись в палитре одна.

Шрифт по умолчанию колонке не навязываем — формат колонки без оформления это
ровно <width>, как пишет платформа.

Попутно два дефекта декомпилятора: стиль колонки не учитывался при отсечении
неиспользуемых стилей и пропадал целиком, а набор из одних неприметных свойств
(отступ, защита) получал зарезервированное имя default и тоже терялся.

На пилоте потери в категории colset[].col[].formatIndex упали с 86 до 39.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:57:52 +03:00