refactor(backend)!: separate algorithm and data layers

Reorganize algorithm packages by business responsibility, move orchestration into services, and keep database access behind pooled repositories.

Harden analysis API validation, remove unsafe legacy simulation endpoints, and add regression and architecture boundary coverage.

BREAKING CHANGE: legacy algorithm module paths and obsolete simulation endpoints are removed.
This commit is contained in:
2026-09-04 17:30:55 +08:00
parent 9b095c7439
commit 5966d039de
91 changed files with 1418 additions and 4020 deletions
+109
View File
@@ -0,0 +1,109 @@
"""Executable dependency rules for the service/algorithm/database layers."""
import ast
from pathlib import Path
APP_ROOT = Path(__file__).parents[2] / "app"
def _app_imports(path: Path) -> set[str]:
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
imports: set[str] = set()
package_parts = ("app", *path.relative_to(APP_ROOT).parent.parts)
for node in ast.walk(tree):
if isinstance(node, ast.Import):
imports.update(alias.name for alias in node.names if alias.name.startswith("app."))
elif isinstance(node, ast.ImportFrom):
if node.level:
parent_count = node.level - 1
if parent_count >= len(package_parts):
continue
base_parts = package_parts[: len(package_parts) - parent_count]
if node.module:
imports.add(".".join((*base_parts, *node.module.split("."))))
else:
imports.update(
".".join((*base_parts, alias.name.split(".")[0]))
for alias in node.names
)
elif node.module and node.module.startswith("app."):
imports.add(node.module)
return imports
def _forbidden_imports(root: Path, prefixes: tuple[str, ...]) -> list[str]:
violations: list[str] = []
for path in sorted(root.rglob("*.py")):
for module in sorted(_app_imports(path)):
if module.startswith(prefixes):
violations.append(f"{path.relative_to(APP_ROOT)} -> {module}")
return violations
def test_algorithms_are_independent_of_application_and_io_layers():
violations = _forbidden_imports(
APP_ROOT / "algorithms",
("app.api", "app.auth", "app.infra", "app.native", "app.services"),
)
assert violations == []
def test_infrastructure_does_not_depend_on_application_or_algorithms():
violations = _forbidden_imports(
APP_ROOT / "infra",
("app.api", "app.algorithms", "app.services"),
)
assert violations == []
def test_services_do_not_execute_sql_directly():
violations = []
for path in sorted((APP_ROOT / "services").rglob("*.py")):
source = path.read_text(encoding="utf-8")
if ".cursor(" in source or ".execute(" in source:
violations.append(str(path.relative_to(APP_ROOT)))
assert violations == []
def test_services_do_not_depend_on_the_http_network_facade():
violations = _forbidden_imports(
APP_ROOT / "services",
("app.services.tjnetwork",),
)
assert violations == []
def test_algorithm_packages_use_explicit_business_names():
legacy_names = {
"burst_location",
"cleaning",
"health",
"isolation",
"leakage",
"sensor",
"simulation",
"water_demand",
}
present_names = {
path.name
for path in (APP_ROOT / "algorithms").iterdir()
if path.is_dir() and any(path.glob("*.py"))
}
assert present_names.isdisjoint(legacy_names)
def test_application_code_does_not_open_direct_psycopg_connections():
violations = []
direct_calls = ("psycopg.connect(", "Connection.connect(", "AsyncConnection.connect(")
for path in sorted(APP_ROOT.rglob("*.py")):
source = path.read_text(encoding="utf-8")
if any(call in source for call in direct_calls):
violations.append(str(path.relative_to(APP_ROOT)))
assert violations == []