fix(seedgo): de-git readme_check — audit reads local files only (DPLAN-0198)

Drops git check-ignore + git log from readme_check. Runtime dirs tolerated via
static list (_is_runtime_artifact); freshness checks date-presence only, no
history comparison. Local-CI parity proven 13/13 both ways (working tree +
git archive clean checkout). Invariant: a checker never consults git or
.gitignore; only bypass.json excludes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AIOSAI
2026-06-06 16:31:38 -07:00
co-authored by Claude Opus 4.8
parent 0661949c15
commit 8efb204486
3 changed files with 121 additions and 225 deletions
@@ -24,7 +24,6 @@ Checks:
import os
import re
import subprocess
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
@@ -36,57 +35,31 @@ from aipass.seedgo.apps.handlers.bypass.utils import is_bypassed
AUDIT_SCOPE = "entry_point"
def _is_gitignored(path: Path) -> bool:
"""Check if a path is covered by .gitignore rules.
# Runtime / generated paths that legitimately may be absent in a clean
# checkout (e.g. CI) while present in a working tree. A README documenting
# one of these is not a violation when it's missing from disk.
# Pure local-file check — never consults git or .gitignore. (A standards
# audit reads the files that are there; git is a separate concern.)
_RUNTIME_ARTIFACTS = {
"logs",
"artifacts",
"dropbox",
"system_logs",
"docs.local",
".trinity",
"STATUS.local.md",
"DASHBOARD.local.json",
}
Uses ``git check-ignore`` from the repo root (discovered via
``git rev-parse --show-toplevel``). Falls back to a built-in list
of known AIPass gitignored patterns when git is unavailable.
"""
try:
top = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
capture_output=True,
text=True,
timeout=5,
)
if top.returncode == 0:
repo_root = top.stdout.strip()
# Test both the bare path and its directory form. .gitignore dir-only
# patterns (trailing slash: logs/, artifacts/, **/*_json/, .trinity/)
# only match when git knows the path is a directory. In a clean
# checkout the gitignored dir does not exist on disk, so git cannot
# infer "directory" from the bare path and reports it un-ignored —
# appending a trailing slash signals directory intent and restores
# the match. (Without this, README dir-tree/dead-link checks fail in
# CI's clean checkout while passing in a working tree.)
for candidate in (str(path), str(path).rstrip("/") + "/"):
result = subprocess.run(
["git", "-C", repo_root, "check-ignore", "-q", candidate],
capture_output=True,
timeout=5,
)
if result.returncode == 0:
return True
return False
except Exception:
logger.info("git check-ignore unavailable for %s", path)
def _is_runtime_artifact(path: Path) -> bool:
"""True if path is a runtime/generated artifact that may be absent in a
clean checkout. Local-file only — never consults git or .gitignore."""
name = path.name
known_ignored = {
"logs",
"artifacts",
"dropbox",
"system_logs",
"docs.local",
".trinity",
}
if name in known_ignored:
if name in _RUNTIME_ARTIFACTS:
return True
if name.endswith("_json"):
return True
if name in ("STATUS.local.md", "DASHBOARD.local.json"):
return True
return False
@@ -243,35 +216,17 @@ def check_required_sections(lines: List[str], file_path: str, bypass_rules: list
return {"name": "Required sections", "passed": False, "message": f"Missing sections: {', '.join(missing)}"}
def _get_latest_py_commit_date(branch_root: Path) -> Optional[datetime]:
"""Get the date of the last git commit that touched a .py file in the branch."""
apps_dir = branch_root / "apps"
if not apps_dir.exists():
return None
try:
result = subprocess.run(
["git", "log", "-1", "--format=%cd", "--date=short", "--", "*.py"],
capture_output=True,
text=True,
cwd=str(apps_dir),
timeout=10,
)
if result.returncode != 0 or not result.stdout.strip():
return None
return datetime.strptime(result.stdout.strip(), "%Y-%m-%d")
except (subprocess.TimeoutExpired, FileNotFoundError, ValueError) as exc:
logger.info("git log for freshness check failed: %s", exc)
return None
def check_last_updated_freshness(
lines: List[str], branch_root: Path, file_path: str, bypass_rules: list | None = None
) -> Dict:
"""
Check that Last Updated date is within 7 days of last git commit touching .py files.
Check that the README declares a well-formed "Last Updated" date.
Uses git history (not filesystem mtime) to avoid false positives from
checkout/merge/pull operations that reset mtimes without semantic changes.
Local-file only: verifies the field is present and parseable. Does NOT
compare against code history — recency is not a property of the files on
disk, so it has no place in a local standards audit (and would diverge
between a working tree and a clean CI checkout). A "code changed, re-check
your README" nudge, if wanted, belongs outside the audit as its own flag.
Looks for patterns:
- *Last Updated: YYYY-MM-DD*
@@ -282,46 +237,27 @@ def check_last_updated_freshness(
if is_bypassed(file_path, "readme", None, bypass_rules):
return {"name": "Last Updated freshness", "passed": True, "message": "Bypassed by bypass rules"}
readme_date = None
date_pattern = re.compile(r"\*{0,2}Last Updated\*{0,2}:\*{0,2}\s*(\d{4}-\d{2}-\d{2})")
for line in lines:
match = date_pattern.search(line)
if match:
try:
readme_date = datetime.strptime(match.group(1), "%Y-%m-%d")
datetime.strptime(match.group(1), "%Y-%m-%d")
except ValueError:
logger.info("Malformed date in README: %s", match.group(1))
readme_date = None
break
logger.info("Malformed Last Updated date in README: %s", match.group(1))
return {
"name": "Last Updated freshness",
"passed": False,
"message": f"Malformed Last Updated date: {match.group(1)}",
}
return {
"name": "Last Updated freshness",
"passed": True,
"message": f"Last Updated date present ({match.group(1)})",
}
if readme_date is None:
return {"name": "Last Updated freshness", "passed": False, "message": 'No "Last Updated" date found in README'}
latest_commit = _get_latest_py_commit_date(branch_root)
if latest_commit is None:
return {"name": "Last Updated freshness", "passed": True, "message": "No git history for .py files (skip)"}
days_behind = (latest_commit - readme_date).days
if days_behind <= 7:
return {
"name": "Last Updated freshness",
"passed": True,
"message": f"README date {readme_date.strftime('%Y-%m-%d')} is within 7 days of latest code commit",
}
return {
"name": "Last Updated freshness",
"passed": False,
"message": (
f"README is {days_behind} days behind last code change"
f" ({latest_commit.strftime('%Y-%m-%d')}). Review and update"
" README CONTENT to reflect recent changes, then set Last"
" Updated. Do not just bump the date."
),
}
return {"name": "Last Updated freshness", "passed": False, "message": 'No "Last Updated" date found in README'}
def check_directory_tree(lines: List[str], branch_root: Path, file_path: str, bypass_rules: list | None = None) -> Dict:
@@ -395,7 +331,7 @@ def check_directory_tree(lines: List[str], branch_root: Path, file_path: str, by
found = True
break
if not found:
if _is_gitignored(branch_root / dir_name):
if _is_runtime_artifact(branch_root / dir_name):
continue
missing_dirs.append(dir_name)
@@ -598,7 +534,7 @@ def check_markdown_links(lines: List[str], branch_root: Path, file_path: str, by
for link_text, link_path in links:
resolved = (branch_root / link_path).resolve()
if not resolved.exists():
if _is_gitignored(branch_root / link_path):
if _is_runtime_artifact(branch_root / link_path):
continue
dead_links.append(f"{link_path} ({link_text})")
+35 -100
View File
@@ -183,16 +183,11 @@ def test_required_sections_alternate_names():
# ===========================================================================
def test_last_updated_freshness_within_7_days(tmp_path):
"""README date within 7 days of last git commit passes."""
from datetime import datetime, timedelta
from unittest.mock import patch
commit_date = datetime.now() - timedelta(days=3)
readme_date = datetime.now() - timedelta(days=5)
def test_last_updated_freshness_date_present(tmp_path):
"""Well-formed Last Updated date passes."""
lines: List[str] = [
"# Branch",
f"*Last Updated: {readme_date.strftime('%Y-%m-%d')}*",
"*Last Updated: 2026-06-01*",
"",
]
branch_root = tmp_path / "mybranch"
@@ -203,24 +198,16 @@ def test_last_updated_freshness_within_7_days(tmp_path):
check_last_updated_freshness,
)
with patch(
"aipass.seedgo.apps.handlers.aipass_standards.readme_check._get_latest_py_commit_date",
return_value=commit_date,
):
result = check_last_updated_freshness(lines, branch_root, "/fake/apps/entry.py")
result = check_last_updated_freshness(lines, branch_root, "/fake/apps/entry.py")
assert result["passed"] is True
assert "present" in result["message"]
def test_last_updated_freshness_stale(tmp_path):
"""README date >7 days behind last git commit fails."""
from datetime import datetime, timedelta
from unittest.mock import patch
commit_date = datetime.now() - timedelta(days=2)
readme_date = datetime.now() - timedelta(days=20)
def test_last_updated_freshness_bold_format(tmp_path):
"""Bold markdown format for Last Updated date passes."""
lines: List[str] = [
"# Branch",
f"**Last Updated:** {readme_date.strftime('%Y-%m-%d')}",
"**Last Updated:** 2026-05-18",
"",
]
branch_root = tmp_path / "mybranch"
@@ -231,40 +218,29 @@ def test_last_updated_freshness_stale(tmp_path):
check_last_updated_freshness,
)
with patch(
"aipass.seedgo.apps.handlers.aipass_standards.readme_check._get_latest_py_commit_date",
return_value=commit_date,
):
result = check_last_updated_freshness(lines, branch_root, "/fake/apps/entry.py")
result = check_last_updated_freshness(lines, branch_root, "/fake/apps/entry.py")
assert result["passed"] is True
assert "2026-05-18" in result["message"]
def test_last_updated_freshness_malformed_date(tmp_path):
"""Malformed date (regex matches but strptime fails) fails."""
lines: List[str] = [
"# Branch",
"*Last Updated: 2026-13-45*",
"",
]
branch_root = tmp_path / "mybranch"
branch_root.mkdir()
(branch_root / "apps").mkdir()
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
check_last_updated_freshness,
)
result = check_last_updated_freshness(lines, branch_root, "/fake/apps/entry.py")
assert result["passed"] is False
assert "days behind" in result["message"]
assert "Do not just bump" in result["message"]
def test_last_updated_freshness_no_git_history(tmp_path):
"""No git history for .py files passes gracefully."""
from unittest.mock import patch
lines: List[str] = [
"# Branch",
"*Last Updated: 2026-01-01*",
"",
]
branch_root = tmp_path / "mybranch"
branch_root.mkdir()
(branch_root / "apps").mkdir()
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
check_last_updated_freshness,
)
with patch(
"aipass.seedgo.apps.handlers.aipass_standards.readme_check._get_latest_py_commit_date",
return_value=None,
):
result = check_last_updated_freshness(lines, branch_root, "/fake/apps/entry.py")
assert result["passed"] is True
assert "skip" in result["message"]
assert "Malformed" in result["message"]
def test_last_updated_freshness_missing():
@@ -286,62 +262,21 @@ def test_last_updated_freshness_missing():
assert "Last Updated" in result["message"]
def test_last_updated_freshness_boundary_7_days(tmp_path):
"""README exactly 7 days behind last commit passes (<=7)."""
from datetime import datetime, timedelta
from unittest.mock import patch
commit_date = datetime.now()
readme_date = commit_date - timedelta(days=7)
lines: List[str] = [
"# Branch",
f"*Last Updated: {readme_date.strftime('%Y-%m-%d')}*",
"",
]
def test_last_updated_freshness_bypassed(tmp_path):
"""Bypassed freshness check passes immediately."""
lines: List[str] = ["# Branch", "No date.", ""]
branch_root = tmp_path / "mybranch"
branch_root.mkdir()
(branch_root / "apps").mkdir()
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
check_last_updated_freshness,
)
with patch(
"aipass.seedgo.apps.handlers.aipass_standards.readme_check._get_latest_py_commit_date",
return_value=commit_date,
):
result = check_last_updated_freshness(lines, branch_root, "/fake/apps/entry.py")
bypass = [{"file": "/fake/apps/entry.py", "standard": "readme"}]
result = check_last_updated_freshness(lines, branch_root, "/fake/apps/entry.py", bypass)
assert result["passed"] is True
def test_last_updated_freshness_boundary_8_days(tmp_path):
"""README 8 days behind last commit fails (>7)."""
from datetime import datetime, timedelta
from unittest.mock import patch
commit_date = datetime.now()
readme_date = commit_date - timedelta(days=8)
lines: List[str] = [
"# Branch",
f"*Last Updated: {readme_date.strftime('%Y-%m-%d')}*",
"",
]
branch_root = tmp_path / "mybranch"
branch_root.mkdir()
(branch_root / "apps").mkdir()
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
check_last_updated_freshness,
)
with patch(
"aipass.seedgo.apps.handlers.aipass_standards.readme_check._get_latest_py_commit_date",
return_value=commit_date,
):
result = check_last_updated_freshness(lines, branch_root, "/fake/apps/entry.py")
assert result["passed"] is False
# ===========================================================================
# 4. readme_check -- check_directory_tree
# ===========================================================================
@@ -473,29 +473,54 @@ def test_check_module_missing_readme_has_8_failures(tmp_path):
assert result["score"] == 0
def test_is_gitignored_directory_only_pattern_nonexistent(tmp_path, monkeypatch):
"""Dir-only .gitignore patterns match non-existent paths via the slash form.
Regression (DPLAN-0195): in a clean checkout (CI) the gitignored dir does
not exist on disk, so ``git check-ignore <bare-path>`` reports it un-ignored
because git cannot confirm "directory" to match a dir-only pattern (trailing
slash). ``_is_gitignored`` must also test the trailing-slash form. Without
this the README dir-tree/dead-link checks passed in a working tree but failed
in CI's clean checkout.
"""
import subprocess
def test_is_runtime_artifact_known_dirs():
"""_is_runtime_artifact recognizes known runtime dirs and suffixes."""
from pathlib import Path
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
_is_gitignored,
_is_runtime_artifact,
)
subprocess.run(["git", "init", "-q", str(tmp_path)], check=True)
(tmp_path / ".gitignore").write_text("logs/\n**/*_json/\n.trinity/\n")
monkeypatch.chdir(tmp_path)
assert _is_runtime_artifact(Path("/any/path/logs")) is True
assert _is_runtime_artifact(Path("/any/path/artifacts")) is True
assert _is_runtime_artifact(Path("/any/path/.trinity")) is True
assert _is_runtime_artifact(Path("/any/path/cli_json")) is True
assert _is_runtime_artifact(Path("/any/path/seedgo_json")) is True
assert _is_runtime_artifact(Path("/any/path/STATUS.local.md")) is True
assert _is_runtime_artifact(Path("/any/path/DASHBOARD.local.json")) is True
assert _is_runtime_artifact(Path("/any/path/docs.local")) is True
assert _is_runtime_artifact(Path("/any/path/dropbox")) is True
assert _is_runtime_artifact(Path("/any/path/system_logs")) is True
assert _is_runtime_artifact(Path("/any/path/src")) is False
assert _is_runtime_artifact(Path("/any/path/apps")) is False
assert _is_runtime_artifact(Path("/any/path/tests")) is False
# Non-existent dirs — only match when the trailing-slash form is tested.
assert _is_gitignored(tmp_path / "logs") is True
assert _is_gitignored(tmp_path / "cli_json") is True
assert _is_gitignored(tmp_path / ".trinity") is True
# A path not covered by any pattern stays un-ignored.
assert _is_gitignored(tmp_path / "src") is False
def test_directory_tree_passes_absent_runtime_dir(tmp_path):
"""Parity regression: README tree lists runtime dir (logs), dir absent on
disk, no git available — tree check passes via _is_runtime_artifact."""
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
check_directory_tree,
)
branch_root = tmp_path / "mybranch"
branch_root.mkdir()
apps_dir = branch_root / "apps"
apps_dir.mkdir()
(apps_dir / "modules").mkdir()
lines = [
"## Architecture",
"```",
"mybranch/",
"├── apps/",
"│ └── modules/",
"├── logs/",
"├── cli_json/",
"└── artifacts/",
"```",
]
result = check_directory_tree(lines, branch_root, str(apps_dir / "entry.py"))
assert result["passed"] is True
assert "verified" in result["message"]