feat(rmi-backend,audit): mount Wave 2 wire-in routers + fix news conflict (P2.2)
Some checks failed
CI / build (push) Failing after 3s

Per AUDIT-2026-Q3.md Phase 2 Wave 2.

News refactor (resolves Wave 1 conflict):
- app.domain.news.router prefix: /api/v1/news -> /api/v1
- Catch-all endpoints moved off /api/v1/news/{news_id}* (which
  shadowed /api/v1/news/feed etc.) to /api/v1/articles/{news_id}*
  via route path updates (4 routes unchanged in count).
- app.routers.news now mounts cleanly alongside domain.news under
  the /api/v1/news/* prefix without /{news_id} shadowing.

Routers mounted (6), all import with LIVE deps (no dead imports):
- market_intel_router       /api/v1/market/*            28 routes  CoinGecko/DexScreener/Polymarket, depends on app.services.prediction_market_intel
- prediction_market_router  /api/v1/prediction-markets/*  6 routes  prediction_market_service.py live
- scam_school               /scam-school/*             11 routes  education platform, depends on app.auth, app.core.redis
- wallet_clustering_router  /api/v1/wallet-clusters/*   8 routes  depends on app.ai_router, bundle_detector, chain_cache, cluster_detection, entity_registry, fraud_gnn, spam_registry, unified_provider, wallet_clustering — all live
- security_intel            /api/v1/security/*         31 routes  crypto security stack, depends on app.cross_chain_correlator, ml_anomaly, onchain_analyzer — all live
- mev_sniper                /api/v1/mev-sniper/*        2 routes  MEV Sniper premium scanner; no internal app.* deps

DEFERRED (1):
- app.tool_fingerprint: 773 lines, scam-infra detection; module has NO APIRouter attribute. Need a thin HTTP wrapper (e.g. routes under /api/v1/tool-fingerprint/analyze) before wire-in.

Net: +7 mount entries, +99 OpenAPI operations.
No pytest regression: 3 failed / 817 passed (baseline 3 failed / 817 passed).
Pre-existing 3 failures are in test_factory_boots.py due to app.core.health_route import error (HEALTH_CHECK_DURATION missing in app.core.metrics), unrelated to Phase 2.
This commit is contained in:
Crypto Rug Munch 2026-07-06 20:15:15 +02:00
parent 5972c01969
commit 966118f23e
2 changed files with 49 additions and 24 deletions

View file

@ -18,6 +18,7 @@ Time-decay scoring for /trending:
social_velocity: tweets/shares in last hour (1 + n/100, capped at 2x)
abs(sentiment): polarizing news ranks higher
"""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
@ -30,7 +31,11 @@ from app.catalog.llm_router import LLMRouter
from app.catalog.models import utcnow
from app.catalog.service import get_catalog
router = APIRouter(prefix="/api/v1/news", tags=["news"])
# Prefix is /api/v1 (not /api/v1/news) per Phase 2 Wave 2 (AUDIT-2026-Q3.md):
# the catch-all endpoints moved to /api/v1/articles/{news_id} so that
# app.routers.news can mount /api/v1/news/{feed,headlines,sources,...}
# without being shadowed by a {news_id} catch-all.
router = APIRouter(prefix="/api/v1", tags=["news"])
# ── Time-decay scoring (per v4.0 §T28) ────────────────────────────
@ -145,7 +150,7 @@ def _adapt_legacy_row(row: dict) -> NewsItemOut:
# ── POST /api/v1/news (list with filters) ─────────────────────────
@router.post("", response_model=NewsListResponse)
@router.post("/news", response_model=NewsListResponse)
async def list_news(
chain: str | None = None,
token: str | None = None,
@ -196,6 +201,7 @@ async def list_news(
)
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"news_list_new_fail: {e}")
# Legacy fallback (crypto_news)
@ -206,7 +212,10 @@ async def list_news(
if category:
query += f" AND category = ${len(params) + 1}"
params.append(category)
query += " ORDER BY ingested_at DESC LIMIT $%d OFFSET $%d" % (len(params)+1, len(params)+2) # noqa: UP031
query += " ORDER BY ingested_at DESC LIMIT $%d OFFSET $%d" % (
len(params) + 1,
len(params) + 2,
) # noqa: UP031
params.extend([limit, offset])
async with catalog._pg_pool.acquire() as conn:
rows = await conn.fetch(query, *params)
@ -214,11 +223,13 @@ async def list_news(
items.append(_adapt_legacy_row(dict(r)))
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"news_list_legacy_fail: {e}")
# T03: cluster into stories if requested
if clustered and items:
from app.domain.news.clusterer import NewsItem, cluster_items, persist_clusters
cluster_items_list = [
NewsItem(
id=it.news_id,
@ -235,6 +246,7 @@ async def list_news(
# persist in background (don't block response)
try:
import asyncio
asyncio.create_task(persist_clusters(stories))
except Exception:
pass
@ -262,7 +274,7 @@ async def list_news(
# ── GET /api/v1/news/trending ─────────────────────────────────────
@router.get("/trending", response_model=NewsListResponse)
@router.get("/news/trending", response_model=NewsListResponse)
async def trending_news(
window_hours: int = Query(168, ge=1, le=720),
limit: int = Query(20, ge=1, le=100),
@ -299,12 +311,11 @@ async def trending_news(
tokens_mentioned=list(r["tokens_mentioned"] or []),
sentiment_score=r["sentiment_score"],
)
item.score = round(
trend_score(hours_old, item.source, item.sentiment_score), 4
)
item.score = round(trend_score(hours_old, item.source, item.sentiment_score), 4)
items.append(item)
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"trending_new_fail: {e}")
# Fallback: crypto_news (legacy) if no new items
@ -333,14 +344,17 @@ async def trending_news(
items.append(item)
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"trending_legacy_fail: {e}")
items.sort(key=lambda x: x.score or 0, reverse=True)
return NewsListResponse(items=items[:limit], total=len(items), offset=0)
# ── GET /api/v1/news/{news_id} ────────────────────────────────────
@router.get("/{news_id}", response_model=NewsItemOut)
# ── GET /api/v1/articles/{news_id} ────────────────────────────────
# Moved off /api/v1/news/{news_id} per Phase 2 Wave 2 (AUDIT-2026-Q3.md)
# so app.routers.news (feed/headlines/sources/...) can coexist.
@router.get("/articles/{news_id}", response_model=NewsItemOut)
async def get_news(news_id: str) -> NewsItemOut:
"""Single news item. Searches both news_items and crypto_news."""
catalog = get_catalog()
@ -382,8 +396,9 @@ async def get_news(news_id: str) -> NewsItemOut:
raise HTTPException(500, f"news_get_fail: {e}") from e
# ── POST /api/v1/news/{news_id}/analyze ───────────────────────────
@router.post("/{news_id}/analyze", response_model=NewsAnalysisResponse)
# ── POST /api/v1/articles/{news_id}/analyze ───────────────────────
# Same move rationale as GET /api/v1/articles/{news_id} above.
@router.post("/articles/{news_id}/analyze", response_model=NewsAnalysisResponse)
async def analyze_news(news_id: str) -> NewsAnalysisResponse:
"""Generate LLM analysis via LiteLLM. Falls back to None if LLM unreachable."""
catalog = get_catalog()
@ -441,9 +456,7 @@ async def analyze_news(news_id: str) -> NewsAnalysisResponse:
return NewsAnalysisResponse(
news_id=news_id, analysis=None, error="LLM router unavailable"
)
return NewsAnalysisResponse(
news_id=news_id, analysis=analysis, model="deepseek-v3"
)
return NewsAnalysisResponse(news_id=news_id, analysis=analysis, model="deepseek-v3")
except HTTPException:
raise
except Exception as e:

View file

@ -78,11 +78,23 @@ ROUTER_MODULES: Final[list[str]] = [
"app.routers.criminal_clusters", # /api/v1/criminal-clusters/* (3 routes, Real-CATS)
"app.rag_metrics_api", # /api/v1/rag/metrics (1 route, uses LIVE rag_observability)
#
# DEFERRED (1) - see AUDIT-2026-Q3.md Phase 2 Wave 2:
# "app.routers.news" - CONFLICTS with app.domain.news.router.
# domain.news has GET /{news_id} catch-all; routes.news has /feed /headlines etc.
# Catch-all in domain.news shadows the new routes when domain is mounted first.
# Needs domain.news refactor (move catch-all to a less-broad path) before mount.
# Wire-in Wave 2 — AUDIT-2026-Q3.md Phase 2.2 — mounted 2026-07-07.
# All routers below import cleanly with LIVE deps; no dead imports.
# Mounted 2026-07-07 — 6 medium-wiring routers + news retry:
"app.routers.news", # /api/v1/news/{feed,headlines,sources,sentiment,categories,stats,comment,comments,internal/scanner-alert} (9 routes, news_service.py live). Wave 1 deferred; resolved by moving domain.news catch-all to /api/v1/articles/{news_id}.
"app.routers.market_intel_router", # /api/v1/market/* (28 routes, CoinGecko + DexScreener + Polymarket). Uses LIVE app.services.prediction_market_intel.
"app.routers.prediction_market_router", # /api/v1/prediction-markets/* (6 routes, prediction_market_service.py live)
"app.routers.scam_school", # /scam-school/* (11 routes, education platform). No prefix — paths absolute.
"app.routers.wallet_clustering_router", # /api/v1/wallet-clusters/* (8 routes, wallet_clustering.py live; depends on app.{ai_router,bundle_detector,cluster_detection,entity_registry,fraud_gnn,unified_provider,wallet_clustering,spam_registry,chain_cache})
"app.routers.security_intel", # /api/v1/security/* (31 routes, crypto security stack). Depends on LIVE app.{cross_chain_correlator,ml_anomaly,onchain_analyzer}.
"app.routers.mev_sniper", # /api/v1/mev-sniper/* (2 routes: /signals + /chains). No internal app.* deps. MEV Sniper = premium scanner feature.
#
# DEFERRED (1) — needs APIRouter:
# "app.tool_fingerprint" — module only (no `router` APIRouter attribute).
# 773 lines, scam-infra detection logic. Wire-in requires a thin
# router wrapper exposing the fingerprint functions as endpoints
# (e.g. POST /api/v1/tool-fingerprint/analyze). Defer until wrapper
# is written.
]