tz-design — Фаза 2: Декомпозиция данных

Часть skill'а tz-design. Условия запуска и условие пропуска Фазы 2 — в SKILL.md. Этот файл читается целиком перед запуском Фазы 2.

Алгоритм

  1. Перечислить все участвующие сущности — существующие (затрагиваемые), новые (создаваемые), модифицируемые. Включая на первый взгляд нелогичные (audit_log, news, system_flags, observer-точки).

  2. Декомпозировать каждую до атомарной модели.

    Атомарная = все поля являются одним из:

    • примитивы (int, string, decimal, bool, datetime, json со схемой);
    • коллекции примитивов (массив строк, JSON-список с описанной структурой);
    • foreign key с явной семантикой связи.

    Если поле — составной объект, который сам по себе имеет жизненный цикл/инварианты → выделить в отдельную сущность, продолжить декомпозицию.

    Стоп-условие: все поля атомарны.

  3. Re-check самодостаточности. Для каждой атомарной модели — вопрос:

    «Достаточно ли этой модели одной для полного описания доменного смысла, или нужны данные извне?»

    Если самодостаточна → отметить явно «доменный смысл полный».

    Если нужны данные извне → обязательно указать достраивающую зависимость одним из:

    • Поглощение/денормализация: «копируем поле X из сущности Y в момент Z, потому что …»
    • Явная FK с семантикой: «field_id → OtherEntity. Семантика: <зачем эта связь, не просто "связан с">»
    • Документированный путь резолвции: «доступ только через Service::method() / Model::scope() — обходные пути запрещены»
    • Контракт со-существования: «сущности X, Y, Z создаются/удаляются вместе, инвариант проверяется в <место> (validateFond / observer / DB::transaction)»

    Без явного выбора — модель помечена «архитектурно недозавершена», переход в Фазу 3 невозможен.

    Decision helper для самодостаточности. Если пользователь подтверждает несамодостаточность, но не понимает какой из 4 типов выбрать — активируй вопросник:

    Помогу выбрать тип достраивающей зависимости. Ответь на 1–2 вопроса:
    
    Q1: Нужна ли историческая фиксация значения (snapshot, актуального на момент создания связи)?
      — Да → Поглощение/денормализация. Копия живёт независимо от источника,
             защищена от будущих изменений (пример: commission_percent в заявке
             на вывод хранится как копия из настроек, чтобы редактирование
             настроек не меняло задним числом сумму комиссии).
      — Нет → переход к Q2.
    
    Q2: В каком отношении сущности с зависимостью?
      — Зависимость существует независимо, может быть удалена/изменена
        → Явная FK с семантикой (типовой случай user → resource).
      — Сущности создаются всегда вместе в одной транзакции, по отдельности невалидны
        → Контракт со-существования (пример: 3 fond_situation при processWithdraw).
      — Зависимость не хранится напрямую, резолвляется через бизнес-логику
        (scope / service / computed property)
        → Документированный путь резолвции (пример: hedge.balance через ledger SUM).
    

    Триггеры активации helper'а: явная просьба пользователя («не понимаю», «помоги выбрать», «какой подходит») ИЛИ повторное «несамодостаточна, не знаю» подряд.

    Auto-выбор зависимости запрещён даже после helper'а — пользователь обязан явно ответить Q1/Q2.

  4. Re-check реестровой модели. Для каждой сущности с коллекцией экземпляров — вопрос:

    «Несёт ли коллекция собственный доменный смысл помимо хранилища?»

    Реестр выделяется в отдельную сущность (свой контроллер, scope'ы, бизнес-методы), если выполнено хотя бы одно:

    • собственное состояние коллекции (текущий активный, версия, счётчик, метаданные коллекции);
    • доменные операции над коллекцией, не сводимые к стандартному Eloquent API («синхронизировать все», «выбрать тот, что подходит по правилу»);
    • несколько потребителей с единообразным API/контрактом.

    Если нет — это просто hasMany от владельца, без отдельной модели управления.

  5. Cross-cutting инварианты. Формулы и правила, связывающие несколько атомарных моделей. Например:

    • fond_situation.summ + hedge_ledger.balance + open_options = real_Bybit_total
    • «Любая WithdrawalRequest имеет ровно 3 связанных fond_situation с конкретными source-значениями».

    Для каждого инварианта: где проверяется (validateFond / observer / тест / DB constraint).

  6. Анализ пробелов. По каждому полю/связи:

    • Кто отвечает за заполнение? (если никто — пробел)
    • Что произойдёт при NULL?
    • Что произойдёт при удалении связанной сущности (CASCADE / RESTRICT / SET NULL)?
    • Как мигрируются существующие записи при добавлении нового поля?

Формат карточки атомарной модели

### <Имя сущности>
- **Назначение:** одна строка.
- **Поля:** список с типами и обоснованием (зачем нужно, кто пишет, кто читает).
- **Жизненный цикл:** создание, переходы, удаление/архивация.
- **Самодостаточность:** ✅ полный доменный смысл  ИЛИ
  ⚠️ Достраивающая зависимость — <тип>: <конкретное описание>
- **Реестровая обёртка:** не нужна  ИЛИ
  Выделена как `<RegistryModel>` с обоснованием: <какой критерий сработал>
- **Источники мутации:** контроллер/сервис/observer/миграция/admin-UI.
- **Источники чтения:** UI/API/отчёт/другой сервис.
- **Участие в инвариантах:** список cross-cutting инвариантов (раздел 5).
- **Поведение при NULL ключевых полей:** конкретно.
- **CASCADE при удалении связанных:** RESTRICT / SET NULL / CASCADE и почему.

Формат output

Вывести карточки атомарных моделей блоками. Между блоками — вопросы по каждому:

  • «Декомпозиция до атомарного уровня — корректна? Есть ли поля, которые я не разбил, хотя надо было?»
  • «Самодостаточность блока X — выбранный способ выражения зависимости (поглощение/FK/путь/контракт) — правильный?»
  • «Реестровая обёртка для коллекции Y — нужна или избыточна?»

Не вываливать все блоки разом без вопросов. Получать подтверждение по каждому.

Условие выхода из Фазы 2

Пользователь подтвердил состав, самодостаточность, реестровый статус, инварианты и lifecycle по всем блокам. Ни одной модели в статусе «архитектурно недозавершена».