From e8ba63e0874395b4e8c6ede250da68efd650449b Mon Sep 17 00:00:00 2001 From: leefer Date: Sun, 2 Aug 2026 00:42:46 +0800 Subject: [PATCH] test: enforce provider construction ownership --- app/config/architecture-inventory.json | 19 ++++++++++++++ app/tests/test_data_gateway.py | 31 +++++++++++++++++++++-- app/tools/build_architecture_inventory.py | 6 +++++ docs/governance/code-reduction.md | 23 +++++++++++++++++ 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/app/config/architecture-inventory.json b/app/config/architecture-inventory.json index 5941ac4..625fdeb 100644 --- a/app/config/architecture-inventory.json +++ b/app/config/architecture-inventory.json @@ -220,6 +220,25 @@ "runtime_role": "index observation fallback" } ], + "provider_construction": [ + { + "client": "TushareClient", + "owner": "backend/data/providers/tushare.py", + "compatibility_fallback": "backend/features/market/service.py" + }, + { + "client": "IfindHttpClient", + "owner": "backend/data/gateway.py" + }, + { + "client": "MarketChartClient", + "owner": "backend/data/gateway.py" + }, + { + "client": "WebRealtimeAggregator", + "owner": "backend/data/gateway.py" + } + ], "numeric_normalization": [ { "function": "finite_number", diff --git a/app/tests/test_data_gateway.py b/app/tests/test_data_gateway.py index 32cfe04..536a0b5 100644 --- a/app/tests/test_data_gateway.py +++ b/app/tests/test_data_gateway.py @@ -1,7 +1,9 @@ from __future__ import annotations +import ast import unittest from datetime import datetime, timedelta +from pathlib import Path from backend.data import ( DataPolicyError, @@ -45,8 +47,6 @@ class DataGatewayTests(unittest.TestCase): self.assertIs(gateway.chart_data.ifind, gateway.ifind) def test_server_has_no_direct_runtime_tushare_construction(self) -> None: - from pathlib import Path - source = ( Path(__file__).resolve().parents[1] / "backend" @@ -57,6 +57,33 @@ class DataGatewayTests(unittest.TestCase): self.assertEqual(source.count("TushareClient(self.token)"), 1) self.assertIn("return gateway.tushare()", source) + def test_provider_construction_has_unique_declared_owners(self) -> None: + root = Path(__file__).resolve().parents[1] + owners = { + "EastmoneyChartClient": {"backend/data/gateway.py"}, + "IfindHttpClient": {"backend/data/gateway.py"}, + "IfindProvider": {"backend/data/gateway.py"}, + "MarketChartClient": {"backend/data/gateway.py"}, + "TushareClient": {"backend/features/market/service.py"}, + "TushareProvider": {"backend/data/gateway.py"}, + "WebRealtimeAggregator": {"backend/data/gateway.py"}, + } + found = {name: set() for name in owners} + 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): + if not isinstance(node, ast.Call): + continue + name = getattr(node.func, "id", None) or getattr(node.func, "attr", None) + if name in found: + found[name].add(relative) + self.assertEqual(found, owners) + provider_source = (root / "backend/data/providers/tushare.py").read_text( + encoding="utf-8" + ) + self.assertIn("client_factory: Callable[[str], TushareClient] = TushareClient", provider_source) + def test_quality_gate_accepts_matching_daily_evidence(self) -> None: timezone = market_timezone() now = datetime(2026, 7, 29, 16, 0, tzinfo=timezone) diff --git a/app/tools/build_architecture_inventory.py b/app/tools/build_architecture_inventory.py index f89a766..f5b84c6 100644 --- a/app/tools/build_architecture_inventory.py +++ b/app/tools/build_architecture_inventory.py @@ -155,6 +155,12 @@ def build() -> dict[str, Any]: {"provider": "eastmoney", "path": "backend/data/realtime.py", "runtime_role": "isolated realtime observation"}, {"provider": "tencent", "path": "backend/data/realtime.py", "runtime_role": "index observation fallback"}, ], + "provider_construction": [ + {"client": "TushareClient", "owner": "backend/data/providers/tushare.py", "compatibility_fallback": "backend/features/market/service.py"}, + {"client": "IfindHttpClient", "owner": "backend/data/gateway.py"}, + {"client": "MarketChartClient", "owner": "backend/data/gateway.py"}, + {"client": "WebRealtimeAggregator", "owner": "backend/data/gateway.py"}, + ], "numeric_normalization": [ {"function": "finite_number", "path": "backend/data/numbers.py"}, {"function": "non_nan_number", "path": "backend/data/numbers.py"}, diff --git a/docs/governance/code-reduction.md b/docs/governance/code-reduction.md index 614898a..cbc57db 100644 --- a/docs/governance/code-reduction.md +++ b/docs/governance/code-reduction.md @@ -24,6 +24,7 @@ | CR-04 | 数值归一化策略 | 四个业务模块分别保留两组完全相同的数值转换函数体 | 由`backend/data/numbers.py`集中拥有两种既有语义,消费者保留原局部别名 | 已完成 | | CR-05 | 根级兼容入口 | 正式后端仍有五处通过迁移兼容模块反向导入规范实现 | 正式代码改用规范路径;兼容入口只服务原公开导入契约 | 已完成 | | CR-06 | 紧凑日期显示 | Tushare与选股引擎各保留一份完全相同的`YYYYMMDD`显示转换 | 由`bootstrap/config.py`拥有唯一格式策略,消费者保留原局部别名 | 已完成 | +| CR-07 | 数据Provider组装 | 核查网关、容器和业务服务是否重复创建外部数据客户端 | 固化唯一创建位置及兼容例外,不改数据源语义 | 已完成 | ## CR-01验收口径 @@ -162,6 +163,28 @@ 本批基线为`xiaobai-reduction-05-compatibility-boundaries-20260801`;检查点为 `xiaobai-reduction-06-date-formatting-20260801`。 +## CR-07验收口径 + +- iFinD、图表、实时观察器和Provider适配器必须只在`build_data_gateway`创建,并由容器共享。 +- Tushare必须继续通过实时Token供应器按需创建,不能为了减少对象数量缓存过期Token。 +- 市场服务中为原版隔离测试桩保留的一处`TushareClient(self.token)`是明确兼容例外,不得被误判为 + 第二条正式数据链路。 +- 不得合并Tushare、iFinD、东方财富和腾讯的传输、缓存、重试或降级逻辑。 + +## CR-07结果 + +- 全后端构造点扫描确认iFinD、MarketChart、东方财富图表和实时观察器均只有网关一个创建位置; + `ApplicationContainer`暴露的是同一对象引用,没有第二份客户端。 +- Tushare Provider使用动态Token供应器,市场服务只有一处已登记测试兼容回退;本批没有发现可安全 + 删除的生产实现,因此不为追求行数强行修改运行代码。 +- 架构清单新增Provider创建所有权,自动测试会在未来出现第二个未登记构造点时失败。 +- 候选322项、纯`app/`导出259项、24个JavaScript文件、API/架构注册表、Git空白检查和SQLite + 完整性检查通过;本批不涉及页面、CSS或浏览器行为。 +- 本批不修改请求频率、缓存、重试、Token更新、数据源选择、计算口径、API、数据库或前端。 + +本批基线为`xiaobai-reduction-06-date-formatting-20260801`;检查点为 +`xiaobai-reduction-07-provider-ownership-20260801`。 + ## 人工验收记录 - 2026-08-01:用户检查CR-02与CR-03运行结果,确认未发现明显异常。本记录仅表示本轮可见功能与