Issue: #551 User-Visible: no
15 KiB
CODE-REVIEW-551-r4
Скоуп
Issue #551 (инфраструктурная, метка infra) — разнесение конвейера ревью на
три job с независимыми бюджетами. Раунды r1–r3 уже признали конструкцию
выполненной (зелёный, без находок); в задачу этот вердикт до сих пор не
применён — оба последних захода (r2, r3) закончились зелёной моделью, но
стадия публикации/интеграции падала на живых прогонах уже после
слияния/повторной активации process.yml, и автор возвращал задачу в
S6-in-progress для фикса именно интеграции, а не по замечанию ревью.
Материал этого раунда — ровно 62e0eff21999eebfc9aecbfe99edb14d9ccf716f.
Дельта отсчитывается от материала r2/r3 (23f48829c26deec968af6645301900880899139e,
дельта r2 от r3 — нулевая, см. ниже): один коммит 62e0eff2 поверх
23f48829, 3 файла, +26/−5 строк (git diff --shortstat 23f48829..62e0eff2):
.github/workflows/process.yml— шаг «Проверить полноту и происхождение результата» в jobintegrate: записьstructured_outputв$GITHUB_OUTPUTзаменена с{ echo <<EOF_RESULT; cat verdict.json; echo EOF_RESULT; }на явныйprintfс гарантированным\nперед закрывающим delimiter'ом;scripts/mutation-gate.mjs— новый мутантreview-integration-delimiter-follows-json-without-newline;test/review-doc-guard.test.mjs— два новыхassertв существующем тесте#551: gates, модель и интеграция…, закрепляющие фикс.
Класса A нет (тот же трек, что r1–r3), ТЗ/ревью ТЗ не требуются.
Что чинит эта дельта (по логам, не со слов автора)
Живой дефект: run 34754400084
упал в integrate с Matching delimiter not found при чтении
structured_output. Причина в бывшем коде: cat "$dir/verdict.json" пишет
JSON как есть (если файл без завершающего LF — а jq -e/сериализаторы это не
гарантируют), а следующая по счёту команда echo 'EOF_RESULT' в блоке { ... } >> file — это отдельный write(), который ничего не подставляет между
предыдущим выводом и своим: без LF в конце JSON delimiter приклеивается к
последней строке значения, и GitHub Actions не находит начало delimiter'а на
отдельной строке.
Фикс (process.yml:1212-1216 после дельты):
printf 'structured_output<<EOF_RESULT\n' >> "$GITHUB_OUTPUT"
cat "$dir/verdict.json" >> "$GITHUB_OUTPUT"
printf '\nEOF_RESULT\n' >> "$GITHUB_OUTPUT"
Явный \n перед EOF_RESULT гарантирует делимитер на отдельной строке
независимо от того, кончается ли verdict.json переводом строки. Если файл
уже кончается LF — на выходе будет один лишний пустой перенос перед
delimiter'ом; это не влияет на jq-парсинг ниже по конвейеру (OUT: ... structured_output, printf '%s' "$OUT" | jq -r) — JSON с хвостовым
whitespace для jq эквивалентен исходному. Прочитано и проверено вручную.
Это прямое попадание в AC3 issue («timeout/cancel/failure каждой стадии оставляет понятное состояние; лимиты review cycles не расходуются на невыполненный review») — двумя живыми прогонами (r2→r3 и r3→r4) подтверждено, что счётчик циклов действительно не тратился на несостоявшуюся интеграцию («блокирующих циклов израсходовано 0 из 4» в постановке этого захода), т.е. защитное свойство AC3 сработало даже при поломке самой публикации.
Проверено
git diff 23f48829..62e0eff2— три файла, ровно эти три хунка, ничего лишнего; jobguard,prepare,model_review, структура контракта, проверки sha256/полей — не тронуты ни строкой.- Прочитано построчно: новый код в
process.yml:1212-1216синтаксически и семантически корректен (см. разбор выше), остальной шаг («Проверить полноту и происхождение результата») не менялся вне этих трёх строк. - Независимо (не полагаясь на заявление автора) прогнал:
node --test --test-name-pattern="#551" test/review-doc-guard.test.mjs→ 1/1 pass;node scripts/mutation-gate.mjs --id=review-integration-delimiter-follows-json-without-newline→ «чистый прогон» + «заявленный тест покраснел на мутанте» → поймано 1 из 1. Мутант возвращает ровно старый баг (printf 'EOF_RESULT\n'без ведущего\n), и новыйassert.match(resultOutput, /cat ...>> "\$GITHUB_OUTPUT"[\s\S]*printf '\\nEOF_RESULT\\n' >>.../)действительно красится на этом мутанте — дисциплина «тест умеет падать» соблюдена для прогнанного мной гейта.node scripts/smoke-select.mjs --base 23f48829 --head 62e0eff2→ «Исполняемого frontend-диффа нет… Browser-smoke этим диффом не выбираются» — выбирать нечего,src/**не тронут.
- ID нового мутанта уникален в реестре (
grep -n 'review-integration-delimiter-follows-json-without-newline' scripts/mutation-gate.mjs— одно определение), guard-команда та же, что и у соседних #551-мутантов, конфликтов нет. - Трейлеры коммита
62e0eff2:Issue: #551,User-Visible: no— верно, правка меняет только служебный CI-механизм передачи данных между stages, видимое поведение продукта не меняется; changelog не требуется.
Не перегонял npx tsc --noEmit / npm test / npm run build + сверку трёх
копий бандла — Validate зелёный на этом точном SHA:
https://github.com/Matysh/houseplan-card/actions/runs/34754874119 (упомянут
автором и подтверждён как ссылка на прогон именно этого коммита). Не
перегонял check-docs.mjs, golden:verify, backend pytest, инварианты
модели, performance-профили и полный набор browser-смоков — diff не касается
src/** (кроме тестового файла test/review-doc-guard.test.mjs, не
влияющего на отпечаток документации), Python, геометрии/layout/
marker.space/open_spans, визуального рендера или производительности;
smoke-select.mjs подтверждает пустой отбор.
Закрытие раунда r3
r3 не содержал находок («Находок нет», зелёный, High 0 / Medium 0) — этому раунду нечего закрывать по замечаниям. Причина существования r4 — операционный сбой стадии публикации/интеграции ПОСЛЕ зелёного вердикта r3 (run 34754400084), диагностированный автором как отдельный дефект границы стадий (delimiter приклеен к JSON). Дельта этого раунда — прямой фикс именно этого сбоя (см. разбор выше); других изменений кода в диапазоне r3→r4 нет.
Унаследовано из r3 (без повторной проверки)
Документ r3 не был закоммичен в docs/reviews/ (интеграция того захода
упала на дефекте, который правит именно материал этого раунда) — источник
вывода r3: комментарий ревью в issue #551 от 2026-09-13T11:29:02Z, материал
23f48829c26deec968af6645301900880899139e (дельта от r2/r3 к r4 — только
три файла выше, всё остальное дерево идентично). Наследуется:
- разнесение конвейера на три job (
prepare/model_review/integrate) с независимыми бюджетами 55/45/55 минут и их назначение по коду — подтверждено в r1 (docs/reviews/CODE-REVIEW-551-r1.md, материал31ef70ce) и повторно не тронуто ни в r2, ни в r3, ни в этой дельте; - контракт передачи между стадиями (
prepared.json/manifest.sha256, полная сверка полей черезjq -e,git rev-parse HEAD/HEAD^{tree}против заявленного материала) — из r1, не задет; - поведение при timeout/cancel/failure каждой стадии и правило «ранний отказ не расходует цикл» — из r1, не задет; двумя живыми провалами интеграции (r2→r3, r3→r4) это правило дополнительно эмпирически подтверждено вне ревью;
- отказ на неполном/чужом evidence (точный набор имён файлов artifact'а, sha256, отказ от failed/cancelled/skipped результата модели) — из r1, не задет;
- фикс r2 «structured verdict сохраняется как объект, а не boolean предиката
jq -e» (process.yml, шаг «Запечатать результат модели», строки до- name: Передать результат интеграции) и его мутационный свидетельreview-model-seals-verdict-as-boolean— заявлен в r2, независимо перепроверен в r3 (node --test --test-name-pattern="#551"+ прогон мутанта, «поймано 1 из 1»); дельта r3→r4 этот шаг не трогает (git diff 23f48829..62e0eff2не включает диапазонseal), поэтому принимается без повторного прогона в этом раунде; - три исходных мутационных свидетеля r1 (
review-model-checks-out-moving-dev,review-integration-skips-evidence-checksum,review-integration-trusts-failed-model) — из r1, не задеты ни одной дельтой r2/r3/r4.
Найдено
Находок нет — ни High, ни Medium, ни Low.
Не проверялось (и почему)
- Полный набор
tsc/test/build+сверка бандла,check-docs.mjs,golden:verify, backend pytest, инварианты модели, performance-профили, полный набор browser-смоков — не требуются по соразмерности (PROCESS.md §8) и по факту зелёного Validate на этом точном SHA (см. выше) плюс пустому отборуsmoke-select.mjs. - Реальное поведение исправленного шага в проде за пределами уже
показанных run — не переисполнялось повторно мной (GitHub Actions
недоступен для интерактивного запуска из этого ревью); проверка —
чтением кода плюс воспроизведением локального юнит-теста и мутанта,
которые моделируют тот же самый путь (
process.yml-текст плюс regex по нему), а не реальным запуском workflow.
Вердикт
Зелёный. Дельта r4 — точечный фикс одной строки протокола передачи данных
между стадиями (GITHUB_OUTPUT delimiter), выявленный вторым живым прогоном
после r3. Фикс корректен (разобран построчно), закреплён тестом и
мутационным свидетелем, который я прогнал независимо и подтвердил, что он
красит именно старое поведение. Всё содержательное из AC issue #551
(разнесение бюджетов, контракт, отказ на неполном evidence, поведение при
провале стадии) наследуется из r1 без изменений; правка r2 (boolean-подмена)
перепроверена в r3 и не задета этой дельтой. Находок нет, скоуп не расширен.
Материал раунда
- Ветка:
issue/551-review-stage-budgets, коммит62e0eff21999— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
8ea4718a882bb877b6288a234823406709f5659cgit log --all --format='%H %T' | grep 8ea4718a882b - Тело issue:
824f489aab52c7d76ade6634cbdb819c9c36ff9261ae2982484d5199a4bc23d0 - Вердикт конвейера:
green· High 0