# Спека следует за кодом, когда гарантия недостижима, а окно узкое - **Дата:** 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'а > случится в эксплуатации хотя бы раз — признак в логах — либо когда стоимость > ручного шага станет заметной. До того — принято и описано.