From b7c93afa2d3814a91ba0622ff4e8e2f318eb6c64 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: Wed, 10 Jun 2026 16:25:17 +0300 Subject: [PATCH] =?UTF-8?q?docs(router-mentor):=20sub-plan=20C2=20(=D0=BC?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=84=D0=B5=D1=81=D1=82=20=D0=B8=D0=BD=D1=82?= =?UTF-8?q?=D0=B5=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20V-1)=20+=20F=20(?= =?UTF-8?q?=D1=80=D0=B0=D0=B4=D0=B0=D1=80=D1=8B=20=D0=BD=D0=B0=D1=85.F2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - C2: реестр деферралов W1-W7 (renderDistricts/«(100%)», reading-wiring, onPlanWrite, warn, мастер-порядок, интеграционный тест, контракты инъекций) - F: graph-radar + skeleton-radar (проверка полноты §8 Q3) Co-Authored-By: Claude Opus 4.8 --- ...-mentor-subplan-c2-integration-manifest.md | 130 +++++++++++ ...er-mentor-subplan-f-completeness-radars.md | 205 ++++++++++++++++++ 2 files changed, 335 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-10-router-mentor-subplan-c2-integration-manifest.md create mode 100644 docs/superpowers/plans/2026-06-10-router-mentor-subplan-f-completeness-radars.md diff --git a/docs/superpowers/plans/2026-06-10-router-mentor-subplan-c2-integration-manifest.md b/docs/superpowers/plans/2026-06-10-router-mentor-subplan-c2-integration-manifest.md new file mode 100644 index 00000000..de4b1e22 --- /dev/null +++ b/docs/superpowers/plans/2026-06-10-router-mentor-subplan-c2-integration-manifest.md @@ -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). diff --git a/docs/superpowers/plans/2026-06-10-router-mentor-subplan-f-completeness-radars.md b/docs/superpowers/plans/2026-06-10-router-mentor-subplan-f-completeness-radars.md new file mode 100644 index 00000000..a5fde453 --- /dev/null +++ b/docs/superpowers/plans/2026-06-10-router-mentor-subplan-f-completeness-radars.md @@ -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-пол), не обнуление.