# Спека следует за кодом, когда гарантия недостижима, а окно узкое

- **Дата:** 2026-08-06
- **Источник:**
  [openspec/changes/archive/2026-08-06-catched-source-type-reread-wording/design.md](../../openspec/changes/archive/2026-08-06-catched-source-type-reread-wording/design.md)

## Решение

Требование `download-tracking` о re-read `source_type` приведено **к коду**, а
не наоборот: перечитывать под блокировкой переходов **после тик-снимка**, а
остаточное окно (апгрейд magnet → `.torrent`, легший в вызов namer'а) названо в
спеке известным ограничением с ценой и маршрутом восстановления. Код не тронут.

Общее правило, которое отсюда следует для проекта: когда спека и код разошлись,
**двигается тот, чья формулировка сильнее рационали**. Требование, обещавшее
больше, чем нужно ради его собственной причины, чинится текстом; недостающая
гарантия чинится кодом.

## Почему

Формулировка была сильнее своей же рационали:

> Рациональ исходного требования — «не полагаться на снимок, снятый ранее вне
> блокировки» — выполнен первым re-read под замком. Формулировка «непосредственно
> перед добавлением» была сильнее рационали и кодом не достигается: между
> re-read и `qbt.Add` стоит namer, вынесенный из-под блокировки намеренно
> (требование «Медленные вызовы SHALL выполняться вне блокировки»).

Цена починки кодом оказалась несоразмерной ущербу:

> Вариант A (пересобирать `addReq` из `before` под замком) отклонён: это не
> однострочник — `sourceAddParts` читает байты `.torrent` и держать его под
> блокировкой нельзя, а при апгрейде корректен был бы и повторный вызов namer'а
> (подсказка имени берётся из другого источника).

Главное же — **молчащая ложная гарантия дороже названного ограничения**. Пока
спека утверждала недостижимое, дефект был невидим ровно потому, что нормативный
дом поведения его отрицал; аудит capability находил его заново.

## Рассмотренные варианты

- **A — починить код** (пересобирать запрос на добавление из свежей записи под
  блокировкой). Отвергнут по цене: чтение блоба `.torrent` под замком
  недопустимо, корректная версия тянет повторный вызов namer'а. Отвергнут
  **отложенно, а не окончательно**: спека поэтому не запрещает его нормативно
  (см. `design.md` D6).
- **B — привести спеку к коду** (принято). Наблюдаемое поведение прежнее,
  меняется заявленное.
- **C — оставить как есть.** Отвергнут: расхождение спека↔код воспроизводится
  каждым аудитом, а читатель спеки считает окно закрытым.

## Последствия

- `+` Нормативный дом поведения перестал утверждать недостижимое; ограничение
  видно и имеет названную цену вместо молчания.
- `+` Парная ложная гарантия снята и в `ingest` («воркер добавит раздачу
  файлом») — иначе она бы просто переехала в соседнюю capability и всплыла
  следующим аудитом.
- `+` Заодно назван исход ветки «сохранённые байты `.torrent` недоступны», не
  заказанной до этого ни одним сценарием.
- `−` Цена окна выше, чем считала постановка задачи: не «подождать и нажать
  `Retry`», а ожидание `magnet_timeout` (дефолт `24h`) **плюс ручной шаг** —
  убрать зависшую раздачу из qBittorrent. `Retry` сам не добивает: `metaDL`
  считается живым и здоровым торрентом, и `Retry` к нему перецепляется без
  повторного `add` (`design.md` D2). На этой пересмотренной цене вопрос
  «чинить ли окно кодом» открыт заново.
- `−` Новая нормативная ветка (недоступные байты `.torrent`) держится на чтении
  кода: теста-оракула у неё нет, её регрессия зелёный гейт не покрасит.
- `−` Прецедент «спека следует за кодом» опасен буквальным применением. Он
  оправдан **только** когда формулировка сильнее рационали и ущерб от разрыва
  назван; «код так делает, значит так и запишем» этой записью не
  санкционируется.

## Триггер пересмотра

Записан отдельно, чтобы не гонять круг заново:

> Окно возвращается в работу вариантом A, когда апгрейд в вызове namer'а
> случится в эксплуатации хотя бы раз — признак в логах — либо когда стоимость
> ручного шага станет заметной. До того — принято и описано.
