diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2cfb529..d4d65c7 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -55,7 +55,11 @@ background scheduler 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 -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 diff --git a/backend/bootstrap/container.py b/backend/bootstrap/container.py index 2640ce1..c872753 100644 --- a/backend/bootstrap/container.py +++ b/backend/bootstrap/container.py @@ -9,10 +9,10 @@ from backend.database.repositories import RepositoryBundle, build_repository_bun from backend.features.alerts import AlertService from backend.features.mentor.agent import MentorSkillRegistry from backend.features.review import TradeJournalService +from backend.features.screener.engine import ScreenerEngine from backend.features.screener.tracking import StrategyTrackingService from backend.jobs import InProcessJobRunner, JobRegistry, SQLiteJobRunRepository from database import ReviewDatabase -from screener import ScreenerEngine from backend.data.providers.ifind_client import IfindHttpClient from backend.data.realtime import WebRealtimeAggregator from backend.features.market.charts import MarketChartClient diff --git a/backend/features/screener/compiler.py b/backend/features/screener/compiler.py index da4406f..167a473 100644 --- a/backend/features/screener/compiler.py +++ b/backend/features/screener/compiler.py @@ -4,7 +4,7 @@ import json from typing import Any 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): diff --git a/backend/features/screener/engine.py b/backend/features/screener/engine.py index e56ceb2..702f5bb 100644 --- a/backend/features/screener/engine.py +++ b/backend/features/screener/engine.py @@ -8,11 +8,11 @@ from collections import defaultdict from datetime import datetime, timedelta from typing import Any -from advanced_strategies import ADVANCED_CURATED_STRATEGIES 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 backend.features.sentiment.engine import build_sentiment_history, latest_contiguous_history -from tushare_client import TushareClient, TushareError REGIMES = { diff --git a/config/architecture-inventory.json b/config/architecture-inventory.json index 1e0b183..f16e44f 100644 --- a/config/architecture-inventory.json +++ b/config/architecture-inventory.json @@ -289,7 +289,7 @@ }, { "path": "backend/features/screener/engine.py", - "bytes": 108394, + "bytes": 108434, "lines": 2206 }, { diff --git a/sync_data.py b/sync_data.py index c3dc0ea..82e1da6 100644 --- a/sync_data.py +++ b/sync_data.py @@ -3,7 +3,8 @@ from __future__ import annotations import argparse 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: diff --git a/tests/test_feature_boundaries.py b/tests/test_feature_boundaries.py index e5f069b..cd0ed75 100644 --- a/tests/test_feature_boundaries.py +++ b/tests/test_feature_boundaries.py @@ -20,12 +20,6 @@ class FeatureBoundaryTests(unittest.TestCase): } violations = [] 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)) for node in ast.walk(tree): names = [] @@ -38,6 +32,53 @@ class FeatureBoundaryTests(unittest.TestCase): violations.append(f"{path.relative_to(ROOT)} -> {name}") 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: for filename in ("alert_service.py", "trade_journal.py", "strategy_tracking.py"): tree = ast.parse((ROOT / filename).read_text(encoding="utf-8"))