Files
transcriber/openspec/changes/archive/2026-08-14-record-ownership/tasks.md
T
av 8af8ec2e54 у записи появился владелец: чужую больше не отдают
- колонка `owner` связью с `users` в обеих коллекциях новым шагом схемы
  `202608140001`; чтение задачи сужено владельцем, и чужая, ничья и
  несуществующая дают один ответ; правило просмотра файлов сужено им же
- приём по HTTP берёт владельца из сессии, а предъявителя без учётной записи
  пользователя отвергает до чтения тела: позже пришлось бы убирать уложенный
  файл, а уборки файлов сервис не умеет. Выборка воркера владельцем не сужается
- удаление учётной записи с записями отвергается стражем, и вешает его сама
  сборка хранилища: сборка, забывшая его позвать, теряла защиту молча
2026-08-14 12:18:11 +03:00

11 KiB

Критерии приёмки задачи

Дословно из tasks/items/record-ownership.md. Файл задачи закрытие удалит — критерии обязаны его пережить.

  • Запрос чужой записи по её идентификатору возвращает «не найдено», а не содержимое и не «доступ запрещён». Оракул — тест: две сессии, задача первой запрашивается второй, ответ 404 и пустое тело. Расхождение: «пустое тело» разошлось с нетронутым сценарием спеки, где ответ по неизвестному идентификатору несёт сообщение о ненайденной задаче. Читается как «то же тело, что и у неизвестного идентификатора, и без состояния и текста расшифровки» — иначе реализация по букве критерия молча снимет действующее требование о сообщении.
  • Запись, заведённая из веба, принадлежит вошедшему. Оракул — тест приёма по HTTP со сверкой колонки владельца.
  • Выборка воркера владельцем не сужается: конвейер обрабатывает записи всех. Оракул — тест: задачи двух владельцев проходят конвейер одним воркером.
  • База заводится с чистого листа, колонка владельца обязательна и без умолчания. Оракул — прогон миграций на пустой базе и попытка вставки без владельца.

Четвёртый критерий разошёлся с рамками той же задачи и правится решением человека на чекпоинте: рамки запрещают назначать владельца записям из Telegram, а обязательная колонка такую запись вставить не даст. Разобрано в design.md, «Open Questions»; ниже план написан по рекомендованному способу — колонка без умолчания, допускающая пустое значение, обязательность приёма по HTTP держит код.

Рубрика ревью дизайна — приёмочные критерии

Порождена проходом rubric до чтения артефактов. Приёмка судится по одному списку: этот блок и блок выше.

  1. Чужая запись неотличима от несуществующей на всех наблюдаемых осях: тот же код, то же тело, та же форма ответа.
  2. Сужение живёт в одном месте пути чтения и обязательно к употреблению: второго читающего входа, у которого сужение можно не позвать, нет.
  3. Пустой владелец на входе чтения не совпадает ни с одной записью — своей, чужой и ничьей.
  4. Записи без владельца недостижимы сужённым путём никому, и недостижимость выведена из правила, а не из того, что таких записей мало.
  5. Владелец назначается сервером из сессии: поле владельца, пришедшее запросом, на результат не влияет.
  6. Владелец появляется в той же операции, что и запись; отказ по отсутствию владельца не оставляет ни задачи, ни файла, и способ этого назван.
  7. Неизменность владельца держит механизм, а не обещание: назван каждый путь, которым владельца можно переписать, и что его удерживает.
  8. Колонка прочитана всеми четырьмя местами правки колонок очереди плюс шагом схемы.
  9. Воркер владельцем не сужается, и это записано нормой, а не оставлено умолчанию.
  10. Судьба записи при исчезновении владельца определена.
  11. Шаг схемы: одно представление «владельца нет», откат не требует переписывания применённого шага.
  12. Проверка владельца и чтение состояния берутся из одного чтения записи, а не двумя раздельными.
  13. Ответ отправителю адресуется по источнику записи, а не по владельцу: запись без владельца получает свой ответ в чат.
  14. Ни ответ, ни журнал не выдают того, что прячет разграничение; различение чужой и несуществующей в журнале допустимо.

1. Схема хранилища

  • 1.1 Новый шаг схемы 202608140001_record_owner.go: колонка owner в таблице задач связью с коллекцией users, без умолчания, пустое значение допустимо, каскадное удаление выключено. Имя колонки — owner, поле сущности — OwnerID; имена названы здесь, потому что разойтись им есть где — семь мест плюс подпись метода
  • 1.2 Тем же шагом — колонка owner в таблице файлов, теми же свойствами; правило просмотра коллекции файлов сужается владельцем вместо прежнего «всякий вошедший»
  • 1.3 Тем же шагом — запрет удаления учётной записи, у которой остались задачи: отказ с причиной, а не снятие ссылки
  • 1.4 Прежние шаги схемы не тронуты — проверяется шагом гейта migrations
  • 1.5 docs/database.md: строки колонок в обеих таблицах, правило выборки по владельцу, новое правило просмотра файлов и запрет удаления учётной записи

2. Сущность и контракт

  • 2.1 internal/entity: у задачи расшифровки появляется владелец
  • 2.2 internal/contract: читающий метод репозитория задач принимает владельца; второго читающего метода не заводится

3. Хранилище задач

  • 3.1 applyToRecord кладёт владельца при заведении
  • 3.2 recordToJob читает владельца
  • 3.3 acquireColumns и acquiredRow читают колонку владельца
  • 3.4 applyOwnedByPipeline владельца не трогает
  • 3.5 Чтение задачи сужено владельцем: задача другого владельца и задача без владельца отдают ту же ошибку, что и несуществующая

4. Приём и опрос

  • 4.1 internal/service: метод заведения задачи из веба принимает владельца и отказывает при пустом; метод заведения из Telegram владельца не назначает. Владелец кладётся и на файл, заводимый при приёме
  • 4.2 internal/controller/http: приём берёт владельца из предъявленной сессии
  • 4.3 internal/controller/http: опрос готовности передаёт владельца в хранилище и отвечает 404 с прежним телом на чужую и на ничью задачу

5. Проверки

  • 5.1 Тест: две сессии, задача первой запрашивается второй — 404, тело без состояния и текста
  • 5.2 Тест: приём по HTTP заводит задачу с владельцем-предъявителем
  • 5.3 Тест: приём по HTTP без узнанной учётной записи задачи не заводит
  • 5.4 Тест: задачи двух владельцев и задача без владельца проходят конвейер одним воркером
  • 5.5 Тест: шаг конвейера, сохраняющий результат, владельца не затирает
  • 5.6 Тест: миграции на пустой базе заводят колонку без умолчания
  • 5.7 Тест: чтение с пустым владельцем не отдаёт ни своей, ни чужой, ни ничьей задачи
  • 5.8 Тест: сессия без учётной записи пользователя получает 403 на приёме, файла и задачи не заводится — узнан, но не запись коллекции пользователей
  • 5.9 Тест: вошедший просит токен чужого файла — отказ; своего — успех
  • 5.10 Тест: удаление учётной записи с задачами отвергается, без задач — проходит
  • 5.11 task gate зелёный

6. Архивация

  • 6.1 На архивации выправить ## Purpose спеки access: преамбула переживает слияние дельт дословно и сегодня утверждает, что разграничения по владельцу нет. Валидатор преамбулу не судит — вспомнить об этом больше некому
  • 6.2 Сверить docs/security.md, docs/passport.md и docs/architecture.md (строка «Разграничения записей по владельцу здесь нет»): все три обещают закрытие разграничением именно этой задачей. Адрес в architecture.md назван отдельно — его нашло ревью, и без него обзор архитектуры отправлял бы следующего читателя чинить уже закрытое