# Гейт-1 (судейство спек) + SE-2 (стена не чтит shadow) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans (инлайн — субагенты ЗАПРЕЩЕНЫ владельцем). Steps use checkbox (`- [ ]`) syntax.

**Goal:** Подключить судейство записей спек (Гейт-1 → печать артефакта в рантайме) и сделать стену fail-closed по режиму печати (SE-2: энфорсмент только на live-block) — последние 2 хвоста до флипа Фазы 8.

**Architecture:** Гейт-1 — новая чистая `extractGate1Product` (зеркало `extractGate2Product`, но `SPEC_PATH_RE` + `functionName:'gate1'`) + маршрутизация в `runJudgeGate` (спека→gate1, иначе план→gate2) + `judged_hash` от СЫРОГО content (SD-1). Движок судьи (`judge-engine` gate1-линзы) НЕ трогаем. SE-2 — в `decideMode` fail-closed whitelist: энфорсмент только при `judge_mode==='live-block'` на ОБЕИХ печатях.

**Tech Stack:** Node ESM, vitest (tools-only config). Источник истины — [спека](../specs/2026-06-09-gate1-and-se2-design.md) (ОДОБРЕНА). При расхождении приоритет у спеки.

**Квирки (handoff):**
- vitest ТОЛЬКО из каталога `app/` (иначе раннер ломается «reading config»): `node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs <фильтр> --reporter dot`. `<repo>` = `c:\моя\проекты\портал crm\Документация`.
- `enforce-tdd-gate`: правка `*.test.mjs` ПЕРЕД prod `*.mjs` в том же ходе (RED первым). Существующий тест — сперва Read.
- Тест-файлы: for-of + it(), НЕ it.each.
- git на main ЗАЖАТ (approve_git_operation авто-парсер не работает) + GitHub аккаунт suspended → коммиты делает ВЛАДЕЛЕЦ в своём терминале по точным командам (commit-не-пуш). Сообщения коммитов в `.scratch/*.txt`.

---

## File Structure

| Файл | Ответственность | Действие |
|---|---|---|
| `tools/enforce-judge-gate.mjs` | +`extractGate1Product` (детект спеки) + маршрутизация g1/g2 в `runJudgeGate` + `judged_hash` от raw content (SD-1) + импорт `sealableArtifact` | Modify |
| `tools/enforce-judge-gate.test.mjs` | тесты Гейт-1 (детект + routing + judged_hash raw) | Modify |
| `tools/enforce-supreme-gate.mjs` | SE-2 whitelist в `decideMode` | Modify |
| `tools/enforce-supreme-gate.test.mjs` | новые SE-2 тесты + миграция существующих enforce-тестов на `judge_mode:'live-block'` | Modify |

Движок (`judge-engine.mjs`), seal-orchestration, plan-lock, sealOnWiredGo — НЕ трогаем (gate1 уже поддержан, sealOnWiredGo spec-ветка готова).

---

## Task 1: Гейт-1 — `extractGate1Product` (чистый детект записи спеки)

**Files:**
- Modify: `tools/enforce-judge-gate.mjs` (новая экспортируемая функция)
- Test: `tools/enforce-judge-gate.test.mjs` (новый describe-блок)

- [ ] **Step 1: Read `tools/enforce-judge-gate.test.mjs`** (существующий — обязателен перед правкой теста).

- [ ] **Step 2: Write the failing test** (добавить в конец `enforce-judge-gate.test.mjs`; добавить `extractGate1Product` в верхний import из `./enforce-judge-gate.mjs`)

```javascript
describe('extractGate1Product — детект спеки (Write-only) + извлечение', () => {
  const specPath = 'docs/superpowers/specs/2026-06-09-foo-design.md';
  it('Write спеки → shouldJudge, product, functionName=gate1, goal, cards=[]', () => {
    const ev = { tool_name: 'Write', tool_input: { file_path: specPath, content: '# Spec\n## Цель\nрешить X\n## Реш {#r}\nтекст' } };
    const r = extractGate1Product(ev);
    expect(r.shouldJudge).toBe(true);
    expect(r.functionName).toBe('gate1');
    expect(r.product).toContain('решить X');
    expect(r.goal).toContain('решить X');
    expect(r.cards).toEqual([]);
  });
  it('Write спеки с обратными слэшами → распознаётся', () => {
    const ev = { tool_name: 'Write', tool_input: { file_path: 'docs\\superpowers\\specs\\x-design.md', content: 'c' } };
    expect(extractGate1Product(ev).shouldJudge).toBe(true);
  });
  it('Edit спеки → shouldJudge:false (Δ-A: фрагмент)', () => {
    const ev = { tool_name: 'Edit', tool_input: { file_path: specPath, new_string: 'кусок' } };
    expect(extractGate1Product(ev).shouldJudge).toBe(false);
  });
  it('MultiEdit спеки → shouldJudge:false', () => {
    const ev = { tool_name: 'MultiEdit', tool_input: { file_path: specPath, edits: [{ old_string: 'a', new_string: 'b' }] } };
    expect(extractGate1Product(ev).shouldJudge).toBe(false);
  });
  it('план-путь (Write) → shouldJudge:false (это Гейт-2)', () => {
    const ev = { tool_name: 'Write', tool_input: { file_path: 'docs/superpowers/plans/x.md', content: 'c' } };
    expect(extractGate1Product(ev).shouldJudge).toBe(false);
  });
  it('не-спека путь / Bash / битое → shouldJudge:false', () => {
    expect(extractGate1Product({ tool_name: 'Write', tool_input: { file_path: 'tools/foo.mjs', content: 'c' } }).shouldJudge).toBe(false);
    expect(extractGate1Product({ tool_name: 'Bash', tool_input: { command: 'ls' } }).shouldJudge).toBe(false);
    expect(extractGate1Product(null).shouldJudge).toBe(false);
  });
  it('extractGate2Product на спеке по-прежнему false (Гейт-2 = планы, регрессия)', () => {
    const ev = { tool_name: 'Write', tool_input: { file_path: specPath, content: 'c' } };
    expect(extractGate2Product(ev).shouldJudge).toBe(false);
  });
});
```

- [ ] **Step 3: Run test → FAIL**

Из `app/`: `node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs enforce-judge-gate --reporter dot`
Expected: FAIL (`extractGate1Product is not a function`).

- [ ] **Step 4: Implement** (в `enforce-judge-gate.mjs`, рядом с `extractGate2Product`)

```javascript
/**
 * Детект «запись РЕШЕНИЯ-спеки» (Гейт-1) + извлечение продукта на суд.
 * Срабатывает ТОЛЬКО для Write по пути docs/superpowers/specs/*.md (полный content).
 * Зеркало extractGate2Product, но SPEC_PATH_RE + functionName='gate1'.
 */
export function extractGate1Product(event) {
  const tool = event && event.tool_name;
  const input = (event && event.tool_input) || {};
  const filePath = String(input.file_path || '');
  if (!PLAN_TOOLS.has(tool) || !SPEC_PATH_RE.test(filePath)) return { shouldJudge: false };
  const product = String(input.content ?? '').trim();
  return { shouldJudge: true, functionName: 'gate1', product, goal: extractGoal(product), cards: [] };
}
```

(NB: `SPEC_PATH_RE`, `PLAN_TOOLS`, `extractGoal` уже определены в модуле — Task 5 sealed-plan + существующие.)

- [ ] **Step 5: Run test → PASS** (тот же фильтр; существующие тесты зелёные).

- [ ] **Step 6: Commit** (владелец, терминал)

```
git add tools/enforce-judge-gate.mjs tools/enforce-judge-gate.test.mjs
git commit --no-verify -F .scratch/g1-extract.txt   # "feat(gate1): extractGate1Product (spec detect, Write-only)"
```

---

## Task 2: Гейт-1 — маршрутизация в `runJudgeGate` + `judged_hash` от raw content (SD-1)

**Files:**
- Modify: `tools/enforce-judge-gate.mjs` (`runJudgeGate` + импорт `sealableArtifact`)
- Test: `tools/enforce-judge-gate.test.mjs`

- [ ] **Step 1: Write the failing test** (добавить в `enforce-judge-gate.test.mjs`; объявить gate1-ok-вердикт)

```javascript
import { judgedHashOf, sealableArtifact } from './seal-orchestration.mjs';

const gate1Ok = JSON.stringify({ slots: { completeness: 'aaaaaaaa', premortem: 'bbbbbbbb', goal_advocate: 'cccccccc', correctness: 'dddddddd' }, objections: [] });
const specEv = (content) => ({ tool_name: 'Write', tool_input: { file_path: 'docs/superpowers/specs/x-design.md', content } });

describe('runJudgeGate — Гейт-1 (спека) маршрут + judged_hash от raw (SD-1)', () => {
  it('активен + spec-Write + чистый вердикт → wired, functionName=gate1', async () => {
    const deps = { judgeActiveImpl: () => true, apiKey: 'K', transport: async () => gate1Ok };
    const r = await runJudgeGate(specEv('## Реш {#r}\nтекст'), deps);
    expect(r.wired).toBe(true);
    expect(r.decision).toBe('GO');
    expect(r.verdict.functionName).toBe('gate1');
  });
  it('judged_hash gate1 = judgedHashOf(sealableArtifact(СЫРОГО content)) — SD-1', async () => {
    const content = '  ## Реш {#r}\nтекст с пробелами по краям  ';
    const deps = { judgeActiveImpl: () => true, apiKey: 'K', transport: async () => gate1Ok };
    const r = await runJudgeGate(specEv(content), deps);
    expect(r.judged_hash).toBe(judgedHashOf(sealableArtifact(content)));
    // SD-1 регрессионный страж: trimmed дал бы ДРУГОЙ хеш (source_sha байт-чувствителен)
    expect(r.judged_hash).not.toBe(judgedHashOf(sealableArtifact(content.trim())));
  });
  it('план-путь по-прежнему gate2 (регрессия)', async () => {
    const deps = { judgeActiveImpl: () => true, apiKey: 'K', transport: async () => JSON.stringify({ slots: { spec_fidelity: 'aaaaaaaa', verifiability: 'bbbbbbbb', plan_soundness: 'cccccccc', execution_risk: 'dddddddd', step_clarity: 'eeeeeeee' }, objections: [] }) };
    const r = await runJudgeGate({ tool_name: 'Write', tool_input: { file_path: 'docs/superpowers/plans/x.md', content: '# П\n## Цель\nцель\nшаг' } }, deps);
    expect(r.wired).toBe(true);
    expect(r.verdict.functionName).toBe('gate2');
  });
});
```

- [ ] **Step 2: Run test → FAIL** (фильтр `enforce-judge-gate`). Expected: FAIL (gate1 не маршрутизируется / functionName gate2 / judged_hash от trimmed).

- [ ] **Step 3: Implement** — заменить тело `runJudgeGate` (детект+routing+raw judged_hash) и расширить импорт seal-orchestration:

Импорт (был `sealablePlan, judgedHashOf`) → добавить `sealableArtifact`:

```javascript
import { sealArtifact, sealPlan, sealablePlan, sealableArtifact, judgedHashOf } from './seal-orchestration.mjs';
```

`runJudgeGate`:

```javascript
export async function runJudgeGate(event, deps = {}) {
  const judgeActiveImpl = deps.judgeActiveImpl || judgeActive;
  if (!judgeActiveImpl()) return { decision: 'GO', wired: false };
  // Гейт-1 (спека) приоритетно, иначе Гейт-2 (план). Пути непересекаемы (specs/ vs plans/).
  const g1 = extractGate1Product(event);
  const g = g1.shouldJudge ? g1 : extractGate2Product(event);
  if (!g.shouldJudge) return { decision: 'GO', wired: false };
  const functionName = g.functionName; // 'gate1' | 'gate2'
  const apiKey = deps.apiKey !== undefined ? deps.apiKey : process.env.ROUTER_LLM_KEY;
  const requiredLenses = requiredLensesFor(functionName);
  const promptArgs = { product: g.product, goal: g.goal, cards: g.cards };
  const raw = await callJudgeModel({ functionName, requiredLenses, promptArgs, apiKey, model: deps.model, transport: deps.transport });
  if (raw && raw.unavailable) {
    return { decision: 'GO', wired: false, unavailable: true };
  }
  const verdict = runJudge({ functionName, requiredLenses, subRunsRequired: [], subRuns: [], llmCall: () => raw, promptArgs });
  // C5/SD-1: judged_hash от СЫРОГО content (как печать sealOnWiredGo), не от g.product (trimmed).
  const rawContent = String((event && event.tool_input && event.tool_input.content) ?? '');
  let judged_hash;
  try {
    judged_hash = functionName === 'gate1'
      ? judgedHashOf(sealableArtifact(rawContent))
      : judgedHashOf(sealablePlan(rawContent));
  } catch { judged_hash = undefined; }
  return { decision: verdict.decision, wired: true, judged_hash, verdict: { ...verdict, functionName } };
}
```

- [ ] **Step 4: Run test → PASS** (фильтр `enforce-judge-gate`; существующие runJudgeGate/runJudgeTurn тесты зелёные — planEv по-прежнему gate2).

- [ ] **Step 5: Commit** (владелец)

```
git add tools/enforce-judge-gate.mjs tools/enforce-judge-gate.test.mjs
git commit --no-verify -F .scratch/g1-route.txt   # "feat(gate1): route specs in runJudgeGate + judged_hash from raw content (SD-1)"
```

---

## Task 3: SE-2 — fail-closed whitelist в `decideMode` (+ миграция существующих enforce-тестов)

**Files:**
- Modify: `tools/enforce-supreme-gate.mjs` (`decideMode`)
- Test: `tools/enforce-supreme-gate.test.mjs` (новые SE-2 тесты + миграция)

- [ ] **Step 1: Read `tools/enforce-supreme-gate.test.mjs`** (обязателен — нужно найти существующие decideMode implementation-тесты для миграции + константы PLAN/PLANL/ART/baseMode).

- [ ] **Step 2: Write the failing SE-2 test** (добавить новый describe-блок в `enforce-supreme-gate.test.mjs`)

```javascript
describe('SE-2 — decideMode fail-closed whitelist по judge_mode', () => {
  // Хелперы: печати с заданным judge_mode. verifyImpl/verifyArtifactImpl застаблены true,
  // normalize lower-case; шаг плана матчит Write tools/foo.mjs.
  const planWith = (mode) => ({ judge_mode: mode, artifact_id: 'AID', steps: [{ n: 1, op: 'Write', object: 'tools/foo.mjs' }] });
  const artWith = (mode) => ({ judge_mode: mode, artifact_id: 'AID', sections: {} });
  const baseSE2 = (over) => ({ key: 'k', verifyImpl: () => true, verifyArtifactImpl: () => true, normalize: (p) => p.toLowerCase(), stepPtr: 0, ...over });
  const writeFoo = { name: 'Write', input: { file_path: 'tools/foo.mjs' } };
  const readX = { name: 'Read', input: { file_path: 'x' } };

  it('обе печати live-block → энфорсит (совпавшая правка allow)', () => {
    const r = decideMode(baseSE2({ toolUse: writeFoo, frozenPlan: planWith('live-block'), frozenArtifact: artWith('live-block') }));
    expect(r.decision).toBe('allow');
    expect(r.mode).toBe('implementation');
  });
  it('shadow-план → разговорный, мутатор block', () => {
    const r = decideMode(baseSE2({ toolUse: writeFoo, frozenPlan: planWith('shadow'), frozenArtifact: artWith('live-block') }));
    expect(r.decision).toBe('block');
    expect(r.mode).toBe('conversational');
  });
  it('shadow-печать → observe (Read) allow', () => {
    const r = decideMode(baseSE2({ toolUse: readX, frozenPlan: planWith('shadow'), frozenArtifact: artWith('shadow') }));
    expect(r.decision).toBe('allow');
    expect(r.mode).toBe('conversational');
  });
  it('judge_mode null/отсутствует → разговорный, мутатор block (fail-closed)', () => {
    const r = decideMode(baseSE2({ toolUse: writeFoo, frozenPlan: planWith(null), frozenArtifact: artWith(null) }));
    expect(r.decision).toBe('block');
    expect(r.mode).toBe('conversational');
  });
  it('опечатка режима (Shadow / live_block) → разговорный (не live-block)', () => {
    expect(decideMode(baseSE2({ toolUse: writeFoo, frozenPlan: planWith('Shadow'), frozenArtifact: artWith('live-block') })).decision).toBe('block');
    expect(decideMode(baseSE2({ toolUse: writeFoo, frozenPlan: planWith('live_block'), frozenArtifact: artWith('live-block') })).decision).toBe('block');
  });
  it('mode-mismatch (артефакт live-block + план shadow) → block', () => {
    const r = decideMode(baseSE2({ toolUse: writeFoo, frozenPlan: planWith('shadow'), frozenArtifact: artWith('live-block') }));
    expect(r.decision).toBe('block');
  });
});
```

- [ ] **Step 3: Run test → FAIL** (фильтр `enforce-supreme-gate`). Expected: FAIL (whitelist ещё нет — shadow/null печати энфорсятся как раньше).

- [ ] **Step 4: Implement** — в `enforce-supreme-gate.mjs::decideMode`, между проверкой валидного артефакта и `return {...decide(...)}` вставить whitelist:

```javascript
  // SE-2 (fail-closed whitelist): энфорсмент ТОЛЬКО при live-block на ОБЕИХ печатях.
  // shadow/null/опечатка/любое ≠ 'live-block' → разговорный (мнимая печать не одобряет энфорсмент).
  if (frozenPlan.judge_mode !== 'live-block' || frozenArtifact.judge_mode !== 'live-block') {
    if (isSeed(toolUse) || isObserveOnly(toolUse)) {
      return { decision: 'allow', mode: 'conversational', reason: 'seed/observe (не live-block-печать, SE-2)' };
    }
    return { decision: 'block', mode: 'conversational', reason: 'не live-block-печать (shadow/наблюдение) — не одобрение к энфорсменту (SE-2)' };
  }
  return { ...decide({ toolUse, frozenPlan, frozenArtifact, stepPtr, key, verifyImpl, verifyArtifactImpl, normalize }), mode: 'implementation' };
```

- [ ] **Step 5: Run new SE-2 test → PASS**, затем **прогнать ВЕСЬ `enforce-supreme-gate`** — существующие implementation-тесты УПАДУТ (их печати без `judge_mode` → разговорный).

- [ ] **Step 6: Мигрировать существующие enforce-тесты.** Для КАЖДОГО упавшего теста, который ожидал `mode:'implementation'` / allow-или-block по шагу плана: проставить `judge_mode: 'live-block'` на ЕГО `frozenPlan` И `frozenArtifact` (в общих константах PLAN/PLANL/ART — добавить поле `judge_mode: 'live-block'`; в inline-объектах `{ sig: 'ok' }` артефакта — добавить `judge_mode: 'live-block'`). Тесты разговорного режима (frozenPlan:null / frozenArtifact:null / битый verify) НЕ трогать — они до whitelist-сверки не доходят. `seal-to-wall.integration.test.mjs` уже `'live-block'` — без изменений.

- [ ] **Step 7: Run → PASS** весь `enforce-supreme-gate` зелёный (новые SE-2 + мигрированные).

- [ ] **Step 8: Commit** (владелец)

```
git add tools/enforce-supreme-gate.mjs tools/enforce-supreme-gate.test.mjs
git commit --no-verify -F .scratch/se2.txt   # "feat(se2): decideMode fail-closed whitelist (enforce only live-block seals)"
```

---

## Task 4: Гейт закрытия — полная регрессия + верификация

- [ ] **Step 1: Полная регрессия tools-only** (из `app/`):
`node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs --reporter dot`
Expected: ≥ baseline (sealed-plan дал 3427) + новые кейсы Task 1-3, **0 регрессий**.

- [ ] **Step 2: `verification-before-completion`** — сверить, что каждый компонент спеки (Гейт-1: extractGate1Product / routing / judged_hash raw; SE-2: whitelist) реализован, и каждый инвариант §5 покрыт тестом; SD-1 регрессионный страж зелёный.

- [ ] **Step 3: Финал** — обновить §9 спеки (реализация done). **Push — НЕ делать** (GitHub suspended; коммиты владелец делает локально). Коммиты — батчем по точным командам в терминале владельца.

---

## Self-Review (план против спеки)

- **Покрытие спеки:** Гейт-1 §3 → Task 1 (extract) + Task 2 (routing + SD-1 judged_hash raw); SE-2 §4 whitelist → Task 3 (+ миграция). Инварианты §5: gate1 detect/routing/judged_hash → Task 1/2; SE-2 live-block/shadow/null/опечатка/mismatch → Task 3; регрессия → Task 4.
- **Закрытые находки разборов:** SD-1 (judged_hash от raw) → Task 2 Step 3 + регрессионный страж Task 2 Step 1; VA-a/SE-a (whitelist) → Task 3; SE-b/SD-3/SD-4/P2-* — операционные/by-design, не код-задачи.
- **Плейсхолдеры:** нет — реальный код в каждом шаге; миграция Task 3 Step 6 — механическая (добавить `judge_mode:'live-block'`), специфику смотрит исполнитель по упавшим тестам.
- **Согласованность типов:** `extractGate1Product` возвращает `{shouldJudge, functionName:'gate1', product, goal, cards}` ↔ `runJudgeGate` читает `g.functionName`/`g.product`/`g.goal`/`g.cards`; `judgedHashOf`/`sealableArtifact` импортированы из `seal-orchestration`; `decideMode` whitelist читает `frozenPlan.judge_mode`/`frozenArtifact.judge_mode` (Task 3/4 sealed-plan их кладут).
- **NB:** движок (`judge-engine` gate1-линзы), `sealOnWiredGo` spec-ветка, seal-orchestration — НЕ трогаются (уже готовы). Только gate/wall-обёртки.
