From 406118bba696cdc740ce791beb2cbf34b4b0e622 Mon Sep 17 00:00:00 2001 From: leefer Date: Fri, 31 Jul 2026 21:24:37 +0800 Subject: [PATCH] migration: close candidate maintenance audit --- app/ARCHITECTURE.md | 88 +++-- app/README.md | 14 +- app/config/README.md | 17 +- app/config/architecture-inventory.json | 336 ++++++++++++++++++ app/tests/test_maintenance_tools.py | 68 ++++ app/tools/README.md | 36 ++ app/tools/build_architecture_inventory.py | 68 ++-- app/tools/build_preservation_manifest.py | 85 +++-- app/tools/split_frontend_runtime.py | 15 + app/tools/verify_baseline.py | 92 ++++- docs/migration/evidence/slice-11/README.md | 11 +- .../migration/evidence/slice-11/api-diff.json | 16 +- .../evidence/slice-11/completion-audit.md | 72 ++++ .../evidence/slice-11/database-diff.json | 4 +- docs/migration/人工维护与本地切换指南.md | 16 +- docs/migration/保真迁移状态.json | 8 +- docs/migration/保真迁移账本.md | 18 +- 17 files changed, 844 insertions(+), 120 deletions(-) create mode 100644 app/config/architecture-inventory.json create mode 100644 app/tests/test_maintenance_tools.py create mode 100644 app/tools/README.md create mode 100644 docs/migration/evidence/slice-11/completion-audit.md diff --git a/app/ARCHITECTURE.md b/app/ARCHITECTURE.md index ff5170d..789a695 100644 --- a/app/ARCHITECTURE.md +++ b/app/ARCHITECTURE.md @@ -1,40 +1,70 @@ -# Architecture +# Candidate architecture -The normative governance contract is documented in -`docs/governance/architecture-standard.md`. This file describes the currently deployed -shape; the standard defines the target boundaries and the rules applied during migration. +`app/` is the behavior-preserving modular candidate. The original `webapp/` runtime remains +the product and visual baseline until manual acceptance. `next/` is a rejected, frozen +implementation and is not a source for this directory. -The application intentionally keeps a small deployment footprint: one Python process, one -SQLite database, and a build-free browser client. The internal boundaries are nevertheless -explicit so new features do not bypass account isolation or data-quality rules. +The application deliberately remains a modular monolith: one Python process, one SQLite WAL +database, and a build-free HTML/CSS/JavaScript client. The migration changed source ownership +and imports, not the technology stack or observable product behavior. -## Backend boundaries +## Runtime path -- `server.py`: application services and HTTP request/response wiring. -- `api_access.py`: the single authorization policy for authenticated, member, and admin APIs. -- `app_config.py`: runtime paths, local environment loading, and shared input validation. -- `database.py`: SQLite schema, migrations, and persistence operations. -- `tushare_client.py` and `realtime_aggregator.py`: external market-data adapters. -- `sentiment_engine.py`, `screener.py`, and `heaven_engine.py`: deterministic domain logic. -- `mentor_agent.py`, `heaven_agent.py`, and `llm_strategy.py`: bounded LLM adapters. +```text +browser + -> frontend/shared/api.js + -> backend HTTP transport and feature HTTP mixins + -> feature services + -> repositories / DataGateway / LLMGateway + -> SQLite / market providers / model providers -## Data ownership +background scheduler + -> backend/jobs + -> the same feature services and repositories +``` -Public market snapshots, stock factors, built-in strategies, limit-up reasons, seat aliases, -and sector-element mappings are shared. Only administrators can modify shared knowledge. +## Source ownership -Watchlists, review notes, custom strategies, screener runs, mentor conversations, birth data, -alerts, trading journals, and assistant conversations are owned by a user ID and must be -queried with that ID. LLM features additionally require active membership. +- `server.py` is the stable command/import facade. Runtime composition lives in + `backend/application.py` and `backend/bootstrap/`. +- `backend/http/` owns common authentication, request IDs, responses, static delivery, and + error normalization. Feature-specific transport handlers live beside their feature. +- `backend/features//` owns the mechanically moved service, repository, HTTP, agent, + or deterministic calculation code for that product area. +- `backend/data/` owns provider construction, source policy, provenance, units, freshness, + coverage, and display-versus-calculation eligibility. +- `backend/database/` owns connection management, ordered migrations, and narrow repository + adapters. Root `database.py` remains the legacy schema/composition anchor and combines the + feature repository mixins; do not add feature queries to it. +- `backend/jobs/` owns job definitions, locks, retries, idempotency, and persisted run state. +- `backend/llm/` owns model selection, membership/quota checks, fallback, streaming rules, + and call audit. Feature agents only prepare context and provider payloads. +- `frontend/shared/` is the only browser API/state/Shell/component boundary. +- `frontend/pages/` owns page-local behavior. The original runtime was split mechanically; + source markers and preservation tests prove that the pieces reassemble to the audited + original, apart from explicitly registered trial retirements. +- `frontend/styles/`, `frontend/shared/tokens.css`, and the Wentian page stylesheet preserve + the approved cascade and light/dark/mobile behavior. +- `config/` is the versioned registry for pages, features, APIs, datasets, quality rules, + jobs, and the generated candidate architecture inventory. -## Data integrity +Root modules such as `screener.py`, `tushare_client.py`, and `mentor_agent.py` are compatibility +aliases to canonical modules. They contain no second implementation and remain only because +the original public import surface is part of the preservation contract. -Production reads never synthesize market prices. A failed live request may use the latest real -snapshot at or before the requested date. When no real snapshot exists, the API reports that -the data is unavailable. Demo builders remain test fixtures only. +## Non-negotiable maintenance rules -## Change contract +1. Preserve account ownership in every user-private query and test it with two accounts. +2. Browser requests go through `frontend/shared/api.js`; provider calls go through the data + boundary; model calls go through `backend/llm/`. +3. Calculation datasets fail closed when required source, date, unit, freshness, or coverage + evidence is missing. Display fallbacks do not silently enter calculations. +4. Do not implement logic in both a root compatibility module and a canonical module. +5. Do not remove compatibility or uncertain code without reference scanning, old/new + differential evidence, browser checks, and manual acceptance. +6. Run `python tools/verify_baseline.py` for every change and add `--e2e` when runtime or + frontend behavior can be affected. -New endpoints must be added to `api_access.required_role` when they need member or admin -access. New user-owned tables must include `user_id`, an ownership index, and cross-account -tests. API payload compatibility is protected by the Python and Playwright suites. +The authoritative migration constraints and handoff procedure are in +`../docs/migration/原版保真迁移总纲.md` and +`../docs/migration/人工维护与本地切换指南.md`. diff --git a/app/README.md b/app/README.md index eda2e01..82bcdf9 100644 --- a/app/README.md +++ b/app/README.md @@ -2,23 +2,27 @@ 一个面向 A 股盘后复盘的本地 Web 工作台。后端使用 Python 访问 Tushare Pro,前端不依赖构建工具。 +本目录是从原版源码逐项移动、机械拆分并完成差分验证的模块化候选,不是依据规格书重新开发的 +第二套产品。人工验收和正式切换前,`webapp/`根目录仍是唯一正式基线;冻结的`next/`不得用于 +部署或后续开发。目录职责见[ARCHITECTURE.md](ARCHITECTURE.md)。 + 当前包含集合竞价、涨停池、炸板池、跌停板、昨日涨停、涨停表现、市场天梯、板块轮动、题材库、人气热榜、龙虎榜和个人复盘工作区。交易日快照与同步记录保存在本地 SQLite 数据库 `data/review.db`。 集合竞价中心采用盘前生命周期:9:15 前显示预告,9:15–9:25 明确等待最终竞价,9:25–9:30 自动读取并重试最终竞价筛选,9:30 后停止更新并冻结为复盘归档。当前 Tushare 只提供 9:25 最终竞价快照,不将其表述为动态虚拟撮合行情。 第三阶段加入了机构席位、席位别名、个股复权日 K、资金流、自选股、涨停原因修订、个股笔记、每日复盘和历史数据回补。 -股票代码在桌面端悬停后会显示分时与日 K 快速预览,默认优先展示日 K;移动端点击代码后从底部打开预览面板。股票详情以及板块、题材、指数详情均可在日 K 与最新分时之间切换。日 K 复用个股详情缓存;分时使用隔离的东方财富分钟图表源和短时内存缓存,只负责展示,不写入主行情、不参与情绪、选股或问天计算。图表源不可用时界面会明确显示“分时不可用”,不会使用日 K 数据模拟分时走势。 +股票代码在桌面端悬停后会显示分时与日 K 快速预览,默认优先展示日 K;移动端点击代码后从底部打开预览面板。股票详情以及板块、题材、指数详情均可在日 K 与最新分时之间切换。日 K 复用个股详情缓存;分时优先使用 iFinD,东方财富仅作隔离的展示兜底,并使用短时内存缓存。图表数据不写入主行情、不参与情绪、选股或问天计算;不可用时明确显示“分时不可用”,不会用日 K 模拟分时走势。 -智能选股模块包含 45 日全市场因子库、六阶段市场识别、七套内置策略、受控公式 DSL、自然语言策略编译、候选排名和滚动回测。竞价涨幅、竞价成交额、竞价换手率与竞价量比随因子数据一并同步,可用于自定义公式和历史回测。首次使用需在页面点击“同步因子数据”。未配置 LLM 时使用本地策略模板;配置兼容 API 后自动切换为主模型编译,主模型失败时自动使用辅助模型,两者均支持独立连通性测试。 +智能选股包含六阶段盘后候选、29 套精选策略、自定义公式 DSL、自然语言公式编译、候选排名和滚动回测。阶段与精选策略在当日行情更新后由后台确定性计算;自定义选股由用户手动执行,LLM 只负责编译自然语言条件,不参与候选筛选。竞价、估值、财务、资金、人气和席位等字段按已登记的数据可用性进入因子库,缺失时明确显示覆盖问题。 -每次选股结果会自动进入五交易日持续跟踪,展示 T+1 开盘/收盘、T+3、T+5、最大涨幅与最大回撤。提醒中心支持手工日期提醒,并在策略首日反馈和五日跟踪完成时生成账号私有的站内提醒。 +候选只有经用户手动加入后才进入五交易日持续跟踪,展示 T+1 开盘/收盘、T+3、T+5、最大涨幅与最大回撤。提醒中心支持手工日期提醒,并在策略首日反馈和五日跟踪完成时生成账号私有的站内提醒。 问师模块会读取当前复盘、近十日市场情绪、涨跌停、昨日反馈、板块轮动、市场阶段、龙虎榜和指定个股数据,再按选中的游资思维 Skill 进行单师对话。对话记录按账号、老师和交易日期保存在服务端;主模型不可用时自动切换辅助模型。 新增公开问师角色时,在 `游资skills` 下增加一个包含 `SKILL.md` 的独立目录,并在 `游资skills/mentor_catalog.json` 中登记素材等级与结构质检。管理员私有角色放在 `data/private-mentor-skills`,该目录不进入 Git 或 Docker 镜像,且只会出现在管理员的问师列表中。系统会从 Skill 的 frontmatter、一级标题、核心模型和引用语中自动生成角色信息,无需修改注册代码。 -问天模块包含三个相互独立的部分:观势以市场数据生成三才六爻,用于观察“势”,行情缺失或自动取象明显偏差时可显式手动校准六爻,人工结果与自动来源严格区分;观气依据干支、精确节气、五运六气及客主加临关系观察“运”,行业五行仅作传统取象归类;观心通过30秒静心、六次三枚铜钱起卦、察念和解卦完成一次不输入问题的问心仪式。卦象、干支、节气与气机关系均由本地确定性程序计算,LLM只负责解释,不参与起卦或改动结果。 +问天模块包含三个相互独立的部分:观势以市场数据生成三才六爻,用于观察“势”,行情缺失或自动取象明显偏差时可显式手动校准六爻,人工结果与自动来源严格区分;观气依据干支、精确节气、五运六气及客主加临关系观察“运”,行业五行仅作传统取象归类;观心先准备1秒,再完成5轮“吸3秒、顿2秒、呼4秒”,随后以六次三枚铜钱起卦、察念和解卦完成一次不输入问题的问心仪式。卦象、干支、节气与气机关系均由本地确定性程序计算,LLM只负责解释,不参与起卦或改动结果。 问天模块使用项目本地的 `lunar-python` 计算历法,并使用 `data/iching_zh.json` 中的固定六十四卦、卦辞和爻辞。第三方授权见 `THIRD_PARTY_NOTICES.md`。 @@ -27,7 +31,7 @@ ## 启动 ```powershell -cd webapp +cd webapp\app python -m pip install -r requirements.txt python server.py ``` diff --git a/app/config/README.md b/app/config/README.md index cc512e0..f27497a 100644 --- a/app/config/README.md +++ b/app/config/README.md @@ -1,12 +1,15 @@ # Governance Registries -These registries describe the approved product surface during architecture migration. +These registries describe the approved product surface of the modular preservation candidate. - `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. +- `api.config.json`: current routes generated from the preserved `server.py` API surface, with + one feature owner and backend access class per route. Its dispatcher implementation lives in + `backend/application.py`. +- `architecture-inventory.json`: generated inventory of candidate pages, routes, tables, + providers, model entry points, CSS layers, and remaining code hotspots. - `data-fields.config.json`: canonical data products, provider eligibility, intended use, and known blocked datasets. - `data-quality.config.json`: freshness, coverage, units, adjustment, point-in-time, and @@ -14,13 +17,15 @@ These registries describe the approved product surface during architecture migra - `jobs.config.json`: background schedules, dependencies, lock keys, retry policy, timeouts, and output versions. -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. +The registries are governance contracts, not substitutes for runtime authorization. Backend +access in `backend/http/routes.py` is authoritative; frontend visibility is only a presentation +concern and never grants access. Regenerate the transitional API inventory after a route change: ```shell python tools/build_api_registry.py python tools/build_api_registry.py --check +python tools/build_architecture_inventory.py +python tools/build_architecture_inventory.py --check ``` diff --git a/app/config/architecture-inventory.json b/app/config/architecture-inventory.json new file mode 100644 index 0000000..6577d3d --- /dev/null +++ b/app/config/architecture-inventory.json @@ -0,0 +1,336 @@ +{ + "schema_version": 1, + "captured_from": "app modular preservation candidate", + "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": 36 + }, + "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", + "job_runs", + "schema_migrations" + ], + "background_job_methods": [ + "_background_refresh_tick" + ], + "external_data_adapters": [ + { + "provider": "tushare", + "path": "backend/data/providers/tushare_client.py", + "runtime_role": "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" + } + ], + "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" + } + ], + "css_layers": [ + "/shared/tokens.css?v=20260729-1", + "/styles/styles.css", + "/styles/renovation.css?v=20260725-5", + "/styles/redesign-v2.css?v=20260728-1", + "/styles/design-system.css?v=20260728-4", + "/styles/theme.css?v=20260728-2", + "/pages/heaven/page.css?v=20260728-7" + ], + "code_hotspots": [ + { + "path": "frontend/styles/styles.css", + "bytes": 361780, + "lines": 15465 + }, + { + "path": "frontend/styles/redesign-v2.css", + "bytes": 263539, + "lines": 8570 + }, + { + "path": "frontend/index.html", + "bytes": 135019, + "lines": 1892 + }, + { + "path": "backend/features/screener/engine.py", + "bytes": 108552, + "lines": 2213 + }, + { + "path": "backend/data/providers/tushare_client.py", + "bytes": 94329, + "lines": 2175 + }, + { + "path": "frontend/app.js", + "bytes": 91151, + "lines": 1939 + }, + { + "path": "frontend/pages/heaven/page.js", + "bytes": 88322, + "lines": 1830 + }, + { + "path": "frontend/styles/renovation.css", + "bytes": 83949, + "lines": 1553 + }, + { + "path": "frontend/pages/heaven/page.css", + "bytes": 73222, + "lines": 1084 + }, + { + "path": "backend/features/heaven/service.py", + "bytes": 64421, + "lines": 1303 + }, + { + "path": "backend/features/market/insights.py", + "bytes": 58150, + "lines": 1314 + }, + { + "path": "frontend/pages/market/runtime.js", + "bytes": 57053, + "lines": 1333 + }, + { + "path": "backend/features/heaven/engine.py", + "bytes": 51670, + "lines": 1181 + }, + { + "path": "backend/application.py", + "bytes": 50934, + "lines": 1165 + }, + { + "path": "frontend/styles/theme.css", + "bytes": 36427, + "lines": 1253 + }, + { + "path": "database.py", + "bytes": 34013, + "lines": 746 + } + ] +} diff --git a/app/tests/test_maintenance_tools.py b/app/tests/test_maintenance_tools.py new file mode 100644 index 0000000..169ced0 --- /dev/null +++ b/app/tests/test_maintenance_tools.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import json +import subprocess +import sys +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] + + +class MaintenanceToolTests(unittest.TestCase): + def test_generated_candidate_registries_are_current(self) -> None: + expected_api = json.loads( + (ROOT / "config" / "api.config.json").read_text(encoding="utf-8") + ) + expected_architecture = json.loads( + (ROOT / "config" / "architecture-inventory.json").read_text( + encoding="utf-8" + ) + ) + self.assertEqual(expected_api, build_api_registry()) + self.assertEqual(expected_architecture, build_architecture_inventory()) + + def test_baseline_verifier_uses_the_candidate_frontend(self) -> None: + source = (ROOT / "tools" / "verify_baseline.py").read_text(encoding="utf-8") + self.assertIn('FRONTEND_ROOT = ROOT / "frontend"', source) + self.assertIn('FRONTEND_ROOT.rglob("*.js")', source) + self.assertIn("start_e2e_server()", source) + self.assertIn("stop_e2e_server(server)", source) + self.assertNotIn('"static/app.js"', source) + + def test_every_tool_has_a_non_mutating_help_path(self) -> None: + for path in sorted((ROOT / "tools").glob("*.py")): + if path.name.startswith("_"): + continue + with self.subTest(tool=path.name): + result = subprocess.run( + [sys.executable, str(path), "--help"], + cwd=ROOT, + capture_output=True, + text=True, + timeout=15, + check=False, + ) + self.assertEqual(result.returncode, 0, result.stderr) + + def test_migration_only_tools_are_explicitly_classified(self) -> None: + readme = (ROOT / "tools" / "README.md").read_text(encoding="utf-8") + for name in ( + "build_preservation_manifest.py", + "move_class_methods.py", + "split_frontend_runtime.py", + ): + self.assertIn(name, readme) + manifest = (ROOT / "tools" / "build_preservation_manifest.py").read_text( + encoding="utf-8" + ) + self.assertNotIn('TARGET = ROOT / "app"', manifest) + self.assertIn('required=True', manifest) + + +if __name__ == "__main__": + unittest.main() diff --git a/app/tools/README.md b/app/tools/README.md new file mode 100644 index 0000000..6f4462e --- /dev/null +++ b/app/tools/README.md @@ -0,0 +1,36 @@ +# Candidate tools + +Run these commands from `webapp/app/`. The tools are separated by responsibility so a +maintenance command cannot be mistaken for a historical migration rewrite. + +## Normal maintenance + +- `python tools/verify_baseline.py`: candidate unit tests, registry checks, every frontend + JavaScript syntax check, Git whitespace check, and read-only SQLite integrity check. +- `python tools/verify_baseline.py --e2e`: the same checks plus Playwright. The verifier owns + the local static-server lifecycle so the command exits cleanly on Windows. +- `python tools/build_api_registry.py [--check]`: generate or verify + `config/api.config.json` from `backend/application.py`. +- `python tools/build_architecture_inventory.py [--check]`: generate or verify + `config/architecture-inventory.json` from the candidate source tree. + +## Acceptance and differential checks + +- `run_preservation_runtime.py`: start an isolated original or candidate runtime with an + explicitly selected data directory and port. +- `compare_preservation_apis.py`: compare authenticated responses from two isolated runtimes. +- `compare_preservation_databases.py`: compare schema and selected table contents from two + SQLite copies. + +These tools require explicit paths and do not select the production database automatically. + +## Migration-only tools + +- `build_preservation_manifest.py`: builds an exact-copy manifest for a specified source and + target. `--output` is mandatory so committed historical evidence is not overwritten. +- `move_class_methods.py`: mechanically moves named class methods between explicit files. +- `split_frontend_runtime.py`: reproduces the one-time Slice 10 split. It refuses to write + unless `--apply` is supplied and is not a normal maintenance command. + +The migration-only tools are retained for audit and reproducibility. They are not part of +application startup, normal testing, or future feature development. diff --git a/app/tools/build_architecture_inventory.py b/app/tools/build_architecture_inventory.py index 3ead445..bccec7f 100644 --- a/app/tools/build_architecture_inventory.py +++ b/app/tools/build_architecture_inventory.py @@ -9,7 +9,7 @@ from typing import Any ROOT = Path(__file__).resolve().parents[1] -OUTPUT = ROOT / "docs" / "governance" / "architecture-inventory.json" +OUTPUT = ROOT / "config" / "architecture-inventory.json" def relative(path: Path) -> str: @@ -51,8 +51,15 @@ def api_inventory(server: str) -> dict[str, list[str]]: 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 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]: @@ -70,11 +77,13 @@ def css_layers(html: str) -> list[str]: def code_hotspots() -> list[dict[str, Any]]: candidates = [ - "server.py", + "backend/application.py", "database.py", - "screener.py", - "market_insights.py", - "tushare_client.py", + "backend/features/screener/engine.py", + "backend/features/market/insights.py", + "backend/data/providers/tushare_client.py", + "backend/features/heaven/service.py", + "backend/features/heaven/engine.py", "frontend/index.html", "frontend/app.js", "frontend/styles/styles.css", @@ -88,6 +97,8 @@ def code_hotspots() -> list[dict[str, Any]]: rows = [] for name in candidates: path = ROOT / name + if not path.is_file(): + continue rows.append( { "path": name, @@ -100,14 +111,21 @@ def code_hotspots() -> list[dict[str, Any]]: def build() -> dict[str, Any]: html = source("frontend/index.html") - server = source("server.py") - database = source("database.py") + server = source("backend/application.py") + 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) + tables = database_inventory(database_sources) return { "schema_version": 1, - "captured_from": "governed source tree", + "captured_from": "app modular preservation candidate", "runtime": { "http_server": "http.server.ThreadingHTTPServer", "application_processes": 1, @@ -126,21 +144,22 @@ def build() -> dict[str, Any]: "api": api, "database_tables": tables, "background_job_methods": python_functions( - "server.py", ("_background", "_run_background", "_schedule_", "run_automatic") + "backend/application.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"}, + {"provider": "tushare", "path": "backend/data/providers/tushare_client.py", "runtime_role": "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"}, ], "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"}, + {"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"}, ], "css_layers": css_layers(html), "code_hotspots": code_hotspots(), @@ -154,7 +173,10 @@ def main() -> int: 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") + 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) diff --git a/app/tools/build_preservation_manifest.py b/app/tools/build_preservation_manifest.py index aa12c9a..dae5bea 100644 --- a/app/tools/build_preservation_manifest.py +++ b/app/tools/build_preservation_manifest.py @@ -1,14 +1,13 @@ from __future__ import annotations +import argparse import hashlib import json from datetime import datetime from pathlib import Path -ROOT = Path(__file__).resolve().parents[1] -TARGET = ROOT / "app" -OUTPUT = ROOT / "docs" / "migration" / "原版资产清单.json" +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] DIRECTORIES = ( "backend", @@ -69,33 +68,57 @@ def digest(path: Path) -> str: return checksum.hexdigest() -def source_files() -> list[Path]: - files = [ROOT / name for name in ROOT_FILES] +def display_path(path: Path, base: Path) -> str: + try: + return path.relative_to(base).as_posix() or "." + except ValueError: + return path.as_posix() + + +def source_files(source_root: Path) -> list[Path]: + files = [source_root / name for name in ROOT_FILES] for directory in DIRECTORIES: + root = source_root / directory + if not root.is_dir(): + continue files.extend( path - for path in (ROOT / directory).rglob("*") + for path in root.rglob("*") if path.is_file() and "__pycache__" not in path.parts ) - return sorted(set(files), key=lambda path: path.relative_to(ROOT).as_posix()) + return sorted( + set(files), key=lambda path: path.relative_to(source_root).as_posix() + ) -def build_manifest() -> dict[str, object]: +def build_manifest( + source_root: Path, + target_root: Path, + source_commit: str, +) -> dict[str, object]: assets = [] mismatches = [] - for source in source_files(): - relative = source.relative_to(ROOT) - target = TARGET / relative - source_hash = digest(source) + for source in source_files(source_root): + relative = source.relative_to(source_root) + target = target_root / relative + source_exists = source.is_file() + source_hash = digest(source) if source_exists else "" target_hash = digest(target) if target.is_file() else "" - status = "identical" if source_hash == target_hash else "mismatch" + if not source_exists: + status = "missing_source" + elif not target.is_file(): + status = "missing_target" + elif source_hash == target_hash: + status = "identical" + else: + status = "mismatch" if status != "identical": mismatches.append(relative.as_posix()) assets.append( { "source": relative.as_posix(), - "target": f"app/{relative.as_posix()}", - "bytes": source.stat().st_size, + "target": display_path(target, REPOSITORY_ROOT), + "bytes": source.stat().st_size if source_exists else 0, "sha256": source_hash, "disposition": "original_copy_pending_move", "status": status, @@ -104,9 +127,9 @@ def build_manifest() -> dict[str, object]: return { "schema_version": 1, "generated_at": datetime.now().astimezone().isoformat(timespec="seconds"), - "source_commit": "41329943c4878fc09ed82ec376eb93ab151e4092", - "source_root": ".", - "target_root": "app", + "source_commit": source_commit, + "source_root": display_path(source_root, REPOSITORY_ROOT), + "target_root": display_path(target_root, REPOSITORY_ROOT), "excluded": [ "next/", "data/review.db and SQLite sidecars", @@ -123,14 +146,34 @@ def build_manifest() -> dict[str, object]: def main() -> int: - manifest = build_manifest() - OUTPUT.write_text( + parser = argparse.ArgumentParser( + description=( + "Build an exact-copy manifest for a preservation migration stage. " + "This is a migration-only tool; it does not describe the final moved layout." + ) + ) + parser.add_argument("--source-root", type=Path, default=REPOSITORY_ROOT) + parser.add_argument("--target-root", type=Path, default=REPOSITORY_ROOT / "app") + parser.add_argument("--source-commit", default="") + parser.add_argument( + "--output", + type=Path, + required=True, + help="write to a new audit path; do not overwrite committed slice evidence", + ) + args = parser.parse_args() + source_root = args.source_root.resolve() + target_root = args.target_root.resolve() + output = args.output.resolve() + manifest = build_manifest(source_root, target_root, args.source_commit) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text( json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) print( f"assets={manifest['asset_count']} mismatches={manifest['mismatch_count']} " - f"output={OUTPUT}" + f"output={output}" ) return 1 if manifest["mismatch_count"] else 0 diff --git a/app/tools/split_frontend_runtime.py b/app/tools/split_frontend_runtime.py index e61e199..41a32d6 100644 --- a/app/tools/split_frontend_runtime.py +++ b/app/tools/split_frontend_runtime.py @@ -1,5 +1,6 @@ from __future__ import annotations +import argparse import hashlib import json import re @@ -91,6 +92,20 @@ def extract_written_chunks(paths: list[Path]) -> dict[tuple[int, int], str]: def main() -> None: + parser = argparse.ArgumentParser( + description="Reproduce the one-time Slice 10 frontend source split" + ) + parser.add_argument( + "--apply", + action="store_true", + help="perform the historical split; this rewrites frontend runtime files", + ) + args = parser.parse_args() + if not args.apply: + parser.error( + "this migration-only tool rewrites files; pass --apply only when " + "reproducing Slice 10 from its documented pre-split checkpoint" + ) source_path = ORIGINAL_STATIC / "app.js" target_path = FRONTEND_ROOT / "app.js" source_bytes = source_path.read_bytes() diff --git a/app/tools/verify_baseline.py b/app/tools/verify_baseline.py index f788c82..9bf656c 100644 --- a/app/tools/verify_baseline.py +++ b/app/tools/verify_baseline.py @@ -5,10 +5,15 @@ import shutil import sqlite3 import subprocess import sys +import time +import urllib.error +import urllib.request from pathlib import Path ROOT = Path(__file__).resolve().parents[1] +FRONTEND_ROOT = ROOT / "frontend" +E2E_URL = "http://127.0.0.1:8876/index.html" def run(label: str, command: list[str]) -> None: @@ -28,8 +33,63 @@ def verify_database() -> None: print(f"\n[database] integrity_check=ok size={database.stat().st_size}") +def url_reachable(url: str) -> bool: + try: + with urllib.request.urlopen(url, timeout=1) as response: + return response.status == 200 + except (OSError, urllib.error.URLError): + return False + + +def start_e2e_server() -> subprocess.Popen[bytes] | None: + if url_reachable(E2E_URL): + print(f"\n[e2e-server] reusing {E2E_URL}") + return None + creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0 + process = subprocess.Popen( + [ + sys.executable, + "-m", + "http.server", + "8876", + "--bind", + "127.0.0.1", + "--directory", + "frontend", + ], + cwd=ROOT, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + creationflags=creation_flags, + ) + deadline = time.monotonic() + 15 + while time.monotonic() < deadline: + if process.poll() is not None: + raise RuntimeError("Playwright static server exited before becoming ready") + if url_reachable(E2E_URL): + print(f"\n[e2e-server] started {E2E_URL}") + return process + time.sleep(0.1) + stop_e2e_server(process) + raise RuntimeError("Playwright static server did not become ready within 15 seconds") + + +def stop_e2e_server(process: subprocess.Popen[bytes] | None) -> None: + if process is None: + return + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=5) + print("\n[e2e-server] stopped") + + def main() -> int: - parser = argparse.ArgumentParser(description="Verify the governance regression baseline") + parser = argparse.ArgumentParser( + description="Verify the modular preservation candidate" + ) parser.add_argument( "--e2e", action="store_true", @@ -38,19 +98,37 @@ def main() -> int: args = parser.parse_args() run("python", [sys.executable, "-m", "unittest", "discover", "-s", "tests"]) + run( + "api-registry", + [sys.executable, "tools/build_api_registry.py", "--check"], + ) + run( + "architecture-inventory", + [sys.executable, "tools/build_architecture_inventory.py", "--check"], + ) node = shutil.which("node") if not node: raise RuntimeError("node is required for JavaScript syntax checks") - for script in ("static/app.js", "static/heaven-loading-v2.js"): - run("javascript", [node, "--check", script]) + scripts = sorted(FRONTEND_ROOT.rglob("*.js")) + if not scripts: + raise RuntimeError(f"no JavaScript files found under {FRONTEND_ROOT}") + for script in scripts: + run( + "javascript", + [node, "--check", script.relative_to(ROOT).as_posix()], + ) run("patch", ["git", "diff", "--check"]) verify_database() if args.e2e: - npm = shutil.which("npm.cmd" if sys.platform == "win32" else "npm") - if not npm: - raise RuntimeError("npm is required for the Playwright suite") - run("playwright", [npm, "run", "test:e2e"]) + npx = shutil.which("npx.cmd" if sys.platform == "win32" else "npx") + if not npx: + raise RuntimeError("npx is required for the Playwright suite") + server = start_e2e_server() + try: + run("playwright", [npx, "playwright", "test", "--reporter=dot"]) + finally: + stop_e2e_server(server) print("\nBaseline verification passed.") return 0 diff --git a/docs/migration/evidence/slice-11/README.md b/docs/migration/evidence/slice-11/README.md index 49b6806..5220cef 100644 --- a/docs/migration/evidence/slice-11/README.md +++ b/docs/migration/evidence/slice-11/README.md @@ -2,6 +2,7 @@ > 基线:`dec3cd12365df5e7d4c391369f14c457cf56d7d9` > 候选回档标签:`xiaobai-preservation-slice-11-candidate-20260731` +> 严格审计候选标签:`xiaobai-preservation-slice-11-audit-candidate-20260731` > 当前结论:自动验收完成,等待用户人工验收;尚未执行正式切换、Docker或NAS部署 ## 1. 本切片做了什么 @@ -30,7 +31,7 @@ | 验证 | 结果 | |---|---:| | 原版`python -m unittest discover -s tests -q` | 231项通过 | -| 迁移版`python -m unittest discover -s tests -q` | 298项通过 | +| 迁移版`python -m unittest discover -s tests -q` | 302项通过 | | 历史切片与前端/清理专项复核 | 通过 | | 迁移版JavaScript语法检查 | 24个文件通过 | | `npx.cmd playwright test --reporter=dot` | 45项通过(1.6分钟) | @@ -44,6 +45,12 @@ 完整API和数据库结果分别见`api-diff.json`与`database-diff.json`,二者`all_equal`均为`true`。 +严格完成审计另外验证了候选维护工具本身:API注册表和候选架构清单均可生成并执行 +`--check`;24个前端脚本全部由统一命令发现;迁移期写入工具必须显式指定输出或`--apply`。 +Windows下Playwright托管Python静态服务器会在45项执行后不退出,统一验证器现改为显式启动、 +探活和停止8876;最终同一命令正常退出并明确报告`45 passed`。四项维护工具契约测试使迁移版 +总数从298增加到302。逐项目标矩阵见`completion-audit.md`。 + ## 3. 真实浏览器验收 - 桌面端复核情绪周期、智能选股三个工作区、问天日间/夜间和加载资源;控制台无新增 @@ -67,7 +74,7 @@ ## 5. 回档与最终确认 -切片11提交和标签只代表“候选可验收”,不代表删除已被永久确认。人工验收前可按 +切片11提交和两个候选标签只代表“候选可验收”,不代表删除已被永久确认。人工验收前可按 `uncertain-code-audit.md`从切片10标签单独恢复任一候选;不需要回退其他已通过迁移切片。 用户人工确认后才允许: diff --git a/docs/migration/evidence/slice-11/api-diff.json b/docs/migration/evidence/slice-11/api-diff.json index bb63b88..9115b6e 100644 --- a/docs/migration/evidence/slice-11/api-diff.json +++ b/docs/migration/evidence/slice-11/api-diff.json @@ -18,8 +18,8 @@ "endpoint": "/api/account/status", "original_status": 200, "migrated_status": 200, - "original_sha256": "094530da01cbc633de4e2a09d1b3a58689ed9743d2ab2207c495e6c44781fc1e", - "migrated_sha256": "094530da01cbc633de4e2a09d1b3a58689ed9743d2ab2207c495e6c44781fc1e", + "original_sha256": "0ac1b88affa62671f14ba9ea0bd42155952fdc2e03dba3573c232bb452733bc6", + "migrated_sha256": "0ac1b88affa62671f14ba9ea0bd42155952fdc2e03dba3573c232bb452733bc6", "equal": true, "first_difference": null }, @@ -62,8 +62,8 @@ "endpoint": "/api/auction?trade_date=2026-07-30", "original_status": 200, "migrated_status": 200, - "original_sha256": "5a6b643ab1d7e2285ae5667bca12e6ed887ba4aa7512ba989e0dcfb2032e755b", - "migrated_sha256": "5a6b643ab1d7e2285ae5667bca12e6ed887ba4aa7512ba989e0dcfb2032e755b", + "original_sha256": "bd75b100485c0afa0cdddc73d1880160eea8c0b8c91f556245c665d591285ed6", + "migrated_sha256": "bd75b100485c0afa0cdddc73d1880160eea8c0b8c91f556245c665d591285ed6", "equal": true, "first_difference": null }, @@ -95,8 +95,8 @@ "endpoint": "/api/dragon-tiger?trade_date=2026-07-30", "original_status": 200, "migrated_status": 200, - "original_sha256": "9d19029f655813ed40a25deb0b3250423a40ae0ee779638ec90f85333f859c6f", - "migrated_sha256": "9d19029f655813ed40a25deb0b3250423a40ae0ee779638ec90f85333f859c6f", + "original_sha256": "1d6936e33aea2bb4258d1d4d007385626d0c638fc290b66271f00649dba59358", + "migrated_sha256": "1d6936e33aea2bb4258d1d4d007385626d0c638fc290b66271f00649dba59358", "equal": true, "first_difference": null }, @@ -216,8 +216,8 @@ "endpoint": "/api/alerts?status=all&as_of=2026-07-30", "original_status": 200, "migrated_status": 200, - "original_sha256": "01e8f169fbc8700313dbdf8379e682b78d7b372cfbee556e1d68f46cd6cbeecf", - "migrated_sha256": "01e8f169fbc8700313dbdf8379e682b78d7b372cfbee556e1d68f46cd6cbeecf", + "original_sha256": "b611e936ada5d547c728e33e811630282d1eecd7c1ab1517e22b34902eda032a", + "migrated_sha256": "b611e936ada5d547c728e33e811630282d1eecd7c1ab1517e22b34902eda032a", "equal": true, "first_difference": null }, diff --git a/docs/migration/evidence/slice-11/completion-audit.md b/docs/migration/evidence/slice-11/completion-audit.md new file mode 100644 index 0000000..8db1a6e --- /dev/null +++ b/docs/migration/evidence/slice-11/completion-audit.md @@ -0,0 +1,72 @@ +# 保真迁移完成度逐项审计 + +> 审计对象:`webapp/app/` +> 审计基线:`xiaobai-preservation-slice-11-candidate-20260731` +> 结论口径:自动证据通过不替代用户人工验收,也不授权部署切换 + +本审计回答的不是“测试是否为绿色”,而是迁移总纲、批准目录、产品表面和人工维护目标是否 +分别有可复验的证据。状态只使用:`自动闭环`、`人工待验`、`明确保留`和`未执行`。 + +## 1. 目标与证据矩阵 + +| 要求 | 当前实现 | 主要证据 | 状态 | +|---|---|---|---| +| 原版是唯一产品/视觉基线 | 所有切片从根目录原版复制或机械移动;规格书不覆盖真实行为 | 总纲;切片00-11源码映射 | 自动闭环 | +| `next/`完全隔离 | `next/`未进入候选导入、资产、测试或部署路径;切片10后Git差分为0 | `next失败冻结记录.md`;Git路径审计 | 自动闭环 | +| 批准的模块化单体目录 | `backend/{bootstrap,http,features,data,database,jobs,llm}`、`frontend/{shared,pages,styles,vendor}`、`config/data/tests/tools`均已归位 | `app/config/architecture-inventory.json`;切片01-10 | 自动闭环 | +| 不制造第二套产品实现 | 根入口是兼容外壳;业务方法从原类机械移动,专项测试逐符号比较AST/哈希 | 切片01-10 preservation tests | 自动闭环 | +| 16个主工作区及内部能力完整 | 页面注册表有16个主工作区;策略跟踪、搜索、详情、提醒、账户和系统管理保留 | `pages.config.json`;前端边界测试;Playwright | 自动闭环 | +| API路径、权限、错误语义不变 | 53个精确路径和11个正则路径有唯一所有者;鉴权在后端执行 | `api.config.json`;HTTP/权限测试;21端点差分 | 自动闭环 | +| 数据源有统一边界和质量规则 | 18个数据集登记来源、用途、单位、新鲜度、覆盖率、复权和失败关闭规则 | data registries;DataGateway/quality tests | 自动闭环 | +| 数据库无损、用户隔离不变 | 36张表由初始schema和有序migration管理;62个schema对象、21张关键表差分相同 | 数据库差分;migration/repository/account-boundary tests | 自动闭环 | +| 后台任务可追踪和重试 | 3个任务登记调度、锁、超时、重试和输出版本;运行状态持久化 | `jobs.config.json`;job runner tests | 自动闭环 | +| LLM唯一治理边界 | 问师、问天、复盘助手和策略编译经统一网关执行会员、额度、模型回退、流式和审计规则 | 切片07/09;LLM gateway/stream tests | 自动闭环 | +| 前端唯一请求出口、Shell和页面职责 | 只有`frontend/shared/api.js`调用`fetch`;Shell、状态、弹窗、页面生命周期及页面模块已归位 | frontend boundary tests;切片10源码重组哈希 | 自动闭环 | +| CSS、主题、动画和移动行为不改写 | 原七层CSS和问天动画按字节/源码移动;令牌层与加载顺序受测试保护 | CSS governance;切片10/11浏览器证据 | 自动闭环 | +| 不确定代码先记录再试删 | 2个文件和5个无消费者函数仅为候选试删;历史表兼容责任继续保留 | `uncertain-code-audit.md`;cleanup contract | 人工待验 | +| 可由人工维护者复验 | 候选README/架构说明、工具分类、API/架构生成器、统一验证命令均可从`app/`独立运行 | `app/tools/README.md`;maintenance tool tests | 自动闭环 | +| 正式数据库与部署不受影响 | 验收只使用隔离SQLite副本和非8765端口 | 各切片README;运行记录 | 自动闭环 | +| Docker/NAS和正式入口切换 | 用户已明确本轮不做NAS Docker测试;人工验收前禁止切换 | 迁移状态和切换指南 | 未执行 | + +## 2. 结构减法的可量化结果 + +- 根`server.py`从原版约5,851行降为25行稳定入口;实现归入启动、HTTP和产品领域模块。 +- 根`database.py`从原版2,839行降为746行,只保留初始schema、组合入口及有明确历史兼容责任的 + 方法;各领域查询已移入对应Repository。 +- 20个其他根Python兼容模块均为1-15行导出/模块别名,不含第二套实现。 +- 原9,283行前端运行时按原连续源码范围拆入共享层和13个页面目录;试删后仍由源码重组测试 + 保护,不能悄悄增加、丢失或重排原行为。 +- 已确认没有消费者的`demo_data.py`、旧问天加载文件和5个函数进入可单项恢复的试删候选; + `wencai_saved_queries`因旧数据库兼容和账号隔离继续保留。 + +大文件不等于自动可删。筛选引擎、Tushare适配器、问天确定性计算和原CSS仍是单一有效实现; +在没有更细的同输入差分与人工视觉确认前继续拆分,反而违反“整理原件而非重拍”的迁移约束。 +它们已登记在`config/architecture-inventory.json`的`code_hotspots`,供后续正常维护按领域处理。 + +## 3. 本次严格审计发现并关闭的缺口 + +候选版初次自动验收后,三个维护工具仍带有旧目录假设:基线验证器引用不存在的`static/`, +架构清单写向不存在的候选文档目录,原样清单生成器假设`app/app`且会因试删文件直接异常。 +这些问题不改变产品页面,却会让接管者无法按指南复验,因此不能视为可维护性完成。 + +关闭方式: + +1. 基线验证器改为检查候选全量测试、两个生成注册表、`frontend/`下全部JavaScript、Git差异和 + 测试数据库完整性。 +2. 候选架构清单固定生成到`config/architecture-inventory.json`,从正式候选路径统计16页、 + 64类路由匹配、36张表、供应商/LLM入口、CSS层和热点文件。 +3. 原样资产清单、方法搬运和前端拆分明确标为迁移期工具;可能写文件的操作必须显式传路径或 + `--apply`,默认帮助路径不改文件。 +4. 新增维护工具契约测试,保证生成文件新鲜、所有工具`--help`可用,旧`static/`路径不会回归。 + +## 4. 仍需用户完成的最终裁决 + +自动部分完成后仍不能执行以下动作: + +1. 把切片11的试删候选从“待人工确认”改成“确认废弃”。 +2. 声称所有页面的视觉、交互手感和动画已经由用户确认等价。 +3. 建立最终完成标签,或把`app/`切换为本地/容器正式入口。 +4. 删除根目录原版、正式数据库备份或冻结的失败记录。 + +本地人工验收入口及逐页清单以`人工维护与本地切换指南.md`为准。发现差异时只回退对应候选, +不使用破坏性Git操作覆盖原版或正式数据。 diff --git a/docs/migration/evidence/slice-11/database-diff.json b/docs/migration/evidence/slice-11/database-diff.json index df31844..a65ce9a 100644 --- a/docs/migration/evidence/slice-11/database-diff.json +++ b/docs/migration/evidence/slice-11/database-diff.json @@ -56,8 +56,8 @@ "table": "alerts", "original_count": 2, "migrated_count": 2, - "original_sha256": "a496857f915d8ba0870274455182a2d7f180a931836b7078e9676dc0e7bbe430", - "migrated_sha256": "a496857f915d8ba0870274455182a2d7f180a931836b7078e9676dc0e7bbe430", + "original_sha256": "3fd3b7ceb0950d543fba9cebdd2b4294fe187b37a6b8d4755a49428661b3ae1c", + "migrated_sha256": "3fd3b7ceb0950d543fba9cebdd2b4294fe187b37a6b8d4755a49428661b3ae1c", "equal": true, "excluded_columns": [] }, diff --git a/docs/migration/人工维护与本地切换指南.md b/docs/migration/人工维护与本地切换指南.md index fc2563c..551c7c9 100644 --- a/docs/migration/人工维护与本地切换指南.md +++ b/docs/migration/人工维护与本地切换指南.md @@ -77,19 +77,13 @@ python -m unittest discover -s tests -q ```powershell cd webapp\app -python -m unittest discover -s tests -q -npx.cmd playwright test --reporter=dot +python tools\verify_baseline.py +python tools\verify_baseline.py --e2e ``` -JavaScript和Git差异: - -```powershell -cd webapp\app -$files = Get-ChildItem frontend -Recurse -Filter *.js -foreach ($file in $files) { node --check $file.FullName } -cd .. -git diff --check -``` +第一条命令已经包含迁移版全量单元测试、API/架构注册表新鲜度、全部前端JavaScript语法、 +Git空白错误和测试数据库只读完整性检查。第二条额外运行Playwright。工具的日常/验收/迁移期 +分类见`app/tools/README.md`;迁移期工具不是正常开发命令。 Playwright需要能够启动本机无头Edge。若测试停在浏览器启动前且没有`msedge`进程,先检查执行 环境是否禁止GUI/无头浏览器进程;这不是页面失败,不要通过删除测试或延长产品超时绕过。 diff --git a/docs/migration/保真迁移状态.json b/docs/migration/保真迁移状态.json index 0ee7eea..fc2be25 100644 --- a/docs/migration/保真迁移状态.json +++ b/docs/migration/保真迁移状态.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "updated_at": "2026-07-31T16:10:00+08:00", + "updated_at": "2026-07-31T20:57:42+08:00", "status": "awaiting_manual_acceptance", "migration_mode": "behavior_preserving_source_migration", "source_of_truth": "current_original_webapp_runtime_and_source", @@ -9,10 +9,10 @@ "failed_roots": [ "next" ], - "current_slice": "slice-11-uncertain-code-audit-final-acceptance-handoff", + "current_slice": "slice-11-strict-completion-audit-maintenance-handoff", "last_completed_slice": "slice-10-frontend-shell-pages-components-css-mobile", - "last_automated_slice": "slice-11-uncertain-code-audit-final-acceptance-handoff", - "last_checkpoint": "xiaobai-preservation-slice-11-candidate-20260731", + "last_automated_slice": "slice-11-strict-completion-audit-maintenance-handoff", + "last_checkpoint": "xiaobai-preservation-slice-11-audit-candidate-20260731", "next_action": "user_acceptance_on_local_port_8797_then_confirm_or_restore_each_trial_retirement; do_not_switch_docker_or_nas_before_approval", "authoritative_documents": [ "AGENTS.md", diff --git a/docs/migration/保真迁移账本.md b/docs/migration/保真迁移账本.md index 7be5580..bf46442 100644 --- a/docs/migration/保真迁移账本.md +++ b/docs/migration/保真迁移账本.md @@ -1,6 +1,6 @@ # 小白复盘保真迁移账本 -> 当前状态:切片11自动验收完成,等待用户人工验收;正式切换尚未执行 +> 当前状态:切片11严格完成审计通过,等待用户人工验收;正式切换尚未执行 本账本是上下文恢复和人工审计的连续记录。任何迁移提交必须在同一提交中更新本文件及 `保真迁移状态.json`。 @@ -31,6 +31,7 @@ | 2026-07-31 | `xiaobai-preservation-slice-09-20260731` | 复盘、自选、交易日志、提醒与复盘助手原实现归位 | 自动、API、数据库、账户隔离与浏览器差分通过,进入切片10 | | 2026-07-31 | `xiaobai-preservation-slice-10-20260731` | 前端Shell、页面、CSS、动画与移动端职责归位 | 源码重组、自动、API、数据库、桌面、夜间、移动端与动画差分通过,进入切片11 | | 2026-07-31 | `xiaobai-preservation-slice-11-candidate-20260731` | 不确定代码审计、全量验收与人工交接准备 | 自动、API、数据库和浏览器验收通过;等待人工确认,未切换部署 | +| 2026-07-31 | `xiaobai-preservation-slice-11-audit-candidate-20260731` | 逐项目标审计并修复候选维护工具旧路径 | 302项测试、24个脚本、45项Playwright、21项API与62项schema差分通过;仍等待人工确认 | ## 资产处置登记 @@ -197,12 +198,25 @@ - 处置:前3类进入可单项回档的候选试删;`wencai_saved_queries`表和方法因旧库兼容与用户隔离继续保留。 - 保护方式:全部历史前端源码测试统一使用审计白名单比较,只允许登记的原`app.js`行号缺失,其他差异仍会失败。 - API与数据库:21个全站只读API全部一致;62个schema对象和21张关键表逐行一致。 -- 验收:原版231项、迁移版298项Python测试、24个JavaScript语法检查和45项Playwright(1.6分钟)通过;桌面日间/夜间、390×844五个移动入口和问天三页真实检查通过。 +- 验收:原版231项、迁移版302项Python测试、24个JavaScript语法检查和45项Playwright通过;桌面日间/夜间、390×844五个移动入口和问天三页真实检查通过。 - 部署边界:未改正式数据库、`.env`、`8765`、Docker或NAS;原版仍是正式运行基线。 - 候选回档:标签`xiaobai-preservation-slice-11-candidate-20260731`;试删最终结论仍等待用户人工确认。 - 完整证据:`docs/migration/evidence/slice-11/README.md`。 - 维护交接:`docs/migration/人工维护与本地切换指南.md`。 +严格完成审计补充: + +- 发现候选`verify_baseline.py`、架构清单和原样资产清单仍带有旧`static/`、候选`docs/`及 + `app/app`路径假设;产品运行不受影响,但人工接管命令不可用,因此属于迁移目标缺口。 +- 修复后统一验证命令检查302项Python测试、两个生成注册表、24个前端脚本、Git空白、SQLite + 完整性和45项Playwright,并在Windows上显式管理8876测试服务器生命周期。 +- 21个固定API使用同一数据库时点重新比较全部一致;数据库62个schema对象和21张关键表一致, + 仅按既有规则排除内置策略启动校准的`updated_at`。 +- 候选自身的16页、64类路由匹配、36张表、数据/LLM/CSS入口和热点文件登记在 + `app/config/architecture-inventory.json`;逐项结论见`completion-audit.md`。 +- 严格审计回档标签为`xiaobai-preservation-slice-11-audit-candidate-20260731`。该标签仍不是最终 + 完成标签,未改变试删候选、人工验收和部署切换边界。 + ## 决策记录 | 日期 | 决策 | 原因 |