- re-read `source_type` перечитывается под блокировкой после тик-снимка, а остаточное окно вывода имени названо известным ограничением с ценой и достоверным маршрутом восстановления (ручной шаг + `Retry`, не самоисцеление) - в `ingest` снята парная ложная гарантия «воркер добавит раздачу файлом», добавлены сценарии на оба окна апгрейда и на недоступные байты `.torrent` - заведён ADR о том, что при разрыве спека↔код двигается тот, чья формулировка сильнее рационали
6.5 KiB
Спека следует за кодом, когда гарантия недостижима, а окно узкое
- Дата: 2026-08-06
- Источник: 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.mdD6). - B — привести спеку к коду (принято). Наблюдаемое поведение прежнее, меняется заявленное.
- C — оставить как есть. Отвергнут: расхождение спека↔код воспроизводится каждым аудитом, а читатель спеки считает окно закрытым.
Последствия
+Нормативный дом поведения перестал утверждать недостижимое; ограничение видно и имеет названную цену вместо молчания.+Парная ложная гарантия снята и вingest(«воркер добавит раздачу файлом») — иначе она бы просто переехала в соседнюю capability и всплыла следующим аудитом.+Заодно назван исход ветки «сохранённые байты.torrentнедоступны», не заказанной до этого ни одним сценарием.−Цена окна выше, чем считала постановка задачи: не «подождать и нажатьRetry», а ожиданиеmagnet_timeout(дефолт24h) плюс ручной шаг — убрать зависшую раздачу из qBittorrent.Retryсам не добивает:metaDLсчитается живым и здоровым торрентом, иRetryк нему перецепляется без повторногоadd(design.mdD2). На этой пересмотренной цене вопрос «чинить ли окно кодом» открыт заново.−Новая нормативная ветка (недоступные байты.torrent) держится на чтении кода: теста-оракула у неё нет, её регрессия зелёный гейт не покрасит.−Прецедент «спека следует за кодом» опасен буквальным применением. Он оправдан только когда формулировка сильнее рационали и ущерб от разрыва назван; «код так делает, значит так и запишем» этой записью не санкционируется.
Триггер пересмотра
Записан отдельно, чтобы не гонять круг заново:
Окно возвращается в работу вариантом A, когда апгрейд в вызове namer'а случится в эксплуатации хотя бы раз — признак в логах — либо когда стоимость ручного шага станет заметной. До того — принято и описано.