feat(m5): fingerprintFresh — свежесть отпечатка green (Пакет 5, 5.3, Δ2)

Δ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) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-07 12:45:12 +03:00
parent cab7ffc3cf
commit ea0dee23e2
2 changed files with 61 additions and 1 deletions
+18 -1
View File
@@ -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);
+43
View File
@@ -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);
});
});