Files
selfpost/docs/documentation-plan.md
T
mix a63e5c9b23 docs: plan specification retirement via D9 migration map
After the documentation pass, specification.md moves to archive once its content lives in product, architecture, development, and security docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 00:24:13 +03:00

29 KiB
Raw Blame History

План документации SelfPost

Зачем этот файл. Документация — часть поставки (deliverables v1.0, пп. 5, 7, 9 в specification.md), а не сопроводительный текст. Перед тегом релиза она обязана описывать то, что делает код, а не то, что задумывалось: расхождение здесь — такой же дефект, как несоответствие требованиям, только обнаруживает его пользователь на своём проде. План описывает, из чего состоит пакет, как он сверяется с кодом, что уже разошлось (первый проход выполнен, результаты ниже) и что с этим делать.

Цель после выполнения плана: specification.md (ТЗ v1.0) выводится из обращения — не потому что требования исчезли, а потому что v1.0 реализован и каждый блок ТЗ получает постоянный дом: пользовательское — в README, устройство — в architecture.md, продуктовые границы — в product.md, обязательная безопасность — в security.md, процесс разработки — в development.md. Живой specification.md после этого держит два риска: дублирование с кодом и ложное ощущение «источника истины», который уже не сверяют. Задача D9 — миграция остатков и снятие файла; до её закрытия ссылки на ТЗ в этом файле — исторические.

Место в общем плане: документационный проход идёт вместе с D.5 (implementation-plan.md) — до тега следующего релиза, после B.1–B.3 и C.4, потому что именно они изменили поведение, которое README описывает (сессии, ротация лога, обязательность SELFPOST_HOSTNAME).

Модель: документация → Sonnet (правило progress.md). Исключение — D6 (HEALTHCHECK/эндпоинт мониторинга) и формулировки про TRUSTED_PROXY_CIDR: инфра/безопасность → Opus.


1. Состав пакета

Поставляемое пользователю (обязательно по ТЗ)

Артефакт Требование Состояние
README.md ТЗ 11 п. 7: установка, требования к площадке, per-domain DNS, прогрев IP, эксплуатация, бэкап/восстановление и экспорт/импорт домена (оба файла — секреты), репозиторий (Codeberg основной / GitHub зеркало), откуда образ (ghcr.io), фиксированный тег, минимальные требования к машине, лицензия в 2–3 предложениях Есть всё, кроме раздела «эксплуатация»; см. находки 1–4, 10
LICENSE ТЗ 11 п. 9: полный текст AGPL-3.0 Полный текст на месте, правок не требует
deploy/docker-compose.yml + фрагменты прокси ТЗ 11 п. 5, ТЗ 10 п. 3: Apache основной, nginx/Caddy/Traefik альтернативами; для каждого — что монтируется и в какие пути Все четыре есть (apache, nginx, caddy, traefik), в README сведены таблицей; см. находки 7, 8
deploy/.env.example Пользовательские переменные с пояснениями Есть 6 переменных; полного справочника нет — находка 3
CHANGELOG.md Keep a Changelog, запись на каждом осмысленном шаге Ведётся; [Unreleased] наполнен B.1B.3, C.4

Рабочие документы проекта (не поставка, но обязаны быть верны)

specification.md — ТЗ v1.0; на вывод после D9 (см. карту миграции ниже). До закрытия D9 — ещё используется как историческая ссылка в находках раздела 3. implementation-plan.md — открытые вопросы для v1.0/v1.x. roadmap.md — линия 2.x.x. progress.md — живой трекер, читается первым после /clear. security.md — обязательные требования безопасности + принятые риски. Этот файл — план по документации.

Новые product.md, architecture.md, development.md — решение принято. В обязательную поставку пользователю (README) они не входят, но без них понимание проекта держится на памяти сессии. Их содержимое заменяет живой specification.md для всех будущих проходов — задачи D8 и D9.

  • docs/product.md — из ТЗ §1–3 и §4.1: зачем SelfPost, инфраструктурные предпосылки, out of scope, мультидоменная модель «домен ↔ приложения ↔ режим From». Границы продукта, меняющиеся только явным решением.
  • docs/architecture.md — as-built по коду: supervisord, postfix, opendkim, panel (HTTP + journal-milter + log-tailer); порядок старта; milter-цепочка; Postfix/SASL/TLS; журнал отправки и L1/L2 rate-limit; бэкап/restore; персистентность /data. Источник истины — код, не ТЗ.
  • docs/development.md — Go локально, go test/go vet, make e2e, когда нужен полный контейнер на dev-сервере, ручная проверка на selfpost.mixfed.ru, протокол коммитов/CHANGELOG, правила для агента (бывшее ТЗ §12). В progress.md — текущее состояние, не процедура.

Карта миграции из specification.md

Блок ТЗ Новый дом Задача
§13, §4.1 product.md D9
§4, §5–7 (техника), §9 architecture.md D8
§7.6 security.md («Обязательные требования») D9
§8 README (таблица env) D2
§9 (путь tar для пользователя) README D3
§1011 README + таблица «Поставка» выше D1D4
§12 development.md D8

После D9: specification.mddocs/archive/specification-v1.0.md (снимок, не правится); в docs/ живых ссылок на него нет.

Границы (без изменений): отдельные CONTRIBUTING.md, man-страницы и сайт документации в объём v1.x не входят.


2. Метод сверки с кодом

Правило: у каждого утверждения в документации есть ровно один источник истины в дереве, и сверка идёт от кода к тексту (что код делает → сказано ли об этом), а не наоборот — иначе не видно того, что забыли описать.

Класс утверждений Источник истины
Переменные окружения, значения по умолчанию loadConfig/envDefault/envIntcmd/panel/main.go:80159; ${VAR:-default} в build/ (postfix-config.sh, postfix-wrapper.sh, postfix-cert-reload.sh, logrotate-loop.sh, entrypoint.sh)
Поведение почтового тракта (порты, TLS, SASL, анти-relay, лимиты, milter-цепочка) build/postfix-config.sh
Экраны и действия панели (что вообще можно делать в эксплуатации) таблица маршрутов internal/web/web.go:133190
Бэкап/восстановление, экспорт/импорт домена, проверка версии internal/backup/backup.go, cmd/selfpost-backup/main.go
Сессии, вход, смена пароля internal/store/sessions.go, internal/web/session.go, internal/web/handlers_account.go
Ротация лога, периодический reload, интервалы build/logrotate-mail.conf, build/logrotate-loop.sh, build/postfix-cert-reload.sh
Деплой: тег образа, порты, монтирования, capabilities deploy/docker-compose.yml, build/Dockerfile
Обязательное содержание README (чеклист поставки) таблица «Поставка» в §1 этого файла (бывшее ТЗ §10–11)
Границы продукта, out of scope product.md (после D9)
As-built устройство (не пользовательский текст) architecture.md (после D8)
Обязательные требования безопасности security.md (после D9)

Порядок прохода: сначала перечислить фактическое (env-ключи, маршруты, postconf-настройки), потом искать каждый пункт в README / architecture.md — так находится и неверное, и отсутствующее. До закрытия D9 для чеклиста README допустима сверка с specification.md §10–11; после D9 — только с §1 этого файла.


3. Результаты первого прохода (выполнен)

Сверено: env-переменные, маршруты панели, конфигурация Postfix, бэкап/CLI, сессии, ротация, compose/Dockerfile. Ниже — всё найденное, по убыванию приоритета. Нумерация сквозная, на неё ссылаются задачи в разделе 4.

Высокий (пробел против ТЗ или вводит в заблуждение)

  1. Нет раздела «эксплуатация» — прямое требование ТЗ 11 п. 7 (specification.md:427). В README нет ни слова про /status (проверки PTR/hostname), /deliveries (журнал отправки, фильтры), /mail-queue (очередь Postfix), /system-log (хвост mail.log), /reload, /account, /backup — при том что всё это реализовано (internal/web/web.go:152–188). Не описана и процедура апгрейда (бамп тега → docker compose up -d), хотя раздел «Fixed image tag» на неё намекает.
  2. Битая ссылка + недокументированные лимиты. deploy/.env.example:13 отсылает к разделу README «Rate limiting», которого в README нет. Сам двухуровневый лимит не описан нигде для пользователя: L1 — smtpd_client_message_rate_limit + anvil_rate_time_unit (build/postfix-config.sh:110), L2 — per-domain/per-app из панели (internal/web/web.go:165, :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:80127). Обвязка: TLS_KEY_FILE, POSTFIX_SENDER_LOGIN_MAPS, MILTER_CONNECT_TIMEOUT/COMMAND/CONTENT (15s/15s/30s, build/postfix-config.sh:133), MILTER_WAIT_TIMEOUT, TLS_RELOAD_INTERVAL_SECONDS (86400, build/postfix-cert-reload.sh:14), LOGROTATE_INTERVAL_SECONDS (21600, 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 требует описать оба пути: «бэкап на лету — панель/CLI» и «прямой tar директории безопасен, если контейнер остановлен». В README (Backup, restore…) есть только первый, поэтому у читателя нет ответа на очевидный вопрос «а можно просто заархивировать ./data?».

Средний (стало неверным после последних фаз)

  1. Статус-баннер устарел. README.md:1215: «under active development» и ссылка на implementation-plan.md как на «phased build plan» — фазы 0→14 закрыты, план теперь про открытые вопросы и линию 2.x. Перед тегом релиза баннер переформулировать (или снять), ссылки уточнить.
  2. implementation-plan.md разошёлся с кодом. Пункт B.1 говорит: «смена пароля завершает все сессии, включая ту, из которой её делают → редирект на /login». Код делает иначе — гасит все, кроме текущей (internal/store/sessions.go:95 DeleteOtherSessions, internal/web/session.go:131), и панель так и пишет («Any other signed-in sessions were signed out», internal/web/handlers_account.go:49). README здесь верен. Расхождение в плане — зафиксировать как осознанное изменение при реализации, а не молча переписать.
  3. Комментарий в compose вводит в заблуждение. deploy/docker-compose.yml:14 велит «fill in .env (hostname, at least one strong TLS_CERT/KEY path)», но TLS_CERT_FILE/TLS_KEY_FILE захардкожены в самом файле (:3031) и в .env.example отсутствуют — настраивается на деле bind mount ./certs, а не переменные.
  4. Порт 587 публикуется всегда (deploy/docker-compose.yml:61), хотя listener появляется только при SUBMISSION_ENABLE=true (build/postfix-config.sh:151). Безвредно (слушать некому), но выглядит как лишний открытый порт — одна строка пояснения в README/compose снимает вопрос.

Низкий / требует решения, а не только текста

  1. /healthz есть, HEALTHCHECK нет. Эндпоинт реализован (internal/web/web.go:133), в build/Dockerfile объявления HEALTHCHECK нет (пробел отмечен и в плане, п. B.3). Решить в D6: либо задокументировать /healthz как точку внешнего мониторинга, либо добавить HEALTHCHECK в образ (это уже код + строка в CHANGELOG). Документировать «здоровье контейнера» до принятия решения нельзя — получится обещание, которого образ не даёт.
  2. Из B.1B.3/C.4 в README попала только обязательность SELFPOST_HOSTNAME. Не описаны: сессия переживает рестарт и живёт по скользящему сроку бездействия (PANEL_SESSION_IDLE_DAYS, «вкладка с автообновлением вход не продлевает» — контринтуитивно и заслуживает строки), ротация mail.log (14 файлов, проверка каждые 6 ч, суточный postfix reload) — README упоминает только «kept 14 days in-image» в требованиях к машине.
  3. Мелочи, без правки кода: Quick start тянет файлы с raw.githubusercontent.com (зеркало), хотя основной репозиторий — Codeberg; тег образа 1.0.0 в compose (:24) придётся бампить в тот же коммит, что и релиз; пустой каталог docs/logo.

Проверено и расхождений не найдено (важно не переделывать): требования к площадке; DNS-раздел (совпадает с тем, что реально проверяет /status и DNS-карточка домена); прогрев IP; смысл фиксированного тега; проверка версии при восстановлении — README описывает её точно так, как ведёт себя CheckRestore (internal/backup/backup.go); форма вызова docker exec … selfpost-backup > … (cmd/selfpost-backup/main.go:7); таблица reverse-proxy и требование пропускать Host; секретность архива и экспорта домена; лицензия; ретенция журнала отправки (90 дней) и то, что она — основной драйвер роста /data.


4. Задачи

Порядок — как перечислены; D1–D3 самые крупные. Каждая заканчивается записью в [Unreleased] 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:152188, кроме 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, с явным «на живом контейнере так делать не надо, потому что SQLite в WAL». Заодно упомянуть, что manifest.json после успешного восстановления потребляется.

D4. Точечные правки. Находки 5, 7, 8, 11: баннер статуса и ссылки (убрать ссылку на specification.md — после D9 продукт в product.md); комментарий в шапке compose про .env; строка про 587; бамп тега образа (делается в релизном коммите, не раньше).

D5. Синхронизация внутренних документов. Находка 6 — поправить B.1 в implementation-plan.md (с пометкой, что решение изменилось при реализации и почему), заодно перечитать progress.md на предмет утверждений, которые код уже опроверг. Не переписывать историю в CHANGELOG.

D6. Решение по HEALTHCHECK//healthz (Opus). Находка 9: либо строка в Dockerfile + описание, либо только описание эндпоинта. Влияет на код образа — поэтому отдельной задачей и не на Sonnet.

D7. Регресс-защита (см. раздел 5).

D8. Новые документы: architecture.md и development.md. Пишутся с нуля и перенимают техническую часть ТЗ (§4–7, §9, §12) в форму as-built / процесса. architecture.md сверяется методом из раздела 2 — по коду, не по памяти. development.md фиксирует воспроизводимый процесс (Go локально / e2e / dev-сервер). Готово, когда: architecture.md перечисляет все managed-процессы из build/supervisord.conf и путь письма через milter-цепочку без противоречий коду; development.md позволяет с нуля повторить локальную сборку + прогон тестов и понять, когда нужен dev-сервер, не заглядывая в память проекта и не открывая specification.md.

D9. Вывод specification.md из обращения (после D1–D8). Закрывает цель плана: живой ТЗ больше не нужен.

  1. Создать docs/product.md — §1–3, §4.1 (см. карту миграции в §1).
  2. Дополнить security.md разделом «Обязательные требования» — самодостаточный чеклист из бывшего §7.6 (setup-link, SASL, сессии, rate-limit логина, html/template, не-root и т.д.), без отсылки «см. ТЗ».
  3. Убедиться, что D1–D4 и D8 покрыли всё из §8–11, что должно жить в README / architecture.md (пройти карту миграции построчно).
  4. Перенести specification.mddocs/archive/specification-v1.0.md без правок текста; в начале архива — одна строка: «исторический снимок v1.0, не источник истины».
  5. Обновить ссылки во всём репозитории: progress.md (убрать «ТЗ: specification.md», заменить на product.md + architecture.md), implementation-plan.md и roadmap.md («Основа» → product.md), security.md, README.md (баннер D4 — не на ТЗ). rg specification\.md по репо — только архив и CHANGELOG/история. Готово, когда: в docs/ нет specification.md; новый агент после /clear может понять продукт, устройство, безопасность и процесс разработки, не открывая архив.

5. Чтобы не разошлось снова

  1. Правило шага: изменение, добавляющее/переименовывающее env-переменную, маршрут панели или наблюдаемое поведение почтового тракта, закрывается только вместе с правкой README/.env.example — в том же коммите, наравне с записью в CHANGELOG (протокол закрытия шага в progress.md).
  2. Дешёвая машинная проверка (D7): тест или скрипт, сверяющий множество ключей loadConfig со списком, объявленным в документации, и падающий на новом недокументированном ключе. Ловит самый частый класс расхождений (находка 3) без ручного прохода. Границу «внутренних» ключей задать явным списком-исключением в самом тесте.
  3. Перед каждым тегом — короткий проход по разделу 2 (источники истины в коде + README + architecture.md + product.md), а не полная ревизия текста и не сверка с архивным ТЗ.

6. Гейт релиза

Документационный проход — часть того же гейта, что e2e (C.4) и ревизия безопасности (D.5): D1–D6 и D9 закрыты до тега. D7 желателен, но тег не блокирует. D8 — обязателен до D9 (архитектура должна существовать до миграции); D8 один тег не блокирует, если D9 отложен, но полное закрытие плана (= вывод specification) — только D1–D9 вместе. Находки, обнаруженные позже, дописываются сюда, а не исправляются молча: этот файл — журнал состояния документации, а не одноразовый список дел.