Files
Cognitive_Agent/tools/validate_repository.py
T
2026-08-26 20:11:09 +08:00

310 lines
12 KiB
Python
Executable File

#!/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
from generate_can_semantic_protocol import (
DEFAULT_CPP as CAN_PROTOCOL_CPP,
DEFAULT_PYTHON as CAN_PROTOCOL_PYTHON,
DEFAULT_SOURCE as CAN_PROTOCOL_SOURCE,
ProtocolError as CanProtocolError,
load_and_validate as load_can_protocol,
render_cpp as render_can_protocol_cpp,
render_python as render_can_protocol_python,
)
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 "<root>"
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 "<root>"
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 validate_generated_can_protocol(errors: list[str]) -> int:
try:
document = load_can_protocol(CAN_PROTOCOL_SOURCE)
expected = {
CAN_PROTOCOL_PYTHON: render_can_protocol_python(document),
CAN_PROTOCOL_CPP: render_can_protocol_cpp(document),
}
except CanProtocolError as exc:
errors.append(f"{relative(CAN_PROTOCOL_SOURCE)}: {exc}")
return 0
for path, content in expected.items():
if not path.is_file():
errors.append(f"{relative(path)}: generated CAN protocol view is missing")
elif path.read_text(encoding="utf-8") != content:
errors.append(
f"{relative(path)}: stale generated CAN protocol view; "
"run make can-protocol-generate"
)
return len(document["rules"])
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)
protocol_rule_count = validate_generated_can_protocol(errors)
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), "
f"{protocol_rule_count} generated CAN protocol rule(s)."
)
return 0
if __name__ == "__main__":
raise SystemExit(main())