From d669a6bcb50542e1146e6101949f5a9b975cd66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 18 Jun 2026 21:29:04 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B2=D0=BE=D0=B7=D1=80=D0=B0=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D1=83=D0=B4=D1=8C=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=BE=D1=85=D0=BE=D0=B4=D1=8F=D1=82=20=D0=B4=D0=BE=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BA=D0=B0=D0=B7=D0=B0=20=D0=B2=D0=B5=D1=80=D0=B4?= =?UTF-8?q?=D0=B8=D0=BA=D1=82=D0=B0=20(visibility-gap)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Контроллер видел голое «NO-GO [judge]» без претензий: показ вердикта берёт поле reason, а pushVerdict писал reason = verdict.reason || recommendation — у судьи recommendation пуст (суть в objections[]), и возражения терялись. Хотя они есть в системе (карточка арбитража / память кругов через formatJudgeObjection) — просто не в показ. Новая judgeSurfaceReason(verdict): reason/ recommendation, иначе formatJudgeObjection(verdict.verdict) — дословные возражения. runJudgeTurn использует её для pushVerdict + writeStage. Поймано вживую: судья дал delivery=internal[heavy] + позиция-без-якорей[light], а контроллеру пришло пусто. Свод 4374 зелёный. Co-Authored-By: Claude Opus 4.8 --- tools/enforce-judge-gate.mjs | 14 +++++++++++++- tools/judge-surface-reason.test.mjs | 26 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 tools/judge-surface-reason.test.mjs diff --git a/tools/enforce-judge-gate.mjs b/tools/enforce-judge-gate.mjs index c573ce1..6875630 100644 --- a/tools/enforce-judge-gate.mjs +++ b/tools/enforce-judge-gate.mjs @@ -95,6 +95,18 @@ export function decide({ mode, verdict, floorBlocked = false } = {}) { return { block: true, message }; } +/** + * Причина судьи для ПОКАЗА вердикта (SP1 visibility-fix): reason/recommendation, а при их + * отсутствии (типично для NO-GO судьи — суть в objections, а не в recommendation) — дословные + * возражения судьи (formatJudgeObjection). Раньше показ брал только reason||recommendation → на + * NO-GO выходило пусто, и контроллер видел голое «NO-GO» без претензий. Тотально (try) → ''. + */ +export function judgeSurfaceReason(verdict) { + const base = (verdict && (verdict.reason || (verdict.verdict && verdict.verdict.recommendation))) || ''; + if (base) return base; + try { return formatJudgeObjection(verdict && verdict.verdict) || ''; } catch { return ''; } +} + /** * Шов судьи (async, §8 + Δ-C): рубильник → детект плана (Write-only) → префетч живого вердикта. * 1) не активен (нет флага/HMAC-ключа судьи) → нейтральный GO, wired:false, $0. @@ -332,7 +344,7 @@ export async function runJudgeTurn(event, { mode, logImpl = logVerdictLine, warn // SP1: громкая видимость вердикта судьи (best-effort, fail-quiet). if (judged) { const sessJ = (event && event.session_id) || 'unknown'; - const judgeReason = (verdict && (verdict.reason || (verdict.verdict && verdict.verdict.recommendation))) || ''; + const judgeReason = judgeSurfaceReason(verdict); try { pushVerdict(sessJ, { outcome: classifyJudgeOutcome(verdict), diff --git a/tools/judge-surface-reason.test.mjs b/tools/judge-surface-reason.test.mjs new file mode 100644 index 0000000..19af3a7 --- /dev/null +++ b/tools/judge-surface-reason.test.mjs @@ -0,0 +1,26 @@ +import { describe, it, expect } from 'vitest'; +import { judgeSurfaceReason } from './enforce-judge-gate.mjs'; + +describe('judgeSurfaceReason — возражения судьи доходят до показа вердикта', () => { + it('NO-GO без reason/recommendation → дословные возражения (formatJudgeObjection)', () => { + const verdict = { decision: 'NO-GO', verdict: { objections: [ + { anchor: { ref: 'delivery=internal при пользовательском результате' }, severity: 'heavy' }, + { anchor: { ref: 'позиция вставки без строк-якорей' }, severity: 'light' }, + ] } }; + const r = judgeSurfaceReason(verdict); + expect(r).toContain('delivery=internal'); + expect(r).toContain('[heavy]'); + expect(r).toContain('позиция вставки'); + }); + it('явный reason имеет приоритет', () => { + expect(judgeSurfaceReason({ reason: 'прямая причина', verdict: { objections: [{ anchor: { ref: 'x' }, severity: 'light' }] } })) + .toBe('прямая причина'); + }); + it('recommendation (если есть) используется как причина', () => { + expect(judgeSurfaceReason({ verdict: { recommendation: 'делай Y' } })).toBe('делай Y'); + }); + it('нет ни reason, ни возражений → пустая строка (без падения)', () => { + expect(judgeSurfaceReason({ verdict: {} })).toBe(''); + expect(judgeSurfaceReason(null)).toBe(''); + }); +});