7.3 KiB
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:
- взять все
SchemaParams; - заданные пользователем → значение из плана;
- незаданные → live → default (уже в
operation_run_bycode.go); - 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:
noop_warn— удалить из state +AddWarning(дефолт для необратимых:ip_space);inverse— если есть «выключающие» значения в modify (напр.needEnableAVI:false);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 (не закрыты ответом)
- Где брать значения для
inverse-стратегии Delete? Дляnetwork«выключающие» значения — это хардкод per-modifier? Как их задать декларативно в YAML, без хардкода в генераторе? - Формат YAML новых полей. Точная схема
delete_strategyиidempotency: enum-значения, дефолты, валидация (fail-fast на неизвестных). - Pre-check «desired == current» — где читать current? Через
RefreshResourceState/state_paramsили отдельный GET? Как сериализовать сравнение для map-fixed/array-map-fixed (порядок ключей)? - Как пометить модификатор «idempotency: check_before_run» на уровне YAML (а не хардкодом в коде reconcile)?
- Что если желаемое == текущее, но была «частичная» ошибка ранее — пропускать run безопасно всегда, или есть исключения?