@tonyclaw/eval-harness 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ def read_json(path: Path) -> dict[str, Any]:
10
+ return json.loads(path.read_text(encoding="utf-8-sig"))
11
+
12
+
13
+ def schema_type_matches(value: Any, expected: str) -> bool:
14
+ if expected == "object":
15
+ return isinstance(value, dict)
16
+ if expected == "array":
17
+ return isinstance(value, list)
18
+ if expected == "string":
19
+ return isinstance(value, str)
20
+ if expected == "integer":
21
+ return isinstance(value, int) and not isinstance(value, bool)
22
+ if expected == "number":
23
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
24
+ if expected == "boolean":
25
+ return isinstance(value, bool)
26
+ if expected == "null":
27
+ return value is None
28
+ return True
29
+
30
+
31
+ def schema_value_type(value: Any) -> str:
32
+ if value is None:
33
+ return "null"
34
+ if isinstance(value, bool):
35
+ return "boolean"
36
+ if isinstance(value, int):
37
+ return "integer"
38
+ if isinstance(value, float):
39
+ return "number"
40
+ if isinstance(value, str):
41
+ return "string"
42
+ if isinstance(value, list):
43
+ return "array"
44
+ if isinstance(value, dict):
45
+ return "object"
46
+ return type(value).__name__
47
+
48
+
49
+ def validate_schema_document(data: Any, schema: dict[str, Any], path: str = "$") -> list[str]:
50
+ errors: list[str] = []
51
+ expected_type = schema.get("type")
52
+ expected_types = expected_type if isinstance(expected_type, list) else ([expected_type] if expected_type else [])
53
+ if expected_types and not any(schema_type_matches(data, str(item)) for item in expected_types):
54
+ errors.append(f"{path}: expected {'/'.join(str(item) for item in expected_types)}, got {schema_value_type(data)}")
55
+ return errors
56
+
57
+ if "const" in schema and data != schema["const"]:
58
+ errors.append(f"{path}: expected const {schema['const']!r}, got {data!r}")
59
+ if "enum" in schema and data not in schema.get("enum", []):
60
+ errors.append(f"{path}: expected one of {schema.get('enum')!r}, got {data!r}")
61
+
62
+ if isinstance(data, dict):
63
+ errors.extend(validate_object(data, schema, path))
64
+ if isinstance(data, list):
65
+ errors.extend(validate_array(data, schema, path))
66
+ if isinstance(data, str):
67
+ errors.extend(validate_string(data, schema, path))
68
+ if isinstance(data, (int, float)) and not isinstance(data, bool):
69
+ errors.extend(validate_number(data, schema, path))
70
+ return errors
71
+
72
+
73
+ def validate_object(data: dict[str, Any], schema: dict[str, Any], path: str) -> list[str]:
74
+ errors: list[str] = []
75
+ for key in schema.get("required", []) or []:
76
+ if key not in data:
77
+ errors.append(f"{path}.{key}: missing required property")
78
+
79
+ properties = schema.get("properties") or {}
80
+ for key, child_schema in properties.items():
81
+ if key in data and isinstance(child_schema, dict):
82
+ errors.extend(validate_schema_document(data[key], child_schema, f"{path}.{key}"))
83
+
84
+ additional = schema.get("additionalProperties", True)
85
+ if additional is False:
86
+ extra = sorted(key for key in data if key not in properties)
87
+ errors.extend(f"{path}.{key}: additional property not allowed" for key in extra)
88
+ elif isinstance(additional, dict):
89
+ for key, value in data.items():
90
+ if key not in properties:
91
+ errors.extend(validate_schema_document(value, additional, f"{path}.{key}"))
92
+ return errors
93
+
94
+
95
+ def validate_array(data: list[Any], schema: dict[str, Any], path: str) -> list[str]:
96
+ errors: list[str] = []
97
+ min_items = schema.get("minItems")
98
+ max_items = schema.get("maxItems")
99
+ if isinstance(min_items, int) and len(data) < min_items:
100
+ errors.append(f"{path}: expected at least {min_items} item(s), got {len(data)}")
101
+ if isinstance(max_items, int) and len(data) > max_items:
102
+ errors.append(f"{path}: expected at most {max_items} item(s), got {len(data)}")
103
+ if isinstance(schema.get("items"), dict):
104
+ for index, item in enumerate(data):
105
+ errors.extend(validate_schema_document(item, schema["items"], f"{path}[{index}]"))
106
+ return errors
107
+
108
+
109
+ def validate_string(data: str, schema: dict[str, Any], path: str) -> list[str]:
110
+ errors: list[str] = []
111
+ min_length = schema.get("minLength")
112
+ max_length = schema.get("maxLength")
113
+ if isinstance(min_length, int) and len(data) < min_length:
114
+ errors.append(f"{path}: expected length >= {min_length}, got {len(data)}")
115
+ if isinstance(max_length, int) and len(data) > max_length:
116
+ errors.append(f"{path}: expected length <= {max_length}, got {len(data)}")
117
+ pattern = schema.get("pattern")
118
+ if isinstance(pattern, str) and re.search(pattern, data) is None:
119
+ errors.append(f"{path}: expected string matching pattern {pattern!r}, got {data!r}")
120
+ return errors
121
+
122
+
123
+ def validate_number(data: int | float, schema: dict[str, Any], path: str) -> list[str]:
124
+ errors: list[str] = []
125
+ minimum = schema.get("minimum")
126
+ maximum = schema.get("maximum")
127
+ if isinstance(minimum, (int, float)) and data < minimum:
128
+ errors.append(f"{path}: expected >= {minimum}, got {data}")
129
+ if isinstance(maximum, (int, float)) and data > maximum:
130
+ errors.append(f"{path}: expected <= {maximum}, got {data}")
131
+ return errors
132
+
133
+
134
+ def load_schema(schema_dir: Path, schema_name: str) -> dict[str, Any]:
135
+ schema_path = schema_dir / schema_name
136
+ if not schema_path.exists() and not schema_dir.is_absolute():
137
+ source_tree_schema = Path(__file__).resolve().parents[2] / schema_dir / schema_name
138
+ if source_tree_schema.exists():
139
+ schema_path = source_tree_schema
140
+ if not schema_path.exists():
141
+ raise RuntimeError(f"schema not found: {schema_path}")
142
+ return read_json(schema_path)
143
+
144
+
145
+ def validate_json_artifact(path: Path, schema_dir: Path, schema_name: str) -> list[str]:
146
+ try:
147
+ data = read_json(path)
148
+ except Exception as exc: # noqa: BLE001 - validation command reports exact broken artifact.
149
+ return [f"{path}: invalid json: {exc}"]
150
+ schema = load_schema(schema_dir, schema_name)
151
+ return [f"{path}: {error}" for error in validate_schema_document(data, schema)]