66 lines
2.8 KiB
Python
66 lines
2.8 KiB
Python
"""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())
|