awesome-loop-engineering / scripts /check_loop_contract_examples.py
cy0307's picture
Sync awesome-loop-engineering
9ec4919 verified
Raw
History Blame Contribute Delete
3.52 kB
#!/usr/bin/env python3
"""Validate example loop contracts against the repository schema.
This intentionally supports only the JSON Schema keywords used by
schemas/loop-contract.schema.json, keeping CI dependency-free.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Any
SCHEMA_PATH = Path("schemas/loop-contract.schema.json")
EXAMPLE_GLOB = "examples/*-loop.json"
def type_matches(value: Any, expected: str) -> bool:
if expected == "object":
return isinstance(value, dict)
if expected == "array":
return isinstance(value, list)
if expected == "string":
return isinstance(value, str)
if expected == "integer":
return isinstance(value, int) and not isinstance(value, bool)
return True
def validate(value: Any, schema: dict[str, Any], path: str) -> list[str]:
errors: list[str] = []
expected_type = schema.get("type")
if isinstance(expected_type, str) and not type_matches(value, expected_type):
return [f"{path}: expected {expected_type}, got {type(value).__name__}"]
if "enum" in schema and value not in schema["enum"]:
errors.append(f"{path}: expected one of {schema['enum']!r}, got {value!r}")
if isinstance(value, str):
min_length = schema.get("minLength")
if isinstance(min_length, int) and len(value) < min_length:
errors.append(f"{path}: string is shorter than {min_length}")
if isinstance(value, int) and not isinstance(value, bool):
minimum = schema.get("minimum")
if isinstance(minimum, int) and value < minimum:
errors.append(f"{path}: value is less than {minimum}")
if isinstance(value, list):
min_items = schema.get("minItems")
if isinstance(min_items, int) and len(value) < min_items:
errors.append(f"{path}: array has fewer than {min_items} items")
item_schema = schema.get("items")
if isinstance(item_schema, dict):
for index, item in enumerate(value):
errors.extend(validate(item, item_schema, f"{path}[{index}]"))
if isinstance(value, dict):
required = schema.get("required", [])
for key in required:
if key not in value:
errors.append(f"{path}: missing required key {key!r}")
properties = schema.get("properties", {})
if schema.get("additionalProperties") is False:
extra = sorted(set(value) - set(properties))
for key in extra:
errors.append(f"{path}: unexpected key {key!r}")
for key, child_schema in properties.items():
if key in value and isinstance(child_schema, dict):
errors.extend(validate(value[key], child_schema, f"{path}.{key}"))
return errors
def main() -> int:
schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
failures: list[str] = []
examples = sorted(Path().glob(EXAMPLE_GLOB))
if not examples:
print(f"No example files found for {EXAMPLE_GLOB}", file=sys.stderr)
return 1
for example in examples:
data = json.loads(example.read_text(encoding="utf-8"))
failures.extend(validate(data, schema, str(example)))
if failures:
print("Loop contract example validation failed:", file=sys.stderr)
for failure in failures:
print(f"- {failure}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())