Files
roadmap/tools/roadmap_validator/paths.py
fbarbu15 845d6b8dcd Chore/roadmap validator (#318)
## 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>
2025-10-28 15:41:11 +02:00

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