#!/usr/bin/env python3 """Read-only consistency validator for the detached knowledge base.""" from __future__ import annotations import ast import hashlib import json import sys from pathlib import Path from typing import Any KNOWLEDGE_ROOT = Path(__file__).resolve().parents[1] BACKEND_ROOT = KNOWLEDGE_ROOT.parent ENTRY_DIRS = { "component": KNOWLEDGE_ROOT / "components", "relation": KNOWLEDGE_ROOT / "relations", "mechanism": KNOWLEDGE_ROOT / "mechanisms", "composition": KNOWLEDGE_ROOT / "compositions", "evidence": KNOWLEDGE_ROOT / "evidence", } REQUIRED_FIELDS = { "component": {"schema_version", "id", "version", "kind", "parameters", "ports", "capability_refs", "maturity"}, "relation": {"schema_version", "id", "version", "relation_type", "endpoint_rules", "constraint_rules", "capability_refs", "maturity"}, "mechanism": {"schema_version", "id", "version", "legacy_family", "members", "relations", "external_ports", "boundary_profiles", "constraint_rules", "capability_refs", "maturity"}, "composition": {"schema_version", "id", "version", "composition_type", "source_port_types", "target_port_types", "connection_relation", "constraint_rules", "capability_refs", "execution_support", "maturity"}, "capability": {"schema_version", "id", "version", "kind", "implementation", "supports", "maturity"}, "evidence": {"schema_version", "id", "version", "evidence_type", "subject_refs", "artifacts", "maturity"}, } def load_json(path: Path, errors: list[str]) -> Any | None: try: return json.loads(path.read_text(encoding="utf-8")) except Exception as exc: # noqa: BLE001 errors.append(f"invalid_json:{path.relative_to(BACKEND_ROOT)}:{exc}") return None def sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def collect_entries(errors: list[str]) -> tuple[dict[str, dict[str, Any]], dict[str, str]]: entries: dict[str, dict[str, Any]] = {} kinds: dict[str, str] = {} for kind, root in ENTRY_DIRS.items(): for path in sorted(root.rglob("*.json")): data = load_json(path, errors) if not isinstance(data, dict): continue entry_id = data.get("id") if not isinstance(entry_id, str): errors.append(f"missing_id:{path.relative_to(BACKEND_ROOT)}") continue if entry_id in entries: errors.append(f"duplicate_id:{entry_id}") continue entries[entry_id] = data kinds[entry_id] = kind capability_registry_path = KNOWLEDGE_ROOT / "capabilities" / "registry.json" registry = load_json(capability_registry_path, errors) if isinstance(registry, dict): for data in registry.get("entries", []): if not isinstance(data, dict) or not isinstance(data.get("id"), str): errors.append("invalid_capability_registry_entry") continue entry_id = data["id"] if entry_id in entries: errors.append(f"duplicate_id:{entry_id}") continue entries[entry_id] = data kinds[entry_id] = "capability" return entries, kinds def validate_required_fields( entries: dict[str, dict[str, Any]], kinds: dict[str, str], errors: list[str], ) -> None: for entry_id, entry in entries.items(): kind = kinds[entry_id] missing = sorted(REQUIRED_FIELDS[kind] - set(entry)) if missing: errors.append(f"missing_fields:{entry_id}:{','.join(missing)}") if entry.get("schema_version") != "1.0": errors.append(f"unsupported_schema_version:{entry_id}:{entry.get('schema_version')}") expected_prefix = f"{kind}." if not entry_id.startswith(expected_prefix): errors.append(f"bad_id_prefix:{entry_id}:expected={expected_prefix}") def referenced_ids(entry: dict[str, Any], kind: str) -> list[str]: refs: list[str] = [] refs.extend(entry.get("capability_refs", [])) if kind == "mechanism": for member in entry.get("members", []): refs.extend( value for key in ("component_ref", "mechanism_ref") if isinstance((value := member.get(key)), str) ) for relation in entry.get("relations", []): refs.extend( value for key in ("relation_ref", "composition_ref") if isinstance((value := relation.get(key)), str) ) elif kind == "composition": relation_ref = entry.get("connection_relation") if isinstance(relation_ref, str): refs.append(relation_ref) elif kind == "evidence": refs.extend(entry.get("subject_refs", [])) return refs def validate_references( entries: dict[str, dict[str, Any]], kinds: dict[str, str], port_ids: set[str], errors: list[str], ) -> None: for entry_id, entry in entries.items(): for reference in referenced_ids(entry, kinds[entry_id]): if reference not in entries: errors.append(f"unknown_reference:{entry_id}:{reference}") for port in entry.get("ports", []): port_type = port.get("type") if port_type not in port_ids: errors.append(f"unknown_port_type:{entry_id}:{port_type}") for port in entry.get("external_ports", []): port_type = port.get("type") if port_type not in port_ids: errors.append(f"unknown_port_type:{entry_id}:{port_type}") for key in ("source_port_types", "target_port_types"): for port_type in entry.get(key, []): if port_type not in port_ids: errors.append(f"unknown_port_type:{entry_id}:{port_type}") def validate_mechanism_graphs(entries: dict[str, dict[str, Any]], errors: list[str]) -> None: for entry_id, mechanism in entries.items(): if not entry_id.startswith("mechanism."): continue member_ids = {member.get("id") for member in mechanism.get("members", [])} if None in member_ids or len(member_ids) != len(mechanism.get("members", [])): errors.append(f"invalid_or_duplicate_member:{entry_id}") relation_ids: set[str] = set() for relation in mechanism.get("relations", []): relation_id = relation.get("id") if relation_id in relation_ids or not relation_id: errors.append(f"invalid_or_duplicate_relation:{entry_id}:{relation_id}") relation_ids.add(relation_id) for endpoint in ("source", "target"): if relation.get(endpoint) not in member_ids: errors.append( f"unknown_relation_endpoint:{entry_id}:{relation_id}:{endpoint}={relation.get(endpoint)}" ) carrier = relation.get("carrier_ref") if carrier is not None and carrier not in member_ids: errors.append(f"unknown_carrier_ref:{entry_id}:{relation_id}:{carrier}") for port in mechanism.get("external_ports", []): if port.get("member") not in member_ids: errors.append(f"unknown_external_port_member:{entry_id}:{port.get('id')}") for profile in mechanism.get("boundary_profiles", []): boundary_members = [profile.get("input"), profile.get("output"), *profile.get("fixed", [])] for member in boundary_members: root_member = member.split(".", 1)[0] if isinstance(member, str) else member if root_member not in member_ids: errors.append(f"unknown_boundary_member:{entry_id}:{profile.get('id')}:{member}") def validate_capability_sources(entries: dict[str, dict[str, Any]], errors: list[str]) -> None: for entry_id, entry in entries.items(): if not entry_id.startswith("capability."): continue implementation = entry.get("implementation", {}) source_path = implementation.get("source_path") if not isinstance(source_path, str): errors.append(f"missing_capability_source:{entry_id}") continue source = BACKEND_ROOT / source_path if not source.is_file(): errors.append(f"missing_capability_source:{entry_id}:{source_path}") continue symbol = implementation.get("symbol") if symbol: try: tree = ast.parse(source.read_text(encoding="utf-8")) except SyntaxError as exc: errors.append(f"invalid_capability_source_python:{entry_id}:{exc}") continue top_level_symbols = { node.name for node in tree.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) } if symbol not in top_level_symbols: errors.append(f"missing_capability_symbol:{entry_id}:{source_path}:{symbol}") def validate_topology_hashes(entries: dict[str, dict[str, Any]], errors: list[str]) -> None: for entry_id, entry in entries.items(): source_topology = entry.get("source_topology") if not source_topology: continue source = BACKEND_ROOT / source_topology["path"] if not source.is_file(): errors.append(f"missing_source_topology:{entry_id}:{source_topology['path']}") continue actual = sha256_file(source) if actual != source_topology.get("sha256"): errors.append( f"source_topology_hash_mismatch:{entry_id}:expected={source_topology.get('sha256')}:actual={actual}" ) def validate_detached_boundary(errors: list[str]) -> None: for path in sorted((BACKEND_ROOT / "src").rglob("*.py")): try: tree = ast.parse(path.read_text(encoding="utf-8")) except SyntaxError: continue for node in ast.walk(tree): module: str | None = None if isinstance(node, ast.ImportFrom): module = node.module elif isinstance(node, ast.Import): for alias in node.names: if alias.name == "knowledge" or alias.name.startswith("knowledge."): errors.append(f"stable_kernel_imports_knowledge:{path.relative_to(BACKEND_ROOT)}:{alias.name}") if module == "knowledge" or (module and module.startswith("knowledge.")): errors.append(f"stable_kernel_imports_knowledge:{path.relative_to(BACKEND_ROOT)}:{module}") def main() -> int: errors: list[str] = [] for path in sorted(KNOWLEDGE_ROOT.rglob("*.json")): load_json(path, errors) catalog = load_json(KNOWLEDGE_ROOT / "catalog.json", errors) if isinstance(catalog, dict): if catalog.get("default_enabled") is not False: errors.append("catalog_default_enabled_must_be_false") if catalog.get("integration_state") != "not_connected": errors.append("catalog_integration_state_must_be_not_connected") entries, kinds = collect_entries(errors) validate_required_fields(entries, kinds, errors) ports = load_json(KNOWLEDGE_ROOT / "ontology" / "port_types.json", errors) port_ids = { entry["id"] for entry in (ports or {}).get("entries", []) if isinstance(entry, dict) and isinstance(entry.get("id"), str) } validate_references(entries, kinds, port_ids, errors) validate_mechanism_graphs(entries, errors) validate_capability_sources(entries, errors) validate_topology_hashes(entries, errors) validate_detached_boundary(errors) if errors: print(f"knowledge validation failed: {len(errors)} error(s)", file=sys.stderr) for error in errors: print(f"- {error}", file=sys.stderr) return 1 counts: dict[str, int] = {} for kind in kinds.values(): counts[kind] = counts.get(kind, 0) + 1 count_text = ", ".join(f"{kind}={counts[kind]}" for kind in sorted(counts)) print(f"knowledge validation passed: {len(entries)} entries ({count_text})") print("integration_state=not_connected default_enabled=false stable_kernel_imports=0") return 0 if __name__ == "__main__": raise SystemExit(main())