name: vortex-initial-architecture description: Design the initial architecture of a new Vortex package — first decompose data models down to atomic units, then lay out bus, config, controller, model, presets, interface, handlers — following the canonical Vortex algorithm (Stage 0 + 7 steps). Use this skill AFTER the layer is determined (typically after vortex-layer-detect) and the user wants to lay out the package structure before coding. Triggers when the user says things like "спроектируй архитектуру", "сформируй структуру пакета", "набросай каркас системы X для Vortex", "определим модели данных пакета", or otherwise asks for the package design canon. MUST run before code generation. If it is not clear that the requested work belongs to Vortex, the skill MUST ask the user to confirm before proceeding.

Vortex — проработка первичной архитектуры пакета

Платформа: Claude Code (CLI от Anthropic). Размещать в ~/.claude/skills/vortex-initial-architecture/SKILL.md (user-level) или .claude/skills/vortex-initial-architecture/SKILL.md (project-level). Использует инструменты Read, Edit, Write, Glob, Grep. На других LLM-платформах требует адаптации формата вызова инструментов.

Скилл проводит проектирование пакета Vortex по канону, шаг за шагом. Применяется после того, как определён слой размещения (через vortex-layer-detect или явно). Цель — выдать согласованную структуру (шина, конфиг, контроллер, модель, пресеты, интерфейс, представления) с обоснованными решениями и явными точками выбора, готовую к кодированию.

0. Триггер и проверка контекста

Запускается, когда пользователь явно или по контексту просит сделать первичную архитектуру нового пакета Vortex.

Если нет явной уверенности, что речь о расширении Vortex — сначала уточнить у пользователя, прежде чем выполнять. Без подтверждения скилл не выполняется.

Предусловия:

  • Слой размещения известен (1, 2 или 3). Если нет — сначала прогнать vortex-layer-detect.
  • Известна цель пакета и базовый функционал.
  • Известна доменная зона (для L3) или платформа (для L2).

1. Сквозные правила проектирования

Эти правила применяются на каждом шаге без исключений. Скилл должен явно проверять их и сообщать о нарушениях.

Технологии должны быть обоснованы

  • Каждая нетривиальная технология (async/UniTask, Addressables, Pool, рефлексия, генерация кода) требует явного ответа: что вынуждает применять это здесь?
  • Если ответ «может пригодится потом» — отложить.
  • Public API синхронный по умолчанию. Async допустим внутри контура контроллера, наружу не торчит без необходимости.
  • Addressables — только при подтверждённой потребности стриминга. На MVP — прямые GameObject prefab ссылки в пресетах.

Проверка на смешение ответственности (обязательно на каждом шаге)

  • На каждый метод/класс/поле — вопрос: «Входит ли это в зону ответственности именно ЭТОГО пакета?»
  • Маркеры смешения:
    • В контроллере хранятся источники доменных данных (стартовые значения, конфиги других модулей). → запрашивать у владельца через GameController ComplexModel.
    • В контроллере есть валидация междоменных правил. → выносить за пределы.
    • В пакете обрабатываются триггеры/ввод/события игрока. → пакет принимает уже-произошедший факт через свой публичный API.
    • В пресете поля типа IsStartCandidate, RequiredQuest, UnlockedBy. → почти всегда смешение, удалять.
  • При обнаружении смешения — выносить ответственность владельцу, пакет принимает результат через API/контракт.

GUID — это строка

  • В Vortex GUID хранится как string, не System.Guid. Все ID (Record.GuidPreset, ключи словарей в моделях, ссылки на пресеты) — string.
  • API Database (GetRecord<T>(string guid), GetNewRecord, GetRecords) работает только со строками.

Кросс-ссылки в пресетах — только GUID-строки

  • В SO-пресетах НЕ делать [SerializeField] OtherPreset[] refs — это ломает удобство Inspector.
  • Хранить как string[] guids с атрибутом [DbRecord(typeof(TargetModel), RecordTypes.X)].
  • Удобный аксессор-свойство (если нужен) приводит GUID к объекту через Database.GetRecord<T> — корректно только для Singleton-пресетов. Для MultiInstance такое приведение семантически неверно.

2. Этап 0 — Определение моделей данных

Этап обязателен и предшествует проработке архитектурных шагов. Цель — определить все или большую часть моделей данных, которые система будет хранить и обрабатывать. На этом этапе решаются только архитектурные вопросы: что есть, что чем описано, где это конфигурируется. Дизайнерские решения (реактивность, владельцы, ЖЦ, кросс-ссылки между моделями) — на следующем этапе.

Понятие атомарной модели

Атомарная модель — модель, состав полей которой подобран так, что:

  • модель целиком описывает одно завершённое доменное понятие (имеет внятный summary над классом — без слов «контейнер», «обёртка», «вспомогательный»);
  • никакое подмножество полей нельзя выделить в отдельную доменную модель без разрушения смысла целого.

Правило формирования модели (триада)

Применяется на каждом шаге декомпозиции, рекурсивно:

  1. Доменный смысл — формулировка summary одной-двумя фразами на естественном языке. Если получается только техническими словами — модель не атомарна или плохо определена.
  2. Набор полей, полностью описывающих заявленный смысл. Не больше, не меньше.
  3. Кандидаты на детализацию — какие поля сами образуют доменное понятие со своим summary. Они выносятся в подмодель, к которой триада применяется снова.

Алгоритм этапа

1. Опорная runtime-модель пакета.
   К ней применяется правило формирования.

2. Рекурсивная детализация по правилу формирования.
   Стоп: модель атомарна (поля = примитивы, коллекции примитивов или GUID-ссылки).

3. Re-check целостности доменных единиц.
   Для каждой атомарной модели — вопрос:
   «Достаточно ли её одной для полного описания доменного смысла,
    или нужны данные извне?»
   Если нужны — это достраивающая зависимость.
   Должна быть явно выражена одним из способов:
     • Поглощение (модели объединяются в одну).
     • Явная GUID-ссылка с понятной семантикой связи.
     • Документированный путь резолвции через известный источник
       (например, прилинкованный префаб и его компоненты).
     • Контракт со-существования (объекты обязаны жить в паре).
   Без явного выбора — архитектура недозавершена.

4. Проверка необходимости реестровой модели.
   Для каждой атомарной модели, у которой существует коллекция
   экземпляров — вопрос: несёт ли коллекция собственный доменный
   смысл помимо «хранилище X-ов»?
   Реестровая модель выделяется, если выполнено хотя бы одно:
     • собственное состояние коллекции (счётчик, currently-selected,
       версия, метаданные);
     • доменные операции над коллекцией, не сводимые к Dictionary/List API;
     • несколько потребителей с единообразным API/контрактом.
   Если нет — коллекция живёт как обычное поле владельца.
   Если да — реестр становится новой атомарной моделью; к ней
   применяются те же правила (триада + re-check).

5. Для каждой модели — точка настройки:
     • Database (Singleton / MultiInstance Record-пара)
     • Config-asset пакета (SettingsPreset / ScriptableObject)
     • Локальная настройка через [SerializeReference] на потребителе
     • Генерируемая в runtime — нет точки настройки;
       создаётся контроллером или другой моделью при работе

6. Варианты схем (наборов моделей с указанием «что хранится — где описывается»)
   выдаются пользователю на выбор. Если на каком-то шаге детализации
   возможны несколько разбиений — генерируется альтернатива.

7. Для каждой модели — потребность в сериализации под Save: да / нет.

Условия завершения этапа

  • Все модели атомарны.
  • Каждая прошла re-check либо самодостаточна, либо её достраивающая зависимость явно выражена.
  • У каждой задана точка настройки или маркер «генерируемая».
  • У каждой решено отношение к Save.

Только после этого скилл переходит к проработке архитектурных шагов 1–7 ниже.

3. Семь шагов архитектурной проработки

Скилл идёт по списку строго по порядку. На каждом шаге, где есть выбор, обязательно представляет варианты пользователю.

Шаг 1. Статическая шина

Точка публичного входа в пакет. Не зависит от частных реализаций, не содержит логики.

Содержимое типовой шины:

public static class XBus
{
    public static IXController Controller { get; private set; }
    public static XModel       Data       => Controller?.Model;
    public static bool         IsReady    => Controller?.IsInitialized ?? false;

    public static event Action OnReady;
    public static event Action OnRelease;

    internal static void Bind(IXController controller);   // вызывается из bootstrap
    internal static void Unbind();
}

Дополнения:

  • Под #if UNITY_EDITOR можно добавить геттеры для инструментария верстальщика (полные списки, отладочные view) — через XModel-аксессоры.
  • В шину НЕ помещаются: бизнес-операции, async-методы, валидация, изменение состояния. Только доступ + события.
  • Расположение: Bus/XBus.cs внутри пакета.

Шаг 2. Конфигурация

Решить: нужен ли пакету конфиг.

Конфиг нужен, если:

  • Есть параметры, которые меняются проектом без перекомпиляции.
  • Нужен выбор реализации контроллера (мокирование, разные стратегии).
  • Есть ссылки на ассеты-шаблоны (View-префабы, default-пресеты).

Канон конфига для L2+:

  • ScriptableObject, в подпапке пакета.
  • Параметры — простые типы или ссылки.
  • Выбор контроллера — НЕ через SO-фабрику и не через [SerializeField] ControllerPreset. Используется имя типа + рефлексия:
    [SerializeField, ClassFilter(typeof(IXController))]
    private string controllerTypeName;
    
  • View в конфиге (если требуется ссылка на префаб) — прямой GameObject link, не Addressables до подтверждённой необходимости.
  • Стартовые/доменные данные (стартовый уровень, начальное состояние) — НЕ в конфиге. Они в GameModel через IGameData-расширение.

Шаг 3. Контроллер

Один узловой контроллер на пакет. Запрещено:

  • Плодить однотипные контроллеры без отличий, кроме ID инстанса.
  • Давать пользователю в конфиге выбор «один большой vs несколько маленьких» — это всегда внутреннее решение.
  • Использовать SO-фабрику для создания экземпляров контроллера.

Канон:

  • Контроллер наследует Singleton<TController> для единообразного доступа.
  • Реализует интерфейс IXController (см. шаг 6).
  • Файл может быть partial, разбит по тематическим файлам (XController.Lifecycle.cs, XController.Streaming.cs и т.п.).
  • Допустимы отдельные статические под-контроллеры по необходимости.
  • Bootstrap: при Init находит тип по config.ControllerTypeName, получает Singleton<T>.Instance, привязывает к шине через XBus.Bind.

Что в контроллере НЕ должно быть (смешение ответственности):

  • Источники доменных данных извне (стартовый уровень — не отсюда).
  • Валидация междоменных правил (квесты, состояние игры).
  • Обработка триггеров и ввода игрока.

Шаг 4. Модель данных

XModel : IReactiveData — runtime-агрегатор состояния пакета.

Канон:

  • Реализация IReactiveData:
    public event Action OnUpdateData;     // event, НЕ Action {get;set;}
    
  • Реактивные поля — через ReactiveValue<T> / IReactiveData-под-модели.
  • Пресеты НЕ помещаются в модель напрямую:
    • Не делать public IReadOnlyDictionary<string, MyPreset> Catalog { get; } с конструкторной инжекцией.
    • Использовать internal Dictionary<string, MyModel> X { get; set; } + публичный метод-аксессор IReadOnlyDictionary<...> GetX().
    • В словаре — рабочие копии типа MyModel : Record (не SO-пресет).
  • Per-entity состояние — выносить в подмодель-обёртку (например, XContainer с ReactiveValue<XState> для load-state).
  • Если требуется сохранение первичных данных пакета — НЕ хранить их в XModel (она derived runtime). Хранить в IGameData-расширении GameModel.

Шаг 5. Пресеты

Пара XPreset : RecordPreset<XModel> (SO в Database) + XModel : Record (рабочая копия в runtime).

Канон:

  • RecordPreset<T> уже даёт GuidPreset, Name, Description, Icon. Не дублировать поля типа TitleKey.
  • Record уже даёт GuidPreset, Name, Description. Метод CopyFrom(preset) маппит поля автоматически по совпадению имён.
  • На XModel ставится [Serializable, POCO] если нужна сериализация.
  • Выбор RecordTypes — обязательное решение, фиксируемое в пресете:
    • Singleton (по умолчанию) — пресет = единственный «образец» концепции в игре, не размножается. Уровни карты, конфиги-биомы, типы оружия как описания.
    • MultiInstance — игра создаёт много отдельных runtime-экземпляров, расходящихся в состоянии. Квесты, предметы инвентаря, NPC-инстансы.
    • Default — Singleton, если домен явно не плодит экземпляры.
  • Контроллер при Init:
    • Singleton-каталог: Database.GetRecords<XModel>() (без New, экземпляры уже единственные).
    • MultiInstance: Database.GetNewRecords<XModel>() для свежих копий.
  • Кросс-ссылки на другие пресеты внутри SO — string[] с [DbRecord(...)], см. сквозные правила.

Шаг 6. Интерфейс контроллера

Канон:

  • Только синхронный публичный API по умолчанию.
  • Async допустим внутри контура контроллера для предзагрузки и подобных операций — наружу не выходит.
  • Минимально: Init(config), Cleanup(), операции домена, события состояния.
  • НЕ помещать в интерфейс: валидацию междоменных правил, обработку триггеров, источники чужих данных.
  • Имена методов — императивные, описывают факт: Enter, Activate, Apply — не RequestEnterIfAllowed.

Типовой эскиз:

public interface IXController
{
    bool IsInitialized { get; }
    XModel Model { get; }

    event Action OnInitialized;
    event Action<string> OnSomethingChanged;   // string = GUID в Vortex

    void Init(XConfig config);
    void DomainOperation(string id);
    void Cleanup();
}

Шаг 7. Handlers и Views

Разделение по способу получения модели:

Профильные (доменные) — компоненты, которые знают про конкретный пакет и читают модель из XBus.Data напрямую. Размещаются в этом же пакете.

  • Имя: XView для представлений (на корне префаба), XHandler для бихейвиор-компонентов.
  • Подписки на реактивные поля модели — стандартная схема OnEnable/OnDisable.
  • Любые переключения визуальных состояний — приоритетно через UIStateSwitcher (см. правило в CLAUDE.md), обращение через enum/int, для бинарного — SwitcherState.On/Off.

Универсальные — компоненты, которые могут работать с произвольной моделью этого типа через IDataStorage<T> контракт. Размещаются в профильных пакетах Vortex (например, в Vortex.Unity.UI.*), не в этом пакете.

  • Применимы, когда модель/её часть может идти в общий UI (отображение коллекций, статистики, прогресса).
  • Получают модель через DataStorageView<T>.

Чего избегать:

  • Триггер-handler'ы для обработки ввода/коллайдеров в этом пакете. Это смешение ответственности — пакет принимает факт через XBus.Controller.Operation(...), а триггеры конструируются в проектном/доменном коде.
  • DataStorageView<XModel> для модели пакета — обычно бессмысленен, потому что модель доступна через статическую шину, оборачивать в IDataStorage не нужно.

3. Формат вывода скилла

Скилл выдаёт ответ в виде последовательной проработки 7 шагов:

## Этап 0. Модели данных
<опорная модель + декомпозиция до атомарных>
<re-check целостности — для каждой атомарной модели:
 самодостаточна или достраивающая зависимость + способ её выражения>
<для каждой модели: точка настройки (Database / config-asset / SerializeReference / runtime-generated)>
<варианты схем (если на каком-то шаге было ветвление) — на выбор пользователю>
<сериализация в Save: для каждой — да/нет>

## Шаг 1. Шина XBus
<содержимое шины, минимум кода>

## Шаг 2. Конфигурация
<нужна или нет; если да — состав XConfig>

## Шаг 3. Контроллер
<имя, наследование Singleton<T>, partial-разбиение>
<что НЕ входит — со ссылками на смешение ответственности>

## Шаг 4. Модель данных
<XModel поля, IReactiveData реализация, под-модели>
<интеграция в GameModel через IGameData если требуется>

## Шаг 5. Пресеты
<пара Preset/Model, RecordTypes выбор с обоснованием>
<кросс-ссылки как GUID-строки>

## Шаг 6. Интерфейс контроллера
<IXController сигнатура, синхронные методы, события>

## Шаг 7. Handlers и Views
<профильные — список с описанием>
<универсальные — что выносится в профильные пакеты Vortex>
<что исключено и почему>

## Сводка
<таблица решений>

## Открытые вопросы
<если есть точки выбора пользователя — перечислить>

4. Граничные случаи

  • Слой размещения не определён — отказаться от выполнения, попросить прогнать vortex-layer-detect.
  • Описание задачи неоднозначно — задавать уточняющие вопросы по двум осям: «что именно делает пакет» и «какие данные первичны». Не выдавать архитектуру на догадках.
  • Пакет L1 — конфиг обычно не нужен, ScriptableObject недопустим (платформо-зависимость), View-handlers в L1 нет. Шаги 2 и 7 проходим формально, отмечая отсутствие.
  • Пакет с полиморфным L1-ядром — если расщепление было предложено и одобрено, скилл прорабатывает архитектуру отдельно для каждого слоя, начиная с L1.
  • Пользователь предлагает архитектурное решение, противоречащее канону — изложить расхождение и попросить решение. Не подавлять возражения молча.

5. Что скилл НЕ делает

  • Не пишет код. Только структура, имена, контракты, обоснования.
  • Не выбирает слой — это работа vortex-layer-detect.
  • Не оценивает качество кода — отдельная задача.
  • Не делает оценку производительности.
  • Не генерирует README — отдельная задача после реализации.