17 KiB
CODE-REVIEW-573-r1
Issue: #573 — Release proof: переиспользовать проверки product tree после
baseline-only commit без второго полного Validate
Трек: infrastructure (класс B/tests/process, класса A нет)
Материал: 47f36e571c723ee163bb10dc44a31582b5e9e5fc (единственный коммит на
issue/573-release-proof-reuse, база origin/dev = 06bf9b69)
Заход: r1 · блокирующих циклов израсходовано (до этого раунда) 0 из 4
Скоуп
Proof Validate до этой задачи называл только tree кандидата. Приёмка
эталонов (ad4000f9 на beta.3) меняла его целиком, хотя продукт не менялся —
второй Validate честно перегонял smoke/perf/6 шардов мутантов, которые уже
были доказаны первым прогоном. Причина: source-fingerprint.mjs называет
корпус строкой-каталогом demo/golden, а замыкание входов раскрывало эту
строку во все текстовые файлы под каталогом, включая
demo/golden/baselines/baselines-index.json — индекс эталонов утекал во
входы smoke/perf/browser-guard мутантов.
Правка вводит:
BASELINE_OVERLAYвcheck-inputs.mjs— раскрытие каталога по строке больше не выдаёт overlay эталонов; явный корень (golden) и явная ссылка на файл — как были.- Составное evidence в proof (
ci-proof.mjs):product.tree(ls-tree без overlay),baselines.{tree,manifestSha256,reviewedRun},keys(content- ключи пяти реюзных job, включая исполненные). evaluateCiProof({expected, reviewedRun})— потребитель на checkout кандидата сверяет evidence, а не верит ему;release-gate.mjs/release-prerelease.mjsсчитаютexpectedтолько когдаHEAD == sha.
Класса A нет, User-Visible: no корректен, трейлеры (Issue: #573,
User-Visible: no) на месте.
Как проверялось
Дешёвые гейты (typecheck, npm test, npm run build, check-docs) на этом
SHA уже подтверждены зелёным Validate (run 35262504454, https://github.com/Matysh/houseplan-card/actions/runs/35262504454) —
не перегонял.
Сам прогнал (диф не трогает src/**, геометрию, рендер — полные наборы не
нужны):
| Гейт | Команда | Результат |
|---|---|---|
| Выбор смоков | node scripts/smoke-select.mjs --base origin/dev --head HEAD |
«Исполняемого frontend-диффа нет … Browser-smoke этим диффом не выбираются» — смоки не нужны, диф не трогает src/** |
| Покрытие входов | node scripts/check-inputs.mjs --coverage |
чисто, 0 неизвестных |
| Заявленное поведение фикса | node scripts/check-inputs.mjs --check=smoke --why=demo/golden/baselines/baselines-index.json |
«не вход smoke» — воспроизвёл ключевую заявку автора вживую |
| Целевые unit-тесты задачи | node --test --test-name-pattern="#573" test/check-inputs.test.mjs test/gate-reuse.test.mjs test/mutation-gate.test.mjs test/ci-proof.test.mjs test/release-gate.test.mjs |
11/11 pass |
| «Тест умеет падать» для AC1/защиты overlay | вручную откатил фильтр !isBaselineOverlay(f) в check-inputs.mjs, перегнал те же 4 файла |
все 4 упали (например mutation-gate.test.mjs: отпечаток свидетеля со смок-гардом изменился — ожидаемое поведение до фикса), затем восстановил файл, git diff чист |
| Инварианты геометрии | — | не нужны: диф не трогает рёбра/толщину/layout |
| golden/pytest/perf | — | не нужны: диф не трогает рендер и custom_components/**/*.py |
Не прогонял: npx tsc --noEmit, npm test (полный), npm run build,
node scripts/check-docs.mjs целиком — покрыты зелёным Validate на этом SHA.
AC → доказательство → чем краснеет
| # (из тела issue) | Доказан | Чем краснеет |
|---|---|---|
| 1. beta.3-воспроизведение: baseline-only commit reuses green, golden перегоняется, final proof green | test/ci-proof.test.mjs #573 AC1; test/gate-reuse.test.mjs #573 (синтетика C→B); реальная пара e5fe4364→ad4000f9 в комментарии автора |
мутант baseline-overlay-leaks-into-every-key (проверено лично, см. таблицу выше — 4/4 теста падают на откате фильтра) |
| 2. source+baseline вместе → соответствующие job исполняются | test/gate-reuse.test.mjs #573 (правка src/card.ts вместе с эталонами меняет ключи smoke/golden/perf) |
тот же мутант (ключи перестают различать источники) |
| 3. красный smoke/mutant/perf не прячется зелёной golden | test/ci-proof.test.mjs #573 AC3 |
контракт #541 (reuse-* мутанты, не менялся в этой задаче) |
| 4. подмена ключа/tree/индекса/reviewed-run → fail-closed | test/ci-proof.test.mjs #573 AC4; test/release-gate.test.mjs #573 |
мутанты proof-trusts-evidence-it-could-verify, reused-marker-key-unchecked-against-candidate, product-tree-identity-counts-baselines — прочитаны, патчи соответствуют текущим строкам кода (проверено чтением, не переисполнял мутацию вручную — они уже отчитаны Validate как «поймано 1 из 1») |
| 5. один terminal contract для release/merge/review | частично доказан — см. находку Medium ниже: реальный вызов evaluateCiProof из merge-candidate.mjs/validate-gate.mjs с настоящим reviewedRun-объектом не покрыт тестом, а поведение фактически новое |
— |
| 6. замер wall time | оценка автора по факту длительностей job прогона ad4000f9 (не тест, честно оформлено как оценка, issue не требовал автотеста) |
н/п |
Находки
Medium (в скоупе — правится в этой же задаче)
evaluateCiProof меняет поведение merge/review-потребителей без теста и
вопреки утверждению автора, что оно не меняется.
- Файл:
scripts/ci-proof.mjs, блокevaluateCiProof(строки 297–311), фактически задействован черезscripts/merge-candidate.mjs(line ~164) иscripts/validate-gate.mjs(line ~77) — оба используютloadGithubProofContext, которая теперь возвращаетreviewedRun. - Суть: блок проверки
Baseline-Reviewedrun (существует/завершён/не отменён/принадлежитvalidate.yml) срабатывает всегда, когдаproof.evidence.baselines.reviewedRunобъявлен — независимо от того, передан лиexpected/policy.full. Он не привязан к release: любой консьюмер (merge,review), которомуloadGithubProofContextвернула реальный (неundefined)reviewedRun, получает новую fail-closed проверку. - Это не гипотетика. Ровно такой коммит — исполнитель принимает эталоны
той же веткой, что и правку, с трейлером
Baseline-Reviewed:на верхушке — уже стандартная практика этого репозитория:9bf41ec3(#584, непосредственно предыдущая задача, слитая перед этой) сделан именно так и прошёл текущий S7/merge. После этой задачи такой коммит на верхушке ветки заставитmerge-candidate.mjsиvalidate-gate.mjs(тот самый гейт, что прямо сейчас ждёт Validate для код-ревью!) дополнительно зависеть от доступности GitHub API для СТАРОГО run'а, названного трейлером, — сбой этого запроса (см.catch { reviewedRun = null; }вloadGithubProofContext) закрывает merge/reviewfailed, хотя раньше они этот run вообще не проверяли. - Воспроизвёл лично:
притом что
evaluateCiProof({ run, proof /* proof.evidence.baselines.reviewedRun: 999 */, jobs, candidate, policy: CI_PROOF_POLICIES.merge, reviewedRun: null }) // → { status: 'failed', note: 'Baseline-Reviewed run 999 is missing, cancelled or not a Validate run' }expectedне передавался вовсе — то есть это не «сверка ожиданий release-потребителя», а самостоятельная новая проверка, которая молча зацепила merge/review. - Не покрыто тестами: единственный тест с
policy: CI_PROOF_POLICIES.merge(test/ci-proof.test.mjs:266) явно передаётreviewedRun: undefined, обходя именно этот путь. Ни вtest/merge-candidate.test.mjs, ни вtest/validate-gate.test.mjsнет ни одного упоминанияreviewedRun— реальный вызов черезloadGithubProofContextне проверен ни разу. - Расходится с хендоффом: автор пишет «Review/merge-потребители
получают тот же evaluate без ожиданий — их семантика не меняется». Это
верно только для сверки
expected(product tree/keys/overlay); проверкаreviewedRun— новая для этих потребителей и отexpectedне зависит. - Почему Medium, не High: направление отказа безопасное (fail-closed, не
скрывает дефект), и для ЭТОЙ задачи не проявляется (у
47f36e57нетBaseline-Reviewed). Риск — будущая спурная поломка merge/review на легитимном сценарии (баг-фикс + приёмка эталонов одной веткой), без единого теста, который поймал бы регресс в любую сторону. - Предлагаемое устранение (на выбор автора): либо ограничить проверку
reviewedRunтем же условием, что иexpected(реально нужна только release-потребителю, который заявлен как единственный, кто про это спрашивает), либо добавить тест наmerge/reviewpolicy с настоящим объектомreviewedRun(успех и отказ) и явно принять новое поведение, поправив формулировку в хендоффе.
Что проверено и корректно
BASELINE_OVERLAY/isBaselineOverlay/closure— воспроизвёл вживую и мутацией; overlay больше не течёт через раскрытие каталога, явная ссылка на файл (test/golden-index.test.mjs-подобный сценарий) остаётся входом.CHECKS.golden.rootsсодержитdemo/golden/**явным корнем — golden по-прежнему видит overlay напрямую, не через закрытие; заявка «golden не теряет эталоны» подтверждена чтением.productTreeId/baselineReviewedRun— детерминированные чистые функции, тесты покрывают приёмку эталонов (дерево не меняется) и правку исходника (дерево меняется), throws на пустом дереве и на трейлере без run id.buildCiProofпишетkeyдля исполненной реюзной job и отвергает чужой ключ маркера реюза — проверено тестом и логикой (симметрично записи и сверке).candidateExpectationsсчитает evidence только когдаHEAD == sha, иначе явный notice «проверяю по GitHub» — корректно ограничивает применимость локального сравнения только собственным checkout'ом.- Golden-гарды мутации (
golden-lamp-out-of-reach,golden-filled-tunnel-removed) — проверил чтением: оба в--mode=captureпо семантическим ассертам,demo/golden/policy.mjsзапрещает verify по одной сцене, так что потеря overlay в их отпечатке не открывает дыру. - Документация (
DEVELOPMENT.md,TESTING.md,STATUS.md) описывает именно то поведение, что в коде — сверил построчно. reviewedRun-проверка НЕ проверяетconclusion === 'success', только «не cancelled» — согласуется с самим сценарием beta.3, где просматриваемый run был красным по golden; намеренно, не дефект.
Чего не проверял
- Полный
npm test/npm run typecheck/npm run build— покрыты зелёным Validate на этом SHA, не перегонял. check-docs.mjsцеликом — то же самое, плюс диф не трогаетsrc/**.- Реальный прогон Validate с настоящим
NEEDS_JSON/reuse.outputs(jobproofв CI) — верю логике и юнит-тестам, живой YAML не выполнял. - Мутанты
proof-trusts-evidence-it-could-verify,reused-marker-key-unchecked-against-candidate,product-tree-identity-counts-baselines— сверил патчи с текущим кодом построчно (совпадают), но не переисполнял их вручную (в отличие отbaseline-overlay-leaks-into-every-key, который проверил лично); полагаюсь на отчёт Validate «поймано 1 из 1».
Итог
Ядро задачи (overlay не течёт в ключи smoke/perf/мутантов, golden видит
overlay явным корнем, release-потребитель сверяет evidence и fail-closed на
подмену) реализовано корректно и доказано тестами, которые я лично убедился,
что умеют падать. Один Medium-дефект в скоупе: новая fail-closed проверка
Baseline-Reviewed run зацепила merge/review-потребителей незаметно для
автора и без единого теста на этом пути, при уже существующем в репозитории
прецеденте (#584), который его бы затронул. High-находок нет.
Материал раунда
- Ветка:
issue/573-release-proof-reuse, коммит47f36e571c72— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
aca987ffcc31c3656ec79fea04db2890c9f2242agit log --all --format='%H %T' | grep aca987ffcc31 - Тело issue:
5ab99e532f34bdfa488c9143f2c1c1361b826159e0707a56ce70a640e8ca4fff - Вердикт конвейера:
yellow· High 0