feat(site-finder): gate verdict aggregator (#32 G5 sub-PR 1/2) #142

Merged
lekss361 merged 1 commit from feat/32-gate-verdict-aggregator into main 2026-05-14 21:59:54 +00:00
lekss361 commented 2026-05-14 21:56:33 +00:00 (Migrated from github.com)

Summary

#32 G5 Gate Verdict — sub-PR 1 of 2 (backend aggregator).

Foundation уже shipped через Sprint 1.1 (PR #116):

  • `nspd_zoning` / `nspd_zouit_overlaps` / `nspd_engineering_nearby` / `nspd_dump`

Этот PR aggregates 4 поля в `gate_verdict` для UI banner (sub-PR 2/2).

Aggregator logic

Condition Result
ПЗЗ NOT Ж-* 🔴 BLOCKER (can_build=False)
ЗОУИТ sub=17 (инжен. охранная) 🔴 BLOCKER
ЗОУИТ другие sub 🟡 WARNING
Нет инжен. сетей в 200м 🟡 WARNING
nspd_dump НЕТ unknown
nspd_dump stale (>180d) source: nspd_dump_partial

Response shape

```json
"gate_verdict": {
"can_build_mkd": true | false | "unknown",
"verdict_label": "Можно" | "Нельзя" | "С ограничениями" | "Нужна проверка",
"blockers": [{"code": "PZZ_NOT_RESIDENTIAL", "detail": "..."}],
"warnings": [...],
"checks_performed": ["ПЗЗ зональность", "ЗОУИТ", "Инжен. сети 200м"],
"source": "nspd_dump" | "nspd_dump_partial" | "no_data"
}
```

Tests

15/15 pass — mock-based, no DB.

Sub-PR 2 (next)

GateVerdictBanner в OverviewTab — coloured banner + collapsible details.

Vault

`code/modules/Module_Gate_Verdict.md` NEW

Closes #140
Refs: #32, #94 (foundation)

## Summary **#32 G5 Gate Verdict — sub-PR 1 of 2** (backend aggregator). Foundation уже shipped через Sprint 1.1 (PR #116): - \`nspd_zoning\` / \`nspd_zouit_overlaps\` / \`nspd_engineering_nearby\` / \`nspd_dump\` Этот PR aggregates 4 поля в \`gate_verdict\` для UI banner (sub-PR 2/2). ## Aggregator logic | Condition | Result | |---|---| | ПЗЗ NOT Ж-* | 🔴 BLOCKER (can_build=False) | | ЗОУИТ sub=17 (инжен. охранная) | 🔴 BLOCKER | | ЗОУИТ другие sub | 🟡 WARNING | | Нет инжен. сетей в 200м | 🟡 WARNING | | nspd_dump НЕТ | ❓ unknown | | nspd_dump stale (>180d) | source: nspd_dump_partial | ## Response shape \`\`\`json \"gate_verdict\": { \"can_build_mkd\": true | false | \"unknown\", \"verdict_label\": \"Можно\" | \"Нельзя\" | \"С ограничениями\" | \"Нужна проверка\", \"blockers\": [{\"code\": \"PZZ_NOT_RESIDENTIAL\", \"detail\": \"...\"}], \"warnings\": [...], \"checks_performed\": [\"ПЗЗ зональность\", \"ЗОУИТ\", \"Инжен. сети 200м\"], \"source\": \"nspd_dump\" | \"nspd_dump_partial\" | \"no_data\" } \`\`\` ## Tests **15/15 pass** — mock-based, no DB. ## Sub-PR 2 (next) GateVerdictBanner в OverviewTab — coloured banner + collapsible details. ## Vault \`code/modules/Module_Gate_Verdict.md\` NEW Closes #140 Refs: #32, #94 (foundation)
lekss361 commented 2026-05-14 21:59:36 +00:00 (Migrated from github.com)

Bot review (SHA e644645, CI )

Чистый pure-function aggregator для G5. Inert до sub-PR 2/2 (UI banner).

Design

Pure function без DB — consumes уже-fetched data из analyze_parcel, простая unit-test'абельность
TypedDict response (Blocker/Warning/GateVerdict с Literal types) — strict contract для UI
Constants на топеRESIDENTIAL_ZONE_PREFIXES, BLOCKER_SUBCATEGORIES, ENGINEERING_NEARBY_THRESHOLD_M=200. Easy to evolve.
Verdict ladder (No dump → Нельзя/Можно/С ограничениями/Нужна проверка) покрывает 4 UI banner state'а из issue
source field (nspd_dump / nspd_dump_partial / no_data) — transparency о data freshness для UI tooltip

Safety

if isinstance(sub, int) and sub in BLOCKER_SUBCATEGORIES — None/non-int subcategory не крашит
(nspd_dump or {}).get("available", False) — None-safe access
(nspd_engineering_nearby or []) — None list корректно треатится как empty

Tests (15/15)

Полное покрытие веток:

  • is_residential_zone: Ж-2 / Ж5 prefix / "Жилая" fallback / П-1 reject / Р-2 reject
  • compute_gate_verdict: no_dump / dump available=False / clean residential → Можно / Производственная → Нельзя / ЗОУИТ sub=17 блок / sub-other warning / no-engineering warning / stale → partial source / checks_performed list / PZZ_UNKNOWN warning

parcels.py integration

Surgical (+8 строк): import + call + поле в response dict. Нет side-effects.

Минор (не блокеры — про future iterations sub-PR 3+)

  1. BLOCKER_SUBCATEGORIES = {17: "..."} — пока только охранные ЛЭП/газа/трубопровода. Санитарно-защитные / водоохранные / особо охраняемые сейчас warnings. MVP acceptable; расширять по мере роста domain knowledge.

  2. RESIDENTIAL_KEYWORDS = ("жил",) substring match — теоретически false-positive на zone like "жилищно-коммунальная" (если такая встретится в данных). Acceptable initial heuristic; усложнять только если будут false positives.

  3. Russian labels в коде ("Можно", "Нельзя") — out-of-scope для i18n в текущем sprint.

approve merge

## Bot review (SHA `e644645`, CI ✅) Чистый pure-function aggregator для G5. Inert до sub-PR 2/2 (UI banner). ### Design ✅ **Pure function без DB** — consumes уже-fetched data из analyze_parcel, простая unit-test'абельность ✅ **TypedDict response** (Blocker/Warning/GateVerdict с Literal types) — strict contract для UI ✅ **Constants на топе** — `RESIDENTIAL_ZONE_PREFIXES`, `BLOCKER_SUBCATEGORIES`, `ENGINEERING_NEARBY_THRESHOLD_M=200`. Easy to evolve. ✅ **Verdict ladder** (No dump → Нельзя/Можно/С ограничениями/Нужна проверка) покрывает 4 UI banner state'а из issue ✅ **`source` field** (`nspd_dump` / `nspd_dump_partial` / `no_data`) — transparency о data freshness для UI tooltip ### Safety ✅ **`if isinstance(sub, int) and sub in BLOCKER_SUBCATEGORIES`** — None/non-int subcategory не крашит ✅ **`(nspd_dump or {}).get("available", False)`** — None-safe access ✅ **`(nspd_engineering_nearby or [])`** — None list корректно треатится как empty ### Tests (15/15) Полное покрытие веток: - `is_residential_zone`: Ж-2 / Ж5 prefix / "Жилая" fallback / П-1 reject / Р-2 reject - `compute_gate_verdict`: no_dump / dump available=False / clean residential → Можно / Производственная → Нельзя / ЗОУИТ sub=17 блок / sub-other warning / no-engineering warning / stale → partial source / checks_performed list / PZZ_UNKNOWN warning ### parcels.py integration Surgical (+8 строк): import + call + поле в response dict. Нет side-effects. ### Минор (не блокеры — про future iterations sub-PR 3+) 1. **`BLOCKER_SUBCATEGORIES = {17: "..."}`** — пока только охранные ЛЭП/газа/трубопровода. Санитарно-защитные / водоохранные / особо охраняемые сейчас warnings. MVP acceptable; расширять по мере роста domain knowledge. 2. **`RESIDENTIAL_KEYWORDS = ("жил",)`** substring match — теоретически false-positive на zone like "жилищно-коммунальная" (если такая встретится в данных). Acceptable initial heuristic; усложнять только если будут false positives. 3. **Russian labels в коде** (`"Можно"`, `"Нельзя"`) — out-of-scope для i18n в текущем sprint. approve merge
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#142
No description provided.