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:
2026-09-22 22:32:18 +09:00
co-authored by Claude Opus 5
parent 2fa910ed74
commit 54f292b08e
8 changed files with 623 additions and 39 deletions
+122 -11
View File
@@ -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: