download-tracking: требование о re-read source_type приведено к коду

- re-read `source_type` перечитывается под блокировкой после тик-снимка, а
  остаточное окно вывода имени названо известным ограничением с ценой и
  достоверным маршрутом восстановления (ручной шаг + `Retry`, не самоисцеление)
- в `ingest` снята парная ложная гарантия «воркер добавит раздачу файлом»,
  добавлены сценарии на оба окна апгрейда и на недоступные байты `.torrent`
- заведён ADR о том, что при разрыве спека↔код двигается тот, чья формулировка
  сильнее рационали
This commit is contained in:
av
2026-08-06 14:44:21 +03:00
parent f42db0a275
commit 30ee598547
11 changed files with 1179 additions and 11 deletions
@@ -0,0 +1,80 @@
# Спека следует за кодом, когда гарантия недостижима, а окно узкое
- **Дата:** 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'а
> случится в эксплуатации хотя бы раз — признак в логах — либо когда стоимость
> ручного шага станет заметной. До того — принято и описано.
+1
View File
@@ -42,6 +42,7 @@
| Дата | Запись | Статус |
| --- | --- | --- |
| 2026-08-06 | [Спека следует за кодом, когда гарантия недостижима, а окно узкое](ADR-2026-08-06-spec-follows-code-on-narrow-window.md) | — |
| 2026-08-04 | [Конвейер ревью и пайплайн задачи переезжают в плагины](ADR-2026-08-04-review-pipeline-to-plugin.md) | — |
| 2026-07-24 | [Локальная сборка образа + доставка docker save/load](ADR-2026-07-24-local-image-build.md) | — |
| 2026-07-23 | [Конвейер ревью: гейт, generative-проходы и триаж](ADR-2026-07-23-review-pipeline-generative.md) | — |