---
type: tech-risks
work_item_id: ET-012
title: "Технические риски — ET-012: Снижение minzoom публичных треков до z5"
version: 1
status: approved
created_at: 2026-06-04
authors:
  - "agent:architect"
---

# Технические риски — ET-012

Технические риски этапа снижения нижнего порога видимости слоя
публичных GPS-треков с z=8 до z=5. Бизнес-риски — в BRD §5
(R-1..R-10). Шкала: вероятность (Н/С/В) × влияние (Н/С/В).

## R-T-1 — Размер MVT-тайла z=5 > 200 KB на реальных данных

- **Описание:** На густонаселённых регионах (Москва, Урал) при росте
  БД до 5000+ треков фильтр `length_m ≥ 10000` + `limit=1500` может
  не сработать как страховка: 1500 треков × 200 байт после
  упрощения = ~300 KB до gzip, что близко к гейту M-8 (200 KB
  декомпрессировано на клиенте).
- **Вероятность / Влияние:** С / С.
- **Митигация:**
  - **Архитектурное решение (ADR-016 §T-2):** выбраны намеренно
    консервативные параметры (`min_length 10 км`, `limit 1500`) — это
    компромисс, а не «впритык». Запас 30-50% по M-8 при текущей БД
    (~500 треков ЦФО).
  - **Хук на снижение:** если PERF-Z5-01 или AC-10 покажут размер
    > 200 KB — снизить `limit` до 1000 в `build_gps_mvt`. Это правка
    одной константы, не требует архитектурного re-decide
    (см. ADR-016 §«Технический долг»).
  - **Тесты:** IT-Z5-01, IT-Z5-02 (REQ-F-11) — гейтируют размер на
    50-200 треков; ручная проверка AC-10 — на реальной БД test-среды
    после деплоя.

## R-T-2 — DP-tolerance 4 км на z5 «убивает» геометрию треков 10-15 км

- **Описание:** Трек длиной 12 км с реальной траекторией (зигзаги
  лесных дорог) после Douglas-Peucker с tolerance 0.04° (~2.6 км
  по долготе на 55° с.ш.) превращается в 2-3 точки → визуально
  «прямая линия от А до Б». Пользователь думает, что трек прямой,
  и недооценивает сложность.
- **Вероятность / Влияние:** В / Н.
- **Митигация:**
  - **Архитектурное решение (ADR-016 §T):** на z5 трек ≤ 5 км
    схлопывается в прямую — это **спецификация**, не баг (BRD §5 R-2).
    На z5 пиксель ≈ 5 км, поэтому даже идеально точный зигзаг
    не видно глазом.
  - **Спецификация поведения** для пользователя: «z5 — общий обзор
    сети; для деталей зумьте до z=10+». Это документировано в
    BRD §2.2 и TRZ §6.
  - **Тест:** TC-UI-12-Z5-Q (качественный) — оператор глазами
    проверяет, что на z5 видны минимум 3 разных «нити» в кадре
    (AC-08).

## R-T-3 — Линия `0.5 px` на z5 невидима на 1×-DPR мониторе

- **Описание:** Если бы оставили `interpolate [..., 8, 1.0, ...]`,
  на z=5 MapLibre сэмплирует значение слева от первого стопа = 1.0,
  но после anti-aliasing на 1× мониторе линия «съедается» до ≤ 0.5px.
- **Вероятность / Влияние:** С (без митигации — В) / Н.
- **Митигация:**
  - **Архитектурное решение (ADR-016 §L-B / REQ-F-05):** явный стоп
    `5, 0.8` в `_gpsLayerDef.paint['line-width']`. 0.8 CSS-px = 1
    физ.px на 1×-мониторе после округления GPU. Стоп `5, 1.8`
    в `_gpsHaloDef` (соотношение ~2.25×) — ореол не «съедает» линию.
  - **Тесты:** TC-UI-01-Z5 (Playwright), TC-UI-10-Z5-MOBILE
    (mobile viewport) — гейтируют видимость линии.

## R-T-4 — bbox-запрос на z5 тянет всю БД (R-tree fallback to full scan)

- **Описание:** Один z=5 тайл накрывает ~1250×1250 км по экватору,
  ~700×1250 на 55° с.ш. При БД 5000 треков по ЦФО — все 5000 строк
  имеют bbox внутри тайла, R-tree-индекс возвращает все ROWID, и
  далее SQLite делает SCAN по 5000 строк для подгрузки полей. На
  CI-runner это ≤ 100 мс, на mva154 — оценочно ≤ 150 мс (HDD-storage).
- **Вероятность / Влияние:** С / Н.
- **Митигация:**
  - **Архитектурное решение (ADR-016 §B):** buffer 10% bbox **не
    меняем** в MVP — лишний 10%-запас погоды не делает при том, что
    основной фильтр — Python-фильтр по `length_m` после SELECT.
  - **PERF-Z5-01** (REQ-F-13) — гейт; при росте БД и деградации —
    добавляем индекс на `length_m DESC` отдельным минорным патчем
    (см. ADR-016 §«Технический долг»).
  - **Метрика M-6/M-7** — наблюдаем p95 в `uvicorn.access` после деплоя
    (см. `07-infra-requirements.md` §7.1).

## R-T-5 — LRU 1024 переполняется при walk-through-world

- **Описание:** Если пользователь панорамирует карту на z=5 по всему
  миру, видит ~1024 уникальных тайла (z5 = 32×32). Серверный
  `_gps_tile_cache` ёмкостью 1024 при FIFO-вытеснении начинает
  выкидывать ранее запрошенные → повторный pan дёргает cold-build
  снова.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
  - **Архитектурное решение (ADR-016 §C):** размер LRU 1024 **не
    меняем** в MVP. На практике пользователь работает с регионом
    (ЦФО + соседние области = ~20-30 тайлов z5).
  - **Метрика M-11** — гейт; если cache hit ratio < 80% — поднимаем
    до 2048 отдельным патчем.
  - **Альтернатива** (отложена): pre-render z=5 grid на диск при
    деплое (ADR-016 §P-B отклонён в MVP, но открыт для отдельного
    work-item).

## R-T-6 — Hint «Зум 8+» забыт в HTML → пользователь видит линии и подсказку «увеличь зум»

- **Описание:** В `src/web/index.html` строка
  `<span ... id="public-tracks-zoom-hint">Зум 8+</span>`. Если в
  ходе реализации правка REQ-F-07 потеряется (например, мердж-конфликт),
  у пользователя на z<5 будет hint «Зум 8+», который противоречит
  фактическому порогу 5.
- **Вероятность / Влияние:** С / Н.
- **Митигация:**
  - **Архитектурное решение (REQ-F-07):** в HTML текст явно меняется
    на «Зум 5+». Логика показа в `_syncGpsLayersVisibility`
    автоматически использует `GPS_TRACKS_MIN_ZOOM` — порог переезжает
    автоматически.
  - **Тесты:** AC-05 (текст «Зум 5+»), TC-UI-04-HINT-OFF /
    TC-UI-05-HINT-ON (Playwright).
  - **Acceptance check** в `02-trz.md` REQ-F-01 `grep` — гарантирует,
    что других вхождений константы со старым значением нет.

## R-T-7 — Halo на спутнике на z5 «глушит» подложку

- **Описание:** Если halo-line-width на z5 окажется слишком большим
  (например, по ошибке остался стоп `5, 4.0`), белый ореол на
  спутниковой подложке закрывает большую часть рельефа в кадре.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
  - **Архитектурное решение (REQ-F-06 / ADR-016 §L-B):** halo z5
    = 1.8 CSS-px; ограничено F-10 BRD `≤ 2 px`. Соотношение к
    line-width (1.8 / 0.8 ≈ 2.25) — стандартное для трэйл-линий.
  - **Тесты:** TC-UI-11-Z5-SAT (Playwright со спутниковой подложкой);
    AC-17 (halo-width ≤ 2 px, halo не «глушит» подложку).

## R-T-8 — Регрессия на z=8..11 из-за разделения tier z≤7 на z≤5/z=6/z=7

- **Описание:** В новой tier-таблице (ADR-016 §«Решение» п.2) ранее
  единый блок `z ≤ 7 → min_length=2000, limit=3000` разбит на
  `z≤5: min_length=10000, limit=1500 | z=6: min_length=5000, limit=2000 | z=7: min_length=2000, limit=3000`.
  Регрессия может проявиться, если при разбиении нечаянно поломан
  z=7 (например, ошибочный `elif z <= 7` вместо `elif z == 7`).
- **Вероятность / Влияние:** С / С.
- **Митигация:**
  - **Архитектурное решение (REQ-F-03):** код-сниппет в TRZ §3.3
    точно указывает структуру `if z <= 5 / elif z == 6 / elif z == 7 / elif z <= 9 / ...`.
  - **Регрессионные тесты:** UT-Z7-01, UT-Z8-01, UT-Z12-01 (REQ-F-09),
    IT-REGRESS-Z8-01, IT-REGRESS-Z10-01 (REQ-F-12), AC-06.
  - **Code review** проверяет if-elif-цепочку построчно.

## R-T-9 — Cache poisoning: после deploy старые тайлы z8-z11 остались с прежней tier-логикой

- **Описание:** `_gps_tile_cache` — in-memory FIFO; при перезапуске
  `app` он очищается автоматически. Но если оператор `docker compose
  restart app` не сделал, а только `docker compose up -d --no-deps app`
  пересобрал образ → новый процесс стартует с пустым кэшем, всё ок.
  Риск только при использовании `docker compose exec` или
  hot-reload (не наш случай в проде).
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
  - **Архитектурное решение:** `docker compose up -d --no-deps app`
    в `07-infra-requirements.md` §6.2 шаг 2 — пересоздаёт контейнер,
    кэш пустой.
  - **Подстраховка:** `POST /api/gps-tracks/cache/clear` в шаге 4
    (на случай race conditions).
  - **Браузерный кэш:** MapLibre LRU при reload очищается;
    `Cache-Control: max-age=300` ограничивает максимум 5 минут
    «застрявших» тайлов в браузерном кэше.

## R-T-10 — `_simplify_coords` падает с ValueError при пустом coords на z=5

- **Описание:** Существующий код: `if len(coords) < 3: return coords`
  — защита от пустых/коротких массивов. После добавления tier для
  z5 проверка остаётся. Но: `shapely.LineString(coords).simplify(0.04, ...)`
  при tolerance ≥ длины трека вернёт LineString из 2 точек (концы)
  или пустую коллекцию. Если результат пустой — fallback `return coords`
  возвращает оригинал.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
  - **Архитектурное решение:** существующий fallback
    `return result if len(result) >= 2 else coords` (mvt.py:50)
    остаётся. Покрытие тестом UT-SIMP-Z5-02 (зигзаг 100 точек →
    2 точки = валидный LineString).
  - **Дополнительный тест** (рекомендуется в pull request):
    `_simplify_coords([(37.0, 55.0), (37.001, 55.001)], 5)` →
    возвращает оригинал (2 точки).

## R-T-11 — Размер MVT z=5 = 0 байт на регионе без длинных треков

- **Описание:** После фильтра `length_m ≥ 10000` в регионах
  с только короткими треками (например, лесопарки внутри города)
  тайл z=5 содержит 0 фич → возвращается `b""`.
  `_row_to_geojson_feature` / `build_gps_mvt` возвращают пустой
  protobuf, что MapLibre корректно интерпретирует как «фич нет».
- **Вероятность / Влияние:** С / Н (это **ожидаемое поведение**).
- **Митигация:**
  - **Архитектурное решение:** на z=5 в регионе без длинных треков —
    пусто. Это **специфицировано** в BRD §2.2 и AC-03 (требуется БД
    с ≥ 50 треков ≥ 10 км по ЦФО).
  - **Тест:** IT-Z5-03 (REQ-F-11) — тайл z=5 за пределами региона
    возвращает 200 с пустым телом.
  - **UX:** пользователь видит «пустую карту» на z=5, но hint не
    показывается (zoom ≥ 5); если пользователь зумит до z=8, появляются
    короткие треки. Естественная семантика.

## R-T-12 — Старый клиент (закэшированный в браузере) делает запросы только на z≥8

- **Описание:** Пользователь с открытой вкладкой неделю назад имеет
  закэшированный `gps_tracks.js` со старым `GPS_TRACKS_MIN_ZOOM = 8`.
  После деплоя при reload `gps_tracks.js` обновится (если есть
  `?v=...` versioning) или дотянется service-worker'ом. **Service
  worker — не настроен в MVP** (PH-9 не реализована).
- **Вероятность / Влияние:** С / Н.
- **Митигация:**
  - **Архитектурное решение:** `src/web/index.html` загружает
    `gps_tracks.js` напрямую (без SW). При reload браузер дёрнет
    последнюю версию (если nginx отдаёт нужные cache-headers).
    Если нет — пользователь сделает `Ctrl+F5` после очередного апа.
  - **Backwards compat:** старый клиент с `MIN_ZOOM=8` продолжает
    работать; он просто не запрашивает z=5..7. Никаких 4xx-ответов
    нет (REQ-F-15 — контракт не сломан).
  - **Митигация в долгую:** PWA / SW (PH-9, отдельный work-item)
    введёт правильную inval-стратегию.

## R-T-13 — DDoS на новый z=5 endpoint (бот ходит по 32×32 z5 grid)

- **Описание:** Поскольку endpoint без auth и без rate-limit,
  скрипт-крулер может запросить все 1024 тайла z=5 за минуту → 1024 ×
  ~200 мс build = ~3.5 минуты CPU на сервере. Не убийственно, но
  заметно.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
  - **Архитектурное решение:** rate-limit **не вводим в MVP** (см.
    `07-infra-requirements.md` §3.2). LRU кэш съест второй проход —
    cold пройдёт один раз.
  - **Мониторинг:** в первую неделю после деплоя оператор смотрит
    `nginx access.log` на аномалии (см. `07-infra-requirements.md` §7.1).
  - **Эскалация:** если обнаружится паттерн — `slowapi`-middleware
    (отдельный DevOps-task).

## R-T-14 — Конфликт с halo при переключении spectator/satellite на z5

- **Описание:** При переключении подложки `applyBaseLayer()` (ET-007)
  должен корректно показать/скрыть halo для GPS-треков. На z=5 halo
  активен (`zoom ≥ GPS_TRACKS_MIN_ZOOM AND zoom < GPS_TRACKS_ZOOM_CUTOFF AND base === 'satellite'`).
  Если в `applyGpsHaloVisibility` есть hardcoded порог z≥8 — будет
  расхождение.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
  - **Архитектурное решение:** в `gps_tracks.js` существующая
    логика `_syncGpsLayersVisibility` / `applyGpsHaloVisibility`
    использует `GPS_TRACKS_MIN_ZOOM` как константу — порог переезжает
    автоматически (verified by `grep` в TRZ §3 REQ-F-01).
  - **Тесты:** TC-UI-11-Z5-SAT (Playwright со спутниковой подложкой),
    AC-17.

## R-T-15 — Performance тест PERF-Z5-01 нестабилен на CI

- **Описание:** PERF-Z5-01 (REQ-F-13) измеряет p95 build_gps_mvt z=5
  при 500 треках. CI-runner может иметь cold I/O в первом прогоне
  → fail. Это flaky-тест.
- **Вероятность / Влияние:** С / Н.
- **Митигация:**
  - **Архитектурное решение:** PERF-тест с маркером `@pytest.mark.perf`
    запускается отдельным джобом (TRZ §3.13) — **не блокирует merge**.
    Логируется в `13-test-report.md` для тренд-анализа.
  - **Дизайн теста:** делать 10 повторов, отбрасывать первый
    (warmup) — стандартный паттерн для micro-benchmark'ов.
  - **Gate**: avg ≤ 200 мс, p95 ≤ 500 мс (gentle).

## R-T-16 — Конфигурация nginx gzip для `application/x-protobuf` пропала

- **Описание:** Если nginx config был перезатёрт (например, после
  переустановки) и `application/x-protobuf` не в `gzip_types`,
  размер MVT z5 пойдёт unzipped (~80 KB на тайл) → мобильный трафик
  и latency растут.
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
  - **Smoke-проверка** в `07-infra-requirements.md` §6.2 шаг 3:
    `curl -I` смотрит на `content-encoding: gzip` после деплоя.
  - Если gzip нет — операт восстанавливает nginx config из git
    (`infra/nginx/openclaw.conf` или эквивалент).

## Сводная таблица

| #     | Риск                                                              | Вер | Влиян | Митигация (тип)                     |
|-------|--------------------------------------------------------------------|-----|-------|--------------------------------------|
| R-T-1 | Размер MVT z5 > 200 KB                                              | С   | С     | Архитектурное (tier T-2) + гейт-тест |
| R-T-2 | DP-tolerance ломает геометрию коротких треков                       | В   | Н     | Спецификация (z5 = обзор)            |
| R-T-3 | Линия невидима на 1×-DPR                                            | С   | Н     | Архитектурное (line-width стоп 0.8)  |
| R-T-4 | bbox-запрос z5 тянет всю БД                                         | С   | Н     | Гейт-метрика + index-watch flag      |
| R-T-5 | LRU 1024 переполнение                                               | Н   | Н     | Метрика M-11; capacity hook          |
| R-T-6 | Hint «Зум 8+» забыт                                                 | С   | Н     | grep-проверка + UI-тест              |
| R-T-7 | Halo «глушит» подложку                                              | Н   | Н     | Архитектурное (1.8 px) + UI-тест     |
| R-T-8 | Регрессия z8-z11 из-за tier-rewrite                                  | С   | С     | Снимок tier в TRZ + регресс-тесты   |
| R-T-9 | Cache poisoning после deploy                                         | Н   | Н     | Procedure (cache clear) в infra      |
| R-T-10| `_simplify_coords` падает на пустых                                  | Н   | Н     | Existing fallback + unit-тест        |
| R-T-11| Пустой MVT в регионе без длинных треков                              | С   | Н     | Specified behavior + IT-Z5-03        |
| R-T-12| Старый клиент в кэше браузера                                        | С   | Н     | Backwards-compat (контракт)          |
| R-T-13| DDoS на новый z=5 endpoint                                           | Н   | Н     | LRU защищает; rate-limit отложен     |
| R-T-14| Halo не sync на z5                                                  | Н   | Н     | Existing-pattern reuse + UI-тест     |
| R-T-15| PERF-тест flaky на CI                                                | С   | Н     | Marker @perf, отдельный джоб         |
| R-T-16| nginx gzip пропал                                                    | Н   | С     | Smoke-проверка после деплоя          |

## Связанные документы

- `01-brd.md` §5 Бизнес-риски R-1..R-10 (часть пересекается)
- `02-trz.md` §3 REQ-F-09..F-14 (тесты), §4 NFR
- `06-adr/ADR-016-z5-tiling-policy.md` §«Решение», §«Последствия»
- `07-infra-requirements.md` §3 (rate-limit), §6 (procedure), §7 (мониторинг)
- `08-data-requirements.md` §3.4 (индексы), §5 (контракты)
