#!/usr/bin/env node /** * criterion-green (Машина 5 Пакет 5, Блок 3, 5.6) — по-критерийный производитель GREEN. * * Для этой цели заменяет «весь-прогон-разом» enforce-verify-record: эмитит подписанный * подписантом GREEN ТОЛЬКО при настоящем зелёном прогоне конкретного критерия. Два условия * настоящего green (оба обязательны): * - testPassed: тест изменённого шага реально прошёл; * - mutationKilled (P18): сломали изменённый код — тест ОБЯЗАН покраснеть; выжил зелёным → * тест ничего не проверяет → НЕ green. * Иначе green:false с причиной (mutation-survived / test-not-passed / no-signer-key) — fail-CLOSE. * * Подпись — над аутентифицирующей тройкой {criterion_id, code_fingerprint, occurrence} (floor-signer, * домен M5_GREEN). green/coverage_of_changed — поля записи; подлинность даёт подпись (Δ5), * свежесть — отпечаток (Δ2). Чистый модуль: факты прогона (testPassed/mutationKilled/отпечаток/ * покрытие) и ключ подписанта инъектируются; никакого fs/исполнения тестов здесь (это — живая * обёртка под активацией владельца). code_fingerprint считается отдельно через codeFingerprint. */ import { createHash } from 'node:crypto'; import { canonicalJson } from './receipt-sign.mjs'; import { signGreen } from './floor-signer.mjs'; /** * Δ2: отпечаток «по делу» = sha256 канонизированной карты {путь: содержимое} изменённых * файлов шага + тест-файлов, которые их прогоняли. Граф импортов НЕ считаем (кросс-язык — * дорого/хрупко; надёжность даёт мутация P18). Правка любого из этих файлов после прогона → * другой отпечаток → green аннулируется fingerprintFresh. Детерминирован (canonicalJson сортит * ключи — порядок файлов не влияет). */ export function codeFingerprint(fileContents = {}) { return createHash('sha256').update(canonicalJson(fileContents)).digest('hex'); } /** * Произвести по-критерийный GREEN. Настоящий green ⇔ testPassed && mutationKilled && есть ключ. * @returns настоящий green: {criterion_id, code_fingerprint, occurrence, sig, green:true, coverage_of_changed} * иначе: {criterion_id, green:false, code_fingerprint, coverage_of_changed, occurrence, reason} */ export function produceGreen({ criterion_id, occurrence, code_fingerprint, coverage_of_changed = null, testPassed = false, mutationKilled = false, signerKey, } = {}) { if (testPassed !== true) { return { criterion_id, green: false, code_fingerprint, coverage_of_changed, occurrence, reason: 'test-not-passed' }; } if (mutationKilled !== true) { // P18: тест прошёл, но мутация выжила — тест не проверяет изменённый код → не green. return { criterion_id, green: false, code_fingerprint, coverage_of_changed, occurrence, reason: 'mutation-survived' }; } const receipt = signGreen({ criterion_id, code_fingerprint, occurrence }, signerKey); if (!receipt) { // Нет ключа подписанта → подлинный green произвести нельзя (fail-CLOSE). return { criterion_id, green: false, code_fingerprint, coverage_of_changed, occurrence, reason: 'no-signer-key' }; } return { ...receipt, green: true, coverage_of_changed }; }