docs(router-mentor): A1 pre-code chain amendments (Write-only, judge-unavailable degraded-allow)
This commit is contained in:
@@ -630,3 +630,115 @@ Expected: GREEN, 0 failed. Прежний baseline ~3292 passed / 2 skipped —
|
||||
**Placeholder scan:** `<wt>` = `c:\моя\проекты\портал crm\Документация\.claude\worktrees\brainrepo` (литеральный путь в git-командах; PowerShell, `git -C "<wt>"`). Прочих плейсхолдеров нет — весь код приведён.
|
||||
|
||||
**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?}`. Имена едины.
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user