- name
- blockwright-review
- description
- Этап 5 Blockwright, глаза — фото-ревью. Используй при работе с review.py, out/review/, findings.md, перспективными камерами, рендерами меша под теми же ракурсами, и когда нужно вынести вердикт по находкам ревью или отклонить находку измеренным числом.
# Этап 5 — глаза
Листы ставят сборку рядом с мешем, и кто-то всё равно должен на них посмотреть.
Никто этого не делает — каждый раз, для семи ракурсов и восьми фотографий.
Фото-ревью — это то самое смотрение, превращённое в шаг, который запускается.
## Что оно делает
`buildings/<name>/review.py` рендерит сборку из перспективных камер, рендерит
эталонную геометрию из тех же камер через Blender, копирует туда же чертежи и
фотографии и пишет промпт, называющий всё это. Всё приземляется в `out/review/`.
Эта папка — вся доказательная база ревью, и читается она **за один раз** моделью,
ведущей сессию.
```sh
python -m buildings.<name>.review --mesh
```
Без `--mesh` рендерится только постройка — быстрее, но сравнивать её будет не с
чем, кроме чертежей и фотографий. С `--mesh` рендерится то, что есть: модель или
захват. Если нет ни того ни другого, скрипт так и говорит и не притворяется.
**Промпт описывает только то, что в папке действительно лежит,** и говорит о
каждом виде картинок, чего он стоит. Захват — надёжен для объёма, врёт про
материал, под крышу не заглядывал. Модель — авторитет по форме, молчит про
материал. Чертёж — намерение, а не постройка. Фотография — материал и ритм, ни
одного размера. Ревьюер, которому пообещали фотографии и не дали ни одной, идёт
искать материал в рендере, где материала нет, и докладывает палитру блоков как
цвет здания.
**Ревью без референсов не бывает.** Если в папке нет ни рендеров эталона, ни
чертежей, ни фотографий, скрипт говорит `not enough for a review` и ревью не
просит. Тогда единственной опорой ревьюера остаётся его собственное представление
о том, как здание должно выглядеть, и на выходе получаются не слабые находки, а
выдуманные. Увидел эту строку — скажи, чего не хватает, и остановись.
## Ревьюер — отдельный агент, и это часть этапа
Ревью делает `blockwright-photo-reviewer`, а не та сессия, которая писала
рецепт. Сессия знает каждое число в `derived.json`, и половина находок
незагрязнённого ревьюера ей недоступна: она смотрит на рендер и видит то, что
собиралась построить.
Два здания сделали ревью сами и оба это записали как нарушение. **Вызов
субагента здесь — не самодеятельность, а шаг этапа**; если инструкции сессии
запрещают агентов вообще, это стоит сказать вслух и пометить находки как
зрячие, а не молча сделать вид, что правило соблюдено.
## Что промпт теперь говорит сам
Три вещи, которых в нём не было и из-за которых ревью промахивалось. Все три
пишет `blockwright.reviewing`, править их в здании не надо:
**Про свет.** Две итерации подряд выдали «одна длинная грань — глухая серая
стена» и «тональность инвертирована». Обе — тень рендера: `render.py` намеренно
светит так, чтобы крыша и стена разошлись по яркости. Промпт теперь говорит
судить материал **по оттенку, не по светлоте**.
**Про цвет.** Строка «игнорируй ограниченность палитры» читалась как «цвет не
смотри», и здание осталось не того цвета с формулировкой «нет подходящего
блока». Теперь: игнорировать **ступеньки и сетку блоков**, а цвет и материал
докладывать всегда. Назвать увиденный цвет — работа ревьюера; искать под него
блок — не его.
**Про части, которые должны отличаться.** Каждый прочий вопрос — «постройка
против эталона». Одинаковость двух башен — отношение **внутри** здания, и её не
видит ни гейт (тот же силуэт, тот же профиль), ни ревью, пока не спросишь. Промпт
спрашивает в обе стороны: одинаковость там, где в оригинале разница, и разница
там, где повтор. Плюс отдельный вопрос про территорию — зонтики, лежаки,
изгороди, посадки: пустая палуба докладывается как пропущенный этаж.
Повторяющийся шум, который остаётся отклонять руками: **«поднять навес выше»**.
Ревьюер видит фотографию изнутри, где фермы читаются арками, и не видит сечения.
Отклонять числом каждый раз заново и записывать отказ.
## Пары кадров могут быть зеркальными
Промпт обещал: «пара с одним номером — одна и та же камера в обеих моделях».
Три здания из пяти стоят к своему захвату зеркально или на пол-оборота
(`registration.turned` в `derived.json`), камеры задаются долями рамы — и пары
показывали **разные стороны здания**. Ревьюер честно сравнивал дворовый фасад с
уличным и объяснял разницу дефектом постройки.
`shot_spec` теперь отражает доли по флипам, которые намерил `derive`, а раму и
датум берёт **из `derived.json`**, а не подгоняет собственные — это та же
болезнь «две регистрации», что была у гейта. Если фит не записан, промпт вместо
обещания печатает предупреждение: сверь, что кадры показывают одну сторону.
## Одна папка, один запрос, один ревьюер
**Один запрос**, потому что ревьюер, видящий все ракурсы разом, способен понять,
что дефект на одном ракурсе и дефект на другом — это один и тот же дефект, и
взвесить, какой ракурс показывает его лучше. Разбитый по запросам, он доложит одно
и то же трижды в трёх разных размерах, и ни у одного запроса не будет
доказательств, чтобы решить, какой прав.
**Один ревьюер**, потому что второй был там, чтобы проверять первого, — а это
проверка инструмента, а не здания. Стоила она эндпоинта, учётных данных и имени
модели, прибитого в репозитории (три вещи, которые протухают), а покупала оценку
согласия, по которой никто ничего не делал.
**Ревьюер — та модель, которая уже работает. Имени модели в этом шаге нет
нигде** — ни в `review.py`, ни в промпте, ни в конфигурации агента.
## Ревьюеру дают картинки и больше ничего
Не `derived.json`, не константы, не вывод гейта. Это то же правило, на котором
работает гейт, применённое к другой половине работы: **постройка оценивается
входом, который ей не давали**.
Ревьюер, держащий измеренные числа, объясняет сборку ей же самой, и находка,
которая важна — та, где сборка уверенно и измеримо неправа насчёт чего-то, чего
числа никогда не покрывали, — ровно та, которую он перестаёт делать.
| Ревью решает | Ревью не решает |
|---|---|
| материал, цвет, ритм, пропорцию | ни одного размера |
| какие части существуют, а каких нет | насколько они высокие или широкие |
| читается ли часть тем, чем она является | стоит ли часть там, где говорит эталон |
Одно исключение по авторитету: если эталон — **полноценная 3D-модель**, то она
решает форму, и расхождение постройки с ней это ошибка постройки. У захвата такого
права нет.
## Измеренное число бьёт находку
Ревьюер не видит сечения по мешу и будет называть вещь низкой всякий раз, когда
настоящее здание выглядит выше, чем захват. Две находки с реального примера, обе
верные как наблюдения и обе отклонённые:
- *«крона навеса должна стоять на 6–8 м выше линии крыши»* — крона измерена в
25.2 м, и меш заканчивается на 25.2 м. Не так была читаемость, а не высота, и
чинилось это понижением хлама на кровельных террасах, пока свод не начал
читаться сводом.
- *«навес вылетает на несколько метров за обе длинные стены»* — меш не читает
ничего выше 21.9 м над передним крылом, поэтому вынос на 23 м — это +2.2 м на
станции, которую сечение оценивает, и она падает.
Обе видны на фотографиях. Ни одной нет в захвате. Спор решает правило этапа 1:
фотографии авторитетны для материала и ритма и никогда для размеров.
**Отклонённая так находка записывается как отклонённая — вместе с числом, которое
её отклонило.** Иначе её найдут снова в следующем раунде, и в раунде после него.
## Цикл, который сходится
Ревью, у которого нет второго прохода, — это не проверка, а мнение. Пять зданий
подряд сделали один раунд, записали половину находок как «исправлено» и на
результат правки не посмотрел никто. «Исправлено» при этом означало «изменился
**код**», а не «изменилась **постройка**» — и на одном здании починка честно
записана как «changed nothing», потому что кто-то случайно глянул.
Раунд k:
**1. Числа.** `python -m tools.run <имя>`. Красный гейт останавливает цикл:
глаза не тратятся на постройку, которую уже отвергли числа.
**2. Рендер.** `python -m buildings.<имя>.review --mesh`. Прошлый раунд
уезжает в `out/review/rounds/NN/`, `DESCRIPTION` обязан быть написан — иначе
отказ.
**3. Разведка.** `blockwright-photo-reviewer`, слепой: без чисел, без кода, без
находок прошлых раундов. Один запрос на всю папку.
**4. Триаж.** Каждая сырая находка ложится в журнал
`buildings/<имя>/findings.md` с идентификатором. Ревьюер слеп по построению и
поднимает одно и то же каждый раунд — счётчик `seen` отличает повтор от новизны.
**5. Починки.** По одной правке на находку, гейт после каждой. `tools/run.py` с
его дельтой делает это дёшево: правка глаз не смеет двигать станции сечения.
**6. Подтверждение.** `python -m tools.review_verify <имя>` собирает
`out/review/verify/` — «до», «после» и референс по каждому **сдвинувшемуся**
ракурсу. Ракурс, который не сдвинулся ни на пиксель, туда не попадает и
называется отдельно: правка до него не дошла. Папка уходит
`blockwright-fix-verifier` — **другому** агенту, с закрытым вопросом «устранён
или нет». Вердикт возвращается в журнал.
`tools/review_diff.py` отвечает на тот же вопрос механически и раньше:
рендерер детерминирован, поэтому неизменившийся кадр — это доказательство, что
правка не легла, а изменившийся кадр там, где никто не работал, — коллатераль.
**7. Когда остановиться.** Цикл закрыт, когда очередная разведка не приносит
ничего выше «мелкой детали», а в журнале каждая находка либо `built`, либо
`rejected` с числом, либо `ungraded` с названным недостающим входом. Или
исчерпан бюджет раундов — тогда это говорится вслух.
**Почему сходится, а не колеблется:** закрытие только по картинке (нет
«исправлено, но ничего не поменялось»); отказ только числом плюс счётчик
рецидивов (нет пережёвывания); одна правка на находку и гейт после каждой
(глаза не ломают размеры); идентификаторы (один дефект не переименовывается в
три).
## Проверяющий — не тот же агент, что ревьюер
Разведка не должна знать, что искали, иначе перестаёт находить неожиданное.
Проверка обязана знать — она судит правку, а не здание. Это единственное место,
где правило «ревьюеру только картинки» гнётся, и гнётся оно осознанно: утечка
намерения безопасна ровно потому, что задача другая. Два разных агента —
структурная гарантия, что наводящий вопрос не попадёт в открытый поиск.
## Находки — это файл, а не разговор
Журнал — `buildings/<имя>/findings.md`, **над** `out/`: всё под `out/`
удаляется целиком, а журнал ничем не пересобирается. Полный формат — в
`/blockwright-review`, раздел «Записать». Хроника раундов сверху (пустой
раунд — заголовок без пунктов: это сходимость), блоки находок снизу:
```markdown
### F-03 | open | gate 5 | round 1 | seen 2 | by: reviewer
**The two towers are identical; the left one is balconied.**
**Fixed:**
**Arose from:** the skeleton offers no balcony primitive.
**Prevented by:** a `build.balcony` primitive and a line in the skeleton.
```
Закрытая находка без **Prevented by** — починка, которую нельзя обобщить.
`by` по умолчанию `reviewer`; `by: human` — нашёл человек.
Четыре диспозиции, и они не взаимозаменяемы:
- **built** — изменился код **и** изменилась картинка. Обе половины
обязательны: `tools/review_diff.py` существует потому, что раньше «built»
означало только первую.
- **rejected** — отклонено **числом**, и число записано. Не «мы не согласны», а
измерение с указанием, откуда оно.
- **open** — увидено, признано, ещё не построено.
- **ungraded** — ничто из поданного не решает. Тот же серый вердикт, что у
гейта, и он обязан назвать недостающий вход.
`seen` — сколько раундов слепой ревьюер это поднимал. **Отклонённая находка,
поднятая трижды, эскалируется человеку.** Ревьюер не упрямится — он не видит
прошлых находок; значит либо число, которым её отклонили, не покрывает того,
что он видит, либо отклонение было неверным. `blockwright.findings` печатает
это на каждом прогоне.
**Отказ «нет подходящего блока» — не аргумент, пока не назван список.**
```python
from blockwright import blocks
print("
".join(blocks.why_not((185, 205, 215))))
```
печатает пять ближайших с расстояниями. Дважды здание оставили не того цвета с
формулировкой «в палитре нет такого оттенка», и оба раза кандидаты нашлись —
один из них в собственном захвате того же здания. Брать ближайший по **оттенку**,
даже если он темнее: цвет верного тона и неверной светлоты читается зданием, а
серый верной светлоты — нет.
Это журнал для глаз ровно так же, как `report.json` — журнал для чисел, и по той
же причине: дефект, который только произносили вслух, нельзя посчитать,
отследить между раундами и показать исправленным.
## Рендерить свежее или не ревьюить
`review.py` отказывается работать против схематики старше рецепта, который её
сделал. Версия этого правила у гейта защищает оценку; эта защищает час чужого
времени, потраченный на починку дефектов, починенных на прошлой неделе.
## Когда запускать
После каждого раунда работы над деталями — это та половина пайплайна, у которой
нет замкнутого цикла. У объёмного этюда есть гейт; у деталей есть это.
## Последний метр: схематика против мира
Ревью рендерит `build.schem` (а на воротах 4, с `--greybox`, — `greybox.schem`).
Человек смотрит в мир, куда кто-то её вставил. Между
этим — ничего, и починка, не доехавшая до мира, читается ровно как починка,
которой не было. Один разбор ушёл в никуда именно так.
`finish` пишет `out/<имя>.stamp.md` — перечень блоков по типам. Мир не хешируется,
но умеет считать:
```
//pos1, //pos2 вокруг вставленной постройки
//count minecraft:white_concrete
```
Совпало — та же сборка. Не совпало — вставляй заново, прежде чем судить скриншот.
Ревью печатает напоминание каждый прогон.
## Камеры
`PLAN` в `review.py` — список кадров. Держи там несколько наружных ракурсов с
разных сторон и **несколько камер прямо внутри территории**: двор, проезд,
уровень земли — это то, что захват реконструирует хуже всего (он снят с воздуха)
и что фотографии показывают лучше всего.
Кадры перспективные, а не ортографические. Ортография даёт силуэт, а силуэт — это
ровно то, что уже проверено числами и что уже один раз соврало.
**Одна камера обязательна и не про здание** — `05-approach` в шаблоне: уровень
глаз, широкий объектив, с той стороны, откуда к зданию подходят пешком. Все
остальные кадры выстроены так, чтобы здание влезло целиком, — а такого вида не
бывает ни у кого. Человек видит первым делом первый этаж и то, как здание
встречает тротуар: вход, стилобат, бордюр. Это и та часть, о которой меньше всего
знает захват с воздуха, и та, которую сборка чаще всего оставляет голым цоколем.
`DESCRIPTION` — одна-две фразы о том, что это за здание. Больше ревьюер о
постройке не знает и знать не должен.
### `azimuth` — не компас
`Outside.azimuth` крутится вокруг **длинной оси самого здания**: 0 — за дальним
торцом по +u, 90 — за фланком по +v. Крой карты стоит к северу под своим углом
(на одном здании 33°), поэтому имя камеры, выведенное из азимута как «east» или
«beach», выведено не из того угла — и переживает всё, кроме взгляда на рендер.
Три камеры одного круга были названы не той стороной.
Две строки печатаются до рендера, чтобы это не проверялось картинкой:
- **где стоит глаз** — доли рамы, и за каким фланком или торцом это. Камера
`beach`, чей глаз за тем фланком, где палубы нет, названа неверно.
- **отказ, если глаз внутри постройки.** Камера в дворе или под навесом стоит в
воздухе и проходит; камера внутри пилона арки рендерит коричневую стену во
весь кадр, и это уже уезжало ревьюеру вместе с описанием того, что кадр
якобы показывает.
View on GitHub