Files
jellybit/openspec/specs/identity/spec.md
T
av d081ef1d30 ingest: закрыты мелочи приёма — вырожденное имя, контракт Result, корреляция add
- имя раздачи нормализуется на границе разбора: вырожденное `-`
  (metainfo.NoName) даёт пустое имя, пробельное схлопывается — сентинел больше
  не доходит ни до контекста распознавания, ни до source_ref, ни до подсказки
  вывода имени
- контракт «на любом пути ошибки приёма результат нулевой» объявлен в ingest и
  удерживается структурно; три транспорта перестали обещать идентификатор,
  которого нет, и коррелируют отказ по request_id
- scoped-логгер загрузки ставится до вызова внешнего сервиса в семи командах
  воркера — записи об отказе qBittorrent и метабаз получили download_id
  и infohash; граница разбора bencode записана в docs/research
2026-08-06 18:20:12 +03:00

119 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# identity Specification
## Purpose
Как система идентифицирует сущности домена: ULID-ключи (канонический
lowercase-вид, нормализация и валидация на входных границах) и корреляция
сущностей в логах по id. Инфохэши загрузки (`download_infohash`),
дедупликация приёма и инвариант «не более одной активной загрузки на
infohash» (атомарный возврат в активное состояние) — в capability `ingest`.
## Requirements
### Requirement: ULID как первичный ключ сущностей
Каждая сущность домена SHALL иметь первичный ключ ULID — TEXT, 26 символов
Crockford base32, генерируемый приложением в момент создания записи через
единственную точку генерации (`internal/ident`). Сущности: `download`,
`recognition`, `hint`, `override`, `metadata_candidate`, `file_link`.
Канонический вид SHALL быть lowercase. Числовые AUTOINCREMENT-ключи в новых таблицах
использоваться SHALL NOT. Идентификатор партии раскладки (`apply_batch_id`)
SHALL генерироваться тем же способом.
#### Scenario: Создание загрузки
- **WHEN** принимается новая загрузка
- **THEN** её `id` — валидный ULID в lowercase
- **AND** `id` уникален глобально (не совпадает с id других сущностей)
#### Scenario: Хронологическая сортировка
- **GIVEN** две загрузки, созданные последовательно
- **WHEN** записи сортируются по `id` лексикографически
- **THEN** порядок совпадает с порядком создания
### Requirement: Нормализация и валидация id на входных границах
Внешние идентификаторы SHALL валидироваться как ULID и нормализоваться к
lowercase до обращения к хранилищу — это касается всех входных границ:
URL `/download/{id}`, параметры форм и команд. Синтаксически невалидный id SHALL обрабатываться как
несуществующая сущность (404 для страниц), без обращения к БД.
#### Scenario: Uppercase-вариант id в URL
- **GIVEN** существующая загрузка с id `01jz…` (lowercase)
- **WHEN** клиент открывает `/download/01JZ…` (uppercase)
- **THEN** открывается страница той же загрузки
#### Scenario: Мусор вместо id
- **WHEN** клиент открывает `/download/abc!!!`
- **THEN** ответ — 404, запрос к БД не выполняется
### Requirement: Корреляция сущностей в логах
Записи журнала, относящиеся к сущности, SHALL содержать её id в атрибуте
`<entity>_id` (`download_id`, `recognition_id`, `batch_id`, …); работа в
контексте загрузки ведётся через scoped-логгер с `download_id`. Благодаря
глобальной уникальности ULID поиск по значению id (grep/jq) SHALL находить
все записи журнала, относящиеся к сущности, независимо от имени поля.
Scoped-логгер SHALL передаваться через `context`, а не доклеиваться к каждой
записи руками. Отсюда обязанность вызывающего, и она ограничена наблюдаемым
исходом: **операция, работающая в контексте загрузки и делающая вызов внешнего
сервиса, SHALL положить scoped-логгер этой загрузки в `context` до такого
вызова** — включая команды, пришедшие с транспорта, а не только фоновый цикл
воркера. Однородность формы у команд, внешних вызовов не делающих, это
требование не нормирует: она принадлежит конвенциям кода.
Причина в том, что клиент внешнего сервиса своей доменной сущности не знает и
знать SHALL NOT — он берёт логгер из `context`. Поэтому вызов внешнего сервиса в
контексте загрузки SHALL давать запись с `download_id` и, когда он известен,
`infohash`; добавлять клиенту поля-дубликаты доменных идентификаторов ради этого
SHALL NOT — источник корреляции один.
Перечень клиентов, ведущих записи о внешних вызовах, живёт в
`docs/conventions/logging.md` и здесь не дублируется. Telegram-клиент таких
записей не ведёт, и уведомление отправляется вне контекста загрузки намеренно
(иначе оно умирало бы вместе с тиком) — это требование его не касается.
Отдельно это важно там, где внешний сервис не сообщает причину отказа: ответ
qBittorrent `Fails.` на добавление раздачи причины не несёт, и единственное, что
делает такую запись пригодной для разбора, — корреляция с загрузкой.
#### Scenario: Путь загрузки по логам
- **GIVEN** загрузка прошла приём, распознавание и раскладку
- **WHEN** журнал фильтруется по значению её `id`
- **THEN** находятся записи всех этапов (ingest, recognition, file-layout)
#### Scenario: Неуспешное добавление в qBittorrent с пути retry
- **GIVEN** загрузка в `failed` с известным инфохэшем, для которой оператор
запросил retry
- **WHEN** qBittorrent отвечает на добавление отказом (`Fails.` либо не-200)
- **THEN** запись о вызове внешнего сервиса содержит `download_id` и `infohash`
загрузки
- **AND** запись находится тем же фильтром по значению id, что и записи
фонового пути добавления
### Requirement: Миграция существующих записей
Существующие записи SHALL получить ULID-идентификаторы одной миграцией с
сохранением всех связей (FK) и хронологии: timestamp-часть ULID SHALL
браться из `created_at` записи, чтобы лексикографический порядок новых id
соответствовал историческому порядку создания. Существующий
`download.infohash` SHALL быть перенесён в `download_infohash`
(нормализация к lowercase, `kind` по длине hex: 40 — `v1`, 64 — `v2`);
столбцы `download.infohash` и `download.idempotency_key` SHALL быть удалены.
#### Scenario: Связи и порядок после миграции
- **GIVEN** БД с загрузками, распознаваниями и файловыми ссылками на
числовых id
- **WHEN** миграция выполнена
- **THEN** все FK-связи сохранены (распознавания/ссылки указывают на те же
загрузки)
- **AND** порядок загрузок по `id` совпадает с порядком по `created_at`
- **AND** каждый прежний `infohash` представлен записью в
`download_infohash`