from __future__ import annotations
from datetime import date, datetime, timedelta
from pathlib import Path
from .config import Settings
from .docs_repository import DocsRepository
REQUIRED_FRONTMATTER = {
"owner",
"status",
"last_reviewed",
"review_interval",
"confidence",
"source_of_truth",
}
def build_freshness_report(settings: Settings) -> dict[str, object]:
repo = DocsRepository(settings)
docs = repo.list_docs()
issues: list[dict[str, str]] = []
for doc in docs:
path = str(doc["path"])
if path == "README.md" or path.startswith("server/"):
continue
frontmatter = doc.get("frontmatter") or {}
if not isinstance(frontmatter, dict):
continue
missing = sorted(REQUIRED_FRONTMATTER - set(frontmatter))
if missing:
issues.append(
{
"path": path,
"severity": "warning",
"code": "missing-frontmatter-fields",
"message": "Missing frontmatter fields: " + ", ".join(missing),
}
)
continue
stale = _is_stale(str(frontmatter["last_reviewed"]), str(frontmatter["review_interval"]))
if stale:
issues.append(
{
"path": path,
"severity": "warning",
"code": "stale-document",
"message": "Document is past its review interval",
}
)
missing_docs = _find_missing_inventory_docs(settings.repo_root)
for inventory_file, target in missing_docs:
issues.append(
{
"path": inventory_file,
"severity": "error",
"code": "missing-doc-link-target",
"message": f"docs target does not exist: {target}",
}
)
return {
"status": "ok" if not issues else "issues",
"document_count": len(docs),
"issue_count": len(issues),
"issues": issues,
}
def _is_stale(last_reviewed: str, interval: str) -> bool:
try:
reviewed = datetime.strptime(last_reviewed, "%Y-%m-%d").date()
except ValueError:
return True
if not interval.endswith("d"):
return True
try:
days = int(interval[:-1])
except ValueError:
return True
return reviewed + timedelta(days=days) < date.today()
def _find_missing_inventory_docs(repo_root: Path) -> list[tuple[str, str]]:
missing: list[tuple[str, str]] = []
inventory_dir = repo_root / "40-inventory"
for inventory_file in inventory_dir.glob("*.yml"):
for line in inventory_file.read_text(encoding="utf-8").splitlines():
stripped = line.strip()
if not stripped.startswith("docs: "):
continue
rel = stripped.split(": ", 1)[1].strip().strip("\"'")
target = (inventory_file.parent / rel).resolve()
if not target.exists():
missing.append((inventory_file.relative_to(repo_root).as_posix(), rel))
return missing