# План (wall-исполнимый): резолв ID узлов → имена в скил-контексте наставника

> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:test-driven-development (inline, RED→GREEN). НЕ субагентами — деликатное ядро.
> Спека: `docs/superpowers/specs/2026-06-13-mentor-skill-resolve-wall-design.md`.

**Goal:** Наставник видит рекомендацию роутера именами скилов (`#4 — markdownlint-cli2`), а не голыми ID, и реально судит их уместность.

**Architecture:** `renderSkillContext` получает `registry` и резолвит каждый ID рекомендации через `registry.indexById` в `#N — имя`; нет реестра / неизвестный ID → голый `#N` (fail-safe). `onPlanWrite` прокидывает уже доступный `registry` в `renderSkillContext`.

**Tech Stack:** Node ESM (.mjs), vitest (tools-config). Регрессия: `npx vitest run --root app --config vitest.config.tools.mjs` (база 3901).

## Цель

Сделать рекомендацию роутера в промпте наставника человекочитаемой (имена вместо ID), чтобы скил-сверка наставника перестала быть полой.

```skills-json
["test-driven-development"]
```

## Шаги (wall-исполнимый линейный TDD-скрипт)

Этот план исполняется ПОД способ-B стеной: каждый шаг (правка теста, прогон vitest, правка исходника) — отдельный шаг плана в порядке исполнения, иначе supreme-gate заблокирует действие вне плана. Шаги БЕЗ `ref` (TDD-микрошаги самодостаточны; закрытая дверь C-5 не применяется — `refResolves` снимает требование при отсутствии `ref`).

```steps-json
[{"n":1,"op":"Edit","object":"tools/mentor-seam.test.mjs"},
{"n":2,"op":"Bash","object":"npx vitest run --root app --config vitest.config.tools.mjs mentor-seam"},
{"n":3,"op":"Edit","object":"tools/mentor-seam.mjs"},
{"n":4,"op":"Bash","object":"npx vitest run --root app --config vitest.config.tools.mjs mentor-seam"},
{"n":5,"op":"Edit","object":"tools/on-plan-write.test.mjs"},
{"n":6,"op":"Bash","object":"npx vitest run --root app --config vitest.config.tools.mjs on-plan-write.test"},
{"n":7,"op":"Edit","object":"tools/on-plan-write.mjs"},
{"n":8,"op":"Bash","object":"npx vitest run --root app --config vitest.config.tools.mjs on-plan-write.test"},
{"n":9,"op":"Bash","object":"npx vitest run --root app --config vitest.config.tools.mjs"}]
```

## Откат

`git restore tools/mentor-seam.mjs tools/mentor-seam.test.mjs tools/on-plan-write.mjs tools/on-plan-write.test.mjs` (изменений в них ещё не закоммичено).

---

## Task 1: `renderSkillContext` резолвит ID → имя через реестр

**Files:**
- Modify: `tools/mentor-seam.mjs` (`renderSkillContext` + новый `resolveNodeName`)
- Test: `tools/mentor-seam.test.mjs`

- [ ] **Step 1 (шаг плана 1): Падающий тест.** Добавить новый describe-блок в `tools/mentor-seam.test.mjs`:

```js
describe('renderSkillContext — резолв ID→имя (фикс)', () => {
  const registry = { indexById: new Map([['#4', { id: '#4', name: 'markdownlint-cli2', slug: 'markdownlint' }]]) };
  it('с реестром резолвит #4 → имя', () => {
    const s = renderSkillContext({ declared: ['executing-plans'], recommendedChain: ['#4'], registry });
    expect(s).toMatch(/#4 — markdownlint-cli2/);
  });
  it('без реестра → голый ID (fail-safe)', () => {
    const s = renderSkillContext({ declared: ['x'], recommendedChain: ['#4'] });
    expect(s).toMatch(/#4/);
    expect(s).not.toMatch(/markdownlint/);
  });
  it('неизвестный ID → голый ID (не падает)', () => {
    const s = renderSkillContext({ declared: ['x'], recommendedChain: ['#999'], registry });
    expect(s).toMatch(/#999/);
  });
});
```

- [ ] **Step 2 (шаг плана 2): Прогон — FAIL.** `npx vitest run --root app --config vitest.config.tools.mjs mentor-seam` → новый тест `#4 — markdownlint-cli2` падает (текущий код склеивает голый `#4`).

- [ ] **Step 3 (шаг плана 3): Реализация.** В `tools/mentor-seam.mjs` заменить `renderSkillContext` (строки 56-65) на:

```js
/** Резолв ID узла → «#N — имя» через registry.indexById; нет узла/реестра → голый #N (fail-safe). */
function resolveNodeName(id, registry) {
  const node = registry && registry.indexById && typeof registry.indexById.get === 'function'
    ? registry.indexById.get(id) : null;
  const label = node && (node.name || node.slug);
  return label ? `${id} — ${label}` : String(id);
}

/** Рендер скил-контекста для промпта наставника (мерж): объявленные в плане скилы +
 *  рекомендация роутера (ID резолвятся в имена через registry). recommendedChain=null →
 *  классификатор недоступен (не сверять). */
export function renderSkillContext({ declared = [], recommendedChain = null, registry = null } = {}) {
  const dec = declared.length ? declared.join(', ') : '(не объявлены)';
  const rec = recommendedChain === null
    ? '(рекомендация роутера недоступна — НЕ заворачивай за скилы)'
    : (Array.isArray(recommendedChain) && recommendedChain.length
        ? recommendedChain.map((id) => resolveNodeName(id, registry)).join(', ')
        : '(роутер ничего не порекомендовал)');
  return `--- СКИЛЫ ---\nОбъявлены в плане: ${dec}\nРекомендация роутера: ${rec}\n`
    + 'Оцени уместность выбора скилов; неуместный/неполный выбор → decision="NO-GO" + что добавить/убрать.';
}
```

- [ ] **Step 4 (шаг плана 4): Прогон — PASS.** `npx vitest run --root app --config vitest.config.tools.mjs mentor-seam` → весь mentor-seam зелёный (старые renderSkillContext-тесты без `registry` дают голый ID — как раньше).

---

## Task 2: `onPlanWrite` прокидывает реестр в `renderSkillContext`

**Files:**
- Modify: `tools/on-plan-write.mjs` (вызов `renderSkillContext` получает `registry`)
- Test: `tools/on-plan-write.test.mjs`

- [ ] **Step 5 (шаг плана 5): Падающий тест.** Добавить `it(...)` в существующий блок `describe('onPlanWrite — скил-сверка через classify (мерж)')` (перед его закрывающим `});`):

```js
  it('резолвит ID рекомендации в имя через registry (фикс)', async () => {
    let capturedUser = null;
    const registry = { indexById: new Map([['#4', { id: '#4', name: 'markdownlint-cli2' }]]) };
    await onPlanWrite({
      planSteps: [{ n: 1, op: 'Edit', object: 'x', ref: 'D1' }],
      declaredSkills: ['executing-plans'],
      classifyImpl: async () => ({ recommended_chain: ['#4'] }),
      registry,
      llmCall: async ({ buildPrompt }) => { capturedUser = buildPrompt().user; return GO; },
      planGoal: 'правка .md',
    });
    expect(capturedUser).toMatch(/markdownlint-cli2/);
    expect(capturedUser).not.toMatch(/#4(?! — )/); // голого #4 без имени нет
  });
```

- [ ] **Step 6 (шаг плана 6): FAIL.** `npx vitest run --root app --config vitest.config.tools.mjs on-plan-write.test` → новый тест падает (registry не прокинут → голый `#4`).

- [ ] **Step 7 (шаг плана 7): Реализация.** В `tools/on-plan-write.mjs` строка 55 — добавить `registry` в вызов:

```js
  const skillContext = renderSkillContext({ declared: declaredSkills, recommendedChain, registry });
```

- [ ] **Step 8 (шаг плана 8): PASS.** `npx vitest run --root app --config vitest.config.tools.mjs on-plan-write.test` → зелёный.

---

## Task 3: Регрессия + финал

- [ ] **Step 9 (шаг плана 9): Полная регрессия.** `npx vitest run --root app --config vitest.config.tools.mjs`
  Expected: весь набор зелёный (база 3901 + новые тесты), 0 регрессий. RED → systematic-debugging. Затем superpowers:verification-before-completion.

```verified-context-json
[{"id":"1","kind":"EXTRACTED","claim":"renderSkillContext склеивает рекомендацию сырыми ID","ref":"tools/mentor-seam.mjs:58","anchor":"export function renderSkillContext"},
{"id":"2","kind":"EXTRACTED","claim":"onPlanWrite строит skillContext без registry","ref":"tools/on-plan-write.mjs:55","anchor":"renderSkillContext({ declared: declaredSkills"},
{"id":"3","kind":"EXTRACTED","claim":"реестр индексирует узлы по id (node.name есть)","ref":"tools/registry-load.mjs:31","anchor":"const indexById = new Map"},
{"id":"4","kind":"EXTRACTED","claim":"узел #4 имеет name markdownlint-cli2","ref":"docs/registry/nodes.yaml:40","anchor":"name: \"markdownlint-cli2\""}]
```
