From 110d4b34215f4535de9101c92b7aad54d55b32f1 Mon Sep 17 00:00:00 2001 From: leefer Date: Wed, 29 Jul 2026 16:57:01 +0800 Subject: [PATCH] docs: inventory current application architecture --- docs/governance/architecture-inventory.json | 317 ++++++++++++++++++++ docs/governance/stage-02-inventory.md | 72 +++++ tools/build_architecture_inventory.py | 165 ++++++++++ 3 files changed, 554 insertions(+) create mode 100644 docs/governance/architecture-inventory.json create mode 100644 docs/governance/stage-02-inventory.md create mode 100644 tools/build_architecture_inventory.py diff --git a/docs/governance/architecture-inventory.json b/docs/governance/architecture-inventory.json new file mode 100644 index 0000000..520faa8 --- /dev/null +++ b/docs/governance/architecture-inventory.json @@ -0,0 +1,317 @@ +{ + "schema_version": 1, + "captured_from": "323c734", + "runtime": { + "http_server": "http.server.ThreadingHTTPServer", + "application_processes": 1, + "database": "SQLite WAL", + "frontend": "build-free HTML/CSS/JavaScript", + "container_port": 8765 + }, + "counts": { + "primary_pages": 16, + "api_exact_paths": 53, + "api_prefixes": 0, + "api_patterns": 11, + "database_tables": 34 + }, + "pages": [ + { + "id": "sentimentCycleView", + "title": "情绪周期" + }, + { + "id": "limitPool", + "title": "涨停池" + }, + { + "id": "brokenView", + "title": "炸板池" + }, + { + "id": "downView", + "title": "跌停板" + }, + { + "id": "yesterdayView", + "title": "昨日涨停" + }, + { + "id": "performanceView", + "title": "涨停表现" + }, + { + "id": "ladderView", + "title": "市场天梯" + }, + { + "id": "rotationView", + "title": "板块轮动" + }, + { + "id": "auctionView", + "title": "集合竞价" + }, + { + "id": "themeLibraryView", + "title": "题材库" + }, + { + "id": "popularityView", + "title": "人气热榜" + }, + { + "id": "dragonView", + "title": "龙虎榜" + }, + { + "id": "screenerView", + "title": "智能选股" + }, + { + "id": "mentorView", + "title": "问师" + }, + { + "id": "heavenView", + "title": "问天" + }, + { + "id": "reviewWorkspaceView", + "title": "我的复盘" + } + ], + "api": { + "exact": [ + "/api/account/birth-profile", + "/api/account/password", + "/api/account/status", + "/api/admin/membership", + "/api/admin/refresh", + "/api/admin/settings", + "/api/admin/settings/test", + "/api/alerts", + "/api/alerts/read-all", + "/api/assistant/chat", + "/api/assistant/messages", + "/api/auction", + "/api/auth/login", + "/api/auth/logout", + "/api/auth/me", + "/api/auth/register", + "/api/backfill", + "/api/chart/intraday", + "/api/dashboard", + "/api/dragon-tiger", + "/api/dragon-tiger/profiles", + "/api/health", + "/api/heaven/hexagram", + "/api/heaven/interpret", + "/api/heaven/personal", + "/api/heaven/readings", + "/api/heaven/sector-phases", + "/api/heaven/setup", + "/api/mentors/chat", + "/api/mentors/messages", + "/api/mentors/preferences", + "/api/mentors/setup", + "/api/notes", + "/api/popularity", + "/api/realtime-aggregate/health", + "/api/reasons", + "/api/rotation/history", + "/api/rotation/members", + "/api/screener/compile", + "/api/screener/run", + "/api/screener/setup", + "/api/screener/strategies", + "/api/screener/sync", + "/api/screener/tracking", + "/api/screener/tracking/refresh", + "/api/search", + "/api/search/detail", + "/api/seat-aliases", + "/api/sentiment/history", + "/api/themes", + "/api/themes/detail", + "/api/trades", + "/api/watchlist" + ], + "prefixes": [], + "patterns": [ + "/api/alerts/(\\d+)", + "/api/alerts/(\\d+)/read", + "/api/heaven/readings/(\\d+)", + "/api/heaven/sector-phases/(.+)", + "/api/notes/(\\d+)", + "/api/screener/strategies/(\\d+)", + "/api/screener/tracking/(\\d+)", + "/api/stock/(\\d{6})", + "/api/stock/(\\d{6})/preview", + "/api/trades/(\\d+)", + "/api/watchlist/(\\d{6})" + ] + }, + "database_tables": [ + "users", + "user_sessions", + "user_credentials", + "user_birth_profiles", + "system_settings", + "llm_usage", + "dashboard_snapshots", + "sync_runs", + "data_snapshots", + "watchlist", + "review_notes", + "reason_overrides", + "seat_aliases", + "sector_phase_overrides", + "stock_master", + "daily_bars", + "benchmark_bars", + "daily_indicators", + "fundamental_indicators", + "moneyflow_daily", + "auction_factors", + "earnings_events", + "popularity_factors", + "lhb_institution_daily", + "screener_strategies", + "screener_runs", + "mentor_messages", + "mentor_preferences", + "wencai_saved_queries", + "strategy_tracks", + "alerts", + "trade_entries", + "assistant_messages", + "heaven_readings" + ], + "background_job_methods": [ + "_background_refresh_loop", + "_run_background_sync", + "_schedule_automatic_screeners", + "_schedule_ifind_event_enrichment", + "run_automatic_screeners" + ], + "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": [ + "/styles.css", + "/renovation.css?v=20260725-5", + "/redesign-v2.css?v=20260728-1", + "/design-system.css?v=20260728-4", + "/theme.css?v=20260728-2", + "/wentian-v2.css?v=20260728-7" + ], + "code_hotspots": [ + { + "path": "static/app.js", + "bytes": 449457, + "lines": 9447 + }, + { + "path": "static/styles.css", + "bytes": 364385, + "lines": 15549 + }, + { + "path": "server.py", + "bytes": 269167, + "lines": 5965 + }, + { + "path": "static/redesign-v2.css", + "bytes": 264960, + "lines": 8616 + }, + { + "path": "static/index.html", + "bytes": 133569, + "lines": 1871 + }, + { + "path": "database.py", + "bytes": 121468, + "lines": 2839 + }, + { + "path": "screener.py", + "bytes": 108535, + "lines": 2213 + }, + { + "path": "tushare_client.py", + "bytes": 94312, + "lines": 2175 + }, + { + "path": "static/renovation.css", + "bytes": 86245, + "lines": 1636 + }, + { + "path": "static/wentian-v2.css", + "bytes": 73222, + "lines": 1084 + }, + { + "path": "market_insights.py", + "bytes": 58066, + "lines": 1312 + }, + { + "path": "static/theme.css", + "bytes": 41279, + "lines": 1407 + } + ] +} diff --git a/docs/governance/stage-02-inventory.md b/docs/governance/stage-02-inventory.md new file mode 100644 index 0000000..4754aa8 --- /dev/null +++ b/docs/governance/stage-02-inventory.md @@ -0,0 +1,72 @@ +# Stage 02: Architecture Inventory + +Date: 2026-07-29 + +The machine-readable inventory is `architecture-inventory.json`. Regenerate it with: + +```shell +python tools/build_architecture_inventory.py +python tools/build_architecture_inventory.py --check +``` + +## Current Shape + +- The product is a single-container modular monolith with one Python process and SQLite WAL. +- The browser is a build-free single-page application served by the same process. +- The primary navigation exposes 16 workspaces. +- The current database bootstrap defines 34 tables. +- HTTP routing and service orchestration are concentrated in `server.py`. +- Schema creation, inline migrations, and all persistence methods are concentrated in + `database.py`. +- The frontend loads six ordered CSS layers and one global application script. +- Background refresh and automatic screener work execute as daemon threads in the HTTP + process. + +## Ownership Inventory + +### Shared market and system data + +Dashboard snapshots, synchronization runs, stock master data, daily bars, benchmark bars, +daily indicators, fundamental indicators, money flow, auction factors, earnings events, +popularity factors, institutional list data, public screener strategies, reason overrides, +seat aliases, sector phase overrides, and encrypted system settings are shared. + +### User-owned data + +Sessions, personal credentials, birth profiles, watchlists, review notes, custom strategies, +private screener runs, mentor messages and preferences, saved queries, tracking entries, +alerts, trades, assistant messages, and heaven readings are user-scoped. Every repository +extracted in later stages must preserve this boundary explicitly. + +## Runtime Entrypoints + +- HTTP and application bootstrap: `server.py` +- Persistence and migrations: `database.py` +- Access policy: `api_access.py` +- Market providers: `tushare_client.py`, `ifind_client.py`, `chart_data_provider.py`, + `realtime_aggregator.py` +- Deterministic computation: `sentiment_engine.py`, `screener.py`, `advanced_strategies.py`, + `market_insights.py`, `heaven_engine.py` +- LLM calls: `mentor_agent.py`, `heaven_agent.py`, `assistant_agent.py`, `llm_strategy.py` +- Browser shell and pages: `static/index.html`, `static/app.js`, and the ordered CSS files + +## Primary Structural Risks + +1. Request routing, use cases, scheduling, provider selection, serialization, and account + context coexist in one service module. +2. Database migrations cannot be reviewed independently from repository behavior. +3. Several business paths instantiate provider clients directly, so source policy is not + centrally enforceable. +4. LLM authorization, quota accounting, model selection, streaming, and error handling are + distributed across multiple adapters. +5. Global DOM state and page behavior share one JavaScript file, increasing cross-page + regression risk. +6. CSS correctness depends on load order and late overrides rather than explicit layer + ownership. +7. In-process daemon jobs have no durable queue and some failure paths are not observable. + +## Inventory Limits + +The JSON inventory records source-level declarations and references. It does not claim that +every declared endpoint is exercised or every table is populated. Runtime coverage and data +quality are separate contracts introduced in later governance stages. diff --git a/tools/build_architecture_inventory.py b/tools/build_architecture_inventory.py new file mode 100644 index 0000000..b056eef --- /dev/null +++ b/tools/build_architecture_inventory.py @@ -0,0 +1,165 @@ +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": "323c734", + "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())