docs(router-mentor): sub-plan C2 (манифест интеграции V-1) + F (радары нах.F2)

- C2: реестр деферралов W1-W7 (renderDistricts/«(100%)», reading-wiring, onPlanWrite, warn, мастер-порядок, интеграционный тест, контракты инъекций)
- F: graph-radar + skeleton-radar (проверка полноты §8 Q3)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-10 16:25:17 +03:00
parent 2eb65f67cb
commit b7c93afa2d
2 changed files with 335 additions and 0 deletions
@@ -0,0 +1,130 @@
# Sub-plan C2 — манифест интеграции (боевая проводка) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans (или subagent-driven-development) для исполнения task-by-task. Steps — чек-боксы (`- [ ]`).
**Goal:** Закрыть V-1 (нет манифеста интеграции) — собрать в ОДНОМ месте КАЖДЫЙ отложенный «→ C» wiring из sub-plan'ов A/B/C/D/E с ЯВНЫМ владельцем (задача C2 vs активация владельца), реально провести модули A/B/D в живой шов C, и добавить кросс-модульный интеграционный тест (V-2) + контракты продакшен-инъекций (C-3).
**Architecture:** C2 — фаза БОЕВОЙ ПРОВОДКИ поверх готовых чистых модулей A/B/C/D/E. Большинство W-пунктов трогают существующие движки М2/М3 АДДИТИВНО (CD-3 «встраиваемся поверх»). Правка `router-engine.mjs` (М3 — наставник-движок, который мы оживляем) — в scope. Правка `enforce-supreme-gate.mjs` (М2) — аддитивный предусловный слой, согласовать с владельцем. Регистрация хуков в settings.json — шаг владельца (вне scope). `llmCall`-транспорт — активация владельца.
**Tech Stack:** Node ESM `tools/*.mjs`, vitest (tools-config, ТОЛЬКО из `app/`: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs <фильтр> --reporter dot`).
**Источник истины:** спека `docs/superpowers/specs/2026-06-10-router-mentor-live-rebuild-design.md` (R6.3) §5.3/§5.8/§6.1/§6.2 + CD-R6-G/CD-3; ревью 2026-06-10 (V-1/V-2/нах.F1/F3/F5/C-2/C-3).
**NB git:** git зажат стеной у Claude — коммиты выполняет владелец вне стены.
**Зависимости:** требует РЕАЛИЗОВАННЫХ A (`artifactHasUnresolvedExtracted`), B (`buildGraphSection`/`districtDetail`), C (`buildMentorPrompt`/`runMentorRound`/`runMentorVerdict`/`freezeGate`/`deriveTaskId`/`appendNegotiation`), D (`decideReadEvent`). Порядок сборки — W5.
---
## Реестр деферралов (V-1 — каждый «→ C» имеет владельца)
| ID | Что отложено | Источник (план/self-review) | Владелец | Закрывает |
|----|--------------|------------------------------|----------|-----------|
| W1 | renderDistricts + «(100%)»-rewrite (catalog≠graph) | B self-review «переписать (100%) → C» | C2-task (правка router-engine, М3) | нах.F1 / C-2 / CD-R6-G |
| W2 | decideReadEvent→стена + contentType→роутер + planAuthorizesPath/probeCount продюсеры | D self-review «→ C» | C2-task (правка enforce-supreme-gate, М2, аддитивно) | нах.F3 / SE5 |
| W3 | onPlanWrite (A0): task-id + runMentorVerdict + персист журнала | C scope «C даёт onPlanWrite» (не построен) | C2-task (чистая ф-я) + регистрация owner | нах.F5 / C-1 |
| W4 | warn→owner-резюме гейта-1 | E self-review «→ C» | C2-task / owner-activation | нах.F3-adjacent / ✅O18 |
| W5 | мастер-порядок сборки sub-plan'ов | (нет — новое) | C2-doc | V-1 |
| W6 | кросс-модульный интеграционный тест | (нет — новое) | C2-task | V-2 |
| W7 | контракты продакшен-инъекций (полярность/форма/источник/дефолт) + readFileImpl | C «Боевая проводка» (неполна) | C2-doc | V-2 / C-3 |
---
### Task W1: renderDistricts + «(100%)»-rewrite — buildRouterPrompt разделяет catalog и graph (нах.F1/C-2/CD-R6-G)
**Files:** Modify: `tools/router-engine.mjs` (`buildRouterPrompt` `[:98-124]`). Test: `tools/router-engine.test.mjs`.
**Корень нах.F1/C-2:** `nodesForCtx = (catalog&&catalog.nodes) || (graph&&graph.nodes) || []` `[:99]` — граф используется лишь как фолбэк каталога; `graphSection` из B (`{kind,districtCount,layer0,staleness}`) НЕ имеет `.nodes` → игнорируется; единая секция «(100%)» `[:114]`. Спека CD-R6-G требует: каталог-скилов и project-граф — РАЗНЫЕ секции; «(100%)» переписать.
- [ ] **Step 1: Write the failing test.** При `graph` с `Array.isArray(graph.layer0)``buildRouterPrompt(...).system` содержит секцию «КАРТА РАЙОНОВ» с именами районов (напр. `tools`); каталог берётся ТОЛЬКО из `catalog.nodes`; строки «(100%)» больше нет. Существующий тест (catalog-only, без graph.layer0) остаётся зелёным.
- [ ] **Step 2: Run — FAIL** (секция районов не рендерится).
- [ ] **Step 3: Implement.** В `buildRouterPrompt`: (а) `nodesForCtx = (catalog && catalog.nodes) || []` (граф больше НЕ фолбэк каталога); (б) `[:114]` `'--- ГРАФ+КАТАЛОГ УЗЛОВ (100%) ---'``'--- КАТАЛОГ УЗЛОВ (skill-каталог) ---'`; (в) при `graph && Array.isArray(graph.layer0)` — добавить ОТДЕЛЬНУЮ секцию `'--- ГРАФ ПРОЕКТА: КАРТА РАЙОНОВ (прунённая; заземление против полного каталога) ---'` + рендер `graph.layer0` (`district`/`nodeCount`/`topNodes`); (г) inline staleness из `graph.staleness` если есть. `groundTrace` `[:73-79]` продолжает заземлять против переданного `graph` (полного нод-графа в runMentorRound — отдельный параметр `groundingGraph`, см. C T5 после SE2).
- [ ] **Step 4: Run — PASS** (районы в system; каталог отдельно; «(100%)» нет; старые router-тесты зелены).
- [ ] **Step 5: Commit** `feat(router-engine): разделить catalog и project-граф + карта районов (W1, нах.F1/CD-R6-G)`.
---
### Task W2: reading-discipline wiring — decideReadEvent в стену + продюсеры предикатов (нах.F3/SE5)
**Files:** Modify: `tools/enforce-supreme-gate.mjs` (`decide` `[:135-181]`). Test: `tools/enforce-supreme-gate.test.mjs`.
**Корень нах.F3/SE5:** D `decideReadEvent` не подключён; `planAuthorizesPath`/`probeCountThisRound` без продюсеров (always-true/0 — тихо отключают гейт ДР-1/probe-cap).
- [ ] **Step 1: Write the failing test.** В impl-режиме (frozenPlan валиден): авторское сырьё-чтение (Read файла кода ВНЕ шага плана) → `decide` блокирует через reading-gate ДР-1; чтение графа (graphify-out путь) / harness-авторизованного-шагом пути → пропуск. `planAuthorizesPath` строится из frozenPlan+stepPtr (НЕ always-true).
- [ ] **Step 2: Run — FAIL.**
- [ ] **Step 3: Implement.** (а) Продюсер `planAuthorizesPath(path)` = есть ли лист плана с object===path через `actionMatchesStep` `[plan-lock.mjs:86]` (НЕ always-true, SE5). (б) В `decide` ДО observe-only пропуска Read `[:137]`: если frozenPlan валиден И tool=Read/Grep — звать `decideReadEvent({path, frozenPlan:true, planAuthorizesPath, isCriticProbe, probeCountThisRound, ...})` (D); при `gate.block` → block с reason ДР-1. (в) `contentType` из `decideReadEvent.content` прокинуть для выбора reading-скила (роутер); (г) `probeCountThisRound` — реальный per-round счётчик из состояния кругов (W3/onPlanWrite). **NB:** аддитивный предусловный слой ПЕРЕД observe-only (CD-3) — согласовать как затрагивающий М2.
- [ ] **Step 4: Run — PASS** (авторское сырьё в impl-режиме блокируется; граф/probe/harness — нет; старые gate-тесты зелены).
- [ ] **Step 5: Commit** `feat(supreme-gate): wire decideReadEvent + planAuthorizesPath продюсер (W2, нах.F3/SE5)`.
---
### Task W3: onPlanWrite (A0) — task-id + производитель вердикта + персист (нах.F5/C-1)
**Files:** Create: `tools/on-plan-write.mjs`. Test: `tools/on-plan-write.test.mjs`.
**Корень нах.F5/C-1:** C заявил `onPlanWrite`, но ни одна задача не построила; это оркестратор, производящий mentor-вердикт при записи плана.
- [ ] **Step 1: Write the failing test.** Первый план задачи → `onPlanWrite` присваивает task-id (`deriveTaskId`, C T1) + персистит; зовёт `runMentorVerdict` (C T5b) → вердикт с `plan_hash` + persist журнала (`appendNegotiation`, C T2, try/catch). Re-issue плана с новым хешем НЕ сбрасывает task-id. Сбой журнала не крашит (best-effort).
- [ ] **Step 2: Run — FAIL** (модуль не существует).
- [ ] **Step 3: Implement.** Контракт: `onPlanWrite({planSteps, sessionId, existingTaskId, runMentorVerdictImpl, persistImpl, llmCall, ...inj}) → {taskId, verdict}`. Поток: `taskId = deriveTaskId({existingTaskId, firstPlanHash: planId(planSteps)})` `[plan-lock.mjs:15-17]` + persist (идемпотентно); `verdict = await runMentorVerdictImpl({plan, planHash: planId(planSteps), ...})`; журнал `appendNegotiation` обёрнут try/catch (SE10 — не крашить). Чистое ядро (I/O/llmCall инъектируются). Регистрация PostToolUse-хука на запись плана — owner-activation.
- [ ] **Step 4: Run — PASS.**
- [ ] **Step 5: Commit** `feat(on-plan-write): A0 task-id + производитель вердикта + персист (W3, нах.F5/C-1)`.
---
### Task W4: warn→owner-резюме гейта-1 (✅O18, нах.F3-adjacent)
**Files:** Modify: место сборки owner-резюме гейта-1 (определяется при сборке потока; если резюме формирует владелец — owner-activation, тогда только документировать). Test: соответствующий.
- [ ] **Step 1.** Прокинуть `decideMode(...).warn/warnReason` `[enforce-supreme-gate.mjs:211-216 после E T4]` в текст owner-резюме гейта-1 (§7.0): строка «энфорсмент off: judge_mode рассинхрон план≠артефакт». Тест: при warn:true резюме содержит предупреждение.
- [ ] **Step 2: Commit / либо пометить owner-activation** если резюме вне кода.
---
### Task W5: мастер-порядок сборки (V-1)
**Files:** (doc — этот раздел)
Порядок реализации sub-plan'ов: **A, B, D, E** (чистые автономные модули, параллельно) → **C** (живой шов, инъекция A/B/D) → **C2** (боевая проводка, эта) → **F** (радары). Регрессия каждого включает соседей. Активация (settings.json/ключ/флаг/`llmCall`) — владелец ПОСЛЕ всех.
---
### Task W6: кросс-модульный интеграционный тест (V-2)
**Files:** Create: `tools/router-mentor-integration.test.mjs`.
**Корень V-2:** per-модульные тесты на стабах прячут ошибки полярности/формы/состояния на РЕАЛЬНОЙ проводке (SE2/SE3/SE5).
- [ ] **Step 1: Write the test.** Провести РЕАЛЬНЫЕ `artifactHasUnresolvedExtracted` (A) как `hasUnresolvedExtractedImpl`, `buildGraphSection`(B) как `graphSection`, `decideReadEvent`(D) в C `buildMentorPrompt`/`runMentorRound`/`freezeGate` с ВЕРНОЙ полярностью/формой. End-to-end: (а) районы B видны в промпте (W1); (б) грязный verity-артефакт → freeze блок (A↔C полярность); (в) `graph` vs `graphSection` не перепутаны (SE2); (г) авторское сырьё в impl блокируется (D↔W2).
- [ ] **Step 2: Run — GREEN.**
- [ ] **Step 3: Commit** `test(router-mentor): кросс-модульный интеграционный тест (W6, V-2)`.
---
### Task W7: контракты продакшен-инъекций (V-2/C-3)
**Files:** (doc — этот раздел; ссылается на код W1-W3/C)
Таблица — обязательна при боевой активации:
| Инъекция | Полярность/форма | Продакшен-источник | Дефолт (поведение) |
|----------|------------------|--------------------|--------------------|
| `hasUnresolvedExtractedImpl` (freeze-gate) | `true=ЕСТЬ неразрешённая EXTRACTED=ГРЯЗНО=блок` | A `artifactHasUnresolvedExtracted(art, readFileImpl)` | undefined → блок (fail-closed) |
| `readFileImpl` (внутри ↑) | строка-контент файла | **C-3:** `(f)=>fs.readFileSync(path.resolve(REPO_ROOT, f),'utf8')` — НЕ голый `fs.readFileSync` (Buffer + относит. путь от CWD хука → промах → вечный блок) | бросок → not-resolved → downgrade |
| `groundingGraph` (runMentorRound) | ПОЛНЫЙ нод-граф для `groundTrace` (форма `{nodes,...}`) | реальный project нод-граф | — (не путать с `graphSection`, SE2) |
| `graphSection` (buildMentorPrompt) | `{kind,layer0,staleness}` для промпта | B `buildGraphSection(...)` | нет → fail-safe stale (B T7) |
| `planAuthorizesPath` (decideReadEvent) | `(path)=>bool` из frozenPlan+stepPtr | W2 продюсер (НЕ always-true, SE5) | always-true ОТКЛЮЧАЕТ гейт — запрещено |
| `probeCountThisRound` (decideReadEvent) | реальный per-round счётчик | состояние кругов C/W3 | 0 ОТКЛЮЧАЕТ probe-cap — запрещено |
| `llmCall` (runMentorRound/Verdict) | `({buildPrompt})→trace\|verdict` | `callAnthropicAPI` (ключ/флаг) | активация владельца |
---
## Self-Review (writing-plans)
**1. Spec coverage:** V-1 (манифест) → реестр + W1-W7. нах.F1/C-2 → W1. нах.F3/SE5 → W2 + W7. нах.F5/C-1 → W3. ✅O18 → W4. V-2 → W6 + W7. C-3 → W7. Мастер-порядок → W5. ✅
**2. Placeholder scan:** W1/W2/W3/W6 — TDD-шаги + контракты с цитатами (верифицированы в ревью); W4 — условный (owner-activation если резюме вне кода, честно помечено); W5/W7 — doc-таблицы. Нет «TBD».
**3. Type consistency:** `onPlanWrite``{taskId,verdict}`; `planAuthorizesPath`→bool; `decideReadEvent``{readKind,content,gate,probe}` (D); `hasUnresolvedExtractedImpl`→bool (полярность true=грязно). Имена согласованы с C/D/A.
**Известное ограничение (честно):** W2/W4 трогают М2 (`enforce-supreme-gate`) аддитивно — согласовать с владельцем как затрагивающие верховную стену (CD-3 «поверх», не пересмотр). Регистрация хуков + `llmCall`-транспорт + ключ/флаг — активация владельца, вне scope. До активации шов инертен (asymptote behavioral-residual, §15 CD-R6-A).
@@ -0,0 +1,205 @@
# Sub-plan F — радары полноты исполнения Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (или executing-plans) task-by-task. Steps — чек-боксы (`- [ ]`).
**Goal:** Чистые модули `tools/graph-radar.mjs` + `tools/skeleton-radar.mjs` — две ступени «лестницы радаров» ДР-3 (§5.7 спеки) как проверка ПОЛНОТЫ исполнения (§8 Q3): **граф-радар** (тронул файл X → граф связывает с Y → адресован ли Y?) и **скелет-радар** (grep заголовков/сигнатур → Claude отчитывается по КАЖДОМУ пункту: затронуто / не релевантно / не читал).
**Architecture:** Чистые ядра по образцу `tools/project-graph.mjs` / `tools/context-verity.mjs` — никакого прямого I/O; граф (выход `parseGraph` из sub-plan B) и список заголовков ИНЪЕКТИРУЮТСЯ. Радары — на ФАЗЕ ИСПОЛНЕНИЯ (после заморозки, во время action): дают контроллеру/наставнику чек-лист «что ещё проверить». Wiring в исполнение — sub-plan C2 / поведенческий (контроллер зовёт радар после шага). Решение владельца 2026-06-10: радары = КОД (закрывает нах.F2, ранее в лимбо «код или поведение»).
**Tech Stack:** Node ESM `.mjs`, vitest (tools-config, ТОЛЬКО из `app/`: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs <фильтр> --reporter dot`; cwd ненадёжен → префиксить `cd app &&`).
**Источник истины:** спека R6.3 §5.7 (лестница радаров) + §8 Q3 (проверка полноты) + ДР-3. Граф — формат B: `{nodes:[{id,label,source_file,...}], links:[{source,target,...}]}` (`source`/`target` = node `id`).
**Scope:** `tools/graph-radar.mjs` + `tools/skeleton-radar.mjs` (+ тесты). **НЕ в F:** wiring в исполнение (C2/поведенческий); реальный grep заголовков (caller грепает, передаёт `headers`); построение нод-графа (B).
**NB git:** git зажат стеной у Claude — коммиты выполняет владелец.
---
### Task 1: `buildGraphRadar` — граф-радар (тронул X → соседи Y → адресован?)
**Files:** Create: `tools/graph-radar.mjs`. Test: `tools/graph-radar.test.mjs`.
**§5.7 граф-радар (файл-уровень).** Для каждого ТРОНУТОГО файла → его узлы в графе → соседи по `links` → чек-лист «граф связывает тронутый X с Y — адресован ли Y?». Чистый (граф инъектируется). `addressed:false` по умолчанию — контроллер/наставник отмечает.
- [ ] **Step 1: Write the failing test**
```js
// tools/graph-radar.test.mjs
import { describe, it, expect } from 'vitest';
import { buildGraphRadar } from './graph-radar.mjs';
const graph = {
nodes: [
{ id: 'x', label: 'X', source_file: 'tools/x.mjs' },
{ id: 'y', label: 'Y', source_file: 'tools/y.mjs' },
{ id: 'z', label: 'Z', source_file: 'tools/z.mjs' },
],
links: [
{ source: 'x', target: 'y', relation: 'references' },
{ source: 'z', target: 'x', relation: 'references' },
],
};
describe('buildGraphRadar (§5.7 граф-радар)', () => {
it('тронут x → соседи y и z в чек-листе (addressed:false)', () => {
const r = buildGraphRadar({ touchedFiles: ['tools/x.mjs'], graph });
const ids = r.neighbors.map((n) => n.node).sort();
expect(ids).toEqual(['y', 'z']);
expect(r.neighbors.every((n) => n.addressed === false)).toBe(true);
expect(r.neighbors.find((n) => n.node === 'y').viaTouched).toBe('x');
});
it('сосед, который САМ тронут, не попадает в чек-лист (уже адресован)', () => {
const r = buildGraphRadar({ touchedFiles: ['tools/x.mjs', 'tools/y.mjs'], graph });
expect(r.neighbors.map((n) => n.node)).toEqual(['z']); // y тронут → не флагуем
});
it('нет тронутых узлов в графе → пустой чек-лист', () => {
expect(buildGraphRadar({ touchedFiles: ['tools/nope.mjs'], graph }).neighbors).toEqual([]);
});
it('битый граф/входы → пусто, не крашит', () => {
expect(buildGraphRadar({ touchedFiles: null, graph: null }).neighbors).toEqual([]);
expect(buildGraphRadar({ touchedFiles: ['a'], graph: { nodes: 'x', links: 1 } }).neighbors).toEqual([]);
});
});
```
- [ ] **Step 2: Run — FAIL** (`Cannot find module './graph-radar.mjs'`).
Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/graph-radar.test.mjs --reporter dot`
- [ ] **Step 3: Write minimal implementation**
```js
// tools/graph-radar.mjs
#!/usr/bin/env node
/**
* graph-radar (§5.7 ДР-3) — файл-уровень проверки полноты: тронул X → граф связывает
* с Y → адресован ли Y? Чистое ядро (граф инъектируется, формат project-graph B).
* Сосед, который сам в touchedFiles, не флагуется (уже адресован).
*/
export function buildGraphRadar({ touchedFiles, graph } = {}) {
const touched = Array.isArray(touchedFiles) ? touchedFiles : [];
const nodes = graph && Array.isArray(graph.nodes) ? graph.nodes : [];
const links = graph && Array.isArray(graph.links) ? graph.links : [];
const touchedSet = new Set(touched);
// id → узел (для source_file соседа)
const byId = new Map(nodes.map((n) => [n && n.id, n]).filter(([id]) => id));
// id тронутых узлов
const touchedIds = new Set(nodes.filter((n) => n && touchedSet.has(n.source_file)).map((n) => n.id));
const seen = new Set();
const neighbors = [];
for (const l of links) {
if (!l || typeof l !== 'object') continue;
for (const [a, b] of [[l.source, l.target], [l.target, l.source]]) {
if (!touchedIds.has(a)) continue; // a — тронутый конец
const nb = byId.get(b);
if (!nb) continue;
if (touchedSet.has(nb.source_file)) continue; // сосед сам тронут → адресован
const key = `${b}<-${a}`;
if (seen.has(key)) continue;
seen.add(key);
neighbors.push({ node: b, label: nb.label ?? null, source_file: nb.source_file ?? null, viaTouched: a, addressed: false });
}
}
return { neighbors, summary: { touchedNodes: touchedIds.size, unaddressed: neighbors.length } };
}
```
- [ ] **Step 4: Run — PASS** (4 passed).
- [ ] **Step 5: Commit** `feat(graph-radar): граф-радар полноты (нах.F2, sub-plan F Task 1)`.
---
### Task 2: `buildSkeletonRadar` — скелет-радар (отчёт по каждому заголовку)
**Files:** Create: `tools/skeleton-radar.mjs`. Test: `tools/skeleton-radar.test.mjs`.
**§5.7 скелет-радар (раздел-уровень).** grep заголовков/сигнатур файла (инъектируются как `headers`); Claude отчитывается по КАЖДОМУ пункту: `touched` / `not-relevant` / `not-read`. Радар строит ЛЕСА: непрорепорченные пункты → `not-reported` (требуют отчёта).
- [ ] **Step 1: Write the failing test**
```js
// tools/skeleton-radar.test.mjs
import { describe, it, expect } from 'vitest';
import { buildSkeletonRadar, REPORT_STATUSES } from './skeleton-radar.mjs';
describe('buildSkeletonRadar (§5.7 скелет-радар)', () => {
const headers = ['## Auth', '## Billing', '## Webhooks'];
it('каждый заголовок → пункт; непрорепорченный → not-reported', () => {
const r = buildSkeletonRadar({ headers, reports: { '## Auth': 'touched' } });
expect(r.items).toHaveLength(3);
expect(r.items.find((i) => i.header === '## Auth').status).toBe('touched');
expect(r.items.find((i) => i.header === '## Billing').status).toBe('not-reported');
expect(r.unreported.map((i) => i.header).sort()).toEqual(['## Billing', '## Webhooks']);
});
it('REPORT_STATUSES заморожен и содержит 4 статуса', () => {
expect(Object.isFrozen(REPORT_STATUSES)).toBe(true);
expect(REPORT_STATUSES).toEqual(['touched', 'not-relevant', 'not-read', 'not-reported']);
});
it('невалидный статус в reports → not-reported (fail-safe)', () => {
const r = buildSkeletonRadar({ headers: ['## A'], reports: { '## A': 'выдумка' } });
expect(r.items[0].status).toBe('not-reported');
});
it('битые входы → пусто, не крашит', () => {
expect(buildSkeletonRadar({ headers: null }).items).toEqual([]);
expect(buildSkeletonRadar({}).unreported).toEqual([]);
});
});
```
- [ ] **Step 2: Run — FAIL** (`Cannot find module './skeleton-radar.mjs'`).
- [ ] **Step 3: Write minimal implementation**
```js
// tools/skeleton-radar.mjs
#!/usr/bin/env node
/**
* skeleton-radar (§5.7 ДР-3) — раздел-уровень: grep заголовков → Claude отчитывается по
* КАЖДОМУ (touched/not-relevant/not-read). Чистое ядро (headers + reports инъектируются).
* Непрорепорченный ИЛИ невалидно-прорепорченный пункт → not-reported (fail-safe: молчание
* = «не отчитался», а не «не релевантно»).
*/
export const REPORT_STATUSES = Object.freeze(['touched', 'not-relevant', 'not-read', 'not-reported']);
const VALID = new Set(['touched', 'not-relevant', 'not-read']);
export function buildSkeletonRadar({ headers, reports } = {}) {
const list = Array.isArray(headers) ? headers : [];
const rep = (reports && typeof reports === 'object') ? reports : {};
const items = list.map((h) => {
const s = rep[h];
return { header: h, status: VALID.has(s) ? s : 'not-reported' };
});
return { items, unreported: items.filter((i) => i.status === 'not-reported') };
}
```
- [ ] **Step 4: Run — PASS** (4 passed).
- [ ] **Step 5: Commit** `feat(skeleton-radar): скелет-радар полноты (нах.F2, sub-plan F Task 2)`.
---
### Task 3: Регрессия + нота wiring
**Files:** (нет правок — проверка)
- [ ] **Step 1: Прогнать ВЕСЬ tools-набор** [НЕ-TDD: регрессия]
Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs --reporter dot`
Expected: PASS — `graph-radar.test.mjs` (4) + `skeleton-radar.test.mjs` (4) включены, 0 регрессий М1–М7 + соседних sub-plan. Зафиксировать число passed.
- [ ] **Step 2: Нота wiring (doc).** Радары зовутся на ФАЗЕ ИСПОЛНЕНИЯ как проверка полноты (§8 Q3): после action-шага контроллер строит `buildGraphRadar({touchedFiles: изменённые файлы, graph})` + `buildSkeletonRadar({headers: grep файла, reports: отчёт Claude})` → чек-лист «что ещё проверить». Wiring — C2/поведенческий (не F). Граф берётся из B (`parseGraph` живого graphify-out).
- [ ] **Step 3: Если RED — systematic-debugging**, точечный фикс, повтор.
---
## Self-Review (writing-plans)
**1. Spec coverage:** §5.7 граф-радар → Task 1. §5.7 скелет-радар → Task 2. §8 Q3 проверка полноты → нота wiring Task 3. нах.F2 (радары как код, решение владельца) → Task 1/2. ✅
**2. Placeholder scan:** реальный код+тесты в каждом шаге; команды Run с ожидаемым выводом; нет «TBD». ✅
**3. Type consistency:** `buildGraphRadar``{neighbors:[{node,label,source_file,viaTouched,addressed}], summary}`; `buildSkeletonRadar``{items:[{header,status}], unreported}`; `REPORT_STATUSES` frozen. ✅
**Известное ограничение (честно):** радары дают ЧЕК-ЛИСТ (что проверить), отметку «адресовано/затронуто» ставит контроллер/наставник/владелец — машина не решает «достаточно ли». Реальный grep заголовков (Task 2 `headers`) и список изменённых файлов (Task 1 `touchedFiles`) — забота caller'а (C2/поведенческий wiring), не F. Это сокращение пропусков (L2-пол), не обнуление.