mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
fix: the screenshot witness floor comes from the set, not from survivors
Тот же дефект, что #408 у golden, и в моём же коде из #401. Порог свидетелей считался от числа кадров, уцелевших на диске: rm docs/images/*.png плюс объявить все десять через --expect-change — уцелевших ноль, порог ноль, причины никто не спрашивает, а в манифесте остаётся {"witnesses":0,"floor":0} без единого слова о произошедшем. Щель была описана в комментарии над самой функцией и оставлена открытой. Порог теперь от набора сценариев, свидетели — из тех, с кем есть что сравнить. Разделение принципиальное: удаление кадров лишает доказательств, но не должно снижать планку. Первичная съёмка идёт через --no-witnesses --reason, как теперь и в golden. Отдельного параметра размера, как в golden, здесь не нужно, и это не небрежность: `ids` и есть набор — docs-accept.mjs передаёт DOC_SCREENSHOTS, а verifyDocsCandidate до того отказывает, если набор сцен в кандидате не совпал с ожидаемым. Пустой ids — отказ, а не ноль. Попутно Low из #405: повторная приёмка неизменённого набора затирала acceptance.declared пустым списком. След приёмки отвечает на вопрос «когда эти пиксели приняли и что тогда объявляли», а обновление отпечатка пикселей не меняет — значит и стирать ответ не должно. Прежний след сохраняется и помечается lastWriteWasFingerprintOnly. Заодно отказ по свидетелям теперь возвращает сами числа: вызывающий печатает свой вердикт, и сочинять их заново ему не из чего. Issue: #409 User-Visible: no
This commit is contained in:
+14
-6
@@ -139,14 +139,22 @@ function main(argv) {
|
||||
const file = byId.get(id);
|
||||
copyFileSync(file.from, file.to);
|
||||
}
|
||||
// След приёмки отвечает на вопрос «когда эти пиксели приняли и что тогда
|
||||
// объявляли». Обновление отпечатка пикселей не меняет — значит и стирать
|
||||
// ответ не должно (#409, Low из #405): прежняя редакция затирала `declared`
|
||||
// пустым списком, и история терялась при первом же refresh.
|
||||
const previous = JSON.parse(readFileSync(resolve(ROOT, 'docs/images/screenshots.json'), 'utf8'))
|
||||
.acceptance;
|
||||
const accepted = {
|
||||
...manifest,
|
||||
acceptance: {
|
||||
declared: [...decision.replace],
|
||||
witnesses: decision.witnesses.length,
|
||||
floor: decision.floor,
|
||||
...(skipWitnesses ? { witnessesSkippedBecause: skipReason } : {}),
|
||||
},
|
||||
acceptance: decision.replace.length
|
||||
? {
|
||||
declared: [...decision.replace],
|
||||
witnesses: decision.witnesses.length,
|
||||
floor: decision.floor,
|
||||
...(skipWitnesses ? { witnessesSkippedBecause: skipReason } : {}),
|
||||
}
|
||||
: { ...(previous || {}), lastWriteWasFingerprintOnly: true },
|
||||
};
|
||||
writeFileSync(
|
||||
resolve(ROOT, 'docs/images/screenshots.json'),
|
||||
|
||||
@@ -31,18 +31,30 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Порог свидетелей. Формула та же, что у golden (`goldenWitnessFloor`), и это
|
||||
* намеренно: два набора картинок в одном репозитории не должны требовать от
|
||||
* человека помнить два разных правила.
|
||||
* Порог свидетелей — от размера НАБОРА сценариев. Формула та же, что у golden
|
||||
* (`goldenWitnessFloor`), и это намеренно: два набора картинок в одном
|
||||
* репозитории не должны требовать от человека помнить два разных правила.
|
||||
*
|
||||
* Для десяти сцен порог равен одной. Этого достаточно, потому что основную
|
||||
* работу делает не порог, а требование «все необъявленные совпали»: расхождение
|
||||
* среды не бывает точечным. Порог закрывает единственную оставшуюся щель —
|
||||
* попытку объявить изменёнными все кадры разом, когда свидетелей не остаётся
|
||||
* вовсе и подтвердить среду становится нечем.
|
||||
*
|
||||
* Первая редакция (#401) считала порог от числа кадров, УЦЕЛЕВШИХ на диске, и
|
||||
* той самой щелью и обходилась: `rm docs/images/*.png` + объявить все десять
|
||||
* через `--expect-change` — уцелевших ноль, порог ноль, причины никто не
|
||||
* спрашивает, а в манифесте остаётся `{"witnesses":0,"floor":0}` без единого
|
||||
* слова о том, что произошло. Щель была описана в этом самом комментарии
|
||||
* строкой выше и оставлена открытой (#409, близнец golden-дефекта #408).
|
||||
*
|
||||
* Отсутствие закоммиченных кадров — не смягчающее обстоятельство. Свидетелем
|
||||
* такая сцена быть не может, это правда; но невозможность доказать среду не
|
||||
* отменяет требования, а требует сказать это вслух — `--no-witnesses
|
||||
* --reason="…"`, и причина уезжает в манифест.
|
||||
*/
|
||||
export const docsWitnessFloor = (committedCount) => (committedCount > 0
|
||||
? Math.min(10, Math.ceil(committedCount * 0.1))
|
||||
export const docsWitnessFloor = (sceneCount) => (sceneCount > 0
|
||||
? Math.min(10, Math.ceil(sceneCount * 0.1))
|
||||
: 0);
|
||||
|
||||
/** Объявленные имена, которых нет в наборе сценариев. */
|
||||
@@ -104,13 +116,28 @@ export function docsAcceptancePlan({
|
||||
+ ' принимать нельзя: растеризация задевает все кадры с текстом сразу' };
|
||||
}
|
||||
|
||||
// Порог — от набора сценариев, свидетели — из тех, с кем есть что сравнить
|
||||
// (#409). Разделение принципиальное: удаление кадров лишает доказательств, но
|
||||
// не должно снижать планку.
|
||||
//
|
||||
// `ids` здесь и есть набор: `docs-accept.mjs` передаёт DOC_SCREENSHOTS, а
|
||||
// `verifyDocsCandidate` до этого отказывает, если набор сцен в кандидате не
|
||||
// совпал с ожидаемым. Отдельный параметр размера (как в golden, где отчёт
|
||||
// бывает частичным) поэтому не нужен — но пустой `ids` означает, что считать
|
||||
// не от чего, и это отказ, а не ноль.
|
||||
if (!ids.length) {
|
||||
return { ...empty, refusal: 'набор сценариев пуст: порог свидетелей считать не от чего' };
|
||||
}
|
||||
const withCommitted = ids.filter((id) => committed[id]);
|
||||
const floor = docsWitnessFloor(withCommitted.length);
|
||||
const floor = docsWitnessFloor(ids.length);
|
||||
const witnesses = withCommitted.filter((id) => !announced.has(id)
|
||||
&& committed[id] === candidate[id]);
|
||||
if (!skipWitnesses && witnesses.length < floor) {
|
||||
return { ...empty, refusal: 'кадров-свидетелей недостаточно:'
|
||||
+ ` ${witnesses.length} из необходимых ${floor}.`
|
||||
// Числа возвращаются и при отказе: вызывающий печатает свой вердикт, и
|
||||
// сочинять их заново ему не из чего.
|
||||
return { ...empty, witnesses, floor, refusal: 'кадров-свидетелей недостаточно:'
|
||||
+ ` ${witnesses.length} из необходимых ${floor}`
|
||||
+ ` (сцен в наборе ${ids.length}, с закоммиченным кадром ${withCommitted.length}).`
|
||||
+ ' Свидетель — необъявленный кадр, совпавший с закоммиченным байт-в-байт;'
|
||||
+ ' только он доказывает, что среда съёмки та же. Если перерисовка'
|
||||
+ ' действительно тотальная и осознанная — --no-witnesses --reason="…"'
|
||||
|
||||
@@ -106,3 +106,62 @@ test('кадр без закоммиченной пары не может быт
|
||||
assert.deepEqual(plan.witnesses, ['alpha']);
|
||||
assert.equal(plan.floor, 1);
|
||||
});
|
||||
|
||||
// --- порог считается от набора, а не от уцелевших кадров (#409) -------------
|
||||
|
||||
test('удаление всех кадров не снижает порог (#409)', () => {
|
||||
// Воспроизведение обхода целиком: rm docs/images/*.png, объявить все десять.
|
||||
// Первая редакция (#401) считала порог от уцелевших — ноль уцелевших давал
|
||||
// ноль порога, и чужая съёмка проходила без единого слова о причине.
|
||||
const ids = Array.from({ length: 10 }, (_, index) => `scene-${index}`);
|
||||
const candidate = Object.fromEntries(ids.map((id) => [id, `foreign-${id}`]));
|
||||
const plan = docsAcceptancePlan({ ids, committed: {}, candidate, declared: ids });
|
||||
assert.equal(plan.floor, 1, 'порог от набора сценариев, а не от нуля уцелевших');
|
||||
assert.equal(plan.witnesses.length, 0);
|
||||
assert.match(plan.refusal, /0 из необходимых 1/);
|
||||
assert.match(plan.refusal, /сцен в наборе 10, с закоммиченным кадром 0/);
|
||||
|
||||
const named = docsAcceptancePlan({
|
||||
ids, committed: {}, candidate, declared: ids,
|
||||
skipWitnesses: true, skipReason: 'первичная съёмка набора',
|
||||
});
|
||||
assert.equal(named.refusal, null, 'законный путь — назвать причину');
|
||||
});
|
||||
|
||||
test('порог держится и при частичной потере кадров (#409)', () => {
|
||||
// Случай, где старая и новая формулы расходятся: 20 сцен в наборе, три кадра
|
||||
// на диске. Было бы 1, стало 2 — и три свидетеля этого уже не хватает.
|
||||
const ids = Array.from({ length: 20 }, (_, index) => `scene-${index}`);
|
||||
const committed = Object.fromEntries(ids.slice(0, 3).map((id) => [id, `same-${id}`]));
|
||||
const candidate = Object.fromEntries(ids.map((id) => [id,
|
||||
committed[id] || `new-${id}`]));
|
||||
const plan = docsAcceptancePlan({
|
||||
ids, committed, candidate, declared: ids.slice(3),
|
||||
});
|
||||
assert.equal(plan.floor, 2);
|
||||
assert.equal(plan.witnesses.length, 3, 'свидетелем может быть только кадр с парой');
|
||||
assert.equal(plan.refusal, null, 'три свидетеля при пороге два — достаточно');
|
||||
|
||||
const stricter = docsAcceptancePlan({
|
||||
ids, committed: { [ids[0]]: 'same-scene-0' },
|
||||
candidate: Object.fromEntries(ids.map((id) => [id,
|
||||
id === ids[0] ? 'same-scene-0' : `new-${id}`])),
|
||||
declared: ids.slice(1),
|
||||
});
|
||||
assert.match(stricter.refusal, /1 из необходимых 2/,
|
||||
'один уцелевший кадр планку не опускает');
|
||||
});
|
||||
|
||||
test('пустой набор сценариев — отказ, а не ноль (#409)', () => {
|
||||
const plan = docsAcceptancePlan({ ids: [], committed: {}, candidate: {} });
|
||||
assert.match(plan.refusal, /набор сценариев пуст/);
|
||||
});
|
||||
|
||||
test('порог совпадает с golden при равном размере набора (#409)', async () => {
|
||||
const { goldenWitnessFloor } = await import('../scripts/golden-acceptance.mjs');
|
||||
// Формулы обязаны совпадать: два набора картинок в одном репозитории не
|
||||
// должны требовать помнить два разных правила.
|
||||
for (const size of [0, 1, 10, 20, 143, 1000]) {
|
||||
assert.equal(docsWitnessFloor(size), goldenWitnessFloor(size), `размер ${size}`);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user