- в конфиг добавлены секция [auth.test_headers] и предохранитель [server] debug: заголовки входа подставляет слой транспорта, второго процесса локальный запуск больше не требует - подкоманда devtools proxy удалена целиком: всё, ради чего её поднимали, делает сам сервис - адресного предохранителя нет по решению владельца — цена названа в ADR и в модели угроз
726 lines
68 KiB
Markdown
726 lines
68 KiB
Markdown
# Триаж ревью — `config-test-headers-login`, 2026-08-23
|
||
|
||
## Сводка
|
||
|
||
- **Режим прогона:** по графу. Стадия — ревью кода.
|
||
- **База диффа:** `origin/master` совпадает с `HEAD`; предмет ревью — рабочее
|
||
дерево, включая неотслеживаемые файлы.
|
||
- **Размер:** 16 изменённых файлов + 4 новых (`test_headers.go`,
|
||
`test_headers_test.go`, `substitute.go`, `substitute_test.go`), ~240 строк
|
||
правок в коде; удалена подкоманда `proxy` пакета `cmd/devtools`.
|
||
- **Сложность:** новый слой транспорта, новая подсекция конфига с проверкой
|
||
старта, снятая оснастка; трогает барьер входа.
|
||
- **Метка:** `large`. Обоснование разметки — своих тем проекта сверх ядра не
|
||
заведено, поэтому `basics` не запускается: все темы разобраны именными
|
||
проходами.
|
||
- **Состояние гейта:** ЗЕЛЁНЫЙ. `task gate BASE=origin/master`, 13 шагов,
|
||
`go test -race` с работающим детектором, `golangci-lint` 0 issues,
|
||
`govulncheck` без находок. Сообщено проходом `autotests`; триаж гейт
|
||
не перезапускал.
|
||
- **Находок на входе:** 21 (A1; S1–S4; C1–C5; R1–R3; V1–V3 + три свойства
|
||
`adversary` без пути; O1). **Осталось в первых двух секциях:** 7.
|
||
|
||
### План с исходом по каждой теме
|
||
|
||
| Тема | Дом | Глубина | Кто закрывает | Исход |
|
||
|---|---|---|---|---|
|
||
| `requirements` | `openspec/specs/access/spec.md` + дельта | разбор | `specs` | **закрыта** проходом `specs`, 4 находки (S1–S4) |
|
||
| `autotests` | `CLAUDE.md`, «Гейт» | — | `autotests` | **закрыта** проходом `autotests`, 1 находка (A1), гейт зелёный |
|
||
| `conventions` | `docs/conventions/` целиком | разбор | `code` | **закрыта** проходом `code`, 5 находок (C1–C5) |
|
||
| `architecture` | `docs/architecture.md` + источник `docs/passport.md` | доказательство | `architecture` | **закрыта** проходом `architecture`, 3 находки (R1–R3) + 2 замечания «дешевле до мерджа» |
|
||
| `security` | `docs/security.md`, «Периметр» | доказательство | `adversary` | **закрыта** проходом `adversary`, 3 находки (V1–V3) + 3 свойства без построенного пути; пути обхода **самой подстановки** не построено (6 отрицательных проб) |
|
||
| `operations` | `docs/architecture.md`, «Эксплуатация» + источник `docs/database.md` | доказательство | `ops` | **закрыта** проходом `ops`, 1 находка (O1) + подтверждённый факт по постмортему 2 |
|
||
| тема проекта | своих тем нет | — | `basics` не запускался | **дома у темы нет**; `basics` на метке `large` не запускается по плану — это не пропуск |
|
||
|
||
**Тем без отчёта нет.** Все шесть заявленных домов дали отчёт.
|
||
|
||
### Сигнал о заниженной метке
|
||
|
||
**Пришёл от одного прохода, возражений нет.** `review-code` метку `large`
|
||
подтвердил и занижения не увидел. `review-basics` на этой метке не запускался,
|
||
поэтому второго независимого голоса нет — согласия двух проходов здесь не было
|
||
и быть не могло.
|
||
|
||
### Находка о самом прогоне
|
||
|
||
**Ни один из шести проходов не объявил свой потолок.** Устав велит называть
|
||
строкой, сколько находок показано, каков был потолок и что осталось за срезом;
|
||
такой строки нет ни у `specs`, ни у `code`, ни у `architecture`, ни у
|
||
`adversary`, ни у `ops`, ни у `autotests`. Это ровно тот повторяющийся пробел,
|
||
который записан в [docs/review.md](../../../../docs/review.md) строками 25–32 по
|
||
прогону `telegram-enabled-flag`: «находок больше нет» в отчёте прохода
|
||
неотличимо от «больше не поместилось». Механизации нет; сверка триажа —
|
||
единственное, что осталось, и она этот пробел только называет, а не закрывает.
|
||
|
||
### Дедупликация
|
||
|
||
- `A1` + `S2` — одна причина (строка журнала старта строится непроверенной
|
||
функцией). Сведены; **понижены**: триаж добыл им однократный оракул прогоном
|
||
бинарника, см. ниже.
|
||
- `S3` + `O1` — **две причины, а не одна**, вопреки подсказке задания: первая —
|
||
отброшенные метаданные разбора (`MetaData.Undecoded()`); вторая — зашитый
|
||
уровень журнала. Разведены в находки 1 и 2: чинятся порознь, и починка одной
|
||
не закрывает другую.
|
||
- `S4` + второе звено `O1` — одна причина (зашитый `LevelInfo`). Сведены в
|
||
находку 1.
|
||
- `R3` + замечание `specs` про два места — одна причина. Сведены в находку 6
|
||
вместе с `C1`: обе легли на одну строку `server.go:44`.
|
||
- `R1` + `R2` — разные причины в разных домах, сведены в одну находку 7 двумя
|
||
названными правками: обе — один абзац, обе про то, что нормативный документ
|
||
описывает изъятие мягче, чем оно есть.
|
||
- **Согласие проходов приоритет поднимало, `Confidence` — нет.** Ни одна
|
||
формулировка ниже не опирается на «подтверждено N проходами».
|
||
|
||
---
|
||
|
||
## Блокирует мердж
|
||
|
||
### 1. Каждый отладочный след нового входа пишется уровнем, который в настоящем прогоне выключен: разработчик получает `401` на всём приложении и пустой журнал
|
||
|
||
- Файл: `cmd/transcriber/main.go:39-41`; `internal/controller/http/substitute.go:97-99`;
|
||
`internal/controller/http/identity.go:106-110`
|
||
- Severity: major
|
||
- Confidence: high
|
||
- Оракул: **свой прогон настоящего бинарника.**
|
||
`go build -o $S/transcriber ./cmd/transcriber`, конфиг из `config.example.toml`
|
||
с `debug = true`, `trusted_proxies = ["127.0.0.1", "::1"]` и правильной секцией
|
||
`[auth.test_headers]`, запрос `curl http://127.0.0.1:18099/app/me` → `200`.
|
||
В журнале — стартовая `WARN "Identity headers are substituted from configuration" headers=[Remote-User]`
|
||
и `INFO "Account created from login header"`; **строки
|
||
`"Identity headers substituted from configuration"` нет ни одной**.
|
||
Второй прогон, тот же конфиг с опечаткой в имени секции → `код=401`, и в
|
||
журнале ровно `INFO "Incoming request" ... http.status_code=401` без единой
|
||
строки о причине.
|
||
Подтверждающая команда:
|
||
`grep -rn "\.Debug(" --include=*.go internal/ cmd/ | grep -v _test` → 6 мест;
|
||
`grep -rn "LevelDebug\|Level:" --include=*.go cmd/ internal/ | grep -v _test`
|
||
→ единственная точка сборки логгера `cmd/transcriber/main.go:40`
|
||
`Level: slog.LevelInfo`, ключа конфига у уровня нет, `os.Getenv`/`os.LookupEnv`
|
||
в непроверочном коде нет вовсе.
|
||
- Последствие: **все шесть отладочных строк сервиса недостижимы в любом
|
||
настоящем прогоне** — и обе, которые завела эта задача. Дельта-спека при этом
|
||
требует отладочную строку прямо (`specs/access/spec.md:193`: «Запрос, которому
|
||
заголовки подставлены, SHALL писаться на отладочном уровне») и в том же файле,
|
||
строка 394, сама формулирует цену: «поломка, записанная уровнем, который в бою
|
||
выключен, не записана вовсе». Критерий приёмки 9 — различимость «узнан
|
||
подстановкой» от «узнан прокси» на уровне запроса — в действительности не
|
||
достигнут ничем. Второе звено дороже первого: `identity.go:106` пишет «Request
|
||
carries no login header» тоже отладочным уровнем, а путь «заголовка нет,
|
||
предохранитель не включён» стал **единственным путём первого локального
|
||
запуска у каждого** после удаления подкоманды `devtools proxy`. Человек видит
|
||
`401` на всём приложении и пустой журнал — то самое «без единого следа», ради
|
||
устранения которого заведены отказы старта.
|
||
- Предложение: развилка ниже.
|
||
- Найдено проходами: `specs` (S4), `ops` (O1, второе звено); оракул добыт триажем
|
||
заново.
|
||
- **Действие: развилка.**
|
||
|
||
> Отладочные строки сервиса не печатаются никогда: уровень журнала зашит
|
||
> `LevelInfo` в единственной точке сборки логгера, ключа конфига у него нет.
|
||
> Спека этой задачи требует отладочную строку подстановки, и критерий 9 на ней
|
||
> стоит. Что делаем?
|
||
>
|
||
> 1. **Завести ключ уровня журнала** (`[server] log_level` либо уровень,
|
||
> выводимый из `[server] debug`). Спека прямо запрещает предохранителю
|
||
> трогать уровень журнала, значит это отдельный ключ и правка спеки
|
||
> `access` — расширение scope и новое имя ключа конфига, а имя ключа
|
||
> `CLAUDE.md` относит к необратимому и требует спросить человека.
|
||
> 2. **Поднять уровень двух строк до `Info`** — «подстановка сработала» и
|
||
> «заголовка входа нет». Обе штатны и частотны; в бою первая не печатается
|
||
> вовсе (предохранитель выключен), вторая станет строкой на каждый
|
||
> неузнанный запрос. Правка спеки нужна тоже — она уровень назначает
|
||
> поимённо.
|
||
> 3. **Снять требование отладочной строки из спеки и критерий 9 вместе с ним**,
|
||
> записав строкой цену: наблюдаемости подстановки на уровне запроса у
|
||
> сервиса нет, стартовой `WARN`-строки признано достаточно.
|
||
|
||
### 2. Опечатка в имени самой секции `[auth.test_headers]` не судится ничем: сервис поднимается, никого не узнаёт и следа не оставляет — ровно тот исход, ради которого заведён второй отказ старта
|
||
|
||
- Файл: `internal/config/config.go:242-256` (метаданные разбора отбрасываются);
|
||
`internal/config/test_headers.go:33-43`
|
||
- Severity: major
|
||
- Confidence: high
|
||
- Оракул: **свой прогон, два уровня.**
|
||
(1) Модульный: декодер `BurntSushi/toml v1.5.0` на входе
|
||
`[server]\ndebug = true\n[auth]\n[auth.test_headrs]\n"Remote-User" = "dev"` даёт
|
||
`decode err=<nil>`, `undecoded=[auth.test_headrs auth.test_headrs.Remote-User]`,
|
||
`Server.Debug=true len(TestHeaders)=0 substituting=false`,
|
||
`ValidateTestHeaders err = <nil>`. Контроль: та же опечатка **внутри** секции
|
||
(`Remote-Usr`) ловится —
|
||
`auth: секция [auth.test_headers] называет заголовок, которого сервис не читает: Remote-Usr`.
|
||
(2) Сквозной: настоящий бинарник на конфиге с `[auth.test_headrs]` поднимается
|
||
без единого предупреждения, `curl /app/me` → `401`, в журнале только
|
||
`INFO "Incoming request" ... http.status_code=401`.
|
||
- Последствие: `design.md` заявляет защиту словами «опечатка в имени иначе
|
||
кончается сервисом, который никого не узнаёт, без единого следа», и защита эта
|
||
покрывает опечатку в имени **ключа**, но не в имени **секции**. Разница для
|
||
человека нулевая — он ошибается в обеих строках одинаково, — а исход
|
||
противоположный: в одном случае отказ старта с именем ключа, в другом
|
||
безмолвный `401` на всём приложении. Стоимость несёт каждый первый локальный
|
||
запуск, потому что подкоманды `devtools proxy` больше нет и обходного пути не
|
||
осталось. Отдельно: та же дыра проглатывает любую опечатку в **любой** секции
|
||
конфига, не только в новой.
|
||
- Предложение: развилка ниже.
|
||
- Найдено проходами: `specs` (S3), `ops` (O1, первое звено); оракул добыт триажем
|
||
заново.
|
||
- **Действие: развилка.**
|
||
|
||
> `toml.DecodeFile` возвращает `MetaData`, и в нём лежит перечень ключей,
|
||
> которых структура не знает. Сервис его отбрасывает, поэтому опечатка в имени
|
||
> секции проходит молча. Что делаем?
|
||
>
|
||
> 1. **Узкая правка в границах задачи:** при `[server] debug = true` и пустой
|
||
> секции имитации ронять старт (или писать `WARN`), если
|
||
> `MetaData.Undecoded()` называет что-нибудь под `auth`. Ловит ровно этот
|
||
> класс, не трогает остальной конфиг, требует одного нового абзаца в
|
||
> дельта-спеке.
|
||
> 2. **Правило шире задачи:** судить `MetaData.Undecoded()` на всём конфиге —
|
||
> непонятый ключ роняет старт с его именем. Закрывает опечатки во всех
|
||
> секциях разом, но это новая норма в `docs/conventions/config.md` и
|
||
> отдельный change: сегодняшние конфиги на сервере могут нести ключи,
|
||
> которых структура уже не знает (постмортем 2 подтвердил, что откат
|
||
> бинарника поверх нового конфига сегодня безопасен **именно** потому, что
|
||
> лишние ключи молча игнорируются, — правило это свойство снимет).
|
||
> 3. **Ничего не делать в коде**, а записать исход строкой в спеке и в
|
||
> `config.example.toml`: «опечатка в имени секции даёт `401` без следа;
|
||
> сверяйте имя секции по образцу». Дешевле всего и честнее молчания.
|
||
|
||
### 3. Спека объявляет матрицу отказов старта полной и называет три случая, а код даёт четвёртый: следующий change снимет его как самодеятельность, и одно из двух значений секции начнёт теряться молча
|
||
|
||
- Файл: `internal/config/test_headers.go:86-93`;
|
||
`openspec/changes/config-test-headers-login/specs/access/spec.md:109`;
|
||
`openspec/changes/config-test-headers-login/tasks.md:227-231`
|
||
- Severity: major
|
||
- Confidence: high
|
||
- Оракул: **свой прогон.** Вход
|
||
`[server] debug = true` + `[auth.test_headers]` с `"Remote-User" = "one"` и
|
||
`"remote-user" = "two"`: декодер даёт `len(c.Auth.TestHeaders) == 2`,
|
||
`HeaderSubstitution()` возвращает `map[Remote-User:two]` — **значение `"one"`
|
||
потеряно**, — а `ValidateTestHeaders` роняет старт строкой
|
||
`auth: в секции [auth.test_headers] два ключа называют один заголовок: имя
|
||
заголовка нечувствительно к регистру, и одно из значений потерялось бы молча`.
|
||
Дословный текст спеки на строке 109: «Отказом MUST быть **каждый из трёх**
|
||
случаев»; сценария про совпадение канонических имён в дельте нет. Дословный
|
||
текст `tasks.md:227`: «Матрица отказов старта **полна и тотальна**: у каждой
|
||
комбинации новых ключей объявлен исход».
|
||
- Последствие: реализатор, читающий спеку как полную матрицу — а `tasks.md`
|
||
велит читать её именно так, — снимет четвёртую проверку как не подпёртую
|
||
требованием. После снятия конфиг с двумя ключами разного регистра поднимает
|
||
сервис, схлопывает их в один и молча берёт тот, который последним лёг в карту:
|
||
порядок перебора TOML не гарантирован, и человек получает то одно значение, то
|
||
другое от запуска к запуску. Это в точности класс «молчаливая потеря
|
||
значения», ради которого проверка и написана. Правка целиком в границах
|
||
задачи: дельта-спека — часть этого change.
|
||
- Предложение: дописать четвёртый случай в требование «Настройка, открывающая
|
||
вход всем, роняет старт» и завести к нему сценарий; поправить `tasks.md:227`
|
||
так, чтобы перечисление читалось «предохранитель × (пустая / заполненная /
|
||
заполненная негодно / заполненная неоднозначно)».
|
||
- Найдено проходом: `specs` (S1); оракул добыт триажем заново.
|
||
- **Действие: инлайн.**
|
||
|
||
---
|
||
|
||
## Стоит исправить сейчас
|
||
|
||
### 4. Бинарник читает `.env`, проект называет его секретосодержащим, а `.gitignore` его не игнорирует: первый же ключ, положенный туда, уезжает в git необратимо
|
||
|
||
- Файл: `.gitignore` (нет записи); `.dockerignore:21`;
|
||
`cmd/transcriber/main.go:114-117`
|
||
- Severity: major
|
||
- Confidence: high
|
||
- Оракул: **свои команды.** `git check-ignore -v .env` → пусто, `exit=1` (файл не
|
||
игнорируется). `.dockerignore:19-21` — комментарий «Настройки с секретами.
|
||
Образ берёт конфиг на сервере, а не из дерева.» и следом строка `.env`.
|
||
`cmd/transcriber/main.go:115` — `godotenv.Load()`. Прогон настоящего бинарника
|
||
печатает при каждом старте
|
||
`level=WARN msg="Warning: .env file not found, using system environment variables"`.
|
||
При этом `grep -rn "os.Getenv\|os.LookupEnv" --include=*.go internal/ cmd/ | grep -v _test`
|
||
→ **ни одного совпадения**: переменных окружения сервис не читает нигде.
|
||
- Последствие: инвариант `CLAUDE.md` — «**Секрет не покидает конфиг.** Ключ
|
||
SpeechKit и пара ключей Object Storage не попадают в git… Нарушение
|
||
необратимо: утёкший ключ отзывают и меняют вручную во всех местах выкладки.
|
||
**critical**». Путь к нарушению короткий и сервис сам его подсказывает:
|
||
стартовая `WARN`-строка сообщает владельцу, что `.env` ожидается, тот заводит
|
||
файл с ключами — и `git add .` кладёт его в индекс, потому что
|
||
`.gitignore` про `.env` не знает. Единственный барьер — `gitleaks git --staged`
|
||
на pre-commit, а он судит по образцам и на произвольном ключе SpeechKit молчать
|
||
вправе. Сама подсказка при этом ложная: ключи из `.env` не сработали бы, читать
|
||
переменные окружения сервису нечем. **`critical` не ставлю:** пути до
|
||
настоящего секрета в git я не построил — работающего мотива положить ключ в
|
||
`.env` у владельца нет, — значит по контракту выше `major` эта находка не
|
||
поднимается.
|
||
- Предложение: строку `.env` в `.gitignore` рядом с `config.toml` — это одна
|
||
строка и весь инлайн. Мёртвый `godotenv.Load()` вместе с зависимостью и
|
||
вводящей в заблуждение `WARN`-строкой уходит задачей: правка не этого change.
|
||
- Найдено проходом: `adversary` (свойство без построенного пути); оракул и
|
||
переформулировка — триаж (проход утверждал, что `.env` читается, но не заметил,
|
||
что читать из него некому).
|
||
- **Действие: инлайн** — только строка в `.gitignore`. Снятие `godotenv` —
|
||
задачей, см. «Урожай».
|
||
|
||
### 5. Чужой адрес почты, занятый первым пришедшим, навсегда и молча оставляет без почты его настоящего владельца — и `docs/security.md` утверждает обратное
|
||
|
||
- Файл: `internal/controller/http/identity.go:134-138`;
|
||
`internal/adapter/repo/sqlite/identity.go:77-110`; текст
|
||
`docs/security.md:71-76`
|
||
- Severity: major
|
||
- Confidence: high
|
||
- Оракул: **свой падающий сценарий, прогнан на настоящей поверхности сервиса**
|
||
(`setupEnv` + `AppChain`, база SQLite во временном каталоге, маршрут
|
||
`/app/me`). Два шага:
|
||
1. Запрос с `Remote-User: squatter` и `Remote-Email: victim@corp.example` с
|
||
доверенного адреса → `200`. `SELECT email FROM users WHERE provider_login='squatter'`
|
||
→ `victim@corp.example`.
|
||
2. Запрос настоящего владельца: `Remote-User: victim`,
|
||
`Remote-Email: victim@corp.example` → `200`, `Account created from login header`.
|
||
`SELECT email ... WHERE provider_login='victim'` → **`""`**. Строк журнала о
|
||
занятом адресе — ноль.
|
||
|
||
Второй прогон показывает асимметрию правила прямо: два значения `Remote-User`
|
||
→ `401`; два значения `Remote-Email` → `200`, и в базу ложится **первое**
|
||
(`attacker-first@example.com`).
|
||
- Последствие: `docs/security.md:71-74` называет прокси, **добавляющий** заголовок
|
||
вместо замены, главной угрозой периметра и пишет: «Половину этой беды сервис
|
||
закрывает сам: запрос с двумя значениями `Remote-User` не узнаёт никого».
|
||
Утверждение верно ровно для одного заголовка из трёх. `Remote-Name` и
|
||
`Remote-Email` берутся `Get()` — первым значением, то есть присланным анонимом,
|
||
— и логин от них не защищает. Уникальный индекс `idx_users_email` не даёт
|
||
двум записям делить адрес, `EnsureUser` найденную запись не переписывает
|
||
никогда, а ветвь отката заводит настоящего владельца через
|
||
`insertUser(tx, login, name, "")` **без строки журнала**. Порча тихая и
|
||
постоянная: обратного пути у неё в коде нет. **Сегодня цена почти нулевая** —
|
||
`contract.UserAccount` поля `Email` не имеет вовсе, приложение почту не
|
||
показывает, доступ по ней не выдаётся; поэтому не блокирует мердж. Цена
|
||
появляется в тот день, когда почту начнут читать (в бэклоге лежит
|
||
`api-tokens`), и к этому дню колонка уже будет испорчена молча.
|
||
- Предложение: судить `Remote-Name` и `Remote-Email` тем же правилом, что
|
||
`Remote-User` — `len(r.Header.Values(name)) != 1` даёт пустое значение, а не
|
||
первое; ветвь отката уникальности почты обязана оставить строку журнала
|
||
видимого уровня.
|
||
- Найдено проходом: `adversary` (V1); оракул добыт триажем заново, обоими
|
||
звеньями.
|
||
- **Действие: развилка.**
|
||
|
||
> Правка меняет боевой путь узнавания, а не отладочную подстановку, — то есть
|
||
> выходит за scope этой задачи и требует нового требования в спеке `access`
|
||
> (сегодня спека нормирует многозначность только для `Remote-User`). Что
|
||
> делаем?
|
||
>
|
||
> 1. **Взять в этот change:** три строки в `identity.go` + строка журнала в
|
||
> ветви отката + абзац в требование спеки. Логика ровно та же, что слой
|
||
> подстановки уже применил к себе («Запрос без `Remote-User`, но с
|
||
> `Remote-Email` иначе собрал бы личность из двух источников»), — правило
|
||
> просто не донесено до соседних двадцати строк.
|
||
> 2. **Отдельной задачей**, потому что предмет другой и цена сегодня нулевая:
|
||
> почту никто не читает. Тогда `docs/security.md:71-74` надо поправить в
|
||
> этом мердже — фраза «половину этой беды сервис закрывает сам» описывает
|
||
> периметр точнее, чем он есть, и ссылаться на неё нельзя.
|
||
> 3. **Только документ:** записать асимметрию как принятую цену, потребовав от
|
||
> прокси перезаписи всех трёх `Remote-*` (требование к контуру уже записано,
|
||
> см. «Недоступно проверке»), и колонку почты не трогать.
|
||
|
||
### 6. Страж, отменяющий слой подстановки, не защищён ни одной проверкой и считает «подставляем ли» вторым выражением: убрать его можно молча, а разойтись он может сам
|
||
|
||
- Файл: `internal/controller/http/server.go:27-49` (строка 44
|
||
`if len(substitution) > 0`); `internal/config/test_headers.go:33-43`;
|
||
`internal/controller/http/substitute_test.go:229-246`
|
||
- Severity: minor
|
||
- Confidence: high
|
||
- Оракул: чтение с двумя дословными сопоставлениями.
|
||
(1) Комментарий `server.go:30-31` утверждает: «Судит эту пустоту точка входа:
|
||
перечень приходит сюда уже готовым», — а строка 44 судит её второй раз своим
|
||
выражением. Тождество двух формулировок (`c.Server.Debug && len(TestHeaders) > 0`
|
||
против `len(substitution) > 0`) сегодня держится только тем, что
|
||
`HeaderSubstitution` возвращает `nil` в ложной ветке; ничем другим оно не
|
||
закреплено.
|
||
(2) Единственная проверка на пустую имитацию — `TestEmptySectionLeavesHeadersUntouched`
|
||
— судит три вещи: код `401`, неизменное число учётных записей и отсутствие в
|
||
журнале строки `"Identity headers are not substituted"`. Строка эта пишется
|
||
**только** в ветке недоверенного пира (`substitute.go:80`), а запрос проверки
|
||
идёт с `trustedPeer`, — значит утверждение истинно при любом исходе. Первые два
|
||
утверждения тоже истинны при снятом страже: заголовка `Remote-User` в запросе
|
||
нет, слой удалил бы `Remote-Email` и пропустил дальше, узнавание всё равно
|
||
ответило бы `401` и записи бы не завело.
|
||
Мутационную сверку — снять страж и убедиться, что проверка зеленеет, — не
|
||
делал: `CLAUDE.md`, «Запреты», прямо запрещает мутационную сверку оракулов.
|
||
- Последствие: сценарий дельта-спеки «Пустая секция имитации ничего не трогает»
|
||
не проверен ничем, хотя в наборе проверок выглядит проверенным, — а именно к
|
||
этому сценарию спека привязывает своё «поведение, которого в бою нет»
|
||
(срезанный `Remote-Email` у запроса без `Remote-User`). Дубль предиката при
|
||
этом расходится в первой же правке, дающей непустой признак при пустой карте:
|
||
исход будет «старт предупреждает о подстановке, а слоя в цепочке нет» — и
|
||
предупреждение соврёт.
|
||
- Предложение: `AppChain` принимает вторым значением сам предикат
|
||
(`substituting bool`) вместо того, чтобы выводить его из длины карты; проверка
|
||
судит **дошедшее до узнавания** — что `Remote-Email`, посланный запросом,
|
||
доехал до обработчика нетронутым, — а не журнал.
|
||
- Найдено проходами: `code/техника` (C1), `architecture` (R3), `specs`
|
||
(замечание про два места).
|
||
- **Действие: инлайн.**
|
||
|
||
### 7. Два нормативных документа описывают изъятие мягче, чем оно есть: паспорт обосновывает его доводом, который в изъятии не действует, а принцип архитектуры запрещает ровно то, что сам приводит примером
|
||
|
||
- Файл: `docs/passport.md:65-75`; `docs/architecture.md:84-91`
|
||
- Severity: minor
|
||
- Confidence: high
|
||
- Оракул: дословное сопоставление внутри одного изменения.
|
||
(1) `docs/passport.md:73` — «подставленное имя проходит то же узнавание, что и
|
||
пришедшее, **а кого пускать, по-прежнему решает провайдер**». Дельта-спека в
|
||
том же изменении: «Подстановка — это настройка, которой сервис называет
|
||
пришедшего сам, **никого не спросив**» (`substitute.go:32-33` теми же словами).
|
||
Провайдера в изъятии нет вовсе. Там же паспорт пишет «*Изъятие одно, и оно про
|
||
прогон без контура*» — код таким не ограничен: подстановка работает везде, где
|
||
адрес пира доверенный, и `docs/security.md:97-102` это признаёт прямо
|
||
(«Боевая поломка машиной не исключена»). Два документа одного изменения
|
||
расходятся об одном изъятии.
|
||
(2) `docs/architecture.md:84-91` — заголовок нормы «Подставной собеседник
|
||
живёт **в коде или в оснастке**», тело — «только под ключом, названным своим
|
||
предметом, — **не под общим словом вроде «режим отладки»**», и следом
|
||
собственным примером норма называет случай, который живёт в боевом бинарнике и
|
||
стоит под `[server] debug`. Единственная операционная формулировка изъятия —
|
||
закрытый перечень следствий — живёт в спеке, и принцип на неё не ссылается.
|
||
- Последствие: `docs/passport.md` — единственный дом границы домена, и `CLAUDE.md`
|
||
велит читать его перед задачей. Записанная так граница перестаёт исключать то,
|
||
ради чего заведена: следующая задача (в бэклоге лежит `api-tokens`) сможет
|
||
сослаться на строку «кого пускать, решает провайдер» как на действующее
|
||
ограничение, которого нет. Норма архитектуры, обратно, читается как «шипящая
|
||
реализация нарушает наш собственный принцип» — а ключ `[server] debug`
|
||
владелец подтвердил дважды и переоткрывать его нельзя; значит расходится норма,
|
||
а не код. `docs/review.md` при этом уже объявил пробел закрытым этой нормой,
|
||
то есть на неё уже сослались.
|
||
- Предложение: в паспорте заменить довод на верный («сервис называет пришедшего
|
||
сам; границы домена это не двигает, потому что учётных записей он по-прежнему
|
||
не заводит и допуска не проверяет — заведение строки первым обращением
|
||
остаётся зеркалированием») и снять слова «оно про прогон без контура». В
|
||
`docs/architecture.md` свести принцип к связывающему правилу со ссылкой на
|
||
требование спеки (закрытый перечень следствий предохранителя), не повторяя
|
||
перечень второй копией.
|
||
- Найдено проходом: `architecture` (R1, R2).
|
||
- **Действие: инлайн** (две правки, оба файла — документация).
|
||
|
||
---
|
||
|
||
## Гипотезы без доказательства
|
||
|
||
Понижены: оракула на этом прогоне триаж не добыл — по бюджету (одна попытка на
|
||
находку ушла на `critical`/`major`) либо по запрету проекта.
|
||
|
||
### Имя копии в каталоге данных выводится из идентификатора записи одним инкрементом
|
||
|
||
- Файл: `internal/ident/ident.go:79-99`; `internal/service/transcribe.go:176-201`
|
||
- Было: minor / high (`adversary`, V2). **Осталось minor:** оракул у прохода
|
||
есть (`TestProbeStorageNameDerivesFromRecordID`, `TestProbeDerivationRate` —
|
||
49 из 50 приёмов подряд), но триаж его не воспроизводил, а согласие одного
|
||
прохода с самим собой подтверждением не является.
|
||
- Последствие: инвариант `CLAUDE.md` «Имя файла на диске задаёт сервис, а в
|
||
журнал не идёт… строка журнала иначе стала бы бессрочным ключом к чужой
|
||
записи» соблюдён по букве и не по назначению: `record_id` и `file_ext`
|
||
присутствуют в журнале порознь и дают имя арифметикой. Доступа это не даёт —
|
||
карточка и файл сужены владельцем, — поэтому не выше minor.
|
||
- Предложение: случайное положительное приращение вместо единицы либо снять
|
||
посылку из инварианта. Уходит задачей.
|
||
|
||
### Управляющий знак в расширении имени даёт отправителю `500`, а владельцу — `ERROR`, неотличимый от аварии хранилища
|
||
|
||
- Файл: `internal/service/transcribe.go:171-188`;
|
||
`internal/adapter/repo/sqlite/file_repo.go:56-67`
|
||
- Было: minor / high (`adversary`, V3). **Осталось minor:** падающий тест у
|
||
прохода есть, триаж его не воспроизводил.
|
||
- Последствие: комментарий `transcribe.go:171-175` объявляет этот исход
|
||
недопустимым и ради него заведён `maxExtLen`; разрез сделан по длине и не
|
||
сделан по составу знаков. Побочно `newWorkFile` не снимает путь с
|
||
`*os.PathError`, и путь во временном каталоге уезжает в строку `ERROR`.
|
||
- Предложение: судить состав знаков расширения тем же приёмом, что и длину.
|
||
Уходит задачей.
|
||
|
||
### Текст отказа провайдера уезжает в `error_text` и в журнал целиком, а самый ожидаемый его вид несёт `sourceURI` — бакет и ключ объекта
|
||
|
||
- Файл: не локализован проходом точнее уровня «ветвь отказа распознавания»
|
||
- Было: minor / medium, **оракула нет и быть не может на этом прогоне**:
|
||
настоящий SpeechKit нужен, а `CLAUDE.md`, «Запреты», прогон на реальных ключах
|
||
запрещает («Yandex Cloud за деньги»). Строка живёт в
|
||
`docs/review.md`, «Недоступно проверке», первым списком.
|
||
- Последствие (условное): ключ объекта в `error_text` виден владельцу записи
|
||
карточкой; инвариант «Содержимое записи остаётся приватным» им прямо не
|
||
нарушен, но перечень того, что уходит в текст ошибки, ничем не ограничен.
|
||
- Предложение: задачей — ограничить текст внешнего отказа перечнем полей, а не
|
||
пересказывать его дословно.
|
||
|
||
### Требования «прокси обязан дописывать `X-Forwarded-For`» нигде не записано, в отличие от `Remote-*`
|
||
|
||
- Файл: `docs/security.md:103-113`
|
||
- Было: minor / medium (`adversary`). **Оракула нет:** правило живёт в
|
||
`files/caddyproxy/Caddyfile.template` репозитория `pet-project-server`, и
|
||
`docs/review.md`, «Недоступно проверке», относит поведение обратного прокси к
|
||
тому, чего не проверит ни один проход.
|
||
- Последствие (условное): `docs/security.md` описывает чтение цепочки
|
||
справа налево словами «правое приписал ближайший к нам прокси» — это
|
||
предположение о прокси, а не требование к нему. Прокси, который `X-Forwarded-For`
|
||
не дописывает вовсе, оставляет ключ бюджета ограничителя целиком в руках
|
||
спрашивающего.
|
||
- Предложение: задачей — дописать требование к контуру рядом с уже записанным
|
||
требованием про `Remote-*`.
|
||
|
||
---
|
||
|
||
## Promote candidates
|
||
|
||
- **Правило «`internal/config` не знает транспорта» механизируемо, но не
|
||
механизировано.** Норма записана этим же изменением
|
||
(`docs/conventions/config.md`, «Проверка, охватывающая две секции разом»), а
|
||
`grep -n "config" internal/archrules/arch_test.go` не даёт ни одного
|
||
совпадения: сканера у правила нет. Кандидат — правило `internal/archrules` по
|
||
нетестовым файлам пакета. Предложил проход `architecture`.
|
||
**Оговорка к механизации:** `internal/config/test_headers_test.go:1-8`
|
||
объявлен `package config` и импортирует `internal/controller/http` — то самое
|
||
ребро. В рабочем бинарнике его нет, но `go test ./internal/config` линкует всю
|
||
поверхность HTTP, а будущая надобность конфига в транспорте даст цикл импорта
|
||
на уровне проверок. Правило по нетестовым файлам этот файл пропустит; правило
|
||
по всем — уронит гейт до переноса файла во внешний `package config_test`.
|
||
- **`errors.Join` в проверках конфига.** `docs/conventions/errors.md`,
|
||
«Несколько ошибок», называет проверку конфига поимённо и требует «все проблемы
|
||
разом»; `ValidateTestHeaders` возвращается на первом несовпадении, записи
|
||
«*Расхождение:*» у пункта нет. Это претензия на правило, а не на этот код:
|
||
проверки остальных секций ведут себя так же. Предложил проход `code` (C5).
|
||
- **Сверка `toml.MetaData.Undecoded()` как норма конфига.** Вариант 2 развилки
|
||
находки 2. Если владелец выберет узкую правку, кандидат остаётся здесь:
|
||
непонятый ключ в любой секции сегодня проходит молча.
|
||
|
||
---
|
||
|
||
## Границы покрытия
|
||
|
||
### План: темы, глубины, дома
|
||
|
||
Воспроизведён таблицей в сводке выше. Дома: `openspec/specs/access/spec.md` +
|
||
дельта (`requirements`, разбор); `CLAUDE.md`, «Гейт» (`autotests`);
|
||
`docs/conventions/` целиком (`conventions`, разбор); `docs/architecture.md` +
|
||
источник `docs/passport.md` (`architecture`, доказательство); `docs/security.md`,
|
||
«Периметр» (`security`, доказательство); `docs/architecture.md`, «Эксплуатация» +
|
||
источник `docs/database.md` (`operations`, доказательство). **Тема без дома
|
||
одна** — «тема проекта»: своих тем сверх ядра проект не завёл, и это состояние
|
||
плана, а не пропуск.
|
||
|
||
### Какие проходы запускались
|
||
|
||
Метка `large`, режим «по графу». Запускались шесть: `autotests`, `specs`, `code`,
|
||
`architecture`, `adversary`, `ops`. Триаж — седьмой.
|
||
|
||
### Какие не запускались и почему
|
||
|
||
- `basics` — **по метке**: на `large` он не запускается, потому что все темы
|
||
ядра разобраны именными проходами, а своих тем у проекта нет. Прямое следствие:
|
||
второго независимого голоса о заниженности метки нет — сигнал пришёл только от
|
||
`review-code`.
|
||
- Проход независимой реализации — снят из конвейера по стоимости.
|
||
- Проход про идиоматичность — упразднён.
|
||
|
||
### Чего каждый запущенный проход не мог проверить в принципе
|
||
|
||
- `autotests` — судит гейт и покрытие, но не судит, **что** проверяют зелёные
|
||
проверки: находка 6 (проверка, которая не может упасть) из его charter'а не
|
||
видна, её принёс `code`.
|
||
- `specs` — судит соответствие кода дельте и полноту дельты; не судит, работает
|
||
ли требование в настоящем прогоне (потому S4 у него остался рассуждением, а
|
||
оракул добыл триаж).
|
||
- `code` — судит записанные конвенции и технику; периметра и эксплуатации не
|
||
касается.
|
||
- `architecture` — судит направления зависимостей и нормы домов; карту графа
|
||
собирал **вручную**, подготовленной команды у проекта нет.
|
||
- `adversary` — строит пути; поведения настоящих внешних собеседников не
|
||
проверяет вовсе.
|
||
- `ops` — судит эксплуатацию по замерам своего прогона; профиля настоящей
|
||
нагрузки у него нет (проект работает на единицах записей в день).
|
||
- **Триаж (этот проход)** — ничего нового не находит по определению: работает с
|
||
чужими выводами, кода в поисках дефектов не читает. Пропуск любого прохода —
|
||
пропуск триажа тоже, и единственное средство против него — поимённая сверка
|
||
плана выше.
|
||
|
||
### Что осталось целиком на человеке
|
||
|
||
**Не проверит ни один проход** (`docs/review.md`, «Недоступно проверке», первый
|
||
список — воспроизводится отдельно от второго намеренно):
|
||
|
||
- `operations`: поведение внешних сервисов под нагрузкой и на границах —
|
||
SpeechKit и Object Storage поднять в тесте нечем;
|
||
- `operations`: реальный профиль нагрузки; утверждения о росте остаются
|
||
условиями, а не замерами;
|
||
- `security`: стойкость `ffmpeg` к вредоносному входу;
|
||
- `security`: поведение настоящей Authelia и правило обратного прокси на домен
|
||
сервиса — **от прокси зависит весь барьер**, он обязан заголовки `Remote-*`
|
||
перезаписывать, а не пропускать пришедшие; правило живёт в
|
||
`pet-project-server`. **Прямо относится к находке 5:** сценарий
|
||
дописывающего прокси проверить отсюда нечем, а асимметричную реакцию сервиса
|
||
на него — можно, и она проверена;
|
||
- `security`: поведение браузера с куками (класс пуст с 2026-08-22, строка стоит
|
||
как маркер).
|
||
|
||
**Перестали проверять сознательно** (второй список, тот же раздел):
|
||
|
||
- `autotests`: разбор вывода настоящего `ffprobe` — проверки получают
|
||
длительность от подставного источника; решение и цена в
|
||
`adr/ADR-2026-08-11-stub-adapters-in-tests.md`;
|
||
- работа сервиса с настоящими внешними собеседниками. Сам сервис поднять можно и
|
||
этот прогон его поднимал; **остаток** — за настоящие SpeechKit и Object
|
||
Storage живой прогон не отвечает, ключи выдуманные, распознавание подменяется
|
||
в коде. Вход живой прогон с 2026-08-22 проверяет целиком, и раздел уже
|
||
переписан этой задачей: подставного прокси нет, заголовок ставит сам сервис.
|
||
|
||
**Общее, вне обоих списков:** история инцидентов, поведение под реальным
|
||
потоком, поведение внешних систем в их версиях, завязка потребителей на текущее
|
||
поведение и вопрос «а нужна ли эта функциональность вообще» — ничем из
|
||
перечисленного конвейер не занимался.
|
||
|
||
### Четыре строки, которых не принесёт ни один проход
|
||
|
||
1. **Решения проекта не сверялись.** `docs/adr.*` — процессный документ, прогон
|
||
его не открывает. Расхождение изменения с записанным решением ловит скилл
|
||
`av-dev:doc-healthcheck`, а не ревью. В этом изменении есть чему разойтись:
|
||
`ADR-2026-08-22-login-by-trusted-header.md` описывает вход, к которому
|
||
заведено изъятие, и сверен с ним никем не был.
|
||
2. **Записанные наблюдения проекта не использовались.** `docs/research.*` — тоже
|
||
процессный. Всякое число в отчёте снято проходом на этом прогоне: бенчмарк
|
||
`BenchmarkSubstituteIdentityHeaders` (2972 ns/op, 5718 B/op, 17 allocs/op) —
|
||
замер прохода `ops`, покрытие (`test_headers.go` 100 %,
|
||
`SubstituteIdentityHeaders` 94,4 %, `SubstitutedHeaderNames` 0,0 %) — вывод
|
||
`go tool cover -func` прохода `autotests`, остальные числа — прогоны триажа с
|
||
приложенными командами.
|
||
3. **Поимённая сверка с руководствами по стилю Go не задавалась ни одним
|
||
проходом.** Различение «идиоматично против распространено» не спрашивает
|
||
никто с тех пор, как упразднён проход про идиоматичность.
|
||
4. **Альтернативной реализации, с которой можно сдиффить решения, у конвейера
|
||
нет.** Проход независимой реализации снят по стоимости, а не по замеру; «не
|
||
знаю, чего не знаю» на этом прогоне не доставал никто.
|
||
|
||
Метка — `large`, поэтому пятой строки (сужение `security`/`operations`/
|
||
`architecture` до записанных инвариантов) здесь нет: все три темы разобраны
|
||
именными проходами на глубине «доказательство».
|
||
|
||
### Каких документов проекта не хватило
|
||
|
||
- **`docs/review.md`, «Типовые ложноположительные» — есть и непуст**, прочитан
|
||
целиком (5 записей, две из них отменённые намеренно). Ни одна находка этого
|
||
прогона под них не подпадает; отсев шёл и по проектному входу, и по общим
|
||
критериям.
|
||
- **Раздела инвариантов `CLAUDE.md` хватило.** На него опираются находки 4
|
||
(«Секрет не покидает конфиг», **critical**, необратимо) и гипотеза про имя
|
||
копии («Имя файла на диске задаёт сервис, а в журнал не идёт», **critical**).
|
||
Ранжирование по обратимости взято оттуда же, а не выведено из кода.
|
||
- **Не хватило: у `[server] debug` нет дома, называющего исчерпывающий перечень
|
||
его следствий в одном месте.** Перечень объявлен трижды — комментарием
|
||
`internal/config/config.go:65-71`, комментарием `config.example.toml:7-13` и
|
||
требованием дельта-спеки, — и три копии уже расходятся: комментарий конфига
|
||
называет пять исключённых следствий, образец — четыре (в нём нет «проверок
|
||
старта»). Строка о том, где перечень канонический, отсутствует, и триаж
|
||
выбирал спеку по общему правилу «спека нормативна», а не по записанному
|
||
указателю.
|
||
- **Не хватило: `docs/conventions/logging.md` не называет, каким уровнем
|
||
пользоваться, когда отладочный недостижим.** Уровень журнала у сервиса зашит,
|
||
ключа у него нет, и конвенция про это молчит — поэтому находка 1 подана
|
||
развилкой, а не инлайном: своего основания выбрать вариант у триажа нет.
|
||
- **Оракул из журнала дефектов не применялся:** ни одна запись журнала
|
||
`docs/review.md` не описывает класс, совпадающий с находками этого прогона.
|
||
Ближайшая — «ключ бюджета ограничителя выбирал тот, кого ограничивают»
|
||
(2026-08-23) — соседствует с гипотезой про `X-Forwarded-For`, но предмет у неё
|
||
другой.
|
||
|
||
### Сработавшие потолки
|
||
|
||
- **По проходам: ни один не сообщил свой потолок.** Ни `autotests` (1 находка),
|
||
ни `specs` (4), ни `code` (5), ни `architecture` (3 + 2 замечания), ни
|
||
`adversary` (3 + 3 свойства), ни `ops` (1) не сказали, каков был их предел и
|
||
что осталось за срезом. Это находка о прогоне, вынесенная в сводку отдельной
|
||
строкой; закрыть её отсюда нечем.
|
||
- **Потолок триажа сработал.** На входе 21 находка, в первые две секции влезло
|
||
7. **Не влезло и здесь названо поимённо:**
|
||
- `A1`/`S2` — `SubstitutedHeaderNames` не вызвана ни одним тестом
|
||
(`go tool cover -func` → 0,0 %). Триаж добыл ей однократный оракул: прогон
|
||
настоящего бинарника печатает `headers=[Remote-User]` — имена, не значения,
|
||
— то есть **дефекта сегодня нет**, есть отсутствие защиты от регрессии
|
||
против MUST NOT дельта-спеки («строка старта MUST не нести значений»).
|
||
Понижено до minor, инлайн-правка на один тест, если у оркестратора останется
|
||
бюджет;
|
||
- `C3` — умолчания `Debug` и `TestHeaders` заданы нулём типа, а не в
|
||
`defaultConfig()`. Нарушена записанная конвенция
|
||
`docs/conventions/config.md:181-191`: «Умолчания задаются в `defaultConfig()`…
|
||
Новое поле требует правки обоих мест», и там же форма обязательного поля —
|
||
«умолчания нет ни в `defaultConfig()`, ни по нулевому значению типа». Форма
|
||
записи необязательного поля стала неотличима от формы обязательного. `nit`,
|
||
инлайн;
|
||
- `C4` — `internal/config/test_headers_test.go` объявлен `package config` и
|
||
импортирует `internal/controller/http`, ребро запрещено той же правкой в
|
||
`docs/conventions/config.md`. `nit`; отражён оговоркой в Promote candidates,
|
||
потому что чинить его в одиночку смысла меньше, чем вместе со сканером;
|
||
- `C5` — `ValidateTestHeaders` возвращается на первом несовпадении вместо
|
||
`errors.Join`. Уехал в Promote candidates: правило шире этого кода;
|
||
- `C2` — комментарий `Dockerfile:49-50` объясняет исключение `cmd/devtools`
|
||
словами «оснастка разработчика (подставной прокси)», а подкоманды больше нет;
|
||
приёмочный шаг `tasks.md:88` искал следы по строке `devtools proxy` и эту не
|
||
поймал. Плюс `internal/controller/http/mounts.go:54-56` перечисляет три звена
|
||
цепочки, тогда как `AppChain` одевает четыре. `nit`, инлайн, две строки;
|
||
- **расхождение записи имени ключа**: `[auth] test_headers` в дельта-спеке,
|
||
`docs/security.md`, `docs/architecture.md` и `docs/review.md` против
|
||
`[auth.test_headers]` в `config.example.toml`, `README.md`, `CLAUDE.md`,
|
||
`docs/conventions/config.md` и в текстах отказов старта самого кода
|
||
(`test_headers.go:16`). Человек, ищущий в конфиге то, что назвал отказ
|
||
старта, найдёт вторую форму; первая в файле не встречается вовсе. **Не
|
||
находка этого прохода:** `CLAUDE.md`, «Гейт», относит согласованность
|
||
документов между собой и с кодом к скиллу `av-dev:doc-healthcheck`. Названо
|
||
здесь, чтобы не потерялось;
|
||
- **рецепт локального входа записан четырьмя копиями** — `config.example.toml`
|
||
(канон), `README.md:48-61`, `CLAUDE.md:276-288`,
|
||
`docs/conventions/config.md:66-75`. Замечание `architecture` «дешевле
|
||
переделать до мерджа»; по той же причине уходит в `doc-healthcheck`;
|
||
- границы прохода `specs`, перенесённые без изменений: значения
|
||
`Remote-Name`/`Remote-Email` в секции имитации на старте не судятся ничем
|
||
(годность проверяется только у логина); `strings.TrimSpace` над именем ключа
|
||
— поведение сверх спеки, последствий у него не найдено.
|
||
|
||
### Урожай враждебного прохода
|
||
|
||
Пути обхода **самой подстановки** — предмета этого изменения — не построено:
|
||
шесть отрицательных проб (два значения `Remote-User` при включённой подстановке
|
||
→ `401`; чужие `remote-email`/`remote-name` в нижнем регистре на живом сервере →
|
||
слой владеет тройкой целиком; сверка адреса идёт той же функцией, что у
|
||
узнавания; неразобранный `RemoteAddr` → отказ подстановки; канонизация ключей
|
||
против регистра, NBSP, ZWSP → старт падает; у `[server] debug` второго
|
||
потребителя нет). Состояния «подстановка работает, а владелец не знает» не
|
||
найдено. **Это положительный исход по теме, а не отсутствие работы.**
|
||
|
||
Построенное легло на соседнюю поверхность. Судьба каждой:
|
||
|
||
| Находка | Чинится этим мерджем? | Куда |
|
||
|---|---|---|
|
||
| V1 — почта берётся первым значением, сквоттинг молчит | **развилка**, вариант 1 берёт в мердж | находка 5 |
|
||
| `.env` не игнорируется git | **да**, одной строкой | находка 4 (инлайн) |
|
||
| `godotenv.Load()` мёртв (ничто не читает переменные окружения), а `WARN` на каждом старте приглашает завести `.env` | нет | **задачей**: снять зависимость `github.com/joho/godotenv` из `go.mod`, четыре строки из `cmd/transcriber/main.go:114-117` и строку `.env` из `.dockerignore` — либо, если переменные окружения нужны, завести им читателя. Оракул для задачи: `grep -rn "os.Getenv\|os.LookupEnv" --include=*.go internal/ cmd/ \| grep -v _test` → пусто; прогон бинарника печатает `level=WARN msg="Warning: .env file not found, using system environment variables"` |
|
||
| V2 — имя копии выводится из `record_id` инкрементом | нет | **задачей**, гипотеза выше; оракулы прохода названы поимённо |
|
||
| V3 — управляющий знак в расширении → `500` и `ERROR` | нет | **задачей**, гипотеза выше |
|
||
| Текст отказа провайдера с `sourceURI` в `error_text` | нет | **задачей**, оракула нет и на этом прогоне быть не может (запрет на реальный SpeechKit) |
|
||
| Требование к прокси про `X-Forwarded-For` не записано | нет | **задачей** в `docs/security.md`, рядом с уже записанным требованием про `Remote-*` |
|
||
|
||
Заведение задач — не работа триажа; формулировки выше отданы с оракулами, чтобы
|
||
ни одна не потерялась при переносе.
|
||
|
||
### Оракулы, добытые триажем
|
||
|
||
Все тесты писались во временные файлы пакетов и **удалены** после прогона;
|
||
рабочее дерево на момент сдачи отчёта содержит ровно те изменения, что были на
|
||
входе (`git status --short` сверен). Прогоны бинарника шли в каталог скретчпада,
|
||
не в `data/`; поднятый сервис остановлен за собой (`pkill`), порт освобождён —
|
||
правило `docs/review.md:16-23`.
|
||
|
||
1. `go test ./internal/config/ -run TestTriage -v` — три случая разбора TOML
|
||
(дубль по регистру, опечатка в имени секции, опечатка в имени ключа).
|
||
2. `go test ./internal/controller/http/ -run TestTriage -v` — сквозной сквоттинг
|
||
почты на настоящей поверхности `AppChain` и асимметрия правила многозначности.
|
||
3. `go build -o <скретчпад>/transcriber ./cmd/transcriber` + два прогона
|
||
настоящего бинарника на порту 18099 с каталогами данных в скретчпаде: конфиг
|
||
с опечаткой в имени секции и конфиг правильный.
|
||
4. `git check-ignore -v .env`; `grep -rn "\.Debug(" --include=*.go internal/ cmd/`;
|
||
`grep -rn "os.Getenv\|os.LookupEnv" --include=*.go internal/ cmd/`;
|
||
`grep -n "config" internal/archrules/arch_test.go`.
|
||
|
||
Гейт триаж не перезапускал: его исход взят у прохода `autotests` как замер,
|
||
снятый на этом прогоне.
|