16 KiB
CODE-REVIEW-541-r1
Issue: #541 · «Один общий доказуемый CI-контракт для review/merge/release»
Трек: инфраструктурный (класс B/C, вход сразу на S7-code-review, решение владельца 13.09.2026)
Материал ревью: 0fd6977b47d6801b5bb8e6943188e3d7291f20b8 (рабочая копия уже на нём)
Заход: r1 · блокирующих циклов израсходовано 0 из 4
Скоуп
Диапазон origin/dev..HEAD, 4 коммита, 17 файлов:
.github/workflows/validate.yml | 93 +++
docs/DEVELOPMENT.md | 15 +-
docs/STATUS.md | 2 +-
docs/TESTING.md | 6 +-
scripts/check-inputs.mjs | 3 +-
scripts/ci-proof.mjs | 339 +++ (новый)
scripts/merge-candidate.mjs | 40 +-
scripts/mutation-gate.mjs | 61 +-
scripts/release-gate.mjs | 58 +-
scripts/release-prerelease.mjs | 20 +-
scripts/validate-gate.mjs | 41 +-
test/check-inputs.test.mjs | 11 +-
test/ci-proof.test.mjs | 190 +++ (новый)
test/merge-candidate.test.mjs | 84 +-
test/release-gate.test.mjs | 71 +-
test/validate-gate.test.mjs | 31 +-
test/validate-workflow.test.mjs | 36 +
Все файлы — классы B (гейты/скрипты/workflow/тесты) и C (документация). Ни
одного файла класса A (src/**, custom_components/**/*.py) — задача
действительно инфраструктурная, полный флоу S1–S6 законно пропущен по §1.
Трейлеры всех четырёх коммитов: Issue: #541 · User-Visible: no — верно для
чисто процессного изменения без пользовательского поведения.
Задача реализует единый машинно-проверяемый артефакт ci-proof-<run>-<attempt>
(houseplan-ci-proof/v1) и одну функцию состояний (green/missing/pending/ cancelled/stale/failed), которую разделяют review (validate-gate.mjs), merge
(merge-candidate.mjs) и release (release-gate.mjs/release-prerelease.mjs),
закрывая аудиторскую находку N2 §10: лёгкий зелёный dispatch мог заменить
старый красный full-прогон, а зелёный dispatch без исполненных mutant-job
проходил как доказательство для merge.
Как проверялось
| Гейт | Статус | Примечание |
|---|---|---|
npx tsc --noEmit, npm test (полный), npm run build + сверка бандла |
не гонял повторно | Validate зелёный на точном SHA 0fd6977b: https://github.com/Matysh/houseplan-card/actions/runs/34744556987 (подтверждено условием ревью — «дешёвые гейты на этом SHA уже подтверждены») |
node --test test/ci-proof.test.mjs test/merge-candidate.test.mjs test/release-gate.test.mjs test/validate-gate.test.mjs test/validate-workflow.test.mjs test/check-inputs.test.mjs |
прогнал сам | 75/75 green |
node scripts/process-gate.mjs (офлайн, без --issues) |
прогнал сам | «гейт пройден, предупреждений 0» |
node scripts/smoke-select.mjs --base origin/dev --head HEAD |
прогнал сам | «Исполняемого frontend-диффа нет» — браузерные смоки нечего выбирать, src/**/*.ts не тронут |
node scripts/check-docs.mjs |
не гонял | не применимо: diff не трогает src/** |
npm run golden:verify |
не гонял | не применимо: нет визуальных изменений |
node scripts/model-invariants.mjs |
не гонял | не применимо: геометрия/layout/толщины не затронуты |
python -m pytest tests_backend -q |
не гонял | не применимо: ни один custom_components/**/*.py не тронут |
YAML-синтаксис validate.yml |
прогнал сам | python3 -c "import yaml; yaml.safe_load(open(...))" — parses OK |
| Байтовая длина имён job (риск усечения GitHub Jobs API >100 UTF-8 байт, дважды ловился на этой же ветке) | прогнал сам | все резолвленные имена ≤112 байт максимум («Мутанты по диффу (1/6)…» = 94, «Доказательство выполненных проверок» = 68) — то же, что уже проверяет test/validate-workflow.test.mjs для preflight |
grep job-графа needs: proof-job против списка реальных job id в workflow |
прогнал сам | все 12 зависимостей существуют, ничего не рассинхронизировано |
Защитные AC — таблица «чем краснеет»
Все три — новый защитный код в scripts/ci-proof.mjs, чистые юниты. Мутировал руками (patch → прогон → откат), дополнительно у трёх из них есть штатные witness в scripts/mutation-gate.mjs, которые я тоже прогнал по отдельности (node scripts/mutation-gate.mjs --id=<id>).
| AC (issue #541) | Чем доказан | Чем краснеет |
|---|---|---|
Rebase/смена run attempt не переносит старое доказательство новому кандидату (проверка proof.run.attempt !== expected.attempt) |
node --test test/ci-proof.test.mjs (тест «SHA, tree, run attempt, event… cannot drift») |
Снял только часть условия про attempt (|| false && proof.run?.attempt !== expected.attempt) — тест покраснел (1 fail из 8). Восстановлено, git status чист |
Лёгкий зелёный run не проходит как full для release (policy?.full && !asBool(proof.request?.full)) |
node scripts/mutation-gate.mjs --id=release-proof-accepts-light-run (witness: test/ci-proof.test.mjs + test/release-gate.test.mjs) |
«release-proof-accepts-light-run: тест покраснел, как обязан» |
Зелёный dispatch без реально доказанных mutant-job не проходит review/merge (policy?.mutants && !asBool(proof.request?.mutants)) |
Ручная мутация той же строки | 1 fail в test/validate-gate.test.mjs («a green foreign dispatch whose mutant jobs were skipped is ignored»). Отдельного witness в mutation-gate.mjs для этой строки нет, но существующий unit-тест (унаследован от #510) реально её защищает |
Reuse без независимо проверенной зелёной source-job не принимается (!source || … || !executedCheckIsGreen(id, source.jobs)) |
node scripts/mutation-gate.mjs --id=ci-proof-trusts-reuse-without-source-job |
«тест покраснел, как обязан» |
Merge не принимает success-conclusion без proof-артефакта (!proof → missing, не green) |
node scripts/mutation-gate.mjs --id=merge-trusts-success-without-proof |
«тест покраснел, как обязан» |
Красный dispatch / отменённый dispatch не проходят review (унаследованные из #510 проверки, переехавшие в ci-proof.mjs) |
node scripts/mutation-gate.mjs --id=review-starts-on-red-validate и --id=review-returns-task-on-cancelled-dispatch |
оба «тест покраснел, как обязан» |
Все шесть прогонов мутации выполнил лично, рабочее дерево после каждого чистое
(git status --short пусто).
Разбор по AC issue (раздел «Приёмка»)
- «Full red → light green не разрешает release; настоящий full green после старого red разрешает» —
test/release-gate.test.mjs: «release skips a newer light proof but does not let it hide an older full failure» и «a later complete full proof refreshes an older red release candidate», оба green. Прочитано и логикуclassifyValidateProofs/selectCiProofVerdict— идут от новейшего run к старому, пропускаяcancelled/stale, первый решительный статус — вердикт. - «Dispatch без реально доказанных mutant jobs не разрешает merge» —
test/ci-proof.test.mjs(«green dispatch without six executed mutant jobs proves neither review nor merge») иtest/merge-candidate.test.mjs(«real merge waiter never accepts a successful dispatch without its proof artifact») — оба green; независимая проверкаexecutedCheckIsGreenберёт реальный список job из GitHub API, а не то, что заявляет сам proof. - «Законное content-addressed reuse принимается только с полными доказательствами» — тест «reuse needs a content key, marker source SHA/run and the successful source job» проверяет три угла: пустой
reuseRuns→ failed, испорченныйkey→ failed, но легитимный reuse на источник с посторонним красным job (сам overall run красный, но конкретная job зелёная) — принимается (это явно документированное поведение, не дыра: content-addressed единица — job, а не run). - «Rebase/run attempt не переносят чужие доказательства» — см. таблицу выше.
- «Тесты сравнивают consumers на одной матрице fixtures» —
test/ci-proof.test.mjs, тест «one state machine gives review, merge and release the same terminal semantics» гоняет один и тот же fixture черезObject.values(CI_PROOF_POLICIES)(review/merge/release), не только regex по YAML (тот отдельно проверяет только структуру workflow,test/validate-workflow.test.mjs).
Отдельно проверил переход документации: docs/TESTING.md/docs/DEVELOPMENT.md меняют формулировку «prereleases need the fast exact-SHA Validate only» → «every release, including a prerelease, needs the full exact-SHA Validate proof». Это выглядело как расширение объёма гейта на бету, но чтением scripts/classify-changes.mjs:heavyGatesRequested подтвердил: коммит с трейлером Release: уже до этой задачи (#479) безусловно требовал heavy=true — новая формулировка лишь чинит устаревший текст под фактическую автоматизацию и закрывает ровно ту дыру, которую описывает аудит (release-скрипт раньше принимал любой success, не разбирая light/full). Прогонов CI это не удорожает.
Что проверено и корректно
buildCiProof/evaluateCiProof— единая точка входа для review/merge/release, консистентна по всем полям (candidate.sha/tree, run.id/attempt/event, requiredChecks/executedChecks/reusedChecks).- Job-правила (
JOB_RULES) независимо перепроверяют состав иconclusionреальных job (не верят самозаявлению proof), включая фиксированное число мутантных шардов (6) и smoke-шардов (3), совпадающее сmatrix.shardв workflow. readCiProofArtifact— самодельный ZIP-ридер, покрыт unit-тестом на сконструированном архиве (store + deflate).parseReuseMarker— fail closed при отсутствии SHA/run/attempt.- Workflow YAML валиден, граф
needsфинальной jobproofсовпадает с реальными job id,if: always()обеспечивает публикацию доказательства и на красном прогоне. - Трейлеры коммитов, ветка
issue/541-ci-proof, отсутствие изменений класса A — соответствуют инфраструктурному треку §1. - Хендофф-история в issue честно фиксирует найденные и исправленные на живом GitHub Actions дефекты (усечение имени job, YAML flow-mapping с выражением) и результаты трёх реальных прогонов workflow, включая наблюдение
staleдля light/non-full runs.
Чего не проверял
- Полный
npm run gate:small/npm testне повторял целиком — заявленный зелёный Validate на точном материале (0fd6977b) уже покрывает typecheck/test/build по условию ревью; повторно прогнал только новые/изменённые целевые файлы тестов. npm run golden:verify,check-docs.mjs,model-invariants.mjs,pytest tests_backend— не применимы к диффу (нетsrc/**, геометрии или Python).- Не воспроизводил реальный workflow_dispatch на GitHub (это уже сделано автором трижды на этой ветке, со ссылками на прогоны в хендоффе); проверил логику и тесты, которые эти прогоны обвязывают.
- Не проверял отдельно
scripts/gate-reuse.mjs/ключи реюза как таковые (не менялись в этом диффе, кроме одной записи вcheck-inputs.mjs, покрытойtest/check-inputs.test.mjs).
Находки
High: 0. Medium: 0.
- Low (не блокирует, оставляю на усмотрение автора/следующей задачи).
scripts/validate-gate.mjs::provesMutantsиrealOps.jobsбольше не вызываются из продовой логикиvalidateGate— решение о доказанности mutant-job теперь принимаетevaluateCiProof/JOB_RULESвci-proof.mjs. Функция и её тесты (test/validate-gate.test.mjs:66-70) остаются рабочими и содержательными сами по себе, но как часть решающего пути они мертвы; риск нулевой (защита не ослаблена, просто задвоена), снимаю без цикла — не переписываю продуктовый код сам, вопрос ревьюеру не задаётся, это техническая деталь автора.
Вердикт
Все AC issue #541 доказаны автотестами, которые я лично прогнал и для критичных защит — лично сломал мутацией и увидел красный. High/Medium находок нет; единственная Low-находка — не более чем указание на будущий cleanup, не блокирует.
Вердикт: зелёный · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 0
Материал раунда
- Ветка:
issue/541-ci-proof, коммит0fd6977b47d6— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
1b1886c210926f1aa43a0db47a979c25472384bdgit log --all --format='%H %T' | grep 1b1886c21092 - Тело issue:
b34ab665a4f9ae621b08dcb3854faec5c4e7fdb1b8e0c3c4e1b5b62ab3eef116 - Вердикт конвейера:
green· High 0