docs: канон приведён к сегодняшнему состоянию после сверки
- периметр: passport.md и security.md больше не утверждают, что HTTP API открыт без аутентификации, а review.md не числит эту находку типовой ложноположительной — приём, опрос и файл закрыты сессией с 2026-08-12; - logging.md писал, что расширение попадает в журнал полем пути: описано изъятие инварианта приватности — собственное поле, имени и пути нет; - узел ревью переименован в repo/pocketbase, поведение конвейера из обзора уехало ссылкой в спеку pipeline, Purpose спеки storage написан вместо заглушки, в ADR о переезде дописано уточнение о действующей раскладке.
This commit is contained in:
@@ -67,6 +67,10 @@ PocketBase заменяет SQLite с goqu и goose и берёт на себя
|
||||
`pb_data/storage/<коллекция>/<запись>/<имя>_<10 случайных символов>` рядом с
|
||||
файлом атрибутов. Момент перехода назначает человек; данные прежней базы не
|
||||
переносятся по прежнему решению задачи `pocketbase-storage`.
|
||||
*Уточнено 2026-08-12:* каталог задаётся ключом `[storage] data_dir` со
|
||||
значением `data`. Суффикс из десяти знаков дописывает конструктор имени,
|
||||
которого сервис не зовёт, — имя задаёт он сам. Действующая раскладка —
|
||||
[../database.md](../database.md), «Представление данных».
|
||||
- `−` вход перестаёт быть нашим: задача `oidc-login` переписывается с
|
||||
собственной обработки ответа провайдера на настройку провайдера в PocketBase.
|
||||
Что делать с сессией и где она живёт, решает уже не наш код.
|
||||
|
||||
+24
-14
@@ -12,9 +12,12 @@
|
||||
потребителей; пятая — исключение из первого абзаца: она нормирует не сервис, а
|
||||
инструмент, которым его собирают, и потребитель у неё другой — тот, кто собирает.
|
||||
|
||||
- [intake](../openspec/specs/intake/spec.md) — **только приём по HTTP**: его
|
||||
нормируют проверки, написанные задачей `http-handler-tests-never-green`
|
||||
2026-08-11;
|
||||
- [intake](../openspec/specs/intake/spec.md) — **только приём по HTTP**: приём и
|
||||
опрос за сессией, имя отправителя не доходит ни до хранилища, ни до журнала,
|
||||
метка метрики несёт только известное расширение. Задачи
|
||||
`http-handler-tests-never-green` и `no-user-filename-in-log` 2026-08-11,
|
||||
`pocketbase-storage` и `oidc-login` 2026-08-12. Приём из Telegram здесь не
|
||||
описан;
|
||||
- [pipeline](../openspec/specs/pipeline/spec.md) — пустой прогон воркера, захват
|
||||
задачи и срок его протухания, число попыток, состояние «мертва» и пауза перед
|
||||
повтором: задачи `errors-as-instead-of-typecast` 2026-08-11 и
|
||||
@@ -40,14 +43,17 @@
|
||||
|
||||
- **Один процесс.** Бот, HTTP-сервер и фоновые воркеры живут в одном бинарнике и
|
||||
делят одну базу. Отдельного воркер-процесса нет намеренно.
|
||||
- **Очередь таблицей.** Состояние задачи лежит коллекцией хранилища, воркер
|
||||
забирает работу одним запросом с захватом. Внешний брокер не заводим: нагрузка
|
||||
— единицы записей в день (оценка владельца, не замер). Готовую библиотеку
|
||||
очереди тоже не заводим — решено 2026-08-11,
|
||||
- **Очередь таблицей.** Состояние задачи лежит коллекцией хранилища; неделимость
|
||||
захвата и порядок выборки нормирует
|
||||
[pipeline](../openspec/specs/pipeline/spec.md), «Захват задачи неделим».
|
||||
Внешний брокер не заводим: нагрузка — единицы записей в день (оценка владельца,
|
||||
не замер). Готовую библиотеку очереди тоже не заводим — решено 2026-08-11,
|
||||
[ADR](adr/ADR-2026-08-11-queue-as-pocketbase-collection.md), сравнение
|
||||
кандидатов в [research/job-queue.md](research/job-queue.md).
|
||||
- **Шаг конвейера идемпотентен по повтору.** Задача, брошенная на середине,
|
||||
достаётся снова по истечении срока захвата и проходит шаг заново.
|
||||
- **Шаг конвейера идемпотентен по повтору.** Что делает срок захвата и когда
|
||||
задача возвращается в работу, нормирует
|
||||
[pipeline](../openspec/specs/pipeline/spec.md), «Брошенная задача возвращается
|
||||
в работу»; здесь это принцип письма шага, а не описание поведения.
|
||||
- **Ядро зависит от интерфейсов.** `internal/service` знает только
|
||||
`internal/contract`; ffmpeg, Yandex, Telegram и хранилище подставляются в
|
||||
`main.go`.
|
||||
@@ -149,11 +155,15 @@
|
||||
|
||||
## Открытые вопросы
|
||||
|
||||
- **Учётные записи.** Вход через OIDC, провайдер — Authelia, а ответ провайдера
|
||||
обрабатывает PocketBase, а не наш код
|
||||
([ADR](adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md)). Не решено, где живёт сессия
|
||||
и как связываются пользователь Telegram и пользователь веба. Панель
|
||||
администратора при этом Authelia не закрывает: у неё свой пароль
|
||||
- **Учётные записи.** Вход через OIDC решён и развёрнут 2026-08-12: провайдер —
|
||||
Authelia, ответ провайдера обрабатывает PocketBase, а не наш код
|
||||
([ADR](adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md)), сессия
|
||||
живёт кукой `transcriber_session` и сама себя не продлевает. Норма —
|
||||
[access](../openspec/specs/access/spec.md), решения —
|
||||
[ADR-2026-08-12-session-without-refresh](adr/ADR-2026-08-12-session-without-refresh.md)
|
||||
и [ADR-2026-08-12-oidc-exchange-via-own-route](adr/ADR-2026-08-12-oidc-exchange-via-own-route.md).
|
||||
**Не решено одно:** как связываются пользователь Telegram и пользователь веба.
|
||||
Панель администратора при этом Authelia не закрывает: у неё свой пароль
|
||||
суперпользователя.
|
||||
- **Приложение.** Экранов нет вовсе, есть только API. Решено делать SPA,
|
||||
устанавливаемое на телефон, а фреймворком взят Vue 3 с роутером пятой версии и
|
||||
|
||||
@@ -130,7 +130,10 @@ TOML. Пустой токен бота ловится в `NewTelegramController`
|
||||
## Структура в коде
|
||||
|
||||
- Весь разбор и проверка — в `internal/config`; наружу отдаётся готовая `Config`.
|
||||
- Одна корневая структура `Config` с под-структурами по секциям (`Server`,
|
||||
`Database`, `Storage`, `Yandex`, `Telegram`).
|
||||
- Одна корневая структура `Config` с под-структурами по секциям. Перечень секций
|
||||
и полей здесь не повторяем: источник истины по составу — `config.dist.toml`,
|
||||
действующие числа — [../database.md](../database.md), «Настройки с числовым
|
||||
значением». Каталог данных задаётся одним ключом `[storage] data_dir`
|
||||
([ADR](../adr/ADR-2026-08-12-single-data-dir-config-key.md)).
|
||||
- Умолчания задаются в `defaultConfig()`, файл их перекрывает. Новое поле
|
||||
требует правки обоих мест.
|
||||
|
||||
@@ -135,8 +135,11 @@ transcriber — **приложение, а не библиотека**: внеш
|
||||
- Не для управления потоком и не для ожидаемых ошибок (нет сети, плохой ввод) —
|
||||
это значения `error`.
|
||||
- `recover` — на верхней границе обработчика, чтобы один паникующий запрос не
|
||||
ронял процесс. В transcriber это `gin.Recovery()`; у воркеров и у бота такой
|
||||
границы **нет**: паника в шаге конвейера роняет процесс целиком.
|
||||
ронял процесс. В transcriber его вешает роутер хранилища сам
|
||||
(`apis.panicRecover`, слой с идентификатором `DefaultPanicRecoverMiddlewareId`
|
||||
на каждом роутере PocketBase): паникующий обработчик отдаёт `500`, процесс
|
||||
живёт. Своего слоя мы не пишем. У воркеров и у бота такой границы **нет**:
|
||||
паника в шаге конвейера роняет процесс целиком.
|
||||
|
||||
## Несколько ошибок
|
||||
|
||||
|
||||
@@ -241,11 +241,14 @@ Object Storage, скачивание файла из Telegram и опрос оп
|
||||
она не логируется — то есть утечки нет, но защищает от неё только отсутствие
|
||||
строки лога.
|
||||
|
||||
*Расхождение:* расширение берётся из имени отправителя дословно
|
||||
*Изъятие, а не расхождение:* расширение берётся из имени отправителя дословно
|
||||
(`filepath.Ext`), поэтому имя `запись.тайное-слово` отдаёт приватный хвост
|
||||
расширением, и в журнал оно попадает полем пути. Наружу — в метку метрики — этот
|
||||
хвост не выходит: там расширение приводится к перечню известных форматов. Остаток
|
||||
описан в [../security.md](../security.md).
|
||||
расширением. В журнал оно идёт **собственным полем** строки приёма — это
|
||||
объявленное изъятие инварианта приватности ([CLAUDE.md](../../CLAUDE.md),
|
||||
«Инварианты»); ни имени файла в хранилище, ни пути к нему в журнале нет вовсе
|
||||
(норма — `openspec/specs/intake`). Наружу — в метку метрики — хвост не выходит:
|
||||
там расширение приводится к перечню известных форматов. Остаток описан в
|
||||
[../security.md](../security.md).
|
||||
|
||||
## Куда пишем и уровень
|
||||
|
||||
|
||||
+17
-9
@@ -22,7 +22,7 @@
|
||||
| Владелец сервиса | Загрузить диктофонную запись или видео из семейного архива с телефона и получить текст. Видеть, кто сколько загрузил и во что это обошлось |
|
||||
| Приглашённый пользователь | Войти в приложение через свою учётную запись, загрузить запись, забрать текст, вернуться к ней через месяц. Приложение ставится на телефон; каждый видит только свои записи |
|
||||
| Пользователь Telegram | Отправить боту голосовое сообщение и получить текст ответом. Работает сегодня |
|
||||
| Внешняя программа | Отдать файл по HTTP, представившись своим токеном, и опросить готовность. Работает сегодня, но токенов нет и доступ не разграничен |
|
||||
| Внешняя программа | Отдать файл по HTTP, представившись своим токеном, и опросить готовность. Сегодня почти не работает: приём и опрос закрыты сессией OIDC, а своего токена у программы нет — годится только кука, снятая из браузера. Токен приносит `api-tokens` |
|
||||
|
||||
**Основной вход — приложение**, бот и HTTP API дополняют его. До 2026-08-11
|
||||
основным был бот, и порядок здесь перевёрнут сознательно: диктофонная запись на
|
||||
@@ -68,8 +68,11 @@
|
||||
файл. Своей записи и работы без сети не делаем — граница цели
|
||||
[web-access](../tasks/items/web-access.md).
|
||||
- **Файловое хранилище общего назначения.** Храним аудио и видео, отданные ради
|
||||
речи в них. Складом произвольных файлов, папками и общим доступом к чужим
|
||||
записям сервис не становится.
|
||||
речи в них. Складом произвольных файлов и папками сервис не становится. Общего
|
||||
доступа к чужим записям целью тоже нет — но **сегодня он есть**: владельца у
|
||||
записи в модели данных не существует, и всякий вошедший видит все записи
|
||||
([security.md](security.md), «Периметр»). Это состояние, а не решение;
|
||||
закрывает его `record-ownership`.
|
||||
- **Учёт денег.** Считаем объём, минуты и токены по каждому пользователю и
|
||||
показываем их владельцу. Цен, счетов и отказов по исчерпании квоты не делаем:
|
||||
пользователя, потратившего слишком много, останавливает разговор или отзыв
|
||||
@@ -95,8 +98,10 @@
|
||||
отличает их по MIME-типу и расширению. Работает сегодня.
|
||||
5. **Загрузка по HTTP.** Программа шлёт `POST /api/audio` со своим токеном,
|
||||
получает идентификатор задачи и опрашивает `GET /api/status/:id`, пока не
|
||||
увидит `done` и текст. Работает сегодня, но без токена и без разграничения
|
||||
доступа.
|
||||
увидит `done` и текст. Сегодня доступно только предъявившему сессию OIDC:
|
||||
анонимный запрос обоими адресами отклоняется. Своего входа у программы нет —
|
||||
его заводит `api-tokens`, — как нет и разграничения записей между
|
||||
пользователями.
|
||||
6. **Отказ на середине.** Конвертация или распознавание не удались — задача
|
||||
переходит в `failed`, а пользователь получает сообщение о том, что именно не
|
||||
вышло, и предложение повторить.
|
||||
@@ -109,7 +114,10 @@
|
||||
которой пользуемся: она и задаёт потолок по длине записи и формату.
|
||||
- **Whisper и его серверные обёртки** — запасной путь, если внешний сервис
|
||||
перестанет устраивать по цене или по качеству русской речи.
|
||||
- **PocketBase** — хранилище взамен сегодняшнего SQLite, решено 2026-08-11
|
||||
([adr](adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md)). Учётные
|
||||
записи оно хранит и получает от Authelia своим провайдером OIDC, но источником
|
||||
их не становится: заводит и проверяет людей по-прежнему Authelia.
|
||||
**PocketBase** из референсов ушла: она больше не кандидат — в стек её перевела
|
||||
задача `pocketbase-storage` 2026-08-12
|
||||
([adr](adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md)); там она
|
||||
держит хранилище, файлы и панель владельца. Схема и
|
||||
раскладка — [database.md](database.md). Учётные записи она хранит и получает от
|
||||
Authelia своим провайдером OIDC, но источником их не становится: заводит и
|
||||
проверяет людей по-прежнему Authelia.
|
||||
|
||||
+15
-8
@@ -38,12 +38,16 @@
|
||||
- вырожденный ответ (пустой, усечённый, без ожидаемого поля) не превращает в
|
||||
успех молча.
|
||||
|
||||
**Репозиторий SQLite** (`adapter/repo/sqlite`):
|
||||
**Репозиторий хранилища** (`internal/adapter/repo/pocketbase`):
|
||||
|
||||
- список колонок совпадает во всех четырёх запросах файла;
|
||||
- `NULL` в колонке разбирается в указатель, а не роняет `Scan`;
|
||||
- захват задачи не выдаёт одну строку двум вызывающим;
|
||||
- ошибка драйвера транслируется в доменную у источника.
|
||||
- список колонок совпадает во всех четырёх местах — `applyToRecord`,
|
||||
`recordToJob`, `acquireColumns`, `acquiredRow` — и в шаге схемы (инвариант
|
||||
[CLAUDE.md](../CLAUDE.md), «Инварианты»);
|
||||
- захват задачи не выдаёт одну строку двум вызывающим, а результат пишет только
|
||||
держатель захвата;
|
||||
- репозиторий кладёт время в сыром запросе тем же видом, каким хранилище пишет
|
||||
свои `created`/`updated` ([database.md](database.md), «Представление данных»);
|
||||
- отказ хранилища не выходит наружу дословно: он несёт ключ файла целиком.
|
||||
|
||||
**Обёртка над внешним процессом** (`adapter/converter/ffmpeg`,
|
||||
`adapter/metaviewer/ffmpeg`):
|
||||
@@ -95,9 +99,12 @@
|
||||
- **«Файлы и объекты не удаляются, диск растёт».** Факт верный и записан в
|
||||
[database.md](database.md); срок хранения не задан сознательно, задачи на него нет.
|
||||
Новой находкой это не считается, пока не измерен рост.
|
||||
- **«HTTP API открыт без аутентификации».** Известно и записано первой строкой
|
||||
[security.md](security.md). Находкой считается только новая поверхность,
|
||||
выставленная наружу, а не повторение этого факта.
|
||||
- **«У записи нет владельца: вошедший видит чужие записи».** Не дефект и не
|
||||
новость: приём, опрос и файл закрыты сессией OIDC с 2026-08-12, а
|
||||
разграничения по владельцу нет сознательно — [security.md](security.md),
|
||||
«Периметр», и `openspec/specs/access`, «Purpose». Находкой считается новая
|
||||
поверхность, выставленная наружу, либо путь к содержимому записи **без**
|
||||
сессии, а не повторение этого факта.
|
||||
|
||||
### Вопросы по темам
|
||||
|
||||
|
||||
+8
-6
@@ -53,8 +53,8 @@
|
||||
|
||||
| Вход | Канал | Кто может слать |
|
||||
| --- | --- | --- |
|
||||
| Аудиофайл и его имя | `POST /api/audio`, multipart-поле `audio` | Любой из интернета |
|
||||
| Идентификатор задачи | `GET /api/status/:id` | Любой из интернета |
|
||||
| Аудиофайл и его имя | `POST /api/audio`, multipart-поле `audio` | Любой вошедший через OIDC; без сессии — `401` до чтения тела |
|
||||
| Идентификатор задачи | `GET /api/status/:id` | Любой вошедший через OIDC; без сессии — `401`, одинаковый для заведённой и незаведённой задачи |
|
||||
| Голосовое, аудио, документ | Telegram, длинный опрос | Любой пользователь Telegram; обрабатывается только из белого списка |
|
||||
| Имя файла в Telegram | Поле `file_path` ответа Bot API | Telegram, а через него — отправитель |
|
||||
| Содержимое аудио | Файл, скармливаемый `ffmpeg` и `ffprobe` | Отправитель по любому из каналов |
|
||||
@@ -283,10 +283,12 @@ Telegram отправителю.
|
||||
- **Стойкость к целенаправленной нагрузке.** Ограничения по числу запросов и по
|
||||
размеру файла нет, и защищаться от исчерпания диска мы сейчас не пытаемся.
|
||||
- **Исчерпание диска приглашёнными.** Записи и тексты хранятся бессрочно
|
||||
(паспорт, 2026-08-11), шестичасовая запись весит единицы гигабайт — оценка, а не
|
||||
замер: `research/` пуст, потолок длины стоит открытым вопросом
|
||||
`architecture.md`, «Долгие записи», — а квот нет и не будет: решено считать расход и показывать его владельцу, а не отказывать
|
||||
(цель `usage-stats`). Перебравшего останавливает разговор или отзыв доступа в
|
||||
(паспорт, 2026-08-11). Шестичасовая запись весит единицы гигабайт — оценка, а
|
||||
не замер: распределения длин у сервиса нет, а самая длинная проверенная запись
|
||||
— 9,6 МБ ([research/pocketbase-defaults.md](research/pocketbase-defaults.md)).
|
||||
Потолок длины стоит открытым вопросом `architecture.md`, «Долгие записи». Квот
|
||||
нет и не будет: решено считать расход и показывать его владельцу, а не
|
||||
отказывать (цель `usage-stats`). Перебравшего останавливает разговор или отзыв доступа в
|
||||
Authelia. Рост каталога данных при этом ничем не наблюдается —
|
||||
открытый вопрос `architecture.md`.
|
||||
- **Перерасход денег на внешних сервисах.** Распознавание и языковая модель
|
||||
|
||||
@@ -199,7 +199,7 @@ Telegram делит с ним общий шаг заведения задачи,
|
||||
|
||||
- **GIVEN** источник метаданных читает запись и отдаёт её длительность
|
||||
- **WHEN** программа шлёт запись с именем, чей хвост после последней точки не
|
||||
принадлежит перечню known-форматов
|
||||
принадлежит перечню известных форматов
|
||||
- **THEN** метка метрики принимает значение `other`
|
||||
- **AND** имя файла в хранилище сохраняет пришедшее расширение
|
||||
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
# storage Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change pocketbase-storage. Update Purpose after archive.
|
||||
|
||||
Где живут запись, её метаданные и её файл: раскладка каталога данных, приведение
|
||||
схемы при подъёме, отдача файла ссылкой по токену, собственная поверхность
|
||||
хранилища и панель владельца.
|
||||
|
||||
Приём и опрос готовности нормирует `intake`, вход и сессию — `access`.
|
||||
Сознательно не описаны: перенос прежних данных — его нет по решению задачи
|
||||
`pocketbase-storage`; удаление записей и файлов — сервис объявлен архивом
|
||||
2026-08-11, а удаление приносит задача `delete-record`.
|
||||
|
||||
## Requirements
|
||||
### Requirement: Сервис поднимается на чистом каталоге данных
|
||||
|
||||
|
||||
@@ -162,10 +162,10 @@ Capability нормирует **не поведение сервиса** для
|
||||
и MUST не зависеть от рабочего каталога, из которого запущен. Шаг MUST только
|
||||
читать: файлов он не правит и разошедшихся мест не чинит.
|
||||
|
||||
Коды выхода MUST следовать словарю прочих проверочных шагов проекта: 0 сошлось,
|
||||
1 расхождение, 2 ошибка употребления, 3 окружение. Своего словаря шаг MUST не
|
||||
заводить: раздел «Гейт» в `CLAUDE.md` объявляет словарь общим, и четвёртый шаг с
|
||||
собственной семантикой сделал бы это утверждение неверным.
|
||||
Коды выхода MUST следовать общему словарю проверочных шагов проекта; словарь
|
||||
объявляет раздел «Гейт» в `CLAUDE.md`, и здесь он не повторяется. Своего словаря шаг
|
||||
MUST не заводить: четвёртый шаг с собственной семантикой сделал бы это
|
||||
утверждение неверным.
|
||||
|
||||
Место, где числа не нашлось вовсе, MUST считаться отказом с именем этого места.
|
||||
«Нечего сравнивать» исходом MUST не быть: пропавшая строка иначе выглядела бы
|
||||
|
||||
Reference in New Issue
Block a user