Тема
ТЗ — Библиотека типовых секций (cms/section-patterns)
Слой: 🟡 функц-модуль · Зрелость доноров: ★ · Донор: studio-core (черновой UX модалки конструктора студии; готового кода/данных для переноса нет) Статус: ТЗ к разработке
Назначение и возможности
Библиотека именованных наборов блоков поверх BlockRegistry ядра — аналог Gutenberg block patterns / готовых секций конструкторов Tilda и Elementor. Даёт редактору «конструктор главной с большими возможностями» без права писать код: вместо сборки секции блок за блоком он вставляет готовый набор блоков одной кнопкой и заполняет несколько плейсхолдеров, либо сохраняет свою удачную секцию как переиспользуемый пресет, либо (роль studio) заводит пресет из HTML, свёрстанного в claude.ai/design.
Важно с первой строки: cms/section-patterns — это не блок (не добавляет новый тип в BlockRegistry) и не шаблон страницы целиком (этим занимается cms/landings — там пресет описывает всю посадочную страницу с её жизненным циклом публикации; здесь — только фрагмент, который редактор вставляет в произвольном месте конструктора любой страницы). Модуль лишь генерирует поддерево блоков в момент вставки — дальше это обычные блоки страницы, никакой «живой связи» с исходным пресетом не остаётся (см. «Крайние случаи»).
- Каталог системных секций по категориям (готовят модули: «hero + слайдер», «сетка каталога», «сетка услуг», «портфолио/галерея», «отзывы», «блок заявки», «SEO-текст + FAQ» и т.д.) — без единой строки хардкода в редакторе: категории и содержимое приходят из реестра;
- вставка секции в конструктор страницы с формой заполнения плейсхолдеров (
) — предпросмотр по категориям с миниатюрой (preview_media_id); - сохранение выделенной секции конструктора как пользовательского пресета (с разметкой, какие значения станут плейсхолдерами при повторном использовании);
- импорт свёрстанного в claude.ai/design HTML как пресета-секции с картой плейсхолдеров — доступно только роли studio (доверенный HTML, см. «Безопасность»);
- экспорт/импорт пресета JSON-файлом (перенос секции между инсталляциями/проектами студии);
- «data-bound» секции — пресеты, включающие блок, который сам умеет получать данные (сетка каталога
cms/commerce-catalog, блок отзывовcms/reviewsи т. п.) — это не новый контракт ядра, а обычныйBlockType, уже способный к самостоятельному рендеру данных по своему пайплайну (контракт блока);section-patternsпросто включает такой блок в поддерево пресета, как и любой другой.
🔄 Ревизия (корпоративный MVP, 15.07.2026). Частный случай data-bound секции — переиспользуемый список записей (слайдер, лента статей, сетка отзывов и т. п.): такие секции используют штатный блок
content-listдвижка типов контента (engine §6), а не собственный механизм вывода списков. Порог тот же, что и у движка в целом: переиспользуемый управляемый набор — тип контента и блокcontent-listнад ним; разовый презентационный список внутри конкретной секции — repeater-поле блока, не тип контента (граница гибрида — engine §1, таблица «на движке / вне движка»).
Зависимости и выключение
requires: ядро (cms/core-contracts: BlockRegistry, FieldTypeRegistry/движок полей, MediaService — превью пресета, RequestContext — локаль/город при материализации) · suggests: cms/landings (потребитель — вставка секции внутрь лендинга тем же действием конструктора) · suggests: cms/audit (запись импорта доверенного HTML в аудит-лог, если включён).
Направление зависимости с модулями-донорами секций — обратное. cms/section-patterns не требует ни cms/banners, ни cms/commerce-catalog, ни cms/reviews и т. п. Это ОНИ, если установлены, регистрируют свои системные секции в SectionPatternRegistry из собственного boot() (через свой suggests: cms/section-patterns). Если модуль-донор секции выключен — section-patterns ничего не знает и не падает: его системная секция просто не появляется в реестре при следующей синхронизации (см. «Крайние случаи»). Никакого нового provides-контракта ядра модуль не вводит: реестр — это точка расширения самого section-patterns, а не канонический контракт ядра из общего реестра (§2 стандарта).
Поведение при выключении модуля: действие «Вставить секцию из библиотеки» и «Сохранить как пресет» пропадают из конструктора страницы (fallback — обычная сборка блок за блоком, как без модуля вообще). Данные (cms_section_patterns_patterns) не удаляются. Ключевой момент инварианта №1 стандарта: секции, уже вставленные ранее, — это обычные блоки в JSONB ревизии страницы, они не хранят ссылку на пресет и продолжают рендериться штатным пайплайном BlockRegistry независимо от состояния section-patterns — выключение модуля не может испортить ни одну существующую страницу.
Модель данных
| Таблица | Ключевые поля | Примечание |
|---|---|---|
cms_section_patterns_patterns | id, slug, title, category, preview_media_id (nullable), schema (jsonb), source (enum: system|user|designed), is_active, sort, insert_count, lock_version, locale (nullable), city_id (nullable), site_id (nullable), external_id (nullable) | пресет секции: поддерево блоков + карта плейсхолдеров |
schema (jsonb) хранит два значения одним документом: blocks — поддерево блоков той же формы, что и cms_page_revisions.blocks (контракт блока), с плейсхолдерами вместо конкретных строковых значений в props; и placeholders — декларация полей заполнения (key, label, type, default nullable), по которой Filament строит форму «заполните секцию» при вставке. category — не PHP Enum, а строковый идентификатор категории, объявленной модулем в SectionPatternRegistry (категории — открытый список, не хардкод палитры). source:
system— заведён кодом модуля-донора через реестр, синхронизирован в таблицу командойcms:section-patterns:sync-system(см. «Фоновая работа»);schema/title/categoryэтой строки не редактируются через UI (владение — код), редактируется толькоis_active/sort;user— редактор сохранил выделение конструктора как пресет; полностью редактируем в рамках правsection-patterns.manage;designed— импортирован доверенный HTML из claude.ai/design; создаётся только подsection-patterns.trusted-import(см. «Безопасность»).
slug — уникален (upsert-ключ синхронизации системных пресетов); category+is_active — составной индекс (выборка библиотеки по категории для модалки); preview_media_id — nullOnDelete() (удаление медиа не рушит пресет, просто пропадает превью); external_id — уникален в пределах таблицы, только для legacy-профилей (§16 стандарта, здесь не применимо — см. «Донорский код»); lock_version — optimistic lock (§4 стандарта) на конкурентную правку пользовательского пресета; insert_count — денормализованный счётчик популярности (атомарный UPDATE ... SET insert_count = insert_count + 1, инкремент вне транзакции вставки — см. «Фоновая работа»), не участвует в бизнес-логике, только сортировка «популярные сверху».
Измерения locale/city_id/site_id — nullable по правилу §4 стандарта: пресет без явного измерения виден во всех локалях/городах/сайтах сети; студийные системные пресеты всегда без измерений (общие для всей инсталляции), пользовательские/designed — редактор опционально ограничивает пресет своим сайтом/локалью (мультисайт-инсталляция студии, где секции одного клиента не должны всплывать в библиотеке другого). Контрактный тест гоняется в режиме «с городами/мультисайтом» и «без» (оба варианта, §4).
ПДн-паспорт. Модуль не хранит персональных данных: schema — авторский контент-шаблон (тексты-плейсхолдеры, разметка), не данные посетителей; insert_count — обезличенный агрегат употребления пресета. Декларация — «ПДн не храню», хуки PrivacyRegistry не регистрируются.
Входные и выходные данные
Входы
| Источник | Поля | Чем валидируется |
|---|---|---|
Регистрация системного пресета модулем (SectionPatternRegistry::register(), код) | slug, title, category, blocks[], placeholders[] | контрактный тест реестра (см. «Тесты») — та же схема полей блоков, что у BlockRegistry |
| Admin: «сохранить выделение как пресет» | title, category, blocks[] (выделенное поддерево конструктора), placeholders[] (какие литералы стали ) | FormRequest-whitelist; blocks[] валидируется как обычное поддерево блока (типы существуют в BlockRegistry, лимит глубины cms.blocks.max_nesting_depth) |
| Admin: правка пользовательского пресета | title, category, schema, is_active, sort, lock_version | FormRequest-whitelist; lock_version сверяется — иначе 409; для source=system разрешены только is_active/sort |
Admin: импорт designed-HTML (section-patterns.trusted-import) | html (raw), placeholders[], title, category | html — размер ≤ section-patterns.max_designed_html_size_kb, оборачивается ровно в один доверенный HTML-блок ядра (Cms\Core\Blocks\HtmlBlock, уже существующий тип, studio-роль — см. ТЗ ядра); плейсхолдеры — валидируемые токены внутри html |
| Admin: импорт JSON-пресета (файл) | пресет целиком (title, category, schema, source) | схема блоков как выше; поле source из файла не доверяется — сервер сканирует итоговое дерево на наличие HTML-блока и требует section-patterns.trusted-import, если он найден, независимо от заявленного source |
Вставка секции в конструктор (POST …/materialize) | pattern_id, values{} (заполненные плейсхолдеры), RequestContext (city/locale) | значения — по типам из placeholders[] пресета (движок полей); отсутствующее обязательное поле без дефолта — 422, не тихая вставка «как есть» |
cms:section-patterns:sync-system | декларации из SectionPatternRegistry (память, не внешний ввод) | идемпотентный upsert по slug |
Всё, что не перечислено выше, модуль обязан отвергать (whitelist-принцип §11 стандарта).
Выходы
| Потребитель | Данные | Формат |
|---|---|---|
| Filament: модалка «Вставить секцию» | список пресетов по категориям с превью | {data, meta}, keyset-пагинация |
POST …/materialize → конструктор страницы | конкретное поддерево блоков (плейсхолдеры заменены, _v актуализирован, свежие id экземпляров блоков) | JSON-поддерево cms_page_revisions.blocks — вставляется клиентом конструктора в черновик ревизии, дальше это обычные блоки |
GET /api/v1/section-patterns (внешние редакторы/островки) | активные пресеты: id, slug, title, category, preview_media_url (без полного schema, если не запрошено явно) | конверт {data, meta} |
| Filament: ресурс пресетов | CRUD-представление, счётчик insert_count | таблица/фильтры по категории и источнику |
| Шина событий | SectionPatternInserted, SectionPatternSaved, SectionPatternImportedDesigned, SectionPatternDeleted | payload → подписчики (счётчик, cms/audit) |
Экспорт пресета (GET …/{id}/export) | пресет целиком | скачиваемый JSON-файл |
Настройки (группа section-patterns)
| Ключ | Тип | Дефолт | affectsPageCache | Описание |
|---|---|---|---|---|
section-patterns.enabled | bool | true | нет | Доступность действий «вставить из библиотеки» / «сохранить как пресет» в конструкторе |
section-patterns.max_user_patterns | int | 200 | нет | Лимит пользовательских пресетов на инсталляцию/сайт; достижение — 422 с понятным текстом при попытке сохранить ещё один |
section-patterns.max_designed_html_size_kb | int | 500 | нет | Максимальный размер импортируемого сырого HTML |
section-patterns.designed_import_enabled | bool | true | нет | Kill-switch: аварийное отключение импорта designed-HTML без выключения модуля целиком; вставка уже импортированных ранее пресетов продолжает работать |
section-patterns.gallery_page_size | int | 24 | нет | Размер страницы пагинации модалки библиотеки |
Ни одна настройка не помечена affectsPageCache — модуль намеренно не касается публичного рендера страниц (см. «Производительность и кеш»): все его настройки влияют только на доступность инструментов конструктора в админке. Секретов нет.
API
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| GET | /api/v1/section-patterns | authenticated, section-patterns.view | Список активных пресетов для внешних редакторов/островков (не анонимный публичный вывод — namespace без /admin выбран для потребителей вне Filament-сессии, но доступ по-прежнему через права) |
| GET/POST/PUT/DELETE | /api/v1/admin/section-patterns | admin (section-patterns.view/.manage/.delete) | CRUD пресетов; для source=system PUT принимает только is_active/sort (иное — 422) |
| POST | /api/v1/admin/section-patterns/{id}/materialize | admin (section-patterns.view) | Заполнить плейсхолдеры → вернуть конкретное поддерево блоков для вставки в черновик страницы |
| POST | /api/v1/admin/section-patterns/import-designed | admin (section-patterns.trusted-import) | Импорт HTML из claude.ai/design как пресета |
| POST | /api/v1/admin/section-patterns/import | admin (section-patterns.manage; повышается до .trusted-import, если в присланном дереве обнаружен HTML-блок) | Импорт пресета из JSON-файла |
| GET | /api/v1/admin/section-patterns/{id}/export | admin (section-patterns.view) | Экспорт пресета JSON-файлом |
Мутации пользовательского пресета принимают lock_version — рассинхрон отдаёт 409 (§7 стандарта). Материализация — не мутация над таблицей пресетов (пресет не меняется от факта вставки, кроме асинхронного инкремента insert_count), поэтому Idempotency-Key на неё не обязателен, но допустим (двойной вызов просто вернёт то же поддерево второй раз — см. «Крайние случаи»).
Компоненты
Filament: действие конструктора страницы «Вставить секцию из библиотеки» (модалка: сетка превью по категориям, поиск по названию, пагинация); действие «Сохранить выделение как пресет» (выбор, какие литеральные значения выделения станут -плейсхолдерами, название, категория); ресурс SectionPatternResource (список с фильтром по категории/источнику/активности, afterSave() → инвалидация тега section-patterns:library, для source=system форма редактирования блокирует поля schema/title/category); действия экспорта/импорта JSON и импорта designed-HTML (кнопка видна только роли studio). Команды: cms:section-patterns:sync-system --json (идемпотентный upsert системных пресетов из реестра), cms:section-patterns:export --slug=<slug> --json.
Демо-контент. Отдельного demo-сидера модулю не требуется: sync-system уже наполняет галерею /_gallery/playground системными пресетами тех модулей-доноров, что установлены в dev-окружении студии (минимум ядро регистрирует 1–2 демонстрационных пресета — «hero + CTA», «три преимущества» — для показа модуля без единого установленного модуля-донора).
Фронтенд-бюджет и a11y. Публичного рендера у модуля нет вовсе (см. «Производительность и кеш») — фронтенд-бюджет применим только к самой модалке конструктора: превью — ленивая подгрузка изображений по видимости сетки, сетка категорий и карточки пресетов управляются с клавиатуры (tabindex, aria-label с названием пресета), форма заполнения плейсхолдеров — обычная форма движка полей ядра (лейблы, ошибки под полем).
События и обмен
| Событие | Когда | Payload |
|---|---|---|
SectionPatternInserted | секция материализована и вставлена в черновик страницы | pattern_id, page_id (nullable), landing_id (nullable) |
SectionPatternSaved | создан/изменён пользовательский пресет | pattern_id, source |
SectionPatternImportedDesigned | импортирован доверенный HTML-пресет | pattern_id, actor_id |
SectionPatternDeleted | пресет удалён (пользовательский или устаревшая системная строка) | pattern_id, source |
Слушает: не имеет входящих подписок (регистрация системных пресетов другими модулями идёт через реестр на boot(), а не через события). FilterBus: section-patterns.registry.categories (модули/тема могут добавить категорию в палитру) и section-patterns.materialize.props (точечная правка props перед вставкой, например локализация дефолтного текста плейсхолдера).
Таблица взаимодействий:
| Сущность/модуль | Канал | Направление | Что происходит |
|---|---|---|---|
SectionPatternRegistry (собственный реестр модуля) | регистрация на boot() модуля-донора | модуль-донор → section-patterns | модуль-донор (cms/banners, cms/commerce-catalog, cms/reviews, cms/forms и т. п.) объявляет системный пресет; при выключении донора пресет пропадает из реестра при следующем sync-system |
BlockRegistry / движок полей | сервис-вызов cms/core-contracts | section-patterns → ядро | материализация проверяет актуальность _v блока и применяет ленивую data-миграцию перед вставкой |
MediaService | сервис-вызов cms/core-contracts | section-patterns → ядро | превью пресета (preview_media_id) |
RequestContext | сервис-вызов cms/core-contracts | ядро → section-patterns | измерения city_id/locale при фильтрации библиотеки |
cms/landings (suggests) | то же действие конструктора | landings ↔ section-patterns | вставка секции внутри лендинга работает тем же действием, что и на обычной странице — landings не знает деталей реализации |
cms/audit (suggests) | событие | section-patterns → audit | SectionPatternImportedDesigned пишется в аудит, если модуль включён |
Очередь section-patterns | очередь | section-patterns → воркер | асинхронный инкремент insert_count по SectionPatternInserted |
Фоновая работа
Очередь section-patterns: слушатель SectionPatternInserted кладёт job атомарного инкремента UPDATE cms_section_patterns_patterns SET insert_count = insert_count + 1 WHERE id = ? (та же техника, что у cms/banners для счётчиков показов — не read-modify-write из PHP, гонка не теряет инкременты). cms:section-patterns:sync-system — не по расписанию, а вызывается лайфцикл-хуком enable модуля-донора (манифест lifecycle.enable) и вручную (деплой/апгрейд); идемпотентен — повторный прогон обновляет schema/title/category существующих system-строк по slug, не создаёт дублей, сохраняет админский override is_active/sort; пресеты, более не объявленные ни одним установленным донором, помечаются is_active = false (мягко, не удаляются — см. «Крайние случаи»).
Метрики и алерты. Счётчики: число активных системных/пользовательских/designed пресетов, давность последнего sync-system, число вставок за период (insert_count в разрезе пресета). Алерт: sync-system не выполнялся дольше N дней при наличии установленных модулей-доноров (несоответствие реестра и таблицы) — через cms/health.
Мини-ранбук.
| Симптом | Что проверить | Чем чинить |
|---|---|---|
| Новый системный пресет модуля-донора не появляется в библиотеке | выполнялся ли cms:section-patterns:sync-system после включения модуля | cms:section-patterns:sync-system --json вручную |
| Пресет пропал после выключения модуля-донора | ожидаемое поведение (см. «Зависимости») | включить модуль обратно — пресет вернётся при следующем sync-system, вставленные ранее секции не пострадали |
| Импорт designed-HTML недоступен | section-patterns.designed_import_enabled (kill-switch), роль пользователя | включить настройку/выдать section-patterns.trusted-import |
insert_count не растёт | лог очереди section-patterns | cms:doctor --json, ручной прогон отложенных job'ов |
Бэкап/рестор. В бэкап — cms_section_patterns_patterns целиком (включая schema). После рестора пересчёта не требуется; при подозрении на рассинхрон с кодом модулей-доноров — cms:section-patterns:sync-system (идемпотентен, безопасен на живом сайте).
Производительность и кеш
Ожидаемый объём: низкие сотни пресетов на инсталляцию (десятки системных на модуль-донор + пользовательские). У модуля нет публичного горячего пути вовсе — в отличие от большинства модулей ядра, section-patterns работает только во время редактирования в Filament, никогда не участвует в рендере публичной страницы (материализованные блоки живут уже без ссылки на пресет — см. «Назначение»). Поэтому ни одна настройка не помечена affectsPageCache, и в ключ page-cache модуль ничего не добавляет.
Тег кеша section-patterns:library (объявляется в CacheTags) кеширует список активных пресетов для модалки конструктора и для GET /api/v1/section-patterns (внешние редакторы/островки могут дёргать список чаще, чем меняется библиотека); инвалидируется своими событиями (SectionPatternSaved/SectionPatternDeleted/после sync-system). Критичные индексы: составной category+is_active (выборка модалки), уникальный slug (upsert-цель sync-system). GIN по schema не заводится — в отличие от cms_page_revisions, здесь не нужен поиск «на каких страницах используется» (это делает сама ревизия страницы), библиотека фильтруется по нормальным колонкам, не по содержимому JSONB.
Безопасность
Границы входа: FormRequest-whitelist на все CRUD-эндпоинты; поддерево блоков пользовательских и импортируемых пресетов валидируется тем же движком, что и BlockRegistry (существующие типы, лимит вложенности cms.blocks.max_nesting_depth). Материализация не доверяет клиенту готовое поддерево — сервер сам собирает его из хранимого schema + присланных значений плейсхолдеров (клиент не может подсунуть произвольный блок под видом «результата материализации»).
Доверенный HTML. Пресет source=designed оборачивается в уже существующий доверенный HTML-блок ядра (studio-роль, {!! !!} только для него, санитайзер не применяется — сам блок доверенный по определению ядра). Импорт такого пресета — только под section-patterns.trusted-import; сервер также сканирует любой импортируемый JSON-пресет на наличие HTML-блока независимо от заявленного source (см. «Входные и выходные данные») — иначе редактор без роли studio мог бы обойти гейт, просто переименовав поле source в файле перед импортом. Разница с обычным rich-text: здесь нет двойного барьера санитизации, потому что доверенный HTML-блок ядра по определению не санитизируется — весь контроль на входе (кто имеет право создать/импортировать такую строку), не на выходе.
Пользовательские regex модуль не вводит (нет полей произвольного regex в собственной схеме).
Матрица ролей:
| Действие | admin | менеджер | редактор | studio |
|---|---|---|---|---|
section-patterns.view (просмотр библиотеки, вставка секции при редактировании страницы) | ✅ | — | ✅ | ✅ |
section-patterns.manage (сохранить выделение как пресет; править/переключать активность своих и видимость системных) | ✅ | — | ✅ | ✅ |
section-patterns.delete (удаление пользовательского пресета; ручное удаление устаревшей системной строки) | ✅ | — | — | ✅ |
section-patterns.trusted-import (импорт designed-HTML; импорт JSON, содержащего HTML-блок) | — | — | — | ✅ |
Менеджер не работает с конструктором страниц ни в одном другом модуле студии (см. cms/landings, cms/banners) — здесь тот же принцип: библиотека секций доступна только редакционным и studio-ролям. ПДн — см. «Модель данных» (не хранит). В логи/метрики не попадает содержимое schema целиком — только pattern_id/slug/агрегаты.
UX-требования
Админ. Пустая библиотека (свежая установка, ни один донор не засинхронизирован) — подсказка «Библиотека пуста — включите модули с готовыми секциями или сохраните свою» вместо пустой сетки без объяснения. Массовое действие в списке пресетов: массовая деактивация устаревших системных строк (is_active = false после sync-system, если админ хочет явно подтвердить очистку). Ошибки — на человеческом языке: превышение section-patterns.max_designed_html_size_kb → «HTML превышает 500 КБ, уменьшите разметку»; конфликт lock_version → «Пресет изменён другим пользователем, обновите страницу»; попытка вставить пресет, ссылающийся на выключенный модуль-донор → «Этот пресет требует модуль «Каталог» — он сейчас недоступен» (не техническая ошибка). Удаление пресета с ненулевым insert_count — подтверждение с цифрой («Пресет использован N раз — удалить только из библиотеки? Уже вставленные секции не пострадают»).
Редактор при вставке. Форма заполнения плейсхолдеров — обычная форма движка полей (валидация, сохранение введённого при ошибке); превью пресета в модалке показывает миниатюру и категорию, не голый slug; вставка — синхронное действие с мгновенной обратной связью (без спиннера на секунды — материализация не ходит во внешние API).
Крайние случаи и типовые баги
- Пресет ссылается на тип блока выключенного/удалённого модуля-донора → материализация атомарна: если хотя бы один тип блока в поддереве неизвестен
BlockRegistry, вставка отменяется целиком с понятным сообщением («требует модуль «Каталог»»), не вставляется частично и не падает 500. - Устаревшая
_vвнутриschemaпресета (модуль-донор с тех пор обновил схему блока с ломающей миграцией) → материализация прогоняет ту же ленивую data-миграцию, что и рендер страницы (контракт блока), прежде чем вернуть поддерево — в ревизию страницы никогда не попадает устаревший_v. - Плейсхолдер не заполнен и не имеет дефолта, а поле обязательно по схеме блока → форма вставки не даёт сохранить без заполнения (обычная валидация движка полей), не подставляет пустую строку молча.
- Двойной клик «Вставить» → не гонка данных: материализация не пишет ничего в БД, кроме асинхронного инкремента
insert_count; повторный клик просто вставляет секцию в конструкторе дважды визуально (ожидаемое поведение UI, лечится обычным «отменить», не требует серверной идемпотентности мутации). - Импорт designed-HTML без роли studio через прямой вызов API (не через скрытую в UI кнопку) → 403 на сервере независимо от видимости элемента интерфейса.
- Импорт JSON с
source: "user", фактически содержащего HTML-блок → сервер не доверяет полюsourceиз файла, сканирует итоговое дерево на HTML-блок и требуетsection-patterns.trusted-importнезависимо от заявленного значения — иначе это обход studio-гейта переименованием поля. - Модуль-донор выключен, а его системные пресеты уже вставлялись на страницы → библиотека при следующем
sync-systemпомечает пресетis_active = false(пропадает из модалки), но уже вставленные ранее секции на страницах — обычные блоки, продолжают рендериться без изменений (ключевой инвариант модуля). - Конкурентная правка пользовательского пресета двумя редакторами →
lock_version, 409, «Пресет изменён другим пользователем, обновите страницу», не «последний победил». - Огромный
schema(неаккуратный designed-импорт на тысячи вложенных блоков) → сохранение отклоняется лимитом вложенностиcms.blocks.max_nesting_depthи/или размером HTML (max_designed_html_size_kb), понятная ошибка, не OOM при последующей материализации. - Апгрейд системного пресета (модуль-донор поменял
schemaпресета в новой версии) →sync-systemобновляетschema/title/categoryпоslugидемпотентно, сохраняя админскийis_active/sort, не создавая дубль строки. - Категория пресета не объявлена ни одним установленным модулем (осталась в БД от выключенного донора) → карточка группируется в служебную категорию «прочее» в модалке, не пропадает молча и не ломает фильтр.
- Модуль не хранит платных внешних API-вызовов и не участвует в постраничном кеше — пункты матрицы продуманности «стоимость внешних API» и «kill-switch page-cache» — не применимо (см. «Настройки»/«Производительность»); собственный kill-switch есть только для designed-импорта.
Донорский код
Готового переносимого кода/данных нет — это новая для парка студии концепция (ближайшие аналоги в индустрии: Gutenberg Block Patterns, Tilda «блоки», Elementor «библиотека блоков»), а не миграция существующей PHP-функциональности. Черновой UX модалки выбора секции в панели студии (studio-core) использован как ориентир для раскладки категорий/превью, без готового кода для переноса.
Миграция legacy-данных (§16 стандарта) — не применимо. У модуля нет донорских таблиц с боевыми данными для маппинга; аналог «наполнения из внешнего источника» — это cms:section-patterns:sync-system, наполняющая таблицу из деклараций модулей-доноров в коде (см. «Фоновая работа»), а не из legacy-БД клиентских проектов.
Тесты и приёмка
- [ ] Контрактный тест
SectionPatternRegistry:sync-systemидемпотентен, upsert поslug, сохраняетis_active/sortпри повторном прогоне - [ ] Материализация пресета со ссылкой на неизвестный/выключенный тип блока отклоняется целиком с понятным сообщением, не вставляется частично, не 500
- [ ] Материализация применяет ленивую data-миграцию устаревшего
_vперед вставкой - [ ] При выключении модуля действия «вставить»/«сохранить как пресет» недоступны, ранее вставленные секции на страницах рендерятся без изменений (ключевой инвариант)
- [ ] Импорт designed-HTML доступен только
section-patterns.trusted-import; прямой вызов API без роли — 403 - [ ] Импорт JSON с фиктивным
source: user, содержащий HTML-блок, требуетsection-patterns.trusted-importвне зависимости от заявленногоsource - [ ] Конкурентная правка пользовательского пресета двумя редакторами → 409 по
lock_version - [ ]
section-patterns.max_user_patternsиmax_designed_html_size_kb— лимиты с понятной ошибкой при достижении, не тихое обрезание - [ ]
section-patterns.designed_import_enabled(kill-switch) отключает импорт без выключения модуля целиком - [ ] Асинхронный инкремент
insert_countне теряет обновления при параллельных вставках (атомарныйUPDATE) - [ ] Инвалидация тега
section-patterns:libraryпри CRUD пресета и послеsync-system - [ ] Измерения
locale/city_id/site_idработают и при их отсутствии (оба режима, §4) - [ ] Права
section-patterns.view/.manage/.delete/.trusted-importразграничены по матрице ролей - [ ] Контрактный набор
cms-testingзелёный, пакет тестируется в testbench-изоляции - [ ] Feature-тест на каждый роут API; тестовая БД только
section-patterns_test,migrate:fresh/refresh/reset/db:wipeзапрещены