mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
refactor: split mutation gate modules (#558)
Issue: #558 User-Visible: no
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
// Fingerprints and the reusable caught-witness ledger.
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { MUTATION_PROOF } from './mutation-guard-outcome.mjs';
|
||||
import { anchorRegion, guardInputs } from './mutation-selection.mjs';
|
||||
import { withoutProductVersion } from './source-fingerprint.mjs';
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
|
||||
/**
|
||||
* Отпечаток свидетеля (#481): содержимое файлов патча и гарда плюс само
|
||||
* объявление мутанта. Строка версии продукта нормализуется, как в
|
||||
* `visualFingerprint` (#245): релизный бамп трогает `houseplan-card.ts` и
|
||||
* `houseplan-editor-runtime.ts`, по которым отбираются десятки мутантов, но
|
||||
* ни одного свидетеля не меняет. Приближение то же, что у отбора по диффу:
|
||||
* непрямые зависимости гарда не учитываются — полный прогон остаётся
|
||||
* контрактом, журнал его не заменяет.
|
||||
*/
|
||||
export function witnessFingerprint(mutant, {
|
||||
root = repoRoot,
|
||||
read = (file) => (existsSync(join(root, file)) ? readFileSync(join(root, file), 'utf8') : ''),
|
||||
exists = (file) => existsSync(join(root, file)),
|
||||
normalize = withoutProductVersion(root),
|
||||
inputsOf = (guard) => guardInputs(guard, { exists, read }),
|
||||
} = {}) {
|
||||
const hash = createHash('sha256');
|
||||
hash.update(JSON.stringify({
|
||||
id: mutant.id,
|
||||
guard: mutant.guard,
|
||||
oracle: mutant.oracle || MUTATION_PROOF.ASSERTION,
|
||||
patches: mutant.patches,
|
||||
}));
|
||||
hash.update('\0');
|
||||
const text = (file) => String(read(file)).replace(/\r\n?/g, '\n');
|
||||
// Сторона патча — только область якоря (#518); сторона гарда — файл целиком:
|
||||
// у гарда якоря нет, он судит поведение и меняется весь.
|
||||
const entries = [
|
||||
...mutant.patches.map((patch, index) => [
|
||||
`${patch.file}#якорь-${index}`,
|
||||
() => anchorRegion(text(patch.file), patch.find),
|
||||
]),
|
||||
...inputsOf(mutant.guard).map((file) => [file, () => text(file)]),
|
||||
].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
||||
for (const [key, valueOf] of entries) {
|
||||
hash.update(key);
|
||||
hash.update('\0');
|
||||
hash.update(normalize(valueOf()));
|
||||
hash.update('\0');
|
||||
}
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
export const LEDGER_SCHEMA = 2;
|
||||
|
||||
const emptyLedger = () => ({ schema: LEDGER_SCHEMA, caught: {} });
|
||||
const validProof = (proof) => Object.values(MUTATION_PROOF).includes(proof);
|
||||
|
||||
/**
|
||||
* Журнал доказанных свидетелей.
|
||||
*
|
||||
* Schema 2 намеренно не принимает старые строки fingerprint: до #550 они не
|
||||
* доказывали, что упал именно oracle, а не tsc/setup перед ним.
|
||||
*/
|
||||
export function readLedger(file) {
|
||||
if (!file || !existsSync(file)) return emptyLedger();
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
||||
if (parsed?.schema !== LEDGER_SCHEMA || typeof parsed.caught !== 'object' || !parsed.caught) {
|
||||
return emptyLedger();
|
||||
}
|
||||
const caught = {};
|
||||
for (const [id, value] of Object.entries(parsed.caught)) {
|
||||
if (typeof value?.fingerprint === 'string' && validProof(value.proof)) caught[id] = { ...value };
|
||||
}
|
||||
return { schema: LEDGER_SCHEMA, caught };
|
||||
} catch {
|
||||
// Битый журнал — не отказ гейта: он лишь сужает работу, и пустой журнал
|
||||
// означает «гонять всё отобранное», то есть прежнее поведение.
|
||||
return emptyLedger();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Записать пойманного свидетеля — сразу, а не в конце прогона: отменённый
|
||||
* или упавший по таймауту шард обязан сохранить уже сделанное, иначе
|
||||
* следующий пуш начинает с нуля (снежный ком #481).
|
||||
*/
|
||||
export function recordCaught(file, ledger, mutant, fingerprint, proof = MUTATION_PROOF.ASSERTION) {
|
||||
if (!validProof(proof)) throw new Error(`нельзя записать недоказанный outcome в ledger: ${proof}`);
|
||||
ledger.caught[mutant.id] = { fingerprint, proof };
|
||||
mkdirSync(dirname(file), { recursive: true });
|
||||
writeFileSync(file, `${JSON.stringify({ schema: LEDGER_SCHEMA, caught: ledger.caught }, null, 2)}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Мутанты, чей отпечаток совпадает с журналом, уже доказали, что ловятся на
|
||||
* этих же входах — их пропуск ничего не ослабляет. Сравнивается ОТПЕЧАТОК,
|
||||
* не факт присутствия id: запись от другого содержимого файлов не считается.
|
||||
*/
|
||||
export function splitByLedger(mutants, ledger, fingerprintOf = (m) => witnessFingerprint(m)) {
|
||||
const run = [];
|
||||
const skipped = [];
|
||||
for (const mutant of mutants) {
|
||||
const fingerprint = fingerprintOf(mutant);
|
||||
const proof = ledger.caught[mutant.id];
|
||||
const expectedProof = mutant.oracle || MUTATION_PROOF.ASSERTION;
|
||||
if (proof?.fingerprint === fingerprint && proof.proof === expectedProof
|
||||
&& validProof(proof.proof)) skipped.push(mutant);
|
||||
else run.push({ mutant, fingerprint });
|
||||
}
|
||||
return { run, skipped };
|
||||
}
|
||||
Reference in New Issue
Block a user