# Дизайн: связка печати (sealed-plan production) — М7 Фаза 8, код-предусловие

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

---

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

Сухой прогон по коду (09.06.2026) вскрыл: **печать плана в рантайме не производится ни одним запускаемым хуком.** Канал печати [judge-seal-channel.mjs](../../../tools/judge-seal-channel.mjs) построен, но хук судьи [enforce-judge-gate.mjs:173](../../../tools/enforce-judge-gate.mjs#L173) (`runJudgeTurn`) на GO только **логирует/блокирует — печать не ставит**. Структурные шаги `{op, object}` из markdown-плана в рантайме никто не извлекает. Стена М2 ([enforce-supreme-gate.mjs:134](../../../tools/enforce-supreme-gate.mjs#L134)) читает `frozen-plan-<session>.json` / `frozen-artifact-<session>.json`, но **их некому записать**.

**Следствие:** регистрация стены М2 без этой связки = блок **любой** правки (опечатанного плана не появится → default-deny, пройдут только seed/observe-only). Поэтому связка печати — **код-предусловие** Фазы 8 (флипа): без неё стену включать нельзя.

Эта спека закрывает производящий путь: **как одобренное решение и план становятся опечатанными артефактом+планом, которые стена матчит.**

---

## 2. Решения владельца (зафиксированы в брейнсторме 09.06.2026)

1. **Отклонение = полный пересмотр плана.** Правка вне опечатанного плана → стена блок → план **переписывается целиком** → судья судит весь план заново → новая печать плана. Роутер задачи (М3) **не** пере-запускается (задача та же). Изменилось само решение → пере-печатывается и артефакт.
2. **Две раздельные печати.** Печать №1 = РЕШЕНИЕ (артефакт, спека). Печать №2 = ПЛАН (шаги), привязан к версии артефакта (`artifact_id`). «Договорились ЧТО» отдельно от «договорились О ШАГАХ».
3. **Closed-door в v1.** Шаги плана ссылаются (`ref`) на разделы решения; стена требует резолюцию ссылки в опечатанном артефакте ([enforce-supreme-gate.mjs:159](../../../tools/enforce-supreme-gate.mjs#L159)).
4. **Per-tool надзор судьи А2 — НЕ в v1.** Судья судит изменение решения/плана, не каждую правку.

---

## 3. Модель: две печати

```
Разговор/брейнсторм
   │  (judge GO на записи spec)
   ▼
Печать №1: frozen-artifact   ← { sections: {<ref>: ...}, artifact_id, frozen_at, sig }
   │
План со встроенными шагами (каждый шаг → {op, object, ref}); печать ШТАМПУЕТ artifact_id текущего артефакта (Claude его не пишет — SD-3)
   │  (judge GO на записи плана)
   ▼
Печать №2: frozen-plan       ← { plan_id, artifact_id, steps:[{op,object,ref,criterion_id}], sig }
   │
Реализация: стена М2 на КАЖДУЮ правку:
   обе печати есть? версии совпадают? шаг матчит? ref резолвится в раздел артефакта? → allow, иначе block
   │
Отклонение → блок → план целиком переписан → judge заново → новая печать №2
Решение изменилось → judge заново на spec → новая печать №1 → план обязан пере-печататься под новую версию
```

---

## 4. Форматы данных

### 4.1 Артефакт (решение)

Выводится из опечатываемой спеки/решения. Форма (вход `freezeArtifact`, [plan-lock.mjs:171](../../../tools/plan-lock.mjs#L171)):

```
{ sections: { "<ref-1>": <непустое>, "<ref-2>": <непустое>, ... } }
```

`sections` — карта `ref → содержимое` разделов решения. `ref` — стабильный якорь раздела (`{#id}`, см. §4.2 + P2-2; НЕ слаг заголовка). После `freezeArtifact`: +`artifact_id` (sha256 канон-содержания), +`frozen_at`, +`sig` (домен `FROZEN_ARTIFACT`). `refResolves` ([plan-lock.mjs:201](../../../tools/plan-lock.mjs#L201)) проверяет наличие ключа в `sections`.

### 4.2 Структурные шаги плана

Встроенный машиночитаемый блок в `docs/superpowers/plans/*.md` (детерминированный парсер, без LLM):

```
{ op: "Edit"|"Write"|"Bash"|"Skill"|..., object: "<путь|команда|skill>", ref: "<ref раздела артефакта>" }
```

`op`/`object` матчатся стеной через `actionMatchesStep` ([plan-lock.mjs:72](../../../tools/plan-lock.mjs#L72)) (op И object точно; файлы через `pathNormalize`, Bash через `normCommand`). `ref` обязателен в v1 (closed-door); `ref` = **явный стабильный якорь** раздела решения (конвенция `{#id}` в спеке, P2-2 — НЕ слаг заголовка: правка прозы заголовка не должна ломать ключ). Парсер C1 строит `sections` по якорям, парсер C2 требует, чтобы `ref` шага совпал с ключом-якорем из `sections`. `criterion_id` проставляется автоматически при печати (`withCriterionIds`, [plan-lock.mjs:34](../../../tools/plan-lock.mjs#L34)).

**Источник блока (решение дизайна):** Claude перечисляет шаги во встроенном блоке плана. Детерминированно (Claude обязуется ровно к тому, что тронет); прозу не парсим (принцип «не угадывать», как `actionMatchesStep`).

### 4.3 Печати (seals)

- `frozen-artifact-<session>.json`: `{ sections, artifact_id, frozen_at, sig }`. Персист `saveFrozenArtifact` ([plan-lock.mjs:188](../../../tools/plan-lock.mjs#L188)).
- `frozen-plan-<session>.json`: `{ plan_id, artifact_id, frozen_at, steps, sig }`. Персист `saveFrozenPlan` ([plan-lock.mjs:147](../../../tools/plan-lock.mjs#L147)).
- Оба ключом `router-mentor-receipts` (keychain, Claude'у недоступен; `resolveReceiptKey`).

---

## 5. Компоненты и где они живут

| # | Компонент | Где | Что делает |
|---|---|---|---|
| C1 | **spec-парсер артефакта** | новый `tools/artifact-from-spec.mjs` | markdown спеки `## ...` → `{ sections: {ref: content} }` детерминированно |
| C2 | **plan-парсер шагов** | новый `tools/plan-steps-parse.mjs` | встроенный блок плана → `[{op,object,ref}]`; валидация формы (fail-CLOSE на брак) |
| C3 | **триггер артефакта** | правка `enforce-judge-gate.mjs` | детект записи `specs/*.md` (новый `SPEC_PATH_RE`) → продукт на суд; на **реальном GO** → seal-канал → `freezeArtifact` → `saveFrozenArtifact` |
| C4 | **триггер плана** | правка `enforce-judge-gate.mjs` | существующий Гейт-2 (`PLAN_PATH_RE`) + на **реальном GO** → парс шагов (C2) + `artifact_id` из текущего артефакта → `freezePlan` → `saveFrozenPlan` |
| C5 | **judged_hash в вердикте** | правка `runJudgeGate`/seal-оркестрация | при суде считать `judged_hash = contentHash(продукт)` → seal-канал сверяет TOCTOU ([judge-seal-channel.mjs:59](../../../tools/judge-seal-channel.mjs#L59)) |
| C6 | **seal-оркестрация** | новый `tools/seal-orchestration.mjs` (или в judge-gate) | `sealOnApproval` ([judge-seal-channel.mjs:48](../../../tools/judge-seal-channel.mjs#L48)) с injected `freezeImpl` = `freezeArtifact`/`freezePlan`; персист Node-fs |

**Печать ставит только хук судьи** на реальном GO. Путь «контроллер сам зовёт freeze» не строится (ключ недоступен). Персист — Node-fs (не Write-инструментом → `runtime-write-deny` не мешает, как `journalAppend`/`logVerdictLine`).

---

## 6. Поток печати (когда именно ставится)

Печать привязана к **реальному** вердикту судьи, отдельно от решения блокировать:

- **inert** (судья выключен): суда нет → GO нет → печати нет.
- **shadow** (судит, логирует, не блокирует): на GO **печать ставится** (видно на доске, стена при этом может быть не зарегистрирована → печать наблюдаема, но не энфорсится — идеально для обкатки).
- **live-block**: GO → печать + allow; NO-GO → блок, печати нет.
- **degraded/neutral GO** (`wired:false` — нет ключа/транспорт упал): **печать НЕ ставится** (это не настоящее одобрение).

Carve-out §6 ([judge-seal-channel.mjs:33](../../../tools/judge-seal-channel.mjs#L33)): деньги/тупик → печать ставит **владелец**, не судья.

---

## 7. Closed-door v1

- Артефакт несёт `sections` (карта `ref → раздел`). `ref` шага плана ОБЯЗАН резолвиться в `sections` опечатанного артефакта ([enforce-supreme-gate.mjs:159](../../../tools/enforce-supreme-gate.mjs#L159)).
- Шаг с `ref`, но без опечатанного артефакта / без раздела → стена блок («вернись в разговор, пере-печатай»).
- Парсер C1 строит `sections`; парсер C2 требует `ref` в каждом шаге (fail-CLOSE на шаг без `ref` в v1).

---

## 8. Поток отклонения (решение №1 владельца)

1. Правка не матчит шаг → стена блок с диагностикой («ожидался шаг N: …»).
2. Claude **переписывает план как ОСТАТОК работы** от точки отклонения (новый встроенный блок шагов; уже сделанное не перечисляется — SD-2, иначе стена ждёт повтора при сбросе указателя в 0).
3. Запись плана → судья судит **весь** план заново (Гейт-2) → на GO **новая печать №2** (новый `plan_id`, тот же `artifact_id`, если решение не менялось).
4. Указатель шага сбрасывается на новый `plan_id` (`resolveStepPtr` R-27, [enforce-supreme-gate.mjs:83](../../../tools/enforce-supreme-gate.mjs#L83)).
5. Если менялось **решение** → сперва пере-печать артефакта (новый `artifact_id`) → план обязан пере-печататься под новую версию (стена требует совпадения версий).

Роутер задачи (М3) не пере-запускается (задача та же — подтверждено владельцем).

---

## 9. Вне scope v1 (YAGNI)

- **Per-tool надзор судьи А2** на каждую правку — НЕ строим (судья на изменении решения/плана).
- **Авто-извлечение шагов из прозы** — НЕ строим (только встроенный блок).
- **Мульти-артефактные планы** (план, опирающийся на >1 решение) — один `artifact_id` на план.
- **Owner-seal UI** сверх существующего carve-out — используем существующий `requiresOwnerSeal`.

---

## 10. Интеграция с существующим кодом (anchors)

- Не трогаем: движок судьи (`judge-engine`/`judge-orchestrator`), пол М5, escape М6, стена decide/decideMode (только **читают** печати — они уже готовы их принять).
- Правим: `enforce-judge-gate.mjs` (триггеры C3/C4/C5/C6 в `runJudgeTurn` на реальном GO).
- Добавляем: `artifact-from-spec.mjs` (C1), `plan-steps-parse.mjs` (C2), seal-оркестрация (C6).
- Переиспользуем: `freezeArtifact`/`freezePlan`/`saveFrozen*` (plan-lock), `sealOnApproval`/`contentHash` (seal-channel), `resolveReceiptKey` (keychain).

---

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

- C1: спека `## A`/`## B` → `sections` с ключами `A`/`B`; пустой раздел отброшен (fail-CLOSE).
- C2: валидный блок → шаги; шаг без `op`/`object`/`ref` → парсер fail-CLOSE (не «пустой джокер», ср. F5 [plan-lock.mjs:77](../../../tools/plan-lock.mjs#L77)).
- C3: запись `specs/*.md` + реальный GO → `frozen-artifact` записан, печать валидна (`verifyFrozenArtifact`).
- C4: запись `plans/*.md` + реальный GO → `frozen-plan` записан, `artifact_id` = текущего артефакта.
- C5: подмена продукта после суда (TOCTOU) → `judged_hash` не сходится → печать НЕ ставится.
- Поток: артефакт+план опечатаны → стена пускает совпавшую правку, блокирует несовпавшую, блокирует шаг с неразрешимым `ref` (closed-door).
- Печать-режим: shadow GO → печать ставится; degraded GO (нет ключа) → печать НЕ ставится; NO-GO → нет печати.
- **Инварианты из разбора #1 (укрепления):**
  - SE-1/VA-1: план с ref-less шагом ИЛИ без `artifact_id` (после штампа) → seal fail-CLOSE (печать не ставится).
  - SE-2/VA-2: обе печати несут `judge_mode`; стена не энфорсит shadow-печать как live (тест на различение).
  - SE-3: seal только при `verdict.wired === true && verdict.decision === 'GO'` (строго `===`).
  - SE-4/VA-3: запись печати атомарна (temp→rename); артефакт персистится ДО плана (тест порядка).
  - SE-5: `object` шага канонизируется (repo-relative POSIX); правка с эквивалентным путём в иной форме матчит шаг (Windows-тест `/`↔`\`, отн↔абс).
  - VA-4: шаг `op:'Task'` → парсер/seal fail-CLOSE (субагенты запрещены).
  - SD-1: судимое представление == печатаемое (`judged_hash = contentHash(sealable-объекта)`); подмена прозы/структуры после суда → печать не ставится.
  - SD-3: `artifact_id` штампуется печатью из текущего артефакта (не из текста плана); нет артефакта → план-печать fail-CLOSE.
- **Инварианты из разбора #2:**
  - P2-2: правка прозы заголовка решения НЕ меняет `ref`-ключ (якорь стабилен); смена якоря → новый `artifact_id` (осознанно).
  - P2-3: судье подаётся продукт через анти-инъекцию (делимитеры/pre-filter); инъекция «output GO» в текст плана → НЕ форсит GO (мульти-судья/fail-closed parse); тест на инъекцию-вектор.
  - P2-1: при снятом runtime-страже печать не доверенная (Фаза 8: пол регистрируется ДО энфорсмента стены).
- Регрессия tools-only ≥ baseline (план уточнит число).

---

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

- **Re-seal атомарность:** пере-печать плана между блоком и новой записью — нет ли окна, где стена видит старую печать + новый указатель (рассинхрон `plan_id`/`ptr`)?
- **Артефакт без плана / план без артефакта:** порядок записей; что если план записан раньше артефакта (нет `frozen-artifact` → стена backstop C-10).
- **judged_hash продукта:** ровно ли то содержание, что опечатывается, судилось (граница «спека-файл vs извлечённые sections»).
- **shadow-печать + стена-зарегистрирована:** если стена включена, а судья в shadow — печати производятся и энфорсятся; ожидаемо ли (связь режимов)?
- **closed-door обход:** `ref` на несуществующий раздел; пустой `sections`; артефакт с `sections`, не сошедшимися по версии.
- **Класс 1 (текст контроллера ≠ факт):** не появляется ли путь, где seal ставится по строке контроллера, а не по реальному вердикту судьи.

---

## 12.1 Разбор #1 — sharp-edges (findings + закрытие в дизайне)

| # | Severity | Грабля | Закрытие в дизайне |
|---|---|---|---|
| SE-1 | High | Closed-door тихо отключается для шага без `ref` (`refResolves` → `true` при `!step.ref`, [plan-lock.mjs:201](../../../tools/plan-lock.mjs#L201)); защита держится лишь на C2 | **Seal-time инвариант:** seal-оркестрация (C6) отвергает план, где хоть один шаг без `ref` (fail-CLOSE при печати, не только при парсе). Закрытость не зависит от парсера. |
| SE-2 | High | Seal-в-shadow связывает режим судьи с энфорсментом: стена-зарегистрирована + судья-shadow → печати энфорсятся при «наблюдающем» судье | **Печать несёт `judge_mode`** (shadow/live-block). Стена (decideMode) НЕ чтит shadow-печать как одобрение к энфорсменту: shadow-seal допустим для наблюдения, но enforcement требует live-block-печати ИЛИ явного owner-флага «обкатка стены». Доска показывает «стена энфорсит shadow-печати», если так настроено. |
| SE-3 | Med | Печать на «truthy» вердикт вместо строгого GO | **Инвариант:** seal только при `verdict.wired === true && verdict.decision === 'GO'` (строго `===`, как N5). Degraded/neutral GO (`wired:false`) → нет печати (уже §6). |
| SE-4 | Med | Персист печати не атомарен (`writeFileSync`) → битый файл кирпичит до пере-печати | **Атомарная запись:** temp-файл → `rename` для `frozen-artifact`/`frozen-plan` (как прочие state-записи эпика). |
| SE-5 | High (Windows) | `object` шага — строка; рассинхрон формы пути (отн/абс, `/`↔`\`, квирк cwd) → стена блокирует легитимную правку | **Канон формы `object`:** repo-relative POSIX. `pathNormalize` приводит и авторскую (план), и событийную (tool) форму к канону ДО матча. C2 валидирует/канонизирует `object` при парсе. (Иначе система неюзабельна на Windows.) |
| SE-6 | info | «Печать плана = благословение его команд» — ложное впечатление | **Явно:** in-plan Bash/PowerShell-шаг остаётся под content-floor М5 (rule 8, Δ7) независимо от печати — seal НЕ отмывает опасную команду мимо пола. |

## 12.2 Разбор #1 — variant-analysis (близнецы найденных классов)

| # | Severity | Близнец (класс) | Закрытие |
|---|---|---|---|
| VA-1 | High | Класс SE-1 («требование снято при отсутствии поля»): план без `artifact_id` → version-binding пропущен (`if (frozenPlan.artifact_id)`, [enforce-supreme-gate.mjs:147](../../../tools/enforce-supreme-gate.mjs#L147)), closed-door резолвит ref против **любой** версии артефакта | **Seal-инвариант:** план без `artifact_id` не печатается (v1 каждый план привязан к версии артефакта). |
| VA-2 | Med | Класс SE-2 («mode не записан»): `judge_mode` нужен на **обеих** печатях | freezeArtifact и freezePlan обе несут `judge_mode`. |
| VA-3 | Med | Класс SE-4 («не-атомарно/порядок»): план записан раньше артефакта → ссылка на неперсистнутый артефакт | **Порядок:** артефакт персистится ДО плана; обе записи атомарны (temp→rename). |
| VA-4 | High | Класс «in-plan отмывает запрещённое»: шаг `op:'Task'` (субагент) | C2/seal **отвергают** `op:'Task'` (субагенты запрещены владельцем) — иначе Task-шаг легализует запрещённого субагента как «в плане». |

## 12.3 Разбор #1 — systematic-debugging (логические дыры потока)

| # | Severity | Дыра (корень) | Закрытие в дизайне |
|---|---|---|---|
| SD-1 | High | judged_hash несогласован: судья судит одно представление (прозу), печать хеширует другое (`{sections}`) → `judged_hash !== contentHash` всегда → артефакт никогда не печатается → стена без артефакта блокирует всё | **Одно каноническое «печатаемое»:** определяем sealable-объект (артефакт `{sections, source}` / план `{steps}`); судья получает ЕГО как продукт, `judged_hash = contentHash(того же объекта)`, `sealOnApproval` сверяет тот же. Одно представление течёт судья→печать. |
| SD-3 | High | Claude не может авторить `artifact_id`: id в runtime-печати, read-path-deny запрещает Claude читать runtime, node закрыт | **Печать штампует** `frozenPlan.artifact_id = currentFrozenArtifact.artifact_id` на печати плана (хук имеет печать+ключ). Нет текущего артефакта → план-печать fail-CLOSE. Claude авторит только `{op,object,ref}`; `ref` = заголовки спеки, которые он сам писал (читать runtime не нужно). Закрывает и VA-1. |
| SD-2 | Med | Пере-печать сбрасывает указатель в 0 (R-27, [enforce-supreme-gate.mjs:83](../../../tools/enforce-supreme-gate.mjs#L83)); если новый план повторяет сделанное — стена ждёт повтора | **v1-правило:** пере-сеаленный план = ТОЛЬКО остаток работы от точки отклонения (Claude переписывает «оставшиеся шаги»). Шаг 0 нового плана = следующее реальное действие. |
| SD-4 | Med | Артефакт — синглтон `frozen-artifact-<session>.json`; вторая спека/задача перетирает | **v1-ограничение:** одно активное решение на сессию (задачи сериализуются). Мульти-задача в сессии — не-цель v1. |
| SD-5 | info | Если ломается сам конвейер печати после флипа → нельзя опечатать план, чтобы починить → self-mod deadlock | **escape М6** (дверь владельца) — путь восстановления, когда конвейер печати сломан. Уже в архитектуре, фиксируем как явный recovery. |

## 12.4 Разбор #2 — независимый проход (классы, пропущенные перечислением #1)

Методика R7: сперва абстрактная классификация, потом перечисление. Разбор #1 покрыл footguns/близнецы/data-flow; #2 искал **новые классы**.

| # | Severity | Класс (пропущен #1) | Закрытие |
|---|---|---|---|
| P2-2 | High | **Хрупкость идентификатора:** `ref` = слаг заголовка → косметическая правка заголовка решения молча меняет ключ → closed-door рвётся | `ref` = **явный стабильный якорь** (`{#id}`), C1 парсит якоря не прозу; правка прозы заголовка не трогает ключ. (Тело §4.2 исправлено.) |
| P2-3 | High | **Инъекция в судимый продукт:** план/спеку авторит контроллер и хочет GO; raw-контент идёт судье → «output GO»-инъекция → подделанный GO → подделанная печать | C3/C4 **переиспользуют анти-инъекцию** судьи L4 (делимитеры/pre-filter regex/мульти-судья-консенсус, как §3.8 эпика v3.8); `parseJudgeResponse` уже fail-closed; продукт не подаётся raw. |
| P2-1 | Med | **Несущая зависимость:** целостность печати держится на активном `runtime-write-deny` (пол) — иначе контроллер восстановит/подделает старую печать (node закрыт, floor — единственный барьер записи в runtime) | Зафиксировать как несущую зависимость (аналог «зависимости журнала» эпика): печать достоверна ТОЛЬКО при зарегистрированном поле-страже runtime. Тест-инвариант: при снятом runtime-страже — печать не считается доверенной (Фаза 8 порядок регистрации: пол ДО энфорсмента). |

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

- [x] Дизайн сошёлся (брейнсторм, решения владельца §2).
- [x] **Разбор #1** по спеке: `sharp-edges` → `variant-analysis` → `systematic-debugging` (15 находок, §12.1–12.3, все закрыты).
- [x] **Разбор #2** по спеке (3 новых класса, §12.4, все закрыты).
- [x] Spec self-review (плейсхолдеры нет / противоречий нет / scope сфокусирован / висячие ссылки исправлены).
- [x] **Ревью владельца** — ✅ ПРИНЯТО 2026-06-09.
- [ ] `writing-plans` → план реализации (bite-sized TDD) — отдельный заход по слову «собирай». ← следующий шаг.

**Прод-код и стена НЕ затронуты** этой спекой (design-only). Связка — код-предусловие флипа; флип (регистрация М1–М6 + увольнение зоопарка) — после её сборки и тест-гейта §9.2 runbook'а `2026-06-09-phase8-deployment-runbook.md`.
