15 KiB
title, namespace, type, created, updated, tags, related
| title | namespace | type | created | updated | tags | related | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 🔁 Round-trip key verification — проверка нового ключа конвертера | personal | how-to | 2026-09-17 | 2026-09-18 |
|
|
🔁 Round-trip key verification
Зеркало навыка
roundtrip-key-verification(категорияsoftware-development, Hermes). Появился из реального бага в ZONT-конвертере 2026-09-17 (коммит87e315c). Разбор в проекте — personal/projects/zont-config-compiler §10.1.
6. 🔴 ПИТФОЛЛ 93 — переименовал ключ словаря → грепни ВСЕ обращения к нему
Маски objcmd перешли с полных ('1 %0') на базы до плейсхолдера ('1 '), но проверка
if _OC_OPS.get(_oc_mask) is None: осталась смотреть на старое имя переменной. Ключа нет →
None → все 6 записей молча ушли в fallback descr+args. Круг зелёный: fallback
доносит байты верно, а новый ключ просто не появляется (ровно §6-сценарий выше).
# переименовал переменную/ключ — найди КАЖДОЕ обращение, а не только определение
grep -n "_oc_mask\|_oc_base" config-to-yml.py # словарь, обе проверки, обе сборки
Симптом, который надо узнать: артефакт показывает старую форму при зелёном круге. Это не «правка не сохранилась» — это fallback, который её перекрывает.
7. 🔴 ПИТФОЛЛ 94 — «объект есть в YAML» ≠ «энкодер его выпустит»
Тела, развёрнутые внутри другого объекта (операнды left/right, source), не лежат
отдельной секцией. Индекс тел, построенный по спискам верхнего уровня с ключом raw, их
не видит → _raw_of() отдаёт None → объекты теряются из выхода.
Разбор был последовательным и вскрыл четыре подловушки:
| Симптом | Причина | Решение |
|---|---|---|
-5 объектов |
индекс не обходил scenarios |
рекурсивный обход вложенного |
всё ещё -5 |
у развёрнутых тел нет raw |
raw клался в тело (костыль, потом снесён) |
-2 |
вложенные операнды left/right не регистрировались |
рекурсия по left/right |
-2 снова |
фильтр x in _known_ids отсекал нужные id |
фильтр убран, дедуп на _register |
Правила:
- Индекс тел собирать по признаку
id+type, а не по наличиюraw. - Строку собирать хелпером по полям —
rawв промежуточном файле не нужен вовсе. _known_ids— НЕ «что уже выпущено», а «отличить литерал от ссылки». Для второго есть отдельное множество (_emitted_ids). Перепутать = самострел: пропустишь ровно то, что надо выпустить.
8. 🔴 ПИТФОЛЛ 95 — костыли в промежуточном формате видно глазом владельца
Чтобы индекс нашёл развёрнутые тела, в них временно клался raw. Круг — зелёный, но артефакт
зарос 88 блоками сырья, и владелец это увидел сразу.
круг зелёный + артефакт зарос костылём = правка не принята
Правило: raw/_-хелперы в выходном файле — только на время отладки. Если костыль нужен
индексу — значит индекс построен по неверному признаку, чинить индекс, а не засорять артефакт.
Когда применять
Любой двусторонний конвертер с тестом байтовой точности: .txt ⇄ .yml, сериализатор с
decode + encode, dump/undump AST, DB ⇄ ORM.
Триггер: добавил новый ключ в декодированный вывод и round-trip зелёный.
🔴 Ловушка
Байтовый round-trip доказывает, что вывод парсера == вход. Про энкодер он не говорит ничего — энкодер получает ровно то, что парсер положил.
Если энкодер игнорирует новый ключ, тест всё равно зелёный:
original.txt → parse → YAML(с новым ключом) → encode(ключ игнорируется) → original.txt ✅ diff = 0
▲ ключ добавлен ▲ ключ потерян, старое поле доносит байты
Потеря невидима: старое поле (args) рядом и по-прежнему собирает байты.
Реальный случай: ключ set_var добавлен в парсер типа 59, энкодер поправлен только
в emit_action. Вторая точка входа — инлайн-ветка emit_step ('descr' in step and 'args' in step) —
ключ игнорировала. Round-trip 🟢, правка пользователя молча терялась.
Обязательный шаг: тест подменой (mutation)
read back («записалось») — не проверка. Подменить новое значение и пересобрать; эффект
обязан появиться в выходе.
cd /tmp && rm -rf t && mkdir t && cd t
cp <реальный-артефакт>.yml t.yml
python3 - <<'EOF'
s = open('t.yml', encoding='utf-8').read()
old = "...блок со старым значением нового ключа..."
new = "...тот же блок, значение заменено на маркер (напр. 7777)..."
assert s.count(old) == 1, s.count(old) # assert уникальности — без случайных мультиправок
open('t.yml', 'w', encoding='utf-8').write(s.replace(old, new))
EOF
python3 <проект>/encode.py t.yml > out.txt
diff <(iconv -f <вх> -t utf-8 <оригинал> | tr -d '\r' | sort) \
<(iconv -f <вых> -t utf-8 out.txt | tr -d '\r' | sort)
# ✅ изменилась РОВНО одна строка, в ней маркер
# ❌ ноль изменений = энкодер ключ проигнорировал → искать ДРУГУЮ ветку кода
Проверять обе половины:
- Парсер — ключ есть в промежуточном файле, на ожидаемом месте.
- Энкодер — подмена доезжает до выхода,
diff= ровно 1 строка.
Найти ВСЕ точки входа
У конвертеров их обычно две и больше: рекурсивный обход и инлайн/ссылочная ветка, либо проход по реестру типов и явный цикл.
grep -n "def emit_<x>\|def dump_<x>" *.py— выписать всех вызывающих.- Не править одну и полагать, что покрыта другая. Дублированные ветки — норма.
- Симптом пропущенной ветки: в тесте подменой остаётся старое значение.
В каждой ветке ставить проверку типа, чтобы плохое значение падало громко (exit 2),
а не тихо уходило сырым полем.
Смежные питфоллы
- Проверять тип производного ключа. Парсер, сохраняющий whole-float как float (
14.5,1.0), даст не-int для ключа-идентификатора. Guard:isinstance(v, int) and not isinstance(v, bool) and not isinstance(v, float) and v != 0.0= «цели нет», а не валидный id — пропускать, не выдумывать. - Один объект в двух секциях → ключ править во всех местах отображения, иначе одна проекция показывает новую семантику, другая старую. Round-trip и тут зелёный.
- Считать вхождения по данным, не по плану: «12 элементов» из плана оказались 10 в конфиге.
- Не резолвить имя из другого объекта. Производный ключ несёт сырой id;
target_nameвместоtarget— запрещённая подстановка.
Что записать после проверки
- Новый ключ, где эмитится, какие точки входа поправлены.
- Команду подмены и наблюдённый результат («1 строка изменилась, маркер на месте»).
- Питфолл «две точки входа» — чтобы следующая сессия сразу проверяла обе.
🔴 Одно имя на два разных действия = схлопывание (2026-09-18, ZONT круг 46)
Симптом: в YAML два разных действия читались под одним ключом, и одно затирало другое.
Случай: set_contour_temp — имя действия у двух разных типов записи:
- тип 9 (команда, поле 4 = уставка числом):
#Z8817=9,'descr',10034,'2782'→ 5.2 °C - тип 59 objcmd (поле 3 = источник/литерал):
#Z10379=59,'objcmd 8669 ",,,%0";#h',9146,0,0
Ветка objcmd проверялась в коде раньше, перехватывала тело типа 9 и собирала
#Z8817=59,'objcmd 10034 ",,,%0";#h',5.2,0,1 вместо #Z8817=9,…,10034,'2782'. Плюс
_register_action9 перезаписывал запись секции → Duplicate ID 8817.
Правила:
- Два разных действия — два разных имени. Одно имя на два типа записи = гарантированный конфликт веток. Владелец формулирует разницу сам: «установить целевую температуру x для контура y» vs «установить температуру контура x в значение y».
- При каждом переименовании или вводе ключа сверить пересечение словарей:
grep -n "_ACTION9_KEYS\|_OBJCMD_MASKS" *.py→ общие имена = баг. - Один id в двух секциях (шаг сценария + объект секции) → одна строка на id: вторая точка уступает первой, а не регистрирует своё тело повторно.
🔴 Артефакт в репо ≠ свежий вывод (2026-09-18, ZONT круг 46)
zont_config/*.yml в репозитории — это выход прошлого прогона. Пока файл не перегенерён, он
показывает старую форму, и правка выглядит невнесённой. test_roundtrip.py пишет в temp,
целевой артефакт не обновляет.
# приёмка: сначала перегенерировать ЦЕЛЕВОЙ файл, потом смотреть глазами
python3 config-to-yml.py zont_config/config_X.txt > zont_config/config_X.yml
grep -n -A4 "<изменённый id>" zont_config/config_X.yml
Симптом: «всё ещё вижу старое» при зелёном круге. Порядок — круг, потом перегенерация, потом чтение артефакта.
🔴 Круг зелёный ≠ приёмка (2026-09-17, ZONT круг 39)
Жёсткий урок той же сессии. Round-trip байтово чистый не означает, что правка верна.
Случай: правка декодера + энкодера прошла круг 9/9, но владелец проекта посмотрел на артефакт и увидел выдуманный ключ и обёртку-список на один элемент. С точки зрения байтов всё правильно; с точки зрения человека — сломано.
круг зелёный + артефакт читается плохо = правка не принята
Правило: перед правкой формы назвать целевую форму словами владельца и только потом трогать код. Если форма не названа — не угадывать: спросить одним вопросом.
Правило трёх: третья подряд правка «вслепую» (форма не названа, гипотеза за гипотезой) — стоп. Не править код. Назвать допущение, которое может быть неверным, и задать ОДИН вопрос.
Симптом «сломано, хотя круг зелёный»: объект, у которого есть id в строке конфига, при
развороте тела в YAML теряет этот id → энкодеру не из чего собрать строку, и объекты уезжают
в raw-секции (в ZONT: орфаны с 46 выросли с 4 до 69). Проверять не только число объектов
и строк, но и состав служебных секций до/после.
Проверка, которую стоит добавить в любой конвертер:
# число объектов в raw-фоллбэк-секциях ДО и ПОСЛЕ правки должно совпадать
grep -c '^\- id:' out.yml.before # <секция-фоллбэк>
grep -c '^\- id:' out.yml.after
Рост этой секции = форма не разобрана, а не «данных нет».