mirror of
https://github.com/vacp2p/roadmap.git
synced 2026-01-09 13:48:09 -05:00
## Summary - Introduce a standalone Python roadmap validator with a CLI entry point, modular validation pipeline, and GitHub Actions wiring so roadmap content can be linted locally and in CI. - Provide reusable validation primitives for path resolution, front-matter parsing, identity checks, task parsing, catalog enforcement, and template adherence. - Document usage, configuration, and workflow behaviour to make the validator approachable for contributors. ## Validator Details - **Core tooling** - Added the `tools/roadmap_validator/` package with `validate.py` (CLI), `validator.py` (orchestration), and helper modules (`tasks.py`, `identity.py`, `paths.py`, `constants.py`, `issues.py`). - CLI supports directory/file targets, skips default filenames, emits GitHub annotations, and integrates optional substring filtering - README explains features, environment variables, and development guidance. - **Catalog and template enforcement** - `catalog.py` verifies each allowed content unit has `index.md` and `preview.md`, confirms roadmap entries appear under the proper quarter/area, and flags stale or missing links. - `templates.py` enforces template basics: front matter completeness, `## Description` ordering/content, template placeholder cleanup, and task section detection. - **Task validation** - `tasks.py` checks required metadata (`owner`, `status`, `start-date`, `end-date`), date formats, populated descriptions/deliverables, TODO markers, tangible deliverable heuristics, and `fully-qualified-name` prefixes. - **Workflow integration** - `.github/workflows/roadmap-validator.yml` runs the validator on pushes and manual dispatch, installs dependencies, scopes validation to changed Markdown, and surfaces findings via GitHub annotations. ## Existing Roadmap Updates - Normalised 2025q4 commitments across Web, DST, QA, SC, and other units by filling in missing descriptions, deliverables, schedule notes, recurring task statuses, and maintenance tasks. - Added tasks where absent, removed remaining template placeholders, aligned fully qualified names, and ensured roadmap files conform to the new validator checks. ## Testing ```bash python tools/roadmap_validator/validate.py *2025q4* ``` CI: `Roadmap Validator` workflow runs automatically on pushes/dispatch. --------- Co-authored-by: kaiserd <1684595+kaiserd@users.noreply.github.com>
89 lines
2.6 KiB
Python
89 lines
2.6 KiB
Python
"""Filesystem helpers for roadmap validation."""
|
|
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Iterable, List, Optional
|
|
|
|
from constants import SKIP_FILENAMES
|
|
|
|
ALLOWED_CONTENT_SUBDIRS = {
|
|
"dst",
|
|
"qa",
|
|
"nim",
|
|
"p2p",
|
|
"rfc",
|
|
"sc",
|
|
"sec",
|
|
"web",
|
|
}
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
CONTENT_ROOT = REPO_ROOT / "content"
|
|
|
|
|
|
def _is_allowed_content_path(path: Path) -> bool:
|
|
"""Return True when the path resides in an allowed content subdirectory."""
|
|
try:
|
|
relative = path.resolve().relative_to(CONTENT_ROOT)
|
|
except ValueError:
|
|
return True
|
|
parts = relative.parts
|
|
if not parts:
|
|
return False
|
|
return parts[0].lower() in ALLOWED_CONTENT_SUBDIRS
|
|
|
|
|
|
def should_skip(path: Path) -> bool:
|
|
"""Return True if the file should be ignored by the validator."""
|
|
return path.name.lower() in SKIP_FILENAMES
|
|
|
|
|
|
def resolve_user_path(raw_target: str) -> Optional[Path]:
|
|
"""Resolve a user-supplied path relative to cwd, repo root, or content root."""
|
|
raw_path = Path(raw_target).expanduser()
|
|
search_paths: List[Path] = []
|
|
if raw_path.is_absolute():
|
|
search_paths.append(raw_path)
|
|
else:
|
|
search_paths.extend(
|
|
[
|
|
Path.cwd() / raw_path,
|
|
REPO_ROOT / raw_path,
|
|
CONTENT_ROOT / raw_path,
|
|
]
|
|
)
|
|
for candidate in search_paths:
|
|
if candidate.exists():
|
|
return candidate.resolve()
|
|
return None
|
|
|
|
|
|
def resolve_targets(targets: Iterable[str]) -> List[Path]:
|
|
"""Expand iterable of target paths into unique Markdown files."""
|
|
md_files: List[Path] = []
|
|
seen: set[Path] = set()
|
|
for raw_target in targets:
|
|
target = resolve_user_path(raw_target)
|
|
if target is None:
|
|
sys.stderr.write(f"Warning: skipping unknown path {raw_target!r}\n")
|
|
continue
|
|
if target.is_dir():
|
|
for file_path in sorted(target.rglob("*.md")):
|
|
if (
|
|
should_skip(file_path)
|
|
or file_path in seen
|
|
or not _is_allowed_content_path(file_path)
|
|
):
|
|
continue
|
|
md_files.append(file_path)
|
|
seen.add(file_path)
|
|
elif target.is_file() and target.suffix.lower() == ".md":
|
|
if should_skip(target) or target in seen or not _is_allowed_content_path(target):
|
|
continue
|
|
md_files.append(target)
|
|
seen.add(target)
|
|
else:
|
|
sys.stderr.write(f"Warning: skipping non-markdown path {raw_target!r}\n")
|
|
return md_files
|