точки входа переехали в cmd/, заведена заглушка OIDC для локального входа
- main.go и journal_route_test.go переехали в cmd/transcriber без правок содержимого; образ собирает ./cmd/transcriber поимённо - cmd/oidcstub отвечает на /authorize, /token и /userinfo, проверок не делает и слушает петлевой адрес: войти без Authelia стало чем - ступень сборки приложения переехала на node:24 с alpine — musl ждёт ответа на AAAA, которого нет, и npm ci висел вместо отказа
This commit is contained in:
+3
-2
@@ -64,8 +64,9 @@ linters:
|
|||||||
msg: 'пишем через slog, а не в stdout напрямую (docs/conventions/logging.md)'
|
msg: 'пишем через slog, а не в stdout напрямую (docs/conventions/logging.md)'
|
||||||
# Конфигурация приезжает из TOML. Перечислены все способы прочитать
|
# Конфигурация приезжает из TOML. Перечислены все способы прочитать
|
||||||
# окружение, а не один: `os.Getenv` без соседей обходится `os.LookupEnv`
|
# окружение, а не один: `os.Getenv` без соседей обходится `os.LookupEnv`
|
||||||
# одной правкой. Окружение читает только godotenv в main.go — он кладёт
|
# одной правкой. Окружение читает только godotenv в точке входа
|
||||||
# .env в окружение процесса, а не в настройки.
|
# `cmd/transcriber` — он кладёт .env в окружение процесса, а не в
|
||||||
|
# настройки.
|
||||||
#
|
#
|
||||||
# Чего правило не ловит: `fmt.Fprintln(os.Stdout, …)` и
|
# Чего правило не ловит: `fmt.Fprintln(os.Stdout, …)` и
|
||||||
# `os.Stdout.WriteString` — первый аргумент по имени функции не судится.
|
# `os.Stdout.WriteString` — первый аргумент по имени функции не судится.
|
||||||
|
|||||||
@@ -119,7 +119,8 @@ go test ./... # в гейте идёт с -race, и там нуже
|
|||||||
go vet ./...
|
go vet ./...
|
||||||
gofmt -l .
|
gofmt -l .
|
||||||
golangci-lint run
|
golangci-lint run
|
||||||
go run . -c config.toml # флаг -c или --config, по умолчанию config.toml
|
go run ./cmd/transcriber -c config.toml # флаг -c или --config, по умолчанию config.toml
|
||||||
|
go run ./cmd/oidcstub # подставной провайдер OIDC для локального входа
|
||||||
task front # приложение: зависимости, Biome, юнит-тесты, сборка
|
task front # приложение: зависимости, Biome, юнит-тесты, сборка
|
||||||
task image # docker-образ; тег и раскладка — docs/architecture.md
|
task image # docker-образ; тег и раскладка — docs/architecture.md
|
||||||
task gate # весь набор проверок разом
|
task gate # весь набор проверок разом
|
||||||
@@ -234,6 +235,12 @@ Node на машину **не ставится**: шаг сборки прило
|
|||||||
ссылки. Расшифровка при выдуманных ключах не работает: её подменяют
|
ссылки. Расшифровка при выдуманных ключах не работает: её подменяют
|
||||||
`internal/adapter/recognizer/memory.go`. Подробности строками в
|
`internal/adapter/recognizer/memory.go`. Подробности строками в
|
||||||
`config.example.toml`.
|
`config.example.toml`.
|
||||||
|
**Войти при выдуманных адресах нельзя** — они никуда не ведут, а сервис без
|
||||||
|
входа не отдаёт ничего. Вместо провайдера поднимается заглушка
|
||||||
|
`cmd/oidcstub` — она отвечает на `/authorize`, `/token` и `/userinfo`, а
|
||||||
|
проверок не делает никаких;
|
||||||
|
значения `[auth]` под неё стоят строками в `config.example.toml`. Ключи
|
||||||
|
боевого провайдера на машине разработчика при этом по-прежнему не нужны.
|
||||||
- **Yandex Cloud за деньги.** Распознавание и хранение в Object Storage
|
- **Yandex Cloud за деньги.** Распознавание и хранение в Object Storage
|
||||||
оплачиваются по факту. Прогон на реальных ключах ради проверки кода запрещён —
|
оплачиваются по факту. Прогон на реальных ключах ради проверки кода запрещён —
|
||||||
подставляй `internal/adapter/recognizer/memory.go`.
|
подставляй `internal/adapter/recognizer/memory.go`.
|
||||||
|
|||||||
+12
-2
@@ -3,7 +3,15 @@
|
|||||||
# Приложение собирается до бинарника: вшивание требует готового каталога.
|
# Приложение собирается до бинарника: вшивание требует готового каталога.
|
||||||
# Имя этого образа — единственное; шаг набора проверок берёт его отсюда же,
|
# Имя этого образа — единственное; шаг набора проверок берёт его отсюда же,
|
||||||
# чтобы версия сборочного окружения не жила вторым числом в Taskfile.yml.
|
# чтобы версия сборочного окружения не жила вторым числом в Taskfile.yml.
|
||||||
FROM docker.io/library/node:24-alpine AS front-build
|
#
|
||||||
|
# Образ на glibc, а не на alpine, и разница здесь не в весе: musl шлёт запросы
|
||||||
|
# `A` и `AAAA` разом и ждёт **оба** ответа, а DNS-сервер, который на `AAAA`
|
||||||
|
# молчит, оставляет его без адреса вовсе — при живом `A`. `npm` на такой отказ
|
||||||
|
# уходит в повторы с нарастающей паузой на каждом пакете, и сборка не краснеет,
|
||||||
|
# а **висит**. Библиотека glibc довольствуется полученным `A` и собирает.
|
||||||
|
# Ступень сборочная: в готовый образ её слои не едут, и лишний вес остаётся
|
||||||
|
# ценой одной сборки, а не размером выкладки.
|
||||||
|
FROM docker.io/library/node:24 AS front-build
|
||||||
|
|
||||||
WORKDIR /web
|
WORKDIR /web
|
||||||
|
|
||||||
@@ -38,7 +46,9 @@ COPY . .
|
|||||||
COPY --from=front-build /web/embed/dist ./web/embed/dist
|
COPY --from=front-build /web/embed/dist ./web/embed/dist
|
||||||
|
|
||||||
# Build the application
|
# Build the application
|
||||||
RUN CGO_ENABLED=0 go build -o transcriber .
|
# Собирается одна точка входа из cmd/, а не весь пакет: соседний cmd/oidcstub —
|
||||||
|
# подставной провайдер для локального запуска, и в образе ему делать нечего.
|
||||||
|
RUN CGO_ENABLED=0 go build -o transcriber ./cmd/transcriber
|
||||||
|
|
||||||
# ----------------
|
# ----------------
|
||||||
# Production stage
|
# Production stage
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
```
|
```
|
||||||
4. Запустите приложение:
|
4. Запустите приложение:
|
||||||
```bash
|
```bash
|
||||||
go run . -c config.toml
|
go run ./cmd/transcriber -c config.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
Сервер запустится на порту из `[server] port`, по умолчанию 8080. Нужен
|
Сервер запустится на порту из `[server] port`, по умолчанию 8080. Нужен
|
||||||
@@ -45,6 +45,15 @@
|
|||||||
[docs/security.md](docs/security.md), «Что разграничивает доступ»; известные
|
[docs/security.md](docs/security.md), «Что разграничивает доступ»; известные
|
||||||
прорехи образца конфига — [docs/conventions/config.md](docs/conventions/config.md).
|
прорехи образца конфига — [docs/conventions/config.md](docs/conventions/config.md).
|
||||||
|
|
||||||
|
Локально провайдера нет, и войти при выдуманных адресах нельзя — вместо него
|
||||||
|
поднимается заглушка:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/oidcstub
|
||||||
|
```
|
||||||
|
|
||||||
|
Значения `[auth]` под неё стоят строками в `config.example.toml`.
|
||||||
|
|
||||||
## Деплой
|
## Деплой
|
||||||
|
|
||||||
Деплой запускается из `pet-project-server`:
|
Деплой запускается из `pet-project-server`:
|
||||||
@@ -82,7 +91,9 @@ inv pl -- transcriber
|
|||||||
|
|
||||||
```
|
```
|
||||||
transcriber/
|
transcriber/
|
||||||
├── main.go # Точка входа: конфиг, миграции, сборка зависимостей, запуск
|
├── cmd/
|
||||||
|
│ ├── transcriber/ # Точка входа сервиса: конфиг, миграции, сборка зависимостей, запуск
|
||||||
|
│ └── oidcstub/ # Подставной провайдер OIDC для локального входа
|
||||||
├── internal/
|
├── internal/
|
||||||
│ ├── entity/ # Модели: задача, файл, результат распознавания
|
│ ├── entity/ # Модели: задача, файл, результат распознавания
|
||||||
│ ├── contract/ # Интерфейсы адаптеров и репозиториев, типы ошибок
|
│ ├── contract/ # Интерфейсы адаптеров и репозиториев, типы ошибок
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
// Команда oidcstub — подставной провайдер OIDC для локального запуска.
|
||||||
|
//
|
||||||
|
// Сервис держит вход через внешнего провайдера, и без него в приложение не
|
||||||
|
// попасть вовсе: секция `[auth]` обязательна, а выдуманные адреса разбираются
|
||||||
|
// как ссылки, но никуда не ведут. Настоящая Authelia — отдельный сервис, который
|
||||||
|
// надо развернуть и настроить, а ключи боевого провайдера на машине разработчика
|
||||||
|
// лежать не должны.
|
||||||
|
//
|
||||||
|
// Заглушка отвечает на `/authorize`, `/token` и `/userinfo`, и этого хватает:
|
||||||
|
// `user_info_url` — обязательный ключ конфига, а при заполненном адресе сведений
|
||||||
|
// библиотека хранилища берёт их обычным запросом с предъявленным токеном и
|
||||||
|
// `id_token` не смотрит вовсе. Поэтому здесь нет ни ключей подписи, ни документа
|
||||||
|
// обнаружения.
|
||||||
|
//
|
||||||
|
// Проверок она не делает никаких — ни секрета клиента, ни проверочного кода
|
||||||
|
// PKCE, ни выданного токена. Вход у неё один и заранее известный: она нужна,
|
||||||
|
// чтобы дойти до куки сессии, а не чтобы изображать провайдера. По той же
|
||||||
|
// причине слушает она только петлевой адрес.
|
||||||
|
//
|
||||||
|
// Запуск:
|
||||||
|
//
|
||||||
|
// go run ./cmd/oidcstub
|
||||||
|
// go run ./cmd/oidcstub -sub local-2
|
||||||
|
//
|
||||||
|
// Другое значение `-sub` заводит второго вошедшего, и почта переезжает вместе с
|
||||||
|
// ним: без своей почты второй вход отвергается вовсе. Хранилище ищет учётную
|
||||||
|
// запись сперва по признаку провайдера, а не найдя — по адресу почты, и на
|
||||||
|
// найденной записи признак уникален. Второй `sub` при общей почте пришёл бы к той
|
||||||
|
// же записи и получил бы отказ уникальности — вход отвечал бы 401, а причина
|
||||||
|
// осталась бы в журнале хранилища строкой про связь.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// authCode — код, который заглушка отдаёт на возврате. Значение постоянное:
|
||||||
|
// одноразовость кода держит провайдер, а здесь её изображать не для кого.
|
||||||
|
const authCode = "local-code"
|
||||||
|
|
||||||
|
// accessToken — то, что уезжает в обмен и приходит обратно заголовком запроса
|
||||||
|
// сведений о вошедшем. Заглушка его не сверяет.
|
||||||
|
const accessToken = "local-token"
|
||||||
|
|
||||||
|
// readHeaderTimeout — потолок чтения заголовков. Заглушка стоит на петлевом
|
||||||
|
// адресе, но сервер без единого срока держит подвисшее соединение вечно.
|
||||||
|
const readHeaderTimeout = 5 * time.Second
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
port := flag.Int("port", 9000, "Порт заглушки")
|
||||||
|
sub := flag.String("sub", "local-1", "Идентификатор вошедшего: разные значения дают разных владельцев записей")
|
||||||
|
name := flag.String("name", "Локальный", "Имя вошедшего")
|
||||||
|
email := flag.String("email", "", "Почта вошедшего; пустое значение даёт <sub>@example.com")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Почта выводится из `sub`, а не стоит своим умолчанием: общая почта у двух
|
||||||
|
// разных `sub` приводит второй вход к первой записи, а признак провайдера на
|
||||||
|
// ней уникален — второй вошедший получал бы 401 вместо своей учётной записи.
|
||||||
|
// Обещание «разные `-sub` дают разных владельцев» держится этой строкой.
|
||||||
|
if *email == "" {
|
||||||
|
*email = *sub + "@example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||||
|
Level: slog.LevelInfo,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/authorize", authorize(logger))
|
||||||
|
mux.HandleFunc("/token", token(logger))
|
||||||
|
mux.HandleFunc("/userinfo", userInfo(logger, *sub, *name, *email))
|
||||||
|
|
||||||
|
// Петлевой адрес, а не все — заглушка пускает кого угодно кем угодно, и в
|
||||||
|
// чужой сети это дыра, а не удобство.
|
||||||
|
addr := fmt.Sprintf("127.0.0.1:%d", *port)
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: mux,
|
||||||
|
ReadHeaderTimeout: readHeaderTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("oidc stub started", "addr", addr, "sub", *sub)
|
||||||
|
|
||||||
|
if err := server.ListenAndServe(); err != nil {
|
||||||
|
logger.Error("oidc stub stopped", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// authorize уводит браузер обратно на адрес возврата с готовым кодом.
|
||||||
|
//
|
||||||
|
// Настоящий провайдер спросил бы здесь имя и пароль; заглушка не спрашивает
|
||||||
|
// ничего и возвращает сразу — в этом вся её работа.
|
||||||
|
func authorize(logger *slog.Logger) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
query := r.URL.Query()
|
||||||
|
|
||||||
|
target, err := url.Parse(query.Get("redirect_uri"))
|
||||||
|
if err != nil || target.Host == "" {
|
||||||
|
logger.Error("authorize rejected", "reason", "redirect_uri is missing or malformed")
|
||||||
|
http.Error(w, "redirect_uri не задан или не разбирается", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Состояние возвращается тем же значением, каким пришло: сервис сверяет
|
||||||
|
// его со своей кукой и без совпадения отвергает возврат.
|
||||||
|
back := target.Query()
|
||||||
|
back.Set("code", authCode)
|
||||||
|
if state := query.Get("state"); state != "" {
|
||||||
|
back.Set("state", state)
|
||||||
|
}
|
||||||
|
target.RawQuery = back.Encode()
|
||||||
|
|
||||||
|
logger.Info("authorize passed", "redirect_host", target.Host)
|
||||||
|
|
||||||
|
http.Redirect(w, r, target.String(), http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// token отдаёт токен в обмен на код. Ни код, ни секрет клиента, ни проверочный
|
||||||
|
// код PKCE не сверяются.
|
||||||
|
func token(logger *slog.Logger) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "обмен идёт методом POST", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// `id_token` не отдаётся намеренно: подписать его нечем, а библиотека
|
||||||
|
// хранилища его и не смотрит — `user_info_url` стоит в перечне
|
||||||
|
// обязательных ключей конфига, и без него сервис не поднимается вовсе.
|
||||||
|
writeJSON(logger, w, map[string]any{
|
||||||
|
"access_token": accessToken,
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 3600,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// userInfo отдаёт сведения о вошедшем.
|
||||||
|
//
|
||||||
|
// `email_verified` обязано быть истинным: без него библиотека адрес почты в
|
||||||
|
// учётную запись не запишет, и запись заведётся без него.
|
||||||
|
func userInfo(logger *slog.Logger, sub, name, email string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
writeJSON(logger, w, map[string]any{
|
||||||
|
"sub": sub,
|
||||||
|
"name": name,
|
||||||
|
"preferred_username": sub,
|
||||||
|
"email": email,
|
||||||
|
"email_verified": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeJSON отвечает разметкой JSON. Отказ записи идёт в журнал: ответ к этому
|
||||||
|
// моменту уже начат, и сказать о нём спрашивающему нечем.
|
||||||
|
func writeJSON(logger *slog.Logger, w http.ResponseWriter, body map[string]any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(body); err != nil {
|
||||||
|
logger.Error("response write failed", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,3 +85,31 @@ redirect_url = "https://transcriber.example.com/auth/callback"
|
|||||||
# Признак `Secure` у куки сессии. Умолчание true; false только для локального
|
# Признак `Secure` у куки сессии. Умолчание true; false только для локального
|
||||||
# запуска по http://localhost, где браузер такую куку не сохранит
|
# запуска по http://localhost, где браузер такую куку не сохранит
|
||||||
secure_cookie = true
|
secure_cookie = true
|
||||||
|
|
||||||
|
# Локальный вход без Authelia.
|
||||||
|
#
|
||||||
|
# Провайдер на машине разработчика не поднимается, а ключи боевого на ней лежать
|
||||||
|
# не должны — вместо провайдера идёт заглушка `cmd/oidcstub`. Она отвечает на
|
||||||
|
# `/authorize`, `/token` и `/userinfo` и не сверяет ни секрет клиента, ни
|
||||||
|
# проверочный код PKCE: вход у неё один и заранее известный. Поднимается
|
||||||
|
# отдельным процессом:
|
||||||
|
#
|
||||||
|
# go run ./cmd/oidcstub
|
||||||
|
#
|
||||||
|
# Замени секцию `[auth]` выше на эти значения — вход пойдёт настоящим путём,
|
||||||
|
# вплоть до куки сессии и заведённой учётной записи в панели:
|
||||||
|
#
|
||||||
|
# auth_url = "http://localhost:9000/authorize"
|
||||||
|
# token_url = "http://localhost:9000/token"
|
||||||
|
# user_info_url = "http://localhost:9000/userinfo"
|
||||||
|
# client_id = "transcriber"
|
||||||
|
# client_secret = "local"
|
||||||
|
# redirect_url = "http://localhost:8080/auth/callback"
|
||||||
|
# secure_cookie = false
|
||||||
|
#
|
||||||
|
# Другое значение `-sub` заводит второго вошедшего: это неизменяемый признак,
|
||||||
|
# по которому обмен ищет учётную запись. Почта переезжает вместе с ним сама —
|
||||||
|
# заглушка выводит её из `-sub`, — и это обязательно: при общей почте второй вход
|
||||||
|
# приходит к первой записи и получает отказ.
|
||||||
|
#
|
||||||
|
# go run ./cmd/oidcstub -sub local-2
|
||||||
|
|||||||
+13
-2
@@ -77,8 +77,8 @@
|
|||||||
[pipeline](../openspec/specs/pipeline/spec.md), «Брошенная задача возвращается
|
[pipeline](../openspec/specs/pipeline/spec.md), «Брошенная задача возвращается
|
||||||
в работу»; здесь это принцип письма шага, а не описание поведения.
|
в работу»; здесь это принцип письма шага, а не описание поведения.
|
||||||
- **Ядро зависит от интерфейсов.** `internal/service` знает только
|
- **Ядро зависит от интерфейсов.** `internal/service` знает только
|
||||||
`internal/contract`; ffmpeg, Yandex и хранилище подставляются в
|
`internal/contract`; ffmpeg, Yandex и хранилище подставляются в точке входа
|
||||||
`main.go`. Правило механизировано тестами-сканерами `internal/archrules`, и
|
`cmd/transcriber`. Правило механизировано тестами-сканерами `internal/archrules`, и
|
||||||
они же держат обратные направления: транспорты не знают друг о друге, адаптер
|
они же держат обратные направления: транспорты не знают друг о друге, адаптер
|
||||||
не знает ни ядра, ни транспортов.
|
не знает ни ядра, ни транспортов.
|
||||||
*Изъятие:* транспорт **вправе** знать адаптер хранилища — `controller/http`
|
*Изъятие:* транспорт **вправе** знать адаптер хранилища — `controller/http`
|
||||||
@@ -219,6 +219,17 @@
|
|||||||
не попадает. Финальный слой — alpine с `ca-certificates` и `ffmpeg`, процесс
|
не попадает. Финальный слой — alpine с `ca-certificates` и `ffmpeg`, процесс
|
||||||
работает под непривилегированным пользователем `transcriber`.
|
работает под непривилегированным пользователем `transcriber`.
|
||||||
|
|
||||||
|
Ступень бинарника собирает **одну** точку входа — `./cmd/transcriber`, а не весь
|
||||||
|
пакет: рядом в `cmd/` живёт `oidcstub`, подставной провайдер OIDC для локального
|
||||||
|
входа, и в образе ему делать нечего.
|
||||||
|
|
||||||
|
Ступень приложения стоит на образе с glibc, а не на alpine, и решает это не вес:
|
||||||
|
у musl запрос имени идёт `A` и `AAAA` разом и ждёт **оба** ответа, поэтому
|
||||||
|
DNS-сервер, молчащий на `AAAA`, оставляет установщика пакетов без адреса при
|
||||||
|
живом `A`. Установщик уходит в повторы с нарастающей паузой на каждом пакете, и
|
||||||
|
сборка не краснеет, а **висит** — исход хуже красного. Слои этой ступени в
|
||||||
|
рабочий слой не едут, поэтому её вес остаётся ценой одной сборки.
|
||||||
|
|
||||||
**По весу финальный образ от ступени приложения не растёт вовсе:** она отдаёт
|
**По весу финальный образ от ступени приложения не растёт вовсе:** она отдаёт
|
||||||
следующей только собранное, а сама в рабочий слой не копируется. Вшитое
|
следующей только собранное, а сама в рабочий слой не копируется. Вшитое
|
||||||
приложение прибавляет к бинарнику 86 072 байта. Время сборки образа не
|
приложение прибавляет к бинарнику 86 072 байта. Время сборки образа не
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
|
|
||||||
**Взято из проекта jellybit.** Расхождения с сегодняшним кодом названы по месту.
|
**Взято из проекта jellybit.** Расхождения с сегодняшним кодом названы по месту.
|
||||||
Главные: комментариями снабжена половина полей; единого места проверки на старте
|
Главные: комментариями снабжена половина полей; единого места проверки на старте
|
||||||
нет: у секций `[auth]` и `[pipeline]` свой `Validate()` в `main.go`, а пустые
|
нет: у секций `[auth]` и `[pipeline]` свой `Validate()` в точке входа, а пустые
|
||||||
ключи `[yandex]` ловит конструктор распознавателя.
|
ключи `[yandex]` ловит конструктор распознавателя.
|
||||||
|
|
||||||
**Механизировано:** запрет `os.Getenv` — `forbidigo` в `.golangci.yml`
|
**Механизировано:** запрет `os.Getenv` — `forbidigo` в `.golangci.yml`
|
||||||
([go-linters.md](go-linters.md), «Механизировано»). Он держит правило «настройки
|
([go-linters.md](go-linters.md), «Механизировано»). Он держит правило «настройки
|
||||||
приезжают из TOML»; `godotenv` в `main.go` по-прежнему загружает `.env`, но кладёт
|
приезжают из TOML»; `godotenv` в `cmd/transcriber` по-прежнему загружает `.env`, но кладёт
|
||||||
его в окружение процесса, а не в настройки приложения.
|
его в окружение процесса, а не в настройки приложения.
|
||||||
|
|
||||||
## Принципы
|
## Принципы
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
- **Конфигурация — только TOML.** Переменные окружения для конфигурации **не
|
- **Конфигурация — только TOML.** Переменные окружения для конфигурации **не
|
||||||
используем**: окружение наследуется дочерними процессами и видно через
|
используем**: окружение наследуется дочерними процессами и видно через
|
||||||
`/proc/<pid>/environ` — для секретов это слабее файла под `0600`.
|
`/proc/<pid>/environ` — для секретов это слабее файла под `0600`.
|
||||||
*Расхождение:* `main.go` зовёт `godotenv.Load()` и молча продолжает без файла.
|
*Расхождение:* `cmd/transcriber` зовёт `godotenv.Load()` и молча продолжает без файла.
|
||||||
- Грузим **один раз при старте** в одну типизированную структуру `Config`
|
- Грузим **один раз при старте** в одну типизированную структуру `Config`
|
||||||
(под-структуры по секциям). Дальше по коду читаем только её — чтения файла в
|
(под-структуры по секциям). Дальше по коду читаем только её — чтения файла в
|
||||||
прикладном коде нет, только загрузчик `internal/config`.
|
прикладном коде нет, только загрузчик `internal/config`.
|
||||||
@@ -62,6 +62,12 @@ force_shutdown_timeout = <N> # ждать остановки ворке
|
|||||||
говорит, какой формы значение здесь ждут. Пустым оставлен только
|
говорит, какой формы значение здесь ждут. Пустым оставлен только
|
||||||
`client_secret` — он и есть секрет.
|
`client_secret` — он и есть секрет.
|
||||||
|
|
||||||
|
Там же, комментарием под секцией, стоит **второй набор значений `[auth]` — под
|
||||||
|
подставной провайдер `cmd/oidcstub`**. Они — не живая форма, и это намеренно:
|
||||||
|
образец описывает боевую выкладку, а локальный вход — способ до неё дойти, и два
|
||||||
|
рабочих набора в одном файле читались бы как выбор без указания, какой из них
|
||||||
|
чей.
|
||||||
|
|
||||||
## Поля по дискриминатору `type`
|
## Поля по дискриминатору `type`
|
||||||
|
|
||||||
Когда набор полей секции зависит от поля-дискриминатора `type` (выбор одного из
|
Когда набор полей секции зависит от поля-дискриминатора `type` (выбор одного из
|
||||||
@@ -130,7 +136,7 @@ TOML. Пустые ключи Yandex ловятся в конструкторе
|
|||||||
входом 2026-08-14: секции больше нет, и своей проверки у неё тоже.
|
входом 2026-08-14: секции больше нет, и своей проверки у неё тоже.
|
||||||
|
|
||||||
Секция `[auth]` — первая, у которой проверка своя и стоит на старте:
|
Секция `[auth]` — первая, у которой проверка своя и стоит на старте:
|
||||||
`AuthConfig.Validate()` зовётся из `main.go` сразу после загрузки и роняет
|
`AuthConfig.Validate()` зовётся из `cmd/transcriber` сразу после загрузки и роняет
|
||||||
процесс с перечнем незаполненных ключей. Причина в цене умолчания: поднявшись с
|
процесс с перечнем незаполненных ключей. Причина в цене умолчания: поднявшись с
|
||||||
молча выключенным входом, сервис остался бы открытым наружу, а узнать об этом
|
молча выключенным входом, сервис остался бы открытым наружу, а узнать об этом
|
||||||
было бы неоткуда. Сообщение называет **имена ключей**, а не значения — значение
|
было бы неоткуда. Сообщение называет **имена ключей**, а не значения — значение
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ OpenSpec.
|
|||||||
{"time":"2026-08-10T11:23:45.123456Z","level":"INFO","msg":"record accepted","capability":"intake","record_id":"…","source":"api","duration_seconds":137}
|
{"time":"2026-08-10T11:23:45.123456Z","level":"INFO","msg":"record accepted","capability":"intake","record_id":"…","source":"api","duration_seconds":137}
|
||||||
```
|
```
|
||||||
|
|
||||||
*Расхождение:* `main.go` ставит `slog.NewTextHandler(os.Stdout, …)`.
|
*Расхождение:* текстовый обработчик ставят **оба** пакета `cmd/` —
|
||||||
|
`slog.NewTextHandler(os.Stdout, …)` и в `transcriber`, и в `oidcstub`. У второго
|
||||||
|
это выбор, а не долг: его вывод читает человек в терминале, и разбирать его
|
||||||
|
отбором никто не станет.
|
||||||
|
|
||||||
## Сообщение
|
## Сообщение
|
||||||
|
|
||||||
@@ -78,7 +81,7 @@ OpenSpec.
|
|||||||
- `slog` не разделяет CRITICAL и FATAL — сбой на старте логируем `ERROR` и
|
- `slog` не разделяет CRITICAL и FATAL — сбой на старте логируем `ERROR` и
|
||||||
завершаем процесс с ненулевым кодом.
|
завершаем процесс с ненулевым кодом.
|
||||||
|
|
||||||
*Расхождение:* уровень зашит константой в `main.go`, `DEBUG` включить нечем.
|
*Расхождение:* уровень зашит константой в `cmd/transcriber`, `DEBUG` включить нечем.
|
||||||
Пустой прогон воркера не логируется вовсе — и это правилу не противоречит.
|
Пустой прогон воркера не логируется вовсе — и это правилу не противоречит.
|
||||||
|
|
||||||
## Время
|
## Время
|
||||||
@@ -208,7 +211,7 @@ Object Storage и опрос операции не логируются ника
|
|||||||
`INFO` они не пишутся.
|
`INFO` они не пишутся.
|
||||||
|
|
||||||
Расхождения здесь больше нет: слой журналирования запросов свой,
|
Расхождения здесь больше нет: слой журналирования запросов свой,
|
||||||
`main.go`, хук `OnServe` — вместе с gin ушёл и `sloggin`. `/health` и `/metrics`
|
`cmd/transcriber`, хук `OnServe` — вместе с gin ушёл и `sloggin`. `/health` и `/metrics`
|
||||||
идут на `DEBUG`, то есть при боевом `INFO` не пишутся вовсе.
|
идут на `DEBUG`, то есть при боевом `INFO` не пишутся вовсе.
|
||||||
|
|
||||||
Хранилище ведёт **свой** журнал запросов в собственной таблице, и он виден
|
Хранилище ведёт **свой** журнал запросов в собственной таблице, и он виден
|
||||||
|
|||||||
@@ -169,6 +169,20 @@ pb_data/storage/<коллекция>/<запись>/<имя>_<10 случайн
|
|||||||
**Куки библиотека не читает вовсе** — сессию берёт только заголовком
|
**Куки библиотека не читает вовсе** — сессию берёт только заголовком
|
||||||
`Authorization` (`apis/middlewares.go`, `getAuthTokenFromRequest`).
|
`Authorization` (`apis/middlewares.go`, `getAuthTokenFromRequest`).
|
||||||
|
|
||||||
|
**Учётную запись обмен ищет двумя способами подряд, а связь с провайдером
|
||||||
|
уникальна.** Сперва — по неизменяемому признаку провайдера, а не найдя — по
|
||||||
|
адресу почты (`apis/record_auth_with_oauth2.go`, ветка `case authUser.Email !=
|
||||||
|
""` → `FindAuthRecordByEmail`). Найденной записи он пытается добавить связь, а на
|
||||||
|
ней стоит уникальный индекс
|
||||||
|
`idx_externalAuths_record_provider (collectionRef, recordRef, provider)`. Отсюда
|
||||||
|
исход, обратный ожидаемому: два разных признака провайдера с **одной** почтой не
|
||||||
|
сливаются в одного владельца молча — второй вход отвергается, обмен отдаёт `400`,
|
||||||
|
сервис — `401` со строкой `Failed to exchange provider code`, а настоящая причина
|
||||||
|
остаётся в журнале хранилища строкой `failed to save linked rel: … Value must be
|
||||||
|
unique`. Дописано 2026-08-15 задачей про заглушку OIDC; получено прогоном против
|
||||||
|
временного каталога — чтение исходников давало ту же цепочку, но противоположную
|
||||||
|
развязку.
|
||||||
|
|
||||||
**Журнал запросов пишет строку запроса целиком.** `activityLogger` на корневом
|
**Журнал запросов пишет строку запроса целиком.** `activityLogger` на корневом
|
||||||
роутере кладёт `RequestURI` полем `url` в таблицу `_logs`, ретеншен по умолчанию
|
роутере кладёт `RequestURI` полем `url` в таблицу `_logs`, ретеншен по умолчанию
|
||||||
`MaxDays: 5`. Значит всё, что пришло параметром адреса, оседает там на пять
|
`MaxDays: 5`. Значит всё, что пришло параметром адреса, оседает там на пять
|
||||||
|
|||||||
@@ -119,6 +119,18 @@
|
|||||||
|
|
||||||
### Типовые ложноположительные
|
### Типовые ложноположительные
|
||||||
|
|
||||||
|
- **«Хранилище молча сливает две учётные записи с одной почтой в одного
|
||||||
|
владельца».** Для версии v0.39.10 неверно, и неверна именно развязка. Первая
|
||||||
|
половина цепочки настоящая: обмен ищет запись по признаку провайдера, а не
|
||||||
|
найдя — по адресу почты, и приходит к чужой записи. Но повесить на неё второй
|
||||||
|
признак он не может — уникальный индекс
|
||||||
|
`idx_externalAuths_record_provider (collectionRef, recordRef, provider)` связь
|
||||||
|
отвергает, обмен отдаёт `400`, а сервис — `401` со строкой
|
||||||
|
`Failed to exchange provider code`. Отказ **громкий**, тихого слияния владельцев
|
||||||
|
не происходит, и ложно-зелёной проверки разграничения такой дефект не даёт.
|
||||||
|
Проверено прогоном 2026-08-15 (задача про заглушку OIDC); найдено чтением
|
||||||
|
исходников библиотеки, опровергнуто запуском — то есть цена гипотезы, добытой
|
||||||
|
без прогона, здесь и измерена.
|
||||||
- **«Воркер глотает ошибку `NoopJobError`».** Не дефект: этот тип означает «задач
|
- **«Воркер глотает ошибку `NoopJobError`».** Не дефект: этот тип означает «задач
|
||||||
в этом состоянии нет», и `internal/controller/worker/worker.go` намеренно не
|
в этом состоянии нет», и `internal/controller/worker/worker.go` намеренно не
|
||||||
логирует его и не считает в метрику. Норма записана требованием
|
логирует его и не считает в метрику. Норма записана требованием
|
||||||
@@ -159,6 +171,14 @@
|
|||||||
|
|
||||||
Форма: `<тема>: <вопрос> (<откуда>)`.
|
Форма: `<тема>: <вопрос> (<откуда>)`.
|
||||||
|
|
||||||
|
- `operations`: не завёл ли инструмент разработчика второй дом тому, что уже есть
|
||||||
|
в проверках. Подставных провайдера OIDC в репозитории теперь два — `cmd/oidcstub`
|
||||||
|
и `fakeProvider` в `internal/controller/http/login_test.go`, — с теми же
|
||||||
|
адресами и той же посылкой про `email_verified`, и они уже разошлись в мелочи
|
||||||
|
(`token_type` «bearer» против «Bearer»). Тем же вопросом судится подставной
|
||||||
|
распознаватель. Записанной конвенции о единственном доме подставных внешних
|
||||||
|
собеседников у проекта нет, поэтому спрашивать надо, а не считать нарушением
|
||||||
|
(ревью задачи про заглушку OIDC, 2026-08-15).
|
||||||
- `operations`: как шаг отвечает на отмену посреди работы — контекст доходит до
|
- `operations`: как шаг отвечает на отмену посреди работы — контекст доходит до
|
||||||
внешнего собеседника и это держат правила `noctx` и `contextcheck`
|
внешнего собеседника и это держат правила `noctx` и `contextcheck`
|
||||||
([conventions/go-linters.md](conventions/go-linters.md), «Отмена и внешний
|
([conventions/go-linters.md](conventions/go-linters.md), «Отмена и внешний
|
||||||
@@ -312,6 +332,27 @@ API и имя не откатываются обратной правкой по
|
|||||||
истории git 2026-08-10: поле «Чем воспроизведён» называет у них коммит, а не
|
истории git 2026-08-10: поле «Чем воспроизведён» называет у них коммит, а не
|
||||||
оракул, и выдумывать оракул задним числом нельзя.
|
оракул, и выдумывать оракул задним числом нельзя.
|
||||||
|
|
||||||
|
## 2026-08-15 — короткая форма рецепта входа не работала, а проверяли длинную [пойман ревью]
|
||||||
|
|
||||||
|
- **Где:** `cmd/oidcstub` — подставной провайдер OIDC для локального входа;
|
||||||
|
доккоммент пакета, подсказка флага `-sub` и проза `config.example.toml`
|
||||||
|
- **Симптом:** рецепт «второй вошедший получается сменой `-sub`» записан в трёх
|
||||||
|
местах и в короткой форме не работал вовсе. Заглушка отдавала обоим `sub` одну
|
||||||
|
и ту же почту умолчанием, вход отвечал `401`, а причина оставалась строкой в
|
||||||
|
журнале хранилища
|
||||||
|
- **Причина:** обмен ищет учётную запись сперва по признаку провайдера, а не
|
||||||
|
найдя — по адресу почты. Второй `sub` при общей почте приходил к первой записи,
|
||||||
|
а признак провайдера на записи уникален — `idx_externalAuths_record_provider` —
|
||||||
|
и связь отвергалась. Умолчание почты стояло своим значением вместо выведенного
|
||||||
|
из `sub`
|
||||||
|
- **Почему не поймали раньше:** рецепт проверяли **длинной** формой, где почта
|
||||||
|
задана флагом явно. Короткую не гонял никто, хотя записана она первой и берут
|
||||||
|
читатели именно её
|
||||||
|
- **Что меняем:** проверять ту форму рецепта, которая записана **короче всех**.
|
||||||
|
Оракул — прогон именно её: два входа подряд разными `-sub` без прочих флагов,
|
||||||
|
затем счёт записей в коллекции пользователей. Само умолчание почты теперь
|
||||||
|
выводится из `-sub`
|
||||||
|
|
||||||
## 2026-08-15 — своя раздача статики потеряла отказ от записи успеха [пойман ревью]
|
## 2026-08-15 — своя раздача статики потеряла отказ от записи успеха [пойман ревью]
|
||||||
|
|
||||||
- **Где:** `internal/controller/http/webapp.go`, регистрация корневого маршрута;
|
- **Где:** `internal/controller/http/webapp.go`, регистрация корневого маршрута;
|
||||||
|
|||||||
@@ -335,6 +335,13 @@ Storage, оттуда его читает SpeechKit. Третий путь —
|
|||||||
|
|
||||||
- **Атака на сам сервер и на контур.** Компрометация хоста, прокси, Docker и
|
- **Атака на сам сервер и на контур.** Компрометация хоста, прокси, Docker и
|
||||||
Ansible — не наша граница.
|
Ansible — не наша граница.
|
||||||
|
- **Машина разработчика и то, что он на ней поднимает.** С 2026-08-15 в
|
||||||
|
репозитории лежит `cmd/oidcstub` — подставной провайдер OIDC, который выдаёт
|
||||||
|
сессию всякому спросившему и не сверяет ни секрета клиента, ни проверочного
|
||||||
|
кода PKCE. Двух вещей это не отменяет, и обе проверяемы: в образ он не едет
|
||||||
|
(ступень собирает `./cmd/transcriber` поимённо), а слушает петлевой адрес.
|
||||||
|
Периметра выкладки заглушка поэтому не касается; кто поднял её у себя в чужой
|
||||||
|
сети, отвечает за это сам.
|
||||||
- **Злоупотребление со стороны пользователя из белого списка.** Приглашённому
|
- **Злоупотребление со стороны пользователя из белого списка.** Приглашённому
|
||||||
доверяем полностью.
|
доверяем полностью.
|
||||||
- **Достоверность расшифровки.** Подмена или искажение текста на стороне
|
- **Достоверность расшифровки.** Подмена или искажение текста на стороне
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Ядро — `internal/service`: оно знает только интерфейсы `internal/contract`, а
|
// Ядро — `internal/service`: оно знает только интерфейсы `internal/contract`, а
|
||||||
// ffmpeg, Yandex и хранилище подставляются в `main.go`
|
// ffmpeg, Yandex и хранилище подставляются в точке входа `cmd/transcriber`
|
||||||
// (docs/architecture.md, «Принципы»).
|
// (docs/architecture.md, «Принципы»).
|
||||||
const core = "internal/service"
|
const core = "internal/service"
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ func TestЯдроНеЗнаетОбАдаптерах(t *testing.T) {
|
|||||||
if strings.HasPrefix(imp, adapterPrefix) {
|
if strings.HasPrefix(imp, adapterPrefix) {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"%s импортирует адаптер %s: ядро зависит от интерфейсов "+
|
"%s импортирует адаптер %s: ядро зависит от интерфейсов "+
|
||||||
"internal/contract, а реализацию подставляет main.go",
|
"internal/contract, а реализацию подставляет cmd/transcriber",
|
||||||
core, imp,
|
core, imp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -52,7 +52,7 @@
|
|||||||
- [🔬 Четыре недоказанные гипотезы о поверхности входа](items/login-surface-hypotheses.md) — Ревью назвало четыре пути, которых не смогло ни подтвердить, ни опровергнуть: браузера и живого провайдера в прогоне не было.
|
- [🔬 Четыре недоказанные гипотезы о поверхности входа](items/login-surface-hypotheses.md) — Ревью назвало четыре пути, которых не смогло ни подтвердить, ни опровергнуть: браузера и живого провайдера в прогоне не было.
|
||||||
- [🐞 Починить срок сессии, который ставит откат шага входа](items/rollback-restores-wrong-session-duration.md) — Константа defaultAuthTokenDuration в шаге 202608120001 названа умолчанием библиотеки, но 1209600 — это 14 суток, а умолчание PocketBase 432000, пять суток: откат объявляет возврат к умолчанию и ставит срок вдвое больше выбранных владельцем семи.
|
- [🐞 Починить срок сессии, который ставит откат шага входа](items/rollback-restores-wrong-session-duration.md) — Константа defaultAuthTokenDuration в шаге 202608120001 названа умолчанием библиотеки, но 1209600 — это 14 суток, а умолчание PocketBase 432000, пять суток: откат объявляет возврат к умолчанию и ставит срок вдвое больше выбранных владельцем семи.
|
||||||
- [🔬 Адрес объекта в тексте отказа SpeechKit](items/speechkit-error-text-leak.md) — Текст отказа операции приходит от Yandex и уезжает в журнал и в колонку error_text: если он несёт URI объекта, из журнала снова собирается ссылка на чужую запись.
|
- [🔬 Адрес объекта в тексте отказа SpeechKit](items/speechkit-error-text-leak.md) — Текст отказа операции приходит от Yandex и уезжает в журнал и в колонку error_text: если он несёт URI объекта, из журнала снова собирается ссылка на чужую запись.
|
||||||
- [🧹 Разобрать мелочи http-транспорта](items/http-transport-nits.md) — Маршруты зарегистрированы дважды, и переименование пути в main.go проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом.
|
- [🧹 Разобрать мелочи http-транспорта](items/http-transport-nits.md) — Маршруты зарегистрированы дважды, и переименование пути в cmd/transcriber проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом.
|
||||||
- [🧹 Запретить обращаться к Bot API мимо клиента бота](items/bot-api-only-through-bot-client.md) — Чистка отказа от адреса с токеном живёт в клиенте; свой http.Client в транспорте вернёт утечку молча — правило noctx такую подмену не ловит, а класс уже стоил одного дефекта.
|
- [🧹 Запретить обращаться к Bot API мимо клиента бота](items/bot-api-only-through-bot-client.md) — Чистка отказа от адреса с токеном живёт в клиенте; свой http.Client в транспорте вернёт утечку молча — правило noctx такую подмену не ловит, а класс уже стоил одного дефекта.
|
||||||
- [🧹 Свести пять расхождений между документами канона](items/docs-consistency-2026-08-13.md) — Сверка 2026-08-13 нашла шесть мест, где два документа отвечают на один вопрос по-разному; одно сведено при повышении раскладки, а три из пяти оставшихся стоят в architecture.md, и по ним читатель строит решения о выкладке и о периметре.
|
- [🧹 Свести пять расхождений между документами канона](items/docs-consistency-2026-08-13.md) — Сверка 2026-08-13 нашла шесть мест, где два документа отвечают на один вопрос по-разному; одно сведено при повышении раскладки, а три из пяти оставшихся стоят в architecture.md, и по ним читатель строит решения о выкладке и о периметре.
|
||||||
- [✨ Сопоставить пользователя Telegram с учётной записью](items/telegram-account-link.md) — Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны.
|
- [✨ Сопоставить пользователя Telegram с учётной записью](items/telegram-account-link.md) — Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны.
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
- **Тип:** chore
|
- **Тип:** chore
|
||||||
- **Категория:** Очередь — Поднято до контракта API: маршруты объявлены дважды, и переименование пути проходит тесты зелёным — переписывать контроллер под слепыми проверками нельзя.
|
- **Категория:** Очередь — Поднято до контракта API: маршруты объявлены дважды, и переименование пути проходит тесты зелёным — переписывать контроллер под слепыми проверками нельзя.
|
||||||
- **Зачем:** Маршруты зарегистрированы дважды, и переименование пути в main.go проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом.
|
- **Зачем:** Маршруты зарегистрированы дважды, и переименование пути в cmd/transcriber проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом.
|
||||||
- **Теги:** review-2026-08-11
|
- **Теги:** review-2026-08-11
|
||||||
|
|
||||||
Два дефекта, найденные ревью change `2026-08-11-fix-http-handler-tests`. Оба в
|
Два дефекта, найденные ревью change `2026-08-11-fix-http-handler-tests`. Оба в
|
||||||
`internal/controller/http`, мерджатся одним заходом.
|
`internal/controller/http`, мерджатся одним заходом.
|
||||||
|
|
||||||
**Маршруты зарегистрированы дважды.** `main.go` объявляет пути приёма и опроса,
|
**Маршруты зарегистрированы дважды.** `cmd/transcriber` объявляет пути приёма и опроса,
|
||||||
и сборка теста объявляет их же своей копией. Проверки ходят по копии, поэтому
|
и сборка теста объявляет их же своей копией. Проверки ходят по копии, поэтому
|
||||||
переименование пути в `main.go` их не роняет — а публичный контракт HTTP API
|
переименование пути в `cmd/transcriber` их не роняет — а публичный контракт HTTP API
|
||||||
проект объявил необратимым. Проверено мутацией на прогоне триажа: замена
|
проект объявил необратимым. Проверено мутацией на прогоне триажа: замена
|
||||||
`api.POST("/audio", …)` на `api.POST("/upload", …)` оставляет тесты зелёными.
|
`api.POST("/audio", …)` на `api.POST("/upload", …)` оставляет тесты зелёными.
|
||||||
Нашёл проход `review-specs`.
|
Нашёл проход `review-specs`.
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
## Затрагивает
|
## Затрагивает
|
||||||
|
|
||||||
- `internal/controller/http` и `main.go` — регистрация маршрутов приёма и
|
- `internal/controller/http` и `cmd/transcriber` — регистрация маршрутов приёма и
|
||||||
опроса, сегодня продублированная в обоих местах;
|
опроса, сегодня продублированная в обоих местах;
|
||||||
- `internal/controller/http/transcribe.go` — журнальная строка обработчика;
|
- `internal/controller/http/transcribe.go` — журнальная строка обработчика;
|
||||||
- `internal/controller/http/transcribe_test.go` — сборка теста, держащая свою
|
- `internal/controller/http/transcribe_test.go` — сборка теста, держащая свою
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ file name too long"`, и поля `file_ext` в ней нет вовсе.
|
|||||||
|
|
||||||
## Затрагивает
|
## Затрагивает
|
||||||
|
|
||||||
- слой журнала запроса в `main.go` и приведение пути `journalRoute`;
|
- слой журнала запроса в `cmd/transcriber` и приведение пути `journalRoute`;
|
||||||
- отказ подготовки рабочей копии в `internal/service`, шаг приёма записи;
|
- отказ подготовки рабочей копии в `internal/service`, шаг приёма записи;
|
||||||
- `docs/conventions/logging.md` — правило о длине поля журнала;
|
- `docs/conventions/logging.md` — правило о длине поля журнала;
|
||||||
- умолчание `MaxHeaderBytes` у сервера: сегодня оно единственное неназванное
|
- умолчание `MaxHeaderBytes` у сервера: сегодня оно единственное неназванное
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ POST /app/audiorecords без сессии → 401 ... http.status_code=0
|
|||||||
|
|
||||||
## Затрагивает
|
## Затрагивает
|
||||||
|
|
||||||
- слой журнала запроса в `main.go`: место, где снимается код ответа;
|
- слой журнала запроса в `cmd/transcriber`: место, где снимается код ответа;
|
||||||
- `docs/conventions/logging.md` — словарь полей запроса.
|
- `docs/conventions/logging.md` — словарь полей запроса.
|
||||||
|
|
||||||
## Критерии приёмки
|
## Критерии приёмки
|
||||||
|
|||||||
Reference in New Issue
Block a user