B14+B15: чистый SearchEngine + слоистая модель данных
B14 (поведение-сохраняющий рефакторинг): - services/search_engine.py — вся SAR-математика из analyze.py: SearchInput/SearchModel, build_search_model (чистая функция, без DB/auth/HTTP), деривации профилей/времени суток, unmodeled. - analyze.py — тонкая обёртка: сборка SearchInput + запись БД. - closed_cases.py — импорты хелперов из движка. - 12 юнит-тестов движка (claude_analyze мокается). - Контракт /analyze не изменён; регресс спеки подтверждён: bike 8yo 2h лес день -> 5.4 км. B15 (слои данных, Alembic): - backend/alembic (env из DATABASE_URL) + миграция 006_b15_layers. - Слой 4: search_models (case_id, version, input_snapshot, model_json). - Слой 3: search_teams, field_observations, areas_checked, found_events (geom JSONB GeoJSON, PostGIS в B16). - Слой 1: reference_priors (пустой, B12 заблокирован). - Бэкфилл: cases.analysis_log (объект с primary_zones) -> search_models v1; legacy-массивы и analysis_log-таблица не тронуты. - Проверено на CT108 в одноразовых pg16-контейнерах: чистая БД (без данных и с ними), бэкфилл=1 из 3 seed-кейсов, downgrade->upgrade идемпотентен, check-constraints работают, источник не модифицирован. pytest: 214 passed.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, Text, ARRAY, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from sqlalchemy.sql import func
|
||||
@@ -117,3 +118,110 @@ class RawDocument(Base):
|
||||
raw_text = Column(Text)
|
||||
extracted_json = Column(JSONB)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
|
||||
# ==================== B15: слоистая модель данных (B13-принцип 2) ====================
|
||||
# Слой 4 (результат): search_models — версионированные поисковые модели (фундамент B18)
|
||||
# Слой 3 (оперативные): field_observations / search_teams / areas_checked / found_events
|
||||
# — схема+модели сейчас, ингестия в B17
|
||||
# Слой 1 (исходные): reference_priors — версионированный справочник априорных
|
||||
# (заглушка под B12/ISRID и экспертные значения; данные вносятся позже)
|
||||
|
||||
|
||||
class SearchModelRecord(Base):
|
||||
"""Слой 4: версия поисковой модели для случая (per-case версионирование)."""
|
||||
__tablename__ = "search_models"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
case_id = Column(UUID(as_uuid=True), ForeignKey("cases.id", ondelete="CASCADE"), nullable=False)
|
||||
version = Column(Integer, nullable=False)
|
||||
input_snapshot = Column(JSONB)
|
||||
model_json = Column(JSONB)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
__table_args__ = (
|
||||
sa.UniqueConstraint("case_id", "version", name="uq_search_models_case_version"),
|
||||
)
|
||||
|
||||
|
||||
class SearchTeam(Base):
|
||||
"""Слой 3: поисковая группа (ингестия — B17)."""
|
||||
__tablename__ = "search_teams"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
case_id = Column(UUID(as_uuid=True), ForeignKey("cases.id", ondelete="CASCADE"), nullable=False)
|
||||
name = Column(String(255), nullable=False)
|
||||
members = Column(Integer)
|
||||
role = Column(String(50))
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
|
||||
class FieldObservation(Base):
|
||||
"""Слой 3: наблюдение в поле (position/observation/track/clue/witness).
|
||||
Геометрия — JSONB GeoJSON (B16 добавит PostGIS-колонки)."""
|
||||
__tablename__ = "field_observations"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
case_id = Column(UUID(as_uuid=True), ForeignKey("cases.id", ondelete="CASCADE"), nullable=False)
|
||||
team_id = Column(UUID(as_uuid=True), ForeignKey("search_teams.id", ondelete="SET NULL"))
|
||||
obs_type = Column(String(20), nullable=False)
|
||||
geom = Column(JSONB)
|
||||
observed_at = Column(DateTime(timezone=True))
|
||||
confidence = Column(Float)
|
||||
description = Column(Text)
|
||||
raw = Column(JSONB)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
__table_args__ = (
|
||||
sa.CheckConstraint(
|
||||
"obs_type IN ('position','observation','track','clue','witness')",
|
||||
name="ck_field_observations_obs_type",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class AreaChecked(Base):
|
||||
"""Слой 3: проверенный участок (результат работы группы)."""
|
||||
__tablename__ = "areas_checked"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
case_id = Column(UUID(as_uuid=True), ForeignKey("cases.id", ondelete="CASCADE"), nullable=False)
|
||||
team_id = Column(UUID(as_uuid=True), ForeignKey("search_teams.id", ondelete="SET NULL"))
|
||||
geom = Column(JSONB)
|
||||
checked_at = Column(DateTime(timezone=True))
|
||||
result = Column(String(20), default="partial")
|
||||
coverage_pct = Column(Float)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
__table_args__ = (
|
||||
sa.CheckConstraint("result IN ('clear','found','partial')", name="ck_areas_checked_result"),
|
||||
)
|
||||
|
||||
|
||||
class FoundEvent(Base):
|
||||
"""Слой 3: событие находки пропавшего."""
|
||||
__tablename__ = "found_events"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
case_id = Column(UUID(as_uuid=True), ForeignKey("cases.id", ondelete="CASCADE"), nullable=False)
|
||||
geom = Column(JSONB)
|
||||
occurred_at = Column(DateTime(timezone=True))
|
||||
condition = Column(String(50))
|
||||
team_id = Column(UUID(as_uuid=True), ForeignKey("search_teams.id", ondelete="SET NULL"))
|
||||
description = Column(Text)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
|
||||
class ReferencePrior(Base):
|
||||
"""Слой 1: версионированный справочник априорных значений
|
||||
(экспертные нормы сейчас, ISRID — после B12). Данные НЕ вносятся (B15)."""
|
||||
__tablename__ = "reference_priors"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
source = Column(String(100), nullable=False)
|
||||
category = Column(String(100), nullable=False)
|
||||
value = Column(Float, nullable=False)
|
||||
units = Column(String(50))
|
||||
provenance = Column(Text)
|
||||
version = Column(Integer, nullable=False, default=1)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
Reference in New Issue
Block a user