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 / "docs" / "governance" / "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]]: exact = sorted(set(re.findall(r'parsed\.path\s*==\s*"(/api/[^"]+)"', server))) prefixes = sorted( set(re.findall(r'parsed\.path\.startswith\(\s*"(/api/[^"]+)"', server)) ) patterns = sorted( set( item for item in re.findall(r'r?["\']([^"\']*?/api/[^"\']+)["\']', server) if "\\d" in item or ".+" in item or "(?P" in item ) ) return {"exact": exact, "prefixes": prefixes, "patterns": patterns} def database_inventory(database: str) -> list[str]: return re.findall(r"CREATE TABLE IF NOT EXISTS\s+([a-zA-Z0-9_]+)", database) 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 code_hotspots() -> list[dict[str, Any]]: candidates = [ "server.py", "database.py", "screener.py", "market_insights.py", "tushare_client.py", "static/index.html", "static/app.js", "static/styles.css", "static/redesign-v2.css", "static/renovation.css", "static/theme.css", "static/wentian-v2.css", ] rows = [] for name in candidates: path = ROOT / name rows.append( { "path": name, "bytes": path.stat().st_size, "lines": len(path.read_text(encoding="utf-8").splitlines()), } ) return sorted(rows, key=lambda item: item["bytes"], reverse=True) def build() -> dict[str, Any]: html = source("static/index.html") server = source("server.py") database = source("database.py") pages = page_inventory(html) api = api_inventory(server) tables = database_inventory(database) return { "schema_version": 1, "captured_from": "governed source tree", "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), }, "pages": pages, "api": api, "database_tables": tables, "background_job_methods": python_functions( "server.py", ("_background", "_run_background", "_schedule_", "run_automatic") ), "external_data_adapters": [ {"provider": "tushare", "path": "tushare_client.py", "runtime_role": "primary deterministic market data"}, {"provider": "ifind", "path": "ifind_client.py", "runtime_role": "realtime, charts, snapshots, enrichment"}, {"provider": "eastmoney", "path": "chart_data_provider.py", "runtime_role": "display chart fallback"}, {"provider": "eastmoney", "path": "realtime_aggregator.py", "runtime_role": "isolated realtime observation"}, {"provider": "tencent", "path": "realtime_aggregator.py", "runtime_role": "index observation fallback"}, ], "llm_entrypoints": [ {"function": "stream_with_mentor", "path": "mentor_agent.py"}, {"function": "interpret_heaven", "path": "heaven_agent.py"}, {"function": "stream_review_assistant", "path": "assistant_agent.py"}, {"function": "compile_strategy_with_llm", "path": "llm_strategy.py"}, {"function": "test_llm_connection", "path": "llm_strategy.py"}, ], "css_layers": css_layers(html), "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())