[HIGH] ПТИЦА: отчёт утверждает отсутствие природного риска, ни разу его не проверив — 12 мест #2934

Closed
opened 2026-08-19 13:10:38 +00:00 by bot-backend · 3 comments
Collaborator

Замер

cad_risk_zones                       0 строк   (таблица пуста)
nspd_quarter_dumps                 669 дампов
  из них с risks_count > 0           0        (НИ ОДНОГО)
  из них с zouit_count  > 0        581        (для сравнения: ЗОУИТ работает)
слои risk_* в features_json          0 объектов на все 669 дампов

Одиннадцать слоёв природного риска (затопление, подтопление, заболачивание, оползни, абразия, эрозия водная/линейная/ветровая, опустынивание и др. — nspd_client.py) запрашиваются на обоих боевых путях (include_risks=True) и ни разу не вернули ни одного объекта.

Важная оговорка, чтобы не переоценить: layers_fetched.append(...) в nspd_client.py:956 стоит до запроса. Поэтому «risk_flooding числится собранным у 603 дампов» означает «собирались запросить», а не «НСПД ответил». Мы не знаем, отвечал ли источник вообще. Это отдельный дефект наблюдаемости: признак «слой опрошен» лжёт по построению, и отдавать его наружу нельзя, пока append не перенесён после успешного ответа.

Что из этого видит пользователь

Проверено 36 утверждений на пяти поверхностях (API, HTML-отчёт, PDF/DOCX, фронтенд, скоринг). Каждое «вводит в заблуждение» отдельно проверял скептик по коду: 12 подтвердились на 9 уникальных местах, 6 опровергнуто, 18 честны или до пользователя не доходят.

1. Самое опасное: строка экспортируемого документа

full_report_html.py:646 и посимвольный двойник full_report_docx.py:407. В §2 «Окружение» → «Геотехника и гидрология»:

Риск подтопления | нет

Подкреплено hydrology.flood_risk_flag — это OSM-прокси «река или канал ближе 200 м». Ни cad_risk_zones (0 строк), ни 11 слоёв risk_* (0 объектов) в эту строку не входят.

Опаснее прочего по трём причинам: это экспортируемый документ, который уходит наружу и живёт после сессии; он печатается на основном пути сразу в двух форматах; и собственная оговорка payload'а уже написана — hydrology["note"]: «Пойма реки (<200м) — повышенный риск подтопления. Точные данные о зонах затопления — в Росреестре (ЗОУИТ типа 33) через ФГИС ТП» — но молча теряется на границе экспортёра. Честный текст есть, его выбрасывают.

2. Зелёная плашка «Риски не обнаружены»

NspdRiskZonesBlock.tsx:74 — success-стиль (#f0fdf4 / #15803d), бейдж «Риски не обнаружены» и текст «Риск-зоны НСПД на участке не выявлены». Ветка zones.length === 0 при том, что бэкенд всегда шлёт пустой список, а условие показа data.nspd_risk_zones !== undefined всегда истинно. То есть плашка показывается всегда.

Самая громкая формулировка во всём продукте — единственная, где словами сказано «не выявлены». Но вторая по важности, а не первая: LandTab рендерится только на /legacy/site-finder, основной маршрут — /site-finder.

3. Остальные подтверждённые

  • parcels.py risks-блок: flood_zone: false и geology_risk_label: "low" в ответе /analyze. «low» выводится по правилу «нет затопления и шум < 65 дБ», то есть наследует пустоту источника.
  • SiteMap.tsx:950 — тумблер слоя «Зоны риска 0» в одном ряду с измеренными «Конкуренты 14», «Красные линии 2». Ноль там неотличим от измеренного нуля.
  • InvestScoreBlock.tsx:82-88 — в hero-панели строка «Риск: Низкий» зелёным; пометка «предв.» лежит только в атрибуте title, в тексте её нет.
  • ptica-adapt.ts:283DevelopmentScan.tsx:73 — гейдж «Сводный риск», «22% Низкий» зелёным.

Порядок починки

Только формулировка (новых данных не нужно, закрывает 8 из 12):

  1. §2 PDF/DOCX: метку → «Пойма реки в 200 м (OSM)», рядом печатать существующий hydrology["note"]. Правку класть общей константой — DOCX уже импортирует хелперы из HTML, иначе форматы разъедутся.
  2. Убрать risks.geology_risk_label — соседний geology.data_available=false тему уже честно закрывает, фронт поле не читает.
  3. Снять success-стиль и слово «не выявлены» с NspdRiskZonesBlock; «н/д» вместо «0» в тумблере; «предв.» видимым текстом.

Смена источника (не формулировка):
4. flood_zone читать ЗОУИТ subcategory 35 «Зона затопления / подтопления» (quarter_dump_lookup.py:104) — живой канал уже в ответе, zouit_count > 0 у 581 из 669 дампов. Мёртвую cad_risk_zones из запроса убрать.

Признак покрытия (единственная настоящая проводка):
5. Прокинуть risks_count в nspd_dump — он уже вычитан (quarter_dump_lookup.py:274) и лежит в layer_counts. layers_fetched наружу не отдавать, пока append стоит до запроса.

Не UI:
6. cad_risk_zones — писателя нет по построению: во всём репозитории только DDL и один SELECT. Либо загрузчик, либо DROP таблицы.
7. Одиннадцать слоёв × 669 дампов × 0 объектов — это похоже на отказ источника, а не на свойство местности. Нужен замер, различающий «НСПД не отдаёт эти слои» и «наши id слоёв устарели».

Что проверено и чинить НЕ надо

  • full_report_html.py:644 «Многолетняя мерзлота | нет» — курируемая константа GEOTECH_BY_REGION[66], значение верное.
  • full_report_html.py:521 «Есть ЗОУИТ | нет» — источник рабочий (581/669), опасное направление уже закрыто автоподменой на «да (по данным НСПД)».
  • report_maps.py:246 — слой не рисует ни пикселя и не имеет легенды: это непокрытие, а не ложное отрицание.
## Замер ``` cad_risk_zones 0 строк (таблица пуста) nspd_quarter_dumps 669 дампов из них с risks_count > 0 0 (НИ ОДНОГО) из них с zouit_count > 0 581 (для сравнения: ЗОУИТ работает) слои risk_* в features_json 0 объектов на все 669 дампов ``` Одиннадцать слоёв природного риска (затопление, подтопление, заболачивание, оползни, абразия, эрозия водная/линейная/ветровая, опустынивание и др. — `nspd_client.py`) запрашиваются на обоих боевых путях (`include_risks=True`) и ни разу не вернули ни одного объекта. **Важная оговорка, чтобы не переоценить:** `layers_fetched.append(...)` в `nspd_client.py:956` стоит **до** запроса. Поэтому «`risk_flooding` числится собранным у 603 дампов» означает «собирались запросить», а не «НСПД ответил». Мы не знаем, отвечал ли источник вообще. Это отдельный дефект наблюдаемости: признак «слой опрошен» лжёт по построению, и отдавать его наружу нельзя, пока append не перенесён после успешного ответа. ## Что из этого видит пользователь Проверено 36 утверждений на пяти поверхностях (API, HTML-отчёт, PDF/DOCX, фронтенд, скоринг). Каждое «вводит в заблуждение» отдельно проверял скептик по коду: **12 подтвердились на 9 уникальных местах, 6 опровергнуто, 18 честны или до пользователя не доходят.** ### 1. Самое опасное: строка экспортируемого документа `full_report_html.py:646` и посимвольный двойник `full_report_docx.py:407`. В §2 «Окружение» → «Геотехника и гидрология»: > **Риск подтопления | нет** Подкреплено `hydrology.flood_risk_flag` — это OSM-прокси «река или канал ближе 200 м». Ни `cad_risk_zones` (0 строк), ни 11 слоёв `risk_*` (0 объектов) в эту строку не входят. Опаснее прочего по трём причинам: это **экспортируемый документ**, который уходит наружу и живёт после сессии; он печатается на **основном** пути сразу в двух форматах; и собственная оговорка payload'а уже написана — `hydrology["note"]`: «Пойма реки (<200м) — повышенный риск подтопления. Точные данные о зонах затопления — в Росреестре (ЗОУИТ типа 33) через ФГИС ТП» — но **молча теряется на границе экспортёра**. Честный текст есть, его выбрасывают. ### 2. Зелёная плашка «Риски не обнаружены» `NspdRiskZonesBlock.tsx:74` — success-стиль (`#f0fdf4` / `#15803d`), бейдж «Риски не обнаружены» и текст «Риск-зоны НСПД на участке не выявлены». Ветка `zones.length === 0` при том, что бэкенд **всегда** шлёт пустой список, а условие показа `data.nspd_risk_zones !== undefined` всегда истинно. То есть плашка показывается **всегда**. Самая громкая формулировка во всём продукте — единственная, где словами сказано «не выявлены». Но **вторая по важности**, а не первая: `LandTab` рендерится только на `/legacy/site-finder`, основной маршрут — `/site-finder`. ### 3. Остальные подтверждённые - `parcels.py` risks-блок: `flood_zone: false` и `geology_risk_label: "low"` в ответе `/analyze`. «low» выводится по правилу «нет затопления и шум < 65 дБ», то есть наследует пустоту источника. - `SiteMap.tsx:950` — тумблер слоя «Зоны риска **0**» в одном ряду с измеренными «Конкуренты 14», «Красные линии 2». Ноль там неотличим от измеренного нуля. - `InvestScoreBlock.tsx:82-88` — в hero-панели строка «Риск: **Низкий**» зелёным; пометка «предв.» лежит только в атрибуте `title`, в тексте её нет. - `ptica-adapt.ts:283` → `DevelopmentScan.tsx:73` — гейдж «Сводный риск», «22% Низкий» зелёным. ## Порядок починки **Только формулировка** (новых данных не нужно, закрывает 8 из 12): 1. §2 PDF/DOCX: метку → «Пойма реки в 200 м (OSM)», рядом печатать существующий `hydrology["note"]`. Правку класть общей константой — DOCX уже импортирует хелперы из HTML, иначе форматы разъедутся. 2. Убрать `risks.geology_risk_label` — соседний `geology.data_available=false` тему уже честно закрывает, фронт поле не читает. 3. Снять success-стиль и слово «не выявлены» с `NspdRiskZonesBlock`; «н/д» вместо «0» в тумблере; «предв.» видимым текстом. **Смена источника** (не формулировка): 4. `flood_zone` читать ЗОУИТ subcategory 35 «Зона затопления / подтопления» (`quarter_dump_lookup.py:104`) — живой канал уже в ответе, `zouit_count > 0` у 581 из 669 дампов. Мёртвую `cad_risk_zones` из запроса убрать. **Признак покрытия** (единственная настоящая проводка): 5. Прокинуть `risks_count` в `nspd_dump` — он уже вычитан (`quarter_dump_lookup.py:274`) и лежит в `layer_counts`. `layers_fetched` наружу **не отдавать**, пока append стоит до запроса. **Не UI:** 6. `cad_risk_zones` — писателя нет по построению: во всём репозитории только DDL и один SELECT. Либо загрузчик, либо DROP таблицы. 7. Одиннадцать слоёв × 669 дампов × 0 объектов — это похоже на отказ источника, а не на свойство местности. Нужен замер, различающий «НСПД не отдаёт эти слои» и «наши id слоёв устарели». ## Что проверено и чинить НЕ надо - `full_report_html.py:644` «Многолетняя мерзлота | нет» — курируемая константа `GEOTECH_BY_REGION[66]`, значение верное. - `full_report_html.py:521` «Есть ЗОУИТ | нет» — источник рабочий (581/669), опасное направление уже закрыто автоподменой на «да (по данным НСПД)». - `report_maps.py:246` — слой не рисует ни пикселя и не имеет легенды: это непокрытие, а не ложное отрицание.
Author
Collaborator

Пункт 1 закрыт и проверен на проде (PR #2935)

В работающем контейнере:

full_report_docx.py:44    FLOOD_PROXIMITY_LABEL         <- импорт общей константы
full_report_docx.py:408   (FLOOD_PROXIMITY_LABEL, ...)  <- используется, своей копии нет
оговорка hydro_note        3 вхождения в каждом из двух файлов
«Риск подтопления»         только в строках 405 и 410 HTML — это КОММЕНТАРИЙ,
                           объясняющий правку; в таблицу больше не попадает

Проверял не факт мержа, а что в отдаваемом документе другой текст.

Остаётся 9 записей из 12

Порядок из тела задачи в силе. Ближайшее по цене/эффекту — снять success-стиль с NspdRiskZonesBlock и «н/д» вместо «0» в тумблере карты: формулировка, новых данных не нужно.

Пункт 4 (перевести flood_zone на ЗОУИТ subcategory 35) требует уточнения ожиданий. Замерил: код 35 в дампах есть, но 8 объектов на 8 кварталов из 669 — против 637 водоохранных зон кодов 6/7, которые флудом не являются. То есть замена источника переводит поле из «спрашиваем пустую таблицу» в «спрашиваем настоящий, но очень редкий реестр». Это улучшение, но flood_zone: false по-прежнему будет означать «зарегистрированной зоны в дампе квартала нет», а не «подтопления не будет» — формулировку придётся менять всё равно.

### Пункт 1 закрыт и проверен на проде (PR #2935) В работающем контейнере: ``` full_report_docx.py:44 FLOOD_PROXIMITY_LABEL <- импорт общей константы full_report_docx.py:408 (FLOOD_PROXIMITY_LABEL, ...) <- используется, своей копии нет оговорка hydro_note 3 вхождения в каждом из двух файлов «Риск подтопления» только в строках 405 и 410 HTML — это КОММЕНТАРИЙ, объясняющий правку; в таблицу больше не попадает ``` Проверял не факт мержа, а что в отдаваемом документе другой текст. ### Остаётся 9 записей из 12 Порядок из тела задачи в силе. Ближайшее по цене/эффекту — снять success-стиль с `NspdRiskZonesBlock` и «н/д» вместо «0» в тумблере карты: формулировка, новых данных не нужно. Пункт 4 (перевести `flood_zone` на ЗОУИТ subcategory 35) требует уточнения ожиданий. Замерил: код 35 в дампах есть, но **8 объектов на 8 кварталов** из 669 — против 637 водоохранных зон кодов 6/7, которые флудом не являются. То есть замена источника переводит поле из «спрашиваем пустую таблицу» в «спрашиваем настоящий, но очень редкий реестр». Это улучшение, но `flood_zone: false` по-прежнему будет означать «зарегистрированной зоны в дампе квартала нет», а не «подтопления не будет» — формулировку придётся менять всё равно.
Author
Collaborator

Замер: слои запрашиваются, но не отдали ни одного объекта ни разу

Проверил по nspd_quarter_dumps (669 дампов, с 16.05).

Слои реально запрашиваются. В layers_fetched каждый из одиннадцати риск-слоёв записан у 603 дампов:

risk_flooding, risk_flooding_underground, risk_swampification, risk_landslide,
risk_abrasion, risk_erosion_water, risk_erosion_linear, risk_erosion_wind,
risk_desertification, risk_clutter, risk_burns

Идентификаторы настоящие (872205, 872202, 872203, …), и layers_fetched.append в _fetch_layer происходит ПОСЛЕ проверки LAYERS.get(layer_key) is None — то есть ключи разрешились. Ошибки _fetch_layer не глушит, они обрушили бы весь harvest; раз все одиннадцать записаны, все одиннадцать отработали без ошибки.

И вернули ноль. По всем 669 дампам:

участков   381
ЗОУИТ     4935
красных линий 4
инженерных 1337
возможностей 15
рисков       0     ← ни одного, никогда

Объяснение «источник лежал» не подходит. НСПД перестал отдавать данные только с 27.07 (см. #2956). А в месяцы, когда он работал, риски всё равно были нулевыми:

месяц ЗОУИТ рисков
2026-05 940 0
2026-06 2164 0
2026-07 1831 0

То есть в июле, когда с того же адреса приходило 1831 ЗОУИТ-объектов, одиннадцать риск-слоёв дали ровно ноль.

Чего я НЕ смог проверить

Остались две гипотезы, и различить их прямо сейчас нельзя:

  1. У НСПД нет покрытия риск-слоями по Свердловской области;
  2. наш запрос к ним формируется так, что отдаёт пусто вместо ошибки (параметры, проекция, тип обхода).

Прямая проба с прода упирается в WAF 403 на IP VPS (#2956) — блокируется всё, включая контрольный слой. Пока адрес не разблокируют, инструмент недоступен, и любой ответ будет догадкой.

Отдельно — про пункт эпика #2464 к этой же функции

В эпике значится quarter_dump_lookup.py:767: _get_risk_zones якобы воспроизводит «PostGIS 3.4 geography × geography ST_Intersection transform error», который соседний _get_red_lines документирует и обходит.

Проверил на боевом PostGIS 3.4.3 — не воспроизводится:

ST_Area(ST_Intersection(polygon::geography, polygon::geography))   → 679552.38
ST_Length(ST_Intersection(linestring::geography, polygon::geography)) → 2538.97

Ловушка в комментарии соседа названа точнее, чем в пункте эпика: она про LINESTRING, а риск-слои полигональные. Плюс за 3 месяца в GlitchTip ноль предупреждений nspd risk zones query failed.

Но этот ноль ничего не доказывает по другой причине: функция ни разу не получала данных на вход. Форма запроса действительно расходится с соседями по файлу (те делают пересечение в planar и кастуют результат), и привести её к общему виду стоит — но как единообразие, а не как починку наблюдаемого дефекта. Отдельного PR ради этого не открываю: пока риск-слои пусты, любая правка здесь непроверяема.

## Замер: слои запрашиваются, но не отдали ни одного объекта ни разу Проверил по `nspd_quarter_dumps` (669 дампов, с 16.05). **Слои реально запрашиваются.** В `layers_fetched` каждый из одиннадцати риск-слоёв записан у 603 дампов: ``` risk_flooding, risk_flooding_underground, risk_swampification, risk_landslide, risk_abrasion, risk_erosion_water, risk_erosion_linear, risk_erosion_wind, risk_desertification, risk_clutter, risk_burns ``` Идентификаторы настоящие (`872205`, `872202`, `872203`, …), и `layers_fetched.append` в `_fetch_layer` происходит ПОСЛЕ проверки `LAYERS.get(layer_key) is None` — то есть ключи разрешились. Ошибки `_fetch_layer` не глушит, они обрушили бы весь harvest; раз все одиннадцать записаны, все одиннадцать отработали **без ошибки**. **И вернули ноль.** По всем 669 дампам: ``` участков 381 ЗОУИТ 4935 красных линий 4 инженерных 1337 возможностей 15 рисков 0 ← ни одного, никогда ``` **Объяснение «источник лежал» не подходит.** НСПД перестал отдавать данные только с 27.07 (см. #2956). А в месяцы, когда он работал, риски всё равно были нулевыми: | месяц | ЗОУИТ | рисков | |---|---|---| | 2026-05 | 940 | 0 | | 2026-06 | 2164 | 0 | | 2026-07 | 1831 | 0 | То есть в июле, когда с того же адреса приходило 1831 ЗОУИТ-объектов, одиннадцать риск-слоёв дали ровно ноль. ## Чего я НЕ смог проверить Остались две гипотезы, и различить их прямо сейчас нельзя: 1. У НСПД нет покрытия риск-слоями по Свердловской области; 2. наш запрос к ним формируется так, что отдаёт пусто вместо ошибки (параметры, проекция, тип обхода). Прямая проба с прода упирается в WAF 403 на IP VPS (#2956) — блокируется всё, включая контрольный слой. Пока адрес не разблокируют, инструмент недоступен, и любой ответ будет догадкой. ## Отдельно — про пункт эпика #2464 к этой же функции В эпике значится `quarter_dump_lookup.py:767`: `_get_risk_zones` якобы воспроизводит «PostGIS 3.4 geography × geography ST_Intersection transform error», который соседний `_get_red_lines` документирует и обходит. Проверил на боевом PostGIS 3.4.3 — **не воспроизводится**: ``` ST_Area(ST_Intersection(polygon::geography, polygon::geography)) → 679552.38 ST_Length(ST_Intersection(linestring::geography, polygon::geography)) → 2538.97 ``` Ловушка в комментарии соседа названа точнее, чем в пункте эпика: она про LINESTRING, а риск-слои полигональные. Плюс за 3 месяца в GlitchTip ноль предупреждений `nspd risk zones query failed`. Но этот ноль ничего не доказывает **по другой причине**: функция ни разу не получала данных на вход. Форма запроса действительно расходится с соседями по файлу (те делают пересечение в planar и кастуют результат), и привести её к общему виду стоит — но как единообразие, а не как починку наблюдаемого дефекта. Отдельного PR ради этого не открываю: пока риск-слои пусты, любая правка здесь непроверяема.
Owner

Ревизия открытых задач 2026-08-30. Проверено в коде на forgejo/main — сделано, закрываю.

risks.geology_risk_label убран, ярлык больше не выводится из шума. tests/test_2934_no_fake_geology_label.py + правки в parcels.py и фронтовом NspdRiskZonesBlock.

Ревизия открытых задач 2026-08-30. Проверено в коде на forgejo/main — сделано, закрываю. `risks.geology_risk_label` убран, ярлык больше не выводится из шума. tests/test_2934_no_fake_geology_label.py + правки в parcels.py и фронтовом NspdRiskZonesBlock.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2934
No description provided.