"""Check references/index.md stays consistent with the ADR files on disk.
Two things go wrong as the record set grows:
1. Orphans — an ADR file exists (active in references/, or tombstoned in
references/ZZ-deprecated/) but references/index.md never lists it.
2. Broken index entries — index.md links to a file that doesn't exist.
It also checks that `supersedes` / `superseded_by` frontmatter is reciprocal:
if ADR-005 says `supersedes: [ADR-002]`, ADR-002's own frontmatter must say
`superseded_by: ADR-005` back. A one-directional pointer is a broken link
waiting to confuse the next reader.
Usage: check_index.py [adr-skill-dir] (defaults to the parent of scripts/)
def parse_frontmatter(text: str) -> dict:
fm = re.match(r"^---\n(.*?)\n---\n", text, re.DOTALL)
id_match = re.search(r"^id:\s*(\S+)", block, re.MULTILINE)
fields["id"] = id_match.group(1).strip('"\'')
status_match = re.search(r"^status:\s*(\S+)", block, re.MULTILINE)
fields["status"] = status_match.group(1).strip('"\'').lower()
def parse_list_field(name: str) -> list[str]:
m = re.search(rf"^{name}:\s*\[(.*?)\]", block, re.MULTILINE)
return [item.strip().strip('"\'') for item in raw.split(",") if item.strip()]
fields["supersedes"] = parse_list_field("supersedes")
# superseded_by is usually a scalar on a tombstone, not a list.
sb_match = re.search(r"^superseded_by:\s*(.+)$", block, re.MULTILINE)
val = sb_match.group(1).strip().strip('"\'')
fields["superseded_by"] = parse_list_field("superseded_by")
elif val and val != "[]":
fields["superseded_by"] = [val]
fields["superseded_by"] = []
fields["superseded_by"] = []
skill_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).resolve().parent.parent
refs_dir = skill_dir / "references"
index_path = refs_dir / "index.md"
deprecated_dir = refs_dir / "ZZ-deprecated"
if not index_path.exists():
print(f"❌ no index found at {index_path}")
index_text = index_path.read_text(encoding="utf-8")
# Collect ADR files: active (references/*.md) + tombstoned (ZZ-deprecated/*.md).
active_files = sorted(refs_dir.glob("adr-*.md"))
deprecated_files = sorted(deprecated_dir.glob("adr-*.md")) if deprecated_dir.is_dir() else []
all_files = active_files + deprecated_files
records = {} # id -> {"path": Path, "fields": dict}
text = f.read_text(encoding="utf-8")
fields = parse_frontmatter(text)
adr_id = fields.get("id")
print(f"❌ {f.relative_to(skill_dir)}: no 'id' in frontmatter")
records[adr_id] = {"path": f, "fields": fields}
# 1. Orphan check: every record's id must appear in index.md.
index_ids = set(re.findall(r"ADR-\d+", index_text))
orphans = [adr_id for adr_id in records if adr_id not in index_ids]
for adr_id in sorted(orphans):
rel = records[adr_id]["path"].relative_to(skill_dir)
print(f"❌ orphan: {adr_id} ({rel}) exists but is not listed in index.md")
print(f"✅ no orphaned ADRs — all {len(records)} records are indexed")
# 2. Broken index entries: every index link must point at a file that exists.
index_links = re.findall(r"\[(ADR-\d+)\]\(([^)]+)\)", index_text)
for adr_id, link in index_links:
target = (refs_dir / link).resolve()
broken.append((adr_id, link))
for adr_id, link in broken:
print(f"❌ broken index entry: {adr_id} -> {link} (file does not exist)")
print(f"✅ no broken index links — all {len(index_links)} index entries resolve to real files")
# 3. Reciprocity check: supersedes <-> superseded_by must point both ways.
for adr_id, rec in records.items():
for target_id in rec["fields"].get("supersedes", []):
target = records.get(target_id)
print(f"❌ {adr_id} supersedes {target_id}, but {target_id} was not found on disk")
back = target["fields"].get("superseded_by", [])
print(f"❌ one-directional supersede: {adr_id} supersedes {target_id}, "
f"but {target_id}'s frontmatter does not say superseded_by: {adr_id}")
for target_id in rec["fields"].get("superseded_by", []):
target = records.get(target_id)
print(f"❌ {adr_id} superseded_by {target_id}, but {target_id} was not found on disk")
back = target["fields"].get("supersedes", [])
print(f"❌ one-directional pointer: {adr_id} says superseded_by: {target_id}, "
f"but {target_id}'s frontmatter does not say supersedes: [{adr_id}]")
print("✅ supersedes / superseded_by pointers are reciprocal (or none declared)")
if __name__ == "__main__":