Some checks failed
Ansible / Reboot Recovery Contract / validate (push) Successful in 1m15s
CD Pipeline / tests (push) Failing after 1m8s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
Code Review / ai-code-review (push) Successful in 27s
180 lines
5.3 KiB
Python
Executable File
180 lines
5.3 KiB
Python
Executable File
#!/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())
|