textmachine/platform/docs/ENGINEERING_STANDARDS.md

102 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Стандарты разработки и критерии приёмки платформы
> Ратифицировано оркестратором №14 по решению владельца 04.08: платформа считает токены пользователей
> и держит их логины — «ошибки недопустимы»; качество и отказоустойчивость выше скорости фич.
> Каждый промт платформенной сессии обязан ссылаться на этот документ; отступление = пинг, не тихая девиация.
## 1. Рамка
- **Несущие пути** — auth/сессии/CSRF, всё денежное (usage, лимиты, биллинг), ингест/супервизия движка,
миграции. Изменение несущего пути = «тяжёлый» класс: полный цикл приёмки §3, без исключений.
- **Сторонние тулы прежде самописного** (решение владельца 04.08): проверенные библиотеки предпочитаются
своему коду везде, КРОМЕ похода в ядро переводов (tmctl зовётся процессом, обвязка своя) и мест, где
stdlib и есть индустриальный стандарт (net/http, database/sql-слой через pgx). Новая зависимость —
строка в `STACK_DECISIONS.md` с датой релиза и «зачем», ратифицирует оркестратор.
- Канон-инварианты зоны без изменений: D39.84 (никаких денежных сумм на проводе/UI/INFO-логах) ·
D39.85 (шов: NDJSON → апсерт `(engine_run_id, seq)` → SSE из Postgres; живой SQLite движка не читать) ·
ПТ-34 (noindex/no-store на всём) · языко-агностичность.
## 2. Базовые стандарты (проверяются каждой приёмкой)
**Безопасность** — базовая линия OWASP **ASVS 5.0.0, целевой L2** (главы, релевантные зоне: V6
аутентификация · V7 сессии · V10 OAuth/OIDC) + OWASP API Security Top 10 2023 (API1 BOLA = проверка
владения книгой на каждом `/v0`-маршруте; API4 = класс PD-2). Что берём и чего не берём из
инструментов — `PLATFORM_DIRECTION.md` §3. Конкретика:
- Сессии: одна серверная, opaque-токен ≥256 бит из crypto/rand, в БД только хеш, немедленный отзыв,
два срока (idle скользит, absolute нет). Плейнтекст-токен не персистится, не логируется и живёт
в памяти только окно обработки запроса (решение владельца 04.08).
- Секреты — только из окружения/секрет-менеджера; в репо и в логи не попадают НИ на каком уровне.
- Ввод: только параметризованные запросы; каждый body-принимающий хендлер несёт `http.MaxBytesReader`
и дедлайн (гейт PD-2 регистра); закрытые словари валидируются на входе И констрейнтами БД.
- Ответы: problem+json; внутренности и оракулы (unknown-vs-expired токен) на провод не текут;
заголовки ПТ-34+nosniff+no-referrer — мидлварью на всё, не дисциплиной хендлера.
- `govulncheck` (`make vuln`) — перед каждым лендингом; красный = блокер лендинга.
**Отказоустойчивость:**
- Ингест идемпотентен (`(engine_run_id, seq)`, high-water mark в одной транзакции с эффектом);
at-least-once — норма, дубль — не ошибка.
- Graceful shutdown с реальным дренажем; пул с лимитами; транзиентные ошибки БД — ретраи с джиттером;
миграции идемпотентны, down-путь существует и гоняется тестом.
- Отказ зависимости — деградация с диагностикой, не тишина: проглоченная ошибка без лога и без
комментария-обоснования = дефект (класс PD-5/PD-16).
**Наблюдаемость** — базовая линия **практики именования Prometheus** (базовые единицы: секунды и
байты; `_total` у счётчиков; единица в имени, не в лейбле) + **четыре золотых сигнала** SRE на
вопрос «что мерить» (внесено P5 по ратифицированному направлению PD-115: у оси появился внешний
эталон, а не только собственная проза зоны). Конкретика:
- структурные логи (route-pattern, не сырой путь — id пользователя/книги в логи не текут),
`X-Request-Id` на каждом ответе, ошибки видимы на своём уровне;
- метрики отдаются в формате Prometheus на ОТДЕЛЬНОМ слушателе (`STACK_DECISIONS` §24); лейбл несёт
паттерн маршрута, никогда путь — иначе это и неограниченная кардинальность, и библиотека
пользователя в индексе оператора;
- Money-числа — только в приватных таблицах, ни в логах, ни в ответах, ни в метриках (кроме
P-5-статуса процентами);
- ⚠ оси **ops** (релиз/откат, ёмкость, восстановление) и **конфигурация** внешнего эталона
по-прежнему не имеют — PD-115 открыт ими.
**Контракт-первичность:** поверхность = `docs/architecture/14-api-contract/openapi.yaml`; расхождение
кода со спекой = дефект чей-то один: либо спека правится через ратификацию, либо код. Генерация
типов/стабов из спеки (oapi-codegen — кандидат, ратификация при P1) предпочтительнее ручного дрифта.
## 3. Критерии приёмки сессии (Definition of Done)
1. `make check` зелёный офлайн; скипы названы вслух. ⚠ **«Скипов ноль» требует ТРЁХ условий, а не
одного** (испр. 22.08 — прежняя редакция называла только DSN и позволяла объявить приёмку
выполненной с молча пропущенной третью батареи): `TM_PLATFORM_TEST_DSN` (схема) ·
`TM_PLATFORM_TEST_ENGINE_BIN` + `TM_PLATFORM_TEST_BOOK_TEMPLATE` (живой рендер конфигурации) ·
ДОСТИЖИМЫЙ пользовательский менеджер systemd (`internal/runner`, `systemdOrSkip` — на хосте без
logind-сессии три теста скипаются и «скипов 0» недостижимо в принципе, `PD-374`). Условие, которое
на этом хосте не выполнено, сессия НАЗЫВАЕТ вместе с числом скипов — «зелёная батарея» без него не
значит ничего. Порядок подъёма PG без root — `STACK_DECISIONS.md`.
2. Каждый деливерабл проверен ИСПОЛНЕНИЕМ (сервер поднят и опрошен, миграции применены дважды,
констрейнты сработали поимённо, супервизор гонял настоящий процесс) — не чтением.
3. **Мутации сажаются в КОПИИ дерева, и копия несёт КАНОН.** Норма изоляции — `D39.113`: правка в
рабочем дереве теряет незакоммиченную работу параллельных сессий, поэтому посадка идёт в копию, и
каждому агенту своя, вместе со своим стендом (база, порты, каталоги). ⚠ **Копировать надо ВМЕСТЕ с
каноном контракта**, иначе копия структурно красная и вердикты лгут:
`cp -a --parents platform docs/architecture/14-api-contract <куда>/`. Голая `cp -a platform` даёт
постоянный `FAIL` у `gates.TestTheAnnouncedContractVersionIsTheOneTheCanonRatified` — гейт читает
канон по пути ВЫШЕ корня модуля (`internal/gates/contract_test.go:14`), — и цена не в потерянном
времени: базовый красный того же теста МАСКИРУЕТ дельту, поэтому мутация самой `ContractVersion`
выходит «выжившей» и даёт ЛОЖНУЮ находку «константа не запинена». С каноном копия зелёная.
Вторая половина того же правила: **вердикт посадки судится по ДЕЛЬТЕ против чистой базовой линии
ТОЙ ЖЕ копии и по ТОПИЧНОСТИ упавшего теста, а не по цвету батареи** — иначе известный флейк даёт
ложное «пойман» и тихо теряет находку о недостающем пине. Записано сюда паком `P8-REVIEW` 24.08
(`PD-395`) именно потому, что прежде рецепт жил ТОЛЬКО в промте ревью-пака, а промты после
лендинга уезжают в `archive/` — и ловушка взводилась заново для каждой следующей сессии.
4. Заявленное в отчёте свойство несущего пути ОБЯЗАНО быть запинено тестом, который ловит мутацию
этого свойства; сессия сама называет в отчёте, какой тест что пинит. Приёмка оркестратора сажает
СВОИ мутации (урок P0: «в БД только хеш» было истинно, но не запинено — посадка выжила).
5. Адверсариальная самопроверка (author≠reviewer) до сдачи; найденное — в отчёт, не молча.
6. Новые находки — строками в `DEFECT_REGISTER.md` тем же деревом; закрытие дефекта — коммит + пинящий тест.
7. Дерево не коммитится сессией — лендит оркестратор после адверсариальной приёмки
(для несущих путей — с воркфлоу-верификаторами и посадками).
8. **Построил механизм — грепни ОТКРЫТЫЕ строки регистра по своим файлам.** Каждое совпадение
получает диспозицию в отчёте: закрыть, сузить, пере-формулировать или оставить с причиной.
Класс «строка `open`, а лекарство уже в дереве» стоил зоне порядка 10% открытых строк и
отправлял следующий пак чинить построенное. Греп — по ПОЛНЫМ путям, не по именам файлов:
замер приёмки 27.08 дал по именам 22 совпадения, почти все ложные (`main.go`, `runner.go`,
`config.go` есть и в `platform/`, и в `backend/`), по полным путям — 2, и оба настоящие.
Ратифицировано `D39.159` п. 7. ⚠ Правило симметрично: якорь строки, УБИТЫЙ твоим переездом,
чинит тот, чей переезд его убил, и называет причину — не следующий читающий пак.