# Машина 3 — Честные контракты: поле `inherent` + гард (#1) — TDD-план

> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans. Steps `- [ ]`.
> **Этот репозиторий:** код строит САМ контроллер inline. `commit` — owner-gated. Существующие тест-файлы расширяем Write полного файла (TDD-гейт отбивает import-only Edit).

**Goal:** Дать карточке навыка честно объявлять, что ему присуще ПО ПРИРОДЕ (поле `inherent` — пункты `{need, rationale}`), с механическим гардом R4 (пустой rationale → невалидно: «прошлый промах» обычно не формулируется как «природа навыка»), и учитывать inherent как названные нужды в сверщике полноты — тогда повторяющееся следствие ловится бесплатно каждый раз (дизайн §4 #1).

**Architecture:** Расширение схемы `skill-contract.mjs` (новое поле `inherent` + валидация-гард + helper `inherentNeedsOf`) + расширение `coverage-machine.effectiveNeeds` (inherent как kind='inherent', наравне с needs/constraints). Аддитивно: контракты без `inherent` → пустой массив → существующее поведение и тесты целы.

**Tech Stack:** Node.js ESM (`.mjs`), vitest tools-only.

**Источник:** spec `2026-06-05-...-design.md` §4 #1 (богатые честные контракты) + R4 (грань: честное самоописание, НЕ накопительный чек-лист промахов). Решение владельца 2026-06-05: новое поле inherent + гард rationale.

**Границы:** массовое наполнение inherent для ~84 узлов — данные (R-11), не код. Семантический разбор «это природа или промах?» — рассуждение/ревью при наполнении; код держит лишь форму (rationale непустой).

---

## File Structure

| Файл | Ответственность | Действие |
|---|---|---|
| `tools/skill-contract.mjs` | +`inherent` в CONTRACT_FIELDS; валидация `inherent[{need,rationale}]` (rationale непустой = гард R4); normalize; `inherentNeedsOf`. | **Modify** |
| `tools/skill-contract.test.mjs` | +describe на inherent. | **Modify** (Write полного) |
| `tools/coverage-machine.mjs` | `effectiveNeeds` включает inherent (kind='inherent'). | **Modify** |
| `tools/coverage-machine.test.mjs` | +тесты: effectiveNeeds inherent + findHoles по inherent. | **Modify** (Write полного) |

---

## Task 1: `skill-contract.mjs` — поле `inherent` + гард rationale + `inherentNeedsOf`

**Files:**
- Modify: `tools/skill-contract.mjs`
- Test: `tools/skill-contract.test.mjs` (Write полного файла + новый describe + импорт `inherentNeedsOf`)

- [ ] **Step 1: Написать падающий тест** — добавить в импорт `inherentNeedsOf` и блок:

```js
describe('inherent (#1 присущее по природе + гард R4 rationale)', () => {
  const withInh = { ...OWN, inherent: [{ need: 'мобильный', rationale: 'фронт по природе делает адаптив' }] };
  it('CONTRACT_FIELDS содержит inherent', () => {
    expect(CONTRACT_FIELDS).toContain('inherent');
  });
  it('валидный inherent {need,rationale} → ok', () => {
    expect(validateContract(withInh).ok).toBe(true);
  });
  it('контракт без inherent остаётся валидным (поле опционально)', () => {
    expect(validateContract(OWN).ok).toBe(true);
  });
  it('гард R4: inherent без rationale → error', () => {
    const r = validateContract({ ...OWN, inherent: [{ need: 'мобильный' }] });
    expect(r.ok).toBe(false); expect(r.errors.some((e) => /rationale/i.test(e))).toBe(true);
  });
  it('inherent не массив → error', () => {
    expect(validateContract({ ...OWN, inherent: 'мобильный' }).ok).toBe(false);
  });
  it('inherent.need пустой → error', () => {
    expect(validateContract({ ...OWN, inherent: [{ need: '', rationale: 'x' }] }).ok).toBe(false);
  });
  it('inherentNeedsOf → список need-строк', () => {
    expect(inherentNeedsOf(withInh)).toEqual(['мобильный']);
    expect(inherentNeedsOf({})).toEqual([]);
  });
  it('normalizeContract: отсутствующий inherent → []', () => {
    expect(normalizeContract({ skill: 'x', kind: 'own' }).inherent).toEqual([]);
  });
});
```

- [ ] **Step 2: RED** — `npx vitest run --root ".claude/worktrees/brainrepo/app" --config "vitest.config.tools.mjs" skill-contract` → FAIL (`inherentNeedsOf` undefined + новые ассерты).

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

(а) В `CONTRACT_FIELDS` добавить `'inherent'`:
```js
export const CONTRACT_FIELDS = Object.freeze([
  'skill', 'kind', 'needs', 'produces', 'constraints',
  'preview-form', 'defaults', 'key-decisions', 'acceptance-criteria', 'inherent',
]);
```

(б) В `validateContract`, перед `return`, добавить блок валидации inherent (гард R4):
```js
  if (c.inherent != null) {
    if (!Array.isArray(c.inherent)) {
      errors.push('inherent: array required');
    } else {
      c.inherent.forEach((it, i) => {
        if (!it || typeof it !== 'object' || typeof it.need !== 'string' || !it.need.trim())
          errors.push(`inherent[${i}].need: non-empty string required`);
        else if (typeof it.rationale !== 'string' || !it.rationale.trim())
          errors.push(`inherent[${i}].rationale: non-empty (зачем по природе) required — гард R4`);
      });
    }
  }
```

(в) В `normalizeContract`, после цикла ARRAY_FIELDS, добавить:
```js
  if (c.inherent == null) c.inherent = [];
```

(г) Новая экспорт-функция (после `producesOf`):
```js
/** Нужды, присущие навыку по природе (#1) — список need-строк для сверщика полноты. */
export function inherentNeedsOf(c) {
  return Array.isArray(c?.inherent)
    ? c.inherent.map((it) => it?.need).filter((n) => typeof n === 'string' && n.trim())
    : [];
}
```

- [ ] **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 "feat(m3): контракт +inherent (присущее по природе) + гард rationale (R4)"
```

---

## Task 2: `coverage-machine.mjs` — `effectiveNeeds` учитывает inherent

**Files:**
- Modify: `tools/coverage-machine.mjs`
- Test: `tools/coverage-machine.test.mjs` (Write полного файла + импорт `effectiveNeeds` + новый describe)

- [ ] **Step 1: Написать падающий тест** — добавить `effectiveNeeds` в импорт и блок:

```js
describe('effectiveNeeds + findHoles c inherent (#1 — присущее по природе = названная нужда)', () => {
  const withInh = { skill: 'fd', needs: ['design'], produces: [], constraints: ['a11y'], inherent: [{ need: 'мобильный', rationale: 'адаптив по природе' }] };
  it('effectiveNeeds включает inherent (kind=inherent) наравне с needs/constraints', () => {
    const en = effectiveNeeds(withInh);
    expect(en).toContainEqual({ token: 'design', kind: 'need' });
    expect(en).toContainEqual({ token: 'a11y', kind: 'constraint' });
    expect(en).toContainEqual({ token: 'мобильный', kind: 'inherent' });
  });
  it('inherent-нужда без producer → дыра kind=inherent', () => {
    const h = findHoles([withInh]);
    expect(h).toContainEqual({ need: 'мобильный', neededBy: 'fd', kind: 'inherent' });
  });
  it('контракт без inherent → поведение не изменилось', () => {
    expect(effectiveNeeds({ skill: 'x', needs: ['n'], constraints: [] })).toEqual([{ token: 'n', kind: 'need' }]);
  });
});
```

- [ ] **Step 2: RED** — `... coverage-machine` → FAIL (effectiveNeeds не возвращает inherent; новый импорт).

- [ ] **Step 3: Реализация** — в `coverage-machine.mjs` заменить `effectiveNeeds`:

```js
/** D — эффективные нужды контракта = needs + constraints + inherent (#1 присущее по природе). */
export function effectiveNeeds(contract) {
  const needs = (contract.needs || []).map((n) => ({ token: n, kind: 'need' }));
  const cons = (contract.constraints || []).map((cN) => ({ token: cN, kind: 'constraint' }));
  const inh = (contract.inherent || []).map((it) => ({ token: it.need, kind: 'inherent' }));
  return [...needs, ...cons, ...inh];
}
```

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

```bash
git -C ".claude/worktrees/brainrepo" add tools/coverage-machine.mjs tools/coverage-machine.test.mjs
git -C ".claude/worktrees/brainrepo" commit -m "feat(m3): coverage effectiveNeeds учитывает inherent как названную нужду"
```

---

## Task 3: Регрессия tools-only (R5)

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

---

## Self-Review (§4 #1 / R4)

| Требование | Где |
|---|---|
| #1 навык объявляет присущее по природе | поле `inherent[{need,rationale}]` (Task 1) |
| повторяющееся следствие = названная нужда, ловится сверщиком | `effectiveNeeds`+`findHoles` (Task 2) |
| R4 грань: не накопительный чек-лист промахов | гард rationale-непустой (Task 1); семантика — ревью при наполнении (данные) |

**Type-consistency:** `inherent`/`need`/`rationale`/`inherentNeedsOf`/kind `'inherent'` единые; аддитивно (контракты без inherent целы). **Scope:** схема+сверщик; наполнение — данные R-11.
