generated from coulomb/repo-seed
Some checks failed
validate-registry / validate (push) Has been cancelled
T02: remove inherited capability.infotech.repo-template and template consumer docs (statehub-register, template-validation-checklist); add capability.infotech.config-surface-atlas and rewrite capabilities.yaml. T03: seed 4 configuration surfaces (state-hub api-config, ops-warden routing-catalog, reuse-surface federation-sources, ops-bridge tunnel-config) with registry/indexes/surfaces.yaml; source-linked, no values, secret deps by reference. T06: add tools/validate_registry.py (schema + index gate), Makefile (make validate), and .github/workflows/validate.yml (GitHub + Gitea Actions); document in stack-and-commands. Verified malformed entries are rejected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
105 lines
3.8 KiB
Python
105 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate config-atlas registry entries.
|
|
|
|
Gate run by agents and CI (ATLAS-WP-0002-T06). Checks:
|
|
1. Every registry/surfaces/*.md frontmatter validates against
|
|
schemas/surface-entry.schema.json (Draft 2020-12).
|
|
2. Entries never inline a configuration value or secret value
|
|
(enforced by additionalProperties:false in the schema).
|
|
3. registry/indexes/surfaces.yaml is consistent with the entry files
|
|
(same id set, and each declared path exists with a matching id).
|
|
|
|
Exit code 0 = pass, 1 = validation failure, 2 = setup error.
|
|
Dependencies: PyYAML, jsonschema.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
try:
|
|
import yaml
|
|
from jsonschema import Draft202012Validator
|
|
except ImportError as exc: # pragma: no cover
|
|
print(f"setup error: missing dependency ({exc}). pip install pyyaml jsonschema", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
SCHEMA_PATH = ROOT / "schemas" / "surface-entry.schema.json"
|
|
SURFACES_DIR = ROOT / "registry" / "surfaces"
|
|
SURFACES_INDEX = ROOT / "registry" / "indexes" / "surfaces.yaml"
|
|
|
|
FRONTMATTER = re.compile(r"^---\n(.*?)\n---\n", re.S)
|
|
|
|
|
|
def load_frontmatter(path: Path) -> dict:
|
|
m = FRONTMATTER.match(path.read_text())
|
|
if not m:
|
|
raise ValueError(f"{path}: missing YAML frontmatter")
|
|
data = yaml.safe_load(m.group(1))
|
|
if not isinstance(data, dict):
|
|
raise ValueError(f"{path}: frontmatter is not a mapping")
|
|
return data
|
|
|
|
|
|
def main() -> int:
|
|
if not SCHEMA_PATH.exists():
|
|
print(f"setup error: schema not found at {SCHEMA_PATH}", file=sys.stderr)
|
|
return 2
|
|
|
|
schema = json.loads(SCHEMA_PATH.read_text())
|
|
Draft202012Validator.check_schema(schema)
|
|
validator = Draft202012Validator(schema)
|
|
|
|
errors: list[str] = []
|
|
seen_ids: dict[str, Path] = {}
|
|
|
|
entry_files = sorted(SURFACES_DIR.glob("*.md")) if SURFACES_DIR.exists() else []
|
|
for path in entry_files:
|
|
try:
|
|
fm = load_frontmatter(path)
|
|
except ValueError as exc:
|
|
errors.append(str(exc))
|
|
continue
|
|
for err in sorted(validator.iter_errors(fm), key=lambda e: list(e.path)):
|
|
loc = "/".join(str(p) for p in err.path) or "(root)"
|
|
errors.append(f"{path.name}: {loc}: {err.message}")
|
|
sid = fm.get("id")
|
|
if sid in seen_ids:
|
|
errors.append(f"{path.name}: duplicate id '{sid}' (also {seen_ids[sid].name})")
|
|
elif sid:
|
|
seen_ids[sid] = path
|
|
# filename should match the id for discoverability
|
|
if sid and path.stem != sid:
|
|
errors.append(f"{path.name}: filename does not match id '{sid}'")
|
|
|
|
# index consistency
|
|
if SURFACES_INDEX.exists():
|
|
index = yaml.safe_load(SURFACES_INDEX.read_text()) or {}
|
|
indexed = {row.get("id"): row for row in index.get("surfaces", [])}
|
|
for sid in indexed.keys() - seen_ids.keys():
|
|
errors.append(f"surfaces.yaml: id '{sid}' indexed but no entry file found")
|
|
for sid in seen_ids.keys() - indexed.keys():
|
|
errors.append(f"surfaces.yaml: entry '{sid}' present but not indexed")
|
|
for sid, row in indexed.items():
|
|
declared = row.get("path")
|
|
if declared and not (ROOT / declared).exists():
|
|
errors.append(f"surfaces.yaml: '{sid}' path '{declared}' does not exist")
|
|
elif entry_files:
|
|
errors.append("registry/indexes/surfaces.yaml missing but surface entries exist")
|
|
|
|
if errors:
|
|
print(f"FAIL: {len(errors)} problem(s) in registry validation:", file=sys.stderr)
|
|
for e in errors:
|
|
print(f" - {e}", file=sys.stderr)
|
|
return 1
|
|
|
|
print(f"OK: {len(entry_files)} surface entr{'y' if len(entry_files)==1 else 'ies'} valid; index consistent.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|