docs: 保留課題をbacklogへ分離し、運用ルール5を追加
未確認事項が「現在の仕様が答えに依存するもの」と「答えが出なくても進められる もの」を混ぜて抱えていた。後者を backlog/README.md へ移し、仕様書の未確認事項を 前者だけに絞る。移す際は保留中の対応範囲・暫定仕様を仕様書本文に明記し、 backlogへの移動が仮説の採用を意味しないようにする。 PHY・ELC・IDXの10件を移し、3ファイルとも未確認事項が空になった。 ルール4では、事実確認の「格上げ条件」と意思決定の「完了条件」を区別する。 仕様書側の保留課題はbacklogの区分へのリンク箇条書きだけに固定し、 末尾は保留課題 → 検討済み・非採用 → 未確認事項の順に置く。 doc-lintをルール5に対応させ、backlogを検査対象に加えた。回帰テストを追加。 Journal: journal/2026-09-22-document-backlog.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DKW58HEg8zAQoLrkWByPfD
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: doc-lint
|
||||
description: ドキュメントがCLAUDE.mdの運用ルール1〜4に沿っているか検査する。推測タグと未確認事項の対応漏れ、journalへのリンク、比較表の混入などを検出する。
|
||||
description: ドキュメントがCLAUDE.mdの運用ルール1〜5に沿っているか検査する。推測タグと未確認事項の対応漏れ、保留課題の書式・参照・必須欄、journalへのリンク、比較表の混入などを検出する。
|
||||
argument-hint: [ファイル...]
|
||||
---
|
||||
|
||||
@@ -15,15 +15,22 @@ argument-hint: [ファイル...]
|
||||
python .claude/skills/doc-lint/doc-lint.py [ファイル...]
|
||||
```
|
||||
|
||||
引数なしで直下の `*.md` と `journal/*.md` を全部見る。ERROR があれば終了コード 1。
|
||||
Python 3.9以上で実行する。引数なしでリポジトリ直下の `*.md`、`journal/*.md`、`backlog/**/*.md` を全部見る。
|
||||
実行場所にかかわらずリポジトリを基準に分類し、明示したパスは実行ディレクトリから解決する。
|
||||
ERROR があれば終了コード 1。存在しない引数のファイルもERRORにする。
|
||||
|
||||
検出する項目:
|
||||
|
||||
| 種別 | 内容 |
|
||||
|---|---|
|
||||
| ERROR | 本文の `[推測:P-n]` に対応する `## 未確認事項` の項目がない |
|
||||
| ERROR | `## 未確認事項` の項目に `- 格上げ条件:` の行がない |
|
||||
| ERROR | 本文の推測タグに対応する項目に `- 格上げ条件:` の内容がない |
|
||||
| ERROR | タグのない未確認事項に `- 格上げ条件:` または `- 完了条件:` の内容がない |
|
||||
| ERROR | `## 未確認事項` のIDが重複している |
|
||||
| ERROR | 仕様書の `## 保留課題` がリンク箇条書き以外を含む、空である、参照先ファイル・見出しがない |
|
||||
| ERROR | 末尾の管理セクションが重複する、保留課題 → 検討済み・非採用 → 未確認事項の順でない |
|
||||
| ERROR | backlogの項目IDが不正・検査対象内で重複する、必須5欄がない・空である |
|
||||
| ERROR | backlogに推測タグがある、参照先ファイルが存在しない |
|
||||
| ERROR | journal へのリンク / journal から他ファイルへのリンク |
|
||||
| ERROR | `## 検討済み・非採用` の項目が journal ファイル名を参照していない |
|
||||
| ERROR | journal のファイル名が `YYYY-MM-DD-<topic>.md` 形式でない |
|
||||
@@ -31,7 +38,12 @@ python .claude/skills/doc-lint/doc-lint.py [ファイル...]
|
||||
| WARN | journal の冒頭20行に結論が見当たらない |
|
||||
| WARN | メインドキュメントに比較表・優先度評価・却下案の記述らしきもの |
|
||||
|
||||
**本文タグのない `## 未確認事項` の項目はエラーにしない。** 「まだ本文に書けない未解決の問い」として正当。
|
||||
**本文タグのない `## 未確認事項` の項目はエラーにしない。** 実装前に判断が必要な未解決の問いとして正当。
|
||||
設計判断は「完了条件」でよい。タグなしの項目が事実確認なのか設計判断なのかは、目視で確認する。
|
||||
|
||||
backlogでは `### PHY-P-14 題名` のように区分付きIDを使い、関連仕様・未決事項・保留できる理由・再開条件・完了条件を必須とする。
|
||||
仕様書側の書式は `- [PHYの保留課題](backlog/README.md#phy)`。
|
||||
backlogは短い候補・制約を保持できるため、メインドキュメント向けの散文ヒューリスティックは適用しない。
|
||||
|
||||
WARN の散文検出は当たりが粗い。取りこぼしも誤検出もあるので、出た/出ないを結論にしない。
|
||||
|
||||
@@ -43,10 +55,12 @@ WARN の散文検出は当たりが粗い。取りこぼしも誤検出もある
|
||||
却下理由ではなく、現在効いている制約として書かれているか。
|
||||
- ✗ `階層構造はKLEが非対応なので諦めた`
|
||||
- ○ `階層クラスターはKLE JSONに出力しない(KLE互換性の制約)`
|
||||
- **裏取りしていない記述にタグが付いていないか。** 検査項目の中で最も重要で、
|
||||
- **本文が前提にする未確認の記述にタグが付いているか。** 検査項目の中で最も重要で、
|
||||
機械では判定できない。「〜できるはず」「〜と考えられる」「実現可能」で実際に試していない
|
||||
記述を探し、`[推測:P-n]` が付いているか確認する。
|
||||
- **同じ事実が複数ドキュメントに複製されていないか。** 片方だけ古くなる。
|
||||
- **保留中の対応範囲が仕様本文にあるか。** 現在の実装が答えに依存する前提をbacklogへ隠していないか。
|
||||
- **再開条件・完了条件が具体的か。** 仕様書の保留課題リンクは該当区分に向き、解決済みの課題が残っていないか。
|
||||
- **journal の冒頭20行で、結論と選択肢が分かるか。** 見出しの有無ではなく中身を読む。
|
||||
- **コミット規約**(必要なら `git log --oneline` を見る)
|
||||
- 1コミット1決定になっているか
|
||||
@@ -59,6 +73,12 @@ ERROR / WARN / 目視で見つけたもの を分けて出す。
|
||||
|
||||
## 注意
|
||||
|
||||
- 運用ルール策定前に書かれた既存ドキュメントは、まだルール1〜4が適用されていない。
|
||||
- 既存ドキュメントへのルール適用は段階的に進めている。
|
||||
WARN が大量に出るのは想定内で、再編(`/restructure-doc`)のタイミングで解消する。
|
||||
再編前のファイルの WARN を個別に直そうとしない。
|
||||
|
||||
スクリプトを変更した場合の回帰検査:
|
||||
|
||||
```
|
||||
python -B -m unittest discover -s .claude/skills/doc-lint -p test_doc_lint.py
|
||||
```
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""CLAUDE.md の運用ルール1〜4のうち、機械的に判定できる項目を検査する。
|
||||
"""CLAUDE.md の運用ルール1〜5のうち、機械的に判定できる項目を検査する。
|
||||
|
||||
使い方:
|
||||
python .claude/skills/doc-lint/doc-lint.py [ファイル...]
|
||||
引数なしなら リポジトリ直下の *.md と journal/*.md を対象にする。
|
||||
引数なしならリポジトリ直下の *.md、journal/*.md、backlog/**/*.md が対象。
|
||||
明示したパスは実行ディレクトリから解決する。
|
||||
ERROR が1件でもあれば終了コード 1。
|
||||
"""
|
||||
import sys, re, os, io, glob
|
||||
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote, urlsplit
|
||||
|
||||
TAG = re.compile(r"\[推測:(P-\d+)\]")
|
||||
ENTRY = re.compile(r"^###\s+(P-\d+)\b")
|
||||
@@ -18,6 +19,12 @@ 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")
|
||||
COMPLETE = re.compile(r"^\s*[-*]\s*完了条件\s*[::]\s*\S")
|
||||
BACKLOG_ENTRY = re.compile(r"^###\s+([A-Za-z][A-Za-z0-9_-]*-P-\d+)\s+\S")
|
||||
BACKLOG_LINK = re.compile(r"^- \[[^\]]+の保留課題\]\(([^)]+)\)$")
|
||||
BACKLOG_FIELDS = ("関連仕様", "未決事項", "保留できる理由", "再開条件", "完了条件")
|
||||
MANAGEMENT_SECTIONS = ("保留課題", "検討済み・非採用", "未確認事項")
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
|
||||
# ルールブック自身は、アンチパターンを例として引用するため散文ヒューリスティックの対象外
|
||||
PROSE_EXEMPT = {"CLAUDE.md"}
|
||||
@@ -28,7 +35,7 @@ def add(sev, path, line, msg):
|
||||
|
||||
def load(path):
|
||||
"""コードフェンス内を空行に潰した (行番号, 本文) の列を返す。"""
|
||||
raw = io.open(path, encoding="utf-8").read().splitlines()
|
||||
raw = Path(path).read_text(encoding="utf-8").splitlines()
|
||||
out, fence = [], False
|
||||
for i, l in enumerate(raw, 1):
|
||||
if l.lstrip().startswith("```"):
|
||||
@@ -55,15 +62,109 @@ def section(lines, title):
|
||||
body.append((n, l))
|
||||
return body
|
||||
|
||||
def check(path):
|
||||
rel = path.replace(os.sep, "/")
|
||||
base = os.path.basename(rel)
|
||||
is_journal = rel.startswith("journal/")
|
||||
def local_target(path, target):
|
||||
"""相対リンクをリンク元基準で解決する。外部URLは対象外。"""
|
||||
parsed = urlsplit(target)
|
||||
if parsed.scheme or parsed.netloc:
|
||||
return None, ""
|
||||
linked = (path.parent / unquote(parsed.path)).resolve() if parsed.path else path
|
||||
return linked, unquote(parsed.fragment)
|
||||
|
||||
|
||||
def check_backlog(path, rel, lines, seen_ids):
|
||||
blocks = []
|
||||
current = None
|
||||
for n, line in lines:
|
||||
if H2.match(line):
|
||||
current = None
|
||||
elif re.match(r"^###\s", line):
|
||||
match = BACKLOG_ENTRY.match(line)
|
||||
current = None
|
||||
if not match:
|
||||
add("ERROR", rel, n, "保留課題の見出しは ### <区分>-P-<番号> <題名> とする")
|
||||
continue
|
||||
pid = match.group(1)
|
||||
if pid in seen_ids:
|
||||
add("ERROR", rel, n, "保留課題のID %s が重複している(初出: %s)" % (pid, seen_ids[pid]))
|
||||
seen_ids[pid] = "%s:%d" % (rel, n)
|
||||
current = (pid, n, [])
|
||||
blocks.append(current)
|
||||
elif current is not None:
|
||||
current[2].append(line)
|
||||
|
||||
for pid, n, body in blocks:
|
||||
for field in BACKLOG_FIELDS:
|
||||
pattern = re.compile(r"^[-*]\s+" + re.escape(field) + r"\s*[::]\s*\S")
|
||||
if not any(pattern.match(line) for line in body):
|
||||
add("ERROR", rel, n, "%s に「- %s:」の内容がない" % (pid, field))
|
||||
|
||||
for n, line in lines:
|
||||
if TAG.search(line):
|
||||
add("ERROR", rel, n, "本文の前提となる推測タグはbacklogではなく仕様書に置く")
|
||||
for target in MDLINK.findall(line):
|
||||
linked, _ = local_target(path, target)
|
||||
if linked is not None and not linked.is_file():
|
||||
add("ERROR", rel, n, "backlogの参照先ファイルがない: %s" % target)
|
||||
|
||||
|
||||
def check_backlog_section(path, rel, lines):
|
||||
positions = []
|
||||
for title in MANAGEMENT_SECTIONS:
|
||||
headings = [n for n, line in lines if H2.match(line) and H2.match(line).group(1) == title]
|
||||
if len(headings) > 1:
|
||||
add("ERROR", rel, headings[1], "## %s が重複している" % title)
|
||||
if headings:
|
||||
positions.append(headings[0])
|
||||
if positions != sorted(positions):
|
||||
add("ERROR", rel, min(positions), "末尾の管理セクションは保留課題 → 検討済み・非採用 → 未確認事項の順に置く")
|
||||
|
||||
deferred = section(lines, "保留課題")
|
||||
if deferred is None:
|
||||
return
|
||||
contents = [(n, line) for n, line in deferred if line.strip()]
|
||||
if not contents:
|
||||
add("ERROR", rel, 1, "空の ## 保留課題 はセクションごと削除する")
|
||||
for n, line in contents:
|
||||
match = BACKLOG_LINK.fullmatch(line)
|
||||
if not match:
|
||||
add("ERROR", rel, n, "保留課題には - [<区分>の保留課題](backlogのファイル#見出し) だけを書く")
|
||||
continue
|
||||
target = match.group(1)
|
||||
linked, anchor = local_target(path, target)
|
||||
if (linked is None or not linked.is_relative_to(ROOT / "backlog")
|
||||
or linked.suffix != ".md" or not anchor):
|
||||
add("ERROR", rel, n, "保留課題はbacklog内のMarkdownファイルの見出しへリンクする: %s" % target)
|
||||
continue
|
||||
if not linked.is_file():
|
||||
add("ERROR", rel, n, "保留課題の参照先ファイルがない: %s" % target)
|
||||
continue
|
||||
headings = set()
|
||||
for _, linked_line in load(linked):
|
||||
heading = re.match(r"^#{1,6}\s+(.+?)\s*#*\s*$", linked_line)
|
||||
if heading:
|
||||
slug = re.sub(r"[^\w\s-]", "", heading.group(1)).lower().replace(" ", "-")
|
||||
headings.add(slug)
|
||||
if anchor not in headings:
|
||||
add("ERROR", rel, n, "保留課題の参照先見出しがない: %s" % target)
|
||||
|
||||
|
||||
def check(path, seen_backlog_ids=None):
|
||||
path = Path(path).resolve()
|
||||
rel = path.relative_to(ROOT).as_posix() if path.is_relative_to(ROOT) else path.as_posix()
|
||||
base = path.name
|
||||
is_journal = path.is_relative_to(ROOT / "journal")
|
||||
is_backlog = path.is_relative_to(ROOT / "backlog")
|
||||
lines = load(path)
|
||||
|
||||
if is_backlog:
|
||||
check_backlog(path, rel, lines, seen_backlog_ids if seen_backlog_ids is not None else {})
|
||||
elif not is_journal:
|
||||
check_backlog_section(path, rel, lines)
|
||||
|
||||
unconf = section(lines, "未確認事項")
|
||||
rejected = section(lines, "検討済み・非採用")
|
||||
unconf_span = set(n for n, _ in unconf) if unconf else set()
|
||||
tagged = {pid for n, line in lines if n not in unconf_span for pid in TAG.findall(line)}
|
||||
|
||||
# --- ルール4: 推測タグと未確認事項の対応 ---
|
||||
entries = set()
|
||||
@@ -80,9 +181,14 @@ def check(path):
|
||||
elif blocks:
|
||||
blocks[-1][2].append(l)
|
||||
for pid, ln, body in blocks:
|
||||
if not any(PROMOTE.match(x) for x in body):
|
||||
has_promote = any(PROMOTE.match(x) for x in body)
|
||||
has_complete = any(COMPLETE.match(x) for x in body)
|
||||
if pid in tagged and not has_promote:
|
||||
add("ERROR", rel, ln,
|
||||
"%s に「- 格上げ条件:」の行がない(何を確認すれば事実に格上げできるか)" % pid)
|
||||
elif not has_promote and not has_complete:
|
||||
add("ERROR", rel, ln,
|
||||
"%s に「- 格上げ条件:」または「- 完了条件:」の内容がない" % pid)
|
||||
|
||||
for n, l in lines:
|
||||
if n in unconf_span:
|
||||
@@ -96,9 +202,10 @@ def check(path):
|
||||
# --- ルール3: journal への/からのリンク禁止 ---
|
||||
for n, l in lines:
|
||||
for target in MDLINK.findall(l):
|
||||
if target.lstrip("./").startswith("journal/"):
|
||||
linked, _ = local_target(path, target)
|
||||
if linked is not None and linked.is_relative_to(ROOT / "journal"):
|
||||
add("ERROR", rel, n, "journal へのリンクが張られている: %s" % target)
|
||||
elif is_journal:
|
||||
elif is_journal and linked is not None:
|
||||
add("ERROR", rel, n, "journal から他ファイルへのリンクが張られている: %s" % target)
|
||||
|
||||
# --- ルール1: 検討済み・非採用 ---
|
||||
@@ -121,7 +228,7 @@ def check(path):
|
||||
add("WARN", rel, 1, "冒頭20行に結論が見当たらない(結論を先頭に置く)")
|
||||
|
||||
# --- ルール1: 調査・比較の混入(要目視確認) ---
|
||||
if not is_journal and base not in PROSE_EXEMPT:
|
||||
if not is_journal and not is_backlog and base not in PROSE_EXEMPT:
|
||||
# 検討済み・非採用 は却下案を列挙する場所なので対象外
|
||||
skip = set(n for n, _ in rejected) if rejected else set()
|
||||
for n, l in lines:
|
||||
@@ -135,16 +242,21 @@ def check(path):
|
||||
add("WARN", rel, n, "優先度評価は journal 行きでは?")
|
||||
|
||||
def main(argv):
|
||||
targets = argv[1:]
|
||||
issues.clear()
|
||||
targets = [Path(arg).resolve() for arg in argv[1:]]
|
||||
if not targets:
|
||||
targets = sorted(glob.glob("*.md")) + sorted(glob.glob("journal/*.md"))
|
||||
targets = (sorted(ROOT.glob("*.md")) + sorted((ROOT / "journal").glob("*.md"))
|
||||
+ sorted((ROOT / "backlog").rglob("*.md")))
|
||||
targets = list(dict.fromkeys(targets))
|
||||
if not targets:
|
||||
print("対象ファイルなし"); return 0
|
||||
print("対象ファイルなし")
|
||||
return 0
|
||||
seen_backlog_ids = {}
|
||||
for t in targets:
|
||||
if os.path.isfile(t):
|
||||
check(t)
|
||||
if t.is_file():
|
||||
check(t, seen_backlog_ids)
|
||||
else:
|
||||
print("見つからない: %s" % t)
|
||||
add("ERROR", t.as_posix(), 1, "対象ファイルが見つからない")
|
||||
|
||||
errs = [i for i in issues if i[0] == "ERROR"]
|
||||
warns = [i for i in issues if i[0] == "WARN"]
|
||||
@@ -154,4 +266,6 @@ def main(argv):
|
||||
return 1 if errs else 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.exit(main(sys.argv))
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
"""文書の分類・参照・完了条件の回帰検査。標準ライブラリだけで実行する。"""
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
spec = importlib.util.spec_from_file_location("doc_lint", Path(__file__).with_name("doc-lint.py"))
|
||||
lint = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(lint)
|
||||
|
||||
|
||||
def backlog_item(pid="PHY-P-14"):
|
||||
return (
|
||||
f"### {pid} 原点の吸着\n\n"
|
||||
"- 関連仕様: [PHY](../PHY.md)\n"
|
||||
"- 未決事項: 吸着が必要か\n"
|
||||
"- 保留できる理由: 数値入力で編集できる\n"
|
||||
"- 再開条件: 原点移動を試作するとき\n"
|
||||
"- 完了条件: 吸着の要否を決める\n"
|
||||
)
|
||||
|
||||
|
||||
class DocLintTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
temporary = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self.root = Path(temporary.name).resolve()
|
||||
root_patch = patch.object(lint, "ROOT", self.root)
|
||||
root_patch.start()
|
||||
self.addCleanup(root_patch.stop)
|
||||
self.write("PHY.md", "# PHY\n")
|
||||
|
||||
def write(self, relative, content):
|
||||
path = self.root / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
return path
|
||||
|
||||
def run_lint(self, *paths):
|
||||
output = io.StringIO()
|
||||
with contextlib.redirect_stdout(output):
|
||||
status = lint.main(["doc-lint.py", *(str(path) for path in paths)])
|
||||
return status, output.getvalue()
|
||||
|
||||
def test_default_scan_includes_backlog_and_accepts_fixed_links(self):
|
||||
self.write("PHY.md", "# PHY\n\n## 保留課題\n\n- [PHYの保留課題](backlog/README.md#phy)\n")
|
||||
self.write("backlog/README.md", "# 保留課題\n\n## PHY\n\n" + backlog_item())
|
||||
self.write("journal/2026-09-22-decision.md", "# 決定\n\n## 結論\n\n採用した。\n")
|
||||
status, output = self.run_lint()
|
||||
self.assertEqual(status, 0, output)
|
||||
self.assertIn("3 files / ERROR 0", output)
|
||||
|
||||
def test_backlog_fields_must_have_values(self):
|
||||
for field in lint.BACKLOG_FIELDS:
|
||||
with self.subTest(field=field):
|
||||
text = backlog_item()
|
||||
text = "\n".join(f"- {field}: " if line.startswith(f"- {field}:") else line
|
||||
for line in text.splitlines())
|
||||
path = self.write("backlog/README.md", "## PHY\n" + text)
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 1)
|
||||
self.assertIn(f"- {field}:", output)
|
||||
|
||||
def test_duplicate_backlog_ids_across_nested_files(self):
|
||||
self.write("backlog/one.md", "## PHY\n" + backlog_item())
|
||||
self.write("backlog/sub/two.md", "## PHY\n" + backlog_item().replace("../PHY.md", "../../PHY.md"))
|
||||
status, output = self.run_lint()
|
||||
self.assertEqual(status, 1)
|
||||
self.assertIn("保留課題のID PHY-P-14 が重複", output)
|
||||
|
||||
def test_incomplete_last_item_cannot_borrow_fields_from_next_section(self):
|
||||
self.write("backlog/README.md", "## PHY\n### PHY-P-14 吸着\n\n## ELC\n" + backlog_item("ELC-P-1"))
|
||||
status, output = self.run_lint()
|
||||
self.assertEqual(status, 1)
|
||||
self.assertIn("PHY-P-14 に「- 完了条件:」", output)
|
||||
|
||||
def test_invalid_backlog_heading_and_missing_reference(self):
|
||||
path = self.write("backlog/README.md", "## PHY\n### P-14 吸着\n" + backlog_item().replace("../PHY.md", "../missing.md"))
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 1)
|
||||
self.assertIn("保留課題の見出し", output)
|
||||
self.assertIn("参照先ファイルがない", output)
|
||||
|
||||
def test_spec_backlog_section_rejects_prose_empty_and_broken_targets(self):
|
||||
self.write("backlog/README.md", "## PHY\n" + backlog_item())
|
||||
cases = {
|
||||
"内容をここにも要約する。": "だけを書く",
|
||||
"": "空の ## 保留課題",
|
||||
"- [PHYの保留課題](backlog/missing.md#phy)": "参照先ファイルがない",
|
||||
"- [PHYの保留課題](backlog/README.md#missing)": "参照先見出しがない",
|
||||
"- [PHYの保留課題](backlog/README.md)": "見出しへリンク",
|
||||
"- [PHYの保留課題](PHY.md#phy)": "backlog内",
|
||||
}
|
||||
for content, expected in cases.items():
|
||||
with self.subTest(content=content):
|
||||
path = self.write("PHY.md", "# PHY\n\n## 保留課題\n\n" + content + "\n")
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 1)
|
||||
self.assertIn(expected, output)
|
||||
|
||||
def test_management_sections_have_one_order(self):
|
||||
path = self.write("PHY.md", "## 未確認事項\n\n(なし)\n\n## 検討済み・非採用\n\n## 未確認事項\n")
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 1)
|
||||
self.assertIn("順に置く", output)
|
||||
self.assertIn("## 未確認事項 が重複", output)
|
||||
|
||||
def test_decisions_accept_completion_but_tagged_assumptions_require_evidence(self):
|
||||
text = "## 未確認事項\n\n### P-1 操作方式\n- 完了条件: 操作方式を決める\n"
|
||||
path = self.write("PHY.md", text)
|
||||
self.assertEqual(self.run_lint(path)[0], 0)
|
||||
path.write_text("本文 [推測:P-1]\n\n" + text, encoding="utf-8")
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 1)
|
||||
self.assertIn("格上げ条件", output)
|
||||
path.write_text("本文 [推測:P-1]\n\n" + text.replace("完了条件", "格上げ条件"), encoding="utf-8")
|
||||
self.assertEqual(self.run_lint(path)[0], 0)
|
||||
|
||||
def test_absolute_journal_paths_keep_journal_checks(self):
|
||||
path = self.write("journal/bad.md", "# 調査\n\n[仕様](../PHY.md)\n")
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 1)
|
||||
self.assertIn("journal のファイル名", output)
|
||||
self.assertIn("journal から他ファイルへのリンク", output)
|
||||
|
||||
def test_backlog_is_not_prose_linted_but_cannot_link_to_journal(self):
|
||||
self.write("journal/2026-09-22-decision.md", "## 結論\n")
|
||||
path = self.write("backlog/README.md", "## PHY\n" + backlog_item() + "\n候補はA案。\n[履歴](../journal/2026-09-22-decision.md)\n")
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 1)
|
||||
self.assertIn("journal へのリンク", output)
|
||||
self.assertIn("WARN 0", output)
|
||||
|
||||
def test_code_examples_do_not_create_real_sections_or_tags(self):
|
||||
path = self.write("PHY.md", "```markdown\n## 保留課題\n[推測:P-9]\n```\n")
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 0, output)
|
||||
|
||||
def test_missing_explicit_file_is_an_error(self):
|
||||
status, output = self.run_lint(self.root / "missing.md")
|
||||
self.assertEqual(status, 1)
|
||||
self.assertIn("対象ファイルが見つからない", output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user