初期検討段階のドキュメントと運用ルールを取り込む

ルール策定前に書かれた設計ドキュメント7点を、以降の基準点として
そのままの状態で記録する。CLAUDE.md のルール1〜4は未適用。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MdLT5x5ka5y6CaWLeE3EKb
This commit is contained in:
2026-09-18 08:43:01 +09:00
co-authored by Claude Opus 5
commit 55243f626d
12 changed files with 2395 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""CLAUDE.md の運用ルール1〜4のうち、機械的に判定できる項目を検査する。
使い方:
python .claude/skills/doc-lint/doc-lint.py [ファイル...]
引数なしなら リポジトリ直下の *.md と journal/*.md を対象にする。
ERROR が1件でもあれば終了コード 1。
"""
import sys, re, os, io, glob
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
TAG = re.compile(r"\[推測:(P-\d+)\]")
ENTRY = re.compile(r"^###\s+(P-\d+)\b")
H2 = re.compile(r"^##\s+(.*?)\s*$")
MDLINK = re.compile(r"\]\(([^)]+\.md[^)]*)\)")
JOURNALFN= re.compile(r"^\d{4}-\d{2}-\d{2}-.+\.md$")
PROMOTE = re.compile(r"^\s*[-*]\s*格上げ条件\s*[::]\s*\S")
# ルールブック自身は、アンチパターンを例として引用するため散文ヒューリスティックの対象外
PROSE_EXEMPT = {"CLAUDE.md"}
issues = []
def add(sev, path, line, msg):
issues.append((sev, path, line, msg))
def load(path):
"""コードフェンス内を空行に潰した (行番号, 本文) の列を返す。"""
raw = io.open(path, encoding="utf-8").read().splitlines()
out, fence = [], False
for i, l in enumerate(raw, 1):
if l.lstrip().startswith("```"):
fence = not fence
out.append((i, ""))
continue
out.append((i, "" if fence else l))
return out
def section(lines, title):
"""## <title> 配下の (行番号, 本文) を返す。無ければ None。"""
start = None
for idx, (n, l) in enumerate(lines):
m = H2.match(l)
if m and m.group(1) == title:
start = idx + 1
break
if start is None:
return None
body = []
for n, l in lines[start:]:
if H2.match(l):
break
body.append((n, l))
return body
def check(path):
rel = path.replace(os.sep, "/")
base = os.path.basename(rel)
is_journal = rel.startswith("journal/")
lines = load(path)
unconf = section(lines, "未確認事項")
rejected = section(lines, "検討済み・非採用")
unconf_span = set(n for n, _ in unconf) if unconf else set()
# --- ルール4: 推測タグと未確認事項の対応 ---
entries = set()
if unconf is not None:
blocks = [] # [(pid, 行番号, [本文行])] 重複IDも個別に検査する
for n, l in unconf:
m = ENTRY.match(l)
if m:
pid = m.group(1)
if pid in entries:
add("ERROR", rel, n, "未確認事項のID %s が重複している" % pid)
entries.add(pid)
blocks.append((pid, n, []))
elif blocks:
blocks[-1][2].append(l)
for pid, ln, body in blocks:
if not any(PROMOTE.match(x) for x in body):
add("ERROR", rel, ln,
"%s に「- 格上げ条件:」の行がない(何を確認すれば事実に格上げできるか)" % pid)
for n, l in lines:
if n in unconf_span:
continue
for pid in TAG.findall(l):
if unconf is None or pid not in entries:
add("ERROR", rel, n,
"本文の [推測:%s] に対応する項目が ## 未確認事項 にない" % pid)
# 逆向き(本文タグのない未確認事項)は「未解決の問い」として正当なのでエラーにしない
# --- ルール3: journal への/からのリンク禁止 ---
for n, l in lines:
for target in MDLINK.findall(l):
if target.lstrip("./").startswith("journal/"):
add("ERROR", rel, n, "journal へのリンクが張られている: %s" % target)
elif is_journal:
add("ERROR", rel, n, "journal から他ファイルへのリンクが張られている: %s" % target)
# --- ルール1: 検討済み・非採用 ---
if rejected is not None:
items = [(n, l) for n, l in rejected if l.strip().startswith(("-", "*"))]
for n, l in items:
if not re.search(r"\(journal/[^)]+\.md\)", l):
add("ERROR", rel, n,
"検討済み・非採用の項目が journal ファイル名を参照していない")
if len(items) > 5:
add("WARN", rel, items[5][0],
"検討済み・非採用が %d 件(目安5件)。古いものから落とす" % len(items))
# --- journal 固有 ---
if is_journal:
if not JOURNALFN.match(base):
add("ERROR", rel, 1, "journal のファイル名が YYYY-MM-DD-<topic>.md 形式でない")
head = " ".join(l for n, l in lines[:20])
if "結論" not in head:
add("WARN", rel, 1, "冒頭20行に結論が見当たらない(結論を先頭に置く)")
# --- ルール1: 調査・比較の混入(要目視確認) ---
if not is_journal and base not in PROSE_EXEMPT:
# 検討済み・非採用 は却下案を列挙する場所なので対象外
skip = set(n for n, _ in rejected) if rejected else set()
for n, l in lines:
if n in skip:
continue
if "メリット" in l and "デメリット" in l:
add("WARN", rel, n, "比較表らしき記述(メリット/デメリット)。journal 行きでは?")
elif re.search(r"却下|不採用|見送っ|も検討したが|案[A-Z]|[A-Z]案", l):
add("WARN", rel, n, "却下案の検討過程らしき記述。制約文に書き換えるか journal へ")
elif re.search(r"優先度", l) and l.lstrip().startswith("#"):
add("WARN", rel, n, "優先度評価は journal 行きでは?")
def main(argv):
targets = argv[1:]
if not targets:
targets = sorted(glob.glob("*.md")) + sorted(glob.glob("journal/*.md"))
if not targets:
print("対象ファイルなし"); return 0
for t in targets:
if os.path.isfile(t):
check(t)
else:
print("見つからない: %s" % t)
errs = [i for i in issues if i[0] == "ERROR"]
warns = [i for i in issues if i[0] == "WARN"]
for sev, path, line, msg in errs + warns:
print("%-5s %s:%d %s" % (sev, path, line, msg))
print("\n%d files / ERROR %d / WARN %d" % (len(targets), len(errs), len(warns)))
return 1 if errs else 0
if __name__ == "__main__":
sys.exit(main(sys.argv))