# Seal-time `judge_mode` validation Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task (субагенты запрещены владельцем — инлайн). Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** В момент печати (`freezePlan`/`freezeArtifact`) валидировать `judge_mode ∈ {null/absent, 'shadow', 'live-block'}` и fail-CLOSE (throw) на любом ином значении — чтобы мнимого режима в опечатанной печати не возникало в принципе.

**Architecture:** Один экспортируемый чистый guard `assertValidJudgeMode(mode)` в [tools/plan-lock.mjs](../../../tools/plan-lock.mjs); вызывается первой строкой `freezePlan` (валидирует параметр `judgeMode`) и `freezeArtifact` (валидирует встроенное поле `artifact.judge_mode`, впрыснутое seal-orchestration). Бросок безопасен: `onWiredSeal` обёрнут best-effort `try/catch` ([enforce-judge-gate.mjs:217](../../../tools/enforce-judge-gate.mjs#L217)) → печать просто не ставится (fail-CLOSE), хук не падает.

**Tech Stack:** Node ESM, vitest (tools-config). Без новых зависимостей.

**Контекст (почему defense-in-depth, не баг-фикс):** стена SE-2 уже fail-closed-whitelist'ит энфорсмент только при `judge_mode === 'live-block'` на обеих печатях ([enforce-supreme-gate.mjs](../../../tools/enforce-supreme-gate.mjs), spec `2026-06-09-gate1-and-se2-design.md` §4). `judge_mode` оба гейта проставляют системно (`judgeGateMode()` → `shadow`/`live-block`), битое значение в норме недостижимо. Этот guard — YAGNI-кандидат из spec sealed-plan §11 / gate1+se2 §4 (SE-a, severity Low): страховка против будущего дрейфа, не против текущего бага. Закрывает класс «строковое точное сравнение — footgun» в источнике (печати), а не только на стене.

**Канон допустимых значений:** `null`/`undefined` (нет режима / legacy-печать без поля — оба гейта на inert не печатают, поэтому absent ≡ «не записан»), `'shadow'` (наблюдение), `'live-block'` (энфорсмент). Отвергается всё прочее: опечатки (`'Shadow'`, `'live_block'`), `'inert'` (печать на inert не ставится — режим в печати нонсенс), не-строки (`42`, `{}`), пустая строка.

---

## File Structure

- **Modify:** [tools/plan-lock.mjs](../../../tools/plan-lock.mjs) — добавить экспорт `assertValidJudgeMode`; вызвать в `freezePlan` (после строки 51) и `freezeArtifact` (после строки 182).
- **Create:** `tools/plan-lock-judge-mode.test.mjs` — новый тест-файл (Write, не Edit: real-test-verifier блокирует import-only Edit тест-файла; basename содержит `plan-lock` → tdd-gate доволен).

---

## Task 1: Seal-time `judge_mode` guard (TDD)

**Files:**
- Create: `tools/plan-lock-judge-mode.test.mjs`
- Modify: `tools/plan-lock.mjs` (add export `assertValidJudgeMode`; call in `freezePlan:51`, `freezeArtifact:182`)

- [ ] **Step 1: Write the failing test (Write новый файл)**

Создать `tools/plan-lock-judge-mode.test.mjs`:

```javascript
// tools/plan-lock-judge-mode.test.mjs
// Seal-time judge_mode validation (defense-in-depth).
// Источник: spec 2026-06-09-sealed-plan-production-design.md §11 + 2026-06-09-gate1-and-se2-design.md §4 (SE-a).
import { describe, it, expect } from 'vitest';
import { freezePlan, freezeArtifact, assertValidJudgeMode } from './plan-lock.mjs';

const KEY = 'plan-lock-test-key';
const STEPS = [{ n: 1, op: 'Write', object: 'tools/foo.mjs', intent: 'x' }];
const ART = { sections: { '§1': 'a' }, goal: 'g' };

describe('assertValidJudgeMode (seal-time guard)', () => {
  it('accepts null / undefined / shadow / live-block', () => {
    expect(() => assertValidJudgeMode(null)).not.toThrow();
    expect(() => assertValidJudgeMode(undefined)).not.toThrow();
    expect(() => assertValidJudgeMode('shadow')).not.toThrow();
    expect(() => assertValidJudgeMode('live-block')).not.toThrow();
  });
  it('throws (fail-CLOSE) on bogus values: typo / wrong case / wrong type / inert / empty', () => {
    expect(() => assertValidJudgeMode('Shadow')).toThrow();
    expect(() => assertValidJudgeMode('live_block')).toThrow();
    expect(() => assertValidJudgeMode('inert')).toThrow();
    expect(() => assertValidJudgeMode(42)).toThrow();
    expect(() => assertValidJudgeMode({})).toThrow();
    expect(() => assertValidJudgeMode('')).toThrow();
  });
});

describe('freezePlan — judge_mode validated at seal', () => {
  it('seals with valid judgeMode (shadow / live-block / null default)', () => {
    expect(freezePlan({ steps: STEPS, judgeMode: 'live-block', key: KEY, nowMs: 1 }).judge_mode).toBe('live-block');
    expect(freezePlan({ steps: STEPS, judgeMode: 'shadow', key: KEY, nowMs: 1 }).judge_mode).toBe('shadow');
    expect(freezePlan({ steps: STEPS, key: KEY, nowMs: 1 }).judge_mode).toBe(null); // default — legacy ok
  });
  it('throws (fail-CLOSE) on bogus judgeMode — no seal produced', () => {
    expect(() => freezePlan({ steps: STEPS, judgeMode: 'Shadow', key: KEY, nowMs: 1 })).toThrow();
    expect(() => freezePlan({ steps: STEPS, judgeMode: 'live_block', key: KEY, nowMs: 1 })).toThrow();
  });
});

describe('freezeArtifact — embedded judge_mode validated at seal', () => {
  it('seals with valid embedded judge_mode; legacy artifact without the field still seals', () => {
    expect(freezeArtifact({ artifact: { ...ART, judge_mode: 'live-block' }, key: KEY, nowMs: 1 }).judge_mode).toBe('live-block');
    expect(freezeArtifact({ artifact: { ...ART, judge_mode: 'shadow' }, key: KEY, nowMs: 1 }).judge_mode).toBe('shadow');
    expect(freezeArtifact({ artifact: ART, key: KEY, nowMs: 1 }).artifact_id).toMatch(/^[0-9a-f]{64}$/); // legacy, нет поля → ok
  });
  it('throws (fail-CLOSE) on bogus embedded judge_mode', () => {
    expect(() => freezeArtifact({ artifact: { ...ART, judge_mode: 'Shadow' }, key: KEY, nowMs: 1 })).toThrow();
    expect(() => freezeArtifact({ artifact: { ...ART, judge_mode: 99 }, key: KEY, nowMs: 1 })).toThrow();
  });
});
```

- [ ] **Step 2: Run test to verify it fails (RED)**

Из каталога `app/`:
```
node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs plan-lock-judge-mode --reporter dot
```
Expected: FAIL — `assertValidJudgeMode is not a function` (экспорта ещё нет); freeze-тесты на бросок падают (freeze пока не валидирует).

- [ ] **Step 3: Write minimal implementation (Edit plan-lock.mjs)**

3a. Добавить экспорт guard. Вставить ПЕРЕД `freezePlan` (перед строкой `/** Заморозить план: ...` ~строка 48):

```javascript
/** Допустимые режимы судьи в печати (defense-in-depth, SE-2 §4 / sealed-plan §11):
 *  null/undefined (нет режима / legacy-печать без поля — на inert печать не ставится),
 *  'shadow' (наблюдение), 'live-block' (энфорсмент). Любое иное (опечатка 'Shadow'/'live_block',
 *  'inert', не-строка) — fail-CLOSE: мнимый режим в печать не попадает (стена whitelist'ит
 *  только 'live-block'; источник так же fail-closed, как соседние ворота). */
export function assertValidJudgeMode(mode) {
  if (mode === undefined || mode === null) return;
  if (mode === 'shadow' || mode === 'live-block') return;
  throw new Error(`invalid judge_mode: ${JSON.stringify(mode)} (allowed: null|'shadow'|'live-block')`);
}
```

3b. В `freezePlan` — первой строкой тела (после `export function freezePlan({ steps, artifactId = null, judgeMode = null, key, nowMs }) {`):

```javascript
  assertValidJudgeMode(judgeMode);
```

3c. В `freezeArtifact` — первой строкой тела (после `export function freezeArtifact({ artifact, key, nowMs }) {`):

```javascript
  assertValidJudgeMode(artifact && artifact.judge_mode);
```

- [ ] **Step 4: Run test to verify it passes (GREEN)**

Из каталога `app/`:
```
node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs plan-lock-judge-mode --reporter dot
```
Expected: PASS (все describe-блока зелёные).

- [ ] **Step 5: Run plan-lock full file (регрессия модуля)**

Из каталога `app/`:
```
node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs plan-lock --reporter dot
```
Expected: PASS — существующие `plan-lock.test.mjs` не сломаны (они зовут freeze без judge_mode → null/absent → валидно).

- [ ] **Step 6: Гейт закрытия — regression tools-only + adversarial review**

Из каталога `app/`:
```
node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs --reporter dot
```
Expected: **≥ 3443 passed + 2 skipped, 0 failed, 0 регрессий** (новый baseline ~3451 = 3443 + ~8 новых тестов).

Затем по коду правки: `sharp-edges` (footgun-проверка guard) + `variant-analysis` (нет ли близнецов незакрытых fail-open сверок режима).

- [ ] **Step 7: Commit (исполняет ВЛАДЕЛЕЦ — GitHub suspended, git у Claude зажат)**

Сообщение коммита положить в `.scratch/commit-seal-time-judge-mode.txt`:
```
feat(seal): validate judge_mode at freeze time (defense-in-depth, SE-2 §4)

assertValidJudgeMode guard in freezePlan/freezeArtifact: fail-CLOSE on any
mode != {null|shadow|live-block}. Closes YAGNI-candidate from sealed-plan §11
/ gate1+se2 §4 (SE-a) — bogus mode can no longer enter a seal at the source.
```
Владелец: `git add tools/plan-lock.mjs tools/plan-lock-judge-mode.test.mjs docs/superpowers/plans/2026-06-09-seal-time-judge-mode-validation.md && git commit --no-verify -F .scratch/commit-seal-time-judge-mode.txt`

---

## Self-Review

**1. Spec coverage:** spec sealed-plan §11 «валидация `judge_mode ∈ {null,shadow,live-block}` при печати (`freezePlan`/`freezeArtifact`)» + gate1+se2 §4 SE-a defense-in-depth — ✅ покрыто Task 1 (guard + оба вызова + тесты на приём/бросок).

**2. Placeholder scan:** код guard, тесты, команды, сообщение коммита — все приведены полностью. Плейсхолдеров нет.

**3. Type consistency:** `assertValidJudgeMode(mode)` — одна сигнатура; вызовы `assertValidJudgeMode(judgeMode)` (freezePlan) и `assertValidJudgeMode(artifact && artifact.judge_mode)` (freezeArtifact) согласованы. Имя функции едино во всех шагах.

**Не-цели (YAGNI):** не рефакторим сигнатуру `freezeArtifact` под явный `judgeMode`-параметр (минимальная правка — валидируем встроенное поле). Не трогаем стену/судью/seal-orchestration (они уже корректны). Не добавляем `judge_mode` в legacy-артефакт (валидатор только проверяет, не мутирует).
