Документация · архитектура · 01.09.2026

Архитектурный атлас «Ролльни»

Четыре листа вместо одного графа зависимостей. Большой граф не отвечает ни на один вопрос — он показывает всё сразу и потому не помогает решить ничего. Здесь каждый лист держит ровно тот уровень детали, который нужен для одного решения, и молчит про остальное.

Числа на листах верны на один коммит и стареют молча — за первый же день под атласом закрылись #146 и #147. Поэтому у него есть свой сторож: atlas-facts.py меряет их заново и говорит, на каком листе править.

исходники docs/architecture/*.d2 листы 4 для обсуждения rollery-atlas.excalidraw снято на 016f20c · 2026-09-06 · 15470 строк

Почему четыре, а не один

Что каждый лист умышленно не показывает

Граф всех зависимостей «Ролльни» рисуется за минуту и бесполезен: двадцать файлов, полторы сотни связей, и ни одного места, где взгляд остановится. Полезен не полный чертёж, а чертёж под решение — и тогда половину связей надо не нарисовать, а сознательно убрать.

Поэтому у каждого листа записано, что он не показывает. Лист 1 не рисует ни одной обратной стрелки — он про то, как задумано. Лист 2 не рисует ни одной здоровой связи — он про то, что мешает. Лист 3 не рисует ветвлений — один путь и целиком. Лист 4 не пересказывает контракт — только порядок, цену и доказательство.

01Карта системы Куда класть новый код и кого этой зоне можно звать. Стрелки только между зонами, внутри зоны — опись.
02Опасные связи Какую связь резать первой. Шесть узлов цепочкой сверху вниз — порядок в цепочке и есть ответ.
03Один жизненный путь Что тронет новый шаг, если его сюда вставить. Жест → состояние → модель → срез → отрисовка → сохранение, без ветвлений.
04Целевая миграция Какой шаг делать следующим и чем доказать, что не сломали. Пять шагов, у каждого четыре строки.

Лист 1 · Карта системы

Что существует

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

Зона — это право вызова, а не папка на диске: catalog живёт в play/model/, а state — один файл в корне play/. Внутри зоны перечислены файлы с числом строк, чтобы было видно, где масса: model/geometry.js — 2902 строк, вдвое больше любого соседа, и это ядро продукта, а не разросшийся утиль.

С 05.09 зон одиннадцать. Появилась core-v2 — второй движок, считающий то же самое в миллиметрах и заново (PR #172). Он не заменяет geometry.js: тот по-прежнему считает игру. У зоны ровно два ребра — раскладка игрока приходит в неё через domain/recipe-v2.js, а наружу уходит снимок (срез и радиус) через adapter.js → bridge.js. Внутрь play/model/** зона не смотрит вовсе: каталог ей передают аргументом, а не импортом.

Две зоны стоят особняком. Фасад domain/roll.js подключён, но боевого потребителя не имеет — его зовёт только собственный сторож. Лаборатория sim/ в рантайме игры отсутствует вовсе: числа оттуда переезжают в каталог руками, через доки, и это единственная связь между ними.

Карта системы
5721 × 2388 Десять зон и право вызова между ними. Внутри зоны — опись файлов с числом строк; стрелок между файлами нет ни одной.

Лист 2 · Фактические опасные зависимости

Что мешает переезду

Решение Какую связь резать первой? Не показывает: ни одной здоровой связи. Порядок сверху вниз — это порядок резки, и он же ответ листа: шаг N нельзя доказать без шага N−1.

Цифры на листе считаны, а не оценены: grep -c по каждому файлу на 01.09.2026. Красным помечено ровно то, что просили — глобалы, порядок загрузки classic-скриптов, связи «рендер → состояние» и временные подмены S.

Главное, что видно из цифр: вход модели не назван. geometry.js читает S.base, S.shape, S.turns, S.hand прямо внутри buildModel — значит посчитать ролл, не тронув живую сессию, нельзя. Все шесть редакций подмены S существуют ровно потому, что безымянный вход надо чем-то подсунуть.

Половина лечения уже стоит в коде: объект g в geometry.js:2403 — «паспорт модели: всё, что геометрия раньше подсматривала в S и B(), снято здесь один раз». Это готовый ModelInput, только собранный внутри функции, а не переданный снаружи.

Одна поправка к первой редакции листа. Файлы были разнесены на «писателей» и «читателей» — деление было моё, а не измеренное. На самом деле ни один файл не является только тем или только другим: ui/actions.js пишет 29 раз и читает 26. Это не смягчает картину, а делает её хуже: у объекта нет ни одной границы — ни на запись, ни на чтение.

РежемУзелГде живёт ОбращенийЧем гасится
1-м① порядок скриптов play/index.html — 16 тегов, ?v=226 16шаг 1 · ES-модули
2-м② S state.js:11 — 238 записей, 215 чтений 453шаги 2 и 4
2-м③ B() state.js:214 — ключ на два поля 75шаг 2 · ModelInput
3-м⑥ подмена S album · roll · checks · puzzle · core-v2/legacy-probe 6 редакцийшаг 3 · чистый домен
4-м④ L в домене domain/roll.js:259 1шаг 3 · удалением
5-м⑤ touchModel() render/slice.js:423 — 7 файлов 96шаг 5 · распадается сам
Опасные зависимости
2523 × 4363 Шесть узлов цепочкой сверху вниз, и порядок в цепочке — это порядок резки. Цифры считаны grep -c на 01.09.2026.

Лист 3 · Один жизненный путь

Протянул циновку → увидел срез

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

Пять фаз — это не пять экранов, а пять мест, где путь меняет владельца. Красная стрелка стоит там, где владелец меняется через глобал, а не через аргумент.

Лист показывает и хорошую новость. Длинная часть пути — фазы 3 и 4, где живёт вся математика, — уже почти чистая: вся грязь собрана на входе в неё, в двух красных стрелках к S. Поднять g в аргумент — и фазы 3–4 отрываются целиком, одним куском.

И плохую. Живой срез во время скрутки (#94, закрыт) — это был тот же путь, вызванный на каждом кадре с другим rollP: «записать в S.rollP, посчитать, вернуть обратно». Предсказание сбылось иначе: новая редакция подмены пришла 05.09 не отсюда, а из ядра V2 — core-v2/legacy-probe.js, файл движка, который от монолита как раз уходит. Приём воспроизводится даже там, где его не хотели.

Один жизненный путь
2756 × 5381 Жест → состояние → модель → срез → отрисовка → сохранение. Красным — места, где путь меняет владельца через глобал, а не через аргумент.

Лист 4 · Целевой путь миграции

ES-модули → ModelInput → чистый домен → команды → снимки

Решение Какой шаг делать следующим и чем я докажу, что не сломала? Не показывает: пересказ доменного контракта. Только порядок, цена и доказательство — у каждого шага ровно четыре строки.
0 Сторож из терминала — ✅ сделано 01.09 · #147 node tools/check.js — правок игрового кода ноль. Замер на 01.09, в момент сдачи: 187 строк, 12,0 с, ВСЁ ЦЕЛО, известно 32, практика сошлось 32 при нуле провалов. Сегодняшние числа — в блоке итогового состояния (190 строк, известно 37); здесь они намеренно не обновляются, иначе запись перестанет говорить, чем задача закрывалась. Вшит в end-of-day.sh. Единственный пункт, который сам ничего не чинил и разблокировал всё. Числа про вёрстку по-прежнему только из ?check в браузере — холст и DPR там настоящие. Сторож умеет умирать молча. 05.09 все headless-инструменты падали на загрузке — state.js стал разбирать ?v2 через URLSearchParams, которого нет в песочнице vm. Пока он лежал, эти три отчёта разъехались с кодом и никто не увидел.
1 ES-модули · #72 Гасит узел ①. Поведение не меняется вовсе. ⚠ Модули не заменяют runtime-тест — это поправка автора стороннего ревью по нашему вопросу.
2 ModelInput — вход модели назван Гасит узлы ② и ③ и закрывает #148. Доказывается слепком baseline-data.js: числа обязаны совпасть побитово.
3 Чистый домен — три удаления, ни одной новой строки Гасит узлы ④ и ⑥. Открывает воркер и обратный поиск (#80): функцию можно звать в цикле, не подменяя сессию на каждой итерации.
4 Команды сессии · контракт §8 Гасит прямую запись UI в S. Переносить по одной команде, после каждой — полный прогон.
5 Снимки · контракт §11 commit 7 Гасит узел ⑤ целиком. Ради чего всё: шкуру можно менять, не трогая математику.

Чего в этих пяти шагах нет

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

#146 — топология кольца за имя — закрыт 01.09, пока собирался атлас. Пять мест снаружи читали wd.rin[NB + b], зная, что слой k = 1 — обёртка. Теперь намотка отдаёт наружу восемь вопросов вместо массивов: внутр, внеш, есть, толщина, витокЕсть, накрыт, крайВитка, голый. Это и был настоящий блокер второй ветки намотки — тот самый, которого стороннее ревью не назвало.

Следом закрыт #147, а в каталоге появилась база узумаки с winding: 'spiral': #142 закрыт. Урамаки (#3, #124) тоже закрыты. Из перечисленного открыт только #74 — роль как свойство укладки.

У вопроса «какой шаг следующий» теперь два ответа. Рядом с этими пятью шагами идёт второй план миграции — docs/decisions/ADR-001-core-v2-scope.md, шесть шагов, логика обратная: не разбирать монолит изнутри, а написать ядро заново в миллиметрах рядом и подключить флагом. Его шаг 4 уже сделан (PR #172, влит 05.09), а с #176 флаг ?v2 считает раскладку игрока, а не заготовку. Пять шагов ниже не меняют ни одного числа; ADR-001 меняет модель. Выбор между ними — решение владельца, а не следствие схемы.

Целевая миграция
2146 × 4899 Пять шагов и предусловие. У каждого: что делаем, что умирает, чем доказываем, что открывает.

Как это смотреть и править

Исходники, пересборка, карта для обсуждения

Листы — не картинки, а текст: .d2 лежит рядом с документацией и правится как код. SVG — производное, пересобирается одной командой.

# поставить d2 (один раз)
brew install d2

# пересобрать все четыре листа
cd docs/architecture && ./build.sh

# живая правка: браузер сам обновляется на каждое сохранение
d2 --watch 02-danger-edges.d2

# карта для обсуждения — пересобрать после правки листов
python3 docs/architecture/build-atlas-excalidraw.py

# эта страница
python3 docs/architecture/build-atlas-page.py

# не разошёлся ли атлас с кодом (и на каком листе править)
python3 docs/architecture/atlas-facts.py
python3 docs/architecture/atlas-facts.py --snapshot   # после правки листов

# сторож самой игры — теперь без браузера (#147)
node tools/check.js

Чем открывать

  • Эту страницу — она самодостаточна: листы вшиты внутрь, файл можно переслать или открыть с диска. Клик по листу открывает его крупно, Esc закрывает.
  • Отдельный лист — docs/architecture/svg/*.svg, открывается любым браузером, масштабируется без потери резкости.
  • Карту для обсуждения — docs/architecture/rollery-atlas.excalidraw: открыть excalidraw.com → Open → выбрать файл. Или расширение «Excalidraw» в VS Code, тогда файл открывается прямо в редакторе. Данные никуда не уходят, всё локально.

Почему карта одна, а листов четыре

Листы отвечают каждый на своё решение и потому нарочно неполны — на них нельзя показать пальцем «вот это к вот этому». Карта наоборот: одна поверхность, где слева что есть, посередине что красное, справа куда едем, и зелёный пунктир справа налево читается как «этот шаг гасит этот узел». Её можно таскать мышью, дописывать и черкать — для этого она и сделана. Внизу оставлено пустое место под два вопроса, которые решает владелец, а не я.

Что править, если код разошёлся с чертежом

Править .d2, а не SVG: SVG перезаписывается при следующей сборке. Excalidraw-карта собирается скриптом, поэтому её тоже правит генератор, а не рука — иначе следующая пересборка сотрёт правку.

Числа руками не считать. atlas-facts.py меряет их сам — счётчики, адреса file:line, длины файлов, статусы задач — и сверяет со слепком, показывая, на каком листе править каждое расхождение. Меряет через git show, а не по рабочему каталогу: над «Ролльней» работает вторая сессия, и замер с полусохранённого файла не соответствует ни одному состоянию проекта.