# Дизайн: Гейт-1 (судейство спек) + SE-2 (стена не чтит shadow-печать) — М7 Фаза 8, 2 хвоста до флипа

**Дата:** 2026-06-09 · **Кодовая фраза эпика:** «роутер-наставник» · **Статус:** ЧЕРНОВИК (ждёт 2 адверсариальных прохода + ревью владельца).
**Тип:** design-only. Кода нет. Реализация — отдельный заход по слову владельца.
**Цепочка дизайна (согласована владельцем):** `brainstorming` → `audit-context-building` → черновик → **разбор #1 + разбор #2** (`sharp-edges` → `variant-analysis` → `systematic-debugging`, оба по написанной спеке) → spec self-review → ревью владельца. `writing-plans` — следующая фаза.

---

## 1. Цель и почему это предусловие флипа

Связка печати (sealed-plan production, спека `2026-06-09-sealed-plan-production-design.md`) собрана, но осталось **2 хвоста**, без которых механизм неполон/небезопасен для флипа:

1. **Гейт-1 не подключён.** Хук судьи [enforce-judge-gate.mjs](../../../tools/enforce-judge-gate.mjs) судит только записи **планов** (Гейт-2, `extractGate2Product`). Записи **спек** не судятся → на реальном GO артефакт не печатается → `sealPlan` всегда упирается в «нет текущего артефакта» (fail-CLOSE). Без печати артефакта вся связка не работает в рантайме.
2. **SE-2 не закрыт.** Печати теперь несут `judge_mode` (собрано в Task 3/4), но стена [enforce-supreme-gate.mjs::decideMode](../../../tools/enforce-supreme-gate.mjs#L187) ещё не различает shadow- и live-печать — shadow-печать энфорсилась бы как настоящее одобрение.

Обе правки — в security-критичные гейт/стену. Движок судьи **не трогаем** (он уже поддерживает gate1).

---

## 2. Решения владельца (зафиксированы 2026-06-09)

1. **Гейт-1 = судить записи спек LLM-судьёй** (как планы), линзы `gate1`. Соответствует модели sealed-plan §3 «Печать №1: frozen-artifact ← judge GO на записи spec». Альтернатива «печатать артефакт по owner-approval вместо судьи» — отклонена.
2. **SE-2 = строгий вариант: shadow-печать НЕ одобрение → стена блокирует мутаторы** (разговорный режим, только seed/observe). Owner-флаг «обкатка стены» (энфорсить shadow для теста стены) в v1 НЕ делаем (YAGNI). *(Разбор #1 уточнил механику до fail-closed whitelist: стена энфорсит ТОЛЬКО при `judge_mode==='live-block'` на ОБЕИХ печатях; shadow/null/опечатка → разговорный — см. §4/§7.)*
3. **2 адверсариальных прохода** по написанной спеке (как у sealed-plan).
4. **Субагенты запрещены** — реализация инлайн через `executing-plans`.

---

## 3. Компонент 1 — Гейт-1 (судейство спек → печать артефакта)

**Движок готов:** [judge-engine.mjs:20](../../../tools/judge-engine.mjs#L20) — `VOTE_LAYOUTS.gate1 = ['completeness','premortem','goal_advocate','correctness']`; `requiredLensesFor('gate1')`, `buildJudgePrompt`, `runJudge` — functionName-агностичны. **Engine не меняем.**

**Что добавляем в [enforce-judge-gate.mjs](../../../tools/enforce-judge-gate.mjs):**

- `extractGate1Product(event)` — зеркало `extractGate2Product`, но детект записи **спеки**: `Write` (только, Δ-A: Edit/MultiEdit = фрагмент) по `SPEC_PATH_RE`. Возвращает `{shouldJudge, functionName:'gate1', product:content, goal:extractGoal(content), cards:[]}`. Иначе `{shouldJudge:false}`.
- `runJudgeGate` — маршрутизация: сперва `extractGate1Product` (спека), иначе `extractGate2Product` (план), иначе `{decision:'GO', wired:false}` ($0). Для выбранного гейта: `requiredLensesFor(functionName)` + `callJudgeModel` + `runJudge` (как сейчас). `judged_hash`:
  - gate1 → `judgedHashOf(sealableArtifact(RAW content))` (`buildArtifact` НЕ бросает — хеш всегда вычислим);
  - gate2 → `judgedHashOf(sealablePlan(RAW content))` (как сейчас, может быть undefined при отсутствии блока).
  - **SD-1 (разбор #1, HIGH):** `judged_hash` считается от **СЫРОГО** `event.tool_input.content` — того же, что использует `sealOnWiredGo` для печати — НЕ от `g.product` (который `.trim()`'ится, как в `extractGate2Product:112`). Для артефакта `source_sha = sha256(content)` чувствителен к байтам: trimmed ≠ untrimmed → `judged_hash` не сошёлся бы с печатью → артефакт НИКОГДА не печатался бы (тихий тотальный отказ Гейт-1). Один канонический вид content течёт судья→печать. (Планы избегали бага случайно: `sealablePlan`={steps} из regex-блока, к внешнему trim нечувствителен.)
- **`extractGate2Product` НЕ трогаем** — закреплённый тест «specs → shouldJudge:false (это Гейт-1, не Гейт-2)» остаётся; Гейт-1 — отдельная функция.
- `sealOnWiredGo` (Task 5) уже имеет ветку `SPEC_PATH_RE → sealArtifact` → печать артефакта замкнётся автоматически, как только `runJudgeGate` отдаст wired GO на spec-Write.

**Дисциплина дороги (инвариант):** `SPEC_PATH_RE` (`docs/superpowers/specs/*.md`) и `PLAN_PATH_RE` (`docs/superpowers/plans/*.md`) непересекаются (разные каталоги) → g1 и g2 не могут сработать одновременно.

**Поведение по режимам** (как у Гейт-2): inert → нет суда; shadow → судит+лог, печать артефакта ставится (наблюдаемо), стену не дёргает (SE-2 ниже); live-block → GO печать+allow, NO-GO блок записи спеки (судим качество решения — намеренно).

---

## 4. Компонент 2 — SE-2 (стена не чтит shadow-печать)

**Что добавляем в [enforce-supreme-gate.mjs::decideMode](../../../tools/enforce-supreme-gate.mjs#L187):** между проверкой валидного артефакта ([:199](../../../tools/enforce-supreme-gate.mjs#L199)) и переходом к `decide()` ([:206](../../../tools/enforce-supreme-gate.mjs#L206)) — проверка режима печати **fail-closed whitelist**:

```
ЭНФОРСМЕНТ (→ decide()) ТОЛЬКО ЕСЛИ:
    frozenPlan.judge_mode === 'live-block' И frozenArtifact.judge_mode === 'live-block'
ИНАЧЕ (shadow / null / опечатка / отсутствует / любое ≠ 'live-block'):
    → разговорный режим: isSeed/isObserveOnly → allow (mode:'conversational'); иначе block
       reason: «не live-block-печать (shadow/наблюдение) — не одобрение к энфорсменту (SE-2)»
```

**Почему whitelist, а не «явный shadow → блок» (разборы #1 — sharp-edges SE-a + variant-analysis VA-a):** проверка `=== 'shadow'` оставляла бы **else-ветку = энфорсмент** (доверие) — это **единственная fail-OPEN сверка во всём коде** (все соседние ворота — `isRealGo`, `finalGate`, `sealOnWiredGo` — fail-CLOSED на не-совпадении). Плюс `judge_mode` — строковое поле точного сравнения: опечатка/неожиданное значение (`'Shadow'`, `'live_block'`) → не `'shadow'` → ошибочный энфорсмент мнимой печати. **Whitelist `=== 'live-block'`** делает любое иное значение (shadow, null, опечатка, отсутствие) → разговорный режим = безопасное направление; закрывает SE-a и VA-a одной сверкой, приводит SE-2 к конвенции кодовой базы.

**OR-логика → AND-логика whitelist (строгость владельца сохранена):** энфорсмент требует `'live-block'` НА ОБЕИХ печатях. Любое расхождение (одна shadow / одна null / mode-mismatch план≠артефакт) → разговорный режим (строжайше).

**Цена (обратная совместимость, для writing-plans):** существующие тесты `decideMode`, ожидающие энфорсмента, строят `frozenPlan`/`frozenArtifact` **без** `judge_mode` → при whitelist уйдут в разговорный режим. Их надо **мигрировать**: проставить `judge_mode:'live-block'` на обеих печатях (семантически верно — они тестируют ЖИВОЙ энфорсмент). Тесты разговорного режима (нет плана / нет артефакта) до whitelist-сверки не доходят — не трогаются. Task 6 integration уже `'live-block'` — без изменений.

**Defense-in-depth (опционально, SE-a):** валидация `judge_mode ∈ {null,'shadow','live-block'}` при печати (`freezePlan`/`freezeArtifact`) — чтобы мнимых значений в печати не возникало в принципе. Не требуется для безопасности (whitelist уже fail-closed), но дешёвая страховка — решаем в плане (YAGNI-кандидат).

**Инвариант:** до `decide()` (энфорсмент) доходят ТОЛЬКО печати с `judge_mode === 'live-block'` на плане И артефакте. Всё прочее → мутаторы block, seed/observe allow.

---

## 5. Стратегия тестирования (TDD-инварианты — для writing-plans)

- **Гейт-1:**
  - `extractGate1Product`: Write спеки (`specs/*.md`) → `shouldJudge:true, functionName:'gate1'`; Edit/MultiEdit спеки → false (Δ-A); план/прочий путь → false; Bash/битое → false.
  - `extractGate2Product` регрессия: спека → по-прежнему `shouldJudge:false` (Гейт-2 = планы).
  - `runJudgeGate`: активен + spec-Write + чистый вердикт → `wired:true`, линзы gate1, `judged_hash = judgedHashOf(sealableArtifact(RAW content))`; активен + spec-Write + якорный NO → NO-GO; план-путь не сломан (g2 как раньше).
  - **SD-1:** `judged_hash` от сырого content сходится с `sealOnWiredGo`→`sealArtifact` → артефакт реально печатается (тест: spec-Write + real GO + judged_hash от raw → `sealArtifact` sealed:true; trimmed-вариант judged_hash НЕ сошёлся бы — регрессионный страж).
  - Интеграция: spec-Write + real GO (через `sealOnWiredGo` прод-deps) → `sealArtifact` вызван, артефакт персистнут.
- **SE-2 (whitelist):**
  - `decideMode` план+артефакт оба `judge_mode='live-block'` → энфорсит (совпавшая правка allow / несовпавшая block) — как Task 6.
  - `decideMode` shadow-печать (план ИЛИ артефакт `judge_mode='shadow'`) + совпавшее мутирующее действие → **block** (разговорный); seed/observe → allow.
  - `decideMode` `judge_mode` null/отсутствует (legacy) → **разговорный** (block мутатор) — НЕ энфорсит (fail-closed); существующие enforce-тесты мигрированы на `'live-block'`.
  - `decideMode` опечатка mode (`'Shadow'`/`'live_block'`) → разговорный (не `'live-block'` → fail-closed).
  - mode-mismatch (артефакт live-block + план shadow, или наоборот) → block (AND-whitelist).
- Регрессия tools-only ≥ baseline (план уточнит число; включает миграцию decideMode enforce-тестов), 0 регрессий.

---

## 6. Открытые пункты для адверсариального разбора (разбор #1/#2)

- **Гейт-1 judged_hash согласованность:** ровно ли sealable-артефакт, который хешируется в вердикте, совпадает с тем, что печатает `sealOnWiredGo`→`sealArtifact` (одно представление судья→печать; TOCTOU при правке между судом и печатью).
- **Гейт-1 over-block:** spec-Write в live-block + NO-GO → запись спеки блокируется; не клинит ли это сам цикл «написать/поправить спеку» (как readonly-Bash клинил per-tool судью)?
- **Гейт-1 ↔ Гейт-2 пересечение:** точно ли пути непересекаемы; что если файл и в specs/, и матчит plan-эвристику (символлинк/обходной путь).
- **SE-2 backward-compat дыра:** не появляется ли путь, где НАСТОЯЩАЯ live-печать имеет `judge_mode` иной формы (регистр/строка) и ошибочно трактуется как НЕ-shadow → энфорсится, хотя должна; или наоборот.
- **SE-2 ↔ escape/floor:** не конфликтует ли shadow-проверка с `escapeGrantOpen` (G-1 α, идёт раньше) и с полом (floorBlocked перевешивает).
- **SE-2 указатель:** shadow→разговорный возвращает allow для observe БЕЗ advanceTo — не сдвигается ли указатель шага ошибочно.
- **Класс «текст контроллера ≠ факт»:** не печатает ли что-то по строке контроллера вместо реального вердикта (Гейт-1 переиспользует существующий анти-инъекционный слой судьи).

---

## 7. Разбор #1 — sharp-edges → variant-analysis → systematic-debugging

| # | Severity | Находка | Закрытие в дизайне |
|---|---|---|---|
| **SD-1** | **High** | **Trim-расхождение → артефакт НИКОГДА не печатается.** `runJudgeGate` (Task 5) считал `judged_hash` от `g.product` = `String(content).trim()` (как `extractGate2Product:112`), а `sealOnWiredGo` берёт сырой `content` для `sealArtifact`. `buildArtifact.source_sha = sha256(content)` чувствителен к байтам → trimmed ≠ untrimmed → `sealOnApproval` TOCTOU-отвергает → тихий тотальный отказ Гейт-1. (Планы избегали случайно — `sealablePlan`={steps} к trim нечувствителен.) | `judged_hash` в `runJudgeGate` считается от **СЫРОГО** `event.tool_input.content` (того же, что печать), не от `g.product`. Один канонический вид судья→печать. Регрессионный тест на trimmed≠raw. §3 + §5. |
| **VA-a** | **Med** | **SE-2 как «явный shadow → блок» — единственная fail-OPEN сверка** против конвенции кодовой базы (`isRealGo`/`finalGate`/`sealOnWiredGo` все fail-CLOSED на не-совпадении). else-ветка = энфорсмент = доверие мнимой печати. | SE-2 переписан на **fail-closed whitelist**: энфорсмент ТОЛЬКО `judge_mode==='live-block'` на ОБЕИХ печатях; всё иное → разговорный. Приводит SE-2 к конвенции. §4. |
| **SE-a** | **Low** | **`judge_mode` — строковое точное сравнение** (footgun): опечатка/неожиданное значение (`'Shadow'`) → не `'shadow'` → ошибочный энфорсмент. Значения системные (`judgeGateMode()`), поэтому Low. | Закрыт тем же whitelist (любое ≠ `'live-block'` → разговорный, safe). Опционально: валидация `judge_mode ∈ {null,shadow,live-block}` при печати (defense-in-depth, YAGNI-кандидат). §4. |
| SE-b | info | Спека Edited (не Write) после печати → не пере-судится → артефакт устаревает. | By-design (Write-only Δ-A, как Гейт-2; пере-печать = полный Write). Задокументировано. |
| SD-3 | info | live-block spec-Write + NO-GO → запись спеки блокируется; риск заклинить цикл правки спеки (как readonly-Bash клинил per-tool судью). | Backstop: escape М6 (дверь владельца) + рекомендация обкатки в shadow перед live-block. Судья бьёт только на Write полной спеки. |
| SD-4 | info | Спека без `{#якорей}` → `sections={}` → закрытая дверь блокирует все ref'ы. | Fail-closed (нет якорей → ref не резолвится → block). Требование к спеке (P2-2): якоря обязательны для работы двери. |
| SD-2/SD-7 | проверено | Порядок escape > SE-2 > floor — безопасен (escape всегда выше, shadow блокирует до энфорсмента, floor внутри `decide`). mode-mismatch план/артефакт → whitelist fail-closed. | Дыр нет. |

## 8. Разбор #2 — независимый проход (новые классы)

Методика R7: сперва абстрактная классификация классов (межхуковая конкуренция / переходы режима / стоимость-DoS / семантический зазор «судимое vs печатаемое» / инъекция в спеку / само-модификация / singleton), затем перечисление. **Новых структурных/HIGH-дыр сверх разбора #1 (SD-1/VA-a) не найдено**; найденное — операционное (fail-closed) либо унаследованная защита.

| # | Класс | Вывод / закрытие |
|---|---|---|
| P2-1 | **Переход режима судьи mid-session** (shadow→live-block) | Печати несут режим НА МОМЕНТ печати. После flip в live-block старые shadow-печати по whitelist → разговорный (не энфорсятся) → контроллер обязан пере-печатать (пере-Write спеки+плана) в live-block. **Fail-closed/безопасно**, операционная заметка (порядок flip: обкатка-shadow ДО регистрации стены). |
| P2-2 | **Инъекция в content спеки** (контроллер хочет GO) | Унаследована анти-инъекция Гейт-2: `runJudge` выводит decision МЕХАНИЧЕСКИ из слотов/возражений (не из поля `decision` модели), `parseJudgeResponse` fail-closed, продукт идёт как `canonicalJson` в user-блоке. Гейт-1 использует тот же движок. **Новой дыры нет.** |
| P2-3 | **Singleton артефакта** (последняя запись `specs/*.md` побеждает) | Контроллер обязан писать спеку-РЕШЕНИЕ последней перед её планом (иначе план привяжется к чужому артефакту → ref не резолвится → block). Fail-closed. Как sealed-plan SD-4 (одно активное решение на сессию). |
| P2-4 | **shadow-обкатка при зарегистрированной стене** | По whitelist+выбору владельца → всё разговорное (мутаторы block). Порядок flip: обкатка судьи (shadow) ДО регистрации стены; live-block — потом. Консистентно. |
| P2-5 | **Межхуковая гонка** (judge-gate пишет печать, стена читает) | Записи на РАЗНЫХ событиях (spec-event печатает артефакт атомарно ДО plan-event); `loadFrozenArtifact` на plan-event видит персистнутое. Последовательно+атомарно (Task 3 temp→rename, VA-3). Гонки нет. |
| P2-6 | **Семантический зазор судимое-vs-печатаемое** | Судья судит ПОЛНЫЙ raw-content спеки; печать = `{sections (по якорям), source_sha (sha256 полного content)}`; `judged_hash` связывает оба → судья одобрил целое, стена резолвит ref→якорь (подмножество). Зазора нет; спрятать в не-якорной прозе нельзя (судья видит всё + source_sha). |
| P2-7 | **gate2 judged_hash смена на raw** (побочка SD-1) | Смена источника `judged_hash` g.product→raw для gate2 безопасна: тесты Task 5 не проверяют значение `judged_hash`; `sealPlan` уже использует raw; для планов trim и так не влиял. Консистентно. |

**Acceptable residual:** P2-1/P2-3/P2-4 — операционные заметки (порядок flip + дисциплина «решение последним»), закрыты fail-closed-поведением + runbook'ом флипа, не код-задачи здесь.

---

## 9. Статус и следующие шаги

- [x] Дизайн сошёлся (brainstorming, решения владельца §2).
- [x] `audit-context-building` — runJudgeGate / decideMode / judge-engine gate1 прочитаны построчно; SE-2 backward-compat (явный shadow) найден.
- [x] **Разбор #1** по спеке (sharp-edges → variant-analysis → systematic-debugging): SD-1 (High, trim→артефакт не печатается), VA-a (Med, SE-2 fail-open→whitelist), SE-a (Low), SE-b/SD-3/SD-4 (info) — §7, все закрыты.
- [x] **Разбор #2** по спеке (новые классы): P2-1..P2-7 — новых структурных дыр нет, операционные заметки fail-closed — §8.
- [x] Spec self-review (плейсхолдеры закрыты §7/§8 / §3↔§5↔§7 и §4↔§7 консистентны / scope сфокусирован / неоднозначностей нет).
- [x] **Ревью владельца** — ✅ ПРИНЯТО 2026-06-09.
- [x] `writing-plans` → план `docs/superpowers/plans/2026-06-09-gate1-and-se2.md`.
- [x] **Реализация** (executing-plans, инлайн TDD): Task 1 `extractGate1Product` (7/7) → Task 2 routing + `judged_hash` от raw/SD-1 (52/52) → Task 3 SE-2 whitelist + миграция (82/82) → гейт закрытия sharp-edges/variant по коду + регрессия **3443 passed / 0 регрессий**. Гейт-1 тесты вынесены в `tools/enforce-judge-gate-gate1.test.mjs` (real-test-verifier блокирует import-only Edit). Коммиты — терминалом владельца (GitHub suspended). **Печать в рантайме до флипа НЕ производится** (стена/судья не зарегистрированы).

**Прод-код и стена НЕ затронуты** этой спекой (design-only). Оба хвоста — код-предусловие флипа; флип — после их сборки и тест-гейта.
