#413 закрыл класс «SHA мёртв уже в момент публикации». Остаётся более частый:
SHA был жив, а умер потом — по корпусу таких объявлений 98 из 804, потому что
ветку задачи после ревью перебазируют, сквошат или удаляют.
SHA коммита — свойство истории, а история переписывается. Содержимое не
переписывается: git адресует деревья и блобы их хешем. На #403 спец-коммит
переехал из 83005c3c в 94502d3d, а блоб ТЗ у обоих один — 56a92e12; по нему
материал находится одной командой независимо от ребейза.
Конвейер снимает якоря там, где читает материал — в шаге перехода на ветку
задачи, пока рабочая копия равна тому, что прочтёт ревьюер. В шаге публикации
спрашивать поздно: дерево уже сброшено на целевую ветку. При публикации якоря
дописываются машинным блоком: дерево материала и блоб каждого ТЗ, каждый со
своей исполнимой командой поиска.
Блок машинный и помечен как машинный. Ревьюер его не заполняет: дисциплина
ручного переписывания SHA здесь уже подвела, и заменять её другой ручной
дисциплиной смысла нет.
Гейт #413 смягчён ровно там, где обязан: осиротевший SHA при живых якорях —
предупреждение, а не отказ. Ронять раунд, который воспроизводим, было бы той
же ошибкой в другую сторону. Отказ остаётся, когда не работает ни один
объявленный способ найти материал.
Проверено на настоящем осиротевшем случае: блок, собранный для 94502d3d,
находит и дерево, и блоб ТЗ; тот же документ с якорями даёт предупреждение
вместо отказа, без якорей — отказ.
Issue: #416
User-Visible: no
SPEC-REVIEW-403-r2 объявил материал раунда на `HEAD = 83005c3c`, и тот же SHA
независимо назвал автор ТЗ в комментарии issue. Разбор подтвердил находку и
уточнил её: коммит существовал, но к моменту публикации был осиротевшим.
Ветку перебазировали за пятнадцать минут ДО публикации документа — спец-коммит
переехал в 94502d3d с тем же сообщением и тем же содержимым (блоб ТЗ у обоих
56a92e12). Через раунд команда `git diff 83005c3c..HEAD` из §2.10 буквально не
работала, и r3 восстанавливал коммит по содержимому диффа руками.
Гейт судит только объявление материала в шапке документа, а не каждое
шестнадцатеричное слово: в прозе SHA упоминаются исторически, и обещания
воспроизводимости на них нет. Границы кандидата подобраны по корпусу — 7–40
знаков, хотя бы одна буква, не после `#`, не внутри длинного хеша; это
отсекает sha256, цвета и номера прогонов.
Достижимость считается от refs/remotes/origin, а не от локальных ссылок.
Разница не теоретическая: осиротевший 83005c3c до сих пор достижим в клоне
автора из необновлённой локальной ветки — локальная проверка сказала бы «всё в
порядке» ровно на той машине, где ошибку и совершили.
Шаг стоит ПОСЛЕ публикации и ДО перестановки метки. Артефакт ревью терялся
здесь трижды (#171, #220), и «вердикт без документа» дороже мёртвой ссылки:
документ сначала спасается, потом судится. Инвариант «метка не сменилась =
прогон упал» при этом сохраняется.
Проверено на настоящих документах: SPEC-REVIEW-403-r2 отказ, CODE-REVIEW-390-r1
проходит, документ без объявления материала не судится.
Issue: #413
User-Visible: no
28.08 коммит bb2919f уехал в dev с тридцатью файлами вместо одного markdown:
откатил отревьюженную реализацию #359, вернул старые чанки, оставил в dist/
двойной набор. dev держал откаченное дерево три часа. Сообщение коммита было
невинным, и от рутины инцидент отличался только диффом.
Механизм воспроизведён локально, а не предположен. `git checkout -- .`
восстанавливает рабочее дерево ИЗ ИНДЕКСА, `git clean -fd` убирает
неотслеживаемое — ни то, ни другое индекс не трогает. Ревьюер работает с Bash и,
проверяя «умеет ли тест падать», вполне может сделать git add; всё оставшееся у
него в индексе прежняя уборка сохраняла, и следующий git commit забирал это
вместе с документом.
Отсюда три рубежа, каждый закрывает свой отрезок пути.
База: reset --hard на свежий origin/$target снимает и индекс, и дерево разом.
Терять нечего — документ приезжает из RUNNER_TEMP, а не из рабочей копии.
Индексируется ровно один путь, а не каталог.
Индекс: перед коммитом дифф проверяется allowlist'ом docs/reviews/.
Диапазон: перед КАЖДЫМ push проверяется origin/$target...HEAD — то есть то, что
пуш добавит в ветку. Проверок две, потому что push делается из двух мест, и
второй путь срабатывает ровно тогда, когда dev ушёл вперёд — в тех самых
условиях, при которых случился bb2919f.
Пустой дифф — тоже отказ: публиковать нечего означает, что документа нет, а
прежняя редакция шага выходила тут с нулём и оставляла вердикт без артефакта
(#171). Сравнение по префиксу каталога, а не подстрокой: docs/reviews-old и
docs/reviewsx разрешёнными не считаются. Форс-пуш отсутствует и закреплён тестом.
Четыре мутанта проверены руками, два добавлены в реестр. Пятый — «убрать одну из
двух проверок диапазона» — сначала выжил: тест требовал наличия, а не количества.
Тест усилен до подсчёта, мутант убит.
Issue: #365
User-Visible: no