nexus-mcp/scripts/update_readme_status.py
nathan fbb90e2500 feat(docs): automate managed README status page
- Add scripts/update_readme_status.py to generate a deterministic status block, enforce traffic-light shard tables, and validate/fix internal links
- Refactor nexus-mcp/README.md into a managed status layout with standardized WIS traceability and Discipline Drives Quality sections
- Aligns with session goals for operational readiness and disciplined documentation as Nexus-MCP scales

Ref: SESSION_SNAPSHOT_2026-04-13
2026-04-13 14:33:14 -04:00

318 lines
10 KiB
Python

#!/usr/bin/env python3
"""Deterministically update nexus-mcp README status sections.
This script standardizes a managed status block in nexus-mcp/README.md using:
- staged diff (git diff --cached)
- latest session snapshot in documentation/project-history/
- TODO / RESTART NOTE markers in changed files
Usage:
python scripts/update_readme_status.py # write updates
python scripts/update_readme_status.py --check # fail if README is stale
"""
from __future__ import annotations
import argparse
import datetime as dt
import re
import subprocess
import sys
from pathlib import Path
STATUS_BEGIN = "<!-- STATUS_PAGE:BEGIN -->"
STATUS_END = "<!-- STATUS_PAGE:END -->"
# Map known moved/renamed docs to current locations.
LINK_REWRITE_MAP = {
"mcp-server/README.md": "Local-Setup.md",
}
def run_git(repo_root: Path, args: list[str]) -> str:
proc = subprocess.run(
["git", *args],
cwd=repo_root,
text=True,
capture_output=True,
check=False,
)
if proc.returncode != 0:
return ""
return proc.stdout
def get_staged_files(repo_root: Path) -> list[str]:
out = run_git(repo_root, ["diff", "--cached", "--name-only"])
return [line.strip() for line in out.splitlines() if line.strip()]
def get_staged_diff(repo_root: Path) -> str:
return run_git(repo_root, ["diff", "--cached"])
def find_latest_snapshot(repo_root: Path) -> Path | None:
snaps = sorted((repo_root / "documentation" / "project-history").glob("SESSION_SNAPSHOT_*.md"))
return snaps[-1] if snaps else None
def detect_components(files: list[str]) -> list[str]:
labels = set()
for path in files:
p = path.lower()
if p.startswith("nexus-mcp/src/"):
labels.add("core")
if "/shards/" in p:
labels.add("shards")
if p.startswith("nexus-mcp/lib/"):
labels.add("adapters")
if p.startswith("nexus-mcp/tests/"):
labels.add("tests")
if p.endswith("readme.md") or "/docs" in p or p.startswith("documentation/"):
labels.add("docs")
if "/workday" in p:
labels.add("workday")
if "/identity" in p:
labels.add("identity")
if "/audit" in p:
labels.add("audit")
if p.startswith(".github/workflows/") or "/hooks/" in p:
labels.add("ci")
if p.endswith("pyproject.toml"):
labels.add("packaging")
return sorted(labels) or ["none"]
def find_notes(repo_root: Path, files: list[str]) -> list[str]:
notes: list[str] = []
pat = re.compile(r"TODO|//\s*RESTART NOTE", re.IGNORECASE)
for rel in files:
p = repo_root / rel
if not p.exists() or p.is_dir():
continue
try:
text = p.read_text(encoding="utf-8", errors="ignore")
except OSError:
continue
for idx, line in enumerate(text.splitlines(), start=1):
if pat.search(line):
notes.append(f"{rel}:{idx}")
return notes
def is_breaking(diff_text: str) -> bool:
if "compose.yaml" not in diff_text and "docker-compose" not in diff_text:
return False
return ("ports:" in diff_text) or ("volumes:" in diff_text)
def render_status_block(
snapshot_name: str,
components: list[str],
notes: list[str],
breaking: bool,
staged_empty: bool,
) -> str:
today = dt.date.today().isoformat()
comp = ", ".join(components)
note_summary = "none" if not notes else f"{len(notes)} marker(s)"
break_txt = "Yes" if breaking else "No"
staged_txt = "No staged files" if staged_empty else "Staged changes detected"
return f"""{STATUS_BEGIN}
## Status Page (Managed)
| Field | Value |
|---|---|
| Last Updated | {today} |
| Latest Session Snapshot | {snapshot_name} |
| Change Signal | {staged_txt} |
| Components Affected | {comp} |
| TODO/RESTART Markers | {note_summary} |
| BREAKING CHANGE (compose ports/volumes) | {break_txt} |
## Shard Status Board (Traffic Light)
| Shard | System(s) | Status | WIS Ref | Flag | Standard Gate |
|---|---|---|---|---|---|
| identity | Active Directory + Entra ID | 🟢 Green | WIS-017 | ENABLE_IDENTITY | Tool tests passing |
| workday | Workday HCM | 🟡 Yellow | WIS-009 | ENABLE_WORKDAY | Credentials + live validation pending |
| audit | Cross-system drift + reporting | 🟡 Yellow | WIS-018 | ENABLE_AUDIT | Verification maturing |
| itsm | BMC Helix ITSM | 🔴 Red | WIS-021 | ENABLE_ITSM | Stub only |
| assets | Lansweeper + Intune | 🔴 Red | WIS-022 | ENABLE_ASSETS | Stub only |
| logistics | FedEx | 🔴 Red | WIS-023 | ENABLE_LOGISTICS | Stub only |
## Discipline Drives Quality
| Pillar | Target Standard | Current Signal |
|---|---|---|
| Type Hinting | Public interfaces typed | 🟢 Pydantic-based schemas in place |
| Pylance | Zero-error baseline | 🟡 Enforced goal, pending full workspace sweep |
| Modular Structure | Orchestrator -> shards -> adapters | 🟢 Applied in current architecture |
| Test Gates | Pre-push tests + validation | 🟢 Active local gate |
| Security Logging | SOC 2 audit trail with redaction | 🟢 Active |
## Sprint Traceability (2026)
| WIS ID | Area | Status |
|---|---|---|
| WIS-009 | Workday integration | 🟡 In progress |
| WIS-017 | Identity integration | 🟢 Production-ready |
| WIS-018 | Audit capability | 🟡 In progress |
| WIS-021 | ITSM shard | 🔴 Planned |
| WIS-022 | Assets shard | 🔴 Planned |
| WIS-023 | Logistics shard | 🔴 Planned |
{STATUS_END}
"""
def update_readme_content(existing: str, block: str) -> str:
if STATUS_BEGIN in existing and STATUS_END in existing:
pattern = re.compile(
rf"{re.escape(STATUS_BEGIN)}.*?{re.escape(STATUS_END)}(?:\r?\n)*",
re.DOTALL,
)
return pattern.sub(block + "\n\n", existing, count=1)
start = existing.find("## Shard Status Board")
end = existing.find("## Folder Structure")
if start != -1 and end != -1 and end > start:
return existing[:start] + block + "\n\n" + existing[end:]
header = "# Nexus-MCP — Enterprise Integration Server\n\n"
if existing.startswith("# Nexus-MCP"):
first_break = existing.find("\n\n")
if first_break != -1:
return existing[: first_break + 2] + block + "\n\n" + existing[first_break + 2 :]
return header + block + "\n\n" + existing
def _is_external_link(target: str) -> bool:
lowered = target.lower()
return (
lowered.startswith("http://")
or lowered.startswith("https://")
or lowered.startswith("mailto:")
or lowered.startswith("#")
)
def _resolve_internal_link(repo_root: Path, readme_path: Path, target: str) -> Path:
clean = target.split("#", 1)[0].strip()
if not clean:
return Path(".")
rel_from_readme = (readme_path.parent / clean).resolve()
if rel_from_readme.exists():
return rel_from_readme
rel_from_repo = (repo_root / clean).resolve()
if rel_from_repo.exists():
return rel_from_repo
return rel_from_readme
def normalize_links(repo_root: Path, readme_path: Path, text: str) -> tuple[str, dict[str, int]]:
"""Validate Markdown links, rewrite known moved targets, remove unresolved links.
"remove" means replacing `[text](missing/path.md)` with plain `text`.
"""
counters = {"rewritten": 0, "removed": 0, "checked": 0}
link_pattern = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
def repl(match: re.Match[str]) -> str:
label = match.group(1)
target = match.group(2).strip()
if _is_external_link(target):
return match.group(0)
counters["checked"] += 1
normalized_target = target
if target in LINK_REWRITE_MAP:
normalized_target = LINK_REWRITE_MAP[target]
counters["rewritten"] += 1
resolved = _resolve_internal_link(repo_root, readme_path, normalized_target)
if resolved.exists():
return f"[{label}]({normalized_target})"
counters["removed"] += 1
return label
updated = link_pattern.sub(repl, text)
# Handle known legacy backtick path outside markdown link format.
if "`mcp-server/README.md`" in updated:
updated = updated.replace("`mcp-server/README.md`", "[Local-Setup.md](Local-Setup.md)")
counters["rewritten"] += 1
return updated, counters
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--check", action="store_true", help="Fail if README would change")
args = parser.parse_args()
repo_root = Path(__file__).resolve().parent.parent
readme = repo_root / "nexus-mcp" / "README.md"
if not readme.exists():
print("ERROR: nexus-mcp/README.md not found")
return 2
staged_files = get_staged_files(repo_root)
staged_diff = get_staged_diff(repo_root)
snapshot = find_latest_snapshot(repo_root)
snapshot_name = snapshot.name if snapshot else "none"
if not staged_files:
print("WARN: git diff --cached is empty; README status generated from current metadata only")
components = detect_components(staged_files)
notes = find_notes(repo_root, staged_files)
breaking = is_breaking(staged_diff)
block = render_status_block(
snapshot_name=snapshot_name,
components=components,
notes=notes,
breaking=breaking,
staged_empty=(len(staged_files) == 0),
)
current = readme.read_text(encoding="utf-8", errors="ignore")
updated = update_readme_content(current, block)
updated, link_stats = normalize_links(repo_root, readme, updated)
if args.check:
if updated != current:
print("README status is stale. Run: python scripts/update_readme_status.py")
return 1
print("README status is up to date")
if link_stats["checked"]:
print(
f"Link check: checked={link_stats['checked']} "
f"rewritten={link_stats['rewritten']} removed={link_stats['removed']}"
)
return 0
if updated != current:
readme.write_text(updated, encoding="utf-8")
print("Updated nexus-mcp/README.md status block")
else:
print("No README changes needed")
if link_stats["checked"]:
print(
f"Link check: checked={link_stats['checked']} "
f"rewritten={link_stats['rewritten']} removed={link_stats['removed']}"
)
return 0
if __name__ == "__main__":
sys.exit(main())