diff --git a/app/config/architecture-inventory.json b/app/config/architecture-inventory.json index 6577d3d..9d0aa96 100644 --- a/app/config/architecture-inventory.json +++ b/app/config/architecture-inventory.json @@ -254,7 +254,7 @@ "code_hotspots": [ { "path": "frontend/styles/styles.css", - "bytes": 361780, + "bytes": 361776, "lines": 15465 }, { @@ -279,12 +279,12 @@ }, { "path": "frontend/app.js", - "bytes": 91151, + "bytes": 89213, "lines": 1939 }, { "path": "frontend/pages/heaven/page.js", - "bytes": 88322, + "bytes": 86493, "lines": 1830 }, { @@ -299,7 +299,7 @@ }, { "path": "backend/features/heaven/service.py", - "bytes": 64421, + "bytes": 63123, "lines": 1303 }, { @@ -309,7 +309,7 @@ }, { "path": "frontend/pages/market/runtime.js", - "bytes": 57053, + "bytes": 55720, "lines": 1333 }, { @@ -319,7 +319,7 @@ }, { "path": "backend/application.py", - "bytes": 50934, + "bytes": 49784, "lines": 1165 }, { @@ -329,7 +329,7 @@ }, { "path": "database.py", - "bytes": 34013, + "bytes": 33284, "lines": 746 } ] diff --git a/app/tests/preservation_helpers.py b/app/tests/preservation_helpers.py index 0e29cd7..308dfeb 100644 --- a/app/tests/preservation_helpers.py +++ b/app/tests/preservation_helpers.py @@ -26,6 +26,7 @@ RETIRED_FRONTEND_SOURCE_RANGES = ( (9022, 9027), (9052, 9055), ) +AUDITED_FRONTEND_SOURCE_LINE_COUNT = 9283 def sha256(path: Path) -> str: @@ -51,12 +52,10 @@ def reassembled_frontend_runtime() -> str: assembled.append(content) next_line = end + 1 - original_line_count = len( - (ORIGINAL_STATIC / "app.js").read_text(encoding="utf-8").splitlines() - ) - if next_line != original_line_count + 1: + if next_line != AUDITED_FRONTEND_SOURCE_LINE_COUNT + 1: raise AssertionError( - f"app.js source coverage ended at {next_line - 1}, expected {original_line_count}" + "app.js source coverage ended at " + f"{next_line - 1}, expected {AUDITED_FRONTEND_SOURCE_LINE_COUNT}" ) return "".join(assembled) diff --git a/app/tests/test_frontend_boundaries.py b/app/tests/test_frontend_boundaries.py index 272b09e..763b411 100644 --- a/app/tests/test_frontend_boundaries.py +++ b/app/tests/test_frontend_boundaries.py @@ -4,6 +4,7 @@ import json import re import unittest from pathlib import Path +from unittest.mock import patch from tests.preservation_helpers import reassembled_frontend_runtime @@ -46,6 +47,14 @@ class FrontendBoundaryTests(unittest.TestCase): self.assertIn("const state = window.XiaobaiState.create({", app) self.assertNotIn("const state = {", app) + def test_candidate_runtime_reassembly_does_not_require_original_static(self) -> None: + with patch( + "tests.preservation_helpers.ORIGINAL_STATIC", + ROOT / "missing-original-static", + ): + app = reassembled_frontend_runtime() + self.assertIn("function openView(", app) + def test_runtime_page_registry_matches_governance_registry(self) -> None: expected = json.loads( (ROOT / "config" / "pages.config.json").read_text(encoding="utf-8") diff --git a/app/tests/test_maintenance_tools.py b/app/tests/test_maintenance_tools.py index 169ced0..36b14cc 100644 --- a/app/tests/test_maintenance_tools.py +++ b/app/tests/test_maintenance_tools.py @@ -3,11 +3,16 @@ from __future__ import annotations import json import subprocess import sys +import tempfile 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 +from tools.build_architecture_inventory import ( + build as build_architecture_inventory, + source_metrics, +) +from tools.verify_baseline import python_test_command ROOT = Path(__file__).resolve().parents[1] @@ -34,6 +39,22 @@ class MaintenanceToolTests(unittest.TestCase): self.assertIn("stop_e2e_server(server)", source) self.assertNotIn('"static/app.js"', source) + def test_standalone_verifier_excludes_only_migration_comparison_modules(self) -> None: + repository_command = python_test_command(preservation_baseline=True) + standalone_command = python_test_command(preservation_baseline=False) + self.assertIn("discover", repository_command) + self.assertTrue(any(item == "tests.test_frontend_contract" for item in standalone_command)) + self.assertFalse(any("test_preservation_" in item for item in standalone_command)) + + def test_architecture_metrics_are_independent_of_checkout_line_endings(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + lf = root / "lf.js" + crlf = root / "crlf.js" + lf.write_bytes(b"const a = 1;\nconst b = 2;\n") + crlf.write_bytes(b"const a = 1;\r\nconst b = 2;\r\n") + self.assertEqual(source_metrics(lf), source_metrics(crlf)) + def test_every_tool_has_a_non_mutating_help_path(self) -> None: for path in sorted((ROOT / "tools").glob("*.py")): if path.name.startswith("_"): diff --git a/app/tools/README.md b/app/tools/README.md index 6f4462e..f1e6397 100644 --- a/app/tools/README.md +++ b/app/tools/README.md @@ -14,6 +14,13 @@ maintenance command cannot be mistaken for a historical migration rewrite. - `python tools/build_architecture_inventory.py [--check]`: generate or verify `config/architecture-inventory.json` from the candidate source tree. +Inside the canonical `webapp/app/` checkout, `verify_baseline.py` runs the full preservation +suite against the retained original baseline and enforces `git diff --check`. In a standalone +`app/` export where that baseline and Git checkout do not exist, the same command runs all +candidate-owned tests, skips only `test_preservation_*` comparison modules, and reports the Git +check as skipped. Product, registry, JavaScript, database, and optional Playwright checks remain +active in both modes. + ## Acceptance and differential checks - `run_preservation_runtime.py`: start an isolated original or candidate runtime with an diff --git a/app/tools/build_architecture_inventory.py b/app/tools/build_architecture_inventory.py index bccec7f..0ee1e3e 100644 --- a/app/tools/build_architecture_inventory.py +++ b/app/tools/build_architecture_inventory.py @@ -75,6 +75,14 @@ def css_layers(html: str) -> list[str]: return re.findall(r']+rel="stylesheet"[^>]+href="([^"]+)"', html) +def source_metrics(path: Path) -> dict[str, int]: + text = path.read_text(encoding="utf-8") + return { + "bytes": len(text.encode("utf-8")), + "lines": len(text.splitlines()), + } + + def code_hotspots() -> list[dict[str, Any]]: candidates = [ "backend/application.py", @@ -99,13 +107,7 @@ def code_hotspots() -> list[dict[str, Any]]: path = ROOT / name if not path.is_file(): continue - rows.append( - { - "path": name, - "bytes": path.stat().st_size, - "lines": len(path.read_text(encoding="utf-8").splitlines()), - } - ) + rows.append({"path": name, **source_metrics(path)}) return sorted(rows, key=lambda item: item["bytes"], reverse=True) diff --git a/app/tools/verify_baseline.py b/app/tools/verify_baseline.py index 9bf656c..bbc571b 100644 --- a/app/tools/verify_baseline.py +++ b/app/tools/verify_baseline.py @@ -21,6 +21,39 @@ def run(label: str, command: list[str]) -> None: subprocess.run(command, cwd=ROOT, check=True) +def python_test_command(preservation_baseline: bool | None = None) -> list[str]: + if preservation_baseline is None: + preservation_baseline = (ROOT.parent / "static" / "app.js").is_file() + if preservation_baseline: + return [sys.executable, "-m", "unittest", "discover", "-s", "tests"] + modules = [ + f"tests.{path.stem}" + for path in sorted((ROOT / "tests").glob("test_*.py")) + if not path.stem.startswith("test_preservation_") + ] + if not modules: + raise RuntimeError("no standalone candidate tests found") + return [sys.executable, "-m", "unittest", *modules] + + +def verify_git_diff() -> None: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + print("\n[patch] skipped: standalone export is not a Git checkout") + return + repository_root = Path(result.stdout.strip()).resolve() + if ROOT != repository_root / "app": + print("\n[patch] skipped: standalone export is outside the canonical app path") + return + run("patch", ["git", "diff", "--check"]) + + def verify_database() -> None: database = ROOT / "data" / "review.db" if not database.exists(): @@ -97,7 +130,7 @@ def main() -> int: ) args = parser.parse_args() - run("python", [sys.executable, "-m", "unittest", "discover", "-s", "tests"]) + run("python", python_test_command()) run( "api-registry", [sys.executable, "tools/build_api_registry.py", "--check"], @@ -117,7 +150,7 @@ def main() -> int: "javascript", [node, "--check", script.relative_to(ROOT).as_posix()], ) - run("patch", ["git", "diff", "--check"]) + verify_git_diff() verify_database() if args.e2e: diff --git a/docs/migration/evidence/slice-10/README.md b/docs/migration/evidence/slice-10/README.md index 4ad541f..5f92c1b 100644 --- a/docs/migration/evidence/slice-10/README.md +++ b/docs/migration/evidence/slice-10/README.md @@ -39,7 +39,7 @@ ## 4. 真实浏览器差分 -- 1920×1080日间模式检查情绪周期、集合竞价、智能选股、观势、观气、观心介绍及观心呼吸。 +- 1920×1080日间模式检查情绪周期、智能选股、观势、观气、观心介绍及观心呼吸。 - 1920×1080夜间模式检查情绪周期的背景、字体、表格、几何和横向溢出。 - 390×844检查情绪周期与观势:移动Shell、底部五入口、页面纵向滚动、无横向溢出及问天特效均一致。 - 观势星空节点90个、观气100个、观心110个;三页八卦节点均为2个。 @@ -48,6 +48,10 @@ - 原版和迁移版检查流程均未产生新增控制台error或warn。 - 动画帧、焦点框和动态状态文字属于采样瞬时状态,因此截图文件哈希不要求相同;可见布局几何、计算样式、节点、动画名称和交互结果必须一致,本次均通过。 - 两个服务使用相同主机名、不同端口时会共享并覆盖登录Cookie;曾导致迁移版切页被误判为失效。逐服务重新登录后行为一致,该问题属于并行验收环境限制,不是产品回归。 +- 2026-08-01像素复核发现`frontend-migrated-auction-light-1920x1080`实际截取了登录状态失效页, + 不能证明集合竞价视觉等价,文件已重命名为`INVALID-login-session`。集合竞价仍有切片05真实页面、 + 本切片源码/样式保真、API及Playwright证据,但最终视觉明确留待人工验收,不以替代证据冒充 + 本切片截图差分。 - 机器可读记录见`browser-acceptance.json`,截图均保存在本目录。 ## 5. 自动验证与保留边界 diff --git a/docs/migration/evidence/slice-10/browser-acceptance.json b/docs/migration/evidence/slice-10/browser-acceptance.json index c37e3cb..bfc6a6c 100644 --- a/docs/migration/evidence/slice-10/browser-acceptance.json +++ b/docs/migration/evidence/slice-10/browser-acceptance.json @@ -1,7 +1,7 @@ { "schema_version": 1, "tested_at": "2026-07-31T12:00:00+08:00", - "result": "passed", + "result": "passed_with_documented_evidence_gap", "environments": { "original": "temporary_original_runtime", "migrated": "temporary_app_runtime", @@ -15,7 +15,6 @@ "theme": "light", "views": [ "sentiment", - "auction", "screener", "heaven_trend", "heaven_fortune", @@ -44,6 +43,14 @@ "equal": true } ], + "invalid_captures": [ + { + "view": "auction", + "file": "frontend-migrated-auction-light-1920x1080.INVALID-login-session.png", + "reason": "The migrated capture shows an expired login session and is not visual-equivalence evidence.", + "replacement_claim": "No replacement screenshot claim; final visual acceptance remains manual." + } + ], "heaven_animation_contract": { "trend_star_nodes": 90, "fortune_star_nodes": 100, @@ -89,5 +96,6 @@ }, "screenshot_policy": "Dynamic animation frames, focus outlines, and live status text may change pixel hashes. Acceptance compares visible geometry, computed styles, DOM state, animation names, interaction results, and overflow behavior.", "known_test_environment_constraint": "Original and migrated services on the same hostname share cookies across ports. Each service must be logged in separately immediately before comparison.", - "all_equal": true + "all_equal": true, + "manual_acceptance_required": true } diff --git a/docs/migration/evidence/slice-10/frontend-migrated-auction-light-1920x1080.png b/docs/migration/evidence/slice-10/frontend-migrated-auction-light-1920x1080.INVALID-login-session.png similarity index 100% rename from docs/migration/evidence/slice-10/frontend-migrated-auction-light-1920x1080.png rename to docs/migration/evidence/slice-10/frontend-migrated-auction-light-1920x1080.INVALID-login-session.png diff --git a/docs/migration/evidence/slice-11/README.md b/docs/migration/evidence/slice-11/README.md index 8dc16e6..5692420 100644 --- a/docs/migration/evidence/slice-11/README.md +++ b/docs/migration/evidence/slice-11/README.md @@ -4,6 +4,7 @@ > 候选回档标签:`xiaobai-preservation-slice-11-candidate-20260731` > 严格审计候选标签:`xiaobai-preservation-slice-11-audit-candidate-20260731` > 跨日复验候选标签:`xiaobai-preservation-slice-11-audit-candidate-20260801` +> 独立维护候选标签:`xiaobai-preservation-slice-11-audit-candidate-standalone-20260801` > 当前结论:自动验收完成,等待用户人工验收;尚未执行正式切换、Docker或NAS部署 ## 1. 本切片做了什么 @@ -32,7 +33,7 @@ | 验证 | 结果 | |---|---:| | 原版`python -m unittest discover -s tests -q` | 231项通过 | -| 迁移版`python -m unittest discover -s tests -q` | 302项通过 | +| 迁移版`python -m unittest discover -s tests -q` | 305项通过 | | 历史切片与前端/清理专项复核 | 通过 | | 迁移版JavaScript语法检查 | 24个文件通过 | | `npx.cmd playwright test --reporter=dot` | 45项通过(1.6分钟) | @@ -58,6 +59,12 @@ Windows下Playwright托管Python静态服务器会在45项执行后不退出, 探活和停止8876;最终同一命令正常退出并明确报告`45 passed`。四项维护工具契约测试使迁移版 总数从298增加到302。逐项目标矩阵见`completion-audit.md`。 +后续独立性审计把仅含`app/`的Git导出放到系统临时目录:运行模块全部解析到导出内部,统一 +维护命令通过242项候选自有测试、两个注册表、24个JavaScript文件和SQLite完整性检查,并明确 +跳过不存在的Git工作区。正式仓库继续执行全部保真差分,最终为305项Python测试和45项 +Playwright通过。架构热点字节数已改为换行归一化后的UTF-8大小,Windows CRLF与Git/Linux LF +不会再制造假差异。 + ## 3. 真实浏览器验收 - 桌面端复核情绪周期、智能选股三个工作区、问天日间/夜间和加载资源;控制台无新增 @@ -70,6 +77,8 @@ Windows下Playwright托管Python静态服务器会在45项执行后不退出, - 同账号、同数据库快照、同主题和同视口下,原版与迁移版问天布局和计算样式一致。 机器可读记录见`browser-acceptance.json`。截图和更广的桌面/移动基线继续沿用切片10证据目录。 +切片10截图的事后像素复核见`screenshot-pixel-audit.json`;九组有效截图平均色差低于0.3/255, +集合竞价候选图因登录状态失效被明确排除,仍等待用户人工视觉验收。 ## 4. 数据与部署边界 diff --git a/docs/migration/evidence/slice-11/completion-audit.md b/docs/migration/evidence/slice-11/completion-audit.md index a04f2c8..62aec7a 100644 --- a/docs/migration/evidence/slice-11/completion-audit.md +++ b/docs/migration/evidence/slice-11/completion-audit.md @@ -24,7 +24,7 @@ | 前端唯一请求出口、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 | 自动闭环 | +| 可由人工维护者复验 | 候选README/架构说明、工具分类、API/架构生成器、统一验证命令均可从`app/`独立运行;系统临时目录导出实测242项候选测试通过 | `app/tools/README.md`;maintenance tool tests;独立导出复验 | 自动闭环 | | 正式数据库与部署不受影响 | 验收只使用隔离SQLite副本和非8765端口 | 各切片README;运行记录 | 自动闭环 | | Docker/NAS和正式入口切换 | 用户已明确本轮不做NAS Docker测试;人工验收前禁止切换 | 迁移状态和切换指南 | 未执行 | @@ -61,6 +61,14 @@ 5. 跨到2026-08-01周六后,实时详情测试中用运行日构造的“固定时间”不再代表交易日;原版和 候选测试夹具同步固定到明确工作日,消除跨午夜/周末不确定性。产品实现与交易日规则未改, 随后统一验收再次通过302项Python测试和45项Playwright。 +6. 独立导出`app/`后成功启动健康接口,236项非迁移业务/前端测试通过;同时发现六项日常前端 + 契约经辅助函数隐式读取旧`static/app.js`。候选运行时重组现使用已审计的9,283行覆盖边界, + 只有明确的保真差分断言继续读取原版,避免未来清理旧目录后日常契约失效。 +7. 对切片10十组截图重新做像素审计,九组平均色差均低于0.3/255;集合竞价候选图实际为登录 + 失效页,已明确标为无效并撤销其截图证明力。集合竞价最终视觉继续列为人工验收项。 +8. 统一验证器现在按环境选择完整保真套件或候选自有套件;系统临时目录中的纯`app/`导出通过 + 242项测试、注册表、24个JavaScript文件和SQLite检查。架构热点大小按归一化UTF-8计算, + CRLF/LF不再导致清单失效;正式仓库最终通过305项测试和45项Playwright。 ## 4. 仍需用户完成的最终裁决 diff --git a/docs/migration/evidence/slice-11/screenshot-pixel-audit.json b/docs/migration/evidence/slice-11/screenshot-pixel-audit.json new file mode 100644 index 0000000..703a97d --- /dev/null +++ b/docs/migration/evidence/slice-11/screenshot-pixel-audit.json @@ -0,0 +1,27 @@ +{ + "schema_version": 1, + "audited_at": "2026-08-01T01:55:00+08:00", + "source_directory": "docs/migration/evidence/slice-10", + "difference_threshold_per_channel": 8, + "valid_pairs": [ + {"view": "dark-sentiment-1920x1080", "size": "1683x1080", "mean_absolute_difference": 0.065, "changed_pixel_percent": 0.123}, + {"view": "dark-sentiment-390x844", "size": "375x812", "mean_absolute_difference": 0.009, "changed_pixel_percent": 0.012}, + {"view": "heaven-fortune-light-1920x1080", "size": "1683x1080", "mean_absolute_difference": 0.043, "changed_pixel_percent": 0.046}, + {"view": "heaven-heart-breath-light-1920x1080", "size": "1683x1080", "mean_absolute_difference": 0.256, "changed_pixel_percent": 0.545}, + {"view": "heaven-heart-intro-light-1920x1080", "size": "1683x1080", "mean_absolute_difference": 0.047, "changed_pixel_percent": 0.068}, + {"view": "heaven-trend-390x844", "size": "390x844", "mean_absolute_difference": 0.284, "changed_pixel_percent": 0.712}, + {"view": "heaven-trend-light-1920x1080", "size": "1683x1080", "mean_absolute_difference": 0.040, "changed_pixel_percent": 0.063}, + {"view": "light-1920x1080", "size": "1683x1080", "mean_absolute_difference": 0.000, "changed_pixel_percent": 0.000}, + {"view": "screener-light-1920x1080", "size": "1683x1080", "mean_absolute_difference": 0.064, "changed_pixel_percent": 0.157} + ], + "invalid_pairs": [ + { + "view": "auction-light-1920x1080", + "mean_absolute_difference": 15.820, + "changed_pixel_percent": 77.932, + "reason": "The migrated image is an expired-login page, not the auction workspace.", + "disposition": "Renamed INVALID-login-session and excluded from visual-equivalence evidence." + } + ], + "conclusion": "Nine valid pairs are geometrically and visually consistent within dynamic rendering noise. Auction remains a manual visual acceptance item." +} diff --git a/docs/migration/保真迁移状态.json b/docs/migration/保真迁移状态.json index 983a8c0..a534cf0 100644 --- a/docs/migration/保真迁移状态.json +++ b/docs/migration/保真迁移状态.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "updated_at": "2026-08-01T01:19:27+08:00", + "updated_at": "2026-08-01T03:14:46+08:00", "status": "awaiting_manual_acceptance", "migration_mode": "behavior_preserving_source_migration", "source_of_truth": "current_original_webapp_runtime_and_source", @@ -12,7 +12,7 @@ "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-strict-completion-audit-maintenance-handoff", - "last_checkpoint": "xiaobai-preservation-slice-11-audit-candidate-20260801", + "last_checkpoint": "xiaobai-preservation-slice-11-audit-candidate-standalone-20260801", "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 4750116..a7df9c2 100644 --- a/docs/migration/保真迁移账本.md +++ b/docs/migration/保真迁移账本.md @@ -32,6 +32,7 @@ | 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差分通过;仍等待人工确认 | +| 2026-08-01 | `xiaobai-preservation-slice-11-audit-candidate-standalone-20260801` | 独立维护与截图证据复核 | 正式仓库305项、独立导出242项测试通过;撤销无效竞价截图;仍等待人工确认 | ## 资产处置登记 @@ -220,6 +221,12 @@ 非交易日误作预期合并日;两版测试夹具同步固定到明确工作日,产品代码和日期规则未改。 - 跨日修复后统一验收再次通过302项Python测试、24个JavaScript文件、SQLite完整性和45项 Playwright;新增候选回档标签`xiaobai-preservation-slice-11-audit-candidate-20260801`。 +- 纯`app/`导出在系统临时目录成功运行统一维护命令:242项候选测试、注册表、24个JavaScript + 文件和SQLite检查通过;正式仓库保真套件为305项Python测试及45项Playwright通过。 +- 日常前端契约不再隐式读取旧`static/app.js`,只有迁移差分断言保留原版依赖;架构热点大小 + 按归一化UTF-8统计,CRLF/LF跨环境结果一致。 +- 切片10集合竞价候选截图实际为登录失效页,已重命名并撤销证明力;九组有效截图像素差异 + 低于动态渲染噪声阈值,集合竞价最终视觉继续等待人工验收。 ## 决策记录