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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user