"""Compare a captured ProjectBrain tools/list with the reviewed Hub catalog. Read-only: this never imports tools or edits catalog policy. Capture discovery from the owned connector and pass the JSON document using --discovery-file. """ from __future__ import annotations import argparse import json from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] def audit(discovery: dict[str, Any], catalog_root: Path) -> dict[str, Any]: tools = discovery.get("result", discovery).get("tools") if not isinstance(tools, list) or not tools: raise ValueError("non-empty tools/list result required") catalog = {} for path in sorted((catalog_root / "tools").glob("projectbrain-*.yaml")): spec = json.loads(path.read_text(encoding="utf-8"))["spec"] catalog[spec["canonicalName"]] = spec["inputSchema"] issues = [] seen = set() for tool in tools: name = tool["name"] if name in seen: issues.append({"tool": name, "code": "duplicate_tool"}) seen.add(name) if name not in catalog: issues.append({"tool": name, "code": "unreviewed_tool"}) continue upstream = tool["inputSchema"] local = catalog[name] for field in sorted(set(upstream.get("properties", {})) - set(local.get("properties", {}))): issues.append({"tool": name, "code": "missing_field", "field": field}) if set(upstream.get("required", [])) != set(local.get("required", [])): issues.append({"tool": name, "code": "required_fields_differ"}) for field, value in upstream.get("properties", {}).items(): target = local.get("properties", {}).get(field, {}) if "type" in value and "type" in target and value["type"] != target["type"]: issues.append({"tool": name, "code": "field_type_differs", "field": field}) if "enum" in value and "enum" in target and value["enum"] != target["enum"]: issues.append({"tool": name, "code": "field_enum_differs", "field": field}) for name in sorted(set(catalog) - seen): issues.append({"tool": name, "code": "upstream_tool_missing"}) return {"passed": not issues, "toolCount": len(tools), "issues": issues} def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--discovery-file", type=Path, required=True) parser.add_argument("--catalog-root", type=Path, default=ROOT / "catalog") args = parser.parse_args() if args.discovery_file.stat().st_size > 2 * 1024 * 1024: parser.error("discovery exceeds 2 MiB") result = audit(json.loads(args.discovery_file.read_text(encoding="utf-8")), args.catalog_root) print(json.dumps(result, sort_keys=True)) return 0 if result["passed"] else 1 if __name__ == "__main__": raise SystemExit(main())