Files
houseplan-card/demo/golden/policy.mjs
T
Codex 3f95dac451 fix: индекс эталонов хранит платформу съёмки, а не платформу приёмки (#571)
Аудит 14.09, §4 и §9. `golden-report.json` не нёс платформу вовсе, поэтому
приёмщик вызывал `captureEnvironment()` у себя и записывал СВОЮ платформу как
платформу кадров. На `ad4000f9` это дало `"platform": "win32"` у кадров,
снятых Linux-прогоном 34853080375, а причина осознанного обхода осталась в
stdout и в индекс не попала — хотя `AGENTS.md` обещает след в обоих местах.
Сами PNG были и остаются целы: 101 свидетель совпал байт-в-байт. Врал
провенанс.

Что сделано:

- отчёт съёмки получил раздел `capture`: платформа, архитектура, сборка
  Chromium, отпечаток материала и — в CI — прогон с попыткой и SHA. Схема
  отчёта поднята до 2;
- приёмка читает среду съёмки из отчёта. Гейт чужой среды теперь судит обе
  стороны: съёмку (из отчёта) и приёмку (свою). Отказ — до единой записи;
- индекс эталонов поднят до схемы 2 и различает `capturedOn` и `acceptedOn`,
  несёт раздел `capture` и причину осознанного обхода в `foreignCapture`;
- отчёт схемы 1 платформы съёмки не несёт физически: это отдельная явная
  ветка, `capturedOn` уезжает `null`. Выдумывать платформу нельзя — ровно этим
  задача и вызвана;
- `--baselines=<dir>` у приёмки: без него проверить «отказ произошёл ДО
  записи» можно было бы только порчей рабочего дерева, то есть никак (#556).

Осознанно отменено решение #455 «не трогать run.mjs»: ради ГЕЙТА цена
фингерпринта не окупалась, ради ПРОВЕНАНСА окупилась — платформу кадров знает
только тот, кто их снял. Плата разовая: пересобран бандл, индекс скриншотов
документации переснят отдельным коммитом. Тест #455 переписан под новый
инвариант, а не удалён.

Свидетели: `test/golden-capture-provenance.test.mjs` — семь проверок, все пять
сценариев приёмки из issue, включая «отказ до записи» и «подмена PNG и неполный
артефакт по-прежнему fail-closed». Мутанты `golden-index-invents-capture-platform`
(возвращает платформу приёмщика) и `golden-report-provenance-optional`
(разрешает отчёт схемы 2 без провенанса) прогнаны лично: оба краснеют.

npm test 2762/2761/0 fail, typecheck чистый.

Issue: #571
User-Visible: no
2026-09-17 23:19:56 +03:00

68 lines
3.9 KiB
JavaScript

import {
captureEnvironment, foreignCaptureAllowance, foreignCaptureRefusal,
} from '../../scripts/capture-environment.mjs';
// The name must NOT end with `manifest.json`: the HACS submission check globs
// `*manifest.json` over the whole clone of the default branch and refuses a
// repository with more than one match (test/repo-hygiene.test.mjs).
export const GOLDEN_BASELINE_MANIFEST = 'baselines-index.json';
/**
* Версия индекса эталонов (#571).
*
* Схема 1 хранила одно поле `platform`, и заполнял его приёмщик своей
* платформой. Схема 2 различает `capturedOn` и `acceptedOn`, несёт раздел
* `capture` из отчёта съёмки и причину осознанного обхода. Читатели обязаны
* понимать обе: артефакты и индексы живут дольше схем.
*/
export const GOLDEN_INDEX_SCHEMA = 2;
/** Платформа кадров по индексу любой схемы: 2 — `capturedOn`, 1 — `platform`. */
export const indexCapturedOn = (index) => (index && typeof index === 'object'
? (index.capturedOn ?? (Number(index.schema) >= GOLDEN_INDEX_SCHEMA ? null : index.platform ?? null))
: null);
/**
* Проверка вызова — и заодно проверка среды съёмки (#455).
*
* Почему здесь, а не в `run.mjs`: `run.mjs` входит в корпус
* `sourceFingerprint`, и его правка объявляет устаревшими закоммиченный бандл,
* манифест скриншотов документации и индекс эталонов. Этот файл из корпуса
* исключён именно как «предикаты, вызываемые до и после съёмки», и уже
* вызывается первой строкой `run.mjs` — гейт встаёт без единой правки
* фингерпринтуемого файла.
*
* Платформа — ПАРАМЕТР, а не `process.platform` внутри: иначе юнит стал бы
* зелёным на Linux и красным на машине владельца, то есть тестом про хост, а
* не про правило.
*/
export const assertGoldenInvocation = (mode, scenarioFilter = '', options = {}) => {
if (!['capture', 'verify'].includes(mode)) throw new Error(`unknown golden mode: ${mode}`);
if (mode === 'verify' && scenarioFilter)
throw new Error('golden verify must run the complete matrix; use capture for a diagnostic --scenario run');
// Диагностический прогон verify в чужой среде законен: он ничего не принимает.
if (mode !== 'capture') return;
const platform = options.platform ?? captureEnvironment().platform;
const allowance = options.allowance ?? foreignCaptureAllowance();
const { refusal, allowance: accepted } = foreignCaptureRefusal({
platform, kind: 'golden', stage: 'capture', allowance,
});
if (refusal) throw new Error(refusal);
if (accepted) console.log(`Чужая среда съёмки разрешена осознанно: ${accepted}`);
};
/** A reviewed golden matrix is exact: neither an orphan PNG nor a stale hash
* entry may survive after a scenario is removed or renamed. */
export const goldenScenarioSetsMatch = (expected, indexed, baselineFiles) => {
const normalized = (values) => [...new Set(values)].sort();
const wanted = normalized(expected);
return JSON.stringify(normalized(indexed)) === JSON.stringify(wanted)
&& JSON.stringify(normalized(baselineFiles)) === JSON.stringify(wanted);
};
export const goldenRunFailed = (mode, manifestValid, results) => {
if (results.some((result) => result.status === 'error')) return true;
return mode === 'verify'
&& (!manifestValid || results.some((result) => result.status !== 'passed'));
};