diff --git a/tools/judge-gate-floor.mjs b/tools/judge-gate-floor.mjs index 42107091..19f0a13d 100644 --- a/tools/judge-gate-floor.mjs +++ b/tools/judge-gate-floor.mjs @@ -69,13 +69,30 @@ export function k5CriterionCheck({ planSteps = [] }) { return { ok: missingCriterion.length === 0, missingCriterion }; } -/** Гейт-3 пол: каждый объявленный критерий сопоставлен с настоящим ЗЕЛЁНЫМ прогоном. */ +/** Гейт-3 пол: каждый объявленный критерий сопоставлен с настоящим ЗЕЛЁНЫМ прогоном. + * Это «green-присутствие» (Δ6 шаг 2): свежесть отпечатка и подпись — отдельные шаги. */ export function criteriaGreenMatched({ criteria = [], greenRuns = [] }) { const greenIds = new Set((greenRuns || []).filter((r) => r && r.green === true).map((r) => r.criterion_id)); const unproven = (criteria || []).filter((c) => !greenIds.has(c.id)).map((c) => c.id); return { ok: unproven.length === 0, unproven }; } +/** + * 5.3 (Δ2, Δ6 шаг 3): свежесть отпечатка. Зелёный прогон засчитывается только если его + * code_fingerprint совпадает с ТЕКУЩИМ (изменённые файлы шага + тест-файлы; считается живым + * гейтом, инъектируется как currentFingerprints[criterion_id]). Правка файла после прогона + * меняет текущий отпечаток → расхождение → green аннулирован (stale). Чистая функция. + * Конструктивно fail-CLOSE: нет текущего отпечатка (undefined) ≠ записанному → stale. + * Красные прогоны не проверяются (их «не-зелёность» ловит criteriaGreenMatched). + */ +export function fingerprintFresh({ greenRuns = [], currentFingerprints = {} }) { + const stale = (greenRuns || []) + .filter((r) => r && r.green === true) + .filter((r) => r.code_fingerprint !== currentFingerprints[r.criterion_id]) + .map((r) => r.criterion_id); + return { ok: stale.length === 0, stale }; +} + /** A2 K2: требуемые навыки взяты по фактическому журналу вызовов (не по тексту). */ export function skillTakenByJournal({ requiredSkills = [], journalSkillCalls = [] }) { const missing = skillsCoveredByJournal(requiredSkills, journalSkillCalls); diff --git a/tools/judge-gate-floor.test.mjs b/tools/judge-gate-floor.test.mjs index 7ffafef7..3b12c407 100644 --- a/tools/judge-gate-floor.test.mjs +++ b/tools/judge-gate-floor.test.mjs @@ -164,3 +164,46 @@ describe('criteriaFromSealedPlan (#5, F3/F9 — критерии из печат expect(r.unsealed).toEqual(['c9']); }); }); + +// 5.3 (Δ2): свежесть отпечатка в judge-gate-floor — green засчитывается только при совпадении +// code_fingerprint зелёного прогона с ТЕКУЩИМ (изменённые файлы шага + тесты). Правка файла +// после прогона меняет текущий отпечаток → green аннулируется. Отдельный шаг лесенки (Δ6). +import { fingerprintFresh } from './judge-gate-floor.mjs'; + +describe('fingerprintFresh (5.3, Δ2): green свеж только при совпадении code_fingerprint', () => { + it('отпечаток зелёного совпадает с текущим → свеж (ok)', () => { + const r = fingerprintFresh({ + greenRuns: [{ criterion_id: 'c1', green: true, code_fingerprint: 'aa' }], + currentFingerprints: { c1: 'aa' }, + }); + expect(r.ok).toBe(true); + expect(r.stale).toEqual([]); + }); + it('файл изменён после прогона (отпечаток разошёлся) → stale, не ok', () => { + const r = fingerprintFresh({ + greenRuns: [{ criterion_id: 'c1', green: true, code_fingerprint: 'aa' }], + currentFingerprints: { c1: 'bb' }, + }); + expect(r.ok).toBe(false); + expect(r.stale).toEqual(['c1']); + }); + it('нет текущего отпечатка для критерия → stale (fail-CLOSE)', () => { + const r = fingerprintFresh({ + greenRuns: [{ criterion_id: 'c1', green: true, code_fingerprint: 'aa' }], + currentFingerprints: {}, + }); + expect(r.ok).toBe(false); + expect(r.stale).toEqual(['c1']); + }); + it('красный прогон (green:false) не проверяется на свежесть (его забота — criteriaGreenMatched)', () => { + const r = fingerprintFresh({ + greenRuns: [{ criterion_id: 'c1', green: false, code_fingerprint: 'aa' }], + currentFingerprints: { c1: 'bb' }, + }); + expect(r.ok).toBe(true); + expect(r.stale).toEqual([]); + }); + it('нет зелёных прогонов → ok (присутствие зелёного — отдельный шаг)', () => { + expect(fingerprintFresh({ greenRuns: [], currentFingerprints: {} }).ok).toBe(true); + }); +});