Три остатка волны #57, все в одной точке — блоке записи XML.
1. Порты расходились по EOL. PS гонял документ через XmlWriter с дефолтным
NewLineHandling.Replace и принудительно переводил весь файл в CRLF, а py
сохранял стиль исходника: role-compile на LF-ном Configuration.xml давал
10311 байт против 10062. Ещё в пяти навыках None стоял, но финальной
нормализации не было, и на LF-исходнике выходил смешанный EOL (meta-edit:
40 CRLF + 136 одиночных LF) — та же форма, что у исходного дефекта #57.
Приведено к форме cf-edit.ps1: None + канонизация к LF + целевой перевод
строки (стиль файла-назначения, для создаваемого файла — канон CRLF).
Replace не годится как замена: он превращает переводы строк внутри значений
атрибутов в .
2. Гард `if (-notmatch CDATA)` не обрабатывал CDATA, а отказывался от канона
во всём файле — то есть деградировал до «не сделал ничего». Заменён
альтернацией: участки CDATA и комментариев возвращаются как есть, замена
идёт только вне них. На реальных данных поведение не меняется — в корпусе
из 476 942 XML нет ни одного CDATA и ни одного комментария.
3. py: три реализации одного правила детекта EOL сведены к одной. Мажоритарное
правило в role-compile/subsystem-compile давало ДРУГОЙ ответ на смешанном
входе. Дефолты _finalize_xml_bytes для нового файла приведены к канону
(UTF-8, CRLF, без хвостового перевода); meta-edit срезает хвост в обеих
ветках, а не только при создании файла.
Попутно, найдено байтовой сверкой портов:
- py вставлял <Role>/<Form>/<Subsystem> с пятью табами вместо трёх —
подстановка по голому </ChildObjects> удваивала отступ строки; снэпшоты
этого не видели, так как схлопывают пробелы между тегами;
- py-порты xdto-* писали XML-декларацию одинарными кавычками (так отдаёт
lxml), платформа и PS пишут двойные.
Регресс: runner 647/647 ps1, 644/647 py (3 skipped), дрейфа снэпшотов нет.
Корпусный раундтрип метаданных: 4897 объектов, match 100%, совпадает с
эталонами захода #57.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>