fix(skills): round-trip сохранение EOL/BOM/encoding в py edit-портах (#44/#46/#47)

Py-порты (lxml) при точечном редактировании существующего 1С-XML переписывали
весь файл: CRLF→LF, encoding="UTF-8"→"utf-8", добавляли финальный перенос,
плодили литерал 
 (сериализация \r из tail'ов). Результат — широкий шумовой
diff и скрытый лишний текст-узел при exit 0 и зелёной валидации. PS1-порты
(XmlDocument) багу не подвержены — эталон.

Фикс во всех 13 round-trip re-serialize py-навыках: перед записью детектится
стиль существующего файла (BOM / EOL / регистр encoding / финальный перенос) и
восстанавливается при сохранении; переносы канонизируются к LF (убирает 
),
затем приводятся к EOL источника. Новый файл (путь не существует) → прежнее
поведение, снапшоты не двигаются. Навыки: cf-edit, meta-edit, meta-remove,
interface-edit, subsystem-edit, skd-edit, form-edit, form-add, help-add,
template-add, template-remove, form-remove, cfe-borrow. Версии py+ps1 подняты
синхронно.

Harness (tests/skills/runner.mjs): снята маска 
 в normalizeXmlContent
(порты её больше не порождают → гвардия ловит регресс); добавлен expect.preserves
— raw-байтовая проверка BOM/EOL/encoding/финального переноса/отсутствия 
в обход нормализации. Регрессионные round-trip кейсы на CRLF+BOM+UTF-8 фикстурах
для cf-edit/meta-edit/subsystem-edit.

Верификация: py 556/556, ps1 556/556; платформа 1С 8.3.24 (verify-snapshots)
cf-edit 12/12, meta-edit/subsystem-edit round-trip загружаются; негатив-тест
подтверждает, что harness ловит дефект на старом коде.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-19 16:53:00 +03:00
co-authored by Claude Opus 4.8
parent fbdf07e18a
commit 57e99d144e
46 changed files with 1698 additions and 103 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# skd-edit v1.29 — Atomic 1C DCS editor
# skd-edit v1.30 — Atomic 1C DCS editor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: парный .py собирает выражения автодат вне f-string ради совместимости с python 3.9 (PEP 701).
param(
+25 -9
View File
@@ -1,4 +1,4 @@
# skd-edit v1.29 — Atomic 1C DCS editor (Python port)
# skd-edit v1.30 — Atomic 1C DCS editor (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -1984,6 +1984,12 @@ raw_root_opening = _root_open_m.group(0) if _root_open_m else None
# Detect line ending convention so save can normalize back to whatever the source used.
line_ending = "\r\n" if "\r\n" in raw_original_text else "\n"
# Round-trip: сохранить BOM / регистр encoding / финальный перенос как в оригинале.
_skd_had_bom = raw_original_bytes.startswith(b"\xef\xbb\xbf")
_skd_body = raw_original_bytes[3:] if _skd_had_bom else raw_original_bytes
_skd_enc_m = re.search(rb'encoding="([^"]+)"', _skd_body[:200])
_skd_enc = _skd_enc_m.group(1).decode("ascii") if _skd_enc_m else "utf-8"
_skd_final_nl = _skd_body.endswith(b"\n")
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(resolved_path, xml_parser)
@@ -3423,7 +3429,10 @@ if not dirty:
sys.exit(0)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
# Round-trip: восстановить регистр encoding как в оригинале.
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + _skd_enc.encode("ascii") + b'"?>')
# Format-preserve post-processing (mirrors PS path):
# (1) restore the original raw <DataCompositionSchema ...> opening tag — lxml collapses
@@ -3435,17 +3444,24 @@ if raw_root_opening:
# defensive — strip any space before `/>` so PS and PY ports stay byte-equivalent.
xml_text = re.sub(r"(?<=\S) />", "/>", xml_text)
# Normalize line endings to match source.
# Канонизировать переносы к LF (убирает возможный &#13;), затем к стилю источника.
xml_text = xml_text.replace("&#13;\n", "\n").replace("&#13;", "").replace("\r\n", "\n").replace("\r", "\n")
if line_ending == "\r\n":
xml_text = re.sub(r"(?<!\r)\n", "\r\n", xml_text)
else:
xml_text = xml_text.replace("\r\n", "\n")
xml_text = xml_text.replace("\n", "\r\n")
xml_bytes = xml_text.encode("utf-8")
if not xml_bytes.endswith(b"\n"):
xml_bytes += b"\n"
# Финальный перенос — как в оригинале.
if line_ending == "\r\n":
xml_bytes = xml_bytes.rstrip(b"\r\n")
if _skd_final_nl:
xml_bytes += b"\r\n"
else:
xml_bytes = xml_bytes.rstrip(b"\n")
if _skd_final_nl:
xml_bytes += b"\n"
with open(resolved_path, "wb") as f:
f.write(b'\xef\xbb\xbf')
if _skd_had_bom:
f.write(b'\xef\xbb\xbf')
f.write(xml_bytes)
print(f"[OK] Saved {resolved_path}")