# Машина 3 — закрытие находок аудита 2026-06-05 — TDD-план

> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans. Steps `- [ ]`. Код строит контроллер inline. `commit` — owner-gated. Тест-файлы расширяем Write полного файла.

**Goal:** Закрыть находки построчного аудита Машины 3 настоящими механическими зубами там, где это возможно, и честно задокументировать остаток там, где код закрыть не может (без призраков).

**Что закрываем кодом (настоящие зубы):**
- **F1 — гард R4 (`skill-contract`):** rationale у inherent отвергается, если содержит маркер прошлого промаха («забыли», «прошлый раз»…) — инъецируемый список. Превращает «почти косметику» в реальный отказ очевидной подмены.
- **F4 — терминатор круга (`round-control`):** невалидный `roundCount` (отриц/NaN) при пробеле → консервативно `escalate-owner` (не вечный soft-return). + точная семантика 0-based в доке.
- **Minor cadence-порог:** `oversizeSignal` дефолт 12→20 (точнее под «десятки»).
- **Minor decision-graph footgun:** новый `buildValidatedGraph` (validate→build, бросает на невалидном) — безопасный вход.

**Что НЕ закрываем кодом (честный остаток — закрытие = документирование, не фейк):**
- **F2 — независимость owner_asks (анти-внушение по смыслу):** роутер-ИИ производит и owner_asks, и сверку; код не видит его рассуждение. Реальный зуб (`groundVerityTrace`, source резолвится) уже есть; «выведено ли независимо» механически непроверяемо (спека R1). Добавить псевдо-проверку = призрак. Закрываем доком-остатком.
- **F3 — «надо ли было рекомендовать домен-навык»:** `domainCallDiscipline` по-честному сторожит исполнение рекомендации, а не её необходимость (это качество выбора роутера — другой слой). Закрываем доком-остатком.

**Tech Stack:** Node.js ESM, vitest tools-only.

**Источник:** аудит этой сессии (построчно, по факту) против spec 2026-06-05 §2/§3/§3А/§4.

---

## Task 1: F1 — гард R4 маркеры промаха (`skill-contract.mjs`)

**Files:** Modify `tools/skill-contract.mjs`, Test `tools/skill-contract.test.mjs` (Write полного + блок; импорт `DEFAULT_MISTAKE_MARKERS`)

- [ ] **Step 1: Тест** (добавить `DEFAULT_MISTAKE_MARKERS` в импорт + блок)

```js
describe('гард R4 усилен — rationale не должен быть исповедью прошлого промаха', () => {
  it('DEFAULT_MISTAKE_MARKERS — непустой список', () => {
    expect(Array.isArray(DEFAULT_MISTAKE_MARKERS)).toBe(true);
    expect(DEFAULT_MISTAKE_MARKERS.length).toBeGreaterThan(0);
  });
  it('rationale с маркером промаха («в прошлый раз забыли») → error', () => {
    const r = validateContract({ ...OWN, inherent: [{ need: 'мобильный', rationale: 'в прошлый раз забыли' }] });
    expect(r.ok).toBe(false); expect(r.errors.some((e) => /промах|R4/i.test(e))).toBe(true);
  });
  it('честная природа-формулировка → ok', () => {
    expect(validateContract({ ...OWN, inherent: [{ need: 'мобильный', rationale: 'фронт по природе делает адаптив' }] }).ok).toBe(true);
  });
  it('маркеры инъецируемы (портативно)', () => {
    const r = validateContract({ ...OWN, inherent: [{ need: 'x', rationale: 'спецслово тут' }] }, { mistakeMarkers: ['спецслово'] });
    expect(r.ok).toBe(false);
  });
});
```

- [ ] **Step 2: RED** — `... skill-contract` → FAIL (`DEFAULT_MISTAKE_MARKERS` undefined + маркер-ассерты).

- [ ] **Step 3: Реализация** — `skill-contract.mjs`:

(а) После `PREVIEW_FORMS` добавить экспорт:
```js
// Маркеры «прошлого промаха» (гард R4): rationale про природу навыка, НЕ про то,
// что мы прошлый раз прошляпили. Generic (не project-specific), инъецируемо.
export const DEFAULT_MISTAKE_MARKERS = Object.freeze([
  'забыл', 'забыли', 'прошлый раз', 'в прошлый раз', 'пропустил', 'пропустили',
  'промах', 'прошляпил', 'упустил', 'не учли', 'в прошлом',
]);
```

(б) Сигнатура `validateContract(c)` → `validateContract(c, { mistakeMarkers = DEFAULT_MISTAKE_MARKERS } = {})`.

(в) В блоке валидации inherent, в ветке где `rationale` непустой — добавить проверку маркера. Заменить ветку проверки rationale:
```js
        else if (typeof it.rationale !== 'string' || !it.rationale.trim())
          errors.push(`inherent[${i}].rationale: non-empty (зачем по природе) required — гард R4`);
        else {
          const low = it.rationale.toLowerCase();
          const hit = (mistakeMarkers || []).find((m) => typeof m === 'string' && m && low.includes(m.toLowerCase()));
          if (hit) errors.push(`inherent[${i}].rationale: похоже на прошлый промах (маркер «${hit}»), нужна «природа навыка» — гард R4`);
        }
```

- [ ] **Step 4: GREEN** — `... skill-contract` → PASS.
- [ ] **Step 5: Commit** *(owner-gated)*

```bash
git -C ".claude/worktrees/brainrepo" add tools/skill-contract.mjs tools/skill-contract.test.mjs
git -C ".claude/worktrees/brainrepo" commit -m "fix(m3): гард R4 — отвергать rationale-маркеры прошлого промаха (аудит F1)"
```

---

## Task 2: F4 — консервативный терминатор круга (`round-control.mjs`)

**Files:** Modify `tools/round-control.mjs`, Test `tools/round-control.test.mjs` (Write полного + блок)

- [ ] **Step 1: Тест** (добавить блок)

```js
describe('decideRoundOutcome — консервативность при битом счётчике (аудит F4)', () => {
  it('невалидный roundCount (отриц) при пробеле → escalate-owner (не вечный loop)', () => {
    expect(decideRoundOutcome({ codeOk: false, routerFaithful: true, roundCount: -1 }).outcome).toBe('escalate-owner');
  });
  it('невалидный roundCount (NaN) при пробеле → escalate-owner', () => {
    expect(decideRoundOutcome({ codeOk: false, routerFaithful: true, roundCount: NaN }).outcome).toBe('escalate-owner');
  });
  it('битый roundCount, но круг сошёлся → всё равно proceed', () => {
    expect(decideRoundOutcome({ codeOk: true, routerFaithful: true, roundCount: NaN }).outcome).toBe('proceed');
  });
});
```

- [ ] **Step 2: RED** — текущий код вернёт `soft-return` на `roundCount:-1` (т.к. `-1 < 3`) → тест падает.

- [ ] **Step 3: Реализация** — заменить тело `decideRoundOutcome`:

```js
export function decideRoundOutcome({ codeOk, routerFaithful, roundCount = 0, maxRounds = 3 } = {}) {
  const gap = !codeOk || !routerFaithful;
  if (!gap) return { outcome: 'proceed', reason: 'круг сошёлся: код + смысл' };
  // roundCount — число завершённых проходов (0-based); escalate при >= maxRounds.
  // Битый/неподанный счётчик → консервативно эскалируем (не зацикливаемся в soft-return).
  const rc = (Number.isInteger(roundCount) && roundCount >= 0) ? roundCount : maxRounds;
  if (rc < maxRounds) return { outcome: 'soft-return', reason: `пробел (код:${codeOk} смысл:${routerFaithful}); проход ${rc}/${maxRounds} — мягкий возврат` };
  return { outcome: 'escalate-owner', reason: `пробел не закрыт за ${maxRounds} прохода — эскалация владельцу (C-12)` };
}
```

Плюс уточнить docstring: «roundCount — счётчик завершённых проходов (0-based), ОБЯЗАН приходить из неподделываемого источника (журнал М1) при live-wiring; модуль сам не инкрементит — без внешнего инкремента терминатор не сработает».

- [ ] **Step 4: GREEN** — PASS (старые 12 + новые 3).
- [ ] **Step 5: Commit** *(owner-gated)*

```bash
git -C ".claude/worktrees/brainrepo" add tools/round-control.mjs tools/round-control.test.mjs
git -C ".claude/worktrees/brainrepo" commit -m "fix(m3): round-control — битый roundCount → консервативный escalate (аудит F4)"
```

---

## Task 3: Minor — `oversizeSignal` дефолт 12→20 (`conversation-cadence.mjs`)

**Files:** Modify `tools/conversation-cadence.mjs`, Test `tools/conversation-cadence.test.mjs` (Write полного — подправить порог в существующем тесте)

- [ ] **Step 1: Тест** — в существующем блоке `oversizeSignal` заменить «десятки вопросов» на 22 (под новый дефолт 20):

```js
  it('десятки вопросов (≥20) → oversize (дроби на волны)', () => {
    const r = oversizeSignal({ questionCount: 22, themeCount: 1 });
    expect(r.oversize).toBe(true);
    expect(r.reasons.some((x) => /волн/i.test(x))).toBe(true);
  });
  it('дюжина вопросов (12) при дефолте 20 → НЕ oversize', () => {
    expect(oversizeSignal({ questionCount: 12, themeCount: 1 }).oversize).toBe(false);
  });
```

(остальные тесты блока — без изменений; «пороги — ручки» тест с `questionThreshold: 4` остаётся валиден).

- [ ] **Step 2: RED** — `questionCount: 12 → НЕ oversize` падает при текущем дефолте 12.

- [ ] **Step 3: Реализация** — в `oversizeSignal` дефолт `questionThreshold = 12` → `20`:

```js
export function oversizeSignal({ questionCount = 0, themeCount = 0 } = {}, { questionThreshold = 20, themeThreshold = 3 } = {}) {
```

- [ ] **Step 4: GREEN** — PASS.
- [ ] **Step 5: Commit** *(owner-gated)*

```bash
git -C ".claude/worktrees/brainrepo" add tools/conversation-cadence.mjs tools/conversation-cadence.test.mjs
git -C ".claude/worktrees/brainrepo" commit -m "fix(m3): oversizeSignal дефолт 12→20 (точнее под «десятки», аудит minor)"
```

---

## Task 4: Minor — безопасный `buildValidatedGraph` (`decision-graph.mjs`)

**Files:** Modify `tools/decision-graph.mjs`, Test `tools/decision-graph.test.mjs` (Write полного + блок)

- [ ] **Step 1: Тест** (импорт +`buildValidatedGraph`, блок)

```js
describe('buildValidatedGraph (validate→build; бросает на невалидном — аудит footgun)', () => {
  it('валидные секции → тот же граф, что buildDecisionGraph', () => {
    expect(buildValidatedGraph(SECTIONS)).toEqual(buildDecisionGraph(SECTIONS));
  });
  it('висящая зависимость → бросает (не строит молча рёбра на несуществующий узел)', () => {
    expect(() => buildValidatedGraph([{ id: 'a', dependsOn: ['нет'] }])).toThrow(/несуществ|невалид/i);
  });
  it('дубль id → бросает', () => {
    expect(() => buildValidatedGraph([{ id: 'a', dependsOn: [] }, { id: 'a', dependsOn: [] }])).toThrow();
  });
});
```

- [ ] **Step 2: RED** — `buildValidatedGraph is not a function`.

- [ ] **Step 3: Реализация** — добавить в `decision-graph.mjs`:

```js
/**
 * Безопасный вход: сперва validateSections, при невалидном — бросает (не строит
 * молча рёбра на несуществующие узлы — аудит-footgun). Иначе — buildDecisionGraph.
 */
export function buildValidatedGraph(sections) {
  const v = validateSections(sections);
  if (!v.ok) throw new Error(`decision-graph невалиден: ${v.errors.join('; ')}`);
  return buildDecisionGraph(sections);
}
```

- [ ] **Step 4: GREEN** — PASS.
- [ ] **Step 5: Commit** *(owner-gated)*

```bash
git -C ".claude/worktrees/brainrepo" add tools/decision-graph.mjs tools/decision-graph.test.mjs
git -C ".claude/worktrees/brainrepo" commit -m "fix(m3): decision-graph buildValidatedGraph — безопасный вход (аудит footgun)"
```

---

## Task 5: Честные доки-остатки (F2 / F3 / cadence / domain — без призраков)

**Files:** Modify docstrings: `tools/router-verity.mjs`, `tools/domain-skill-discipline.mjs`, `tools/conversation-cadence.mjs`. Без логики → без тестов.

- [ ] **Step 1:** В `router-verity.mjs` шапку дополнить честным остатком F2:
> «ОСТАТОК (R1, непроверяемо кодом): что owner_asks выведен из сырых слов НЕЗАВИСИМО, а не подогнан под план, механически не проверяется — роутер-ИИ производит и вывод, и сверку. Реальный механический зуб — groundVerityTrace (source обязан резолвиться). Содержательная независимость — на судье/владельце.»

- [ ] **Step 2:** В `domain-skill-discipline.mjs` шапку дополнить честным scope F3:
> «SCOPE: сторожит ИСПОЛНЕНИЕ рекомендации (recommended→called по журналу), НЕ её НЕОБХОДИМОСТЬ (надо ли было рекомендовать домен-навык — это качество выбора роутера, другой слой; здесь не ловится).»

- [ ] **Step 3:** В `conversation-cadence.mjs` шапку дополнить:
> «ЭТО ДИСПЕТЧЕР-ДАННЫЕ: checksForEvent возвращает id-проверок; ИСПОЛНЕНИЕ (вызов router-emerge/runVerityCheck/coverage-machine) — на вызывающем при live-wiring. Сам модуль ничего не запускает.»

- [ ] **Step 4:** Commit *(owner-gated)*

```bash
git -C ".claude/worktrees/brainrepo" add tools/router-verity.mjs tools/domain-skill-discipline.mjs tools/conversation-cadence.mjs
git -C ".claude/worktrees/brainrepo" commit -m "docs(m3): честные остатки аудита F2/F3 + cadence-диспетчер (без призраков)"
```

---

## Task 6: Регрессия tools-only

- [ ] `npx vitest run --root ".claude/worktrees/brainrepo/app" --config "vitest.config.tools.mjs"` (`dangerouslyDisableSandbox=true`) → базлайн 2357 + новые (~12). Ноль RED. Доложить.

---

## Self-Review

| Находка | Закрытие |
|---|---|
| F1 R4-гард слаб | маркеры промаха в rationale → error (Task 1) — настоящий зуб |
| F4 терминатор от внешнего счётчика | битый счётчик → консерв. escalate + точная дока (Task 2) |
| F2 анти-внушение по смыслу | НЕ кодом (призрак) → честный док-остаток (Task 5) |
| F3 domain «надо ли» | НЕ кодом → честный scope-док (Task 5) |
| minor cadence-порог | 12→20 (Task 3) |
| minor decision footgun | buildValidatedGraph (Task 4) |
| minor cadence номинальна | док-диспетчер (Task 5) |

**Type-consistency:** `DEFAULT_MISTAKE_MARKERS`/`mistakeMarkers`/`buildValidatedGraph`/`rc` единые; обратная совместимость (validateContract 2-й арг опционален; oversizeSignal порог — ручка). **Без призраков:** F2/F3 закрыты честным документированием, не фейк-проверкой.
