docs(opus): задокументировать архитектуру модификаторов + открытые вопросы
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
# 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
|
||||
безопасно всегда, или есть исключения?
|
||||
Reference in New Issue
Block a user