Files
DashBoard/scripts/check_full_modernization_gates.py
egg 7cb0985b12 feat(modernization): full architecture blueprint with hardening follow-up
Implement phased modernization infrastructure for transitioning from
multi-page legacy routing to SPA portal-shell architecture, plus
post-delivery hardening fixes for policy loading, fallback consistency,
and governance drift detection.

Key changes:
- Add route contract enrichment with scope/visibility/compatibility policies
- Canonical 302 redirects from legacy direct-entry to /portal-shell/ routes
- Asset readiness enforcement and runtime fallback retirement for in-scope routes
- Shared feature-flag helpers (env > config > default) replacing duplicated _to_bool
- Defensive copy for lru_cached policy payloads preventing mutation corruption
- Unified retired-fallback response helper across app and blueprint routes
- Frontend/backend route-contract cross-validation in governance gates
- Shell CSS token fallback values for routes rendered outside shell scope
- Local-safe .env.example defaults with production recommendation comments
- Legacy contract fallback warning logging and single-hop redirect optimization

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 11:26:02 +08:00

461 lines
17 KiB
Python
Executable File

#!/usr/bin/env python3
"""Run governance/quality/readiness checks for full modernization change."""
from __future__ import annotations
import argparse
import json
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
DOCS_DIR = ROOT / "docs" / "migration" / "full-modernization-architecture-blueprint"
SCOPE_MATRIX_FILE = DOCS_DIR / "route_scope_matrix.json"
ROUTE_CONTRACT_FILE = DOCS_DIR / "route_contracts.json"
EXCEPTION_REGISTRY_FILE = DOCS_DIR / "exception_registry.json"
QUALITY_POLICY_FILE = DOCS_DIR / "quality_gate_policy.json"
ASSET_MANIFEST_FILE = DOCS_DIR / "asset_readiness_manifest.json"
KNOWN_BUG_BASELINE_FILE = DOCS_DIR / "known_bug_baseline.json"
MANUAL_ACCEPTANCE_FILE = DOCS_DIR / "manual_acceptance_records.json"
BUG_REVALIDATION_FILE = DOCS_DIR / "bug_revalidation_records.json"
STYLE_INVENTORY_FILE = DOCS_DIR / "style_inventory.json"
OUTPUT_REPORT_FILE = DOCS_DIR / "quality_gate_report.json"
FRONTEND_ROUTE_CONTRACT_FILE = ROOT / "frontend" / "src" / "portal-shell" / "routeContracts.js"
GLOBAL_SELECTOR_PATTERN = re.compile(r"(^|\\s)(:root|body)\\b", re.MULTILINE)
FRONTEND_ROUTE_ENTRY_PATTERN = re.compile(
r"""['"](?P<key>/[^'"]+)['"]\s*:\s*buildContract\(\s*{(?P<body>.*?)}\s*\)""",
re.DOTALL,
)
FRONTEND_ROUTE_FIELD_PATTERN = re.compile(r"""route\s*:\s*['"](?P<route>/[^'"]+)['"]""")
FRONTEND_SCOPE_FIELD_PATTERN = re.compile(r"""scope\s*:\s*['"](?P<scope>[^'"]+)['"]""")
SHELL_TOKEN_VAR_PATTERN = re.compile(r"""var\(\s*(--portal-[\w-]+)(?P<fallback>\s*,[^)]*)?\)""")
@dataclass
class CheckReport:
mode: str
errors: list[str] = field(default_factory=list)
warnings: list[str] = field(default_factory=list)
info: list[str] = field(default_factory=list)
def fail(self, message: str) -> None:
self.errors.append(message)
def warn(self, message: str) -> None:
self.warnings.append(message)
def note(self, message: str) -> None:
self.info.append(message)
def to_dict(self) -> dict[str, Any]:
return {
"mode": self.mode,
"errors": self.errors,
"warnings": self.warnings,
"info": self.info,
"passed": not self.errors,
}
def _load_json(path: Path, *, default: dict[str, Any] | None = None) -> dict[str, Any]:
if not path.exists():
return dict(default or {})
return json.loads(path.read_text(encoding="utf-8"))
def _display_path(path: Path) -> str:
try:
return str(path.relative_to(ROOT))
except ValueError:
return str(path)
def _route_css_targets() -> dict[str, list[Path]]:
return {
"/wip-overview": [ROOT / "frontend/src/wip-overview/style.css"],
"/wip-detail": [ROOT / "frontend/src/wip-detail/style.css"],
"/hold-overview": [ROOT / "frontend/src/hold-overview/style.css"],
"/hold-detail": [ROOT / "frontend/src/hold-detail/style.css"],
"/hold-history": [ROOT / "frontend/src/hold-history/style.css"],
"/resource": [ROOT / "frontend/src/resource-status/style.css"],
"/resource-history": [ROOT / "frontend/src/resource-history/style.css"],
"/qc-gate": [ROOT / "frontend/src/qc-gate/style.css"],
"/job-query": [ROOT / "frontend/src/job-query/style.css"],
"/tmtt-defect": [ROOT / "frontend/src/tmtt-defect/style.css"],
"/admin/pages": [ROOT / "src/mes_dashboard/templates/admin/pages.html"],
"/admin/performance": [ROOT / "src/mes_dashboard/templates/admin/performance.html"],
}
def _find_global_selectors(path: Path) -> list[str]:
if not path.exists():
return []
text = path.read_text(encoding="utf-8", errors="ignore")
selectors = []
for match in GLOBAL_SELECTOR_PATTERN.finditer(text):
selectors.append(match.group(2))
return sorted(set(selectors))
def _find_shell_tokens_without_fallback(path: Path) -> list[str]:
if not path.exists() or path.suffix.lower() != ".css":
return []
text = path.read_text(encoding="utf-8", errors="ignore")
missing: list[str] = []
for match in SHELL_TOKEN_VAR_PATTERN.finditer(text):
token = match.group(1)
fallback = match.group("fallback")
if fallback is None:
missing.append(token)
return sorted(set(missing))
def _check_scope_matrix(scope_matrix: dict[str, Any], report: CheckReport) -> tuple[set[str], set[str]]:
in_scope = {
str(item.get("route", "")).strip()
for item in scope_matrix.get("in_scope", [])
if str(item.get("route", "")).strip().startswith("/")
}
deferred = {
str(item.get("route", "")).strip()
for item in scope_matrix.get("deferred", [])
if str(item.get("route", "")).strip().startswith("/")
}
if not in_scope:
report.fail("scope matrix has no in-scope routes")
if "/admin/pages" not in in_scope or "/admin/performance" not in in_scope:
report.fail("scope matrix must include /admin/pages and /admin/performance")
required_deferred = {"/tables", "/excel-query", "/query-tool", "/mid-section-defect"}
if deferred != required_deferred:
report.fail("scope matrix deferred routes mismatch expected policy")
return in_scope, deferred
def _check_route_contracts(
route_contracts: dict[str, Any],
in_scope: set[str],
report: CheckReport,
) -> dict[str, dict[str, Any]]:
required_fields = {
"route",
"route_id",
"scope",
"render_mode",
"owner",
"visibility_policy",
"canonical_shell_path",
"rollback_strategy",
}
routes = route_contracts.get("routes", [])
if not isinstance(routes, list):
report.fail("route contract file routes must be a list")
return {}
route_map: dict[str, dict[str, Any]] = {}
for entry in routes:
if not isinstance(entry, dict):
report.fail("route contract entry must be object")
continue
route = str(entry.get("route", "")).strip()
if not route.startswith("/"):
report.fail(f"invalid route contract route: {route!r}")
continue
route_map[route] = entry
missing = sorted(field for field in required_fields if not str(entry.get(field, "")).strip())
if missing:
report.fail(f"{route} missing required contract fields: {', '.join(missing)}")
missing_routes = sorted(in_scope - set(route_map.keys()))
if missing_routes:
report.fail("in-scope routes missing contracts: " + ", ".join(missing_routes))
for route in sorted(in_scope):
entry = route_map.get(route)
if not entry:
continue
if str(entry.get("scope", "")).strip() != "in-scope":
report.fail(f"{route} must be scope=in-scope")
if route.startswith("/admin/") and str(entry.get("visibility_policy", "")).strip() != "admin_only":
report.fail(f"{route} must be admin_only visibility")
return route_map
def _load_frontend_route_contract_inventory(
path: Path,
report: CheckReport,
) -> dict[str, str]:
if not path.exists():
report.fail(f"frontend route contract file missing: {_display_path(path)}")
return {}
text = path.read_text(encoding="utf-8")
route_scopes: dict[str, str] = {}
for match in FRONTEND_ROUTE_ENTRY_PATTERN.finditer(text):
route_key = match.group("key")
body = match.group("body")
route_match = FRONTEND_ROUTE_FIELD_PATTERN.search(body)
scope_match = FRONTEND_SCOPE_FIELD_PATTERN.search(body)
if route_match is None:
report.fail(f"{route_key} missing route field in frontend route contract")
continue
if scope_match is None:
report.fail(f"{route_key} missing scope field in frontend route contract")
continue
route_value = route_match.group("route").strip()
scope = scope_match.group("scope").strip()
if route_value != route_key:
report.fail(
f"{route_key} frontend contract key/route mismatch "
f"(route field: {route_value})"
)
continue
route_scopes[route_key] = scope
if not route_scopes:
report.fail("frontend route contract inventory parse returned no routes")
return route_scopes
def _check_frontend_backend_route_contract_parity(
backend_route_map: dict[str, dict[str, Any]],
frontend_route_scope_map: dict[str, str],
report: CheckReport,
) -> None:
backend_routes = set(backend_route_map.keys())
frontend_routes = set(frontend_route_scope_map.keys())
backend_only = sorted(backend_routes - frontend_routes)
if backend_only:
report.fail(
"backend route contracts missing from frontend routeContracts.js: "
+ ", ".join(backend_only)
)
frontend_only = sorted(frontend_routes - backend_routes)
if frontend_only:
report.fail(
"frontend routeContracts.js routes missing from backend route contracts: "
+ ", ".join(frontend_only)
)
for route in sorted(backend_routes & frontend_routes):
backend_scope = str(backend_route_map[route].get("scope", "")).strip()
frontend_scope = str(frontend_route_scope_map[route]).strip()
if backend_scope != frontend_scope:
report.fail(
f"route scope mismatch for {route}: "
f"backend={backend_scope!r}, frontend={frontend_scope!r}"
)
def _check_quality_policy(
quality_policy: dict[str, Any],
deferred: set[str],
report: CheckReport,
) -> str:
configured_mode = str(quality_policy.get("severity_mode", {}).get("current", "warn")).strip().lower()
if configured_mode not in {"warn", "block"}:
report.fail("quality gate severity_mode.current must be warn or block")
configured_mode = "warn"
excluded = {
str(route).strip()
for route in quality_policy.get("deferred_routes_excluded", [])
if str(route).strip().startswith("/")
}
if excluded != deferred:
report.fail("quality gate deferred exclusion list must match scope matrix deferred list")
return configured_mode
def _check_exception_registry(
exception_registry: dict[str, Any],
report: CheckReport,
) -> dict[str, dict[str, Any]]:
entries = exception_registry.get("entries", [])
if not isinstance(entries, list):
report.fail("exception registry entries must be a list")
return {}
lookup: dict[str, dict[str, Any]] = {}
for entry in entries:
if not isinstance(entry, dict):
report.fail("exception entry must be object")
continue
entry_id = str(entry.get("id", "")).strip()
scope = str(entry.get("scope", "")).strip()
owner = str(entry.get("owner", "")).strip()
milestone = str(entry.get("milestone", "")).strip()
if not entry_id:
report.fail("exception entry missing id")
continue
if not scope.startswith("/"):
report.fail(f"{entry_id} missing valid scope route")
if not owner:
report.fail(f"{entry_id} missing owner")
if not milestone:
report.fail(f"{entry_id} missing milestone")
lookup[scope] = entry
return lookup
def _check_style_governance(
in_scope: set[str],
exception_by_scope: dict[str, dict[str, Any]],
report: CheckReport,
) -> None:
route_targets = _route_css_targets()
for route in sorted(in_scope):
for path in route_targets.get(route, []):
selectors = _find_global_selectors(path)
if selectors:
if route in exception_by_scope:
report.warn(
f"{route} uses global selectors {selectors} in {_display_path(path)} "
"with approved exception"
)
else:
report.fail(
f"{route} uses global selectors {selectors} in {_display_path(path)} "
"without exception"
)
missing_shell_fallbacks = _find_shell_tokens_without_fallback(path)
if not missing_shell_fallbacks:
continue
if route in exception_by_scope:
report.warn(
f"{route} uses shell tokens without fallback {missing_shell_fallbacks} "
f"in {_display_path(path)} with approved exception"
)
continue
report.fail(
f"{route} uses shell tokens without fallback {missing_shell_fallbacks} "
f"in {_display_path(path)}"
)
def _check_asset_readiness(
asset_manifest: dict[str, Any],
deferred: set[str],
report: CheckReport,
) -> None:
required = asset_manifest.get("in_scope_required_assets", {})
if not isinstance(required, dict) or not required:
report.fail("asset readiness manifest missing in_scope_required_assets")
return
declared_deferred = {
str(route).strip()
for route in asset_manifest.get("deferred_routes", [])
if str(route).strip().startswith("/")
}
if declared_deferred != deferred:
report.fail("asset readiness deferred route list must match scope matrix")
dist_dir = ROOT / "src/mes_dashboard/static/dist"
for route, assets in sorted(required.items()):
if not isinstance(assets, list) or not assets:
report.fail(f"asset manifest route {route} must define non-empty asset list")
continue
for filename in assets:
if not isinstance(filename, str) or not filename.strip():
report.fail(f"asset manifest route {route} contains invalid filename")
continue
asset_path = dist_dir / filename
if not asset_path.exists():
report.warn(f"missing dist asset for {route}: {filename}")
def _check_content_safety(
known_bug_baseline: dict[str, Any],
manual_acceptance: dict[str, Any],
bug_revalidation: dict[str, Any],
in_scope: set[str],
report: CheckReport,
) -> None:
baseline_routes = set((known_bug_baseline.get("routes") or {}).keys())
missing_baselines = sorted(in_scope - baseline_routes)
if missing_baselines:
report.fail("known bug baseline missing routes: " + ", ".join(missing_baselines))
records = manual_acceptance.get("records", [])
if not isinstance(records, list):
report.fail("manual acceptance records must be a list")
replay_records = bug_revalidation.get("records", [])
if not isinstance(replay_records, list):
report.fail("bug revalidation records must be a list")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--mode",
choices=("warn", "block"),
default=None,
help="Gate severity mode override (default: use quality_gate_policy.json)",
)
parser.add_argument(
"--report",
default=str(OUTPUT_REPORT_FILE),
help="Output report JSON path",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
scope_matrix = _load_json(SCOPE_MATRIX_FILE)
route_contracts = _load_json(ROUTE_CONTRACT_FILE)
exception_registry = _load_json(EXCEPTION_REGISTRY_FILE)
quality_policy = _load_json(QUALITY_POLICY_FILE)
asset_manifest = _load_json(ASSET_MANIFEST_FILE)
known_bug_baseline = _load_json(KNOWN_BUG_BASELINE_FILE)
manual_acceptance = _load_json(MANUAL_ACCEPTANCE_FILE, default={"records": []})
bug_revalidation = _load_json(BUG_REVALIDATION_FILE, default={"records": []})
_ = _load_json(STYLE_INVENTORY_FILE, default={})
report = CheckReport(mode=args.mode or "warn")
in_scope, deferred = _check_scope_matrix(scope_matrix, report)
backend_route_map = _check_route_contracts(route_contracts, in_scope, report)
frontend_route_scope_map = _load_frontend_route_contract_inventory(FRONTEND_ROUTE_CONTRACT_FILE, report)
_check_frontend_backend_route_contract_parity(backend_route_map, frontend_route_scope_map, report)
configured_mode = _check_quality_policy(quality_policy, deferred, report)
report.mode = args.mode or configured_mode
exception_by_scope = _check_exception_registry(exception_registry, report)
_check_style_governance(in_scope, exception_by_scope, report)
_check_asset_readiness(asset_manifest, deferred, report)
_check_content_safety(known_bug_baseline, manual_acceptance, bug_revalidation, in_scope, report)
output_path = Path(args.report)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(report.to_dict(), ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
if report.mode == "block" and report.errors:
print(f"[BLOCK] modernization gates failed with {len(report.errors)} error(s)")
for error in report.errors:
print(f"- {error}")
return 1
if report.errors:
print(f"[WARN] modernization gates found {len(report.errors)} error(s) but mode is warn")
for error in report.errors:
print(f"- {error}")
else:
print("[OK] modernization gates passed")
if report.warnings:
print(f"[WARN] additional warnings: {len(report.warnings)}")
for warning in report.warnings:
print(f"- {warning}")
return 0
if __name__ == "__main__":
raise SystemExit(main())