refactor: separate config debt from boundaries

This commit is contained in:
jxxghp
2026-08-24 01:19:32 +08:00
parent bc8e4e75e3
commit 99c915b93a
5 changed files with 117 additions and 21 deletions
@@ -109,6 +109,9 @@
宿主 Module 的部署配置读取也已统一到 `RuntimeSettingsCompat`PostgreSQL、Redis、qBittorrent、
rTorrent 和 Transmission 不再直接导入全局 Settings,模块目录由架构测试保持零直连;兼容代理仍保留
启动早期与旧插件/测试的动态 Settings 注入语义。
配置治理基线进一步区分债务与批准边界:canonical 未批准 Settings 直连和非组合根
`SystemConfigOper()` 构造均为 `0``app/db/base.py``engine.py``session.py` 的启动前数据库基础设施
读取及 startup 唯一 Oper 构造点以带理由的固定边界登记。四个集合都只能减少,不能新增或换位置。
### P2:中长期可演进性债务
@@ -1256,6 +1259,10 @@ FS proxy、Local/WebPush 存储适配以及 Module host adapter 切换到动态
Settings,避免代理自递归,配置债务由 15 个文件降至 8 个。数据库引擎/Session 与下载器模块保留底层
Settings 读取作为基础设施边界,架构基线已明确记录该例外,启动与架构专项测试通过。
2026-08-24 将 configuration baseline 升级为 schema v2:待整改的直接 Settings 与 Oper 构造均清零;
数据库模型/引擎/Session 的 3 个启动前读取和 startup 的唯一 Oper 构造点进入带理由的批准边界。ratchet
同时冻结债务与批准边界,批准项不能扩张,因此不会通过新增“例外”掩盖配置回流。
同日修正适配器配置下沉边界:OCR、CookieCloud、DoH、Rust 和资源签名等低层实现不再直接依赖
`app.application`,由 `app.runtime.settings` 端口承接组合根注入;未启动装配时仍回退旧 Settings ABI
架构依赖专项和官方插件语义观察均通过。
+33 -4
View File
@@ -65,6 +65,15 @@ CONFIGURATION_EXCLUDED_ROOTS = (
APP_ROOT / "runtime" / "compat",
APP_ROOT / "testing",
)
FOUNDATIONAL_SETTINGS_BOUNDARIES = {
"app/db/base.py": "模型声明阶段必须在运行时配置服务装配前确定数据库主键类型",
"app/db/engine.py": "数据库引擎是运行时配置服务的底层依赖,不能通过兼容代理自递归",
"app/db/session.py": "数据库会话与连接配额必须在应用组合根装配前可用",
}
COMPOSITION_ROOT_OPER_BOUNDARIES = {
("app/startup/initializers/modules.py", "SystemConfigOper"):
"启动组合根负责构造唯一的系统配置数据库适配器",
}
def discover_modules() -> dict[str, Path]:
@@ -118,9 +127,11 @@ def iter_runtime_import_nodes(tree: ast.AST):
def collect_configuration_debt_baseline() -> dict[str, Any]:
"""收集宿主 canonical 代码直接读取 settings 和构造数据库配置适配器的债务"""
"""分离配置债务与数据库基础设施、组合根的固定批准边界"""
settings_files: list[str] = []
foundational_settings: list[dict[str, str]] = []
oper_calls: list[dict[str, Any]] = []
composition_root_calls: list[dict[str, str]] = []
for path in sorted(APP_ROOT.rglob("*.py")):
if any(path.is_relative_to(root) for root in CONFIGURATION_EXCLUDED_ROOTS):
continue
@@ -143,14 +154,22 @@ def collect_configuration_debt_baseline() -> dict[str, Any]:
if alias.name == "SystemConfigOper"
)
if imports_settings:
settings_files.append(relative)
if reason := FOUNDATIONAL_SETTINGS_BOUNDARIES.get(relative):
foundational_settings.append({"file": relative, "reason": reason})
else:
settings_files.append(relative)
for node in ast.walk(tree):
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Name):
continue
if node.func.id in direct_oper_names:
oper_calls.append({"file": relative, "name": node.func.id})
call = {"file": relative, "name": node.func.id}
boundary_key = (relative, node.func.id)
if reason := COMPOSITION_ROOT_OPER_BOUNDARIES.get(boundary_key):
composition_root_calls.append({**call, "reason": reason})
else:
oper_calls.append(call)
return {
"schema_version": 1,
"schema_version": 2,
"scope": {
"root": "app",
"excluded": [
@@ -164,10 +183,18 @@ def collect_configuration_debt_baseline() -> dict[str, Any]:
"count": len(settings_files),
"files": settings_files,
},
"foundational_settings_boundaries": {
"count": len(foundational_settings),
"entries": foundational_settings,
},
"system_config_oper_constructions": {
"count": len(oper_calls),
"calls": oper_calls,
},
"composition_root_oper_boundaries": {
"count": len(composition_root_calls),
"entries": composition_root_calls,
},
}
@@ -1326,6 +1353,8 @@ def configuration_ratchet_matches(
sections = (
("settings_imports", "files"),
("system_config_oper_constructions", "calls"),
("foundational_settings_boundaries", "entries"),
("composition_root_oper_boundaries", "entries"),
)
for section, entries_key in sections:
expected_section = expected.get(section, {})
+32 -14
View File
@@ -1,5 +1,32 @@
{
"schema_version": 1,
"composition_root_oper_boundaries": {
"count": 1,
"entries": [
{
"file": "app/startup/initializers/modules.py",
"name": "SystemConfigOper",
"reason": "启动组合根负责构造唯一的系统配置数据库适配器"
}
]
},
"foundational_settings_boundaries": {
"count": 3,
"entries": [
{
"file": "app/db/base.py",
"reason": "模型声明阶段必须在运行时配置服务装配前确定数据库主键类型"
},
{
"file": "app/db/engine.py",
"reason": "数据库引擎是运行时配置服务的底层依赖,不能通过兼容代理自递归"
},
{
"file": "app/db/session.py",
"reason": "数据库会话与连接配额必须在应用组合根装配前可用"
}
]
},
"schema_version": 2,
"scope": {
"excluded": [
"app/plugins",
@@ -10,20 +37,11 @@
"root": "app"
},
"settings_imports": {
"count": 3,
"files": [
"app/db/base.py",
"app/db/engine.py",
"app/db/session.py"
]
"count": 0,
"files": []
},
"system_config_oper_constructions": {
"calls": [
{
"file": "app/startup/initializers/modules.py",
"name": "SystemConfigOper"
}
],
"count": 1
"calls": [],
"count": 0
}
}
+29 -1
View File
@@ -53,7 +53,7 @@ def _performance_sample(
def _transaction_sample(methods: list[dict[str, str]]) -> dict:
"""构造最小事务债务 fixture,供单向 ratchet 行为测试。"""
return {
"schema_version": 1,
"schema_version": 2,
"scope": "app/db/models and app/db/oper transaction ownership debt",
"model_decorators": {
"count": len(methods),
@@ -75,8 +75,12 @@ def _transaction_sample(methods: list[dict[str, str]]) -> dict:
def _configuration_sample(
settings_files: list[str],
oper_calls: list[dict[str, str]],
foundational_entries: list[dict[str, str]] | None = None,
composition_entries: list[dict[str, str]] | None = None,
) -> dict:
"""构造最小配置债务 fixture,供单向 ratchet 行为测试。"""
foundational_entries = foundational_entries or []
composition_entries = composition_entries or []
return {
"schema_version": 1,
"scope": {
@@ -91,6 +95,14 @@ def _configuration_sample(
"count": len(oper_calls),
"calls": oper_calls,
},
"foundational_settings_boundaries": {
"count": len(foundational_entries),
"entries": foundational_entries,
},
"composition_root_oper_boundaries": {
"count": len(composition_entries),
"entries": composition_entries,
},
}
@@ -414,6 +426,22 @@ def test_configuration_ratchet_allows_removal_but_rejects_new_access() -> None:
)
def test_configuration_ratchet_rejects_new_approved_boundaries() -> None:
"""批准边界同样只能减少,不能借新增理由把新债务改名为豁免。"""
existing = {"file": "app/db/base.py", "reason": "model declaration"}
added = {"file": "app/db/new.py", "reason": "new exemption"}
expected = _configuration_sample([], [], [existing])
assert architecture_baseline.configuration_ratchet_matches(
expected,
_configuration_sample([], [], []),
)
assert not architecture_baseline.configuration_ratchet_matches(
expected,
_configuration_sample([], [], [existing, added]),
)
def test_architecture_write_host_only_updates_host_files(
tmp_path: Path,
monkeypatch,
+16 -2
View File
@@ -173,11 +173,11 @@ def test_host_oper_does_not_call_base_implicit_write_wrappers() -> None:
def test_configuration_debt_baseline_tracks_canonical_direct_access() -> None:
"""配置债务基线必须排除插件兼容面,并冻结两个可下降的直接访问集合"""
"""配置基线必须把零债务与固定基础设施边界分开冻结"""
baseline_path = BASELINE_ROOT / "configuration-debt-baseline.json"
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
assert baseline["schema_version"] == 1
assert baseline["schema_version"] == 2
assert baseline["scope"]["excluded"] == [
"app/plugins",
"app/sdk",
@@ -190,6 +190,20 @@ def test_configuration_debt_baseline_tracks_canonical_direct_access() -> None:
assert baseline["system_config_oper_constructions"]["count"] == len(
baseline["system_config_oper_constructions"]["calls"]
)
assert baseline["settings_imports"] == {"count": 0, "files": []}
assert baseline["system_config_oper_constructions"] == {
"count": 0,
"calls": [],
}
assert {
entry["file"]
for entry in baseline["foundational_settings_boundaries"]["entries"]
} == {"app/db/base.py", "app/db/engine.py", "app/db/session.py"}
assert baseline["foundational_settings_boundaries"]["count"] == 3
assert baseline["composition_root_oper_boundaries"]["count"] == 1
assert baseline["composition_root_oper_boundaries"]["entries"][0]["file"] == (
"app/startup/initializers/modules.py"
)
def test_startup_performance_baseline_records_normal_and_safe_lifecycle_resources():