check_prompts.py

scripts/check_prompts.py(129 行)

check_prompts.py
# -*- coding: utf-8 -*-
"""Lv.1 プロンプトと Lv.2 エージェントの食い違いチェック(保守用・標準ライブラリだけ)

同じレビュアーが2か所にいる:
  - prompts/<名前>.md              … チャットに貼る版(Lv.1)
  - .claude/agents/<名前>-reviewer.md … Claude Code のエージェント版(Lv.2)
片方だけ直すと、同じレビュアーなのに見るところや点数の名前がずれていく。それを見つけるためのスクリプト。

確かめること:
  1. 5人のレビュアーそれぞれについて、2つのファイルが両方あるか(無ければエラー)
  2. スコア行のラベル(「法務スコア」など)が2つのファイルで一致するか(ずれていればエラー)
     ※ラベルはファイルから読み取る。このスクリプトには対応関係(どのファイル同士が組か)だけを書く
  3. 番号付きのチェック項目(「1. **見出し**」の形)の見出しが、片方にしか無いもの(警告だけ)
     ※チャット版は短く要約しているので、一方の見出しがもう一方に含まれていれば同じ項目とみなす

使い方(プロジェクトのルートで):
    python -X utf8 scripts/check_prompts.py
    python -X utf8 scripts/check_prompts.py --root <テンプレートのフォルダ>

終了コード: ファイルが無い・ラベルがずれている → 1、それ以外(警告だけ・問題なし) → 0
"""
import argparse
import io
import re
import sys
from pathlib import Path

sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")

# チャット版の名前 → エージェント版のファイル名(対応関係だけをここに持つ)
PAIRS = {
    "legal": "legal-reviewer.md",
    "consistency": "consistency-reviewer.md",
    "growth": "growth-reviewer.md",
    "readers-panel": "readers-panel.md",
    "confidentiality": "confidentiality-reviewer.md",
}

# スコア行: **法務スコア: XX/100点 のような形からラベルを取り出す
SCORE_RE = re.compile(r"\*\*([^*\s::`]+スコア)[::]\s*XX/100")
# 番号付きのチェック項目: 行頭の「1. **見出し**」
ITEM_RE = re.compile(r"^\s*\d+\.\s+\*\*(.+?)\*\*", re.M)


def score_labels(text: str) -> list:
    """スコア行のラベルを、出てきた順に重複なしで返す"""
    seen = []
    for m in SCORE_RE.finditer(text):
        if m.group(1) not in seen:
            seen.append(m.group(1))
    return seen


def item_titles(text: str) -> list:
    """番号付きチェック項目の見出しを返す(コードブロックの中は除く)"""
    text = re.sub(r"```.*?```", "", text, flags=re.S)
    return [m.group(1).strip() for m in ITEM_RE.finditer(text)]


def _norm(s: str) -> str:
    """比較用に、空白と記号を落とす"""
    return re.sub(r"[\s・、。()()「」【】//::]", "", s)


def _has_match(title: str, others: list) -> bool:
    """title と同じ項目が others にあるか(どちらかがもう一方を含めば同じとみなす)"""
    t = _norm(title)
    for o in others:
        n = _norm(o)
        if t and n and (t in n or n in t):
            return True
    return False


def main() -> int:
    ap = argparse.ArgumentParser(description="prompts/ と .claude/agents/ のレビュアー定義の食い違いを調べる")
    ap.add_argument("--root", default="", help="プロジェクトのルート(省略時は scripts/ の1つ上)")
    a = ap.parse_args()
    root = Path(a.root).resolve() if a.root else Path(__file__).resolve().parent.parent

    errors = 0
    warnings = 0
    for name, agent_file in PAIRS.items():
        p_path = root / "prompts" / f"{name}.md"
        a_path = root / ".claude" / "agents" / agent_file
        print(f"■ {name}  (prompts/{name}.md ↔ .claude/agents/{agent_file})")

        missing = [str(p.relative_to(root)) for p in (p_path, a_path) if not p.is_file()]
        if missing:
            for m in missing:
                print(f"  ❌ ファイルがありません: {m}")
            errors += 1
            continue

        p_text = p_path.read_text(encoding="utf-8", errors="replace")
        a_text = a_path.read_text(encoding="utf-8", errors="replace")

        # スコア行のラベル
        p_labels, a_labels = score_labels(p_text), score_labels(a_text)
        if not p_labels or not a_labels:
            print(f"  ❌ スコア行が見つかりません(チャット版: {p_labels or 'なし'} / エージェント版: {a_labels or 'なし'})")
            errors += 1
        elif sorted(p_labels) != sorted(a_labels):
            print(f"  ❌ スコアのラベルが違います(チャット版: {'・'.join(p_labels)} / エージェント版: {'・'.join(a_labels)})")
            errors += 1
        else:
            print(f"  ✅ スコアのラベル一致: {'・'.join(p_labels)}")

        # 番号付きチェック項目(警告だけ)
        p_items, a_items = item_titles(p_text), item_titles(a_text)
        only_p = [t for t in p_items if not _has_match(t, a_items)]
        only_a = [t for t in a_items if not _has_match(t, p_items)]
        for t in only_p:
            print(f"  ⚠️ チャット版にだけある項目: {t}")
        for t in only_a:
            print(f"  ⚠️ エージェント版にだけある項目: {t}")
        warnings += len(only_p) + len(only_a)
        if not only_p and not only_a:
            print(f"  ✅ チェック項目の見出しは対応しています({len(p_items)} / {len(a_items)} 項目)")

    print()
    print(f"結果: エラー {errors} 件・警告 {warnings} 件")
    if warnings:
        print("  警告は「片方にしか無い項目」です。チャット版は短くまとめているので、意図した差なら直さなくてかまいません。")
    return 1 if errors else 0


if __name__ == "__main__":
    raise SystemExit(main())

MIT ライセンスで配布しています。