Files
enduro-trails/docs/work-items/ET-009/02-trz.md
claude-bot eaa6b4cd27
Some checks failed
CI / lint (push) Failing after 4s
CI / test (push) Failing after 5s
CI / build (push) Has been skipped
feat(ET-009): analyst artifacts — BRD, TRZ, AC, test plan
2026-06-01 17:51:47 +00:00

24 KiB
Raw Blame History

type, work_item_id, title, version, status, created_at, updated_at, authors, related
type work_item_id title version status created_at updated_at authors related
trz ET-009 ТЗ: Новые источники GPS-треков — EnduroRussia и Wikiloc 1 draft 2026-06-01 2026-06-01
agent:analyst
ET-008

ТЗ — ET-009: Новые источники GPS-треков — EnduroRussia и Wikiloc

1. Терминология

  • Source — внешний поставщик GPS-треков, описан записью в config/gps_sources.yaml. Реализуется python-классом-наследником SourceParser в src/api/gps_tracks/sources/<source_id>.py.
  • Region — географическая область сбора, описана записью в config/gps_regions.yaml. Содержит bbox и список активных sources для этой области.
  • Pipeline-guard — проверка _check_license_adr() в scripts/gps_collect.py, которая блокирует загрузку source-парсера если его ADR в license_adr имеет status != 'accepted'.
  • Activity-mapping — словарь MAPPING в каждом parser-модуле, переводящий внутренние категории источника в каноничные ACTIVITY_TYPES (src/api/gps_tracks/models.py).
  • Dedup-key — детерминированный ключ, по которому треки из разных источников сливаются в одну запись (реализация в src/api/gps_tracks/dedup.py:compute_dedup_key, ET-008).
  • Graceful-stop — поведение Wikiloc-парсера при HTTP 403/429: return из async-генератора без raise, что приводит к статусу прогона partial или rate_limited без падения процесса.

2. Архитектурные опоры из ET-008

ET-009 не строит новых модулей. Используются:

  • src/api/gps_tracks/sources/base.py:SourceParser — базовый класс.
  • src/api/gps_tracks/sources/enduro_russia.py:EnduroRussiaParser — реализован.
  • src/api/gps_tracks/sources/wikiloc.py:WikilocParser — реализован.
  • scripts/gps_collect.py — оркестратор pipeline, поддерживает per-source rate-limit, licensing-guard, dedup, upsert.
  • src/api/gps_tracks/db.py:upsert_track — merge по dedup_key, объединение sources и external_urls.
  • src/api/gps_tracks/endpoint.py/api/gps-tracks, /api/gps-tracks/tiles/{z}/{x}/{y}.mvt, /api/gps-tracks/health.
  • src/web/gps_tracks.js (или эквивалент в ET-008) — клиентский слой с динамическим фильтром источников.

ET-009 = конфиг + фикстуры + тесты + продакшн-прогон.

3. Требования

REQ-F-01 — Конфиг: enduro_russia.base_url

Файл config/gps_sources.yaml, запись с id: enduro_russia, поле base_url устанавливается в https://endurorussia.ru (без дефиса).

Текущее значение https://enduro-russia.ru (с дефисом) считается багом и должно быть заменено.

Acceptance check. После правки:

grep "base_url" config/gps_sources.yaml | grep enduro

выводит base_url: "https://endurorussia.ru".

REQ-F-02 — Конфиг: enduro_russia.enabled

В той же записи enabled: true.

Acceptance check. В config/gps_sources.yaml строка enabled: true находится непосредственно под id: enduro_russia.

REQ-F-03 — Конфиг: запись wikiloc

В config/gps_sources.yaml добавляется новая запись с полями:

  - id: wikiloc
    name: "Wikiloc"
    enabled: true
    license_adr: "docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md"
    base_url: "https://www.wikiloc.com"
    rate_limit_sec: 10
    user_agent: "enduro-trails/1.0 (+https://openclaw.mva154.duckdns.org/enduro/)"
    attribution: "© Wikiloc contributors"
    parser_module: "src.api.gps_tracks.sources.wikiloc"
    save_user_field: false
    source_priority: 70
    activity_filter: [motorcycle, enduro]
    max_tracks_per_run: 50

max_tracks_per_run — soft-cap для первого прогона, чтобы не тратить часы на rate-limit (см. BRD R-6); реализуется в parser'е через счётчик внутри collect(). Если поля в parser ещё нет — добавить поддержку:

max_tracks = self.config.get("max_tracks_per_run")
yielded = 0
# в основном цикле перед yield:
if max_tracks is not None and yielded >= max_tracks:
    logger.info("Wikiloc: reached max_tracks_per_run=%d, stopping", max_tracks)
    return
yielded += 1

REQ-F-04 — Конфиг: регион tsfo_plus_chuvashia

В config/gps_regions.yaml, запись tsfo_plus_chuvashia.sources дополняется до [osm, enduro_russia, wikiloc, ttrails]. Порядок важен: ttrails остаётся, но он enabled: false в sources.yaml — он автоматически пропускается guard'ом.

Поле enabled: true региона не меняется.

REQ-F-05 — Pipeline licensing-guard

scripts/gps_collect.py:_check_license_adr (строки 3773) не изменяется. Перед активацией ET-009 выполнить:

grep -E "^status:" docs/work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md
grep -E "^status:" docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md

Оба значения должны быть accepted. Иначе — STOP и эскалация архитектору.

REQ-F-06 — Тест-фикстура EnduroRussia API

Создаётся файл tests/fixtures/gps-tracks/enduro-russia-api-tracks-page1.json с реальным snapshot ответа https://endurorussia.ru/api/tracks?page=0&limit=50.

Минимальные требования к snapshot:

  • ≥ 5 items.
  • Каждый item содержит id (int), name (str), difficulty (str), created_at (str ISO).
  • Поле total (int) присутствует.

Снимок делается разово, вручную через curl, сохраняется в репо; не зависит от состояния сайта.

REQ-F-07 — Тест-фикстуры EnduroRussia GPX

Создаются 3 файла tests/fixtures/gps-tracks/enduro-russia-track-{1,2,3}.gpx с реальными GPX-файлами из API. Один из них должен:

  • содержать <trk><trkseg><trkpt> с ≥ 10 точками;
  • лежать в bbox региона tsfo_plus_chuvashia (29..47.5 longitude, 49.5..60.0 latitude);
  • иметь creator или metadata, идентифицирующее источник.

Второй GPX должен быть пустой (<trkseg></trkseg>) или с 0 trkpt — для проверки skip-логики _parse_gpx.

Третий GPX — c одной точкой за пределами bbox — для проверки bbox-фильтрации.

REQ-F-08 — Тест-фикстура Wikiloc HTML страницы поиска

Файл tests/fixtures/gps-tracks/wikiloc-search-page1.html — реальный снимок GET /wikiloc/find.do?act=19&sw=…&ne=…&page=0. Должен содержать ≥ 5 ссылок на треки в формате /trails/<slug>/<id>.

REQ-F-09 — Тест-фикстуры Wikiloc страницы трека и GPX

  • tests/fixtures/gps-tracks/wikiloc-trail-page.html — снимок страницы одного трека Wikiloc; должен содержать <h1> с названием и либо прямую ссылку на .gpx, либо downloadTrail.do?id=<id>.
  • tests/fixtures/gps-tracks/wikiloc-track.gpx — реальный GPX, возвращаемый /wikiloc/downloadTrail.do?id=<id> для трека, совпадающего по координатам с одним из EnduroRussia-треков — для теста dedup-merge.
  • tests/fixtures/gps-tracks/wikiloc-rate-limited.html — пустой файл (используется в тесте 429, реальный HTML не важен, достаточно тестового мока httpx, который вернёт 429).

REQ-F-10 — Unit-тесты EnduroRussia parser

Файл tests/unit/test_gps_tracks_enduro_russia.py (новый).

Покрытие:

  • UT-ER-01. _parse_gpx принимает фикстурный GPX enduro-russia-track-1.gpx → возвращает TrackInsert с points_count >= 10, min_lon/max_lon/min_lat/max_lat корректны, length_m > 0, external_url = "https://endurorussia.ru/tracks/<id>".
  • UT-ER-02. _parse_gpx принимает фикстуру enduro-russia-track-2.gpx (пустой) → возвращает None.
  • UT-ER-03. Bbox-фильтр: трек 3 (точка за пределами региона) при пересечении с region bbox → _bbox_intersects возвращает False, collect() не yield-ит этот трек.
  • UT-ER-04. MAPPING маппит "hard" → "enduro", "мото" → "moto", "unknown" → "other" (default через map_activity).
  • UT-ER-05. EnduroRussiaParser.__init__ принимает конфиг с base_url: "https://endurorussia.ru" и сохраняет его (без замены на дефис-вариант). Регрессия для R-4.
  • UT-ER-06. collect() корректно прерывается, когда fetched_so_far >= total.
  • UT-ER-07. При HTTP 429 на /api/tracks — генератор завершается без exception.
  • UT-ER-08. При HTTP 429 на /api/tracks/{id}/gpx — генератор завершается без exception, треки, уже yield-нутые до этого, сохраняются.

REQ-F-11 — Unit-тесты Wikiloc parser

Файл tests/unit/test_gps_tracks_wikiloc.py (новый).

  • UT-WL-01. _extract_track_paths из фикстуры wikiloc-search-page1.html возвращает ≥ 5 уникальных путей.
  • UT-WL-02. _extract_gpx_url: из HTML с downloadTrail.do?id=X возвращает абсолютный URL https://www.wikiloc.com/wikiloc/downloadTrail.do?id=X.
  • UT-WL-03. _extract_gpx_url: из HTML без явных ссылок возвращает fallback https://www.wikiloc.com/wikiloc/downloadTrail.do?id=<track_id>.
  • UT-WL-04. _extract_track_name извлекает текст <h1>.
  • UT-WL-05. _parse_gpx на фикстуре wikiloc-track.gpx возвращает TrackInsert с правильными bbox и activity_type='moto' (для activity-категории motorcycle).
  • UT-WL-06. MAPPING маппит "motorcycle" → "moto", "hiking" → "hike", "mtb" → "bicycle".
  • UT-WL-07. collect() останавливается при 403 на странице поиска (graceful-stop).
  • UT-WL-08. collect() останавливается при 429 на странице трека, но уже yield-нутые треки сохраняются.
  • UT-WL-09. Соблюдение rate_limit_sec: между двумя последовательными HTTP-запросами asyncio.sleep вызывается с аргументом ≥ конфигурируемого значения. (Mock asyncio.sleep, проверка count и аргументов.)
  • UT-WL-10. max_tracks_per_run: при max_tracks_per_run=2 и mock поиске на ≥ 5 треков — collect() yield-ит ровно 2 трека.

REQ-F-12 — Integration-тест pipeline на mock-источниках

Файл tests/integration/test_pipeline_et009.py (новый).

Использует respx или httpx_mock для подмены HTTP. Запускает scripts/gps_collect.py:main (через asyncio.run) с временной БД.

  • IT-ER-01. Pipeline с mock EnduroRussia (фикстурный JSON + 3 GPX) + регион tsfo_plus_chuvashia → в БД 2 трека (третий отфильтрован bbox-ом), pipeline_runs[-1].status='ok', tracks_new=2.
  • IT-WL-01. Pipeline с mock Wikiloc (фикстурный HTML поиска + 1 страница трека + 1 GPX) → в БД 1 трек, pipeline_runs[-1].status='ok', tracks_new=1.
  • IT-WL-02. Mock Wikiloc возвращает 403 на странице поиска → pipeline_runs[-1].status='partial' или 'rate_limited', tracks_new=0, exit-code pipeline не 0 (есть error) либо exit-code 0 при условии что graceful-stop не считается error — выбрать одно поведение и зафиксировать тест на нём. Решение: graceful-stop ≠ error, exit-code 0, status 'partial'.
  • IT-DEDUP-01. Pipeline сначала собирает EnduroRussia (1 трек), затем Wikiloc (1 трек с теми же координатами и длиной ±5%, той же датой ±1 день) → в БД одна запись с sources=['enduro_russia','wikiloc'], external_urls=[endurorussia.ru/…, wikiloc.com/…], метаданные имеют приоритет enduro_russia (если source_priority=80 выше чем у wikiloc=70 — см. ET-008 dedup-merge).
  • IT-LIC-01. Искусственно поменять status: acceptedstatus: proposed в копии ADR-010 (через временный GPS_SOURCES_CONFIG env с другим путём license_adr) → pipeline пропускает source с pipeline_runs[-1].status='skipped_license'.

REQ-F-13 — Стили: цвета по источнику

В файлах src/web/style.json и src/web/style-dark.json слой gps-tracks-layer (или его эквивалент из ET-008) содержит match-expression line-color:

[
  "match",
  ["get", "source"],
  "osm",            "#3cb44b",
  "enduro_russia",  "#e6194b",
  "wikiloc",        "#4363d8",
  "#808080"
]

Цвета — приближённо, окончательная палитра согласуется с UX в момент реализации. Главное: для всех трёх известных источников ID-→-цвет задан, fallback есть.

Аналогично для gps-tracks-halo-satellite — halo всегда белый/ полупрозрачный, цвет линии берётся тот же.

REQ-F-14 — Атрибуция

После первого прогона, при наличии в БД треков из enduro_russia, endpoint /api/gps-tracks/health возвращает в поле attributions (если уже есть в ET-008) или в эквивалентном — список:

["© OpenStreetMap contributors (ODbL)", "EnduroRussia.ru", "© Wikiloc contributors"]

Клиент src/web/gps_tracks.js подмешивает эти строки в MapLibre attribution control (через map.getControl(...) или эквивалент). Если в ET-008 атрибуция формируется на клиенте по статическому маппингу source_id → label — расширить маппинг:

const SOURCE_ATTRIBUTIONS = {
  osm: "© OpenStreetMap contributors (ODbL)",
  enduro_russia: "EnduroRussia.ru",
  wikiloc: "© Wikiloc contributors",
  ttrails: "ttrails.ru",
};

REQ-F-15 — Контрактный smoke-тест EnduroRussia API

Файл tests/contract/test_endurorussia_api_smoke.py (новый, помечается маркером @pytest.mark.network и не запускается в обычном CI; запускается вручную или в nightly).

  • CT-ER-01. GET https://endurorussia.ru/api/tracks?page=0&limit=5 возвращает 200, JSON с ключами items, total.
  • CT-ER-02. GET https://endurorussia.ru/api/tracks/{first_id}/gpx возвращает 200, Content-Type содержит xml или gpx, тело парсится defusedxml без exception.

Назначение: при поломке внешнего API мы узнаём об этом из nightly, а не из тишины health-эндпоинта.

REQ-F-16 — Контрактный smoke-тест Wikiloc (опционально)

Из-за rate-limit и риска бана не делаем регулярный smoke-тест Wikiloc. Вместо этого фиксируем в docs/work-items/ET-009/13-test-report.md после первой ручной проверки факт того, что find.do отвечает 200 с ожидаемой структурой.

REQ-F-17 — Первый продакшн-прогон

После мерджа в main и деплоя в test-среду оператор запускает:

ssh mva154
cd /opt/enduro-trails
python scripts/gps_collect.py --region tsfo_plus_chuvashia --source enduro_russia
# (ждать ≈ 25 минут)
python scripts/gps_collect.py --region tsfo_plus_chuvashia --source wikiloc
# (ждать до достижения max_tracks_per_run, обычно 10-20 минут)

Результат фиксируется в 14-deploy-log.md:

  • tracks_by_source.enduro_russia (ожидаем ≥ 200);
  • tracks_by_source.wikiloc (ожидаем ≥ 1);
  • длительность каждого прогона;
  • errors_count (ожидаем 0 или ≤ 5% от tracks_new).

REQ-F-18 — Не менять контракт /api/gps-tracks

Endpoint /api/gps-tracks сохраняет интерфейс ET-008. Новые ID источников (enduro_russia, wikiloc) появляются в значениях полей ответа естественным образом; никаких новых query-параметров или полей в FeatureCollection не вводится.

REQ-F-19 — Не менять алгоритм дедупликации

compute_dedup_key в dedup.py не меняется. Никаких новых правил для пары (enduro_russia, wikiloc) — стандартный bbox+length+date-алгоритм должен справиться (см. ADR-006).

REQ-F-20 — Документация

В docs/work-items/ET-009/ должны существовать после Анализа:

  • 00-business-request.md (есть)
  • 01-brd.md (создаётся в ET-009)
  • 02-trz.md (этот файл)
  • 03-acceptance-criteria.md
  • 04-test-plan.yaml
  • 04b-ui-test-cases.md

После реализации добавляются: 07-infra-requirements.md, 08-data-requirements.md, 10-tech-risks.md, 12-review.md, 13-test-report.md, 14-deploy-log.md.

4. Не-функциональные требования

NFR-01 — Производительность сбора

EnduroRussia: при rate_limit_sec=5 и 305 треках полный прогон региона tsfo_plus_chuvashia укладывается в ≤ 30 минут (305 × 5 сек ≈ 25 мин + overhead).

Wikiloc: первый прогон ограничен max_tracks_per_run=50 → максимум 50 × (1 search + 1 trail + 1 gpx) × 10 сек ≈ 25 минут.

NFR-02 — Стабильность

Падение Wikiloc-парсера не должно валить весь pipeline. Покрывается существующей логикой scripts/gps_collect.py (per-source error не помечает остальные как error).

NFR-03 — Размер БД

Прирост data/gps_tracks.sqlite после первого прогона ET-009: ≤ 100 MB при 200 треков EnduroRussia + 50 Wikiloc. Если фактический прирост существенно больше — фиксируется в 14-deploy-log.md.

NFR-04 — Логирование

Pipeline и parser используют существующий logger стандартного формата. Никаких новых форматов или sinks ET-009 не добавляет.

NFR-05 — Безопасность

XML-парсинг GPX выполняется через defusedxml.ElementTree (как в ET-008). Никаких изменений по security ET-009 не вносит.

NFR-06 — Совместимость

Контракт /api/gps-tracks* не меняется. Существующие клиенты (включая старые версии браузеров пользователей) продолжают работать без обновления.

5. План работ (для разработчика)

  1. Сверка ADR-010 / ADR-012 → status: accepted (REQ-F-05). Если нет — STOP.
  2. Правка config/gps_sources.yaml (REQ-F-01, F-02, F-03).
  3. Правка config/gps_regions.yaml (REQ-F-04).
  4. Снапшот реальных ответов API/HTML и сохранение как фикстуры (REQ-F-06..F-09). Снимки берутся до unit-тестов, чтобы тесты опирались на реальные данные.
  5. Расширение Wikiloc-парсера max_tracks_per_run (если ещё нет).
  6. Написание unit-тестов (REQ-F-10, F-11).
  7. Написание integration-тестов (REQ-F-12).
  8. Контрактный smoke-тест EnduroRussia (REQ-F-15).
  9. Расширение стилей карты (REQ-F-13).
  10. Атрибуция в клиенте (REQ-F-14).
  11. Прогон всех тестов локально (make test).
  12. Code review → merge → deploy в test.
  13. Ручной первый прогон (REQ-F-17). Запись в 14-deploy-log.md.
  14. Проверка UI по тест-плану 04b-ui-test-cases.md.

6. Открытые вопросы и решения по умолчанию

Вопрос Решение по умолчанию
Считать ли graceful-stop Wikiloc ошибкой? Нет. pipeline_runs.status='partial', exit-code 0. (См. IT-WL-02.)
Запускать ли cron автоматически после ET-009? Нет. Cron включается отдельным DevOps-task'ом после двух успешных ручных прогонов подряд.
Маппить ли wikiloc.act=motorcycle (19) на enduro или moto? moto (более широкая категория). MAPPING уже так сконфигурирован.
Что делать с старым URL enduro-russia.ru в external_url ранее собранных треков? Опциональный one-shot UPDATE-скрипт; в ET-009 не обязателен (база test-среды чистая для практических целей).
Wikiloc возвращает creator=Wikiloc в GPX тех же треков, что и EnduroRussia? Нормально — на это и нужен dedup-merge.
Нужно ли менять source_priority? Нет. osm=100, enduro_russia=80, wikiloc=70 — порядок задаёт приоритет метаданных при merge.