From 2a7f8551b171b80b97c8ea62c1993753eca65c22 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 30 Aug 2026 11:52:59 +0800 Subject: [PATCH] refactor(startup): move domain assembly to composition --- app/startup/composition/domain.py | 43 +++++++++++++++++++++++++ app/startup/initializers/domain.py | 43 +++---------------------- docs/rules/05-architecture.md | 8 +++-- tests/test_architecture_dependencies.py | 35 ++++++++++++++++++++ 4 files changed, 89 insertions(+), 40 deletions(-) create mode 100644 app/startup/composition/domain.py diff --git a/app/startup/composition/domain.py b/app/startup/composition/domain.py new file mode 100644 index 000000000..065bced0c --- /dev/null +++ b/app/startup/composition/domain.py @@ -0,0 +1,43 @@ +"""领域规则、解析运行时与技术端口的宿主组合 owner。""" + +from app.adapters.network.resolver import SocketDnsResolver +from app.adapters.system import rust as rust_accelerator +from app.adapters.system.host import SystemUtils +from app.application.directory import configure_disk_topology +from app.application.recognition import RecognitionRuleService +from app.application.rules import configure_filter_rule_parser +from app.application.security.url import configure_dns_resolver +from app.application.transfer.workflow import configure_directory_size +from app.domain.media import configure_search_source_provider +from app.domain.meta.customization import configure_customization_provider +from app.domain.meta.releasegroup import configure_release_groups_provider +from app.domain.meta.runtime import configure_recognition_runtime +from app.domain.meta.words import configure_custom_words_provider +from app.domain.metainfo import clear_rust_parse_options_cache +from app.runtime.settings import get_runtime_setting + + +def compose_domain_dependencies() -> None: + """构造并发布领域模型使用的规则、配置和技术 Adapter。""" + from app.domain.projection.tmdb import configure_image_url_builder + + rule_service = RecognitionRuleService() + configure_dns_resolver(SocketDnsResolver()) + configure_disk_topology(SystemUtils) + configure_filter_rule_parser(rust_accelerator) + configure_directory_size(SystemUtils) + configure_customization_provider(rule_service.get_customization) + configure_release_groups_provider(rule_service.get_release_groups) + configure_custom_words_provider(rule_service.get_custom_words) + configure_search_source_provider(lambda: get_runtime_setting("SEARCH_SOURCE")) + configure_image_url_builder(get_runtime_setting("TMDB_IMAGE_URL")) + configure_recognition_runtime( + media_extensions_provider=lambda: ( + *get_runtime_setting("RMT_MEDIAEXT"), + *get_runtime_setting("RMT_SUBEXT"), + *get_runtime_setting("RMT_AUDIOEXT"), + ), + audio_extensions_provider=lambda: get_runtime_setting("RMT_AUDIOEXT"), + accelerator=rust_accelerator, + ) + clear_rust_parse_options_cache() diff --git a/app/startup/initializers/domain.py b/app/startup/initializers/domain.py index 0dfcc53e5..094cff1f6 100644 --- a/app/startup/initializers/domain.py +++ b/app/startup/initializers/domain.py @@ -1,41 +1,8 @@ -from app.adapters.network.resolver import SocketDnsResolver -from app.adapters.system import rust as rust_accelerator -from app.adapters.system.host import SystemUtils -from app.application.directory import configure_disk_topology -from app.application.recognition import RecognitionRuleService -from app.application.rules import configure_filter_rule_parser -from app.application.security.url import configure_dns_resolver -from app.application.transfer.workflow import configure_directory_size -from app.domain.media import configure_search_source_provider -from app.domain.meta.customization import configure_customization_provider -from app.domain.meta.releasegroup import configure_release_groups_provider -from app.domain.meta.runtime import configure_recognition_runtime -from app.domain.meta.words import configure_custom_words_provider -from app.domain.metainfo import clear_rust_parse_options_cache -from app.runtime.settings import get_runtime_setting +"""领域依赖生命周期入口。""" + +from app.startup.composition.domain import compose_domain_dependencies def configure_domain_dependencies() -> None: - """在组合根集中注入领域模型需要的配置、持久化规则和加速适配器。""" - from app.domain.projection.tmdb import configure_image_url_builder - - rule_service = RecognitionRuleService() - configure_dns_resolver(SocketDnsResolver()) - configure_disk_topology(SystemUtils) - configure_filter_rule_parser(rust_accelerator) - configure_directory_size(SystemUtils) - configure_customization_provider(rule_service.get_customization) - configure_release_groups_provider(rule_service.get_release_groups) - configure_custom_words_provider(rule_service.get_custom_words) - configure_search_source_provider(lambda: get_runtime_setting('SEARCH_SOURCE')) - configure_image_url_builder(get_runtime_setting('TMDB_IMAGE_URL')) - configure_recognition_runtime( - media_extensions_provider=lambda: ( - *get_runtime_setting('RMT_MEDIAEXT'), - *get_runtime_setting('RMT_SUBEXT'), - *get_runtime_setting('RMT_AUDIOEXT'), - ), - audio_extensions_provider=lambda: get_runtime_setting('RMT_AUDIOEXT'), - accelerator=rust_accelerator, - ) - clear_rust_parse_options_cache() + """按生命周期顺序调用领域组合 owner。""" + compose_domain_dependencies() diff --git a/docs/rules/05-architecture.md b/docs/rules/05-architecture.md index 32b4f6032..cb8b2820e 100644 --- a/docs/rules/05-architecture.md +++ b/docs/rules/05-architecture.md @@ -157,8 +157,12 @@ access provider assembly; it returns the persistence factories needed by the run owner, which alone constructs `AuthenticationRuntime`. `startup/composition/network.py` owns concrete network-test, image, internal-address and message-ingress Adapter wiring -while retaining lazy RuntimeSettings reads. Initializers call these owners in -startup order and must not recreate their concrete construction. + while retaining lazy RuntimeSettings reads. Initializers call these owners in + startup order and must not recreate their concrete construction. +`startup/composition/domain.py` owns DNS/System/Rust Adapter assembly together +with the single `RecognitionRuleService` used to publish media-recognition rule +providers. `initializers/domain.py` is only the lifecycle hook and must not +construct a parallel rule service or import concrete adapters. `startup/composition/outbox.py` owns durable handler validation, lazy event/notification replay binding and the short-transaction dispatcher factory. Initializers call this owner in diff --git a/tests/test_architecture_dependencies.py b/tests/test_architecture_dependencies.py index 0fc138c62..ff300ebc6 100644 --- a/tests/test_architecture_dependencies.py +++ b/tests/test_architecture_dependencies.py @@ -2428,6 +2428,41 @@ def test_plugin_market_transport_is_process_shared_across_compat_and_composition assert PluginMarketTransport.get_existing_instance() is _plugin_market_transport +def test_domain_initializer_delegates_construction_to_composition(): + """领域 initializer 只负责生命周期调用,不得直接构造 Adapter 或领域服务。""" + initializer_path = APP_ROOT / "startup" / "initializers" / "domain.py" + initializer_tree = ast.parse( + initializer_path.read_text(encoding="utf-8-sig"), + filename=str(initializer_path), + ) + imported_modules = { + node.module + for node in ast.walk(initializer_tree) + if isinstance(node, ast.ImportFrom) and node.module + } + assert imported_modules == {"app.startup.composition.domain"} + calls = [ + ast.unparse(node.func) + for node in ast.walk(initializer_tree) + if isinstance(node, ast.Call) + ] + assert calls == ["compose_domain_dependencies"] + + composition_path = APP_ROOT / "startup" / "composition" / "domain.py" + composition_tree = ast.parse( + composition_path.read_text(encoding="utf-8-sig"), + filename=str(composition_path), + ) + composition_imports = { + node.module + for node in ast.walk(composition_tree) + if isinstance(node, ast.ImportFrom) and node.module + } + assert "app.adapters.network.resolver" in composition_imports + assert "app.adapters.system.host" in composition_imports + assert "app.application.recognition" in composition_imports + + PROCESS_LEVEL_ROOTS = ( "app.api", "app.chain",