core-runtime-engine 11.5.1__py3-none-any.whl
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.
- core_runtime/__init__.py +10 -0
- core_runtime/__version__.py +17 -0
- core_runtime/cli/__init__.py +7 -0
- core_runtime/cli/__main__.py +22 -0
- core_runtime/cli/bump_version.py +176 -0
- core_runtime/cli/contract_preflight.py +69 -0
- core_runtime/cli/create_domain.py +66 -0
- core_runtime/cli/doctor.py +44 -0
- core_runtime/cli/inventory.py +85 -0
- core_runtime/cli/lint.py +8 -0
- core_runtime/cli/main.py +579 -0
- core_runtime/cli/release_check.py +65 -0
- core_runtime/cli/repair_artifact_paths.py +48 -0
- core_runtime/cli/sync_template.py +67 -0
- core_runtime/cli/validate.py +73 -0
- core_runtime/core/__init__.py +34 -0
- core_runtime/core/audit_event.py +760 -0
- core_runtime/core/audit_trail_index.py +100 -0
- core_runtime/core/canonicalization.py +55 -0
- core_runtime/core/contract_evaluator.py +945 -0
- core_runtime/core/contract_executability.py +307 -0
- core_runtime/core/contract_loader.py +57 -0
- core_runtime/core/contract_probes.py +630 -0
- core_runtime/core/contract_program.py +131 -0
- core_runtime/core/contract_program_registry.py +104 -0
- core_runtime/core/contract_program_v2.py +126 -0
- core_runtime/core/dsk_v3.py +142 -0
- core_runtime/core/explainability.py +2115 -0
- core_runtime/core/numeric_normalization.py +120 -0
- core_runtime/core/rule_anchor.py +1388 -0
- core_runtime/core/schema_fingerprint.py +69 -0
- core_runtime/core/sensor_evidence.py +548 -0
- core_runtime/data/contracts/CoreAnchor.sol +109 -0
- core_runtime/data/contracts/CoreRuleAnchor.abi.json +111 -0
- core_runtime/data/contracts/CoreRuleAnchor.bin +1 -0
- core_runtime/data/contracts/CoreRuleAnchor.build.json +20 -0
- core_runtime/data/contracts/CoreRuleAnchor.runtime.bin +1 -0
- core_runtime/data/contracts/CoreRuleAnchor.sol +74 -0
- core_runtime/data/package_data_manifest.v1.json +173 -0
- core_runtime/data/schemas/core/causal_trace.v1.json +141 -0
- core_runtime/data/schemas/core/context_gate.v1.json +38 -0
- core_runtime/data/schemas/core/context_threshold.v1.json +46 -0
- core_runtime/data/schemas/core/contract_program.v1.json +186 -0
- core_runtime/data/schemas/core/contract_program.v2.json +187 -0
- core_runtime/data/schemas/core/control_decision.v1.json +114 -0
- core_runtime/data/schemas/core/dsk.v3.json +105 -0
- core_runtime/data/schemas/core/effect_result.v1.json +39 -0
- core_runtime/data/schemas/core/entropy_signal.v1.json +108 -0
- core_runtime/data/schemas/core/execution_receipt.v1.json +93 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v1.json +87 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v2.json +72 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v3.json +38 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v4.json +72 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v5.json +38 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v6.json +116 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v7.json +37 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v8.json +114 -0
- core_runtime/data/schemas/core/frozen_rule_set.v1.json +282 -0
- core_runtime/data/schemas/core/memory_artifact.v1.json +120 -0
- core_runtime/data/schemas/core/memory_generation_result.v1.json +37 -0
- core_runtime/data/schemas/core/operational_learning_event.v1.json +63 -0
- core_runtime/data/schemas/core/pattern_candidate.v1.json +114 -0
- core_runtime/data/schemas/core/physical_safety_assurance_case.v1.json +676 -0
- core_runtime/data/schemas/core/policy_lifecycle.v1.json +99 -0
- core_runtime/data/schemas/core/retention_manifest.v1.json +53 -0
- core_runtime/data/schemas/core/reversibility_policy.v1.json +107 -0
- core_runtime/data/schemas/core/rule_anchor_batch.v1.json +93 -0
- core_runtime/data/schemas/core/rule_anchor_chain_evidence.v1.json +56 -0
- core_runtime/data/schemas/core/rule_approval.v1.json +49 -0
- core_runtime/data/schemas/core/rule_approval_request.v1.json +42 -0
- core_runtime/data/schemas/core/state_transition.v1.json +115 -0
- core_runtime/data/schemas/core/task_closeout.v1.json +47 -0
- core_runtime/data/schemas/core/template_promotion_candidate.v1.json +83 -0
- core_runtime/data/schemas/core/unsigned_rule_anchor_deployment.v1.json +106 -0
- core_runtime/data/schemas/core/unsigned_rule_anchor_transaction.v1.json +116 -0
- core_runtime/tooling/__init__.py +48 -0
- core_runtime/tooling/bump_version.py +900 -0
- core_runtime/tooling/contract_preflight.py +293 -0
- core_runtime/tooling/create_domain.py +254 -0
- core_runtime/tooling/diagnostics.py +129 -0
- core_runtime/tooling/doctor.py +482 -0
- core_runtime/tooling/file_inventory.py +182 -0
- core_runtime/tooling/json_checks.py +100 -0
- core_runtime/tooling/release_check.py +1017 -0
- core_runtime/tooling/repair_artifact_paths.py +458 -0
- core_runtime/tooling/report_writer.py +176 -0
- core_runtime/tooling/repository_inventory.py +399 -0
- core_runtime/tooling/safety_checks.py +172 -0
- core_runtime/tooling/sync_template.py +303 -0
- core_runtime/tooling/validation.py +507 -0
- core_runtime/tooling/version_inventory.py +256 -0
- core_runtime_engine-11.5.1.dist-info/METADATA +35 -0
- core_runtime_engine-11.5.1.dist-info/RECORD +96 -0
- core_runtime_engine-11.5.1.dist-info/WHEEL +5 -0
- core_runtime_engine-11.5.1.dist-info/entry_points.txt +2 -0
- core_runtime_engine-11.5.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2115 @@
|
|
|
1
|
+
"""CORE v4.3 - Static Explainability Layer.
|
|
2
|
+
|
|
3
|
+
Read-only explanations over completed CORE executions.
|
|
4
|
+
|
|
5
|
+
This module never:
|
|
6
|
+
- schedules execution
|
|
7
|
+
- mutates runtime state
|
|
8
|
+
- mutates KnowledgeBase
|
|
9
|
+
- changes replay semantics
|
|
10
|
+
- changes fingerprints
|
|
11
|
+
- recomputes domain answers
|
|
12
|
+
- introduces probabilistic behavior
|
|
13
|
+
|
|
14
|
+
The explainer consumes existing artifacts only:
|
|
15
|
+
- ExecutionGraph
|
|
16
|
+
- EventLog
|
|
17
|
+
- KnowledgeBase
|
|
18
|
+
- ReplayMetadata when available
|
|
19
|
+
|
|
20
|
+
Missing evidence is treated as a normal outcome. Only invalid query input
|
|
21
|
+
or clearly corrupt inputs should raise exceptions.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from collections.abc import Mapping as AbcMapping
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from types import MappingProxyType
|
|
29
|
+
from typing import Any, Literal, Mapping, Protocol, Sequence
|
|
30
|
+
|
|
31
|
+
from core_runtime.core.canonicalization import canonical_graph_nodes
|
|
32
|
+
from core_runtime.core.audit_event import report_correlation_id, router_correlation_id
|
|
33
|
+
from core_runtime.core.audit_trail_index import AuditTrailIndex
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
ExplanationStatus = Literal["complete", "partial", "missing", "unsupported"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AuditExplainabilityAPI(Protocol):
|
|
40
|
+
def cause_of_event(self, event_id: str) -> list[str]: ...
|
|
41
|
+
|
|
42
|
+
def lineage_of_fact(self, fact_id: str) -> list[str]: ...
|
|
43
|
+
|
|
44
|
+
def trace_of_report(self, report_id: str) -> list[str]: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _freeze_value(value: Any) -> Any:
|
|
48
|
+
if isinstance(value, dict):
|
|
49
|
+
return MappingProxyType({k: _freeze_value(v) for k, v in value.items()})
|
|
50
|
+
if isinstance(value, list):
|
|
51
|
+
return tuple(_freeze_value(v) for v in value)
|
|
52
|
+
if isinstance(value, tuple):
|
|
53
|
+
return tuple(_freeze_value(v) for v in value)
|
|
54
|
+
if isinstance(value, set):
|
|
55
|
+
return frozenset(_freeze_value(v) for v in value)
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _unfreeze_value(value: Any) -> Any:
|
|
60
|
+
if isinstance(value, MappingProxyType):
|
|
61
|
+
return {k: _unfreeze_value(v) for k, v in value.items()}
|
|
62
|
+
if isinstance(value, tuple):
|
|
63
|
+
return [_unfreeze_value(v) for v in value]
|
|
64
|
+
if isinstance(value, frozenset):
|
|
65
|
+
return [_unfreeze_value(v) for v in value]
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _normalize_id(value: str) -> str:
|
|
70
|
+
if not isinstance(value, str):
|
|
71
|
+
raise TypeError(f"Expected string identifier, got {type(value).__name__}")
|
|
72
|
+
normalized = value.strip()
|
|
73
|
+
if not normalized:
|
|
74
|
+
raise ValueError("Identifier must not be empty")
|
|
75
|
+
return normalized
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _sorted_unique(values: Sequence[str]) -> tuple[str, ...]:
|
|
79
|
+
return tuple(sorted({value for value in values if isinstance(value, str) and value.strip()}))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _dedupe_sorted_stable(values: Sequence[str]) -> tuple[str, ...]:
|
|
83
|
+
return tuple(sorted({value for value in values if isinstance(value, str) and value.strip()}))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _has_any_artifact(*artifacts: Any | None) -> bool:
|
|
87
|
+
return any(artifact is not None for artifact in artifacts)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _call_optional_method(artifact: Any, method_names: Sequence[str], *args: Any) -> Any | None:
|
|
91
|
+
for method_name in method_names:
|
|
92
|
+
method = getattr(artifact, method_name, None)
|
|
93
|
+
if not callable(method):
|
|
94
|
+
continue
|
|
95
|
+
try:
|
|
96
|
+
return method(*args)
|
|
97
|
+
except TypeError:
|
|
98
|
+
continue
|
|
99
|
+
except Exception:
|
|
100
|
+
continue
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _artifact_field(artifact: Any, field_name: str, default: Any = "") -> Any:
|
|
105
|
+
if isinstance(artifact, Mapping):
|
|
106
|
+
return artifact.get(field_name, default)
|
|
107
|
+
return getattr(artifact, field_name, default)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _extract_explainability_payload(value: Any, target_id: str) -> dict[str, list[str]]:
|
|
111
|
+
result: dict[str, list[str]] = {
|
|
112
|
+
"graph_node_ids": [],
|
|
113
|
+
"event_ids": [],
|
|
114
|
+
"fact_ids": [],
|
|
115
|
+
"projection_ids": [],
|
|
116
|
+
"constraint_ids": [],
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if value is None:
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
if isinstance(value, list) or isinstance(value, tuple) or isinstance(value, frozenset):
|
|
123
|
+
for item in value:
|
|
124
|
+
extracted = _extract_explainability_payload(item, target_id)
|
|
125
|
+
for key, values in extracted.items():
|
|
126
|
+
result[key].extend(values)
|
|
127
|
+
return {key: list(_dedupe_sorted_stable(values)) for key, values in result.items()}
|
|
128
|
+
|
|
129
|
+
data = _to_mapping(value)
|
|
130
|
+
if not data and isinstance(value, str):
|
|
131
|
+
data = {"id": value}
|
|
132
|
+
|
|
133
|
+
def add(key: str, candidate: Any) -> None:
|
|
134
|
+
if candidate is None:
|
|
135
|
+
return
|
|
136
|
+
if isinstance(candidate, str):
|
|
137
|
+
result[key].append(candidate)
|
|
138
|
+
elif isinstance(candidate, list) or isinstance(candidate, tuple):
|
|
139
|
+
for item in candidate:
|
|
140
|
+
if isinstance(item, str):
|
|
141
|
+
result[key].append(item)
|
|
142
|
+
|
|
143
|
+
aliases = {
|
|
144
|
+
"graph_node_ids": ("graph_node_ids", "node_ids", "nodes", "node_id", "id"),
|
|
145
|
+
"event_ids": ("event_ids", "events", "event_id"),
|
|
146
|
+
"fact_ids": ("fact_ids", "facts", "fact_id"),
|
|
147
|
+
"projection_ids": ("projection_ids", "projections", "projection_id"),
|
|
148
|
+
"constraint_ids": ("constraint_ids", "constraints", "constraint_id"),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for output_key, input_keys in aliases.items():
|
|
152
|
+
for input_key in input_keys:
|
|
153
|
+
if input_key in data:
|
|
154
|
+
add(output_key, data[input_key])
|
|
155
|
+
|
|
156
|
+
if target_id:
|
|
157
|
+
lowered = target_id.lower()
|
|
158
|
+
if "event" in lowered:
|
|
159
|
+
result["event_ids"].append(target_id)
|
|
160
|
+
elif "fact" in lowered:
|
|
161
|
+
result["fact_ids"].append(target_id)
|
|
162
|
+
elif "projection" in lowered:
|
|
163
|
+
result["projection_ids"].append(target_id)
|
|
164
|
+
elif "constraint" in lowered:
|
|
165
|
+
result["constraint_ids"].append(target_id)
|
|
166
|
+
else:
|
|
167
|
+
result["graph_node_ids"].append(target_id)
|
|
168
|
+
|
|
169
|
+
return {key: list(_dedupe_sorted_stable(values)) for key, values in result.items()}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _native_graph_lookup(execution_graph: Any, target_id: str) -> dict[str, list[str]]:
|
|
173
|
+
"""
|
|
174
|
+
Best-effort read-only lookup using native ExecutionGraph APIs if available.
|
|
175
|
+
|
|
176
|
+
Returns keys:
|
|
177
|
+
- graph_node_ids
|
|
178
|
+
- event_ids
|
|
179
|
+
- fact_ids
|
|
180
|
+
- projection_ids
|
|
181
|
+
- constraint_ids
|
|
182
|
+
|
|
183
|
+
Never mutates the graph. Never raises for missing native APIs.
|
|
184
|
+
"""
|
|
185
|
+
result: dict[str, list[str]] = {
|
|
186
|
+
"graph_node_ids": [],
|
|
187
|
+
"event_ids": [],
|
|
188
|
+
"fact_ids": [],
|
|
189
|
+
"projection_ids": [],
|
|
190
|
+
"constraint_ids": [],
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if execution_graph is None:
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
native_payload = _call_optional_method(
|
|
197
|
+
execution_graph,
|
|
198
|
+
(
|
|
199
|
+
"explain",
|
|
200
|
+
"lookup",
|
|
201
|
+
"lookup_node",
|
|
202
|
+
"find_node",
|
|
203
|
+
"find_related",
|
|
204
|
+
"find_related_ids",
|
|
205
|
+
"get_related_ids",
|
|
206
|
+
),
|
|
207
|
+
target_id,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if native_payload is not None:
|
|
211
|
+
extracted = _extract_explainability_payload(native_payload, target_id)
|
|
212
|
+
for key, values in extracted.items():
|
|
213
|
+
if key in result:
|
|
214
|
+
result[key].extend(values)
|
|
215
|
+
|
|
216
|
+
return {key: list(_dedupe_sorted_stable(values)) for key, values in result.items()}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _native_event_lookup(event_log: Any, target_id: str) -> dict[str, list[str]]:
|
|
220
|
+
result: dict[str, list[str]] = {
|
|
221
|
+
"graph_node_ids": [],
|
|
222
|
+
"event_ids": [],
|
|
223
|
+
"fact_ids": [],
|
|
224
|
+
"projection_ids": [],
|
|
225
|
+
"constraint_ids": [],
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if event_log is None:
|
|
229
|
+
return result
|
|
230
|
+
|
|
231
|
+
native_payload = _call_optional_method(
|
|
232
|
+
event_log,
|
|
233
|
+
(
|
|
234
|
+
"lookup_event",
|
|
235
|
+
"find_event",
|
|
236
|
+
"lookup",
|
|
237
|
+
"find_related",
|
|
238
|
+
"find_related_ids",
|
|
239
|
+
"get_related_ids",
|
|
240
|
+
"by_task",
|
|
241
|
+
"by_type",
|
|
242
|
+
),
|
|
243
|
+
target_id,
|
|
244
|
+
)
|
|
245
|
+
if native_payload is None:
|
|
246
|
+
native_payload = _call_optional_method(event_log, ("all",))
|
|
247
|
+
|
|
248
|
+
if native_payload is not None:
|
|
249
|
+
extracted = _extract_explainability_payload(native_payload, target_id)
|
|
250
|
+
for key, values in extracted.items():
|
|
251
|
+
if key in result:
|
|
252
|
+
result[key].extend(values)
|
|
253
|
+
|
|
254
|
+
return {key: list(_dedupe_sorted_stable(values)) for key, values in result.items()}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _native_knowledge_base_lookup(knowledge_base: Any, target_id: str) -> dict[str, list[str]]:
|
|
258
|
+
result: dict[str, list[str]] = {
|
|
259
|
+
"graph_node_ids": [],
|
|
260
|
+
"event_ids": [],
|
|
261
|
+
"fact_ids": [],
|
|
262
|
+
"projection_ids": [],
|
|
263
|
+
"constraint_ids": [],
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if knowledge_base is None:
|
|
267
|
+
return result
|
|
268
|
+
|
|
269
|
+
native_payload = _call_optional_method(
|
|
270
|
+
knowledge_base,
|
|
271
|
+
(
|
|
272
|
+
"query_fact",
|
|
273
|
+
"query_by_hash",
|
|
274
|
+
"lookup_ids",
|
|
275
|
+
"lookup_fact",
|
|
276
|
+
"find_fact",
|
|
277
|
+
"lookup",
|
|
278
|
+
),
|
|
279
|
+
target_id,
|
|
280
|
+
)
|
|
281
|
+
if native_payload is None:
|
|
282
|
+
native_payload = _call_optional_method(knowledge_base, ("all_facts",))
|
|
283
|
+
|
|
284
|
+
if native_payload is not None:
|
|
285
|
+
extracted = _extract_explainability_payload(native_payload, target_id)
|
|
286
|
+
for key, values in extracted.items():
|
|
287
|
+
if key in result:
|
|
288
|
+
result[key].extend(values)
|
|
289
|
+
|
|
290
|
+
return {key: list(_dedupe_sorted_stable(values)) for key, values in result.items()}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _native_audit_lookup(audit_trail: Any, target_id: str) -> dict[str, list[str]]:
|
|
294
|
+
result: dict[str, list[str]] = {
|
|
295
|
+
"graph_node_ids": [],
|
|
296
|
+
"event_ids": [],
|
|
297
|
+
"fact_ids": [],
|
|
298
|
+
"projection_ids": [],
|
|
299
|
+
"constraint_ids": [],
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if audit_trail is None:
|
|
303
|
+
return result
|
|
304
|
+
|
|
305
|
+
if isinstance(audit_trail, AuditTrailIndex):
|
|
306
|
+
entries = list(audit_trail.events)
|
|
307
|
+
else:
|
|
308
|
+
entries = []
|
|
309
|
+
if hasattr(audit_trail, "events"):
|
|
310
|
+
try:
|
|
311
|
+
entries = list(getattr(audit_trail, "events"))
|
|
312
|
+
except Exception:
|
|
313
|
+
entries = []
|
|
314
|
+
elif hasattr(audit_trail, "all") and callable(getattr(audit_trail, "all")):
|
|
315
|
+
try:
|
|
316
|
+
entries = list(audit_trail.all())
|
|
317
|
+
except Exception:
|
|
318
|
+
entries = []
|
|
319
|
+
elif isinstance(audit_trail, Mapping):
|
|
320
|
+
raw_events = audit_trail.get("events", [])
|
|
321
|
+
try:
|
|
322
|
+
entries = list(raw_events)
|
|
323
|
+
except Exception:
|
|
324
|
+
entries = []
|
|
325
|
+
|
|
326
|
+
if not entries:
|
|
327
|
+
return result
|
|
328
|
+
|
|
329
|
+
correlation_match = target_id.startswith("report::")
|
|
330
|
+
correlation_target = target_id if correlation_match else ""
|
|
331
|
+
|
|
332
|
+
for entry in entries:
|
|
333
|
+
entry_id = _artifact_field(entry, "event_id", "")
|
|
334
|
+
entry_type = _artifact_field(entry, "event_type", "")
|
|
335
|
+
entry_correlation = _artifact_field(entry, "correlation_id", "")
|
|
336
|
+
entry_payload = _artifact_field(entry, "payload", {})
|
|
337
|
+
if target_id in {entry_id, entry_type, entry_correlation} or _contains_target(entry_payload, target_id):
|
|
338
|
+
if entry_id:
|
|
339
|
+
result["event_ids"].append(str(entry_id))
|
|
340
|
+
if entry_correlation:
|
|
341
|
+
result["projection_ids"].append(str(entry_correlation))
|
|
342
|
+
if correlation_match and entry_correlation == correlation_target:
|
|
343
|
+
if entry_id:
|
|
344
|
+
result["event_ids"].append(str(entry_id))
|
|
345
|
+
if entry_correlation:
|
|
346
|
+
result["projection_ids"].append(str(entry_correlation))
|
|
347
|
+
for key_name in ("fact_id", "projection_id", "constraint_id"):
|
|
348
|
+
value = _artifact_field(entry_payload, key_name, "")
|
|
349
|
+
if isinstance(value, str) and value == target_id:
|
|
350
|
+
if key_name == "fact_id":
|
|
351
|
+
result["fact_ids"].append(value)
|
|
352
|
+
elif key_name == "projection_id":
|
|
353
|
+
result["projection_ids"].append(value)
|
|
354
|
+
elif key_name == "constraint_id":
|
|
355
|
+
result["constraint_ids"].append(value)
|
|
356
|
+
|
|
357
|
+
return {key: list(_dedupe_sorted_stable(values)) for key, values in result.items()}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _string_values(value: Any) -> list[str]:
|
|
361
|
+
"""Extract string leaves from shallow/semi-structured values."""
|
|
362
|
+
strings: list[str] = []
|
|
363
|
+
|
|
364
|
+
if value is None:
|
|
365
|
+
return strings
|
|
366
|
+
|
|
367
|
+
if isinstance(value, str):
|
|
368
|
+
if value.strip():
|
|
369
|
+
strings.append(value)
|
|
370
|
+
return strings
|
|
371
|
+
|
|
372
|
+
if isinstance(value, Mapping):
|
|
373
|
+
for key, item in value.items():
|
|
374
|
+
strings.extend(_string_values(key))
|
|
375
|
+
strings.extend(_string_values(item))
|
|
376
|
+
return strings
|
|
377
|
+
|
|
378
|
+
if isinstance(value, Sequence) and not isinstance(value, (bytes, bytearray, str)):
|
|
379
|
+
for item in value:
|
|
380
|
+
strings.extend(_string_values(item))
|
|
381
|
+
return strings
|
|
382
|
+
|
|
383
|
+
to_dict = getattr(value, "to_dict", None)
|
|
384
|
+
if callable(to_dict):
|
|
385
|
+
try:
|
|
386
|
+
strings.extend(_string_values(to_dict()))
|
|
387
|
+
except Exception:
|
|
388
|
+
pass
|
|
389
|
+
return strings
|
|
390
|
+
|
|
391
|
+
if hasattr(value, "__dict__"):
|
|
392
|
+
try:
|
|
393
|
+
strings.extend(_string_values(vars(value)))
|
|
394
|
+
except Exception:
|
|
395
|
+
pass
|
|
396
|
+
return strings
|
|
397
|
+
|
|
398
|
+
for attr in ("id", "event_id", "fact_id", "projection_id", "node_id", "constraint_id"):
|
|
399
|
+
if hasattr(value, attr):
|
|
400
|
+
try:
|
|
401
|
+
attr_value = getattr(value, attr)
|
|
402
|
+
except Exception:
|
|
403
|
+
continue
|
|
404
|
+
strings.extend(_string_values(attr_value))
|
|
405
|
+
|
|
406
|
+
return strings
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _contains_target(value: Any, target_id: str) -> bool:
|
|
410
|
+
return target_id in _string_values(value)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _contains_any_target(value: Any, target_ids: Sequence[str]) -> bool:
|
|
414
|
+
target_set = {target_id for target_id in target_ids if isinstance(target_id, str) and target_id.strip()}
|
|
415
|
+
if not target_set:
|
|
416
|
+
return False
|
|
417
|
+
values = set(_string_values(value))
|
|
418
|
+
return bool(values.intersection(target_set))
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _summary(
|
|
422
|
+
entity: str,
|
|
423
|
+
target_id: str,
|
|
424
|
+
status: ExplanationStatus,
|
|
425
|
+
*,
|
|
426
|
+
graph_node_ids: Sequence[str] = (),
|
|
427
|
+
event_ids: Sequence[str] = (),
|
|
428
|
+
fact_ids: Sequence[str] = (),
|
|
429
|
+
projection_ids: Sequence[str] = (),
|
|
430
|
+
constraint_ids: Sequence[str] = (),
|
|
431
|
+
) -> str:
|
|
432
|
+
if status == "missing":
|
|
433
|
+
return f"No static explainability evidence found for {entity} '{target_id}'."
|
|
434
|
+
if status == "unsupported":
|
|
435
|
+
return f"Static explainability is unsupported for {entity} '{target_id}' with the provided artifacts."
|
|
436
|
+
|
|
437
|
+
parts = [f"Static explanation for {entity} '{target_id}' is {status}."]
|
|
438
|
+
if graph_node_ids:
|
|
439
|
+
parts.append(f"graph_nodes={len(graph_node_ids)}")
|
|
440
|
+
if event_ids:
|
|
441
|
+
parts.append(f"events={len(event_ids)}")
|
|
442
|
+
if fact_ids:
|
|
443
|
+
parts.append(f"facts={len(fact_ids)}")
|
|
444
|
+
if projection_ids:
|
|
445
|
+
parts.append(f"projections={len(projection_ids)}")
|
|
446
|
+
if constraint_ids:
|
|
447
|
+
parts.append(f"constraints={len(constraint_ids)}")
|
|
448
|
+
return " ".join(parts)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _router_summary(
|
|
452
|
+
entity: str,
|
|
453
|
+
target_id: str,
|
|
454
|
+
status: ExplanationStatus,
|
|
455
|
+
*,
|
|
456
|
+
profile_id: str | None = None,
|
|
457
|
+
expert_id: str | None = None,
|
|
458
|
+
reason: str | None = None,
|
|
459
|
+
selected_count: int | None = None,
|
|
460
|
+
rejected_count: int | None = None,
|
|
461
|
+
total_proposals: int | None = None,
|
|
462
|
+
validation_found: bool = False,
|
|
463
|
+
report_found: bool = False,
|
|
464
|
+
replay_status: str | None = None,
|
|
465
|
+
) -> str:
|
|
466
|
+
if status == "missing":
|
|
467
|
+
return f"No static router explainability evidence found for {entity} '{target_id}'."
|
|
468
|
+
if status == "unsupported":
|
|
469
|
+
return f"Static router explainability is unsupported for {entity} '{target_id}' with the provided artifacts."
|
|
470
|
+
|
|
471
|
+
parts = [f"Static router explanation for {entity} '{target_id}' is {status}."]
|
|
472
|
+
if profile_id:
|
|
473
|
+
parts.append(f"profile={profile_id}")
|
|
474
|
+
if expert_id:
|
|
475
|
+
parts.append(f"expert={expert_id}")
|
|
476
|
+
if reason:
|
|
477
|
+
parts.append(f"reason={reason}")
|
|
478
|
+
if selected_count is not None:
|
|
479
|
+
parts.append(f"selected={selected_count}")
|
|
480
|
+
if rejected_count is not None:
|
|
481
|
+
parts.append(f"rejected={rejected_count}")
|
|
482
|
+
if total_proposals is not None:
|
|
483
|
+
parts.append(f"proposals={total_proposals}")
|
|
484
|
+
if validation_found:
|
|
485
|
+
parts.append("validation=present")
|
|
486
|
+
if report_found:
|
|
487
|
+
parts.append("report=present")
|
|
488
|
+
if replay_status:
|
|
489
|
+
parts.append(f"replay={replay_status}")
|
|
490
|
+
return " ".join(parts)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _int_payload_field(value: Any) -> int | None:
|
|
494
|
+
if isinstance(value, bool):
|
|
495
|
+
return int(value)
|
|
496
|
+
if isinstance(value, int):
|
|
497
|
+
return value
|
|
498
|
+
if isinstance(value, str):
|
|
499
|
+
try:
|
|
500
|
+
return int(value.strip())
|
|
501
|
+
except ValueError:
|
|
502
|
+
return None
|
|
503
|
+
if isinstance(value, float):
|
|
504
|
+
return int(value)
|
|
505
|
+
return None
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _audit_event_sort_key(event: Any) -> tuple[int, str, str, str]:
|
|
509
|
+
logical_tick = _artifact_field(event, "logical_tick", 0)
|
|
510
|
+
try:
|
|
511
|
+
tick = int(logical_tick)
|
|
512
|
+
except (TypeError, ValueError):
|
|
513
|
+
tick = 0
|
|
514
|
+
return (
|
|
515
|
+
tick,
|
|
516
|
+
str(_artifact_field(event, "correlation_id", "")),
|
|
517
|
+
str(_artifact_field(event, "event_type", "")),
|
|
518
|
+
str(_artifact_field(event, "event_id", "")),
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _router_payload_records(payload: Any, key: str) -> list[dict[str, Any]]:
|
|
523
|
+
if not isinstance(payload, Mapping):
|
|
524
|
+
return []
|
|
525
|
+
records = payload.get(key, [])
|
|
526
|
+
if not isinstance(records, Sequence) or isinstance(records, (bytes, bytearray, str)):
|
|
527
|
+
return []
|
|
528
|
+
results: list[dict[str, Any]] = []
|
|
529
|
+
for record in records:
|
|
530
|
+
if isinstance(record, Mapping):
|
|
531
|
+
results.append(dict(record))
|
|
532
|
+
return results
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _is_supported_artifact(artifact: Any) -> bool:
|
|
536
|
+
if artifact is None:
|
|
537
|
+
return False
|
|
538
|
+
if isinstance(artifact, Mapping):
|
|
539
|
+
return True
|
|
540
|
+
if isinstance(artifact, Sequence) and not isinstance(artifact, (bytes, bytearray, str)):
|
|
541
|
+
return True
|
|
542
|
+
|
|
543
|
+
supported_attrs = (
|
|
544
|
+
"to_dict",
|
|
545
|
+
"node_by_id",
|
|
546
|
+
"topological_order",
|
|
547
|
+
"edges_to",
|
|
548
|
+
"edges_from",
|
|
549
|
+
"all",
|
|
550
|
+
"by_type",
|
|
551
|
+
"query_fact",
|
|
552
|
+
"query_by_hash",
|
|
553
|
+
"all_facts",
|
|
554
|
+
"fingerprint",
|
|
555
|
+
"__dict__",
|
|
556
|
+
)
|
|
557
|
+
return any(hasattr(artifact, attr) for attr in supported_attrs)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _artifact_documents(artifact: Any) -> list[Any]:
|
|
561
|
+
docs: list[Any] = []
|
|
562
|
+
if artifact is None:
|
|
563
|
+
return docs
|
|
564
|
+
|
|
565
|
+
if isinstance(artifact, Mapping):
|
|
566
|
+
docs.append(artifact)
|
|
567
|
+
|
|
568
|
+
if hasattr(artifact, "__dict__"):
|
|
569
|
+
try:
|
|
570
|
+
docs.append(dict(vars(artifact)))
|
|
571
|
+
except Exception:
|
|
572
|
+
pass
|
|
573
|
+
|
|
574
|
+
if hasattr(artifact, "nodes"):
|
|
575
|
+
try:
|
|
576
|
+
docs.append({"nodes": getattr(artifact, "nodes")})
|
|
577
|
+
except Exception:
|
|
578
|
+
pass
|
|
579
|
+
if hasattr(artifact, "edges"):
|
|
580
|
+
try:
|
|
581
|
+
docs.append({"edges": getattr(artifact, "edges")})
|
|
582
|
+
except Exception:
|
|
583
|
+
pass
|
|
584
|
+
if hasattr(artifact, "events"):
|
|
585
|
+
try:
|
|
586
|
+
docs.append({"events": getattr(artifact, "events")})
|
|
587
|
+
except Exception:
|
|
588
|
+
pass
|
|
589
|
+
if hasattr(artifact, "facts"):
|
|
590
|
+
try:
|
|
591
|
+
docs.append({"facts": getattr(artifact, "facts")})
|
|
592
|
+
except Exception:
|
|
593
|
+
pass
|
|
594
|
+
|
|
595
|
+
to_dict = getattr(artifact, "to_dict", None)
|
|
596
|
+
if callable(to_dict):
|
|
597
|
+
try:
|
|
598
|
+
docs.append(to_dict())
|
|
599
|
+
except Exception:
|
|
600
|
+
pass
|
|
601
|
+
|
|
602
|
+
return docs
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
@dataclass(frozen=True)
|
|
606
|
+
class ExplainabilityWarning:
|
|
607
|
+
"""Serializable warning emitted by static explainability."""
|
|
608
|
+
|
|
609
|
+
code: str
|
|
610
|
+
message: str
|
|
611
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
612
|
+
|
|
613
|
+
def to_dict(self) -> dict[str, Any]:
|
|
614
|
+
return {
|
|
615
|
+
"code": self.code,
|
|
616
|
+
"message": self.message,
|
|
617
|
+
"metadata": dict(self.metadata),
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _to_mapping(value: Any) -> dict[str, Any]:
|
|
622
|
+
if value is None:
|
|
623
|
+
return {}
|
|
624
|
+
if isinstance(value, dict):
|
|
625
|
+
return dict(value)
|
|
626
|
+
if isinstance(value, AbcMapping):
|
|
627
|
+
return dict(value)
|
|
628
|
+
if hasattr(value, "to_dict") and callable(getattr(value, "to_dict")):
|
|
629
|
+
try:
|
|
630
|
+
data = value.to_dict()
|
|
631
|
+
except Exception:
|
|
632
|
+
return {}
|
|
633
|
+
if isinstance(data, dict):
|
|
634
|
+
return data
|
|
635
|
+
if hasattr(value, "__dict__"):
|
|
636
|
+
try:
|
|
637
|
+
return dict(vars(value))
|
|
638
|
+
except Exception:
|
|
639
|
+
return {}
|
|
640
|
+
return {}
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _collect_fingerprint_candidates(data: Any, *, prefix: str = "") -> dict[str, str]:
|
|
644
|
+
"""Collect explicit fingerprint/hash/digest fields from nested mappings."""
|
|
645
|
+
candidates: dict[str, str] = {}
|
|
646
|
+
|
|
647
|
+
if isinstance(data, dict):
|
|
648
|
+
for key, value in data.items():
|
|
649
|
+
key_str = str(key)
|
|
650
|
+
path = f"{prefix}.{key_str}" if prefix else key_str
|
|
651
|
+
lowered = key_str.lower()
|
|
652
|
+
|
|
653
|
+
if isinstance(value, str) and (
|
|
654
|
+
"fingerprint" in lowered
|
|
655
|
+
or "hash" in lowered
|
|
656
|
+
or "digest" in lowered
|
|
657
|
+
):
|
|
658
|
+
candidates[path] = value
|
|
659
|
+
elif isinstance(value, dict):
|
|
660
|
+
candidates.update(_collect_fingerprint_candidates(value, prefix=path))
|
|
661
|
+
elif isinstance(value, list):
|
|
662
|
+
for index, item in enumerate(value):
|
|
663
|
+
if isinstance(item, dict):
|
|
664
|
+
candidates.update(
|
|
665
|
+
_collect_fingerprint_candidates(item, prefix=f"{path}[{index}]")
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
return candidates
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _fingerprint_leaf_key(path: str) -> str:
|
|
672
|
+
leaf = path.rsplit(".", 1)[-1]
|
|
673
|
+
return leaf.split("[", 1)[0]
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _is_comparable_fingerprint_value(value: str) -> bool:
|
|
677
|
+
return isinstance(value, str) and len(value.strip()) >= 16
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _manifest_fingerprint_warnings(
|
|
681
|
+
*,
|
|
682
|
+
execution_graph: Any | None,
|
|
683
|
+
manifest: Any | None,
|
|
684
|
+
) -> list[dict[str, Any]]:
|
|
685
|
+
"""Best-effort manifest consistency check for frozen artifacts."""
|
|
686
|
+
if execution_graph is None or manifest is None:
|
|
687
|
+
return []
|
|
688
|
+
|
|
689
|
+
manifest_data = _to_mapping(manifest)
|
|
690
|
+
graph_data = _to_mapping(execution_graph)
|
|
691
|
+
if not manifest_data or not graph_data:
|
|
692
|
+
return []
|
|
693
|
+
|
|
694
|
+
expected_candidates = _collect_fingerprint_candidates(manifest_data)
|
|
695
|
+
actual_candidates = _collect_fingerprint_candidates(graph_data)
|
|
696
|
+
if not expected_candidates or not actual_candidates:
|
|
697
|
+
return []
|
|
698
|
+
|
|
699
|
+
expected_by_leaf: dict[str, tuple[str, str]] = {
|
|
700
|
+
_fingerprint_leaf_key(path): (path, value)
|
|
701
|
+
for path, value in expected_candidates.items()
|
|
702
|
+
}
|
|
703
|
+
actual_by_leaf: dict[str, tuple[str, str]] = {
|
|
704
|
+
_fingerprint_leaf_key(path): (path, value)
|
|
705
|
+
for path, value in actual_candidates.items()
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
warnings: list[dict[str, Any]] = []
|
|
709
|
+
for leaf_key in sorted(set(expected_by_leaf).intersection(actual_by_leaf)):
|
|
710
|
+
expected_path, expected = expected_by_leaf[leaf_key]
|
|
711
|
+
actual_path, actual = actual_by_leaf[leaf_key]
|
|
712
|
+
if (
|
|
713
|
+
expected
|
|
714
|
+
and actual
|
|
715
|
+
and _is_comparable_fingerprint_value(expected)
|
|
716
|
+
and _is_comparable_fingerprint_value(actual)
|
|
717
|
+
and expected != actual
|
|
718
|
+
):
|
|
719
|
+
warnings.append(
|
|
720
|
+
{
|
|
721
|
+
"code": "fingerprint_mismatch",
|
|
722
|
+
"message": (
|
|
723
|
+
f"Fingerprint mismatch for '{leaf_key}'. Static explanation may "
|
|
724
|
+
"be inconsistent with the certified manifest."
|
|
725
|
+
),
|
|
726
|
+
"fingerprint_key": leaf_key,
|
|
727
|
+
"manifest_path": expected_path,
|
|
728
|
+
"graph_path": actual_path,
|
|
729
|
+
"expected": expected,
|
|
730
|
+
"actual": actual,
|
|
731
|
+
}
|
|
732
|
+
)
|
|
733
|
+
return warnings
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def _status_from_evidence_with_artifacts(
|
|
737
|
+
*groups: Sequence[str],
|
|
738
|
+
artifacts_present: bool = False,
|
|
739
|
+
) -> ExplanationStatus:
|
|
740
|
+
non_empty = sum(1 for group in groups if group)
|
|
741
|
+
if non_empty >= 2:
|
|
742
|
+
return "complete"
|
|
743
|
+
if non_empty == 1:
|
|
744
|
+
return "partial"
|
|
745
|
+
if artifacts_present:
|
|
746
|
+
return "unsupported"
|
|
747
|
+
return "missing"
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
@dataclass(frozen=True)
|
|
751
|
+
class ExplanationResult:
|
|
752
|
+
"""Serializable, read-only explanation payload."""
|
|
753
|
+
|
|
754
|
+
query: str
|
|
755
|
+
target_id: str
|
|
756
|
+
status: ExplanationStatus
|
|
757
|
+
summary: str
|
|
758
|
+
graph_node_ids: tuple[str, ...] = field(default_factory=tuple)
|
|
759
|
+
event_ids: tuple[str, ...] = field(default_factory=tuple)
|
|
760
|
+
fact_ids: tuple[str, ...] = field(default_factory=tuple)
|
|
761
|
+
projection_ids: tuple[str, ...] = field(default_factory=tuple)
|
|
762
|
+
constraint_ids: tuple[str, ...] = field(default_factory=tuple)
|
|
763
|
+
warnings: tuple[ExplainabilityWarning, ...] = field(default_factory=tuple)
|
|
764
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
765
|
+
|
|
766
|
+
def __post_init__(self) -> None:
|
|
767
|
+
object.__setattr__(self, "warnings", tuple(self.warnings))
|
|
768
|
+
object.__setattr__(self, "metadata", _freeze_value(dict(self.metadata)))
|
|
769
|
+
|
|
770
|
+
def to_dict(self) -> dict[str, Any]:
|
|
771
|
+
return {
|
|
772
|
+
"query": self.query,
|
|
773
|
+
"target_id": self.target_id,
|
|
774
|
+
"status": self.status,
|
|
775
|
+
"summary": self.summary,
|
|
776
|
+
"graph_node_ids": list(self.graph_node_ids),
|
|
777
|
+
"event_ids": list(self.event_ids),
|
|
778
|
+
"fact_ids": list(self.fact_ids),
|
|
779
|
+
"projection_ids": list(self.projection_ids),
|
|
780
|
+
"constraint_ids": list(self.constraint_ids),
|
|
781
|
+
"warnings": [warning.to_dict() for warning in self.warnings],
|
|
782
|
+
"metadata": _unfreeze_value(self.metadata),
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
class StaticExplainer:
|
|
787
|
+
"""Read-only static explainer for completed CORE executions."""
|
|
788
|
+
|
|
789
|
+
def __init__(
|
|
790
|
+
self,
|
|
791
|
+
*,
|
|
792
|
+
execution_graph: Any | None = None,
|
|
793
|
+
event_log: Any | None = None,
|
|
794
|
+
knowledge_base: Any | None = None,
|
|
795
|
+
replay_metadata: Any | None = None,
|
|
796
|
+
audit_trail: Any | None = None,
|
|
797
|
+
manifest: Any | None = None,
|
|
798
|
+
) -> None:
|
|
799
|
+
self._execution_graph = execution_graph
|
|
800
|
+
self._event_log_artifact = event_log
|
|
801
|
+
self._knowledge_base_artifact = knowledge_base
|
|
802
|
+
self._replay_metadata = replay_metadata
|
|
803
|
+
self._audit_trail_artifact = audit_trail
|
|
804
|
+
self._manifest = manifest
|
|
805
|
+
self._artifact_warnings = tuple(
|
|
806
|
+
ExplainabilityWarning(
|
|
807
|
+
code=item["code"],
|
|
808
|
+
message=item["message"],
|
|
809
|
+
metadata={
|
|
810
|
+
key: value
|
|
811
|
+
for key, value in item.items()
|
|
812
|
+
if key not in {"code", "message"}
|
|
813
|
+
},
|
|
814
|
+
)
|
|
815
|
+
for item in _manifest_fingerprint_warnings(
|
|
816
|
+
execution_graph=execution_graph,
|
|
817
|
+
manifest=manifest,
|
|
818
|
+
)
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
def cause_of_projection(self, projection_id: str) -> ExplanationResult:
|
|
822
|
+
target_id = _normalize_id(projection_id)
|
|
823
|
+
native_graph = _native_graph_lookup(self._graph(), target_id)
|
|
824
|
+
native_event = _native_event_lookup(self._event_log(), target_id)
|
|
825
|
+
native_kb = _native_knowledge_base_lookup(self._knowledge_base(), target_id)
|
|
826
|
+
native_audit = _native_audit_lookup(self._audit_trail(), target_id)
|
|
827
|
+
|
|
828
|
+
projection_ids = _dedupe_sorted_stable(
|
|
829
|
+
list(native_graph["projection_ids"])
|
|
830
|
+
+ list(native_event["projection_ids"])
|
|
831
|
+
+ list(native_kb["projection_ids"])
|
|
832
|
+
+ list(native_audit["projection_ids"])
|
|
833
|
+
+ list(self._projection_ids(target_id))
|
|
834
|
+
)
|
|
835
|
+
graph_node_ids = _dedupe_sorted_stable(
|
|
836
|
+
list(native_graph["graph_node_ids"])
|
|
837
|
+
+ list(native_event["graph_node_ids"])
|
|
838
|
+
+ list(native_kb["graph_node_ids"])
|
|
839
|
+
+ list(native_audit["graph_node_ids"])
|
|
840
|
+
+ list(self._projection_graph_nodes(target_id))
|
|
841
|
+
)
|
|
842
|
+
graph_node_ids = _dedupe_sorted_stable(list(graph_node_ids) + list(self._graph_lineage(graph_node_ids)))
|
|
843
|
+
event_ids = _dedupe_sorted_stable(
|
|
844
|
+
list(native_graph["event_ids"])
|
|
845
|
+
+ list(native_event["event_ids"])
|
|
846
|
+
+ list(native_kb["event_ids"])
|
|
847
|
+
+ list(native_audit["event_ids"])
|
|
848
|
+
+ list(self._projection_event_ids(target_id, projection_ids))
|
|
849
|
+
)
|
|
850
|
+
fact_ids = _dedupe_sorted_stable(
|
|
851
|
+
list(native_graph["fact_ids"])
|
|
852
|
+
+ list(native_event["fact_ids"])
|
|
853
|
+
+ list(native_kb["fact_ids"])
|
|
854
|
+
+ list(native_audit["fact_ids"])
|
|
855
|
+
+ list(self._projection_fact_ids(target_id, projection_ids))
|
|
856
|
+
)
|
|
857
|
+
constraint_ids = _dedupe_sorted_stable(
|
|
858
|
+
list(native_graph["constraint_ids"])
|
|
859
|
+
+ list(native_audit["constraint_ids"])
|
|
860
|
+
+ list(self._constraint_ids(graph_node_ids))
|
|
861
|
+
)
|
|
862
|
+
status = self._status(graph_node_ids, event_ids, fact_ids, projection_ids, constraint_ids)
|
|
863
|
+
return ExplanationResult(
|
|
864
|
+
query="cause_of_projection",
|
|
865
|
+
target_id=target_id,
|
|
866
|
+
status=status,
|
|
867
|
+
summary=_summary(
|
|
868
|
+
"projection",
|
|
869
|
+
target_id,
|
|
870
|
+
status,
|
|
871
|
+
graph_node_ids=graph_node_ids,
|
|
872
|
+
event_ids=event_ids,
|
|
873
|
+
fact_ids=fact_ids,
|
|
874
|
+
projection_ids=projection_ids,
|
|
875
|
+
constraint_ids=constraint_ids,
|
|
876
|
+
),
|
|
877
|
+
graph_node_ids=graph_node_ids,
|
|
878
|
+
event_ids=event_ids,
|
|
879
|
+
fact_ids=fact_ids,
|
|
880
|
+
projection_ids=projection_ids,
|
|
881
|
+
constraint_ids=constraint_ids,
|
|
882
|
+
warnings=self._artifact_warnings,
|
|
883
|
+
metadata=self._metadata(query="cause_of_projection"),
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
def lineage_of_fact(self, fact_id: str) -> ExplanationResult:
|
|
887
|
+
target_id = _normalize_id(fact_id)
|
|
888
|
+
native_graph = _native_graph_lookup(self._graph(), target_id)
|
|
889
|
+
native_event = _native_event_lookup(self._event_log(), target_id)
|
|
890
|
+
native_kb = _native_knowledge_base_lookup(self._knowledge_base(), target_id)
|
|
891
|
+
native_audit = _native_audit_lookup(self._audit_trail(), target_id)
|
|
892
|
+
|
|
893
|
+
fact_ids = _dedupe_sorted_stable(
|
|
894
|
+
list(native_graph["fact_ids"])
|
|
895
|
+
+ list(native_event["fact_ids"])
|
|
896
|
+
+ list(native_kb["fact_ids"])
|
|
897
|
+
+ list(native_audit["fact_ids"])
|
|
898
|
+
+ list(self._fact_ids(target_id))
|
|
899
|
+
)
|
|
900
|
+
graph_node_ids = _dedupe_sorted_stable(
|
|
901
|
+
list(native_graph["graph_node_ids"])
|
|
902
|
+
+ list(native_event["graph_node_ids"])
|
|
903
|
+
+ list(native_kb["graph_node_ids"])
|
|
904
|
+
+ list(native_audit["graph_node_ids"])
|
|
905
|
+
+ list(self._fact_graph_nodes(target_id, fact_ids))
|
|
906
|
+
)
|
|
907
|
+
graph_node_ids = _dedupe_sorted_stable(list(graph_node_ids) + list(self._graph_lineage(graph_node_ids)))
|
|
908
|
+
event_ids = _dedupe_sorted_stable(
|
|
909
|
+
list(native_graph["event_ids"])
|
|
910
|
+
+ list(native_event["event_ids"])
|
|
911
|
+
+ list(native_kb["event_ids"])
|
|
912
|
+
+ list(native_audit["event_ids"])
|
|
913
|
+
+ list(self._fact_event_ids(target_id, fact_ids))
|
|
914
|
+
)
|
|
915
|
+
projection_ids = _dedupe_sorted_stable(
|
|
916
|
+
list(native_graph["projection_ids"])
|
|
917
|
+
+ list(native_event["projection_ids"])
|
|
918
|
+
+ list(native_kb["projection_ids"])
|
|
919
|
+
+ list(native_audit["projection_ids"])
|
|
920
|
+
+ list(self._fact_projection_ids(target_id, fact_ids))
|
|
921
|
+
)
|
|
922
|
+
parent_fact_ids = _dedupe_sorted_stable(
|
|
923
|
+
list(native_graph["fact_ids"])
|
|
924
|
+
+ list(native_event["fact_ids"])
|
|
925
|
+
+ list(native_kb["fact_ids"])
|
|
926
|
+
+ list(native_audit["fact_ids"])
|
|
927
|
+
+ list(self._parent_fact_ids(target_id, fact_ids))
|
|
928
|
+
)
|
|
929
|
+
status = self._status(graph_node_ids, event_ids, fact_ids or parent_fact_ids, projection_ids)
|
|
930
|
+
return ExplanationResult(
|
|
931
|
+
query="lineage_of_fact",
|
|
932
|
+
target_id=target_id,
|
|
933
|
+
status=status,
|
|
934
|
+
summary=_summary(
|
|
935
|
+
"fact",
|
|
936
|
+
target_id,
|
|
937
|
+
status,
|
|
938
|
+
graph_node_ids=graph_node_ids,
|
|
939
|
+
event_ids=event_ids,
|
|
940
|
+
fact_ids=parent_fact_ids or fact_ids,
|
|
941
|
+
projection_ids=projection_ids,
|
|
942
|
+
),
|
|
943
|
+
graph_node_ids=graph_node_ids,
|
|
944
|
+
event_ids=event_ids,
|
|
945
|
+
fact_ids=parent_fact_ids or fact_ids,
|
|
946
|
+
projection_ids=projection_ids,
|
|
947
|
+
warnings=self._artifact_warnings,
|
|
948
|
+
metadata=self._metadata(query="lineage_of_fact"),
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
def trace_of_event(self, event_id: str) -> ExplanationResult:
|
|
952
|
+
target_id = _normalize_id(event_id)
|
|
953
|
+
native_graph = _native_graph_lookup(self._graph(), target_id)
|
|
954
|
+
native_event = _native_event_lookup(self._event_log(), target_id)
|
|
955
|
+
native_kb = _native_knowledge_base_lookup(self._knowledge_base(), target_id)
|
|
956
|
+
native_audit = _native_audit_lookup(self._audit_trail(), target_id)
|
|
957
|
+
|
|
958
|
+
event_ids = _dedupe_sorted_stable(
|
|
959
|
+
list(native_graph["event_ids"])
|
|
960
|
+
+ list(native_event["event_ids"])
|
|
961
|
+
+ list(native_kb["event_ids"])
|
|
962
|
+
+ list(native_audit["event_ids"])
|
|
963
|
+
+ list(self._event_ids(target_id))
|
|
964
|
+
)
|
|
965
|
+
graph_node_ids = _dedupe_sorted_stable(
|
|
966
|
+
list(native_graph["graph_node_ids"])
|
|
967
|
+
+ list(native_event["graph_node_ids"])
|
|
968
|
+
+ list(native_kb["graph_node_ids"])
|
|
969
|
+
+ list(native_audit["graph_node_ids"])
|
|
970
|
+
+ list(self._event_graph_nodes(target_id, event_ids))
|
|
971
|
+
)
|
|
972
|
+
graph_node_ids = _dedupe_sorted_stable(list(graph_node_ids) + list(self._graph_lineage(graph_node_ids)))
|
|
973
|
+
fact_ids = _dedupe_sorted_stable(
|
|
974
|
+
list(native_graph["fact_ids"])
|
|
975
|
+
+ list(native_event["fact_ids"])
|
|
976
|
+
+ list(native_kb["fact_ids"])
|
|
977
|
+
+ list(native_audit["fact_ids"])
|
|
978
|
+
+ list(self._event_fact_ids(target_id, event_ids))
|
|
979
|
+
)
|
|
980
|
+
projection_ids = _dedupe_sorted_stable(
|
|
981
|
+
list(native_graph["projection_ids"])
|
|
982
|
+
+ list(native_event["projection_ids"])
|
|
983
|
+
+ list(native_kb["projection_ids"])
|
|
984
|
+
+ list(native_audit["projection_ids"])
|
|
985
|
+
+ list(self._event_projection_ids(target_id, event_ids))
|
|
986
|
+
)
|
|
987
|
+
event_ids = _dedupe_sorted_stable(list(event_ids) + list(self._event_neighborhood(target_id, event_ids)))
|
|
988
|
+
status = self._status(graph_node_ids, event_ids, fact_ids, projection_ids)
|
|
989
|
+
return ExplanationResult(
|
|
990
|
+
query="trace_of_event",
|
|
991
|
+
target_id=target_id,
|
|
992
|
+
status=status,
|
|
993
|
+
summary=_summary(
|
|
994
|
+
"event",
|
|
995
|
+
target_id,
|
|
996
|
+
status,
|
|
997
|
+
graph_node_ids=graph_node_ids,
|
|
998
|
+
event_ids=event_ids,
|
|
999
|
+
fact_ids=fact_ids,
|
|
1000
|
+
projection_ids=projection_ids,
|
|
1001
|
+
),
|
|
1002
|
+
graph_node_ids=graph_node_ids,
|
|
1003
|
+
event_ids=event_ids,
|
|
1004
|
+
fact_ids=fact_ids,
|
|
1005
|
+
projection_ids=projection_ids,
|
|
1006
|
+
warnings=self._artifact_warnings,
|
|
1007
|
+
metadata=self._metadata(query="trace_of_event"),
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
def origin_projection(self, output_id: str) -> ExplanationResult:
|
|
1011
|
+
target_id = _normalize_id(output_id)
|
|
1012
|
+
native_graph = _native_graph_lookup(self._graph(), target_id)
|
|
1013
|
+
native_event = _native_event_lookup(self._event_log(), target_id)
|
|
1014
|
+
native_kb = _native_knowledge_base_lookup(self._knowledge_base(), target_id)
|
|
1015
|
+
native_audit = _native_audit_lookup(self._audit_trail(), target_id)
|
|
1016
|
+
|
|
1017
|
+
projection_ids = _dedupe_sorted_stable(
|
|
1018
|
+
list(native_graph["projection_ids"])
|
|
1019
|
+
+ list(native_event["projection_ids"])
|
|
1020
|
+
+ list(native_kb["projection_ids"])
|
|
1021
|
+
+ list(native_audit["projection_ids"])
|
|
1022
|
+
+ list(self._origin_projection_ids(target_id))
|
|
1023
|
+
)
|
|
1024
|
+
graph_node_ids = _dedupe_sorted_stable(
|
|
1025
|
+
list(native_graph["graph_node_ids"])
|
|
1026
|
+
+ list(native_event["graph_node_ids"])
|
|
1027
|
+
+ list(native_kb["graph_node_ids"])
|
|
1028
|
+
+ list(native_audit["graph_node_ids"])
|
|
1029
|
+
+ list(self._origin_projection_graph_nodes(target_id, projection_ids))
|
|
1030
|
+
)
|
|
1031
|
+
graph_node_ids = _dedupe_sorted_stable(list(graph_node_ids) + list(self._graph_lineage(graph_node_ids)))
|
|
1032
|
+
event_ids = _dedupe_sorted_stable(
|
|
1033
|
+
list(native_graph["event_ids"])
|
|
1034
|
+
+ list(native_event["event_ids"])
|
|
1035
|
+
+ list(native_kb["event_ids"])
|
|
1036
|
+
+ list(native_audit["event_ids"])
|
|
1037
|
+
+ list(self._origin_projection_event_ids(target_id, projection_ids))
|
|
1038
|
+
)
|
|
1039
|
+
fact_ids = _dedupe_sorted_stable(
|
|
1040
|
+
list(native_graph["fact_ids"])
|
|
1041
|
+
+ list(native_event["fact_ids"])
|
|
1042
|
+
+ list(native_kb["fact_ids"])
|
|
1043
|
+
+ list(native_audit["fact_ids"])
|
|
1044
|
+
+ list(self._origin_projection_fact_ids(target_id, projection_ids))
|
|
1045
|
+
)
|
|
1046
|
+
status = self._status(graph_node_ids, event_ids, fact_ids, projection_ids)
|
|
1047
|
+
return ExplanationResult(
|
|
1048
|
+
query="origin_projection",
|
|
1049
|
+
target_id=target_id,
|
|
1050
|
+
status=status,
|
|
1051
|
+
summary=_summary(
|
|
1052
|
+
"output",
|
|
1053
|
+
target_id,
|
|
1054
|
+
status,
|
|
1055
|
+
graph_node_ids=graph_node_ids,
|
|
1056
|
+
event_ids=event_ids,
|
|
1057
|
+
fact_ids=fact_ids,
|
|
1058
|
+
projection_ids=projection_ids,
|
|
1059
|
+
),
|
|
1060
|
+
graph_node_ids=graph_node_ids,
|
|
1061
|
+
event_ids=event_ids,
|
|
1062
|
+
fact_ids=fact_ids,
|
|
1063
|
+
projection_ids=projection_ids,
|
|
1064
|
+
warnings=self._artifact_warnings,
|
|
1065
|
+
metadata=self._metadata(query="origin_projection"),
|
|
1066
|
+
)
|
|
1067
|
+
|
|
1068
|
+
def cause_of_event(self, event_id: str) -> ExplanationResult:
|
|
1069
|
+
target_id = _normalize_id(event_id)
|
|
1070
|
+
native_audit = _native_audit_lookup(self._audit_trail(), target_id)
|
|
1071
|
+
native_event = _native_event_lookup(self._event_log(), target_id)
|
|
1072
|
+
native_graph = _native_graph_lookup(self._graph(), target_id)
|
|
1073
|
+
native_kb = _native_knowledge_base_lookup(self._knowledge_base(), target_id)
|
|
1074
|
+
|
|
1075
|
+
event_ids = _dedupe_sorted_stable(
|
|
1076
|
+
[target_id]
|
|
1077
|
+
+ list(native_audit["event_ids"])
|
|
1078
|
+
+ list(native_event["event_ids"])
|
|
1079
|
+
+ list(native_graph["event_ids"])
|
|
1080
|
+
+ list(native_kb["event_ids"])
|
|
1081
|
+
)
|
|
1082
|
+
projection_ids = _dedupe_sorted_stable(
|
|
1083
|
+
list(native_audit["projection_ids"])
|
|
1084
|
+
+ list(native_event["projection_ids"])
|
|
1085
|
+
+ list(native_graph["projection_ids"])
|
|
1086
|
+
+ list(native_kb["projection_ids"])
|
|
1087
|
+
)
|
|
1088
|
+
fact_ids = _dedupe_sorted_stable(
|
|
1089
|
+
list(native_audit["fact_ids"])
|
|
1090
|
+
+ list(native_event["fact_ids"])
|
|
1091
|
+
+ list(native_graph["fact_ids"])
|
|
1092
|
+
+ list(native_kb["fact_ids"])
|
|
1093
|
+
)
|
|
1094
|
+
graph_node_ids = _dedupe_sorted_stable(
|
|
1095
|
+
list(native_audit["graph_node_ids"])
|
|
1096
|
+
+ list(native_event["graph_node_ids"])
|
|
1097
|
+
+ list(native_graph["graph_node_ids"])
|
|
1098
|
+
+ list(native_kb["graph_node_ids"])
|
|
1099
|
+
)
|
|
1100
|
+
constraint_ids = _dedupe_sorted_stable(
|
|
1101
|
+
list(native_audit["constraint_ids"])
|
|
1102
|
+
+ list(native_event["constraint_ids"])
|
|
1103
|
+
+ list(native_graph["constraint_ids"])
|
|
1104
|
+
)
|
|
1105
|
+
status = self._status(graph_node_ids, event_ids, fact_ids, projection_ids, constraint_ids)
|
|
1106
|
+
return ExplanationResult(
|
|
1107
|
+
query="cause_of_event",
|
|
1108
|
+
target_id=target_id,
|
|
1109
|
+
status=status,
|
|
1110
|
+
summary=_summary(
|
|
1111
|
+
"event",
|
|
1112
|
+
target_id,
|
|
1113
|
+
status,
|
|
1114
|
+
graph_node_ids=graph_node_ids,
|
|
1115
|
+
event_ids=event_ids,
|
|
1116
|
+
fact_ids=fact_ids,
|
|
1117
|
+
projection_ids=projection_ids,
|
|
1118
|
+
constraint_ids=constraint_ids,
|
|
1119
|
+
),
|
|
1120
|
+
graph_node_ids=graph_node_ids,
|
|
1121
|
+
event_ids=event_ids,
|
|
1122
|
+
fact_ids=fact_ids,
|
|
1123
|
+
projection_ids=projection_ids,
|
|
1124
|
+
constraint_ids=constraint_ids,
|
|
1125
|
+
warnings=self._artifact_warnings,
|
|
1126
|
+
metadata=self._metadata(query="cause_of_event"),
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
def trace_of_report(self, report_id: str) -> ExplanationResult:
|
|
1130
|
+
target_id = _normalize_id(report_id)
|
|
1131
|
+
correlation_id = target_id if target_id.startswith("report::") else report_correlation_id(target_id)
|
|
1132
|
+
native_audit = _native_audit_lookup(self._audit_trail(), correlation_id)
|
|
1133
|
+
|
|
1134
|
+
event_ids = _dedupe_sorted_stable(list(native_audit["event_ids"]))
|
|
1135
|
+
projection_ids = _dedupe_sorted_stable(list(native_audit["projection_ids"]) + [correlation_id])
|
|
1136
|
+
fact_ids = _dedupe_sorted_stable(list(native_audit["fact_ids"]))
|
|
1137
|
+
graph_node_ids = _dedupe_sorted_stable(list(native_audit["graph_node_ids"]))
|
|
1138
|
+
constraint_ids = _dedupe_sorted_stable(list(native_audit["constraint_ids"]))
|
|
1139
|
+
status = self._status(graph_node_ids, event_ids, fact_ids, projection_ids, constraint_ids)
|
|
1140
|
+
return ExplanationResult(
|
|
1141
|
+
query="trace_of_report",
|
|
1142
|
+
target_id=target_id,
|
|
1143
|
+
status=status,
|
|
1144
|
+
summary=_summary(
|
|
1145
|
+
"report",
|
|
1146
|
+
target_id,
|
|
1147
|
+
status,
|
|
1148
|
+
graph_node_ids=graph_node_ids,
|
|
1149
|
+
event_ids=event_ids,
|
|
1150
|
+
fact_ids=fact_ids,
|
|
1151
|
+
projection_ids=projection_ids,
|
|
1152
|
+
constraint_ids=constraint_ids,
|
|
1153
|
+
),
|
|
1154
|
+
graph_node_ids=graph_node_ids,
|
|
1155
|
+
event_ids=event_ids,
|
|
1156
|
+
fact_ids=fact_ids,
|
|
1157
|
+
projection_ids=projection_ids,
|
|
1158
|
+
constraint_ids=constraint_ids,
|
|
1159
|
+
warnings=self._artifact_warnings,
|
|
1160
|
+
metadata=self._metadata(query="trace_of_report", correlation_id=correlation_id),
|
|
1161
|
+
)
|
|
1162
|
+
|
|
1163
|
+
def cause_of_router_selection(self, routing_id: str, expert_id: str) -> ExplanationResult:
|
|
1164
|
+
routing_id = _normalize_id(routing_id)
|
|
1165
|
+
expert_id = _normalize_id(expert_id)
|
|
1166
|
+
|
|
1167
|
+
validation_event = self._router_validation_event(routing_id)
|
|
1168
|
+
evaluation_event = self._router_evaluation_event(routing_id)
|
|
1169
|
+
replay_event = self._router_replay_event(routing_id)
|
|
1170
|
+
report_event = self._router_report_event(routing_id)
|
|
1171
|
+
|
|
1172
|
+
selected_record = self._router_expert_record(evaluation_event, "selected_experts", expert_id)
|
|
1173
|
+
selected_ids = self._router_record_ids(evaluation_event, "selected_expert_ids")
|
|
1174
|
+
reason = str(selected_record.get("reason", "")).strip() if selected_record else ""
|
|
1175
|
+
profile_id = str(_artifact_field(_artifact_field(evaluation_event, "payload", {}), "profile_id", "")).strip()
|
|
1176
|
+
report_id = self._router_report_id(report_event)
|
|
1177
|
+
projection_ids = _dedupe_sorted_stable([routing_id, report_id])
|
|
1178
|
+
event_ids = _dedupe_sorted_stable(
|
|
1179
|
+
list(self._event_ids_for_router_events((validation_event, evaluation_event, replay_event, report_event)))
|
|
1180
|
+
)
|
|
1181
|
+
status = self._status(event_ids, projection_ids)
|
|
1182
|
+
warnings = list(self._artifact_warnings)
|
|
1183
|
+
if evaluation_event is not None and selected_record is None:
|
|
1184
|
+
status = "partial"
|
|
1185
|
+
warnings.append(
|
|
1186
|
+
ExplainabilityWarning(
|
|
1187
|
+
code="router_selection_record_missing",
|
|
1188
|
+
message="Router evaluation evidence was found, but no explicit selected expert record matched the query.",
|
|
1189
|
+
metadata={"routing_id": routing_id, "expert_id": expert_id},
|
|
1190
|
+
)
|
|
1191
|
+
)
|
|
1192
|
+
elif evaluation_event is None and event_ids:
|
|
1193
|
+
status = "partial"
|
|
1194
|
+
|
|
1195
|
+
summary = _router_summary(
|
|
1196
|
+
"selection",
|
|
1197
|
+
routing_id,
|
|
1198
|
+
status,
|
|
1199
|
+
profile_id=profile_id or None,
|
|
1200
|
+
expert_id=expert_id,
|
|
1201
|
+
reason=reason or None,
|
|
1202
|
+
selected_count=_int_payload_field(_artifact_field(_artifact_field(evaluation_event, "payload", {}), "selected_count", None)),
|
|
1203
|
+
rejected_count=_int_payload_field(_artifact_field(_artifact_field(evaluation_event, "payload", {}), "rejected_count", None)),
|
|
1204
|
+
total_proposals=_int_payload_field(_artifact_field(_artifact_field(evaluation_event, "payload", {}), "total_proposals", None)),
|
|
1205
|
+
validation_found=validation_event is not None,
|
|
1206
|
+
report_found=report_event is not None,
|
|
1207
|
+
replay_status=str(_artifact_field(_artifact_field(replay_event, "payload", {}), "status", "")).strip() or None,
|
|
1208
|
+
)
|
|
1209
|
+
return ExplanationResult(
|
|
1210
|
+
query="cause_of_router_selection",
|
|
1211
|
+
target_id=f"{routing_id}::{expert_id}",
|
|
1212
|
+
status=status,
|
|
1213
|
+
summary=summary,
|
|
1214
|
+
projection_ids=projection_ids,
|
|
1215
|
+
event_ids=event_ids,
|
|
1216
|
+
warnings=tuple(warnings),
|
|
1217
|
+
metadata=self._metadata(
|
|
1218
|
+
query="cause_of_router_selection",
|
|
1219
|
+
routing_id=routing_id,
|
|
1220
|
+
expert_id=expert_id,
|
|
1221
|
+
profile_id=profile_id or None,
|
|
1222
|
+
selected_record=selected_record or {},
|
|
1223
|
+
selected_expert_ids=selected_ids,
|
|
1224
|
+
report_id=report_id,
|
|
1225
|
+
router_events=self._router_event_summaries((validation_event, evaluation_event, replay_event, report_event)),
|
|
1226
|
+
),
|
|
1227
|
+
)
|
|
1228
|
+
|
|
1229
|
+
def cause_of_router_rejection(self, routing_id: str, expert_id: str) -> ExplanationResult:
|
|
1230
|
+
routing_id = _normalize_id(routing_id)
|
|
1231
|
+
expert_id = _normalize_id(expert_id)
|
|
1232
|
+
|
|
1233
|
+
validation_event = self._router_validation_event(routing_id)
|
|
1234
|
+
evaluation_event = self._router_evaluation_event(routing_id)
|
|
1235
|
+
replay_event = self._router_replay_event(routing_id)
|
|
1236
|
+
report_event = self._router_report_event(routing_id)
|
|
1237
|
+
|
|
1238
|
+
rejected_record = self._router_expert_record(evaluation_event, "rejected_experts", expert_id)
|
|
1239
|
+
rejected_ids = self._router_record_ids(evaluation_event, "rejected_expert_ids")
|
|
1240
|
+
reason = str(rejected_record.get("reason", "")).strip() if rejected_record else ""
|
|
1241
|
+
profile_id = str(_artifact_field(_artifact_field(evaluation_event, "payload", {}), "profile_id", "")).strip()
|
|
1242
|
+
report_id = self._router_report_id(report_event)
|
|
1243
|
+
projection_ids = _dedupe_sorted_stable([routing_id, report_id])
|
|
1244
|
+
event_ids = _dedupe_sorted_stable(
|
|
1245
|
+
list(self._event_ids_for_router_events((validation_event, evaluation_event, replay_event, report_event)))
|
|
1246
|
+
)
|
|
1247
|
+
status = self._status(event_ids, projection_ids)
|
|
1248
|
+
warnings = list(self._artifact_warnings)
|
|
1249
|
+
if evaluation_event is not None and rejected_record is None:
|
|
1250
|
+
status = "partial"
|
|
1251
|
+
warnings.append(
|
|
1252
|
+
ExplainabilityWarning(
|
|
1253
|
+
code="router_rejection_record_missing",
|
|
1254
|
+
message="Router evaluation evidence was found, but no explicit rejected expert record matched the query.",
|
|
1255
|
+
metadata={"routing_id": routing_id, "expert_id": expert_id},
|
|
1256
|
+
)
|
|
1257
|
+
)
|
|
1258
|
+
elif evaluation_event is None and event_ids:
|
|
1259
|
+
status = "partial"
|
|
1260
|
+
|
|
1261
|
+
summary = _router_summary(
|
|
1262
|
+
"rejection",
|
|
1263
|
+
routing_id,
|
|
1264
|
+
status,
|
|
1265
|
+
profile_id=profile_id or None,
|
|
1266
|
+
expert_id=expert_id,
|
|
1267
|
+
reason=reason or None,
|
|
1268
|
+
selected_count=_int_payload_field(_artifact_field(_artifact_field(evaluation_event, "payload", {}), "selected_count", None)),
|
|
1269
|
+
rejected_count=_int_payload_field(_artifact_field(_artifact_field(evaluation_event, "payload", {}), "rejected_count", None)),
|
|
1270
|
+
total_proposals=_int_payload_field(_artifact_field(_artifact_field(evaluation_event, "payload", {}), "total_proposals", None)),
|
|
1271
|
+
validation_found=validation_event is not None,
|
|
1272
|
+
report_found=report_event is not None,
|
|
1273
|
+
replay_status=str(_artifact_field(_artifact_field(replay_event, "payload", {}), "status", "")).strip() or None,
|
|
1274
|
+
)
|
|
1275
|
+
return ExplanationResult(
|
|
1276
|
+
query="cause_of_router_rejection",
|
|
1277
|
+
target_id=f"{routing_id}::{expert_id}",
|
|
1278
|
+
status=status,
|
|
1279
|
+
summary=summary,
|
|
1280
|
+
projection_ids=projection_ids,
|
|
1281
|
+
event_ids=event_ids,
|
|
1282
|
+
warnings=tuple(warnings),
|
|
1283
|
+
metadata=self._metadata(
|
|
1284
|
+
query="cause_of_router_rejection",
|
|
1285
|
+
routing_id=routing_id,
|
|
1286
|
+
expert_id=expert_id,
|
|
1287
|
+
profile_id=profile_id or None,
|
|
1288
|
+
rejected_record=rejected_record or {},
|
|
1289
|
+
rejected_expert_ids=rejected_ids,
|
|
1290
|
+
report_id=report_id,
|
|
1291
|
+
router_events=self._router_event_summaries((validation_event, evaluation_event, replay_event, report_event)),
|
|
1292
|
+
),
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
def trace_of_router_decision(self, routing_id: str) -> ExplanationResult:
|
|
1296
|
+
routing_id = _normalize_id(routing_id)
|
|
1297
|
+
|
|
1298
|
+
validation_event = self._router_validation_event(routing_id)
|
|
1299
|
+
evaluation_event = self._router_evaluation_event(routing_id)
|
|
1300
|
+
replay_event = self._router_replay_event(routing_id)
|
|
1301
|
+
report_event = self._router_report_event(routing_id)
|
|
1302
|
+
report_id = self._router_report_id(report_event)
|
|
1303
|
+
projection_ids = _dedupe_sorted_stable([routing_id, report_id])
|
|
1304
|
+
event_ids = _dedupe_sorted_stable(
|
|
1305
|
+
list(self._event_ids_for_router_events((validation_event, evaluation_event, replay_event, report_event)))
|
|
1306
|
+
)
|
|
1307
|
+
status = self._status(event_ids, projection_ids)
|
|
1308
|
+
evaluation_payload = _artifact_field(evaluation_event, "payload", {})
|
|
1309
|
+
profile_id = str(_artifact_field(evaluation_payload, "profile_id", "")).strip()
|
|
1310
|
+
summary = _router_summary(
|
|
1311
|
+
"decision trace",
|
|
1312
|
+
routing_id,
|
|
1313
|
+
status,
|
|
1314
|
+
profile_id=profile_id or None,
|
|
1315
|
+
selected_count=_int_payload_field(_artifact_field(evaluation_payload, "selected_count", None)),
|
|
1316
|
+
rejected_count=_int_payload_field(_artifact_field(evaluation_payload, "rejected_count", None)),
|
|
1317
|
+
total_proposals=_int_payload_field(_artifact_field(evaluation_payload, "total_proposals", None)),
|
|
1318
|
+
validation_found=validation_event is not None,
|
|
1319
|
+
report_found=report_event is not None,
|
|
1320
|
+
replay_status=str(_artifact_field(_artifact_field(replay_event, "payload", {}), "status", "")).strip() or None,
|
|
1321
|
+
)
|
|
1322
|
+
return ExplanationResult(
|
|
1323
|
+
query="trace_of_router_decision",
|
|
1324
|
+
target_id=routing_id,
|
|
1325
|
+
status=status,
|
|
1326
|
+
summary=summary,
|
|
1327
|
+
projection_ids=projection_ids,
|
|
1328
|
+
event_ids=event_ids,
|
|
1329
|
+
warnings=self._artifact_warnings,
|
|
1330
|
+
metadata=self._metadata(
|
|
1331
|
+
query="trace_of_router_decision",
|
|
1332
|
+
routing_id=routing_id,
|
|
1333
|
+
profile_id=profile_id or None,
|
|
1334
|
+
selected_expert_ids=self._router_record_ids(evaluation_event, "selected_expert_ids"),
|
|
1335
|
+
rejected_expert_ids=self._router_record_ids(evaluation_event, "rejected_expert_ids"),
|
|
1336
|
+
report_id=report_id,
|
|
1337
|
+
router_events=self._router_event_summaries((validation_event, evaluation_event, replay_event, report_event)),
|
|
1338
|
+
validation_event=self._router_event_summary(validation_event),
|
|
1339
|
+
evaluation_event=self._router_event_summary(evaluation_event),
|
|
1340
|
+
replay_event=self._router_event_summary(replay_event),
|
|
1341
|
+
report_event=self._router_event_summary(report_event),
|
|
1342
|
+
),
|
|
1343
|
+
)
|
|
1344
|
+
|
|
1345
|
+
def trace_of_router_report(self, report_id: str) -> ExplanationResult:
|
|
1346
|
+
report_id = _normalize_id(report_id)
|
|
1347
|
+
correlation_id = report_id if report_id.startswith("report::") else report_correlation_id(report_id)
|
|
1348
|
+
native_audit = _native_audit_lookup(self._audit_trail(), correlation_id)
|
|
1349
|
+
report_event = self._router_report_event_by_report_id(report_id)
|
|
1350
|
+
event_ids = _dedupe_sorted_stable(
|
|
1351
|
+
list(native_audit["event_ids"])
|
|
1352
|
+
+ ([self._event_identifier(report_event)] if report_event is not None else [])
|
|
1353
|
+
)
|
|
1354
|
+
projection_ids = _dedupe_sorted_stable(
|
|
1355
|
+
list(native_audit["projection_ids"]) + ([correlation_id] if correlation_id else [])
|
|
1356
|
+
)
|
|
1357
|
+
fact_ids = _dedupe_sorted_stable(list(native_audit["fact_ids"]))
|
|
1358
|
+
graph_node_ids = _dedupe_sorted_stable(list(native_audit["graph_node_ids"]))
|
|
1359
|
+
constraint_ids = _dedupe_sorted_stable(list(native_audit["constraint_ids"]))
|
|
1360
|
+
status = self._status(graph_node_ids, event_ids, fact_ids, projection_ids, constraint_ids)
|
|
1361
|
+
summary = _router_summary(
|
|
1362
|
+
"batch report",
|
|
1363
|
+
report_id,
|
|
1364
|
+
status,
|
|
1365
|
+
report_found=report_event is not None,
|
|
1366
|
+
)
|
|
1367
|
+
return ExplanationResult(
|
|
1368
|
+
query="trace_of_router_report",
|
|
1369
|
+
target_id=report_id,
|
|
1370
|
+
status=status,
|
|
1371
|
+
summary=summary,
|
|
1372
|
+
graph_node_ids=graph_node_ids,
|
|
1373
|
+
event_ids=event_ids,
|
|
1374
|
+
fact_ids=fact_ids,
|
|
1375
|
+
projection_ids=projection_ids,
|
|
1376
|
+
constraint_ids=constraint_ids,
|
|
1377
|
+
warnings=self._artifact_warnings,
|
|
1378
|
+
metadata=self._metadata(
|
|
1379
|
+
query="trace_of_router_report",
|
|
1380
|
+
correlation_id=correlation_id,
|
|
1381
|
+
report_id=report_id,
|
|
1382
|
+
report_event=self._router_event_summary(report_event),
|
|
1383
|
+
),
|
|
1384
|
+
)
|
|
1385
|
+
|
|
1386
|
+
def evidence_for_router_decision(self, routing_id: str) -> ExplanationResult:
|
|
1387
|
+
routing_id = _normalize_id(routing_id)
|
|
1388
|
+
|
|
1389
|
+
validation_event = self._router_validation_event(routing_id)
|
|
1390
|
+
evaluation_event = self._router_evaluation_event(routing_id)
|
|
1391
|
+
replay_event = self._router_replay_event(routing_id)
|
|
1392
|
+
report_event = self._router_report_event(routing_id)
|
|
1393
|
+
report_id = self._router_report_id(report_event)
|
|
1394
|
+
projection_ids = _dedupe_sorted_stable([routing_id, report_id])
|
|
1395
|
+
event_ids = _dedupe_sorted_stable(
|
|
1396
|
+
list(self._event_ids_for_router_events((validation_event, evaluation_event, replay_event, report_event)))
|
|
1397
|
+
)
|
|
1398
|
+
status = self._status(event_ids, projection_ids)
|
|
1399
|
+
evaluation_payload = _artifact_field(evaluation_event, "payload", {})
|
|
1400
|
+
report_id = self._router_report_id(report_event)
|
|
1401
|
+
summary = _router_summary(
|
|
1402
|
+
"evidence bundle",
|
|
1403
|
+
routing_id,
|
|
1404
|
+
status,
|
|
1405
|
+
profile_id=str(_artifact_field(evaluation_payload, "profile_id", "")).strip() or None,
|
|
1406
|
+
selected_count=_int_payload_field(_artifact_field(evaluation_payload, "selected_count", None)),
|
|
1407
|
+
rejected_count=_int_payload_field(_artifact_field(evaluation_payload, "rejected_count", None)),
|
|
1408
|
+
total_proposals=_int_payload_field(_artifact_field(evaluation_payload, "total_proposals", None)),
|
|
1409
|
+
validation_found=validation_event is not None,
|
|
1410
|
+
report_found=report_event is not None,
|
|
1411
|
+
replay_status=str(_artifact_field(_artifact_field(replay_event, "payload", {}), "status", "")).strip() or None,
|
|
1412
|
+
)
|
|
1413
|
+
return ExplanationResult(
|
|
1414
|
+
query="evidence_for_router_decision",
|
|
1415
|
+
target_id=routing_id,
|
|
1416
|
+
status=status,
|
|
1417
|
+
summary=summary,
|
|
1418
|
+
projection_ids=projection_ids,
|
|
1419
|
+
event_ids=event_ids,
|
|
1420
|
+
warnings=self._artifact_warnings,
|
|
1421
|
+
metadata=self._metadata(
|
|
1422
|
+
query="evidence_for_router_decision",
|
|
1423
|
+
routing_id=routing_id,
|
|
1424
|
+
profile_id=str(_artifact_field(evaluation_payload, "profile_id", "")).strip() or None,
|
|
1425
|
+
report_id=report_id,
|
|
1426
|
+
fixture_file=self._router_fixture_file_from_routing_id(routing_id),
|
|
1427
|
+
selected_expert_ids=self._router_record_ids(evaluation_event, "selected_expert_ids"),
|
|
1428
|
+
rejected_expert_ids=self._router_record_ids(evaluation_event, "rejected_expert_ids"),
|
|
1429
|
+
router_events=self._router_event_summaries((validation_event, evaluation_event, replay_event, report_event)),
|
|
1430
|
+
validation_event=self._router_event_summary(validation_event),
|
|
1431
|
+
evaluation_event=self._router_event_summary(evaluation_event),
|
|
1432
|
+
replay_event=self._router_event_summary(replay_event),
|
|
1433
|
+
report_event=self._router_event_summary(report_event),
|
|
1434
|
+
),
|
|
1435
|
+
)
|
|
1436
|
+
|
|
1437
|
+
# ------------------------------------------------------------------
|
|
1438
|
+
# Internal lookup helpers
|
|
1439
|
+
# ------------------------------------------------------------------
|
|
1440
|
+
|
|
1441
|
+
def _metadata(self, **extra: Any) -> dict[str, Any]:
|
|
1442
|
+
metadata: dict[str, Any] = {
|
|
1443
|
+
"artifacts_present": _has_any_artifact(
|
|
1444
|
+
self._execution_graph,
|
|
1445
|
+
self._event_log_artifact,
|
|
1446
|
+
self._knowledge_base_artifact,
|
|
1447
|
+
self._replay_metadata,
|
|
1448
|
+
self._audit_trail_artifact,
|
|
1449
|
+
),
|
|
1450
|
+
"supported": {
|
|
1451
|
+
"execution_graph": _is_supported_artifact(self._execution_graph),
|
|
1452
|
+
"event_log": _is_supported_artifact(self._event_log_artifact),
|
|
1453
|
+
"knowledge_base": _is_supported_artifact(self._knowledge_base_artifact),
|
|
1454
|
+
"replay_metadata": _is_supported_artifact(self._replay_metadata),
|
|
1455
|
+
"audit_trail": _is_supported_artifact(self._audit_trail_artifact),
|
|
1456
|
+
},
|
|
1457
|
+
}
|
|
1458
|
+
if self._artifact_warnings:
|
|
1459
|
+
metadata["warnings"] = [warning.to_dict() for warning in self._artifact_warnings]
|
|
1460
|
+
metadata.update(extra)
|
|
1461
|
+
return metadata
|
|
1462
|
+
|
|
1463
|
+
def _status(self, *groups: Sequence[str]) -> ExplanationStatus:
|
|
1464
|
+
return _status_from_evidence_with_artifacts(
|
|
1465
|
+
*groups,
|
|
1466
|
+
artifacts_present=_has_any_artifact(
|
|
1467
|
+
self._execution_graph,
|
|
1468
|
+
self._event_log_artifact,
|
|
1469
|
+
self._knowledge_base_artifact,
|
|
1470
|
+
self._replay_metadata,
|
|
1471
|
+
self._audit_trail_artifact,
|
|
1472
|
+
),
|
|
1473
|
+
)
|
|
1474
|
+
|
|
1475
|
+
def _graph(self) -> Any | None:
|
|
1476
|
+
return self._execution_graph
|
|
1477
|
+
|
|
1478
|
+
def _event_log(self) -> Any | None:
|
|
1479
|
+
return self._event_log_artifact
|
|
1480
|
+
|
|
1481
|
+
def _knowledge_base(self) -> Any | None:
|
|
1482
|
+
return self._knowledge_base_artifact
|
|
1483
|
+
|
|
1484
|
+
def _audit_trail(self) -> Any | None:
|
|
1485
|
+
return self._audit_trail_artifact
|
|
1486
|
+
|
|
1487
|
+
def _audit_entries(self) -> list[Any]:
|
|
1488
|
+
audit = self._audit_trail()
|
|
1489
|
+
if audit is None:
|
|
1490
|
+
return []
|
|
1491
|
+
if isinstance(audit, AuditTrailIndex):
|
|
1492
|
+
return list(audit.events)
|
|
1493
|
+
if hasattr(audit, "events"):
|
|
1494
|
+
try:
|
|
1495
|
+
entries = list(getattr(audit, "events"))
|
|
1496
|
+
except Exception:
|
|
1497
|
+
entries = []
|
|
1498
|
+
elif hasattr(audit, "all") and callable(getattr(audit, "all")):
|
|
1499
|
+
try:
|
|
1500
|
+
entries = list(audit.all())
|
|
1501
|
+
except Exception:
|
|
1502
|
+
entries = []
|
|
1503
|
+
elif isinstance(audit, Mapping):
|
|
1504
|
+
raw_events = audit.get("events", [])
|
|
1505
|
+
try:
|
|
1506
|
+
entries = list(raw_events)
|
|
1507
|
+
except Exception:
|
|
1508
|
+
entries = []
|
|
1509
|
+
else:
|
|
1510
|
+
entries = []
|
|
1511
|
+
return sorted(entries, key=_audit_event_sort_key)
|
|
1512
|
+
|
|
1513
|
+
def _router_events_by_correlation(self, routing_id: str) -> list[Any]:
|
|
1514
|
+
correlation_id = router_correlation_id(routing_id)
|
|
1515
|
+
audit = self._audit_trail()
|
|
1516
|
+
if isinstance(audit, AuditTrailIndex):
|
|
1517
|
+
return list(audit.lookup_correlation(correlation_id))
|
|
1518
|
+
return [
|
|
1519
|
+
event
|
|
1520
|
+
for event in self._audit_entries()
|
|
1521
|
+
if _artifact_field(event, "correlation_id", "") == correlation_id
|
|
1522
|
+
]
|
|
1523
|
+
|
|
1524
|
+
def _router_events_of_type(self, event_type: str) -> list[Any]:
|
|
1525
|
+
audit = self._audit_trail()
|
|
1526
|
+
if isinstance(audit, AuditTrailIndex):
|
|
1527
|
+
return list(audit.lookup_type(event_type))
|
|
1528
|
+
return [
|
|
1529
|
+
event
|
|
1530
|
+
for event in self._audit_entries()
|
|
1531
|
+
if _artifact_field(event, "event_type", "") == event_type
|
|
1532
|
+
]
|
|
1533
|
+
|
|
1534
|
+
def _router_validation_event(self, routing_id: str) -> Any | None:
|
|
1535
|
+
for event in self._router_events_by_correlation(routing_id):
|
|
1536
|
+
if _artifact_field(event, "event_type", "") == "RouterFixtureValidated":
|
|
1537
|
+
return event
|
|
1538
|
+
return None
|
|
1539
|
+
|
|
1540
|
+
def _router_evaluation_event(self, routing_id: str) -> Any | None:
|
|
1541
|
+
for event in self._router_events_by_correlation(routing_id):
|
|
1542
|
+
if _artifact_field(event, "event_type", "") == "RouterEligibilityEvaluated":
|
|
1543
|
+
return event
|
|
1544
|
+
return None
|
|
1545
|
+
|
|
1546
|
+
def _router_replay_event(self, routing_id: str) -> Any | None:
|
|
1547
|
+
replay_events = [
|
|
1548
|
+
event
|
|
1549
|
+
for event in self._router_events_by_correlation(routing_id)
|
|
1550
|
+
if _artifact_field(event, "event_type", "") in {"RouterReplayCertified", "RouterReplayDiverged"}
|
|
1551
|
+
]
|
|
1552
|
+
if replay_events:
|
|
1553
|
+
return replay_events[0]
|
|
1554
|
+
return None
|
|
1555
|
+
|
|
1556
|
+
def _router_report_event(self, routing_id: str) -> Any | None:
|
|
1557
|
+
fixture_name = self._router_fixture_file_from_routing_id(routing_id)
|
|
1558
|
+
report_events = self._router_events_of_type("RouterBatchReportGenerated")
|
|
1559
|
+
if fixture_name:
|
|
1560
|
+
for event in report_events:
|
|
1561
|
+
payload = _artifact_field(event, "payload", {})
|
|
1562
|
+
fixture_files = _string_values(_artifact_field(payload, "fixture_files", []))
|
|
1563
|
+
if fixture_name in fixture_files:
|
|
1564
|
+
return event
|
|
1565
|
+
if len(report_events) == 1:
|
|
1566
|
+
return report_events[0]
|
|
1567
|
+
return None
|
|
1568
|
+
|
|
1569
|
+
def _router_report_event_by_report_id(self, report_id: str) -> Any | None:
|
|
1570
|
+
correlation_id = report_id if report_id.startswith("report::") else report_correlation_id(report_id)
|
|
1571
|
+
audit = self._audit_trail()
|
|
1572
|
+
if isinstance(audit, AuditTrailIndex):
|
|
1573
|
+
for event in audit.lookup_correlation(correlation_id):
|
|
1574
|
+
if _artifact_field(event, "event_type", "") == "RouterBatchReportGenerated":
|
|
1575
|
+
return event
|
|
1576
|
+
for event in self._router_events_of_type("RouterBatchReportGenerated"):
|
|
1577
|
+
payload = _artifact_field(event, "payload", {})
|
|
1578
|
+
if report_id in {
|
|
1579
|
+
_artifact_field(payload, "report_id", ""),
|
|
1580
|
+
_artifact_field(event, "correlation_id", ""),
|
|
1581
|
+
_artifact_field(event, "event_id", ""),
|
|
1582
|
+
}:
|
|
1583
|
+
return event
|
|
1584
|
+
return None
|
|
1585
|
+
|
|
1586
|
+
def _router_expert_record(self, event: Any | None, key: str, expert_id: str) -> dict[str, Any] | None:
|
|
1587
|
+
if event is None:
|
|
1588
|
+
return None
|
|
1589
|
+
payload = _artifact_field(event, "payload", {})
|
|
1590
|
+
records = _router_payload_records(payload, key)
|
|
1591
|
+
for record in records:
|
|
1592
|
+
if str(record.get("expert_id", "")).strip() == expert_id:
|
|
1593
|
+
return record
|
|
1594
|
+
ids_key = {
|
|
1595
|
+
"selected_experts": "selected_expert_ids",
|
|
1596
|
+
"rejected_experts": "rejected_expert_ids",
|
|
1597
|
+
}.get(key, "")
|
|
1598
|
+
if ids_key and expert_id in _string_values(_artifact_field(payload, ids_key, [])):
|
|
1599
|
+
fallback_reason = ""
|
|
1600
|
+
reason_codes = _string_values(_artifact_field(payload, "reason_codes", []))
|
|
1601
|
+
if len(reason_codes) == 1:
|
|
1602
|
+
fallback_reason = reason_codes[0]
|
|
1603
|
+
return {
|
|
1604
|
+
"decision": "selected" if key == "selected_experts" else "rejected",
|
|
1605
|
+
"expert_id": expert_id,
|
|
1606
|
+
"proposal_id": "",
|
|
1607
|
+
"reason": fallback_reason,
|
|
1608
|
+
}
|
|
1609
|
+
return None
|
|
1610
|
+
|
|
1611
|
+
def _router_record_ids(self, event: Any | None, key: str) -> tuple[str, ...]:
|
|
1612
|
+
if event is None:
|
|
1613
|
+
return ()
|
|
1614
|
+
payload = _artifact_field(event, "payload", {})
|
|
1615
|
+
ids_key = {
|
|
1616
|
+
"selected_expert_ids": "selected_expert_ids",
|
|
1617
|
+
"rejected_expert_ids": "rejected_expert_ids",
|
|
1618
|
+
}.get(key, key)
|
|
1619
|
+
return _dedupe_sorted_stable(_string_values(_artifact_field(payload, ids_key, [])))
|
|
1620
|
+
|
|
1621
|
+
def _router_event_summary(self, event: Any | None) -> dict[str, Any]:
|
|
1622
|
+
if event is None:
|
|
1623
|
+
return {}
|
|
1624
|
+
payload = _artifact_field(event, "payload", {})
|
|
1625
|
+
return {
|
|
1626
|
+
"event_id": self._event_identifier(event),
|
|
1627
|
+
"event_type": _artifact_field(event, "event_type", ""),
|
|
1628
|
+
"correlation_id": _artifact_field(event, "correlation_id", ""),
|
|
1629
|
+
"logical_tick": _artifact_field(event, "logical_tick", 0),
|
|
1630
|
+
"payload": _to_mapping(payload),
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
def _router_event_summaries(self, events: Sequence[Any | None]) -> list[dict[str, Any]]:
|
|
1634
|
+
return [summary for summary in (self._router_event_summary(event) for event in events) if summary]
|
|
1635
|
+
|
|
1636
|
+
def _event_ids_for_router_events(self, events: Sequence[Any | None]) -> tuple[str, ...]:
|
|
1637
|
+
event_ids: list[str] = []
|
|
1638
|
+
for event in events:
|
|
1639
|
+
if event is None:
|
|
1640
|
+
continue
|
|
1641
|
+
event_ids.append(self._event_identifier(event))
|
|
1642
|
+
return _dedupe_sorted_stable(event_ids)
|
|
1643
|
+
|
|
1644
|
+
def _router_report_id(self, event: Any | None) -> str:
|
|
1645
|
+
if event is None:
|
|
1646
|
+
return ""
|
|
1647
|
+
payload = _artifact_field(event, "payload", {})
|
|
1648
|
+
report_id = str(_artifact_field(payload, "report_id", "")).strip()
|
|
1649
|
+
if report_id:
|
|
1650
|
+
return report_id
|
|
1651
|
+
return _artifact_field(event, "correlation_id", "")
|
|
1652
|
+
|
|
1653
|
+
def _router_fixture_file_from_routing_id(self, routing_id: str) -> str:
|
|
1654
|
+
parts = routing_id.split(":")
|
|
1655
|
+
if len(parts) == 3 and parts[0] == "routing" and parts[2] == "v1" and parts[1].strip():
|
|
1656
|
+
return f"{parts[1].strip()}_routing.json"
|
|
1657
|
+
return ""
|
|
1658
|
+
|
|
1659
|
+
def _graph_nodes(self) -> list[Any]:
|
|
1660
|
+
graph = self._graph()
|
|
1661
|
+
if graph is None:
|
|
1662
|
+
return []
|
|
1663
|
+
if hasattr(graph, "topological_order") and callable(getattr(graph, "topological_order")):
|
|
1664
|
+
try:
|
|
1665
|
+
nodes = list(graph.topological_order())
|
|
1666
|
+
except Exception:
|
|
1667
|
+
nodes = []
|
|
1668
|
+
if nodes:
|
|
1669
|
+
return nodes
|
|
1670
|
+
nodes = _artifact_field(graph, "nodes", None)
|
|
1671
|
+
if nodes is None:
|
|
1672
|
+
return []
|
|
1673
|
+
try:
|
|
1674
|
+
if isinstance(nodes, Mapping):
|
|
1675
|
+
return list(nodes.values())
|
|
1676
|
+
return list(nodes)
|
|
1677
|
+
except TypeError:
|
|
1678
|
+
return []
|
|
1679
|
+
|
|
1680
|
+
def _graph_edges(self) -> list[Any]:
|
|
1681
|
+
graph = self._graph()
|
|
1682
|
+
if graph is None:
|
|
1683
|
+
return []
|
|
1684
|
+
edges = _artifact_field(graph, "edges", None)
|
|
1685
|
+
if edges is None:
|
|
1686
|
+
return []
|
|
1687
|
+
try:
|
|
1688
|
+
if isinstance(edges, Mapping):
|
|
1689
|
+
return list(edges.values())
|
|
1690
|
+
return list(edges)
|
|
1691
|
+
except TypeError:
|
|
1692
|
+
return []
|
|
1693
|
+
|
|
1694
|
+
def _graph_lineage(self, node_ids: Sequence[str]) -> tuple[str, ...]:
|
|
1695
|
+
graph = self._graph()
|
|
1696
|
+
if graph is None or not node_ids:
|
|
1697
|
+
return ()
|
|
1698
|
+
node_map = {_artifact_field(node, "node_id", ""): node for node in self._graph_nodes()}
|
|
1699
|
+
reverse: dict[str, list[str]] = {}
|
|
1700
|
+
for edge in self._graph_edges():
|
|
1701
|
+
reverse.setdefault(_artifact_field(edge, "target_id", ""), []).append(_artifact_field(edge, "source_id", ""))
|
|
1702
|
+
visited: set[str] = set()
|
|
1703
|
+
frontier = list(node_ids)
|
|
1704
|
+
lineage: list[str] = []
|
|
1705
|
+
while frontier:
|
|
1706
|
+
current = frontier.pop(0)
|
|
1707
|
+
for parent_id in sorted(reverse.get(current, [])):
|
|
1708
|
+
if parent_id and parent_id not in visited:
|
|
1709
|
+
visited.add(parent_id)
|
|
1710
|
+
lineage.append(parent_id)
|
|
1711
|
+
frontier.append(parent_id)
|
|
1712
|
+
ordered_nodes = canonical_graph_nodes([node_map[nid] for nid in lineage if nid in node_map])
|
|
1713
|
+
return tuple(_artifact_field(node, "node_id", "") for node in ordered_nodes if _artifact_field(node, "node_id", ""))
|
|
1714
|
+
|
|
1715
|
+
def _graph_nodes_matching(self, target_id: str, *, node_type: str | None = None) -> tuple[str, ...]:
|
|
1716
|
+
return self._graph_nodes_matching_any((target_id,), node_type=node_type)
|
|
1717
|
+
|
|
1718
|
+
def _graph_nodes_matching_any(
|
|
1719
|
+
self,
|
|
1720
|
+
target_ids: Sequence[str],
|
|
1721
|
+
*,
|
|
1722
|
+
node_type: str | None = None,
|
|
1723
|
+
) -> tuple[str, ...]:
|
|
1724
|
+
hits: list[str] = []
|
|
1725
|
+
target_set = {target_id for target_id in target_ids if isinstance(target_id, str) and target_id.strip()}
|
|
1726
|
+
if not target_set:
|
|
1727
|
+
return ()
|
|
1728
|
+
for node in self._graph_nodes():
|
|
1729
|
+
if node_type is not None and _artifact_field(node, "node_type", "") != node_type:
|
|
1730
|
+
continue
|
|
1731
|
+
candidates = {
|
|
1732
|
+
_artifact_field(node, "node_id", ""),
|
|
1733
|
+
_artifact_field(node, "node_type", ""),
|
|
1734
|
+
_artifact_field(node, "fingerprint", ""),
|
|
1735
|
+
str(_artifact_field(node, "origin_event_seq", "")),
|
|
1736
|
+
str(_artifact_field(node, "logical_order", "")),
|
|
1737
|
+
}
|
|
1738
|
+
candidates.update(_string_values(_artifact_field(node, "event_ids", [])))
|
|
1739
|
+
candidates.update(_string_values(_artifact_field(node, "fact_ids", [])))
|
|
1740
|
+
candidates.update(_string_values(_artifact_field(node, "projection_ids", [])))
|
|
1741
|
+
candidates.update(_string_values(_artifact_field(node, "constraint_ids", [])))
|
|
1742
|
+
if target_set.intersection(candidates) or _contains_any_target(_artifact_field(node, "payload", {}), tuple(target_set)):
|
|
1743
|
+
hits.append(_artifact_field(node, "node_id", ""))
|
|
1744
|
+
return _sorted_unique(hits)
|
|
1745
|
+
|
|
1746
|
+
def _event_entries(self) -> list[Any]:
|
|
1747
|
+
log = self._event_log()
|
|
1748
|
+
if log is None:
|
|
1749
|
+
return []
|
|
1750
|
+
if hasattr(log, "all") and callable(getattr(log, "all")):
|
|
1751
|
+
try:
|
|
1752
|
+
return list(log.all())
|
|
1753
|
+
except Exception:
|
|
1754
|
+
return []
|
|
1755
|
+
docs = _artifact_documents(log)
|
|
1756
|
+
for doc in docs:
|
|
1757
|
+
if isinstance(doc, Mapping) and "events" in doc:
|
|
1758
|
+
events = doc.get("events", [])
|
|
1759
|
+
try:
|
|
1760
|
+
if isinstance(events, Mapping):
|
|
1761
|
+
return list(events.values())
|
|
1762
|
+
return list(events)
|
|
1763
|
+
except TypeError:
|
|
1764
|
+
return []
|
|
1765
|
+
return []
|
|
1766
|
+
|
|
1767
|
+
def _event_ids(self, target_id: str) -> tuple[str, ...]:
|
|
1768
|
+
hits: list[str] = []
|
|
1769
|
+
for event in self._event_entries():
|
|
1770
|
+
candidates = {
|
|
1771
|
+
_artifact_field(event, "event_id", ""),
|
|
1772
|
+
str(_artifact_field(event, "seq", "")),
|
|
1773
|
+
_artifact_field(event, "event_type", ""),
|
|
1774
|
+
_artifact_field(event, "task_id", ""),
|
|
1775
|
+
_artifact_field(event, "domain_name", ""),
|
|
1776
|
+
_artifact_field(event, "fingerprint", lambda: "")() if callable(_artifact_field(event, "fingerprint", None)) else "",
|
|
1777
|
+
}
|
|
1778
|
+
if target_id in candidates or _contains_target(_artifact_field(event, "payload", {}), target_id):
|
|
1779
|
+
hits.append(self._event_identifier(event))
|
|
1780
|
+
return _sorted_unique(hits)
|
|
1781
|
+
|
|
1782
|
+
def _event_identifier(self, event: Any) -> str:
|
|
1783
|
+
event_id = _artifact_field(event, "event_id", None)
|
|
1784
|
+
if event_id:
|
|
1785
|
+
return str(event_id)
|
|
1786
|
+
fingerprint = _artifact_field(event, "fingerprint", None)
|
|
1787
|
+
if callable(fingerprint):
|
|
1788
|
+
try:
|
|
1789
|
+
return str(fingerprint())
|
|
1790
|
+
except Exception:
|
|
1791
|
+
pass
|
|
1792
|
+
seq = _artifact_field(event, "seq", None)
|
|
1793
|
+
if seq is not None:
|
|
1794
|
+
return f"seq:{seq}"
|
|
1795
|
+
return str(_artifact_field(event, "event_type", "event"))
|
|
1796
|
+
|
|
1797
|
+
def _event_neighborhood(self, target_id: str, event_ids: Sequence[str]) -> tuple[str, ...]:
|
|
1798
|
+
events = self._event_entries()
|
|
1799
|
+
if not events:
|
|
1800
|
+
return ()
|
|
1801
|
+
indexed = list(enumerate(events))
|
|
1802
|
+
matched_indices: list[int] = []
|
|
1803
|
+
for index, event in indexed:
|
|
1804
|
+
candidates = {
|
|
1805
|
+
_artifact_field(event, "event_id", ""),
|
|
1806
|
+
str(_artifact_field(event, "seq", "")),
|
|
1807
|
+
_artifact_field(event, "event_type", ""),
|
|
1808
|
+
_artifact_field(event, "task_id", ""),
|
|
1809
|
+
_artifact_field(event, "domain_name", ""),
|
|
1810
|
+
self._event_identifier(event),
|
|
1811
|
+
}
|
|
1812
|
+
if target_id in candidates or _contains_target(_artifact_field(event, "payload", {}), target_id):
|
|
1813
|
+
matched_indices.append(index)
|
|
1814
|
+
if not matched_indices:
|
|
1815
|
+
return ()
|
|
1816
|
+
neighbors: list[str] = []
|
|
1817
|
+
for index in matched_indices:
|
|
1818
|
+
for neighbor_index in (index - 1, index + 1):
|
|
1819
|
+
if 0 <= neighbor_index < len(events):
|
|
1820
|
+
neighbors.append(self._event_identifier(events[neighbor_index]))
|
|
1821
|
+
return _sorted_unique(neighbors)
|
|
1822
|
+
|
|
1823
|
+
def _facts(self) -> list[Any]:
|
|
1824
|
+
kb = self._knowledge_base()
|
|
1825
|
+
if kb is None:
|
|
1826
|
+
return []
|
|
1827
|
+
if hasattr(kb, "all_facts") and callable(getattr(kb, "all_facts")):
|
|
1828
|
+
try:
|
|
1829
|
+
return list(kb.all_facts())
|
|
1830
|
+
except Exception:
|
|
1831
|
+
return []
|
|
1832
|
+
if hasattr(kb, "query_relations") and callable(getattr(kb, "query_relations")):
|
|
1833
|
+
try:
|
|
1834
|
+
return list(kb.query_relations())
|
|
1835
|
+
except Exception:
|
|
1836
|
+
return []
|
|
1837
|
+
docs = _artifact_documents(kb)
|
|
1838
|
+
for doc in docs:
|
|
1839
|
+
if isinstance(doc, Mapping) and "facts" in doc:
|
|
1840
|
+
facts = doc.get("facts", [])
|
|
1841
|
+
try:
|
|
1842
|
+
if isinstance(facts, Mapping):
|
|
1843
|
+
return list(facts.values())
|
|
1844
|
+
return list(facts)
|
|
1845
|
+
except TypeError:
|
|
1846
|
+
return []
|
|
1847
|
+
return []
|
|
1848
|
+
|
|
1849
|
+
def _fact_ids(self, target_id: str) -> tuple[str, ...]:
|
|
1850
|
+
hits: list[str] = []
|
|
1851
|
+
for fact in self._facts():
|
|
1852
|
+
candidates = {
|
|
1853
|
+
_artifact_field(fact, "fact_id", ""),
|
|
1854
|
+
_artifact_field(fact, "fact_hash", ""),
|
|
1855
|
+
_artifact_field(fact, "transaction_id", ""),
|
|
1856
|
+
_artifact_field(fact, "event_log_fingerprint", ""),
|
|
1857
|
+
_artifact_field(fact, "schema_version", ""),
|
|
1858
|
+
_artifact_field(fact, "fingerprint", lambda: "")() if callable(_artifact_field(fact, "fingerprint", None)) else "",
|
|
1859
|
+
}
|
|
1860
|
+
if target_id in candidates or _contains_target(_artifact_field(fact, "metadata", {}), target_id):
|
|
1861
|
+
hits.append(_artifact_field(fact, "fact_id", ""))
|
|
1862
|
+
return _sorted_unique(hits)
|
|
1863
|
+
|
|
1864
|
+
def _fact_graph_nodes(self, target_id: str, fact_ids: Sequence[str]) -> tuple[str, ...]:
|
|
1865
|
+
hits: list[str] = []
|
|
1866
|
+
hits.extend(self._graph_nodes_matching(target_id))
|
|
1867
|
+
if fact_ids:
|
|
1868
|
+
hits.extend(self._graph_nodes_matching_any(fact_ids))
|
|
1869
|
+
return _sorted_unique(hits)
|
|
1870
|
+
|
|
1871
|
+
def _fact_metadata(self, target_id: str) -> list[Any]:
|
|
1872
|
+
matches = []
|
|
1873
|
+
for fact in self._facts():
|
|
1874
|
+
candidates = {
|
|
1875
|
+
_artifact_field(fact, "fact_id", ""),
|
|
1876
|
+
_artifact_field(fact, "fact_hash", ""),
|
|
1877
|
+
}
|
|
1878
|
+
if target_id in candidates or _contains_target(_artifact_field(fact, "metadata", {}), target_id):
|
|
1879
|
+
matches.append(fact)
|
|
1880
|
+
return matches
|
|
1881
|
+
|
|
1882
|
+
def _fact_projection_ids(self, target_id: str, fact_ids: Sequence[str]) -> tuple[str, ...]:
|
|
1883
|
+
projection_ids: list[str] = []
|
|
1884
|
+
for fact in self._fact_metadata(target_id):
|
|
1885
|
+
projection_ids.extend(_projection_ids_from_metadata(_artifact_field(fact, "metadata", {})))
|
|
1886
|
+
return _sorted_unique(projection_ids)
|
|
1887
|
+
|
|
1888
|
+
def _parent_fact_ids(self, target_id: str, fact_ids: Sequence[str]) -> tuple[str, ...]:
|
|
1889
|
+
parent_ids: list[str] = []
|
|
1890
|
+
for fact in self._fact_metadata(target_id):
|
|
1891
|
+
parent_ids.extend(_parent_fact_ids_from_metadata(_artifact_field(fact, "metadata", {})))
|
|
1892
|
+
return _sorted_unique(parent_ids or list(fact_ids))
|
|
1893
|
+
|
|
1894
|
+
def _fact_event_ids(self, target_id: str, fact_ids: Sequence[str]) -> tuple[str, ...]:
|
|
1895
|
+
event_ids: list[str] = []
|
|
1896
|
+
for fact in self._fact_metadata(target_id):
|
|
1897
|
+
event_ids.extend(_event_ids_from_metadata(_artifact_field(fact, "metadata", {})))
|
|
1898
|
+
event_ids.extend(_event_ids_from_metadata({"event_log_fingerprint": _artifact_field(fact, "event_log_fingerprint", "")}))
|
|
1899
|
+
return _sorted_unique(event_ids)
|
|
1900
|
+
|
|
1901
|
+
def _projection_ids(self, target_id: str) -> tuple[str, ...]:
|
|
1902
|
+
hits: list[str] = []
|
|
1903
|
+
hits.extend(self._projection_graph_nodes(target_id))
|
|
1904
|
+
for fact in self._fact_metadata(target_id):
|
|
1905
|
+
hits.extend(_projection_ids_from_metadata(_artifact_field(fact, "metadata", {})))
|
|
1906
|
+
for event in self._event_entries():
|
|
1907
|
+
if _artifact_field(event, "event_type", "") == "ProjectionCommitted" and (
|
|
1908
|
+
target_id in {
|
|
1909
|
+
_artifact_field(event, "event_id", ""),
|
|
1910
|
+
str(_artifact_field(event, "seq", "")),
|
|
1911
|
+
self._event_identifier(event),
|
|
1912
|
+
_artifact_field(event, "task_id", ""),
|
|
1913
|
+
}
|
|
1914
|
+
or _contains_target(_artifact_field(event, "payload", {}), target_id)
|
|
1915
|
+
):
|
|
1916
|
+
hits.extend(_projection_ids_from_metadata(_artifact_field(event, "payload", {})))
|
|
1917
|
+
hits.append(self._event_identifier(event))
|
|
1918
|
+
return _sorted_unique(hits)
|
|
1919
|
+
|
|
1920
|
+
def _projection_graph_nodes(self, target_id: str) -> tuple[str, ...]:
|
|
1921
|
+
return self._graph_nodes_matching(target_id, node_type="ProjectionCommitted")
|
|
1922
|
+
|
|
1923
|
+
def _projection_event_ids(self, target_id: str, projection_ids: Sequence[str]) -> tuple[str, ...]:
|
|
1924
|
+
hits: list[str] = []
|
|
1925
|
+
for event in self._event_entries():
|
|
1926
|
+
if _artifact_field(event, "event_type", "") != "ProjectionCommitted":
|
|
1927
|
+
continue
|
|
1928
|
+
payload = _artifact_field(event, "payload", {})
|
|
1929
|
+
candidates = {
|
|
1930
|
+
_artifact_field(event, "event_id", ""),
|
|
1931
|
+
str(_artifact_field(event, "seq", "")),
|
|
1932
|
+
self._event_identifier(event),
|
|
1933
|
+
_artifact_field(event, "task_id", ""),
|
|
1934
|
+
}
|
|
1935
|
+
if target_id in candidates or _contains_target(payload, target_id) or any(projection_id in _string_values(payload) for projection_id in projection_ids):
|
|
1936
|
+
hits.append(self._event_identifier(event))
|
|
1937
|
+
return _sorted_unique(hits)
|
|
1938
|
+
|
|
1939
|
+
def _projection_fact_ids(self, target_id: str, projection_ids: Sequence[str]) -> tuple[str, ...]:
|
|
1940
|
+
hits: list[str] = []
|
|
1941
|
+
for fact in self._facts():
|
|
1942
|
+
metadata = _artifact_field(fact, "metadata", {})
|
|
1943
|
+
if target_id in {
|
|
1944
|
+
_artifact_field(fact, "fact_id", ""),
|
|
1945
|
+
_artifact_field(fact, "fact_hash", ""),
|
|
1946
|
+
} or any(projection_id in _string_values(metadata) for projection_id in projection_ids):
|
|
1947
|
+
hits.append(_artifact_field(fact, "fact_id", ""))
|
|
1948
|
+
return _sorted_unique(hits)
|
|
1949
|
+
|
|
1950
|
+
def _constraint_ids(self, graph_node_ids: Sequence[str]) -> tuple[str, ...]:
|
|
1951
|
+
hits: list[str] = []
|
|
1952
|
+
graph = self._graph()
|
|
1953
|
+
if graph is None or not hasattr(graph, "node_by_id"):
|
|
1954
|
+
return ()
|
|
1955
|
+
for node_id in graph_node_ids:
|
|
1956
|
+
node = graph.node_by_id(node_id)
|
|
1957
|
+
if node is None:
|
|
1958
|
+
continue
|
|
1959
|
+
if _artifact_field(node, "node_type", "") == "ConstraintVerified":
|
|
1960
|
+
hits.append(_artifact_field(node, "node_id", ""))
|
|
1961
|
+
return _sorted_unique(hits)
|
|
1962
|
+
|
|
1963
|
+
def _event_graph_nodes(self, target_id: str, event_ids: Sequence[str]) -> tuple[str, ...]:
|
|
1964
|
+
hits: list[str] = []
|
|
1965
|
+
event_seq_ids = {self._event_sequence_from_id(event_id) for event_id in event_ids if self._event_sequence_from_id(event_id) is not None}
|
|
1966
|
+
for node in self._graph_nodes():
|
|
1967
|
+
if _artifact_field(node, "origin_event_seq", None) in event_seq_ids:
|
|
1968
|
+
hits.append(_artifact_field(node, "node_id", ""))
|
|
1969
|
+
elif target_id in {
|
|
1970
|
+
_artifact_field(node, "event_id", ""),
|
|
1971
|
+
_artifact_field(node, "node_id", ""),
|
|
1972
|
+
_artifact_field(node, "fingerprint", ""),
|
|
1973
|
+
_artifact_field(node, "node_type", ""),
|
|
1974
|
+
} or target_id in _string_values(_artifact_field(node, "event_ids", [])) or target_id in _string_values(_artifact_field(node, "fact_ids", [])) or _contains_target(_artifact_field(node, "payload", {}), target_id):
|
|
1975
|
+
hits.append(_artifact_field(node, "node_id", ""))
|
|
1976
|
+
return _sorted_unique(hits)
|
|
1977
|
+
|
|
1978
|
+
def _event_sequence_from_id(self, event_id: str) -> int | None:
|
|
1979
|
+
if event_id.startswith("seq:"):
|
|
1980
|
+
try:
|
|
1981
|
+
return int(event_id.split(":", 1)[1])
|
|
1982
|
+
except ValueError:
|
|
1983
|
+
return None
|
|
1984
|
+
for event in self._event_entries():
|
|
1985
|
+
if self._event_identifier(event) == event_id:
|
|
1986
|
+
seq = _artifact_field(event, "seq", None)
|
|
1987
|
+
return int(seq) if seq is not None else None
|
|
1988
|
+
return None
|
|
1989
|
+
|
|
1990
|
+
def _event_fact_ids(self, target_id: str, event_ids: Sequence[str]) -> tuple[str, ...]:
|
|
1991
|
+
hits: list[str] = []
|
|
1992
|
+
event_sequences = {self._event_sequence_from_id(event_id) for event_id in event_ids if self._event_sequence_from_id(event_id) is not None}
|
|
1993
|
+
for fact in self._facts():
|
|
1994
|
+
metadata = _artifact_field(fact, "metadata", {})
|
|
1995
|
+
if target_id in {
|
|
1996
|
+
_artifact_field(fact, "fact_id", ""),
|
|
1997
|
+
_artifact_field(fact, "fact_hash", ""),
|
|
1998
|
+
} or _contains_target(metadata, target_id):
|
|
1999
|
+
hits.append(_artifact_field(fact, "fact_id", ""))
|
|
2000
|
+
elif event_sequences:
|
|
2001
|
+
source_event_ids = _event_ids_from_metadata(metadata)
|
|
2002
|
+
if any(str(seq) in source_event_ids for seq in event_sequences):
|
|
2003
|
+
hits.append(_artifact_field(fact, "fact_id", ""))
|
|
2004
|
+
return _sorted_unique(hits)
|
|
2005
|
+
|
|
2006
|
+
def _event_projection_ids(self, target_id: str, event_ids: Sequence[str]) -> tuple[str, ...]:
|
|
2007
|
+
hits: list[str] = []
|
|
2008
|
+
event_sequences = {self._event_sequence_from_id(event_id) for event_id in event_ids if self._event_sequence_from_id(event_id) is not None}
|
|
2009
|
+
for event in self._event_entries():
|
|
2010
|
+
seq = _artifact_field(event, "seq", None)
|
|
2011
|
+
if seq not in event_sequences and self._event_identifier(event) not in event_ids and target_id not in {
|
|
2012
|
+
_artifact_field(event, "event_id", ""),
|
|
2013
|
+
_artifact_field(event, "event_type", ""),
|
|
2014
|
+
_artifact_field(event, "task_id", ""),
|
|
2015
|
+
} and not _contains_target(_artifact_field(event, "payload", {}), target_id):
|
|
2016
|
+
continue
|
|
2017
|
+
if _artifact_field(event, "event_type", "") == "ProjectionCommitted":
|
|
2018
|
+
hits.extend(_projection_ids_from_metadata(_artifact_field(event, "payload", {})))
|
|
2019
|
+
hits.append(self._event_identifier(event))
|
|
2020
|
+
return _sorted_unique(hits)
|
|
2021
|
+
|
|
2022
|
+
def _origin_projection_ids(self, target_id: str) -> tuple[str, ...]:
|
|
2023
|
+
hits: list[str] = []
|
|
2024
|
+
hits.extend(self._projection_graph_nodes(target_id))
|
|
2025
|
+
for fact in self._fact_metadata(target_id):
|
|
2026
|
+
hits.extend(_projection_ids_from_metadata(_artifact_field(fact, "metadata", {})))
|
|
2027
|
+
for event in self._event_entries():
|
|
2028
|
+
if _artifact_field(event, "event_type", "") == "ProjectionCommitted" and (
|
|
2029
|
+
target_id in {
|
|
2030
|
+
_artifact_field(event, "event_id", ""),
|
|
2031
|
+
str(_artifact_field(event, "seq", "")),
|
|
2032
|
+
self._event_identifier(event),
|
|
2033
|
+
_artifact_field(event, "task_id", ""),
|
|
2034
|
+
}
|
|
2035
|
+
or _contains_target(_artifact_field(event, "payload", {}), target_id)
|
|
2036
|
+
):
|
|
2037
|
+
hits.extend(_projection_ids_from_metadata(_artifact_field(event, "payload", {})))
|
|
2038
|
+
hits.append(self._event_identifier(event))
|
|
2039
|
+
return _sorted_unique(hits)
|
|
2040
|
+
|
|
2041
|
+
def _origin_projection_graph_nodes(self, target_id: str, projection_ids: Sequence[str]) -> tuple[str, ...]:
|
|
2042
|
+
hits: list[str] = []
|
|
2043
|
+
hits.extend(self._projection_graph_nodes(target_id))
|
|
2044
|
+
if projection_ids:
|
|
2045
|
+
hits.extend(self._graph_nodes_matching_any(projection_ids, node_type="ProjectionCommitted"))
|
|
2046
|
+
hits.extend(self._graph_nodes_matching_any(projection_ids))
|
|
2047
|
+
return _sorted_unique(hits)
|
|
2048
|
+
|
|
2049
|
+
def _origin_projection_event_ids(self, target_id: str, projection_ids: Sequence[str]) -> tuple[str, ...]:
|
|
2050
|
+
hits: list[str] = []
|
|
2051
|
+
for event in self._event_entries():
|
|
2052
|
+
if _artifact_field(event, "event_type", "") == "ProjectionCommitted":
|
|
2053
|
+
payload = _artifact_field(event, "payload", {})
|
|
2054
|
+
if target_id in {
|
|
2055
|
+
_artifact_field(event, "event_id", ""),
|
|
2056
|
+
str(_artifact_field(event, "seq", "")),
|
|
2057
|
+
self._event_identifier(event),
|
|
2058
|
+
_artifact_field(event, "task_id", ""),
|
|
2059
|
+
} or _contains_target(payload, target_id) or any(projection_id in _string_values(payload) for projection_id in projection_ids):
|
|
2060
|
+
hits.append(self._event_identifier(event))
|
|
2061
|
+
return _sorted_unique(hits)
|
|
2062
|
+
|
|
2063
|
+
def _origin_projection_fact_ids(self, target_id: str, projection_ids: Sequence[str]) -> tuple[str, ...]:
|
|
2064
|
+
hits: list[str] = []
|
|
2065
|
+
for fact in self._facts():
|
|
2066
|
+
metadata = _artifact_field(fact, "metadata", {})
|
|
2067
|
+
if target_id in {
|
|
2068
|
+
_artifact_field(fact, "fact_id", ""),
|
|
2069
|
+
_artifact_field(fact, "fact_hash", ""),
|
|
2070
|
+
} or any(projection_id in _string_values(metadata) for projection_id in projection_ids):
|
|
2071
|
+
hits.append(_artifact_field(fact, "fact_id", ""))
|
|
2072
|
+
return _sorted_unique(hits)
|
|
2073
|
+
|
|
2074
|
+
|
|
2075
|
+
def _projection_ids_from_metadata(metadata: Any) -> list[str]:
|
|
2076
|
+
projection_ids: list[str] = []
|
|
2077
|
+
if isinstance(metadata, Mapping):
|
|
2078
|
+
for key in ("projection_id", "projection_ids", "projection_hash", "origin_projection_id", "source_projection_id"):
|
|
2079
|
+
if key in metadata:
|
|
2080
|
+
projection_ids.extend(_string_values(metadata[key]))
|
|
2081
|
+
elif isinstance(metadata, Sequence) and not isinstance(metadata, (bytes, bytearray, str)):
|
|
2082
|
+
projection_ids.extend(_string_values(metadata))
|
|
2083
|
+
elif isinstance(metadata, str):
|
|
2084
|
+
projection_ids.append(metadata)
|
|
2085
|
+
return projection_ids
|
|
2086
|
+
|
|
2087
|
+
|
|
2088
|
+
def _parent_fact_ids_from_metadata(metadata: Any) -> list[str]:
|
|
2089
|
+
parent_ids: list[str] = []
|
|
2090
|
+
if isinstance(metadata, Mapping):
|
|
2091
|
+
for key in ("parent_fact_id", "parent_fact_ids", "source_fact_id", "source_fact_ids", "upstream_fact_ids"):
|
|
2092
|
+
if key in metadata:
|
|
2093
|
+
parent_ids.extend(_string_values(metadata[key]))
|
|
2094
|
+
elif isinstance(metadata, Sequence) and not isinstance(metadata, (bytes, bytearray, str)):
|
|
2095
|
+
parent_ids.extend(_string_values(metadata))
|
|
2096
|
+
return parent_ids
|
|
2097
|
+
|
|
2098
|
+
|
|
2099
|
+
def _event_ids_from_metadata(metadata: Any) -> list[str]:
|
|
2100
|
+
event_ids: list[str] = []
|
|
2101
|
+
if isinstance(metadata, Mapping):
|
|
2102
|
+
for key in ("event_id", "event_ids", "source_event_id", "source_event_ids", "origin_event_id", "origin_event_ids"):
|
|
2103
|
+
if key in metadata:
|
|
2104
|
+
event_ids.extend(_string_values(metadata[key]))
|
|
2105
|
+
elif isinstance(metadata, Sequence) and not isinstance(metadata, (bytes, bytearray, str)):
|
|
2106
|
+
event_ids.extend(_string_values(metadata))
|
|
2107
|
+
return event_ids
|
|
2108
|
+
|
|
2109
|
+
|
|
2110
|
+
__all__ = [
|
|
2111
|
+
"ExplanationStatus",
|
|
2112
|
+
"ExplainabilityWarning",
|
|
2113
|
+
"ExplanationResult",
|
|
2114
|
+
"StaticExplainer",
|
|
2115
|
+
]
|