Add BaZi translation skill

This commit is contained in:
zfusx
2026-06-03 18:17:37 +08:00
commit 926acb0fe8
8 changed files with 671 additions and 0 deletions
+276
View File
@@ -0,0 +1,276 @@
#!/usr/bin/env python3
"""Scan source files for suspicious BaZi terminology translations."""
from __future__ import annotations
import argparse
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
SKILL_DIR = Path(__file__).resolve().parents[1]
DEFAULT_TERMS = SKILL_DIR / "references" / "terms.md"
EXCLUDE_DIRS = {
".git",
".hg",
".svn",
".idea",
".vscode",
"__pycache__",
"node_modules",
"vendor",
"dist",
"build",
"coverage",
".next",
".nuxt",
".venv",
"venv",
}
TEXT_EXTENSIONS = {
".c",
".cc",
".cpp",
".cs",
".css",
".csv",
".go",
".h",
".hpp",
".html",
".java",
".js",
".json",
".jsx",
".kt",
".lua",
".md",
".mjs",
".php",
".py",
".rb",
".rs",
".scss",
".sh",
".sql",
".svelte",
".swift",
".toml",
".ts",
".tsx",
".txt",
".vue",
".xml",
".yaml",
".yml",
}
@dataclass(frozen=True)
class BadTerm:
bad: str
preferred: str
zh: str
standard_en: str
reason: str
@property
def normalized(self) -> str:
return normalize_identifier(self.bad)
def split_md_row(line: str) -> list[str]:
stripped = line.strip()
if not stripped.startswith("|") or not stripped.endswith("|"):
return []
return [cell.strip().strip("`").strip() for cell in stripped.strip("|").split("|")]
def normalize_cell(value: str) -> str:
value = re.sub(r"<br\s*/?>", ",", value, flags=re.IGNORECASE)
value = value.replace("", ",").replace("", ",").replace("", ",").replace(";", ",")
return value.strip()
def normalize_identifier(value: str) -> str:
return re.sub(r"[^a-z0-9]+", "", value.lower())
def parse_bad_terms(terms_path: Path) -> list[BadTerm]:
rows: list[BadTerm] = []
header: list[str] | None = None
for raw_line in terms_path.read_text(encoding="utf-8").splitlines():
cells = split_md_row(raw_line)
if not cells:
continue
if "简体" in cells and "代码名" in cells and "避免" in cells:
header = cells
continue
if cells and all(set(cell) <= {"-"} for cell in cells):
continue
if not header or len(cells) < len(header):
continue
row = dict(zip(header, cells))
preferred = row.get("代码名", "").strip("` ")
avoid = normalize_cell(row.get("避免", ""))
if not preferred or not avoid or avoid == "-":
continue
for item in [part.strip("` ") for part in avoid.split(",")]:
if not item or item == "-":
continue
rows.append(
BadTerm(
bad=item,
preferred=preferred,
zh=row.get("简体", ""),
standard_en=row.get("标准英文", ""),
reason=f"{row.get('简体', '')} should use {preferred}",
)
)
return rows
def parse_approved_names(terms_path: Path) -> list[str]:
names: set[str] = set()
header: list[str] | None = None
for raw_line in terms_path.read_text(encoding="utf-8").splitlines():
cells = split_md_row(raw_line)
if not cells:
continue
if "简体" in cells and "代码名" in cells:
header = cells
continue
if cells and all(set(cell) <= {"-"} for cell in cells):
continue
if not header or len(cells) < len(header):
continue
row = dict(zip(header, cells))
code_name = row.get("代码名", "").strip("` ")
if code_name and code_name != "-":
names.add(code_name)
return sorted(names, key=str.lower)
def iter_files(paths: list[Path]) -> list[Path]:
found: list[Path] = []
for path in paths:
if not path.exists():
print(f"warning: path does not exist: {path}", file=sys.stderr)
continue
if path.is_file():
if is_text_candidate(path):
found.append(path)
continue
for root, dirs, files in os.walk(path):
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
for name in files:
candidate = Path(root) / name
if is_text_candidate(candidate):
found.append(candidate)
return found
def is_text_candidate(path: Path) -> bool:
if path.name.startswith(".") and path.suffix == "":
return False
return path.suffix.lower() in TEXT_EXTENSIONS
def line_has_bad_term(line: str, bad_term: BadTerm) -> bool:
bad = bad_term.bad
lower = line.lower()
identifier_like_bad_term = re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", bad) is not None
pascal_case_bad_term = bad[:1].isupper() and identifier_like_bad_term
if " " in bad:
words = [re.escape(part) for part in bad.lower().split()]
if re.search(r"\b" + r"\s+".join(words) + r"\b", lower):
return True
if identifier_like_bad_term:
bad_norm = bad_term.normalized
for identifier in re.findall(r"[A-Za-z_][A-Za-z0-9_]*", line):
if pascal_case_bad_term:
if identifier == bad:
return True
continue
if identifier[:1].isupper() and bad[:1].islower():
continue
if normalize_identifier(identifier) == bad_norm:
return True
return False
if pascal_case_bad_term:
if re.search(r"\b" + re.escape(bad) + r"\b", line):
return True
elif re.search(r"\b" + re.escape(bad.lower()) + r"\b", lower):
return True
return False
def scan(paths: list[Path], bad_terms: list[BadTerm]) -> list[tuple[Path, int, str, BadTerm]]:
issues: list[tuple[Path, int, str, BadTerm]] = []
for path in iter_files(paths):
try:
text = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
try:
text = path.read_text(encoding="utf-8-sig")
except UnicodeDecodeError:
continue
for line_no, line in enumerate(text.splitlines(), start=1):
for bad_term in bad_terms:
if line_has_bad_term(line, bad_term):
issues.append((path, line_no, line.strip(), bad_term))
return issues
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("paths", nargs="*", type=Path, help="files or directories to scan")
parser.add_argument("--terms", type=Path, default=DEFAULT_TERMS, help="path to terms.md")
parser.add_argument("--no-fail", action="store_true", help="always exit 0")
parser.add_argument("--list-approved", action="store_true", help="print approved code names and exit")
args = parser.parse_args()
if not args.terms.exists():
print(f"terms file not found: {args.terms}", file=sys.stderr)
return 2
if args.list_approved:
for name in parse_approved_names(args.terms):
print(name)
return 0
if not args.paths:
parser.error("provide at least one file or directory")
bad_terms = parse_bad_terms(args.terms)
issues = scan(args.paths, bad_terms)
if not issues:
print("No suspicious BaZi terminology names found.")
return 0
for path, line_no, line, bad_term in issues:
print(f"{path}:{line_no}: {bad_term.bad} -> {bad_term.preferred}")
print(f" zh: {bad_term.zh}; standard_en: {bad_term.standard_en}")
print(f" line: {line}")
print(f"\n{len(issues)} issue(s) found.")
return 0 if args.no_fail else 1
if __name__ == "__main__":
raise SystemExit(main())