- re-read `source_type` перечитывается под блокировкой после тик-снимка, а остаточное окно вывода имени названо известным ограничением с ценой и достоверным маршрутом восстановления (ручной шаг + `Retry`, не самоисцеление) - в `ingest` снята парная ложная гарантия «воркер добавит раздачу файлом», добавлены сценарии на оба окна апгрейда и на недоступные байты `.torrent` - заведён ADR о том, что при разрыве спека↔код двигается тот, чья формулировка сильнее рационали
81 lines
6.5 KiB
Markdown
81 lines
6.5 KiB
Markdown
# Спека следует за кодом, когда гарантия недостижима, а окно узкое
|
||
|
||
- **Дата:** 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'а
|
||
> случится в эксплуатации хотя бы раз — признак в логах — либо когда стоимость
|
||
> ручного шага станет заметной. До того — принято и описано.
|