-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy path0001-Tighten-typing-for-validators-and-observability.patch
More file actions
251 lines (222 loc) · 8.58 KB
/
0001-Tighten-typing-for-validators-and-observability.patch
File metadata and controls
251 lines (222 loc) · 8.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
From 9c14419f4836c515ba59482b94a365414e64c0c9 Mon Sep 17 00:00:00 2001
From: Reuben Bowlby <reuben@hummbl.io>
Date: Fri, 2 Jan 2026 17:55:24 -0500
Subject: [PATCH 1/3] Tighten typing for validators and observability
- Add concrete typing to validator functions and observability sink/event helpers
- Safe access for registry/maps, string coercion for subclass ids, and pyright suppression for jsonschema
- Type fixes in tests for Path inputs and event sink signatures
---
base120/observability.py | 25 ++++++++++++++-----------
base120/validators/errors.py | 16 ++++++++++------
base120/validators/mappings.py | 7 +++++--
base120/validators/schema.py | 8 ++++++--
base120/validators/validate.py | 27 +++++++++++++++++----------
tests/test_corpus.py | 2 +-
tests/test_observability.py | 4 +++-
7 files changed, 56 insertions(+), 33 deletions(-)
diff --git a/base120/observability.py b/base120/observability.py
index 388a6d1..cc19218 100644
--- a/base120/observability.py
+++ b/base120/observability.py
@@ -5,12 +5,14 @@ Provides structured event emission for validator runs.
Uses standard library only - no runtime dependencies.
"""
+from typing import Any, Callable, Iterable, Mapping, Optional, TextIO, cast
+
import json
import sys
from datetime import datetime, timezone
-def create_event_sink(output=None):
+def create_event_sink(output: Optional[TextIO] = None) -> Callable[[Mapping[str, Any]], None]:
"""
Create a standard event sink that logs structured JSON events.
@@ -26,8 +28,9 @@ def create_event_sink(output=None):
"""
if output is None:
output = sys.stdout
+ output = cast(TextIO, output)
- def sink(event):
+ def sink(event: Mapping[str, Any]) -> None:
try:
json.dump(event, output)
output.write('\n')
@@ -41,13 +44,13 @@ def create_event_sink(output=None):
def create_validator_event(
- artifact_id,
- schema_version,
- result,
- error_codes,
- failure_mode_ids,
- correlation_id=None
-):
+ artifact_id: str,
+ schema_version: str,
+ result: str,
+ error_codes: Iterable[str],
+ failure_mode_ids: Iterable[str],
+ correlation_id: Optional[str] = None,
+) -> Mapping[str, Any]:
"""
Create a validator_result event conforming to the observability schema.
@@ -62,12 +65,12 @@ def create_validator_event(
Returns:
Dict conforming to validator_result event schema
"""
- event = {
+ event: dict[str, Any] = {
"event_type": "validator_result",
"artifact_id": artifact_id,
"schema_version": schema_version,
"result": result,
- "error_codes": error_codes,
+ "error_codes": list(error_codes),
"failure_mode_ids": sorted(failure_mode_ids), # Ensure sorted for consistency
"timestamp": datetime.now(timezone.utc).isoformat()
}
diff --git a/base120/validators/errors.py b/base120/validators/errors.py
index 32ae3e7..7fbbb00 100644
--- a/base120/validators/errors.py
+++ b/base120/validators/errors.py
@@ -1,14 +1,18 @@
-def resolve_errors(fms: list[str], err_registry: list[dict]) -> list[str]:
+from typing import Any, Mapping, Sequence
+
+
+def resolve_errors(fms: list[str], err_registry: Sequence[Mapping[str, Any]]) -> list[str]:
# FM30 dominance: escalation suppresses all other errors
if "FM30" in fms:
return sorted(
- entry["id"]
+ str(entry.get("id", ""))
for entry in err_registry
- if "FM30" in entry["fm"]
+ if "FM30" in entry.get("fm", [])
)
- errs = []
+ errs: list[str] = []
for entry in err_registry:
- if any(fm in fms for fm in entry["fm"]):
- errs.append(entry["id"])
+ fm_list = entry.get("fm", [])
+ if any(fm in fms for fm in fm_list):
+ errs.append(str(entry.get("id", "")))
return sorted(set(errs))
diff --git a/base120/validators/mappings.py b/base120/validators/mappings.py
index 987e6c5..fc1e86c 100644
--- a/base120/validators/mappings.py
+++ b/base120/validators/mappings.py
@@ -1,2 +1,5 @@
-def resolve_failure_modes(subclass: str, mappings: dict) -> list[str]:
- return mappings.get("mappings", {}).get(subclass, [])
+from typing import Mapping, Sequence
+
+
+def resolve_failure_modes(subclass: str, mappings: Mapping[str, Mapping[str, Sequence[str]]]) -> list[str]:
+ return list(mappings.get("mappings", {}).get(subclass, []))
diff --git a/base120/validators/schema.py b/base120/validators/schema.py
index e786cd2..e06579e 100644
--- a/base120/validators/schema.py
+++ b/base120/validators/schema.py
@@ -1,6 +1,10 @@
+from typing import Any, Mapping
+
+# pyright: reportMissingModuleSource=false
from jsonschema import Draft202012Validator
-def validate_schema(artifact: dict, schema: dict) -> list[str]:
+
+def validate_schema(artifact: Mapping[str, Any], schema: Mapping[str, Any]) -> list[str]:
validator = Draft202012Validator(schema)
- errors = list(validator.iter_errors(artifact))
+ errors = list(validator.iter_errors(artifact)) # type: ignore[call-overload]
return ["ERR-SCHEMA-001"] if errors else []
diff --git a/base120/validators/validate.py b/base120/validators/validate.py
index deb6a04..aae1783 100644
--- a/base120/validators/validate.py
+++ b/base120/validators/validate.py
@@ -1,17 +1,19 @@
+from typing import Any, Callable, Mapping, MutableSequence, Optional, Sequence
+
from base120.validators.schema import validate_schema
from base120.validators.mappings import resolve_failure_modes
from base120.validators.errors import resolve_errors
def validate_artifact(
- artifact: dict,
- schema: dict,
- mappings: dict,
- err_registry: list[dict],
- event_sink=None
+ artifact: Mapping[str, Any],
+ schema: Mapping[str, Any],
+ mappings: Mapping[str, Any],
+ err_registry: Sequence[Mapping[str, Any]],
+ event_sink: Optional[Callable[[Mapping[str, Any]], None]] = None,
) -> list[str]:
- errs = []
- fms = []
+ errs: MutableSequence[str] = []
+ fms: list[str] = []
# 1. Schema validation
errs.extend(validate_schema(artifact, schema))
@@ -22,7 +24,7 @@ def validate_artifact(
return sorted(set(errs))
# 2. Subclass → FM
- subclass = artifact.get("class")
+ subclass = str(artifact.get("class", ""))
fms = resolve_failure_modes(subclass, mappings)
# 3. FM → ERR
@@ -34,7 +36,12 @@ def validate_artifact(
return sorted(set(errs))
-def _emit_event(artifact, error_codes, failure_mode_ids, event_sink):
+def _emit_event(
+ artifact: Mapping[str, Any],
+ error_codes: Sequence[str],
+ failure_mode_ids: Sequence[str],
+ event_sink: Optional[Callable[[Mapping[str, Any]], None]],
+) -> None:
"""
Emit validator_result event if event_sink is provided.
@@ -54,7 +61,7 @@ def _emit_event(artifact, error_codes, failure_mode_ids, event_sink):
schema_version="v1.0.0",
result=result,
error_codes=sorted(set(error_codes)),
- failure_mode_ids=failure_mode_ids
+ failure_mode_ids=list(failure_mode_ids),
)
event_sink(event)
diff --git a/tests/test_corpus.py b/tests/test_corpus.py
index a96a581..7dc0062 100644
--- a/tests/test_corpus.py
+++ b/tests/test_corpus.py
@@ -15,7 +15,7 @@ with open(ROOT / "registries" / "mappings.json") as f:
with open(ROOT / "registries" / "err.json") as f:
ERR_REGISTRY = json.load(f)["registry"]
-def load_json(path):
+def load_json(path: str | Path):
with open(path) as f:
return json.load(f)
diff --git a/tests/test_observability.py b/tests/test_observability.py
index f12f1e1..432fe03 100644
--- a/tests/test_observability.py
+++ b/tests/test_observability.py
@@ -7,6 +7,7 @@ Verifies event emission contract for validator runs.
import json
from pathlib import Path
from io import StringIO
+from typing import Any, Mapping
from base120.validators.validate import validate_artifact
from base120.observability import create_event_sink, create_validator_event
@@ -130,7 +131,7 @@ def test_backward_compatibility_without_event_sink():
def test_event_emission_failure_does_not_propagate():
"""Errors during event emission do not affect validation."""
- def failing_sink(event):
+ def failing_sink(event: Mapping[str, Any]) -> None:
raise Exception("Event emission failed!")
artifact = {
@@ -161,6 +162,7 @@ def test_unknown_artifact_id():
}
errors = validate_artifact(artifact, SCHEMA, MAPPINGS, ERR_REGISTRY, event_sink=event_sink)
+ assert errors == []
output.seek(0)
event = json.loads(output.read().strip())
--
2.52.0