PLANNER — Модель данных и алгоритм генератора

Дата: 2026-05-06. Статус: дизайн-документ, согласован с пользователем (4 развилки + модель длительностей). Контекст: этот документ — первичная истина для модуля PLANNER в части автоматического планирования ППР (ЗН по расписанию). Сидер и реализация Phase 6/7/8 должны соответствовать ему.

Связанные документы:

Ключевая идея: PLANNER — самостоятельный модуль (не надстройка над VAULT). Он использует справочники VAULT как входные данные (Договоры, Пакеты, ТСО, Шаблоны ППР, Чек-листы), но логика генерации, оперативного планирования и публикации — собственная.


1. Что справочник, что модуль

Сущность Где живёт Тип
Договор VAULT document
Пакет обслуживания VAULT directory
Физобъект VAULT directory
Элемент ТСО VAULT directory
Тип ТСО VAULT directory (hierarchical)
Фактор обслуживания VAULT directory
SLA VAULT directory
Операция (чек-листа) VAULT directory
Шаблон чек-листа VAULT directory
Шаблон ППР (MaintenanceTemplate) VAULT directory
PlannedWork (ЗН-черновик) PLANNER (модель) модуль
PlannedWorkAssignee PLANNER (модель) модуль
planGenerator (cron) PLANNER (сервис) модуль
DnD-планировщик (UI) PLANNER (frontend) модуль
Заказ-Наряд (опубликованный) VAULT document (создаётся на Phase 6 publish)

Принцип разделения: всё что описывает «как устроен мир заказчика» (объекты, оборудование, регламенты, операции) — справочники. Всё что про процесс генерации/планирования/публикации — модуль.


2. ER-диаграмма справочников

[Diagram]

Пояснения к ER


3. Модель длительности операций (трёхуровневый резолв)

При генерации ЗН для каждой операции на конкретном ТСО система резолвит длительность по приоритету:

1. Override (Операция × Тип ТСО):
   operation.duration_by_type[tso.type_id].duration_minutes
2. Default операции:
   operation.duration_minutes_default

Если ни override, ни default не задан — validation error при сохранении операции.

Расчётная трудоёмкость ППР-шаблона на 1 ТСО

duration_minutes_per_tso(template, tso) =
    Σ resolve_duration(item.operation, tso.type_id)
        for item in template.checklist_template.items
        if item.uslovie_aktivacii == null OR
           item.uslovie_aktivacii ∈ tso.faktory
  + Σ resolve_duration(avto_op.operation, tso.type_id)
        for faktor in tso.faktory
        for avto_op in faktor.avto_operations

Расчётная трудоёмкость на пачку ТСО (для bin-packing 24ч-лимита)

duration_minutes_total(template, tso_list) =
    Σ duration_minutes_per_tso(template, tso) for tso in tso_list

Авто-операции считаются per-instance ТСО (без дедупликации). Это сознательное упрощение MVP: безопаснее переоценить, чем недооценить. Дедупликация по location_group — задача v2.

Поле duration_hours на ППР-шаблоне

В скрине пользователя поле было «обязательное, число». В модели оставляем два поля:

Поле Источник Назначение
duration_hours_normative ручной ввод то, что зафиксировано в договоре с заказчиком (биллинг, отчёты PRISM)
duration_hours_calculated COMPUTED (на лету при чтении) системный расчёт по операциям/факторам, для bin-packing 24ч-лимита

UI шаблона ППР показывает оба + индикатор расхождения.


4. Алгоритм автоматического генератора (planGenerator)

[Diagram]

Псевдокод

function generatePlannedWorks():
    horizon = now() + 30 days
    for template in PPR_TEMPLATE.where(active=true):
        dates = recurrenceService.computeOccurrences(template.recurrence_rule, horizon)
        for date in dates:
            if PlannedWork.exists(template_id=template.id, scheduled_at=date):
                continue  # idempotent

            tso_list = collectTSO(template)
            groups  = groupBy(tso_list, t => t.physobject_id)

            for physobject_id, tso_group in groups:
                total_minutes = computeTotalDuration(template, tso_group)

                if total_minutes <= 24 * 60:
                    createPlannedWork(template, date, physobject_id, tso_group)
                else:
                    sub_groups = binPack(tso_group, max_minutes=24*60)
                    for sub in sub_groups:
                        createPlannedWork(template, date, physobject_id, sub)

function collectTSO(template):
    paket = template.paket
    physobjects = paket.paket_physobjects.map(_.physobject_id)

    # avto-filtr
    auto = TSO.where(physobject_id IN physobjects, tso_type_id = template.tso_type_id)

    # primenit ruchnye pravki iz paket_tso
    excluded = paket.paket_tso.where(mode='manual_exclude').map(_.tso_id)
    included = paket.paket_tso.where(mode='manual_include').map(_.tso_id)

    return (auto - excluded) ∪ TSO.where(id IN included, tso_type_id = template.tso_type_id)

function computeTotalDuration(template, tso_list):
    total = 0
    for tso in tso_list:
        # operacii iz check-lista
        for item in template.checklist_template.items:
            if item.uslovie_aktivacii is null OR item.uslovie_aktivacii in tso.faktory:
                total += resolveDuration(item.operation, tso.tso_type_id)
        # avto-operacii ot faktorov (per-instance, bez deduplikacii)
        for faktor in tso.faktory:
            for avto in faktor.avto_operations:
                total += resolveDuration(avto.operation, tso.tso_type_id)
    return total

function resolveDuration(operation, tso_type_id):
    override = operation.duration_by_type.find(_.tso_type_id == tso_type_id)
    if override: return override.duration_minutes
    return operation.duration_minutes_default

Инварианты генератора

  1. Один ЗН = один физобъект. Никогда не смешивать ТСО разных физобъектов.
  2. ЗН ≤ 24ч. При превышении — bin-packing на N подгрупп (каждая ≤ 24ч).
  3. Идемпотентность. Повторный запуск не создаёт дубликатов (проверка по template_id × scheduled_at × physobject_id).
  4. Чек-листы инлайнятся при создании ЗН. Снимок чек-листа сохраняется в PlannedWork (не REF), чтобы редактирование шаблона не задним числом ломало уже сгенерированные ЗН.

5. Граф «Фактор → авто-операции чек-листа»

[Diagram]

Порядок операций в ЗН:

  1. Сначала авто-операции от факторов (подготовительные: допуски, подъёмы).
  2. Потом операции из шаблона чек-листа (основные: «Протереть», «Проверить»).
  3. В конце — авто-операции от факторов с признаком «завершающие» (спуск).

Это управляется полем sort_order в TABLE_PART авто-операций фактора + смысловыми категориями операций.


6. Сценарий «10 камер, 3 на высоте, ежеквартальное ТО протереть»

Вводные

Расчёт

Камера Операция «Протереть» Авто от фактора Σ на 1 ТСО
#1-#7 (обычные) 30 мин 30 мин × 7 = 3ч 30мин
#8-#10 (на высоте) 30 мин 60 + 60 + 30 = 2ч 30мин × 3 =
Σ на физобъекте 12ч 30мин

12ч 30мин ≤ 24ч → один ЗН на физобъект.

PlannedWork:


7. Открытые вопросы (отложены до v2)

  1. Дедупликация авто-операций по location_group. Если 3 камеры на высоте на одной мачте — подъём один раз. Требует поля location_group на ТСО + умной группировки в computeTotalDuration.
  2. Spec-equipment планирование. Если ППР требует АГП (автогидроподъёмник), нужно его «забронировать» на дату — отдельный справочник «Спецтехника» + резервирование.
  3. Многодневные ЗН. Если пакет операций > 24ч на одном физобъекте — сейчас делим на N разовых ЗН. В будущем — «многодневный визит» с непрерывным выполнением.
  4. Сезонность факторов. Например «уличная» камера зимой требует доп. операции — это recurrence_rule фактора, не ТСО. Сейчас не предусмотрено.
  5. Биллинг по нормативу vs факту. Сравнение duration_hours_normative (договор) vs duration_hours_actual (по подписанным чек-листам в CHRONOS). Отчёт в PRISM.

8. Соответствие с текущими моделями PLANNER

Концепт документа Текущая модель PLANNER Изменения
Шаблон ППР PlanTemplate Переименовать в MaintenanceTemplate? Или оставить PlanTemplate как универсальный, а ППР-шаблоны хранить как справочник VAULT (тип directory)?
PlannedWork PlannedWork Добавить physobject_id, tso_instances (JSON snapshot), template_id на VAULT-справочник
PlannedWorkAssignee PlannedWorkAssignee Добавить conflicted BOOLEAN, conflictReason STRING (для CADRE C5)
recurrenceRule уже есть переиспользуем для MaintenanceTemplate.recurrence_rule
planGenerator уже есть cron расширить логикой группировки по физобъекту + bin-packing 24ч

Главный архитектурный выбор для реализации Phase 6+:

MaintenanceTemplate — это справочник VAULT (directory), а не модель PLANNER. PLANNER читает его через NEXUS-провайдер.

Текущий PlanTemplate (модель в backend/modules/planner/models/) остаётся как универсальный шаблон планируемых работ для случаев, когда автоматическая генерация по справочнику не применима (разовые работы, аварийные работы и т.д.). ППР как декларативная конфигурация — переезжает в VAULT.

Это требует:

  1. Создать в VAULT справочник Шаблоны ППР (через сидер).
  2. NEXUS-binding PLANNER.maintenanceTemplate → справочник.
  3. Адаптировать planGenerator чтобы он читал шаблоны через nexus.resolve('PLANNER.maintenanceTemplate').getEntries().
  4. Опубликованный ЗН → справочник Заказ-Наряды (Phase 6, через NEXUS).

9. План реализации (не для этого документа, но для связки с roadmap)

Этап Plan-A: справочники в сидере

  1. Создать в FSM-сидере справочники: Договор, Пакет обслуживания, Физобъект, Тип ТСО, Элемент ТСО, Фактор, Операция, Шаблон чек-листа, Шаблон ППР, SLA.
  2. Добавить демо-данные для сценария «ТЭЦ-14, 10 камер, 3 на высоте».
  3. Добавить enum-группы (тип работ, периодичность).

Этап Plan-B: Phase 6 PLANNER (publish)

  1. NEXUS-binding PLANNER.workOrderDocument → справочник Заказ-Наряды.
  2. NEXUS-binding PLANNER.maintenanceTemplate → справочник Шаблоны ППР.
  3. В publishBulk создавать запись в Заказ-Наряды через NEXUS, заполнять workOrderId.
  4. Поля conflicted / conflictReason на PlannedWorkAssignee (inline-миграция).

Этап Plan-C: новый planGenerator

  1. Адаптировать чтение шаблонов через NEXUS (а не через PlanTemplate модель).
  2. Добавить группировку ТСО по физобъекту + bin-packing 24ч.
  3. Резолв длительностей через трёхуровневую матрицу.
  4. Авто-операции от факторов.

Этап Plan-D: CADRE-C5 интеграция

  1. Хук onPlannedWorkAssigneeAdding — звонок в CADRE.
  2. CADRE: conflictDetector.js, эндпоинты /cadre/conflicts.
  3. UI ConflictsFeedView — реализация (сейчас заглушка).

Этап Plan-E: PLANNER Phase 7 (AUTOMATON триггеры)

  1. Хуки onPlannedWorkPublished, onPlannedWorkOverdue, onPlannedWorkCompleted.
  2. AUTOMATON ноды для PLANNER.

Этап Plan-F: PLANNER Phase 8 (RoleConfig)

  1. UI PlannerRoleConfigView (модель уже есть).
  2. Scope-фильтры в endpoints.

10. План правки FSM-сидера (Plan-A детально)

Сидер находится в backend/seeds/data/fsm-service-company.gdd (зашифрованный) + .json (расшифрованный, может быть устаревшим). Перед правкой — scripts/decrypt-seed.js (актуализировать .json).

Новые справочники для добавления

Справочник kind Иерархия Демо-записей минимум Зависимости
Договор document нет 2 (на двух разных заказчиков) Заказчик, Регион
Пакет обслуживания directory нет 3 (СКУД на ТЭЦ-14, Видео на ТЭЦ-14, СКУД на офисе) Договор, SLA
Физобъект directory нет 4 (ТЭЦ-14 главный корпус, ТЭЦ-14 склад, офис Лужники, офис Тверская) Заказчик, Регион
Тип ТСО directory да (folders+items) 8: Камера → Камера на высоте, Камера обычная; СКУД → Турникет, Контроллер; ОПС → Извещатель
Элемент ТСО directory нет 14 (10 камер на ТЭЦ-14 — 3 на высоте + 7 обычных, 2 турникета на ТЭЦ-14, 2 турникета в офисе) Физобъект, Тип ТСО, Фактор
Фактор обслуживания directory нет 4 («на высоте», «во взрывоопасной зоне», «уличная установка», «доступ ограничен») Операция
SLA directory нет 3 («8x5 standard», «24x7 critical», «Working hours response 4h»)
Операция directory нет 10 («Протереть», «Проверить разъёмы», «Проверить угол», «Оформить наряд-допуск», «Подъём с подготовкой», «Спуск», «Очистка от пыли», «Проверка крепежа», «Проверка изоляции», «Тест работоспособности») Тип ТСО (для override)
Шаблон чек-листа directory нет 3 («ТО камеры — очистка», «ТО камеры — проверка», «ТО турникета — квартальное») Операция, Фактор
Шаблон ППР (MaintenanceTemplate) directory нет 4 («Ежеквартальная очистка камер ТЭЦ-14», «Ежемесячная проверка камер ТЭЦ-14», «Ежеквартальное ТО турникетов ТЭЦ-14», «Ежеквартальное ТО турникетов офис») Пакет, Тип ТСО, Чек-лист

Новые ENUM-группы

Группа Значения
Тип работ ППР ppr (ППР), repair (Ремонт), inspection (Обследование), emergency (Аварийный)
Периодичность (для UI-выбора) weekly, monthly, quarterly, semiannual, yearly
Статус ТСО active, decommissioned, under_repair, planned_install
Mode состава ТСО в Пакете auto, manual_include, manual_exclude

Поля справочников (детально)

Договор

Пакет обслуживания

Физобъект

Элемент ТСО

Тип ТСО (hierarchical)

Фактор обслуживания

SLA

Операция

Шаблон чек-листа

Шаблон ППР

Демо-сценарий «ТЭЦ-14, 10 камер, 3 на высоте»

Заказчики:
  [Z1] АО ТЭЦ-14
  [Z2] ООО Лужники-офис

Регионы:
  [R1] Москва (есть в сидере)

Договоры:
  [D1] Договор обслуживания 2026/01 (заказчик Z1, c 2026-01-01)

Пакеты:
  [P1] ТО видеонаблюдения на ТЭЦ-14 (договор D1, SLA «8x5 standard»)
       physobjects: [PH1, PH2]
       tso_composition: mode=auto

Физобъекты:
  [PH1] ТЭЦ-14 главный корпус (заказчик Z1, регион R1)
  [PH2] ТЭЦ-14 склад (заказчик Z1, регион R1)

Типы ТСО:
  [TT1] Камера (parent=null)
    [TT2] Камера на высоте (parent=TT1)
    [TT3] Камера обычная (parent=TT1)

Факторы:
  [F1] На высоте
       avto_operations: [OP_DOPUSK, OP_PODYOM, OP_SPUSK]

Операции:
  [OP_PROTERET]   Протереть
                  duration_minutes_default = 30
                  duration_by_type: (TT2: 30)  ← переопределение для камеры на высоте
  [OP_PROVERIT]   Проверить разъёмы
                  duration_minutes_default = 10
  [OP_DOPUSK]     Оформить наряд-допуск (60 мин)
  [OP_PODYOM]     Подъём с подготовкой (60 мин)
  [OP_SPUSK]      Спуск (30 мин)

Элементы ТСО (на PH1):
  [TSO1..TSO7]   камеры обычные (tso_type=TT3, факторов нет)
  [TSO8..TSO10]  камеры на высоте (tso_type=TT2, factors=[F1])

Шаблоны чек-листов:
  [CL1] ТО камеры — очистка
        items: [{operation: OP_PROTERET, sort_order: 1}]

Шаблоны ППР:
  [MT1] Ежеквартальная очистка камер ТЭЦ-14
        paket=P1, type=ppr, tso_type=TT1 (Камера — общий тип, иерархия покроет TT2 и TT3)
        checklist_template=CL1
        recurrence_rule={type:'quarterly', dayOfMonth:15, timeOfDay:'09:00'}
        duration_hours_normative=12.5
        duration_hours_calculated=COMPUTED

Расчёт duration_hours_calculated для MT1:

Идемпотентность сидера

Все справочники добавляются через seed.directory resolver — идемпотентно по code (там где есть) или по name + parent (там где нет code). Перезапуск сидера не должен создавать дубликаты.

Что не входит в Plan-A сидера


Конец документа.