---
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 (все гейты)
