From ea0dee23e24af9da7a331abff2cd79591be6831f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sun, 7 Jun 2026 12:45:12 +0300 Subject: [PATCH] =?UTF-8?q?feat(m5):=20fingerprintFresh=20=E2=80=94=20?= =?UTF-8?q?=D1=81=D0=B2=D0=B5=D0=B6=D0=B5=D1=81=D1=82=D1=8C=20=D0=BE=D1=82?= =?UTF-8?q?=D0=BF=D0=B5=D1=87=D0=B0=D1=82=D0=BA=D0=B0=20green=20(=D0=9F?= =?UTF-8?q?=D0=B0=D0=BA=D0=B5=D1=82=205,=205.3,=20=CE=942)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Δ2: зелёный прогон засчитывается только если его code_fingerprint совпадает с текущим (изменённые файлы шага + тесты; currentFingerprints инъектируется живым гейтом). Правка файла после прогона → расхождение → green аннулирован (stale). Чистая функция, fail-CLOSE (нет текущего отпечатка ≠ записанному → stale). Красные прогоны не проверяются (их «не-зелёность» ловит criteriaGreenMatched). По авторитетному Δ6 — ОТДЕЛЬНЫЙ шаг лесенки критерий-гейта (не внутрь criteriaGreenMatched, которая остаётся «green-присутствием», Δ6 шаг 2). +5 тестов. judge-gate-floor 32/32 (чисто аддитивно). Co-Authored-By: Claude Opus 4.8 (1M context) --- tools/judge-gate-floor.mjs | 19 ++++++++++++++- tools/judge-gate-floor.test.mjs | 43 +++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) 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); + }); +});