#!/usr/bin/env node /** * judge-evaluator (D31, §9.4) — оценщик ПОСТФАКТУМ. Отдельный (не сам судья): * сверяет прошлые вердикты судьи И роутера с тем, что вылезло НИЖЕ (на Гейте-3 / у * владельца). Промах = проблема, которую пропустил GO-вердикт. Систематическую халтуру * ловит по СЛЕДУ (частота промахов), а не по одному вердикту. Чистая функция над данными; * источник (лог вердиктов J8 + всплывшие проблемы) подаётся снаружи (4-G/живой прогон). */ const DEFAULT_THRESHOLD = 0.2; function rate(misses, totalGo) { return totalGo > 0 ? misses / totalGo : 0; } /** * @param {object} a * @param {Array<{verdict_id,decision,source?}>} a.verdicts - прошлые вердикты (GO/NO-GO) * @param {Array<{missed_by,where?}>} a.surfaced - проблемы, вылезшие ниже (missed_by → id вердикта) * @param {number} [a.threshold] */ export function evaluatePostfactum({ verdicts = [], surfaced = [], threshold = DEFAULT_THRESHOLD } = {}) { const goById = new Map(); for (const v of verdicts) { if (v && v.decision === 'GO') goById.set(v.verdict_id, v.source || 'unknown'); } // Промах = всплывшая проблема, пропущенная GO-вердиктом (NO-GO-вердикт её «поймал» — не промах). const missList = (surfaced || []).filter((s) => s && goById.has(s.missed_by)); const totalGo = goById.size; const misses = missList.length; const bySource = {}; const sources = new Set([...goById.values()]); for (const src of sources) { const srcGo = [...goById.values()].filter((x) => x === src).length; const srcMiss = missList.filter((s) => goById.get(s.missed_by) === src).length; bySource[src] = { totalGo: srcGo, misses: srcMiss, missRate: rate(srcMiss, srcGo) }; } const missRate = rate(misses, totalGo); return { totalGo, misses, missRate, flag: totalGo > 0 && missRate > threshold, bySource }; }