9d8d3de782
Разбор «перемежающегося degraded судьи» по systematic-debugging: действующего бага нет (ключ SET, 28/28 вердиктов чистые, degraded-строки несверяемы — at:null, без парного WARN). Гипотеза «retry/таймаут» не подтверждена → таймаут не трогали. Вместо этого закрыта слепота диагностики (TDD, под maintenance): - callJudgeModel различает no_key vs transport_error+errorType (classifyLLMError); - причина протекает в вердикт → warnJudgeUnavailable (+cause/error_type/at) и seal-запись; - main() передаёт nowMs: Date.now() → seal/verdict/warn больше не at:null (логи сверяемы). Файлы: tools/seal-log.mjs, tools/enforce-judge-gate.mjs. +9 тестов; 2 exact-match приведены к новому контракту. Регрессия tools-only 3829 GREEN (база 3820), 0 регрессий. cspell-words.txt +8 терминов. Роадмап: секция «Печать M7» + degraded-наблюдаемость. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55 lines
3.1 KiB
JavaScript
55 lines
3.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* seal-log (M7 наблюдаемость печати) — закрывает дыру «провал печати нигде не логируется».
|
|
* Каждая запись плана/спеки порождает строку в ~/.claude/runtime/seal-attempts.jsonl:
|
|
* был ли судья активен, был ли вердикт wired, пыталась ли печать, встала ли, и ПОЧЕМУ нет.
|
|
* Чистый buildSealEntry (детерминирован, тестируется) + best-effort logSealAttempt (I/O).
|
|
*/
|
|
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { runtimeDir } from './enforce-hook-helpers.mjs';
|
|
|
|
const SEAL_LOG = 'seal-attempts.jsonl';
|
|
|
|
/**
|
|
* Чистая сборка записи попытки печати. Печать «пытается» ТОЛЬКО на wired GO; иначе
|
|
* фиксируем причину пропуска (судья не активен / вердикт не wired / NO-GO / провал печати).
|
|
*/
|
|
export function buildSealEntry({ functionName, judgeActive, wired, decision, sealResult, cause = null, errorType = null, nowMs = null } = {}) {
|
|
const active = judgeActive !== false; // undefined → считаем активным (по умолчанию)
|
|
const isWired = !!wired;
|
|
const attempted = isWired && decision === 'GO'; // печать положена только на wired GO
|
|
const sealed = !!(sealResult && sealResult.sealed === true);
|
|
|
|
let reason = null;
|
|
if (!active) reason = 'судья не активен (no-op, $0)';
|
|
// M7 (2026-06-13): degraded больше не безымянный — причина (no_key / transport_error:<тип>)
|
|
// протекает в reason, чтобы diagnose «почему судья недоступен» без других логов.
|
|
else if (!isWired) reason = `вердикт не wired (судья недоступен/degraded${cause ? `: ${cause}${errorType ? `/${errorType}` : ''}` : ''}) — печать не пыталась`;
|
|
else if (decision === 'NO-GO') reason = 'судья NO-GO — печать не положена';
|
|
else if (sealResult && sealResult.sealed !== true) reason = sealResult.reason ?? 'печать не встала (причина не указана)';
|
|
|
|
return {
|
|
kind: 'seal_attempt',
|
|
functionName: functionName ?? null,
|
|
judge_active: active,
|
|
wired: isWired,
|
|
decision: decision ?? null,
|
|
seal_attempted: attempted,
|
|
sealed,
|
|
kind_sealed: sealResult ? (sealResult.kind ?? null) : null,
|
|
cause: cause ?? null,
|
|
error_type: errorType ?? null,
|
|
reason,
|
|
at: nowMs,
|
|
};
|
|
}
|
|
|
|
/** Best-effort append в ~/.claude/runtime/seal-attempts.jsonl (Node fs, не Write-tool). */
|
|
export function logSealAttempt(entry, { fsImpl = { appendFileSync, mkdirSync }, dir = runtimeDir() } = {}) {
|
|
try {
|
|
fsImpl.mkdirSync(dir, { recursive: true });
|
|
fsImpl.appendFileSync(join(dir, SEAL_LOG), JSON.stringify(entry) + '\n');
|
|
} catch { /* наблюдаемость best-effort — никогда не ломает хук */ }
|
|
}
|