generated from coulomb/repo-seed
feat(registry): complete ATLAS-WP-0002 T02, T03, T06
Some checks failed
validate-registry / validate (push) Has been cancelled
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>
This commit is contained in:
104
tools/validate_registry.py
Normal file
104
tools/validate_registry.py
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user