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.
Files changed (96) hide show
  1. core_runtime/__init__.py +10 -0
  2. core_runtime/__version__.py +17 -0
  3. core_runtime/cli/__init__.py +7 -0
  4. core_runtime/cli/__main__.py +22 -0
  5. core_runtime/cli/bump_version.py +176 -0
  6. core_runtime/cli/contract_preflight.py +69 -0
  7. core_runtime/cli/create_domain.py +66 -0
  8. core_runtime/cli/doctor.py +44 -0
  9. core_runtime/cli/inventory.py +85 -0
  10. core_runtime/cli/lint.py +8 -0
  11. core_runtime/cli/main.py +579 -0
  12. core_runtime/cli/release_check.py +65 -0
  13. core_runtime/cli/repair_artifact_paths.py +48 -0
  14. core_runtime/cli/sync_template.py +67 -0
  15. core_runtime/cli/validate.py +73 -0
  16. core_runtime/core/__init__.py +34 -0
  17. core_runtime/core/audit_event.py +760 -0
  18. core_runtime/core/audit_trail_index.py +100 -0
  19. core_runtime/core/canonicalization.py +55 -0
  20. core_runtime/core/contract_evaluator.py +945 -0
  21. core_runtime/core/contract_executability.py +307 -0
  22. core_runtime/core/contract_loader.py +57 -0
  23. core_runtime/core/contract_probes.py +630 -0
  24. core_runtime/core/contract_program.py +131 -0
  25. core_runtime/core/contract_program_registry.py +104 -0
  26. core_runtime/core/contract_program_v2.py +126 -0
  27. core_runtime/core/dsk_v3.py +142 -0
  28. core_runtime/core/explainability.py +2115 -0
  29. core_runtime/core/numeric_normalization.py +120 -0
  30. core_runtime/core/rule_anchor.py +1388 -0
  31. core_runtime/core/schema_fingerprint.py +69 -0
  32. core_runtime/core/sensor_evidence.py +548 -0
  33. core_runtime/data/contracts/CoreAnchor.sol +109 -0
  34. core_runtime/data/contracts/CoreRuleAnchor.abi.json +111 -0
  35. core_runtime/data/contracts/CoreRuleAnchor.bin +1 -0
  36. core_runtime/data/contracts/CoreRuleAnchor.build.json +20 -0
  37. core_runtime/data/contracts/CoreRuleAnchor.runtime.bin +1 -0
  38. core_runtime/data/contracts/CoreRuleAnchor.sol +74 -0
  39. core_runtime/data/package_data_manifest.v1.json +173 -0
  40. core_runtime/data/schemas/core/causal_trace.v1.json +141 -0
  41. core_runtime/data/schemas/core/context_gate.v1.json +38 -0
  42. core_runtime/data/schemas/core/context_threshold.v1.json +46 -0
  43. core_runtime/data/schemas/core/contract_program.v1.json +186 -0
  44. core_runtime/data/schemas/core/contract_program.v2.json +187 -0
  45. core_runtime/data/schemas/core/control_decision.v1.json +114 -0
  46. core_runtime/data/schemas/core/dsk.v3.json +105 -0
  47. core_runtime/data/schemas/core/effect_result.v1.json +39 -0
  48. core_runtime/data/schemas/core/entropy_signal.v1.json +108 -0
  49. core_runtime/data/schemas/core/execution_receipt.v1.json +93 -0
  50. core_runtime/data/schemas/core/frozen_release_manifest.v1.json +87 -0
  51. core_runtime/data/schemas/core/frozen_release_manifest.v2.json +72 -0
  52. core_runtime/data/schemas/core/frozen_release_manifest.v3.json +38 -0
  53. core_runtime/data/schemas/core/frozen_release_manifest.v4.json +72 -0
  54. core_runtime/data/schemas/core/frozen_release_manifest.v5.json +38 -0
  55. core_runtime/data/schemas/core/frozen_release_manifest.v6.json +116 -0
  56. core_runtime/data/schemas/core/frozen_release_manifest.v7.json +37 -0
  57. core_runtime/data/schemas/core/frozen_release_manifest.v8.json +114 -0
  58. core_runtime/data/schemas/core/frozen_rule_set.v1.json +282 -0
  59. core_runtime/data/schemas/core/memory_artifact.v1.json +120 -0
  60. core_runtime/data/schemas/core/memory_generation_result.v1.json +37 -0
  61. core_runtime/data/schemas/core/operational_learning_event.v1.json +63 -0
  62. core_runtime/data/schemas/core/pattern_candidate.v1.json +114 -0
  63. core_runtime/data/schemas/core/physical_safety_assurance_case.v1.json +676 -0
  64. core_runtime/data/schemas/core/policy_lifecycle.v1.json +99 -0
  65. core_runtime/data/schemas/core/retention_manifest.v1.json +53 -0
  66. core_runtime/data/schemas/core/reversibility_policy.v1.json +107 -0
  67. core_runtime/data/schemas/core/rule_anchor_batch.v1.json +93 -0
  68. core_runtime/data/schemas/core/rule_anchor_chain_evidence.v1.json +56 -0
  69. core_runtime/data/schemas/core/rule_approval.v1.json +49 -0
  70. core_runtime/data/schemas/core/rule_approval_request.v1.json +42 -0
  71. core_runtime/data/schemas/core/state_transition.v1.json +115 -0
  72. core_runtime/data/schemas/core/task_closeout.v1.json +47 -0
  73. core_runtime/data/schemas/core/template_promotion_candidate.v1.json +83 -0
  74. core_runtime/data/schemas/core/unsigned_rule_anchor_deployment.v1.json +106 -0
  75. core_runtime/data/schemas/core/unsigned_rule_anchor_transaction.v1.json +116 -0
  76. core_runtime/tooling/__init__.py +48 -0
  77. core_runtime/tooling/bump_version.py +900 -0
  78. core_runtime/tooling/contract_preflight.py +293 -0
  79. core_runtime/tooling/create_domain.py +254 -0
  80. core_runtime/tooling/diagnostics.py +129 -0
  81. core_runtime/tooling/doctor.py +482 -0
  82. core_runtime/tooling/file_inventory.py +182 -0
  83. core_runtime/tooling/json_checks.py +100 -0
  84. core_runtime/tooling/release_check.py +1017 -0
  85. core_runtime/tooling/repair_artifact_paths.py +458 -0
  86. core_runtime/tooling/report_writer.py +176 -0
  87. core_runtime/tooling/repository_inventory.py +399 -0
  88. core_runtime/tooling/safety_checks.py +172 -0
  89. core_runtime/tooling/sync_template.py +303 -0
  90. core_runtime/tooling/validation.py +507 -0
  91. core_runtime/tooling/version_inventory.py +256 -0
  92. core_runtime_engine-11.5.1.dist-info/METADATA +35 -0
  93. core_runtime_engine-11.5.1.dist-info/RECORD +96 -0
  94. core_runtime_engine-11.5.1.dist-info/WHEEL +5 -0
  95. core_runtime_engine-11.5.1.dist-info/entry_points.txt +2 -0
  96. 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
+ ]