# -*- 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 ライセンスで配布しています。