#!/usr/bin/env python3 """Validate repository contracts and isolated domain deliverables.""" from __future__ import annotations import json import sys from pathlib import Path from typing import Any try: import yaml from jsonschema import Draft202012Validator, FormatChecker, RefResolver except ImportError as exc: # pragma: no cover - dependency bootstrap path raise SystemExit( f"Missing validation dependency: {exc.name}. Install with `pip install -e '.[test]'`." ) from exc ROOT = Path(__file__).resolve().parents[1] CONTRACT_SCHEMA_DIR = ROOT / "contracts" / "schemas" KIND_TO_SCHEMA = { "IntentIR": "intent-ir.schema.json", "FunctionConfig": "function-config.schema.json", "FunctionPreset": "function-preset.schema.json", "SceneConfig": "scene-config.schema.json", "CommandPlan": "command-plan.schema.json", "TraceEvent": "trace-event.schema.json", "ExecutionReport": "execution-report.schema.json", "DomainManifest": "domain-manifest.schema.json", "CapabilityCatalog": "capability-catalog.schema.json", "SignalMapping": "signal-mapping.schema.json", "PlatformManifest": "platform-manifest.schema.json", } def relative(path: Path) -> str: return str(path.relative_to(ROOT)) def load_json(path: Path) -> dict[str, Any]: with path.open(encoding="utf-8") as handle: value = json.load(handle) if not isinstance(value, dict): raise ValueError("root must be an object") return value def load_yaml(path: Path) -> dict[str, Any]: with path.open(encoding="utf-8") as handle: value = yaml.safe_load(handle) if not isinstance(value, dict): raise ValueError("root must be a mapping") return value def collect_schemas( errors: list[str], ) -> tuple[dict[str, dict[str, Any]], dict[str, dict[str, Any]]]: schemas: dict[str, dict[str, Any]] = {} schema_store: dict[str, dict[str, Any]] = {} for path in sorted(CONTRACT_SCHEMA_DIR.glob("*.json")): try: schema = load_json(path) Draft202012Validator.check_schema(schema) schema_id = schema.get("$id") if not isinstance(schema_id, str): errors.append(f"{relative(path)}: missing string $id") continue schemas[path.name] = schema schema_store[schema_id] = schema except Exception as exc: # noqa: BLE001 - report every repository error together errors.append(f"{relative(path)}: invalid JSON Schema: {exc}") return schemas, schema_store def public_documents() -> list[Path]: paths = list((ROOT / "contracts" / "examples").glob("*.yaml")) paths.extend((ROOT / "platforms").glob("*/manifest.yaml")) for domain_dir in sorted((ROOT / "domains").iterdir()): if not domain_dir.is_dir(): continue paths.extend( path for path in [ domain_dir / "manifest.yaml", domain_dir / "config" / "capabilities.yaml", domain_dir / "can" / "signal-mapping.yaml", ] if path.exists() ) paths.extend((domain_dir / "config" / "presets").glob("*.yaml")) return sorted(paths) def validate_documents( schemas: dict[str, dict[str, Any]], schema_store: dict[str, dict[str, Any]], errors: list[str], ) -> int: count = 0 checker = FormatChecker() for path in public_documents(): count += 1 try: document = load_yaml(path) except Exception as exc: # noqa: BLE001 errors.append(f"{relative(path)}: invalid YAML: {exc}") continue kind = document.get("kind") schema_name = KIND_TO_SCHEMA.get(kind) if schema_name is None: errors.append(f"{relative(path)}: unsupported or missing kind {kind!r}") continue schema = schemas.get(schema_name) if schema is None: errors.append(f"{relative(path)}: required schema {schema_name} was not loaded") continue resolver = RefResolver.from_schema(schema, store=schema_store) validator = Draft202012Validator( schema, resolver=resolver, format_checker=checker ) for issue in sorted(validator.iter_errors(document), key=lambda item: list(item.path)): location = ".".join(str(part) for part in issue.absolute_path) or "" errors.append(f"{relative(path)}:{location}: {issue.message}") if kind in {"FunctionConfig", "FunctionPreset"}: domain = document.get("spec", {}).get("domain") domain_schema_path = ROOT / "domains" / str(domain) / "schemas" / "function-parameters.schema.json" if domain_schema_path.exists(): try: domain_schema = load_json(domain_schema_path) Draft202012Validator.check_schema(domain_schema) domain_validator = Draft202012Validator(domain_schema, format_checker=checker) for issue in sorted( domain_validator.iter_errors(document), key=lambda item: list(item.path) ): location = ".".join(str(part) for part in issue.absolute_path) or "" errors.append( f"{relative(path)}:{location}: domain validation: {issue.message}" ) except Exception as exc: # noqa: BLE001 errors.append(f"{relative(domain_schema_path)}: invalid domain schema: {exc}") return count def validate_domains(errors: list[str]) -> int: count = 0 for domain_dir in sorted((ROOT / "domains").iterdir()): if not domain_dir.is_dir() or domain_dir.name.startswith("_"): continue count += 1 domain = domain_dir.name required_paths = [ domain_dir / "README.md", domain_dir / "OWNERS.yaml", domain_dir / "manifest.yaml", domain_dir / "config" / "capabilities.yaml", domain_dir / "schemas" / "function-parameters.schema.json", domain_dir / "can" / "signal-mapping.yaml", domain_dir / "tests" / "unit", domain_dir / "tests" / "contract", domain_dir / "tests" / "integration", ] for path in required_paths: if not path.exists(): errors.append(f"domains/{domain}: missing {relative(path)}") try: manifest = load_yaml(domain_dir / "manifest.yaml") catalog = load_yaml(domain_dir / "config" / "capabilities.yaml") mapping = load_yaml(domain_dir / "can" / "signal-mapping.yaml") except Exception as exc: # noqa: BLE001 errors.append(f"domains/{domain}: cannot perform semantic checks: {exc}") continue for label, document in [("manifest", manifest), ("catalog", catalog), ("mapping", mapping)]: actual_domain = document.get("spec", {}).get("domain") if actual_domain != domain: errors.append( f"domains/{domain}: {label} spec.domain is {actual_domain!r}, expected {domain!r}" ) manifest_spec = manifest.get("spec", {}) for field in ["function_schema", "capabilities", "signal_mapping"]: value = manifest_spec.get(field) if isinstance(value, str) and not (domain_dir / value).resolve().is_file(): errors.append(f"domains/{domain}: manifest {field} does not exist: {value}") capabilities = catalog.get("spec", {}).get("capabilities", []) capability_names = [item.get("name") for item in capabilities if isinstance(item, dict)] if len(capability_names) != len(set(capability_names)): errors.append(f"domains/{domain}: duplicate capability names") for name in capability_names: if not isinstance(name, str) or not name.startswith(f"{domain}."): errors.append(f"domains/{domain}: capability must start with '{domain}.': {name!r}") for preset_path in sorted((domain_dir / "config" / "presets").glob("*.yaml")): preset = load_yaml(preset_path) if preset.get("spec", {}).get("domain") != domain: errors.append(f"{relative(preset_path)}: preset domain must be {domain}") for operation in preset.get("spec", {}).get("operations", []): capability = operation.get("capability") if capability not in capability_names: errors.append( f"{relative(preset_path)}: unknown capability {capability!r}" ) mapping_spec = mapping.get("spec", {}) for item in mapping_spec.get("mappings", []): capability = item.get("capability") if capability not in capability_names: errors.append( f"domains/{domain}/can/signal-mapping.yaml: unknown capability {capability!r}" ) for platform in manifest_spec.get("supported_platforms", []): if not (ROOT / "platforms" / platform / "manifest.yaml").is_file(): errors.append(f"domains/{domain}: unsupported platform reference {platform!r}") return count def validate_platform_resources(errors: list[str], warnings: list[str]) -> int: count = 0 for manifest_path in sorted((ROOT / "platforms").glob("*/manifest.yaml")): count += 1 manifest = load_yaml(manifest_path) spec = manifest.get("spec", {}) platform_dir = manifest_path.parent execution_enabled = spec.get("execution_enabled") is True refs = list(spec.get("dbc_files", [])) + list(spec.get("calibrations", {}).values()) for ref in refs: target = platform_dir / str(ref) if target.is_file(): continue message = f"{relative(manifest_path)}: referenced platform resource is missing: {ref}" if execution_enabled: errors.append(message) else: warnings.append(message + " (allowed because execution_enabled is false)") return count def main() -> int: errors: list[str] = [] warnings: list[str] = [] schemas, schema_store = collect_schemas(errors) document_count = validate_documents(schemas, schema_store, errors) domain_count = validate_domains(errors) platform_count = validate_platform_resources(errors, warnings) for warning in warnings: print(f"WARNING: {warning}") if errors: for error in errors: print(f"ERROR: {error}", file=sys.stderr) print(f"Validation failed with {len(errors)} error(s).", file=sys.stderr) return 1 print( "Repository validation passed: " f"{len(schemas)} schemas, {document_count} documents, " f"{domain_count} domains, {platform_count} platform(s)." ) return 0 if __name__ == "__main__": raise SystemExit(main())