refactor: enforce canonical backend imports
This commit is contained in:
+5
-1
@@ -55,7 +55,11 @@ background scheduler
|
|||||||
|
|
||||||
Root modules such as `screener.py`, `tushare_client.py`, and `mentor_agent.py` are compatibility
|
Root modules such as `screener.py`, `tushare_client.py`, and `mentor_agent.py` are compatibility
|
||||||
aliases to canonical modules. They contain no second implementation and remain only because
|
aliases to canonical modules. They contain no second implementation and remain only because
|
||||||
the original public import surface is part of the preservation contract.
|
the original public import surface is part of the preservation contract. Canonical backend
|
||||||
|
modules must import other canonical modules directly rather than routing through these aliases.
|
||||||
|
The remaining `api_access` import in `backend/application.py` and preserved lazy
|
||||||
|
`sentiment_engine` import in the screener repository are registered transition boundaries;
|
||||||
|
the root `database.py` remains the documented schema/composition anchor.
|
||||||
|
|
||||||
## Non-negotiable maintenance rules
|
## Non-negotiable maintenance rules
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ from backend.database.repositories import RepositoryBundle, build_repository_bun
|
|||||||
from backend.features.alerts import AlertService
|
from backend.features.alerts import AlertService
|
||||||
from backend.features.mentor.agent import MentorSkillRegistry
|
from backend.features.mentor.agent import MentorSkillRegistry
|
||||||
from backend.features.review import TradeJournalService
|
from backend.features.review import TradeJournalService
|
||||||
|
from backend.features.screener.engine import ScreenerEngine
|
||||||
from backend.features.screener.tracking import StrategyTrackingService
|
from backend.features.screener.tracking import StrategyTrackingService
|
||||||
from backend.jobs import InProcessJobRunner, JobRegistry, SQLiteJobRunRepository
|
from backend.jobs import InProcessJobRunner, JobRegistry, SQLiteJobRunRepository
|
||||||
from database import ReviewDatabase
|
from database import ReviewDatabase
|
||||||
from screener import ScreenerEngine
|
|
||||||
from backend.data.providers.ifind_client import IfindHttpClient
|
from backend.data.providers.ifind_client import IfindHttpClient
|
||||||
from backend.data.realtime import WebRealtimeAggregator
|
from backend.data.realtime import WebRealtimeAggregator
|
||||||
from backend.features.market.charts import MarketChartClient
|
from backend.features.market.charts import MarketChartClient
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import json
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from backend.llm import transport as llm_transport
|
from backend.llm import transport as llm_transport
|
||||||
from screener import FACTOR_FIELDS, REGIMES
|
from backend.features.screener.engine import FACTOR_FIELDS, REGIMES
|
||||||
|
|
||||||
|
|
||||||
class LLMCompilerError(RuntimeError):
|
class LLMCompilerError(RuntimeError):
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ from collections import defaultdict
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from advanced_strategies import ADVANCED_CURATED_STRATEGIES
|
|
||||||
from backend.data.numbers import finite_number as _number
|
from backend.data.numbers import finite_number as _number
|
||||||
|
from backend.data.providers.tushare_client import TushareClient, TushareError
|
||||||
|
from backend.features.screener.strategies import ADVANCED_CURATED_STRATEGIES
|
||||||
from database import ReviewDatabase
|
from database import ReviewDatabase
|
||||||
from backend.features.sentiment.engine import build_sentiment_history, latest_contiguous_history
|
from backend.features.sentiment.engine import build_sentiment_history, latest_contiguous_history
|
||||||
from tushare_client import TushareClient, TushareError
|
|
||||||
|
|
||||||
|
|
||||||
REGIMES = {
|
REGIMES = {
|
||||||
|
|||||||
@@ -289,7 +289,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "backend/features/screener/engine.py",
|
"path": "backend/features/screener/engine.py",
|
||||||
"bytes": 108394,
|
"bytes": 108434,
|
||||||
"lines": 2206
|
"lines": 2206
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-1
@@ -3,7 +3,8 @@ from __future__ import annotations
|
|||||||
import argparse
|
import argparse
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from server import SERVICE, normalize_date
|
from backend.application import SERVICE
|
||||||
|
from backend.bootstrap.config import normalize_date
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
|||||||
@@ -20,12 +20,6 @@ class FeatureBoundaryTests(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
violations = []
|
violations = []
|
||||||
for path in FEATURES.rglob("*.py"):
|
for path in FEATURES.rglob("*.py"):
|
||||||
# The screener engine is an exact-preservation move of the legacy
|
|
||||||
# calculation module. Its provider dependency is covered by the
|
|
||||||
# slice equivalence tests and will be addressed only after the
|
|
||||||
# behavior-preserving migration is complete.
|
|
||||||
if path.relative_to(FEATURES).as_posix() == "screener/engine.py":
|
|
||||||
continue
|
|
||||||
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||||
for node in ast.walk(tree):
|
for node in ast.walk(tree):
|
||||||
names = []
|
names = []
|
||||||
@@ -38,6 +32,53 @@ class FeatureBoundaryTests(unittest.TestCase):
|
|||||||
violations.append(f"{path.relative_to(ROOT)} -> {name}")
|
violations.append(f"{path.relative_to(ROOT)} -> {name}")
|
||||||
self.assertEqual(violations, [])
|
self.assertEqual(violations, [])
|
||||||
|
|
||||||
|
def test_backend_uses_root_compatibility_modules_only_at_declared_boundaries(self) -> None:
|
||||||
|
compatibility_modules = {
|
||||||
|
"advanced_strategies",
|
||||||
|
"alert_service",
|
||||||
|
"api_access",
|
||||||
|
"app_config",
|
||||||
|
"assistant_agent",
|
||||||
|
"chart_data_provider",
|
||||||
|
"heaven_agent",
|
||||||
|
"heaven_engine",
|
||||||
|
"ifind_client",
|
||||||
|
"llm_strategy",
|
||||||
|
"llm_stream",
|
||||||
|
"market_insights",
|
||||||
|
"mentor_agent",
|
||||||
|
"realtime_aggregator",
|
||||||
|
"screener",
|
||||||
|
"security",
|
||||||
|
"sentiment_engine",
|
||||||
|
"server",
|
||||||
|
"strategy_tracking",
|
||||||
|
"trade_journal",
|
||||||
|
"tushare_client",
|
||||||
|
}
|
||||||
|
allowed = {
|
||||||
|
"backend/application.py": {"api_access"},
|
||||||
|
"backend/features/screener/repository.py": {"sentiment_engine"},
|
||||||
|
}
|
||||||
|
violations = []
|
||||||
|
for path in (ROOT / "backend").rglob("*.py"):
|
||||||
|
relative = path.relative_to(ROOT).as_posix()
|
||||||
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
names = []
|
||||||
|
if isinstance(node, ast.Import):
|
||||||
|
names = [alias.name for alias in node.names]
|
||||||
|
elif isinstance(node, ast.ImportFrom) and node.module:
|
||||||
|
names = [node.module]
|
||||||
|
for name in names:
|
||||||
|
root_name = name.split(".")[0]
|
||||||
|
if (
|
||||||
|
root_name in compatibility_modules
|
||||||
|
and root_name not in allowed.get(relative, set())
|
||||||
|
):
|
||||||
|
violations.append(f"{relative} -> {name}")
|
||||||
|
self.assertEqual(violations, [])
|
||||||
|
|
||||||
def test_legacy_service_modules_are_compatibility_exports_only(self) -> None:
|
def test_legacy_service_modules_are_compatibility_exports_only(self) -> None:
|
||||||
for filename in ("alert_service.py", "trade_journal.py", "strategy_tracking.py"):
|
for filename in ("alert_service.py", "trade_journal.py", "strategy_tracking.py"):
|
||||||
tree = ast.parse((ROOT / filename).read_text(encoding="utf-8"))
|
tree = ast.parse((ROOT / filename).read_text(encoding="utf-8"))
|
||||||
|
|||||||
Reference in New Issue
Block a user