Files
tf_provider/HISTORY/OPUS/2026-09-22_modifier_architecture_project.md
T
Repinoid 2d8e435dd4 docs: пометить отменённый заход модификаторов как LEGACY + исправить ложные факты
- баннеры «ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО» на 4 файла HISTORY/OPUS/2026-09-22_modifier_* и docs/60_strategy/modifier_resources_ideology_and_specification.md
- vIPConfigure: replace-семантика, НЕ накопительная (по тесту docs/ORG_IP_MODIFIER_TEST_2026-09-22.md)
- обновлены ссылки на перенесённые материалы (docs/... -> NOTES/..., HOW_TO/...)
2026-09-24 07:51:25 +03:00

15 KiB
Raw Blame History

⛔ ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ. Архитектура отменённого захода (kind: modifier в YAML + реестр в генераторе). История. Актуально: NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md.

Opus: архитектура модификаторов (project, полный) — 2026-09-22

Источник: ответ Opus на prompt_for_opus_modifier_architecture_full.md.

Ключевая модель

Модификатор — декларативная проекция подмножества полей родителя, а не «действие». Отсюда:

  • один reconcile (Create ≡ Update), не два разных пути;
  • payload всегда полный по своим полям (не дельта);
  • источник истины — родитель; модификатор в state хранит только read-back.

1. Сравнить и применить — полный payload, не дельта

Дельта запрещена: бэкенд трактует отсутствующий параметр как reset-to-default (класс A). Reconcile:

  1. взять все SchemaParams;
  2. заданные пользователем → значение из плана;
  3. незаданные → live → default (уже в operation_run_bycode.go);
  4. drift в Read — сравнение модели с state_params родителя (state_refresh.go).

2. Досылка незаданных (заливы A и B) — канон

CompactParams в шаблоне + досылка в клиенте — два конца одного бага. Правило по приоритету (уже в operation_run_bycode.go:98-118):

Ситуация Что слать
задан пользователем значение из плана
не задан, есть live ParamValue live
не задан, нет live, есть DefaultValue дефолт
не задан, ничего нет пропустить (не синтезировать)

Дыра: CompactParams в modifier.go:92 выкидывает пустые ДО клиента (теряется «задал пусто» vs «не задал»). → Убрать CompactParams из шаблона модификатора, передавать map напрямую. Единственная точка решения — клиент. CompactParams оставить только для instance-ресурсов.

3. Delete / rollback

No-op Delete = скрытый drift (класс D). Пока обратный payload не подтверждён — допустимы 3 стратегии через флаг YAML delete_strategy:

  1. noop_warn — удалить из state + AddWarning (дефолт для необратимых: ip_space);
  2. inverse — если есть «выключающие» значения в modify (напр. needEnableAVI:false);
  3. error — запретить destroy (AddError), если откат критичен.

Обратный payload — та же modify с выключающими значениями. Для ip_space его нет → только noop_warn.

4. Idempotency + ID

  • ID = идентичность (родитель + имя модификатора) = instanceUID:modifierName. Это правильно и не должен меняться per-apply. opUid в ID не класть (иначе replace). opUid — только в лог/приватный state.
  • Двойная аллокация (класс E) защищается не ID, а идемпотентностью modify: pre-check «desired == current» → пропустить run. Для ip_space перед modify читать state_params; если целевое достигнуто — skip.

5. Связь с родителем

  • <service>_id — ссылка на родителя (Required, уже так). depends_on не нужен — пользователь передаёт UUID.
  • Borrow state не нужен: Read тянет state_params родителя по UUID.
  • Родителя нет (ShouldRemoveFromState) → модификатор удаляется из state (уже есть).

6. Create vs Update

Единый reconcile(ctx, plan); Create и Update вызывают его (устраняет дубль веток).

7. Полный перечень кейсов (13 шт)

# Кейс Поведение
1 create родителя → create модификатора reconcile, полный payload
2 изменение одного поля полный payload, соседние не сбрасываются (A)
3 partial params досылка live→default→skip (B)
4 integer > 0 без значения/дефолта пропустить (не слать "0")
5 is_modifiable:true (needEnableAVI) не CreateOnly, менять без replace (C)
6 destroy модификатора по delete_strategy (D)
7 replace/taint reconcile + idempotency pre-check (E)
8 повторный apply без изменений desired==current → skip
9 родитель удалён remove из state
10 API не вернул код в state_params unknown→null (уже)
11 два модификатора разных типов разные ID
12 operation in progress waitForInstanceIdle (уже)
13 drift на платформе Read → план показывает изменение

Сводка мест правки

Место Правка
modifier.go:92 убрать CompactParams → прямой map (п.2)
modifier.go:77 единый reconcile() (п.6)
modifier.go:156 delete_strategy (п.3)
modifier.go:100 ID = instanceUID:modifierName (п.4)
RunOperationByCodeWithTimeout / reconcile idempotency pre-check (п.4,7)
params.go:116 учитывать modifier-канал/is_modifiable (класс C, кейс 5)
loader модификаторов YAML-поля delete_strategy, idempotency
operation_run_bycode.go оставить как есть (guard корректен)

Открытые вопросы к Opus (не закрыты ответом)

  1. Где брать значения для inverse-стратегии Delete? Для network «выключающие» значения — это хардкод per-modifier? Как их задать декларативно в YAML, без хардкода в генераторе?
  2. Формат YAML новых полей. Точная схема delete_strategy и idempotency: enum-значения, дефолты, валидация (fail-fast на неизвестных).
  3. Pre-check «desired == current» — где читать current? Через RefreshResourceState/state_params или отдельный GET? Как сериализовать сравнение для map-fixed/array-map-fixed (порядок ключей)?
  4. Как пометить модификатор «idempotency: check_before_run» на уровне YAML (а не хардкодом в коде reconcile)?
  5. Что если желаемое == текущее, но была «частичная» ошибка ранее — пропускать run безопасно всегда, или есть исключения?

Ответы Opus №2 (уточнения по 5 вопросам)

1. inverse-Delete — только декларативно в YAML, хардкод запрещён

Обратный payload зависит от параметров: needEnableAVI:false валиден, а virtualServicesCount (integer > 0) обнулить нечем → 0 невозможен. Значит inverse-значения задаются явным блоком в YAML. Если хоть один параметр не имеет валидного inverse — стратегия inverse недопустима (fail-fast в загрузчике). Для ip_space inverse нет вообще → только noop_warn.

2. Точная схема YAML новых полей

operations:
  - kind: modifier
    modifier: network
    action: modify
    delete_strategy: noop_warn      # enum: noop_warn | inverse | error
    idempotency: check_before_run   # enum: none | check_before_run
    delete_params:                  # обязателен ТОЛЬКО при delete_strategy: inverse
      - code: needEnableAVI
        value: "false"
    params: [...]

Go-контракт (OperationSpec):

DeleteStrategy string      `yaml:"delete_strategy,omitempty"` // "" → noop_warn
Idempotency    string      `yaml:"idempotency,omitempty"`     // "" → none
DeleteParams   []ParamSpec `yaml:"delete_params,omitempty"`

Дефолты: delete_strategy → noop_warn; idempotency → none. Fail-fast в ValidateSpec: значение вне enum → ошибка; inverse с пустым delete_params → ошибка; delete_params.code нет в params → ошибка; inverse-значение нарушает constraint параметра → ошибка на этапе генерации. GenModifier получает DeleteStrategy, Idempotency, DeleteParams.

3. Откуда читать current + как сравнивать

Читать из state_params, отдельный GET не делать (это уже источник истины для Read; второй источник = риск рассогласования).

Сравнение по типу:

Тип Как сравнивать
bool/int/string равенство после normalizeUniversalValueV6
map-fixed JSONStringsEquivalent (игнор порядка ключей)
array-map-fixed deep-equal с сохранением порядка элементов (порядок значим)

Порядок ключей map-fixed — нормализовать (не значим). Порядок элементов array-map-fixed — НЕ нормализовать (значим).

4. idempotency декларативно

Поле idempotency на modify-операции в YAML → ValidateSpec → GenModifier.Idempotency → шаблон modifier.go в reconcile() эмитит pre-check {{- if eq .Idempotency "check_before_run" }}. Для ip_space — в YAML; для остальных — дефолт none.

5. Когда безопасно skip run при desired == current (НЕ всегда)

Три условия безопасного skip:

  1. Инстанс idle — если pending/in-progress, сначала waitForInstanceIdle, потом перечитать state_params (иначе mid-flight аллокация даст ложное «уже равно»).
  2. current из живого state_params, НЕ из TF-state — после частичной ошибки TF-state может врать, а state_params отражает реальную платформу.
  3. Сравнение по всем полям, не по одному — skip только при совпадении ВСЕХ полей.

Итог:

idle? нет → wait, re-read
всё-live == всё-desired? да → skip run
иначе → reconcile (полный payload)

Ответы Opus №3 (сверка с фактическим кодом, расхождения + сомнения)

Факт №1: контракт — в lib, поведение — в GenModifier

Подтверждено: delete_strategy/idempotency/delete_params добавляются в lib.OperationSpec (TOOLS/lib/types.go), resource-generator получает через алиас (types.go:19). Ссылка «types.go:39» была неточной — канон в lib.

Граница:

Где Что
lib.OperationSpec / lib.ParamSpec всё из YAML, видно обоим генераторам
GenModifier (локально) производные для шаблона, флаги Needs*, готовый inverse-список

Правило: парсится из YAML → lib; вычисляется загрузчиком для шаблона → GenModifier.

Факт №2: pre-check — в core (вариант A), экспортировать сравнение

normalizeUniversalValueV6 приватная и требует universalCfsParam — в resources_core этих данных нет. Pre-check делать в core, не в resources_core и не в шаблоне.

Конкретно — экспортированный метод в core, вызывается из operation_run_bycode.go сразу после fetchOperationCfsParams:

func (c *UniversalClient) modifierDesiredEqualsCurrent(
    desired map[string]string, cfsParams []universalCfsParam) bool

Сравнение: нормализовать обе стороны через normalizeUniversalValueV6; для map-fixed/array-map-fixed — JSON-эквивалентность. Но JSONStringsEquivalent лежит в resources_core → импорт в core даст цикл. Вынести JSON-эквивалентность в нейтральный пакет (core/jsonutil или в сам core) и переиспользовать в обоих местах.

Вариант C (только JSONStringsEquivalent без нормализации) — отклонён (ложный diff true/1).

Управление: RunInstanceOperationUniversalByCode получает флаг idempotent (из GenModifier.Idempotency → шаблон → параметр вызова); idle-гейт выше pre-check.

Дополнительные сомнения (ответы)

  1. Idempotency и полный payload НЕ конфликтуют (разные уровни). Бинарно на весь модификатор: ALL == ALL → skip целиком; любое расхождение → полный payload. Полудельты нет.
  2. Частичный inverse — допустим и правилен. delete_params покрывает только обратимые поля; необратимые/constraint просто не входят. Fail-fast смягчить: ошибка не «inverse обязан покрыть всё», а «код в delete_params обязан существовать в params и value удовлетворять constraint». Delete при inverse = modify с delete_params + досылка live остальных (полный payload).
  3. noop_warn дефолт — оставить, но критичные — вручную error. Дефолт мягкий (noop_warn, всегда с AddWarning), а необратимые (ip_space) автор спеки явно помечает delete_strategy: error в YAML. Генератор сам не решает обратимо/необратимо.