Этот документ отвечает на практический вопрос: в какой крейт положить новый код и через какие границы его провести.
Полная целевая архитектура зафиксирована в
README.md. Здесь описаны действующие границы workspace и
правила разработки.
В workspace ровно шесть локальных крейтов:
┌──────────────┐
│ server │ CLI, HTTP
└──────┬───────┘
│
┌──────▼───────┐
│ app │ use cases, wiring
└──┬────┬────┬─┘
│ │ │
┌─────────────┘ │ └─────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ store │ │ render │ │ search │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└─────────────┬────┴────┬─────────────┘
│ core │
└─────────┘
В терминах разрешённых локальных зависимостей:
server -> app
app -> core + store + render + search
store -> core
render -> core
search -> core
core -> ни от одного локального крейта
Направление проверяет:
python3 scripts/check_architecture.pyСкрипт читает реальный cargo metadata, поэтому добавить запрещённую
зависимость «только на время» незаметно не получится.
Чистая доменная модель:
- типизированные
UserId,PageId,GroupId; - действия, входы и решения compatibility ACL;
- в будущем — страницы, ревизии, команды и доменные invariants.
Здесь не должно быть Axum, SQLx, HTTP-клиентов, файловой системы, Tokio-задач или знания о формате таблиц. Доменное правило должно проверяться обычным детерминированным unit-тестом.
Текущий API разобран в core-development.md.
Адаптеры устойчивого хранения:
- bounded PostgreSQL pool;
- встроенные forward-only migrations;
- проверка PostgreSQL и версии схемы;
- deadline для probes/migrations;
- подготовка приватных локальных каталогов
blobs/иtmp/без symlink ancestors и переходов через...
Доменные repositories и транзакции страниц пока относятся к roadmap.
store может переводить строки БД в типы core, но не принимает HTTP-решений и
не вызывает render или search.
Граница обработки Wikidot-разметки. Сейчас здесь есть только закреплённый профиль FTML 1.41.0 и исполняемые compatibility probes.
Production include resolver, module registry, hooks, sanitizer, cache и
ограниченный blocking executor ещё не реализованы. Поэтому результат
HtmlRender сейчас нельзя напрямую отдавать пользователю.
Подробнее: compat/ftml-1.41.md.
Точную выбранную версию и Cargo feature surface дополнительно фиксирует
python3 scripts/check_ftml_contract.py.
Изолирует Meilisearch. Текущий SearchClient:
- проверяет URL и разрешает только HTTP(S);
- не принимает credentials/query/fragment в URL;
- помечает ключ как sensitive header;
- запрещает redirects и proxy для диагностических запросов;
- ограничивает timeout и размер ответа;
- проверяет health и точную версию Meilisearch.
Индексация, outbox consumer и reindex пока относятся к roadmap.
Прикладной слой и composition root:
- загрузка и проверка конфигурации;
- env overrides;
- отдельный command-scoped
MigrationConfig, не загружающий search/storage секреты; - сборка
AppServices; - orchestration для
migrate,doctor, readiness и search status; - Argon2id
PasswordService.
Будущие use cases должны жить здесь: слой координирует доменное правило и адаптеры, но не знает о JSON, cookies, HTTP status codes или Axum extractors.
PasswordService имеет синхронный CPU-intensive API. Пока auth flow не
реализован, он не вызывается из async HTTP handler. При интеграции в M1 такие
операции должны идти через ограниченный blocking executor, а не блокировать
Tokio worker.
Внешняя транспортная граница:
- Clap CLI;
- Axum router;
- HTTP status/JSON;
- request IDs, tracing, timeout и security headers;
- graceful shutdown.
Handler должен быть тонким: разобрать и ограничить внешний ввод, вызвать
app, затем перевести типизированный результат в безопасный HTTP-ответ.
Доменное правило или SQL внутри handler — признак неверной границы.
Задайте вопросы по порядку:
- Это правило wiki, которое можно вычислить без I/O? Тогда
core. - Это чтение/запись PostgreSQL или локальных файлов? Тогда
store. - Это parsing/rendering Wikidot? Тогда
render. - Это общение с Meilisearch? Тогда
search. - Это законченная операция, координирующая правила и адаптеры? Тогда
app. - Это HTTP, CLI, JSON, headers или process lifecycle? Тогда
server.
Если код «нужен сразу в двух соседних адаптерах», обычно координация принадлежит
app. Не связывайте store, render и search друг с другом напрямую.
Не каждая функция обязана менять все шесть крейтов. Правильный путь проходит только через нужные слои:
внешний запрос
│
▼
server: validation transport-формата
│
▼
app: use case и порядок шагов
│
├──► core: доменные типы, переход состояния, проверка прав
│
├──► store: транзакционная запись/чтение
│
├──► render: безопасный render, если он нужен
│
└──► search: enqueue/index operation, если она нужна
│
▼
server: безопасный HTTP/CLI результат
- Зафиксируйте контракт. Для совместимости опишите наблюдаемое поведение и
truth table в
docs/compat/; для нового поведения сформулируйте invariants. - Добавьте домен. Типы и чистые правила идут в
coreвместе с unit- и property-тестами. - Добавьте нужные адаптеры. SQL и migration — в
store; FTML — вrender; Meilisearch — вsearch. Пропустите ненужные адаптеры. - Соберите use case.
appопределяет transaction boundary, порядок проверок, deadlines и компенсацию производных операций. - Откройте transport.
serverдобавляет request/response DTO, ограничения ввода, route/CLI и отображение ошибок. - Закройте вертикаль тестами. Unit-тесты домена, adapter/integration-тесты, handler-тест и негативные security cases.
- Обновите документацию. Уберите пометку roadmap только после появления рабочего API и проверок.
Это roadmap blueprint, не существующий API:
coreзадаст валидированную команду создания, идентификаторы и invariants;storeатомарно запишет страницу, первую ревизию, hash chain и outbox row;appпроверит ACL и вызовет транзакцию;serverограничит размер тела, распарсит запрос и вернёт корректный status;renderне обязан быть частью write transaction;searchне вызывается синхронно из транзакции: будущий worker прочитает outbox и обновит производный индекс.
Этот пример показывает границы, но не обещает имена будущих Rust-типов.
- На границе крейта используйте типизированный
thiserrorenum. - Сохраняйте исходную ошибку через
#[source]/#[from], но давайте ей безопасный стабильный контекст. - Не возвращайте наружу сырой текст SQLx, Reqwest, Argon2 или файловой системы.
- Невалидное или неполное состояние безопасности обрабатывайте fail-closed.
- Не превращайте ожидаемую ошибку пользователя в panic.
- Transport отдельно решает, что логировать, а что показывать клиенту.
Текущие примеры: StoreError, StorageError, SearchError, ConfigError,
PasswordError, StartupError, ServerError.
- Никогда не помещайте пароли, DSN, API keys, session tokens или исходный password в tracing fields и тексты ошибок.
- Для содержащих секрет структур пишите редактированный
Debug. ТекущиеDatabaseOptionsи конфигурационные secrets уже следуют этому правилу. - Помечайте auth headers как sensitive.
- Не передавайте секретный header через redirects или недоверенный proxy.
- В тестах используйте только заведомо фиктивные значения.
- PostgreSQL, HTTP и файловые операции выполняются асинхронно.
- CPU-heavy parsing, sanitizing и Argon2 нельзя выполнять прямо на Tokio worker.
- Для них нужен bounded blocking executor: ограниченная очередь, ограниченное число задач, deadline и понятная ошибка перегрузки.
- Не используйте неограниченный
spawn_blockingна каждый пользовательский запрос. - На внешнем I/O всегда должны быть timeout и предел размера ответа/ввода.
Ограниченный executor для auth/render — roadmap; это обязательное условие перед подключением соответствующего синхронного API к HTTP.
- В runtime-коде не используйте
unwrap()иexpect()на данных, конфигурации, I/O и состоянии, которое может быть некорректным. expect()допустим в тестах для создания fixture и улучшения сообщения о поломке самого теста.- Panic не является обработкой ошибки пользователя. HTTP panic catcher — последняя защита процесса, а не штатный control flow.
- Workspace запрещает
unsafeчерезunsafe_code = "forbid".
- Каждая коллекция, тело запроса, recursion/include depth и очередь должны иметь обоснованный предел.
- Используйте bounded pool и backpressure, а не бесконечное накопление задач.
- Не держите SQL transaction открытой во время render или сетевого вызова к Meilisearch.
- Перед добавлением реальных blob-операций открывайте файлы относительно
заранее открытого
dirfdсopenat2/O_NOFOLLOW(или безопасным платформенным эквивалентом). Текущая проверка пути нужна для bootstrap, но сама по себе не является защитой от TOCTOU при конкурентной замене path. - PostgreSQL остаётся источником истины; индекс и render cache производны.
- Сначала измеряйте hot path, затем оптимизируйте без нарушения границ.
Экспортируйте из lib.rs только устойчивую границу крейта. Внутренние модули
можно оставлять private и переэкспортировать нужные типы:
mod implementation;
pub use implementation::{PublicError, PublicService};Это позволяет менять раскладку файлов без переписывания потребителей. Не экспортируйте конкретный SQLx pool, Reqwest client или Axum type из нижнего слоя, если потребителю достаточно доменного интерфейса.
Перед реализацией проверьте:
- правило находится в самом нижнем подходящем слое;
- новая локальная зависимость разрешена DAG;
serverне содержит SQL и доменных решений;- адаптеры не вызывают друг друга;
- внешний ввод ограничен и валидирован;
- секреты не попадают в
Debug, ошибки и tracing; - blocking work не выполняется на async worker;
- ошибочное состояние закрывает доступ, а не открывает его;
- roadmap API не выдан за уже реализованный.
Полный критерий готовности: testing.md.