Название: WikiNEXT
Репозиторий / workspace: wikinext-engine
Основной бинарник: wikinext
Статус: архитектура зафиксирована, реализуется M0
Дата документа: 2026-07-30
Лицензия: AGPL-3.0-or-later
Название WikiNEXT сохраняется осознанно. Существует старый неактивный проект WikiNEXT из другой ниши; это известно и не является причиной менять бренд. До публичного коммерческого использования отдельно проверяются домены и товарные знаки, но переименование проекта не планируется.
Этот файл — архитектурный манифест и целевое состояние WikiNEXT. Чтобы
запустить текущую реализацию или начать писать код, откройте
docs/README.md.
- быстрый локальный старт;
- архитектура и границы шести крейтов;
- работа с типизированными ID и ACL в
wikinext-core; - тесты и Definition of Done;
- заметки о совместимости.
В руководствах явно отмечено, какой API уже существует, а что пока является roadmap. Это важно: описанная ниже целевая wiki значительно шире текущего operational slice M0.
WikiNEXT — быстрый, безопасный и стабильный wiki-движок с clean-room ядром, ориентированный на совместимость с Wikidot-семейством и полный перенос Backrooms Wiki RU с RuFoundation.
Движок должен перенести и сохранить:
- страницы и их полную историю ревизий;
- исходную Wikidot-разметку;
- теги, рейтинги и комментарии;
- вложения и их версии;
- parent-связи и редиректы;
- аккаунты, авторство, группы, ранги и права;
- темы, компоненты и страницы навигации;
- совместимое HTML-представление, достаточное для существующих CSS-тем.
WikiNEXT — движок, а не конкретная тематическая wiki. При этом одно развёртывание WikiNEXT обслуживает один wiki-сайт. Для нескольких независимых wiki поднимаются отдельные экземпляры.
Интерфейс и системный контент только русские. Инфраструктуры локализации нет намеренно.
WikiNEXT всегда работает как single-node система:
- один физический или виртуальный сервер;
- один экземпляр приложения
wikinext; - один PostgreSQL;
- один Meilisearch;
- локальное файловое хранилище вложений;
- фоновые воркеры запускаются внутри процесса
wikinext; - локальные in-process кэши;
- один wiki-сайт на одно развёртывание.
Не поддерживаются и не проектируются заранее:
- горизонтальное масштабирование приложения;
- несколько одновременно работающих экземпляров
wikinext; - распределённая инвалидация кэша;
- Redis;
- Redis Streams;
- Kafka, RabbitMQ и другие внешние брокеры;
- S3 как основное хранилище;
- multi-site SaaS в одном процессе;
- распределённые блокировки;
- shared sessions между узлами.
PostgreSQL и Meilisearch могут работать отдельными системными сервисами, но находятся на том же сервере или в той же доверенной локальной сети. Meilisearch не выставляется напрямую в интернет.
В первую очередь не делаем:
- визуальный SPA-конструктор;
- универсальную CMS;
- маркетплейс плагинов;
- динамическую загрузку
dylibили WASM-плагинов; - полную совместимость со всеми историческими багами Wikidot;
- поддержку нескольких языков интерфейса;
- федерацию wiki;
- real-time совместное редактирование;
- распределённую архитектуру «на будущее».
Совместимость реализуется там, где она нужна для реального контента Backrooms Wiki RU и стабильной Wikidot-семантики.
| Компонент | Зафиксированная версия / правило | Назначение |
|---|---|---|
| Rust | 1.97.1 | основной язык, фиксируется в rust-toolchain.toml |
| PostgreSQL | 18.4 | единственный источник истины для структурированных данных |
| ftml | 1.41.0 | Wikidot parser, AST, include pipeline, HTML/Text render |
| wikidot-normalize | 0.12.0 | нормализация имён страниц |
| Meilisearch | 1.45.1 | основной полнотекстовый поиск с P0 |
| Axum + Tokio + Tower | стабильные, зафиксированные в Cargo.lock |
HTTP и async runtime |
| SQLx | стабильная, зафиксированная в Cargo.lock |
PostgreSQL, compile-time SQL checks |
| Askama | стабильная, зафиксированная в Cargo.lock |
серверные HTML-каркасы и служебный UI |
| HTMX | зафиксированный локальный asset | интерактивность без SPA |
| moka | стабильная, зафиксированная в Cargo.lock |
локальные AST- и render-кэши |
| Argon2id | через Rust crate argon2 |
пароли |
| tracing | стабильная | structured logs и spans |
Точные номера обычных Rust-зависимостей фиксируются в Cargo.lock в M0. Манифест фиксирует архитектурно значимые внешние версии, а не копирует весь lockfile.
PostgreSQL является каноническим хранилищем. Meilisearch — производный и полностью перестраиваемый индекс. Кэш — одноразовое состояние процесса.
WikiNEXT с первого коммита лицензируется под AGPL-3.0-or-later.
Обязательно присутствуют:
LICENSE;- SPDX
AGPL-3.0-or-laterв workspace и публикуемых crate; THIRD_PARTY_LICENSES.md;- автоматическая проверка лицензий зависимостей;
- ссылка на исходный код работающей версии сервиса;
- инструкция сборки и развёртывания.
Причина выбора — зависимость ftml, распространяемая под AGPL-3.0-or-later. Это принято как сознательное ограничение проекта.
wikidot-normalize используется по MIT.
Код WikiJump, Wikidot и RuFoundation не копируется. Допустимы:
- изучение наблюдаемого поведения;
- чтение открытого кода для документирования совместимости;
- сравнение входов и выходов;
- собственные golden tests;
- собственные импортёры;
- использование RuFoundation как источника данных миграции.
Нельзя переносить чужие реализации механически или маскировать скопированный код как clean-room.
Не вводим распределённые компоненты и абстракции, которые не нужны одному серверу. Локальная функция предпочтительнее внешнего сервиса, если внешний сервис не даёт необходимого функционала.
Meilisearch является исключением, потому что полнотекстовый поиск нужен сразу и сознательно вынесен в специализированный движок.
Страница, ревизия, права, теги, задания, аудит и состояние индексации считаются сохранёнными только после успешного commit в PostgreSQL.
Meilisearch, локальные кэши и временные файлы можно удалить и восстановить.
Совместимость определяется не заявлениями, а сравнением:
- исходной разметки;
- AST;
- итогового DOM;
- поведения include;
- модулей;
- прав;
- импорта и повторного импорта.
Любая цепочка рендера заканчивается проверяемой политикой безопасности. Расширение не может обойти sanitizer, ACL или проверку файлов.
Trait вводится, когда:
- реально существует несколько реализаций;
- требуется тестовый fake;
- граница отделяет внешний сервис;
- без trait возникает нежелательная связность.
Не создаём интерфейс только потому, что «когда-нибудь может появиться второй backend».
Непроверенное поведение помечается как задача M0 и не превращается в выдуманный контракт.
Файлы и модули должны иметь одну понятную ответственность. Жёстких лимитов вида «ровно 4–12 файлов в папке» или обязательного CI-лимита строк нет. Большой файл делится, когда у него появляются разные причины для изменения, а не по формальному счётчику.
Граф зависимостей без циклов:
wikinext-server → wikinext-app → {wikinext-store, wikinext-render, wikinext-search} → wikinext-core
| Крейт | Ответственность |
|---|---|
wikinext-core |
доменные типы, идентификаторы, ошибки, валидация, policy-модель, revision hashing, extension contracts |
wikinext-store |
PostgreSQL repositories, транзакции, миграции, sessions, audit, outbox, durable jobs, локальное файловое хранилище вложений |
wikinext-render |
адаптер ftml, includes, module layer, HTML/Text render, sanitize pipeline, diff, DOM hooks |
wikinext-search |
Meilisearch client, schema индекса, документы, обновление индекса, rebuild и проверка состояния |
wikinext-app |
use-cases, ACL checks, orchestration, локальные кэши, invalidation, embedded workers, импорт |
wikinext-server |
HTTP, маршруты, middleware, Askama shell, HTMX endpoints, CLI и запуск процесса |
wikinext-core не зависит от SQLx, Axum, Meilisearch или файловой системы.
wikinext-app является единственным слоем, в котором координируются права, запись страницы, аудит, outbox, кэш и внешние эффекты.
| Сущность | Суть |
|---|---|
User |
id, login, normalized_login, email, password_hash, rank, status, created_at |
Group |
именованная группа и её системные свойства |
GroupMembership |
user_id, group_id, период действия, источник назначения |
PermissionAction |
типизированное действие: read, edit, create, delete, restore, rename, manage_files, rate, comment, moderate, manage_acl, admin |
AclRule |
scope, principal, action, effect allow/deny |
AclVersion |
immutable-снимок набора правил |
AclVersionEntry |
конкретное правило внутри версии |
Session |
opaque session id hash, user_id, created_at, last_seen_at, expires_at, revoked_at |
Гость существует виртуально с rank 0. Зарегистрированный пользователь по умолчанию получает rank 1.
| Сущность | Суть |
|---|---|
Page |
стабильный id, namespace, slug, full_name, current_revision_id, current_acl_version_id, parent_page_id, counters, created_by, updated_by |
PageContentBlob |
sha256, source, byte_size; content-addressed дедуп исходника |
Revision |
immutable revision с parent, author, action, content_sha256, meta_hash, chain_hash и временем |
RevisionMeta |
immutable snapshot title, page_type, deleted, locked, acl_version_id, render settings и совместимых полей |
Tag |
нормализованный тег |
PageTag |
текущий набор тегов |
RevisionTag |
snapshot тегов конкретной ревизии |
Redirect |
старый или альтернативный full_name → page_id |
PageDependency |
from_page, from_revision, to_page или unresolved_ref, kind=link/include |
PageWatch |
пользовательская подписка на страницу |
page_type:
page;component;theme;system;redirect.
| Сущность | Суть |
|---|---|
Rating |
page_id, user_id, value; уникальная пара page/user |
RatingEvent |
история изменения или модерационной компенсации рейтинга |
Comment |
threaded comment, source, rendered_html, parent_id, moderation state, soft delete |
CommentRevision |
история правок комментария при включённой политике редактирования |
| Сущность | Суть |
|---|---|
Attachment |
page_id, logical filename, current_version_id, soft delete |
AttachmentVersion |
content_type, size, sha256, uploaded_by, created_at, metadata JSONB |
FileBlob |
sha256, size, storage state и относительный content-addressed path |
Blurhash и другие вычисленные данные находятся в AttachmentVersion.metadata.
| Сущность | Суть |
|---|---|
AuditEvent |
append-only событие actor/action/target/request/ip/before/after |
SearchOutbox |
durable событие синхронизации PostgreSQL → Meilisearch |
Job |
durable фоновое задание, attempts, run_at, lock, error, idempotency_key |
ImportMapping |
связь внешних id RuFoundation с id WikiNEXT |
SchemaState |
версии внутренних схем рендера, поиска и совместимости |
Любое изменение состояния страницы создаёт новую immutable-ревизию:
- изменение source;
- title;
- tags;
- page type;
- lock;
- deleted state;
- ACL;
- совместимых render options.
Рейтинги, комментарии, watch и файлы имеют собственную историю и не превращаются в page revision, если не меняют непосредственно состояние страницы.
Исходник страницы хранится в PageContentBlob по SHA-256. Несколько ревизий могут ссылаться на один blob.
Это сохраняется ради:
- полной совместимости истории;
- дедупликации одинакового контента;
- быстрых revert;
- проверки целостности;
- импорта больших цепочек ревизий.
RevisionMeta хранит полный снимок необходимых метаданных, а не только номер изменившегося поля.
Права представлены ссылкой на immutable AclVersion, поэтому откат ревизии действительно восстанавливает исторический набор прав.
Теги снапшотятся через RevisionTag.
content_hash = SHA256(source bytes);meta_hash = SHA256(canonical revision metadata bytes);chain_hash = SHA256(domain_separator + parent_chain_hash + page_id + revision_id + action + author_id + timestamp + content_hash + meta_hash).
Canonical format является версионированным бинарным форматом WikiNEXT, а не неупорядоченным JSON.
Root revision использует нулевой parent hash.
Hash chain предназначена для обнаружения случайной порчи и несанкционированных изменений, которые не сопровождались полным пересчётом цепочки. Она не считается криптографическим доказательством против администратора БД, имеющего возможность переписать всю историю.
В P2 может быть добавлена периодическая подпись checkpoint hash внешним ключом.
- Edit: новая ревизия с parent=current.
- Revert: новая ревизия с parent=current и состоянием выбранной исторической ревизии.
- Delete: новая ревизия с
is_deleted=true. - Restore: новая ревизия с
is_deleted=false. - Rename: page_id остаётся стабильным, full_name меняется, старое имя становится Redirect.
- ACL change: новая AclVersion и новая page revision.
- Tag change: новая page revision и новый snapshot тегов.
- Hard purge: отдельная опасная admin-команда, не часть обычного UI.
Каждая запись принимает base_revision_id.
Если base_revision_id != current_revision_id, запись не применяется автоматически. Пользователь получает conflict view с diff.
Page lock является отдельным флагом и не заменяется ACL.
Успешное изменение страницы выполняется одной PostgreSQL-транзакцией:
- проверка optimistic lock;
- проверка прав;
- создание или reuse content blob;
- создание AclVersion при изменении прав;
- создание RevisionMeta;
- создание Revision;
- запись RevisionTag;
- обновление current state в Page;
- обновление PageTag;
- обновление Redirect при rename;
- запись AuditEvent;
- запись SearchOutbox;
- commit.
После commit:
- локальный кэш инвалидируется;
- embedded worker обрабатывает SearchOutbox;
- Meilisearch обновляется с eventual consistency.
HTTP-запрос не вызывает Meilisearch внутри PostgreSQL-транзакции.
rank: u32;- guest rank = 0;
- обычный зарегистрированный пользователь = 1;
- rank наследует базовые возможности нижних rank;
- группы добавляют нелинейные роли;
- ACL действует на уровнях
global → namespace → page; - principal может быть user или group;
- effect — allow или deny;
- lock страницы остаётся отдельным состоянием.
Policy resolver:
- находится в
wikinext-core; - является чистой функцией без IO;
- получает полностью подготовленный контекст;
- возвращает не только bool, но и объяснение решения;
- полностью покрывается table-driven и property tests;
- не зависит от HTTP/UI;
- fail-closed при неизвестном или повреждённом состоянии.
Точная precedence-модель RuFoundation должна быть прочитана и зафиксирована в M0-Compat до реализации M1. Нельзя сначала реализовать произвольный deny wins, а затем переделывать весь движок во время импорта.
До завершения M0-Compat обязательны ответы:
- что является default allow/default deny;
- как взаимодействуют global, namespace и page;
- может ли более конкретный allow перебить общий deny;
- user rule против group rule;
- несколько конфликтующих group rules;
- rank против ACL;
- admin bypass;
- lock против edit permission;
- права на удалённую страницу;
- права на include приватного компонента.
Все проверки выполняются в wikinext-app.
UI лишь отображает уже вычисленный результат.
Render include проверяет read permission каждой включённой страницы.
Private content не помещается в общий render cache и не индексируется в публичный Meilisearch index.
P0 namespaces:
- default;
sandbox;component;theme;nav;system.
Namespace является частью full_name и политик доступа.
Точный набор legacy namespace и alias определяется инвентаризацией RuFoundation в M0-Compat.
Системные страницы не обходят обычную историю ревизий. Изменение темы, компонента или панели создаёт такую же проверяемую ревизию.
Askama рендерит:
- общий HTML shell;
- header;
- side;
- main;
- footer;
- auth/admin UI;
- history/diff/editor pages;
- HTMX fragments.
Тело wiki-страницы рендерится через ftml.
Зоны заполняются include системных страниц пространства nav:.
Предварительные имена:
nav:top;nav:side;nav:foot.
Фактический набор и alias берутся из реального seed RuFoundation в M0-Compat.
Правка панели с сайта является обычной правкой nav:* страницы с историей, diff и revert.
theme — versioned CSS и связанные assets.
component — переиспользуемая wiki-страница с аргументами include вида {$name}.
Отдельного FTML layout-документа нет. HTML shell остаётся контролируемым сервером, а темы меняют внешний вид через CSS и разрешённые assets.
Рендер по умолчанию использует ftml::Layout::Wikidot. Если golden tests покажут, что конкретные темы RuFoundation требуют другого layout или их fork-семантики, решение принимается по результатам M0-Compat.
Полный pipeline:
- загрузка Page, Revision, RevisionMeta и security context;
- проверка read permission;
- разрешение include через наш
Includer; ftml::include;preprocess;tokenize;parse;- выполнение typed ftml modules;
- выполнение custom ModuleRegistry;
HtmlRender;- trusted pre-sanitize HTML hooks;
- финальный allowlist sanitizer;
- safe DOM attribute hooks;
- Askama shell;
- параллельный
TextRenderдля поискового документа; - запись PageDependency;
- помещение результата в разрешённый локальный кэш.
Наш Includer:
- резолвит страницы, components и themes;
- проверяет ACL каждой цели;
- поддерживает аргументы;
- ограничивает глубину;
- ограничивает общее число include;
- ограничивает раскрытый размер;
- отслеживает cycles;
- имеет общий deadline рендера;
- кэширует повторный fetch в рамках одного request;
- возвращает явную безопасную ошибку при отсутствии страницы.
Off-site PageRef в P0 не загружается по сети. Он становится interwiki link или безопасным placeholder по политике.
Произвольный HTML hook никогда не выполняется после sanitizer.
Есть два класса расширений:
pre_sanitize_html_hook— может менять HTML, после него всегда запускается sanitizer;safe_dom_attribute_hook— работает с уже очищенным DOM и может добавлять только allowlisted class/data/aria атрибуты.
Blurhash реализуется через safe DOM hook: добавляет class и data-blurhash, но не вставляет произвольный script/style.
SyntaxTree кэшируется только после проверки реальной serde-совместимости ftml 1.41.0.
Ключ:
content_hash + ftml_version + parser_settings_fingerprint
Кэш локальный, bounded и одноразовый.
ftml 1.41.0 типизирует ограниченный набор модулей, включая:
- Backlinks;
- Categories;
- Join;
- PageTree;
- Rate.
Остальные Wikidot-модули реализует наш ModuleRegistry.
Контракт:
Module {
name
parse_args
authorize
execute
cache_policy
}
Неизвестный модуль:
- не роняет страницу;
- не исчезает молча;
- рендерит диагностический placeholder;
- пишет structured warning с page_id и module name.
Поведение ftml на неизвестном [[module X]] проверяется прототипом в M0, до реализации основного renderer. После теста фиксируется один механизм:
- извлечение custom modules до ftml и безопасные placeholders;
- либо обработка сохранённых AST-узлов, если ftml их предоставляет.
P0:
- ListPages;
- CSS;
- Redirect;
- Backlinks;
- Categories;
- Join.
P1:
- Rate;
- Comments;
- PageTree;
- TagCloud;
- Files;
- CountPages;
- SiteChanges;
- Search;
- Watchers.
P2:
- ListUsers;
- Members;
- Gallery;
- Feed;
- форумные;
- административные;
- deprecated aliases.
Rate и Comments не считаются P0, пока отсутствуют их полноценные модели и UI.
Meilisearch обязателен для P0 и является основным полнотекстовым поиском.
PostgreSQL не дублирует полноценный FTS. В нём остаются:
- точный поиск страницы по full_name;
- lookup по id;
- административные выборки;
- источник данных для полного reindex.
Индексируемый документ содержит:
- page_id;
- full_name;
- namespace;
- slug;
- title;
- plain_text;
- tags;
- page_type;
- rating_score;
- created_at;
- updated_at;
- revision_id;
- search_schema_version.
Searchable fields с приоритетом:
- title;
- full_name;
- tags;
- plain_text.
Filterable fields:
- namespace;
- tags;
- page_type.
Sortable fields:
- rating_score;
- created_at;
- updated_at;
- title.
В основной индекс попадают только страницы, которые:
- доступны анонимному пользователю;
- не зависят от user/group ACL;
- не включают приватный контент;
- не удалены;
- не скрыты системной политикой.
Это исключает утечку title, tags, excerpts и существования приватной страницы через поиск.
P0 не пытается кодировать произвольный ACL resolver в Meilisearch filters.
Безопасный поиск по непубличным страницам является отдельной P2-задачей и допускается только после отдельной модели visibility и security tests.
При изменении страницы событие записывается в SearchOutbox в той же PostgreSQL-транзакции.
Embedded worker:
- выбирает события батчами;
- блокирует их через
FOR UPDATE SKIP LOCKED; - строит актуальный документ из PostgreSQL;
- выполняет upsert/delete в Meilisearch;
- подтверждает событие после успешного ответа;
- повторяет ошибки с exponential backoff;
- отправляет постоянно падающее событие в dead state;
- работает идемпотентно.
Несколько событий одной страницы могут быть схлопнуты до последнего состояния.
Команда wikinext search reindex:
- создаёт индекс новой schema version;
- экспортирует public documents из PostgreSQL;
- загружает батчами;
- проверяет количество и sample queries;
- переключает alias;
- удаляет старый индекс после подтверждения.
Meilisearch не входит в канонический backup: индекс всегда можно перестроить из PostgreSQL.
При недоступности Meilisearch:
- чтение и редактирование страниц продолжаются;
- SearchOutbox накапливается;
- search endpoint возвращает явный degraded response;
/readyzне обязан выключать весь сайт из-за временной поломки поиска;- отдельный search health показывает проблему.
Используется только локальный moka.
Кэши:
- parsed AST;
- публичный rendered body;
- короткий lookup page metadata;
- request-local include fetch.
Redis и L2 cache отсутствуют.
В общий process cache попадает только страница, для которой доказано:
- anonymous read разрешён;
- вся include dependency closure также public;
- вывод не зависит от текущего пользователя;
- модуль не имеет user-specific output;
- тема и компоненты публичны.
Ключ включает:
- page_id;
- revision_id;
- ftml_version;
- parser settings fingerprint;
- render policy version;
- module registry version;
- theme revision;
- component dependency fingerprint.
Private, group-only и user-specific страницы рендерятся без shared render cache.
PageDependency сохраняет include/link graph последнего актуального рендера.
После изменения страницы:
- удаляется её cache entry;
- по reverse include graph находятся зависимые страницы;
- invalidation идёт транзитивно;
- ограничение глубины защищает от повреждённого графа;
- при сомнении очищается весь render cache.
Так как процесс один, pub/sub не нужен.
Фоновые задания хранятся в PostgreSQL и выполняются embedded workers внутри wikinext.
Типовые задания:
- SearchOutbox processing;
- render dependency rebuild;
- attachment metadata;
- blurhash;
- sitemap/RSS;
- notification delivery;
- import batches;
- orphan cleanup;
- integrity verification.
Job содержит:
- kind;
- payload version;
- state;
- priority;
- run_at;
- attempts;
- max_attempts;
- locked_at;
- locked_by;
- last_error;
- idempotency_key;
- created_at;
- finished_at.
Правила:
- обработчики идемпотентны;
- lock имеет timeout;
- crash процесса не теряет job;
- retry использует backoff;
- poison job не блокирует очередь;
- graceful shutdown перестаёт брать новые jobs и завершает текущие до deadline.
Отдельный JobQueue trait не создаётся, пока существует только PostgreSQL implementation.
Вложения хранятся на локальном диске сервера в content-addressed структуре:
data/blobs/ab/cd/<full-sha256>
Upload pipeline:
- ограничение размера до чтения всего body;
- запись во временный файл;
- вычисление SHA-256;
- content sniffing;
- проверка allowlist MIME;
- дополнительная проверка опасных форматов;
- fsync временного файла;
- atomic rename в blob path;
- PostgreSQL-транзакция AttachmentVersion;
- orphan cleanup при сбое между filesystem и DB.
Файлы не лежат в webroot.
Выдача идёт через контроллер, который:
- проверяет read permission страницы;
- выставляет корректный Content-Type;
- задаёт Content-Disposition;
- поддерживает ETag;
- запрещает MIME confusion;
- применяет CSP/sandbox для потенциально активного контента.
Для public assets темы может быть отдельная строгая allowlist-политика.
S3 backend не планируется, так как сервер всегда один.
Backup обязан включать PostgreSQL и data/blobs.
Динамические плагины отвергнуты.
Расширения:
- компилируются вместе с WikiNEXT;
- регистрируются явно;
- включаются compile-time feature или runtime config;
- не получают прямого обхода app-layer;
- используют типизированные contracts.
Точки расширения:
- module registry;
- pre-sanitize render hook;
- safe DOM attribute hook;
- attachment processing hook;
- search document enrichment;
- job handler registry;
- audit enrichment;
- import transformer.
Первое доказательное расширение — blurhash:
- attachment hook вычисляет blurhash;
- metadata сохраняется в AttachmentVersion;
- safe DOM hook на локальных изображениях добавляет class и
data-blurhash; - клиентский локальный script отображает placeholder;
- исходная wiki-разметка не меняется.
- Argon2id;
- параметры хеширования хранятся в hash string и обновляются при входе;
- optional server-side pepper хранится вне БД;
- opaque session token;
- в БД хранится hash токена;
- HttpOnly;
- Secure;
- SameSite=Lax или Strict по конкретному flow;
- rotation после login и privilege change;
- idle timeout;
- absolute timeout;
- revoke all sessions;
- local in-process rate limit на login/register/reset;
- audit auth events.
- token для state-changing browser requests;
- проверка Origin;
- fallback Referer policy;
- никакого изменения состояния через GET;
- SameSite cookie не считается единственной защитой.
- финальный allowlist sanitizer;
scriptзапрещён в пользовательской разметке;- event handlers запрещены;
javascript:запрещён;data:ограничен;- iframe выключен по умолчанию;
- raw HTML выключен по умолчанию;
- внешние ссылки получают
rel="noopener noreferrer"; - пользовательский CSS не получает доступ к опасным URL-схемам;
- theme CSS редактируется только отдельным permission;
- CSP остаётся последним ограничением.
- ACL каждой страницы;
- cycle detection;
- max depth;
- max count;
- max expanded bytes;
- render deadline;
- bounded module output;
- отсутствие сетевого include в P0.
- параметризованные SQLx queries;
- dynamic sort/filter только через enum/whitelist;
- отдельная migration owner role;
- application role без DDL;
- audit table защищена от UPDATE/DELETE application role;
- DB constraints дублируют критичные доменные инварианты.
- Content-Security-Policy;
- X-Content-Type-Options;
- Referrer-Policy;
- Permissions-Policy;
- HSTS после корректного HTTPS;
- frame-ancestors;
- безопасная cookie policy.
- Meilisearch доступен только приложению;
- master key обязателен;
- browser не получает административный Meili key;
- public index не содержит private pages;
- search snippets дополнительно escape/sanitize;
- index schema не публикует скрытые поля.
- никаких
unwrap/expectв request и worker paths; - typed errors по слоям;
- request_id и job_id во всех логах;
- graceful shutdown;
- bounded DB pool;
- bounded render blocking pool;
- deadlines для HTTP, render и Meili;
- retry только для идемпотентных операций;
- schema migrations forward-only;
- backup до migration;
- periodic restore test;
- PostgreSQL constraints для current revision, unique full_name и membership;
- outbox гарантирует восстановление поискового индекса;
- local cache можно полностью очистить;
- Meilisearch можно полностью удалить и перестроить;
- filesystem reconciliation обнаруживает missing/orphan blobs;
- integrity checker проверяет revision chains и attachment hashes.
/healthz— процесс жив;/readyz— PostgreSQL доступен, schema version совместима, обязательные директории доступны;/status/search— Meilisearch и backlog outbox;/status/jobs— queue lag и failed jobs;/status/storage— доступность blob directory.
Временная поломка Meilisearch переводит поиск в degraded state, но не делает всю wiki недоступной.
Канонический backup:
- PostgreSQL base backup / dump;
- WAL/PITR по выбранной production-схеме;
- snapshot
data/blobs; - конфигурация без секретов;
- отдельно сохранённые secrets;
- manifest версий.
Meilisearch backup необязателен: после восстановления PostgreSQL выполняется wikinext search reindex.
Проверка восстановления выполняется периодически на отдельной временной директории или машине.
Single-node означает единую точку отказа, поэтому backup и проверенный restore являются обязательной частью production, а не P2-фичей.
Цели являются ориентирами после измерения на целевом сервере:
- p95 public cached page:
< 100–150 ms; - p95 uncached render:
< 300–500 ms; - p95 search:
< 200–300 ms; - cache hit популярных public pages:
> 80%; - edit transaction без индексации:
< 250 msпри обычной странице; - SearchOutbox lag в норме:
< 5 s.
Методы:
- AST cache;
- public render cache;
- ETag/304;
- bounded
spawn_blockingrender pool; - keyset pagination;
- batch loading;
- no N+1;
- индексы PostgreSQL;
- батчи Meilisearch;
- outbox coalescing;
- compression;
- streaming больших файлов;
- preload только измеренно полезных данных.
Не оптимизируем ценой нарушения ACL, истории или корректности.
Обязательные ограничения и индексы:
- unique
normalized_login; - unique normalized email при политике уникальности;
- unique
full_name; - unique
(page_id, user_id)для Rating; - unique
(user_id, group_id)для active membership; - unique
(page_id, tag_id)для PageTag; - unique content sha256;
- unique file sha256;
- index revisions
(page_id, created_at desc, id desc); - index comments
(page_id, created_at, id); - index parent pages;
- index reverse PageDependency по
to_page_id; - index SearchOutbox по state/run_at;
- index Job по state/run_at/priority;
- partial indexes для active sessions;
- foreign keys с осознанными delete rules;
- check constraints для rating value, states и rank.
Page.current_revision_id должен ссылаться на ревизию той же страницы. Если обычный FK не выражает это напрямую, используется составной unique/FK или deferred constraint trigger.
- normalization;
- full_name;
- policy resolver;
- canonical metadata encoding;
- content/meta/chain hashes;
- module args;
- sanitizer policies;
- search document mapping;
- job backoff;
- import normalization.
- migrations;
- repositories;
- page transaction;
- optimistic conflict;
- revert;
- delete/restore;
- ACL version rollback;
- append-only audit;
- outbox idempotency;
- worker crash recovery;
- attachment reconciliation;
- Meilisearch upsert/delete/reindex.
Используются testcontainers PostgreSQL и Meilisearch.
- реальные страницы Backrooms Wiki RU;
- includes;
- components;
- themes;
- nav pages;
- modules;
- edge-case markup;
- DOM normalization;
- visual screenshot tests для критичных тем;
- сравнение fork FTML RuFoundation и upstream ftml.
- XSS payload corpus;
- CSS escapes;
- malformed URLs;
- include cycles/bombs;
- permission bypass;
- private include leakage;
- search metadata leakage;
- CSRF;
- session fixation;
- upload polyglots;
- path traversal;
- decompression bombs;
- oversized module output.
- slug/full_name invariants;
- revision chain integrity;
- ACL resolver determinism;
- PageRef parse;
- import idempotency;
- no duplicate current tags;
- reindex equivalence.
- popular page reads;
- cache stampede;
- uncached render;
- ListPages;
- recent changes;
- search;
- edits under contention;
- large history;
- import throughput;
- attachment download.
Обязательные проверки:
cargo fmt --check;cargo clippy --workspace --all-targets -- -D warnings;- unit/integration tests;
- doctests;
cargo deny;cargo audit;- license compatibility;
- migration apply на чистой БД;
- migration apply на previous schema fixture;
- SQLx offline metadata check;
- forbidden dependency directions;
cargo treereview для ftml features;- security test subset;
- golden render subset;
- reproducible release build.
Toolchain фиксирован, поэтому -D warnings не зависит от внезапного обновления compiler в CI.
Жёсткого CI-лимита строк в файле нет.
- workspace, toolchain, AGPL license;
- config TOML/env;
- structured logs;
- migrations;
- users;
- register/login/logout;
- PostgreSQL sessions;
- CSRF;
- auth rate limit;
- rank/groups/ACL;
- страницы create/view/edit;
- full revision model;
- history/diff/revert;
- rename/redirect;
- soft delete/restore;
- parent hierarchy;
- tags;
- recent changes;
- ftml render;
- includes;
- sanitizer;
- P0 modules;
- sandbox;
- backlinks;
- Meilisearch;
- SearchOutbox;
- reindex;
- local AST/render cache;
- transitive invalidation;
- audit;
- health/readiness;
- backup/restore procedure;
- CLI doctor/integrity checks.
Критерий P0: пользователь может зарегистрироваться, войти, создать и править страницу; каждое изменение создаёт совместимую ревизию; история, diff, revert, права, include, поиск, tags и backlinks работают; XSS и private search leak не проходят; после удаления Meilisearch индекс полностью восстанавливается.
nav:*shell;- themes;
- components;
- theme editor с preview;
- ratings;
- comments;
- moderation;
- attachment versions;
- files module;
- page watch;
- page lock UI;
- P1 modules;
- blurhash;
- RSS/Atom;
- sitemap;
- JSON read API;
- admin panel;
- user/group/ACL management;
- failed jobs UI;
- search backlog UI.
- P2 modules;
- interwiki;
- Git export/import;
- drafts;
- page branches;
- watch notifications;
- Prometheus/OpenTelemetry;
- secure private-page search;
- write API;
- token auth;
- anti-spam;
- captcha adapter;
- external auth hooks;
- signed history checkpoints;
- пользовательские theme overrides, если они реально нужны.
Не появляются в P2:
- Redis;
- S3 primary storage;
- multi-node;
- multi-site;
- dynamic plugins.
Источник — RuFoundation на Django.
Используются:
- Django
dumpdata; - прямой read-only доступ к БД при необходимости;
- export API;
- файловое хранилище RuFoundation;
- код RuFoundation как спецификация поведения.
Родной Wikidot snapshot не является основным источником, если он теряет историю, голоса или авторство.
- users;
- logins;
- status;
- ranks;
- groups;
- memberships;
- pages;
- full revision history;
- source;
- titles;
- tags;
- page types;
- ACL;
- deleted/locked states;
- ratings;
- comments;
- attachments и versions;
- parent links;
- redirects;
- themes;
- components;
- nav pages;
- timestamps;
- authorship;
- external ids.
Пароли не переносятся. Для импортированных аккаунтов выполняется принудительная установка нового пароля через безопасный recovery flow.
- read source;
- normalize;
- map external ids;
- validate references;
- import users/groups;
- import pages;
- import content blobs;
- import revisions chronologically;
- import ACL versions и revision snapshots;
- import tags;
- import redirects/parents;
- import ratings/comments;
- import attachments;
- rebuild current counters;
- rebuild dependencies;
- enqueue search documents;
- reindex Meilisearch;
- verify checksums и sample pages.
Импорт:
- идемпотентен;
- resumable;
- batch-based;
- имеет dry-run;
- пишет подробный отчёт;
- не создаёт дубликаты;
- хранит ImportMapping;
- сохраняет исходные timestamps;
- сохраняет внешний revision id в import metadata;
- рассчитывает WikiNEXT chain hash поверх импортированной последовательности.
Минимум:
- случайная выборка не менее 100 страниц;
- все критичные theme/component/nav pages;
- страницы с глубокими include;
- страницы с нестандартными модулями;
- страницы с длинной историей;
- private pages;
- deleted pages;
- attachments;
- comments и ratings.
Проверяется:
- DOM-эквивалентность;
- видимые различия;
- ссылки;
- tags;
- ratings;
- comments;
- files;
- history;
- authors;
- ACL;
- повторный импорт;
- отсутствие search leak.
Один бинарник wikinext.
Основные команды:
wikinext serve
wikinext migrate
wikinext doctor
wikinext integrity verify
wikinext history verify
wikinext search reindex
wikinext search status
wikinext jobs status
wikinext jobs retry <id>
wikinext attachments reconcile
wikinext import rufoundation --config import.toml
wikinext import rufoundation --dry-run
wikinext serve запускает:
- HTTP server;
- SearchOutbox worker;
- durable job workers;
- periodic maintenance;
- graceful shutdown coordinator.
Отдельный worker process не нужен.
Минимальный production stack на одном сервере:
- reverse proxy с TLS;
wikinext;- PostgreSQL 18.4;
- Meilisearch 1.45.1;
- локальный
data/blobs; - backup storage.
Рекомендуемая изоляция:
- отдельные system users;
- Meilisearch слушает loopback/private interface;
- PostgreSQL не открыт публично;
- секреты находятся в защищённом env file/secret store;
- приложение не запускается от root;
- data directories имеют минимальные права;
- reverse proxy ограничивает body size и timeouts;
- systemd restart policy без бесконечного crash loop;
- disk space alerts;
- backup alerts.
Контейнеры допустимы, но не обязательны. Архитектура не зависит от Docker.
| Milestone | Scope | Exit criteria |
|---|---|---|
| M0 | workspace, AGPL, toolchain, CI, config, errors, logging, crate graph, PostgreSQL/Meili dev environment | clean build, CI green, migrations skeleton, сервисы проверяются doctor |
| M0-Compat | прототип ftml 1.41, unknown modules, real RuFoundation ACL resolver, FTML fork delta, nav inventory, module inventory, sample import | contracts прав и renderer зафиксированы тестами до основной реализации |
| M1 | users, sessions, Argon2id, login/register/logout, CSRF, rate limit, rank/groups/ACL | auth flow и точный resolver работают, session revoke проверен |
| M2 | pages, content blobs, revisions, metadata/ACL snapshots, tags snapshots, edit/history/diff/revert/delete/restore/rename | полный immutable revision cycle и integrity verify работают |
| M3 | ftml pipeline, Includer, P0 modules, sanitizer, dependency graph, AST/public render cache | разметка и includes безопасно рендерятся, transitive invalidation работает |
| M4 | tags UI, recent changes, backlinks, Meilisearch schema, SearchOutbox, reindex, sandbox, hardening | P0 завершён; поиск перестраивается с нуля и не раскрывает private pages |
| M5 | nav shell, themes, components, preview, ratings, comments | визуал и основной социальный функционал редактируются с сайта |
| M6 | attachments, versions, Files, watch, P1 modules, blurhash, RSS/sitemap | основной функционал Backrooms Wiki готов |
| M7 | admin panel, JSON read API, permissions UI, operations UI, production hardening | сайт управляется без ручного SQL, восстановление проверено |
| M-Import | полный RuFoundation reader/importer, dry-run, idempotency, DOM/data verification | данные полны, выборка эквивалентна, повторный импорт не создаёт дубликаты |
M0-Compat является блокирующим для M1 и M3 в соответствующих частях. Семантика прав не откладывается до конца проекта.
| Риск | Митигация |
|---|---|
| fork FTML RuFoundation расходится с upstream 1.41 | M0-Compat, golden tests, собственный compatibility layer |
| ftml ограниченно поддерживает modules | ModuleRegistry и ранний prototype неизвестных modules |
| тяжёлое дерево ftml | feature audit и cargo tree; отключение ненужных default features после проверки |
| AGPL ограничивает лицензию | AGPL-3.0-or-later с первого коммита, принято |
| ACL реализован неверно | resolver читается до M1 и фиксируется compatibility tests |
| include DoS | limits, deadline, cycle detection, bounded output |
| private data попадает в поиск | индекс только anonymous-public pages, security tests |
| Meilisearch недоступен | durable outbox, degraded search, полный reindex |
| Meilisearch version migration | индекс производный; новый index + alias swap вместо доверия внутреннему DB format |
| cache отдаёт чужой HTML | shared cache только для доказанно public dependency closure |
| DB и filesystem расходятся | atomic file placement, reconciliation, orphan cleanup, backups |
| revision chain создаёт ложное чувство защиты | явно ограниченная модель угроз; optional signed checkpoints |
| RuFoundation меняется во время разработки | фиксированный migration snapshot и versioned importer |
| single-node является точкой отказа | обязательные backup, PITR, blob snapshots и restore drills |
| имя WikiNEXT пересекается со старым проектом | выбор названия осознанно зафиксирован; перед публичной коммерциализацией отдельная проверка бренда |
- название — WikiNEXT;
- лицензия — AGPL-3.0-or-later;
- язык UI — только русский;
- один deployment = один wiki-сайт;
- один сервер и один app instance;
- Redis отсутствует;
- S3 отсутствует;
- PostgreSQL — source of truth;
- Meilisearch обязателен с P0;
- Meilisearch index перестраиваем;
- фоновые задания — PostgreSQL + embedded workers;
- вложения — локальный content-addressed filesystem;
- полная revision/meta/tag/ACL history сохраняется;
- динамические плагины отсутствуют;
- renderer заканчивается sanitizer;
- private pages не попадают в public search;
- права RuFoundation изучаются до реализации ACL;
- импорт должен быть идемпотентным.
- точная precedence прав RuFoundation;
- admin bypass;
- lock semantics;
- импорт групп и rank.
- точные сигнатуры ftml 1.41.0;
- поведение неизвестного module;
- serde стабильность SyntaxTree;
- нужный Layout;
- CSS/DOM compatibility profile;
- лимиты include.
- schema Meilisearch;
- ranking rules на реальном русском контенте;
- stop words/synonyms;
- public-page eligibility algorithm;
- outbox batch size.
- Django models и их версии;
- формат файлов;
- полный module inventory;
- nav pages;
- FTML fork delta;
- migration cutover procedure.
Эти пункты являются исследовательскими задачами с конкретными exit criteria, а не поводом оставлять архитектуру неопределённой.
Первый operational slice M0 содержит:
- фиксированный crate DAG и автоматическую проверку его направления;
- строгую TOML-конфигурацию с env overrides и редактированием секретов в Debug;
- команды
serve,migrate,doctor; - PostgreSQL pool и forward-only migration skeleton;
- отдельный Meilisearch adapter с проверкой версии;
- локальные data directories вне webroot;
/healthz,/readyz,/status/search;- request id, structured tracing, deadlines, security headers и graceful shutdown.
Локальный запуск и полный набор команд описаны в
docs/development.md.
Ограниченный renderer probe закрепляет FTML 1.41.0, Layout::Wikidot,
простой HTML/Text render, fallback неизвестных модулей и serde-поведение AST.
Результат и его границы описаны в
docs/compat/ftml-1.41.md; sanitizer и полный
production pipeline в этот прототип не входят.
Role/category precedence RuFoundation уже закреплён clean-room compatibility
truth table и исполняемыми тестами в wikinext-core. Результат описан в
docs/compat/rufoundation-acl.md.
Финальная ACL-модель WikiNEXT global → namespace → page остаётся открытой
задачей M1 и не подменяется этим compatibility resolver.