5 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| bc1da8eac9 |
fix(gate): area-gate ЗОУИТ utility-easement blocker (sub-17) — снять over-block
ZOUIT_OVERLAP_SUB17 (единственный subcategory-blocker = охранная зона ЛЭП/газа/трубопровода) был ХАРД-блокером на ЛЮБОЕ пересечение → блокировал 434/574 (75.6%) участков как «нельзя строить МКД». Прод-замер: реальное покрытие участка этими зонами median=6%, p90=47%, только 1/17 >60% — тонкие инж-коридоры. По РФ охранная зона ограничивает застройку ВНУТРИ полосы, а не весь участок (МКД сажается на незанятой части). Хард-блок на 6%-покрытии неверен и (после фикса резолва зон #1891) прятал financial_estimate у ~90% участков. Фикс: area-gate. - quarter_dump_lookup._get_zouit_overlaps + _get_cad_zouit_overlaps: добавлен coverage_pct (доля площади участка под зоной, в geography м², NULLIF-guard). - gate_verdict.compute_gate_verdict: sub-17 (и cad network/blocker) АГРЕГИРУЮТСЯ (sum-capped, консервативно к блоку) → блокер ТОЛЬКО если покрытие > settings.gate_zouit_engineering_blocker_min_coverage (0.6, env-tunable), иначе WARNING ZOUIT_ENGINEERING_PARTIAL с % покрытия. Больше нет 4× дубль-блокеров. СЗЗ/OKN/прочие ЗОУИТ — без изменений (warnings). main_vri/резидентность — без изменений. - coverage_pct отсутствует (legacy/synthetic) → трактуем как 0 → warning (документировано: дамп всегда даёт coverage; ложный хард-блок дороже). Тесты: +помесь gate/aggregation/coverage (131 combined passed); правка _make_zouit_row (7-я колонка coverage_pct) в tests/test_quarter_dump_lookup.py. Полный бэкенд локально: 2 failed → fixed → перепроверка. ruff чисто. Схема не менялась (gate_verdict/overlaps — dict[str,Any], api-types regen не нужен). Refs #1881 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|||
| cbe509f092 |
fix(site_finder): read real NSPD territorial-zone keys in _get_zoning
_get_zoning() read non-existent props (top-level reg_numb_border / zone_code / type_zone / name) so nspd_zoning.zone_code & zone_name were NULL on every parcel. Real dump props (verified prod, 796 features) carry only the registration number, duplicated across descr / label / externalKey / options.reg_numb_border (no human zone name exists in the NSPD dump). Map both fields to that reg-number with safe nested narrowing (isinstance options dict) + legacy fallbacks; honest — no fabricated name. Surfacing reg_numb_border also unblocks the КСИТ crosswalk (#1843). Regression tests use the real prop shape (full / nested-only / opaque→None). |
|||
| 50555821fb |
feat(site_finder): routing POI decay (#41) + OSM utility distances (#44) + ЗОУИТ geom (#255bk)
Some checks failed
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 7m42s
CI / backend-tests (pull_request) Successful in 7m40s
Deploy / changes (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
Deploy / build-frontend (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
#41: per-category routing-radius decay через reusable ORS /matrix client (foot-walk время + per-category пороги + decay-curves), opt-in ?decay=routing, graceful straight-line fallback (analyze не 500 при ORS down) + length-guard durations==destinations. #44: +5 OSM-категорий (transformer/gas/water/sewerage/heat) via Overpass nwr; nearest_{water_main,substation,gas,heat}_m в analyze.utilities. #255bk: ST_AsGeoJSON(CAST(geom AS geometry)) в _get_cad_zouit_overlaps → geom_geojson. Closes #41 Closes #44 |
|||
| e9585bcd3b |
test(suite): re-align drifted tests with current code (CI-rehab 2/3)
Suite was uncollectable for a while → code evolved, mocks didn't (42 failed / 5 errors → 1687 passed / 0 failed / 0 errors). TEST-ONLY, no app code changed, no real bugs found (code-reviewed ✅ — no test weakened to fake-green). - best_layouts: sum_deals→deals_window rename; velocity-scaling direction for new SF-01 divisors (last_month=1.0/last_year=12.0) - quarter_dump_lookup: 7→10-tuple mock rows (risks/opportunity/red_lines cols); fix early-exit call-counts for the cad_zouit fallback (#232) - cadastre_bulk: add xmin/ymin/xmax/ymax to harvest-quarter db mock - nspd_client: add required QuarterDump.opportunity - TopLayoutRow: add required is_oversold - analyze_{market_price,recent_permits,inline_weights}: SQL-signature dispatch instead of fragile positional db.execute indices (SF-B5 query reorder; also fixes a double-POST geom-starvation false-negative) - custom_pois: move centroid >15km so center_bonus=0 isolates custom-POI delta - layout/report PDF: runtime WeasyPrint probe (skip macOS dev, RUN on CI) - mv_layout SQL: normalize +psycopg DSN + connectivity-probe skipif - admin token tests: skip (X-Admin-Token gate removed #437/#426; protection is Caddy basic_auth + RBAC, covered by test_rbac) Refs #944. |
|||
|
|
1c2f80a4b8
|
feat(site-finder): integrate nspd_quarter_dumps cache в analyze_parcel (#94 Sprint 1.1 FINAL) (#116)
* feat(site-finder): integrate nspd_quarter_dumps cache в analyze_parcel (#94 Sprint 1.1 #4 FINAL) Замыкает Sprint 1.1 из #94 part 2 plan. После этого PR пользователь видит свежие НСПД данные в UI (frontend integration — отдельный PR). Backend (new app/services/site_finder/quarter_dump_lookup.py): - `derive_quarter_cad(cad_num)` — 3/4/5-сегмент → quarter (3-segment) - `get_quarter_dump_data(db, cad_num, parcel_wkt)` — main entrypoint: - Reads nspd_quarter_dumps row для derived quarter - Freshness threshold: 180 days - Missing/stale/harvest_error → trigger harvest_quarter.apply_async() fire- and-forget (lazy import против circular), return EMPTY_DUMP_RESULT - Fresh + parcel_wkt=None → metadata only (no spatial queries) - Fresh + geometry → 3 spatial queries via jsonb_array_elements + ST_Transform (3857→4326) + ST_Intersects / ST_DWithin - 3 private helpers: - `_get_zoning` — point-in-polygon parcel centroid vs territorial_zones, LIMIT 1 - `_get_zouit_overlaps` — все zouit_% layers пересекающиеся с parcel - `_get_engineering_nearby` — engineering_structures в 200m, sorted by distance - `EMPTY_DUMP_RESULT` module-level constant — DRY для no-dump fallback (used in get_quarter_dump_data internal + analyze_parcel try/except wrap) Backend (parcels.py): - Import EMPTY_DUMP_RESULT + get_quarter_dump_data - Call wrapped в try/except — если nspd_quarter_dumps недоступна (DB timeout / table missing) → EMPTY_DUMP_RESULT fallback вместо 500 (consistent с resilience pattern других optional fetches) - Response gets 4 new fields: - nspd_zoning: dict | None (G1 ПЗЗ — zone_code, zone_name, source) - nspd_zouit_overlaps: list[dict] (G3 — overlaps в parcel, per ЗОУИТ group) - nspd_engineering_nearby: list[dict] (I3 — engineering structures в 200m) - nspd_dump: dict (freshness metadata — available, fetched_at_utc, stale, harvest_triggered, total_features) Tests: 13 new в test_quarter_dump_lookup.py (mock-based, no real DB): - derive_quarter_cad 5 edge cases (3seg, 4seg, 5seg, invalid, whitespace) - get_quarter_dump no_row → harvest triggered - stale (>180d) → harvest triggered, stale=True - harvest_error row → retry harvest triggered - parcel_wkt=None → metadata only (1 DB call) - fresh + zoning extraction - fresh + zouit_overlaps list - fresh integration: все 4 keys present 47 pre-existing tests still pass. Code review (code-reviewer pre-push): MINOR, 0 blocking. Applied 2 of 4: - ✅ #1: try/except wrap around get_quarter_dump_data в analyze_parcel (защита от DB unavailability) + DRY через EMPTY_DUMP_RESULT module const - ✅ #2: removed redundant nspd_zoning.fetched_at_utc (DRY — freshness в nspd_dump.fetched_at_utc) - ⏭ Deferred (acceptable): #3 ad-hoc harvest_quarter retry cooldown для harvest_error rows (только при high traffic + persistent NSPD errors); #4 raw_props в response — tech debt, убрать вместе с frontend PR Performance note: 3 spatial queries per analyze adds ~10-50ms on typical ~100-feature quarter. Mitigation if quarters grow dense: materialized per-layer sub-table (отдельная DB issue). Closes Sprint 1.1 part of #94. Frontend rendering этих 4 полей — отдельный PR (next: #112 / #115 / #114). * fix(site-finder): address PR #116 auto-review M1-M5 M1 (mutation risk): replace EMPTY_DUMP_RESULT direct refs with _make_empty_result() factory. dict(...) shallow copy left nested nspd_dump shared by reference across concurrent requests — single mutation pollutes module sentinel for all subsequent calls. Now каждый caller gets independent dict. M2 (O(N) spatial scan): SELECT extended denormalized counts (territorial_zones_count, zouit_count, engineering_count). Each spatial helper accepts layer_counts and early-returns when count=0 — skips heavy jsonb_array_elements + ST_Transform/ST_Intersects scan entirely. Critical для quarters с 2000+ features. M4 (documentation): _trigger_harvest docstring describes known burst/no-dedup limitation + TODO Redis SETNX (отдельный PR). M5 (test fragility): _make_db_mock_with_spatial docstring describes positional-call contract — db.execute order (0=dump, 1=zoning, 2=zouit, 3=engineering) и зависимость от count-values. +4 new tests (17 total, all pass): - test_make_empty_result_returns_independent_copies (mutation safety) - test_make_empty_result_overrides - test_early_exit_all_counts_zero_no_spatial_queries - test_early_exit_partial_counts Per auto-review on |