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:
@@ -7,8 +7,9 @@ argument-hint: [決定の要旨]
|
||||
|
||||
# 決定を記録する
|
||||
|
||||
CLAUDE.md のルール1〜4に従い、決定を4箇所に分散して記録する。
|
||||
どれか1つでも欠けると後から追えなくなるので、全部やる。
|
||||
共通の運用ルールは CLAUDE.md を正とする。
|
||||
現在の仕様はメインドキュメント、判断の経緯はjournal、変更の要旨はコミットに記録する。
|
||||
「検討済み・非採用」には、将来の再提案を防ぐために必要な案を選んで残す。
|
||||
|
||||
## 手順
|
||||
|
||||
@@ -30,17 +31,22 @@ CLAUDE.md のルール1〜4に従い、決定を4箇所に分散して記録す
|
||||
- ✗ `B案はKLEパーサーが壊れるので却下した`
|
||||
- ○ `クラスター情報はKLE JSONに出力しない(KLE互換性の制約)`
|
||||
- 比較表・検討過程・ベンチマーク結果をここに書かない。
|
||||
- 他の担当の詳細を外へ移したあとも、この文書だけで担当範囲が分かるか確認する。
|
||||
境界の理解に必要な一文と参照は残す。参照先の仕様は複製しない。
|
||||
|
||||
### 2. 落とした案を1行残す
|
||||
### 2. 非採用一覧に残す案を選ぶ
|
||||
|
||||
メインドキュメント末尾の `## 検討済み・非採用` に追記する。
|
||||
各案について、この記録が将来のどんな再提案を防ぐかを判断する。
|
||||
本文の制約だけで十分なら一覧へ重複記載しない。実際に検討した別方針で、
|
||||
再提案されうるものは、経緯への入口として末尾の `## 検討済み・非採用` に1案1行で残す。
|
||||
|
||||
```
|
||||
- labels[9] へのID埋め込み (journal/2026-09-16-cluster-metadata.md)
|
||||
```
|
||||
|
||||
- 理由は書かない。journalファイル名だけ。
|
||||
- セクションが5行を超えたら、古いものから落とす。長くなるのは書きすぎのサイン。
|
||||
- 目安は5行まで。件数や古さだけで機械的に削除せず、再提案を防ぐ必要性で掲載を見直す。
|
||||
- 掲載する案がなければセクションを設けない。一覧から外しても経緯はjournalに残す。
|
||||
|
||||
### 3. journal を書く
|
||||
|
||||
@@ -55,7 +61,7 @@ CLAUDE.md のルール1〜4に従い、決定を4箇所に分散して記録す
|
||||
冒頭20行で結論と選択肢が分かること。整形は最低限でよい。
|
||||
他ファイルへのリンクも、他ファイルからのリンクも張らない。
|
||||
|
||||
### 4. 推測を扱う
|
||||
### 4. 推測と未決の問いを扱う
|
||||
|
||||
この決定が未確認の推測に依存する場合:
|
||||
|
||||
@@ -65,6 +71,11 @@ CLAUDE.md のルール1〜4に従い、決定を4箇所に分散して記録す
|
||||
逆に、この決定で検証が済んだ推測があれば、タグを外して `## 未確認事項` から削除する。
|
||||
本文タグと末尾項目は必ず対で操作する(片方だけ残さない)。
|
||||
|
||||
本文の未決記述は、読者が現在の対応範囲を理解するために必要かを判断する。
|
||||
対応する問いがあればCLAUDE.mdのルール4に従ってリンクし、問いの詳細は複製しない。
|
||||
必要な問いが未登録なら未確認事項を追加し、実装にも現在の説明にも不要な言及なら本文から外す。
|
||||
今は答えに依存せず進められる課題はルール5に従う。「未決」という単語だけで分類しない。
|
||||
|
||||
### 5. コミットする
|
||||
|
||||
メインドキュメントと journal を**同じコミット**に入れる。1コミット1決定。
|
||||
|
||||
@@ -7,6 +7,7 @@ argument-hint: [ファイル...]
|
||||
# ドキュメントのルール違反を検査する
|
||||
|
||||
機械的に判定できる項目はスクリプトで検出し、判断が要る項目だけ目視で見る。
|
||||
共通の運用ルールは CLAUDE.md を正とする。
|
||||
**検出して報告するだけで、勝手に直さない。** 修正は内容を確認してもらってから。
|
||||
|
||||
## 1. スクリプトを走らせる
|
||||
@@ -27,6 +28,8 @@ ERROR があれば終了コード 1。存在しない引数のファイルもERR
|
||||
| ERROR | 本文の推測タグに対応する項目に `- 格上げ条件:` の内容がない |
|
||||
| ERROR | タグのない未確認事項に `- 格上げ条件:` または `- 完了条件:` の内容がない |
|
||||
| ERROR | `## 未確認事項` のIDが重複している |
|
||||
| ERROR | 未確認事項へのローカルリンクの参照先ファイル・見出しがない、または個別項目へのリンクが未確認事項の項目を指していない |
|
||||
| ERROR | リンクラベルの課題番号と参照先の課題番号が一致しない |
|
||||
| ERROR | 仕様書の `## 保留課題` がリンク箇条書き以外を含む、空である、参照先ファイル・見出しがない |
|
||||
| ERROR | 末尾の管理セクションが重複する、保留課題 → 検討済み・非採用 → 未確認事項の順でない |
|
||||
| ERROR | backlogの項目IDが不正・検査対象内で重複する、必須5欄がない・空である |
|
||||
@@ -35,6 +38,7 @@ ERROR があれば終了コード 1。存在しない引数のファイルもERR
|
||||
| ERROR | `## 検討済み・非採用` の項目が journal ファイル名を参照していない |
|
||||
| ERROR | journal のファイル名が `YYYY-MM-DD-<topic>.md` 形式でない |
|
||||
| WARN | `## 検討済み・非採用` が5件を超えた |
|
||||
| WARN | 本文の段落・箇条書き項目に、リンクのない課題番号や、課題への参照を伴わない「未決」「未定」がある |
|
||||
| WARN | journal の冒頭20行に結論が見当たらない |
|
||||
| WARN | メインドキュメントに比較表・優先度評価・却下案の記述らしきもの |
|
||||
|
||||
@@ -46,6 +50,11 @@ backlogでは `### PHY-P-14 題名` のように区分付きIDを使い、関連
|
||||
backlogは短い候補・制約を保持できるため、メインドキュメント向けの散文ヒューリスティックは適用しない。
|
||||
|
||||
WARN の散文検出は当たりが粗い。取りこぼしも誤検出もあるので、出た/出ないを結論にしない。
|
||||
未確認事項リンクの検査対象は `[P-3](#p-3-...)`・`[MAPのP-3](MAPレイヤー.md#p-3-...)` 等の
|
||||
インラインリンク。Markdown全般のリンク検査ではない。
|
||||
「未決」「未定」のWARNは、次行の参照も同じ段落として扱う。コード例、末尾の管理セクション、
|
||||
journal、backlog、ルールブック自体はこのWARNの対象外とする。
|
||||
WARNから課題の追加や本文の削除を自動決定しない。
|
||||
|
||||
## 2. スクリプトで見られない項目を確認する
|
||||
|
||||
@@ -59,6 +68,15 @@ WARN の散文検出は当たりが粗い。取りこぼしも誤検出もある
|
||||
機械では判定できない。「〜できるはず」「〜と考えられる」「実現可能」で実際に試していない
|
||||
記述を探し、`[推測:P-n]` が付いているか確認する。
|
||||
- **同じ事実が複数ドキュメントに複製されていないか。** 片方だけ古くなる。
|
||||
- **他の担当の説明を移した結果、責務の境界まで消えていないか。**
|
||||
この文書だけで担当範囲が分かるための一文と参照は残し、他の担当の詳細仕様は参照先で管理する。
|
||||
- **非採用一覧の各項目は、将来のどんな再提案を防ぐか。**
|
||||
本文の制約で十分か、実際に検討した別方針として経緯への入口が必要かを判断する。
|
||||
件数・古さ・重複だけで一律に追加・削除しない。
|
||||
- **本文の未決記述は、読者に何を伝えるためにあるか。**
|
||||
現在の対応範囲は本文に残し、問いの詳細は未確認事項へ集約する。
|
||||
対応項目へのリンク、必要な課題の追加、不要な言及の削除のいずれかを判断する。
|
||||
リンクが存在しても、その問いが本文の内容に対応するかは目視で確認する。
|
||||
- **保留中の対応範囲が仕様本文にあるか。** 現在の実装が答えに依存する前提をbacklogへ隠していないか。
|
||||
- **再開条件・完了条件が具体的か。** 仕様書の保留課題リンクは該当区分に向き、解決済みの課題が残っていないか。
|
||||
- **journal の冒頭20行で、結論と選択肢が分かるか。** 見出しの有無ではなく中身を読む。
|
||||
@@ -68,7 +86,9 @@ WARN の散文検出は当たりが粗い。取りこぼしも誤検出もある
|
||||
|
||||
## 3. 報告する
|
||||
|
||||
ERROR / WARN / 目視で見つけたもの を分けて出す。
|
||||
機械検査のERROR / WARNと、目視で確認した内容・未確認の範囲を分けて報告する。
|
||||
機械検査が0件でも、内容の分類・責務の境界・記録の必要性が適切だった証明にはしない。
|
||||
目視確認を省いた場合はその旨を明記する。
|
||||
修正案は出してよいが、適用は確認を取ってから。事実関係の誤りを見つけた場合も、直さず指摘する。
|
||||
|
||||
## 注意
|
||||
|
||||
@@ -17,6 +17,11 @@ 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[^)]*)\)")
|
||||
INLINE_LINK = re.compile(r"(?<!!)\[([^\]\n]+)\]\(\s*(<[^>\n]+>|[^\s)]+)\s*\)")
|
||||
INLINE_CODE = re.compile(r"(\x60+).*?\1", re.S)
|
||||
ISSUE_ID = re.compile(r"(?<![A-Za-z0-9_-])P-\d+(?![A-Za-z0-9_-])")
|
||||
ISSUE_ANCHOR = re.compile(r"^p-\d+(?:-|$)")
|
||||
HEADING = re.compile(r"^#{1,6}\s+(.+?)\s*#*\s*$")
|
||||
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")
|
||||
@@ -36,13 +41,20 @@ def add(sev, path, line, msg):
|
||||
def load(path):
|
||||
"""コードフェンス内を空行に潰した (行番号, 本文) の列を返す。"""
|
||||
raw = Path(path).read_text(encoding="utf-8").splitlines()
|
||||
out, fence = [], False
|
||||
out, fence = [], None
|
||||
for i, l in enumerate(raw, 1):
|
||||
if l.lstrip().startswith("```"):
|
||||
fence = not fence
|
||||
marker = re.match(r"^ {0,3}(\x60{3,}|~{3,})(.*)$", l)
|
||||
if fence is not None:
|
||||
out.append((i, ""))
|
||||
if (marker and marker.group(1)[0] == fence[0]
|
||||
and len(marker.group(1)) >= fence[1] and not marker.group(2).strip()):
|
||||
fence = None
|
||||
continue
|
||||
if marker:
|
||||
fence = (marker.group(1)[0], len(marker.group(1)))
|
||||
out.append((i, ""))
|
||||
continue
|
||||
out.append((i, "" if fence else l))
|
||||
out.append((i, l))
|
||||
return out
|
||||
|
||||
def section(lines, title):
|
||||
@@ -71,6 +83,108 @@ def local_target(path, target):
|
||||
return linked, unquote(parsed.fragment)
|
||||
|
||||
|
||||
def heading_index(lines):
|
||||
"""ATX見出しのアンカーを作る。同じ見出しの連番も扱う。"""
|
||||
headings = {}
|
||||
for n, line in lines:
|
||||
match = HEADING.match(line)
|
||||
if not match:
|
||||
continue
|
||||
title = match.group(1)
|
||||
slug = re.sub(r"[^\w\s-]", "", title).lower().replace(" ", "-")
|
||||
anchor, suffix = slug, 0
|
||||
while anchor in headings:
|
||||
suffix += 1
|
||||
anchor = f"{slug}-{suffix}"
|
||||
headings[anchor] = (n, title)
|
||||
return headings
|
||||
|
||||
|
||||
def body_paragraphs(lines):
|
||||
"""改行をまたぐ本文段落を返す。見出し・空行・次の箇条書きで区切る。"""
|
||||
paragraph = []
|
||||
in_management = False
|
||||
for n, line in lines:
|
||||
if HEADING.match(line):
|
||||
if paragraph:
|
||||
yield paragraph
|
||||
paragraph = []
|
||||
h2 = H2.match(line)
|
||||
if h2:
|
||||
in_management = h2.group(1) in MANAGEMENT_SECTIONS
|
||||
elif line.startswith("# "):
|
||||
in_management = False
|
||||
continue
|
||||
if in_management:
|
||||
continue
|
||||
if not line.strip() or re.match(r"^\s*(?:[-*+]|\d+[.)])\s+", line):
|
||||
if paragraph:
|
||||
yield paragraph
|
||||
paragraph = []
|
||||
if line.strip():
|
||||
paragraph.append((n, line))
|
||||
if paragraph:
|
||||
yield paragraph
|
||||
|
||||
|
||||
def check_issue_references(path, rel, lines):
|
||||
"""課題リンクの構造はERROR、本文の参照漏れの候補はWARNにする。"""
|
||||
targets = {}
|
||||
valid_link_lines = set()
|
||||
# インラインコードも位置を変えずにマスクする。
|
||||
text = "\n".join(line for _, line in lines)
|
||||
masked = INLINE_CODE.sub(lambda m: re.sub(r"[^\n]", " ", m.group()), text).split("\n")
|
||||
prose_lines = [(n, line) for (n, _), line in zip(lines, masked)]
|
||||
for n, line in prose_lines:
|
||||
for match in INLINE_LINK.finditer(line):
|
||||
label, target = match.groups()
|
||||
target = target.strip("<>")
|
||||
linked, anchor = local_target(path, target)
|
||||
if linked is None:
|
||||
continue
|
||||
label_ids = ISSUE_ID.findall(label)
|
||||
if not (label_ids or ISSUE_ANCHOR.match(anchor) or anchor == "未確認事項"):
|
||||
continue
|
||||
if not linked.is_file():
|
||||
add("ERROR", rel, n, "未確認事項リンクの参照先ファイルがない: %s" % target)
|
||||
continue
|
||||
if linked not in targets:
|
||||
target_lines = load(linked)
|
||||
entries = {
|
||||
ln: ENTRY.match(value).group(1)
|
||||
for ln, value in (section(target_lines, "未確認事項") or [])
|
||||
if ENTRY.match(value)
|
||||
}
|
||||
targets[linked] = (heading_index(target_lines), entries)
|
||||
headings, entries = targets[linked]
|
||||
if not anchor or anchor not in headings:
|
||||
add("ERROR", rel, n, "未確認事項リンクの参照先見出しがない: %s" % target)
|
||||
continue
|
||||
target_line, _ = headings[anchor]
|
||||
if anchor == "未確認事項" and not label_ids:
|
||||
continue
|
||||
target_id = entries.get(target_line)
|
||||
if target_id is None:
|
||||
add("ERROR", rel, n, "課題リンクが未確認事項の個別項目を指していない: %s" % target)
|
||||
elif any(pid != target_id for pid in label_ids):
|
||||
add("ERROR", rel, n, "リンクラベルの課題番号と参照先が一致しない: %s → %s" % (label, target_id))
|
||||
else:
|
||||
valid_link_lines.add(n)
|
||||
|
||||
for paragraph in body_paragraphs(prose_lines):
|
||||
n = paragraph[0][0]
|
||||
text = "\n".join(line for _, line in paragraph)
|
||||
# リンクと推測タグには別の構造検査がある。裸の課題番号だけを探す。
|
||||
plain = TAG.sub("", INLINE_LINK.sub("", text))
|
||||
bare_ids = sorted(set(ISSUE_ID.findall(plain)))
|
||||
if bare_ids:
|
||||
add("WARN", rel, n, "本文にリンクのない課題番号がある(要確認): %s" % ", ".join(bare_ids))
|
||||
if (re.search(r"未決|未定", plain)
|
||||
and not any(ln in valid_link_lines for ln, _ in paragraph)
|
||||
and not TAG.search(text)):
|
||||
add("WARN", rel, n, "本文の未決・未定に課題への参照がない(要確認)。リンク・課題追加・言及削除を判断する")
|
||||
|
||||
|
||||
def check_backlog(path, rel, lines, seen_ids):
|
||||
blocks = []
|
||||
current = None
|
||||
@@ -138,12 +252,7 @@ def check_backlog_section(path, rel, lines):
|
||||
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)
|
||||
headings = heading_index(load(linked))
|
||||
if anchor not in headings:
|
||||
add("ERROR", rel, n, "保留課題の参照先見出しがない: %s" % target)
|
||||
|
||||
@@ -160,6 +269,8 @@ def check(path, seen_backlog_ids=None):
|
||||
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)
|
||||
if base not in PROSE_EXEMPT:
|
||||
check_issue_references(path, rel, lines)
|
||||
|
||||
unconf = section(lines, "未確認事項")
|
||||
rejected = section(lines, "検討済み・非採用")
|
||||
@@ -217,7 +328,7 @@ def check(path, seen_backlog_ids=None):
|
||||
"検討済み・非採用の項目が journal ファイル名を参照していない")
|
||||
if len(items) > 5:
|
||||
add("WARN", rel, items[5][0],
|
||||
"検討済み・非採用が %d 件(目安5件)。古いものから落とす" % len(items))
|
||||
"検討済み・非採用が %d 件(目安5件)。再提案を防ぐ必要性で掲載を見直す" % len(items))
|
||||
|
||||
# --- journal 固有 ---
|
||||
if is_journal:
|
||||
|
||||
@@ -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