doc: аудит Fable 5 + моё мнение (Opus vs Fable)

This commit is contained in:
2026-07-31 23:18:42 +04:00
parent f470b79ca1
commit bddbdfb63b
3 changed files with 210 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
# План: Decouple polygon v0.2.1
Цель: разнести монолитный `app.py` (400 строк, 17 роутов) на отдельные blueprint-файлы
по шаблону app-autotest. Никакого инлайн-CSS/HTML — всё в `static/` и `templates/`.
## Текущее → Целевое
```
polygon/site/
├── app.py (400 строк, всё в одном)
├── mock_state.py
├── state_machine.py
├── config_loader.py
└── from_stands.py
polygon/site/
├── app.py # ТОЛЬКО: Flask(), register_blueprint, /health, app.run()
├── routes/
│ ├── root.py # GET / (render_template + style.css)
│ ├── services_routes.py # GET /api/v1/svc/services, GET /services/<id>
│ ├── instances_routes.py # GET/POST /api/v1/svc/instances, GET /instances/<uid>
│ ├── operations_routes.py # POST /instanceOperations, GET default, GET status, POST params, GET validate
│ ├── run.py # POST /instanceOperations/<uid>/run
│ └── mock_routes.py # POST /_mock/reset, GET /_mock/state, GET /_mock/services, POST /_mock/delay
├── state/
│ ├── mock_state.py # MockState (без изменений)
│ └── state_machine.py # apply_effect (без изменений)
├── config/
│ └── loader.py # load_services() (бывший config_loader.py)
├── converter/
│ └── from_stands.py # конвертер (без изменений)
├── utils/
│ ├── pluralize.py # _pluralize() — одна функция
│ └── now.py # _now() — одна функция
├── static/
│ └── style.css # CSS из index() — тёмная тема
├── templates/
│ └── index.html # HTML из index() — Jinja2 с {{ VERSION }}
└── services/
└── ...yaml # без изменений
```
## Пошагово
### Шаг 1. Утилиты
- `utils/now.py``_now()` из app.py (одна функция)
- `utils/pluralize.py``_pluralize()` из state_machine.py (одна функция)
### Шаг 2. CSS и HTML
- `static/style.css` — вынести инлайн-CSS из f-строки `index()`
- `templates/index.html` — вынести HTML из f-строки, использовать Jinja2 `{{ version }}`, `<link rel="stylesheet">`
### Шаг 3. Маршруты — 6 blueprint-файлов
Каждый blueprint импортирует нужные модули из `state/`, `config/`, `utils/`.
- `routes/root.py``bp = Blueprint("root", __name__)`, роут `/` с `render_template`
- `routes/services_routes.py``bp = Blueprint("services", __name__)`, 2 роута
- `routes/instances_routes.py``bp = Blueprint("instances", __name__)`, 3 роута
- `routes/operations_routes.py``bp = Blueprint("operations", __name__)`, 5 роутов
- `routes/run.py``bp = Blueprint("run", __name__)`, 1 роут
- `routes/mock_routes.py``bp = Blueprint("mock", __name__)`, 4 роута
### Шаг 4. app.py — только скелет
```python
import os
from flask import Flask
from routes.root import bp as root_bp
from routes.services_routes import bp as services_bp
# ... все 6 blueprint'ов
from config.loader import load_services
os.environ.setdefault("WEB_CONCURRENCY", "1")
VERSION = "0.2.1"
MOCK_OP_DELAY = float(os.getenv("MOCK_OP_DELAY", "0.1"))
app = Flask(__name__, template_folder="templates", static_folder="static")
app.register_blueprint(root_bp)
app.register_blueprint(services_bp)
# ... все 6
@app.route("/health")
def health():
return "OK"
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=5000)
```
### Шаг 5. Импорты в state_machine и from_stands
- `state_machine.py`: `from utils.pluralize import pluralize` (вместо `_pluralize()` внутри)
- `from_stands.py`: импортирует `pluralize` из `utils/`
## Что НЕ меняется
- `state/mock_state.py` — как есть
- `state/state_machine.py` — только импорт `pluralize`
- `converter/from_stands.py` — только импорт `pluralize`
- `services/*.yaml` — данные
- `tests/` — только поправить пути импорта (site → site.state и т.д.)
## Верификация
1. `python -m py_compile` на ВСЕХ .py файлах
2. `python -c "from app import app; [print(r.rule) for r in app.url_map.iter_rules()]"` → те же 17 роутов
3. `pytest tests/ -v` → 19/19 PASS
4. Запустить `python app.py` → curl /health, /services, /instances → работает
@@ -0,0 +1,62 @@
# Архитектурный аудит polygon v0.2.5 — ответ Claude Fable 5
Дата: 2026-07-31
## Итоговая оценка
Архитектура соразмерна задаче и в целом здоровая. Blueprint'ы, data-driven конфиги, единый config/loader.py, синхронный run — правильный выбор. Код читаемый, докстринги честные, критичные контракты задокументированы.
Главный структурный риск: сервис задеплоен как публичный shared-эндпоинт, но спроектирован как эксклюзивный однопользовательский стенд.
---
## Находки
### 🔴 1. Основной API полностью открыт на публичном домене
MOCK_AUTH_TOKEN защищает только /_mock/*. POST /instances, /instanceOperations, run — без проверки.
### 🔴 2. sleep(DELAY) блокирует /health → риск рестарта контейнера
1 sync-воркер, sleep в run замораживает весь сервер включая /health. Комбинация delay=30 + несколько run → healthcheck не отвечает → Nubes рестартит под → состояние потеряно.
### 🔴 3. Гарантия «1 воркер» через setdefault — не работает
os.environ.setdefault("WEB_CONCURRENCY", "1") выполняется при импорте app, а gunicorn читает WEB_CONCURRENCY при старте мастера — до импорта. Если платформа выставит WEB_CONCURRENCY=4, будет 4 воркера и 4 несвязанных MockState.
### 🟡 4. 500-ки на невалидном вводе
set_param: int(param_id) без try → ValueError → 500.
/_mock/delay: float("garbage") → 500. Отрицательные значения принимаются молча.
### 🟡 5. _extract_subresource_name — хрупкий
Берёт первое непустое строковое значение. Порядок = порядок set_param. Если executor отправит пароль раньше имени — subresource назовётся паролем.
### 🟡 6. Мок слишком «добрый»
- validate-cfs всегда 200 (required не проверяются)
- modify принимает isModifiable: false
- операция на creating-инстансе (modify до завершения create)
- svcOperationId не сверяется с serviceId инстанса
- isSuccessful всегда True — негативные сценарии нельзя тестировать
### 🟢 7. Мелочи
- operations/op_params не чистятся (кроме reset)
- stages: [] всегда пуст
- total в пагинации — проверить с реальным API
---
## Ответы на вопросы
**Q1. In-memory vs Redis.** Redis не нужен. Потеря состояния при рестарте — фича.
**Q2. Параллельные тесты.** (A) polygon per-session (рекомендовано) или (B) namespace-изоляция.
**Q3. Генерация YAML при старте.** Нет. Оффлайн + drift-check тест.
**Q4. Мониторинг.** Prometheus избыточен. Достаточно логов + /_mock/state.
**Q5. Готовность к CI.** К последовательному — готов. Блокеры: auth API (1), cap DELAY (2), симуляция ошибок (6).
## Приоритеты
1. Токен на весь API + cap DELAY — дёшево, закрывает оба 🔴
2. /_mock/fail-next — открывает негативные тесты
3. Runtime-проверка воркеров + фикс int(param_id)
4. Модель владения (per-session) — отложить
+34
View File
@@ -0,0 +1,34 @@
# Моё мнение — сравнение аудитов Опуса и Fable 5
Дата: 2026-07-31
## Кто что нашёл
| Находка | Опус | Fable 5 |
|---------|------|---------|
| Auth на основном API | 🔴 | 🔴 |
| sleep блокирует /health → pod restart | — | 🔴 |
| setdefault не работает (gunicorn timing) | — | 🔴 |
| int(param_id) без try → 500 | — | 🟡 |
| modify на creating-инстансе | — | 🟡 |
| Публичный деплой vs single-user | 🔴 | — |
| _extract_subresource_name хрупкий | 🟡 | 🟡 |
| Память не чистится | 🟡 | 🟢 |
| Нет симуляции ошибок | 🟡 | 🟡 |
| Синхронный sleep vs ленивый dtFinish | 🟡 | — |
| stages: [] всегда пуст | — | 🟢 |
## Оценка
**Опус** — архитектор. Смотрит на систему сверху: «правильно ли спроектировано под задачу?». Нашёл структурный разрыв (публичный деплой vs однопользовательский дизайн), дал стратегические рекомендации (per-session модель).
**Fable 5** — инженер. Смотрит на код снизу: «что сломается при эксплуатации?». Нашёл три бага которые никто не заметил:
- `setdefault("WEB_CONCURRENCY", "1")` — не работает (gunicorn читает env ДО импорта app)
- `sleep(DELAY)` блокирует `/health` → если delay > healthcheck timeout → Nubes рестартит под
- `int(param_id)` без try → 500 на кривом JSON
## Что делать
Приоритет Fable прагматичнее: две строчки кода (cap DELAY + токен) спасут от краша пода. Приоритет Опуса стратегический: per-session модель нужна для параллельного CI, но это потом.
**Порядок:** Fable → Опус. Сначала закрыть риски эксплуатации, потом архитектурные улучшения.