from __future__ import annotations import ast import hashlib import re from pathlib import Path APP_ROOT = Path(__file__).resolve().parents[1] ORIGINAL_ROOT = APP_ROOT.parent FRONTEND_ROOT = APP_ROOT / "frontend" ORIGINAL_STATIC = ORIGINAL_ROOT / "static" SOURCE_RANGE = re.compile( r"/\* PRESERVATION-SOURCE-BEGIN app\.js:(\d+)-(\d+) \*/\n" r"(.*?)" r"/\* PRESERVATION-SOURCE-END app\.js:\1-\2 \*/\n?", re.DOTALL, ) # These exact original app.js line ranges were retired in slice 11 after the # definition-only symbols passed static, runtime, and compatibility review. RETIRED_FRONTEND_SOURCE_RANGES = ( (3537, 3540), (4139, 4145), (4973, 4989), (9022, 9027), (9052, 9055), ) AUDITED_FRONTEND_SOURCE_LINE_COUNT = 9283 # CR-12 through CR-14 remove only declarations repeated by a later rule under # the same cascade context. Entries are either an exact retired fragment or an # exact (source, replacement) pair. Optional trailing values declare the source # count and how many leading occurrences to transform when an identical later # copy must remain. Every other byte remains part of the accepted CSS baseline. AUDITED_CSS_RETIREMENTS = { "styles.css": ( ".workspace-view.active-view {\n display: block;\n}\n\n", "body.sidebar-collapsed {\n grid-template-columns: 64px minmax(0, 1fr);\n}\n\n", ".rotation-day:last-child { border-right: 0; }\n\n", ( "#screenerView .probability-value strong,\n" "#screenerView .probability-value small {\n" " display: block;\n" "}\n\n" ), ( ( "@media (min-width: 721px) and (max-width: 1279px) {\n" " .market-tape {\n" " display: none;\n" " }\n\n" " .app-header {" ), ( "@media (min-width: 721px) and (max-width: 1279px) {\n" " .app-header {" ), ), ( " .module-nav .nav-brand,\n" " .module-nav .nav-group-label,\n" " .module-nav .market-sub-tab,\n" " .module-nav .sidebar-collapse-button {\n" " display: none;\n" " }\n\n" ), ( " .module-nav .nav-group,\n" " body.sidebar-collapsed .module-nav .nav-group {\n" " display: contents;\n" " margin: 0;\n" " padding: 0;\n" " border: 0;\n" " }\n\n" ), ( " .module-nav .module-tab.mobile-primary-tab,\n" " body.sidebar-collapsed .module-nav .module-tab.mobile-primary-tab {\n" " display: flex;\n" " }\n\n" ), " body.mentor-directory-open {\n overflow: hidden;\n }\n\n", ( " body,\n" " body.sidebar-collapsed {\n" " display: block;\n" " min-height: 100dvh;\n" " padding-bottom: calc(68px + env(safe-area-inset-bottom));\n" " }\n\n" ), " .brand-block {\n height: 42px;\n }\n\n", ( " .header-actions {\n" " position: absolute;\n" " inset: 56px 10px auto;\n" " display: flex;\n" " justify-content: space-between;\n" " gap: 6px;\n" " }\n\n" ), ( " .header-date-group {\n" " height: 42px;\n" " min-width: 0;\n" " flex: 1;\n" " }\n\n" ), ( " .header-actions > .icon-button {\n" " width: 40px;\n" " min-width: 40px;\n" " min-height: 42px;\n" " }\n\n" ), " .overview-strip .metric:nth-of-type(1) { grid-column: 2; grid-row: 1; }\n", " .overview-strip .metric:nth-of-type(3) { grid-column: 2; grid-row: 2; }\n", " .curated-strategy-list {\n grid-template-columns: 1fr;\n }\n\n", " .quant-universe-grid { grid-template-columns: 1fr 1fr; }\n", ( " .auction-workspace-layout { grid-template-columns: 1fr; }\n", "", 2, 1, ), ( " .auction-unified-table-frame { min-height: 360px; max-height: none; }\n", "", 2, 1, ), " .theme-library-layout { display: block; min-height: 0; }\n", ( ".brand-block {\n gap: 10px;\n}\n\n", "", 2, 1, ), ( " html,\n" " body {\n" " min-width: 320px;\n" " width: 100%;\n" " }\n\n", "", 2, 1, ), ( " .module-nav .module-tab .lucide {\n" " width: 20px;\n" " height: 20px;\n" " }\n\n", "", 2, 1, ), ( " .sentiment-trend-panel {\n" " border-right: 0;\n" " border-bottom: 1px solid var(--border);\n" " }\n\n", "", 2, 1, ), " .auction-dataset-segments { min-width: 430px; }\n", ), "renovation.css": ( ".overview-strip .sentiment-block { padding-left: 0; }\n", ( '.overview-strip[data-overview-expanded="true"] .metric-value ' "{ font-size: 18px; }\n\n" ), ( ".curated-strategy-list { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }\n", "", 2, 1, ), ( ".sentiment-detail-toolbar { margin-top: 0; }\n\n", "", 2, 1, ), ( ".sentiment-detail-toolbar { margin-top: 0; }\n", "", 2, 1, ), ".review-workspace .workspace-section + .workspace-section { border-top: 1px solid var(--border); }\n\n", ( " body,\n" " body.sidebar-collapsed {\n" " display: block;\n" " min-height: 100dvh;\n" " padding-bottom: calc(68px + env(safe-area-inset-bottom));\n" " }\n\n", "", 2, 1, ), ( " .app-header {\n" " width: 100%;\n" " height: 108px;\n" " min-height: 108px;\n" " position: relative;\n" " display: flex;\n" " align-items: flex-start;\n" " padding: 8px 10px 0;\n" " }\n\n", "", 2, 1, ), (" .brand-block { height: 42px; }\n", "", 2, 1), (" .brand-block h1 { font-size: 16px; }\n", "", 2, 1), ( " .header-actions { position: absolute; inset: 56px 10px auto; display: flex; justify-content: space-between; gap: 6px; }\n", "", 2, 1, ), ( " .header-date-group { height: 42px; min-width: 0; flex: 1; }\n", "", 2, 1, ), ( " .header-date-group .date-input { width: 104px; flex: 1; }\n", "", 2, 1, ), ( " .header-actions > .icon-button { width: 40px; min-width: 40px; min-height: 42px; }\n", "", 2, 1, ), ( " .module-nav,\n" " body.sidebar-collapsed .module-nav {\n" " width: 100%;\n" " height: calc(64px + env(safe-area-inset-bottom));\n" " min-height: 64px;\n" " position: fixed;\n" " inset: auto 0 0;\n" " z-index: 45;\n" " display: grid;\n" " grid-template-columns: repeat(5, minmax(0, 1fr));\n" " align-items: stretch;\n" " padding: 4px 4px max(4px, env(safe-area-inset-bottom));\n" " overflow: hidden;\n" " border-top: 1px solid var(--border);\n" " border-right: 0;\n" " background: rgba(255, 255, 255, .98);\n" " box-shadow: 0 -5px 18px rgba(16, 24, 40, .08);\n" " }\n\n", "", 2, 1, ), ( " .app-main { width: 100%; min-height: calc(100dvh - 176px); margin: 0; padding: 10px 8px 20px; }\n", "", 2, 1, ), ( " .workspace-view,\n" ' body[data-active-view="screenerView"] .workspace-view,\n' ' body[data-active-view="mentorView"] .workspace-view,\n' ' body[data-active-view="reviewWorkspaceView"] .workspace-view { padding: 0 0 76px; }\n', "", 2, 1, ), (".insight-rail { gap: 12px; }\n", "", 2, 1), ("#screenerView .screener-strategy-view { gap: 12px; }\n", "", 2, 1), (".mentor-chat-form .button { min-height: 38px; }\n\n", "", 2, 1), ( ".review-workspace .notes-history-section { grid-area: notes; }\n", "", 3, 2, ), ( ".sentiment-stage-guide-grid article.current strong,\n" ".sentiment-stage-guide-grid article.current small { color: var(--danger); }\n", "", 2, 1, ), ( ".review-workspace {\n" " grid-template-columns: minmax(0, 1fr) 360px;\n" ' grid-template-areas: "watch journal" "trades journal" "notes notes";\n' " align-items: start;\n" " gap: 12px;\n" "}\n", "", 2, 1, ), (".review-workspace .watchlist-section { grid-area: watch; }\n", "", 2, 1), (".review-workspace .journal-section { grid-area: journal; }\n", "", 2, 1), (".review-workspace .trade-journal-section { grid-area: trades; }\n", "", 2, 1), ), "redesign-v2.css": ( "#dragonView .dragon-operation-table .dragon-col-reason { width: auto; }\n", ( " .market-tape { display: none; }\n" " .header-actions { width: 100%; }\n" " .header-command-group { position: absolute; }\n" ), ( " .sidebar-brand,\n" " .module-nav .nav-group-label,\n" " .sidebar-collapse-button,\n" " .module-nav .market-sub-tab { display: none; }\n" " .module-nav .nav-group,\n" " body.sidebar-collapsed .module-nav .nav-group { display: contents; }\n" " .module-nav .module-tab,\n" " body.sidebar-collapsed .module-nav .module-tab { display: none; }\n" ), " .module-nav .module-tab.mobile-primary-tab span { display: inline; }\n", ( " .overview-strip .metric:nth-of-type(n + 4),\n" " .overview-strip .metric-wide { display: none; }\n" " .overview-toggle { display: none; }\n" ), ( ( " #screenerView .quant-summary-pane .quant-universe-grid,\n" " #screenerView .quant-formula-summary,\n" " #screenerView .quant-execution-actions { grid-template-columns: 1fr; }\n" " #screenerView .quant-score-row,\n" " #screenerView .quant-filter-row { grid-template-columns: 1fr; }\n" " #screenerTrackingView { padding: 10px; }\n" ), ( " #screenerView .quant-summary-pane .quant-universe-grid,\n" " #screenerView .quant-formula-summary,\n" " #screenerView .quant-execution-actions { grid-template-columns: 1fr; }\n" " #screenerTrackingView { padding: 10px; }\n" ), ), ), } # User-approved product behavior changes remain separate from code-retirement # records. Each entry is an exact source/replacement pair. Optional trailing # values declare the expected source count and how many leading occurrences to # transform when one identical occurrence must remain. AUDITED_CSS_REPLACEMENTS = { "redesign-v2.css": ( ( ( '@media (min-width: 721px) {\n' ' body[data-active-view="dragonView"] .app-main { display: flex; flex-direction: column; overflow: hidden; }\n' ' body[data-active-view="dragonView"] .overview-strip { flex: 0 0 auto; }\n' ' body[data-active-view="dragonView"] #dragonView.active-view {\n' ' min-height: 0;\n' ' flex: 1 1 auto;\n' ' display: flex;\n' ' flex-direction: column;\n' ' }\n' ' body[data-active-view="dragonView"] .dragon-page-head-v2 { flex: 0 0 auto; }\n' ' body[data-active-view="dragonView"] .dragon-daily-content-v2,\n' ' body[data-active-view="dragonView"] .dragon-empty-state-v2 { flex: 1 1 auto; }\n' '}\n' ), ( '@media (min-width: 721px) {\n' ' body[data-active-view="dragonView"] .app-main { height: var(--workspace-height); min-height: 0; display: block; overflow: auto; }\n' ' body[data-active-view="dragonView"] #dragonView.active-view {\n' ' display: block;\n' ' overflow: visible;\n' ' }\n' '}\n' ), ), ( ( "/* The Dragon-Tiger page stays still; only the selected trader's operations scroll. */\n" '@media (min-width: 721px) {\n' ' body[data-active-view="dragonView"] .dragon-daily-content-v2 {\n' ' min-height: 0;\n' ' display: grid;\n' ' grid-template-rows: auto auto auto minmax(150px, 1fr) auto;\n' ' overflow: hidden;\n' ' }\n\n' ' body[data-active-view="dragonView"] #dragonView .dragon-card-stage-v2 {\n' ' flex: 0 0 auto;\n' ' }\n\n' ' body[data-active-view="dragonView"] #dragonView .dragon-trader-detail-v2 {\n' ' min-height: 0;\n' ' display: flex;\n' ' flex-direction: column;\n' ' }\n\n' ' body[data-active-view="dragonView"] #dragonView .dragon-detail-header {\n' ' flex: 0 0 auto;\n' ' }\n\n' ' body[data-active-view="dragonView"] #dragonView .dragon-trader-detail .trader-operations {\n' ' min-height: 0;\n' ' flex: 1 1 auto;\n' ' overflow: auto;\n' ' overscroll-behavior: contain;\n' ' scrollbar-gutter: stable;\n' ' }\n\n' ' body[data-active-view="dragonView"] #dragonView .dragon-unclassified-v2 {\n' ' max-height: 180px;\n' ' overflow: auto;\n' ' }\n' ), ( '/* The Dragon-Tiger page owns vertical scrolling; wide operation tables scroll horizontally. */\n' '@media (min-width: 721px) {\n' ' body[data-active-view="dragonView"] .dragon-daily-content-v2 {\n' ' display: block;\n' ' overflow: visible;\n' ' }\n\n' ' body[data-active-view="dragonView"] #dragonView .dragon-trader-detail-v2 {\n' ' display: block;\n' ' }\n\n' ' body[data-active-view="dragonView"] #dragonView .dragon-trader-detail .trader-operations {\n' ' max-height: none;\n' ' overflow: auto;\n' ' }\n\n' ' body[data-active-view="dragonView"] #dragonView .dragon-unclassified-v2 {\n' ' max-height: none;\n' ' overflow: visible;\n' ' }\n' ), ), ), "design-system.css": ( (' [data-active-view="dragonView"],\n', "", 4, 3), (' #dragonView.active-view,\n', ""), (' #dragonView .dragon-page-head-v2,\n', ""), (' #dragonView .dragon-daily-content-v2,\n', ""), ( ' #dragonView .dragon-daily-content-v2{overflow:hidden}\n' ' #dragonView .dragon-trader-detail-v2{min-height:0}\n' ' #dragonView .dragon-trader-detail .trader-operations{min-height:0;overflow:auto}\n\n', "", ), ), } def sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def function_contract(path: Path, name: str) -> tuple[str, str]: tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) function = next( node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == name ) body = ast.Module(body=function.body, type_ignores=[]) return ( ast.dump(function.args, include_attributes=False), ast.dump(body, include_attributes=False), ) def module_contract( path: Path, *, excluded_definitions: set[str] | None = None, excluded_import_modules: set[str] | None = None, exclude_imports: bool = False, ) -> str: excluded_definitions = excluded_definitions or set() excluded_import_modules = excluded_import_modules or set() tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) tree.body = [ node for node in tree.body if not (exclude_imports and isinstance(node, (ast.Import, ast.ImportFrom))) and not ( isinstance(node, ast.ImportFrom) and node.module in excluded_import_modules ) and getattr(node, "name", None) not in excluded_definitions ] return ast.dump(tree, include_attributes=False) def reassembled_frontend_runtime() -> str: chunks: dict[tuple[int, int], str] = {} for path in FRONTEND_ROOT.rglob("*.js"): for match in SOURCE_RANGE.finditer(path.read_text(encoding="utf-8")): source_range = (int(match.group(1)), int(match.group(2))) if source_range in chunks: raise AssertionError(f"duplicate app.js source range: {source_range}") chunks[source_range] = match.group(3) assembled: list[str] = [] next_line = 1 for (start, end), content in sorted(chunks.items()): if start != next_line: raise AssertionError( f"app.js source coverage gap: expected line {next_line}, got {start}" ) assembled.append(content) next_line = end + 1 if next_line != AUDITED_FRONTEND_SOURCE_LINE_COUNT + 1: raise AssertionError( "app.js source coverage ended at " f"{next_line - 1}, expected {AUDITED_FRONTEND_SOURCE_LINE_COUNT}" ) return "".join(assembled) def original_runtime_after_audited_retirements() -> str: lines = (ORIGINAL_STATIC / "app.js").read_text(encoding="utf-8").splitlines( keepends=True ) retired = { line_number for start, end in RETIRED_FRONTEND_SOURCE_RANGES for line_number in range(start, end + 1) } return "".join( line for line_number, line in enumerate(lines, start=1) if line_number not in retired ) def assert_frontend_runtime_matches_audited_baseline(testcase) -> None: testcase.assertEqual( reassembled_frontend_runtime(), original_runtime_after_audited_retirements(), ) def assert_moved_asset_matches( testcase, original_relative: str, frontend_relative: str | None = None, ) -> None: target_relative = frontend_relative or original_relative if ( original_relative in AUDITED_CSS_RETIREMENTS or original_relative in AUDITED_CSS_REPLACEMENTS ): original = (ORIGINAL_STATIC / original_relative).read_text(encoding="utf-8") for retired in AUDITED_CSS_RETIREMENTS.get(original_relative, ()): source, replacement, *count_override = ( retired if isinstance(retired, tuple) else (retired, "") ) expected_count = count_override[0] if count_override else 1 replacement_count = count_override[1] if len(count_override) > 1 else 1 testcase.assertEqual(original.count(source), expected_count, source) original = original.replace(source, replacement, replacement_count) for replacement in AUDITED_CSS_REPLACEMENTS.get(original_relative, ()): source, target, *count_override = replacement expected_count = count_override[0] if count_override else 1 replacement_count = count_override[1] if len(count_override) > 1 else expected_count testcase.assertEqual(original.count(source), expected_count, source) original = original.replace(source, target, replacement_count) testcase.assertEqual( (FRONTEND_ROOT / target_relative).read_text(encoding="utf-8"), original, original_relative, ) return testcase.assertEqual( sha256(FRONTEND_ROOT / target_relative), sha256(ORIGINAL_STATIC / original_relative), original_relative, ) def assert_page_prefix_matches(testcase, page_relative: str) -> None: original = (ORIGINAL_STATIC / page_relative).read_text(encoding="utf-8") migrated = (FRONTEND_ROOT / page_relative).read_text(encoding="utf-8") testcase.assertTrue( migrated.startswith(original.rstrip("\n") + "\n\n"), page_relative, )