docs(structure): govern .planning/ and register the ops runbook
- docs/STRUCTURE.md: add a .planning/ registry (vision/roadmap/history/breakdown
roles), broaden the roadmap role, add enforcement items 8-10.
- tools/check_requirements.py: scan .planning/ too - every .planning/**.md must be
registered, registered files must exist, requirement rows rejected there.
- .planning/: PROJECT.md, ROADMAP.md, MILESTONES.md, and milestones/v0.5/M3/
{roadmap.md,task_breakdown.md}. M3 breakdown holds only the tasks; decisions live
in the contracts.
- docs/DEPLOYMENT.md: add the ops runbook (registered as ops).
This commit is contained in:
@@ -16,14 +16,20 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DOCS = ROOT / "docs"
|
||||
PLANNING = ROOT / ".planning"
|
||||
STRUCTURE = DOCS / "STRUCTURE.md"
|
||||
REQUIREMENTS = DOCS / "REQUIREMENTS.md"
|
||||
MILESTONES = DOCS / "MILESTONES.md"
|
||||
|
||||
EXEMPT_PREFIXES = (("docs", "archive"), ("docs", "_templates"))
|
||||
|
||||
ROLES = "index|intent|meta|requirements|scope|capability|contract|practice"
|
||||
REGISTRY_ROW = re.compile(rf"^\|\s*`(docs/[^`]+\.md)`\s*\|\s*({ROLES})\s*\|")
|
||||
ROLES = (
|
||||
"index|intent|meta|requirements|scope|capability|contract|practice|ops"
|
||||
"|vision|roadmap|history|breakdown"
|
||||
)
|
||||
REGISTRY_ROW = re.compile(
|
||||
rf"^\|\s*`((?:docs|\.planning)/[^`]+\.md)`\s*\|\s*({ROLES})\s*\|"
|
||||
)
|
||||
NAMESPACE_ROW = re.compile(r"^\|\s*`([A-Z]+)`\s*\|")
|
||||
REQUIREMENT_ROW = re.compile(
|
||||
r"^\|\s*([A-Z]+)-(\d+)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*$"
|
||||
@@ -58,13 +64,19 @@ def main() -> int:
|
||||
if not namespaces:
|
||||
errors.append("STRUCTURE.md declares no ID namespaces")
|
||||
|
||||
# 1. every non-exempt doc is registered
|
||||
planning_files = sorted(PLANNING.rglob("*.md")) if PLANNING.is_dir() else []
|
||||
|
||||
# 1. every non-exempt doc is registered; every planning file is registered
|
||||
for path in sorted(DOCS.rglob("*.md")):
|
||||
rel = path.relative_to(ROOT).as_posix()
|
||||
if path.relative_to(ROOT).parts[:2] in EXEMPT_PREFIXES:
|
||||
continue
|
||||
if rel not in registered:
|
||||
errors.append(f"unregistered doc: {rel}")
|
||||
for path in planning_files:
|
||||
rel = path.relative_to(ROOT).as_posix()
|
||||
if rel not in registered:
|
||||
errors.append(f"unregistered planning file: {rel}")
|
||||
|
||||
# 2. every registered doc exists
|
||||
for rel in registered:
|
||||
@@ -110,7 +122,7 @@ def main() -> int:
|
||||
errors.append(f"{loc} {rid} invalid Test ref '{ref}' (use a path, an ID, or —)")
|
||||
|
||||
# 6. requirement rows appear only in REQUIREMENTS.md
|
||||
for path in sorted(DOCS.rglob("*.md")):
|
||||
for path in sorted(DOCS.rglob("*.md")) + planning_files:
|
||||
if path.relative_to(ROOT).parts[:2] in EXEMPT_PREFIXES or path in {
|
||||
REQUIREMENTS,
|
||||
STRUCTURE,
|
||||
@@ -148,8 +160,10 @@ def main() -> int:
|
||||
print(f" - {error}")
|
||||
return 1
|
||||
|
||||
planning_registered = sum(1 for rel in registered if rel.startswith(".planning/"))
|
||||
print(
|
||||
f"PASS check_requirements: {len(registered)} docs, "
|
||||
f"PASS check_requirements: {len(registered) - planning_registered} docs, "
|
||||
f"{planning_registered} planning, "
|
||||
f"{len(namespaces)} namespaces, {len(ids)} requirements"
|
||||
)
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user