ci: proof различает продуктовое дерево и overlay эталонов (#573)

Приёмка эталонов на beta.3 (`ad4000f9`) стоила второго полного Validate —
22 минуты, из них 17–22 на шард мутантов. Причина одна: корпус отпечатка
(`source-fingerprint.mjs`) называет `demo/golden` строкой-каталогом, а
замыкание входов раскрывало каталог во все текстовые файлы под ним, включая
`baselines-index.json`. Индекс становился входом smoke, performance_smoke и
каждого гарда через `serve.mjs`: на реальной паре C→B ключи smoke/perf были
DIFFERENT, отпечатки 181 из 183 браузерных свидетелей менялись, журнал их не
пропускал.

- `check-inputs.mjs`: `BASELINE_OVERLAY` — раскрытие каталога не выдаёт
  overlay; явный корень golden и явная ссылка на файл — как были. На паре
  C→B: ключи smoke/perf/parity/backend same, golden DIFFERENT; отпечатки
  743 из 744 равны; план мутантов B с журналом C — 0–1 на шард вместо 38–44
- `ci-proof.mjs`: составное evidence — product tree без overlay, overlay
  (tree, sha256 индекса, run из `Baseline-Reviewed`), content-ключи всех
  реюзных job (исполненных тоже); `evaluateCiProof({expected, reviewedRun})`
  сверяет с локальным расчётом, fail-closed на ключ, tree, индекс, reviewed
  run, маркер с чужим ключом; proof без evidence при ожиданиях — stale
- `release-gate.mjs` / `release-prerelease.mjs`: ожидания считаются на
  checkout кандидата (`candidateExpectations`), чужой checkout — notice
- мутанты: `baseline-overlay-leaks-into-every-key`,
  `proof-trusts-evidence-it-could-verify`,
  `reused-marker-key-unchecked-against-candidate`,
  `product-tree-identity-counts-baselines`; перенацелен
  `ci-proof-ignores-run-attempt`
- docs: TESTING (правило overlay), DEVELOPMENT (evidence в release proof),
  STATUS

Issue: #573
User-Visible: no
This commit is contained in:
Claude
2026-09-17 22:00:15 +03:00
parent 06bf9b6921
commit 47f36e571c
14 changed files with 578 additions and 29 deletions
+24 -2
View File
@@ -35,6 +35,22 @@ export const BUILD_OUTPUT = [
'dist/**', 'custom_components/houseplan/frontend/**', 'demo/srv/assets/**',
];
/**
* Принятые эталоны и их индекс — overlay поверх продуктового дерева (#573).
*
* С ними сравнивает ровно одна проверка — `golden`, и у неё они стоят явным
* корнем. Никакая другая job их не читает: смок и перф-смок сверяют бандл с
* отпечатком исходников, а отпечаток (`source-fingerprint.mjs`) берёт из
* `demo/golden` только `*.mjs`. Но корпус там назван строкой-каталогом, и
* раскрытие каталога отдавало всем этим проверкам ещё и
* `baselines-index.json` — так приёмка 13 кадров на `v1.76.0-beta.3`
* (`ad4000f9`) сменила ключи smoke и performance_smoke и отпечатки 181 из 183
* свидетелей с браузерным гардом, и второй полный Validate повторил 22 минуты
* работы, которую первый уже сделал. Раскрытие каталога overlay не выдаёт;
* явный корень и явная ссылка на файл — как были.
*/
export const BASELINE_OVERLAY = ['demo/golden/baselines/**'];
/**
* Не входы Validate — с причиной. Каждая запись отвечает на вопрос «кто это
* исполняет и почему не Validate».
@@ -83,6 +99,7 @@ export const globToRegExp = (glob) => {
return new RegExp(`^${re}$`);
};
const matchesAny = (file, globs) => globs.some((glob) => globToRegExp(glob).test(file));
export const isBaselineOverlay = (file) => matchesAny(file, BASELINE_OVERLAY);
/** Отслеживаемые файлы (git), либо обход дерева там, где git недоступен. */
export function trackedFiles(root) {
@@ -253,8 +270,13 @@ export function closure(root, entries, { tracked = trackedFiles(root), stopAt =
for (const ref of data) {
if (trackedSet.has(ref)) { note(ref, file); seen.add(ref); continue; }
// каталог по строке — данные; бинарные файлы под ним код по строке не
// читает (эталоны golden входят в свою проверку явным корнем)
if (isDir(ref)) for (const f of tracked) if (f.startsWith(`${ref}/`) && !BINARY.test(f)) { note(f, file); seen.add(f); }
// читает, а overlay эталонов принадлежит только своей проверке (#573):
// и то и другое входит в golden явным корнем
if (isDir(ref)) {
for (const f of tracked) {
if (f.startsWith(`${ref}/`) && !BINARY.test(f) && !isBaselineOverlay(f)) { note(f, file); seen.add(f); }
}
}
}
}
return [...seen].sort();