#!/usr/bin/env python3 """Guard AwoooP public copy against legacy manual-gate defaults. This guard is static and read-only. It prevents AwoooP pages and serialized message payloads from reintroducing old manual-gate wording as the default state for low / medium / high controlled automation. """ from __future__ import annotations import argparse import json from pathlib import Path from typing import Any TEXT_FILES = [ Path("apps/web/messages/zh-TW.json"), Path("apps/web/messages/en.json"), ] AWOOOP_SOURCE_ROOT = Path("apps/web/src/app/[locale]/awooop") ALERTS_ROUTE = AWOOOP_SOURCE_ROOT / "alerts" / "page.tsx" FORBIDDEN_FRAGMENTS = [ "待人工決策", "等待人工決策", "阻塞與人工閘門", "人工接手", "人工決策佇列", "人工關卡", "人工 Gate", "人工 gate", "人工閘門", "人工升級", "待 owner 複核", "未批准不會執行", "等 負責人審查", "owner review", "owner packet", "manual gate", "manual handoff", ] REQUIRED_FRAGMENTS = [ "待 AI 受控決策", "阻塞與 AI 受控隊列", "AI 處置包與工作項", "受控執行邊界", "受控授權閘門", "controlled gate", "controlled review", "AI 受控 Gate", "AI 受控補齊:NO_ACTION", "等待 AI 受控補齊", "受控驗收流程", "負責人脫敏證據收件", "Controlled Review", "AI 受控 review", ] AWOOOP_LIVE_FORBIDDEN_FRAGMENTS = [ "人工 Gate", "人工介入", "待人工", "等待人工", "人工回覆", "人工收件", "人工判斷", "人工交接", "人工方案", "人工確認無需動作", "人工:{human}", "需要人工={human}", "人工覆核", "人工檢查", "Owner review", "Owner Review", "負責人審查", ] AWOOOP_ALLOWED_LEGACY_PATH_PREFIXES = [ "awooop.approvals.legacyHitl.", ] def _iter_guarded_files(root: Path) -> list[Path]: files = [root / path for path in TEXT_FILES] source_root = root / AWOOOP_SOURCE_ROOT files.extend(sorted(source_root.rglob("*.tsx"))) return files def _collect_awooop_message_violations(path: Path, root: Path) -> list[str]: data = json.loads(path.read_text(encoding="utf-8")) awooop = data.get("awooop") if not isinstance(awooop, dict): return [f"{path.relative_to(root)}: missing awooop namespace"] violations: list[str] = [] def walk(value: Any, parts: list[str]) -> None: if isinstance(value, dict): for key, child in value.items(): walk(child, [*parts, key]) return if isinstance(value, list): for index, child in enumerate(value): walk(child, [*parts, str(index)]) return if not isinstance(value, str): return dotted = ".".join(parts) if any(dotted.startswith(prefix) for prefix in AWOOOP_ALLOWED_LEGACY_PATH_PREFIXES): return for fragment in AWOOOP_LIVE_FORBIDDEN_FRAGMENTS: if fragment in value: relative = path.relative_to(root) violations.append(f"{relative}:{dotted}: forbidden live AwoooP copy {fragment!r}") walk(awooop, ["awooop"]) return violations def _collect_forbidden_line_violations(path: Path, root: Path, text: str) -> list[str]: violations: list[str] = [] for line_number, line in enumerate(text.splitlines(), start=1): for fragment in FORBIDDEN_FRAGMENTS: if fragment in line: relative = path.relative_to(root) violations.append(f"{relative}:{line_number}: forbidden {fragment!r}") return violations def validate(root: Path) -> None: root = root.resolve() violations: list[str] = [] guarded_text = [] for path in _iter_guarded_files(root): if not path.exists(): violations.append(f"{path.relative_to(root)}: missing guarded file") continue text = path.read_text(encoding="utf-8") guarded_text.append(text) if path.name.endswith(".json"): violations.extend(_collect_awooop_message_violations(path, root)) violations.extend(_collect_forbidden_line_violations(path, root, text)) alerts_route = root / ALERTS_ROUTE if not alerts_route.exists(): violations.append(f"{ALERTS_ROUTE}: missing AwoooP Alerts route") else: route_text = alerts_route.read_text(encoding="utf-8") if "#ai-alert-card-delivery-readback" not in route_text: violations.append(f"{ALERTS_ROUTE}: missing ai-alert-card-delivery-readback redirect target") combined_text = "\n".join(guarded_text) for fragment in REQUIRED_FRAGMENTS: if fragment not in combined_text: violations.append(f"messages/source: missing required controlled automation text {fragment!r}") if violations: formatted = "\n".join(violations[:40]) raise SystemExit(f"BLOCKED awooop_controlled_automation_copy_guard:\n{formatted}") def main() -> int: parser = argparse.ArgumentParser(description="Validate AwoooP controlled automation public copy.") parser.add_argument("--root", default=".") args = parser.parse_args() validate(Path(args.root)) print("AWOOOP_CONTROLLED_AUTOMATION_COPY_GUARD_OK") return 0 if __name__ == "__main__": raise SystemExit(main())