初期検討段階のドキュメントと運用ルールを取り込む
ルール策定前に書かれた設計ドキュメント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:
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: doc-lint
|
||||
description: ドキュメントがCLAUDE.mdの運用ルール1〜4に沿っているか検査する。推測タグと未確認事項の対応漏れ、journalへのリンク、比較表の混入などを検出する。
|
||||
argument-hint: [ファイル...]
|
||||
---
|
||||
|
||||
# ドキュメントのルール違反を検査する
|
||||
|
||||
機械的に判定できる項目はスクリプトで検出し、判断が要る項目だけ目視で見る。
|
||||
**検出して報告するだけで、勝手に直さない。** 修正は内容を確認してもらってから。
|
||||
|
||||
## 1. スクリプトを走らせる
|
||||
|
||||
```
|
||||
python .claude/skills/doc-lint/doc-lint.py [ファイル...]
|
||||
```
|
||||
|
||||
引数なしで直下の `*.md` と `journal/*.md` を全部見る。ERROR があれば終了コード 1。
|
||||
|
||||
検出する項目:
|
||||
|
||||
| 種別 | 内容 |
|
||||
|---|---|
|
||||
| ERROR | 本文の `[推測:P-n]` に対応する `## 未確認事項` の項目がない |
|
||||
| ERROR | `## 未確認事項` の項目に `- 格上げ条件:` の行がない |
|
||||
| ERROR | `## 未確認事項` のIDが重複している |
|
||||
| ERROR | journal へのリンク / journal から他ファイルへのリンク |
|
||||
| ERROR | `## 検討済み・非採用` の項目が journal ファイル名を参照していない |
|
||||
| ERROR | journal のファイル名が `YYYY-MM-DD-<topic>.md` 形式でない |
|
||||
| WARN | `## 検討済み・非採用` が5件を超えた |
|
||||
| WARN | journal の冒頭20行に結論が見当たらない |
|
||||
| WARN | メインドキュメントに比較表・優先度評価・却下案の記述らしきもの |
|
||||
|
||||
**本文タグのない `## 未確認事項` の項目はエラーにしない。** 「まだ本文に書けない未解決の問い」として正当。
|
||||
|
||||
WARN の散文検出は当たりが粗い。取りこぼしも誤検出もあるので、出た/出ないを結論にしない。
|
||||
|
||||
## 2. スクリプトで見られない項目を確認する
|
||||
|
||||
こちらが本体。ファイルを読んで判断する。
|
||||
|
||||
- **「なぜBではないのか」が理由として残っていないか。**
|
||||
却下理由ではなく、現在効いている制約として書かれているか。
|
||||
- ✗ `階層構造はKLEが非対応なので諦めた`
|
||||
- ○ `階層クラスターはKLE JSONに出力しない(KLE互換性の制約)`
|
||||
- **裏取りしていない記述にタグが付いていないか。** 検査項目の中で最も重要で、
|
||||
機械では判定できない。「〜できるはず」「〜と考えられる」「実現可能」で実際に試していない
|
||||
記述を探し、`[推測:P-n]` が付いているか確認する。
|
||||
- **同じ事実が複数ドキュメントに複製されていないか。** 片方だけ古くなる。
|
||||
- **journal の冒頭20行で、結論と選択肢が分かるか。** 見出しの有無ではなく中身を読む。
|
||||
- **コミット規約**(必要なら `git log --oneline` を見る)
|
||||
- 1コミット1決定になっているか
|
||||
- journal を伴う決定のメッセージに `Journal:` 行があるか
|
||||
|
||||
## 3. 報告する
|
||||
|
||||
ERROR / WARN / 目視で見つけたもの を分けて出す。
|
||||
修正案は出してよいが、適用は確認を取ってから。事実関係の誤りを見つけた場合も、直さず指摘する。
|
||||
|
||||
## 注意
|
||||
|
||||
- 運用ルール策定前に書かれた既存ドキュメントは、まだルール1〜4が適用されていない。
|
||||
WARN が大量に出るのは想定内で、再編(`/restructure-doc`)のタイミングで解消する。
|
||||
再編前のファイルの WARN を個別に直そうとしない。
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user