Commit Graph
54 Commits
Author SHA1 Message Date
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 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 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 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 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
Nick ShirokovandClaude Opus 5 d3c16f56c5 feat(mxl-compile): стиль умеет все свойства формата, а не семь
Стиль описывал 7 свойств из 47, которые платформа хранит в <format>. Не было
самого частого тега корпуса — backColor (1.7 млн вхождений), а также textColor,
textPlacement, protection, hidden, indent, borderColor, textOrientation и хвоста.

Теперь ключ стиля — имя тега платформы, без исключений: помнить, какие ключи
названы по-своему, больше не нужно. Прежние align/valign/wrap продолжают
работать молча, как синонимы; там же CSS-имена (background, color,
border-bottom) и русские имена свойств. Таблицы типов значений и допустимых
значений перечислений сняты с корпуса, а не выписаны на глаз.

Рамка: пять плоских ключей (border и четыре стороны) со значением
{ style, width } либо строкой стиля; линия регистрируется в палитре <line>,
которая раньше знала только «тонкую» и «толстую» Solid. Четыре одинаковые
стороны сворачиваются в один <border> — правило проверено на корпусе:
70 265 свёрнутых форматов против 36 783 записанных по сторонам, и среди
вторых нет ни одного с четырьмя совпадающими значениями.

Цвет — нотация самой платформы: #RRGGBB, style:Имя, web:Имя, win:Имя. Первые
две пишутся дословно (префикс style объявлен в корне документа), для web и win
платформа дописывает объявление xmlns прямо на узел — делаем так же.

containsValue / valueType / controlType намеренно не заведены: это свойства
конкретной ячейки, а стиль — сущность общая, один на многие ячейки.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:34:54 +03:00
Nick ShirokovandClaude Opus 5 c559696c5a refactor(mxl-compile): формат — таблица тегов вместо двенадцати полей
Формат был фиксированной записью из 12 полей, выписанной руками в четырёх
местах: ключ дедупликации, набор свойств, резолв стиля и эмиссия палитры.
Добавление свойства стоило восьми правок в двух портах, а свойств у формата
в выгрузке 47.

Теперь формат — набор «тег платформы → значение», а порядок эмиссии задаёт
один канонический список тегов. Список снят с корпуса ERP: 766 960 форматов,
ни один не нарушает эту последовательность.

Вывод не меняется: пилот из 40 макетов скомпилировался побайтово так же, как
до правки, обоими портами.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 14:45:30 +03:00
Nick ShirokovandClaude Opus 5 cd9a800f8b fix(mxl-compile): писать номер колонки и пустые строки как платформа
Две правки эмиссии, обе выведены из корпуса erp_8.3.24 (10924 макета,
15.2 млн ячеек) и проверены на нём же без единого контрпримера.

Номер колонки <i> платформа пишет только при разрыве последовательности:
подряд идущая ячейка его не несёт, читатель ведёт счётчик сам. Мы писали
всегда — из 1.2 млн записанных платформой номеров ни один не избыточен.

Подряд идущие одинаковые ПУСТЫЕ строки платформа хранит одним rowsItem
с indexTo; непустые не схлопывает, даже когда они совпадают (98153 таких
случая в корпусе). Мы не эмитили indexTo вовсе.

На пилоте (40 макетов) доля строк документа, совпавших с оригиналом
байт в байт, выросла с 0.3% до 16.7%; все 9 диапазонов indexTo оригинала
воспроизведены точно.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 13:04:59 +03:00
Nick ShirokovandClaude Opus 5 3e7e105c43 fix(mxl-compile): писать ошибки в stderr напрямую, а не через Write-Error
Write-Error оборачивает сообщение в ErrorRecord: приписывает имя скрипта,
добавляет хвост CategoryInfo и ломает текст по ширине окна консоли. Длинные
сообщения приезжали разорванными посреди слова, и по ним нельзя было
ни искать подстроку, ни читать их глазами.

Остальные четырнадцать мест в файле уже писали через Console.Error —
приведены оставшиеся шесть. Тексты сообщений не менялись.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 12:16:42 +03:00
Nick ShirokovandClaude Opus 5 683ed8e08d feat(mxl-compile,mxl-decompile): textLanguages — текст строкой на всех языках макета
Строка в text/template разворачивается по одному элементу на каждый язык из
документного ключа textLanguages (по умолчанию только ru). Декомпилятор собирает
набор языков по всем текстам макета и пишет строкой текст, одинаковый на всём
наборе; различающийся остаётся объектом.

Языки текстов и языки, объявленные в конфигурации, — разные вещи: типовые ERP
объявляют один ru, а тексты хранят под ru и en. Набор постоянен внутри
конфигурации, поэтому он документный, а не вычисляемый: к моменту компиляции
тексты уже свёрнуты в строки.

Заодно: эмиссия tl проверяет НАЛИЧИЕ ключа text/template, а не истинность —
пустая строка это текст, платформа такие ячейки пишет с пустым tl, а по
истинности он терялся.

Проверено на пилоте (40 макетов ERP): скомпилированный XML совпадает с прошлым
прогоном байт в байт обоими рантаймами, объём JSON −47%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 12:00:20 +03:00
Nick ShirokovandClaude Opus 5 4cceb03269 feat(mxl-compile,mxl-decompile): позиционный список ячеек в выводе декомпилятора
Компилятор давно принимал короткую форму, а декомпилятор всегда писал самую
развёрнутую. Теперь он выбирает самую короткую из применимых, как это делает
skd-decompile.

Короткая запись стала свойством СПИСКА ЯЧЕЕК, а не строки: cells принимает
позиционный список, поэтому строка с height или rowStyle тоже может им пользоваться.
Раньше наличие свойств строки лишало её короткой записи — искусственная связка.
Если же своих свойств у строки нет, она пишется просто массивом, без ключа cells.

Позиционный список опознаётся по наличию элемента-строки или null; список из одних
объектов читается как обычный — для простой строки обе трактовки дают один результат.

Три дефекта, вскрытых проверкой на потери (каждый раз XML переставал совпадать с
прежним, и это приводило к причине):

- компилятор двигал позицию на единицу за элемент, поэтому объектный элемент со
  span: 3 занимал одну позицию вместо трёх и всё правее него уезжало влево. У маркеров
  ">" этого не было — каждый съедает свою позицию;
- проход «удалить неиспользуемые стили» не заглядывал в строки-массивы: у массива нет
  свойства cells, и стиль, использованный только внутри такой строки, вырезался как
  неиспользуемый;
- в py тот же проход падал на позиционном списке — "style" in c для None бросает
  TypeError, а для строки делает подстрочный поиск.

Плюс ловушка PowerShell: += разворачивает вложенный массив, и строка-массив
расплющивалась в список строк — нужна запятая.

Проверено: XML на всех 40 макетах пилота совпал с прежним БАЙТ В БАЙТ, то есть
сокращение без потерь. Объём декомпилированного JSON меньше на 11%. Тесты 54/54 на
обоих рантаймах, гарды зелёные.

В WORKFLOW уточнена находка про паритет портов: декомпиляторы расходятся по JSON на
33 макетах из 40, а компиляторы на одном и том же входе — всего на 2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:35:58 +03:00
Nick ShirokovandClaude Opus 5 1e06e43e92 feat(mxl-compile,mxl-decompile): многоязычный текст ячейки
Платформа хранит текст ячейки по элементу на язык. Декомпилятор брал ПЕРВЫЙ и терял
остальное, компилятор писал всегда ru захардкоженно. В корпусе ERP текст лежит и под
ru, и под en у 10 730 макетов из 10 924 — то есть терялось у 98%.

Форма взята готовая — конвенция ML-значений из docs/meta-dsl-spec.md §4.4, по которой
уже живут synonym, tooltip и title в метаданных и формах: строка означает русский
текст, объект даёт по надписи на язык в порядке ключей. Новых понятий не вводили,
ключи те же (text и template).

Документный ключ для языков НЕ заводили, и это измерение, а не экономия: блок
languageSettings мы и так эмитим байт в байт как платформа (currentLanguage ru,
defaultLanguage ru, один languageInfo). Отклонения редки — 8 макетов ERP с
currentLanguage en, 3 без него, 157 с объявленной парой ru+en, всего около 1,6%.
Заодно это снимает развилку из разбора PR #62: список языков компилятор дописывать
не должен, платформа его не дописывает.

Эффект на пилоте: категория row[].cell[].tl упала с 47 252 до 12 488 — минус 74%,
самое крупное улучшение кампании. Остаток частично классифицирован: 348 строк — ячейки
с ПУСТЫМ <tl> у оригинала, которых мы не эмитим вовсе; полностью разобрать мешает
обрезка диффа по 400 строк на объект.

Снэпшоты не дрейфанули: кейсы пишут текст строкой, и вывод для неё прежний.

Проверено: тесты 54/54 на обоих рантаймах, verify-snapshots 24/24, вывод портов
совпадает байт в байт и в компиляции, и в декомпиляции.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 20:52:03 +03:00
Nick ShirokovandClaude Opus 5 572b75984a fix(mxl-compile): неоформленная ячейка ссылается на формат по умолчанию
Платформа держит для неоформленного макета ОДИН формат — ширину колонки, он же
defaultFormatIndex, и все ячейки указывают на него. Компилятор заводил каждой ячейке
собственный формат с <font>0</font>, где ноль означает «шрифт не задан», то есть
формат был пуст по смыслу и отличался от умолчания только своим существованием.

Теперь ячейка, у которой не задано ничего (шрифт умолчательный, нет рамок,
выравнивания, переноса, типа заполнения и формата числа), ссылается на
defaultFormatIndex. На минимальном макете палитра сократилась с двух форматов до
одного и совпала по форме с платформенной.

На корпусе правка закрывает случай неоформленной ячейки начисто: на макете
АтрибВыгрузкиXML2015Кв1 расхождения по формату исчезли полностью, блок диффа
сократился с 75 строк до 40, и всё оставшееся — многоязычный текст. В агрегате по
пилоту это 497 строк из 64 865: остальное держат макеты, где ячейки реально
оформлены, и там расходится состав самих форматов — отдельная категория.

Дрейф 18 снэпшотов разобран построчно, посторонних строк нет: 170 сменившихся
индексов <f>, 36 границ <format> (палитра стала короче), 18 удалённых <font>.

Проверено: тесты 53/53 на обоих рантаймах, verify-snapshots 23/23.

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

Но выводился он неправильно: сырой хэш форматировался как UUID, без битов версии и
варианта. В выдаваемом значении версия оказывалась 7, вариант 1 — таких не бывает,
то есть формально это был не UUID, а шестнадцатеричная строка нужной формы. Платформа
такое принимает (проверено сертификацией), но упереться в валидатор мы могли в любой
момент — ровно того же сорта риск, что сертификация уже ловила в этой ветке.

Теперь это штатный UUID версии 3 (имя + MD5, RFC 4122): для задачи «вывести
идентификатор из имени» существует именно он. Детерминированность сохранена, оба
порта дают одинаковое значение.

Проверено: тесты 53/53 на обоих рантаймах, verify-snapshots 23/23.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 20:07:02 +03:00
Nick ShirokovandClaude Opus 5 cbe7abdcbb fix(mxl-compile): не эмитить fillType=Text, UUID для идентификатора раскладки
Две правки по корпусу и по 1С-сертификации.

fillType=Text платформа практически не пишет: на выборке корпуса 344 981 текстовая
ячейка из 348 023 (99,1%) ссылается на формат БЕЗ fillType — наличие <tl> и так
означает текст. Parameter и Template платформа пишет, их оставляем. Правило то же,
что и везде: эмитим то, что эмитит платформа, а не то, что верно в рантайме.

Идентификатор колоночной раскладки платформа хранит как UUID и другой не принимает —
макет с <id>узкая</id> она отвергала целиком. Имя, похожее на UUID (то есть пришедшее
декомпиляцией), проходит насквозь, иначе поехал бы раундтрип; читаемое имя автора
превращается в UUID ДЕТЕРМИНИРОВАННО, из хэша имени, чтобы повторная компиляция
давала тот же файл. Оба порта дают одинаковый хэш.

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

Дрейф 33 снэпшотов разобран построчно, посторонних строк нет: 116 сменившихся
индексов формата, 49 удалённых fillType, 43 перестановки свойств внутри палитры,
16 границ <format> (палитра стала короче).

Проверено: тесты 53/53 на обоих рантаймах, verify-snapshots 23/23.

Замечание на будущее: на корпусе эта правка снимает лишь 170 расхождений из 64 865
в категории формата ячейки. Основная причина другая — платформа даёт неоформленной
ячейке ссылку на формат по умолчанию, а компилятор заводит ей собственный формат
с <font>. Это отдельный заход, он сдвинет снэпшоты повторно.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 19:58:35 +03:00
Nick ShirokovandClaude Opus 5 8302b17814 fix(mxl-compile): внятный отказ на объектную форму columnSet
Раскладка адресуется именем из columnSets. Инлайновой формы в этом DSL нет ни у
чего: style у ячейки и font внутри стиля тоже только имена — именованные сущности
объявляются один раз и всюду адресуются по имени. Объектная форма стала бы второй
формой одного и создала бы асимметрию «раскладку инлайнить можно, а стиль нельзя».

Объект в columnSet и раньше не портил вывод — навык падал с «Unknown 'columnSet'»,
подставляя сериализованный объект. Но сериализация у портов разная (@{columns=5}
против {'columns': 5}), то есть сообщения расходились. Теперь это отдельная проверка
с текстом, одинаковым в обоих портах и прямо называющим правило.

Кейсы: column-sets (позитивный, ссылка и объявление доезжают до XML) и
error-columnset-inline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 19:26:01 +03:00
Nick ShirokovandClaude Opus 5 a1642cbe60 feat(mxl-compile,mxl-decompile): колоночные раскладки
Инкремент B кампании mxl-roundtrip. Табличный документ может задавать индивидуальные
ширины колонок для группы строк — в XML это несколько элементов <columns>, из которых
раскладка БЕЗ <id> является умолчанием (ровно одна в каждом макете корпуса), а остальные
адресуются GUID, на который ссылаются строки и области. Декомпилятор читал только первый
<columns>: ширины прочих раскладок терялись, а на макетах с пустым умолчанием он отдавал
columns: 0. Несколько раскладок есть у 63% макетов ERP.

DSL: документные columns/columnWidths — раскладка по умолчанию, дополнительные объявляются
в columnSets (ключ — идентификатор), область ссылается ключом columnSet. Форма повторяет
уже принятую в этом DSL пару «словарь именованных штук наверху — ссылка по имени на
потребителе», как styles/style и fonts/font.

Склейки раскладок по содержимому НЕТ, и это исправление собственной ошибки: замер, на
котором строилось прежнее решение, вырезал идентификатор через findtext('columnsID'),
тогда как у набора он называется <id> (columnsID — имя только у ссылок). Вырезание не
срабатывало, и наборы «различались» просто из-за разных GUID. Правильный замер: в 337
макетах из 517 есть наборы с полностью одинаковым содержимым, до 59 дублей в одном
макете. Содержимое раскладку не опознаёт — опознаёт идентификатор.

Попутно закрыты два дефекта, вскрытые корпусом:

- проверка обязательных полей смотрела на истинность значения, а не на наличие ключа:
  columns: 0 — осмысленная величина (умолчание пустое, все строки в именованных
  раскладках), и компилятор объявлял её отсутствующей. Тот же класс, что был с col;

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

На пилоте из 40 макетов ERP отказов больше нет ни одного (было 26): цикл проходят все 40
на обоих рантаймах. Скомпилированный XML портов совпадает байт в байт.

Записана находка (debug/mxl-roundtrip/WORKFLOW.md): порты mxl-decompile расходятся по
выходному JSON на 11 макетах из 12 — порядок стилей в словаре, при идентичном XML.
Не ловилось потому, что кейсы снэпшотят входной Template.xml, а не выходной JSON.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 19:01:47 +03:00
Nick ShirokovandClaude Opus 5 720ea05325 feat(mxl-compile,mxl-decompile): безымянные блоки и области координатами
Инкремент A кампании mxl-roundtrip. Блочная форма DSL не выражала больше половины
корпуса: у 34% макетов ERP есть строки вне именованных областей, у 21% нет ни одной
области типа Rows. Декомпилятор такие строки терял, а на макетах целиком из
Rectangle отдавал areas: [] — компилятор отвечал "Required field 'areas' is missing".
На пилоте из 40 макетов это 10 отказов из 26.

Что сделано:

- имя у блока стало необязательным. Блок без имени — просто кусок сетки; именованную
  область он не создаёт. Отдельный «плоский режим» не нужен: макет без выразимых
  блоков это один безымянный блок;

- namedAreas — именованные области координатами, для всего, что блоком не ложится
  (не-Rows и пересекающиеся). Тип области НЕ указывается: он выводится из заданных
  осей, ровно как в ТабличныйДокумент.Область() — только строки дают полосу строк,
  только колонки полосу колонок, обе оси прямоугольник. Так нельзя написать
  противоречие вроде type: Rows с колоночными координатами;

- диапазон записывается уже существующей грамматикой DSL (как ключи columnWidths):
  число или "N-M". Список через запятую запрещён — область непрерывна, платформа
  разрывную не хранит;

- прощающим вводом принимается платформенный адрес "R1C1:R2C2" и правило «0 значит 1»;
  в документацию не вынесено;

- декомпилятор перестал пропускать области не-Rows (там стоял безусловный continue) и
  режет сетку на блоки детерминированно: непересекающиеся Rows задают границы, дыры
  становятся безымянными блоками, остальное уходит в namedAreas.

Отдельно: именованные элементы теперь эмитятся отсортированными по имени. Платформа
хранит их именно так — на выборке 541 макета с несколькими элементами иного порядка
нет ни разу. Сортировка ординальная и регистронезависимая; Sort-Object по умолчанию
сортирует по текущей культуре и на кириллице дал бы другой порядок. Отсюда дрейф 13
снэпшотов — чистая перестановка, диффы симметричны, число элементов не изменилось.

На пилоте отказы areas: [] закрыты полностью (10 → 0), цикл переживают 24 макета
вместо 14. Оставшиеся 16 отказов — колоночные раскладки, это инкремент B.

Правка на ps1, зазеркалена в py; вывод портов и в компиляции, и в декомпиляции
совпадает байт в байт.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 16:48:33 +03:00
Nick ShirokovandClaude Opus 5 f6a478c85c fix(mxl-decompile,mxl-compile): дробный размер шрифта
Размер шрифта в платформенных макетах бывает дробным (8.3, 6.8, 9.8, 11.3, 14.3).
Оба навыка приводили его к целому: py падал голым ValueError на int(), ps1 ТИХО
округлял через [int] — снова шумный отказ против тихой порчи, как было с col.

Найдено раундтрипом по корпусу ERP 8.3.24: на стратифицированной выборке из 40
макетов это давало 12 отказов декомпиляции — 30% выборки и ВСЕ отказы этого этапа.
После правки декомпиляция не падает ни на одном, цикл переживают 14 макетов
вместо 10.

Размер читается инвариантной культурой и остаётся целым, когда дробной части нет,
иначе "10" превратилось бы в "10.0". Эмиссия в XML тоже переведена на инвариантную
культуру: интерполяция строкой отдавала бы "8,3" под русской локалью.

Правка на ps1, зазеркалена в py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 15:03:15 +03:00
Nick ShirokovandClaude Opus 5 6719d7e647 feat(mxl-compile): короткая форма строки — массив ячеек
Строка, записанная массивом ("rows": [["А","Б","В"]]), молча превращалась в
пустую строку в обоих портах. Форма не выдумана: ровно так документированы
макеты в skd-compile, и привычка естественно переносится на табличный документ.

Масштаб потери был виден в самом репозитории — ПЯТЬ кейсов семейства mxl-*
написаны в этой форме и потому проверяли пустые макеты: снэпшот
lenient-key-case содержал единственную строку <empty>true</empty>. После правки
все пять снэпшотов наполнились ячейками, которые терялись.

Семантика повторяет skd-compile: позиция ячейки — индекс в массиве, ">"
продолжает ячейку слева (span), "|" — сверху (rowspan), null пропускает
колонку, "{Имя}" даёт параметр. Элемент можно записать и объектом, когда нужен
style или detail; col в нём запрещён — это смешение двух способов адресации.
Разворот идёт отдельным пред-проходом в обычные строки с явными col/span/rowspan,
поэтому остальной компилятор о короткой форме не знает.

Форма документирована — в отличие от прощающего пропуска col: модель пишет канон
соседнего навыка, и выравнивание двух табличных DSL убирает развилку. По каскаду
в SKILL.md только правило и пример, подробности и таблица ошибок — в спеке
(обе копии: docs/ и reference/ навыка).

Правка на ps1, зазеркалена в py: вывод портов совпадает байт в байт, decompile →
compile возвращает исходный XML байт в байт.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 14:07:51 +03:00
Nick ShirokovandClaude Opus 5 f33436adff feat(mxl-compile): прощающий ввод — ячейка без col
Ячейка без col роняла py голым KeyError, а ps1 молча брал null как 0 и писал
Col = -1 — битую ячейку без единого сообщения. При этом опустить col модели
естественно: DSL устроен как HTML-таблица (rows → cells), и одиночный заголовок
или обычная строка пишутся без позиций.

Строка, в которой col нет НИ У ОДНОЙ ячейки, теперь раскладывается слева направо
с учётом span и занятых сверху rowspan-колонок. Смешанную строку не угадываем —
это опечатка; переполнение columns и явный col вне 1..columns тоже дают внятную
ошибку в stderr вместо тихой порчи.

Документация не менялась: col остаётся единственной каноничной формой, прощающий
ввод живёт только в коде — как регистронезависимость ключей DSL. Второй способ
адресации в SKILL.md превратил бы одно правило в развилку.

Правка сделана на ps1 и зазеркалена в py; вывод портов на общем примере совпадает
байт в байт.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 13:04:03 +03:00
Nick ShirokovandClaude Opus 5 57b0f95b5f fix(skills): регистронезависимые ключи DSL в py-портах
Дорожка DSL из кампании паритета. PowerShell читает свойства объекта из
ConvertFrom-Json без учёта регистра, Python — точным совпадением, поэтому
"Name" вместо "name" в py-порте молча не находился: навык печатал [OK], а
свойство в выход не попадало. На skd-compile это давало схему без запроса.

CIDict + ci_json (эталон — meta-compile) внесены в 10 портов, читающих
пользовательский DSL: cf-edit, form-compile, form-edit, interface-edit,
meta-edit, mxl-compile, role-compile, skd-compile, subsystem-compile,
subsystem-edit. Обёрнуты точки разбора DSL, операций и JSON, приходящего
значением параметра; реестр .v8-project.json намеренно не трогаем.

skd-edit в список не входит: он принимает операцию через -Operation с
choices, что уже закрыто ci_parse_args.

Каждому навыку добавлен кейс lenient-key-case: вход с перемешанным регистром
ключей, эталон один на оба порта — раннер гоняет обе среды, поэтому кейс сам
по себе проверяет паритет.

Проверка: 683/683 (PS), 680+3 skipped (PY), гарды 4/4, версии портов совпадают.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 20:15:39 +03:00
Nick ShirokovandClaude Opus 5 d325a3d5af fix(skills): регистронезависимые параметры во всех py-портах
Дорожка CLI из кампании паритета: PowerShell не различает регистр ни в именах
параметров, ни в значениях [ValidateSet], argparse различает и в том и в
другом. Проверено на читающем навыке: cf-info -Mode BRIEF на PS отрабатывает,
на py падал.

ci_parse_args (эталон — meta-compile) вставлен в 68 py-портов, вызовы
parser.parse_args заменены. Навыков с DSL меньше трети, поэтому именно эта
правка делает паритет общим: *-info, *-validate, db-*, cfe-*, web-* тоже
принимают ввод так же, как их .ps1.

Реестр check-inline-drift дополнен списком потребителей — и сразу окупился:
поймал, что массовая замена переписала последнюю строку внутри самого
хелпера и копии стали рекурсивными.

Версии бампнуты синхронно в обоих портах всех 68 навыков.

Проверка: 673/673 (PS), 670+3 skipped (PY), гарды 4/4; паритет версий портов
проверен по заголовкам.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 20:00:31 +03:00
Nick ShirokovandClaude Opus 5 01749ad6ee refactor(skills): свести write_xml_file и write_utf8_bom к общему эталону
У write_xml_file в docstring прямо написано «Держать копии одинаковыми —
сознательно: разошедшиеся копии сводят на нет весь смысл», и при этом копий было
три варианта. Различие оказалось не в логике нормализации, а в имени
вспомогательной функции записи байт: write_utf8_bom (11 навыков),
write_text_with_bom (form-add, help-add, template-add), save_text_bom
(cfe-borrow) — тела всех трёх побайтово эквивалентны.

Имя сведено к write_utf8_bom, тело помощника — к общему эталону (различались
только имена переменных). PS1-порт Write-XmlFile расхождений не имел.

Обе семьи переведены из храповика в реестр с эталоном: 24 семьи, 309 копий,
разъехавшихся осталось 4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 12:29:43 +03:00
Nick ShirokovandClaude Opus 5 974ca3aac9 fix(form,skd,init,meta-edit): развести экранирование атрибута и текста
esc_xml был один на две задачи, и в атрибутном контексте применялся текстовый
вариант без &quot;. form-compile с choiceParameters, имя которого содержит
кавычку, выдавал <app:item name="Отбор.Наименование "кавычка"">: lxml падает с
attributes construct error, то есть файл невалиден как XML. Затронуто 9 мест в
form-compile и 3 в skd-compile, одинаково в обоих портах.

Раундтрип через базу показал границу: в ТЕКСТЕ элемента платформа экранирует
только & < > (кавычка и апостроф возвращаются сырыми байт-в-байт), а в ЗНАЧЕНИИ
АТРИБУТА пишет &quot; — внутри "..." литеральная кавычка невалидна. Поэтому две
функции с говорящими именами, а не одна с флагом:
  esc_xml      — значение атрибута: & < > "
  esc_xml_text — текст элемента:    & < >

Заодно закрыто расхождение портов в init-навыках: PY экранировал текст с &quot;,
PS1 — через SecurityElement::Escape (ещё и &apos;), а epf-init/erf-init в PS1 не
экранировали вовсе, из-за чего амперсанд в синониме давал невалидный XML.

Кейсы: form-compile/attr-value-escaping, skd-compile/additional-properties-escaping,
cf-init/synonym-escaping — все три проверены негативным прогоном на обоих портах
и верификацией снэпшотов на платформе.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 20:53:12 +03:00
Nick ShirokovandClaude Opus 5 3ef9e76f03 feat(epf-init,erf-init,form-add,form-compile,template-add,mxl-compile,help-add): версия формата у автономных EPF/ERF
Детекторы версии искали только Configuration.xml, поэтому внутри автономной
внешней обработки или отчёта всегда получалось 2.17 — независимо от version в
корне самого объекта. Теперь версия наследуется от корня EPF/ERF: подъём по
дереву проверяет и <каталог>.xml с корнем ExternalDataProcessor/ExternalReport,
а form-add и template-add читают её прямо из файла объекта, с которым работают.

Только после этого у epf-init/erf-init появился параметр -FormatVersion
(2.17…2.21, дефолт 2.17 — прежнее поведение). Без наследования он дал бы
обработку 2.21, внутри которой формы и макеты остаются 2.17.

Проверено сквозным прогоном: epf-init -FormatVersion 2.21 → form-add →
template-add → mxl-compile, все файлы 2.21 с xmlns:pal, порты идентичны.

Снэпшоты role-info/role-validate/cf-edit обновлены — их фикстуры строит
role-compile, дрейф только от смены его формата на платформенный.

648/648 ps1, 645/648 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 21:41:42 +03:00
Nick ShirokovandClaude Opus 5 5c3449d6c2 feat(mxl-compile): определение версии формата + xmlns:pal в 2.21
У корня <document> нет атрибута version, поэтому версия берётся из конфигурации,
в дерево которой пишется макет (подъём до Configuration.xml, как в остальных
навыках); вне конфигурации остаётся прежнее поведение — 2.17.

Платформа 8.5 объявляет палитру и в теле MXL: в выгрузке УНФ 8.5 xmlns:pal стоит
у всех 3471 файлов с корнем document.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 21:01:45 +03:00
Nick ShirokovandClaude Opus 5 4833c3b707 fix(mxl-compile): PS-порт не создавал каталог назначения
Наполнение макета по свежему пути (Templates/<Имя>/Ext/Template.xml) на
PowerShell падало с «Could not find a part of the path». py-порт каталог
создаёт, form-compile и skd-compile — тоже; не создавал только этот порт.

Тестами не ловилось: во всех кейсах mxl-compile писал в уже существующий
каталог. Добавлен кейс с записью по несуществующему пути — проверено, что на
коде до правки он падает.

Найдено при разборе вопроса, нужна ли заглушка тела макета в template-add:
оказалось, заглушка держала не замысел, а этот дефект.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:11:11 +03:00
Nick ShirokovandClaude Opus 5 0b8231c467 fix(17 навыков): CRLF в разделителях строк XML (#57)
Головной дефект тикета: cfe-init выдавал Configuration.xml с 10 CR на 70 строк, а
роль и язык — вовсе без CR. Теперь 70/70, последний байт `>`.

Правило: меняем только РАЗДЕЛИТЕЛИ строк, содержимое текстовых узлов не трогаем.
Платформа его не трогает тоже — в запросах СКД из чистой выгрузки встречаются и
CRLF, и одиночный LF. Поэтому сплошная нормализация файла применяется только там,
где многострочных текстовых узлов нет по построению (скелеты), а в объектном XML
и формах разделители правятся в точке сборки.

Источников оказалось четыре, а не один:

1. Скелеты (cf-init, cfe-init, epf-init, erf-init, form-add, help-add,
   template-add) собираются here-string'ами, а .ps1/.py в репозитории хранятся с
   LF — отсюда LF и смешанный EOL.
2. py-порты склеивали документ через '\n'.join(lines); PS в тех же местах давал
   CRLF через AppendLine — порты расходились побайтово.
3. Билдеры Predefined в meta-compile собирались с явным LF и даже сворачивали
   CRLF→LF из общего эмиттера типов.
4. Чтение существующего файла в python БЕЗ newline='' молча схлопывает CRLF в LF
   (универсальные переводы строк), и запись потом кладёт LF. Так role-compile и
   subsystem-compile переписывали в LF весь Configuration.xml. meta-compile это
   уже делал правильно — там newline='' стоял с фикса #44/#46/#47.

Отдельно: XML-парсер по спецификации схлопывает CRLF при разборе, поэтому
lxml-порты (xdto-compile, xdto-edit) отдавали LF-документ там, где .NET возвращал
CRLF через NewLineHandling. Восстанавливаем EOL исходного файла.

Разделение канона и сохранения стиля:

- файл СОЗДАЁМ — канон (CRLF, без хвоста);
- существующий ПРАВИМ — наследуем его EOL, включая перевод строки вставки
  (контракт #44/#46/#47). Иначе LF-проект получал бы смешанные файлы — ровно то,
  на что заведён #57. Кейсы roundtrip-crlf-preserve остаются зелёными.

Хелпер записи скопирован в каждый навык (навыки автономны). У meta-compile он
называется Write-XmlFileKeepEol / write_xml_file_keep_eol: там нормализовать EOL
НЕЛЬЗЯ (многострочные запрос, синоним, значение заполнения), и одинаковое имя при
разном поведении было бы ловушкой.

Заодно: имя временного файла батча в meta-compile.ps1 получило GUID. Фиксированное
"meta-compile-batch-$idx.json" в общем %TEMP% сталкивало два параллельных запуска
(«file is being used by another process») — из-за этого полный набор приходилось
гонять с урезанной параллельностью. py-порт уже брал mkstemp.

Аудит: было ~250 файлов с дефектом EOL, стало 0 в обоих портах. Осталось четыре
законных случая — фикстуры roundtrip-crlf-preserve (сохранение стиля) и запрос
динсписка с многострочным текстом. Дрейф снэпшотов — 1 файл. Тесты 641/641.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:37:26 +03:00
Nick ShirokovandClaude Opus 5 abe9431d8f fix(9 навыков): без хвостового перевода строки в конце XML (#57)
Конфигуратор не пишет перевод строки в конце файла — последний байт `>`. Это видно
на чистой выгрузке пустой ИБ (Windows и macOS) и на всех 8 выгрузках в cfsrc.
Наши эмиттеры добавляли лишний, причём оба порта и по разным причинам:

- PS: StringBuilder.AppendLine дописывает Environment.NewLine после последней строки;
- PY: '\n'.join(lines) + '\n' — хвост добавлен явно.

В meta-compile заведена единая точка записи Write-XmlFile: там документ собирают
и AppendLine, и явные `n в билдерах Predefined/Content/Flowchart, а выходов семь.
В остальных обрезка стоит в самом вызове записи.

Модули .bsl, JSON-выход form-compile, HTML-справка help-add и XSD из xdto-decompile
сюда НЕ входят: канон Конфигуратора описывает XML метаданных, а у этих артефактов
свои правила. Результаты XmlDocument.Save тоже не трогаются — у них хвоста нет.

Навыки редактирования продолжают сохранять стиль ВХОДНОГО файла: кейс
subsystem-edit/roundtrip-crlf-preserve, где фикстура намеренно с хвостом, остаётся
зелёным. Это контракт #44/#46/#47, и он не отменяется.

Дрейф снэпшотов: 547 файлов, у каждого только последняя строка, содержательных
изменений ноль. Тесты 641/641.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 13:53:05 +03:00
Nick ShirokovandClaude Opus 5 919d49fe14 fix(form-edit,meta-edit,mxl-compile,role-compile,subsystem-*): кавычки в тексте не экранируем
Правило платформы единое и подтверждено дважды. По корпусу трёх конфигураций:
92142 сырых кавычки в тексте элементов и НИ ОДНОЙ &quot;; в условиях RLS —
16334 сырых против нуля экранированных (при этом &amp; платформа пишет, то есть
амперсанд экранируется, а кавычка нет). Загрузка на стенде: оба варианта
принимаются, но выгружает платформа сырую кавычку — то есть &quot; не ошибка,
а лишний шум в роундтрипе.

Решение было принято раньше в form-compile (там оно и записано комментарием) и
в skd-compile/skd-edit, но шесть навыков из него выпали. Приведены к общему виду:
где Esc-Xml использовался только для текста — функция стала текстовой; в meta-edit,
где она нужна и для атрибута, текстовые места переведены на существующий
Esc-XmlText. Атрибуты нигде не затронуты: там экранирование кавычек обязательно.

Дрейф эталонов — три строки, все условия RLS; role-info и role-validate строят
фикстуры прогоном role-compile (кросс-навыковый пересъём).

Проверка: полная сюита 630/630 на PowerShell и 627+3 skipped на python;
1С-сертификация role-compile 9/9, subsystem-compile 9/9, subsystem-edit 6/6,
form-edit 6/6, meta-edit 16/16. В mxl-compile кейс guard-allow-external падает
и до правки — не связан.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 15:08:55 +03:00
Nick ShirokovandClaude Opus 4.8 ddc1641176 fix(support-guard): не блокировать автономные внешние обработки/отчёты (#39)
При поиске корня конфигурации guard поднимался по дереву вверх и «проскакивал»
собственный корень автономной внешней обработки/отчёта (ExternalDataProcessor /
ExternalReport), лежащей внутри дерева выгрузки конфигурации. Если у охватывающей
конфигурации выключена возможность изменения (G=1), внешний объект ложно
блокировался как «объект типовой конфигурации на поддержке», а info-навыки
выводили нерелевантную строку «Поддержка: конфигурация read-only».

Теперь climb останавливается на границе автономного объекта: если целевой файл или
встреченный по пути <каталог>.xml имеет корень ExternalDataProcessor/ExternalReport,
подъём прекращается и объект не привязывается к конфигурации. Корень внешнего объекта
всегда глубже Configuration.xml, поэтому встречается первым — регрессии для обычных
объектов конфигурации нет.

Синхронно во всех копиях guard-а (навыки автономны): хук support-state.mjs
(decideSupport + findConfigRoot), 16 мутаторов (Assert-EditAllowed), 5 info-навыков
и meta-info (Get-SupportStatusForPath / Get-ObjectSupportStatus) — ps1 и py. Для
info-навыков строка «Поддержка:» для внешнего объекта опускается.

Тесты: hooks/test/run.mjs — секция внешней границы (G=1 + встроенная EPF);
tests/skills — кейсы mxl-compile (guard пропускает) и mxl-info (строка опущена).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 19:46:54 +03:00
Nick ShirokovandClaude Opus 4.8 a991458ef2 feat(mutators): предметная диагностика support-guard (§1B) по причине отказа
Синхронизация §1B с улучшенным текстом хука (ветка feat/support-guard-hooks):
вместо общего списка всех вариантов — текст под конкретную причину
(capability-off / locked / not-removed) с подставленным реальным путём и
точными командами support-edit. Понятно модели вне контекста.

- Все 16 навыков-мутаторов (оба рантайма): Assert-EditAllowed /
  assert_edit_allowed строят сообщение по $code/code причины отказа.
- Терминология «редактирование» (как у платформы 1С).
- Версии заголовков подняты в обоих рантаймах.
- EOL/BOM каждого файла сохранены; deny-тесты 16/16 на PS и PY.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:18:21 +03:00
Nick ShirokovandClaude Opus 4.8 de04a8dc7a feat(support-edit): навык переключения состояния поддержки + ссылка из диагностики гарда
Замыкает петлю issue #23: после отказа support-guard модель может
легитимно включить редактирование. support-edit правит правила поддержки
в Ext/ParentConfigurations.bin выгрузки (только XML; в ИБ — полной загрузкой):
  -Path <путь> -Set editable|off-support|locked   (пообъектно или корень)
  -Path <путь> -Capability on|off                  (возможность изменения)
Path-based, симметрично гарду — модель берёт путь прямо из отказа.

Реализация: regex-замена in-place по разобранному формату bin (round-trip
байт-в-байт; парсер подтверждён consumed=100% на корпусе acc/erp/K=7).
При выключенной возможности (G=1) пообъектный -Set отказывает с подсказкой
«сначала -Capability on» (явные шаги). Включение возможности ставит всё на
замок (вендорские флаги «не редактировать» в выгрузке недоступны — массовой
разблокировки нет; non-goal). Оба порта дают байт-в-байт идентичный bin.

Диагностика support-guard (32 файла, 16 мутаторов × 2 порта) теперь печатает
готовую команду support-edit под конкретный отказ.

Тесты: фикстуры g0/g1 + кейсы set-editable/off-support/предусловие/capability
со снапшотами bin; зелёные на PowerShell и Python. Все 16 мутаторов +
support-edit зелёные на обоих рантаймах.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 16:45:43 +03:00
Nick ShirokovandClaude Opus 4.8 b17dd5d04c feat(mutators): размножить support-guard на остальные навыки-мутаторы
Контракт Assert-EditAllowed (дословная копия из meta-edit, оба порта)
добавлен в 13 навыков-мутаторов. Всем единый вызов require=editable —
«кого проверять» решает не навык, а walk-up по файловой системе от пути
цели:
  - целевой файл имеет root-uuid → его f1 (правка существующего элемента);
  - иначе ближайший вверх <dir>.xml с uuid → f1 владельца (добавление
    дочернего: форма/реквизит/макет к объекту);
  - иначе корень Configuration.xml → f1 корня (новый объект верхнего уровня).

Это воспроизводит семантику поддержки 1С автоматически, поэтому один и
тот же навык проверяет разное по состоянию дампа: skd-compile/mxl-compile/
form-compile в существующий макет/форму → f1 этого элемента (modify), в
несуществующий → f1 владельца. Проверено обоими сценариями.

Навыки: form-edit, form-add, form-compile, skd-edit, skd-compile, cf-edit,
subsystem-edit, subsystem-compile, interface-edit, template-add, help-add,
mxl-compile, role-compile. Диагностика через [Console]::Error.WriteLine +
exit 1 (как в эталоне). Версии подняты в обоих портах.

Проверено: deny на обоих портах (крафт-фикстуры на копиях, корпус не
тронут); все 16 навыков-мутаторов зелёные на PowerShell и Python
(264 кейса). BOM сохранён везде.

Follow-up (в upload-плане): пред-существующий баг help-add
Detect-FormatVersion (Substring на кириллице, до гарда, не связан);
авторезолв пути в meta-edit; per-skill committed deny-тесты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:27:38 +03:00
Nick ShirokovandClaude Opus 4.6 972cd5061d fix: resolve remaining 19 Python test failures — 285/285 on both runtimes
Script logic fixes (PY mirroring PS1):
- skd-compile: fix (?i) regex flag placement for Python 3.11+
- mxl-compile: handle list-of-lists row format (PS1 silently ignores)
- subsystem-compile: add "objects" → "content" synonym alias
- role-compile: add "rights" → "objects" synonym alias
- meta-compile: sort HTTP/Web service method/operation iteration
- form-edit: insert ChildItems after Events/AutoCommandBar (not at end)
- mxl-compile: sort colWidthMap iteration in both PS1 and PY for
  deterministic format indices

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 20:30:45 +03:00
Nick ShirokovandClaude Opus 4.6 250978c2fd fix: resolve FINDINGS — synonyms, path resolution, exit codes
Skill fixes (all ps1+py, version bumped to v1.1):
- role-compile: accept "rights" as synonym for "objects"
- subsystem-compile: accept "objects" as synonym for "content"
- form-add: resolve directory path to .xml (like meta-validate)
- form-info: resolve directory path to Ext/Form.xml (like form-validate)
- mxl-compile: support absolute OutputPath (IsPathRooted check)
- meta-remove: exit 1 when object not found; v1.1
- cfe-patch-method: accept plural type names (Catalogs → Catalog)

Test fixes:
- basic-role.json: use canonical "objects" key
- basic.json (subsystem): use canonical "content" key
- catalog-form.json: fix outdated DSL format
- New synonym test cases for role-compile and subsystem-compile
- error-not-found.json: expect error (exit 1)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:25:52 +03:00
Nick ShirokovandClaude Opus 4.6 88f74e96f0 fix(python): add stderr UTF-8 encoding for Windows compatibility
Without reconfiguring stderr, Cyrillic error messages appear garbled
on Windows (cp1251 default). Mirrors the existing stdout fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 18:31:27 +03:00
Nick ShirokovandClaude Opus 4.6 d6abb2b651 fix(python): add stdout UTF-8 encoding for Windows compatibility
Python on Windows defaults to cp1251 for piped stdout, which cannot
handle Unicode box-drawing characters used in info/analysis output.
Added sys.stdout.reconfigure(encoding="utf-8") to all 59 Python scripts.

Tested on real config data: epf-init, epf-validate, cf-info, cf-validate,
meta-info, form-info, role-info, skd-info, subsystem-info — all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:29:26 +03:00
Nick ShirokovandClaude Opus 4.6 86a959a354 feat(crossplatform): add Python 3 ports for all 58 PS1 skill scripts
Add cross-platform Python alternatives alongside existing PowerShell
scripts. PS1 remains the default runtime; Python is opt-in via switch
scripts. All parameters are identical between runtimes.

New files:
- 58 Python scripts in .claude/skills/*/scripts/*.py
- scripts/switch-to-python.py and switch-to-powershell.py
- docs/python-porting-guide.md
- __pycache__/ added to .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:16:07 +03:00
Nick ShirokovandClaude Opus 4.6 703c3e6f80 Add version headers with source URL to all 22 skill scripts
Each .ps1 now starts with skill name, version (v1.0), and repo link.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 20:01:10 +03:00
Nick ShirokovandClaude Opus 4.6 044bc18974 Add img-grid skill and page auto-sizing to mxl-compile
- New skill /img-grid: overlays numbered grid on images to help
  determine column proportions for MXL template generation
- Add "page" field to MXL DSL ("A4-landscape", "A4-portrait", or
  number) that auto-calculates defaultWidth from column proportions
- Update DSL spec, mxl-compile SKILL.md, MXL guide, README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:44:31 +03:00
Nick ShirokovandClaude Opus 4.6 bca022fba8 Add relative column width support ("Nx" syntax) to mxl-compile
Allows specifying column widths as multiplier of defaultWidth (e.g. "2x",
"0.5x") alongside absolute values. Makes template generation from images
easier — model specifies proportions instead of exact pixel values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 20:47:32 +03:00
Nick ShirokovandClaude Opus 4.6 0e680d151d Remove unused styles, add empty row compression to DSL
- Decompiler: remove styles not referenced by any cell or rowStyle
- Decompiler: compress consecutive empty rows into {"empty": N}
- Compiler: support {"empty": N} row placeholder
- DSL spec: document empty field on rows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 20:09:13 +03:00
Nick ShirokovandClaude Opus 4.6 1ed5d783e8 Rename numberFormat to format in mxl-compile DSL
The field covers all 1C format strings: numbers, dates, booleans.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 18:35:44 +03:00