mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-17 13:45:34 +02:00
fix(governance): validate inline ADR successors
This commit is contained in:
@@ -38,6 +38,9 @@ ENTRY_RE = re.compile(
|
||||
CODE_PATH_RE = re.compile(r"`([^`\n]+)`")
|
||||
TEST_ID_RE = re.compile(r"\bTEST-[A-Z0-9][A-Z0-9-]*\b")
|
||||
ADR_TARGET_RE = re.compile(r"\b(?:ADR-)?\d{3,4}-[a-z0-9-]+\.md\b", re.IGNORECASE)
|
||||
ADR_INLINE_SUCCESSOR_RE = re.compile(
|
||||
r"(?im)^(?:[-*]\s+)?(?:\*\*(?:status|状态)\*\*|(?:status|状态))\s*[::]\s*`?superseded`?\s+by\s+(ADR-\d{3,4})\s*$"
|
||||
)
|
||||
IGNORED_DIRS = {".git", ".governance", ".venv", "node_modules", "vendor", "__pycache__"}
|
||||
IGNORED_REFERENCE_MARKERS = ("*", "{", "}", "<", ">", "…", "...")
|
||||
GIT_TIMEOUT_SECONDS = 5
|
||||
@@ -442,6 +445,16 @@ def parse_adr_status(text: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def inline_adr_successor(text: str) -> str | None:
|
||||
match = ADR_INLINE_SUCCESSOR_RE.search(text)
|
||||
return match.group(1).upper() if match else None
|
||||
|
||||
|
||||
def adr_identifier(text: str) -> str | None:
|
||||
match = re.search(r"(?im)^#\s+(ADR-\d{3,4})\b", text)
|
||||
return match.group(1).upper() if match else None
|
||||
|
||||
|
||||
def indexed_adr_paths(
|
||||
root: Path, index: Path, index_text: str, report: Report
|
||||
) -> set[Path]:
|
||||
@@ -520,6 +533,16 @@ def check_adr_lifecycle(
|
||||
"ADR status is unsupported: "
|
||||
f"{diagnostic(path.relative_to(root))} -> {status}"
|
||||
)
|
||||
successor = inline_adr_successor(text)
|
||||
if successor and successor not in {
|
||||
identifier
|
||||
for source_text in sources.values()
|
||||
if (identifier := adr_identifier(source_text)) is not None
|
||||
}:
|
||||
report.fail(
|
||||
"ADR inline successor does not exist: "
|
||||
f"{diagnostic(path.relative_to(root))} -> {successor}"
|
||||
)
|
||||
supersedes = re.search(
|
||||
r"(?ims)^##\s+Supersedes\s*\n(?P<body>.*?)(?:\n## |\Z)", text
|
||||
)
|
||||
|
||||
@@ -38,6 +38,9 @@ ENTRY_RE = re.compile(
|
||||
CODE_PATH_RE = re.compile(r"`([^`\n]+)`")
|
||||
TEST_ID_RE = re.compile(r"\bTEST-[A-Z0-9][A-Z0-9-]*\b")
|
||||
ADR_TARGET_RE = re.compile(r"\b(?:ADR-)?\d{3,4}-[a-z0-9-]+\.md\b", re.IGNORECASE)
|
||||
ADR_INLINE_SUCCESSOR_RE = re.compile(
|
||||
r"(?im)^(?:[-*]\s+)?(?:\*\*(?:status|状态)\*\*|(?:status|状态))\s*[::]\s*`?superseded`?\s+by\s+(ADR-\d{3,4})\s*$"
|
||||
)
|
||||
IGNORED_DIRS = {".git", ".governance", ".venv", "node_modules", "vendor", "__pycache__"}
|
||||
IGNORED_REFERENCE_MARKERS = ("*", "{", "}", "<", ">", "…", "...")
|
||||
GIT_TIMEOUT_SECONDS = 5
|
||||
@@ -442,6 +445,16 @@ def parse_adr_status(text: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def inline_adr_successor(text: str) -> str | None:
|
||||
match = ADR_INLINE_SUCCESSOR_RE.search(text)
|
||||
return match.group(1).upper() if match else None
|
||||
|
||||
|
||||
def adr_identifier(text: str) -> str | None:
|
||||
match = re.search(r"(?im)^#\s+(ADR-\d{3,4})\b", text)
|
||||
return match.group(1).upper() if match else None
|
||||
|
||||
|
||||
def indexed_adr_paths(
|
||||
root: Path, index: Path, index_text: str, report: Report
|
||||
) -> set[Path]:
|
||||
@@ -520,6 +533,16 @@ def check_adr_lifecycle(
|
||||
"ADR status is unsupported: "
|
||||
f"{diagnostic(path.relative_to(root))} -> {status}"
|
||||
)
|
||||
successor = inline_adr_successor(text)
|
||||
if successor and successor not in {
|
||||
identifier
|
||||
for source_text in sources.values()
|
||||
if (identifier := adr_identifier(source_text)) is not None
|
||||
}:
|
||||
report.fail(
|
||||
"ADR inline successor does not exist: "
|
||||
f"{diagnostic(path.relative_to(root))} -> {successor}"
|
||||
)
|
||||
supersedes = re.search(
|
||||
r"(?ims)^##\s+Supersedes\s*\n(?P<body>.*?)(?:\n## |\Z)", text
|
||||
)
|
||||
|
||||
@@ -374,15 +374,32 @@ def test_adr_scope_accepts_superseded_status_with_successor(project: Path) -> No
|
||||
adr_dir = project / "docs" / "adr"
|
||||
adr_dir.mkdir(parents=True)
|
||||
(adr_dir / "README.md").write_text(
|
||||
"[decision](0001-storage.md)\n", encoding="utf-8"
|
||||
"[old](0001-storage.md)\n[new](0002-storage.md)\n", encoding="utf-8"
|
||||
)
|
||||
(adr_dir / "0001-storage.md").write_text(
|
||||
"# ADR-0001\n\n**Status**: superseded by ADR-0002\n", encoding="utf-8"
|
||||
)
|
||||
(adr_dir / "0002-storage.md").write_text(
|
||||
"# ADR-0002\n\n**Status**: accepted\n", encoding="utf-8"
|
||||
)
|
||||
result = run_audit(project, "adr")
|
||||
assert result.returncode == 0, result.stdout
|
||||
|
||||
|
||||
def test_adr_scope_rejects_missing_inline_superseded_successor(project: Path) -> None:
|
||||
adr_dir = project / "docs" / "adr"
|
||||
adr_dir.mkdir(parents=True)
|
||||
(adr_dir / "README.md").write_text(
|
||||
"[decision](0001-storage.md)\n", encoding="utf-8"
|
||||
)
|
||||
(adr_dir / "0001-storage.md").write_text(
|
||||
"# ADR-0001\n\n**Status**: superseded by ADR-9999\n", encoding="utf-8"
|
||||
)
|
||||
result = run_audit(project, "adr")
|
||||
assert result.returncode == 1
|
||||
assert "ADR inline successor does not exist" in result.stdout
|
||||
|
||||
|
||||
def test_adr_scope_audits_common_adr_prefixed_filenames(project: Path) -> None:
|
||||
adr_dir = project / "docs" / "adr"
|
||||
adr_dir.mkdir(parents=True)
|
||||
|
||||
Reference in New Issue
Block a user