feat(nspd): TIER 4 opportunity + red lines (#94 PR2 of 4) #220

Merged
lekss361 merged 2 commits from feat/94-pr2-opportunity-redlines into main 2026-05-16 17:09:53 +00:00
Owner

Summary

Sub-PR 2 of 4 для #94 (NSPD extended layers).

  • TIER 4 opportunity layers (5 types): auction (37299), scheme (37294), free (37298), future (36473), oopt (875845)
  • Red lines (layer 879243) — extract в /analyze через existing core harvest path (dump.red_lines); НЕТ duplicate harvest / NEТ separate column (fix после code-reviewer feedback)
  • Denorm columns в nspd_quarter_dumps: has_auction_parcels, opportunity_count + partial index для быстрого filter "найти кварталы с аукционом"
  • 2 UI блока: NspdOpportunityBlock (color-coded by type) + NspdRedLinesBlock (ALERT при intersect)

Roadmap (#94)

  • PR1 (#217): TIER 3 risk zones — merged
  • PR2 (этот): TIER 4 opportunity + red lines
  • PR3: denorm nspd_parcels / nspd_buildings (~380 LOC)
  • PR4: TIER 5 ARN квартирография heatmap (~450 LOC)

Files (12, +966/-9)

  • data/sql/89_nspd_quarter_dumps_opportunity_flag.sql (NEW, +43) — 2 cols + 1 partial index
  • backend/app/services/scrapers/nspd_client.py (+33) — QUARTER_OPPORTUNITY_LAYERS + opportunity field
  • backend/app/workers/tasks/nspd_sync.py (+39) — _build_* helpers + extended UPSERT
  • backend/app/services/site_finder/quarter_dump_lookup.py (+265) — _get_opportunity_parcels + _get_red_lines
  • backend/app/schemas/parcel.py (+39) — OpportunityParcel + RedLine
  • backend/app/api/v1/parcels.py (+11) — analyze fields
  • backend/tests/services/test_quarter_dump_lookup.py (+200) — 17 new tests (28 total)
  • Frontend: NspdOpportunityBlock.tsx (+162), NspdRedLinesBlock.tsx (+133), LandTab.tsx (+30), types/nspd.ts (+17), types/site-finder.ts (+3)

Test plan

  • Apply 89_*.sql миграция (через deploy.yml)
  • curl /analyze на cad_num в квартале с auction-парцелом → response.nspd_opportunity_parcels not empty
  • UI: LandTab → NspdOpportunityBlock badges (auction=orange, scheme=blue, etc.)
  • UI: NspdRedLinesBlock ALERT если intersect
  • cd backend && uv run pytest tests/services/test_quarter_dump_lookup.py -v → 28/28 pass

Pre-push checks

  • ruff + ruff-format: pass
  • prettier: pass
  • code-reviewer subagent: APPROVE (after red_lines duplication fix — column red_lines_count_v2 + QUARTER_RED_LINES_LAYERS group + include_red_lines param + _build_red_lines_count_v2 ВСЁ удалено; layer 879243 → существующий dump.red_lines core path)
  • phantom column gate (PR #216): new denorm cols has_auction_parcels, opportunity_count существуют ТОЛЬКО после миграции 89_*.sql apply. Migration runs ПЕРЕД containers up → safe в проде.

Known follow-ups

  • Phase 2 (issue TBD): UI для просмотра opportunity quarters на карте через has_auction_parcels partial index
  • Phantom gate (PR #216) НЕ покрывает new cols — добавить в test_phantom_columns.py follow-up issue
  • Severity mapping NspdRiskZonesBlock (4/11 layers) — расширить если нужно

Part of #94

## Summary **Sub-PR 2 of 4** для #94 (NSPD extended layers). - **TIER 4 opportunity layers** (5 types): auction (37299), scheme (37294), free (37298), future (36473), oopt (875845) - **Red lines** (layer 879243) — extract в `/analyze` через existing core harvest path (`dump.red_lines`); НЕТ duplicate harvest / NEТ separate column (fix после code-reviewer feedback) - Denorm columns в `nspd_quarter_dumps`: `has_auction_parcels`, `opportunity_count` + partial index для быстрого filter "найти кварталы с аукционом" - 2 UI блока: `NspdOpportunityBlock` (color-coded by type) + `NspdRedLinesBlock` (ALERT при intersect) ## Roadmap (#94) - ✅ **PR1** (#217): TIER 3 risk zones — merged - ⏳ **PR2** (этот): TIER 4 opportunity + red lines - ⏳ **PR3**: denorm `nspd_parcels` / `nspd_buildings` (~380 LOC) - ⏳ **PR4**: TIER 5 ARN квартирография heatmap (~450 LOC) ## Files (12, +966/-9) - `data/sql/89_nspd_quarter_dumps_opportunity_flag.sql` (NEW, +43) — 2 cols + 1 partial index - `backend/app/services/scrapers/nspd_client.py` (+33) — QUARTER_OPPORTUNITY_LAYERS + opportunity field - `backend/app/workers/tasks/nspd_sync.py` (+39) — `_build_*` helpers + extended UPSERT - `backend/app/services/site_finder/quarter_dump_lookup.py` (+265) — `_get_opportunity_parcels` + `_get_red_lines` - `backend/app/schemas/parcel.py` (+39) — OpportunityParcel + RedLine - `backend/app/api/v1/parcels.py` (+11) — analyze fields - `backend/tests/services/test_quarter_dump_lookup.py` (+200) — 17 new tests (28 total) - Frontend: `NspdOpportunityBlock.tsx` (+162), `NspdRedLinesBlock.tsx` (+133), `LandTab.tsx` (+30), `types/nspd.ts` (+17), `types/site-finder.ts` (+3) ## Test plan - [ ] Apply 89_*.sql миграция (через deploy.yml) - [ ] curl `/analyze` на cad_num в квартале с auction-парцелом → response.nspd_opportunity_parcels not empty - [ ] UI: LandTab → NspdOpportunityBlock badges (auction=orange, scheme=blue, etc.) - [ ] UI: NspdRedLinesBlock ALERT если intersect - [ ] `cd backend && uv run pytest tests/services/test_quarter_dump_lookup.py -v` → 28/28 pass ## Pre-push checks - ruff + ruff-format: pass - prettier: pass - code-reviewer subagent: APPROVE (after red_lines duplication fix — column `red_lines_count_v2` + `QUARTER_RED_LINES_LAYERS` group + `include_red_lines` param + `_build_red_lines_count_v2` ВСЁ удалено; layer 879243 → существующий `dump.red_lines` core path) - phantom column gate (PR #216): new denorm cols `has_auction_parcels`, `opportunity_count` существуют ТОЛЬКО после миграции 89_*.sql apply. Migration runs ПЕРЕД containers up → safe в проде. ## Known follow-ups - Phase 2 (issue TBD): UI для просмотра opportunity quarters на карте через `has_auction_parcels` partial index - Phantom gate (PR #216) НЕ покрывает new cols — добавить в `test_phantom_columns.py` follow-up issue - Severity mapping `NspdRiskZonesBlock` (4/11 layers) — расширить если нужно Part of #94
lekss361 added 1 commit 2026-05-16 16:07:20 +00:00
- NSPDClient: QUARTER_OPPORTUNITY_LAYERS (auction/scheme/free/future/oopt)
  + QuarterDump.opportunity field
- nspd_sync: harvest_quarter accepts include_opportunity, denorm cols
  has_auction_parcels + opportunity_count in UPSERT
- quarter_dump_lookup:
  - _get_opportunity_parcels (sort by distance, early-exit on count=0)
  - _get_red_lines (query existing dump.red_lines core path, layer='red_lines')
- SQL 89_*: has_auction_parcels + opportunity_count + partial index
- Pydantic: OpportunityParcel + RedLine schemas
- /analyze: nspd_opportunity_parcels + nspd_red_lines fields
- Frontend: NspdOpportunityBlock + NspdRedLinesBlock + LandTab integration
- Tests: 28 total (11 PR1 + 17 PR2), all pass

Red lines uses existing core harvest path (layer 879243 already in dump.red_lines
from PR1+core) — no duplicate harvest, no red_lines_count_v2 column needed.

Part of #94
Author
Owner

🟠 HIGH — изменения нужны перед merge

Deep review PR #220 (sha ed3c128, +966/−9, 12 files). Сборка корректная, контракт API/типы/тесты согласованы, но есть два содержательных issue + один cosmetic. Auto-merge запрещён (см. блок в конце).

🔴 BLOCK 1 — Filename collision на data/sql/89_* (sql.md violation)

В репозитории уже есть data/sql/89_drop_dead_brin_rosreestr_deals.sql (applied в prod _schema_migrations). Новый файл назван тоже 89_*89_nspd_quarter_dumps_opportunity_flag.sql. Последняя применённая миграция в prod — 97_objective_backfill_indexes.sql.

Что сейчас произойдёт на deploy (per .forgejo/workflows/deploy.yml line 208 — ls -1 data/sql/*.sql | sort):

  1. 89_drop_dead...sql уже applied → skip
  2. 89_nspd_quarter_dumps_opportunity_flag.sql → НЕ в _schema_migrations → apply. Применится после уже-применённого 97_ — нарушает «NN order».

Технически работает (DDL аддитивный), но нарушает convention из .claude/rules/sql.md: «NN_topic.sql где NN — следующий sequential номер. Проверка: ls data/sql/ | sort | tail -5».

Fix: git mv data/sql/89_nspd_quarter_dumps_opportunity_flag.sql data/sql/98_nspd_quarter_dumps_opportunity_flag.sql + update комментариев header.

🟠 HIGH 2 — _get_red_lines: ST_Area(ST_Intersection(LINESTRING::geography, POLYGON::geography)) неверная семантика + runtime error

Layer 879243 (red_lines) — это LINESTRING/MULTILINESTRING. В SQL _get_red_lines рассчитывается ST_Area(ST_Intersection(geom::geography, parcel::geography)). Проблемы:

  1. Семантика: ST_Intersection(line, polygon) → line/point. ST_Area(line) = 0. Поле intersection_area_sqm для пересекающейся красной линии будет постоянно ~0 (или NaN) — мисс metric. Правильная метрика — ST_Length(...) в метрах (длина пересечения).
  2. Runtime error: ST_Intersection(::geography, ::geography) для line × polygon у PostGIS 3.4 регулярно даёт transform: tolerance condition error (-20) — я воспроизвёл локально на live DB (см. ниже). Try/except в Python проглотит исключение и вернёт [] для всего квартала, даже если интерсект есть. Tests мокают rows и runtime error не ловят.

Доказательство:

SELECT ST_Area(ST_Intersection(
  ST_GeomFromText('LINESTRING(0 0, 100 100)', 4326)::geography,
  ST_GeomFromText('POLYGON((10 10, 90 10, 90 90, 10 90, 10 10))', 4326)::geography
));
-- ERROR: transform: tolerance condition error (-20)

Fix: заменить intersection_area_sqmintersection_length_m, использовать ST_Length(ST_Intersection(geom_4326, parcel_4326)::geography) (cast результата, не операндов). Обновить:

  • backend/app/schemas/parcel.py :: RedLine (поле + docstring)
  • backend/app/services/site_finder/quarter_dump_lookup.py :: _get_red_lines (SQL + key)
  • frontend/src/types/nspd.ts :: RedLine
  • frontend/src/components/site-finder/NspdRedLinesBlock.tsx :: formatArea → formatLength
  • backend/tests/services/test_quarter_dump_lookup.py — тесты переписать на новое поле + добавить тест на DB exception (как в opportunity).

🟡 MEDIUM — _OPPORTUNITY_TYPE_LABELS бесполезен

quarter_dump_lookup.py:571-577 — словарь маппит ключи в самих себя:

_OPPORTUNITY_TYPE_LABELS = {
    "auction_parcels": "auction_parcels",
    "scheme_parcels": "scheme_parcels",
    ...
}

Метод .get(short_name, short_name) всегда возвращает short_name. Либо удалить dict + вернуть raw_layer.removeprefix("opportunity_") напрямую, либо положить туда настоящие human-labels (если планировались — UI labels уже в frontend LAYER_CONFIG).

Что хорошо

  • Migration idempotent (ADD COLUMN IF NOT EXISTS + BEGIN/COMMIT + partial index IF NOT EXISTS) — правильно.
  • Partial index WHERE has_auction_parcels = TRUE — корректный паттерн, не путает с UI-фильтром.
  • SQL во всех новых helpers использует CAST(:wkt AS ...) стиль (не :wkt::type) — соблюдён psycopg v3 trap.
  • Параметризация bind-vars везде (no f-string SQL) — соблюдена.
  • harvest_quarter и harvest_stale_quarters пробрасывают include_opportunity=True consistently — нет regression в TIER 3 fanout.
  • _upsert_dump partial-fail branch (dump=None) корректно ставит has_auction_parcels=False, opportunity_count=0.
  • 5 layer IDs (37299/37294/37298/36473/875845) для opportunity и 879243 для red_lines соответствуют issue spec, LAYERS dict в nspd_client.py:71-105 содержит все ID.
  • API контракт backward-compatible (Optional[list], default []).
  • Frontend buildNspdViewerUrl использует encodeURIComponent — XSS-safe. Текстовый рендер cad_num внутри <a> тоже safe (React escaping).
  • 17 новых тестов покрывают auction/scheme/free/oopt extraction, distance sort, intersect/nearby split, empty, early-exit, db-exception (для opportunity).

Резюме

Verdict: 🟠 HIGH (changes requested). Fix необходим перед merge:

  1. Rename migration 89_98_.
  2. Red lines metric: intersection_area_sqmintersection_length_m + ST_Length (схема + SQL + frontend + tests).
  3. (nice-to-have) drop _OPPORTUNITY_TYPE_LABELS или наполнить реальными labels.

Blocked for auto-merge per .claude/rules/git-pr.md scope policy (touches data/sql/**, backend/app/api/v1/**, backend/app/services/**, backend/app/services/scrapers/**). Human merge required.

<!-- gendesign-review-bot: sha=ed3c128 verdict=changes --> ## 🟠 HIGH — изменения нужны перед merge Deep review PR #220 (sha `ed3c128`, +966/−9, 12 files). Сборка корректная, контракт API/типы/тесты согласованы, но есть два содержательных issue + один cosmetic. Auto-merge запрещён (см. блок в конце). ### 🔴 BLOCK 1 — Filename collision на `data/sql/89_*` (sql.md violation) В репозитории уже есть `data/sql/89_drop_dead_brin_rosreestr_deals.sql` (applied в prod `_schema_migrations`). Новый файл назван **тоже `89_*`** — `89_nspd_quarter_dumps_opportunity_flag.sql`. Последняя применённая миграция в prod — `97_objective_backfill_indexes.sql`. Что сейчас произойдёт на deploy (per `.forgejo/workflows/deploy.yml` line 208 — `ls -1 data/sql/*.sql | sort`): 1. `89_drop_dead...sql` уже applied → skip 2. `89_nspd_quarter_dumps_opportunity_flag.sql` → НЕ в `_schema_migrations` → apply. **Применится после уже-применённого 97_** — нарушает «NN order». Технически работает (DDL аддитивный), но нарушает convention из `.claude/rules/sql.md`: «`NN_topic.sql` где NN — следующий sequential номер. Проверка: `ls data/sql/ | sort | tail -5`». **Fix:** `git mv data/sql/89_nspd_quarter_dumps_opportunity_flag.sql data/sql/98_nspd_quarter_dumps_opportunity_flag.sql` + update комментариев header. ### 🟠 HIGH 2 — `_get_red_lines`: `ST_Area(ST_Intersection(LINESTRING::geography, POLYGON::geography))` неверная семантика + runtime error Layer 879243 (`red_lines`) — это `LINESTRING/MULTILINESTRING`. В SQL `_get_red_lines` рассчитывается `ST_Area(ST_Intersection(geom::geography, parcel::geography))`. Проблемы: 1. **Семантика:** `ST_Intersection(line, polygon)` → line/point. `ST_Area(line)` = 0. Поле `intersection_area_sqm` для пересекающейся красной линии будет постоянно ~0 (или NaN) — мисс metric. Правильная метрика — **`ST_Length(...)` в метрах** (длина пересечения). 2. **Runtime error:** `ST_Intersection(::geography, ::geography)` для line × polygon у PostGIS 3.4 регулярно даёт `transform: tolerance condition error (-20)` — я воспроизвёл локально на live DB (см. ниже). Try/except в Python проглотит исключение и вернёт `[]` для всего квартала, даже если интерсект есть. Tests мокают rows и runtime error не ловят. Доказательство: ```sql SELECT ST_Area(ST_Intersection( ST_GeomFromText('LINESTRING(0 0, 100 100)', 4326)::geography, ST_GeomFromText('POLYGON((10 10, 90 10, 90 90, 10 90, 10 10))', 4326)::geography )); -- ERROR: transform: tolerance condition error (-20) ``` **Fix:** заменить `intersection_area_sqm` → `intersection_length_m`, использовать `ST_Length(ST_Intersection(geom_4326, parcel_4326)::geography)` (cast результата, не операндов). Обновить: - `backend/app/schemas/parcel.py :: RedLine` (поле + docstring) - `backend/app/services/site_finder/quarter_dump_lookup.py :: _get_red_lines` (SQL + key) - `frontend/src/types/nspd.ts :: RedLine` - `frontend/src/components/site-finder/NspdRedLinesBlock.tsx :: formatArea → formatLength` - `backend/tests/services/test_quarter_dump_lookup.py` — тесты переписать на новое поле + добавить тест на DB exception (как в opportunity). ### 🟡 MEDIUM — `_OPPORTUNITY_TYPE_LABELS` бесполезен `quarter_dump_lookup.py:571-577` — словарь маппит ключи в самих себя: ```python _OPPORTUNITY_TYPE_LABELS = { "auction_parcels": "auction_parcels", "scheme_parcels": "scheme_parcels", ... } ``` Метод `.get(short_name, short_name)` всегда возвращает `short_name`. Либо удалить dict + вернуть `raw_layer.removeprefix("opportunity_")` напрямую, либо положить туда настоящие human-labels (если планировались — UI labels уже в frontend `LAYER_CONFIG`). ### ✅ Что хорошо - Migration idempotent (`ADD COLUMN IF NOT EXISTS` + `BEGIN/COMMIT` + partial index `IF NOT EXISTS`) — правильно. - Partial index `WHERE has_auction_parcels = TRUE` — корректный паттерн, не путает с UI-фильтром. - SQL во всех новых helpers использует `CAST(:wkt AS ...)` стиль (не `:wkt::type`) — соблюдён psycopg v3 trap. - Параметризация bind-vars везде (no f-string SQL) — соблюдена. - `harvest_quarter` и `harvest_stale_quarters` пробрасывают `include_opportunity=True` consistently — нет regression в TIER 3 fanout. - `_upsert_dump` partial-fail branch (`dump=None`) корректно ставит `has_auction_parcels=False, opportunity_count=0`. - 5 layer IDs (37299/37294/37298/36473/875845) для opportunity и 879243 для red_lines соответствуют issue spec, `LAYERS` dict в `nspd_client.py:71-105` содержит все ID. - API контракт backward-compatible (`Optional[list]`, default `[]`). - Frontend `buildNspdViewerUrl` использует `encodeURIComponent` — XSS-safe. Текстовый рендер cad_num внутри `<a>` тоже safe (React escaping). - 17 новых тестов покрывают auction/scheme/free/oopt extraction, distance sort, intersect/nearby split, empty, early-exit, db-exception (для opportunity). ### Резюме Verdict: **🟠 HIGH (changes requested)**. Fix необходим перед merge: 1. Rename migration `89_` → `98_`. 2. Red lines metric: `intersection_area_sqm` → `intersection_length_m` + `ST_Length` (схема + SQL + frontend + tests). 3. (nice-to-have) drop `_OPPORTUNITY_TYPE_LABELS` или наполнить реальными labels. --- ❌ **Blocked for auto-merge** per `.claude/rules/git-pr.md` scope policy (touches `data/sql/**`, `backend/app/api/v1/**`, `backend/app/services/**`, `backend/app/services/scrapers/**`). Human merge required.
lekss361 added 1 commit 2026-05-16 17:04:00 +00:00
- data/sql/98_*: rename from 89 to avoid collision with existing 89_drop_dead_brin
- _get_red_lines: ST_Length(ST_Intersection(planar)::geography) instead of
  ST_Area(ST_Intersection(::geography, ::geography)) — fixes PostGIS 3.4
  tolerance error and returns correct non-zero length for LINESTRING intersections
- Rename intersection_area_sqm → intersection_length_m across schema, TS types,
  frontend component, and tests; add test_get_red_lines_db_exception_returns_empty

Addresses review-bot feedback on PR #220.
Author
Owner

🟢 APPROVE — fixup verified (re-review on fdb5483)

Все 3 issue из предыдущего review (ed3c128, comment #566) адресованы. Контракт, тесты и live DB sanity сходятся.

Fix verification

🔴 #1 — Migration rename — DONE

  • data/sql/89_nspd_quarter_dumps_opportunity_flag.sql98_nspd_quarter_dumps_opportunity_flag.sql
  • Live _schema_migrations подтверждает: applied 89..97, новый 98_* встаёт sequential без conflicts (verified via mcp__postgres-gendesign).
  • BEGIN/COMMIT + ADD COLUMN IF NOT EXISTS + CREATE INDEX IF NOT EXISTS — idempotent .

🟠 #2 — Red lines SQL semantic + runtime bug — DONE

  • Schema: intersection_area_sqmintersection_length_m (parcel.py:117, nspd.ts:63).
  • SQL новый pattern (quarter_dump_lookup.py _get_red_lines):
    ST_Length(
      ST_Intersection(geom_4326, parcel_4326)::geography
    )
    
    Cast в ::geography после ST_Intersection — обходит PostGIS 3.4 tolerance bug. Подтверждено на live DB:
    SELECT ST_Length(ST_Intersection(LINESTRING_4326, POLYGON_4326)::geography);
    → 3248.85 m  ✅ (no error)
    
    Старый broken pattern ST_Area(ST_Intersection(::geography, ::geography)) возвращал 0.0 (wrong metric on lines).
  • Дополнительный bonus: ST_Intersects pre-check + раздельная семантика intersecting (distance=null) vs nearby (length=null) — UI ALERT logic корректный.
  • Frontend NspdRedLinesBlock: formatLength (км/м), ALERT banner для intersect — XSS-safe (encodeURIComponent + React text escaping).
  • Tests: test_get_red_lines_intersects / _nearby_only / _empty / _early_exit / _db_exception — все на новое поле.

🟡 #3_OPPORTUNITY_TYPE_LABELS dead code — NOT FIXED (downgrading to nit)

  • Dict всё ещё мапит {"auction_parcels": "auction_parcels", ...}.get(short_name, short_name) всегда возвращает short_name. 5 LOC dead code (quarter_dump_lookup.py:571-577).
  • Это cosmetic — не блокирует. Human-labels уже в frontend LAYER_CONFIG. Можно убрать в PR3 cleanup или оставить как extension point если планируется backend-side label override.

Non-regression checks

  • Migration idempotent + correct sequential number
  • Partial index WHERE has_auction_parcels = TRUE — корректный low-cardinality pattern
  • Все :param bindings; нет :x::type (psycopg v3 trap OK); CAST(:features_json AS jsonb) правильно
  • Никаких f-string SQL / import psycopg2 / print() / import requests
  • API backward-compatible (Optional[list], .get(..., []) defaults)
  • _upsert_dump partial-fail branch (dump=None) ставит has_auction_parcels=False, opportunity_count=0 — нет partial nullable state
  • harvest_quarter + harvest_stale_quarters consistently пробрасывают include_opportunity=True
  • Frontend: encodeURIComponent для NSPD viewer URL, React text rendering — XSS-safe
  • 17 новых тестов, включая edge cases (empty, early-exit, db-exception)
  • Comment в SQL header корректно фиксирует: «red_lines is TIER 1 core layer — always harvested» (no separate v2 column duplication — фикс из предыдущей итерации)

Резюме

Verdict: 🟢 APPROVE. All 🔴 / 🟠 issues fixed. 🟡 dead-code dict — оставлен, не блокирует.


Blocked for auto-merge per .claude/rules/git-pr.md scope policy (touches data/sql/**, backend/app/api/v1/**, backend/app/services/**, backend/app/services/scrapers/**). Human merge required.

<!-- gendesign-review-bot: sha=fdb5483 verdict=approve --> ## 🟢 APPROVE — fixup verified (re-review on `fdb5483`) Все 3 issue из предыдущего review (`ed3c128`, comment #566) адресованы. Контракт, тесты и live DB sanity сходятся. ### ✅ Fix verification **🔴 #1 — Migration rename** — DONE - `data/sql/89_nspd_quarter_dumps_opportunity_flag.sql` → `98_nspd_quarter_dumps_opportunity_flag.sql` - Live `_schema_migrations` подтверждает: applied `89..97`, новый `98_*` встаёт sequential без conflicts (verified via `mcp__postgres-gendesign`). - BEGIN/COMMIT + `ADD COLUMN IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS` — idempotent ✅. **🟠 #2 — Red lines SQL semantic + runtime bug** — DONE - Schema: `intersection_area_sqm` → `intersection_length_m` (parcel.py:117, nspd.ts:63). - SQL новый pattern (quarter_dump_lookup.py `_get_red_lines`): ```sql ST_Length( ST_Intersection(geom_4326, parcel_4326)::geography ) ``` Cast в `::geography` **после** ST_Intersection — обходит PostGIS 3.4 tolerance bug. Подтверждено на live DB: ``` SELECT ST_Length(ST_Intersection(LINESTRING_4326, POLYGON_4326)::geography); → 3248.85 m ✅ (no error) ``` Старый broken pattern `ST_Area(ST_Intersection(::geography, ::geography))` возвращал `0.0` (wrong metric on lines). - Дополнительный bonus: `ST_Intersects` pre-check + раздельная семантика `intersecting` (distance=null) vs `nearby` (length=null) — UI ALERT logic корректный. - Frontend `NspdRedLinesBlock`: `formatLength` (км/м), ALERT banner для intersect — XSS-safe (encodeURIComponent + React text escaping). - Tests: `test_get_red_lines_intersects` / `_nearby_only` / `_empty` / `_early_exit` / `_db_exception` — все на новое поле. **🟡 #3 — `_OPPORTUNITY_TYPE_LABELS` dead code** — NOT FIXED (downgrading to nit) - Dict всё ещё мапит `{"auction_parcels": "auction_parcels", ...}` → `.get(short_name, short_name)` всегда возвращает `short_name`. 5 LOC dead code (quarter_dump_lookup.py:571-577). - Это cosmetic — не блокирует. Human-labels уже в frontend `LAYER_CONFIG`. Можно убрать в PR3 cleanup или оставить как extension point если планируется backend-side label override. ### ✅ Non-regression checks - ✅ Migration idempotent + correct sequential number - ✅ Partial index `WHERE has_auction_parcels = TRUE` — корректный low-cardinality pattern - ✅ Все `:param` bindings; нет `:x::type` (psycopg v3 trap OK); `CAST(:features_json AS jsonb)` правильно - ✅ Никаких f-string SQL / `import psycopg2` / `print()` / `import requests` - ✅ API backward-compatible (`Optional[list]`, `.get(..., [])` defaults) - ✅ `_upsert_dump` partial-fail branch (`dump=None`) ставит `has_auction_parcels=False, opportunity_count=0` — нет partial nullable state - ✅ `harvest_quarter` + `harvest_stale_quarters` consistently пробрасывают `include_opportunity=True` - ✅ Frontend: `encodeURIComponent` для NSPD viewer URL, React text rendering — XSS-safe - ✅ 17 новых тестов, включая edge cases (empty, early-exit, db-exception) - ✅ Comment в SQL header корректно фиксирует: «red_lines is TIER 1 core layer — always harvested» (no separate v2 column duplication — фикс из предыдущей итерации) ### Резюме **Verdict: 🟢 APPROVE.** All 🔴 / 🟠 issues fixed. 🟡 dead-code dict — оставлен, не блокирует. --- ❌ **Blocked for auto-merge** per `.claude/rules/git-pr.md` scope policy (touches `data/sql/**`, `backend/app/api/v1/**`, `backend/app/services/**`, `backend/app/services/scrapers/**`). Human merge required.
lekss361 merged commit 5a7d405c77 into main 2026-05-16 17:09:53 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#220
No description provided.