From d32796277c192391cc8e18216bcf0878dbef3f36 Mon Sep 17 00:00:00 2001 From: IK Date: Mon, 21 Sep 2026 18:04:13 +0700 Subject: [PATCH] test: bound tackle freshness score --- apps/api/app/schemas.py | 2 +- apps/api/openapi.json | 2 ++ apps/api/tests/test_analytics.py | 16 ++++++++++++++++ docs/ROADMAP.md | 2 +- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/api/app/schemas.py b/apps/api/app/schemas.py index f708210..f84dea6 100644 --- a/apps/api/app/schemas.py +++ b/apps/api/app/schemas.py @@ -118,7 +118,7 @@ class TackleCombinationOut(BaseModel): catches: int unique_players: int last_seen_at: datetime - freshness_score: int + freshness_score: int = Field(ge=0, le=100) status: str explanation: str diff --git a/apps/api/openapi.json b/apps/api/openapi.json index b50648c..c588edf 100644 --- a/apps/api/openapi.json +++ b/apps/api/openapi.json @@ -2290,6 +2290,8 @@ "type": "string" }, "freshness_score": { + "maximum": 100.0, + "minimum": 0.0, "title": "Freshness Score", "type": "integer" }, diff --git a/apps/api/tests/test_analytics.py b/apps/api/tests/test_analytics.py index 5da9ec5..42a61df 100644 --- a/apps/api/tests/test_analytics.py +++ b/apps/api/tests/test_analytics.py @@ -6,6 +6,7 @@ from sqlalchemy.orm import Session from app.database import Base from app.models import CatchReport, CatchTackleComponent, Fish, ModerationStatus, SourceType, Spot, Waterbody from app.routers.analytics import tackle_combinations +from app.schemas import TackleCombinationOut def test_tackle_recommendation_requires_samples_and_independent_players() -> None: @@ -99,3 +100,18 @@ def test_tackle_analytics_exposes_decay_and_prefers_fresher_equal_samples() -> N assert rows[0].freshness_score <= 100 engine.dispose() + + +def test_tackle_freshness_score_contract_rejects_out_of_range_values() -> None: + base = { + "role": "lure", "value": "Spinner", "catches": 3, "unique_players": 2, + "last_seen_at": datetime.now(timezone.utc), "status": "recommendation", "explanation": "ok", + } + assert TackleCombinationOut(**base, freshness_score=0).freshness_score == 0 + assert TackleCombinationOut(**base, freshness_score=100).freshness_score == 100 + for value in (-1, 101): + try: + TackleCombinationOut(**base, freshness_score=value) + except ValueError: + continue + raise AssertionError(f"freshness score {value} was accepted") diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index ea5eb60..827c4e0 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -72,7 +72,7 @@ - [ ] **G04 · Crosswalk и нормализация.** Добавлен offline `gear_crosswalk`: нормализация регистра/пробелов/`е/ё`, точное имя или alias плюс совместимая категория, консервативная проверка brand/family. Неоднозначные, несовместимые, брендовые и unmatched-строки получают review-статус без canonical key; исходное значение сохраняется. Остаётся подать реальные RF4DB/RF4MAP/RF4 Posts identities и вручную подтвердить результаты. - [ ] **G05 · Связи с уловами и источниками.** Добавлены `catch_tackle_component` и offline `gear_components`: можно сохранять несколько unresolved/canonical компонентов с ролью, порядком, исходным значением, source identity и `raw_payload`; legacy `bait_id` не меняется. Parser сохраняет порядок оборудования из detail и разделяет bait/rig в catch-полях. Запись компонентов подключена к community import, официальному импорту и пользовательской форме; повторная обработка идемпотентна. Canonical-привязка и безопасный backfill остаются только после подтверждённого crosswalk. - [ ] **G06 · API и публичный каталог.** Добавлены пагинированный `/api/v1/tackle/items` с фильтрами по категории, бренду, семейству и уровню, detail endpoints для предмета и монтажа, а также ordered `tackle_components` в ответе уловов точки. Ответы показывают только канонические характеристики, provenance, timestamp проверки и `missing_fields`; рейтинг эффективности не добавляется. Публичный каталог теперь имеет detail-route `/tackle/items/:id` с 404/unavailable-различением, паспортом данных и явным списком недостающих полей; CatchList показывает ordered-компоненты, ведёт в canonical detail только при наличии `tackle_item_id`, а unresolved raw value оставляет текстом. -- [ ] **G07 · Аналитика сочетаний и рекомендации.** Добавлен `/api/v1/analytics/tackle`: approved-наблюдения группируются по роли и исходному компоненту с фильтрами водоёма, рыбы, метода и окна; дубликаты одного улова не увеличивают счётчик, а минимум наблюдений и независимых игроков отделяет факт использования от рекомендации. Публичный `/tackle/analytics` показывает фильтры, выборку, независимых игроков и раздельные `recommendation`/`insufficient_data` states без рейтинга эффективности. Добавлен объяснимый `freshness_score` с half-life `12.5` часа: он не меняет raw-счётчики и пороги, но стабильно ставит более свежую равную выборку выше старой и виден на карточке. Остаётся дальнейшая визуальная приёмка. +- [ ] **G07 · Аналитика сочетаний и рекомендации.** Добавлен `/api/v1/analytics/tackle`: approved-наблюдения группируются по роли и исходному компоненту с фильтрами водоёма, рыбы, метода и окна; дубликаты одного улова не увеличивают счётчик, а минимум наблюдений и независимых игроков отделяет факт использования от рекомендации. Публичный `/tackle/analytics` показывает фильтры, выборку, независимых игроков и раздельные `recommendation`/`insufficient_data` states без рейтинга эффективности. Добавлен объяснимый `freshness_score` с half-life `12.5` часа и контрактом `0..100`: он не меняет raw-счётчики и пороги, но стабильно ставит более свежую равную выборку выше старой и виден на карточке. Остаётся дальнейшая визуальная приёмка. - [ ] **G08 · Медиа и качество.** Добавлены отдельные reviewed-роли `tackle_card`, `tackle_detail`, `rig_diagram`, `tackle_screenshot` для `tackle`, а также CLI-параметр `--media-role`; offline audit отклоняет неизвестную роль и несовпадение роли с entity type. Существующие dimensions, MIME, SHA-256, прозрачность, aspect ratio, provenance и атомарное продвижение сохраняются. Остаётся провести реальный contact-sheet review для будущих tackle-кандидатов без автоматической публикации. - [ ] **G09 · Приёмка и эксплуатация.** Добавлены fixture/regression tests для crosswalk, media roles, идемпотентных компонентов, пустых результатов и многокомпонентных наблюдений; каталог `/tackle` включён в visual-matrix, narrow smoke и accessibility routes. Offline catalog/media audits и сохранение старых данных при сбое импорта проходят. TEMP-only query-plan gate для фильтра каталога и группировки сочетаний использует индексы и укладывается в 250 мс. Chromium подтвердил empty-state публичного каталога и отсутствие overflow на 320 px; обычный E2E пропускает bootstrap без явных переменных, отдельный bootstrap с токеном проходит. Остаются HTTP/browser acceptance для неоднозначных и многокомпонентных комплектов, ручной visual review и проверяемый счётчик по категориям либо явный `unknown`. Сетевые тесты не выполнять; импорт оставить opt-in, последовательным и под общим cooldown/backoff.