agent: decide/doc-lintを更新し、.agents と .claude を同期
codexが .agents 側で更新した内容を .claude へ取り込み、名前依存の箇所だけ 置換した。両ツリーの差分はCLAUDE.md/AGENTS.mdとパスの表記のみ。 - 非採用一覧の掲載基準をルール1の変更に合わせる - 本文の「未決」はリンク・課題追加・言及削除のいずれかを判断する(ルール4) - 責務を外へ移したあと、担当範囲が分かる一文と参照が残っているか確認する - doc-lintに未確認事項リンクの参照先・ラベル整合の検査(ERROR)と、 リンクのない課題番号・参照なしの「未決」「未定」の検出(WARN)を追加 - load() のコードフェンス処理が ~~~ や長さ違いを扱えていなかったのを修正 - .Codex という実在しないパスの参照を .agents に修正 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtVk8W4yK6j3eCqQpEQBLS
This commit is contained in:
@@ -5,6 +5,7 @@ import io
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@@ -24,6 +25,14 @@ def backlog_item(pid="PHY-P-14"):
|
||||
)
|
||||
|
||||
|
||||
def issue_section():
|
||||
return (
|
||||
"## 未確認事項\n\n"
|
||||
"### P-1 動作の範囲\n\n- 完了条件: 範囲を決める\n\n"
|
||||
"### P-3 対応・状態管理\n\n- 完了条件: 管理方法を決める\n"
|
||||
)
|
||||
|
||||
|
||||
class DocLintTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
temporary = tempfile.TemporaryDirectory()
|
||||
@@ -145,6 +154,147 @@ class DocLintTests(unittest.TestCase):
|
||||
self.assertEqual(status, 1)
|
||||
self.assertIn("対象ファイルが見つからない", output)
|
||||
|
||||
def test_issue_links_accept_same_file_cross_file_and_encoded_anchors(self):
|
||||
self.write("MAPレイヤー.md", "# MAP\n\n" + issue_section())
|
||||
encoded = quote("MAPレイヤー.md") + "#" + quote("p-3-対応状態管理")
|
||||
path = self.write(
|
||||
"PHY.md",
|
||||
"# PHY\n\n範囲は未決。\n[P-1](#p-1-動作の範囲)で扱う。\n\n"
|
||||
"[MAPのP-3](MAPレイヤー.md#p-3-対応状態管理)へ。\n\n"
|
||||
f"[MAPのP-3]({encoded})へ。\n\n" + issue_section(),
|
||||
)
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 0, output)
|
||||
self.assertIn("ERROR 0 / WARN 0", output)
|
||||
|
||||
def test_issue_links_reject_missing_file_heading_and_fragment(self):
|
||||
self.write("MAP.md", "# MAP\n\n" + issue_section())
|
||||
for target, expected in (
|
||||
("missing.md#p-3-対応状態管理", "参照先ファイルがない"),
|
||||
("MAP.md#p-3-古い題名", "参照先見出しがない"),
|
||||
("MAP.md", "参照先見出しがない"),
|
||||
):
|
||||
with self.subTest(target=target):
|
||||
path = self.write("PHY.md", f"[P-3]({target})\n")
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 1, output)
|
||||
self.assertIn(expected, output)
|
||||
|
||||
def test_issue_label_must_match_real_entry_in_unconfirmed_section(self):
|
||||
self.write("MAP.md", "# MAP\n\n### P-2 本文の見出し\n\n" + issue_section())
|
||||
for link, expected in (
|
||||
("[P-3](MAP.md#p-1-動作の範囲)", "課題番号と参照先が一致しない"),
|
||||
("[MAPのP-3](MAP.md#p-1-動作の範囲)", "課題番号と参照先が一致しない"),
|
||||
("[P-2](MAP.md#p-2-本文の見出し)", "個別項目を指していない"),
|
||||
("[P-3](MAP.md#未確認事項)", "個別項目を指していない"),
|
||||
):
|
||||
with self.subTest(link=link):
|
||||
path = self.write("PHY.md", link + "\n")
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 1, output)
|
||||
self.assertIn(expected, output)
|
||||
|
||||
def test_duplicate_heading_slug_resolves_actual_target(self):
|
||||
self.write("MAP.md", "# MAP\n\n### P-1 動作の範囲\n\n" + issue_section())
|
||||
path = self.write("PHY.md", "[P-1](MAP.md#p-1-動作の範囲-1)\n")
|
||||
self.assertEqual(self.run_lint(path)[0], 0)
|
||||
path.write_text("[P-1](MAP.md#p-1-動作の範囲)\n", encoding="utf-8")
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 1, output)
|
||||
self.assertIn("個別項目を指していない", output)
|
||||
|
||||
def test_unresolved_prose_warns_without_failing_or_modifying_documents(self):
|
||||
text = "# PHY\n\n方式は未定。P-3で検討する。\n\n" + issue_section()
|
||||
path = self.write("PHY.md", text)
|
||||
before = path.read_bytes()
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 0, output)
|
||||
self.assertIn("リンクのない課題番号", output)
|
||||
self.assertIn("未決・未定に課題への参照がない", output)
|
||||
self.assertEqual(path.read_bytes(), before)
|
||||
|
||||
def test_another_issue_link_does_not_hide_bare_id(self):
|
||||
path = self.write(
|
||||
"PHY.md", "P-3と[P-1](#p-1-動作の範囲)を参照。\n\n" + issue_section()
|
||||
)
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 0, output)
|
||||
self.assertIn("リンクのない課題番号がある(要確認): P-3", output)
|
||||
|
||||
def test_unrelated_links_and_section_links_do_not_hide_unresolved_prose(self):
|
||||
for link in ("[仕様](PHY.md)", "[一覧](#未確認事項)", "[外部](https://example.com/)"):
|
||||
with self.subTest(link=link):
|
||||
path = self.write("PHY.md", f"範囲は未決。{link}\n\n" + issue_section())
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 0, output)
|
||||
self.assertIn("未決・未定に課題への参照がない", output)
|
||||
|
||||
def test_broken_link_does_not_count_as_resolved_reference(self):
|
||||
path = self.write("PHY.md", "範囲は未決。\n[P-1](#p-1-missing)\n")
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 1, output)
|
||||
self.assertIn("参照先見出しがない", output)
|
||||
self.assertIn("未決・未定に課題への参照がない", output)
|
||||
|
||||
def test_paragraph_and_list_boundaries_do_not_borrow_links(self):
|
||||
for body in (
|
||||
"範囲は未決。\n\n[P-1](#p-1-動作の範囲)\n",
|
||||
"- 範囲は未決。\n- [P-1](#p-1-動作の範囲)\n",
|
||||
"1. 範囲は未決。\n2. [P-1](#p-1-動作の範囲)\n",
|
||||
"範囲は未決。\n## 次の話\n[P-1](#p-1-動作の範囲)\n",
|
||||
):
|
||||
with self.subTest(body=body):
|
||||
path = self.write("PHY.md", body + "\n" + issue_section())
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 0, output)
|
||||
self.assertIn("未決・未定に課題への参照がない", output)
|
||||
path = self.write(
|
||||
"PHY.md", "- 範囲は未決。\n [P-1](#p-1-動作の範囲)\n\n" + issue_section()
|
||||
)
|
||||
self.assertIn("ERROR 0 / WARN 0", self.run_lint(path)[1])
|
||||
|
||||
def test_code_examples_external_links_and_assumptions_are_not_prose_warnings(self):
|
||||
path = self.write(
|
||||
"PHY.md",
|
||||
"\x60未決 P-8 [P-8](#missing)\x60\n\n"
|
||||
"\x60\x60\x60markdown\n未定 P-8 [P-8](#missing)\n\x60\x60\x60\n\n"
|
||||
"~~~markdown\n未定 P-8 [P-8](#missing)\n~~~\n\n"
|
||||
"[P-99](https://example.com/#p-99)\n\n"
|
||||
"範囲は未定 [推測:P-1]。\n\n"
|
||||
"## 未確認事項\n\n### P-1 調査\n\n- 格上げ条件: 検証する\n",
|
||||
)
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 0, output)
|
||||
self.assertIn("ERROR 0 / WARN 0", output)
|
||||
|
||||
def test_management_sections_and_non_spec_files_are_exempt_from_prose_warnings(self):
|
||||
self.write("CLAUDE.md", "未決 P-8 [P-8](#missing)\n")
|
||||
self.write("journal/2026-09-22-decision.md", "## 結論\n\n未定 P-8\n")
|
||||
self.write("backlog/README.md", "## PHY\n\n" + backlog_item() + "\n未定 P-8\n")
|
||||
self.write("PHY.md", "# PHY\n\n" + issue_section() + "\n未定 P-8\n")
|
||||
status, output = self.run_lint()
|
||||
self.assertEqual(status, 0, output)
|
||||
self.assertIn("WARN 0", output)
|
||||
|
||||
def test_links_inside_unconfirmed_items_are_still_checked(self):
|
||||
path = self.write("PHY.md", issue_section() + "\n[P-8](#missing)\n")
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 1, output)
|
||||
self.assertIn("参照先見出しがない", output)
|
||||
|
||||
def test_rejected_count_warning_does_not_select_entries_for_deletion(self):
|
||||
path = self.write(
|
||||
"PHY.md", "## 検討済み・非採用\n\n"
|
||||
+ "".join(f"- 案{i} (journal/2026-09-22-decision.md)\n" for i in range(6))
|
||||
)
|
||||
before = path.read_bytes()
|
||||
status, output = self.run_lint(path)
|
||||
self.assertEqual(status, 0, output)
|
||||
self.assertIn("6 件(目安5件)", output)
|
||||
self.assertIn("再提案を防ぐ必要性", output)
|
||||
self.assertNotIn("古いものから落とす", output)
|
||||
self.assertEqual(path.read_bytes(), before)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user