local-qa · git:20260924.2784cb2 · 2026-09-24 · sha256 b8470efe325fe773

local-qa git:20260924.2784cb2A

Immutable. This exact content never changes and is served at /api/v1/blob/b8470efe325fe773.

---
name: local-qa
description: >
  Ворота ПОСТОЯННОЙ ЖИЗНИ детали: у каждой запчасти, которую мы изобрели / починили / перепочинили /
  подкорректировали, обязаны появиться два спутника — ТЕСТ, который гоняет регресс-сетка, и ПАСПОРТ,
  по которому её починит слабейший починщик. Триггеры: "/qa", "полный QA", "сделай QA", "покрой тестами",
  "напиши тест на", "паспорт детали", "документация на деталь", "обнови документацию", "тесты написаны?",
  "документация есть?", "cover with tests", "write a passport". Делает 8 шагов: (0) RECALL и границы детали;
  (1) прогон /tt, если ещё не было (горячая проверка — не дублируем); (2) РЕПРО-тест, который КРАСНЕЕТ на
  баге, если деталь чинили; (3) написать тест по чек-листу 10 классов (встроенный `--selftest` или файл
  `_test_X.py`); (4) доказать, что СЕТКА его видит и гоняет — «тест есть» ≠ «тест зелёный»;
  (5) сломать деталь нарочно и убедиться, что тест краснеет (тест, ни разу не покрасневший, не тест);
  (6) паспорт по единому шаблону + обновление при каждой перепочинке; (7) видимость — карта покрытия,
  MOC контура, пер-машинность отчёта; (8) вердикт числом из сетки, а не словом «покрыто».
  НЕ ДУБЛЬ `/tt`: `/tt` = «работает ли то, что мы только что собрали» (разово, горячо);
  `/qa` = «заметим ли мы, когда оно сломается, и сможет ли Антон починить это без меня» (навсегда).
  Канон: `reglament-test-i-dokumentaciya-na-kazhduyu-zhivuyu-detal` (+ поправки 28.07 и 29.07),
  `reglament-tri-pravila-protiv-[человек]-kachestva`, [[coverage-and-docs-every-part]],
  [[repairability-first-how-we-build]], [[fix-root-cause-not-symptoms]], [[silent-degradation-vs-crash]],
  [[test-after-build-skill]]; движки: `regress_run.py`, `coverage_map.py`, `part_passport.py`.
permissions: [filesystem, shell]
risk_level: shell-local
processes_untrusted_data: false
disable-model-invocation: false
origin: [машина флота]
related: reglament-test-i-dokumentaciya-na-kazhduyu-zhivuyu-detal
---

# /qa — тест и паспорт на каждую деталь

> 🧒 **При докладе Антону:** заверши блоком «Простыми словами» ([[eli5-always]]). Только ему — не внутри паспортов.

**Когда зову:** родилась НОВАЯ деталь · починил старую · чиню ВТОРОЙ раз ту же (перепочинка) · подкрутил поведение. Любой из четырёх случаев = деталь меняется, значит её спутники устарели.

**Граница с соседями (не дублируем, каждый уровень ссылается вниз — АК-47):**
| | вопрос | когда | живёт сколько |
|---|---|---|---|
| `/tt` | работает ли то, что мы только что собрали? | сразу после сборки, горячий контекст | один прогон |
| **`/qa`** | **заметим ли, когда сломается, и починит ли это Антон без меня?** | **перед тем как отпустить деталь в жизнь** | **навсегда: тест в сетке + паспорт** |
| `/retro` | что мы вообще насобирали за сессию | конец сессии | аудит строки «покрыто? ✅/❌» |

---

## Шаг 0 — RECALL и границы детали

- Что именно правили: файл, зона (`~/.claude/scripts` · `$IMPORTS_ROOT` · `$MACHINE_BUS_DIR/_engine` · скилл · хук).
- Деталь уже известна системе? `grep -n "<имя>" 00-System/Coverage-Map.md` и `ls 00-System/Parts/*/<имя>.md`.
- Есть ли уже тест/паспорт — их **обновляем**, а не пишем второй ([[«один источник, ноль копий»|coverage-and-docs-every-part]]).
- Деталь МЁРТВАЯ (никто не ссылается, не трогали 30 дней) и мы её не оживляем → **не покрываем, а предлагаем снести** (`00-System/Dead-Code-Candidates.md`). Док, который врёт, дороже отсутствующего.
- ⚠️ НОВАЯ деталь покрывается ВСЕГДА, безусловно — «подождём, приживётся ли» запрещено (anton 28.07).

## Шаг 1 — `/tt` прогнан?

Нет → сперва `/tt` (живой прогон + внешний ломатель Codex/Grok/Gemini). `/qa` не заменяет горячую проверку и не повторяет её.

## Шаг 2 — Чинили? Сперва тест, который КРАСНЕЕТ

Порядок при починке и особенно при **перепочинке**:

1. Написать проверку, воспроизводящую баг **на текущем сломанном коде** → она обязана упасть. Не упала — ты не понял баг, лечишь не то.
2. Только потом чинить корень ([[fix-root-cause-not-symptoms]]).
3. Прогнать снова → зелёный. Проверка остаётся в сетке навсегда как регрессия.

**Чиню второй раз ту же деталь** = сигнал, что первый фикс лечил симптом. Тогда обязательно: строка в паспорт в раздел «что ломается чаще всего» + вопрос «какой КЛАСС я закрываю?» (одна деталь чинится дважды → скорее всего болеют и соседи по контуру).

## Шаг 3 — Написать тест: чек-лист 10 классов

**Куда:** встроенный `--selftest` внутри самой детали — дефолт (едет вместе с деталью на все машины, невозможно «забыть доставить»); отдельный `_test_<имя>.py` — когда нужны тяжёлые фикстуры или деталь нельзя дёрнуть флагом. `.ps1` → `-SelfTest` + `Invoke-SelfTest`.

**Правила прогона:** только фейковые данные во временной папке (`tempfile.TemporaryDirectory`), ни одного живого файла флота; ни одной живой отправки наружу (транспорты — заглушками); встроенный selftest обязан укладываться в **60 секунд** (лимит сетки), поэтому реальные таймауты не «выжидаем», а временно подменяем константы модуля.

**Чек-лист (проходить по всем десяти, отсутствие пункта — осознанное решение, а не забывчивость):**

1. **[человек] путь** — делает то, ради чего написана.
2. **Второй актор** — параллельный писатель/процесс: ничего не потеряно (гонка настоящими процессами, не только в голове).
3. **Повтор** — идемпотентность: второй одинаковый вызов не плодит дубль. Проверять по РЕАЛЬНО записанному маркеру, а не по тому, что «мы вроде бы пишем».
4. **Мусорный вход** — пусто, без обязательной шапки, кривой путь, отсутствующая зависимость.
5. **Отказ громкий** — при отказе exit-код ненулевой И состояние НЕ изменилось (проверить размер/хэш файла после отказа).
6. **Exit-коды** — каждый описанный код воспроизведён тестом. Код, который никто не проверил, врёт первым.
7. **Кодировки и пути** — BOM, CRLF, UTF-16, `C:` vs `/Users`, `$VARS` вместо хардкода машины.
8. **Слой видимости** — доказательство работы существует: лог, счётчик, строка. «Сработало, но не видно» = ❌ ([[silent-degradation-vs-crash]]).
9. **Ноль-результат ≠ здоровье** — пустая папка / нет данных / нечего сторожить обязаны давать ТРЕВОГУ, а не тихий зелёный ноль.
10. **Тихая деградация** — упавшая рельса/недоставленное сообщение не превращаются в `exit 0`.

## Шаг 4 — Доказать, что СЕТКА его гоняет

Тест, который никто не запускает, не существует (канон 29.07):
```bash
python3 ~/.claude/scripts/regress_run.py --list | grep -i "<имя детали>"
```
Не нашёлся → **корень чинить в сетке, а не в своей памяти**: деталь лежит в зоне, которую сетка не обходит (так `[шина]/_engine` был невидим до 29.07), либо имя файла попало в исключения (`daemon`, `server`, `watcher_loop`, `listener`), либо встроенный selftest не распознан (нужны И флаг `--selftest`, И `def selftest`).

## Шаг 5 — Сломать нарочно: тест обязан покраснеть

Тест, который **ни разу не был красным**, не тест, а украшение. Испортить деталь на минуту (убрать гард, вернуть старую строку) → прогнать → убедиться, что покраснел именно тот пункт → вернуть. Если с первого написания всё зелёное — почти всегда проверяется не то.

## Шаг 6 — Документация: docstring в коде, паспорт только для меж-файлового

⭐ **Поправка канона 07.08 (§5.8, вышла ПОСЛЕ рождения скилла): у Python-детали основной док = docstring В САМОМ ФАЙЛЕ, отдельный md-пересказ одного файла ⛔.** Docstring несёт: назначение · вход/выход · кто дёргает · рельса (чей оплаченный бак жжёт) · updated-дата; правка кода = правка docstring **тем же коммитом**; тест назван в docstring. Паспорт в `00-System/Parts/<контур>/` пишется только когда есть **МЕЖ-файловая картина** (деталь + сторож + промпты + launchd — как связаны и кто кого будит); пересказывать в нём содержимое одного файла — дубль, который разъедется.

Генератор паспортов: `python ~/.claude/scripts/part_passport.py` (ест `coverage_map.py --dump-rows`); руками пишутся только поля, которых машина не знает.

Поля (docstring — обязательные; паспорт — когда картина шире файла):
- **Что делает** — одной фразой, без жаргона;
- **Вход / Выход** — аргументы и что реально появляется на диске/экране;
- **Кто дёргает** — рутины, промпты, другие детали (иначе паспорт не ответит на «а кто её вызывает?»);
- **Зависит от**;
- **⭐ Что ломается чаще всего** — руками, из РЕАЛЬНЫХ сбоев, а не из фантазии;
- **⭐ Как понять, что сломалась** — exit-коды, строка в логе, отсутствие heartbeat, счётчик;
- **⭐ Как починить руками** — конкретные команды, первым делом прогон selftest;
- **Тест** — путь, число проверок, что именно они сторожат;
- **Граница** — чего деталь НЕ делает (тут рождаются ложные ожидания).

**Обновление дока = часть починки, а не «потом».** Поменял поведение/флаг/exit-код → правишь docstring (и паспорт, если он есть) в том же заходе. Поймал новый сбой → он дописывается в «что ломается чаще всего» вместе с лечением. Перепочинка начинается с чтения дока: если он описывает старое поведение, ты чинишь по вранью.

⚠️ MOC контура и общие реестры — **дописываем строку, не перегенерируем файл целиком** ([[reglament-obshchiy-fayl-nikogda-ne-perepisyvay-tselikom]]).

⚠️ **Паспорт ездит дальше, чем движок.** Паспорта лежат в волте (все 6 узлов), а детали — в `~/.claude/scripts` (receive-only, кладёт хаб). Поэтому на узле легко встретить документацию детали, которой тут нет (поймано 29.07: `have.py part_passport.py` → «✅ ЕСТЬ» и показал `.md`, а самого генератора на [машина флота] нет). Два следствия: (1) в паспорте пишем РЕАЛЬНЫЙ путь и, если деталь hub-only, говорим это прямо, а не оставляем `[путь владельца]`; (2) прежде чем сказать «движка нет» или отдать работу другому узлу — `python3 ~/.claude/scripts/have.py <имя>`, и проверить, что совпадение не документация ([[text-detectors-overcount-use-ground-truth]], гейт доставки `lint_script_delivery.py`).

## Шаг 7 — Видимость и пер-машинность

- Карта покрытия: `python3 ~/.claude/scripts/coverage_map.py --json` — деталь обязана числиться живой, с тестом и доком.
- Строка в MOC контура + счётчик деталей обновлён.
- ⚠️ Отчёт о прогоне — **пер-машинный**. Общий файл, куда пишут все узлы, теряет чужие результаты молча (замер 28–29.07: ноут написал в 16:38, хаб затёр в 18:28, красные тесты ноута исчезли). Общий допустим только как сводка, пересобираемая из пер-машинных файлов.
- Деталь живёт в синкаемой зоне → доставка по [[reglament-lyubaya-pochinka-raskatyvaetsya-na-ves-flot]]: пакет в `_transit`, `deploy_register.py --tier 1` с машинными apply/verify, табло `fleet_fix_audit.py`. `~/.claude/**` у пиров receive-only — правку туда кладёт ХАБ, локальная будет откачена.

## Шаг 8 — Вердикт ЧИСЛОМ

```bash
python3 ~/.claude/scripts/regress_run.py        # ⚠️ не на бегу: полный прогон = минуты
```
В отчёте Антону: **сколько зелёных / сколько красных / что именно красное**. Формулировки «тесты написаны», «покрыто», «документация есть» без числа из сетки — запрещены, это то же самое, что «раскатано» без таблицы применения.

- ✅ **PASS** — тест в сетке, зелёный, хоть раз краснел; паспорт с тремя ручными полями; деталь видна в карте покрытия.
- ⚠️ **PARTIAL** — что-то из этого отсутствует; назвать поимённо и завести задачу.
- ❌ **FAIL** — тест не гоняется сеткой ИЛИ паспорт описывает не то поведение.

## Границы

- Markdown-оркестратор, своего движка НЕ заводит: гоняет `regress_run.py`, `coverage_map.py`, `part_passport.py`. (Новый движок = новая деталь, которой снова нужны тест и паспорт — так растёт долг.)
- Не покрываем мёртвое: его сносим списком через Антона (снос необратим).
- Секреты и содержимое `secrets/` в паспорта не попадают.
- Публичная документация — отдельный контур (Mission-2) и отдельный гейт согласования.