fix: возражения судьи доходят до показа вердикта (visibility-gap)
Контроллер видел голое «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 <noreply@anthropic.com>
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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('');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user