# Sonnet: код-ревью contracts-flask (2026-08-26) ## Контекст - Написан промпт для Sonnet: `contracts-flask/docs/prompt-sonnet-architecture.md` (задача — прочитать файлы текущего сервиса сверки и дать описание/архитектуру). - Sonnet прочитал все указанные файлы и вместо описания выдал **код-ревью** с 10 находками (2 критические, 3 средних, 2 XSS, 3 мелких). Архитектурное описание — отдельным документом (не выдано). ## Находки Sonnet ### 🔴 Критические 1. **`process.py` + `spec_events.py` — UPDATE/DELETE никогда не применяются (сломана частичная сверка).** LLM возвращает `target_id: "r1"` (индекс строки), а `apply_ops()` читает `op.get("target_hash", "")`. Поля `target_hash` в ответе LLM нет → все UPDATE/DELETE из `mode=partial` молча уходят в `UNRESOLVED`. Трассировка: `_build_spec_text()` показывает строки `[id: r1]` → LLM возвращает `target_id: "r1"` → `process.py` передаёт ops без трансляции r1→hash → `spec_events.py` `th = op.get("target_hash","")` = "" → UNRESOLVED. Работает только `mode=full_replace` (все ADD) и первый документ (extract, только ADD). 2. **`Dockerfile` — образ не запустится: `upload/` не скопирован.** `COPY site /app/site` — только site/, `upload/` отсутствует. При старте `routes/__init__.py` делает `from upload.backend.upload_refs import ...`, `app.py` добавляет `/app` в sys.path. В образе `/app/upload/` нет → `ModuleNotFoundError`. ### 🟠 Средние 3. **`db/prompts.py` — `list_by_role()` запрашивает несуществующий столбец `created_by`.** В схеме `prompts` нет `created_by` → `sqlite3.OperationalError` → `/api/prompts/list` всегда 500. 4. **`routes/prompts_bp.py` — вызов несуществующих функций:** - `db_prompts.insert(...)` — нет (есть `save_new_version()`); - `db_prompts.delete(...)` — нет (есть `delete_prompt()`). `/api/prompts/save` и `/api/prompts/delete` падают с `AttributeError`. 5. **`services/llm.py`, `services/classify.py` — захардкожены `LLM_URL/LLM_KEY/LLM_MODEL` в обход `config.py`.** `config.py` читает из `LLM_API_URL`, а compare/classify игнорируют его. Конфигурация расщеплена на 3 блока. ### 🟡 XSS (frontend) 6. **`compare.js` — `nr.name` в таблице операций не экранирован** (`escHtml()` отсутствует). Имя услуги из договора (текст LLM) вставляется в innerHTML → XSS. 7. **`compare.js` — `sec.filename` в заголовке секции не экранирован.** Имя файла от пользователя → XSS. ### ⚪ Мелкие 8. **`llm_prompt.py`** — устаревший комментарий «Lucee API» (читает напрямую из SQLite). 9. **`db/connection.py`** — `_pg_to_sqlite`: `gen_random_uuid()` добавляет `?` в SQL, но не добавляет значение в params (мёртвый код, но при исполнении сломает запрос). 10. **`services/classify.py`** — комментарий «re-classify after file changes» вводит в заблуждение: `reset_classify_status()` сбрасывает только `processing`→`pending`, уже классифицированные не трогает. ## Итоговая таблица | # | Файл | Проблема | Критичность | |---|---|---|---| | 1 | process.py, spec_events.py | UPDATE/DELETE → UNRESOLVED, частичная сверка сломана | 🔴 критический | | 2 | Dockerfile | upload/ не скопирован → образ не стартует | 🔴 критический | | 3 | db/prompts.py | created_by отсутствует → 500 на /api/prompts/list | 🟠 средний | | 4 | prompts_bp.py | insert()/delete() — не те имена функций → 500 | 🟠 средний | | 5 | llm.py, classify.py | захардкожены LLM_URL/KEY/MODEL в обход config.py | 🟠 средний | | 6 | compare.js | nr.name не экранирован → XSS | 🟡 XSS | | 7 | compare.js | sec.filename не экранирован → XSS | 🟡 XSS | | 8 | llm_prompt.py | устаревший комментарий | ⚪ мелкое | | 9 | connection.py | gen_random_uuid() мёртвый код с ошибкой | ⚪ мелкое | | 10 | classify.py | комментарий re-classify вводит в заблуждение | ⚪ мелкое | ## Статус - ✅ Все 10 находок исправлены (v2.0.15, commit `e3f5581`, запушено). - #1 (критично) — сам: `process.py` трансляция `target_id`→`target_hash`. - #2–#10 (механика) — Флаш-субагент: Dockerfile, prompts CRUD, XSS, конфиг LLM, комментарии. - Верифицировано: `py_compile`, `node -c`, `import app` — OK. - Архитектурное описание от Sonnet — так и не получено (вместо него — ревью). Если нужно — отдельным запросом.