1. `load_water_reserves_from_docx` собирал `result` с ключом `period`,
печатал ПОЛНЫЙ словарь в лог, а возвращал
`{k: v for … if isinstance(v, int)}` — период это строка или None,
поэтому выбрасывался всегда. По логам казалось, что период отдаётся;
вызывающий не получал его ни разу.
Фильтр стоял ради аннотации `dict[str, int]` и ничего не защищал:
соседняя ветка `load_water_reserves` кладёт в тот же словарь
`{"error": str(...)}`, а единственный потребитель — задача
`sync_water_reserves` — результат логирует и возвращает как есть.
Период полезен: без него «загружено 42 записи» не отличить от
прошлогодних. Аннотация исправлена, иначе следующий проход mypy вернул
бы фильтр обратно — на это поставлен отдельный тест.
2. `get_sqlite_info` — TOCTOU: `p.exists()`, затем незащищённый
`p.stat()`. try/except покрывал только `sqlite3.connect` ниже, поэтому
OSError из stat улетал наружу и превращал диагностическую функцию в
источник отказа. Файл между проверками реально исчезает — его
переписывает выгрузка Объектива. Теперь отдаём то, что успели узнать,
с ключом `stat_error`.
3. `place_program` — предупреждение «участок мал» печатало КАТАЛОЖНЫЕ
`house.footprint_*`, хотя ставили по `fp_w`/`fp_d`. При
переопределённом в программе габарите сообщение называло размер,
которым никто не пытался ставить, и уводило от причины.
Двусторонне: против origin/main четыре теста красные с конкретными
значениями («период выброшен из ответа: {'records': 1, 'inserted': 1,
'updated': 0}», «аннотация всё ещё требует только int: dict[str, int]»).
Контроли зелёные с обеих сторон: отсутствующий файл по-прежнему даёт
exists=False без ошибки; `fp_w`/`fp_d` — действительно те размеры,
которыми ставят (иначе первый тест сверял бы имена, а не смысл).
pytest backend/tests/services/ — 3207 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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).