Files
vector/vector_tasks.md
T
Sadmin 15e7e27cb8 B21: терминология МЧС РБ — РЦУ РЧС / ОУМЧС / Г(Р)ОЧС
Правка Виктора: республиканский штаб → РЦУ РЧС; областное ЦОУ → ОУМЧС;
райЦОУ → Г(Р)ОЧС. cou_units → mchs_units (иерархия rcu → oblast →
gor_rayon); unit_id вместо cou_id; роли: admin=РЦУ РЧС,
coordinator=ОУМЧС, operator=Г(Р)ОЧС. Статус: план согласован (Виктор:
«в остальном согласен») — готов к E1.
2026-09-09 21:00:19 +03:00

84 KiB
Raw Blame History

ВЕКТОР — очередь технического долга для делегирования

Проект: ВЕКТОР (Система определения приоритетных направлений поиска, 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.bak
    • backend/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.css
  • components/BehavioralProfile.tsx + .css
  • components/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, и так же ждёт backend detect_psychotype) vs unfamiliar_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.

Отсюда два независимых рычага (не взаимоисключающие):

  1. Строгая категоризованная форма ввода (чипы/дропдауны/числа) — убирает саму НЕОБХОДИМОСТЬ в LLM, скоринг становится чистой математикой. Свободнотекстовое поле circumstances оставить для чтения человеком.
  2. Локальная модель — сохраняет 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 и «Жёсткие исключения» в начале файла).

B13 — Структурирование «Вектора» в систему подсистем (видение-каркас)

Тип: архитектура / реструктуризация, needs-context. Исключено из делегирования локальной модели: затрагивает контракты данных, схему БД, бизнес-логику SAR.

Цель: ВЕКТОР — не один монолит, а система относительно независимых подсистем с чистыми границами. Упрощает разработку, тестирование и будущее подключение ATAK/Meshtastic.

Подсистемы:

ВЕКТОР
│
├── Core
│   ├── модель поиска
│   ├── расчёт вероятностей
│   ├── ISRID
│   ├── Ring Model / dispersion
│   ├── категории пропавшего
│   └── временная динамика
│
├── GIS
│   ├── OpenStreetMap
│   ├── DEM / рельеф
│   ├── гидрография
│   ├── лес / болота / поля
│   ├── дороги / тропы
│   └── здания
│
├── Search Area
│   ├── исходная точка
│   ├── вероятностная поверхность
│   ├── зоны поиска
│   ├── маршруты
│   └── исключённые территории
│
├── Field Data
│   ├── GPS групп
│   ├── обнаружения
│   ├── следы
│   ├── свидетельства
│   ├── проверенные участки
│   └── результаты поиска
│
├── Real-time
│   ├── ATAK
│   ├── Meshtastic
│   ├── треки групп
│   ├── события
│   └── перерасчёт модели
│
├── Backend
│   ├── API
│   ├── БД
│   ├── очереди задач
│   └── авторизация
│
└── UI
    ├── карта
    ├── карточка поиска
    ├── вероятностная карта
    ├── группы
    └── статистика

Ключевые принципы (добавлены к изначальному скелету B13):

  1. Развязка движка. Search Engine — чистая функция: исходные данные + данные поиска + параметры человека → (ProbabilitySurface, SearchAreas, Recommendations). Он ничего не знает об ATAK, Meshtastic, веб-интерфейсе, БД, auth. Доставка (web/ATAK/Mesh) — отдельный слой, подписанный на результат.

  2. Четыре слоя данных, не смешиваются:

    • Слой 1 — исходные (неизменяемые): OSM, DEM, гидрография, лес, дороги, здания, ISRID, исторические поиски.
    • Слой 2 — конкретный поиск: исходная точка, время исчезновения, категория, возраст/пол/состояние, известный маршрут, погодные условия, доп. факторы.
    • Слой 3 — оперативные (во время операции): Position, Observation, Track, Clue, Team, AreaChecked, WitnessReport, FoundPerson.
    • Слой 4 — результат модели: ProbabilitySurface, SearchZone, Priority, Confidence, RecommendedRoute. Результат не смешивается с исходными данными.
  3. Итеративная модель. Не «Вектор один раз нарисовал круг», а постоянный пересчёт наиболее вероятного положения по мере поступления полевых данных: расчёт → поисковые группы → найдено/не найдено → новые данные → новый расчёт. Сюда же естественно ложатся ATAK + Meshtastic.

  4. Очередь задач для тяжёлых расчётов (terrain analysis, probability calculation, routing, recalculation) — сейчас analyze_case синхронно ждёт Overpass + Claude.

Технический контур:

        PostgreSQL
           │
        ├── PostGIS
           │
        └── данные поисков

             ▲
             │
      ┌──────┴────────┐
      │ Vector Backend │
      ├────────────────┤
      │ Search Engine  │
      │ GIS Engine     │
      │ ISRID Engine   │
      │ Routing        │
      │ Event Engine   │
      └──────┬─────────┘
             │
             ├── Web UI
             ├── ATAK
             └── Meshtastic

Зависимости / блокеры:

  • ISRID (B12) — внешний правовой блокер. Итеративная модель (B18) строится на экспертных коэффициентах сейчас; ISRID подключается позже как источник априорных, когда вопрос правового статуса решён.
  • B1 (ядро скоринга) — коэффициенты под ревью человека. Экстракция движка (B14) их НЕ меняет; любая калибровка — отдельно через B1.
  • Реал-тайм (B19) — фактически отдельный проект, оформляется после ядра и Field Data.

Декомпозиция в фазы: B14 → B15 → (B16, B17 параллельно) → B18 → B19. См. ниже.


B14 — Чистый SearchEngine (поведение-сохраняющий)

Тип: refactor / архитектура, needs-context. Фундамент всей развязки (B13-принцип 1). Низкий риск: поведение НЕ меняется, коэффициенты НЕ трогаются (B1-safe).

Что: Вынести расчёт поисковой модели из backend/routers/analyze.py в отдельный модуль services/search_engine.py с чистой границей:

  • SearchInput (pydantic): age, gender, terrain, weather, elapsed_hours, season, diagnosis_type, has_transport, psychotype_answers, profiles, lat/lon, loss_time, … — то, что сегодня собирается в analyze_case из payload + db.get_case.
  • SearchModel (результат): max_distance_km, coefficients, time_of_day, psychotype (+modifiers +recommendations), weights, distance_multiplier, active_profiles, critical_warnings, urgency, primary_zones, search_radius_km, key_locations, behavioral_prediction, immediate_actions, summary, fallback_used.
  • build_search_model(input: SearchInput) -> SearchModel — чистая функция, БЕЗ DB / auth / HTTP. Внутри вызывает calculate_max_distance, claude_analyze, WeightedScorer, detect_psychotype (вся логика из analyze_case переезжает сюда).
  • analyze.py::analyze_case становится тонкой обёрткой: собрать SearchInput из payload + db.get_case → build_search_model → записать analysis_log → вернуть. Вся SAR-математика — в search_engine.py.

Границы (НЕ делать в B14):

  • НЕ менять коэффициенты (distance_service, scoring_service) — это B1.
  • НЕ менять схему БД / миграции.
  • НЕ добавлять Field Data / итеративность / PostGIS — это B15–B18.
  • НЕ менять внешний контракт ответа /analyze (фронт B5 от него зависит) — состав полей тот же.

Критерий приёмки:

  • analyze.py не содержит расчётной логики (только сборка SearchInput + запись БД + возврат).
  • Поведение /analyze идентично: bike 8yo 2h лес день → max_distance 5.4; РАС+bike → профили [РАС, велосипед]; psychotype-answers → detect. Регресс-кейсы из B5 проходят.
  • PYTHONPATH=/app python -m pytest backend/tests -q — зелёный; добавлены юнит-тесты на build_search_model (моки на claude_analyze).

B15 — Четыре слоя данных: схема + миграция

Тип: schema / migration, needs-context. Исключено из делегирования: миграции БД.

Что: Ввести слоистую модель данных (B13-принцип 2) через Alembic-миграцию:

  • Слой 2 (поиск): cases — уже есть; оставить как карточку поиска (исходная точка, время, категория, параметры человека), вынеся поля других слоёв.
  • Слой 4 (результат): analysis_log → явная таблица search_models (case_id, version, input_snapshot, model_json, created_at) с версионированием (несколько версий на случай — фундамент для B18).
  • Слой 3 (оперативные): НОВЫЕ таблицы field_observations (position/observation/track/clue/witness), search_teams, areas_checked, found_events — пока схема + пустые + модели; ингестия в B17.
  • Слой 1 (исходные): версионированный справочник reference_priors (source, category, value, units, provenance) — заглушка под B12 (ISRID) и экспертные значения; данные вносятся позже, сейчас структура.
  • Бэкфилл существующих cases/analysis_log в новую схему без потери данных.
  • API /analyze и /cases продолжают работать (адаптер поверх новой схемы).

Границы: НЕ заполнять reference_priors (B12 заблокирован). НЕ реализовывать ингестию полевых данных (B17). Коэффициенты не трогать.

Критерий приёмки:

  • alembic upgrade head проходит чисто на пустой и на текущей БД; даун-миграция работает.
  • Существующие кейсы и analysis_log доступны после миграции (бэкфилл проверен на тестовом наборе).
  • /analyze и /cases отвечают тем же контрактом (фронт B5 не сломался).
  • pytest backend/tests — зелёный.

B16 — GIS: PostGIS + локальный OSM (замена живого Overpass)

Тип: infra / architecture, needs-context. Крупная инфра-задача.

Мотив: services/geo_service.py дёргает живой Overpass API (8 направлений × 4 дистанции = 32 запроса на анализ) — медленно и ненадёжно: при недоступности Overpass зоны получают одинаковые нейтральные оценки (см. тест B5 — все зоны с одинаковым скором). Локальный OSM в PostGIS даёт быстрые пространственные запросы, рельеф (DEM), гидрографию — фундамент для качественной вероятностной поверхности.

Что:

  • Подключить PostGIS к vector-postgres (расширение postgis, docker-compose).
  • Импорт OSM-экстракта региона (Беларусь / округа) через osm2pgsql в таблицы PostGIS (roads, water, forest/wood, buildings, landuse).
  • Переписать geo_service.build_search_zones на PostGIS-запросы (ST_DWithin, ST_Intersects, длина дорог в буфере, расстояние до ближайших water/settlement/forest) вместо Overpass.
  • Опционально DEM / рельеф (уклон, проходимость) — можно вынести в B16b.
  • Сохранить контракт Zone (forest_pct, road_density, water_distance_km, settlement_distance_km), чтобы scoring_service не менялся.

Границы: Коэффициенты скоринга не трогать. DEM — опционально. ISRID — отдельно (B12).

Критерий приёмки:

  • vector-postgres имеет PostGIS; OSM-данные загружены.
  • build_search_zones(53.9, 27.56, case, 1.08) отдаёт зоны с РЕАЛЬНЫМИ характеристиками (road_density/water/forest различаются между направлениями) без обращений во внешний интернет.
  • Время анализа < 1с (без Overpass-латентности и без Claude fallback).
  • test_geo_service адаптированы и зелёные.

B17 — Field Data: модель + ингестия операционных данных

Тип: new subsystem, needs-context. Зависит от B15 (оперативные таблицы).

Что: Реализовать слой 3 (B13-принцип 2) — приём и хранение операционных данных во время поиска:

  • Модели/таблицы: field_observations (тип: position/observation/track/clue/witness, geom, timestamp, team_id, confidence, raw), search_teams (id, name, members, role), areas_checked (geom-полигон, team_id, checked_at, result), found_events.
  • API приёма: POST /api/v1/cases/{id}/observations, …/teams, …/areas_checked; GET для списка/карты. Auth: operator/field/admin.
  • Валидация (тип наблюдения, геом в разумных пределах, таймстемп).
  • События новых наблюдений — источник для B18 (перерасчёт).

Границы: НЕ реализовывать сам пересчёт (B18). НЕ подключать ATAK/Meshtastic (B19) — только API-ингестия. Коэффициенты не трогать.

Критерий приёмки:

  • Создание/чтение наблюдений/групп/проверенных участков через API работает (тесты).
  • Геометрии в PostGIS (если B16 готов) или в JSONB-заглушке (оговорить, если B16 ещё нет).
  • Событие нового наблюдения попадает в очередь/лог для B18.

B18 — Итеративный пересчёт модели + очередь задач

Тип: architecture / core value, needs-context. Зависит от B14 (движок), B15 (версионирование), B17 (источник событий). Самая ценная фаза (B13-принцип 3).

Что:

  • Job-queue (arq / Celery / RQ — выбрать) для тяжёлых расчётов: probability, routing, recalculation. Сейчас analyze_case синхронный — не масштабируется на итерации.
  • Event-driven пересчёт: поступление новых полевых данных (B17) → задача «пересчитать модель для case_id» → build_search_model с учётом оперативных данных (исключённые территории из areas_checked, уточнения из observations/clues) → новая версия search_models (B15).
  • Версионирование: история моделей на случай; UI/ATAK показывают актуальную.
  • Учёт «найдено/не найдено» — подтверждение/опровержение наблюдений корректирует апостериорную поверхность.
  • Работает на экспертных коэффициентах; ISRID (B12) подключается как источник априорных, когда разблокируется.

Границы: Коэффициенты — B1 (ревью). ATAK/Mesh — B19. Без ISRID-данных пока (экспертные априоры).

Критерий приёмки:

  • Поступление observation/area_checked ставит задачу пересчёта; новая версия модели сохраняется.
  • Тяжёлый расчёт не блокирует HTTP-запрос (async через очередь).
  • История версий модели доступна; можно откатиться/сравнить.
  • Демонстрация: кейс → отметка «сектор N проверен, никого нет» → пересчёт снижает приоритет N, поднимает соседние.

B19 — «Контур» — отдельный полевой проект (взаимодействует с Вектором)

Тип: отдельный проект, не фаза Вектора. Вектор-сторона взаимодействия = B17 + B18 + контракт обмена. Сам Контур (ATAK/Meshtastic/GPS) — собственная спецификация и репозиторий, здесь не детализируется.

Разделение ролей:

  • Вектор — «где искать?»: вероятностное моделирование, приоритетные зоны, перерасчёт по новым данным.
  • Контур — «как эффективно выполнять поиск в поле?»: полевая координация и ситуационная осведомлённость.
  • Проекты независимы: Вектор работает без Контура (подготовка/расчёт/планирование), Контур — без Вектора (обычный поиск, ручное распределение, GPS-трекинг, работа без связи с сервером).

Компоненты Контура:

  • ATAK — ситуационная карта, визуализация, GPS-треки, поисковые зоны, маркеры, координация участников, геоданные.
  • Meshtastic — автономный LoRa-mesh-канал: координаты, короткие сообщения, полевые события; отказоустойчивая связь при отсутствии инфраструктуры.
  • GPS — положение групп, кинологов, БПЛА, ресурсов.

Возможности Контура: объективный контроль прохождения зон (покрытие вместо «вроде прошли»), передача обнаружений (след/предмет/место/препятствие), фиксация препятствий (болота/завалы/водные преграды/склоны/закрытые территории), координация ресурсов в реальном времени (ближайший кинолог к следу, БПЛА к участку).

Замкнутый цикл:

ВЕКТОР → зоны/приоритеты/маршруты/задачи → КОНТУР → выполнение в поле → наблюдения (треки/обнаружения/препятствия/покрытие) → ВЕКТОР → перерасчёт → обновлённые задачи → КОНТУР

Контракт обмена (граница проектов):

  • Вектор → Контур: поисковые зоны, приоритеты, вероятностные значения, рекомендуемые маршруты, задачи для ресурсов, направления поиска.
  • Контур → Вектор: GPS-треки, обследованные территории, обнаруженные следы/предметы, препятствия, новые наблюдения, данные кинологов/БПЛА.

Развязка: движок Вектора не знает протоколов ATAK/Meshtastic — он читает/пишет только контракт; адаптеры Контура транслируют. Конкретизация B13-принципа 1.

Что в репо Вектора: B17 (приём полевых данных, Контур→Вектор), B18 (перерасчёт + выдача зон, Вектор→Контур), и отдельная задача «Контракт обмена» — версионированный формат зон/приоритетов/наблюдений на границе, без зависимости от ATAK/Mesh. Что НЕ в репо Вектора: ATAK-плагины, Meshtastic-firmware/протокол, полевой UI Контура — отдельный проект.


B20 — Контракт обмена Вектор ↔ Контур (граница проектов)

Тип: spec / architecture, needs-context. Фундамент развязки (B13-принцип 1): единый версионированный формат данных на границе, без зависимости от ATAK/Meshtastic. От него зависят B17 (ингестия) и B18 (выдача зон) — они реализуются поверх этого контракта.

Решение Виктора (2026-09-09): LLM убран из проекта полностью. Zones/ probability для outbound отдаёт детерминированный движок (rules_analysis + scoring + гео), без Claude/Anthropic. Нарративные поля /analyze — правила.

Что: Зафиксировать контракт обмена между Вектором и Контуром — два направления, оба версионированные (schema_version), протокольно-агностичные. Транспорт (ATAK CoT / Meshtastic / HTTP / websocket) — дело адаптеров Контура, не Вектора; движок работает только с форматом.

Вектор → Контур (outbound):

  • SearchZones[]: {zone_id, geom (Polygon GeoJSON), priority (1..N), probability (0..1), reasoning, recommended_resources[]}
  • RecommendedRoutes[]: {route_id, geom (LineString), for_role, estimated_time}
  • ResourceTasks[]: {task_id, target_zone_id, resource_type (team|dog|uav), instruction}
  • SearchModelMeta: {case_id, model_version, generated_at, valid_until, max_distance_km}
  • Событие: «новая версия модели доступна».

Контур → Вектор (inbound):

  • Positions (поток): {team_id, geom (Point), timestamp, accuracy}
  • Tracks: {team_id, geom (LineString), from, to}
  • AreasChecked: {area_id, geom (Polygon), team_id, checked_at, result (clear|found|partial), coverage_pct}
  • Observations: {obs_id, type (clue|item|place|obstacle|witness), geom, timestamp, team_id, confidence, description, media_ref?}
  • FoundEvent: {case_id, geom, timestamp, condition}
  • Событие: «новое наблюдение» / «зона проверена» (триггерит перерасчёт B18).

Реализация: Pydantic-схемы в backend/schemas/contract.py, сериализация GeoJSON, schema_version на каждом сообщении. Вектор отдаёт outbound через endpoint (SSE / websocket / poll — выбрать) и принимает inbound через endpoints B17. build_search_model (B14) отдаёт результат в формате outbound-контракта — в коде движка ни одного упоминания ATAK/Mesh.

Границы: НЕ реализовывать ATAK-плагины / Meshtastic-протокол (это Контур, отдельный репо). НЕ менять коэффициенты (B1). НЕ привязывать к конкретному транспортному протоколу — контракт = формат данных, транспорт = адаптер.

Критерий приёмки:

  • Pydantic-схемы обоих направлений определены, валидируются (round-trip: объект → JSON → объект).
  • build_search_model (B14) отдаёт результат в формате outbound-контракта (SearchZones + meta); grep по services/search_engine.py не находит «ATAK»/«Meshtastic»/«CoT».
  • B17 endpoints приёма принимают inbound-контракт и складывают в Field Data таблицы (B15).
  • Человекочитаемая документация контракта (markdown или OpenAPI-схема) — для разработчиков Контура.
  • Тесты: round-trip сериализация + валидация кейсов (clue / obstacle / area_checked / zone).

Сводка

Категория Кол-во Куда
Atomic (backend) A1–A9 локальной LLM
Atomic (frontend) A10–A16 локальной LLM
Needs-context B1–B12 сильной модели / человеку
Архитектура / рефакторинг B13 (каркас), B14 (SearchEngine), B15 (слои данных), B16 (GIS/PostGIS), B17 (Field Data), B18 (итеративность + queue), B20 (контракт Вектор↔Контур) сильной модели / человеку
Отдельный проект B19 «Контур» (ATAK+Meshtastic, полевая система) отдельный репо/спецификация

Рекомендуемый порядок: B14 (чистый движок) → B15 (слои данных) → B20 (контракт Вектор↔Контур) → B16 (GIS) и B17 (Field Data) параллельно → B18 (итеративность + queue). B12 (ISRID) разблокируется внешне (правовой статус) и подключается как источник априорных к B18. B19 «Контур» — отдельный проект (свой репо); со стороны Вектора: B17+B18+B20. Коэффициенты (B1) — под ревью человека на каждом шаге.


B21 — Мультипоиск и многопользовательский доступ (масштаб на республику)

Статус: ПЛАН на ревью Виктора. Основа — паттерн админки РВС (rbac-admin-schema-pattern: RBAC + сессии + аудит) и домен КОНТУРа (SEARCH_OPERATION + OPERATION_MEMBERSHIP). Написан 2026-09-09.

1. Контекст и требование

ВЕКТОР как программное средство на всю республику МЧС:

  • Несколько поисков одновременно — сегодня API работает с произвольным case_id, но UI предполагает «текущий поиск»: дашборд, карта, анализ привязаны к одному кейсу без понятия «операция».
  • Много пользователей — каждое подразделение (ОУМЧС / Г(Р)ОЧС) = аккаунты; РЦУ РЧС видит все поиски, подразделения — свои.
  • КОНТУР подключается к каждому поиску — outbound-контракт B20 операционно: зоны по конкретной операции, не «в абстракт».

Основа: админка РВС (RBAC 4 роли × 9 прав, server-side sessions, audit 90 дней, password policy) — паттерн проверен в проде РВС.

2. Модель данных (новые таблицы + алиасы)

2.1 Поисковая операция (мультипоиск)

search_operations:
  id           uuid PK
  title        text            -- «Поиск: Иванов П., Минский р-н»
  case_id      fk → cases      -- карточка (матмодель живёт в cases)
  status       enum(planned, active, paused, completed, archived)
  unit_id      fk → mchs_units -- ответственное подразделение (ОУМЧС/Г(Р)ОЧС)
    contour_operation_id uuid null  -- заполняется при первой отправке зон в КОНТУР
  created_by   fk → users
  created_at / updated_at / closed_at

Решение по «case vs operation»: НЕ дублируем SEARCH_OPERATION из КОНТУРа — cases остаётся карточкой+расчётом (B15-слои на месте), search_operations — надстройка со статусом/владением/участниками. КОНТУР-маппинг: поле contour_operation_id, заполняется при первой отправке зон.

2.2 Пользователи и подразделения (RBAC по образцу РВС)

mchs_units:                      -- справочник подразделений МЧС
  id uuid PK, name, region (область), kind (rcu|oblast|gor_rayon), parent_id null
  -- РЦУ РЧС (kind=rcu) → ОУМЧС (oblast) → Г(Р)ОЧС (gor_rayon)

users (расширение существующей):
  + unit_id       fk → mchs_units, null  -- подразделение (у РЦУ РЧС-admin = null)
  + position      text
  + status        enum(active, locked, disabled)
  + failed_login_count, locked_until, must_change_password
  + created_by, created_at, updated_at

roles / user_roles / role_permissions / permissions  -- как в РВС
sessions (server-side, token, ip, ua, expires_at, idle_timeout)
auth_events (login/logout/lockout, indexed DESC)
audit_events + audit_changes (field-level old→new, immutable, retention 90d)
security_settings (password policy, lockout, timeouts)

Роли (стартовые, is_system):

  • admin — РЦУ РЧС: все права, все операции
  • coordinator — ОУМЧС: все поиски своей области, управление пользователями подчинённых Г(Р)ОЧС
  • operator — Г(Р)ОЧС: свои поиски (create/update)
  • observer — только просмотр (старшие смены, аналитика)

2.2.1 Скоуп данных (отличие от РВС)

К функциональным правам РВС добавляется географический скоуп: какие поиски видны. Фильтр по unit_id ∈ {своё подразделение + подчинённые} (дерево mchs_units); admin (РЦУ РЧС) — без фильтра. Тот же скоуп в cases/analyze/water.

3. Мультипоиск в UI

  • Дашборд «Активные поиски» (главная после логина): карточки активных операций (title, подразделение, elapsed, статус), фильтры по статусу/области, счётчики. Клик → рабочее пространство операции (/op/{id}/…).
  • Создание поиска = CaseForm + шаг «операция» (title, подразделение — по умолчанию подразделение создателя).
  • Завершение: статус completed + поля исхода (переезд из cases).

4. КОНТУР-интеграция (мультипоиск)

  • Импорт зон готов на КОНТУРе (этап 9): operation_id задаётся параметром импорта; ВЕКТОР хранит contour_operation_id и передаёт при отправке.
  • Отправка зон: кнопка/авто при новой версии модели → POST /api/v1/vector/zones.
  • Обратный поток: GET /api/v1/vector/areas-checked — «проверенные квадраты» на карте ВЕКТОРа.
  • Межсервисный токен — в secrets, не в репо.

5. Этапы внедрения

# Этап Состав Зависимости
E1 RBAC-ядро mchs_units, расширение users, роли/права, сессии, auth-журнал, seed; логин по пользователям, /me, смена пароля паттерн РВС
E2 Скоуп и аудит гео-скоуп в эндпоинтах, audit_events, security_settings, админ-панель пользователей (как РВС) E1
E3 Операции search_operations + статусная машина, дашборд активных поисков, переключение E1
E4 КОНТУР-мультипоиск contour_operation_id, отправка зон, areas-checked на карте E3
E5 B17 (лёгкий) ингестия field_observations от КОНТУРа (tracks/areas_checked) E4

6. Границы (НЕ делать сейчас)

  • Саморегистрация подразделений — аккаунты создаёт РЦУ РЧС/ОУМЧС.
  • SSO/ActiveDirectory — после пилота.
  • B18/B19 — отдельные задачи.
  • WebSockets для списка поисков — polling на старте.

7. Критерии приёмки (сводно)

  • Два одновременных активных поиска у двух подразделений: РЦУ РЧС видит оба, Г(Р)ОЧС — только свой; переключение без потери контекста.
  • Вход через server-side сессии; аудит фиксирует входы и админ-действия, ретенция 90 дней.
  • Зоны операции уезжают в КОНТУР и видны в штаб-панели CT130:8080 под своим operation_id; вторая операция не смешивается.