From 93fcb5e14174ba2e5f72cd8be45bd876ac8eccd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Tue, 9 Jun 2026 04:51:39 +0300 Subject: [PATCH] docs(router-mentor): R-08 hierarchical waves design spec --- ...er-mentor-r08-hierarchical-waves-design.md | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-09-router-mentor-r08-hierarchical-waves-design.md diff --git a/docs/superpowers/specs/2026-06-09-router-mentor-r08-hierarchical-waves-design.md b/docs/superpowers/specs/2026-06-09-router-mentor-r08-hierarchical-waves-design.md new file mode 100644 index 00000000..158dae1a --- /dev/null +++ b/docs/superpowers/specs/2026-06-09-router-mentor-r08-hierarchical-waves-design.md @@ -0,0 +1,139 @@ +# R-08 — Иерархические волны стены М2 (единый дерево-указатель) — Design + +**Дата:** 2026-06-09 · **Кодовая фраза эпика:** «роутер-наставник». +**Ветка:** `worktree-brainrepo` · commit-not-push. +**Статус:** дизайн утверждён владельцем (две ключевые развилки + понимание задачи). Следующий +шаг — adversarial-разбор (`audit-context-building → sharp-edges → variant-analysis`), затем +`writing-plans`. Это **самая рискованная M2-работа** эпика (меняет формат подписанного +плана + step-модель стены) → adversarial-разбор обязателен ДО плана. + +--- + +## 1. Проблема и цель + +**Сегодня** стена М2 (`enforce-supreme-gate`) пропускает мутирующее действие только если оно +совпадает со **следующим шагом** плоского запечатанного плана (`actionMatchesStep(step, action)`, +`step = nextStep(steps, ptr)`, на allow → `ptr+1`). Указатель — целое число, подписанное +(`signStepState`, R-19) и привязанное к `plan_id` (R-27). + +**Трение:** если посреди исполнения крупный шаг нужно раздробить на под-шаги, сделать это «на +месте» нельзя — только вернуться в разговор и **перепечатать весь план** (closed-door C-5). + +**Цель R-08:** разрешить плану быть **иерархическим** — крупный шаг несёт заранее утверждённые +`substeps`. Стена при достижении контейнера автоматически спускается в под-план, матчит листья, +выходит. Снимает трение без перепечати всего плана. + +## 2. Инвариант безопасности (ядро) + +**Под-шаги берутся ТОЛЬКО из подписанного плана** (решение владельца). Крупный шаг и все его +под-шаги пишутся и запечатываются **вместе одной HMAC-печатью** до начала работы. `freezePlan` +уже канонизирует весь массив шагов целиком (`canonicalJson`) — вложенные `substeps` попадают под +ту же подпись автоматически. **Нет ни одного пути кода, где текущий лист-шаг приходит не из +запечатанного дерева** → контроллер физически не может подсунуть неподписанный под-шаг. +Инвариант «действие = подписанный шаг» сохранён. + +**Отвергнуто (DANGEROUS):** контроллер выдумывает под-шаги на ходу → стена бесполезна (любое +действие оправдывается выдуманным под-шагом). Этого пути в дизайне нет. + +## 3. Модель контейнеров + +Шаг с `substeps` — **чистый контейнер-группа**: сам действие НЕ матчит. Указатель автоматически +спускается в его под-план; матчат только **листья** (шаги без `substeps`). Закрытая дверь +(`ref`, C-5) и Δ7-проверка разрушительного применяются на **листе**. Контейнер может нести `ref`? +— нет (YAGNI): `ref`/`op`/`object` осмысленны только на листе; у контейнера значимо лишь +`substeps` (+ необязательная метка `n`/`title` для читаемости). Контейнер без `substeps` или с +пустым `substeps` = брак дерева → fail-CLOSED (block). + +## 4. Единый дерево-указатель (выбор владельца) + +Целочисленный указатель заменяется **деревом-указателем для всего** (`step-pointer.mjs`: стек +`[{index, length}]`). Плоский план = дерево глубины 1 → указатель ведёт себя байт-в-байт как +целочисленный (корневой уровень, advance = +1). + +**Компоненты:** + +### 4.1 `step-pointer.mjs` (есть createPointer/advance/enterSubPlan/exitSubPlan/isDone) +- `+ serializePointer(ptr)` / `deserializePointer(state)` — стек ↔ компактная форма для подписи. +- `+ advanceOverTree(steps, ptr)` — **детерминированный** один ход по дереву: на листе → `advance`; + если следующая позиция — контейнер → `enterSubPlan(length=substeps.length)`; если уровень + исчерпан → `exitSubPlan` и повтор на родителе. Структура хода берётся ИЗ дерева — контроллер + на неё не влияет. Защита от бесконечного спуска/подъёма: жёсткий предел глубины (напр. 8) + + предел итераций; превышение → throw (fail-CLOSED в обёртке стены). + +### 4.2 `plan-lock.mjs` +- `withCriterionIds` рекурсит в `substeps` → `criterion_id` запечатан на всех уровнях. +- `+ treeLeafAt(steps, ptr)` — текущий лист по указателю (спуск через контейнеры по `currentPath`). + Указатель не резолвится в лист (за концом / битая структура) → `null`. +- `freezePlan` / `verifyFrozenPlan` — по сути не меняются (печать уже покрывает дерево); добавить + только рекурсию `criterion_id`. `planId` уже = sha256 канонизированного дерева. +- `+ validatePlanTree(steps)` — структурная валидация ДО доверия: `substeps` либо отсутствует, + либо непустой массив; нет контейнера без листьев; глубина ≤ предела; нет «контейнер с op/object, + претендующих на матч». Невалидно → стена block (fail-CLOSED). (Чистая, тестируемая.) + +### 4.3 `enforce-supreme-gate.mjs` +- `signStepState`/`verifyStepState`/`resolveStepPtr` переводятся с целого `ptr` на + **сериализованный стек** (по-прежнему подписан R-19 + привязан `plan_id` R-27). Подмена любого + уровня стека / чужой план / битая подпись / легаси-целое → **сброс к корню** (как сегодня + `resolveStepPtr` на mismatch). Корневой указатель = `createPointer({length: steps.length})`. +- `decide`/`decideMode`/`runGate`: берут лист через `treeLeafAt(frozenPlan.steps, ptr)` вместо + `nextStep`; `advanceTo` = сериализованный `advanceOverTree(...)` вместо `ptr+1`. Лист `null` → + block («план исчерпан / указатель не резолвится»). `validatePlanTree` неуспех → block. +- `actionMatchesStep(leaf, action)` — **не меняется** (матч листа идентичен сегодняшнему). +- Δ7 (разрушительное in-plan), закрытая дверь C-5 (`ref` листа), артефакт-версия, escape G-1 α — + **не трогаются**, применяются на листе. + +## 5. Обратная совместимость (нулевая регрессия — жёсткое требование) + +- Плоский план (без `substeps`) обязан вести себя **идентично** сегодняшнему. Все существующие + тесты `enforce-supreme-gate.test.mjs` / `plan-lock.test.mjs` / `step-pointer.test.mjs` остаются + **GREEN** без правок (regression-guard в плане). +- Легаси-состояние указателя (старое целое в `plan-step-`) под новый/любой план → + `resolveStepPtr` возвращает свежий корневой указатель (старая сессия стартует заново — то же + поведение, что сегодня на plan_id-mismatch). Никакой миграции формата на диске не требуется. + +## 6. Fail-CLOSED (постура как сейчас) + +- Битое дерево (циклы, `substeps` не массив, превышение глубины, контейнер без листьев) → block. +- Указатель не резолвится в лист → block. +- Сетап-исключение → panic-ветка (G-1 α escape оценён, иначе block) — без изменений. +- Аварийный выход владельца (escape) и Δ7 — без изменений. + +## 7. Adversarial-разбор (что обязаны прожарить следующие скилы ДО плана) + +Список классов-дыр для `audit-context-building → sharp-edges → variant-analysis`: + +1. **Неподписанный под-шаг через любой путь.** Доказать: ВСЕ пути `decide`/`decideMode`/`runGate` + берут лист только из `frozenPlan.steps` (под печатью). Нет ветки, где лист/под-шаг приходит из + события, env, файла указателя, или вычисляется контроллером. +2. **Подмена позиции указателя.** Сериализованный стек подписан; подмена уровня → сброс к корню. + Проверить: нельзя «перескочить» вглубь к разрушительному листу мимо родительских проверок. +3. **`advanceOverTree` недетерминированность / влияние контроллера.** Ход полностью функция от + (дерево, текущая позиция). Никаких входов от события. Доказать терминируемость (предел глубины + + итераций), отсутствие зацикливания спуск↔подъём. +4. **Битое/враждебное дерево.** Цикл (`substeps` ссылается на предка — невозможно в JSON-дереве, + но проверить копии/глубину), гигантская глубина (DoS), контейнер с `op/object` (попытка + сделать контейнер матчабельным). Всё → fail-CLOSED. +5. **Регрессия плоского пути.** Доказать байт-в-байт идентичность для depth-1. +6. **Закрытая дверь / Δ7 / артефакт на листе.** Убедиться, что C-5/Δ7/artifact_id проверки + срабатывают на листе так же, как сегодня на плоском шаге (не обойдены спуском). +7. **Легаси-состояние.** Старое целое `ptr` под дерево-план → сброс, не мис-резолв вглубь. + +## 8. Тестирование (TDD, наблюдаемый RED до prod-правки) + +`step-pointer`: serialize/deserialize round-trip · advanceOverTree спуск-в-контейнер · +авто-подъём при исчерпании · +1 на листе · предел глубины→throw · depth-N обход целиком. +`plan-lock`: treeLeafAt (лист / контейнер-спуск / за концом→null) · withCriterionIds рекурсия · +validatePlanTree (валидное / контейнер-без-листьев / substeps-не-массив / глубина) · +freezePlan покрывает substeps подписью (правка под-шага ломает sig). +`enforce-supreme-gate`: **плоский план идентичен** (регрессия) · дерево: матч листа вглубь · +контейнер-не-матчит-действие · подмена стека→сброс к корню · битое дерево→block · легаси-целое→ +сброс · Δ7/ref/artifact на листе · escape не тронут. +Регрессия tools-only ≥ baseline (на момент старта плана). + +## 9. Границы (жёсткое) + +- Трогаем ТОЛЬКО `step-pointer.mjs` / `plan-lock.mjs` / `enforce-supreme-gate.mjs` (+ их тесты). +- Стену М2 **усиливаем под волны, не ослабляя**: инвариант §2 сохранён, fail-CLOSED §6 сохранён, + пол М5 / судья М4 / escape М6 не трогаются. +- Живьём стена заработает только после Фазы 8 (рубильник владельца) — R-08 готовит возможность. +- commit-not-push; НЕ `requesting-code-review` (жёсткое правило сессии).