From e311efeeec7fc2edb79fa5d5a93314301593e229 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: Tue, 9 Jun 2026 08:30:43 +0300 Subject: [PATCH] docs(router-mentor): A1 pre-code chain amendments (Write-only, judge-unavailable degraded-allow) --- ...-09-router-mentor-A1-judge-gate2-wiring.md | 112 ++++++++++++++++++ ...ter-mentor-A1-judge-gate2-wiring-design.md | 32 +++++ 2 files changed, 144 insertions(+) diff --git a/docs/superpowers/plans/2026-06-09-router-mentor-A1-judge-gate2-wiring.md b/docs/superpowers/plans/2026-06-09-router-mentor-A1-judge-gate2-wiring.md index c3dc650e..2afb402f 100644 --- a/docs/superpowers/plans/2026-06-09-router-mentor-A1-judge-gate2-wiring.md +++ b/docs/superpowers/plans/2026-06-09-router-mentor-A1-judge-gate2-wiring.md @@ -630,3 +630,115 @@ Expected: GREEN, 0 failed. Прежний baseline ~3292 passed / 2 skipped — **Placeholder scan:** `` = `c:\моя\проекты\портал crm\Документация\.claude\worktrees\brainrepo` (литеральный путь в git-командах; PowerShell, `git -C ""`). Прочих плейсхолдеров нет — весь код приведён. **Type consistency:** `runJudgeGate(event, deps)` возвращает `{decision, wired, verdict?}`; `runJudgeTurn` читает `verdict.wired` и зовёт `buildVerdictEntry(verdict, nowMs)`; `buildVerdictEntry` читает `judgeResult.verdict` (есть при wired:true). `requiredLensesFor('gate2')` = 5 линз — тесты подают все 5 слотов. `decide({mode, verdict, floorBlocked})` — контракт существующих тестов сохранён (Task 6 их не ломает). Имена функций едины во всех тасках. + +--- + +## Поправки после pre-code adversarial-цепочки (применять ПОВЕРХ Task 2/3/4/5/6) + +Спека §9 (audit-context → sharp-edges → variant-analysis vs живой v4-судья). Дельты: + +### Δ-A (Task 2 — детектор только на `Write`) + +`PLAN_TOOLS` сужается до `Write` (Гейт-2 судит полный план; Edit/MultiEdit несут только фрагмент): + +```js +const PLAN_TOOLS = new Set(['Write']); // SE-FIX-2: только полный content; Edit/MultiEdit — фрагмент, не судим +``` + +`product = String(input.content ?? '').trim();` (без `new_string`). +**Тесты Task 2 правятся:** «Edit плана → product=new_string» → **«Edit плана → shouldJudge:false»**; +добавить «MultiEdit плана → shouldJudge:false». Кейсы Write/путь/слэши/не-план — без изменений. + +### Δ-B (Task 3 — `callJudgeModel`: unavailable ≠ malformed) + +`callJudgeModel` различает «не смог запуститься» (degraded) и «запустился, но мусор» (NO-GO): + +```js +export async function callJudgeModel({ functionName, requiredLenses, promptArgs, apiKey, model = CLASSIFIER_MODEL, transport = callAnthropicAPI }) { + if (!apiKey) return { unavailable: true }; // нет транспорт-ключа → судья недоступен ($0) + const base = buildJudgePrompt({ functionName, requiredLenses, ...promptArgs }); + const prompt = { system: base.system + '\n' + JSON_DIRECTIVE, user: base.user }; + try { + const text = await transport(prompt, { apiKey, model }); + return parseJudgeResponse(text); // ran: валидный → {slots,...}; мусор → {} (NO-GO) + } catch { + return { unavailable: true }; // транспорт бросил → недоступен (не NO-GO) + } +} +``` + +**Тест Task 3 правится:** «нет apiKey → транспорт не зовётся, `{}`» → **«нет apiKey → `{unavailable:true}`, +транспорт не зовётся»**; «транспорт бросил → `{}`» → **«транспорт бросил → `{unavailable:true}`»**. +Кейс «валидный JSON → распарсен» — без изменений. + +### Δ-C (Task 4 — `runJudgeGate`: unavailable → degraded ALLOW, не NO-GO) + +После `callJudgeModel`, перед `runJudge`: + +```js + const raw = await callJudgeModel({ ... }); + if (raw && raw.unavailable) { + return { decision: 'GO', wired: false, unavailable: true }; // VA-FIX-1: судья недоступен → degraded allow (+WARN в main) + } + const verdict = runJudge({ ... llmCall: () => raw, ... }); + return { decision: verdict.decision, wired: true, verdict: { ...verdict, functionName: 'gate2' } }; +``` + +**Тест Task 4 правится:** «нет ROUTER_LLM_KEY → вердикт NO-GO» → **«нет ROUTER_LLM_KEY → `wired:false`, +`unavailable:true`, `decision:'GO'`, транспорт не зовётся»**. Кейсы «не активен», «не план», «чистый +вердикт → GO», «якорное NO → NO-GO» — без изменений. + +### Δ-D (Task 5/6 — лог только реального вердикта + main гибрид) + +`buildVerdictEntry`/`logVerdictLine` зовутся **только при реальном вердикте** (`wired === true`). +При `unavailable` (degraded-allow) — НЕ verdict-запись, а отдельная WARN-строка (best-effort): + +```js +export function warnJudgeUnavailable(event, { fsImpl = fsDefault, dir = runtimeDir() } = {}) { + try { + fsImpl.mkdirSync(dir, { recursive: true }); + fsImpl.appendFileSync(join(dir, 'judge-verdicts.jsonl'), + JSON.stringify({ kind: 'judge_unavailable', at: null, note: 'нет ROUTER_LLM_KEY или транспорт недоступен' }) + '\n'); + } catch { /* best-effort */ } +} +``` + +`runJudgeTurn` (Task 6) — лог-ветвление + main через `exitDisciplineDecision` (async-aware, fail-CLOSE +только на throw/malformed; MUST-VERIFY-1 verified — `disciplineOutcome` await'ит санк): + +```js +export async function runJudgeTurn(event, { mode, logImpl = logVerdictLine, warnImpl = warnJudgeUnavailable, nowMs, ...deps } = {}) { + if (mode === 'inert') return { block: false }; + let verdict; + try { verdict = await runJudgeGate(event, deps); } + catch { return { block: mode === 'live-block' }; } // истинный баг → live-block fail-CLOSE; shadow allow + if (verdict && verdict.wired) { // реальный вердикт + try { logImpl(buildVerdictEntry(verdict, nowMs)); } catch { /* best-effort */ } + } else if (verdict && verdict.unavailable) { // судья недоступен → WARN, не verdict + try { warnImpl(event); } catch { /* best-effort */ } + } + if (mode === 'shadow') return { block: false }; // D28: всегда allow + const d = decide({ mode, verdict, floorBlocked: false }); // live-block: degraded-GO → allow; реальный NO-GO → block + return { block: d.block, message: d.message }; +} +``` + +`main()` без изменений относительно Task 6 (он уже зовёт `runJudgeTurn` → `exitDecision`). NB: degraded-allow +проходит через `decide({mode:'live-block', verdict:{decision:'GO',...}})` → `finalGate(GO, floorBlocked:false)` +→ allow ✓. Истинный throw уже пойман в `runJudgeTurn` (live-block→block) — это и есть fail-CLOSE на баг. + +**Тесты Task 6 правятся:** «live-block + нет ключа» (если присутствовал) трактуется как degraded-allow; +добавить кейс «live-block + unavailable → allow + warnImpl вызван». Кейсы inert/shadow/GO/NO-GO — без изменений. + +### Δ-E (документация спенд-семантики) + +В docstring шапки `enforce-judge-gate.mjs` (Task 3, при добавлении функций) зафиксировать: **inert=$0 / +shadow=метеренный / block=метеренный**; env строгие (`ENABLED` ровно `1`, `MODE` ровно `block`; опечатка +→ безопасный дефолт). Отложенные residuals (дедуп content-hash, budget-cap) — строкой «follow-up». + +### Self-review поправок + +Покрытие §9: п.1→Δ-A, п.2→Δ-B/Δ-C, п.3→Δ-D (main гибрид), п.4→Δ-E, п.5→Δ-D (warn-ветвь), п.6→Δ-E +(residuals строкой), п.7→подтверждено (Write-only Δ-A / engine-no-main / secure-default). Type-consistency: +`runJudgeGate` теперь возвращает `{decision,wired,verdict?,unavailable?}`; `runJudgeTurn` ветвится по +`wired`/`unavailable`; `callJudgeModel` → `{unavailable:true}|{}|{slots,objections,decision?}`. Имена едины. diff --git a/docs/superpowers/specs/2026-06-09-router-mentor-A1-judge-gate2-wiring-design.md b/docs/superpowers/specs/2026-06-09-router-mentor-A1-judge-gate2-wiring-design.md index cc5dfbc1..70d844a3 100644 --- a/docs/superpowers/specs/2026-06-09-router-mentor-A1-judge-gate2-wiring-design.md +++ b/docs/superpowers/specs/2026-06-09-router-mentor-A1-judge-gate2-wiring-design.md @@ -136,3 +136,35 @@ PreToolUse event - Цепочка кода после спеки/плана: `audit-context-building → sharp-edges → variant-analysis → writing-plans → test-driven-development → verification-before-completion → regression`. НЕ `requesting-code-review`. + +## 9. Поправки после pre-code adversarial-цепочки (2026-06-09) + +Цепочка (audit-context-building → sharp-edges → variant-analysis, сверка с живым v4-судьёй +`enforce-llm-judge-per-tool`) дала упрочнения дизайна — применяются поверх §4: + +1. **Детектор только на `Write`** (SE-FIX-2). Гейт-2 судит **полный план**; `Edit`/`MultiEdit` + на PreToolUse несут лишь фрагмент (old/new), не весь файл — судить фрагмент бессмысленно + (ложные NO-GO). `writing-plans` создаёт план через `Write` (полный `content`) — момент приёмки + ловится. Побочно снижает повторные срабатывания. +2. **judge-unavailable ≠ malformed verdict** (VA-FIX-1, зеркало v4 `{block:false, degraded:true}`). + `callJudgeModel`: нет `apiKey` ИЛИ транспорт бросил → `{unavailable:true}` (без throw, $0); + транспорт вернул текст, но парс невалиден → `{}`. `runJudgeGate`: `unavailable` → + `{decision:'GO', wired:false, unavailable:true}` (degraded ALLOW + громкий WARN, **не** NO-GO). + Только реальный невалидный вердикт → NO-GO (fail-closed). Различает «судья не может работать» + (operator-misconfig → деградация, как `judgeHealth` floor-only) от «судья сказал НЕТ» (block). +3. **main() гибрид fail-философии** (VA-NOTE-2 + MUST-VERIFY-1). `exitDisciplineDecision` await'ит + async-санк и fail-CLOSE только на throw/malformed. live-block: `await exitDisciplineDecision(async + () => { v = await runJudgeGate(event); logRealVerdict(v); return decide({mode, verdict:v, + floorBlocked:false}); })`. degraded-allow (`unavailable`) → decide(GO) → allow; реальный NO-GO → + block; истинный баг кода (throw) → fail-CLOSE block. shadow: run + log-real + always allow. +4. **Спенд-семантика явная** (SE-DOC-1): **inert = $0** (нет флага/HMAC-ключа); **shadow = метеренный** + (реальный LLM-вызов за каждую запись плана); **block = метеренный**. env строгие: + `ROUTER_MENTOR_JUDGE_ENABLED` ровно `1`, `ROUTER_MENTOR_JUDGE_MODE` ровно `block`; опечатка → + безопасный дефолт (inert/shadow). +5. **Логируем только реальный вердикт** (VA-OK-2, зеркало v4 `verdict!==undefined`): `unavailable`/ + degraded-allow → НЕ verdict-запись, а отдельная WARN-строка. +6. **Отложено (documented residuals):** дедуп по content-hash (SE-DEFER-1) + per-session budget-cap + (VA-NOTE-1) — Write-only уже ограничивает частоту; revisit, если shadow-стоимость заметна. +7. **Подтверждено ✓:** движок `judge-engine`/orchestrator без `main()`/транспорта (мис-регистрация + тратить не может); A1 стартует узко (Write-only) → не наследует v4 over-block по оси scope; + `judgeActive` default-inert при опечатке флага/режима (secure-by-default).