From a96b902df055e78c32960d2d392e4bbe1db9a75f Mon Sep 17 00:00:00 2001 From: Mikhail Yenuchenko Date: Mon, 3 Aug 2026 16:18:32 +0300 Subject: [PATCH] docs: documentation plan with a code cross-check pass The documentation is part of the deliverable (spec 11.5/11.7/11.9), so it has to describe what the code does, not what was intended. Adds docs/documentation-plan.md: the package inventory against the spec, the per-claim sources of truth in the tree, the results of a first cross-check pass (11 findings, most notably the missing "operations" section required by spec 11.7, the absent env-var reference, .env.example's dangling link to a README "Rate limiting" section, and the unwritten "tar while stopped" backup path from spec 9), and tasks D1-D7 gating the next release tag. progress.md points at it so it survives a context reset. Co-Authored-By: Claude Opus 5 --- docs/documentation-plan.md | 248 +++++++++++++++++++++++++++++++++++++ docs/progress.md | 1 + 2 files changed, 249 insertions(+) create mode 100644 docs/documentation-plan.md diff --git a/docs/documentation-plan.md b/docs/documentation-plan.md new file mode 100644 index 0000000..8c5a858 --- /dev/null +++ b/docs/documentation-plan.md @@ -0,0 +1,248 @@ +# План документации SelfPost + +**Зачем этот файл.** Документация — часть поставки (ТЗ раздел 11, пп. 5, 7, 9), +а не сопроводительный текст. Перед тегом релиза она обязана описывать **то, что +делает код**, а не то, что задумывалось: расхождение здесь — такой же дефект, +как несоответствие ТЗ, только обнаруживает его пользователь на своём проде. +План описывает, из чего состоит пакет, как он сверяется с кодом, что уже +разошлось (первый проход выполнен, результаты ниже) и что с этим делать. + +**Место в общем плане:** документационный проход идёт вместе с **D.5** +([implementation-plan.md](implementation-plan.md)) — до тега следующего релиза, +после B.1–B.3 и C.4, потому что именно они изменили поведение, которое README +описывает (сессии, ротация лога, обязательность `SELFPOST_HOSTNAME`). + +**Модель:** документация → Sonnet (правило [progress.md](progress.md)). +Исключение — **D6** (HEALTHCHECK/эндпоинт мониторинга) и формулировки про +`TRUSTED_PROXY_CIDR`: инфра/безопасность → Opus. + +--- + +## 1. Состав пакета + +### Поставляемое пользователю (обязательно по ТЗ) + +| Артефакт | Требование | Состояние | +|---|---|---| +| [README.md](../README.md) | ТЗ 11 п. 7: установка, требования к площадке, per-domain DNS, прогрев IP, **эксплуатация**, бэкап/восстановление и экспорт/импорт домена (оба файла — секреты), репозиторий (Codeberg основной / GitHub зеркало), откуда образ (`ghcr.io`), фиксированный тег, минимальные требования к машине, лицензия в 2–3 предложениях | Есть всё, **кроме раздела «эксплуатация»**; см. находки 1–4, 10 | +| [LICENSE](../LICENSE) | ТЗ 11 п. 9: полный текст AGPL-3.0 | Полный текст на месте, правок не требует | +| [deploy/docker-compose.yml](../deploy/docker-compose.yml) + фрагменты прокси | ТЗ 11 п. 5, ТЗ 10 п. 3: Apache основной, nginx/Caddy/Traefik альтернативами; для каждого — что монтируется и в какие пути | Все четыре есть ([apache](../deploy/apache/), [nginx](../deploy/nginx/), [caddy](../deploy/caddy/), [traefik](../deploy/traefik/)), в README сведены таблицей; см. находки 7, 8 | +| [deploy/.env.example](../deploy/.env.example) | Пользовательские переменные с пояснениями | Есть 6 переменных; полного справочника нет — находка 3 | +| [CHANGELOG.md](../CHANGELOG.md) | Keep a Changelog, запись на каждом осмысленном шаге | Ведётся; `[Unreleased]` наполнен B.1–B.3, C.4 | + +### Рабочие документы проекта (не поставка, но обязаны быть верны) + +[specification.md](specification.md) — ТЗ v1.0, источник истины по требованиям. +[implementation-plan.md](implementation-plan.md) — открытые вопросы и линия 2.x. +[progress.md](progress.md) — живой трекер, читается первым после `/clear`. +[security.md](security.md) — принятые риски. Этот файл — план по документации. + +**Границы:** отдельные `CONTRIBUTING.md`, `docs/architecture.md`, man-страницы и +сайт документации ТЗ **не требует** и в объём v1.x не входят. Dev-петля (сборка +и тесты на dev-сервере) остаётся в `progress.md` и в память проекта — выносить +её в поставляемую документацию не нужно. + +--- + +## 2. Метод сверки с кодом + +Правило: у каждого утверждения в документации есть **ровно один источник истины +в дереве**, и сверка идёт от кода к тексту (что код делает → сказано ли об +этом), а не наоборот — иначе не видно того, что забыли описать. + +| Класс утверждений | Источник истины | +|---|---| +| Переменные окружения, значения по умолчанию | `loadConfig`/`envDefault`/`envInt` — [cmd/panel/main.go:80](../cmd/panel/main.go:80)–159; `${VAR:-default}` в [build/](../build/) (`postfix-config.sh`, `postfix-wrapper.sh`, `postfix-cert-reload.sh`, `logrotate-loop.sh`, `entrypoint.sh`) | +| Поведение почтового тракта (порты, TLS, SASL, анти-relay, лимиты, milter-цепочка) | [build/postfix-config.sh](../build/postfix-config.sh) | +| Экраны и действия панели (что вообще можно делать в эксплуатации) | таблица маршрутов [internal/web/web.go:133](../internal/web/web.go:133)–190 | +| Бэкап/восстановление, экспорт/импорт домена, проверка версии | [internal/backup/backup.go](../internal/backup/backup.go), [cmd/selfpost-backup/main.go](../cmd/selfpost-backup/main.go) | +| Сессии, вход, смена пароля | [internal/store/sessions.go](../internal/store/sessions.go), [internal/web/session.go](../internal/web/session.go), [internal/web/handlers_account.go](../internal/web/handlers_account.go) | +| Ротация лога, периодический reload, интервалы | [build/logrotate-mail.conf](../build/logrotate-mail.conf), [build/logrotate-loop.sh](../build/logrotate-loop.sh), [build/postfix-cert-reload.sh](../build/postfix-cert-reload.sh) | +| Деплой: тег образа, порты, монтирования, capabilities | [deploy/docker-compose.yml](../deploy/docker-compose.yml), [build/Dockerfile](../build/Dockerfile) | +| Требования, а не реализация (что вообще должно быть описано) | [specification.md](specification.md) разделы 9, 10, 11 | + +Порядок прохода: сначала перечислить фактическое (env-ключи, маршруты, +`postconf`-настройки), потом искать каждый пункт в README — так находится и +неверное, и **отсутствующее**. + +--- + +## 3. Результаты первого прохода (выполнен) + +Сверено: env-переменные, маршруты панели, конфигурация Postfix, бэкап/CLI, +сессии, ротация, compose/Dockerfile. Ниже — всё найденное, по убыванию +приоритета. Нумерация сквозная, на неё ссылаются задачи в разделе 4. + +**Высокий (пробел против ТЗ или вводит в заблуждение)** + +1. **Нет раздела «эксплуатация»** — прямое требование ТЗ 11 п. 7 + ([specification.md:427](specification.md)). В README нет ни слова про + `/status` (проверки PTR/hostname), `/sendlog` (журнал отправки, фильтры), + `/queue` (очередь Postfix), `/logtail` (хвост `mail.log`), `/reload`, + `/account`, `/backup` — при том что всё это реализовано + ([internal/web/web.go:152](../internal/web/web.go:152)–188). Не описана и + процедура апгрейда (бамп тега → `docker compose up -d`), хотя раздел + «Fixed image tag» на неё намекает. +2. **Битая ссылка + недокументированные лимиты.** + [deploy/.env.example:13](../deploy/.env.example:13) отсылает к разделу README + «Rate limiting», которого в README нет. Сам двухуровневый лимит не описан + нигде для пользователя: L1 — `smtpd_client_message_rate_limit` + + `anvil_rate_time_unit` ([build/postfix-config.sh:110](../build/postfix-config.sh:110)), + L2 — per-domain/per-app из панели + ([internal/web/web.go:165](../internal/web/web.go:165), + [:169](../internal/web/web.go:169)), с записью `rejected` в журнал. +3. **Нет справочника переменных окружения.** В `.env.example` шесть штук; код + читает заметно больше. Панель: `SELFPOST_DATA_DIR`, `SELFPOST_DB_PATH`, + `SELFPOST_SETUP_TOKEN_FILE`, `PANEL_HTTP_ADDR`, `JOURNAL_MILTER_SOCKET`, + `MAIL_LOG`, `PANEL_COOKIE_SECURE`, `TLS_CERT_FILE`, `OPENDKIM_SOCKET`, + `OPENDKIM_DIR`, `DKIM_SELECTOR_DEFAULT`, `SASL_DB_PATH`, `SASL_REALM`, + `POSTFIX_DIR` ([cmd/panel/main.go:80](../cmd/panel/main.go:80)–127). + Обвязка: `TLS_KEY_FILE`, `POSTFIX_SENDER_LOGIN_MAPS`, + `MILTER_CONNECT_TIMEOUT`/`COMMAND`/`CONTENT` (15s/15s/30s, + [build/postfix-config.sh:133](../build/postfix-config.sh:133)), + `MILTER_WAIT_TIMEOUT`, `TLS_RELOAD_INTERVAL_SECONDS` (86400, + [build/postfix-cert-reload.sh:14](../build/postfix-cert-reload.sh:14)), + `LOGROTATE_INTERVAL_SECONDS` (21600, + [build/logrotate-loop.sh:17](../build/logrotate-loop.sh:17)). + Отдельно: **`TRUSTED_PROXY_CIDR`** — переменная с последствиями для + безопасности (доверие `X-Forwarded-For` при rate-limit логина), описана + только в `.env.example`, в README её нет вообще. + *Решение, которое надо принять в задаче D2:* какие переменные публичные + (таблица в README), а какие внутренние (достаточно комментария в коде) — + документировать все 20+ вредно, они станут «поддерживаемым интерфейсом». +4. **Оговорка ТЗ 9 про прямой `tar` не отражена.** + [specification.md:375](specification.md) требует описать оба пути: «бэкап на + лету — панель/CLI» и «прямой `tar` директории безопасен, если контейнер + остановлен». В README ([Backup, restore…](../README.md)) есть только первый, + поэтому у читателя нет ответа на очевидный вопрос «а можно просто + заархивировать `./data`?». + +**Средний (стало неверным после последних фаз)** + +5. **Статус-баннер устарел.** [README.md:12](../README.md:12)–15: «under active + development» и ссылка на `implementation-plan.md` как на «phased build plan» + — фазы 0→14 закрыты, план теперь про открытые вопросы и линию 2.x. Перед + тегом релиза баннер переформулировать (или снять), ссылки уточнить. +6. **`implementation-plan.md` разошёлся с кодом.** Пункт B.1 говорит: «смена + пароля завершает **все** сессии, включая ту, из которой её делают → редирект + на `/login`». Код делает иначе — гасит все, **кроме** текущей + ([internal/store/sessions.go:95](../internal/store/sessions.go:95) + `DeleteOtherSessions`, [internal/web/session.go:131](../internal/web/session.go:131)), + и панель так и пишет («Any other signed-in sessions were signed out», + [internal/web/handlers_account.go:49](../internal/web/handlers_account.go:49)). + README здесь **верен**. Расхождение в плане — зафиксировать как осознанное + изменение при реализации, а не молча переписать. +7. **Комментарий в compose вводит в заблуждение.** + [deploy/docker-compose.yml:14](../deploy/docker-compose.yml:14) велит + «fill in .env (hostname, at least one strong TLS_CERT/KEY path)», но + `TLS_CERT_FILE`/`TLS_KEY_FILE` захардкожены в самом файле + ([:30](../deploy/docker-compose.yml:30)–31) и в `.env.example` отсутствуют — + настраивается на деле **bind mount** `./certs`, а не переменные. +8. **Порт 587 публикуется всегда** ([deploy/docker-compose.yml:61](../deploy/docker-compose.yml:61)), + хотя listener появляется только при `SUBMISSION_ENABLE=true` + ([build/postfix-config.sh:151](../build/postfix-config.sh:151)). Безвредно + (слушать некому), но выглядит как лишний открытый порт — одна строка + пояснения в README/compose снимает вопрос. + +**Низкий / требует решения, а не только текста** + +9. **`/healthz` есть, `HEALTHCHECK` нет.** Эндпоинт реализован + ([internal/web/web.go:133](../internal/web/web.go:133)), в + [build/Dockerfile](../build/Dockerfile) объявления `HEALTHCHECK` нет (пробел + отмечен и в плане, п. B.3). Решить в D6: либо задокументировать `/healthz` + как точку внешнего мониторинга, либо добавить `HEALTHCHECK` в образ (это уже + код + строка в CHANGELOG). Документировать «здоровье контейнера» до принятия + решения нельзя — получится обещание, которого образ не даёт. +10. **Из B.1–B.3/C.4 в README попала только обязательность `SELFPOST_HOSTNAME`.** + Не описаны: сессия переживает рестарт и живёт по скользящему сроку + бездействия (`PANEL_SESSION_IDLE_DAYS`, «вкладка с автообновлением вход не + продлевает» — контринтуитивно и заслуживает строки), ротация `mail.log` + (14 файлов, проверка каждые 6 ч, суточный `postfix reload`) — README + упоминает только «kept 14 days in-image» в требованиях к машине. +11. **Мелочи, без правки кода:** Quick start тянет файлы с + `raw.githubusercontent.com` (зеркало), хотя основной репозиторий — + Codeberg; тег образа `1.0.0` в compose ([:24](../deploy/docker-compose.yml:24)) + придётся бампить в тот же коммит, что и релиз; пустой каталог `docs/logo`. + +**Проверено и расхождений не найдено** (важно не переделывать): требования к +площадке; DNS-раздел (совпадает с тем, что реально проверяет `/status` и +DNS-карточка домена); прогрев IP; смысл фиксированного тега; проверка версии при +восстановлении — README описывает её точно так, как ведёт себя `CheckRestore` +([internal/backup/backup.go](../internal/backup/backup.go)); форма вызова +`docker exec … selfpost-backup > …` ([cmd/selfpost-backup/main.go:7](../cmd/selfpost-backup/main.go:7)); +таблица reverse-proxy и требование пропускать `Host`; секретность архива и +экспорта домена; лицензия; ретенция журнала отправки (90 дней) и то, что она — +основной драйвер роста `/data`. + +--- + +## 4. Задачи + +Порядок — как перечислены; D1–D3 самые крупные. Каждая заканчивается записью в +`[Unreleased]` [CHANGELOG.md](../CHANGELOG.md) и коммитом. + +**D1. README: раздел «Operations» + «Rate limiting».** Закрывает находки 1, 2 и +часть 10. Что описать: экраны панели и зачем они нужны (`/status` и что именно +он проверяет; журнал отправки со статусами `queued/sent/rejected`; очередь; +хвост `mail.log`; `/reload`; смена учётных данных); двухуровневый лимит с +именами переменных L1 и указанием, что L2 задаётся в панели per-domain/app; +процедура апгрейда версии; поведение сессии (скользящий срок, опросы не +продлевают, смена пароля гасит остальные). Тон и объём — как в существующих +разделах: практика, а не пересказ ТЗ. +*Готово, когда:* каждый маршрут из [internal/web/web.go:152](../internal/web/web.go:152)–188, +кроме HTMX-фрагментов, либо описан, либо сознательно опущен; ссылка +«Rate limiting» из `.env.example` ведёт в существующий якорь. + +**D2. Справочник переменных окружения.** Закрывает находку 3. Сначала решение о +границе публичного набора, затем таблица (имя, назначение, дефолт, где +задаётся) в README + синхронизация `.env.example` и комментариев в compose. +`TRUSTED_PROXY_CIDR` описать с прямым предупреждением: неверное значение +позволяет подделать ключ rate-limit'а логина. +*Готово, когда:* каждый ключ из `loadConfig` и из `${VAR:-…}` в `build/*.sh` +попал либо в таблицу, либо в явный список «внутренние, не интерфейс»; дефолты в +таблице совпадают с кодом дословно. + +**D3. Бэкап: дописать путь «остановленный контейнер + `tar`».** Закрывает +находку 4 — по формулировке [specification.md:375](specification.md), с явным +«на живом контейнере так делать не надо, потому что SQLite в WAL». Заодно +упомянуть, что `manifest.json` после успешного восстановления потребляется. + +**D4. Точечные правки.** Находки 5, 7, 8, 11: баннер статуса и ссылки; комментарий +в шапке compose про `.env`; строка про 587; бамп тега образа (делается в +релизном коммите, не раньше). + +**D5. Синхронизация внутренних документов.** Находка 6 — поправить B.1 в +`implementation-plan.md` (с пометкой, что решение изменилось при реализации и +почему), заодно перечитать `progress.md` на предмет утверждений, которые код уже +опроверг. Не переписывать историю в CHANGELOG. + +**D6. Решение по `HEALTHCHECK`/`/healthz` (Opus).** Находка 9: либо строка в +`Dockerfile` + описание, либо только описание эндпоинта. Влияет на код образа — +поэтому отдельной задачей и не на Sonnet. + +**D7. Регресс-защита (см. раздел 5).** + +--- + +## 5. Чтобы не разошлось снова + +1. **Правило шага:** изменение, добавляющее/переименовывающее env-переменную, + маршрут панели или наблюдаемое поведение почтового тракта, закрывается + только вместе с правкой README/`.env.example` — в том же коммите, наравне с + записью в CHANGELOG (протокол закрытия шага в [progress.md](progress.md)). +2. **Дешёвая машинная проверка (D7):** тест или скрипт, сверяющий множество + ключей `loadConfig` со списком, объявленным в документации, и падающий на + новом недокументированном ключе. Ловит самый частый класс расхождений + (находка 3) без ручного прохода. Границу «внутренних» ключей задать явным + списком-исключением в самом тесте. +3. **Перед каждым тегом** — короткий проход по разделу 2 этого файла (семь + источников истины), а не полная ревизия текста. + +--- + +## 6. Гейт релиза + +Документационный проход — часть того же гейта, что e2e (C.4) и ревизия +безопасности (D.5): **D1–D6 закрыты до тега**. D7 — желателен, но тег не +блокирует. Находки, обнаруженные позже, дописываются сюда, а не исправляются +молча: этот файл — журнал состояния документации, а не одноразовый список дел. diff --git a/docs/progress.md b/docs/progress.md index 3d0b838..9bb201a 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -42,6 +42,7 @@ - **B.2 реализован** (не выкачен на прод): ротация `mail.log` ушла с `copytruncate` на «переименовать + `postfix reload`» — `build/logrotate-mail.conf` (`nocreate` заменён на `create 0644 root root` **не по плану, а по стендовой проверке**: после reload Postfix пересоздаёт лог сам только в момент следующей фактической записи и с режимом `0600`, недоступным непривилегированной панели, — `create` в logrotate закрывает это, отдавая файл ей же на 644 сразу после переименования); `follow()` в `internal/logtail/logtail.go` при обнаружении смены inode дочитывает старый дескриптор ещё раз перед переключением; `readLogTail()` в `internal/web/handlers_monitor.go` считает отсутствующий файл пустым экраном, а не ошибкой. Проверено на стенде (`selfpost.mixfed.ru`, отдельный контейнер `selfpost:b2test2`): цикл трафик → принудительная ротация → файл пуст и сразу читаем непривилегированным uid панели (0 читает `mail.log` сразу после rename, без окна недоступности) → новый трафик после ротации уходит в новый файл на 644, ничего не потеряно по обе стороны rename. `go vet`/`go test ./...`/`gofmt -l .` чистые (на dev-сервере; локально на Windows `TestFollowTailsAndRotates` падает — rename открытого файла запрещён ОС, к делу не относится). - **B.3 реализован** (не выкачен на прод): `build/entrypoint.sh` проверяет `SELFPOST_HOSTNAME` до `postfix-config.sh` и до `supervisord` — при пустом значении `exit 1` с развёрнутым текстом ошибки (что это за имя, почему обязательно, пример, где задаётся); плюс синтаксическая проверка через `case`: минимум одна точка, без схемы/порта/пробелов (`*://*`, `*:*`, пробел/таб — тот же класс тихого спам-отказа, что и пустое значение). `saslRealm()` и fallback в `postfix-config.sh` не тронуты — после гейта эти ветки мертвы. Заодно отмечена обязательность переменной в `README.md` и `deploy/.env.example`. Проверено на стенде (`selfpost.mixfed.ru`, отдельный образ `selfpost:b3test`, cap-list как в поставляемом compose): без переменной — `exit 1` с ожидаемым текстом, без бесконечного тихого retry; `https://mail.example.com:465` и `localhost` отклонены с понятными сообщениями; валидный `mail.example.com` — обычный старт, все процессы supervisord поднимаются. `go vet`/`go test ./...` чистые. - **C.4 реализован** (не выкачен на прод — это CI/тестовая инфраструктура, а не образ): герметичный контейнерный e2e отдельным Go-модулем `test/e2e/` (свой `go.mod`, не подхватывается `go test ./...` основного модуля) поверх поставляемого `deploy/docker-compose.yml` плюс `test/e2e/compose.override.yml` (самоподписанный сертификат, `PANEL_COOKIE_SECURE=false`, `SELFPOST_HOSTNAME=mail.e2e.test`, высокие порты `20465/20587/20080`, изолированный compose-проект `selfpost-e2e`, свой `--project-directory` — прод на том же хосте не задет). Герметичная почта: CoreDNS (`test/e2e/dns/Corefile` — авторитетна только для `e2e.test`, `file`-плагин с саб-директивой `reload` перечитывает `db.zone` по mtime, без сигналов) плюс `smtp-sink` из пакета postfix (`test/e2e/sink/`) как sink-MX. Сценарий (`test/e2e/*_test.go`): старт контейнера → все supervisord-программы `RUNNING` (`postfix-reload` — `STOPPED`) → токен из `/data/setup-token` → setup → login → добавление домена → DKIM-запись **скраплена со страницы панели** и опубликована в фейковую зону → добавление приложения → SMTP AUTH на 465 → письмо на sink → DKIM-подпись проверена (`go-msgauth/dkim` с кастомным `LookupTXT` через CoreDNS) против ключа **из DNS**, не из панели напрямую → send-log `queued → sent`. Негативы: без AUTH, relay на чужой домен без AUTH, sender/login mismatch (`reject_sender_login_mismatch` репортится Postfix'ом на RCPT, не MAIL — `smtpd_delay_reject=yes` по умолчанию), L1-лимит (anvil, override `RATE_LIMIT_MESSAGES_PER_IP=50` — специально высокий, чтобы остальные под-тесты не расходовали общий бюджет по IP раньше времени; сам тест шлёт до 60 раз, ждёт отказа), L2-лимит через панель (домен/приложение → `rejected`-строка в send-log), fail-open journal-milter'а (`supervisorctl stop panel`, письмо всё равно принято, контейнер жив), пустой/синтаксически неверный `SELFPOST_HOSTNAME` (отдельный один-разовый контейнер, не общий стенд), сессия переживает `docker restart` (плюс явное ожидание готовности smtps-порта после рестарта — панель и Postfix поднимаются независимо). `make e2e` — локальный/dev-server прогон. Найдено и исправлено по ходу стендовой проверки: `reload` — саб-директива `file`-плагина CoreDNS, а не отдельный топ-левел плагин (топ-левел `reload` следит за самим Corefile, не за зоной); `docker compose build.context` резолвится относительно `--project-directory`, а не относительно файла, где объявлен; `smtp-sink` отказывается стартовать от root без `-u`; `html/template` эскейпит `+` в `+` даже в тексте — скрапер значений со страницы обязан `html.UnescapeString`; проверки состояния сразу после `up`/`restart` должны поллиться, а не разово опрашиваться (supervisord/postfix поднимаются не мгновенно). **Проверено на dev-сервере (`selfpost.mixfed.ru`)**: `make e2e` — зелёный (`go vet`/`gofmt -l` тоже чистые в обоих модулях). `release.yml` переработан: job `prepare` (версия из тега) → матрица `[ubuntu-latest, ubuntu-24.04-arm]` — каждая нативно собирает образ (`--load`), прогоняет e2e, пушит тег `X.Y.Z-amd64`/`X.Y.Z-arm64` → job `merge` — `docker buildx imagetools create` в единый тег `X.Y.Z`; `setup-qemu-action` убран. Не проверено вживую (нельзя без реального тега): сам workflow на GitHub Actions — синтаксис вычитан, логика идентична локальному `make e2e` пути. +- **Дальше — документация:** [documentation-plan.md](documentation-plan.md) — состав пакета (ТЗ 11), метод сверки с кодом, результаты первого прохода (11 находок: нет раздела «эксплуатация» по ТЗ 11 п.7, нет справочника env-переменных, битая ссылка на «Rate limiting» из `.env.example`, не отражена оговорка ТЗ 9 про прямой `tar`) и задачи D1–D7. Часть предрелизного гейта наравне с e2e и ревизией безопасности. - **Дальше:** пункт **D.5** плана — предрелизная проверка на уязвимости моделью Fable по всему дифу от `v1.0.0` плюс повторный проход по ТЗ 7.6; вместе с e2e (C.4, готов) это гейт перед тегом релиза. - **Дальше — то, что перечислено в `implementation-plan.md`:** открытые вопросы закрыты, раздел E теперь только указатель на объём 2.x (входящий релей O1+ и роль администратора домена; 2FA снята с рассмотрения); остаются принятые риски безопасности (переехали в [security.md](security.md): `POST` без `Sec-Fetch-Site`/`Origin` пропускается, CSRF-токенов нет) и опциональная **Фаза O1+** (входящий релей, линия 2.x.x, требует согласования). - **Прод:** `selfpost.mixfed.ru`, реальный Let's Encrypt сертификат, живой e2e (DKIM/SPF pass). Контейнер там всё ещё на образе v1.0 — Фаза 14 в него не выкатывалась. При апгрейде: админа один раз разлогинит (сменилось имя cookie), а от reverse-proxy требуется передача исходного `Host` (Apache-фрагмент из `deploy/` это делает).