Тема
ТЗ — Ядро (cms/core) + API ядра
Слой: 🟢 ядро (всегда загружено, не выключается) · Статус: ТЗ к разработке Контракты подсистем ядра детально описаны в спеках фазы 0 — этот файл сводит поверхность ядра: подсистемы, полный REST API, группы настроек, компоненты, события. Пара к этому ТЗ — реестр критериев ядра: проверяемые критерии/инварианты/гипотезы, граница «ядро vs модуль» и пробелы ⬜, из которых собирается следующая ревизия контрактов. Новая возможность ядра — сначала строка в реестре, потом ревизия здесь, потом код.
Состав ядра (раздел A каталога)
| Группа | Подсистемы | Спека |
|---|---|---|
| Базовые механизмы | Kernel/DI, реестр модулей, события/фильтры, очереди, планировщик, кеш-слой, settings-store, движок полей, API-слой (REST), i18n строк, логи, файлы, медиатека, миграции данных, /local | три оси · performance · lifecycle |
| Контент-движок | Страницы+блоки (BlockRegistry), типы контента (JSONB+GIN), таксономии, черновики/ревизии, меню, виджеты, конструктор форм, заявки, Email | модель данных · блоки · виджеты · поля |
| Безопасность | Аутентификация, RBAC (Shield+spatie), Policies, rate-limit, защита форм, шифрование полей, согласия ПДн (база), фильтрация 5 границ входа | безопасность |
| SEO-база | Мета/шаблоны/canonical, sitemap, robots/редиректы, Schema.org/OG, ЧПУ | SEO |
| Редакторы | Сущностей, типов данных, файловый менеджер, пользователи, структура/разделы, меню, блоков (drag&drop), админ-shell+дашборд | subsystems §7a |
Контракты ядра для модулей (cms/core-contracts)
Модуль зависит только от интерфейсов — не от реализации ядра. Канонический набор:
| Контракт | Что даёт модулю |
|---|---|
BlockRegistry / WidgetRegistry / FieldTypeRegistry | Регистрация блоков, виджетов, типов полей |
PageTemplateRegistry | Регистрация шаблонов страницы (раскладок рендера): имя → подпись; значение живёт в cms_pages.template, вьюха — каскадом темы layouts/<name>.blade.php с fallback на дефолт (реализован волной A корп-MVP) |
RegionContext | Текущий город запроса (id/slug/название). Ядро биндит Null-реализацию («один город»); cms/multicity перекрывает живой — потребители (SEO, каталог, head-scripts) не зависят от модуля (реализован волной B корп-MVP; мастер-план §10.3) |
Enum GrammaticalCase + Cms\Core\Text\DeclensionEngine/HasDeclensions | 6 падежей и эвристика склонений (правила — config('cms.declensions.rules')); трейт даёт declensions jsonb на сущности с автозаполнением. Поднято из cms/services-seo волной B (мастер-план §10.4) |
SettingsStore | Чтение/декларация настроек (группа модуля, кеш групп) |
PageRepository / ContentRepository / TaxonomyRepository | Чтение контента ядра без raw SQL |
MediaService | Загрузка/конверсии/привязка файлов |
LeadService | Создание лида (форма/чат/UGC → заявка) |
FilterBus | Встраивание в конвейеры (канонические фильтры, приоритеты); события издаются/слушаются нативным Event + #[CmsListener] — отдельного EventBus-интерфейса нет (три оси) |
CacheTags | Объявление и инвалидация тегов кеша модуля |
RequestContext | Текущие site_id / locale / city_id / пользователь |
NotificationDispatch | Отправка через шину уведомлений (если модуль включён — иначе log-fallback) |
ScheduleRegistrar | Регистрация cron-задач (registerScheduleVia) |
SitemapRegistry | Декларативная регистрация URL-источников модуля в /sitemap.xml (ревизия 14.07.2026) |
RedirectService | Центральные 301/410-редиректы: смена URL любой сущности — через него (ревизия 14.07.2026) |
PrivacyRegistry | Регистрация обработчиков «выгрузить всё по субъекту» / «забыть по запросу»: модуль декларирует, какие свои данные экспортирует и как обезличивает; ядро агрегирует в cms:privacy:export / cms:privacy:forget --user= --dry-run (ревизия 14.07.2026) |
Каналы обмена и запреты — обмен данными.
Модель данных ядра (свод)
Эталонная схема и решения по хранению — модель данных; здесь свод для навигации (ядро владеет cms_*, модули в них не пишут):
| Группа | Таблицы | Ключевое |
|---|---|---|
| Страницы и ревизии | cms_pages, cms_page_revisions | published_revision_id (nullable) на странице; блоки — JSONB ревизии; lock_version — optimistic lock публикации (⬜ спроектировано, миграции пока нет — 2026_07_11_000002_create_cms_pages_table колонку не заводит) |
| Типы контента | cms_content_types, cms_content_entries | декларации полей; данные JSONB + GIN; slug уникален в типе |
| Связи типов контента (N:N) | cms_content_relations | ⬜ спроектирована, в БД не заведена — engine §5.2 |
| Навигация и виджеты | cms_menus/cms_menu_items, cms_widgets | дерево пунктов; область + условия показа + props JSONB |
| Настройки | cms_settings | group+key uniq, измерения locale/city nullable; донор universal 1:1 |
| SEO | cms_redirects (реализовано); cms_seo_meta/cms_seo_templates — ⬜ спроектировано, таблиц нет | редиректы — таблица RedirectService (from uniq, hit-счётчик); SEO-мета временно живёт в таблицах модуля cms/services-seo (cms_services_seo_meta_overrides и др.), консолидация в ядровые cms_seo_meta/cms_seo_templates не проведена |
| Формы и лиды | cms_forms, cms_leads | декларации движка полей; payload JSONB, consent_at, status Enum |
| Медиа | media (spatie/medialibrary) | конверсии, коллекции; удаление — с проверкой использования в ревизиях |
| Служебные | cms_module_versions, cms_module_history, cms_block_usage (mat. view), cms_privacy_requests | версии/журнал обновлений; карта блоков для data-миграций; журнал запросов субъектов ПДн (PrivacyRegistry) |
Все cms_* следуют спекам моделей: $casts на каждый JSONB/enum/date, FK constrained() + index(), измерения site_id/locale/city_id nullable там, где сущность контентная.
Контекст запроса (site / locale / city)
Middleware ядра резолвит RequestContext до контроллеров: сайт (при cms/multisite), локаль (цепочка резолва), город (при cms/multicity), пользователь. Модули не парсят URL/заголовки сами — берут контекст; ядро автоматически включает эти измерения в ключи кеша и в скоупы репозиториев.
Порядок резолва зафиксирован (ревизия 14.07.2026): tenant (при cms/multitenancy) и site резолвятся до обращения к page-cache — ключ page-cache всегда включает tenant/site/locale/city; попадание в чужой кеш исключено конструктивно, а не дисциплиной модулей.
Жизненный цикл публичного запроса
запрос → page-cache (hit → ответ без БД) → RequestContext → маршрутизация (ЧПУ)
→ страница + опубликованная ревизия → рендер блоков (BlockRegistry, barrier:
сбой блока → fallback, не 500) → SEO-резолвер (мета/Schema.org)
→ ответ + теги кеша (страница, области, использованные сущности)Персонализация и page-cache (ревизия 14.07.2026). Два штатных режима, самодельная сегментация в модулях запрещена:
- блочный — блок объявляет себя
personalizedв контракте BlockRegistry (сегмент feature-flag, содержимое корзины): кешируется каркас страницы, персонализированный фрагмент рендерится отдельно (островом на клиенте или сегментированным фрагмент-кешем ядра); - полностраничный — модуль декларирует дополнительное измерение ключа page-cache для конкретных страниц (вариант A/B-теста, сегмент кампании) через механизм сегментации ядра — по аналогии со встроенными измерениями
tenant/site/locale/city; число значений измерения ограничено (защита от взрыва кардинальности кеша).
API ядра (REST, /api/v1)
Общие правила — API-контракт: единый конверт {data, meta} / ошибки {error: {type, code, message, fields, doc_url}}, Sanctum-токены с abilities, rate-limit per-граница, версия в пути. Публичные эндпоинты читают опубликованный контент; /admin/* — под токеном с ability и permission.
Конвенции, обязательные и для API модулей (модуль расширяет API в своём неймспейсе /api/v1/<slug>/… и /api/v1/admin/<slug>/…):
- конверт:
{"data": …, "meta": {…}}; коллекции — keyset-пагинация (meta.next_cursor, параметрcursor=),OFFSET-пагинация в API запрещена; - конверт ошибок — по API-контракту:
{"error": {"type": …, "code": …, "message": …, "fields": {…}, "doc_url": …}}; полеcode— стабильная машиночитаемая строка (token_expired,rate_limited,version_conflict), клиенты ветвятся поcode, не по текстуmessage(подтверждено ревизией 14.07.2026); - фильтры
filter[поле]=, сортировкаsort=-created_at— строго по whitelist FormRequest; неизвестный параметр → 422, не игнор; - коды: 401 нет токена · 403 нет права · 404 нет ресурса/не опубликован · 409 конфликт версии (optimistic lock) · 422 валидация · 429 rate-limit (+
Retry-After); - деградация публичного чтения — не 5xx: если взаимозаменяемый провайдер (поиск, подсказки, курсы) недоступен, эндпоинт отвечает
200с fallback-данными или пустымdataи флагомmeta.degraded: true(+meta.degraded_reasonдля логов); 5xx допустим только при отказе самого ядра; - мутации с денежным/внешним эффектом (включая выпуск токенов и ключей) — заголовок
Idempotency-Key; - депрекация: поле/эндпоинт помечается в OpenAPI + заголовок
Deprecation, удаление — только в мажоре ядра; - persistent-роуты (ревизия 14.07.2026, п. 14): модуль может пометить комплаенс-роут
persistent— он остаётся жив при выключенном модуле, запросы складываются в очередь/журнал и применяются при включении (обязательный кейс — One-Click Unsubscribe RFC 8058, отзыв согласия); - каждый эндпоинт покрыт OpenAPI-атрибутами (автоспека) и feature-тестом.
Аутентификация и пользователи
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| POST | /api/v1/auth/token | публич. (строгий rate-limit) | Выдача Sanctum-токена (email+пароль → token c abilities) |
| DELETE | /api/v1/auth/token | токен | Отзыв текущего токена |
| GET | /api/v1/auth/me | токен | Профиль текущего пользователя, роли, abilities |
| GET/PUT | /api/v1/admin/users, /users/{id} | users.manage | CRUD пользователей, назначение ролей |
Страницы, ревизии, preview
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| GET | /api/v1/pages | публич. | Список опубликованных страниц (фильтры: раздел, тип; пагинация) |
| GET | /api/v1/pages/{slug} | публич. | Страница + разрешённые блоки опубликованной ревизии |
| GET | /api/v1/preview/{token} | подписанный токен | Preview черновой ревизии (не индексируется) |
| GET/POST/PUT/DELETE | /api/v1/admin/pages… | pages.manage | CRUD страниц (черновик-ревизия) |
| GET | /api/v1/admin/pages/{id}/revisions | pages.manage | История ревизий |
| POST | /api/v1/admin/pages/{id}/revisions/{rev}/publish | pages.publish | Публикация ревизии |
| POST | /api/v1/admin/pages/{id}/revisions/{rev}/rollback | pages.publish | Откат на ревизию |
| PUT | /api/v1/admin/pages/{id}/blocks | pages.manage | Сохранение состава блоков черновика (валидация схем по BlockRegistry) |
Типы контента и записи
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| GET | /api/v1/content/{type} | публич. | Записи типа (фильтры по полям JSONB — whitelist, GIN) |
| GET | /api/v1/content/{type}/{slug} | публич. | Одна запись |
| CRUD | /api/v1/admin/content/{type}… | content.{type}.manage | Управление записями |
| CRUD | /api/v1/admin/content-types… | studio (конфигурируемо настройкой content-types.type_management_role — engine §11) | Управление самими типами (декларации полей) |
| GET | /api/v1/content-types/{type}/schema | публич. | JSON Schema полей типа (движок полей → JSON Schema) — интегратор видит структуру без чтения кода (ревизия №2, п. 3) |
| GET | /api/v1/taxonomies/{tax}/terms | публич. | Термины таксономии (дерево) |
Публичные list/detail-роуты типов с включённым seo_enabled регистрируются в SitemapRegistry по событию ContentTypeSaved — тип попадает в /sitemap.xml без ручного добавления источника; детали резолва схемы и роутинга — engine, «Маршрутизация list/detail».
Меню, виджеты, темы
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| GET | /api/v1/menus/{slug} | публич. | Дерево меню (headless-рендер) |
| CRUD | /api/v1/admin/menus… | menus.manage | Управление меню |
| CRUD | /api/v1/admin/widgets… | widgets.manage | Виджеты: тип, область, условия показа, is_active |
| GET | /api/v1/admin/themes | studio-роль | Список тем + манифесты |
| POST | /api/v1/admin/themes/{name}/activate | studio-роль | Активация темы (preflight → ThemeChanged) |
Медиа
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| POST | /api/v1/admin/media | media.upload | Загрузка (MIME-whitelist, лимиты, re-encode изображений; при включённой реализации upload-scanner файл до вердикта — в карантине pending, ревизия №2 п. 4) |
| GET | /api/v1/admin/media | media.view | Поиск/список, коллекции |
| DELETE | /api/v1/admin/media/{id} | media.manage | Удаление (с проверкой использования в ревизиях) |
Формы и заявки
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| GET | /api/v1/forms/{slug} | публич. | JSON-схема формы (для headless-рендера) |
| POST | /api/v1/forms/{slug}/submit | публич. (rate-limit, honeypot, CSRF/капча) | Отправка → лид; consent_at обязателен при сборе ПДн |
| GET/PUT | /api/v1/admin/leads… | leads.manage | Просмотр/статусы заявок |
| CRUD | /api/v1/admin/forms… | forms.manage | Конструктор форм (декларации движка полей) |
Настройки, система, служебное
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| GET/PUT | /api/v1/admin/settings/{group} | settings.manage | Чтение/запись группы настроек (секреты не хранятся — только .env) |
| GET | /api/v1/system/health | health-токен | Агрегат health-чеков ядра + модулей (для мониторинга/Caddy) |
| GET | /api/v1/admin/system/map | studio-роль | Машинная карта: модули, версии, зависимости, роуты (= cms:map --json) |
| GET | /api/v1/search | публич. | Поиск по контенту ядра (PG tsvector; модуль Поиск заменяет провайдера) |
| CRUD | /api/v1/admin/redirects… | redirects.manage | Ручные 301/410 + просмотр автосозданных (RedirectService); hit-счётчик, поиск петель |
| GET | /sitemap.xml, /robots.txt | публич. | SEO-база (вне /api, генерирует ядро) |
Группы настроек ядра (settings-store)
| Группа | Примеры ключей | affectsPageCache |
|---|---|---|
site | название, логотип, контакты, телефон, режим обслуживания | ✅ |
seo | шаблоны мета по типам, суффикс title, счётчик robots | ✅ |
media | пресеты конверсий, webp/avif, лимиты загрузки | — |
forms | капча вкл/провайдер, честный текст согласия ПДн | ✅ |
cache | TTL page-cache, исключения путей, debug-token | — |
mail | транспорт (не секреты), from-имя/адрес | — |
locale | локали сайта, дефолт, fallback-цепочка | ✅ |
security | режим CSP (off/report-only/enforce), отчётный endpoint, SRI вкл/выкл (ревизия №2, п. 5) | ✅ |
system | пороги диска для health (disk_warn_percent=15, disk_critical_percent=5), ретеншн ревизий (ревизия №2, п. 2) | — |
content-types | type_management_role (studio/admin — кто создаёт типы), max_types, max_indexed_properties_total (лимиты — защита общей таблицы cms_content_entries от неограниченного числа индексируемых свойств) — engine §11 | — |
Компоненты ядра
- Блоки (BlockRegistry): hero, текст, изображение/галерея, CTA, форма (по slug), колонки/контейнер, секция (варианты оформления), HTML (доверенный, studio-роль), designed-секция (доверенный HTML с плейсхолдерами
, значения экранируются; studio-роль), меню-якоря — стартовый набор темы; репитер-блоки общего назначения (волна A корп-MVP): преимущества (features), шаги (steps), карточки/CTA-сетка (cards), аккордеон, табы (CSS-only), таблица, статичные отзывы (reviews_inline) — декларативные схемы на repeater-полях движка, barrier-safe; content-list (волна C, engine §6) — список записей выбранного типа контента на произвольной странице (тип/сортировка/лимит/ вид карточки; barrier-safe при неизвестном типе); - Виджеты: меню, контакты, соцсети, произвольный текст;
- Filament: ресурсы страниц/типов/меню/виджетов/медиа/пользователей/лидов, страница настроек по группам, дашборд (заявки, здоровье), редактор блоков drag&drop;
- Команды:
cms:doctor,cms:map,cms:hooks,cms:cache:inspect,cms:theme:resolve,cms:blocks:migrate,cms:postupgrade,cms:content:cleanup-orphans(уборка осиротевших связей типов, волна C;cms:content:migrate {type}— к реализации вместе с ломающими миграциями схем),cms:privacy:export/cms:privacy:forget --user= --dry-run(агрегация поPrivacyRegistry, п. 15 ревизии) — все с--json; восстановительные команды идемпотентны и допустимы на живом сайте.
События ядра
Канон — три оси: PageSaved/PagePublished, ContentEntrySaved, LeadCreated, MediaUploaded, SettingChanged, MenuSaved, WidgetSaved/WidgetDeleted, ThemeChanged, ModuleEnabled/ModuleDisabled, UserRegistered, CacheFlushRequested.
Расширение канона (ревизия 14.07.2026, по итогам углубления ТЗ модулей):
- события удаления:
PageDeleted,ContentEntryDeleted,MediaDeleted,UserDeleted,MenuDeleted— без них невозможны purge CDN, деиндексация поиска, чистка рекомендаций и ПДн-каскад «забыть по запросу»; payload — облегчённый (id + slug/путь удалённого), не полный объект: сущности уже нет; - события аутентификации:
UserLoggedIn,UserLoggedOut,UserPasswordChanged— канонические факты канала 1 (ядро транслирует framework-события в свои DTO); потребители: session, audit, attack-monitor, two-factor — слушают их, а неIlluminate\Auth\Events\*напрямую; UserMerged(primary_user_id, secondary_user_id)— слияние аккаунтов; потребители переносят свои данные (баллы, заказы, согласия) со вторичного на первичный;LeadStatusChanged(lead, from, to)— смена статуса заявки: двусторонняя синхронизация с CRM и триггеры маркетинга без опроса таблицы лидов;- payload смены адреса:
PagePublished(иContentEntrySavedпри смене slug) несётprevious_slug(nullable) — потребители (cdn, seo-редиректы) делают purge и 301 старого URL без самодельного диффа.
Права ядра
pages.view/manage/publish · content.{type}.view/manage · menus.manage · widgets.manage · media.view/upload/manage · forms.manage · leads.manage · settings.manage · users.manage · redirects.manage · studio-роль (темы, типы, HTML-блок, произвольные regex, privacy-операции cms:privacy:*, карта системы /system/map).
content.{type}.view гейтит просмотр записей типа в админке (read-only роли, генерик- ресурс ContentEntryResource в режиме чтения); публичные GET-эндпоинты (/api/v1/content/{type}…) остаются без прав — они читают опубликованный контент как и раньше. ⬜ В коде RbacSynchronizer::desiredPermissions() пока генерирует только content.{type}.manage на каждый тип — .view заводится доработкой при реализации движка типов контента (engine §11).
Безопасность ядра (свод)
Полная модель — безопасность; контрольные точки ядра:
- 5 границ входа фильтруются стандартизированными обработчиками ядра: формы (rules движка полей → FormRequest), админка (FormRequest + Policy), API (whitelist фильтров/сортировок, 422 на неизвестный параметр), файлы (MIME-whitelist, re-encode изображений, лимиты), вебхуки (подпись, только через
cms/webhooks-in). - Rich-text — двойной барьер: санитизация на сохранении и на выводе;
{!! !!}— только HTML-блок под studio-ролью. Пользовательские regex — через ReDoS-валидатор. - Шифрование полей:
encrypted-cast для чувствительных столбцов; секреты — только.env→config(), settings-store их не принимает (валидация схемы настройки). - Sanctum-токены: abilities ∩ permissions (двойная проверка), строгий rate-limit на
/auth/token, отзыв всех токенов при смене пароля (UserPasswordChanged). - Preview — только подписанный токен с TTL,
noindex; черновики недоступны без него. - ПДн:
consent_atна границе форм; каскады «выгрузить/забыть» —PrivacyRegistry(п. 15); ПДн и секреты не попадают в логи и телеметрию. - CSP/SRI — дефолт ядра (ревизия №2, п. 5): middleware ядра отдаёт CSP с nonce для инлайн-вставок; источники собираются каноническим фильтром
security.csp(тема декларирует свои вtheme.json, модули-вставщики — pixels, analytics, integration-* — добавляют фильтром); внедрение — черезreport-only, перевод вenforce— осознанное действие; SRI — на ассеты темы из манифеста сборки. - Скан загрузок (ревизия №2, п. 4): медиатека резолвит
upload-scanner(provides-контракт) при каждой загрузке; вердикт «заражён» → файл в карантин + событие + алерт; 0 реализаций — скан пропускается (деградация штатна, managed-парку реализация рекомендована для UGC-сайтов).
UX-требования админки ядра
- Пустые состояния каждого ресурса — подсказка «что нажать» (нет страниц → «Создать страницу», нет форм → ссылка на конструктор), не пустая таблица.
- Конфликт 409 (optimistic lock) — диалог «страница изменена другим пользователем: обновить / сравнить / перезаписать», не немой сбой сохранения.
- Массовые действия на списках: публикация/снятие, смена раздела, удаление с подтверждением и числом затронутых.
- Ошибки — на человеческом языке (аудитория «админ» из lifecycle): «что случилось и что нажать», код — в раскрываемых деталях для поддержки.
- Необратимое (удаление страницы с ревизиями, медиа, пользователя) — подтверждение с перечнем последствий (где используется, что станет с данными).
- Инвалидация кеша после
SettingChanged— индикатор «кеш обновится в течение N с», не мгновенная блокировка интерфейса. - Вся админка мультиязычна (
lang/), доступна с клавиатуры; drag&drop-редактор имеет клавиатурную альтернативу порядка блоков.
Эксплуатация ядра (ранбук)
- Метрики (Pulse): p95 ответа публичных страниц, hit-rate page-cache, глубина очередей, частота сбоев слушателей/фильтров; суточный пинг телеметрии — в центр обновлений.
- Типовые инциденты: 500 после деплоя →
cms:doctor --json+cms:postupgrade; устаревшие страницы →cms:cache:inspect(теги, последняя инвалидация); очередь встала → воркер/queue:retry, отставание видно в health; блок «упал» → barrier уже отдал fallback, смотреть отчёт рендера в логе каналаcms. - Бэкап/рестор: в бэкап входят все
cms_*,mediaи.env; после рестора обязательныcms:postupgrade(кеши,cms_block_usage) и переиндексация модулей поиска — рестор без них не считается завершённым. - Данные ядра ретеншнятся: ревизии страниц — политика хранения N последних (настройка),
cms_module_historyи журнал редиректов — партиции/очистка по возрасту. - Диск под контролем (ревизия №2, п. 2): встроенный health-чек ядра — свободное место на дисках БД/медиа/логов; warn при ≤
system.disk_warn_percent, critical при ≤system.disk_critical_percent(алерт до падения БД, не после). Расширенные чеки (inode, прогноз роста, тренды) — зонаcms/health. - Staging-клон (ревизия №2, п. 6): проверка обновления перед продом — клон сайта командой
cms/backup(cms:backup:clone-to-staging): рестор бэкапа в staging-окружение- обязательные трансформации (домен,
robotsnoindex, включение kill-switch всех модулей с внешними эффектами — почта, вебхуки, пиксели, платежи — по стандарту §6 kill-switch обязателен именно поэтому). Клон без глушения внешних эффектов — инцидент (staging рассылает письма клиентам).
- обязательные трансформации (домен,
Производительность ядра (бюджеты — контрактные тесты)
| Горячий путь | Бюджет |
|---|---|
| Публичная страница, page-cache hit | 0 запросов к БД |
| Публичная страница, page-cache miss | ≤ 15 запросов (страница+ревизия+блоки+меню+виджеты+SEO; бюджет — performance), без N+1 |
| Чтение группы настроек на горячем пути | 0 запросов (кеш группы) |
| Админ-список любого ресурса | keyset, ≤ 15 запросов, счётчики без COUNT(*) по всей таблице |
/api/v1/system/health | ≤ 2 с при таймауте каждого чека ≤ 500 мс |
Инвалидация — только тегами по событиям; Cache::flush() в ядре и модулях запрещён.
Крайние случаи ядра (инварианты поведения)
- Параллельная публикация одной страницы двумя админами → optimistic lock по ревизии, второй получает 409 и диалог «обновить и повторить», не молчаливую перезапись.
- Откат на ревизию, ссылающуюся на удалённое медиа → страница публикуется, битые медиа-блоки рендерят fallback (barrier), в админке — предупреждение со списком.
- Выключение модуля во время запросов «в полёте» → текущие запросы дорабатывают со старым составом реестров; новые запросы видят fallback блоков/виджетов модуля.
SettingChangedсaffectsPageCache→ инвалидация page-cache батчем по тегу, не синхронно в запросе админа; админ видит «кеш обновится в течение N секунд».- Страница из 100+ блоков → рендер укладывается в бюджет за счёт кеша областей и фрагментов; редактор предупреждает о превышении рекомендуемого размера.
- Сущность без измерений (сайт без multisite/multicity/локалей) → nullable-измерения, один и тот же код-путь; контрактный тест ядра гоняет оба режима.
- Slug-коллизия (страница ↔ тип контента ↔ модульный роут) → детерминированный приоритет: точный роут ядра → модульный префикс → страница; конфликт при сохранении ловится валидацией, а не 500 в рантайме.
- Сбой слушателя события → изолируется (report + продолжение потока), бизнес-операция издателя не откатывается; сбой фильтра FilterBus — пропуск фильтра с алертом.
- Удаление медиа, используемого в опубликованной ревизии → блокируется с перечнем страниц («используется на N страницах»); форс — только с подтверждением, блоки переходят на fallback (barrier).
- Истёкший preview-токен → страница «ссылка предпросмотра устарела, запросите новую» (не 403 без объяснений и не показ черновика).
- Смена slug опубликованной страницы → автоматический 301 через
RedirectService(поprevious_slug), petля A→B→A схлопывается сервисом до одного прыжка. cms:privacy:forgetпо пользователю с заказами/финансовыми записями → обезличивание ПДн-полей вместо удаления строк (append-only не нарушается); обработчик каждого модуля сам решает «удалить или обезличить», ядро агрегирует отчёт.- Два воркера очередей обрабатывают одно событие (ретрай после таймаута) → слушатели ядра идемпотентны; инвалидация кеша повторно — no-op.
- Redis недоступен (ревизия №2, п. 1) → сайт живёт без кеша: кеш-слой падает на null-store с флагом
degraded(страницы рендерятся с miss-бюджетом, медленнее — не 500), rate-limit деградирует на database-драйвер, health красный по чеку redis. Сессии админки Redis не требуют (database-драйвер по умолчанию — осознанно ради этого инварианта). Проверка — chaos-тест вcms/testing: поднять playground, убить Redis, публичные страницы и вход в админку живы.
Ревизия контрактов 14.07.2026 (сводка для модулей)
Изменения этого файла по итогам углубления ТЗ модулей (v2.1/v2.2); ТЗ с пометками «⚠️ Противоречие» приводить в соответствие с этой ревизией:
| # | Изменение | Потребители |
|---|---|---|
| 1 | Конверт ошибок: обязательное машиночитаемое поле code (форма конверта — по API-контракту) | api-tokens, mobile-api, все клиенты API |
| 2 | Конвенция деградации публичного чтения: 200 + meta.degraded, не 5xx | search, dadata, cbr-rates, любые provides-провайдеры |
| 3 | События удаления PageDeleted/ContentEntryDeleted/MediaDeleted/UserDeleted | cdn, search, related-content, ПДн-каскады (consents) |
| 4 | События аутентификации UserLoggedIn/UserLoggedOut/UserPasswordChanged | session, audit, attack-monitor, two-factor |
| 5 | previous_slug в payload PagePublished/ContentEntrySaved | cdn, SEO-редиректы |
| 6 | Порядок резолва: tenant/site → page-cache; измерения в ключе кеша | multitenancy, multisite, multicity |
| 7 | Флаг personalized в контракте блока; сегментация — только механизмом ядра | feature-flags, ab-testing, commerce-cart, кабинеты |
| 8 | Idempotency-Key распространён на выпуск токенов/ключей | api-tokens |
| 9 | Реестр provides пополнен: geo-provider, import-target; резолв дисков медиатеки — через существующий storage-backend | session, multicity, import, storage-s3 |
| 10 | SitemapRegistry в cms/core-contracts: модуль декларативно регистрирует свои URL-источники для /sitemap.xml (по аналогии с BlockRegistry) | services-seo, seo-filter, blog, multicity, landings |
| 11 | Канонический фильтр FilterBus seo.meta — единая точка, где модули дописывают/переопределяют мета страницы | services-seo, seo-filter, multicity, ab-testing |
| 12 | Центральный сервис редиректов ядра (RedirectService): «изменён URL → 301» доступен модулям как сервис, а не самописные таблицы | faq, landings, seo-engine, multicity |
| 13 | Событие UserMerged(primary_user_id, secondary_user_id) — слияние аккаунтов (гостевой → зарегистрированный, дубли) | loyalty, referral, кабинеты, consents, commerce-orders |
| 14 | Комплаенс-роуты: модуль может пометить роут persistent — он жив и при выключенном модуле (обработка в очередь/лог); обязательный кейс — One-Click Unsubscribe (RFC 8058) | newsletter, email-marketing, consents |
| 15 | PrivacyRegistry в cms/core-contracts + команды cms:privacy:export/forget --user= --dry-run: единый каскад «выгрузить/забыть» по всем модулям (152-ФЗ) | consents и каждый модуль с ПДн-паспортом |
| 16 | Событие LeadStatusChanged(lead, from, to) — синхронизация статусов заявок | integration-crm, email-marketing, analytics |
Ревизия контрактов №2 (14.07.2026): закрытие пробелов реестра
Первый плановый цикл механизма «реестр критериев → ревизия → код»: закрыты все накопленные пробелы ⬜ (2.8, 2.9, 4.7, 6.8, 6.9, 8.8):
| # | Пробел | Решение | Потребители |
|---|---|---|---|
| 1 | 2.8 chaos без Redis | Инвариант «Redis недоступен → сайт живёт без кеша»: null-store + degraded, rate-limit на database-драйвер, сессии в БД по умолчанию; chaos-тест в cms/testing | ядро, health, все модули с кешем |
| 2 | 2.9 диск | Встроенный health-чек free-space с порогами system.disk_warn_percent / disk_critical_percent; расширенные чеки — cms/health | health, backup, sentry |
| 3 | 4.7 схема типов | GET /api/v1/content-types/{type}/schema — JSON Schema полей типа из движка полей | mobile-api, integration-*, headless-фронты |
| 4 | 6.8 скан загрузок | Новый provides-контракт upload-scanner (реестр §2 стандарта); медиатека: карантин pending до вердикта, 0 реализаций = скан пропускается; модуль-реализация (ClamAV) — кандидат каталога | медиатека ядра, ugc, moderation |
| 5 | 6.9 CSP/SRI | CSP-middleware ядра (nonce, режимы off/report-only/enforce, группа настроек security), канонический фильтр security.csp для источников модулей, SRI на ассеты темы | pixels, analytics, integration-yandex/google, темы |
| 6 | 8.8 staging-клон | cms:backup:clone-to-staging в cms/backup: рестор + трансформации (домен, noindex, kill-switch всех внешних эффектов — §6 стандарта) | backup, updates (проверка обновлений) |
| 7 | дрейф реестра provides | Легализованы контракты, фактически объявленные ТЗ, но отсутствовавшие в реестре §2: cabinet-shell, domain-management, fleet-monitoring, marketplace-catalog, two-factor-auth, ugc-publication, update-client (найдено новым CI-скриптом scripts/check-deps.mjs; впредь дрейф ловится им автоматически) | cabinet-b2c/b2b, domains, fleet-dashboard, marketplace-modules, two-factor, ugc, updates |
Критерии приёмки
- [ ] Контрактные тесты
cms-testingзелёные (блоки, поля, API-конверт, бюджет запросов); - [ ] все публичные границы валидируют вход (FormRequest, whitelist сортировок/фильтров);
- [ ] page-cache отдаёт страницу без запросов к БД; инвалидация тегами по событиям;
- [ ]
cms:doctor --jsonдиагностирует ядро без модулей; playground-профиль сидится; - [ ] OpenAPI-спека генерируется из кода и покрывает все таблицы этого файла;
- [ ] все 16 пунктов ревизии 14.07.2026 реализованы и покрыты контрактными тестами (
codeв ошибках,meta.degraded, события удаления/аутентификации/UserMerged/LeadStatusChanged,previous_slug, порядок tenant/site → page-cache,personalized,SitemapRegistry,RedirectService,PrivacyRegistry, persistent-роуты); - [ ] все 6 пунктов ревизии №2 реализованы и покрыты тестами (chaos-тест без Redis, health-чек диска, schema-endpoint типов, карантин
upload-scanner, CSP report-only→enforce + фильтрsecurity.csp, staging-клон с глушением внешних эффектов); - [ ] каждый инвариант из «Крайних случаев ядра» — отдельный тест (409 при параллельной публикации, fallback битого медиа, изоляция сбоя слушателя, slug-коллизии, обезличивание при
privacy:forget); - [ ]
cms:privacy:export/forget --dry-runагрегируют по всем зарегистрированным обработчикам и печатают отчёт без записи; - [ ] UX-требования админки проверены на playground: пустые состояния, диалог 409, подтверждения необратимого, индикатор инвалидации кеша.