Files
portal/tools/judge-slop-counter.mjs
T
Дмитрий 69e20099db fix(router-mentor): sharp-edges audit M1-M4 — close 8 misuse-resistance holes
Второй аудит машин 1-4 другим объективом (sharp-edges: устойчивость к
неправильному применению / мягкие умолчания / совпадение по пустоте-подстроке).
Криптоядра здоровы (подтверждено). 8 реальных дыр закрыты по TDD:

M3:
- coverage-machine F-1: покрытие считалось по двусторонней ПОДСТРОКЕ — produces
  "a" покрывал запрос "audit-rls-policy" (ложное «всё покрыто»). Новый tokensCover:
  точное равенство ИЛИ подмножество слов по границам. coveringSkill + coverageRegistry.
- router-engine F-8: confidence не проверялся на диапазон — 5/Infinity проходили как
  «уверен» (обход воздержания 5.2), -3 как принуд. abstain. validateTrace: [0,1] finite.
- round-control C: пустой roundKey="" активировал managed-режим (!= null) → все сессии
  делили один счётчик-бакет. Теперь managed требует непустую строку.
- router-learning-queue G: повторное approve уже-решённого id повторно клало запись в
  фонд (дубль). applyApprovalBatch: переводит только status==='pending'.

M2:
- plan-lock F5: шаг с пустым object был джокером (object:'' матчил действие, чей путь
  не извлёкся → object''). actionMatchesStep: пустой object шага не матчит ничего.

M4 (инертна; чистые fail-closed правки кода, корректны и при включении):
- judge-slop-counter H: битый/null вердикт в списке ронял счёт (v.missing на null).
  Теперь не крашит, считается халтурой (безопасная сторона).
- judge-engine J: consensusDecision на пустом/битом списке дрейфовал к GO. Теперь GO
  только если есть голоса И каждый чистый GO; иначе NO-GO (fail-closed для hard-risk).
- judge-orchestrator K: finalGate снимал вето пола на любой falsy floorBlocked
  (undefined от упавшей проверки = fail-open). Теперь снять может только явный false.

Регрессия tools-only 2555 passed + 2 skip (+15 TDD-тестов, 0 регрессий).

Осознанно НЕ менялось (без призраков):
- M1 receipt-sign domain default '' / разделитель пробел — backward-compat контракт
  (тест 18-19), инъективен на enum-доменах без пробелов.
- M1 action-journal атомарность записи головы + битая .jsonl строка — fail-closed
  (битьё → verifyChain ok:false → стена блокирует); чистого behavioral-теста нет.
- M3 round-control requiredSkills=[] — контракт вызывающего (пустой = не требуется).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 06:24:21 +03:00

27 lines
1.7 KiB
JavaScript

#!/usr/bin/env node
/**
* judge-slop-counter (§9.5) — частота халтуры судьи: голые ГО / тонкие вердикты /
* пропуски линзы. Чистая функция над списком вердиктов (каждый уже несёт missing[]/
* trivial[] из validateVerdictSlots). Живой источник (лог J8) вшивается в 4-E/4-F;
* здесь — детерминированный счёт + флаг по порогу. Ловит систематическую штамповку
* (не качество одного вердикта — это оценщик постфактум 4-F/D31).
*/
const DEFAULT_THRESHOLD = 0.3;
export function computeSlopStats(verdicts = [], { threshold = DEFAULT_THRESHOLD } = {}) {
const total = verdicts.length;
let lensSkips = 0, thinVerdicts = 0, dirty = 0, clean = 0;
for (const v of verdicts) {
// H (аудит M1-M4): битый/отсутствующий вердикт не роняет счёт и считается халтурой
// (безопасная сторона: больше флага, не меньше). Раньше v.missing на null → краш.
if (!v || typeof v !== 'object') { dirty++; continue; }
const hasMissing = Array.isArray(v.missing) && v.missing.length > 0;
const hasTrivial = Array.isArray(v.trivial) && v.trivial.length > 0;
if (hasMissing) lensSkips++;
if (hasTrivial) thinVerdicts++;
if (hasMissing || hasTrivial) dirty++; else clean++;
}
const slopRate = total ? dirty / total : 0;
return { total, lensSkips, thinVerdicts, clean, slopRate, flag: total > 0 && slopRate > threshold };
}