Ячейка получила третий параметр — pictureParameter, имя параметра, которым
подставляют картинку. Сама картинка задаётся оформлением (picIndex), а этот
тег живёт у ячейки, последним из её параметров, и уживается с текстом.
В корпусе таких ячеек 21 в 9 макетах; теперь все 21 возвращаются обратно.
Прозрачность картинки сведена к одному ключу transparent: false — фона нет,
{ x, y } — прозрачен цвет пикселя с этими координатами. Два способа записи
у платформы исключают друг друга (t принимает только false, включённую
прозрачность выражают tx/ty), так что двум ключам DSL соответствовал один
флажок диалога.
Заодно выровнен порядок ключей в проверке «объект описывает ячейку»: в
py-порте не хватало note.
Атрибут прозрачности живёт не только у картинки с данными: платформа пишет
<picture t="false" ref="v8ui:Имя"/>, причём t перед ref. Компилятор в этой
ветке его терял, декомпилятор не читал — в «Бухгалтерии предприятия» на
8.3.27 таких записей 67.
Заодно уточнена картина по конфигурациям: t="false" стоит у 11 135 картинок
БП, значения true не бывает нигде — включённую прозрачность платформа
выражает координатами пикселя, и вместе с t они не встречаются.
Рисунок поверх сетки — картинка, фигура, надпись — теперь описывается в DSL
и возвращается из макета: два якоря «ячейка + смещение», тип, ссылка на
картинку, надпись, расшифровка, имя, порядок перекрытия.
Оформление рисунка разложено надвое: общее (заливка, шрифт, выравнивание)
берётся из именованного стиля, а линия и её стороны — собственные ключи
рисунка. У ячейки таких свойств не бывает: все 2 135 записей палитры с ними
принадлежат рисункам.
Палитра картинок: ссылка на библиотеку платформы, данные base64, пустая
запись «картинка не задана» (151 в корпусе) и координата пикселя прозрачного
цвета (tx/ty — проверено, это не размеры картинки).
Заодно: висячий пустой колонтитул в ps1-порте (пустой словарь в PowerShell
истинен, порты расходились на 3 макетах из 40) и проверка индекса формата
рисунка в mxl-validate.
Стенды Рисунки, Рисунки2, КартинкаВЯчейке проходят раундтрип байт в байт
в обоих портах; на 35 корпусных макетах с рисунками расхождение сократилось
у всех 35.
Оба списка снимались с корпуса ERP и потому отвергали законные значения.
Узоров платформа знает семнадцать («Узор 1» … «Узор 17»), а в корпусе
встречаются только шесть номеров — Pattern4 компилятор отклонял.
Со стилями линии тоньше: палитра одна на документ, но Конфигуратор
предлагает разные наборы в разных местах. У рамки ячейки семь значений
(корпус их покрывал), у линии рисунка — шесть других, и трёх из них
(Dashed, DashDotted, DashDottedDotted) в корпусе нет ни одной. Домен —
объединение обоих наборов.
Обе поправки сняты с диалогов Конфигуратора, а не додуманы по образцу.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Именованная область ссылается на дополнительный набор колонок тегом
columnsID, и эта ссылка терялась целиком: на пилоте кампании она давала
3572 расхождения из 3572 в своей категории, на корпусе таких областей
913 483 прямоугольных, 20 063 полосы строк и 743 полосы колонок.
Ключ namedAreas[].columnSet имеет три состояния: ключа нет — привязка
выводится из накрытых строк (совпадает у 1 021 570 областей корпуса из
1 044 339), "" — привязки нет вовсе, имя набора — явная привязка.
Переопределение обязательно: у областей типа Rows 13 623 повторяют
раскладку строк, а 8 179 её не несут при тех же строках.
Декомпилятор пишет ключ, только когда он не выводится, а область, чья
привязка расходится с раскладкой её строк, уводит из блочной формы в
namedAreas — иначе блоком её не выразить.
Пустая строка тоже попадает в карту «строка → раскладка»: без этого
привязка области, накрывающей пустые строки, не выводилась.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ключи header и footer описывают колонтитул: три слота (left, center,
right), общий шрифт и вертикальное выравнивание, признак вывода и
страница, с которой печатать. Ключ printSettings — плоский набор
параметров печати с именами тегов платформы; порядок компилятор
выставляет сам, незнакомый ключ отвергает.
Слот устроен как ячейка: ссылка на формат плюс текст. Текст бывает двух
видов, и это не текст против шаблона, а обычная строка против
форматированной: у форматированной разметка живёт прямо в содержимом
(<b>жирный</>, <fontsize 12>, <colorstyle -16>), поэтому она возится как
есть. Признак вывода и стартовая страница лежат в формате как <height>
и <width>, но читаются только у записи с <height>: палитра
дедуплицирована, и колонтитул без своих настроек ссылается на чужую
запись, где <width> — ширина колонки (637 таких ссылок против 153 своих).
Перенос строки внутри текста колонтитула платформа хранит голым LF при
CRLF во всём остальном файле — единственное такое место в документе,
поэтому содержимое пишется одной строкой вывода.
Формат колонтитула регистрируется до отсева неиспользуемых шрифтов:
иначе шрифт, на который ссылается только колонтитул, выбрасывался бы
из палитры.
Стенд Колонтитулы собирается байт в байт обоими портами и добавлен
в платформенные фикстуры.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Документные ключи rowGroups и columnGroups описывают сворачиваемые
группы: диапазон, имя, признак свёрнутости и расположение заголовка.
Форма плоская, как в формате: вложенность выражена вхождением одного
диапазона в другой. Дерево было бы читаемее, но платформа хранит плоско,
и для правки чужого макета дерево дороже — чтобы добавить группу внутрь
существующей, пришлось бы искать родителя. Частичных пересечений
платформа не порождает (корпус: 40 620 886 пар непересекающихся,
599 958 вложенных, ни одного частичного), поэтому компилятор их
отвергает.
Число уровней вложенности не задаётся: оно совпало с <vgLevels> у всех
1797 макетов корпуса. Порядок записи компилятор приводит к
платформенному — родитель раньше вложенных, по возрастанию начала
(совпало у 1798 макетов из 1798).
Имя группы вывести нельзя, хотя 143 455 имён корпуса выглядят как
автоматические R<номер строки>: 16 666 из них протухли после вставки
строк, а 10 287 групп имени не имеют вовсе. Расположение заголовка взяло
ключ titleLocation — то же свойство и то же имя, что в DSL форм.
mxl-validate: проверка 14 — перевёрнутый диапазон, частичное пересечение,
начало группы за пределами документа.
Стенд Группировки собирается байт в байт обоими портами и добавлен
в платформенные фикстуры.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ключ ячейки note описывает всплывающую подсказку: строкой, объектом
языков или полной формой с оформлением, признаком авторазмера и
геометрией окошка.
Из четырнадцати тегов, которые платформа пишет в примечание, настоящей
информации несут пять. drawingType, pictureSize и id — константы на всём
корпусе. Якорь конца это координаты самой ячейки (1087 примечаний из
1087), якорь начала — 1/1 (1085 из 1087, три исключения в одном макете
берёт раундтрип-ключ anchor). Остаётся текст, стиль, autoSize и четыре
смещения, причём autoSize описывает не наличие геометрии, а пересчёт
размера: при true координаты всё равно записаны и осмысленны.
Стиль примечания — обычная запись палитры; без своего стиля пишем тот,
что даёт Конфигуратор (926 примечаний корпуса из 1087). Формат
примечания добавлен в сбор именованных стилей декомпилятора и в перечень
владельцев формата: иначе стиль подсказки вырезался как неиспользуемый и
ссылка оставалась висячей.
mxl-validate: индекс формата примечания проверяется наравне с ячейкой,
строкой и колонкой.
Стенд СПримечанием собирается байт в байт обоими портами и добавлен в
платформенные фикстуры.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ключ ячейки value несёт значение, набранное в поле ввода, ключ control —
сериализованные настройки самого элемента управления. Оба живут у ячейки,
а не в палитре формата: у двух ячеек с одинаковым оформлением значения
разные.
Тип значения выражается литералом JSON и приведения к объявленному типу
НЕ делается. Так пишет платформа: ссылочный и составной тип она хранит
строкой, а при смене типа ячейки прежнее значение не переписывает — на
корпусе 560 ячеек объявлены числом, но несут строку. Приведение здесь
означало бы переписывать данные при пересборке. Дата литералом JSON не
выражается, поэтому едет строкой и читается как дата только у ячейки,
объявленной датой.
Настройки элемента управления возим дословно: 26 370 вхождений корпуса
дают всего 163 различных блоба, разбирать структуру незачем. Переводы
строк внутри блоба приводим к LF на чтении — XML-парсеры двух портов
нормализуют их по-разному, и без этого порты давали разный JSON на одном
файле.
Стенд СЗначениями снова собирается байт в байт обоими портами: фикстура
обновлена до версии со значениями, флажком и блобом настроек.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ключ detail описывал ячейку только вместе с param, а платформа ставит
расшифровку самостоятельно: на корпусе ERP 20 404 ячейки несут её БЕЗ
параметра (12 653 пустых, 5 949 с текстом, 1 802 поля ввода) против
8 582 с параметром. Компилятор такую расшифровку молча выбрасывал,
декомпилятор молча не читал, а ячейку, где кроме расшифровки ничего нет,
считал заполнителем строки и терял целиком.
Заодно выправлен порядок тегов ячейки: расшифровка идёт ПОСЛЕ текста,
а не перед ним (корпус: f · parameter · tl|v · detailParameter).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Тег ячейки <control> и тег формата <controlType> в выгрузке — разные
вещи: первый несёт сериализованные настройки элемента управления, второй
GUID его вида. Ключ DSL описывал второй, а назывался как первый, и при
поддержке настроек отображение стало бы перекрёстным.
Свести их в один ключ нельзя: <controlType> живёт в палитре и разделяется
ячейками с одинаковым оформлением, а настройки принадлежат конкретной
ячейке. Синоним не заводим — он занял бы ровно то имя, которое
освобождается.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Тип значения ячейки и элемент управления переживают цикл компиляции и
декомпиляции. Макет стенда с полями ввода собирается из своего же JSON
байт в байт.
DSL: два ключа ячейки — valueType (грамматика типа семьи: примитивы с
квалификаторами, ссылочные типы, категории целиком, составной через " + ")
и control (input/checkbox, синонимы, GUID для неизвестных элементов, none
для формата вовсе без тега). containsValue отдельным ключом не выражается:
он выводится из наличия типа. Текст и шаблон в такой ячейке запрещены,
параметр и расшифровка допустимы.
Умолчания голых типов взяты платформенные (строка без длины безлимитна,
число без параметров без ограничения разрядности), поэтому эмиттер типа
свой, а не копия meta-compile; resolve_type_str скопирован из эталона
семьи и объявлен в реестре дрейфа. Канон типа в ключе дедупликации
палитры развёрнут: иначе разные написания одного типа дали бы две
одинаковые записи формата и сдвинули бы все ссылки ячеек.
mxl-validate: проверка согласованности таких форматов — тип без признака
значения, ячейка с текстом и значением одновременно, посторонний тег
внутри типа, формат значения у строки или колонки, неизвестный GUID
элемента управления.
Попутно сведены два расхождения портов mxl-validate: py молчал там, где
ps1 писал строку об отсутствии шрифтов, и шапка подробного вывода
печаталась в разной форме.
Версии сведены: mxl-compile 1.41 в обоих портах (было 1.40/1.39).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Шрифт бывает не собственным описанием, а ссылкой: на элемент стиля конфигурации
(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>
У текста ячейки три состояния, а выражались два: тега нет вовсе, тег с пустым
текстом на каждый язык — и третье, <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>
Прежняя формулировка противопоставляла «ячейку» и «многоязычный текст», хотя
текст тоже даёт ячейку. Разница не в этом: объект либо описывает СВОЙСТВА
ячейки (среди ключей есть ключ её схемы), либо является её ЗНАЧЕНИЕМ, и тогда
ключи — идентификаторы языков.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
В позиционной записи строки элемент — это значение содержимого ячейки, а значение
текста по общей конвенции бывает строкой либо объектом «язык → текст». Значит
объект {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>
Контролируемый стенд (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>
Мы всегда заводили шрифт Arial 10 по умолчанию, даже когда его никто не
использует. Платформа так не делает: у макета без оформления элемента <font>
нет вовсе. Неиспользуемые шрифты теперь отбрасываются, ссылки перенумеровываются
(индексы шрифтов позиционные).
Проверено на контролируемом стенде: после правки простейший макет расходится
с платформенным ровно одной строкой — объявлением языков в шапке.
Кейс font-fractional-size дополнен: стиль, задающий ТОЛЬКО шрифт по умолчанию,
равнозначен отсутствию оформления (ячейка получает <f>0</f>), поэтому шрифт
остаётся неиспользованным. Чтобы кейс продолжал проверять целый размер, стилю
добавлено второе свойство.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Мы регистрировали формат по умолчанию первым, из-за чего вся палитра шла
со сдвигом относительно платформенной. На корпусе он последний в 8285 макетах
из 10 863, первым — в 25.
Подтверждено контролируемым стендом: в макетах, где ширины заданы на уровне
документа, палитра идёт «ширины колонок в порядке колонок, умолчание последним».
Порядок палитры не виден в семантическом диффе (правило palette-index разрешает
ссылки и тем самым его прячет), поэтому мерялся отдельно: на пилоте совпавших
с оригиналом записей с начала палитры стало 467 из 2274 против 23, палитр,
совпавших целиком, — 4 против 2.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
У строки есть собственный формат: платформа хранит в нём скрытие (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>
Ячейка без собственного оформления ссылалась на формат по умолчанию. Платформа
в этом случае пишет <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>
Платформа иногда перечисляет колонку в раскладке с <formatIndex>0</formatIndex> —
колонка объявлена, формата у неё нет (4% макетов корпуса). Компилятор такие
опускал, декомпилятор их не видел, и элемент терялся целиком.
Выражается пустым значением в columnStyles. Для авторинга это бесполезно —
ни одна задача не звучит как «объяви колонку без свойств», — поэтому в описании
DSL записи нет: она нужна только чтобы раундтрип не терял байты.
На пилоте потери в категории colset[].col[].formatIndex упали с 39 до 35.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
У ячейки есть 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>
Стиль описывал 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>
Формат был фиксированной записью из 12 полей, выписанной руками в четырёх
местах: ключ дедупликации, набор свойств, резолв стиля и эмиссия палитры.
Добавление свойства стоило восьми правок в двух портах, а свойств у формата
в выгрузке 47.
Теперь формат — набор «тег платформы → значение», а порядок эмиссии задаёт
один канонический список тегов. Список снят с корпуса ERP: 766 960 форматов,
ни один не нарушает эту последовательность.
Вывод не меняется: пилот из 40 макетов скомпилировался побайтово так же, как
до правки, обоими портами.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Две правки эмиссии, обе выведены из корпуса 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>
Строка в 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>
Компилятор давно принимал короткую форму, а декомпилятор всегда писал самую
развёрнутую. Теперь он выбирает самую короткую из применимых, как это делает
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>
Платформа хранит текст ячейки по элементу на язык. Декомпилятор брал ПЕРВЫЙ и терял
остальное, компилятор писал всегда 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>
Платформа держит для неоформленного макета ОДИН формат — ширину колонки, он же
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>
Идентификатор колоночной раскладки выводится из имени детерминированно — это
требование, а не оптимизация: случайный давал бы другой файл при каждой компиляции,
снэпшоты не совпадали бы сами с собой, а повторная сборка того же определения
порождала бы диф.
Но выводился он неправильно: сырой хэш форматировался как 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>
Две правки по корпусу и по 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>
Раскладка адресуется именем из 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>
Инкремент 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>
Инкремент 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>
Размер шрифта в платформенных макетах бывает дробным (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>
Строка, записанная массивом ("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>
Ячейка без 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>
Дорожка 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>
Дорожка 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>
У 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>
esc_xml был один на две задачи, и в атрибутном контексте применялся текстовый
вариант без ". form-compile с choiceParameters, имя которого содержит
кавычку, выдавал <app:item name="Отбор.Наименование "кавычка"">: lxml падает с
attributes construct error, то есть файл невалиден как XML. Затронуто 9 мест в
form-compile и 3 в skd-compile, одинаково в обоих портах.
Раундтрип через базу показал границу: в ТЕКСТЕ элемента платформа экранирует
только & < > (кавычка и апостроф возвращаются сырыми байт-в-байт), а в ЗНАЧЕНИИ
АТРИБУТА пишет " — внутри "..." литеральная кавычка невалидна. Поэтому две
функции с говорящими именами, а не одна с флагом:
esc_xml — значение атрибута: & < > "
esc_xml_text — текст элемента: & < >
Заодно закрыто расхождение портов в init-навыках: PY экранировал текст с ",
PS1 — через SecurityElement::Escape (ещё и '), а 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>
Детекторы версии искали только 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>
У корня <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>
Наполнение макета по свежему пути (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>
Головной дефект тикета: 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>
Конфигуратор не пишет перевод строки в конце файла — последний байт `>`. Это видно
на чистой выгрузке пустой ИБ (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>
Правило платформы единое и подтверждено дважды. По корпусу трёх конфигураций:
92142 сырых кавычки в тексте элементов и НИ ОДНОЙ "; в условиях RLS —
16334 сырых против нуля экранированных (при этом & платформа пишет, то есть
амперсанд экранируется, а кавычка нет). Загрузка на стенде: оба варианта
принимаются, но выгружает платформа сырую кавычку — то есть " не ошибка,
а лишний шум в роундтрипе.
Решение было принято раньше в 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>
При поиске корня конфигурации 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>
Синхронизация §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>