Финальная часть эпика #1953: пользователь выбирает типовые дома
(тип × этажность × число секций) вместо авто max-FAR раскладки, формируя
building_program из Stage 3a.
Бэкенд:
- GET /api/v1/concepts/house-types — read-only каталог HOUSE_TYPES
(section_type, label_ru, footprint w×d + sqm, default_floors, housing_class)
как single source of truth; фронт ничего не хардкодит.
- Схема HouseTypeCatalog / HouseTypeCatalogItem в schemas/concept.py.
- Тесты эндпоинта: полнота каталога + совпадение ключей с available_section_types.
Кодген: api-types.ts перегенерён (dump OpenAPI → openapi-typescript →
project-local prettier 3.9.0); 2-й прогон без диффа.
Фронтенд:
- useHouseTypes() (TanStack useQuery, staleTime Infinity) в concept-api.ts;
building_program в ConceptInput, placed_count/requested_count в ConceptVariant.
- HouseProgramPicker: toggle «Авто (max-FAR)» (default, program omit → greedy)
vs «Выбрать дома» (список каталожных типов, count 1-50 / floors 1-40, дефолт
из каталога; габариты/этажность/класс как подсказка). Смонтирован в
Section7Concept и на странице /concept.
- Partial-fit заметка в ConceptVariantsResult: при placed<requested честное
«Разместилось N из M секций — участок вмещает меньше» (нейтрально, не ошибка).
Extend the concept generator so ConceptInput can carry an optional
building_program (list of typed houses from a catalog). When present,
placement lays out EXACTLY that program — for each item, place `count`
sections of the catalog footprint at the item's floors — instead of the
greedy max-FAR coverage-cap sweep. When absent, the existing greedy
behavior is unchanged (byte-for-byte backward-compatible).
- catalog.py: hardcoded HOUSE_TYPES (panel_econom, monolith_comfort,
tower_business, lowrise_comfort, townhouse) — sane-default catalog,
promote to DB later; get_house_type / available_section_types lookups.
- schema: additive BuildingProgramItem {section_type, floors, count} and
ConceptInput.building_program (default None -> greedy). ConceptVariant
gains optional placed_count / requested_count (partial-fit signal).
- placement: shared _Placer (collision/STRtree/setback machine extracted
from greedy sweep, reused — no duplication); place_program +
place_program_variant; branch in place_all_strategies on
building_program. Mixed-floor TEAP via exact per-floor-group aggregation
(GFA = sum(area_i * floors_i), no rounding drift).
- partial fit: when the parcel can't fit all sections, place as many as
fit and report placed_count < requested_count (no hard-422); zero-fit
still raises ParcelGeometryError (-> 422).
- API: validate program section_type keys against the catalog (unknown ->
422) before placement.
- tests: catalog integrity, greedy backward-compat, exact 2-item program +
TEAP reflection, over-packed partial placement, API program path.
- regenerate frontend api-types.ts (OpenAPI codegen gate stays green).
Code-review follow-up: /recompute hardcoded price_source="objective_district_median"
for any body-supplied market_price_per_sqm, mislabeling the honesty-flag once
Stage 2b forwards a price whose genuine source differs (objective_geo_radius /
district_reference / class_norm from financial_estimate).
- schemas/concept.py: add optional price_source: str | None to MassingProgram.
- api/v1/concepts.py: on FAST path use payload.price_source if provided, else the
default label (now a module-level constant _DEFAULT_PRERESOLVED_SOURCE). DB-fallback
and class-norm paths keep their own resolved source unchanged.
- tests: assert body-provided price_source echoes through to financial.price_source
(not overwritten), and the default label applies when the front omits it.
Stage 2a of epic #1953: backend service + endpoint for live economic recompute
driven by the interactive 3D massing (Stage 2b debounced sliders).
- teap.py: add pure synthesize_teap_from_program(total_footprint_sqm, floors,
site_area_sqm, housing_class, sections) — builds a TEAP from the SCALAR
aggregate footprint × floors, mirroring synthesize_teap_from_buildability and
reusing the same shared norm constants (_OFFICE_SHARE_OF_GFA / _EFFICIENCY_BY_CLASS
/ _AVG_APARTMENT_SQM / _PARKING_PER_APARTMENT) — single source of truth.
- schemas/concept.py: add MassingProgram (program contract, optional pre-resolved
market_price_per_sqm + parcel_centroid_wkt) and MassingRecomputeOutput (teap + financial).
- api/v1/concepts.py: add POST /api/v1/concepts/recompute — synthesize TEAP → run
the existing pure compute_financial. FAST path uses body market_price_per_sqm
(no DB); else _lookup_market_price by centroid via run_in_threadpool; else class norm.
- tests: synthesize_teap_from_program (gfa math, parity with compute_teap, class
efficiency, sections no-op) + endpoint (200, coherent output, price passthrough
skips DB, DB fallback, class-norm default, floors validation).