diff --git a/app/ARCHITECTURE.md b/app/ARCHITECTURE.md index 2cfb529..d4d65c7 100644 --- a/app/ARCHITECTURE.md +++ b/app/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/app/backend/bootstrap/container.py b/app/backend/bootstrap/container.py index 2640ce1..c872753 100644 --- a/app/backend/bootstrap/container.py +++ b/app/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/app/backend/features/screener/compiler.py b/app/backend/features/screener/compiler.py index da4406f..167a473 100644 --- a/app/backend/features/screener/compiler.py +++ b/app/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/app/backend/features/screener/engine.py b/app/backend/features/screener/engine.py index e56ceb2..702f5bb 100644 --- a/app/backend/features/screener/engine.py +++ b/app/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/app/config/architecture-inventory.json b/app/config/architecture-inventory.json index 1e0b183..f16e44f 100644 --- a/app/config/architecture-inventory.json +++ b/app/config/architecture-inventory.json @@ -289,7 +289,7 @@ }, { "path": "backend/features/screener/engine.py", - "bytes": 108394, + "bytes": 108434, "lines": 2206 }, { diff --git a/app/sync_data.py b/app/sync_data.py index c3dc0ea..82e1da6 100644 --- a/app/sync_data.py +++ b/app/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/app/tests/test_feature_boundaries.py b/app/tests/test_feature_boundaries.py index e5f069b..cd0ed75 100644 --- a/app/tests/test_feature_boundaries.py +++ b/app/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")) diff --git a/docs/governance/code-reduction.md b/docs/governance/code-reduction.md index 8c24e01..6b2e07d 100644 --- a/docs/governance/code-reduction.md +++ b/docs/governance/code-reduction.md @@ -22,6 +22,7 @@ | CR-02 | HTTP精确POST委托 | 27个端点重复使用“比较路径、调用无参数处理器、返回”三行分支 | 用公开/受保护两张显式映射统一委托,同时保留复杂路由的原控制流 | 已完成 | | CR-03 | 股票市场后缀转换 | Tushare业务与iFinD图表各保留一份完全相同的沪深京代码转换函数 | 图表复用`bootstrap/config.py::tushare_code`,只保留一份函数体 | 已完成 | | CR-04 | 数值归一化策略 | 四个业务模块分别保留两组完全相同的数值转换函数体 | 由`backend/data/numbers.py`集中拥有两种既有语义,消费者保留原局部别名 | 已完成 | +| CR-05 | 根级兼容入口 | 正式后端仍有五处通过迁移兼容模块反向导入规范实现 | 正式代码改用规范路径;兼容入口只服务原公开导入契约 | 已完成 | ## CR-01验收口径 @@ -115,6 +116,29 @@ 本批基线为`xiaobai-reduction-03-market-symbol-20260801`;检查点为 `xiaobai-reduction-04-numeric-normalization-20260801`。 +## CR-05验收口径 + +- 逐项扫描根级Python入口、生产代码、测试、工具和动态导入;没有消费者或兼容责任的入口才能删除。 +- 规范后端不得经由`screener`、`advanced_strategies`、`tushare_client`或`server`兼容入口 + 间接访问已经归位的实现。 +- 所有根级模块继续保持原导入名称、导出对象及模块对象身份,既有启动命令和第三方维护脚本不受影响。 +- `api_access`、选股Repository的惰性`sentiment_engine`导入及根级`database.py`属于已登记边界, + 分别留到HTTP、Repository阶段处理,不在本批跨边界修改。 + +## CR-05结果 + +- 审计确认21个根级兼容入口均有测试、工具、启动或原公开导入契约消费者,因此本批没有冒险删除文件。 +- 容器、策略编译器、选股引擎及数据同步命令的五处导入改为规范模块路径,正式代码不再通过四个根级 + 兼容模块反向进入实现;运行代码行数未增加。 +- 特性边界测试取消选股引擎旧例外,并新增全后端兼容导入门禁;只允许两项已登记过渡边界,后续代码 + 无法重新引入隐式根级依赖。 +- 候选321项、纯`app/`导出258项、24个JavaScript文件、API/架构注册表、Git空白检查和SQLite + 完整性检查通过;本批不涉及页面、CSS或浏览器行为。 +- 本批不修改业务计算、策略公式、数据源、API、数据库、LLM、权限、前端或部署。 + +本批基线为`xiaobai-reduction-04-numeric-normalization-20260801`;检查点为 +`xiaobai-reduction-05-compatibility-boundaries-20260801`。 + ## 人工验收记录 - 2026-08-01:用户检查CR-02与CR-03运行结果,确认未发现明显异常。本记录仅表示本轮可见功能与