docs(seal): sealed-plan production design spec + TDD implementation plan
Спека (ОДОБРЕНА владельцем, 2 адверсариальных разбора, 18 находок закрыты) + bite-sized TDD-план (7 задач) для код-предусловия флипа Фазы 8 (производство двух печатей: артефакт-решение + план-шаги). design-only — прод-код и стена не затронуты. Эпик «роутер-наставник» М7.
This commit is contained in:
@@ -1974,3 +1974,39 @@ monitorится
|
||||
guillemets
|
||||
mirror'ящий
|
||||
plan'овский
|
||||
|
||||
# 2026-06-09 sealed-plan production — spec + plan terms
|
||||
sealable
|
||||
backstop
|
||||
энфорсмент
|
||||
энфорсмента
|
||||
энфорсментом
|
||||
энфорсятся
|
||||
энфорсит
|
||||
энфорсить
|
||||
энфорсменту
|
||||
неперсистнутый
|
||||
сеаленный
|
||||
делимитеры
|
||||
делимитер
|
||||
слаг
|
||||
плейсхолдеров
|
||||
резолвиться
|
||||
резолюцию
|
||||
когерентность
|
||||
когерентен
|
||||
распарсить
|
||||
провалидировать
|
||||
детерминированный
|
||||
детерминированную
|
||||
канонизирует
|
||||
канонизируется
|
||||
канонизировать
|
||||
хеш
|
||||
хеша
|
||||
хеширует
|
||||
хешировала
|
||||
хеширования
|
||||
хешируется
|
||||
штампует
|
||||
штампуется
|
||||
|
||||
+67
-60
@@ -1,22 +1,46 @@
|
||||
# Brain Status (auto-generated)
|
||||
|
||||
Last updated: 2026-05-30T13:11:39.164Z
|
||||
Last updated: 2026-06-09T11:24:48.158Z
|
||||
|
||||
| Контролёр | Состояние | Детали |
|
||||
|---|---|---|
|
||||
| C1 L1-watcher | ✅ | [l1-watcher] OK — 0 drift |
|
||||
| C2 Cross-ref consistency | ✅ | [cross-ref-checker] OK — 0 drift in 4 files |
|
||||
| C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 0 week(s) ago |
|
||||
| C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 1 week(s) ago |
|
||||
| C4 Сигнальный статус | ✅ | This file (self-reference) |
|
||||
| C5 Observer-coverage | ⚠️ | 752 episode(s) this month · Stop-hook + post-commit OK · 20 missed activation(s) — see /brain-retro |
|
||||
| C5 Observer-coverage | ✅ | 765 episode(s) this month · Stop-hook + post-commit OK |
|
||||
| C6 Chain map sync | ✅ | [chain-map-checker] OK — 16 chains in sync |
|
||||
|
||||
## Кто на посту (оборона М1–М6)
|
||||
|
||||
⚠️ **ПОСТ ПУСТОЙ** — не зарегистрированы: enforce-floor.mjs, enforce-supreme-gate.mjs, enforce-judge-gate.mjs, enforce-snapshot.mjs, enforce-floor-escape-consume.mjs, enforce-skill-journaler.mjs, enforce-verify-gate.mjs, enforce-criterion-gate.mjs (оборона НЕ подтверждена; SE-B/Δ8)
|
||||
|
||||
Судья М4: **inert** (inert $0 / shadow / floor-only / live-block)
|
||||
|
||||
| Машина / страж | Хук | Зарегистрирован |
|
||||
|---|---|---|
|
||||
| М1/М5 Нормативный страж (КАРТА/ЗАКОН) | `enforce-normative-content-rules.mjs` | ✅ |
|
||||
| М5 Read-exfil страж | `enforce-read-path-deny.mjs` | ✅ |
|
||||
| М5 Egress-exfil страж | `enforce-mcp-classification.mjs` | ✅ |
|
||||
| enforce-coverage-verify.mjs | `enforce-coverage-verify.mjs` | ✅ |
|
||||
| enforce-todowrite-skill-verifier.mjs | `enforce-todowrite-skill-verifier.mjs` | ✅ |
|
||||
| М5 Пол (вето-до-плана / content-floor) | `enforce-floor.mjs` | 🔴 |
|
||||
| М2 Стена (действие = шаг плана) | `enforce-supreme-gate.mjs` | 🔴 |
|
||||
| М4 Судья (приёмка + надзор) | `enforce-judge-gate.mjs` | 🔴 |
|
||||
| М6 Снимок (точка отката) | `enforce-snapshot.mjs` | 🔴 |
|
||||
| М6 Escape владельца (законная дверь) | `enforce-floor-escape-consume.mjs` | 🔴 |
|
||||
| М1 Журналер навыков | `enforce-skill-journaler.mjs` | 🔴 |
|
||||
| enforce-verify-gate.mjs | `enforce-verify-gate.mjs` | 🔴 |
|
||||
| enforce-criterion-gate.mjs | `enforce-criterion-gate.mjs` | 🔴 |
|
||||
|
||||
Недавние escape владельца: 0 · Недавние блоки: 0
|
||||
|
||||
## Метрики (информационные, не алерты)
|
||||
|
||||
- Observer evidence: 752 episodes this month, 0 observer_error markers, 186 PII matches before filter
|
||||
- Legacy v1 episodes (not in factor analysis): 613
|
||||
- Last /brain-retro: 0 day(s) ago
|
||||
- Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 20. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store).
|
||||
- Observer evidence: 765 episodes this month, 0 observer_error markers, 0 PII matches before filter
|
||||
- Legacy v1 episodes (not in factor analysis): 765
|
||||
- Last /brain-retro: 13 day(s) ago
|
||||
- Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 0. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store).
|
||||
|
||||
## Метрики дисциплины
|
||||
|
||||
@@ -24,16 +48,14 @@ Baseline дисциплины роутера (этап 2 router discipline overh
|
||||
|
||||
| Тип задачи | Эпизодов | % с триггер-матчем | % через скил |
|
||||
|---|---|---|---|
|
||||
| analysis | 34 | 23.5% | 14.7% |
|
||||
| planning | 25 | 12.0% | 16.0% |
|
||||
| bugfix | 25 | 24.0% | 20.0% |
|
||||
| feature | 19 | 10.5% | 0.0% |
|
||||
| cleanup | 6 | 0.0% | 0.0% |
|
||||
| refactor | 1 | 0.0% | 0.0% |
|
||||
| planning | 108 | 9.3% | 18.5% |
|
||||
| analysis | 35 | 5.7% | 0.0% |
|
||||
| feature | 27 | 11.1% | 3.7% |
|
||||
| bugfix | 26 | 15.4% | 19.2% |
|
||||
|
||||
Router step distribution: 1: 330, 2: 279, 3: 67, 5: 67
|
||||
Router step distribution: 1: 363, 2: 287, 3: 18, 5: 81
|
||||
|
||||
Boundaries applied (ADR / границы): 76 of 743 эпизодов (10.2%).
|
||||
Boundaries applied (ADR / границы): 8 of 749 эпизодов (1.1%).
|
||||
|
||||
## Активные многоэтапные проекты
|
||||
|
||||
@@ -45,22 +67,16 @@ Boundaries applied (ADR / границы): 76 of 743 эпизодов (10.2%).
|
||||
|
||||
## Длинные сессии
|
||||
|
||||
⚠️ Сегодня (2026-05-30 UTC) есть сессии с ≥50 ходов — корреляция с падением дисциплины роутинга (retro #5 candidate B).
|
||||
|
||||
| session_id | макс. ход | % regulated | последний эпизод |
|
||||
|---|---|---|---|
|
||||
| `52b2b52d` | 75 | 3% | 2026-05-30T11:45:39.213Z |
|
||||
|
||||
Long sessions correlate with discipline drift. Если % regulated просел в текущей сессии — рассмотри перезапуск.
|
||||
Ни одной сессии с >50 ходов сегодня (UTC). ✅
|
||||
|
||||
## Стоимость месяца
|
||||
|
||||
| Компонент | Токены (in/out) | USD |
|
||||
|---|---|---|
|
||||
| Classifier (Sonnet 4.6) | 12550/86494 | $1.34 |
|
||||
| Classifier (Sonnet 4.6) | 47009/200345 | $3.15 |
|
||||
| Self-assessment (Sonnet 4.6) | 0/0 | $0.00 |
|
||||
| Reviewer (Opus 4.7 + fallback) | 0/0 | $0.00 |
|
||||
| **Итого** | | **$1.34** |
|
||||
| **Итого** | | **$3.15** |
|
||||
|
||||
## Аномалии классификатора
|
||||
|
||||
@@ -73,50 +89,20 @@ Episodes since last run: 542 / threshold: 10
|
||||
|
||||
## Reviewer: субагент vs fallback
|
||||
|
||||
0 эпизодов проверено из 752.
|
||||
0 эпизодов проверено из 765.
|
||||
|
||||
## Reviewer findings
|
||||
|
||||
Проверено: 372 эпизодов. **69 actionable** (wrong_skill + wrong_chain_order).
|
||||
|
||||
### error_root_cause
|
||||
|
||||
| cause | count |
|
||||
|---|---:|
|
||||
| n/a | 271 |
|
||||
| wrong_skill | 55 |
|
||||
| external_failure | 28 |
|
||||
| wrong_chain_order | 14 |
|
||||
| wrong_tool | 4 |
|
||||
|
||||
### Топ alternative_better
|
||||
|
||||
| recommended | count |
|
||||
|---|---:|
|
||||
| #19 | 18 |
|
||||
| #25 | 15 |
|
||||
| #34 | 8 |
|
||||
| #18 | 8 |
|
||||
| #33 | 3 |
|
||||
|
||||
### node_quality
|
||||
|
||||
| judgment | count |
|
||||
|---|---:|
|
||||
| disputable | 207 |
|
||||
| correct | 120 |
|
||||
| wrong_node | 40 |
|
||||
| underkill | 3 |
|
||||
| overkill | 2 |
|
||||
(нет проверенных эпизодов в текущем периоде)
|
||||
|
||||
## Использование override-фраз
|
||||
|
||||
⚠️ Превышен порог override-использования сегодня (≥5/день)
|
||||
|
||||
|
||||
| Фраза | За всё время | За сегодня |
|
||||
|---|---|---|
|
||||
| `recovery` | 2302 | 23 ⚠️ |
|
||||
| `без скилов` | 507 | 40 ⚠️ |
|
||||
| `recovery` | 2302 | 0 |
|
||||
| `без скилов` | 507 | 0 |
|
||||
| `ремонт инфраструктуры` | 331 | 0 |
|
||||
| `срочно` | 225 | 0 |
|
||||
| `memory dump` | 46 | 0 |
|
||||
@@ -125,7 +111,28 @@ Episodes since last run: 542 / threshold: 10
|
||||
|
||||
## System Health
|
||||
|
||||
Долго работающих процессов нет (порог CPU > 1ч).
|
||||
Топ-3 процессов с CPU > 1ч:
|
||||
|
||||
| PID | Имя | CPU-время | Возраст |
|
||||
|---|---|---|---|
|
||||
| 3916 | MsMpEng | 3.13ч | NaNч |
|
||||
| 1208 | svchost | 1.19ч | 0.0ч |
|
||||
|
||||
⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий.
|
||||
|
||||
## Очередь обучения роутера
|
||||
|
||||
Очередь пуста — нет кандидатов на одобрение.
|
||||
|
||||
## Покрытие дверей
|
||||
|
||||
⚠️ Есть забытые двери (matcher: (хук НЕ зарегистрирован)).
|
||||
|
||||
Непокрытые мутирующие инструменты: Edit, Write, MultiEdit, NotebookEdit, Bash, Task, Skill
|
||||
|
||||
## Целостность журналов действий
|
||||
|
||||
Журналов сессий не найдено (`~/.claude/runtime/action-journal-*.jsonl`).
|
||||
|
||||
## Алерт-индикаторы
|
||||
|
||||
|
||||
@@ -0,0 +1,574 @@
|
||||
# Sealed-Plan Production Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans (inline — субагенты ЗАПРЕЩЕНЫ владельцем, handoff §5). Steps use checkbox (`- [ ]`) syntax.
|
||||
|
||||
**Goal:** Построить рантайм-производство двух печатей (артефакт-решение + план-шаги), чтобы стена М2 имела что матчить — код-предусловие флипа Фазы 8.
|
||||
|
||||
**Architecture:** Два чистых парсера (спека→`{sections}`, план→`[{op,object,ref}]`) + seal-оркестрация, которую хук судьи зовёт на РЕАЛЬНОМ GO (spec-Write → печать артефакта; plan-Write → печать плана со штампом `artifact_id`). Печать привязана к `judged_hash` судимого объекта (одно представление течёт судья→печать). Персист атомарен, артефакт ДО плана. Закрыты 18 находок двух адверсариальных разборов.
|
||||
|
||||
**Tech Stack:** Node ESM, vitest (tools-only config), переиспользование `plan-lock.mjs` / `judge-seal-channel.mjs` / `receipt-sign.mjs` / `path-normalization.mjs` / `receipt-key-config.mjs`.
|
||||
|
||||
**Источник истины:** [docs/superpowers/specs/2026-06-09-sealed-plan-production-design.md](../specs/2026-06-09-sealed-plan-production-design.md) (ОДОБРЕНА). При расхождении — приоритет у спеки.
|
||||
|
||||
**Квирки (handoff §5/§7):** vitest через Bash одной абс.командой `node "<repo>\app\node_modules\vitest\vitest.mjs" run --root "<repo>\app" --config "<repo>\app\vitest.config.tools.mjs" <фильтр> --reporter dot`; тест-файлы **for-of + it()**, НЕ `it.each`; enforce-tdd-gate требует правки `*.test.mjs` ПЕРЕД правкой prod `*.mjs` в том же ходе (RED первым); коммит-не-пуш, git через PowerShell.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| Файл | Ответственность | Действие |
|
||||
|---|---|---|
|
||||
| `tools/artifact-from-spec.mjs` | спека markdown → `{sections, source_sha}` по якорям `{#id}` (P2-2) | Create |
|
||||
| `tools/plan-steps-parse.mjs` | план markdown → `[{op,object,ref}]`; валидация fail-CLOSE; reject `op:'Task'` (VA-4); канон `object` (SE-5) | Create |
|
||||
| `tools/seal-orchestration.mjs` | sealable-объекты + `judgedHashOf` (SD-1) + `sealArtifact`/`sealPlan` (real-GO, TOCTOU, carve-out, штамп `artifact_id`, fail-CLOSE) | Create |
|
||||
| `tools/plan-lock.mjs` | +`judgeMode` в подписанную базу `freezePlan`; атомарный персист `saveFrozen*` (SE-4/VA-2/VA-3) | Modify |
|
||||
| `tools/enforce-judge-gate.mjs` | в `runJudgeTurn`: spec-Write GO → `sealArtifact`; plan-Write GO → `sealPlan`; анти-инъекция продукта (P2-3) | Modify |
|
||||
| `tools/*.test.mjs` (зеркальные) | тесты каждого модуля | Create |
|
||||
|
||||
`<repo>` = `c:\моя\проекты\портал crm\Документация`.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: C1 — парсер артефакта из спеки (`artifact-from-spec.mjs`)
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `tools/artifact-from-spec.mjs`
|
||||
- Test: `tools/artifact-from-spec.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```javascript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseAnchoredSections, buildArtifact } from './artifact-from-spec.mjs';
|
||||
|
||||
describe('artifact-from-spec', () => {
|
||||
const md = [
|
||||
'# Spec',
|
||||
'## Решение А {#dec-a}',
|
||||
'Текст А.',
|
||||
'## Решение Б {#dec-b}',
|
||||
'Текст Б.',
|
||||
].join('\n');
|
||||
|
||||
for (const [name, fn] of [['parses anchored sections', () => {
|
||||
const { sections } = parseAnchoredSections(md);
|
||||
expect(Object.keys(sections).sort()).toEqual(['dec-a', 'dec-b']);
|
||||
expect(sections['dec-a']).toContain('Текст А.');
|
||||
}]]) it(name, fn);
|
||||
|
||||
it('heading WITHOUT anchor is not a section key', () => {
|
||||
const { sections } = parseAnchoredSections('## Без якоря\nx');
|
||||
expect(Object.keys(sections)).toEqual([]);
|
||||
});
|
||||
|
||||
it('empty-content section is dropped (fail-CLOSE)', () => {
|
||||
const { sections } = parseAnchoredSections('## Пусто {#e}\n\n## Полно {#f}\ny');
|
||||
expect(Object.keys(sections)).toEqual(['f']);
|
||||
});
|
||||
|
||||
it('buildArtifact returns {sections, source_sha} deterministically', () => {
|
||||
const a1 = buildArtifact(md); const a2 = buildArtifact(md);
|
||||
expect(a1.source_sha).toEqual(a2.source_sha);
|
||||
expect(typeof a1.source_sha).toBe('string');
|
||||
expect(a1.sections['dec-a']).toBeTruthy();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test → FAIL**
|
||||
|
||||
Run: `node "<repo>\app\node_modules\vitest\vitest.mjs" run --root "<repo>\app" --config "<repo>\app\vitest.config.tools.mjs" artifact-from-spec --reporter dot`
|
||||
Expected: FAIL (module not found / not a function).
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* artifact-from-spec (C1, §5) — спека markdown → {sections, source_sha}.
|
||||
* ref = ЯВНЫЙ якорь `## ... {#id}` (P2-2, НЕ слаг заголовка). Пустой раздел отброшен
|
||||
* (fail-CLOSE). source_sha — sha256 исходного текста (входит в sealable-объект, SD-1).
|
||||
*/
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
const ANCHOR_RE = /^#{1,6}\s+.*\{#([a-zA-Z0-9._-]+)\}\s*$/;
|
||||
|
||||
/** markdown → { sections: { <anchor>: <содержимое раздела> } }. */
|
||||
export function parseAnchoredSections(md) {
|
||||
const lines = String(md == null ? '' : md).split(/\r?\n/);
|
||||
const sections = {};
|
||||
let curKey = null; let buf = [];
|
||||
const flush = () => {
|
||||
if (curKey) { const body = buf.join('\n').trim(); if (body) sections[curKey] = body; }
|
||||
buf = [];
|
||||
};
|
||||
for (const line of lines) {
|
||||
const m = line.match(ANCHOR_RE);
|
||||
const isHeading = /^#{1,6}\s+/.test(line);
|
||||
if (m) { flush(); curKey = m[1]; }
|
||||
else if (isHeading) { flush(); curKey = null; } // заголовок без якоря закрывает раздел
|
||||
else if (curKey) buf.push(line);
|
||||
}
|
||||
flush();
|
||||
return { sections };
|
||||
}
|
||||
|
||||
/** sealable-объект артефакта: { sections, source_sha }. */
|
||||
export function buildArtifact(md) {
|
||||
const { sections } = parseAnchoredSections(md);
|
||||
const source_sha = createHash('sha256').update(String(md == null ? '' : md)).digest('hex');
|
||||
return { sections, source_sha };
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test → PASS**
|
||||
|
||||
Run: тот же vitest-фильтр `artifact-from-spec`. Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```
|
||||
# PowerShell, из <repo>
|
||||
git add tools/artifact-from-spec.mjs tools/artifact-from-spec.test.mjs
|
||||
git commit -F .scratch/c1.txt # msg: "feat(seal): C1 artifact-from-spec parser (anchored sections, P2-2)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: C2 — парсер шагов плана (`plan-steps-parse.mjs`)
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `tools/plan-steps-parse.mjs`
|
||||
- Test: `tools/plan-steps-parse.test.mjs`
|
||||
|
||||
Контракт блока в плане: огороженный ` ```steps-json ` блок с JSON-массивом `[{op,object,ref}]`.
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```javascript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { extractStepsBlock, parsePlanSteps } from './plan-steps-parse.mjs';
|
||||
|
||||
const good = [
|
||||
'# Plan', '```steps-json',
|
||||
'[{"op":"Edit","object":"app/Foo.php","ref":"dec-a"},',
|
||||
' {"op":"Bash","object":"npm run build","ref":"dec-b"}]',
|
||||
'```',
|
||||
].join('\n');
|
||||
|
||||
describe('plan-steps-parse', () => {
|
||||
it('extracts and parses valid steps', () => {
|
||||
const steps = parsePlanSteps(good);
|
||||
expect(steps).toHaveLength(2);
|
||||
expect(steps[0]).toMatchObject({ op: 'Edit', ref: 'dec-a' });
|
||||
});
|
||||
it('canonicalizes file object to repo-relative POSIX (SE-5)', () => {
|
||||
const md = '```steps-json\n[{"op":"Write","object":"app\\\\Bar.php","ref":"r"}]\n```';
|
||||
expect(parsePlanSteps(md)[0].object).toBe('app/Bar.php');
|
||||
});
|
||||
it('rejects step without ref (SE-1) → throws', () => {
|
||||
const md = '```steps-json\n[{"op":"Edit","object":"x"}]\n```';
|
||||
expect(() => parsePlanSteps(md)).toThrow(/ref/);
|
||||
});
|
||||
it('rejects op:Task (VA-4, субагенты запрещены) → throws', () => {
|
||||
const md = '```steps-json\n[{"op":"Task","object":"coder","ref":"r"}]\n```';
|
||||
expect(() => parsePlanSteps(md)).toThrow(/Task/);
|
||||
});
|
||||
it('missing block → throws (fail-CLOSE)', () => {
|
||||
expect(() => parsePlanSteps('# Plan no block')).toThrow();
|
||||
});
|
||||
it('non-JSON block → throws', () => {
|
||||
expect(() => parsePlanSteps('```steps-json\nnot json\n```')).toThrow();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test → FAIL**
|
||||
|
||||
Run: vitest-фильтр `plan-steps-parse`. Expected: FAIL.
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* plan-steps-parse (C2, §5) — план markdown → [{op,object,ref}]. fail-CLOSE на любой брак.
|
||||
* Reject op:'Task' (VA-4). Канон object: файлы → repo-relative POSIX (SE-5). ref обязателен (SE-1).
|
||||
*/
|
||||
import { pathNormalize } from './path-normalization.mjs';
|
||||
|
||||
const BLOCK_RE = /```steps-json\s*\n([\s\S]*?)\n```/;
|
||||
const FILE_OPS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
|
||||
|
||||
/** Достать содержимое блока ```steps-json``` или null. */
|
||||
export function extractStepsBlock(md) {
|
||||
const m = String(md == null ? '' : md).match(BLOCK_RE);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
function canonObject(op, object) {
|
||||
const s = String(object ?? '');
|
||||
if (FILE_OPS.has(op)) { try { return pathNormalize(s).replace(/\\/g, '/'); } catch { return s.replace(/\\/g, '/'); } }
|
||||
return s;
|
||||
}
|
||||
|
||||
/** Распарсить + провалидировать шаги. Бросает на брак (fail-CLOSE). */
|
||||
export function parsePlanSteps(md) {
|
||||
const raw = extractStepsBlock(md);
|
||||
if (raw == null) throw new Error('plan-steps-parse: нет блока ```steps-json``` (fail-CLOSE)');
|
||||
let arr;
|
||||
try { arr = JSON.parse(raw); } catch { throw new Error('plan-steps-parse: блок не валидный JSON (fail-CLOSE)'); }
|
||||
if (!Array.isArray(arr) || arr.length === 0) throw new Error('plan-steps-parse: ожидался непустой массив');
|
||||
return arr.map((s, i) => {
|
||||
if (!s || typeof s !== 'object') throw new Error(`plan-steps-parse: шаг ${i} не объект`);
|
||||
const op = String(s.op || '');
|
||||
const object = String(s.object ?? '');
|
||||
const ref = String(s.ref ?? '');
|
||||
if (!op) throw new Error(`plan-steps-parse: шаг ${i} без op`);
|
||||
if (op === 'Task') throw new Error('plan-steps-parse: op:Task запрещён (субагенты, VA-4)');
|
||||
if (!object.trim()) throw new Error(`plan-steps-parse: шаг ${i} без object`);
|
||||
if (!ref.trim()) throw new Error(`plan-steps-parse: шаг ${i} без ref (closed-door, SE-1)`);
|
||||
return { op, object: canonObject(op, object), ref };
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test → PASS** (vitest-фильтр `plan-steps-parse`).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```
|
||||
git add tools/plan-steps-parse.mjs tools/plan-steps-parse.test.mjs
|
||||
git commit -F .scratch/c2.txt # "feat(seal): C2 plan-steps parser (fail-CLOSE, no-Task VA-4, canon SE-5)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: plan-lock — judge_mode в подписи плана + атомарный персист
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `tools/plan-lock.mjs` (`freezePlan`, `saveFrozenArtifact`, `saveFrozenPlan`)
|
||||
- Test: `tools/plan-lock.test.mjs` (добавить кейсы — Read файла перед правкой существующего теста, harness)
|
||||
|
||||
- [ ] **Step 1: Write the failing test (добавить в plan-lock.test.mjs)**
|
||||
|
||||
```javascript
|
||||
it('freezePlan carries judge_mode in signed base (VA-2)', () => {
|
||||
const key = 'k';
|
||||
const p = freezePlan({ steps: [{ op: 'Edit', object: 'a', ref: 'r' }], artifactId: 'aid', judgeMode: 'shadow', key });
|
||||
expect(p.judge_mode).toBe('shadow');
|
||||
expect(verifyFrozenPlan(p, key)).toBe(true);
|
||||
const tampered = { ...p, judge_mode: 'live-block' };
|
||||
expect(verifyFrozenPlan(tampered, key)).toBe(false); // judge_mode в подписи
|
||||
});
|
||||
it('saveFrozenPlan writes atomically via temp+rename (SE-4)', () => {
|
||||
const calls = [];
|
||||
const fsImpl = { writeFileSync: (p, d) => calls.push(['write', p]), renameSync: (a, b) => calls.push(['rename', a, b]) };
|
||||
saveFrozenPlan({ plan: { plan_id: 'x' }, sessionId: 's', runtimeDir: '/r', fsImpl });
|
||||
expect(calls.some(c => c[0] === 'rename')).toBe(true); // прошёл через rename
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → FAIL** (vitest-фильтр `plan-lock`). Expected: FAIL (judge_mode/rename отсутствуют).
|
||||
|
||||
- [ ] **Step 3: Implement (правки в plan-lock.mjs)**
|
||||
|
||||
`freezePlan` — добавить `judgeMode`:
|
||||
|
||||
```javascript
|
||||
export function freezePlan({ steps, artifactId = null, judgeMode = null, key, nowMs }) {
|
||||
const sealedSteps = withCriterionIds(steps);
|
||||
const id = planId(sealedSteps);
|
||||
const base = { plan_id: id, artifact_id: artifactId, judge_mode: judgeMode,
|
||||
frozen_at: typeof nowMs === 'number' ? nowMs : Date.now(), steps: sealedSteps };
|
||||
return { ...base, sig: signPayload(base, key, RECEIPT_DOMAINS.FROZEN_PLAN) };
|
||||
}
|
||||
```
|
||||
|
||||
Атомарный персист — общий помощник + замена в обоих `save*`:
|
||||
|
||||
```javascript
|
||||
function writeAtomic(path, data, fsImpl) {
|
||||
const tmp = `${path}.tmp-${planId([path, data.length])}`;
|
||||
fsImpl.writeFileSync(tmp, data);
|
||||
fsImpl.renameSync(tmp, path);
|
||||
}
|
||||
export function saveFrozenPlan({ plan, sessionId, runtimeDir, fsImpl = fsDefault }) {
|
||||
writeAtomic(planPath(runtimeDir, sessionId), JSON.stringify(plan), fsImpl);
|
||||
}
|
||||
export function saveFrozenArtifact({ artifact, sessionId, runtimeDir, fsImpl = fsDefault }) {
|
||||
writeAtomic(artifactPath(runtimeDir, sessionId), JSON.stringify(artifact), fsImpl);
|
||||
}
|
||||
```
|
||||
|
||||
(NB: `verifyFrozenPlan` не трогаем — `verifyReceipt` пересчитывает base из всех полей кроме sig, judge_mode попадёт автоматически. Существующие печати без judge_mode: base без поля → verify по-прежнему ок.)
|
||||
|
||||
- [ ] **Step 4: Run → PASS** (vitest-фильтр `plan-lock`, регрессия существующих кейсов зелёная).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```
|
||||
git add tools/plan-lock.mjs tools/plan-lock.test.mjs
|
||||
git commit -F .scratch/t3.txt # "feat(seal): plan-lock judge_mode-in-sig (VA-2) + atomic persist (SE-4)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: seal-orchestration — sealable + judged_hash + sealArtifact/sealPlan
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `tools/seal-orchestration.mjs`
|
||||
- Test: `tools/seal-orchestration.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```javascript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { sealableArtifact, sealablePlan, judgedHashOf, sealArtifact, sealPlan } from './seal-orchestration.mjs';
|
||||
import { contentHash } from './judge-seal-channel.mjs';
|
||||
|
||||
const specMd = '## Реш {#dec-a}\nтекст';
|
||||
const planMd = '```steps-json\n[{"op":"Edit","object":"app/Foo.php","ref":"dec-a"}]\n```';
|
||||
const KEY = 'k';
|
||||
const goVerdict = (obj) => ({ wired: true, decision: 'GO', judged_hash: judgedHashOf(obj), verdict_id: 'v1' });
|
||||
|
||||
describe('seal-orchestration', () => {
|
||||
it('judgedHashOf == contentHash of same sealable object (SD-1)', () => {
|
||||
const a = sealableArtifact(specMd);
|
||||
expect(judgedHashOf(a)).toBe(contentHash(a));
|
||||
});
|
||||
it('sealArtifact on real GO → sealed, verifiable', () => {
|
||||
const a = sealableArtifact(specMd);
|
||||
const r = sealArtifact({ md: specMd, verdict: goVerdict(a), key: KEY, judgeMode: 'live-block' });
|
||||
expect(r.sealed).toBe(true);
|
||||
expect(r.seal.sections['dec-a']).toBeTruthy();
|
||||
expect(r.seal.judge_mode).toBe('live-block');
|
||||
});
|
||||
it('sealArtifact NOT sealed on degraded GO (wired:false, SE-3)', () => {
|
||||
const a = sealableArtifact(specMd);
|
||||
const r = sealArtifact({ md: specMd, verdict: { wired: false, decision: 'GO' }, key: KEY, judgeMode: 'shadow' });
|
||||
expect(r.sealed).toBe(false);
|
||||
});
|
||||
it('sealArtifact NOT sealed on judged_hash mismatch (SD-1/TOCTOU)', () => {
|
||||
const r = sealArtifact({ md: specMd, verdict: { wired: true, decision: 'GO', judged_hash: 'WRONG' }, key: KEY, judgeMode: 'live-block' });
|
||||
expect(r.sealed).toBe(false);
|
||||
});
|
||||
it('sealPlan stamps artifact_id from current artifact (SD-3)', () => {
|
||||
const planObj = sealablePlan(planMd);
|
||||
const r = sealPlan({ md: planMd, currentArtifact: { artifact_id: 'AID' }, verdict: goVerdict(planObj), key: KEY, judgeMode: 'live-block' });
|
||||
expect(r.sealed).toBe(true);
|
||||
expect(r.seal.artifact_id).toBe('AID');
|
||||
expect(r.seal.judge_mode).toBe('live-block');
|
||||
});
|
||||
it('sealPlan fail-CLOSE without current artifact (VA-1/SD-3)', () => {
|
||||
const planObj = sealablePlan(planMd);
|
||||
const r = sealPlan({ md: planMd, currentArtifact: null, verdict: goVerdict(planObj), key: KEY, judgeMode: 'live-block' });
|
||||
expect(r.sealed).toBe(false);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → FAIL** (vitest-фильтр `seal-orchestration`).
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* seal-orchestration (C3-C6, §5) — собрать sealable-объект, сверить judged_hash (SD-1),
|
||||
* поставить печать ТОЛЬКО на реальном GO (SE-3), со штампом artifact_id (SD-3) и judge_mode.
|
||||
* Печать-движок инъектируется (в проде freezeArtifact/freezePlan Машины 2).
|
||||
*/
|
||||
import { buildArtifact } from './artifact-from-spec.mjs';
|
||||
import { parsePlanSteps } from './plan-steps-parse.mjs';
|
||||
import { contentHash, sealOnApproval } from './judge-seal-channel.mjs';
|
||||
import { freezeArtifact, freezePlan } from './plan-lock.mjs';
|
||||
|
||||
export function sealableArtifact(md) { return buildArtifact(md); } // {sections, source_sha}
|
||||
export function sealablePlan(md) { return { steps: parsePlanSteps(md) }; } // {steps:[{op,object,ref}]}
|
||||
export function judgedHashOf(obj) { return contentHash(obj); }
|
||||
|
||||
function isRealGo(v) { return !!(v && v.wired === true && v.decision === 'GO'); }
|
||||
|
||||
/** Печать артефакта на реальном GO (carve-out деньги/тупик — через sealOnApproval). */
|
||||
export function sealArtifact({ md, verdict, key, judgeMode, nowMs, freezeImpl = freezeArtifact }) {
|
||||
if (!isRealGo(verdict)) return { sealed: false, reason: 'нет реального GO (SE-3)' };
|
||||
const artifact = sealableArtifact(md);
|
||||
if (verdict.judged_hash !== judgedHashOf(artifact)) return { sealed: false, reason: 'judged_hash mismatch (SD-1/TOCTOU)' };
|
||||
return sealOnApproval({ artifact: { ...artifact, judge_mode: judgeMode }, verdict, key, nowMs, freezeImpl });
|
||||
}
|
||||
|
||||
/** Печать плана: штамп artifact_id текущего артефакта (SD-3); нет артефакта → fail-CLOSE (VA-1). */
|
||||
export function sealPlan({ md, currentArtifact, verdict, key, judgeMode, nowMs, freezeImpl = freezePlan }) {
|
||||
if (!isRealGo(verdict)) return { sealed: false, reason: 'нет реального GO (SE-3)' };
|
||||
if (!currentArtifact || !currentArtifact.artifact_id) return { sealed: false, reason: 'нет текущего артефакта (VA-1/SD-3)' };
|
||||
let planObj;
|
||||
try { planObj = sealablePlan(md); } catch (e) { return { sealed: false, reason: `парс плана fail-CLOSE: ${e.message}` }; }
|
||||
if (verdict.judged_hash !== judgedHashOf(planObj)) return { sealed: false, reason: 'judged_hash mismatch (SD-1/TOCTOU)' };
|
||||
const seal = freezeImpl({ steps: planObj.steps, artifactId: currentArtifact.artifact_id, judgeMode, key, nowMs });
|
||||
return { sealed: true, seal };
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run → PASS** (vitest-фильтр `seal-orchestration`).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```
|
||||
git add tools/seal-orchestration.mjs tools/seal-orchestration.test.mjs
|
||||
git commit -F .scratch/t4.txt # "feat(seal): orchestration sealArtifact/sealPlan (SD-1/SD-3/SE-3/VA-1)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Wire в enforce-judge-gate — печать на реальном GO + анти-инъекция (P2-3)
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `tools/enforce-judge-gate.mjs` (новый `SPEC_PATH_RE`, ветвление продукта; в `runJudgeTurn` — печать на `wired` GO)
|
||||
- Test: `tools/enforce-judge-gate.test.mjs` (Read перед правкой существующего теста, harness)
|
||||
|
||||
**Принцип P2-3:** продукт на суд (`extractGate2Product` / новый `extractSpecProduct`) подаётся через существующую анти-инъекцию судьи (делимитеры/pre-filter из `judge-engine`/`buildJudgePrompt`); `parseJudgeResponse` уже fail-closed. Печать ставится из чистой seal-оркестрации (Task 4), НЕ из текста контроллера.
|
||||
|
||||
- [ ] **Step 1: Write the failing test (добавить)**
|
||||
|
||||
```javascript
|
||||
import { sealOnWiredGo, SPEC_PATH_RE } from './enforce-judge-gate.mjs';
|
||||
|
||||
it('SPEC_PATH_RE matches docs/superpowers/specs/*.md', () => {
|
||||
expect(SPEC_PATH_RE.test('docs/superpowers/specs/2026-06-09-x-design.md')).toBe(true);
|
||||
expect(SPEC_PATH_RE.test('docs/superpowers/plans/x.md')).toBe(false);
|
||||
});
|
||||
it('sealOnWiredGo: spec-Write + real GO → sealArtifact invoked', () => {
|
||||
const event = { tool_name: 'Write', tool_input: { file_path: 'docs/superpowers/specs/x-design.md', content: '## R {#r}\nt' } };
|
||||
const calls = [];
|
||||
const r = sealOnWiredGo({ event, verdict: { wired: true, decision: 'GO' }, judgeMode: 'shadow',
|
||||
deps: { sealArtifact: (a) => { calls.push(['artifact', a.judgeMode]); return { sealed: true }; }, persistArtifact: () => {} } });
|
||||
expect(calls[0]).toEqual(['artifact', 'shadow']);
|
||||
});
|
||||
it('sealOnWiredGo: degraded GO (wired:false) → no seal call', () => {
|
||||
const event = { tool_name: 'Write', tool_input: { file_path: 'docs/superpowers/specs/x-design.md', content: 'x' } };
|
||||
const calls = [];
|
||||
sealOnWiredGo({ event, verdict: { wired: false, decision: 'GO' }, judgeMode: 'shadow',
|
||||
deps: { sealArtifact: () => { calls.push('a'); return {}; } } });
|
||||
expect(calls).toEqual([]);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → FAIL** (vitest-фильтр `enforce-judge-gate`).
|
||||
|
||||
- [ ] **Step 3: Implement (в enforce-judge-gate.mjs)**
|
||||
|
||||
```javascript
|
||||
export const SPEC_PATH_RE = /(^|[/\\])docs[/\\]superpowers[/\\]specs[/\\][^/\\]+\.md$/i;
|
||||
|
||||
/**
|
||||
* Печать на ВЫЧИСЛЕННОМ реальном GO (wired===true). spec-Write → sealArtifact;
|
||||
* plan-Write → sealPlan (currentArtifact из загрузки). Persist инъектируется.
|
||||
* Чистая (без stdin/exit) — для теста. Возвращает {sealed?, kind?}.
|
||||
*/
|
||||
export function sealOnWiredGo({ event, verdict, judgeMode, deps = {} }) {
|
||||
if (!(verdict && verdict.wired === true && verdict.decision === 'GO')) return { sealed: false };
|
||||
const fp = String(event?.tool_input?.file_path || '');
|
||||
const content = String(event?.tool_input?.content ?? '');
|
||||
const key = deps.key !== undefined ? deps.key : (deps.resolveReceiptKey ? deps.resolveReceiptKey() : null);
|
||||
if (SPEC_PATH_RE.test(fp)) {
|
||||
const r = (deps.sealArtifact)({ md: content, verdict, key, judgeMode });
|
||||
if (r.sealed && deps.persistArtifact) deps.persistArtifact(r.seal);
|
||||
return { sealed: r.sealed, kind: 'artifact' };
|
||||
}
|
||||
if (PLAN_PATH_RE.test(fp)) {
|
||||
const cur = deps.loadCurrentArtifact ? deps.loadCurrentArtifact() : null;
|
||||
const r = (deps.sealPlan)({ md: content, currentArtifact: cur, verdict, key, judgeMode });
|
||||
if (r.sealed && deps.persistPlan) deps.persistPlan(r.seal);
|
||||
return { sealed: r.sealed, kind: 'plan' };
|
||||
}
|
||||
return { sealed: false };
|
||||
}
|
||||
```
|
||||
|
||||
В `runJudgeTurn` после `verdict = await runJudgeGate(...)`, если `verdict.wired` — вызвать `sealOnWiredGo({ event, verdict, judgeMode: mode, deps: <прод-deps> })` (прод-deps: `sealArtifact`/`sealPlan` из seal-orchestration; `resolveReceiptKey`; `loadFrozenArtifact`; `saveFrozenArtifact`/`saveFrozenPlan` атомарные — артефакт ДО плана по порядку событий, VA-3). Печать best-effort, НЕ влияет на block-решение (печать = одобрение, не энфорсмент).
|
||||
|
||||
**judged_hash в вердикте (C5):** в `runJudgeGate`/`callJudgeModel` продукт, отданный судье, = sealable-объект (`sealableArtifact`/`sealablePlan`), и `verdict.judged_hash = judgedHashOf(тот же объект)` (одно представление, SD-1). Добавить в `runJudgeGate` присвоение `judged_hash` перед возвратом.
|
||||
|
||||
- [ ] **Step 4: Run → PASS** (vitest-фильтр `enforce-judge-gate`, регрессия зелёная).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```
|
||||
git add tools/enforce-judge-gate.mjs tools/enforce-judge-gate.test.mjs
|
||||
git commit -F .scratch/t5.txt # "feat(seal): wire seal-on-wired-GO into judge-gate (P2-3 anti-inject, SD-1 judged_hash)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Интеграционный тест — печать → стена пускает/блокирует/closed-door
|
||||
|
||||
**Files:**
|
||||
|
||||
- Test: `tools/seal-to-wall.integration.test.mjs` (Create)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```javascript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { sealArtifact, sealPlan, judgedHashOf, sealableArtifact, sealablePlan } from './seal-orchestration.mjs';
|
||||
import { decideMode } from './enforce-supreme-gate.mjs';
|
||||
|
||||
const KEY = 'k';
|
||||
const specMd = '## Реш {#dec-a}\nтекст';
|
||||
const planMd = '```steps-json\n[{"op":"Edit","object":"app/Foo.php","ref":"dec-a"}]\n```';
|
||||
|
||||
describe('seal → wall integration', () => {
|
||||
it('matching edit allowed; non-matching blocked; bad ref blocked (closed-door)', () => {
|
||||
const aObj = sealableArtifact(specMd);
|
||||
const aRes = sealArtifact({ md: specMd, verdict: { wired: true, decision: 'GO', judged_hash: judgedHashOf(aObj), verdict_id: 'v' }, key: KEY, judgeMode: 'live-block' });
|
||||
const pObj = sealablePlan(planMd);
|
||||
const pRes = sealPlan({ md: planMd, currentArtifact: aRes.seal, verdict: { wired: true, decision: 'GO', judged_hash: judgedHashOf(pObj), verdict_id: 'v' }, key: KEY, judgeMode: 'live-block' });
|
||||
|
||||
const base = { frozenPlan: pRes.seal, frozenArtifact: aRes.seal, key: KEY, stepPtr: 0 };
|
||||
// совпавшая правка
|
||||
expect(decideMode({ ...base, toolUse: { name: 'Edit', input: { file_path: 'app/Foo.php' } } }).decision).toBe('allow');
|
||||
// несовпавшая
|
||||
expect(decideMode({ ...base, toolUse: { name: 'Edit', input: { file_path: 'app/Other.php' } } }).decision).toBe('block');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → FAIL/наблюдать** — убедиться, что тест краснеет до согласования форм (если стена ждёт `step.ref` резолюцию, шаг несёт `ref:'dec-a'`, артефакт имеет `sections['dec-a']` → closed-door проходит). Подогнать формы при необходимости.
|
||||
|
||||
- [ ] **Step 3: Зафиксировать форму** — если decideMode требует `frozenArtifact.sections`/`artifact_id` совпадений, проверить, что `aRes.seal.artifact_id` === `pRes.seal.artifact_id`. Доработок prod-кода быть не должно (Task 1-5 уже дают формы); если тест вскрыл рассинхрон — это находка, чинить по systematic-debugging.
|
||||
|
||||
- [ ] **Step 4: Run → PASS**.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```
|
||||
git add tools/seal-to-wall.integration.test.mjs
|
||||
git commit -F .scratch/t6.txt # "test(seal): integration seal → supreme-gate allow/block/closed-door"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Гейт закрытия — регрессия + финал
|
||||
|
||||
- [ ] **Step 1:** Полная регрессия tools-only:
|
||||
Run: `node "<repo>\app\node_modules\vitest\vitest.mjs" run --root "<repo>\app" --config "<repo>\app\vitest.config.tools.mjs" --reporter dot`
|
||||
Expected: ≥ baseline (текущий main baseline ~3398) + новые кейсы Task 1-6, **0 регрессий**.
|
||||
|
||||
- [ ] **Step 2:** `verification-before-completion` — сверить, что каждый компонент C1-C6 спеки реализован и каждый инвариант §11 покрыт тестом.
|
||||
|
||||
- [ ] **Step 3:** Финальный коммит-сводка (если нужен) + обновить §13 спеки (реализация done). **Push — НЕ делать** (только по слову «пуш»).
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (план против спеки)
|
||||
|
||||
- **Покрытие спеки:** C1→Task1, C2→Task2, C3/C4/C5/C6→Task3-5, closed-door→Task2(ref)+Task6, инварианты §11 (SE-1..SD-3, P2-1..3)→Task2-6. P2-1 (несущая зависимость runtime-страж) — проверяется на Фазе 8 (порядок регистрации), не код-задача здесь; P2-3 анти-инъекция — Task5 (переиспользование судьи).
|
||||
- **Плейсхолдеры:** нет — реальный код в каждом шаге.
|
||||
- **Согласованность типов:** `judgedHashOf`/`sealable*` (Task4) ↔ `judged_hash` в вердикте (Task5); `freezePlan({steps,artifactId,judgeMode,key})` (Task3) ↔ вызов в `sealPlan` (Task4) ↔ интеграция (Task6) — совпадают.
|
||||
- **NB scope:** анти-инъекция (P2-3) опирается на существующий слой судьи — Task5 переиспользует, не строит заново; если слоя нет в коде судьи — отдельная под-задача (вскроется на Task5 audit-context-building).
|
||||
@@ -0,0 +1,235 @@
|
||||
# Дизайн: связка печати (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`.
|
||||
Reference in New Issue
Block a user