from __future__ import annotations import argparse import ast import json import re from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] OUTPUT = ROOT / "config" / "architecture-inventory.json" def relative(path: Path) -> str: return path.relative_to(ROOT).as_posix() def source(path: str) -> str: return (ROOT / path).read_text(encoding="utf-8") def page_inventory(html: str) -> list[dict[str, str]]: items: list[dict[str, str]] = [] seen: set[str] = set() pattern = re.compile( r']+data-view="(?P[^"]+)"[^>]+title="(?P[^"]+)"', re.IGNORECASE, ) for match in pattern.finditer(html): page_id = match.group("id") if page_id in seen: continue seen.add(page_id) items.append({"id": page_id, "title": match.group("title")}) return items def api_inventory(server: str) -> dict[str, list[str]]: routes = json.loads(source("config/api.config.json"))["routes"] exact = sorted( {item["path"] for item in routes if item["match"] == "exact"} ) prefixes = sorted( set(re.findall(r'parsed\.path\.startswith\(\s*"(/api/[^"]+)"', server)) ) patterns = sorted( {item["path"] for item in routes if item["match"] == "regex"} ) return {"exact": exact, "prefixes": prefixes, "patterns": patterns} def database_inventory(sources: list[str]) -> list[str]: tables: list[str] = [] for database in sources: tables.extend( re.findall( r"CREATE TABLE IF NOT EXISTS\s+([a-zA-Z0-9_]+)", database ) ) return list(dict.fromkeys(tables)) def python_functions(path: str, prefixes: tuple[str, ...]) -> list[str]: tree = ast.parse(source(path), filename=path) result: list[str] = [] for node in ast.walk(tree): if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name.startswith(prefixes): result.append(node.name) return sorted(set(result)) def css_layers(html: str) -> list[str]: return re.findall(r'<link[^>]+rel="stylesheet"[^>]+href="([^"]+)"', html) def frontend_fragments(registry: str) -> list[str]: return re.findall( r'^\s*\["[^"]+", "(/pages/[^"]+/page\.html(?:\?[^"]*)?)", \[', registry, re.MULTILINE, ) def source_metrics(path: Path) -> dict[str, int]: text = path.read_text(encoding="utf-8") return { "bytes": len(text.encode("utf-8")), "lines": len(text.splitlines()), } def code_hotspots() -> list[dict[str, Any]]: candidates = [ "backend/application.py", "backend/http/dispatch.py", "backend/features/system/service.py", "backend/features/accounts/application.py", "backend/jobs/service.py", "database.py", "backend/features/screener/engine.py", "backend/features/screener/catalog.py", "backend/features/screener/data_sync.py", "backend/features/screener/indicators.py", "backend/features/screener/factors.py", "backend/features/screener/formula.py", "backend/features/screener/regime.py", "backend/features/screener/selection.py", "backend/features/screener/backtest.py", "backend/features/market/insights.py", "backend/features/market/insights_context.py", "backend/features/market/insights_auction_scoring.py", "backend/features/market/insights_auction_data.py", "backend/features/market/insights_auction.py", "backend/features/market/insights_themes.py", "backend/features/market/insights_popularity.py", "backend/data/providers/tushare_client.py", "backend/data/providers/tushare_transport.py", "backend/data/providers/tushare_helpers.py", "backend/data/providers/tushare_dashboard.py", "backend/data/providers/tushare_indices.py", "backend/data/providers/tushare_industries.py", "backend/data/providers/tushare_sectors.py", "backend/data/providers/tushare_dragon_tiger.py", "backend/data/providers/tushare_stocks.py", "backend/data/providers/tushare_daily.py", "backend/features/heaven/service.py", "backend/features/heaven/manual.py", "backend/features/heaven/trend.py", "backend/features/heaven/market_context.py", "backend/features/heaven/readings.py", "backend/features/heaven/engine.py", "frontend/index.html", "frontend/bootstrap.js", "frontend/pages.config.js", "frontend/app.js", "frontend/shared/context.js", "frontend/shared/application.js", "frontend/shared/feedback.js", "frontend/shared/dashboard.js", "frontend/shared/session.js", "frontend/shared/admin.js", "frontend/shared/theme.js", "frontend/shared/table.js", "frontend/pages/heaven/foundation.css", "frontend/pages/screener/foundation.css", "frontend/pages/auction/foundation.css", "frontend/shared/shell.css", "frontend/pages/heaven/page.js", ] candidates.extend( relative(path) for path in sorted((ROOT / "frontend" / "pages" / "market").glob("*.js")) ) candidates.extend( relative(path) for path in sorted((ROOT / "backend" / "features").glob("*/routes.py")) ) candidates.extend( relative(path) for path in sorted((ROOT / "frontend" / "pages").glob("*/page.html")) ) rows = [] for name in candidates: path = ROOT / name if not path.is_file(): continue rows.append({"path": name, **source_metrics(path)}) return sorted(rows, key=lambda item: item["bytes"], reverse=True) def build() -> dict[str, Any]: html = source("frontend/index.html") page_registry = source("frontend/pages.config.js") fragments = frontend_fragments(page_registry) http_sources = [source("backend/http/dispatch.py")] http_sources.extend( path.read_text(encoding="utf-8") for path in sorted((ROOT / "backend" / "features").glob("*/routes.py")) ) server = "\n".join(http_sources) database_sources = [source("database.py")] database_sources.extend( path.read_text(encoding="utf-8") for path in sorted((ROOT / "backend" / "database" / "migrations").glob("m*.py")) ) database_sources.append( source("backend/database/migrations/runner.py") ) pages = page_inventory(html) api = api_inventory(server) tables = database_inventory(database_sources) return { "schema_version": 1, "captured_from": "app accepted modular runtime", "runtime": { "http_server": "http.server.ThreadingHTTPServer", "application_processes": 1, "database": "SQLite WAL", "frontend": "build-free HTML/CSS/JavaScript", "container_port": 8765, }, "counts": { "primary_pages": len(pages), "api_exact_paths": len(api["exact"]), "api_prefixes": len(api["prefixes"]), "api_patterns": len(api["patterns"]), "database_tables": len(tables), "frontend_page_fragments": len(fragments), }, "pages": pages, "api": api, "database_tables": tables, "background_job_methods": python_functions( "backend/jobs/service.py", ("_background", "_run_background", "_schedule_", "run_automatic"), ), "external_data_adapters": [ {"provider": "tushare", "path": "backend/data/providers/tushare_client.py", "runtime_role": "stable client facade for primary deterministic market data"}, {"provider": "ifind", "path": "backend/data/providers/ifind_client.py", "runtime_role": "realtime, charts, snapshots, enrichment"}, {"provider": "eastmoney", "path": "backend/features/market/charts.py", "runtime_role": "display chart fallback"}, {"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_domains": [ {"provider": "tushare", "path": "backend/data/providers/tushare_transport.py", "responsibility": "HTTP transport and provider errors"}, {"provider": "tushare", "path": "backend/data/providers/tushare_dashboard.py", "responsibility": "market overview and realtime breadth"}, {"provider": "tushare", "path": "backend/data/providers/tushare_indices.py", "responsibility": "market indices"}, {"provider": "tushare", "path": "backend/data/providers/tushare_industries.py", "responsibility": "Shenwan membership and industry snapshots"}, {"provider": "tushare", "path": "backend/data/providers/tushare_sectors.py", "responsibility": "generic sector snapshots"}, {"provider": "tushare", "path": "backend/data/providers/tushare_dragon_tiger.py", "responsibility": "hot-money directory and dragon-tiger activity"}, {"provider": "tushare", "path": "backend/data/providers/tushare_stocks.py", "responsibility": "stock detail and intraday bars"}, {"provider": "tushare", "path": "backend/data/providers/tushare_daily.py", "responsibility": "trading calendar, daily bars, and limit lists"}, {"provider": "tushare", "path": "backend/data/providers/tushare_helpers.py", "responsibility": "shared deterministic normalization helpers"}, ], "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"}, ], "heaven_service_owners": { "facade": "backend/features/heaven/service.py", "manual_validation_and_safety": "backend/features/heaven/manual.py", "trend_orchestration_and_quality": "backend/features/heaven/trend.py", "market_context": "backend/features/heaven/market_context.py", "readings_and_interpretation": "backend/features/heaven/readings.py", }, "market_insight_owners": { "facade": "backend/features/market/insights.py", "shared_context": "backend/features/market/insights_context.py", "auction_scoring": "backend/features/market/insights_auction_scoring.py", "auction_data": "backend/features/market/insights_auction_data.py", "auction_orchestration": "backend/features/market/insights_auction.py", "themes": "backend/features/market/insights_themes.py", "popularity": "backend/features/market/insights_popularity.py", }, "application_owners": { "composition_root": "backend/application.py", "http_dispatch": "backend/http/dispatch.py", "system_service": "backend/features/system/service.py", "account_bridge": "backend/features/accounts/application.py", "job_lifecycle": "backend/jobs/service.py", "feature_routes": "backend/features/*/routes.py", }, "numeric_normalization": [ {"function": "finite_number", "path": "backend/data/numbers.py"}, {"function": "non_nan_number", "path": "backend/data/numbers.py"}, ], "date_formatting": [ {"function": "display_compact_date", "path": "backend/bootstrap/config.py"}, ], "llm_entrypoints": [ {"function": "stream_with_mentor", "path": "backend/features/mentor/agent.py"}, {"function": "interpret_heaven", "path": "backend/features/heaven/agent.py"}, {"function": "stream_review_assistant", "path": "backend/features/review/agent.py"}, {"function": "compile_strategy_with_llm", "path": "backend/features/screener/compiler.py"}, {"function": "test_llm_connection", "path": "backend/features/screener/compiler.py"}, ], "llm_transport": [ {"function": "chat_completion", "path": "backend/llm/transport.py"}, {"function": "stream_chat_completion", "path": "backend/llm/transport.py"}, ], "http_transport": [ {"function": "send_json", "path": "backend/http/handler.py"}, {"function": "send_ndjson_stream", "path": "backend/http/handler.py"}, ], "css_layers": css_layers(html), "frontend_composition": { "shell": "frontend/index.html", "bootstrap": "frontend/bootstrap.js", "registry": "frontend/pages.config.js", "startup": "frontend/app.js", "runtime_owners": { "context": "frontend/shared/context.js", "application": "frontend/shared/application.js", "feedback": "frontend/shared/feedback.js", "dashboard": "frontend/shared/dashboard.js", "session": "frontend/shared/session.js", "admin": "frontend/shared/admin.js", "theme": "frontend/shared/theme.js", "table": "frontend/shared/table.js", }, "market_runtime_owners": { "breadth": "frontend/pages/market/breadth.js", "charts": "frontend/pages/market/charts.js", "entity_detail": "frontend/pages/market/entity-detail.js", "stock_detail": "frontend/pages/market/stock-detail.js", "preview": "frontend/pages/market/preview.js", "search": "frontend/pages/market/search.js", "bindings": "frontend/pages/market/bindings.js", }, "fragments": [path.split("?", 1)[0] for path in fragments], }, "code_hotspots": code_hotspots(), } def main() -> int: parser = argparse.ArgumentParser(description="Build the current architecture inventory") parser.add_argument("--check", action="store_true", help="fail when the committed inventory is stale") args = parser.parse_args() rendered = json.dumps(build(), ensure_ascii=False, indent=2) + "\n" if args.check: if not OUTPUT.exists() or OUTPUT.read_text(encoding="utf-8") != rendered: raise SystemExit( "architecture inventory is stale; run " "tools/build_architecture_inventory.py" ) print("Architecture inventory is current.") return 0 OUTPUT.parent.mkdir(parents=True, exist_ok=True) OUTPUT.write_text(rendered, encoding="utf-8") print(relative(OUTPUT)) return 0 if __name__ == "__main__": raise SystemExit(main())