architect(ET): auto-commit from architect run_id=79
Some checks failed
CI / lint (push) Successful in 5s
CI / test (push) Failing after 5s
CI / build (push) Has been skipped

This commit is contained in:
2026-06-04 09:40:50 +00:00
parent 7df1ffe75c
commit 6b88bcee28
5 changed files with 1263 additions and 0 deletions

View File

@@ -20,3 +20,4 @@
| ADR-014 | GPX-download эндпоинт публичного трека: `xml.etree.ElementTree`-builder + fetch+Blob на клиенте | accepted | 2026-06-03 | [ET-011](../../work-items/ET-011/06-adr/ADR-014-gpx-download-endpoint.md) |
| ADR-015 | Политика реэкспорта публичных треков: per-source `download_allowed` в `gps_sources.yaml`, default-deny (whitelist `osm` для MVP) | accepted | 2026-06-03 | [ET-011](../../work-items/ET-011/06-adr/ADR-015-source-redistribution-policy.md) |
| ADR-016 | Снижение minzoom публичных GPS-треков до z5: калибровка существующих tier-таблиц `build_gps_mvt`/`_simplify_coords`, on-demand MVT остаётся, без heat-map/clustering | accepted | 2026-06-04 | [ET-012](../../work-items/ET-012/06-adr/ADR-016-z5-tiling-policy.md) |
| ADR-017 | Zoom-aware paint для hillshade/TRI на z9-z11: `interpolate`-выражения по `raster-opacity` и `raster-contrast`, `raster-resampling: 'nearest'`, понижение UI-минзума hillshade с 10 до 9; без перегенерации растровых тайлов | accepted | 2026-06-04 | [ET-013](../../work-items/ET-013/06-adr/ADR-017-zoom-aware-terrain-paint.md) |

View File

@@ -0,0 +1,367 @@
---
type: adr
work_item_id: ET-013
adr_id: ADR-017
title: "ADR-017: Zoom-aware paint для hillshade/TRI — калибровка клиентских raster-слоёв вместо перегенерации тайлов"
status: accepted
created_at: 2026-06-04
updated_at: 2026-06-04
authors:
- "agent:architect"
supersedes: []
superseded_by: []
labels:
- "ET-013:terrain-paint"
- "minor-change"
---
# ADR-017 — Zoom-aware paint для hillshade/TRI на z9-z11
## Статус
**Accepted.** Архитектурное решение для ET-013.
Это **калибровка клиентского рендера** растровых terrain-слоёв
(а не пересмотр архитектуры рельефа из PH-6). BRD §3 F-14 допускает
отсутствие отдельного ADR. ADR оформляется по прецеденту ADR-016
(ET-012) — ради единого индекса архитектурных решений и чтобы
зафиксировать **причины отклонения** более «жирных» альтернатив
(перегенерация hillshade с z-factor 2.5, переход на raster-dem,
multidirectional hillshade, theme-specific paint-таблицы), иначе
они вернутся в обсуждение в следующем work-item.
## Контекст
### Текущее состояние (после PH-6 / ET-007)
- Растровые тайлы рельефа нарезаны **z8-z14** (PNG 256×256) из
SRTM 30 м: hillshade (azimuth 315°, altitude 45°, z-factor 1.5),
TRI (5-уровневая классификация), hypso (в UI не подключён).
- Раздача — `GET /terrain/{layer}/{z}/{x}/{y}.png` через FastAPI
(`src/api/main.py:1240`), `Cache-Control: immutable`.
- Клиент (`src/web/app.js`) создаёт MapLibre raster source/layer
динамически в `applyTerrainLayer(id, tileUrl, enabled, opacity,
minzoom, maxzoom)`. **Сигнатура хардкодит paint:**
`{ 'raster-opacity': opacity_number, 'raster-resampling': 'linear' }`.
- Вызовы (`src/web/app.js:2782-2783`):
- hillshade: `opacity=0.40, minzoom=10, maxzoom=15`.
- TRI: `opacity=0.70, minzoom=5, maxzoom=15`.
- UI-минзум hillshade в `updateHillshadeAvailability` (строка 3368):
`if (zoom < 10) cb.disabled = true`.
- В стилях `style.json` / `style-dark.json` растровых terrain-слоёв
**нет** — они добавляются динамически из JS.
### Проблема
При зумах z9-z11 (ключевой масштаб для выбора эндуро-маршрута между
двумя точками) рельеф визуально «теряется»:
- z9: hillshade выключен UI-гейтом, TRI с opacity 0.70 виден, но
пятна мельче чем на z8.
- z10-z11: hillshade включается, но opacity 0.40 + отсутствие
усиления контраста + linear-resampling делают тени «бледной
плёнкой»; TRI на тех же opacity не компенсирует.
Архитектурный вопрос: **как восстановить выразительность z9-z11
без перегенерации растровых тайлов, без новых endpoint'ов, без
новых данных и без смены paint-pipeline'а у MapLibre.**
## Рассмотренные варианты
### Вариант P (Pipeline) — где править
- **P-A — Frontend paint-калибровка** (выбран):
- paint hillshade/TRI становится zoom-aware через MapLibre
`interpolate`-выражение по `['zoom']`.
- Меняются параметры существующих paint-properties:
`raster-opacity`, `raster-contrast`, `raster-resampling`.
- 0 изменений в backend, 0 в тайлах на диске.
- **P-B — Перегенерация hillshade с z-factor 2.5-3.0 для z9-z14.**
Отклонён в этой задаче:
- Требует доступа к infra-pipeline SRTM, пересборки и редеплоя
растровых тайлов (без CI-автоматизации сейчас).
- Долгий feedback-loop (часы регенерации на регион); калибровка
paint даёт результат за минуты.
- Затрагивает все zoom-уровни сразу, в т.ч. z8 (регрессия BRD F-11).
- **Открыт как follow-up** «hillshade-rerender-z9-z14», если P-A
окажется недостаточным.
- **P-C — Переход на MapLibre `hillshade` (raster-dem) layer.**
Отклонён:
- Требует поднять DEM в формате Terrarium/Mapbox-RGB (новый
pipeline, новые тайлы, новый source-type, новые URL).
- Это смена архитектуры рельефа, не калибровка. Большой скачок
рисков и времени реализации.
- Не решает поставленную проблему быстрее, чем P-A.
- **P-D — Векторные горизонтали (contours).**
Отклонён:
- Контуров в стэке нет. Это новая фича уровня PH-6.5, требует
pipeline на отдельных vector tiles (планировщик стилей,
атрибуты высот, симплификация).
- Не заменяет hillshade/TRI, а дополняет — другая фича.
- **P-E — Multidirectional hillshade (4 азимута, blend).**
Отклонён:
- Требует пересборки тайлов и комбинирующего layer.
- Дороже P-A на порядок при том же визуальном эффекте на z9-z11.
### Вариант O (Opacity scaling) — как именно скалировать opacity
- **O-A — Step-функция через `case [zoom_in [9,10,11]]`.** Отклонён —
ступенчатые скачки видны как «вспышки» при плавном зуме.
- **O-B — Linear `interpolate` со stops для z9-z14** (выбран):
- Hillshade `raster-opacity`: `9→0.65, 10→0.60, 11→0.55, 12→0.50, 14→0.40`.
- Поведение на z<9 не определено (но не нужно — UI-гейт отключает слой).
- На z14-z15 значение «закреплено» на исходных 0.40 (clamping
у MapLibre на верхнем стопе) → регрессия z14 (BRD F-12, AC-10)
выполняется автоматически.
- TRI `raster-opacity`: `5→0.55, 7→0.65, 8→0.70, 9→0.80, 10→0.85,
11→0.85, 12→0.75, 15→0.70`.
- Точка `8→0.70` явная → регрессия z8 (BRD F-11, AC-06) выполняется
автоматически.
- **O-C — Exponential `interpolate ['exponential', 2]`.** Отклонён:
- Перерасход контраста на z11-z12 → темно/«пересвет» (R-1).
- Linear проще и достаточен для 5 stops в узком диапазоне.
### Вариант C (Contrast) — добавлять ли raster-contrast
- **C-A — Добавить `raster-contrast` zoom-aware для hillshade**
(выбран):
- Stops: `9→0.40, 10→0.35, 11→0.30, 12→0.15, 14→0.00`.
- На z14 значение 0 → регрессия (AC-10) выполняется автоматически.
- Только для hillshade. На TRI контраст не имеет смысла
(категориальная палитра), его не трогаем.
- **C-B — Не трогать контраст, поднять только opacity.** Отклонён:
- Opacity 0.65 без контраста на z9 — это просто «более тёмная
плёнка», а не «более выразительный рельеф». Качественный тест
(TC-UI-04-Z10-Q) на этом варианте не пройдёт.
- **C-C — Уменьшать `raster-brightness-min/max` вместо contrast.**
Отклонён:
- Более сложная двухпараметрическая настройка для того же эффекта.
- `raster-contrast` — стандартный для подобных случаев property.
### Вариант R (Resampling) — nearest vs linear
- **R-A — `'nearest'` на hillshade и TRI** (выбран):
- hillshade на nearest сохраняет «жёсткие края» теней SRTM — рельеф
читается резче.
- TRI — категориальная палитра; linear-resampling размывает границы
между уровнями шероховатости → пятна «текут». `'nearest'`
сохраняет границы.
- MapLibre **не поддерживает** `interpolate` для `raster-resampling`
→ выбираем глобально `'nearest'` для обоих слоёв. На z12-z14
компромисс приемлем (текстура остаётся читаемой при overzoom;
см. R-T-3).
- **R-B — Глобально `'linear'`.** Отклонён:
- Сохраняет текущую «размытую» картинку, проблема не решается.
- **R-C — Динамическое переключение `nearest`↔`linear` через
отдельный layer.** Отклонён:
- Удваивает количество raster-layers (2 hillshade + 2 TRI), плюс
логика «когда какой layer показывать» по `getZoom()` →
сложность не оправдана.
### Вариант U (UI gate) — минзум hillshade
- **U-A — Понизить UI-порог с 10 до 9** (выбран):
- Тайлы z9 на диске **есть** (нарезка z8-z14 по PH-6 BRD; pre-deploy
smoke в `07-infra-requirements.md` §6.2 шаг 1 это подтверждает).
- Аналогично понижается `source.minzoom` с 10 до 9 (BRD F-02,
REQ-F-02).
- HTML hint обновляется с «Зум 10+» на «Зум 9+» (REQ-F-10).
- **U-B — Понизить дальше до z8.** Отклонён:
- На z8 hillshade-тайлы 256 px покрывают ~150 км по широте — крупные
тени становятся неразборчивым «шумом». TRI работает лучше.
- Если будущий BRD захочет — отдельная задача.
- **U-C — Не менять UI-порог, оставить 10.** Отклонён:
- Тогда на z9 пользователь не видит hillshade вообще — основная
жалоба BRD не решается.
### Вариант T (Theme-specific paint) — отдельные таблицы для dark/satellite
- **T-A — Один paint для всех тем** (выбран в MVP):
- Простой код, одна правда о stops.
- AC-11 (dark) и AC-12 (satellite) — качественные проверки. Если
оператор подтвердит читаемость на dark и satellite — конец истории.
- Соглашение: если AC-11/AC-12 проваливаются — открывается **ADR-018
"theme-specific terrain paint"** как follow-up; в нём вводится
подписка на `theme-change` и переключение paint через
`setPaintProperty` (BRD R-2, R-3).
- **T-B — Сразу theme-specific paint в ET-013.** Отклонён:
- Преждевременная сложность; неизвестно, действительно ли нужны
разные stops (вероятность по риск-таблице: средне-низкая).
- Расширяет scope: понадобится подписка на смену темы, отдельные
константы, новые тесты на каждый theme×layer×zoom.
### Вариант A (API-расширение `applyTerrainLayer`) — как передавать paint
- **A-A — Обратно-совместимое расширение: `opacityOrPaint: number |
object`** (выбран):
- Внутри функции — нормализация: если число → старый paint-объект
с `linear` resampling; если объект → используется как есть.
- Сохраняет старый контракт для возможных будущих вызовов
(сейчас вызовов только два, оба в `onTerrainCheckbox`).
- Unit-тестируется через AC-22, UT-COMPAT-01.
- **A-B — Сменить сигнатуру на `applyTerrainLayer(id, tileUrl,
enabled, paint, minzoom, maxzoom)` без обратной совместимости.**
Отклонён:
- Если в будущем кто-то скопирует функцию для других raster-слоёв
(POI tiles, scenic) с числом — придётся переписывать вызовы.
- Стоимость обратной совместимости — 3 строки кода.
- **A-C — Завести новые функции `applyHillshadeLayer` /
`applyTRILayer`.** Отклонён:
- Дубликация. `applyTerrainLayer` уже обобщённая, она и есть точка
расширения.
### Вариант M (Module split) — выносить ли константы в отдельный файл
- **M-A — `HILLSHADE_PAINT` / `TRI_PAINT` живут в `app.js` рядом с
`TERRAIN_BASE_URL`** (выбран):
- В стэке нет JS-bundler'а, нет ES-import-graph'а (vanilla JS,
скрипты грузятся `<script src=...>`).
- Выделять отдельный модуль `terrain-paint.js` ради двух констант
— преждевременная фрагментация.
- Unit-тестируются Python-парсером по grep (REQ-F-13 Вариант B);
JS-test-раннера в проекте нет.
- **M-B — Отдельный модуль `src/web/terrain-paint.js`.** Отклонён в MVP:
- Требует либо ставить vitest/jest (превышение scope ET-013), либо
подключать через `<script>` с глобальными переменными — не
эстетично.
- Если в будущем потребуется JS-test-инфраструктура (PWA, сложная
логика) — модуль выделяется тогда же.
## Решение
1. **Frontend paint-калибровка (P-A)**. Никаких изменений в backend
`src/api/main.py`, в нарезке растровых тайлов на диске, в `style.json` /
`style-dark.json`, в nginx, в Docker.
2. **UI-минзум hillshade понижается с 10 до 9 (U-A)** в
`updateHillshadeAvailability` (порог `zoom < 9`), HTML hint
`«Зум 9+»`, `source.minzoom = 9` через параметр в `applyTerrainLayer`.
3. **Контракт `applyTerrainLayer` расширяется (A-A)**: четвёртый
параметр принимает либо `number` (старый контракт → `raster-opacity` +
`linear`-resampling), либо `object` paint-properties. Внутри
функции — нормализация.
4. **Hillshade paint (O-B + C-A + R-A)** — константа `HILLSHADE_PAINT`
в `app.js`:
- `raster-opacity`: `interpolate linear zoom [9→0.65, 10→0.60,
11→0.55, 12→0.50, 14→0.40]`.
- `raster-contrast`: `interpolate linear zoom [9→0.40, 10→0.35,
11→0.30, 12→0.15, 14→0.00]`.
- `raster-resampling`: `'nearest'`.
5. **TRI paint (O-B + R-A)** — константа `TRI_PAINT`:
- `raster-opacity`: `interpolate linear zoom [5→0.55, 7→0.65,
8→0.70, 9→0.80, 10→0.85, 11→0.85, 12→0.75, 15→0.70]`.
- `raster-resampling`: `'nearest'`.
6. **Один paint для всех тем (T-A)** — без специальных таблиц для
`theme-dark` и для спутниковой подложки в MVP. Если AC-11/AC-12
проваливаются — открывается ADR-018 как follow-up.
7. **Константы живут в `app.js` (M-A)** рядом с `TERRAIN_BASE_URL`.
## Классификация изменения
**minor-change.**
Меняются 3 файла:
- `src/web/app.js` (расширение `applyTerrainLayer`, добавление двух
констант, обновление двух вызовов, изменение одного порога).
- `src/web/index.html` (текст одного `<span>`).
- `tests/unit/test_terrain_paint.py` + `tests/integration/test_terrain_z9_tiles.py`
(новые).
Не меняются:
- `src/api/main.py`.
- `data/terrain/*` (тайлы на диске).
- `style.json`, `style-dark.json`.
- `config/*.yaml`.
- `Dockerfile`, `docker-compose.yml`, nginx.
Эскалация: **не arch:major-change.** Не требует расширенного approve.
## Последствия
### Положительные
- Перепады на z9-z11 читаются сопоставимо с z8 (BRD §1, BRD M-9,
AC-07..AC-09) без перегенерации тайлов.
- Hillshade становится доступен на z9 (BRD F-01, AC-01, AC-03) —
пользователь видит тени на «обзорном» зуме планирования маршрута.
- Регрессия z8 (BRD F-11, AC-06) и z14 (BRD F-12, AC-10) выполняется
автоматически за счёт явных stops в `interpolate`.
- Backend, тайлы, конфиги не трогаются → 0 риск регрессии
серверной/инфраструктурной части.
- `applyTerrainLayer` остаётся обратно-совместимым → если позже
появится ещё один raster-слой (например, hypso в UI) — функция
переиспользуется.
### Отрицательные / Принимаем
- На z12-z14 `'nearest'`-resampling даёт лёгкую «пикселизацию»
hillshade при overzoom (R-T-3 в `10-tech-risks.md`). Принимаем:
на z12+ пользователь обычно отключает hillshade в пользу подложки,
альтернатива (два layer'а с разным resampling) — overkill.
- Сетевой трафик PNG-тайлов рельефа может вырасти до +35% на
типичной сессии активного зумирования (BRD M-10, NFR-03).
Принимаем: `Cache-Control: immutable` + браузерный кэш + nginx-кэш
поглощают это после первого визита.
- Один paint для всех тем может оказаться неоптимальным для
`theme-dark` или спутника. Принимаем риск; митигация через
follow-up ADR-018 если AC-11/AC-12 проваливаются.
### Технический долг
- **TD-1: Перегенерация hillshade с z-factor 2.5-3.0 для z9-z14.**
Открыт как follow-up «hillshade-rerender-z9-z14» при недостаточности
ET-013. Вероятность по риск-таблице — низкая.
- **TD-2: Theme-specific paint (ADR-018).** Открывается при провале
AC-11 или AC-12.
- **TD-3: Подключение гипсометрии (hypso) в UI.** Тайлы есть, чекбокса
нет. Отдельная задача (не зависит от ET-013).
- **TD-4: Возможное вынесение `HILLSHADE_PAINT` / `TRI_PAINT` в
отдельный модуль `src/web/terrain-paint.js`** — когда в проекте
появится JS-test-инфраструктура.
- **TD-5: Multidirectional hillshade** — отдельный work-item, если
ET-013 окажется недостаточным и пользователи продолжат жаловаться
на «плоскость» рельефа на крупных зумах.
## Альтернативы для будущего
| # | Идея | Когда возвращаться |
|---|------|---------------------|
| F-1 | Перегенерация hillshade с z-factor 2.5 | Если AC-07..AC-09 не выполняются после калибровки stops |
| F-2 | Theme-specific paint (ADR-018) | Если AC-11 или AC-12 проваливаются |
| F-3 | Подключение hypso в UI | По бизнес-запросу |
| F-4 | Переход на raster-dem (Mapbox Terrain RGB) | При смене стратегии рельефа целиком |
| F-5 | Векторные горизонтали (contours) | Отдельная фича PH-6.5 |
| F-6 | Multidirectional hillshade | При жалобах на плоскость на z12+ |
## Связанные документы
- BRD: `docs/work-items/ET-013/01-brd.md` §3 (F-01..F-14), §5 (R-1..R-11), §2.4 (out of scope reasoning)
- TRZ: `docs/work-items/ET-013/02-trz.md` §3 (REQ-F-01..REQ-F-21)
- AC: `docs/work-items/ET-013/03-acceptance-criteria.md` (AC-01..AC-22)
- Инфра: `docs/work-items/ET-013/07-infra-requirements.md`
- Данные: `docs/work-items/ET-013/08-data-requirements.md`
- Риски: `docs/work-items/ET-013/10-tech-risks.md`
- Глобальный ADR-индекс: `docs/architecture/adr/README.md`
- Архитектура рельефа PH-6: `docs/phases/PH-6.terrain/` (наследие)
- Прецедент ADR-016 (ET-012) — формат «калибровочного» ADR

View File

@@ -0,0 +1,249 @@
---
type: infra-requirements
work_item_id: ET-013
title: "Инфраструктурные требования — ET-013: Zoom-aware paint для terrain-слоёв на z9-z11"
version: 1
status: approved
created_at: 2026-06-04
authors:
- "agent:architect"
---
# Инфраструктурные требования — ET-013
## 1. Резюме
ET-013 — **frontend paint-калибровка**. Меняются два файла исходного
кода (`src/web/app.js`, `src/web/index.html`) + добавляются тесты.
Инфраструктура **не меняется**:
- 0 новых docker-сервисов;
- 0 изменений в `Dockerfile`;
- 0 изменений в `docker-compose.yml`;
- 0 новых файлов БД, миграций, индексов;
- 0 новых cron-записей;
- 0 новых env / секретов / API-ключей;
- 0 новых исходящих HTTPS-соединений;
- 0 новых портов;
- 0 изменений в nginx (тайлы рельефа отдаются с тех же путей
`/enduro/terrain/{layer}/{z}/{x}/{y}.png`);
- 0 изменений в backend (`src/api/main.py:terrain_tile` без правок).
Эскалация: **minor change** (см. ADR-017 §«Классификация изменения»).
## 2. Контейнеры и сервисы
| Аспект | Требование |
|----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Новый сервис | **Нет** |
| Изменения `Dockerfile` | **Нет** |
| Изменения `docker-compose.yml` | **Нет** |
| Перезапуск `app` после деплоя | Нужен — `docker compose up -d --no-deps app` (≈ 5 сек простоя). Подхватывает обновлённые `src/web/app.js` и `src/web/index.html` (отдаются как статика из контейнера) |
| Перезапуск `gps-collector` | Не нужен (не затронут) |
| Очистка серверных кэшей | Не требуется (backend не меняется; `/terrain/*` endpoint и `Cache-Control: max-age=31536000, immutable` без изменений) |
| Очистка клиентских кэшей | Не требуется как часть деплоя, но пользователю при первой загрузке после деплоя браузер дёрнет свежий `app.js` (cache-busting через nginx if-modified-since) |
### 2.1 Зависимости между сервисами
Без изменений vs PH-6 / ET-007:
- `app` → файлы `/app/data/terrain/{hillshade,tri,hypso}/{z}/{x}/{y}.png`
(read-only при отдаче клиенту).
- `nginx (host)``app:8000` через docker-network bridge.
## 3. Сеть
| Аспект | Требование |
|--------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Новые входящие порты | **Нет** |
| Изменения nginx | **Нет** (`location /enduro/terrain/` без правок; новые комбинации `(z, x, y)` для z=9 — просто другие значения существующего path-параметра) |
| nginx gzip для PNG | Не применяется (PNG уже сжат). Без изменений vs PH-6 |
| Кэш-заголовки на `/terrain/*` | Без изменений: `Cache-Control: public, max-age=31536000, immutable` (см. `src/api/main.py:1252`). Браузерный кэш + nginx-кэш агрессивно поглощают повторы |
| Новые исходящие соединения | **Нет** — никаких внешних API не дёргается, всё локально |
| CORS | Без изменений; `/terrain/*` отдаётся в том же origin, что и `index.html` |
| HTTPS / TLS | Без изменений — nginx с Let's Encrypt сертификатом DuckDNS |
### 3.1 Ingress / Egress — оценка дельты
Изменения сетевого паттерна (BRD M-10, NFR-03):
- **Hillshade**: UI-минзум понижается с 10 до 9 → пользователь видит
слой на одной zoom-ступени раньше. Один тайл z9 == 4 тайла z10 по
покрытию территории, поэтому при «активной zoom-сессии» z=8→z=12
с включённым hillshade добавляется ≤ 1 zoom-ступень тайлов.
- **TRI**: minzoom источника не меняется (5), opacity меняется только
для уже-запрашиваемых тайлов. Дельта запросов **0**.
- Итого: при типичной сессии «10 зумов между z8 и z12 с обоими слоями»
объём PNG растёт **≤ 35%** (BRD M-10, AC-21).
Размер одного PNG-тайла рельефа (terrain) ≈ 8-30 KB (без gzip — PNG
уже сжат). На сессию: было ~60 тайлов × 20 KB = 1.2 MB, станет
~80 тайлов × 20 KB = 1.6 MB. Дельта на пользователя: ~0.4 MB.
При 10 одновременных пользователях на mva154 — пик ≈ 4 MB/сек
дополнительного uplink, мизер по сравнению с uplink сервера
(≥ 100 Mbps по DuckDNS).
Кэш браузера (`immutable, max-age=31536000`) поглощает 2-й и
последующие визиты целиком.
### 3.2 Rate-limit на `/terrain/*`
**Не вводим в этой итерации.** PNG-тайлы — статика с агрессивным
кэшем; DDoS-стоимость низкая (sendfile из ФС без вычислений). Если в
проде обнаружится скан z=9-z=14 grid'а — добавляется отдельным
DevOps-task'ом, не в ET-013.
## 4. Серверные ресурсы
| Аспект | Требование |
|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CPU `app` | Без изменений по архитектуре. Раздача PNG — `FileResponse` (sendfile, zero-copy через ядро), CPU-cost пренебрежимый. Рост запросов до +35% даёт +0.5% CPU на сервере при пике сессий |
| RAM `app` | Без изменений. PNG не буферизуются в памяти; sendfile из файловой системы |
| Disk `app` | Без изменений. Тайлы рельефа лежат в `/home/slin/enduro-trails/data/terrain/{hillshade,tri,hypso}/{z}/{x}/{y}.png` (объём по PH-6 baseline). Никаких новых файлов / volume |
| CPU `gps-collector` | Без изменений (не затронут) |
| RAM `gps-collector` | Без изменений |
| Disk `gps-collector` | Без изменений |
### 4.1 Размер тайлов рельефа на диске
**Не меняется.** ET-013 не перегенерирует тайлы; используются
существующие нарезки z8-z14 из PH-6. Если pre-deploy smoke
(см. §6.2 шаг 1) обнаружит отсутствие тайлов z9-z11 — задача
останавливается, открывается PH-6 follow-up на догенерацию
(BRD R-11, AC-19).
## 5. Конфигурация и секреты
| Аспект | Требование |
|-------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
| Новые env-переменные | **Нет** |
| Новые секреты | **Нет** |
| Новые API-ключи | **Нет** |
| Изменения `config/*.yaml` | **Нет** |
| Изменения runtime config | **Нет**`HILLSHADE_PAINT` и `TRI_PAINT` — JS-константы, живут в коде и меняются коммитом (BRD §6 q&a, ADR-017 §M) |
| Изменения `style.json` / `style-dark.json` | **Нет** — растровые terrain-слои добавляются динамически из JS, в стилях не описаны |
## 6. Деплой
### 6.1 Среды
- **dev (локально)**: `make dev` (docker compose up `app`). Достаточно
`git pull && make dev` для смены поведения.
- **test (mva154)**: `https://openclaw.mva154.duckdns.org/enduro/`.
CI/CD — Gitea Actions; деплой через `make deploy-test` или ручной
SSH + `docker compose up -d --no-deps --build app` (см. §6.2).
- **prod** — пока не задействован; ET-013 деплоится только в test.
### 6.2 Процедура деплоя в test
Последовательность шагов (REQ-F-20 в TRZ §3):
1. **Pre-deploy smoke**: проверить наличие тайлов z9-z11 на test-среде:
```bash
curl -sI 'https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/9/308/158.png' | head -1
curl -sI 'https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/10/617/317.png' | head -1
curl -sI 'https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/11/1234/635.png' | head -1
```
Ожидается `HTTP/1.1 200 OK` на все три. Если хотя бы один 404 —
merge приостанавливается (AC-19), открывается PH-6 follow-up на
догенерацию тайлов.
2. **Сборка образа**: `docker compose build app` на mva154 (после `git pull`).
3. **Перезапуск `app`**: `docker compose up -d --no-deps app`.
4. **Post-deploy smoke**:
```bash
# Проверка статики app.js обновился
curl -s 'https://openclaw.mva154.duckdns.org/enduro/app.js' | grep -c 'HILLSHADE_PAINT'
# Ожидается ≥ 1
```
5. **Ручная валидация AC-03..AC-12** через DevTools:
- открыть карту, центр над Окой/югом Москвы (`[37.6, 54.5]`);
- `window._map.setZoom(9)` — кнопка «Тени рельефа» активна, hint скрыт;
- включить «Тени рельефа» и «Перепады»;
- скриншоты на z9/z10/z11/z14 → визуальная приёмка AC-07..AC-10;
- переключить тему `theme-dark` → проверить AC-11;
- переключить подложку `#base-btn-satellite` → проверить AC-12.
6. **Запись результатов в `13-test-report.md` и `14-deploy-log.md`**.
### 6.3 Rollback
В случае проблем (например, AC-11 «hillshade сливается с dark-темой»,
без возможности быстрой donastройки stops):
1. **Frontend rollback**: `git revert <commit>` + `docker compose up -d --no-deps --build app`.
2. **Cache invalidation**: не требуется (backend не меняется, browser
cache на статике `app.js` инвалидируется по if-modified-since
автоматически).
RTO: ≤ 5 минут (один `docker compose up -d --no-deps app`).
RPO: 0 — никаких изменений в БД, никаких данных не теряется.
### 6.4 CI/CD-гейты
- `make lint` (ruff + eslint) — должен быть зелёным (AC-18).
- `make test` (pytest unit + integration) — зелёный (AC-15..AC-17).
- `pytest tests/integration/test_terrain_z9_tiles.py` — c
`@pytest.mark.skipif` для CI без данных (AC-16), не блокирует
merge.
## 7. Observability / Логирование
| Аспект | Требование |
|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| Новые лог-сообщения | **Нет** (NFR-06 в TRZ §4) |
| Существующие лог-сообщения | `uvicorn.access` логирует все запросы к `/terrain/*` с длиной ответа — этого достаточно для мониторинга дельты трафика после деплоя |
| Метрики / Prometheus | Не вводим в MVP |
| Health-endpoint | `GET /api/health` (если есть) — без изменений |
### 7.1 Что мониторить после деплоя
В `nginx access.log` на mva154 (вручную, без алёртов) — первая неделя:
- **Запросы к `/terrain/hillshade/9/*/*.png`**: должны появиться
(раньше клиент их не дёргал). Если 404 — `data/terrain/hillshade/9/`
отсутствует, инцидент (BRD R-11).
- **Объём ответов**: ≤ +35% к baseline на терминальную пользовательскую
сессию (BRD M-10, AC-21).
- **Status codes**: только 200/304 (304 от if-modified-since). Никаких
500/502 быть не должно.
## 8. Резервное копирование / Disaster recovery
| Аспект | Требование |
|------------------------------|-----------------------------------------------------------------------------------------------------|
| Backup БД | Без изменений vs ET-008/PH-6 (ET-013 не трогает БД) |
| Backup тайлов рельефа | Без изменений vs PH-6. Регенерируемы из SRTM при необходимости |
| Время восстановления (RTO) | ≤ 5 минут (rollback контейнера, см. §6.3) |
| Точка восстановления (RPO) | 0 — никаких данных не теряется |
## 9. Безопасность
| Аспект | Требование |
|-------------------------------------|-------------------------------------------------------------------------------------------------------------------------|
| Auth / Authorization | Без изменений (NFR-05 в TRZ §4). `/terrain/*` — публичный (как и был) |
| Валидация входных данных | Без изменений; existing валидация `(z, x, y)` в `terrain_tile` уже корректно принимает любые валидные z |
| CSP | Без изменений |
| Rate-limit | Не вводим в MVP (см. §3.2) |
| TLS | Без изменений — nginx с Let's Encrypt сертификатом DuckDNS |
## 10. Совместимость
| Аспект | Требование |
|--------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| API контракт `/terrain/*` | Не меняется (REQ-F-18). Любые клиенты (старые tab'ы со старым `app.js`) продолжают работать; они просто не дёргают z=9 hillshade |
| MapLibre GL JS совместимость | MapLibre 4.7.0 (`index.html:10`) поддерживает `interpolate` для `raster-opacity` и `raster-contrast`. `raster-resampling` не поддерживает `interpolate` — поэтому глобально `'nearest'` (см. ADR-017 §R) |
| Совместимость с PH-6 stack | Никаких изменений; калибровка идёт поверх существующих PH-6 тайлов |
| Совместимость с ET-007 (Спутник) | AC-12 проверяет визуально. В случае проблем — открывается ADR-018 (theme-specific paint) |
| Совместимость с ET-005 (units), ET-006 (GPX), ET-008 (public tracks) | Без изменений; ET-013 трогает только terrain-слои |
| Совместимость с OSRM | Не затронуто (роутинг работает с OSRM-графом независимо) |
| localStorage migration | Не нужно (REQ-F-17). Существующие ключи `terrain-hillshade`, `terrain-tri` — без изменений. Пользователи с включённым hillshade автоматически увидят слой на z9 при следующей загрузке |
## 11. Связанные документы
- `01-brd.md` §3 (F-01..F-14), §6 (Зависимости, инфра), AC §AC-19 (pre-deploy check)
- `02-trz.md` §3 REQ-F-20 Деплой и валидация, §4 NFR
- `06-adr/ADR-017-zoom-aware-terrain-paint.md` §«Классификация изменения», §«Последствия»
- `08-data-requirements.md` (этот пакет)
- `10-tech-risks.md` (этот пакет)
- `docs/work-items/ET-012/07-infra-requirements.md` — образец «zero-infra» work-item (наследие)
- `docs/work-items/ET-011/07-infra-requirements.md` — образец «zero-infra» work-item (наследие)

View File

@@ -0,0 +1,289 @@
---
type: data-requirements
work_item_id: ET-013
title: "Требования к данным — ET-013: Zoom-aware paint для terrain-слоёв на z9-z11"
version: 1
status: approved
created_at: 2026-06-04
authors:
- "agent:architect"
---
# Требования к данным — ET-013
## 1. Резюме
ET-013 — **pure client render change**. Никаких изменений схемы БД,
никаких новых таблиц/индексов/миграций, никаких изменений тайлов на
диске, никаких новых ключей `localStorage`, никаких изменений
конфигов источников.
Меняется **только то, как уже существующие PNG-тайлы рельефа
отрисовываются MapLibre на клиенте**:
- `raster-opacity` становится `interpolate`-выражением по `['zoom']`
(вместо константы).
- Для hillshade добавляется `raster-contrast` (тоже `interpolate`).
- `raster-resampling` для обоих terrain-слоёв переключается с
`'linear'` на `'nearest'`.
**Меняется:**
- набор `raster paint properties` у двух MapLibre-слоёв
(`terrain-hillshade`, `terrain-tri`);
- визуальная читаемость рельефа на z9-z11 (целевое улучшение).
**Не меняется:**
- содержимое и формат PNG-тайлов в `data/terrain/{hillshade,tri,hypso}/`
(PH-6 наследие);
- schema БД `centralfederal.sqlite` и `gps_tracks.sqlite`;
- контракт API `/terrain/{layer}/{z}/{x}/{y}.png` (REQ-F-18);
- содержимое тайлов hypso (в UI не подключён, OOS);
- параметры генератора hillshade на сервере (azimuth, altitude,
z-factor — PH-6, OOS);
- параметры классификации TRI (5-уровневая палитра — PH-6, OOS);
- ключи `localStorage` (`terrain-hillshade`, `terrain-tri` — REQ-F-17);
- содержимое `config/*.yaml`;
- стили `style.json`, `style-dark.json` (растровые terrain-слои в
них не описаны — добавляются динамически из JS).
## 2. Архитектурные границы данных
| Слой данных | Тип | Расположение | Изменения в ET-013 |
|-----------------------------------|----------------|----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------|
| OSM-vector (`trails`) | существующий | `/app/data/centralfederal.sqlite` | **нет** |
| Личные GPX треки (ET-006) | существующий | браузер (memory) | **нет** |
| Публичные GPS треки (ET-008) | существующий | `/app/data/gps_tracks.sqlite` | **нет** |
| OSRM-граф | существующий | `/app/data/enduro.osrm.*` | **нет** |
| Terrain hillshade PNG | существующий | `data/terrain/hillshade/{z}/{x}/{y}.png` (z=8..14) | **read-only**: добавляется новая комбинация `(z=9, x, y)`, которая клиент раньше не запрашивал. Тайлы на диске уже есть (PH-6 нарезка) |
| Terrain TRI PNG | существующий | `data/terrain/tri/{z}/{x}/{y}.png` (z=8..14) | **read-only**: те же тайлы, что и раньше; меняется только paint |
| Terrain hypso PNG | существующий | `data/terrain/hypso/{z}/{x}/{y}.png` | **не используется** в ET-013 (OOS) |
| User UI state | существующий | `localStorage` | **нет** новых ключей, нет миграции |
| MapLibre client tile cache | существующий | браузер (LRU MapLibre, ~100 MB) | **расширяется ключевым пространством**: теперь могут лежать тайлы hillshade с `z = 9` (раньше не запрашивались) |
| Серверный кэш `/terrain/*` | не предусмотрен | n/a (FileResponse + Cache-Control immutable) | **нет** |
## 3. Серверные данные
### 3.1 Структура `data/terrain/`
**Без изменений vs PH-6.** Структура каталога:
```
data/terrain/
├── hillshade/
│ ├── 8/{x}/{y}.png # baseline
│ ├── 9/{x}/{y}.png # используется ET-013 впервые на клиенте
│ ├── 10/{x}/{y}.png # baseline (10+ уже использовался)
│ ├── 11/{x}/{y}.png
│ ├── 12/{x}/{y}.png
│ ├── 13/{x}/{y}.png
│ └── 14/{x}/{y}.png
├── tri/ # та же структура, z=8..14
└── hypso/ # та же структура, в UI не подключён
```
Никаких ALTER/CREATE/INSERT/UPDATE/DELETE на стороне данных. Никакой
догенерации тайлов. Никакого преобразования формата (PNG остаётся
PNG 256×256).
### 3.2 Объёмы данных
| Метрика | Текущее (PH-6) | После ET-013 | Гейт |
|------------------------------------------|---------------------|-------------------------------|------------------------------------------------------|
| Объём PNG hillshade на диске | ~ X MB (PH-6 baseline) | без изменений | n/a |
| Объём PNG TRI на диске | ~ Y MB | без изменений | n/a |
| Запросы hillshade за сессию | N (только z≥10) | ~ 1.25-1.35 × N (добавился z=9) | BRD M-10: ≤ +35% |
| Запросы TRI за сессию | M (z=5..14) | без изменений | n/a |
### 3.3 Pre-deploy validation тайлов z9-z11
**Обязательная проверка перед merge** (BRD R-11, AC-19):
```bash
curl -sI 'https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/9/308/158.png' | head -1
curl -sI 'https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/10/617/317.png' | head -1
curl -sI 'https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/11/1234/635.png' | head -1
```
Ожидается `HTTP/1.1 200 OK` на все три. Если 404 — задача
останавливается, открывается PH-6 follow-up «hillshade-z9-z14
backfill». См. `07-infra-requirements.md` §6.2 шаг 1.
### 3.4 API endpoint `terrain_tile`
**Без изменений** (`src/api/main.py:1240`):
- URL: `GET /terrain/{layer}/{z}/{x}/{y}.png`, `layer ∈ {hillshade, tri, hypso}`.
- Возвращает: PNG из файловой системы (sendfile через `FileResponse`).
- Заголовки: `Cache-Control: public, max-age=31536000, immutable`
без изменений. Браузерный кэш и nginx-кэш агрессивно поглощают
повторы.
- Контракт OpenAPI — без изменений (REQ-F-18, NFR-04).
## 4. Клиентские данные
### 4.1 localStorage
**Без изменений vs PH-6 / ET-007.** Используются ключи:
| Ключ | Назначение | Изменения в ET-013 |
|----------------------------|---------------------------------------------|--------------------|
| `terrain-hillshade` | `'1' | '0'` — чекбокс «Тени рельефа» | **нет** |
| `terrain-tri` | `'1' | '0'` — чекбокс «Перепады» | **нет** |
REQ-F-17 в TRZ §3: «никакой миграции localStorage не нужно».
Существующие сессии при следующей загрузке автоматически получают
новый UI-порог 9 (вместо 10) и новые `HILLSHADE_PAINT` / `TRI_PAINT`
константы. Если у пользователя `terrain-hillshade === '1'` и текущий
zoom ≥ 9 — hillshade покажется автоматически (раньше показался бы
только на z ≥ 10).
### 4.2 MapLibre LRU (browser-side)
Браузерный MapLibre кэширует растровые тайлы в собственном LRU
(~100 MB по умолчанию). После ET-013:
- Ключевое пространство кэша: `(source_id, z, x, y)` — расширяется
для `terrain-hillshade-source` на `z = 9` (раньше source имел
`minzoom: 10` → запросов z=9 не было).
- Объём — управляется MapLibre, ~100 MB. Дельта мизерная (тайл
hillshade ≈ 8-30 KB).
- Никакой синхронизации/инвалидации не нужно (тайлы на сервере
не меняются; `Cache-Control: immutable` гарантирует консистентность).
### 4.3 In-memory paint constants
Новые константы в `src/web/app.js` после `TERRAIN_BASE_URL`:
```js
const HILLSHADE_PAINT = {
'raster-opacity': ['interpolate', ['linear'], ['zoom'],
9, 0.65, 10, 0.60, 11, 0.55, 12, 0.50, 14, 0.40],
'raster-contrast': ['interpolate', ['linear'], ['zoom'],
9, 0.40, 10, 0.35, 11, 0.30, 12, 0.15, 14, 0.00],
'raster-resampling': 'nearest'
};
const TRI_PAINT = {
'raster-opacity': ['interpolate', ['linear'], ['zoom'],
5, 0.55, 7, 0.65, 8, 0.70,
9, 0.80, 10, 0.85, 11, 0.85,
12, 0.75, 15, 0.70],
'raster-resampling': 'nearest'
};
```
- Это **компилируемые MapLibre `interpolate`-выражения**, не «данные»
в архитектурном смысле. Живут в коде, изменяются коммитом
(BRD §6 q&a «Делать ли paint-таблицы переменными окружения /
config'ом? Нет — преждевременная абстракция»).
- Память: < 1 KB суммарно. Производительность: MapLibre кэширует
скомпилированные выражения (NFR-01).
## 5. Контракты API
### 5.1 `GET /terrain/{layer}/{z}/{x}/{y}.png`
| Аспект | До ET-013 | После ET-013 |
|-----------------------|--------------------------------------------------------|-------------------------------------------------------------------------------------|
| Поддерживаемые `layer`| `hillshade`, `tri`, `hypso` | без изменений |
| Path-параметр `z` | принимается любой валидный z, тайлы на диске z=8..14 | без изменений |
| Response 200 | для существующих `(z, x, y)` PNG | без изменений |
| Response 404 | для несуществующих `(z, x, y)` | без изменений |
| Response Content-Type | `image/png` | без изменений |
| Cache-Control | `public, max-age=31536000, immutable` | без изменений |
**Старые клиенты** (старый `app.js` со старым `minzoom = 10` для
hillshade) — продолжают работать. Никакого breaking change в
контракте нет (NFR-04).
### 5.2 Прочие endpoint'ы
ET-013 не трогает: `/api/gps-tracks/*`, `/api/trails/*`, `/api/route/*`,
`/api/health`. Их контракты — без изменений.
## 6. Миграции
**Нет.** Никаких миграций БД, миграций localStorage, миграций
конфигов, миграций тайлов.
При деплое в test:
- `data/terrain/*` — без изменений (read-only для `app`).
- БД `centralfederal.sqlite`, `gps_tracks.sqlite` — без изменений.
- Серверный кэш — отсутствует у `/terrain/*` (статическая раздача
с `Cache-Control: immutable`).
- Клиентский MapLibre LRU — самоочищается при reload браузера;
явной миграции не нужно.
- localStorage — старые ключи интерпретируются как раньше;
включённый ранее hillshade автоматически появится на z9 (REQ-F-17,
AC-14).
## 7. Тестовые данные
### 7.1 Для unit-тестов
`tests/unit/test_terrain_paint.py` (новый, REQ-F-13 / REQ-F-14):
- Python-парсер исходного `src/web/app.js` через `re`.
- Никаких внешних зависимостей.
- Никаких фикстур данных.
- Проверяет наличие `HILLSHADE_PAINT` / `TRI_PAINT`, наличие
ключевых stops (`9, 0.65`, `11, 0.55`, `14, 0.40`, `8, 0.70`,
`10, 0.85`), наличие `'raster-resampling': 'nearest'`, порог
`zoom < 9` в `updateHillshadeAvailability`.
### 7.2 Для integration-тестов
`tests/integration/test_terrain_z9_tiles.py` (новый, REQ-F-15):
- Использует FastAPI `TestClient` для `src/api/main.py:app`.
- Опирается на наличие файла `data/terrain/hillshade/9/<x>/<y>.png`
если каталога нет, тест `skipped` с reason (CI без данных).
- На test-среде mva154 (где данные есть) — выполняется как
smoke-проверка endpoint'а.
- Дополнительно: `test_hillshade_invalid_zoom_404` — sanity на
невалидном zoom.
### 7.3 Для UI-тестов (Playwright)
`04b-ui-test-cases.md` — список тест-кейсов TC-UI-01..TC-UI-10:
- Запускается на test-среде `https://openclaw.mva154.duckdns.org/enduro/`.
- Данные — реальные PNG-тайлы рельефа на mva154 (PH-6 нарезка).
- Скриншот-эталоны для AC-06..AC-12 (визуальная читаемость) — в
`tests/e2e/screenshots/et013/`.
- Скриншоты сравниваются оператором (качественная приёмка), не
пиксельный diff (BRD M-9, R-1..R-3).
## 8. Резервные копии и DR
Без изменений vs PH-6.
- БД `centralfederal.sqlite`, `gps_tracks.sqlite` — бэкап тем же
crontab-скриптом, что и раньше; ET-013 не трогает.
- PNG-тайлы `data/terrain/*` — регенерируются из SRTM при необходимости
(PH-6 pipeline). RPO для тайлов = время регенерации (часы),
но они read-only и не теряются при деплое ET-013.
RPO для ET-013: 0 (никаких данных не пишется/не теряется).
## 9. Privacy / Compliance
| Аспект | Требование |
|-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| PII | **Нет.** PNG-тайлы рельефа — derivative из SRTM 30 м (NASA, public domain). Никаких персональных данных нигде в data-flow ET-013 |
| Licensing | **Без изменений** (PH-6 наследие: SRTM 30 m — public domain; derivative PNG распространяется свободно). ET-013 не меняет источник данных |
| Attribution | MapLibre attribution control отображает атрибуцию активных источников (OSM, Esri). Атрибуция SRTM/NASA не выводится в UI (PH-6 решение); ET-013 это не меняет |
| GDPR / 152-ФЗ | Не применимо (нет PII) |
## 10. Связанные документы
- `01-brd.md` §2.1 (текущая реализация), §3 (F-01..F-14), §6 (Зависимости.Данные)
- `02-trz.md` §3 REQ-F-04..REQ-F-09 (paint constants), REQ-F-13..REQ-F-15 (тесты), REQ-F-17 (localStorage), REQ-F-18 (API), REQ-F-19 (configs/styles)
- `06-adr/ADR-017-zoom-aware-terrain-paint.md` §«Решение», §«Последствия»
- `07-infra-requirements.md` §3 (network), §6 (deploy procedure), §3.1 (ingress estimate)
- `10-tech-risks.md` (этот пакет)
- `docs/work-items/ET-012/08-data-requirements.md` — образец «read-pattern change» документа (наследие)
- `docs/phases/PH-6.terrain/` — наследие нарезки тайлов

View File

@@ -0,0 +1,357 @@
---
type: tech-risks
work_item_id: ET-013
title: "Технические риски — ET-013: Zoom-aware paint для terrain-слоёв на z9-z11"
version: 1
status: approved
created_at: 2026-06-04
authors:
- "agent:architect"
---
# Технические риски — ET-013
Технические риски этапа калибровки клиентского paint для растровых
terrain-слоёв. Бизнес-риски — в BRD §5 (R-1..R-11). Шкала:
вероятность (Н/С/В) × влияние (Н/С/В).
## R-T-1 — Тайлы hillshade z9-z11 отсутствуют на test-среде
- **Описание:** BRD §2.1 утверждает, что PH-6 нарезала hillshade
z8-z14. Если реальная нарезка на mva154 отличается (например,
z10-z14), при включении hillshade на z9 пользователь увидит
404-шахматную доску, а в DevTools — череду failed requests.
- **Вероятность / Влияние:** Н / В.
- **Митигация:**
- **Архитектурное решение (ADR-017 §U-A):** pre-deploy smoke
`curl -I` на 3 разных тайла (z9/z10/z11) над ЦФО — обязателен
перед merge (`07-infra-requirements.md` §6.2 шаг 1, AC-19).
- **Эскалация:** при 404 — задача останавливается, открывается
PH-6 follow-up «hillshade-z9-z14 backfill». ET-013 не мержится.
- **Acceptance гейт:** AC-19 в `03-acceptance-criteria.md`.
## R-T-2 — `raster-contrast` 0.40 даёт «пересвет» / черноту на тёмных тайлах
- **Описание:** На z9-z11 hillshade-тайлы из тёмных лесных зон
(низкая средняя яркость PNG) при `raster-contrast: 0.40` могут
«провалиться в черноту» — пиксели clipping'уются к 0, тени
превращаются в чёрные кляксы, теряя информацию.
- **Вероятность / Влияние:** С / С.
- **Митигация:**
- **Архитектурное решение (ADR-017 §C-A):** stops контраста
подобраны консервативно (0.40 на z9 → быстрый спад к 0 на z14);
значения калибруются по результатам визуальной приёмки.
- **Acceptance гейт:** TC-UI-04-Z10-Q (BRD R-1, AC-07..AC-09)
— оператор смотрит скриншоты на холмистом районе. При
«пересвете» — снижаем contrast в stops до 0.25-0.30 итеративно.
- **Принцип:** stops живут в коде, правка — одна строка, не ADR.
## R-T-3 — `'nearest'`-resampling на overzoom z12-z14 даёт пикселизацию
- **Описание:** При overzoom (когда MapLibre тянет тайл z14 для
z15-z18) `'nearest'`-resampling показывает крупные квадраты вместо
плавных теней. Это особенно заметно на hillshade.
- **Вероятность / Влияние:** С / Н.
- **Митигация:**
- **Архитектурное решение (ADR-017 §R-A):** MapLibre не
поддерживает `interpolate` для `raster-resampling`, поэтому
глобальное `'nearest'` — единственный простой путь. Альтернатива
(два layer'а) отклонена как overkill.
- **Контекст использования:** на z12+ пользователь обычно
отключает hillshade в пользу подложки (для города нужны улицы,
а не тени). Это вторичный сценарий.
- **Acceptance гейт:** AC-10 (TC-UI-06-Z14-Q) — оператор
подтверждает «не темнее и не контрастнее, чем до ET-013» (т.к.
opacity и contrast уже вернулись к baseline). Пикселизация
допустима, если не нарушает читаемость.
- **Fallback:** если визуально неприемлемо — отдельным минорным
патчем вводится второй layer hillshade с `'linear'` для z12+,
переключаемый по `getZoom()`. Это **не часть ET-013**.
## R-T-4 — Сетевой трафик растёт > +35% при активной zoom-сессии
- **Описание:** Снижение UI-минзума hillshade с 10 до 9 добавляет
+1 zoom-уровень. На активной сессии (пользователь крутит зум
z8→z11→z8→z11 много раз) первая загрузка z9 тайлов даёт
заметную дельту трафика. BRD M-10 = ≤ +35%.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
- **Архитектурное решение:** `Cache-Control: public,
max-age=31536000, immutable` (`src/api/main.py:1252`) +
браузерный кэш + nginx-кэш. После первого визита повторные
запросы дают 304 If-Modified-Since (или вовсе не доходят до
сервера — browser hits memory cache).
- **Acceptance гейт:** AC-21 в `03-acceptance-criteria.md` —
network-traffic ≤ 135% от baseline на сценарии zoom-петли
z=8→9→10→11→10→9→8.
- **Мониторинг:** см. `07-infra-requirements.md` §7.1 — первая
неделя оператор смотрит `nginx access.log` на аномалии.
## R-T-5 — На тёмной теме (ET-007 `theme-dark`) hillshade с opacity 0.65 + contrast 0.40 сливается в кашу
- **Описание:** Тёмная подложка + полупрозрачный тёмный hillshade
с усиленным контрастом → визуально неразличимая «грязь». BRD R-2.
- **Вероятность / Влияние:** С / С.
- **Митигация:**
- **Архитектурное решение (ADR-017 §T-A):** в MVP — один paint
для всех тем. Если AC-11 проваливается — открывается ADR-018
«theme-specific terrain paint» с отдельной таблицей stops для
`theme-dark` (через подписку на `theme-change` event и
`setPaintProperty`).
- **Acceptance гейт:** AC-11 (TC-UI-09-Z10-DARK-Q) — оператор
проверяет на dark + holmistom районе. Если провал — фиксируется
в `13-test-report.md` и открывается follow-up.
- **Принцип:** не плодим сложность пока не доказана необходимость.
## R-T-6 — На спутниковой подложке (ET-007) hillshade «глушит» снимок
- **Описание:** Esri World Imagery уже содержит визуальный рельеф
(тени снимков). Поверх него полупрозрачный hillshade с opacity
0.65 → снимок превращается в «серую плёнку», пользователь теряет
цвета поверхности. BRD R-3.
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
- **Архитектурное решение (ADR-017 §T-A):** UX-нота: на спутнике
пользователь обычно отключает hillshade — снимок и так
«показывает» рельеф. Если AC-12 проваливается — open ADR-018
с правилом «на satellite layer'е opacity hillshade = старые
0.40» (через подписку на `applyBaseLayer`).
- **Acceptance гейт:** AC-12 (TC-UI-08-Z10-SAT-Q).
- **Принцип:** не плодим сложность пока не доказана необходимость.
## R-T-7 — TRI с opacity 0.85 на z9-z11 перекрывает грунтовки/тропы
- **Описание:** Слой `trails-*` (грунтовки, тропы) рисуется тонкими
линиями. Если TRI поднять до opacity 0.85, цветные пятна
категориальной палитры могут визуально «убить» линии трасс.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
- **Архитектурное решение:** существующая логика в
`applyTerrainLayer` (`src/web/app.js:3337-3339`) вставляет
terrain-слои **перед** первым `trails-*` или `poi-*` слоем —
z-order корректный. TRI рисуется ПОД линиями трасс, не НАД.
- **Тесты:** AC-07..AC-09 (визуальная приёмка на холмистом
районе с грунтовками).
## R-T-8 — MapLibre 4.7.0 не поддерживает `interpolate` для `raster-contrast`
- **Описание:** Если документация MapLibre врёт или версия 4.7.0
имеет regression на `raster-contrast` с zoom-выражением, paint
не применится, в DevTools будет warning, hillshade покажется с
default contrast = 0.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
- **Архитектурное решение (NFR-04 в TRZ §4):** MapLibre 4.7.0
официально поддерживает `interpolate` для всех raster paint
properties, кроме `raster-resampling`. Проверка — публичная
документация maplibre.org.
- **Smoke-проверка после деплоя:** DevTools
`window._map.getPaintProperty('terrain-hillshade', 'raster-contrast')`
должен вернуть массив `['interpolate', ...]` (AC-04).
- **Fallback:** если фактически не работает — заменить на
`case`-step выражение (грубое stepwise) или просто оставить
числовую константу `0.30` для z9-z11 (одно значение, без
zoom-плавности).
## R-T-9 — Регрессия z8: после правки TRI_PAINT на z8 перепады выглядят иначе
- **Описание:** В новой `TRI_PAINT` для z=8 стоит `0.70` — точно
как было. Но если при правке нечаянно поставить `8, 0.75` (или
пропустить стоп для z8 — тогда `interpolate` между `7→0.65` и
`9→0.80` даст на z8 значение ~0.72), регрессия z8 нарушится.
- **Вероятность / Влияние:** С / С.
- **Митигация:**
- **Архитектурное решение (ADR-017 §O-B):** в `TRI_PAINT` явно
указан стоп `8, 0.70` (не полагаемся на интерполяцию между
соседними стопами).
- **Acceptance гейт:** AC-06 (TC-UI-02-Z8-REGR) — скриншот
сравнивается с до-ET-013 baseline.
- **Unit-тест:** REQ-F-13 проверяет наличие `8, 0.70` в исходнике
`TRI_PAINT` через regex.
## R-T-10 — Регрессия z14: hillshade «не возвращается» к baseline
- **Описание:** Если stops `HILLSHADE_PAINT` не закрываются явным
стопом на z14 (например, `14, 0.40, 14, 0.00`), MapLibre
экстраполирует за пределами последнего стопа, и на z14-z15
hillshade может остаться «перегретым» (opacity 0.55, contrast
0.20).
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
- **Архитектурное решение (ADR-017 §O-B / §C-A):** `interpolate`
у MapLibre clamp'ит значения за пределами крайних stops
(clamping behavior). Явные стопы `14, 0.40` для opacity и
`14, 0.00` для contrast обеспечивают регрессию z14.
- **Acceptance гейт:** AC-10 (TC-UI-06-Z14-Q) — скриншот
сравнивается с до-ET-013 baseline.
- **Unit-тест:** REQ-F-13 проверяет наличие `14, 0.40` и `14, 0`
в исходнике `HILLSHADE_PAINT`.
## R-T-11 — `applyTerrainLayer` ломает обратную совместимость
- **Описание:** При расширении сигнатуры
`opacity → opacityOrPaint: number | object` существующая логика
(если есть где-то ещё в `src/web/`) может сломаться при передаче
числа.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
- **Архитектурное решение (ADR-017 §A-A):** внутри функции —
нормализация `(typeof opacityOrPaint === 'number') ? {…linear…} :
opacityOrPaint`. Старый контракт работает без изменений.
- **Acceptance гейт:** AC-22, UT-COMPAT-01 (REQ-F-14) — статический
grep по `src/web/*.js`: подтверждает, что вызовов
`applyTerrainLayer` только два (оба в `onTerrainCheckbox`), оба
переведены на новые константы.
- **Принцип:** unit-тест на нормализацию + явный комментарий
`// ET-013: backwards-compat shim` в коде.
## R-T-12 — Старый клиент (закэшированный в браузере) не подхватывает новый `app.js`
- **Описание:** Пользователь с открытой вкладкой неделю назад имеет
закэшированный старый `app.js` со старым `applyTerrainLayer` без
paint-нормализации. При reload браузер должен дёрнуть свежий
`app.js`. Service worker — не настроен в MVP (PH-9 не реализована).
- **Вероятность / Влияние:** С / Н.
- **Митигация:**
- **Архитектурное решение:** `src/web/index.html` загружает
`app.js` напрямую (без SW). nginx + `Cache-Control` на `*.js`
— стандартные (не immutable; If-Modified-Since работает).
При reload браузер делает conditional GET → 200 (если файл
изменился) или 304.
- **Backwards compat:** старый клиент с `minzoom=10` для hillshade
продолжает работать; он просто не запрашивает hillshade z=9.
Никаких 4xx-ответов нет (REQ-F-18 — контракт неизменен).
- **Митигация в долгую:** PWA / SW (PH-9) введёт правильную
inval-стратегию.
## R-T-13 — Hint «Зум 10+» забыт в HTML → расхождение с фактическим порогом
- **Описание:** В `src/web/index.html` строка
`<span id="terrain-hillshade-hint">Зум 10+</span>`. Если правка
REQ-F-10 потеряется (например, мердж-конфликт), у пользователя
на z<9 будет hint «Зум 10+», который противоречит фактическому
порогу 9.
- **Вероятность / Влияние:** С / Н.
- **Митигация:**
- **Архитектурное решение (REQ-F-10):** в HTML текст явно
меняется на «Зум 9+». Это атомарная правка, проверяется
grep'ом.
- **Acceptance гейт:** AC-01 — проверяет `«Зум 9+»` в исходнике
`index.html`. AC-03 — проверяет `hint.style.display === 'none'`
на z=9.
- **Unit-тест:** REQ-F-14 (UT-REG-02) — grep по строке `zoom < 9`
в `app.js` и `«Зум 9+»` в `index.html`.
## R-T-14 — `nearest`-resampling на TRI делает «зернистую» картинку, пользователю не нравится
- **Описание:** TRI — категориальная палитра (5 уровней). На
`'nearest'` ясно видны 30-метровые SRTM-клетки, картинка
выглядит «зернистой». BRD R-10 классифицирует это как «желаемое
поведение» (показ «реальных» границ перепадов), но возможен
субъективный негативный отзыв.
- **Вероятность / Влияние:** С / Н.
- **Митигация:**
- **Архитектурное решение (ADR-017 §R-A):** на TRI «зернистость»
— спецификация. Категориальные данные требуют резких границ,
`'linear'` их размывает.
- **Fallback:** если AC-07..AC-09 проваливаются с пометкой
«зернисто» — откатывается F-09 (TRI → `'linear'`), hillshade
остаётся на `'nearest'`. Это одна строка кода в `TRI_PAINT`.
- **Acceptance гейт:** AC-07..AC-09 — оператор подтверждает
качественную приёмку.
## R-T-15 — Performance деградация из-за `interpolate` в paint
- **Описание:** Если MapLibre на каждом zoom-tick пересчитывает
`interpolate`-выражение без кэширования, на слабых устройствах
(mobile, low-end) может появиться jank при зуме.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
- **Архитектурное решение (NFR-01 в TRZ §4):** MapLibre кэширует
скомпилированные `interpolate`-выражения; вычисление при
смене zoom — < 1 мс на frame.
- **Эмпирически:** существующие слои `gps_tracks.js`,
`trails-*` уже используют `interpolate` по zoom без жалоб.
- **Тест:** AC-13 (TC-UI-07-Z9-MOBILE) — Playwright mobile
viewport, проверяет работоспособность; не measure'ит FPS, но
регрессия проявится визуально.
## R-T-16 — Pre-deploy smoke не покрывает все регионы (тайлы z9 могут отсутствовать вне ЦФО)
- **Описание:** Pre-deploy `curl` проверяет 3 тайла над ЦФО. Если
нарезка z9 ограничена только ЦФО, пользователь над Уралом /
Алтаем увидит 404. По BRD §6 это OOS (MVP покрывает только
ЦФО), но риск стоит явно зафиксировать.
- **Вероятность / Влияние:** С / Н.
- **Митигация:**
- **Архитектурное решение:** в MVP test-среда обслуживает ЦФО
(`centralfederal.sqlite`). Тайлы вне ЦФО — out of scope.
- **Принцип:** если пользователь панорамирует за пределы ЦФО,
на z9-z14 он увидит «шахматку» из 404 и для terrain, и для
trails — это известная граница MVP, не баг ET-013.
- **Документация:** зафиксировать в `14-deploy-log.md` как
«known limitation».
## R-T-17 — `eslint` падает на новых `interpolate`-массивах
- **Описание:** Если в проекте настроен `eslint` с правилами
`no-magic-numbers` или жёстким `max-len`, длинные массивы
`['interpolate', ['linear'], ['zoom'], 9, 0.65, …]` могут
завалить линтер.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
- **Архитектурное решение:** существующие JS-файлы
(`gps_tracks.js`) уже используют похожие массивы — значит,
eslint их пропускает.
- **Acceptance гейт:** AC-18 (`make lint` зелёный). При проблеме
— добавить `// eslint-disable-next-line` точечно.
## R-T-18 — Калибровка stops «не угадывает» желаемую читаемость с первого раза
- **Описание:** Значения `9→0.65, 10→0.60, 11→0.55` для hillshade
выбраны архитектором по эстимейту из BRD. На реальных данных
оператор может сказать «на z9 ещё мало, на z10 уже слишком
темно». Это **итеративный процесс**, не «упало».
- **Вероятность / Влияние:** В / Н.
- **Митигация:**
- **Архитектурное решение:** stops живут в JS-константах
`HILLSHADE_PAINT` / `TRI_PAINT`. Правка одной цифры — одна
строка кода + новый коммит. Не требует архитектурного
re-decide (ADR-017 §«Технический долг» TD-1).
- **Процесс:** после первого деплоя — фикс stops по фидбеку
оператора без новой задачи. Учитывать в bandwidth-плане до
закрытия ET-013.
- **Гейт:** AC-07..AC-09 — качественные, оператор-driven.
Они и есть «точка калибровки».
## Сводная таблица
| # | Риск | Вер | Влиян | Митигация (тип) |
|-------|--------------------------------------------------------------------|-----|-------|--------------------------------------------------|
| R-T-1 | Тайлы z9-z11 отсутствуют | Н | В | Pre-deploy smoke + AC-19; STOP на 404 |
| R-T-2 | `raster-contrast` 0.40 — пересвет/чернота | С | С | Итеративная калибровка stops; AC-07..AC-09 |
| R-T-3 | `'nearest'` пикселизация на z12+ | С | Н | Принимается; fallback — двойной layer |
| R-T-4 | Трафик +35% превышает гейт M-10 | Н | Н | `immutable` кэш; AC-21 |
| R-T-5 | Hillshade на тёмной теме — каша | С | С | AC-11; follow-up ADR-018 при провале |
| R-T-6 | Hillshade «глушит» спутник | Н | С | AC-12; follow-up ADR-018 при провале |
| R-T-7 | TRI 0.85 перекрывает trails | Н | Н | Existing z-order (terrain ПОД trails) |
| R-T-8 | MapLibre 4.7.0 не поддерживает interpolate для raster-contrast | Н | Н | Документация подтверждает; fallback — case-step |
| R-T-9 | Регрессия z8 TRI | С | С | Явный стоп `8, 0.70`; AC-06; unit-тест |
| R-T-10| Регрессия z14 hillshade | Н | С | Явные стопы `14, 0.40` и `14, 0`; AC-10 |
| R-T-11| `applyTerrainLayer` обратная совместимость | Н | Н | Нормализация внутри функции; UT-COMPAT-01 |
| R-T-12| Старый клиент в кэше браузера | С | Н | Backwards-compat контракта |
| R-T-13| Hint «Зум 10+» забыт | С | Н | grep-проверка + AC-01 |
| R-T-14| TRI `'nearest'` — зернисто | С | Н | Specified behavior; fallback — откат F-09 |
| R-T-15| `interpolate` deg performance | Н | Н | MapLibre кэширует expr; NFR-01 |
| R-T-16| Pre-deploy smoke ≠ покрытие региона | С | Н | Known MVP limitation; deploy-log |
| R-T-17| eslint падает на длинных массивах | Н | Н | Существующий код уже использует такие массивы |
| R-T-18| Stops не угадывают с первого раза | В | Н | Итеративная калибровка; AC-07..AC-09 — qualitative |
## Связанные документы
- `01-brd.md` §5 Бизнес-риски R-1..R-11 (часть пересекается)
- `02-trz.md` §3 REQ-F-04..REQ-F-15 (paint, тесты), §4 NFR-01..NFR-07
- `06-adr/ADR-017-zoom-aware-terrain-paint.md` §«Решение», §«Последствия», §«Технический долг»
- `07-infra-requirements.md` §3 (network), §6 (deploy procedure), §7 (мониторинг)
- `08-data-requirements.md` §3.3 (pre-deploy validation), §5 (API contracts)
- `03-acceptance-criteria.md` AC-01..AC-22 (все гейты)