Skip to content

Latest commit

 

History

History
80 lines (59 loc) · 7.55 KB

deterministic_rendering.md

File metadata and controls

80 lines (59 loc) · 7.55 KB

Design — Детерминизм кончался на байтах графа, а обещан был ответу

Статус: ✅ сделано (2026-09-12, без изменения схемы; сьют 928 → 938). Мотивирует: гэп cycle_rotation_nondeterminism_2026-09-12 — codemap#20, от лаборатории при подъёме пина на 0.0.17. Бэклог: R1-C54. Пользовательские доки: ../ci.md, README («deterministic»).

Неизменные инварианты: source-only, детерминизм, два тира, resolved-or-honestly-flagged, закрытый словарь рёбер, extras открытый, ответ называет, чего он не судил.


1. Что обещано и где проверялось

README обещает deterministic (canonical sorted JSON, no timestamps — diffable). CI держит это отдельной работой: собрать одно дерево дважды и сравнить байты графа. И только их.

Между «артефакт воспроизводим» и «ответ воспроизводим» — зазор, в который и провалился дефект: граф побайтово один и тот же, а печать цепочки цикла меняется от прогона к прогону, потому что nx.simple_cycles входит в цикл там, где его застал обход множеств, то есть хэши строк.

Заявитель почти записал это в журнал как поведенческую правку релиза. Не записал потому, что перемерил внутри одной версии прежде, чем писать вывод, — и это самая дорогая часть находки: дефект подделывается под сдвиг поведения, а наша собственная процедура релиза как раз и сверяет вывод двух опубликованных версий на одном дереве.

2. D1 — канонизировать у источника

Сделано: ротация приводится к началу с минимального узла в query.py, где циклы рождаются.

Замер решил вопрос «где», а не вкус: под восемью хэш-сидами разошлись три поверхности, а не одна заявленная — check, report architecture (markdown) и build_architecture (JSON, который отдаёт MCP-инструмент architecture). report dependencies и живые доки устойчивы, потому что печатают счётчики, а не цепочки.

  • Отвергнуто: канонизировать в arch.py. Вылечило бы гейт — тот, про который пришла заявка, — и оставило течь отчёт и машиночитаемый ответ MCP. Чинить там, куда указал репорт, было бы ремонтом по симптому.

3. D2 — канонизировать и порядок циклов

arch.py сортировал sorted(cycles, key=lambda c: (len(c), c)) — и это выглядело канонизацией. Не было: ключ двигался вместе с ротацией, ["a","b"] и ["b","a"] — один цикл и два ключа. После ротации сортировка начинает значить то, чем казалась, поэтому она переехала в тот же помощник: сначала ротация, потом сортировка, один раз, у источника.

4. D3 — сторож поперёк сидов, а не юнит на ротацию

PYTHONHASHSEED читается один раз при старте интерпретатора, поэтому условие нельзя создать monkeypatch'ем внутри процесса — сторож порождает процессы. Восемь сидов × семь поверхностей, и в список включены те три, что уже были устойчивы: регрессия в них обязана ронять тест, а сторож уже́ проверяет заявление целиком, а не тот угол, откуда пришла жалоба.

Положительный контроль двойной. Юнитом — три ротации одного цикла сводятся к одной форме, и тест краснеет, если канонизацию убрать. Интеграционно — измерено до правки: три различных вывода у каждой из трёх поверхностей на восьми сидах (числа в гэпе §2).

5. Приёмка

  1. ✅ До правки: 3 различных вывода на 8 сидов у check, report architecture и JSON architecture; у байтов графа, report dependencies и доков — 1.
  2. ✅ После: 12 сидов, все шесть поверхностей по одному выводу.
  3. ✅ На живом дереве (bquant, 9 настоящих ленивых циклов): пять сидов — один и тот же текст, a3f5b2a5…, и все цепочки начинаются со своего минимального узла.
  4. ✅ Фикстура сторожа несёт и 3-цикл, и 2-цикл, поэтому проверяется и порядок самих циклов, а не только ротация внутри каждого.
  5. ✅ Схема не менялась; graph.json не менялся — правка живёт целиком в слое ответа.

Чего это не покрывает. Сторож перебирает восемь сидов, а не все возможные; это не доказательство, а выборка, которой достаточно, чтобы поймать зависимость от порядка хэшей (до правки она ловилась на любых двух-трёх сидах из восьми). Другие источники порядка — greedy modularity в communities, порядок jedi в deep-тире — этим проходом не исследовались; deep-тир вообще не байт-стабилен по построению (R1-C42), и обещание про него сужено отдельно.

6. Чего не увидел проход R1-C53, хотя смотрел рядом

R1-C53 спрашивал у каждой операции: объявляешь ли ты своё сужение. Правильный вопрос, и не тот: ответ может быть полон, объявлен — и разный от прогона к прогону. Воспроизводимость ответа в матрице сужений не значилась ни как класс, ни как исключение. Добавлено восьмым классом reproducibility с этим сторожем как его объявлением.