Both are Section B (needs-context): B11 touches the AnalysisResult contract and the question of where an LLM is needed at all; B12 touches scoring coefficients and the DB schema, which project rules keep off limits to automated edits. B11 records the key finding that the LLM does not compute priorities -- the math already lives in geo_service/scoring_service/psychotype_service and analyze_with_fallback already returns a full AnalysisResult with no API. The only unique in-hot-path LLM role is parsing free-text reports, so a strict intake form and a local provider are independent levers. B12 notes ISRID supplies empirical priors for the currently hand-tuned zone weights, with the legal status of the database flagged as a blocking question to resolve before any data enters the repo. Summary table updated to B1-B12. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
50 KiB
ВЕКТОР — очередь технического долга для делегирования
Проект: ВЕКТОР (Система определения приоритетных направлений поиска, SAR/МЧС)
Стек: FastAPI + PostgreSQL 16 + React (CRA), docker-compose
Расположение: LXC 108 (Lab, 192.168.0.99), путь /root/vector
Дата анализа: 2026-07-21
Состояние на момент анализа: git чистый, ветка master, 126 passed, 26 skipped (async-тесты пропущены — нет pytest-asyncio).
Как пользоваться этим файлом
- Раздел A — ATOMIC. Задачи для локальной LLM (Qwen2.5-coder, 12GB VRAM, ~16–32k контекста). Каждый промт самодостаточен: весь нужный код встроен в промт, модель НЕ читает остальной проект. Бери задачу, копируй блок
PROMPTцеликом в модель, применяй ответ, проверяй поКритерий приёмки. - Раздел B — NEEDS-CONTEXT. НЕ давать локальной модели. Требуют понимания нескольких файлов, контрактов данных, роутинга или бизнес-логики SAR. Оставлены для более сильной модели / человека.
Жёсткие исключения (соблюдены во всём файле)
Ни одна ATOMIC-задача не трогает:
- ❌ алгоритм зонального скоринга (7 факторов) — весь
backend/services/scoring_service.pyвынесен в раздел B; - ❌ схему БД / миграции (
backend/models.py,backend/migrations/*); - ❌ auth/security (
backend/routers/auth.py, JWT, bcrypt, роли); - ❌ бизнес-логику МЧС/SAR (коэффициенты дистанции, поведенческие профили, психотипы как методику).
РАЗДЕЛ A — ATOMIC (для локальной LLM)
Отсортировано по модулю. Внутри модуля — по порядку файла.
A1 — backend/services/distance_service.py · защита от None
- Файл / строки:
backend/services/distance_service.py:111-121,148-158,168-178 - Тип: missing validation
- Сложность: atomic
PROMPT (скопировать в модель целиком):
Ты правишь один Python-файл. Ниже три функции. У каждой первый аргумент — строка, и сразу вызывается `.lower()`. Если аргумент окажется None, будет AttributeError. Добавь защиту: если аргумент None или пустой, используй значение по умолчанию (для terrain → вернуть 0.5; для time_of_day → вести себя как 'день' → вернуть 1.0; для weather → вести себя как отсутствие осадков → вернуть 1.0). Не меняй числовые коэффициенты и словари. Верни ТОЛЬКО три изменённые функции целиком, в блоке ```python.
def get_terrain_coefficient(terrain: str) -> float:
terrain_lower = terrain.lower()
terrain_map = {
'лесная дорога': 0.8, 'сложный лес': 0.25, 'густой лес': 0.25,
'простой лес': 0.5, 'лес': 0.5, 'дорога': 0.8, 'тропа': 0.8,
'болото': 0.2, 'поле': 0.9, 'луг': 0.9, 'город': 1.0,
'населённый пункт': 1.0, 'горы': 0.3, 'овраг': 0.3
}
for key, value in terrain_map.items():
if key in terrain_lower:
return value
return 0.5
def get_time_of_day_coefficient(time_of_day: str) -> float:
time_lower = time_of_day.lower()
if 'ночь' in time_lower:
return 0.5
elif 'сумерки' in time_lower or 'вечер' in time_lower:
return 0.5
else:
return 1.0
def get_weather_coefficient(weather: str) -> float:
weather_lower = weather.lower()
if 'ливень' in weather_lower or 'сильный дождь' in weather_lower:
return 0.6
elif 'дождь' in weather_lower:
return 0.8
elif 'туман' in weather_lower:
return 0.7
elif 'снег' in weather_lower or 'метель' in weather_lower:
return 0.6
elif 'жара' in weather_lower:
return 0.8
else:
return 1.0
Критерий приёмки:
get_terrain_coefficient(None) == 0.5,get_time_of_day_coefficient(None) == 1.0,get_weather_coefficient(None) == 1.0.- Все существующие значения не изменились:
get_terrain_coefficient('густой лес') == 0.25,get_weather_coefficient('туман') == 0.7. PYTHONPATH=/root/vector python -m pytest backend/tests/test_distance_service.py -q— зелёный.
A2 — backend/services/claude_service.py · заменить print() на логирование
- Файл / строки:
backend/services/claude_service.py:66,111,247(и добавить импорт/логгер вверху файла, строки 5-9) - Тип: missing error handling (диагностика в stdout вместо логгера)
- Сложность: atomic
PROMPT:
В Python-файле три места пишут диагностику через print(). Замени их на стандартный модуль logging. Вверху файла (рядом с существующими импортами `import os`, `import json`) добавь `import logging` и создай модульный логгер `logger = logging.getLogger(__name__)`. Затем замени каждый print на соответствующий вызов logger. Для строк с ошибками используй logger.warning(...). Не меняй никакую другую логику. Верни ТОЛЬКО unified diff (формат `diff`), затрагивающий импорты и три строки.
Текущие строки:
(строка 66) — внутри except при недоступности Claude API:
print(f"Claude API unavailable: {e}. Using fallback scoring service.")
(строка 111) — внутри except geo/scoring в analyze_with_claude:
print(f"Geo/scoring service error: {e}")
(строка 247) — внутри except geo/scoring в analyze_with_fallback:
print(f"Geo/scoring service error in fallback: {e}")
Существующие импорты вверху файла:
import os
import json
from typing import Dict, List, Optional
from pydantic import BaseModel
import httpx
Критерий приёмки:
grep -n "print(" backend/services/claude_service.py→ пусто.- В начале файла есть
import loggingиlogger = logging.getLogger(__name__). PYTHONPATH=/root/vector python -c "import backend.services.claude_service"— без ошибок.
A3 — backend/services/claude_service.py · устаревший ID модели Claude
- Файл / строки:
backend/services/claude_service.py:157 - Тип: bug (несуществующая/устаревшая модель)
- Сложность: atomic
PROMPT:
В Python-файле в теле POST-запроса к Anthropic API указан устаревший идентификатор модели. Замени строковое значение модели с "claude-sonnet-4-20250514" на "claude-sonnet-5". Больше НИЧЕГО не меняй (max_tokens, заголовки, структуру messages оставь как есть). Верни ТОЛЬКО одну изменённую строку.
Текущая строка (157):
"model": "claude-sonnet-4-20250514",
Критерий приёмки:
- Строка 157 содержит
"model": "claude-sonnet-5",. grep -n "claude-sonnet-4" backend/services/claude_service.py→ пусто.
Примечание для проверяющего: актуальные ID на дату анализа —
claude-sonnet-5(баланс скорость/качество),claude-opus-4-8(максимум качества). Для JSON-анализа кейсаclaude-sonnet-5— разумный дефолт.
A4 — backend/routers/analyze.py · устаревший datetime.utcnow()
- Файл / строки:
backend/routers/analyze.py:3(импорт) и:106 - Тип: bug (deprecated API, начиная с Python 3.12
datetime.utcnow()deprecated) - Сложность: atomic
PROMPT:
В Python-файле используется устаревший `datetime.utcnow()`. Замени его на timezone-aware вариант `datetime.now(timezone.utc)`. Для этого:
1. В строке импорта `from datetime import datetime` добавь `timezone`: `from datetime import datetime, timezone`.
2. Замени `datetime.utcnow().isoformat()` на `datetime.now(timezone.utc).isoformat()`.
Больше ничего не меняй. Верни ТОЛЬКО две изменённые строки с указанием, какую на какую.
Текущий импорт (строка 3):
from datetime import datetime
Текущая строка (106), внутри словаря result:
'analyzed_at': datetime.utcnow().isoformat(),
Критерий приёмки:
grep -n "utcnow" backend/routers/analyze.py→ пусто.- Импорт содержит
timezone. PYTHONPATH=/root/vector python -c "import backend.routers.analyze"— без ошибок.
A5 — backend/main.py · миграция @app.on_event('startup') → lifespan
- Файл / строки:
backend/main.py:38-40 - Тип: bug (deprecated в текущих версиях FastAPI/Starlette)
- Сложность: atomic
PROMPT:
В FastAPI-приложении используется устаревший декоратор `@app.on_event('startup')`. Перепиши его на современный lifespan-контекст. Требования:
1. Добавь `from contextlib import asynccontextmanager` в импорты.
2. Создай асинхронную функцию lifespan, которая на старте вызывает init_db(), затем yield.
3. Передай её в конструктор: `app = FastAPI(title='Vector API', version='0.1.0', lifespan=lifespan)`.
4. Удали старый блок `@app.on_event('startup')` / `def startup()`.
Функция lifespan должна быть объявлена ДО создания app. init_db уже импортирован как `from backend.database import init_db`. Верни ТОЛЬКО изменённые фрагменты в блоке ```python.
Текущий релевантный код:
app = FastAPI(title='Vector API', version='0.1.0')
app.add_middleware(
CORSMiddleware,
allow_origins=cors_origins,
allow_credentials=allow_credentials,
allow_methods=['*'],
allow_headers=['*'],
)
@app.on_event('startup')
def startup() -> None:
init_db()
Критерий приёмки:
grep -n "on_event" backend/main.py→ пусто.- Есть
@asynccontextmanagerиlifespan=lifespanв вызовеFastAPI(...). PYTHONPATH=/root/vector python -c "import backend.main"— без ошибок; приложение стартует,GET /api/v1/health→{"status":"ok"}.
A6 — backend/routers/admin.py · обработка ошибок парсинга .docx
- Файл / строки:
backend/routers/admin.py:28-38(функция_extract_docx_text) и:134-139(эндпоинтadmin_parse_doc) - Тип: missing error handling
- Сложность: atomic
Не трогает auth: декоратор роутера и
require_rolesостаются без изменений.
PROMPT:
В этом FastAPI-роутере функция `_extract_docx_text` разбирает .docx (это zip с XML). Если загружен НЕ .docx (битый файл, не-zip, нет word/document.xml), `zipfile.ZipFile` или `archive.read` бросят исключение, и клиент получит непонятную 500-ю. Оберни разбор так, чтобы при ошибке чтения архива/XML эндпоинт возвращал HTTP 400 с понятным сообщением.
Сделай ДВА изменения, не трогая ничего другого:
1. В функции `_extract_docx_text` оберни блок чтения zip и парсинга XML в try/except (перехватывай `zipfile.BadZipFile`, `KeyError`, `ET.ParseError`). При любой из этих ошибок брось `ValueError("Не удалось прочитать .docx: файл повреждён или имеет неверный формат")`.
2. В эндпоинте `admin_parse_doc` оберни вызов `_extract_docx_text(content)` в try/except ValueError и при ошибке брось `raise HTTPException(status_code=400, detail=str(e))`.
`HTTPException` уже импортирован. `zipfile`, `ET` (xml.etree.ElementTree) уже импортированы. Верни ТОЛЬКО две изменённые функции целиком в блоке ```python.
Текущий код:
def _extract_docx_text(content: bytes) -> str:
with zipfile.ZipFile(BytesIO(content)) as archive:
xml = archive.read('word/document.xml')
root = ET.fromstring(xml)
ns = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
paragraphs: list[str] = []
for paragraph in root.findall('.//w:body/w:p', ns):
texts = [node.text for node in paragraph.findall('.//w:t', ns) if node.text]
if texts:
paragraphs.append(''.join(texts).strip())
return '\n'.join(paragraphs).strip()
@router.post('/parse-doc', response_model=ParseDocResponse)
async def admin_parse_doc(file: UploadFile = File(...)) -> dict:
content = await file.read()
raw_text = _extract_docx_text(content)
preview = _coerce_preview(raw_text)
return {'filename': file.filename, 'parsed': True, 'preview': preview, 'raw_text': raw_text}
Критерий приёмки:
- Загрузка валидного .docx по-прежнему возвращает 200 и корректный
preview(тестbackend/tests/test_parse_doc_api.pyзелёный). - Загрузка мусорного файла (не-zip) → HTTP 400 с сообщением, а не 500.
PYTHONPATH=/root/vector python -m pytest backend/tests/test_parse_doc_api.py -q— зелёный.
A7 — тесты: включить pytest-asyncio (26 пропущенных тестов)
- Файл / строки:
backend/requirements.txt:15(добавить строку) + новый файлpytest.iniв корне репозитория - Тип: missing test (тесты есть, но молча пропускаются)
- Сложность: atomic
PROMPT:
В проекте есть async-тесты, помеченные @pytest.mark.asyncio, но плагин pytest-asyncio не установлен, поэтому 26 тестов молча пропускаются. Нужно:
1. В конец файла requirements (см. содержимое ниже) добавить строку: pytest-asyncio==0.24.0
2. Создать НОВЫЙ файл pytest.ini в корне репозитория со следующим содержимым (включает автоматический режim asyncio и регистрирует маркер):
[pytest]
asyncio_mode = auto
testpaths = backend/tests
markers =
asyncio: async test powered by pytest-asyncio
Верни: (а) финальную строку, которую добавить в requirements.txt; (б) полное содержимое нового файла pytest.ini.
Текущий requirements.txt (последние строки):
pydantic==2.10.3
pydantic-settings==2.6.1
email-validator==2.1.0
psycopg2-binary==2.9.12
pytest==8.3.4
Критерий приёмки:
- После
pip install pytest-asyncio==0.24.0(внутри.venv) и добавленияpytest.ini:PYTHONPATH=/root/vector python -m pytest backend/tests -qпоказывает 0 skipped по причине "async def not natively supported" (число passed вырастает с 126 в сторону ~150). - Никаких новых падений; предупреждение
PytestUnknownMarkWarningдляasyncioисчезает.
A8 — удаление мёртвого кода backend (механическое)
- Файлы (удалить целиком):
backend/api/v1/analyze.py,backend/api/v1/auth.py,backend/api/v1/cases.py,backend/api/v1/stats.py,backend/api/v1/__init__.py,backend/api/v1/analyze.py.backup, и пустой каталогbackend/api/(весьapi/— старая копия,main.pyподключает толькоbackend/routers/*)backend/models_new.py,backend/models_updated.py,backend/models_backup.py(используется толькоbackend/models.py)backend/services/geo_service.py.bak,backend/services/scoring_service.py.bakbackend/check_model.py(отладочный скрипт с битымimport models)
- Тип: dead code
- Сложность: atomic (чисто механическое удаление — не требует чтения кода)
Подтверждено анализом:
main.pyимпортируетbackend.routers.*; ничего вне каталогаbackend/api/его не импортирует;models_*.pyне импортируются нигде;.bak/.backup— резервные копии.
PROMPT (это shell-задача, не правка кода):
Выполни удаление перечисленных файлов и проверь, что тесты не сломались. Не удаляй ничего сверх списка.
git rm backend/api/v1/analyze.py backend/api/v1/auth.py backend/api/v1/cases.py backend/api/v1/stats.py backend/api/v1/__init__.py backend/api/v1/analyze.py.backup
git rm backend/api/__init__.py
git rm backend/models_new.py backend/models_updated.py backend/models_backup.py
git rm backend/services/geo_service.py.bak backend/services/scoring_service.py.bak
git rm backend/check_model.py
Затем:
PYTHONPATH=/root/vector python -m pytest backend/tests -q
Критерий приёмки:
- Перечисленные файлы отсутствуют; каталог
backend/api/удалён. PYTHONPATH=/root/vector python -m pytest backend/tests -q— то же число passed, что и до удаления (никаких новых ошибок импорта).PYTHONPATH=/root/vector python -c "import backend.main"— без ошибок.
A9 — документация: единый адрес и имя БД
- Файлы / строки:
PSYCHOTYPE_SUMMARY.md(везде192.168.0.99),SERVICES_README.md,PSYCHOTYPE_API.md,MOBILE_FORM_README.md(адреса.99);DEPLOY.md(имя БДsar_mchsи путь/root/sar-mchs) - Тип: naming / консистентность документации
- Сложность: atomic
⚠️ УСТАРЕЛО (2026-07-25): канон ниже оказался неверным — адрес 192.168.0.108 взят из номера VMID, а не из реальной сети. Контейнер на DHCP и фактически имеет 192.168.0.99; замена .99 → .108 была ошибочной и откачена. Раздел сохранён как история выполненной задачи — не выполнять повторно.
Канон (исходный, НЕВЕРНЫЙ): адрес 192.168.0.108, БД vector_mchs, путь проекта /root/vector.
PROMPT:
Задача — привести документацию к единым значениям. В markdown-файлах замени по всему тексту:
- 192.168.0.99 → 192.168.0.108
- имя базы данных sar_mchs → vector_mchs
- путь /root/sar-mchs → /root/vector
- в командах `cd /root/sar-mchs` → `cd /root/vector`
Это простая текстовая замена, кода не касается. Выполни sed-заменой по указанным .md-файлам и верни список выполненных команд:
sed -i 's/192\.168\.0\.99/192.168.0.108/g' PSYCHOTYPE_SUMMARY.md SERVICES_README.md PSYCHOTYPE_API.md MOBILE_FORM_README.md
sed -i 's/sar_mchs/vector_mchs/g; s#/root/sar-mchs#/root/vector#g' DEPLOY.md
Критерий приёмки:
grep -rn "192.168.0.99\|sar_mchs\|sar-mchs" *.md→ пусто.- Смысл документов не изменился, только адреса/имена.
A10 — frontend/src/pages/analysis/AnalysisResult.jsx · неверный парсинг времени суток
- Файл / строки:
frontend/src/pages/analysis/AnalysisResult.jsx:42-48 - Тип: bug
- Сложность: atomic
PROMPT:
В React-компоненте функция getTimeOfDay ожидает строку "HH:MM" и берёт часы через lostTime.split(':')[0]. Но на вход приходит значение из <input type="datetime-local"> в формате "YYYY-MM-DDTHH:MM" (например "2026-07-21T14:30"). Тогда split(':')[0] === "2026-07-21T14", а parseInt даёт 2026 → время суток определяется неверно почти всегда.
Почини getTimeOfDay так, чтобы она корректно извлекала час из обоих форматов:
- если строка содержит 'T' — берём часть после 'T', затем часы до ':';
- иначе — берём часы до ':' как раньше.
Логика диапазонов (день/сумерки/ночь) и возвращаемые строки не меняются. Верни ТОЛЬКО исправленную функцию в блоке ```jsx.
Текущий код:
const getTimeOfDay = (lostTime) => {
if (!lostTime) return 'день';
const hour = parseInt(lostTime.split(':')[0]);
if (hour >= 6 && hour < 18) return 'день';
if (hour >= 18 && hour < 22) return 'сумерки';
return 'ночь';
};
Критерий приёмки:
getTimeOfDay("2026-07-21T14:30")→'день';getTimeOfDay("2026-07-21T23:00")→'ночь';getTimeOfDay("2026-07-21T20:00")→'сумерки'.getTimeOfDay("08:15")(старый формат) →'день';getTimeOfDay("")→'день'.
A11 — frontend/src/components/CaseForm/Step4Environment.jsx · неверный номер шага в заголовке
- Файл / строки:
frontend/src/components/CaseForm/Step4Environment.jsx:58 - Тип: bug (текст UI противоречит динамическому индикатору «Шаг X из Y»)
- Сложность: atomic
PROMPT:
В React-компоненте захардкожен заголовок "Шаг 5: Среда и местность", но этот компонент рендерится как шаг 4 (в родителе CaseForm он идёт под индексом, отображаемым как «Шаг 4»). Заголовок вводит в заблуждение и конфликтует с динамическим индикатором. Убери жёсткий номер шага из заголовка, оставив только название раздела.
Замени строку:
<h2>Шаг 5: Среда и местность</h2>
на:
<h2>Среда и местность</h2>
Верни ТОЛЬКО изменённую строку.
Критерий приёмки:
- Строка 58 больше не содержит «Шаг 5».
- Никакой другой разметки не тронуто.
A12 — frontend/src/components/CaseForm/Step4Environment.jsx · обработка ошибок геолокации
- Файл / строки:
frontend/src/components/CaseForm/Step4Environment.jsx:28-54 - Тип: missing error handling (используется блокирующий
alert, нет структурного состояния ошибки) - Сложность: atomic
PROMPT:
В React-компоненте функция handleGetLocation сообщает об ошибках геолокации через блокирующий alert(). Замени alert на запись сообщения об ошибке в состояние формы через updateData (ключ gps_error), чтобы UI мог показать ошибку не блокируя поток. Требования:
- при отсутствии navigator.geolocation: updateData({ gps_error: 'Геолокация не поддерживается вашим браузером' }); return;
- перед запросом координат сбрасывай ошибку: updateData({ gps_loading: true, gps_error: null });
- в success-колбэке помимо координат ставь gps_error: null;
- в error-колбэке: updateData({ gps_error: 'Ошибка получения координат: ' + error.message, gps_loading: false });
Не удаляй gps_loading, не меняй параметры getCurrentPosition (enableHighAccuracy/timeout/maximumAge). Верни ТОЛЬКО изменённую функцию handleGetLocation в блоке ```jsx.
Текущий код:
const handleGetLocation = () => {
if (!navigator.geolocation) {
alert("Геолокация не поддерживается вашим браузером");
return;
}
updateData({ gps_loading: true });
navigator.geolocation.getCurrentPosition(
(position) => {
updateData({
tnp_lat: position.coords.latitude.toFixed(6),
tnp_lon: position.coords.longitude.toFixed(6),
gps_loading: false
});
},
(error) => {
alert("Ошибка получения координат: " + error.message);
updateData({ gps_loading: false });
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
}
);
};
Критерий приёмки:
- В функции больше нет
alert(. - При ошибке/отказе в правах в состоянии формы появляется
gps_errorс текстом; при успехеgps_error === null. - Координаты по-прежнему пишутся в
tnp_lat/tnp_lonс 6 знаками.
A13 — frontend/src/components/SearchMap.jsx · CDN-иконки и рассинхрон версии Leaflet
- Файл / строки:
frontend/src/components/SearchMap.jsx:186-187 - Тип: bug (внешние
raw.githubusercontent/cdnjs, версия 1.7.1 против 1.9.4 в остальном коде) - Сложность: atomic
PROMPT:
В React-компоненте иконки маркеров Leaflet грузятся с ненадёжных внешних CDN (raw.githubusercontent.com и cdnjs) и с версией 1.7.1, тогда как остальной код использует unpkg leaflet 1.9.4. Замени оба URL на unpkg-адреса версии 1.9.4 (стандартный маркер Leaflet), чтобы источник был единым и стабильным.
Замени:
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
на:
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
Верни ТОЛЬКО две изменённые строки.
Критерий приёмки:
- В строках 186-187 нет
raw.githubusercontent.comиcdnjs.cloudflare.com. - Оба URL указывают на
unpkg.com/leaflet@1.9.4. - Карта на странице анализа отображает маркеры (визуальная проверка).
A14 — frontend/src/components/CaseForm/CaseForm.jsx · убрать отладочный console.log
- Файл / строки:
frontend/src/components/CaseForm/CaseForm.jsx:119 - Тип: dead code
- Сложность: atomic
PROMPT:
В React-компоненте в боевом пути отправки формы остался отладочный вывод. Удали строку:
console.log("Case created:", result);
Ничего кроме этой строки не меняй. Верни подтверждение и номер удалённой строки.
Критерий приёмки:
grep -n "console.log" frontend/src/components/CaseForm/CaseForm.jsx→ пусто.- Логика отправки/навигации не изменена.
A15 — frontend/src/pages/AdminDashboard.jsx · молча проглоченная ошибка загрузки дашборда
- Файл / строки:
frontend/src/pages/AdminDashboard.jsx:98-121 - Тип: missing error handling
- Сложность: atomic
PROMPT:
В React-компоненте эффект загрузки дашборда при ошибке просто ставит dashboard=null и не показывает пользователю ничего (в отличие от загрузчика кейсов, который вызывает setError). Добавь пользовательское сообщение об ошибке через уже существующий сеттер setError.
В catch-блоке эффекта загрузки дашборда, внутри `if (err.name !== 'AbortError') { ... }`, помимо `setDashboard(null);` добавь `setError(err.message || 'Ошибка загрузки дашборда');`. Ничего другого не меняй. Верни ТОЛЬКО изменённый catch/finally блок в ```jsx.
Текущий код:
} catch (err) {
if (err.name !== 'AbortError') {
setDashboard(null);
}
} finally {
setDashboardLoading(false);
}
Критерий приёмки:
- При падении запроса дашборда в состоянии выставляется
errorс сообщением. setDashboard(null)сохранён;AbortErrorпо-прежнему игнорируется.
Проверяющему: убедиться, что
setErrorв этом компоненте — общий стейт с загрузчиком кейсов и его отображение не конфликтует. Если нужно раздельное сообщение — см. B-раздел (рефакторинг стейта AdminDashboard).
A16 — frontend/src/App.js · isMobile не реагирует на ресайз (опционально)
- Файл / строки:
frontend/src/App.js:30 - Тип: simple refactor
- Сложность: atomic
PROMPT:
В React-приложении признак мобильного вычисляется один раз при монтировании и не обновляется при ресайзе/повороте экрана:
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) || window.innerWidth < 768;
Замени на React-состояние с подпиской на событие resize. Используй хуки useState и useEffect (проверь, что они импортированы из 'react'; если нет — добавь в существующий импорт). Реализация:
const [isMobile, setIsMobile] = useState(
() => /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) || window.innerWidth < 768
);
useEffect(() => {
const onResize = () => setIsMobile(
/iPhone|iPad|iPod|Android/i.test(navigator.userAgent) || window.innerWidth < 768
);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
Не меняй, как isMobile передаётся дальше. Верни изменённый фрагмент и, при необходимости, обновлённую строку импорта из 'react', в блоке ```jsx.
Критерий приёмки:
isMobileпересчитывается при изменении ширины окна (проверка в DevTools: сузить окно < 768 → рендерится мобильный путь без перезагрузки).- Слушатель события снимается в cleanup (нет утечки).
- Импорт из
reactсодержитuseStateиuseEffect.
РАЗДЕЛ B — NEEDS-CONTEXT (не давать локальной модели)
Требуют понимания нескольких файлов, контрактов данных, роутинга или бизнес-логики. Оставлено для сильной модели / человека.
B1 — Весь backend/services/scoring_service.py (ядро: 7 факторов)
Тип: исключено по правилам. Любые правки (в т.ч. неиспользуемый импорт Optional на строке 6) делать вручную: файл — ядро зонального скоринга, риск задеть коэффициенты. Отдельно: неиспользуемый Optional — тривиально, но НЕ делегировать.
B2 — backend/services/claude_service.py:174-181 · хрупкий парсинг JSON из ответа Claude
Тип: missing error handling. content.split("```json")[1]... упадёт при ином форматировании ответа модели; json.loads без try/except уронит весь анализ. Нужно устойчивое извлечение JSON + graceful fallback на scoring — требует понимания контракта AnalysisResult и того, как analyze_case переключается на fallback.
B3 — backend/services/stats_service.py:19-39 vs backend/routers/stats.py:19-35 · дублированная логика скоринга рекомендаций
Тип: simple refactor, но needs-context. Две почти одинаковые реализации score-логики (одна в сервисе, одна прямо в роутере) с разными правилами. Свести к одной — требует решения, какая версия «правильная», и затрагивает контракт RecommendationRequest.
B4 — Удаление/подключение мёртвых frontend-компонентов
Тип: dead code, needs-context (продуктовое решение «удалить или доподключить»).
Не подключены ни к одному маршруту (цепочка index.js → App.js):
components/MobileForm.jsxиcomponents/MobileForm.tsx(дубликаты друг друга; ни один не импортируется — живая мобильная форма этоCaseForm),components/MobileForm.csscomponents/BehavioralProfile.tsx+.csscomponents/AnalysisLayout/*,components/MapView/*,components/ResultPanel/*(MapView/ResultPanel импортируются только из неиспользуемого AnalysisLayout)components/admin/CasesList*,components/admin/CaseDetail*,components/admin/Statistics*,components/admin/HeatmapView*—AdminDashboardне содержит вложенных<Routes>, поэтому весь набор недостижимcomponents/CaseForm/Step5Resources.jsx— не импортируется вCaseForm.jsx(рендерятся только Step1-4 + Step2b)
Решение (удалить vs довести маршрутизацию /admin/* и шаг ресурсов) — за человеком. Внутри этих файлов есть свои баги (напр. опечатка isPsycotypeSelected в MobileForm.tsx:269, захардкоженные URL http://192.168.0.99:8000 в MobileForm.jsx:49 и HeatmapView.jsx:73, NaN-гварды в ResultPanel), но чинить их бессмысленно, пока не решён вопрос удаления.
B5 — Рассинхрон контрактов данных между тремя поддоменами (desktop/mobile/admin)
Тип: bug, needs-context. Одни и те же поля кодируются по-разному:
- gender:
'мужской'/'женский'(MobileForm.jsx) vs'male'/'female'(MobileForm.tsx, admin) vs'М'/'Ж'(Step1Child.jsx) vs'm'/'f'(backend_normalize_case_data) - ключи психотипа:
unfamiliar_behavior/leadership(Step2bPsychotype.jsx, и так же ждёт backenddetect_psychotype) vsunfamiliar_env/group_role(MobileForm, admin CaseDetail) — из-за этого ответы психотипа десктоп-формы не отображаются в админке - транспорт:
'велосипед'/'автомобиль'(BehavioralProfile.tsx) vs'bike'/'scooter'/'other'/'none'(Step2Health.jsx) vs профили'велосипед'/'самокат'(backend scoring) Нужна единая согласованная схема через границу frontend↔backend — сознательное проектное решение.
B6 — frontend/src/components/CaseForm/CaseForm.jsx:90-129 · навигация через window.location.href
Тип: bug, needs-context. После создания кейса используется window.location.href = '/analysis/${id}' — полная перезагрузка и потеря SPA-состояния, хотя приложение уже в <Router>. Замена на useNavigate затрагивает роутинг и структуру компонента.
B7 — frontend/src/pages/AdminDashboard.jsx:58-96 · повторная загрузка из-за selectedCaseId в deps
Тип: bug, needs-context. Эффект загрузки списка зависит от selectedCaseId и сам же его выставляет внутри → двойной fetch при первом рендере и после сброса фильтров. Исправление требует понимания взаимодействия эффектов и стейта дашборда.
B8 — note vs notes — рассогласование имени поля
Тип: naming/bug, needs-context. Frontend (AdminDashboard.jsx:215-216, handleSave) и backend (schemas.py имеет и note, и notes; роутеры вручную маппят note→notes) по-разному называют одно поле. Нужно унифицировать по всей цепочке (schema + роутеры + фронт) — контрактное изменение.
B9 — Отсутствие тестов и валидации пропсов на фронтенде
Тип: missing test / missing PropTypes, needs-context. Под frontend/src нет ни одного теста; ни один .jsx-компонент не использует PropTypes. Расчёт зон/времени в AnalysisResult.jsx и многошаговая валидация CaseForm не покрыты. Объём — целая инициатива, не атомарная задача.
B10 — Секреты и деплой (.env)
Тип: security/deploy, исключено из делегирования. .env содержит плейсхолдеры ANTHROPIC_API_KEY=your_... и слабый JWT_SECRET=your_jwt_secret_here_change_in_production. Установка реального ключа Claude (для выхода из fallback) и генерация стойкого JWT_SECRET — ручная операция с секретами.
B11 — Переход с Anthropic API на локальный Ollama
Тип: архитектура / приватность, needs-context. Исключено из делегирования: затрагивает контракт AnalysisResult и границу «где вообще нужна LLM».
Мотив: данные о пропавших без вести (особенно о детях) не должны покидать контур. Сейчас backend/services/claude_service.py ходит во внешний Anthropic API. Целевой провайдер — локальная Ollama (lxc200 на pve-node2, 192.168.0.17, уже поднята под OpenHands).
Ключевой факт для решения: LLM НЕ считает приоритеты. Вся математика уже в geo_service.build_search_zones + scoring_service.WeightedScorer (7 взвешенных факторов + множители по возрасту/сезону/профилю) + psychotype_service. Метод analyze_with_fallback уже отдаёт полноценный AnalysisResult вообще без API. Единственная уникальная роль модели в hot-path — разбор неструктурированного текста донесения в поля profiles/circumstances; остальное (behavioral_prediction, immediate_actions, summary) — переупаковка уже отранжированного топ-5.
Отсюда два независимых рычага (не взаимоисключающие):
- Строгая категоризованная форма ввода (чипы/дропдауны/числа) — убирает саму НЕОБХОДИМОСТЬ в LLM, скоринг становится чистой математикой. Свободнотекстовое поле
circumstancesоставить для чтения человеком. - Локальная модель — сохраняет LLM, но переносит провайдера внутрь контура. Недетерминизм и риск галлюцинаций при этом НЕ исчезают.
Рекомендуемая слоёная схема: скоринг ВСЕГДА детерминированный (то есть инвертировать текущую логику — сегодняшний fallback становится основным путём, а не аварийным); форма — способ ввода по умолчанию; Ollama — опциональный ЧЕРНОВОЙ парсер, который предзаполняет форму для подтверждения оператором, никогда не имеет решающего слова и никогда не считает приоритеты.
Объём работ: абстрагировать провайдера за интерфейсом (сейчас класс жёстко завязан на пакет anthropic), вынести выбор в конфиг (LLM_PROVIDER, OLLAMA_BASE_URL, имя модели), учесть отсутствие у Ollama нативного tool-use и более слабое следование формату JSON (из-за этого B2 — устойчивое извлечение JSON — становится критичным), покрыть тестами оба провайдера. Отдельно решить судьбу ANTHROPIC_API_KEY в .env (см. B10).
Зависимости: пересекается с B2 (парсинг JSON) и B10 (секреты). Делать после решения по B4 (мёртвая мобильная форма) — иначе неясно, какая форма ввода целевая.
B12 — Внесение данных из ISRID (международная база инцидентов ПСР)
Тип: данные / доменная модель, needs-context. Исключено из делегирования: напрямую затрагивает коэффициенты скоринга и схему БД.
Что это: ISRID (International Search & Rescue Incident Database, Robert Koester, «Lost Person Behavior») — международная база инцидентов поисково-спасательных операций со статистикой поведения потерявшихся по категориям субъекта: кольцевые модели и модели рассеивания (дистанции по процентилям от точки потери), типовые места обнаружения, зависимость от рельефа и категории субъекта.
Зачем именно нам: сейчас веса зонального скоринга (forest 0.25, water 0.20, …) и радиусы зон заданы вручную, экспертно. ISRID даёт внешние ЭМПИРИЧЕСКИЕ априорные значения для тех же величин по международной выборке — это позволяет заменить часть догадок статистикой и резко снижает объём собственных данных, нужный для калибровки.
Как стыкуется с планом по 14-летнему архиву спецдонесений: ISRID — источник априорных значений, собственный архив — источник локальной калибровки (местная специфика: рельеф, климат, структура вызовов). Правильный порядок — сначала ISRID как базовая линия, затем поправки по своим данным. Именно эта комбинация делает переход к чисто статистической модели реалистичным.
Что нужно решить до начала:
- Правовой статус — блокирующий вопрос. ISRID не является свободно распространяемой базой: доступ к данным и производным моделям регулируется автором/правообладателем. Условия использования (в частности, допустимость зашивать производные коэффициенты в наш продукт) нужно выяснить ДО внесения каких-либо данных в репозиторий. Вопрос не технический.
- Единицы и категории. Категории субъекта в ISRID (hiker, dementia, child по возрастным группам, autism, …) не совпадают один-к-одному с нашими профилями; дистанции даны в своих единицах и привязаны к типу местности. Нужны явные словари соответствия и решение, что делать с профилями без аналога.
- Редкие профили. По РАС и эпилепсии выборки малы даже в ISRID — там сохранить экспертные коэффициенты и помечать их как «не выведенные из данных».
- Куда кладём. Отдельный версионированный справочник априорных значений с указанием источника на каждую величину, а не правка констант в коде: нужно всегда понимать, какое число откуда взялось (ISRID / свой архив / эксперт).
Жёсткое правило: ни ISRID, ни архив не дают права автоматически перезаписывать формулу весов — изменение коэффициентов проходит через ревью человека (см. B1 и «Жёсткие исключения» в начале файла).
Сводка
| Категория | Кол-во | Куда |
|---|---|---|
| Atomic (backend) | A1–A9 | локальной LLM |
| Atomic (frontend) | A10–A16 | локальной LLM |
| Needs-context | B1–B12 | сильной модели / человеку |
Рекомендуемый порядок для локальной модели: A8 и A7 первыми (чистка мёртвого кода и зелёный CI дают чистую базу для проверки остальных), затем backend A1–A6, затем docs A9, затем frontend A10–A16. После каждой backend-задачи прогонять PYTHONPATH=/root/vector python -m pytest backend/tests -q.