[2026-09-17] eagle: personal/projects/zont-config-compiler.md personal/tech/roundtrip-key-verification.md
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
---
|
||||
title: 🔁 Round-trip key verification — проверка нового ключа конвертера
|
||||
namespace: personal
|
||||
type: how-to
|
||||
created: '2026-09-17'
|
||||
updated: '2026-09-17'
|
||||
tags:
|
||||
- personal
|
||||
- how-to
|
||||
- testing
|
||||
- converters
|
||||
- reference
|
||||
related:
|
||||
- '[[personal/projects/zont-config-compiler]]'
|
||||
---
|
||||
|
||||
# 🔁 Round-trip key verification
|
||||
|
||||
> Зеркало навыка `roundtrip-key-verification` (категория `software-development`, Hermes).
|
||||
> Появился из реального бага в ZONT-конвертере 2026-09-17 (коммит `87e315c`).
|
||||
> Разбор в проекте — [[personal/projects/zont-config-compiler]] §10.1.
|
||||
|
||||
## Когда применять
|
||||
|
||||
Любой двусторонний конвертер с тестом байтовой точности: `.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` («записалось») — **не** проверка. Подменить новое значение и пересобрать; эффект
|
||||
обязан появиться в выходе.
|
||||
|
||||
```bash
|
||||
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)
|
||||
# ✅ изменилась РОВНО одна строка, в ней маркер
|
||||
# ❌ ноль изменений = энкодер ключ проигнорировал → искать ДРУГУЮ ветку кода
|
||||
```
|
||||
|
||||
Проверять **обе** половины:
|
||||
1. **Парсер** — ключ есть в промежуточном файле, на ожидаемом месте.
|
||||
2. **Энкодер** — подмена доезжает до выхода, `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. Новый ключ, где эмитится, какие точки входа поправлены.
|
||||
2. Команду подмены и наблюдённый результат («1 строка изменилась, маркер на месте»).
|
||||
3. Питфолл «две точки входа» — чтобы следующая сессия сразу проверяла обе.
|
||||
Reference in New Issue
Block a user