chore: register pages features APIs and data contracts
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# Governance Registries
|
||||
|
||||
These registries describe the approved product surface during architecture migration.
|
||||
|
||||
- `pages.config.json`: primary page identity, navigation group, access expectation, scrolling,
|
||||
and mobile composition policy.
|
||||
- `features.config.json`: feature ownership, backend access class, data scope, and availability.
|
||||
- `api.config.json`: transitional inventory of current routes, generated from `server.py` and
|
||||
assigned to a feature owner.
|
||||
- `data-fields.config.json`: canonical data products, provider eligibility, intended use, and
|
||||
known blocked datasets.
|
||||
|
||||
During Stage 04 these files are contract inputs, not runtime replacements. Backend access in
|
||||
`api_access.py` remains authoritative until the HTTP governance phase switches it atomically.
|
||||
Frontend visibility remains a presentation concern and never grants backend access.
|
||||
|
||||
Regenerate the transitional API inventory after a route change:
|
||||
|
||||
```shell
|
||||
python tools/build_api_registry.py
|
||||
python tools/build_api_registry.py --check
|
||||
```
|
||||
@@ -0,0 +1,524 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"generated_from": "server.py",
|
||||
"routes": [
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/api/account/birth-profile",
|
||||
"match": "exact",
|
||||
"feature": "account",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/account/birth-profile",
|
||||
"match": "exact",
|
||||
"feature": "account",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/account/password",
|
||||
"match": "exact",
|
||||
"feature": "account",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/account/status",
|
||||
"match": "exact",
|
||||
"feature": "account",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/admin/membership",
|
||||
"match": "exact",
|
||||
"feature": "admin",
|
||||
"access": "admin"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/admin/refresh",
|
||||
"match": "exact",
|
||||
"feature": "admin",
|
||||
"access": "admin"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/admin/settings",
|
||||
"match": "exact",
|
||||
"feature": "admin",
|
||||
"access": "admin"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/admin/settings",
|
||||
"match": "exact",
|
||||
"feature": "admin",
|
||||
"access": "admin"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/admin/settings/test",
|
||||
"match": "exact",
|
||||
"feature": "admin",
|
||||
"access": "admin"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/alerts",
|
||||
"match": "exact",
|
||||
"feature": "alerts",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/alerts",
|
||||
"match": "exact",
|
||||
"feature": "alerts",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/api/alerts/(\\d+)",
|
||||
"match": "regex",
|
||||
"feature": "alerts",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/alerts/(\\d+)/read",
|
||||
"match": "regex",
|
||||
"feature": "alerts",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/alerts/read-all",
|
||||
"match": "exact",
|
||||
"feature": "alerts",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/assistant/chat",
|
||||
"match": "exact",
|
||||
"feature": "review",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/api/assistant/messages",
|
||||
"match": "exact",
|
||||
"feature": "review",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/assistant/messages",
|
||||
"match": "exact",
|
||||
"feature": "review",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/auction",
|
||||
"match": "exact",
|
||||
"feature": "auction",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/auth/login",
|
||||
"match": "exact",
|
||||
"feature": "auth",
|
||||
"access": "public"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/auth/logout",
|
||||
"match": "exact",
|
||||
"feature": "auth",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/auth/me",
|
||||
"match": "exact",
|
||||
"feature": "auth",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/auth/register",
|
||||
"match": "exact",
|
||||
"feature": "auth",
|
||||
"access": "public"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/backfill",
|
||||
"match": "exact",
|
||||
"feature": "admin",
|
||||
"access": "admin"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/chart/intraday",
|
||||
"match": "exact",
|
||||
"feature": "charts",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/dashboard",
|
||||
"match": "exact",
|
||||
"feature": "market",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/dragon-tiger",
|
||||
"match": "exact",
|
||||
"feature": "dragon_tiger",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/dragon-tiger/profiles",
|
||||
"match": "exact",
|
||||
"feature": "dragon_tiger",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/health",
|
||||
"match": "exact",
|
||||
"feature": "health",
|
||||
"access": "public"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/heaven/hexagram",
|
||||
"match": "exact",
|
||||
"feature": "heaven",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/heaven/interpret",
|
||||
"match": "exact",
|
||||
"feature": "heaven",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/heaven/personal",
|
||||
"match": "exact",
|
||||
"feature": "heaven",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/heaven/readings",
|
||||
"match": "exact",
|
||||
"feature": "heaven",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/api/heaven/readings/(\\d+)",
|
||||
"match": "regex",
|
||||
"feature": "heaven",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/heaven/sector-phases",
|
||||
"match": "exact",
|
||||
"feature": "heaven",
|
||||
"access": "admin"
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/api/heaven/sector-phases/(.+)",
|
||||
"match": "regex",
|
||||
"feature": "heaven",
|
||||
"access": "admin"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/heaven/setup",
|
||||
"match": "exact",
|
||||
"feature": "heaven",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/mentors/chat",
|
||||
"match": "exact",
|
||||
"feature": "mentor",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/api/mentors/messages",
|
||||
"match": "exact",
|
||||
"feature": "mentor",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/mentors/messages",
|
||||
"match": "exact",
|
||||
"feature": "mentor",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/mentors/preferences",
|
||||
"match": "exact",
|
||||
"feature": "mentor",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/mentors/setup",
|
||||
"match": "exact",
|
||||
"feature": "mentor",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/notes",
|
||||
"match": "exact",
|
||||
"feature": "review",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/notes",
|
||||
"match": "exact",
|
||||
"feature": "review",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/api/notes/(\\d+)",
|
||||
"match": "regex",
|
||||
"feature": "review",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/popularity",
|
||||
"match": "exact",
|
||||
"feature": "popularity",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/realtime-aggregate/health",
|
||||
"match": "exact",
|
||||
"feature": "market",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/reasons",
|
||||
"match": "exact",
|
||||
"feature": "admin",
|
||||
"access": "admin"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/rotation/history",
|
||||
"match": "exact",
|
||||
"feature": "rotation",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/rotation/members",
|
||||
"match": "exact",
|
||||
"feature": "rotation",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/screener/compile",
|
||||
"match": "exact",
|
||||
"feature": "screener",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/screener/run",
|
||||
"match": "exact",
|
||||
"feature": "screener",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/screener/setup",
|
||||
"match": "exact",
|
||||
"feature": "screener",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/screener/strategies",
|
||||
"match": "exact",
|
||||
"feature": "screener",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/api/screener/strategies/(\\d+)",
|
||||
"match": "regex",
|
||||
"feature": "screener",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/screener/sync",
|
||||
"match": "exact",
|
||||
"feature": "screener",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/screener/tracking",
|
||||
"match": "exact",
|
||||
"feature": "screener",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/screener/tracking",
|
||||
"match": "exact",
|
||||
"feature": "screener",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/api/screener/tracking/(\\d+)",
|
||||
"match": "regex",
|
||||
"feature": "screener",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/screener/tracking/refresh",
|
||||
"match": "exact",
|
||||
"feature": "screener",
|
||||
"access": "member"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/search",
|
||||
"match": "exact",
|
||||
"feature": "search",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/search/detail",
|
||||
"match": "exact",
|
||||
"feature": "search",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/seat-aliases",
|
||||
"match": "exact",
|
||||
"feature": "admin",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/seat-aliases",
|
||||
"match": "exact",
|
||||
"feature": "admin",
|
||||
"access": "admin"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/sentiment/history",
|
||||
"match": "exact",
|
||||
"feature": "sentiment",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/stock/(\\d{6})",
|
||||
"match": "regex",
|
||||
"feature": "market",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/stock/(\\d{6})/preview",
|
||||
"match": "regex",
|
||||
"feature": "market",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/themes",
|
||||
"match": "exact",
|
||||
"feature": "themes",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/themes/detail",
|
||||
"match": "exact",
|
||||
"feature": "themes",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/trades",
|
||||
"match": "exact",
|
||||
"feature": "review",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/trades",
|
||||
"match": "exact",
|
||||
"feature": "review",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/api/trades/(\\d+)",
|
||||
"match": "regex",
|
||||
"feature": "review",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/watchlist",
|
||||
"match": "exact",
|
||||
"feature": "review",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/watchlist",
|
||||
"match": "exact",
|
||||
"feature": "review",
|
||||
"access": "authenticated"
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/api/watchlist/(\\d{6})",
|
||||
"match": "regex",
|
||||
"feature": "review",
|
||||
"access": "authenticated"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"providers": {
|
||||
"tushare": {"class": "licensed", "calculation_allowed": true},
|
||||
"ifind": {"class": "licensed", "calculation_allowed": true},
|
||||
"eastmoney": {"class": "public_web", "calculation_allowed": false},
|
||||
"tencent": {"class": "public_web", "calculation_allowed": false},
|
||||
"local": {"class": "derived", "calculation_allowed": true},
|
||||
"unresolved": {"class": "missing", "calculation_allowed": false}
|
||||
},
|
||||
"datasets": [
|
||||
{"id": "market.trade_calendar", "entity": "market", "frequency": "daily", "primary": "tushare", "fallbacks": [], "usage": "calculation", "fields": ["trade_date", "is_open", "previous_open_date"]},
|
||||
{"id": "market.stock_master", "entity": "stock", "frequency": "event", "primary": "tushare", "fallbacks": [], "usage": "calculation", "fields": ["ts_code", "name", "industry", "market", "list_date"]},
|
||||
{"id": "market.stock_daily", "entity": "stock", "frequency": "daily", "primary": "tushare", "fallbacks": [], "usage": "calculation", "adjustment": "current-unadjusted", "fields": ["open", "high", "low", "close", "pct_chg", "volume_shares", "amount_yuan"]},
|
||||
{"id": "market.daily_valuation", "entity": "stock", "frequency": "daily", "primary": "tushare", "fallbacks": [], "usage": "calculation", "fields": ["turnover_rate_pct", "volume_ratio", "total_mv_10k_yuan", "circ_mv_10k_yuan", "pe_ttm", "pb", "ps_ttm", "dv_ttm_pct"]},
|
||||
{"id": "market.fundamentals", "entity": "stock", "frequency": "quarterly", "primary": "tushare", "fallbacks": [], "usage": "calculation", "point_in_time": "announcement_date", "fields": ["roe_pct", "roa_pct", "roic_pct", "gross_margin_pct", "net_profit_yoy_pct", "revenue_yoy_pct", "operating_cashflow_quality"]},
|
||||
{"id": "market.moneyflow", "entity": "stock", "frequency": "daily", "primary": "tushare", "fallbacks": [], "usage": "calculation", "fields": ["small_net_yuan", "medium_net_yuan", "large_net_yuan", "extra_large_net_yuan", "total_net_yuan"]},
|
||||
{"id": "market.industry_sw", "entity": "industry", "frequency": "daily", "primary": "tushare", "fallbacks": [], "usage": "calculation", "fields": ["industry_code", "industry_name", "level", "members", "pct_chg", "turnover_rate_pct"]},
|
||||
{"id": "market.limit_events", "entity": "stock", "frequency": "daily", "primary": "tushare", "fallbacks": [], "usage": "calculation", "fields": ["limit_type", "first_time", "last_time", "open_times", "limit_reason", "consecutive_boards"]},
|
||||
{"id": "market.auction_close", "entity": "stock", "frequency": "daily", "primary": "tushare", "fallbacks": [], "usage": "calculation", "fields": ["price", "volume_shares", "amount_yuan", "pre_close", "turnover_rate_pct", "volume_ratio", "float_share"]},
|
||||
{"id": "market.auction_dynamic", "entity": "stock", "frequency": "snapshot", "primary": "ifind", "fallbacks": [], "usage": "calculation", "freshness_seconds": 10, "fields": ["quote_time", "price", "pct_chg", "volume_shares", "amount_yuan"]},
|
||||
{"id": "market.popularity", "entity": "stock", "frequency": "daily", "primary": "tushare", "fallbacks": [], "usage": "calculation", "fields": ["ths_rank", "dc_rank", "rank_change", "dual_source"]},
|
||||
{"id": "market.dragon_tiger", "entity": "stock", "frequency": "daily", "primary": "tushare", "fallbacks": [], "usage": "calculation", "fields": ["seat_name", "buy_yuan", "sell_yuan", "net_buy_yuan", "side", "reason"]},
|
||||
{"id": "chart.stock_daily", "entity": "stock", "frequency": "daily", "primary": "ifind", "fallbacks": [], "usage": "display", "adjustment": "forward1", "fields": ["open", "high", "low", "close", "volume_shares", "amount_yuan"]},
|
||||
{"id": "chart.intraday", "entity": "stock_or_index_or_board", "frequency": "minute", "primary": "ifind", "fallbacks": ["eastmoney"], "usage": "display", "fields": ["quote_time", "open", "high", "low", "close", "avg_price", "volume_shares", "amount_yuan"]},
|
||||
{"id": "observation.realtime_indices", "entity": "index", "frequency": "snapshot", "primary": "eastmoney", "fallbacks": ["tencent"], "usage": "display", "fields": ["quote_time", "price", "pct_chg", "amount_yuan"]},
|
||||
{"id": "derived.sentiment", "entity": "market", "frequency": "daily", "primary": "local", "fallbacks": [], "usage": "calculation", "fields": ["temperature", "stage", "direction", "confidence", "component_scores"]},
|
||||
{"id": "research.consensus", "entity": "stock", "frequency": "event", "primary": "unresolved", "fallbacks": [], "usage": "blocked", "fields": ["consensus_profit", "forecast_revision", "rating_change", "target_price", "report_count"]},
|
||||
{"id": "market.level2", "entity": "stock", "frequency": "tick", "primary": "unresolved", "fallbacks": [], "usage": "blocked", "fields": ["order_queue", "unmatched_orders", "tick_trades", "tick_orders", "open_board_depth"]}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"roles": ["public", "authenticated", "member", "admin"],
|
||||
"features": [
|
||||
{"id": "health", "title": "健康检查", "access": "public", "data_scope": "system", "enabled": true},
|
||||
{"id": "auth", "title": "账户认证", "access": "public", "data_scope": "user", "enabled": true},
|
||||
{"id": "account", "title": "账户设置", "access": "authenticated", "data_scope": "user", "enabled": true},
|
||||
{"id": "admin", "title": "系统管理", "access": "admin", "data_scope": "system", "enabled": true},
|
||||
{"id": "market", "title": "市场总览", "access": "authenticated", "data_scope": "shared", "enabled": true},
|
||||
{"id": "sentiment", "title": "情绪周期", "access": "authenticated", "data_scope": "shared", "enabled": true},
|
||||
{"id": "pools", "title": "市场股池", "access": "authenticated", "data_scope": "shared", "enabled": true},
|
||||
{"id": "ladder", "title": "市场天梯", "access": "authenticated", "data_scope": "shared", "enabled": true},
|
||||
{"id": "rotation", "title": "板块轮动", "access": "authenticated", "data_scope": "shared", "enabled": true},
|
||||
{"id": "auction", "title": "集合竞价", "access": "authenticated", "data_scope": "shared", "enabled": true},
|
||||
{"id": "themes", "title": "题材库", "access": "authenticated", "data_scope": "shared", "enabled": true},
|
||||
{"id": "popularity", "title": "人气热榜", "access": "authenticated", "data_scope": "shared", "enabled": true},
|
||||
{"id": "dragon_tiger", "title": "龙虎榜", "access": "authenticated", "data_scope": "shared", "enabled": true},
|
||||
{"id": "search", "title": "全局搜索", "access": "authenticated", "data_scope": "shared", "enabled": true},
|
||||
{"id": "charts", "title": "行情图表", "access": "authenticated", "data_scope": "shared", "enabled": true},
|
||||
{"id": "screener", "title": "智能选股", "access": "member", "data_scope": "mixed", "daily_llm_quota": true, "enabled": true},
|
||||
{"id": "mentor", "title": "问师", "access": "member", "data_scope": "user", "daily_llm_quota": true, "enabled": true},
|
||||
{"id": "heaven", "title": "问天", "access": "member", "data_scope": "user", "daily_llm_quota": true, "enabled": true},
|
||||
{"id": "review", "title": "我的复盘", "access": "authenticated", "data_scope": "user", "enabled": true},
|
||||
{"id": "alerts", "title": "提醒中心", "access": "authenticated", "data_scope": "user", "enabled": true}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"pages": [
|
||||
{"id": "sentimentCycleView", "title": "情绪周期", "feature": "sentiment", "group": "market", "access": "authenticated", "desktop_scroll": "page", "mobile_layout": "dedicated", "default": true},
|
||||
{"id": "limitPool", "title": "涨停池", "feature": "pools", "group": "market", "access": "authenticated", "desktop_scroll": "page", "mobile_layout": "dedicated", "default": false},
|
||||
{"id": "brokenView", "title": "炸板池", "feature": "pools", "group": "market", "access": "authenticated", "desktop_scroll": "page", "mobile_layout": "dedicated", "default": false},
|
||||
{"id": "downView", "title": "跌停板", "feature": "pools", "group": "market", "access": "authenticated", "desktop_scroll": "page", "mobile_layout": "dedicated", "default": false},
|
||||
{"id": "yesterdayView", "title": "昨日涨停", "feature": "pools", "group": "market", "access": "authenticated", "desktop_scroll": "page", "mobile_layout": "dedicated", "default": false},
|
||||
{"id": "performanceView", "title": "涨停表现", "feature": "pools", "group": "market", "access": "authenticated", "desktop_scroll": "page", "mobile_layout": "dedicated", "default": false},
|
||||
{"id": "ladderView", "title": "市场天梯", "feature": "ladder", "group": "market", "access": "authenticated", "desktop_scroll": "page", "mobile_layout": "dedicated", "default": false},
|
||||
{"id": "rotationView", "title": "板块轮动", "feature": "rotation", "group": "market", "access": "authenticated", "desktop_scroll": "page", "mobile_layout": "dedicated", "default": false},
|
||||
{"id": "auctionView", "title": "集合竞价", "feature": "auction", "group": "market", "access": "authenticated", "desktop_scroll": "page", "mobile_layout": "dedicated", "default": false},
|
||||
{"id": "themeLibraryView", "title": "题材库", "feature": "themes", "group": "market", "access": "authenticated", "desktop_scroll": "page", "mobile_layout": "dedicated", "default": false},
|
||||
{"id": "popularityView", "title": "人气热榜", "feature": "popularity", "group": "market", "access": "authenticated", "desktop_scroll": "page", "mobile_layout": "dedicated", "default": false},
|
||||
{"id": "dragonView", "title": "龙虎榜", "feature": "dragon_tiger", "group": "market", "access": "authenticated", "desktop_scroll": "page", "mobile_layout": "dedicated", "default": false},
|
||||
{"id": "screenerView", "title": "智能选股", "feature": "screener", "group": "intelligence", "access": "member", "desktop_scroll": "page", "mobile_layout": "dedicated", "default": false},
|
||||
{"id": "mentorView", "title": "问师", "feature": "mentor", "group": "intelligence", "access": "member", "desktop_scroll": "page", "mobile_layout": "dedicated", "default": false},
|
||||
{"id": "heavenView", "title": "问天", "feature": "heaven", "group": "intelligence", "access": "member", "desktop_scroll": "page", "mobile_layout": "dedicated", "default": false},
|
||||
{"id": "reviewWorkspaceView", "title": "我的复盘", "feature": "review", "group": "personal", "access": "authenticated", "desktop_scroll": "page", "mobile_layout": "dedicated", "default": false}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# Stage 04: Governance Registries
|
||||
|
||||
Date: 2026-07-29
|
||||
|
||||
## Result
|
||||
|
||||
Four versioned registries now describe the existing product before runtime decomposition:
|
||||
|
||||
1. `config/pages.config.json` registers all 16 primary workspaces.
|
||||
2. `config/features.config.json` registers 20 feature owners and their access/data scope.
|
||||
3. `config/api.config.json` registers 74 method/path combinations, including dynamic paths.
|
||||
4. `config/data-fields.config.json` registers 18 initial canonical data products and provider
|
||||
eligibility.
|
||||
|
||||
The API registry is generated from the current request handler so route drift fails tests. The
|
||||
other registries are curated contracts and may change only through an explicit product or data
|
||||
governance decision.
|
||||
|
||||
## Access Decisions Preserved
|
||||
|
||||
- Health, login, and registration are public.
|
||||
- Market views require an authenticated account.
|
||||
- Intelligent screening, Mentor, Wentian, and their history endpoints require membership.
|
||||
- My Review remains visible to authenticated users; LLM assistant endpoints remain member
|
||||
gated inside that workspace.
|
||||
- System management and shared knowledge modification remain administrator-only.
|
||||
- Frontend visibility does not grant API access.
|
||||
|
||||
## Data Decisions Preserved
|
||||
|
||||
- Tushare and licensed iFinD data may be calculation inputs when a dataset contract permits it.
|
||||
- Eastmoney and Tencent public endpoints are display-only in the initial registry.
|
||||
- Analyst consensus history and Level-2 microstructure remain blocked rather than silently
|
||||
approximated.
|
||||
- Chart fallbacks remain separate from calculation datasets.
|
||||
- The current unadjusted deterministic daily-bar behavior is recorded honestly; adjustment
|
||||
normalization is deferred to the data-quality phase rather than changed here.
|
||||
|
||||
## Transitional Rule
|
||||
|
||||
These files do not yet replace `server.py`, `api_access.py`, or the current frontend navigation.
|
||||
They are checked against those surfaces to prevent untracked drift. Later phases make each
|
||||
registry authoritative in one atomic migration with compatibility tests.
|
||||
|
||||
## Verification
|
||||
|
||||
`tests/test_governance_registries.py` verifies uniqueness, page coverage, current API coverage,
|
||||
explicit public routes, feature ownership, role validity, and provider eligibility.
|
||||
@@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from tools.build_api_registry import build as build_api_registry
|
||||
from tools.build_architecture_inventory import build as build_architecture_inventory
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CONFIG = ROOT / "config"
|
||||
|
||||
|
||||
def load(name: str) -> dict:
|
||||
return json.loads((CONFIG / name).read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
class GovernanceRegistryTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.features = load("features.config.json")
|
||||
self.pages = load("pages.config.json")
|
||||
self.api = load("api.config.json")
|
||||
self.data = load("data-fields.config.json")
|
||||
|
||||
def test_features_are_unique_and_use_declared_roles(self) -> None:
|
||||
roles = set(self.features["roles"])
|
||||
items = self.features["features"]
|
||||
ids = [item["id"] for item in items]
|
||||
self.assertEqual(len(ids), len(set(ids)))
|
||||
self.assertTrue(all(item["access"] in roles for item in items))
|
||||
self.assertTrue(all(isinstance(item["enabled"], bool) for item in items))
|
||||
|
||||
def test_page_registry_matches_the_current_primary_navigation(self) -> None:
|
||||
inventory_pages = build_architecture_inventory()["pages"]
|
||||
registered = self.pages["pages"]
|
||||
self.assertEqual(
|
||||
[(item["id"], item["title"]) for item in registered],
|
||||
[(item["id"], item["title"]) for item in inventory_pages],
|
||||
)
|
||||
feature_ids = {item["id"] for item in self.features["features"]}
|
||||
self.assertTrue(all(page["feature"] in feature_ids for page in registered))
|
||||
self.assertEqual(sum(bool(page.get("default")) for page in registered), 1)
|
||||
|
||||
def test_api_registry_is_current_and_owned(self) -> None:
|
||||
self.assertEqual(self.api, build_api_registry())
|
||||
feature_ids = {item["id"] for item in self.features["features"]}
|
||||
roles = set(self.features["roles"])
|
||||
keys = []
|
||||
for route in self.api["routes"]:
|
||||
keys.append((route["method"], route["path"], route["match"]))
|
||||
self.assertIn(route["feature"], feature_ids)
|
||||
self.assertIn(route["access"], roles)
|
||||
self.assertEqual(len(keys), len(set(keys)))
|
||||
|
||||
def test_public_routes_are_explicitly_limited(self) -> None:
|
||||
public = {
|
||||
(item["method"], item["path"])
|
||||
for item in self.api["routes"]
|
||||
if item["access"] == "public"
|
||||
}
|
||||
self.assertEqual(
|
||||
public,
|
||||
{
|
||||
("GET", "/api/health"),
|
||||
("POST", "/api/auth/login"),
|
||||
("POST", "/api/auth/register"),
|
||||
},
|
||||
)
|
||||
|
||||
def test_calculation_datasets_use_approved_providers(self) -> None:
|
||||
providers = self.data["providers"]
|
||||
ids = []
|
||||
for dataset in self.data["datasets"]:
|
||||
ids.append(dataset["id"])
|
||||
self.assertIn(dataset["primary"], providers)
|
||||
for fallback in dataset.get("fallbacks", []):
|
||||
self.assertIn(fallback, providers)
|
||||
if dataset["usage"] == "calculation":
|
||||
self.assertTrue(providers[dataset["primary"]]["calculation_allowed"])
|
||||
if dataset["primary"] == "unresolved":
|
||||
self.assertEqual(dataset["usage"], "blocked")
|
||||
self.assertEqual(len(ids), len(set(ids)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,107 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUTPUT = ROOT / "config" / "api.config.json"
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
|
||||
def _owner(path: str) -> str:
|
||||
ordered = (
|
||||
("/api/health", "health"),
|
||||
("/api/auth", "auth"),
|
||||
("/api/admin", "admin"),
|
||||
("/api/account", "account"),
|
||||
("/api/sentiment", "sentiment"),
|
||||
("/api/dashboard", "market"),
|
||||
("/api/realtime-aggregate", "market"),
|
||||
("/api/rotation", "rotation"),
|
||||
("/api/auction", "auction"),
|
||||
("/api/themes", "themes"),
|
||||
("/api/popularity", "popularity"),
|
||||
("/api/dragon-tiger", "dragon_tiger"),
|
||||
("/api/search", "search"),
|
||||
("/api/chart", "charts"),
|
||||
("/api/stock", "market"),
|
||||
("/api/screener", "screener"),
|
||||
("/api/mentors", "mentor"),
|
||||
("/api/heaven", "heaven"),
|
||||
("/api/alerts", "alerts"),
|
||||
("/api/trades", "review"),
|
||||
("/api/assistant", "review"),
|
||||
("/api/watchlist", "review"),
|
||||
("/api/notes", "review"),
|
||||
("/api/reasons", "admin"),
|
||||
("/api/seat-aliases", "admin"),
|
||||
("/api/backfill", "admin"),
|
||||
)
|
||||
for prefix, owner in ordered:
|
||||
if path.startswith(prefix):
|
||||
return owner
|
||||
raise ValueError(f"API owner is not registered: {path}")
|
||||
|
||||
|
||||
def _role(method: str, path: str) -> str:
|
||||
if path == "/api/health" or path in {"/api/auth/register", "/api/auth/login"}:
|
||||
return "public"
|
||||
from api_access import required_role
|
||||
|
||||
sample = re.sub(r"\\d\{6\}", "000001", path)
|
||||
sample = re.sub(r"\\d\+", "1", sample)
|
||||
sample = sample.replace("(.+)", "sample").replace("(", "").replace(")", "")
|
||||
return required_role(method, sample)
|
||||
|
||||
|
||||
def build() -> dict:
|
||||
text = (ROOT / "server.py").read_text(encoding="utf-8")
|
||||
method_matches = list(re.finditer(r"^ def do_(GET|POST|DELETE)\(", text, re.MULTILINE))
|
||||
routes = []
|
||||
for index, match in enumerate(method_matches):
|
||||
method = match.group(1)
|
||||
end = method_matches[index + 1].start() if index + 1 < len(method_matches) else len(text)
|
||||
block = text[match.start():end]
|
||||
exact_paths = set(re.findall(r'parsed\.path\s*==\s*"(/api/[^"]+)"', block))
|
||||
patterns = set(
|
||||
re.findall(
|
||||
r're\.(?:fullmatch|match)\(\s*r?["\']([^"\']*?/api/[^"\']+)["\']\s*,\s*parsed\.path',
|
||||
block,
|
||||
)
|
||||
)
|
||||
for path in sorted(exact_paths):
|
||||
routes.append(
|
||||
{"method": method, "path": path, "match": "exact", "feature": _owner(path), "access": _role(method, path)}
|
||||
)
|
||||
for path in sorted(patterns):
|
||||
normalized = path.replace("^", "").replace("$", "")
|
||||
routes.append(
|
||||
{"method": method, "path": normalized, "match": "regex", "feature": _owner(normalized), "access": _role(method, normalized)}
|
||||
)
|
||||
routes.sort(key=lambda item: (item["path"], item["method"], item["match"]))
|
||||
return {"schema_version": 1, "generated_from": "server.py", "routes": routes}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Build the transitional API ownership registry")
|
||||
parser.add_argument("--check", action="store_true")
|
||||
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("API registry is stale; run tools/build_api_registry.py")
|
||||
print("API registry is current.")
|
||||
return 0
|
||||
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUTPUT.write_text(rendered, encoding="utf-8")
|
||||
print(OUTPUT.relative_to(ROOT).as_posix())
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user