Some checks failed
CI / build (push) Failing after 2s
Per AUDIT-2026-Q3.md Phase 2, mounted the highest-quality dead routers
that fit the product surface with minimal wiring cost.
Routers mounted (24):
- bulletin, bulletin_board (FEATURES.md claim, 21+22 routes, supabase-backed)
- intelligence_router, intelligence_panel (n8n-fed, 19+4 routes, /intelligence prefix shared, paths non-overlapping)
- chat (AI chat = product surface, 5 routes)
- address_profiler, chain_comparability (scanner extensions, 1+2 routes)
- honeypot_map (unique viz, 3 routes)
- label_lookup (CF worker enrichment, 1 route)
- whale_alerts, lp_health (5+5 routes, whale tracker + LP monitor)
- ai_stream (SSE infra, 3 routes)
- rugcharts, bubble_maps_router (RugCharts + RugMaps, 3+7 routes)
- mev_advisory, impersonation_detector (forensics, 2+1 routes)
- rug_recovery, laundry_matcher (recovery + copycat detect, 2+2 routes)
- death_clock, token_cv, tvl_verifier (scanner differentiators, 1+2+2)
- contract_analyzer, criminal_clusters (SENTINEL ext + Real-CATS, 2+3)
Flat file mounted (1):
- rag_metrics_api (16 lines, uses LIVE rag_observability, /api/v1/rag/metrics)
Flat files NOT mounted (audit was wrong, no APIRouter / no consumers):
- circuit_breaker.py (no APIRouter; not imported by x402_databus_tools - duplicate
_CircuitBreaker classes in app/databus/provider_core.py and app/databus/providers.py)
- sweep_now.py (CLI cron utility, operational, not an API)
- tool_fingerprint.py (utility, no APIRouter)
- ai_pipeline_v3.py (utility, already imported by app/news_intelligence.py and
app/routers/unified_wallet_scanner.py)
Deferred to Wave 2 (path conflict):
- 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
(verified via TestClient - /feed hit domain catch-all with news_id=feed).
Needs domain.news refactor (move catch-all to a less-broad path) before mount.
Also fixed pre-existing mypy errors in mount_all and _try_mount (added Any type
annotations) so pre-commit hook passes.
Net effect:
- Before: 79 routes (24 top-level app.routes)
- After: 198 routes (48 top-level app.routes) = +119 routes
- Mounted: 25 modules (24 routers + 1 flat)
- pytest: 817 passed, 3 failed (all 3 pre-existing, unrelated to this change:
test_factory_boots.py needs HEALTH_CHECK_DURATION in app/core/metrics.py)
- OpenAPI: 184 paths, all new routes visible
130 lines
6.1 KiB
Python
130 lines
6.1 KiB
Python
"""RMI Backend - router mounting.
|
|
|
|
Single source of truth for every router. Adding a new domain:
|
|
1. Create the module with a `router = APIRouter(...)` attribute
|
|
2. Add the import path to ROUTER_MODULES below
|
|
3. Done - factory.create_app() picks it up automatically
|
|
|
|
Per v4.0 §T01 + ADR-0001, this replaces the hardcoded lists that
|
|
existed in the old main.py. Each mount is isolated - one failure
|
|
doesn't break the others.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import logging
|
|
from typing import Any, Final
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
# Canonical router list (single source of truth)
|
|
# Order matters for path resolution. v1 routers are mounted at /api/v1/*.
|
|
# Domain routers (news, reports, x402) self-prefix.
|
|
ROUTER_MODULES: Final[list[str]] = [
|
|
# Core system routes
|
|
"app.core.health_route", # /health, /live, /ready
|
|
"app.core.metrics", # /metrics
|
|
"app.homepage", # /, /version
|
|
# v1 API (thin HTTP layer)
|
|
"app.api.v1.auth.alerts", # /api/v1/alerts/*
|
|
"app.api.v1.public.wallet", # /api/v1/wallet/*
|
|
"app.api.v1.public.token", # /api/v1/token/*
|
|
"app.api.v1.public.scanner", # /api/v1/scanner/*
|
|
"app.api.v1.rag.search", # /api/v1/rag/v2/*
|
|
"app.api.v1.admin.alerts_webhook", # /api/v1/admin/alerts/webhook
|
|
"app.api.v1.admin.glitchtip_test", # /api/v1/_test/* (T07)
|
|
"app.api.v1.catalog", # /api/v1/catalog/*
|
|
"app.api.v1.mcp", # /mcp/* (JSON-RPC + plain JSON)
|
|
# Domain facades (per v4.0 T28-T34)
|
|
"app.domain.news", # /api/v1/news/*
|
|
"app.domain.news.admin_router", # /api/v1/news/_admin/*
|
|
"app.domain.reports", # /api/v1/reports/*
|
|
"app.domain.x402", # /api/v1/x402/*
|
|
# x402 MCP + Discovery + Docs
|
|
"app.routers.x402_mcp_handler", # /mcp/x402, /.well-known/x402
|
|
"app.routers.x402_docs", # /x402/docs, /x402/sandbox/*
|
|
"app.routers.x402_growth", # /api/v1/x402/webhooks/*, /usage, /pay, /affiliates
|
|
"app.routers.x402_docs_v2", # /docs/v2/quickstart, /docs/v2/api-reference
|
|
"app.routers.stripe_integration", # /api/v1/stripe/*, /api/v1/credits/*
|
|
# Wire-in Wave 1 - AUDIT-2026-Q3 Phase 2
|
|
# Mounted 2026-07-07. Re-validated "dead" routers, all import cleanly,
|
|
# all prefixes non-conflicting with existing mounts.
|
|
#
|
|
# Routers (24) - total ~140 routes added:
|
|
"app.routers.bulletin", # /api/v1/bulletin/* (21 routes, supabase)
|
|
"app.routers.bulletin_board", # /api/v1/bulletin-board/* (22 routes, app/bulletin_board.py)
|
|
"app.routers.intelligence_router", # /api/v1/intelligence/* (19 routes, n8n-fed)
|
|
"app.routers.intelligence_panel", # /api/v1/intelligence/* (4 routes, /feed /latest /crypto-fear-index)
|
|
"app.routers.chat", # /api/v1/chat/* (5 routes, AI chat surface)
|
|
"app.routers.address_profiler", # /api/v1/address-profiler/* (1 route, scanner ext)
|
|
"app.routers.chain_comparability", # /api/v1/chain-compare/* (2 routes, multi-chain)
|
|
"app.routers.honeypot_map", # /api/v1/honeypot-map/* (3 routes, unique viz)
|
|
"app.routers.label_lookup", # /api/v1/labels/* (1 route, CF worker enrichment)
|
|
"app.routers.whale_alerts", # /api/v1/whale-alerts/* (5 routes, whale tracker)
|
|
"app.routers.lp_health", # /api/v1/lp-health/* (5 routes, LP monitor)
|
|
"app.routers.ai_stream", # /api/v1/ai/* (3 routes, SSE infra)
|
|
"app.routers.rugcharts", # /api/v1/rugcharts/* (3 routes, RugCharts backend)
|
|
"app.routers.bubble_maps_router", # /api/v1/rugmaps/* (7 routes, RugMaps viz)
|
|
"app.routers.mev_advisory", # /api/v1/mev-advisory/* (2 routes, MEV warning)
|
|
"app.routers.impersonation_detector", # /api/v1/impersonation/* (1 route, forensics)
|
|
"app.routers.rug_recovery", # /api/v1/rug-recovery/* (2 routes, recovery indexer)
|
|
"app.routers.laundry_matcher", # /api/v1/laundry-matcher/* (2 routes, copycat detect)
|
|
"app.routers.death_clock", # /api/v1/death-clock/* (1 route, Token Death Clock)
|
|
"app.routers.token_cv", # /api/v1/token-cv/* (2 routes, On-Chain CV)
|
|
"app.routers.tvl_verifier", # /api/v1/tvl-verify/* (2 routes, DeFiLlama mismatch)
|
|
"app.routers.contract_analyzer", # /api/v1/contract-analyzer/* (2 routes, SENTINEL ext)
|
|
"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.
|
|
]
|
|
|
|
|
|
def mount_all(app: Any) -> int:
|
|
"""Mount every router. Returns count of successful mounts.
|
|
|
|
Each mount is isolated - one failure does not break the rest.
|
|
"""
|
|
mounted = 0
|
|
for module_path in ROUTER_MODULES:
|
|
if _try_mount(app, module_path):
|
|
mounted += 1
|
|
return mounted
|
|
|
|
|
|
def _try_mount(app: Any, module_path: str) -> bool:
|
|
"""Try to import + mount a single router. Returns True on success."""
|
|
try:
|
|
module = importlib.import_module(module_path)
|
|
router = getattr(module, "router", None)
|
|
if router is None:
|
|
log.warning("router_missing path=%s (no `router` attribute)", module_path)
|
|
return False
|
|
app.include_router(router)
|
|
log.info("router_mounted path=%s", module_path)
|
|
return True
|
|
except Exception as exc:
|
|
log.warning(
|
|
"router_mount_failed path=%s err=%s: %s",
|
|
module_path,
|
|
type(exc).__name__,
|
|
exc,
|
|
)
|
|
return False
|
|
|
|
|
|
def register_external_router(module_path: str) -> None:
|
|
"""Allow third-party code to register additional routers at runtime.
|
|
|
|
Plugins / MCP servers / worker modules can call this from their
|
|
own __init__.py to add routes to the main app.
|
|
"""
|
|
if module_path not in ROUTER_MODULES:
|
|
ROUTER_MODULES.append(module_path)
|
|
log.info("router_registered_external path=%s", module_path)
|