# Машина 3 / под-план 3-A — Контракты скилов — план реализации

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax.
> **NB сборки (урок M2):** субагент-исполнитель НЕ проходит TDD-гейт (его правка теста невидима хуку) → код строит САМ контроллер по TDD (тест→RED→прод→GREEN в одном ходе).

**Goal:** дать машинно-читаемую схему контракта скила `{needs/produces/constraints + preview-form/defaults/key-decisions/acceptance-criteria}`, механический валидатор формы и G4 хеш-страж дрейфа чужих скилов — фундамент для машины охвата 3-C (рёбра графа needs↔produces) и входа роутера 3-D.

**Architecture:** чистое ядро `tools/skill-contract.mjs` (схема + валидатор + нормализация + accessors needs/produces + G4 хеш-страж) + загрузчик `tools/skill-contract-registry.mjs` (чистый `buildRegistry` над записями + `loadRegistry` с диска, fs инъектируется — паттерн проекта). Без LLM: полнота/тип слотов — операции над структурой, не «ум ИИ» («умный не баран»). Портативность: никакого project-хардкода имён скилов в коде — контракты лежат данными на диске.

**Tech Stack:** Node.js ESM, `node:crypto` (sha256 для G4), vitest tools-only. Образец дисциплины — `tools/plan-lock.mjs` (Машина 2).

---

## ⚠️ Контекст исполнения (как M1/M2)
- Worktree `brainrepo`, ветка `worktree-brainrepo`. Git — только `git -C "<worktree>"`, по одной команде.
- Тесты: `npx vitest run --root ".claude/worktrees/brainrepo/app" --config vitest.config.tools.mjs <фильтр>` с `dangerouslyDisableSandbox=true`, через Bash (гейт ищет Bash-vitest для RED).
- Тест-файлы — в `<worktree>\tools\` (конфиг резолвит `../tools/*.test.mjs`).
- TDD-гейт прод-файла требует в одном ходе: Read этого плана прямыми слэшами + правка теста + Bash-vitest RED, потом прод.
- Тест-файлы писать ЦЕЛИКОМ через Write (real-test-verifier блокирует голую правку импорта без `expect`).

## Границы 3-A (что НЕ здесь)
- Чтение контрактов в машину охвата A/B/C/D, look-ahead, L-ядро роутера → **3-D/3-C**.
- Защита контракта от само-ослабления (убрал нужду/критерий → блок + да владельца + diff, анти-отрава) → **Машина 5** (здесь только схема, не гейт).
- Массовое НАПИСАНИЕ контрактов для всех ~86 узлов → задача наполнения (данные), не код 3-A. Здесь — машинерия + 2 образца (own + external).

---

## Структура файлов

**Создаём:**
- `tools/skill-contract.mjs` — схема (`CONTRACT_FIELDS`, `PREVIEW_FORMS`), `validateContract`, `normalizeContract`, `needsOf`/`producesOf`, `contractHash`, `checkContractDrift` (G4).
- `tools/skill-contract-registry.mjs` — `buildRegistry` (чистый, над записями) + `loadRegistry` (диск, fs инъектируется).
- Тесты: `tools/skill-contract.test.mjs`, `tools/skill-contract-registry.test.mjs`.
- Образцы данных: `docs/registry/contracts/writing-plans.contract.json` (own), `docs/registry/contracts/operations-process-doc.contract.json` (external, с `source{version,hash,path}`).

**Порядок:** схема+валидатор (Task 1) → нормализация+accessors (Task 2) → G4 хеш-страж (Task 3) → buildRegistry (Task 4) → loadRegistry+диск (Task 5) → образцы+инварианты (Task 6).

---

## Task 1: Схема контракта + механический валидатор формы

**Files:** Create `tools/skill-contract.mjs`, `tools/skill-contract.test.mjs`

- [ ] **Step 1: Падающий тест** — `tools/skill-contract.test.mjs`

```js
import { describe, it, expect } from 'vitest';
import { CONTRACT_FIELDS, PREVIEW_FORMS, validateContract } from './skill-contract.mjs';

const OWN = {
  skill: 'writing-plans', kind: 'own',
  needs: ['spec'], produces: ['implementation-plan'], constraints: ['no code'],
  'preview-form': 'outline', defaults: ['bite-sized tasks'],
  'key-decisions': ['file structure'], 'acceptance-criteria': ['each step 2-5 min'],
};
const EXT = {
  skill: 'operations:process-doc', kind: 'external',
  needs: ['as-is process'], produces: ['process doc'], constraints: [],
  'preview-form': 'none', defaults: [], 'key-decisions': [], 'acceptance-criteria': ['doc covers all steps'],
  source: { version: '1.2.0', hash: 'a'.repeat(64), path: 'x/SKILL.md' },
};

describe('CONTRACT_FIELDS / PREVIEW_FORMS', () => {
  it('canonical fields present (C-13 + L-поля)', () => {
    for (const f of ['skill','kind','needs','produces','constraints','preview-form','defaults','key-decisions','acceptance-criteria'])
      expect(CONTRACT_FIELDS).toContain(f);
  });
  it('preview-form includes none', () => { expect(PREVIEW_FORMS).toContain('none'); });
});

describe('validateContract (форма, без LLM)', () => {
  it('valid own contract → ok', () => { expect(validateContract(OWN)).toEqual({ ok: true, errors: [] }); });
  it('valid external contract → ok', () => { expect(validateContract(EXT).ok).toBe(true); });
  it('non-object → error', () => { expect(validateContract(null).ok).toBe(false); });
  it('empty skill → error', () => { expect(validateContract({ ...OWN, skill: '' }).ok).toBe(false); });
  it('bad kind → error', () => { expect(validateContract({ ...OWN, kind: 'maybe' }).ok).toBe(false); });
  it('needs not string-array → error', () => { expect(validateContract({ ...OWN, needs: 'spec' }).ok).toBe(false); });
  it('bad preview-form → error', () => { expect(validateContract({ ...OWN, 'preview-form': 'fancy' }).ok).toBe(false); });
  it('external without source → error', () => {
    const { source, ...noSrc } = EXT; expect(validateContract(noSrc).ok).toBe(false);
  });
  it('external with bad hash → error', () => {
    expect(validateContract({ ...EXT, source: { version: '1', hash: 'short' } }).ok).toBe(false);
  });
});
```

- [ ] **Step 2: RED** — `npx vitest run --root ".claude/worktrees/brainrepo/app" --config vitest.config.tools.mjs skill-contract` → FAIL (import).

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

```js
#!/usr/bin/env node
/**
 * skill-contract — схема контракта скила (C-13 + L-серия) + механический
 * валидатор формы + G4 хеш-страж дрейфа чужих скилов. Чистый модуль (без LLM):
 * полнота/тип слотов — операции над структурой («умный не баран»). Фундамент
 * для машины охвата 3-C (needs/produces = рёбра графа) и входа роутера 3-D.
 */
import { createHash } from 'node:crypto';

// Канонические поля (C-13 базовые needs/produces/constraints + L-поля).
export const CONTRACT_FIELDS = Object.freeze([
  'skill', 'kind', 'needs', 'produces', 'constraints',
  'preview-form', 'defaults', 'key-decisions', 'acceptance-criteria',
]);

// L1 preview-form — закрытый список форм дешёвого образца ('none' для мелочи).
export const PREVIEW_FORMS = Object.freeze(['none', 'outline', 'mockup', 'sample', 'dry-run', 'diagram']);

const ARRAY_FIELDS = Object.freeze(['needs', 'produces', 'constraints', 'defaults', 'key-decisions', 'acceptance-criteria']);

function isStringArray(v) { return Array.isArray(v) && v.every((x) => typeof x === 'string'); }

/**
 * Механический валидатор контракта → {ok, errors[]}. Проверяет ФОРМУ (наличие/тип
 * полей, enum preview-form, source{version,hash} у external), НЕ «полноту под
 * задачу» (это машина охвата 3-C).
 */
export function validateContract(c) {
  if (!c || typeof c !== 'object') return { ok: false, errors: ['contract is not an object'] };
  const errors = [];
  if (typeof c.skill !== 'string' || !c.skill.trim()) errors.push('skill: non-empty string required');
  if (c.kind !== 'own' && c.kind !== 'external') errors.push("kind: must be 'own' or 'external'");
  for (const f of ARRAY_FIELDS) if (!isStringArray(c[f])) errors.push(`${f}: array of strings required`);
  if (!PREVIEW_FORMS.includes(c['preview-form'])) errors.push(`preview-form: one of ${PREVIEW_FORMS.join('|')}`);
  if (c.kind === 'external') {
    const s = c.source;
    if (!s || typeof s !== 'object') errors.push('external: source{version,hash} required');
    else {
      if (typeof s.version !== 'string' || !s.version.trim()) errors.push('source.version: non-empty string required');
      if (typeof s.hash !== 'string' || !/^[0-9a-f]{64}$/.test(s.hash)) errors.push('source.hash: sha256 hex required');
    }
  }
  return { ok: errors.length === 0, errors };
}
```

- [ ] **Step 4: GREEN** — прогон → PASS.
- [ ] **Step 5: Commit** — `git -C "<worktree>" add tools/skill-contract.mjs tools/skill-contract.test.mjs` затем `git -C "<worktree>" commit -m "feat(m3-a): skill-contract schema + mechanical form validator (C-13/L)"`

---

## Task 2: Нормализация + accessors needs/produces

**Files:** Modify `tools/skill-contract.mjs`, `tools/skill-contract.test.mjs`

- [ ] **Step 1: Падающие тесты (добавить)**

```js
import { normalizeContract, needsOf, producesOf } from './skill-contract.mjs';

describe('normalizeContract', () => {
  it('missing arrays → [] and preview-form → none', () => {
    const n = normalizeContract({ skill: ' x ', kind: 'own' });
    expect(n.skill).toBe('x');
    for (const f of ['needs','produces','constraints','defaults','key-decisions','acceptance-criteria']) expect(n[f]).toEqual([]);
    expect(n['preview-form']).toBe('none');
  });
  it('does not mutate input', () => {
    const raw = { skill: 'x', kind: 'own' };
    normalizeContract(raw); expect(raw.needs).toBeUndefined();
  });
});

describe('needsOf / producesOf', () => {
  it('return copies of arrays', () => {
    const c = { needs: ['a'], produces: ['b'] };
    expect(needsOf(c)).toEqual(['a']); expect(producesOf(c)).toEqual(['b']);
    needsOf(c).push('z'); expect(c.needs).toEqual(['a']);
  });
  it('missing → []', () => { expect(needsOf({})).toEqual([]); expect(producesOf({})).toEqual([]); });
});
```

- [ ] **Step 2: RED.**
- [ ] **Step 3: Реализация (дописать в `tools/skill-contract.mjs`)**

```js
/** Нормализация формы перед чтением/валидацией: отсутствующие массивы → [],
 *  preview-form → 'none', trim skill. Не «исправляет» невалидное — только форма. */
export function normalizeContract(raw) {
  const c = { ...(raw || {}) };
  if (typeof c.skill === 'string') c.skill = c.skill.trim();
  for (const f of ARRAY_FIELDS) if (c[f] == null) c[f] = [];
  if (c['preview-form'] == null) c['preview-form'] = 'none';
  return c;
}

/** Нужды контракта (копия) — рёбра графа 3-C. */
export function needsOf(c) { return Array.isArray(c?.needs) ? [...c.needs] : []; }
/** Что производит (копия) — рёбра графа 3-C. */
export function producesOf(c) { return Array.isArray(c?.produces) ? [...c.produces] : []; }
```

- [ ] **Step 4: GREEN.**
- [ ] **Step 5: Commit** — `git -C "<worktree>" commit -am "feat(m3-a): normalizeContract + needs/produces accessors"`

---

## Task 3: G4 — хеш-страж дрейфа чужого скила

**Files:** Modify `tools/skill-contract.mjs`, `tools/skill-contract.test.mjs`

- [ ] **Step 1: Падающие тесты (добавить)**

```js
import { contractHash, checkContractDrift } from './skill-contract.mjs';

describe('contractHash', () => {
  it('sha256 hex, deterministic', () => {
    expect(contractHash('abc')).toMatch(/^[0-9a-f]{64}$/);
    expect(contractHash('abc')).toBe(contractHash('abc'));
    expect(contractHash('abc')).not.toBe(contractHash('abd'));
  });
});

describe('checkContractDrift (G4)', () => {
  const ext = (hash) => ({ skill: 's', kind: 'external', source: { version: '1', hash } });
  it('own contract → дрейф не сторожится (ok)', () => {
    expect(checkContractDrift({ contract: { skill: 's', kind: 'own' }, currentContent: 'anything' }).ok).toBe(true);
  });
  it('external, hash совпал → ok, not drifted', () => {
    const h = contractHash('SKILL body');
    expect(checkContractDrift({ contract: ext(h), currentContent: 'SKILL body' })).toMatchObject({ ok: true, drifted: false });
  });
  it('external, содержание изменилось → drifted + fallback soft-reasoning', () => {
    const h = contractHash('old body');
    const r = checkContractDrift({ contract: ext(h), currentContent: 'NEW body' });
    expect(r.drifted).toBe(true); expect(r.ok).toBe(false); expect(r.fallback).toBe('soft-reasoning');
  });
  it('external без сохранённого отпечатка → drifted (нельзя доверять)', () => {
    const r = checkContractDrift({ contract: { skill: 's', kind: 'external', source: { version: '1' } }, currentContent: 'x' });
    expect(r.drifted).toBe(true);
  });
});
```

- [ ] **Step 2: RED.**
- [ ] **Step 3: Реализация (дописать)**

```js
/** sha256 содержания — отпечаток чужого скила для G4. */
export function contractHash(content) {
  return createHash('sha256').update(String(content ?? '')).digest('hex');
}

/**
 * G4 — хеш-страж дрейфа (C-13 ключевой гард-рейл): сверяет сохранённый отпечаток
 * (contract.source.hash) с актуальным содержанием чужого SKILL.md. Расхождение →
 * drifted=true + fallback 'soft-reasoning' (откат на мягкое рассуждение судьи).
 * Своим (kind='own') не сторожим — контракт пишем сами.
 */
export function checkContractDrift({ contract, currentContent }) {
  if (!contract || contract.kind !== 'external')
    return { ok: true, drifted: false, reason: 'own/нет внешнего источника — дрейф не сторожится' };
  const stored = contract.source?.hash;
  const actual = contractHash(currentContent);
  if (!stored) return { ok: false, drifted: true, reason: 'нет сохранённого отпечатка внешнего скила', fallback: 'soft-reasoning' };
  if (stored !== actual)
    return { ok: false, drifted: true, reason: `дрейф чужого скила: ${stored.slice(0, 12)}… ≠ ${actual.slice(0, 12)}…`, fallback: 'soft-reasoning' };
  return { ok: true, drifted: false, reason: 'отпечаток совпал' };
}
```

- [ ] **Step 4: GREEN.**
- [ ] **Step 5: Commit** — `git -C "<worktree>" commit -am "feat(m3-a): G4 drift-guard (contractHash + checkContractDrift)"`

---

## Task 4: buildRegistry — чистая сборка набора контрактов

**Files:** Create `tools/skill-contract-registry.mjs`, `tools/skill-contract-registry.test.mjs`

- [ ] **Step 1: Падающий тест** — `tools/skill-contract-registry.test.mjs`

```js
import { describe, it, expect } from 'vitest';
import { buildRegistry } from './skill-contract-registry.mjs';

const own = (skill) => ({ skill, kind: 'own', needs: [], produces: [], constraints: [], 'preview-form': 'none', defaults: [], 'key-decisions': [], 'acceptance-criteria': [] });

describe('buildRegistry (чистый)', () => {
  it('валидные контракты собираются', () => {
    const r = buildRegistry([{ contract: own('a') }, { contract: own('b') }]);
    expect(r.contracts).toHaveLength(2); expect(r.errors).toEqual([]); expect(r.driftFlags).toEqual([]);
  });
  it('невалидный контракт → в errors, не в contracts', () => {
    const r = buildRegistry([{ contract: { skill: '', kind: 'own' } }]);
    expect(r.contracts).toHaveLength(0); expect(r.errors).toHaveLength(1);
  });
  it('дубль skill → ошибка, первый остаётся', () => {
    const r = buildRegistry([{ contract: own('a') }, { contract: own('a') }]);
    expect(r.contracts).toHaveLength(1); expect(r.errors[0].errors[0]).toMatch(/duplicate/);
  });
  it('external дрейф → в driftFlags, контракт всё равно загружен (с пометкой)', () => {
    const ext = { skill: 'e', kind: 'external', needs: [], produces: [], constraints: [], 'preview-form': 'none', defaults: [], 'key-decisions': [], 'acceptance-criteria': [], source: { version: '1', hash: 'f'.repeat(64) } };
    const r = buildRegistry([{ contract: ext, currentContent: 'changed body' }]);
    expect(r.contracts).toHaveLength(1); expect(r.driftFlags).toHaveLength(1); expect(r.driftFlags[0].skill).toBe('e');
  });
});
```

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

```js
#!/usr/bin/env node
/**
 * skill-contract-registry — сборка набора контрактов скилов. Чистый buildRegistry
 * (над записями {contract, currentContent?}) + loadRegistry (диск, fs инъектируется).
 * Валидирует каждый, для external сверяет дрейф G4. Вход для машины охвата 3-C.
 */
import fsDefault from 'node:fs';
import { validateContract, normalizeContract, checkContractDrift } from './skill-contract.mjs';

/** Чистая сборка: валидирует, ловит дубли, помечает дрейф external. */
export function buildRegistry(entries) {
  const contracts = [], errors = [], driftFlags = [], seen = new Set();
  for (const e of entries || []) {
    const c = normalizeContract(e.contract);
    const v = validateContract(c);
    if (!v.ok) { errors.push({ skill: c.skill || '(?)', errors: v.errors }); continue; }
    if (seen.has(c.skill)) { errors.push({ skill: c.skill, errors: ['duplicate skill contract'] }); continue; }
    seen.add(c.skill);
    if (c.kind === 'external') {
      const d = checkContractDrift({ contract: c, currentContent: e.currentContent });
      if (d.drifted) driftFlags.push({ skill: c.skill, reason: d.reason, fallback: d.fallback });
    }
    contracts.push(c);
  }
  return { contracts, errors, driftFlags };
}
```

- [ ] **Step 4: GREEN.**
- [ ] **Step 5: Commit** — `git -C "<worktree>" add tools/skill-contract-registry.mjs tools/skill-contract-registry.test.mjs` затем commit `"feat(m3-a): buildRegistry — validate + dedupe + drift-flag"`

---

## Task 5: loadRegistry — загрузка с диска (fs инъектируется)

**Files:** Modify `tools/skill-contract-registry.mjs`, `tools/skill-contract-registry.test.mjs`

- [ ] **Step 1: Падающие тесты (добавить)**

```js
import { loadRegistry } from './skill-contract-registry.mjs';

function memFs(map) {
  return {
    readdirSync: () => Object.keys(map).map((p) => p.split('/').pop()),
    readFileSync: (p) => { const k = String(p); if (!(k in map)) { const e = new Error('ENOENT'); e.code='ENOENT'; throw e; } return map[k]; },
  };
}

describe('loadRegistry (диск, fs инъектируется)', () => {
  it('читает *.contract.json, валидирует, external тянет source.path для дрейфа', () => {
    const ownC = JSON.stringify({ skill: 'wp', kind: 'own', needs: [], produces: [], constraints: [], 'preview-form': 'none', defaults: [], 'key-decisions': [], 'acceptance-criteria': [] });
    const extC = JSON.stringify({ skill: 'pd', kind: 'external', needs: [], produces: [], constraints: [], 'preview-form': 'none', defaults: [], 'key-decisions': [], 'acceptance-criteria': [], source: { version: '1', hash: '0'.repeat(64), path: '/skills/pd/SKILL.md' } });
    const map = { '/c/wp.contract.json': ownC, '/c/pd.contract.json': extC, '/skills/pd/SKILL.md': 'current body' };
    const r = loadRegistry({ dir: '/c', fsImpl: memFs(map) });
    expect(r.contracts).toHaveLength(2);
    expect(r.driftFlags).toHaveLength(1); // hash 0..0 ≠ хеш 'current body'
  });
  it('игнорирует не-.contract.json', () => {
    const map = { '/c/readme.md': 'x' };
    const r = loadRegistry({ dir: '/c', fsImpl: memFs(map) });
    expect(r.contracts).toEqual([]); expect(r.errors).toEqual([]);
  });
});
```

> **NB по memFs:** `readdirSync` отдаёт basename'ы, `readFileSync` ждёт полный путь `${dir}/${file}`. В `loadRegistry` собираем путь как `` `${dir}/${f}` ``.

- [ ] **Step 2: RED.**
- [ ] **Step 3: Реализация (дописать в `tools/skill-contract-registry.mjs`)**

```js
/** Загрузка с диска: dir/*.contract.json. Для external читает source.path
 *  (актуальный SKILL.md) для дрейф-сверки G4, если путь задан и доступен. */
export function loadRegistry({ dir, fsImpl = fsDefault }) {
  const files = fsImpl.readdirSync(dir).filter((f) => f.endsWith('.contract.json'));
  const entries = files.map((f) => {
    const raw = JSON.parse(fsImpl.readFileSync(`${dir}/${f}`, 'utf8'));
    let currentContent;
    if (raw && raw.kind === 'external' && raw.source && raw.source.path) {
      try { currentContent = fsImpl.readFileSync(raw.source.path, 'utf8'); } catch { currentContent = undefined; }
    }
    return { contract: raw, currentContent };
  });
  return buildRegistry(entries);
}
```

- [ ] **Step 4: GREEN.**
- [ ] **Step 5: Commit** — `git -C "<worktree>" commit -am "feat(m3-a): loadRegistry — disk loader, external pulls source.path for G4"`

---

## Task 6: Образцы контрактов + инварианты 3-A + регрессия

**Files:** Create `docs/registry/contracts/writing-plans.contract.json`, `docs/registry/contracts/operations-process-doc.contract.json`, `tools/m3a-contract-invariants.test.mjs`

- [ ] **Step 1: Образец own** — `docs/registry/contracts/writing-plans.contract.json`

```json
{
  "skill": "writing-plans",
  "kind": "own",
  "needs": ["spec or requirements", "file structure decisions"],
  "produces": ["implementation-plan with bite-sized TDD tasks"],
  "constraints": ["only writes the plan file", "no code, no reads beyond plan authoring"],
  "preview-form": "outline",
  "defaults": ["one action per step (2-5 min)", "test->RED->code->GREEN->commit"],
  "key-decisions": ["file structure / decomposition", "task granularity"],
  "acceptance-criteria": ["every step has concrete content (no placeholders)", "types consistent across tasks"]
}
```

- [ ] **Step 2: Образец external** — `docs/registry/contracts/operations-process-doc.contract.json`

```json
{
  "skill": "operations:process-doc",
  "kind": "external",
  "needs": ["as-is process description"],
  "produces": ["structured process documentation"],
  "constraints": ["marketplace skill — outputs doc only"],
  "preview-form": "none",
  "defaults": ["follow operations plugin process-doc template"],
  "key-decisions": ["scope of the process being documented"],
  "acceptance-criteria": ["doc covers all process steps and owners"],
  "source": { "version": "1.2.0", "hash": "0000000000000000000000000000000000000000000000000000000000000000", "path": "" }
}
```

> **NB:** `hash` образца — плейсхолдер нулями + `path: ""` (пустой → дрейф-сверка пропускается, `currentContent` undefined → external без отпечатка-сверки даст «нет содержания»: при пустом path loadRegistry не читает файл, buildRegistry получает `currentContent: undefined` → `contractHash(undefined)` ≠ нули → дрейф-флаг). Чтобы образец грузился без шумного дрейф-флага в инварианте, в тесте подаём его через `buildRegistry` с `currentContent`, чей хеш = нули НЕ совпадёт — поэтому инвариант проверяет, что образец ВАЛИДЕН по форме (а не отсутствие дрейфа). Реальный отпечаток проставляется при наполнении (вне 3-A).

- [ ] **Step 3: Инвариант-тест** — `tools/m3a-contract-invariants.test.mjs`

```js
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { validateContract } from './skill-contract.mjs';

const here = dirname(fileURLToPath(import.meta.url));
const contractsDir = join(here, '..', 'docs', 'registry', 'contracts');

describe('Машина 3-A — инварианты контрактов', () => {
  it('образец own (writing-plans) валиден по форме', () => {
    const c = JSON.parse(readFileSync(join(contractsDir, 'writing-plans.contract.json'), 'utf8'));
    expect(validateContract(c)).toEqual({ ok: true, errors: [] });
  });
  it('образец external (operations:process-doc) валиден по форме', () => {
    const c = JSON.parse(readFileSync(join(contractsDir, 'operations-process-doc.contract.json'), 'utf8'));
    expect(validateContract(c).ok).toBe(true);
  });
});
```

> **NB путь:** тест-файл лежит в `<worktree>/tools/`, контракты — в `<worktree>/docs/registry/contracts/`. `join(here, '..', 'docs', ...)` поднимается из `tools/` в корень worktree. Проверить фактический `here` при RED; если конфиг резолвит из другого корня — поправить относительный путь (записать в журнал, если разойдётся).

- [ ] **Step 4: GREEN** — `npx vitest run --root ".claude/worktrees/brainrepo/app" --config vitest.config.tools.mjs m3a-contract-invariants`

- [ ] **Step 5: Полная регрессия 3-A + всё** — `npx vitest run --root ".claude/worktrees/brainrepo/app" --config vitest.config.tools.mjs` → все прежние + M1 + M2 + 3-A зелёные.

- [ ] **Step 6: Commit** — `git -C "<worktree>" add docs/registry/contracts/ tools/m3a-contract-invariants.test.mjs` затем commit `"test(m3-a): contract sample fixtures (own+external) + 3-A invariants"`

---

## Self-Review (против канона §2 3-A + roadmap C-13)
- **Схема `{needs/produces/constraints + preview-form/defaults/key-decisions/acceptance-criteria}`** → Task 1 `CONTRACT_FIELDS` + `validateContract`. ✅
- **Свои контракты пишем сами** → формат own + образец writing-plans (Task 6). ✅
- **Чужие — тонкий адаптер + G4 хеш-страж дрейфа (версия+хеш, расхождение → флаг + откат на мягкое рассуждение)** → Task 3 `checkContractDrift` (`fallback: 'soft-reasoning'`) + external `source{version,hash,path}` + образец external (Task 6). ✅
- **Фундамент для машины охвата 3-C** → `needsOf`/`producesOf` (Task 2) + `buildRegistry`/`loadRegistry` (Task 4/5) дают набор контрактов с рёбрами. ✅
- **Без LLM, «умный не баран»** → весь модуль чистый, операции над структурой. ✅
- **Портативность** → имена скилов не зашиты в код, лежат данными в `docs/registry/contracts/`. ✅
- **[ВОПРОС записан в журнал]** G1–G3/G5–G6 в спеке поимённо НЕ перечислены (назван только G4) → реализован G4 + механические валидаторы формы; остальные гард-рейлы уточнить у владельца/при сборке 3-C.
- **[ДОПУЩЕНИЕ]** PREVIEW_FORMS список (`none/outline/mockup/sample/dry-run/diagram`) — выведен из духа L1, не зафиксирован дословно в спеке → расширяемый.
- **[ДОПУЩЕНИЕ]** контракты хранятся как `docs/registry/contracts/*.contract.json` рядом с `docs/registry/nodes.yaml` → согласовать с 3-B (граф узлов из реестра).

**Заглушек нет. Само-ослабление контракта (M5) и массовое наполнение — вне 3-A (отмечено).**
