mirror of
https://github.com/vacp2p/roadmap.git
synced 2026-01-08 21:27:58 -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>
81 lines
2.6 KiB
Python
81 lines
2.6 KiB
Python
"""High-level validation orchestration."""
|
|
|
|
from pathlib import Path
|
|
from typing import List, Optional, Tuple
|
|
|
|
import yaml
|
|
|
|
from identity import derive_identity, validate_identity
|
|
from issues import ValidationIssue
|
|
from paths import should_skip
|
|
from tasks import TaskReport, parse_tasks
|
|
from templates import TemplateValidator
|
|
|
|
|
|
def parse_front_matter(lines: List[str]) -> Tuple[dict, int]:
|
|
if not lines:
|
|
return {}, 0
|
|
idx = 0
|
|
while idx < len(lines) and not lines[idx].strip():
|
|
idx += 1
|
|
if idx >= len(lines) or lines[idx].strip() != "---":
|
|
return {}, 0
|
|
end_idx = idx + 1
|
|
while end_idx < len(lines) and lines[end_idx].strip() != "---":
|
|
end_idx += 1
|
|
if end_idx >= len(lines):
|
|
raise ValueError("Unterminated YAML front matter")
|
|
data = yaml.safe_load("\n".join(lines[idx + 1 : end_idx])) or {}
|
|
if not isinstance(data, dict):
|
|
raise ValueError("Front matter must parse to a mapping")
|
|
return data, end_idx + 1
|
|
|
|
|
|
def validate_file(path: Path) -> List[ValidationIssue]:
|
|
if should_skip(path):
|
|
return []
|
|
|
|
content = path.read_text(encoding="utf-8")
|
|
lines = content.splitlines()
|
|
issues: List[ValidationIssue] = []
|
|
|
|
try:
|
|
front_matter, body_start = parse_front_matter(lines)
|
|
except Exception as exc: # pragma: no cover - defensive guard
|
|
return [ValidationIssue(path, 1, f"failed to parse front matter: {exc}")]
|
|
|
|
identity, identity_issues = derive_identity(path)
|
|
issues.extend(identity_issues)
|
|
|
|
expected_base = None
|
|
if identity:
|
|
issues.extend(
|
|
validate_identity(path, front_matter, lines, body_start, identity)
|
|
)
|
|
expected_base = identity.expected_base
|
|
|
|
template_validator = TemplateValidator(path=path, lines=lines, body_start=body_start, front_matter=front_matter)
|
|
template_issues = template_validator.run()
|
|
issues.extend(template_issues)
|
|
|
|
if template_validator.task_section is None:
|
|
issues.append(ValidationIssue(path, None, "missing `## Task List` section"))
|
|
return issues
|
|
|
|
start, end = template_validator.task_section
|
|
tasks: List[TaskReport] = parse_tasks(lines, start, end, expected_base)
|
|
if not tasks:
|
|
issues.append(ValidationIssue(path, None, "no tasks found under `## Task List`"))
|
|
return issues
|
|
|
|
for task in tasks:
|
|
for task_issue in task.issues:
|
|
issues.append(
|
|
ValidationIssue(
|
|
path=path,
|
|
line=task_issue.line,
|
|
message=f"Task `{task.name}` {task_issue.message}",
|
|
)
|
|
)
|
|
return issues
|