#!/usr/bin/env python3 """classify_workspace.py — 工作区半自动分类判定(规范 catalog 驱动) 扫描目标目录,按 catalog.yaml 判定顺序 + 信号扫描,输出「建议类型 + 依据 + 置信度」。 由人确认后写入 .workspace.yaml。不自动搬任何文件。 用法: python3 classify_workspace.py [--catalog PATH] [--write] [--quiet] --catalog 规范 catalog.yaml 路径(默认自动找:环境变量 STANDARD_ROOT 或 /srv/share/docs/standards/workspace-directory-standard/catalog.yaml) --write 确认后写入 .workspace.yaml(交互询问;--yes 跳过询问) --yes 配合 --write 自动确认 --quiet 只输出一行结论 信号说明(catalog.yaml signals): 扫描顶层文件/目录名 + 少量文件内容特征,为每个类别给出命中信号列表。 最终建议 = 判定顺序中首个「命中其强制信号或人工问答为是」的类别。 """ import argparse import os import sys from pathlib import Path DEFAULT_CATALOG = "/srv/share/docs/standards/workspace-directory-standard/catalog.yaml" # 强制信号:这些特征存在即强指向该类别(比 catalog signals 更强) STRONG_SIGNALS = { # W1 三层结构需全部齐全(防服务型误判:服务也有 projects/) "W1": ["ALL3"], "W2": ["systemd", "Dockerfile", "docker-compose.yml", "docker-compose.yaml", "supervisord.conf", "*.service", "*server*", "*_api", "*bridge*", "*.db", "*runtime*"], "W3": ["pyproject.toml", "package.json", "Cargo.toml", "go.mod", "setup.py", "requirements.txt", "pom.xml"], "W4": ["inbox", "domains", "indexes", "*.db", "search*", "semantic_search", "vector*", "*knowledge*", "*memory*", "_规范", "_模板", "*域*"], # 知识库/检索特征 "W6": ["inventory", "shared", "共享", "migration"], } def load_catalog(path: Path) -> dict: try: import yaml except ImportError: print("ERROR: 需要 pyyaml(pip install pyyaml)", file=sys.stderr) sys.exit(1) with open(path, encoding="utf-8") as f: return yaml.safe_load(f) def scan_signals(target: Path) -> dict: """扫描目录,返回每个类别的命中信号列表""" hits = {k: [] for k in STRONG_SIGNALS} names = [p.name for p in target.iterdir()] if target.is_dir() else [] lower_names = [n.lower() for n in names] for cls, sigs in STRONG_SIGNALS.items(): for s in sigs: if s == "ALL3": # W1 三层结构齐全判定:methods+projects+deliverables 全存在 need = {"methods", "projects", "deliverables"} present = need & set(lower_names) if len(present) == 3: hits[cls].append("methods+projects+deliverables 齐全") continue if s.startswith("*") and s.endswith("*"): pat = s.strip("*").lower() if any(pat in n for n in lower_names): hits[cls].append(s) elif s.startswith("*"): pat = s[1:].lower() if any(n.endswith(pat) for n in lower_names): hits[cls].append(s) elif s.endswith("*"): pat = s[:-1].lower() if any(n.startswith(pat) for n in lower_names): hits[cls].append(s) else: if s.lower() in lower_names: hits[cls].append(s) # 内容嗅探:文件内关键信号(只读前几层,不深扫) def sniff(path: Path, sigs: list, limit: int = 4000) -> list: found = [] try: with open(path, "r", encoding="utf-8", errors="ignore") as f: head = f.read(limit) for s in sigs: if s.lower() in head.lower(): found.append(s) except Exception: pass return found for p in target.iterdir(): if p.is_file() and p.suffix.lower() in (".py", ".sh", ".yaml", ".yml", ".json", ".toml", ".ini"): if p.name == ".workspace.yaml": continue if "Dockerfile" in hits["W2"] or "docker-compose" in str(p).lower(): pass for cls, sigs in { "W2": ["systemd", "uvicorn", "gunicorn", "0.0.0.0", "127.0.0.1", "FastAPI", "Flask", "scheduler"], "W3": ["pytest", "unittest", "if __name__", "typer", "click", "setuptools"], }.items(): found = sniff(p, sigs) for s in found: if s not in hits[cls]: hits[cls].append(s) return hits def decide(target: Path, hits: dict, order: list, questions: dict, interactive: bool = True) -> tuple: """判定顺序执行:首个命中强信号或人工确认为是 → 建议类别""" reasons = [] for cls in order: strong = hits.get(cls, []) if strong: reasons.append(f"检测到 {cls} 特征: {', '.join(strong)}") return cls, reasons, 0.9 # 无强信号 → 走 catalog 决策问题(交互) if not interactive: return "W5", ["非交互模式,无强特征,兜底 W5 单项目(请人工复核)"], 0.4 print("未检测到强特征目录,请回答几个问题:") for cls in order: q = questions.get(cls) if not q: continue while True: ans = input(f" {q} [y/N] ").strip().lower() if ans in ("y", "yes"): reasons.append(f"问答确认: {q}") return cls, reasons, 0.7 if ans in ("n", "no", ""): break print(" 请输入 y 或 n") return "W5", ["无强特征且未命中其他类,兜底 W5 单项目"], 0.6 def write_workspace_yaml(target: Path, cls: str, catalog: dict) -> None: """写入 .workspace.yaml(登记卡)""" cfg = catalog["classes"][cls] tpl = f"""schema_version: 1 name: {target.name} class: {cls} profile: {cfg.get('en', '')} business_domain: 待定 owner: user primary_location: 待定 status: active created_at: {Path('_').stem or ''} last_reviewed_at: {Path('_').stem or ''} canonical_path: {target} related_services: [] git_remote: "" """ # 用当前日期 import datetime today = datetime.date.today().isoformat() tpl = tpl.replace(f"created_at: {Path('_').stem or ''}", f"created_at: {today}") \ .replace(f"last_reviewed_at: {Path('_').stem or ''}", f"last_reviewed_at: {today}") (target / ".workspace.yaml").write_text(tpl, encoding="utf-8") print(f"已写入: {target / '.workspace.yaml'}") def main(): ap = argparse.ArgumentParser(description="工作区半自动分类判定") ap.add_argument("target", help="目标目录路径") ap.add_argument("--catalog", default=os.environ.get("STANDARD_ROOT", DEFAULT_CATALOG)) ap.add_argument("--write", action="store_true", help="确认后写入 .workspace.yaml") ap.add_argument("--yes", action="store_true", help="跳过交互确认") ap.add_argument("--quiet", action="store_true") args = ap.parse_args() target = Path(args.target) if not target.is_dir(): print(f"ERROR: 不是目录: {target}", file=sys.stderr) sys.exit(1) catalog_path = Path(args.catalog) if not catalog_path.exists(): print(f"ERROR: catalog 不存在: {catalog_path}", file=sys.stderr) sys.exit(1) catalog = load_catalog(catalog_path) order = catalog["decision_order"] questions = { cls: info["decisions"][0]["question"] for cls, info in catalog["classes"].items() if info.get("decisions") } hits = scan_signals(target) cls, reasons, conf = decide(target, hits, order, questions, interactive=not args.quiet) if args.quiet: print(f"{cls} (置信度 {conf:.0%})") return print(f"\n建议分类: {cls} — {catalog['classes'][cls]['name']} (置信度 {conf:.0%})") for r in reasons: print(f" 依据: {r}") print(f" 模板: {catalog['classes'][cls]['template']}") print(f" 采用 v2 三层结构: {'是' if catalog['classes'][cls].get('uses_v2') else '否'}") if args.write: if args.yes: write_workspace_yaml(target, cls, catalog) else: confirm = input("\n确认写入 .workspace.yaml? [y/N] ").strip().lower() if confirm in ("y", "yes"): write_workspace_yaml(target, cls, catalog) else: print("未写入。可手动编辑 .workspace.yaml 或调整分类后重跑。") if __name__ == "__main__": main()