From 2c123762625fc2266ae4ce79a7ade6f045e7b9cb Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Thu, 13 Aug 2026 16:00:31 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=83=D0=BF=D1=80=D0=B0=D0=B7=D0=B4=D0=BD=D1=91?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B9=20=D1=80=D0=BE=D0=B0=D0=B4=D0=BC=D0=B0?= =?UTF-8?q?=D0=BF=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B0=D0=B4=D1=80=D0=B5=D1=81?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B,=20=D1=83=20=D0=B2=D0=BE=D1=81?= =?UTF-8?q?=D1=8C=D0=BC=D0=B8=20=D1=84=D0=B0=D0=BA=D1=82=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=20=D0=B4=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ссылки на tasks/ROADMAP.md переведены на BACKLOG.md и на openspec/specs, упоминания целей — на задачи, которые эту работу делают; - судьи документации нашли восемь расхождений: Purpose спеки pipeline объявлял неописанным то, что уже нормирован пятью требованиями, вид времени в конвенции спорил со схемой, а квоты, шесть часов и отказ от Web Push жили сразу в двух документах без ссылки друг на друга; - два числа получили провенанс: 259 200 запросов в сутки и потолок в шесть часов теперь ведут к записке разведки, а не читаются как замер. --- docs/architecture.md | 16 ++++++++++------ docs/conventions/database.md | 5 +++++ docs/conventions/web-ui.md | 21 +++++++++++++-------- docs/database.md | 4 ++-- docs/passport.md | 12 ++++++------ docs/research/spa-framework.md | 3 ++- docs/review.md | 12 ++++++++---- docs/security.md | 9 +++++---- openspec/config.yaml | 3 ++- openspec/specs/pipeline/spec.md | 16 ++++++++-------- 10 files changed, 61 insertions(+), 40 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 5373517..9b6fe55 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -5,7 +5,7 @@ помечены маркером долга и переезжают туда первой же задачей, которая их трогает. Документ описывает **сегодняшнее** устройство. Куда проект идёт — в -[passport.md](passport.md) и в [tasks/ROADMAP.md](../tasks/ROADMAP.md); что из +[passport.md](passport.md) и в [tasks/BACKLOG.md](../tasks/BACKLOG.md); что из этого ещё не решено — в разделе «Открытые вопросы». Заведены пять capability. Четыре первые нормируют **поведение сервиса** для его @@ -81,7 +81,7 @@ | Отправитель Telegram | `internal/adapter/telegram` | Отправка текста, деление длинного по словам | | Репозитории | `internal/adapter/repo/pocketbase` | Задачи и файлы коллекциями хранилища; захват — сырым запросом | | Шаги схемы | `internal/adapter/repo/pocketbase/migrations` | Файл на шаг, имя файла — имя шага; там же имена коллекций | -| Панель владельца | `internal/adapter/repo/pocketbase`, `panel.go` | Правка задачи в панели проходит те же правила перехода, что и правка из кода | +| Панель владельца | `internal/adapter/repo/pocketbase`, `panel.go` | Панель хранилища; правила правки задачи нормирует [storage](../openspec/specs/storage/spec.md), «Владелец видит записи в панели» | @@ -192,8 +192,11 @@ текст расшифровки начинает уходить на сторону — сдвиг периметра [security.md](security.md). - **Долгие записи.** Потолок сегодня неизвестен и не замерялся: 20 МиБ на приём - из Telegram — точно, ограничения `deferred-general` по длине — нет. Расчётный - потолок проекта — шесть часов, и он взят с запасом, а не замером. + из Telegram — точно, ограничения `deferred-general` по длине — нет. Расчётные + шесть часов нормирует [storage](../openspec/specs/storage/spec.md), «Файл + записи живёт в хранилище»; откуда взято число — + [research/pocketbase-defaults.md](research/pocketbase-defaults.md), «Чего эта + записка не узнала». - **Приём большого файла.** Форма читается целиком, предел памяти под multipart задан числом в [database.md](database.md), «Настройки с числовым значением»; обрыв начинает загрузку заново. @@ -218,8 +221,9 @@ - **Очередь.** Модель очереди сделана задачей `pocketbase-storage` 2026-08-12 ([ADR](adr/ADR-2026-08-11-queue-as-pocketbase-collection.md)) и нормирована спекой `pipeline`. Не решено, отказываться ли от холостого опроса: три воркера - дают 259 200 запросов в сутки при нагрузке в единицы записей в день, и во что - это обходится, никто не мерил. + дают 259 200 запросов к базе в сутки — расчёт из паузы воркера, а не замер + ([research/job-queue.md](research/job-queue.md), «Как снималось»), — при + нагрузке в единицы записей в день, и во что это обходится, никто не мерил. - **Наблюдаемость.** `/metrics` остаётся и развивается. Чем — дописывать счётчики через `client_golang` или перейти на OpenTelemetry с трассировкой — решает разведка `opentelemetry-fit`. Коллектор был бы процессом, которого в diff --git a/docs/conventions/database.md b/docs/conventions/database.md index 0964e26..0b648b6 100644 --- a/docs/conventions/database.md +++ b/docs/conventions/database.md @@ -58,6 +58,11 @@ лексикографическую сортировку TEXT, то есть хронологию (`ORDER BY created_at`). Единая точка генерации — приложение, а не умолчание в схеме: так забытая вставка падает громко. Измерение длительности — не метка времени. + *Расхождение:* вид времени задаёт хранилище — `2006-01-02 15:04:05.000Z`, + пробел вместо `T` и доли секунды ([../database.md](../database.md), «Время»). + Правило RFC 3339 действует на то, что пишем мы сами мимо хранилища; вид + хранилища не меняем — сравнение строк в сыром запросе побайтово, и + разошедшийся вид молча обращает условие срока захвата в константу. - Миграции — шаги PocketBase на Go (`internal/adapter/repo/pocketbase/migrations`, файл на шаг): коллекции и их поля заводятся кодом. При изменении структуры обновляем схему diff --git a/docs/conventions/web-ui.md b/docs/conventions/web-ui.md index 44c8eb4..ead9724 100644 --- a/docs/conventions/web-ui.md +++ b/docs/conventions/web-ui.md @@ -33,11 +33,13 @@ - **Шрифты и скрипты — со своего хоста**, без внешних. Внешних ресурсов времени выполнения нет. - **Офлайн-чтения расшифровок и очереди отправки без сети не делаем** — граница - цели [web-access](../../tasks/items/web-access.md). Без сети приложение - показывает состояние, а не пустой экран. -- **Web Push не делаем**: уведомления идут через apprise и ntfy, цель - [ready-notification](../../tasks/items/ready-notification.md). -- **Записи звука в приложении не делаем** — файл выбирают системным диалогом. + из [паспорта](../passport.md). Без сети приложение показывает состояние, а не + пустой экран. +- **Web Push не делаем**: уведомления идут через apprise и ntfy — решение живёт + в [architecture.md](../architecture.md), «Уведомления», делает его + [ntfy-delivery](../../tasks/items/ntfy-delivery.md). +- **Записи звука в приложении не делаем** — граница из + [паспорта](../passport.md), «Диктофон»; файл выбирают системным диалогом. ## Фреймворк и сборка @@ -55,9 +57,12 @@ ## Маршруты -- **Четыре экрана, одна таблица маршрутов** через `createRouter`. Маршруты по - файлам не включаем: сборочная надстройка роутера пятой версии стоит 34 пакета - в установке и на четырёх маршрутах не окупается. +- **Одна таблица маршрутов** через `createRouter`. Маршруты по файлам не + включаем: сборочная надстройка роутера пятой версии стоит 34 пакета в + установке и на нашем числе маршрутов не окупается + ([research/spa-framework.md](../research/spa-framework.md), «Vue»). Сколько + экранов и какие — не здесь: состав нормирует спека приложения, а до неё его + держит [spa-skeleton](../../tasks/items/spa-skeleton.md). - **Адреса обычные, а не после решётки** (`createWebHistory`). Отсюда требование к серверу: неизвестный путь **вне** `/api/` отдаёт `index.html`, а не `404`; пути внутри `/api/` в приложение не проваливаются никогда. diff --git a/docs/database.md b/docs/database.md index 70ee25b..27d4e91 100644 --- a/docs/database.md +++ b/docs/database.md @@ -16,8 +16,8 @@ CGO сборке не нужен. Каталог у шагов свой, а не файл внутри пакета репозитория, и причина внешняя: шаг гейта сверяет изменённые шаги схемы с правкой этого документа по **префиксу -пути** (`.av-dev.toml`, ключ `migrations` секции `[docs]`), а префикс наводится только на -каталог. Имена коллекций живут там же, рядом с шагом, который их заводит; пакет +пути**, а префикс наводится только на каталог. Где этот префикс задан — +[conventions/go-linters.md](conventions/go-linters.md), «Механизировано». Имена коллекций живут там же, рядом с шагом, который их заводит; пакет репозитория берёт их оттуда. **Идентификаторы** записей выдаёт хранилище — 15 знаков собственного алфавита. diff --git a/docs/passport.md b/docs/passport.md index f58127a..bd8a9f0 100644 --- a/docs/passport.md +++ b/docs/passport.md @@ -1,7 +1,7 @@ # Паспорт проекта Зачем это и для кого. [architecture.md](architecture.md) отвечает «как -устроено», [tasks/ROADMAP.md](../tasks/ROADMAP.md) — «в каком порядке», паспорт — +устроено», [tasks/BACKLOG.md](../tasks/BACKLOG.md) — «в каком порядке», паспорт — «зачем и для кого». ## Цель @@ -32,8 +32,8 @@ - запись любого распространённого формата принимается без предварительной подготовки, включая дорожку из видео; -- запись длиной до шести часов доходит до текста, а не прерывается ошибкой при - достижении предела; +- запись расчётного потолка — шести часов — доходит до текста, а не прерывается + ошибкой при достижении предела (норма — `openspec/specs/storage`); - сервисом пользуются несколько человек, и записи одного не видны другому; - текст доступен там же, где загружали, — в приложении и в Telegram. Человек узнаёт о его готовности, не держа приложение открытым; @@ -54,7 +54,8 @@ - **Разговор о записи.** Ответы на вопросы по содержанию и поиск по смыслу — за границей. Заголовок, пересказ и темы **внутри** границы: она сдвинута 2026-08-10, и до того запись читалась «мы отдаём текст, а не выводы из него». - Направление — цель [text-insights](../tasks/items/text-insights.md). + Считать уровни текста берётся задача + [llm-insights-adapter](../tasks/items/llm-insights-adapter.md). - **Собственные модели.** Не обучаем и не держим у себя ни модель распознавания, ни языковую модель: и речь, и выводы из текста считает внешний сервис. - **Управление учётными записями.** Пользователей заводит и проверяет внешний @@ -65,8 +66,7 @@ - **Живая расшифровка.** Работаем с готовой записью, поток в реальном времени не обрабатываем. - **Диктофон.** Запись звука делает телефон, а приложение принимает готовый - файл. Своей записи и работы без сети не делаем — граница цели - [web-access](../tasks/items/web-access.md). + файл. Своей записи и работы без сети не делаем. - **Файловое хранилище общего назначения.** Храним аудио и видео, отданные ради речи в них. Складом произвольных файлов и папками сервис не становится. Общего доступа к чужим записям целью тоже нет — но **сегодня он есть**: владельца у diff --git a/docs/research/spa-framework.md b/docs/research/spa-framework.md index 17658d1..6c3a274 100644 --- a/docs/research/spa-framework.md +++ b/docs/research/spa-framework.md @@ -25,7 +25,8 @@ Nuxt, Next — не рассматривали: конвенция правил, и в сборке он у всех троих совпал до байта (883 Б), что и подтверждает одинаковость экрана. - **Четыре маршрута** — тот же экран плюс три заглушки и переходы между ними: - столько экранов у цели [web-access](../../tasks/items/web-access.md). Роутеры + столько экранов заводит задача + [spa-skeleton](../../tasks/items/spa-skeleton.md). Роутеры `svelte-spa-router` 5.1.1, `vue-router` 5.2.0 и 4.6.4, `react-router` 8.3.0. - **Размеры** — `stat -c%s` и `gzip -9c | wc -c` по файлам `dist/`. Числа Vite в своём выводе печатает по другому уровню сжатия, поэтому в таблицах ниже стоят diff --git a/docs/review.md b/docs/review.md index 4adf265..0f7e11a 100644 --- a/docs/review.md +++ b/docs/review.md @@ -151,8 +151,10 @@ ещё — самый большой. - `conventions`: новая колонка правится во всех четырёх местах репозитория (CLAUDE.md, «Инварианты»). -- `autotests`: покрыт ли изменённый шаг конвейера хоть одним тестом — сегодня - тестов два файла, и оба мимо конвейера. +- `autotests`: покрыт ли изменённый шаг конвейера хоть одним **проходящим** + тестом. Что уже закрыто проверками, видно по журналу дефектов ниже и по + [conventions/go-linters.md](conventions/go-linters.md), «Механизировано»; + числа файлов здесь не называем — оно протухает с каждой задачей. - `autotests`: судит ли проверка ответа по готовому ответу, а не по изменяемому состоянию обработчика — **только там, где ответ идёт мимо recorder**, через свой `http.ResponseWriter`. Обращение к живой карте recorder'а с @@ -188,8 +190,10 @@ - вход через OIDC и разграничение доступа: как связаны пользователь Telegram и пользователь приложения, до начала работы назвать нельзя; -- всё, что делается на выбранном фреймворке впервые: форма решения нащупывается - по ходу, пока конвенция веб-UI пуста; +- всё, что делается на выбранном фреймворке впервые: правила + [conventions/web-ui.md](conventions/web-ui.md) выведены из выбора и из замера + на пробном экране, а не из написанного кода, и первая же задача проверяет их + собой — форма решения нащупывается по ходу; - установка на телефон: service worker перехватывает запросы, и что он кэширует, до работы назвать нельзя; - работа с записями в несколько часов: потолки внешних сервисов не замерены, diff --git a/docs/security.md b/docs/security.md index da0bcd0..0472d06 100644 --- a/docs/security.md +++ b/docs/security.md @@ -298,10 +298,11 @@ Telegram отправителю. не замер: распределения длин у сервиса нет, а самая длинная проверенная запись — 9,6 МБ ([research/pocketbase-defaults.md](research/pocketbase-defaults.md)). Потолок длины стоит открытым вопросом `architecture.md`, «Долгие записи». Квот - нет и не будет: решено считать расход и показывать его владельцу, а не - отказывать (цель `usage-stats`). Перебравшего останавливает разговор или отзыв доступа в - Authelia. Рост каталога данных при этом ничем не наблюдается — - открытый вопрос `architecture.md`. + нет — это граница домена, [passport.md](passport.md), «Учёт денег»; расход + считают `usage-accounting` и `admin-stats-screen`. Для модели угроз отсюда + следует одно: ни числом запросов, ни размером записи вошедший не ограничен, и + защищаться от исчерпания диска мы не пытаемся. Рост каталога данных при этом + ничем не наблюдается — открытый вопрос `architecture.md`. - **Перерасход денег на внешних сервисах.** Распознавание и языковая модель оплачиваются по факту; потолка на пользователя нет по тому же решению. - **Стойкость `ffmpeg` к вредоносному входу.** Разбор чужого формата отдан diff --git a/openspec/config.yaml b/openspec/config.yaml index ddd4728..8a86702 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -32,7 +32,8 @@ context: | - docs/architecture.md — устройство; docs/security.md — периметр; docs/database.md — схема и настройки с числами; docs/adr/ — почему решено так; docs/research/ — что уже измерено; - - tasks/ROADMAP.md — что приложение уже умеет и чего ещё не умеет. + - openspec/specs/ — что приложение уже умеет; tasks/BACKLOG.md — что осталось + и в каком порядке это берут. Пересказа этих документов здесь нет намеренно: второй дом факта расходится с первым молча, и заметно это становится в предложении, которое уже написано. diff --git a/openspec/specs/pipeline/spec.md b/openspec/specs/pipeline/spec.md index 2c5034a..5ca293d 100644 --- a/openspec/specs/pipeline/spec.md +++ b/openspec/specs/pipeline/spec.md @@ -5,14 +5,14 @@ Конвейер расшифровки: как задача движется по состояниям, что делает воркер, когда работы нет, и что считается отказом шага. -Описан пока **только пустой прогон воркера** — тот, что нормируют проверки -пакета `internal/controller/worker` и перевод признака в `internal/service`. -Сознательно не описаны переходы состояний и цепочка `created → converted → -transcribe → done | failed`, захват задачи и срок его протухания, отмена -контекста посреди шага, освобождение ресурсов внешних клиентов. Это не значит, -что такого поведения нет: оно живёт в коде, а требования на него не написаны, -потому что требование без проверки — предположение, а не норма. Первая задача, -которая трогает любое из перечисленного, дописывает его сюда. +Описаны пустой прогон воркера, неделимость захвата и срок его протухания, число +попыток и состояние «мертва», нарастающая пауза перед повтором и условие записи +результата держателем захвата. Сознательно не описаны: цепочка переходов +`created → converted → transcribe → done | failed`, отмена контекста посреди +шага и освобождение ресурсов внешних клиентов. Это не значит, что такого +поведения нет: оно живёт в коде, а требования на него не написаны, потому что +требование без проверки — предположение, а не норма. Первая задача, которая +трогает любое из перечисленного, дописывает его сюда. ## Requirements ### Requirement: Пустой прогон воркера — не отказ