Skip to content

ТЗ — Библиотека типовых секций (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_patternsid, 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_idnullOnDelete() (удаление медиа не рушит пресет, просто пропадает превью); 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_versionFormRequest-whitelist; lock_version сверяется — иначе 409; для source=system разрешены только is_active/sort
Admin: импорт designed-HTML (section-patterns.trusted-import)html (raw), placeholders[], title, categoryhtml — размер ≤ 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, SectionPatternDeletedpayload → подписчики (счётчик, cms/audit)
Экспорт пресета (GET …/{id}/export)пресет целикомскачиваемый JSON-файл

Настройки (группа section-patterns)

КлючТипДефолтaffectsPageCacheОписание
section-patterns.enabledbooltrueнетДоступность действий «вставить из библиотеки» / «сохранить как пресет» в конструкторе
section-patterns.max_user_patternsint200нетЛимит пользовательских пресетов на инсталляцию/сайт; достижение — 422 с понятным текстом при попытке сохранить ещё один
section-patterns.max_designed_html_size_kbint500нетМаксимальный размер импортируемого сырого HTML
section-patterns.designed_import_enabledbooltrueнетKill-switch: аварийное отключение импорта designed-HTML без выключения модуля целиком; вставка уже импортированных ранее пресетов продолжает работать
section-patterns.gallery_page_sizeint24нетРазмер страницы пагинации модалки библиотеки

Ни одна настройка не помечена affectsPageCache — модуль намеренно не касается публичного рендера страниц (см. «Производительность и кеш»): все его настройки влияют только на доступность инструментов конструктора в админке. Секретов нет.

API

МетодПутьДоступНазначение
GET/api/v1/section-patternsauthenticated, section-patterns.viewСписок активных пресетов для внешних редакторов/островков (не анонимный публичный вывод — namespace без /admin выбран для потребителей вне Filament-сессии, но доступ по-прежнему через права)
GET/POST/PUT/DELETE/api/v1/admin/section-patternsadmin (section-patterns.view/.manage/.delete)CRUD пресетов; для source=system PUT принимает только is_active/sort (иное — 422)
POST/api/v1/admin/section-patterns/{id}/materializeadmin (section-patterns.view)Заполнить плейсхолдеры → вернуть конкретное поддерево блоков для вставки в черновик страницы
POST/api/v1/admin/section-patterns/import-designedadmin (section-patterns.trusted-import)Импорт HTML из claude.ai/design как пресета
POST/api/v1/admin/section-patterns/importadmin (section-patterns.manage; повышается до .trusted-import, если в присланном дереве обнаружен HTML-блок)Импорт пресета из JSON-файла
GET/api/v1/admin/section-patterns/{id}/exportadmin (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-contractssection-patterns → ядроматериализация проверяет актуальность _v блока и применяет ленивую data-миграцию перед вставкой
MediaServiceсервис-вызов cms/core-contractssection-patterns → ядропревью пресета (preview_media_id)
RequestContextсервис-вызов cms/core-contractsядро → section-patternsизмерения city_id/locale при фильтрации библиотеки
cms/landings (suggests)то же действие конструктораlandingssection-patternsвставка секции внутри лендинга работает тем же действием, что и на обычной странице — landings не знает деталей реализации
cms/audit (suggests)событиеsection-patternsauditSectionPatternImportedDesigned пишется в аудит, если модуль включён
Очередь 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-patternscms: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 запрещены

Внутренняя база знаний студии