Files
Beyond-KLE/.claude/skills/doc-lint/test_doc_lint.py
T
ewdernaandClaude Opus 5 5b671976d5 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
2026-09-22 19:52:39 +09:00

151 lines
7.0 KiB
Python

"""文書の分類・参照・完了条件の回帰検査。標準ライブラリだけで実行する。"""
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()